Хранилище лидов
* {
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
ВК
Квиз
Дзен
-
Оплат с прошлых периодов
Лиды и оплаты по источникам
| Источник |
Лидов |
Счетов |
Оплат |
Провалов |
Конверсия |
Ср. чек |
Выручка |
Все
Даша Ш
Виктория Г
Кристина В
Кристина А
Екатерина И
Суббота М
Ефимова
Усова
#
-
Оплат с прошлых периодов
R
-
Сумма с прошлых периодов
| Менеджер |
Лидов |
Оплат |
Сумма оплат |
Конверсия |
Оплат (прошл.) |
Сумма (прошл.) |
Всего за месяц |
Всего оплат |
План |
% плана |
Провалов |
Все
Дима
Регина
Паша
Кристина
Илья
| Маркетолог |
Лидов |
Оплат |
Сумма оплат |
Конверсия |
План |
% плана |
Провалов |
% провалов |
| Менеджер |
План на месяц |
Текущий результат |
Выполнение |
Осталось |
| Даша Ш |
|
- |
- |
- |
| Виктория Г |
|
- |
- |
- |
| Кристина В |
|
- |
- |
- |
| Кристина А |
|
- |
- |
- |
| Екатерина И |
|
- |
- |
- |
| Суббота М |
|
- |
- |
- |
| Ефимова |
|
- |
- |
- |
| Усова |
|
- |
- |
- |
| Маркетолог |
План на месяц |
Текущий результат |
Выполнение |
Осталось |
| Дима |
|
- |
- |
- |
| Регина |
|
- |
- |
- |
| Паша |
|
- |
- |
- |
| Кристина |
|
- |
- |
- |
| Илья |
|
- |
- |
- |
Все
Новый
В работе
КП отправлено
Переговоры
Отказ
Провал
Все
Нет счета
Бронь
Оплата
Частичная
Отказ
Все
Даша Ш
Виктория Г
Кристина В
Кристина А
Екатерина И
Суббота М
Ефимова
Усова
| Номер |
Компания |
Услуга |
Источник |
Маркетолог |
Менеджер |
Дата |
Статус |
Счет |
Оплата |
|
Все
Даша Ш
Виктория Г
Кристина В
Кристина А
Екатерина И
Суббота М
Ефимова
Усова
Все
Дима
Регина
Паша
Кристина
Илья
// ==================== 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 '
';
}).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();