Хранилище лидов
Хранилище лидов * { margin: 0; padding: 0; box-sizing: border-box; } :root { --bg-base: #f5f7fa; --bg-surface: #ffffff; --bg-card: #ffffff; --bg-card-hover: #f8fafc; --bg-elevated: #f1f5f9; --bg-input: #f8fafc; --bg-sidebar: #1e293b; --bg-sidebar-hover: #334155; --bg-sidebar-active: rgba(59, 130, 246, 0.2); --text-primary: #1e293b; --text-secondary: #64748b; --text-muted: #94a3b8; --text-sidebar: #e2e8f0; --text-sidebar-muted: #94a3b8; --border-color: #e2e8f0; --border-light: #f1f5f9; --accent: #3b82f6; --accent-hover: #2563eb; --accent-soft: rgba(59, 130, 246, 0.1); --success: #10b981; --success-soft: rgba(16, 185, 129, 0.1); --warning: #f59e0b; --warning-soft: rgba(245, 158, 11, 0.1); --danger: #ef4444; --danger-soft: rgba(239, 68, 68, 0.1); --purple: #8b5cf6; --purple-soft: rgba(139, 92, 246, 0.1); --cyan: #06b6d4; --cyan-soft: rgba(6, 182, 212, 0.1); --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); --shadow: 0 1px 3px rgba(0, 0, 0, 0.1); --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); --radius-sm: 6px; --radius: 8px; --radius-lg: 12px; --sidebar-width: 240px; } [data-theme="dark"] { --bg-base: #0f172a; --bg-surface: #1e293b; --bg-card: #1e293b; --bg-card-hover: #334155; --bg-elevated: #334155; --bg-input: #0f172a; --bg-sidebar: #0f172a; --bg-sidebar-hover: #1e293b; --text-primary: #f1f5f9; --text-secondary: #94a3b8; --text-muted: #64748b; --border-color: #334155; --border-light: #1e293b; --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow: 0 1px 3px rgba(0, 0, 0, 0.4); --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4); } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg-base); color: var(--text-primary); min-height: 100vh; line-height: 1.5; font-size: 14px; display: flex; } /* Sidebar */ .sidebar { width: var(--sidebar-width); background: var(--bg-sidebar); min-height: 100vh; padding: 20px 12px; position: fixed; left: 0; top: 0; bottom: 0; display: flex; flex-direction: column; z-index: 100; overflow-y: auto; } .sidebar-header { padding: 0 12px 20px; border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: 20px; } .sidebar-logo { font-size: 1.1rem; font-weight: 600; color: var(--text-sidebar); } .sidebar-subtitle { font-size: 0.75rem; color: var(--text-sidebar-muted); margin-top: 4px; } .sidebar-section { margin-bottom: 24px; } .sidebar-section-title { font-size: 0.65rem; font-weight: 600; color: var(--text-sidebar-muted); text-transform: uppercase; letter-spacing: 0.1em; padding: 0 12px; margin-bottom: 8px; } .nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--radius-sm); color: var(--text-sidebar); font-size: 0.85rem; font-weight: 450; cursor: pointer; transition: all 0.15s; border: none; background: none; width: 100%; text-align: left; margin-bottom: 2px; } .nav-item:hover { background: var(--bg-sidebar-hover); } .nav-item.active { background: var(--bg-sidebar-active); color: var(--accent); } .nav-icon { width: 18px; height: 18px; opacity: 0.7; } .nav-item.active .nav-icon { opacity: 1; } .sidebar-footer { margin-top: auto; padding-top: 16px; border-top: 1px solid rgba(255,255,255,0.1); } .theme-toggle { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--radius-sm); color: var(--text-sidebar); font-size: 0.85rem; cursor: pointer; transition: all 0.15s; border: none; background: none; width: 100%; } .theme-toggle:hover { background: var(--bg-sidebar-hover); } .marketer-select { margin-top: 12px; } .marketer-select label { display: block; font-size: 0.65rem; color: var(--text-sidebar-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px; padding: 0 4px; } .marketer-select select { width: 100%; background: var(--bg-sidebar-hover); border: none; border-radius: var(--radius-sm); padding: 8px 10px; color: var(--text-sidebar); font-size: 0.85rem; cursor: pointer; } /* Main */ .main { margin-left: var(--sidebar-width); flex: 1; min-height: 100vh; padding: 24px 32px; max-width: calc(100vw - var(--sidebar-width)); } .page { display: none; } .page.active { display: block; } /* Page Header */ .page-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; flex-wrap: wrap; gap: 16px; } .page-title { font-size: 1.5rem; font-weight: 600; color: var(--text-primary); } .page-subtitle { color: var(--text-secondary); font-size: 0.875rem; margin-top: 4px; } /* Period Filter */ .period-filter { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } .period-btn { padding: 6px 14px; border: 1px solid var(--border-color); background: var(--bg-card); color: var(--text-secondary); border-radius: var(--radius-sm); font-size: 0.8rem; cursor: pointer; transition: all 0.15s; } .period-btn:hover { border-color: var(--accent); color: var(--accent); } .period-btn.active { background: var(--accent); border-color: var(--accent); color: white; } .period-custom { display: flex; gap: 8px; align-items: center; } .period-custom input[type="date"] { padding: 5px 10px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); background: var(--bg-card); color: var(--text-primary); font-size: 0.8rem; } .period-compare-toggle { display: flex; align-items: center; gap: 6px; margin-left: 12px; padding-left: 12px; border-left: 1px solid var(--border-color); } .period-compare-toggle input[type="checkbox"] { cursor: pointer; } .period-compare-toggle label { font-size: 0.8rem; color: var(--text-secondary); cursor: pointer; } /* Cards */ .card { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius); margin-bottom: 20px; box-shadow: var(--shadow-sm); } .card-header { padding: 16px 20px; border-bottom: 1px solid var(--border-light); display: flex; justify-content: space-between; align-items: center; } .card-title { font-size: 0.95rem; font-weight: 600; color: var(--text-primary); } .card-body { padding: 20px; } /* Stats Grid */ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 24px; } .stats-grid-6 { grid-template-columns: repeat(6, 1fr); } @media (max-width: 1400px) { .stats-grid-6 { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 900px) { .stats-grid-6 { grid-template-columns: repeat(2, 1fr); } } .stat-card { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius); padding: 20px; box-shadow: var(--shadow-sm); position: relative; } .stat-card.highlight { border-color: var(--accent); background: var(--accent-soft); } .stat-card.success-highlight { border-color: var(--success); background: var(--success-soft); } .stat-card.warning-highlight { border-color: var(--warning); background: var(--warning-soft); } .stat-icon { width: 40px; height: 40px; border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; margin-bottom: 12px; font-size: 1.1rem; } .stat-icon.blue { background: var(--accent-soft); color: var(--accent); } .stat-icon.green { background: var(--success-soft); color: var(--success); } .stat-icon.yellow { background: var(--warning-soft); color: var(--warning); } .stat-icon.red { background: var(--danger-soft); color: var(--danger); } .stat-icon.purple { background: var(--purple-soft); color: var(--purple); } .stat-icon.cyan { background: var(--cyan-soft); color: var(--cyan); } .stat-value { font-size: 1.75rem; font-weight: 700; color: var(--text-primary); margin-bottom: 4px; } .stat-value.small { font-size: 1.25rem; } .stat-label { font-size: 0.8rem; color: var(--text-muted); } .stat-change { position: absolute; top: 16px; right: 16px; font-size: 0.75rem; padding: 2px 8px; border-radius: 4px; } .stat-change.positive { background: var(--success-soft); color: var(--success); } .stat-change.negative { background: var(--danger-soft); color: var(--danger); } .stat-compare { margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--border-color); font-size: 0.75rem; color: var(--text-muted); } .stat-compare-value { color: var(--text-secondary); font-weight: 500; } /* Progress Bar */ .progress-bar { height: 8px; background: var(--bg-elevated); border-radius: 4px; overflow: hidden; margin-top: 12px; } .progress-bar-fill { height: 100%; border-radius: 4px; transition: width 0.3s ease; } .progress-bar-fill.green { background: var(--success); } .progress-bar-fill.blue { background: var(--accent); } .progress-bar-fill.yellow { background: var(--warning); } .progress-bar-fill.red { background: var(--danger); } .progress-label { display: flex; justify-content: space-between; font-size: 0.75rem; margin-top: 6px; color: var(--text-muted); } /* Top Performer Card */ .top-performer { background: linear-gradient(135deg, var(--accent-soft) 0%, var(--purple-soft) 100%); border: 1px solid var(--accent); border-radius: var(--radius); padding: 16px 20px; margin-bottom: 16px; } .top-performer-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; } .top-performer-name { font-size: 1.1rem; font-weight: 600; color: var(--text-primary); } .top-performer-stats { display: flex; gap: 20px; margin-top: 8px; font-size: 0.85rem; color: var(--text-secondary); } /* Forms */ .form-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; } @media (max-width: 1200px) { .form-grid { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 900px) { .form-grid { grid-template-columns: repeat(2, 1fr); } } @media (max-width: 600px) { .form-grid { grid-template-columns: 1fr; } } .form-group { display: flex; flex-direction: column; gap: 6px; } .form-group.span-2 { grid-column: span 2; } .form-group.span-full { grid-column: 1 / -1; } .form-label { font-size: 0.8rem; font-weight: 500; color: var(--text-secondary); } .form-control { background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 10px 12px; color: var(--text-primary); font-size: 0.875rem; transition: all 0.15s; } .form-control:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .form-control::placeholder { color: var(--text-muted); } select.form-control { cursor: pointer; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%2394a3b8' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10l-5 5z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; padding-right: 32px; } textarea.form-control { min-height: 60px; resize: vertical; } /* Buttons */ .btn { padding: 10px 20px; border: none; border-radius: var(--radius-sm); font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: all 0.15s; display: inline-flex; align-items: center; gap: 6px; } .btn:disabled { opacity: 0.6; cursor: not-allowed; } .btn-primary { background: var(--accent); color: white; } .btn-primary:hover:not(:disabled) { background: var(--accent-hover); } .btn-secondary { background: var(--bg-elevated); color: var(--text-secondary); border: 1px solid var(--border-color); } .btn-secondary:hover:not(:disabled) { background: var(--bg-card-hover); color: var(--text-primary); } .btn-success { background: var(--success); color: white; } .btn-sm { padding: 6px 14px; font-size: 0.8rem; } .btn-group { display: flex; gap: 10px; flex-wrap: wrap; } /* Loading Spinner */ .spinner { width: 16px; height: 16px; border: 2px solid transparent; border-top-color: currentColor; border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .loading-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; z-index: 9999; } .loading-overlay .spinner { width: 40px; height: 40px; border-width: 4px; border-top-color: var(--accent); } /* Tables */ .table-container { overflow-x: auto; border: 1px solid var(--border-color); border-radius: var(--radius); } table { width: 100%; border-collapse: collapse; font-size: 0.8rem; } thead { background: var(--bg-elevated); } th { padding: 12px 14px; text-align: left; font-weight: 600; color: var(--text-secondary); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.03em; white-space: nowrap; position: relative; } th.sortable { cursor: pointer; user-select: none; } th.sortable:hover { background: var(--bg-card-hover); } th.sortable::after { content: ' '; opacity: 0.3; font-size: 0.7rem; } th.sort-asc::after { content: ' [A]'; opacity: 1; color: var(--accent); } th.sort-desc::after { content: ' [D]'; opacity: 1; color: var(--accent); } td { padding: 12px 14px; color: var(--text-primary); border-top: 1px solid var(--border-light); } tbody tr:hover { background: var(--bg-card-hover); } tbody tr.highlight-row { background: var(--success-soft); } tbody tr.warning-row { background: var(--warning-soft); } tbody tr.danger-row { background: var(--danger-soft); } /* Badges */ .badge { display: inline-flex; align-items: center; padding: 3px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 500; } .badge-blue { background: var(--accent-soft); color: var(--accent); } .badge-green { background: var(--success-soft); color: var(--success); } .badge-yellow { background: var(--warning-soft); color: var(--warning); } .badge-red { background: var(--danger-soft); color: var(--danger); } .badge-purple { background: var(--purple-soft); color: var(--purple); } .badge-cyan { background: var(--cyan-soft); color: var(--cyan); } .badge-gray { background: var(--bg-elevated); color: var(--text-muted); } /* Filters Row */ .filters-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; } .filter-group { display: flex; flex-direction: column; gap: 4px; min-width: 150px; } .filter-label { font-size: 0.7rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; } /* Search */ .search-box { position: relative; } .search-box input { width: 100%; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 10px 12px 10px 40px; font-size: 0.875rem; color: var(--text-primary); } .search-box input:focus { outline: none; border-color: var(--accent); } .search-box svg { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text-muted); } /* Lead Card */ .lead-card { background: var(--bg-elevated); border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 14px 16px; margin-bottom: 10px; cursor: pointer; transition: all 0.15s; } .lead-card:hover { border-color: var(--accent); } .lead-card.selected { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .lead-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; } .lead-card-title { font-weight: 500; color: var(--text-primary); } .lead-card-meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 6px; font-size: 0.8rem; color: var(--text-secondary); } /* Funnel */ .funnel { display: flex; align-items: center; gap: 12px; padding: 20px; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius); margin-bottom: 24px; overflow-x: auto; } .funnel-step { flex: 1; min-width: 120px; text-align: center; } .funnel-value { font-size: 1.5rem; font-weight: 700; color: var(--text-primary); } .funnel-label { font-size: 0.75rem; color: var(--text-muted); margin-top: 4px; } .funnel-arrow { color: var(--text-muted); font-size: 1.2rem; } /* Charts */ .charts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; margin-bottom: 24px; } .chart-card { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius); padding: 20px; } .chart-title { font-size: 0.9rem; font-weight: 600; margin-bottom: 16px; color: var(--text-primary); } /* Payment Item */ .payment-item { display: flex; justify-content: space-between; align-items: center; padding: 14px 16px; background: var(--bg-elevated); border-radius: var(--radius-sm); margin-bottom: 8px; border-left: 3px solid var(--success); } .payment-item.previous-period { border-left-color: var(--purple); } .payment-item-info { display: flex; flex-direction: column; gap: 4px; } .payment-item-company { font-weight: 500; color: var(--text-primary); } .payment-item-meta { font-size: 0.8rem; color: var(--text-muted); } .payment-item-amount { font-size: 1.1rem; font-weight: 600; color: var(--success); } .payment-item.previous-period .payment-item-amount { color: var(--purple); } /* Modal */ .modal-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); display: none; align-items: center; justify-content: center; z-index: 1000; padding: 20px; } .modal-overlay.show { display: flex; } .modal { background: var(--bg-surface); border-radius: var(--radius-lg); max-width: 900px; width: 100%; max-height: 90vh; overflow-y: auto; box-shadow: var(--shadow-md); } .modal-header { padding: 20px 24px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; } .modal-title { font-size: 1.1rem; font-weight: 600; } .modal-close { background: var(--bg-elevated); border: none; width: 32px; height: 32px; border-radius: var(--radius-sm); color: var(--text-muted); cursor: pointer; font-size: 1.2rem; display: flex; align-items: center; justify-content: center; } .modal-close:hover { background: var(--danger-soft); color: var(--danger); } .modal-body { padding: 24px; } .modal-footer { padding: 16px 24px; border-top: 1px solid var(--border-color); display: flex; justify-content: flex-end; gap: 10px; } /* Tabs */ .tabs { display: flex; gap: 4px; background: var(--bg-elevated); padding: 4px; border-radius: var(--radius-sm); margin-bottom: 16px; } .tab { padding: 8px 16px; border: none; background: none; color: var(--text-muted); font-size: 0.8rem; font-weight: 500; cursor: pointer; border-radius: 4px; transition: all 0.15s; } .tab:hover { color: var(--text-secondary); } .tab.active { background: var(--bg-card); color: var(--text-primary); box-shadow: var(--shadow-sm); } /* Toast */ .toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 2000; } .toast { padding: 12px 20px; border-radius: var(--radius-sm); font-size: 0.875rem; font-weight: 500; box-shadow: var(--shadow-md); margin-top: 8px; animation: slideIn 0.2s ease; } @keyframes slideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .toast-success { background: var(--success); color: white; } .toast-error { background: var(--danger); color: white; } .toast-info { background: var(--accent); color: white; } /* Warning */ .warning-box { background: var(--warning-soft); border: 1px solid var(--warning); border-radius: var(--radius-sm); padding: 10px 14px; margin-top: 8px; font-size: 0.8rem; color: var(--warning); display: none; } .warning-box.show { display: block; } /* Connection Status */ .connection-status { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: var(--radius-sm); font-size: 0.75rem; margin-bottom: 12px; } .connection-status.connected { background: var(--success-soft); color: var(--success); } .connection-status.disconnected { background: var(--danger-soft); color: var(--danger); } .connection-status .dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; } /* Action Button */ .action-btn { padding: 5px 10px; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-elevated); color: var(--text-secondary); cursor: pointer; font-size: 0.75rem; transition: all 0.15s; } .action-btn:hover { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); } /* Code */ code { background: var(--bg-elevated); padding: 2px 6px; border-radius: 4px; font-family: 'SF Mono', Consolas, monospace; font-size: 0.8rem; color: var(--accent); } /* Matrix */ .matrix-cell { text-align: center; } .matrix-cell.highlight { background: var(--purple-soft); color: var(--purple); font-weight: 600; } .matrix-cell.current { background: var(--success-soft); color: var(--success); font-weight: 600; } /* Empty State */ .empty-state { text-align: center; padding: 40px 20px; color: var(--text-muted); } /* Summary Cards Row */ .summary-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-bottom: 24px; } @media (max-width: 900px) { .summary-row { grid-template-columns: 1fr; } } .summary-card { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius); padding: 20px; } .summary-card-title { font-size: 0.9rem; font-weight: 600; margin-bottom: 16px; color: var(--text-primary); display: flex; align-items: center; gap: 8px; } .summary-list { list-style: none; } .summary-list li { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px dashed var(--border-light); } .summary-list li:last-child { border-bottom: none; } /* Plan Input */ .plan-input-group { display: flex; align-items: center; gap: 8px; } .plan-input { width: 120px; padding: 6px 10px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); background: var(--bg-input); color: var(--text-primary); font-size: 0.85rem; text-align: right; } .plan-input:focus { outline: none; border-color: var(--accent); } /* Responsive */ @media (max-width: 768px) { .sidebar { transform: translateX(-100%); transition: transform 0.3s; } .sidebar.open { transform: translateX(0); } .main { margin-left: 0; max-width: 100vw; padding: 16px; } .charts-grid { grid-template-columns: 1fr; } .funnel { flex-direction: column; } .funnel-arrow { transform: rotate(90deg); } .page-header { flex-direction: column; } .period-filter { width: 100%; } } /* Mobile Menu Button */ .mobile-menu-btn { display: none; position: fixed; top: 16px; left: 16px; z-index: 101; background: var(--bg-sidebar); border: none; border-radius: var(--radius-sm); padding: 10px; color: white; cursor: pointer; } @media (max-width: 768px) { .mobile-menu-btn { display: block; } } /* Failed Leads List */ .failed-lead-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: var(--danger-soft); border-radius: var(--radius-sm); margin-bottom: 8px; border-left: 3px solid var(--danger); } .failed-lead-info { display: flex; flex-direction: column; gap: 2px; } .failed-lead-company { font-weight: 500; color: var(--text-primary); } .failed-lead-meta { font-size: 0.8rem; color: var(--text-muted); } .failed-lead-manager { font-size: 0.85rem; color: var(--danger); font-weight: 500; }
Предыдущий период Тот же период год назад Выбрать период
-
Всего лидов
-
Оплачено
-
В работе
-
Провалов
-
Выручка
%
-
Конверсия
Выполнение плана маркетинга
- / -
0% Осталось: -
Выполнение плана продаж
- / -
0% Осталось: -
Лучший менеджер
-
Оплат: - Сумма: - Конверсия: -
Лучший маркетолог
-
Лидов: - Оплат: - Выполнение: -
-
Лиды
->
-
Счета
->
-
Оплаты
->
-
Выручка
Динамика по периодам
Распределение по источникам
Провалы по менеджерам
Эффективность источников
Источник Лидов Оплат Конверсия Выручка
Лид с таким номером уже существует
Выберите Рассылка Авито Яндекс Директ Сайт TG канал Статус TG Статус WA ВК Квиз Дзен
Выберите Форма обратной связи Почта Звонок Авито WA TG Онлайн-чат Яндекс Директ
Выберите Дима Регина Паша Кристина Илья
Выберите Даша Ш Виктория Г Кристина В Кристина А Екатерина И Суббота М Ефимова Усова
Новый В работе КП отправлено Переговоры Отказ Провал
Данные по счету

Выберите Даша Ш Виктория Г Кристина В Кристина А Екатерина И Суббота М Ефимова Усова
Бронь Оплата Частичная оплата Перевыставлен Отказ
-
Всего оплат за месяц
-
С лидов текущего месяца
-
С лидов прошлых периодов
#
-
Количество оплат
Текущий месяц Оплаты с лидов текущего месяца
Количество оплат: -
Сумма: -
Доля от общей суммы: -
Прошлые периоды Оплаты с лидов прошлых периодов
Количество оплат: -
Сумма: -
Доля от общей суммы: -
Структура оплат
Оплаты по услугам
Оплаты с лидов текущего месяца
0 оплат
Нет данных
Оплаты с лидов прошлых периодов
0 оплат
Нет данных
Матрица: месяц лида / месяц оплаты
Оплаты по источникам
Источник Оплат (тек. месяц) Сумма (тек.) Оплат (прошлые) Сумма (прошлые) Всего
Все Рассылка Авито Яндекс Директ Сайт TG канал Статус TG Статус WA ВК Квиз Дзен
%
-
Конверсия
R
-
Средний чек
-
Оплат с прошлых периодов
-
Провалов
Лиды и оплаты по источникам
Доля выручки
Детализация
Источник Лидов Счетов Оплат Провалов Конверсия Ср. чек Выручка
Все Даша Ш Виктория Г Кристина В Кристина А Екатерина И Суббота М Ефимова Усова
#
-
Всего лидов
R
-
Общая выручка
#
-
Оплат с прошлых периодов
R
-
Сумма с прошлых периодов
Сравнение менеджеров
Менеджер Лидов Оплат Сумма оплат Конверсия Оплат (прошл.) Сумма (прошл.) Всего за месяц Всего оплат План % плана Провалов
Выручка по менеджерам
Конверсия по менеджерам
Провалы по менеджерам
Выполнение плана
Проваленные лиды
Нет проваленных лидов
Все Дима Регина Паша Кристина Илья
#
-
Всего лидов
#
-
Оплат по лидам
R
-
Сумма оплат
#
-
Провалов
Выполнение плана отдела маркетинга
- / -
0% Осталось: -
Сравнение маркетологов
Маркетолог Лидов Оплат Сумма оплат Конверсия План % плана Провалов % провалов
Лиды по маркетологам
Выручка по маркетологам
Выполнение плана
Провалы по маркетологам
Проваленные лиды
Нет проваленных лидов
Отдел продаж - Планы менеджеров
Менеджер План на месяц Текущий результат Выполнение Осталось
Даша Ш - - -
Виктория Г - - -
Кристина В - - -
Кристина А - - -
Екатерина И - - -
Суббота М - - -
Ефимова - - -
Усова - - -
Отдел маркетинга - Планы маркетологов
Маркетолог План на месяц Текущий результат Выполнение Осталось
Дима - - -
Регина - - -
Паша - - -
Кристина - - -
Илья - - -
История выполнения планов
Все Новый В работе КП отправлено Переговоры Отказ Провал
Все Нет счета Бронь Оплата Частичная Отказ
Все Даша Ш Виктория Г Кристина В Кристина А Екатерина И Суббота М Ефимова Усова
Результаты
Номер Компания Услуга Источник Маркетолог Менеджер Дата Статус Счет Оплата
// ==================== CONFIGURATION ==================== const GAS_URL = 'https://script.google.com/macros/s/AKfycbxthBSdcl1EykIdXKsJiWjuB-meog4BPvCjNtwze3sLk7S2Gp5FLjNThLBYm57d39OR/exec'; // ==================== STATE ==================== let leads = []; let filteredLeads = []; let plans = { managers: {}, marketers: {}, salesTotal: 0, marketingTotal: 0 }; let isConnected = false; let sortState = {}; let currentPeriod = 'month'; let periodDates = { from: null, to: null }; let comparePeriodDates = { from: null, to: null }; let compareEnabled = false; const MANAGERS = ['Даша Ш', 'Виктория Г', 'Кристина В', 'Кристина А', 'Екатерина И', 'Суббота М', 'Ефимова', 'Усова']; const MARKETERS = ['Дима', 'Регина', 'Паша', 'Кристина', 'Илья']; const chartColors = { blue: '#3b82f6', green: '#10b981', yellow: '#f59e0b', red: '#ef4444', purple: '#8b5cf6', cyan: '#06b6d4', palette: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444', '#06b6d4', '#ec4899', '#84cc16', '#f97316', '#14b8a6'] }; let charts = {}; // ==================== API ==================== async function apiCall(action, data = {}) { if (!GAS_URL || GAS_URL.includes('ВСТАВЬТЕ')) { console.warn('GAS URL не настроен'); return { success: false, error: 'GAS URL не настроен' }; } try { const response = await fetch(GAS_URL, { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'text/plain' }, body: JSON.stringify({ action, ...data }) }); const result = await response.json(); if (!isConnected) { isConnected = true; updateConnectionStatus(); } return result; } catch (error) { console.error('API Error:', error); isConnected = false; updateConnectionStatus(); return { success: false, error: error.message }; } } function updateConnectionStatus() { const el = document.getElementById('connectionStatus'); const text = document.getElementById('connectionText'); if (isConnected) { el.className = 'connection-status connected'; text.textContent = 'Подключено'; } else { el.className = 'connection-status disconnected'; text.textContent = !GAS_URL || GAS_URL.includes('ВСТАВЬТЕ') ? 'Настройте GAS URL' : 'Нет соединения'; } } function showLoading() { const overlay = document.createElement('div'); overlay.className = 'loading-overlay'; overlay.id = 'loadingOverlay'; overlay.innerHTML = '
'; document.body.appendChild(overlay); } function hideLoading() { const overlay = document.getElementById('loadingOverlay'); if (overlay) overlay.remove(); } // ==================== DATA OPERATIONS ==================== async function loadLeads() { showLoading(); const result = await apiCall('getLeads'); hideLoading(); if (result.success) { leads = result.data || []; filteredLeads = [...leads]; return true; } else { showToast('Ошибка загрузки данных', 'error'); return false; } } async function loadPlansData() { const result = await apiCall('getPlans'); if (result.success && result.data) { plans = result.data; } } async function savePlans(type) { const month = document.getElementById('planMonth').value; if (!month) { showToast('Выберите месяц', 'error'); return; } const planData = { month: month, type: type }; if (type === 'managers') { planData.salesTotal = parseFloat(document.getElementById('planSalesTotal').value) || 0; planData.plans = {}; document.querySelectorAll('.plan-input[data-manager]').forEach(input => { const manager = input.dataset.manager; planData.plans[manager] = parseFloat(input.value) || 0; }); } else { planData.marketingTotal = parseFloat(document.getElementById('planMarketingTotal').value) || 0; planData.plans = {}; document.querySelectorAll('.plan-input[data-marketer]').forEach(input => { const marketer = input.dataset.marketer; planData.plans[marketer] = parseFloat(input.value) || 0; }); } showLoading(); const result = await apiCall('savePlans', planData); hideLoading(); if (result.success) { showToast('Планы сохранены', 'success'); await loadPlansData(); updatePlanResults(); } else { showToast(result.error || 'Ошибка сохранения', 'error'); } } async function saveLead(leadData) { showLoading(); const result = await apiCall('addLead', { lead: leadData }); hideLoading(); if (result.success) { showToast('Лид сохранен', 'success'); await loadLeads(); return true; } else { showToast(result.error || 'Ошибка сохранения', 'error'); return false; } } async function updateLead(rowIndex, leadData) { showLoading(); const result = await apiCall('updateLead', { rowIndex, lead: leadData }); hideLoading(); if (result.success) { showToast('Данные обновлены', 'success'); await loadLeads(); return true; } else { showToast(result.error || 'Ошибка обновления', 'error'); return false; } } async function checkDuplicate(number) { const result = await apiCall('checkDuplicate', { number }); return result.success && result.exists; } // ==================== PERIOD HELPERS ==================== function getPeriodDates(period) { const now = new Date(); let from, to; switch (period) { case 'day': from = new Date(now.getFullYear(), now.getMonth(), now.getDate()); to = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59); break; case 'week': const dayOfWeek = now.getDay(); const diffToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1; from = new Date(now.getFullYear(), now.getMonth(), now.getDate() - diffToMonday); to = new Date(from.getFullYear(), from.getMonth(), from.getDate() + 6, 23, 59, 59); break; case 'month': from = new Date(now.getFullYear(), now.getMonth(), 1); to = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59); break; case 'quarter': const quarterMonth = Math.floor(now.getMonth() / 3) * 3; from = new Date(now.getFullYear(), quarterMonth, 1); to = new Date(now.getFullYear(), quarterMonth + 3, 0, 23, 59, 59); break; case 'year': from = new Date(now.getFullYear(), 0, 1); to = new Date(now.getFullYear(), 11, 31, 23, 59, 59); break; case 'custom': from = document.getElementById('periodFrom').value ? new Date(document.getElementById('periodFrom').value) : null; to = document.getElementById('periodTo').value ? new Date(document.getElementById('periodTo').value + 'T23:59:59') : null; break; default: from = new Date(now.getFullYear(), now.getMonth(), 1); to = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59); } return { from, to }; } function getComparePeriodDates() { const type = document.getElementById('comparePeriodType').value; const current = periodDates; if (!current.from || !current.to) return { from: null, to: null }; const duration = current.to - current.from; switch (type) { case 'prev': return { from: new Date(current.from.getTime() - duration - 86400000), to: new Date(current.from.getTime() - 86400000) }; case 'prevYear': return { from: new Date(current.from.getFullYear() - 1, current.from.getMonth(), current.from.getDate()), to: new Date(current.to.getFullYear() - 1, current.to.getMonth(), current.to.getDate(), 23, 59, 59) }; case 'custom': const fromVal = document.getElementById('compareFrom').value; const toVal = document.getElementById('compareTo').value; return { from: fromVal ? new Date(fromVal) : null, to: toVal ? new Date(toVal + 'T23:59:59') : null }; default: return { from: null, to: null }; } } function filterByPeriod(data, from, to, dateField = 'leadDate') { if (!from && !to) return data; return data.filter(item => { const itemDate = item[dateField] ? new Date(item[dateField]) : null; if (!itemDate) return false; if (from && itemDate to) return false; return true; }); } function formatDateISO(date) { if (!date) return ''; const d = new Date(date); return d.toISOString().slice(0, 10); } function getCurrentMonth() { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; } function getMonthDates(monthStr) { const [year, month] = monthStr.split('-').map(Number); return { from: new Date(year, month - 1, 1), to: new Date(year, month, 0, 23, 59, 59) }; } // ==================== INIT ==================== async function init() { // Theme const savedTheme = localStorage.getItem('theme') || 'light'; setTheme(savedTheme); // Set default dates document.getElementById('leadDate').valueAsDate = new Date(); document.getElementById('planMonth').value = getCurrentMonth(); document.getElementById('paymentsMonth').value = getCurrentMonth(); // Set period dates periodDates = getPeriodDates('month'); // Check connection updateConnectionStatus(); // Load data const loaded = await loadLeads(); await loadPlansData(); if (loaded) { updateDashboard(); showToast('Загружено: ' + leads.length + ' лидов', 'info'); } // Setup event listeners setupEventListeners(); setupSortableHeaders(); } function setupEventListeners() { // Navigation document.querySelectorAll('.nav-item').forEach(item => { item.addEventListener('click', function() { document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active')); document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); this.classList.add('active'); const page = this.dataset.page; document.getElementById(page).classList.add('active'); // Update page content if (page === 'dashboard') updateDashboard(); if (page === 'payments-analytics') updatePaymentsAnalytics(); if (page === 'sources') updateSourcesPage(); if (page === 'managers') updateManagersPage(); if (page === 'marketers') updateMarketersPage(); if (page === 'plans') { loadPlans(); updatePlanResults(); } if (page === 'all-leads') filterAllLeads(); // Close mobile sidebar document.getElementById('sidebar').classList.remove('open'); }); }); // Period filter buttons document.querySelectorAll('.period-btn').forEach(btn => { btn.addEventListener('click', function() { const period = this.dataset.period; document.querySelectorAll('.period-btn').forEach(b => b.classList.remove('active')); this.classList.add('active'); currentPeriod = period; // Show/hide custom period inputs const customEl = document.getElementById('periodCustom'); if (period === 'custom') { customEl.style.display = 'flex'; } else { customEl.style.display = 'none'; periodDates = getPeriodDates(period); updateDashboard(); } }); }); // Custom period inputs document.getElementById('periodFrom').addEventListener('change', function() { periodDates = getPeriodDates('custom'); updateDashboard(); }); document.getElementById('periodTo').addEventListener('change', function() { periodDates = getPeriodDates('custom'); updateDashboard(); }); // Compare toggle document.getElementById('enableCompare').addEventListener('change', function() { compareEnabled = this.checked; document.getElementById('comparePeriodCard').style.display = this.checked ? 'block' : 'none'; updateDashboard(); }); // Compare period type document.getElementById('comparePeriodType').addEventListener('change', function() { const isCustom = this.value === 'custom'; document.getElementById('compareCustomDates').style.display = isCustom ? 'block' : 'none'; document.getElementById('compareCustomDatesTo').style.display = isCustom ? 'block' : 'none'; if (!isCustom) { comparePeriodDates = getComparePeriodDates(); updateDashboard(); } }); document.getElementById('compareFrom').addEventListener('change', function() { comparePeriodDates = getComparePeriodDates(); updateDashboard(); }); document.getElementById('compareTo').addEventListener('change', function() { comparePeriodDates = getComparePeriodDates(); updateDashboard(); }); // Marketer filter document.getElementById('currentMarketer').addEventListener('change', function() { const page = document.querySelector('.page.active').id; if (page === 'dashboard') updateDashboard(); if (page === 'payments-analytics') updatePaymentsAnalytics(); if (page === 'sources') updateSourcesPage(); if (page === 'managers') updateManagersPage(); if (page === 'marketers') updateMarketersPage(); if (page === 'all-leads') filterAllLeads(); }); // Payments month filter document.getElementById('paymentsMonth').addEventListener('change', function() { updatePaymentsAnalytics(); }); // Lead form - duplicate check document.getElementById('leadNumber').addEventListener('input', async function() { const val = this.value.trim(); if (val.length >= 2) { const exists = await checkDuplicate(val); document.getElementById('duplicateWarning').classList.toggle('show', exists); } else { document.getElementById('duplicateWarning').classList.remove('show'); } }); // Lead form submit document.getElementById('leadForm').addEventListener('submit', async function(e) { e.preventDefault(); const leadData = { number: document.getElementById('leadNumber').value || 'AUTO-' + Date.now(), company: document.getElementById('companyName').value, phone: String(document.getElementById('phone').value || ''), email: document.getElementById('email').value, service: document.getElementById('service').value, source: document.getElementById('source').value, channel: document.getElementById('channel').value, marketer: document.getElementById('marketer').value, manager: document.getElementById('manager').value, leadDate: document.getElementById('leadDate').value || formatDateISO(new Date()), leadStatus: document.getElementById('leadStatus').value, comment: document.getElementById('comment').value }; const saved = await saveLead(leadData); if (saved) { this.reset(); document.getElementById('leadDate').valueAsDate = new Date(); document.getElementById('duplicateWarning').classList.remove('show'); } }); // Payment search document.getElementById('paymentSearch').addEventListener('input', function() { const q = this.value.toLowerCase(); if (q.length (l.number && l.number.toLowerCase().includes(q)) || (l.company && l.company.toLowerCase().includes(q)) ).slice(0, 5); renderPaymentSearchResults(results); }); // Payment form submit document.getElementById('paymentForm').addEventListener('submit', async function(e) { e.preventDefault(); const rowIndex = document.getElementById('paymentLeadId').value; const leadData = { invoiceAmount: parseFloat(document.getElementById('invoiceAmount').value) || null, invoiceDate: document.getElementById('invoiceDate').value || null, invoiceManager: document.getElementById('invoiceManager').value, invoiceStatus: document.getElementById('invoiceStatus').value, paidAmount: parseFloat(document.getElementById('paidAmount').value) || null, paymentDate: document.getElementById('paymentDate').value || null, invoiceComment: document.getElementById('invoiceComment').value }; const updated = await updateLead(rowIndex, leadData); if (updated) { cancelPayment(); document.getElementById('paymentSearch').value = ''; document.getElementById('paymentSearchResults').innerHTML = ''; } }); // All leads search document.getElementById('allLeadsSearch').addEventListener('input', filterAllLeads); // Modal tabs document.querySelectorAll('.modal .tab').forEach(tab => { tab.addEventListener('click', function() { document.querySelectorAll('.modal .tab').forEach(t => t.classList.remove('active')); this.classList.add('active'); const tabName = this.dataset.tab; document.getElementById('editTabLead').style.display = tabName === 'lead' ? 'block' : 'none'; document.getElementById('editTabInvoice').style.display = tabName === 'invoice' ? 'block' : 'none'; }); }); // Modal close document.getElementById('editModal').addEventListener('click', function(e) { if (e.target.id === 'editModal') closeModal(); }); document.getElementById('failedLeadsModal').addEventListener('click', function(e) { if (e.target.id === 'failedLeadsModal') closeFailedLeadsModal(); }); document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closeModal(); closeFailedLeadsModal(); } }); // Failed leads modal filters document.getElementById('failedFilterManager').addEventListener('change', renderFailedLeadsModal); document.getElementById('failedFilterMarketer').addEventListener('change', renderFailedLeadsModal); } function setupSortableHeaders() { document.querySelectorAll('th.sortable').forEach(th => { th.addEventListener('click', function() { const table = this.closest('table'); const tableId = table.id; const field = this.dataset.field; if (!sortState[tableId]) sortState[tableId] = {}; if (sortState[tableId].field === field) { sortState[tableId].asc = !sortState[tableId].asc; } else { sortState[tableId].field = field; sortState[tableId].asc = true; } table.querySelectorAll('th').forEach(h => { h.classList.remove('sort-asc', 'sort-desc'); }); this.classList.add(sortState[tableId].asc ? 'sort-asc' : 'sort-desc'); // Re-render appropriate table if (tableId === 'allLeadsTable') renderAllLeads(); if (tableId === 'dashSourceTable') renderDashSourceTable(getFilteredData()); if (tableId === 'srcTable') updateSourcesPage(); if (tableId === 'mgrTable') updateManagersPage(); if (tableId === 'mktTable') updateMarketersPage(); }); }); } // ==================== HELPERS ==================== function getFilteredData() { const marketer = document.getElementById('currentMarketer').value; let data = marketer ? leads.filter(l => l.marketer === marketer) : [...leads]; if (periodDates.from || periodDates.to) { data = filterByPeriod(data, periodDates.from, periodDates.to, 'leadDate'); } return data; } function getFilteredByMarketer() { const marketer = document.getElementById('currentMarketer').value; return marketer ? leads.filter(l => l.marketer === marketer) : leads; } function sortData(data, tableId) { const state = sortState[tableId]; if (!state || !state.field) return data; return [...data].sort((a, b) => { let valA = a[state.field]; let valB = b[state.field]; if (valA == null) valA = ''; if (valB == null) valB = ''; if (typeof valA === 'number' && typeof valB === 'number') { return state.asc ? valA - valB : valB - valA; } return state.asc ? String(valA).localeCompare(String(valB)) : String(valB).localeCompare(String(valA)); }); } function formatCurrency(n) { return new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format(n || 0); } function formatShortCurrency(n) { if (!n) return '0'; if (n >= 1000000) return (n / 1000000).toFixed(1) + ' млн'; if (n >= 1000) return Math.round(n / 1000) + ' тыс'; return n.toString(); } function formatDate(d) { return d ? new Date(d).toLocaleDateString('ru-RU') : '-'; } function formatPercent(value, total) { if (!total) return '0%'; return Math.round((value / total) * 100) + '%'; } function getProgressBarClass(percent) { if (percent >= 100) return 'green'; if (percent >= 70) return 'blue'; if (percent >= 40) return 'yellow'; return 'red'; } function getBadge(status) { const map = { 'бронь': 'badge-yellow', 'оплата': 'badge-green', 'частичная оплата': 'badge-purple', 'перевыставлен': 'badge-yellow', 'отказ': 'badge-red' }; return '' + status + ''; } function getStatusBadge(status) { const map = { 'новый': 'badge-blue', 'в работе': 'badge-yellow', 'КП отправлено': 'badge-purple', 'переговоры': 'badge-yellow', 'отказ': 'badge-red', 'провал': 'badge-red' }; return '' + status + ''; } function showToast(msg, type) { const t = document.createElement('div'); t.className = 'toast toast-' + type; t.textContent = msg; document.getElementById('toastContainer').appendChild(t); setTimeout(function() { t.remove(); }, 3000); } function getChartOptions() { const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; const textColor = isDark ? '#94a3b8' : '#64748b'; const gridColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'; return { textColor, gridColor }; } function toggleTheme() { const current = document.documentElement.getAttribute('data-theme'); setTheme(current === 'dark' ? 'light' : 'dark'); } function setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); document.getElementById('themeLabel').textContent = theme === 'dark' ? 'Светлая тема' : 'Темная тема'; // Recreate charts Object.values(charts).forEach(function(chart) { if (chart) chart.destroy(); }); charts = {}; const page = document.querySelector('.page.active'); if (page) { const pageId = page.id; if (pageId === 'dashboard') updateDashboard(); if (pageId === 'payments-analytics') updatePaymentsAnalytics(); if (pageId === 'sources') updateSourcesPage(); if (pageId === 'managers') updateManagersPage(); if (pageId === 'marketers') updateMarketersPage(); } } function toggleSidebar() { document.getElementById('sidebar').classList.toggle('open'); } // ==================== DASHBOARD ==================== function updateDashboard() { const marketer = document.getElementById('currentMarketer').value; const data = getFilteredData(); document.getElementById('dashboardSubtitle').textContent = marketer ? 'Статистика для: ' + marketer : 'Статистика по всем маркетологам'; // Calculate metrics const total = data.length; const paid = data.filter(l => l.paidAmount > 0); const inProgress = data.filter(l => ['новый', 'в работе', 'КП отправлено', 'переговоры'].includes(l.leadStatus) && !l.paidAmount ); const failed = data.filter(l => l.leadStatus === 'провал'); const revenue = paid.reduce(function(s, l) { return s + (l.paidAmount || 0); }, 0); const withInvoice = data.filter(l => l.invoiceStatus); const conversion = total > 0 ? Math.round((paid.length / total) * 100) : 0; // Update stats document.getElementById('dashTotal').textContent = total; document.getElementById('dashPaid').textContent = paid.length; document.getElementById('dashInProgress').textContent = inProgress.length; document.getElementById('dashRefused').textContent = failed.length; document.getElementById('dashRevenue').textContent = formatShortCurrency(revenue); document.getElementById('dashConversion').textContent = conversion + '%'; // Compare period if (compareEnabled) { comparePeriodDates = getComparePeriodDates(); if (comparePeriodDates.from && comparePeriodDates.to) { const compareData = filterByPeriod(getFilteredByMarketer(), comparePeriodDates.from, comparePeriodDates.to, 'leadDate'); const cTotal = compareData.length; const cPaid = compareData.filter(l => l.paidAmount > 0); const cInProgress = compareData.filter(l => ['новый', 'в работе', 'КП отправлено', 'переговоры'].includes(l.leadStatus) && !l.paidAmount ); const cFailed = compareData.filter(l => l.leadStatus === 'провал'); const cRevenue = cPaid.reduce(function(s, l) { return s + (l.paidAmount || 0); }, 0); const cConversion = cTotal > 0 ? Math.round((cPaid.length / cTotal) * 100) : 0; updateCompareStats('dashTotal', total, cTotal); updateCompareStats('dashPaid', paid.length, cPaid.length); updateCompareStats('dashInProgress', inProgress.length, cInProgress.length); updateCompareStats('dashRefused', failed.length, cFailed.length); updateCompareStats('dashRevenue', revenue, cRevenue, true); updateCompareStats('dashConversion', conversion, cConversion, false, true); } } else { hideCompareStats(); } // Funnel document.getElementById('funnelLeads').textContent = total; document.getElementById('funnelInvoices').textContent = withInvoice.length; document.getElementById('funnelPaid').textContent = paid.length; document.getElementById('funnelRevenue').textContent = formatShortCurrency(revenue); // Plans updateDashboardPlans(data); // Top performers updateTopPerformers(data); // Charts updateDashCharts(data); // Source table renderDashSourceTable(data); // Failed chart updateFailedChart(data); } function updateCompareStats(baseId, current, previous, isCurrency, isPercent) { const compareEl = document.getElementById(baseId + 'Compare'); if (!compareEl) return; compareEl.style.display = 'block'; let diff = current - previous; let diffPercent = previous > 0 ? Math.round((diff / previous) * 100) : (current > 0 ? 100 : 0); let prevDisplay = previous; if (isCurrency) prevDisplay = formatShortCurrency(previous); if (isPercent) prevDisplay = previous + '%'; let sign = diff > 0 ? '+' : ''; let diffDisplay = diff; if (isCurrency) diffDisplay = formatShortCurrency(Math.abs(diff)); if (isPercent) diffDisplay = Math.abs(diff) + ' п.п.'; const isPositive = diff > 0; const colorClass = isPositive ? 'color: var(--success)' : (diff < 0 ? 'color: var(--danger)' : ''); compareEl.innerHTML = 'Пред. период: ' + prevDisplay + ' ' + '(' + sign + diffDisplay + ')'; } function hideCompareStats() { ['dashTotal', 'dashPaid', 'dashInProgress', 'dashRefused', 'dashRevenue', 'dashConversion'].forEach(function(id) { const el = document.getElementById(id + 'Compare'); if (el) el.style.display = 'none'; }); } function updateDashboardPlans(data) { const now = new Date(); const currentMonth = getCurrentMonth(); // Get payments for current month const monthPayments = leads.filter(function(l) { if (!l.paidAmount || !l.paymentDate) return false; return l.paymentDate.startsWith(currentMonth); }); // Marketing plan (by marketer's leads) const marketer = document.getElementById('currentMarketer').value; let marketingRevenue = 0; let marketingPlan = 0; if (marketer) { marketingRevenue = monthPayments .filter(l => l.marketer === marketer) .reduce(function(s, l) { return s + (l.paidAmount || 0); }, 0); marketingPlan = (plans.marketers && plans.marketers[marketer]) || 0; } else { marketingRevenue = monthPayments.reduce(function(s, l) { return s + (l.paidAmount || 0); }, 0); marketingPlan = plans.marketingTotal || 0; } const marketingPercent = marketingPlan > 0 ? Math.round((marketingRevenue / marketingPlan) * 100) : 0; const marketingRemain = Math.max(0, marketingPlan - marketingRevenue); document.getElementById('dashMarketingPlanValue').textContent = formatShortCurrency(marketingRevenue) + ' / ' + formatShortCurrency(marketingPlan); document.getElementById('dashMarketingPlanBar').style.width = Math.min(100, marketingPercent) + '%'; document.getElementById('dashMarketingPlanBar').className = 'progress-bar-fill ' + getProgressBarClass(marketingPercent); document.getElementById('dashMarketingPlanPercent').textContent = marketingPercent + '%'; document.getElementById('dashMarketingPlanRemain').textContent = 'Осталось: ' + formatShortCurrency(marketingRemain); // Sales plan const salesRevenue = monthPayments.reduce(function(s, l) { return s + (l.paidAmount || 0); }, 0); const salesPlan = plans.salesTotal || 0; const salesPercent = salesPlan > 0 ? Math.round((salesRevenue / salesPlan) * 100) : 0; const salesRemain = Math.max(0, salesPlan - salesRevenue); document.getElementById('dashSalesPlanValue').textContent = formatShortCurrency(salesRevenue) + ' / ' + formatShortCurrency(salesPlan); document.getElementById('dashSalesPlanBar').style.width = Math.min(100, salesPercent) + '%'; document.getElementById('dashSalesPlanBar').className = 'progress-bar-fill ' + getProgressBarClass(salesPercent); document.getElementById('dashSalesPlanPercent').textContent = salesPercent + '%'; document.getElementById('dashSalesPlanRemain').textContent = 'Осталось: ' + formatShortCurrency(salesRemain); } function updateTopPerformers(data) { const currentMonth = getCurrentMonth(); // Best manager const managerStats = {}; MANAGERS.forEach(function(m) { managerStats[m] = { leads: 0, paid: 0, revenue: 0 }; }); leads.forEach(function(l) { if (!l.manager || !managerStats[l.manager]) return; if (l.paymentDate && l.paymentDate.startsWith(currentMonth)) { managerStats[l.manager].paid++; managerStats[l.manager].revenue += l.paidAmount || 0; } if (l.leadDate && l.leadDate.startsWith(currentMonth)) { managerStats[l.manager].leads++; } }); let bestManager = null; let bestManagerRevenue = 0; Object.keys(managerStats).forEach(function(m) { if (managerStats[m].revenue > bestManagerRevenue) { bestManagerRevenue = managerStats[m].revenue; bestManager = m; } }); if (bestManager) { const stats = managerStats[bestManager]; const conv = stats.leads > 0 ? Math.round((stats.paid / stats.leads) * 100) : 0; document.getElementById('topManagerName').textContent = bestManager; document.getElementById('topManagerPaid').textContent = stats.paid; document.getElementById('topManagerSum').textContent = formatCurrency(stats.revenue); document.getElementById('topManagerConv').textContent = conv + '%'; } else { document.getElementById('topManagerName').textContent = '-'; document.getElementById('topManagerPaid').textContent = '-'; document.getElementById('topManagerSum').textContent = '-'; document.getElementById('topManagerConv').textContent = '-'; } // Best marketer const marketerStats = {}; MARKETERS.forEach(function(m) { marketerStats[m] = { leads: 0, revenue: 0, plan: (plans.marketers && plans.marketers[m]) || 0 }; }); leads.forEach(function(l) { if (!l.marketer || !marketerStats[l.marketer]) return; if (l.leadDate && l.leadDate.startsWith(currentMonth)) { marketerStats[l.marketer].leads++; } if (l.paymentDate && l.paymentDate.startsWith(currentMonth)) { marketerStats[l.marketer].revenue += l.paidAmount || 0; } }); let bestMarketer = null; let bestMarketerRevenue = 0; Object.keys(marketerStats).forEach(function(m) { if (marketerStats[m].revenue > bestMarketerRevenue) { bestMarketerRevenue = marketerStats[m].revenue; bestMarketer = m; } }); if (bestMarketer) { const stats = marketerStats[bestMarketer]; const planPercent = stats.plan > 0 ? Math.round((stats.revenue / stats.plan) * 100) : 0; document.getElementById('topMarketerName').textContent = bestMarketer; document.getElementById('topMarketerLeads').textContent = stats.leads; document.getElementById('topMarketerSum').textContent = formatCurrency(stats.revenue); document.getElementById('topMarketerPlan').textContent = planPercent + '%'; } else { document.getElementById('topMarketerName').textContent = '-'; document.getElementById('topMarketerLeads').textContent = '-'; document.getElementById('topMarketerSum').textContent = '-'; document.getElementById('topMarketerPlan').textContent = '-'; } } function updateDashCharts(data) { const opts = getChartOptions(); const months = ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек']; // Leads and payments by month const monthlyLeads = months.map(function(_, i) { return data.filter(function(l) { return l.leadDate && new Date(l.leadDate).getMonth() === i; }).length; }); const monthlyPaid = months.map(function(_, i) { return data.filter(function(l) { return l.paymentDate && new Date(l.paymentDate).getMonth() === i; }).length; }); if (charts.dashChart) charts.dashChart.destroy(); charts.dashChart = new Chart(document.getElementById('dashChart'), { type: 'bar', data: { labels: months, datasets: [ { label: 'Лиды', data: monthlyLeads, backgroundColor: chartColors.blue, borderRadius: 4 }, { label: 'Оплаты', data: monthlyPaid, backgroundColor: chartColors.green, borderRadius: 4 } ] }, options: { responsive: true, plugins: { legend: { labels: { color: opts.textColor } } }, scales: { x: { grid: { display: false }, ticks: { color: opts.textColor } }, y: { grid: { color: opts.gridColor }, ticks: { color: opts.textColor } } } } }); // Revenue by source const bySource = {}; data.forEach(function(l) { var s = l.source || 'Не указан'; if (!bySource[s]) bySource[s] = 0; bySource[s] += l.paidAmount || 0; }); var sourceLabels = Object.keys(bySource); var sourceValues = Object.values(bySource); if (charts.dashSourceChart) charts.dashSourceChart.destroy(); charts.dashSourceChart = new Chart(document.getElementById('dashSourceChart'), { type: 'doughnut', data: { labels: sourceLabels, datasets: [{ data: sourceValues, backgroundColor: chartColors.palette, borderWidth: 0 }] }, options: { responsive: true, plugins: { legend: { position: 'right', labels: { color: opts.textColor } } } } }); } function updateFailedChart(data) { const opts = getChartOptions(); const byManager = {}; MANAGERS.forEach(function(m) { byManager[m] = 0; }); data.forEach(function(l) { if (l.leadStatus === 'провал' && l.manager && byManager.hasOwnProperty(l.manager)) { byManager[l.manager]++; } }); var labels = Object.keys(byManager).filter(function(m) { return byManager[m] > 0; }); var values = labels.map(function(m) { return byManager[m]; }); if (charts.dashFailedChart) charts.dashFailedChart.destroy(); if (labels.length === 0) { document.getElementById('dashFailedChart').parentElement.innerHTML = '
Нет проваленных лидов за период
'; return; } charts.dashFailedChart = new Chart(document.getElementById('dashFailedChart'), { type: 'bar', data: { labels: labels, datasets: [{ label: 'Провалы', data: values, backgroundColor: chartColors.red, borderRadius: 4 }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { color: opts.gridColor }, ticks: { color: opts.textColor } }, y: { grid: { display: false }, ticks: { color: opts.textColor } } } } }); } function renderDashSourceTable(data) { const bySource = {}; data.forEach(function(l) { var s = l.source || 'Не указан'; if (!bySource[s]) bySource[s] = { source: s, leads: 0, paid: 0, revenue: 0 }; bySource[s].leads++; if (l.paidAmount) { bySource[s].paid++; bySource[s].revenue += l.paidAmount; } }); Object.values(bySource).forEach(function(d) { d.conv = d.leads ? Math.round((d.paid / d.leads) * 100) : 0; }); var tableData = Object.values(bySource); tableData = sortData(tableData, 'dashSourceTable'); if (!sortState.dashSourceTable || !sortState.dashSourceTable.field) { tableData.sort(function(a, b) { return b.leads - a.leads; }); } document.getElementById('dashSourceTableBody').innerHTML = tableData.map(function(d) { return '' + '' + d.source + '' + '' + d.leads + '' + '' + d.paid + '' + '' + d.conv + '%' + '' + formatCurrency(d.revenue) + '' + ''; }).join(''); } // ==================== PAYMENTS ANALYTICS ==================== function updatePaymentsAnalytics() { const monthStr = document.getElementById('paymentsMonth').value || getCurrentMonth(); const monthDates = getMonthDates(monthStr); const marketer = document.getElementById('currentMarketer').value; // Get all payments in selected month let paymentsThisMonth = leads.filter(function(l) { if (!l.paidAmount || !l.paymentDate) return false; var payDate = new Date(l.paymentDate); return payDate >= monthDates.from && payDate = monthDates.from && leadDate <= monthDates.to) { currentMonthLeads.push(l); } else { prevPeriodLeads.push(l); } }); // Calculate totals var totalSum = paymentsThisMonth.reduce(function(s, l) { return s + (l.paidAmount || 0); }, 0); var currentSum = currentMonthLeads.reduce(function(s, l) { return s + (l.paidAmount || 0); }, 0); var prevSum = prevPeriodLeads.reduce(function(s, l) { return s + (l.paidAmount || 0); }, 0); // Update stats document.getElementById('payTotalSum').textContent = formatCurrency(totalSum); document.getElementById('payCurrentLeadsSum').textContent = formatCurrency(currentSum); document.getElementById('payPrevLeadsSum').textContent = formatCurrency(prevSum); document.getElementById('payTotalCount').textContent = paymentsThisMonth.length; // Summary cards document.getElementById('payCurrentCount').textContent = currentMonthLeads.length; document.getElementById('payCurrentSumDisplay').textContent = formatCurrency(currentSum); document.getElementById('payCurrentPercent').textContent = formatPercent(currentSum, totalSum); document.getElementById('payPrevCount').textContent = prevPeriodLeads.length; document.getElementById('payPrevSumDisplay').textContent = formatCurrency(prevSum); document.getElementById('payPrevPercent').textContent = formatPercent(prevSum, totalSum); // Badges document.getElementById('payCurrentBadge').textContent = currentMonthLeads.length + ' оплат'; document.getElementById('payPrevBadge').textContent = prevPeriodLeads.length + ' оплат'; // Render lists renderPaymentsList('payCurrentList', currentMonthLeads, false); renderPaymentsList('payPrevList', prevPeriodLeads, true); // Charts updatePaymentsCharts(currentSum, prevSum, paymentsThisMonth); // Matrix renderPaymentsMatrix(); // By source table renderPaymentsBySource(currentMonthLeads, prevPeriodLeads); } function renderPaymentsList(containerId, payments, isPrevPeriod) { var container = document.getElementById(containerId); if (payments.length === 0) { container.innerHTML = '
Нет оплат
'; return; } payments.sort(function(a, b) { return (b.paidAmount || 0) - (a.paidAmount || 0); }); container.innerHTML = payments.slice(0, 10).map(function(l) { var leadDateStr = l.leadDate ? formatDate(l.leadDate) : '-'; var payDateStr = l.paymentDate ? formatDate(l.paymentDate) : '-'; var daysDelay = ''; if (isPrevPeriod && l.leadDate && l.paymentDate) { var days = Math.round((new Date(l.paymentDate) - new Date(l.leadDate)) / 86400000); daysDelay = ' | Через ' + days + ' дн.'; } return '
' + '
' + '
' + (l.company || l.number || 'Без названия') + '
' + '
' + 'Услуга: ' + (l.service || '-') + ' | Лид: ' + leadDateStr + ' | Оплата: ' + payDateStr + daysDelay + '
' + '
' + 'Маркетолог: ' + (l.marketer || '-') + ' | Менеджер: ' + (l.manager || '-') + ' | Источник: ' + (l.source || '-') + '
' + '
' + '
' + formatCurrency(l.paidAmount) + '
' + '
'; }).join(''); if (payments.length > 10) { container.innerHTML += '
И еще ' + (payments.length - 10) + ' оплат...
'; } } function updatePaymentsCharts(currentSum, prevSum, allPayments) { var opts = getChartOptions(); // Structure chart (pie) if (charts.payStructureChart) charts.payStructureChart.destroy(); charts.payStructureChart = new Chart(document.getElementById('payStructureChart'), { type: 'doughnut', data: { labels: ['С лидов текущего месяца', 'С лидов прошлых периодов'], datasets: [{ data: [currentSum, prevSum], backgroundColor: [chartColors.blue, chartColors.purple], borderWidth: 0 }] }, options: { responsive: true, plugins: { legend: { position: 'bottom', labels: { color: opts.textColor } } } } }); // By services var byService = {}; allPayments.forEach(function(l) { var svc = l.service || 'Не указана'; if (!byService[svc]) byService[svc] = 0; byService[svc] += l.paidAmount || 0; }); var servicesSorted = Object.entries(byService) .sort(function(a, b) { return b[1] - a[1]; }) .slice(0, 8); if (charts.payServicesChart) charts.payServicesChart.destroy(); charts.payServicesChart = new Chart(document.getElementById('payServicesChart'), { type: 'bar', data: { labels: servicesSorted.map(function(s) { return s[0].length > 20 ? s[0].substring(0, 20) + '...' : s[0]; }), datasets: [{ label: 'Сумма', data: servicesSorted.map(function(s) { return s[1]; }), backgroundColor: chartColors.green, borderRadius: 4 }] }, options: { indexAxis: 'y', responsive: true, plugins: { legend: { display: false } }, scales: { x: { grid: { color: opts.gridColor }, ticks: { color: opts.textColor } }, y: { grid: { display: false }, ticks: { color: opts.textColor } } } } }); } function renderPaymentsMatrix() { var months = ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек']; var marketer = document.getElementById('currentMarketer').value; var matrix = {}; months.forEach(function(m) { matrix[m] = {}; months.forEach(function(p) { matrix[m][p] = 0; }); }); var dataToUse = marketer ? leads.filter(function(l) { return l.marketer === marketer; }) : leads; dataToUse.forEach(function(l) { if (l.paidAmount && l.leadDate && l.paymentDate) { var lm = new Date(l.leadDate).getMonth(); var pm = new Date(l.paymentDate).getMonth(); matrix[months[lm]][months[pm]] += l.paidAmount; } }); document.getElementById('payMatrixHead').innerHTML = 'Лид / Оплата' + months.map(function(m) { return '' + m + ''; }).join('') + ''; document.getElementById('payMatrixBody').innerHTML = months.map(function(lm, li) { return '' + '' + lm + '' + months.map(function(pm, pi) { var v = matrix[lm][pm]; var cellClass = 'matrix-cell'; if (pi === li && v) cellClass += ' current'; else if (pi > li && v) cellClass += ' highlight'; return '' + (v ? formatShortCurrency(v) : '-') + ''; }).join('') + ''; }).join(''); } function renderPaymentsBySource(currentLeads, prevLeads) { var bySource = {}; currentLeads.forEach(function(l) { var src = l.source || 'Не указан'; if (!bySource[src]) bySource[src] = { source: src, curCount: 0, curSum: 0, prevCount: 0, prevSum: 0 }; bySource[src].curCount++; bySource[src].curSum += l.paidAmount || 0; }); prevLeads.forEach(function(l) { var src = l.source || 'Не указан'; if (!bySource[src]) bySource[src] = { source: src, curCount: 0, curSum: 0, prevCount: 0, prevSum: 0 }; bySource[src].prevCount++; bySource[src].prevSum += l.paidAmount || 0; }); var tableData = Object.values(bySource); tableData.sort(function(a, b) { return (b.curSum + b.prevSum) - (a.curSum + a.prevSum); }); document.getElementById('paySourcesBody').innerHTML = tableData.map(function(d) { var total = d.curSum + d.prevSum; return '' + '' + d.source + '' + '' + d.curCount + '' + '' + formatCurrency(d.curSum) + '' + '' + d.prevCount + '' + '' + formatCurrency(d.prevSum) + '' + '' + formatCurrency(total) + '' + ''; }).join(''); } // ==================== SOURCES PAGE ==================== function updateSourcesPage() { var from = document.getElementById('srcDateFrom').value; var to = document.getElementById('srcDateTo').value; var srcFilter = document.getElementById('srcFilterSource').value; var marketer = document.getElementById('currentMarketer').value; var data = leads.filter(function(l) { if (marketer && l.marketer !== marketer) return false; if (from && l.leadDate to) return false; if (srcFilter && l.source !== srcFilter) return false; return true; }); var paid = data.filter(function(l) { return l.paidAmount > 0; }); var revenue = paid.reduce(function(s, l) { return s + l.paidAmount; }, 0); var prevPeriodPayments = paid.filter(function(l) { if (!l.paymentDate || !l.leadDate) return false; return new Date(l.paymentDate).getMonth() > new Date(l.leadDate).getMonth() || new Date(l.paymentDate).getFullYear() > new Date(l.leadDate).getFullYear(); }); var failed = data.filter(function(l) { return l.leadStatus === 'провал'; }); document.getElementById('srcConversion').textContent = data.length ? Math.round((paid.length / data.length) * 100) + '%' : '0%'; document.getElementById('srcAvgCheck').textContent = paid.length ? formatCurrency(revenue / paid.length) : '0'; document.getElementById('srcPrevPeriod').textContent = prevPeriodPayments.length; document.getElementById('srcRefused').textContent = failed.length; updateSourceCharts(data); renderSourceTable(data); } function updateSourceCharts(data) { var opts = getChartOptions(); var bySource = {}; data.forEach(function(l) { var s = l.source || 'Не указан'; if (!bySource[s]) bySource[s] = { leads: 0, paid: 0, revenue: 0 }; bySource[s].leads++; if (l.paidAmount) { bySource[s].paid++; bySource[s].revenue += l.paidAmount; } }); var sorted = Object.entries(bySource).sort(function(a, b) { return b[1].leads - a[1].leads; }); if (charts.srcBarChart) charts.srcBarChart.destroy(); charts.srcBarChart = new Chart(document.getElementById('srcBarChart'), { type: 'bar', data: { labels: sorted.map(function(s) { return s[0]; }), datasets: [ { label: 'Лиды', data: sorted.map(function(s) { return s[1].leads; }), backgroundColor: chartColors.blue, borderRadius: 4 }, { label: 'Оплаты', data: sorted.map(function(s) { return s[1].paid; }), backgroundColor: chartColors.green, borderRadius: 4 } ] }, options: { responsive: true, plugins: { legend: { labels: { color: opts.textColor } } }, scales: { x: { grid: { display: false }, ticks: { color: opts.textColor } }, y: { grid: { color: opts.gridColor }, ticks: { color: opts.textColor } } } } }); if (charts.srcPieChart) charts.srcPieChart.destroy(); charts.srcPieChart = new Chart(document.getElementById('srcPieChart'), { type: 'doughnut', data: { labels: sorted.map(function(s) { return s[0]; }), datasets: [{ data: sorted.map(function(s) { return s[1].revenue; }), backgroundColor: chartColors.palette, borderWidth: 0 }] }, options: { responsive: true, plugins: { legend: { position: 'right', labels: { color: opts.textColor } } } } }); } function renderSourceTable(data) { var bySource = {}; data.forEach(function(l) { var s = l.source || 'Не указан'; if (!bySource[s]) bySource[s] = { source: s, leads: 0, invoices: 0, paid: 0, refused: 0, revenue: 0 }; bySource[s].leads++; if (l.invoiceStatus) bySource[s].invoices++; if (l.paidAmount) { bySource[s].paid++; bySource[s].revenue += l.paidAmount; } if (l.leadStatus === 'провал') bySource[s].refused++; }); Object.values(bySource).forEach(function(d) { d.conv = d.leads ? Math.round((d.paid / d.leads) * 100) : 0; d.avgCheck = d.paid ? Math.round(d.revenue / d.paid) : 0; }); var tableData = Object.values(bySource); tableData = sortData(tableData, 'srcTable'); if (!sortState.srcTable || !sortState.srcTable.field) { tableData.sort(function(a, b) { return b.leads - a.leads; }); } document.getElementById('srcTableBody').innerHTML = tableData.map(function(d) { return '' + '' + d.source + '' + '' + d.leads + '' + '' + d.invoices + '' + '' + d.paid + '' + '' + d.refused + '' + '' + d.conv + '%' + '' + formatCurrency(d.avgCheck) + '' + '' + formatCurrency(d.revenue) + '' + ''; }).join(''); } function resetSourcesFilters() { document.getElementById('srcDateFrom').value = ''; document.getElementById('srcDateTo').value = ''; document.getElementById('srcFilterSource').value = ''; updateSourcesPage(); } // ==================== MANAGERS PAGE ==================== function updateManagersPage() { var from = document.getElementById('mgrDateFrom').value; var to = document.getElementById('mgrDateTo').value; var mgrFilter = document.getElementById('mgrFilterManager').value; var marketer = document.getElementById('currentMarketer').value; var currentMonth = getCurrentMonth(); var data = leads.filter(function(l) { if (!l.manager) return false; if (marketer && l.marketer !== marketer) return false; if (from && l.leadDate to) return false; if (mgrFilter && l.manager !== mgrFilter) return false; return true; }); // Calculate totals var totalLeads = data.length; var totalRevenue = data.reduce(function(s, l) { return s + (l.paidAmount || 0); }, 0); // Previous period payments (payment this month, lead from before) var prevPeriodPayments = leads.filter(function(l) { if (!l.paidAmount || !l.paymentDate || !l.manager) return false; if (marketer && l.marketer !== marketer) return false; if (mgrFilter && l.manager !== mgrFilter) return false; if (!l.paymentDate.startsWith(currentMonth)) return false; if (!l.leadDate) return true; return !l.leadDate.startsWith(currentMonth); }); var prevPeriodCount = prevPeriodPayments.length; var prevPeriodSum = prevPeriodPayments.reduce(function(s, l) { return s + (l.paidAmount || 0); }, 0); document.getElementById('mgrTotalLeads').textContent = totalLeads; document.getElementById('mgrTotalRevenue').textContent = formatShortCurrency(totalRevenue); document.getElementById('mgrPrevPeriodCount').textContent = prevPeriodCount; document.getElementById('mgrPrevPeriodSum').textContent = formatShortCurrency(prevPeriodSum); renderManagerTable(data, currentMonth); updateManagerCharts(data, currentMonth); renderManagerFailedList(data); } function renderManagerTable(data, currentMonth) { var byManager = {}; MANAGERS.forEach(function(m) { byManager[m] = { manager: m, leads: 0, paid: 0, paidSum: 0, prevCount: 0, prevSum: 0, failed: 0, plan: (plans.managers && plans.managers[m]) || 0 }; }); data.forEach(function(l) { if (!byManager[l.manager]) return; byManager[l.manager].leads++; if (l.paidAmount) { byManager[l.manager].paid++; byManager[l.manager].paidSum += l.paidAmount; } if (l.leadStatus === 'провал') byManager[l.manager].failed++; }); // Add previous period payments var marketer = document.getElementById('currentMarketer').value; leads.forEach(function(l) { if (!l.paidAmount || !l.paymentDate || !l.manager) return; if (!byManager[l.manager]) return; if (marketer && l.marketer !== marketer) return; if (!l.paymentDate.startsWith(currentMonth)) return; if (!l.leadDate || !l.leadDate.startsWith(currentMonth)) { byManager[l.manager].prevCount++; byManager[l.manager].prevSum += l.paidAmount; } }); Object.values(byManager).forEach(function(d) { d.conv = d.leads ? Math.round((d.paid / d.leads) * 100) : 0; d.totalSum = d.paidSum + d.prevSum; d.totalCount = d.paid + d.prevCount; d.planPercent = d.plan ? Math.round((d.totalSum / d.plan) * 100) : 0; }); var tableData = Object.values(byManager).filter(function(d) { return d.leads > 0 || d.prevCount > 0; }); tableData = sortData(tableData, 'mgrTable'); if (!sortState.mgrTable || !sortState.mgrTable.field) { tableData.sort(function(a, b) { return b.totalSum - a.totalSum; }); } document.getElementById('mgrTableBody').innerHTML = tableData.map(function(d) { var planClass = d.planPercent >= 100 ? 'highlight-row' : (d.planPercent >= 70 ? '' : (d.planPercent > 0 ? 'warning-row' : '')); return '' + '' + d.manager + '' + '' + d.leads + '' + '' + d.paid + '' + '' + formatCurrency(d.paidSum) + '' + '' + d.conv + '%' + '' + d.prevCount + '' + '' + formatCurrency(d.prevSum) + '' + '' + formatCurrency(d.totalSum) + '' + '' + d.totalCount + '' + '' + formatCurrency(d.plan) + '' + '' + d.planPercent + '%' + '' + d.failed + '' + ''; }).join(''); } function updateManagerCharts(data, currentMonth) { var opts = getChartOptions(); var byManager = {}; MANAGERS.forEach(function(m) { byManager[m] = { leads: 0, paid: 0, revenue: 0, failed: 0, plan: (plans.managers && plans.managers[m]) || 0, totalRevenue: 0 }; }); data.forEach(function(l) { if (!byManager[l.manager]) return; byManager[l.manager].leads++; if (l.paidAmount) { byManager[l.manager].paid++; byManager[l.manager].revenue += l.paidAmount; } if (l.leadStatus === 'провал') byManager[l.manager].failed++; }); // Add all month payments for plan calculation var marketer = document.getElementById('currentMarketer').value; leads.forEach(function(l) { if (!l.paidAmount || !l.paymentDate || !l.manager) return; if (!byManager[l.manager]) return; if (marketer && l.marketer !== marketer) return; if (l.paymentDate.startsWith(currentMonth)) { byManager[l.manager].totalRevenue += l.paidAmount; } }); var sorted = Object.entries(byManager) .filter(function(e) { return e[1].leads > 0 || e[1].totalRevenue > 0; }) .sort(function(a, b) { return b[1].totalRevenue - a[1].totalRevenue; }); // Revenue chart if (charts.mgrRevenueChart) charts.mgrRevenueChart.destroy(); charts.mgrRevenueChart = new Chart(document.getElementById('mgrRevenueChart'), { type: 'bar', data: { labels: sorted.map(function(s) { return s[0]; }), datasets: [{ label: 'Выручка', data: sorted.map(function(s) { return s[1].totalRevenue; }), backgroundColor: chartColors.green, borderRadius: 4 }] }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false }, ticks: { color: opts.textColor } }, y: { grid: { color: opts.gridColor }, ticks: { color: opts.textColor } } } } }); // Conversion chart if (charts.mgrConvChart) charts.mgrConvChart.destroy(); charts.mgrConvChart = new Chart(document.getElementById('mgrConvChart'), { type: 'bar', data: { labels: sorted.map(function(s) { return s[0]; }), datasets: [{ label: 'Конверсия %', data: sorted.map(function(s) { return s[1].leads ? Math.round((s[1].paid / s[1].leads) * 100) : 0; }), backgroundColor: chartColors.blue, borderRadius: 4 }] }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false }, ticks: { color: opts.textColor } }, y: { max: 100, grid: { color: opts.gridColor }, ticks: { color: opts.textColor } } } } }); // Failed chart if (charts.mgrFailedChart) charts.mgrFailedChart.destroy(); charts.mgrFailedChart = new Chart(document.getElementById('mgrFailedChart'), { type: 'bar', data: { labels: sorted.map(function(s) { return s[0]; }), datasets: [{ label: 'Провалы', data: sorted.map(function(s) { return s[1].failed; }), backgroundColor: chartColors.red, borderRadius: 4 }] }, options: { indexAxis: 'y', responsive: true, plugins: { legend: { display: false } }, scales: { x: { grid: { color: opts.gridColor }, ticks: { color: opts.textColor } }, y: { grid: { display: false }, ticks: { color: opts.textColor } } } } }); // Plan chart if (charts.mgrPlanChart) charts.mgrPlanChart.destroy(); charts.mgrPlanChart = new Chart(document.getElementById('mgrPlanChart'), { type: 'bar', data: { labels: sorted.map(function(s) { return s[0]; }), datasets: [ { label: 'Факт', data: sorted.map(function(s) { return s[1].totalRevenue; }), backgroundColor: chartColors.green, borderRadius: 4 }, { label: 'План', data: sorted.map(function(s) { return s[1].plan; }), backgroundColor: chartColors.blue, borderRadius: 4 } ] }, options: { responsive: true, plugins: { legend: { labels: { color: opts.textColor } } }, scales: { x: { grid: { display: false }, ticks: { color: opts.textColor } }, y: { grid: { color: opts.gridColor }, ticks: { color: opts.textColor } } } } }); } function renderManagerFailedList(data) { var failed = data.filter(function(l) { return l.leadStatus === 'провал'; }); var container = document.getElementById('mgrFailedList'); if (failed.length === 0) { container.innerHTML = '
Нет проваленных лидов
'; return; } failed.sort(function(a, b) { return (b.leadDate || '').localeCompare(a.leadDate || ''); }); container.innerHTML = failed.slice(0, 5).map(function(l) { return '
' + '
' + '
' + (l.company || l.number || 'Без названия') + '
' + '
' + 'Услуга: ' + (l.service || '-') + ' | Источник: ' + (l.source || '-') + ' | Дата: ' + formatDate(l.leadDate) + '
' + '
' + '
' + (l.manager || '-') + '
' + '
'; }).join(''); if (failed.length > 5) { container.innerHTML += ''; } } function resetManagersFilters() { document.getElementById('mgrDateFrom').value = ''; document.getElementById('mgrDateTo').value = ''; document.getElementById('mgrFilterManager').value = ''; updateManagersPage(); } // ==================== MARKETERS PAGE ==================== function updateMarketersPage() { var from = document.getElementById('mktDateFrom').value; var to = document.getElementById('mktDateTo').value; var mktFilter = document.getElementById('mktFilterMarketer').value; var currentMonth = getCurrentMonth(); var data = leads.filter(function(l) { if (!l.marketer) return false; if (from && l.leadDate to) return false; if (mktFilter && l.marketer !== mktFilter) return false; return true; }); // Calculate totals var totalLeads = data.length; var paidLeads = data.filter(function(l) { return l.paidAmount > 0; }); var totalPaid = paidLeads.length; var totalRevenue = paidLeads.reduce(function(s, l) { return s + (l.paidAmount || 0); }, 0); var totalFailed = data.filter(function(l) { return l.leadStatus === 'провал'; }).length; document.getElementById('mktTotalLeads').textContent = totalLeads; document.getElementById('mktTotalPaid').textContent = totalPaid; document.getElementById('mktTotalRevenue').textContent = formatShortCurrency(totalRevenue); document.getElementById('mktTotalFailed').textContent = totalFailed; // Department plan updateMarketerDeptPlan(currentMonth); renderMarketerTable(data, currentMonth); updateMarketerCharts(data, currentMonth); renderMarketerFailedList(data); } function updateMarketerDeptPlan(currentMonth) { var mktFilter = document.getElementById('mktFilterMarketer').value; // Get all payments this month for marketers var monthPayments = leads.filter(function(l) { if (!l.paidAmount || !l.paymentDate) return false; if (!l.paymentDate.startsWith(currentMonth)) return false; if (mktFilter && l.marketer !== mktFilter) return false; return true; }); var totalRevenue = monthPayments.reduce(function(s, l) { return s + (l.paidAmount || 0); }, 0); var totalPlan = mktFilter ? ((plans.marketers && plans.marketers[mktFilter]) || 0) : (plans.marketingTotal || 0); var percent = totalPlan > 0 ? Math.round((totalRevenue / totalPlan) * 100) : 0; var remain = Math.max(0, totalPlan - totalRevenue); document.getElementById('mktDeptPlanValue').textContent = formatShortCurrency(totalRevenue) + ' / ' + formatShortCurrency(totalPlan); document.getElementById('mktDeptPlanBar').style.width = Math.min(100, percent) + '%'; document.getElementById('mktDeptPlanBar').className = 'progress-bar-fill ' + getProgressBarClass(percent); document.getElementById('mktDeptPlanPercent').textContent = percent + '%'; document.getElementById('mktDeptPlanRemain').textContent = 'Осталось: ' + formatShortCurrency(remain); } function renderMarketerTable(data, currentMonth) { var byMarketer = {}; MARKETERS.forEach(function(m) { byMarketer[m] = { marketer: m, leads: 0, paid: 0, paidSum: 0, failed: 0, plan: (plans.marketers && plans.marketers[m]) || 0 }; }); data.forEach(function(l) { if (!byMarketer[l.marketer]) return; byMarketer[l.marketer].leads++; if (l.paidAmount) { byMarketer[l.marketer].paid++; byMarketer[l.marketer].paidSum += l.paidAmount; } if (l.leadStatus === 'провал') byMarketer[l.marketer].failed++; }); // Calculate plan based on all payments this month leads.forEach(function(l) { if (!l.paidAmount || !l.paymentDate || !l.marketer) return; if (!byMarketer[l.marketer]) return; if (l.paymentDate.startsWith(currentMonth)) { if (!byMarketer[l.marketer].monthRevenue) byMarketer[l.marketer].monthRevenue = 0; byMarketer[l.marketer].monthRevenue += l.paidAmount; } }); Object.values(byMarketer).forEach(function(d) { d.conv = d.leads ? Math.round((d.paid / d.leads) * 100) : 0; d.failedPercent = d.leads ? Math.round((d.failed / d.leads) * 100) : 0; d.monthRevenue = d.monthRevenue || d.paidSum; d.planPercent = d.plan ? Math.round((d.monthRevenue / d.plan) * 100) : 0; }); var tableData = Object.values(byMarketer).filter(function(d) { return d.leads > 0; }); tableData = sortData(tableData, 'mktTable'); if (!sortState.mktTable || !sortState.mktTable.field) { tableData.sort(function(a, b) { return b.paidSum - a.paidSum; }); } document.getElementById('mktTableBody').innerHTML = tableData.map(function(d) { var planClass = d.planPercent >= 100 ? 'highlight-row' : (d.planPercent >= 70 ? '' : (d.planPercent > 0 ? 'warning-row' : '')); return '' + '' + d.marketer + '' + '' + d.leads + '' + '' + d.paid + '' + '' + formatCurrency(d.paidSum) + '' + '' + d.conv + '%' + '' + formatCurrency(d.plan) + '' + '' + d.planPercent + '%' + '' + d.failed + '' + '' + d.failedPercent + '%' + ''; }).join(''); } function updateMarketerCharts(data, currentMonth) { var opts = getChartOptions(); var byMarketer = {}; MARKETERS.forEach(function(m) { byMarketer[m] = { leads: 0, paid: 0, revenue: 0, failed: 0, plan: (plans.marketers && plans.marketers[m]) || 0, monthRevenue: 0 }; }); data.forEach(function(l) { if (!byMarketer[l.marketer]) return; byMarketer[l.marketer].leads++; if (l.paidAmount) { byMarketer[l.marketer].paid++; byMarketer[l.marketer].revenue += l.paidAmount; } if (l.leadStatus === 'провал') byMarketer[l.marketer].failed++; }); // Month revenue for plan leads.forEach(function(l) { if (!l.paidAmount || !l.paymentDate || !l.marketer) return; if (!byMarketer[l.marketer]) return; if (l.paymentDate.startsWith(currentMonth)) { byMarketer[l.marketer].monthRevenue += l.paidAmount; } }); var sorted = Object.entries(byMarketer) .filter(function(e) { return e[1].leads > 0; }) .sort(function(a, b) { return b[1].revenue - a[1].revenue; }); // Leads chart if (charts.mktLeadsChart) charts.mktLeadsChart.destroy(); charts.mktLeadsChart = new Chart(document.getElementById('mktLeadsChart'), { type: 'bar', data: { labels: sorted.map(function(s) { return s[0]; }), datasets: [{ label: 'Лиды', data: sorted.map(function(s) { return s[1].leads; }), backgroundColor: chartColors.blue, borderRadius: 4 }] }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false }, ticks: { color: opts.textColor } }, y: { grid: { color: opts.gridColor }, ticks: { color: opts.textColor } } } } }); // Revenue chart if (charts.mktRevenueChart) charts.mktRevenueChart.destroy(); charts.mktRevenueChart = new Chart(document.getElementById('mktRevenueChart'), { type: 'bar', data: { labels: sorted.map(function(s) { return s[0]; }), datasets: [{ label: 'Выручка', data: sorted.map(function(s) { return s[1].revenue; }), backgroundColor: chartColors.green, borderRadius: 4 }] }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false }, ticks: { color: opts.textColor } }, y: { grid: { color: opts.gridColor }, ticks: { color: opts.textColor } } } } }); // Plan chart if (charts.mktPlanChart) charts.mktPlanChart.destroy(); charts.mktPlanChart = new Chart(document.getElementById('mktPlanChart'), { type: 'bar', data: { labels: sorted.map(function(s) { return s[0]; }), datasets: [ { label: 'Факт', data: sorted.map(function(s) { return s[1].monthRevenue; }), backgroundColor: chartColors.green, borderRadius: 4 }, { label: 'План', data: sorted.map(function(s) { return s[1].plan; }), backgroundColor: chartColors.purple, borderRadius: 4 } ] }, options: { responsive: true, plugins: { legend: { labels: { color: opts.textColor } } }, scales: { x: { grid: { display: false }, ticks: { color: opts.textColor } }, y: { grid: { color: opts.gridColor }, ticks: { color: opts.textColor } } } } }); // Failed chart if (charts.mktFailedChart) charts.mktFailedChart.destroy(); charts.mktFailedChart = new Chart(document.getElementById('mktFailedChart'), { type: 'bar', data: { labels: sorted.map(function(s) { return s[0]; }), datasets: [{ label: 'Провалы', data: sorted.map(function(s) { return s[1].failed; }), backgroundColor: chartColors.red, borderRadius: 4 }] }, options: { indexAxis: 'y', responsive: true, plugins: { legend: { display: false } }, scales: { x: { grid: { color: opts.gridColor }, ticks: { color: opts.textColor } }, y: { grid: { display: false }, ticks: { color: opts.textColor } } } } }); } function renderMarketerFailedList(data) { var failed = data.filter(function(l) { return l.leadStatus === 'провал'; }); var container = document.getElementById('mktFailedList'); if (failed.length === 0) { container.innerHTML = '
Нет проваленных лидов
'; return; } failed.sort(function(a, b) { return (b.leadDate || '').localeCompare(a.leadDate || ''); }); container.innerHTML = failed.slice(0, 5).map(function(l) { return '
' + '
' + '
' + (l.company || l.number || 'Без названия') + '
' + '
' + 'Маркетолог: ' + (l.marketer || '-') + ' | Источник: ' + (l.source || '-') + ' | Дата: ' + formatDate(l.leadDate) + '
' + '
' + '
' + (l.manager || '-') + '
' + '
'; }).join(''); if (failed.length > 5) { container.innerHTML += ''; } } function resetMarketersFilters() { document.getElementById('mktDateFrom').value = ''; document.getElementById('mktDateTo').value = ''; document.getElementById('mktFilterMarketer').value = ''; updateMarketersPage(); } // ==================== PLANS PAGE ==================== function loadPlans() { var currentMonth = getCurrentMonth(); var month = document.getElementById('planMonth').value || currentMonth; // Set input values from plans document.getElementById('planSalesTotal').value = plans.salesTotal || ''; document.getElementById('planMarketingTotal').value = plans.marketingTotal || ''; document.querySelectorAll('.plan-input[data-manager]').forEach(function(input) { var manager = input.dataset.manager; input.value = (plans.managers && plans.managers[manager]) || ''; }); document.querySelectorAll('.plan-input[data-marketer]').forEach(function(input) { var marketer = input.dataset.marketer; input.value = (plans.marketers && plans.marketers[marketer]) || ''; }); updatePlanResults(); } function updatePlanResults() { var currentMonth = getCurrentMonth(); // Calculate manager results var managerRevenue = {}; MANAGERS.forEach(function(m) { managerRevenue[m] = 0; }); leads.forEach(function(l) { if (!l.paidAmount || !l.paymentDate || !l.manager) return; if (!l.paymentDate.startsWith(currentMonth)) return; if (managerRevenue.hasOwnProperty(l.manager)) { managerRevenue[l.manager] += l.paidAmount; } }); MANAGERS.forEach(function(m) { var result = managerRevenue[m]; var plan = (plans.managers && plans.managers[m]) || 0; var percent = plan > 0 ? Math.round((result / plan) * 100) : 0; var remain = Math.max(0, plan - result); var resultEl = document.getElementById('planResult' + m); var percentEl = document.getElementById('planPercent' + m); var remainEl = document.getElementById('planRemain' + m); if (resultEl) resultEl.textContent = formatCurrency(result); if (percentEl) { percentEl.textContent = percent + '%'; percentEl.style.color = percent >= 100 ? 'var(--success)' : (percent >= 70 ? 'var(--warning)' : 'var(--danger)'); } if (remainEl) remainEl.textContent = formatCurrency(remain); }); // Calculate marketer results var marketerRevenue = {}; MARKETERS.forEach(function(m) { marketerRevenue[m] = 0; }); leads.forEach(function(l) { if (!l.paidAmount || !l.paymentDate || !l.marketer) return; if (!l.paymentDate.startsWith(currentMonth)) return; if (marketerRevenue.hasOwnProperty(l.marketer)) { marketerRevenue[l.marketer] += l.paidAmount; } }); MARKETERS.forEach(function(m) { var result = marketerRevenue[m]; var plan = (plans.marketers && plans.marketers[m]) || 0; var percent = plan > 0 ? Math.round((result / plan) * 100) : 0; var remain = Math.max(0, plan - result); var resultEl = document.getElementById('planMktResult' + m); var percentEl = document.getElementById('planMktPercent' + m); var remainEl = document.getElementById('planMktRemain' + m); if (resultEl) resultEl.textContent = formatCurrency(result); if (percentEl) { percentEl.textContent = percent + '%'; percentEl.style.color = percent >= 100 ? 'var(--success)' : (percent >= 70 ? 'var(--warning)' : 'var(--danger)'); } if (remainEl) remainEl.textContent = formatCurrency(remain); }); // Update plan history chart updatePlanHistoryChart(); } function updatePlanHistoryChart() { var opts = getChartOptions(); var months = ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек']; var currentYear = new Date().getFullYear(); var monthlyRevenue = months.map(function(_, i) { return leads.filter(function(l) { if (!l.paidAmount || !l.paymentDate) return false; var d = new Date(l.paymentDate); return d.getFullYear() === currentYear && d.getMonth() === i; }).reduce(function(s, l) { return s + (l.paidAmount || 0); }, 0); }); var monthlyPlan = months.map(function() { return plans.salesTotal || 0; }); if (charts.planHistoryChart) charts.planHistoryChart.destroy(); charts.planHistoryChart = new Chart(document.getElementById('planHistoryChart'), { type: 'line', data: { labels: months, datasets: [ { label: 'Факт', data: monthlyRevenue, borderColor: chartColors.green, backgroundColor: chartColors.green + '20', fill: true, tension: 0.3 }, { label: 'План', data: monthlyPlan, borderColor: chartColors.blue, borderDash: [5, 5], fill: false } ] }, options: { responsive: true, plugins: { legend: { labels: { color: opts.textColor } } }, scales: { x: { grid: { display: false }, ticks: { color: opts.textColor } }, y: { grid: { color: opts.gridColor }, ticks: { color: opts.textColor } } } } }); } // ==================== ALL LEADS ==================== function filterAllLeads() { var q = document.getElementById('allLeadsSearch').value.toLowerCase(); var status = document.getElementById('allFilterStatus').value; var invoice = document.getElementById('allFilterInvoice').value; var mgr = document.getElementById('allFilterManager').value; var from = document.getElementById('allFilterFrom').value; var to = document.getElementById('allFilterTo').value; var marketer = document.getElementById('currentMarketer').value; filteredLeads = leads.filter(function(l) { if (marketer && l.marketer !== marketer) return false; if (q && !( (l.number && l.number.toLowerCase().includes(q)) || (l.company && l.company.toLowerCase().includes(q)) || (l.phone && l.phone.includes(q)) )) return false; if (status && l.leadStatus !== status) return false; if (invoice === 'none' && l.invoiceStatus) return false; if (invoice && invoice !== 'none' && l.invoiceStatus !== invoice) return false; if (mgr && l.manager !== mgr) return false; if (from && l.leadDate to) return false; return true; }); document.getElementById('allLeadsCount').textContent = '(' + filteredLeads.length + ')'; renderAllLeads(); } function renderAllLeads() { var data = sortData(filteredLeads, 'allLeadsTable'); document.getElementById('allLeadsBody').innerHTML = data.map(function(l) { var rowClass = ''; if (l.leadStatus === 'провал') rowClass = 'danger-row'; else if (l.paidAmount > 0) rowClass = 'highlight-row'; return '' + '' + (l.number || '-') + '' + '' + (l.company || '-') + '' + '' + (l.service || '-') + '' + '' + (l.source || '-') + '' + '' + (l.marketer || '-') + '' + '' + (l.manager || '-') + '' + '' + formatDate(l.leadDate) + '' + '' + getStatusBadge(l.leadStatus) + '' + '' + (l.invoiceStatus ? getBadge(l.invoiceStatus) : '-') + '' + '' + (l.paidAmount ? formatCurrency(l.paidAmount) : '-') + '' + '' + ''; }).join(''); } function resetAllFilters() { document.getElementById('allLeadsSearch').value = ''; document.getElementById('allFilterStatus').value = ''; document.getElementById('allFilterInvoice').value = ''; document.getElementById('allFilterManager').value = ''; document.getElementById('allFilterFrom').value = ''; document.getElementById('allFilterTo').value = ''; filterAllLeads(); } // ==================== PAYMENT SEARCH ==================== function renderPaymentSearchResults(results) { if (!results.length) { document.getElementById('paymentSearchResults').innerHTML = '
Ничего не найдено
'; return; } document.getElementById('paymentSearchResults').innerHTML = results.map(function(l) { return '
' + '
' + '
' + (l.company || 'Без названия') + '
' + (l.invoiceStatus ? getBadge(l.invoiceStatus) : 'Нет счета') + '
' + '
' + '
Номер: ' + (l.number || '-') + '
' + '
Дата: ' + formatDate(l.leadDate) + '
' + '
Маркетолог: ' + (l.marketer || '-') + '
' + '
' + '
'; }).join(''); } function selectLeadForPayment(rowIndex) { var lead = leads.find(function(l) { return l.rowIndex === rowIndex; }); if (!lead) return; document.getElementById('paymentFormCard').style.display = 'block'; document.getElementById('selectedLeadInfo').textContent = (lead.number || '-') + ' - ' + (lead.company || 'Без названия') + ' (Маркетолог: ' + (lead.marketer || 'не указан') + ')'; document.getElementById('paymentLeadId').value = rowIndex; document.getElementById('invoiceAmount').value = lead.invoiceAmount || ''; document.getElementById('invoiceDate').value = lead.invoiceDate || ''; document.getElementById('invoiceManager').value = lead.invoiceManager || ''; document.getElementById('invoiceStatus').value = lead.invoiceStatus || 'бронь'; document.getElementById('paidAmount').value = lead.paidAmount || ''; document.getElementById('paymentDate').value = lead.paymentDate || ''; document.getElementById('invoiceComment').value = lead.invoiceComment || ''; } function cancelPayment() { document.getElementById('paymentFormCard').style.display = 'none'; document.getElementById('paymentForm').reset(); } // ==================== EDIT MODAL ==================== function openEditModal(rowIndex) { var lead = leads.find(function(l) { return l.rowIndex === rowIndex; }); if (!lead) return; document.getElementById('editId').value = lead.id || ''; document.getElementById('editRowIndex').value = rowIndex; document.getElementById('editNumber').value = lead.number || ''; document.getElementById('editCompany').value = lead.company || ''; document.getElementById('editPhone').value = lead.phone || ''; document.getElementById('editEmail').value = lead.email || ''; document.getElementById('editService').value = lead.service || ''; document.getElementById('editSource').value = lead.source || ''; document.getElementById('editMarketer').value = lead.marketer || ''; document.getElementById('editManager').value = lead.manager || ''; document.getElementById('editLeadDate').value = lead.leadDate || ''; document.getElementById('editLeadStatus').value = lead.leadStatus || ''; document.getElementById('editComment').value = lead.comment || ''; document.getElementById('editInvoiceAmount').value = lead.invoiceAmount || ''; document.getElementById('editInvoiceDate').value = lead.invoiceDate || ''; document.getElementById('editInvoiceManager').value = lead.invoiceManager || ''; document.getElementById('editInvoiceStatus').value = lead.invoiceStatus || ''; document.getElementById('editPaidAmount').value = lead.paidAmount || ''; document.getElementById('editPaymentDate').value = lead.paymentDate || ''; document.getElementById('editInvoiceComment').value = lead.invoiceComment || ''; // Reset tabs document.querySelectorAll('.modal .tab').forEach(function(t, i) { t.classList.toggle('active', i === 0); }); document.getElementById('editTabLead').style.display = 'block'; document.getElementById('editTabInvoice').style.display = 'none'; document.getElementById('editModal').classList.add('show'); } function closeModal() { document.getElementById('editModal').classList.remove('show'); } async function saveEdit() { var rowIndex = parseInt(document.getElementById('editRowIndex').value); var leadData = { company: document.getElementById('editCompany').value, phone: String(document.getElementById('editPhone').value || ''), email: document.getElementById('editEmail').value, service: document.getElementById('editService').value, source: document.getElementById('editSource').value, marketer: document.getElementById('editMarketer').value, manager: document.getElementById('editManager').value, leadDate: document.getElementById('editLeadDate').value, leadStatus: document.getElementById('editLeadStatus').value, comment: document.getElementById('editComment').value, invoiceAmount: parseFloat(document.getElementById('editInvoiceAmount').value) || null, invoiceDate: document.getElementById('editInvoiceDate').value || null, invoiceManager: document.getElementById('editInvoiceManager').value, invoiceStatus: document.getElementById('editInvoiceStatus').value, paidAmount: parseFloat(document.getElementById('editPaidAmount').value) || null, paymentDate: document.getElementById('editPaymentDate').value || null, invoiceComment: document.getElementById('editInvoiceComment').value }; var updated = await updateLead(rowIndex, leadData); if (updated) { closeModal(); filterAllLeads(); } } // ==================== FAILED LEADS MODAL ==================== function showFailedLeadsModal() { document.getElementById('failedLeadsModal').classList.add('show'); renderFailedLeadsModal(); } function closeFailedLeadsModal() { document.getElementById('failedLeadsModal').classList.remove('show'); } function renderFailedLeadsModal() { var mgrFilter = document.getElementById('failedFilterManager').value; var mktFilter = document.getElementById('failedFilterMarketer').value; var globalMarketer = document.getElementById('currentMarketer').value; var failed = leads.filter(function(l) { if (l.leadStatus !== 'провал') return false; if (globalMarketer && l.marketer !== globalMarketer) return false; if (mgrFilter && l.manager !== mgrFilter) return false; if (mktFilter && l.marketer !== mktFilter) return false; return true; }); var container = document.getElementById('failedLeadsListModal'); if (failed.length === 0) { container.innerHTML = '
Нет проваленных лидов
'; return; } failed.sort(function(a, b) { return (b.leadDate || '').localeCompare(a.leadDate || ''); }); container.innerHTML = failed.map(function(l) { return '
' + '
' + '
' + (l.company || l.number || 'Без названия') + '
' + '
' + 'Услуга: ' + (l.service || '-') + ' | Источник: ' + (l.source || '-') + ' | Маркетолог: ' + (l.marketer || '-') + ' | Дата: ' + formatDate(l.leadDate) + '
' + '
' + '
' + (l.manager || '-') + '
' + '
'; }).join(''); } // ==================== EXPORT ==================== function exportCSV() { var headers = [ 'number', 'company', 'phone', 'email', 'service', 'source', 'channel', 'marketer', 'manager', 'leadDate', 'leadStatus', 'invoiceAmount', 'invoiceDate', 'invoiceStatus', 'paidAmount', 'paymentDate' ]; var csv = '\uFEFF' + headers.join(';') + '\n'; filteredLeads.forEach(function(l) { csv += headers.map(function(h) { var v = l[h] || ''; v = String(v); if (v.includes(';') || v.includes('"')) { v = '"' + v.replace(/"/g, '""') + '"'; } return v; }).join(';') + '\n'; }); var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); var link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'leads_' + formatDateISO(new Date()) + '.csv'; link.click(); showToast('Экспортировано', 'success'); } // ==================== START ==================== init();