368 lines
20 KiB
JavaScript
368 lines
20 KiB
JavaScript
// Демо-данные: ~30 заявок как в брифе (30-50 в день, пик до 100)
|
||
const DEMO_DATA = [
|
||
{ id: 1, name: "Алиев Арман Берикович", phone: "+7 777 123 45 67", address: "г. Алматы, ул. Абая 10, кв. 5", tariff: "Домашний интернет 100 Мбит/с", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false },
|
||
{ id: 2, name: "Байкова Гульнара Талгатовна", phone: "+7 701 234 56 78", address: "г. Алматы, пр. Достык 45, офис 12", tariff: "ТВ + Интернет", manager: "Айгуль К.", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false },
|
||
{ id: 3, name: "Васильченко Игорь Петрович", phone: "+7 778 345 67 89", address: "г. Алматы, ул. Толе би 88, кв. 42", tariff: "Домашний интернет 500 Мбит/с", manager: "", status: "новая", comment: "Клиент просит перезвонить после 18:00", date: "2026-08-28", isDuplicate: false },
|
||
{ id: 4, name: "Гребенюк Сергей Владимирович", phone: "+7 702 456 78 90", address: "г. Алматы, мкр. Аксай-2, д. 15", tariff: "Тройной пакет", manager: "Серик Ж.", status: "на проверке", comment: "", date: "2026-08-26", isDuplicate: false },
|
||
{ id: 5, name: "Дюсенова Айнур Маратовна", phone: "+7 777 567 89 01", address: "г. Алматы, ул. Сатпаева 22, кв. 18", tariff: "Домашний интернет 50 Мбит/с", manager: "Бауыржан Т.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false },
|
||
{ id: 6, name: "Ержанов Максат", phone: "+7 701 678 90 12", address: "г. Алматы, ул. Рыскулова 100, кв. 55", tariff: "Бизнес тариф", manager: "", status: "спорная", comment: "Нужно уточнить юридический адрес", date: "2026-08-25", isDuplicate: false },
|
||
{ id: 7, name: "Жуманова Лейла Ахметовна", phone: "+7 778 789 01 23", address: "г. Алматы, пр. Абая 150, кв. 88", tariff: "Домашний интернет 100 Мбит/с", manager: "Айгуль К.", status: "завершена", comment: "", date: "2026-08-24", isDuplicate: false },
|
||
{ id: 8, name: "Зинченко Александр", phone: "+7 702 890 12 34", address: "г. Алматы, ул. Макатаева 35, офис 5", tariff: "ТВ + Интернет", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-28", isDuplicate: false },
|
||
{ id: 9, name: "Ибраева Сауле", phone: "+7 777 901 23 45", address: "г. Алматы, мкр. Коктем-3, д. 20, кв. 10", tariff: "Домашний интернет 500 Мбит/с", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false },
|
||
{ id: 10, name: "Ким Дмитрий Юрьевич", phone: "+7 701 012 34 56", address: "г. Алматы, ул. Бейбитшилик 12, кв. 30", tariff: "Домашний интернет 100 Мбит/с", manager: "Бауыржан Т.", status: "на проверке", comment: "Клиент на больничном", date: "2026-08-26", isDuplicate: false },
|
||
{ id: 11, name: "Лебедева Наталья", phone: "+7 778 123 45 67", address: "г. Алматы, пр. Райымбека 200, кв. 75", tariff: "Тройной пакет", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false },
|
||
{ id: 12, name: "Мамедов Руслан", phone: "+7 702 234 56 78", address: "г. Алматы, ул. Желтоксан 55, офис 8", tariff: "Бизнес тариф", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false },
|
||
{ id: 13, name: "Нургалиева Айжан", phone: "+7 777 345 67 89", address: "г. Алматы, мкр. Таугуль-3, д. 8А, кв. 22", tariff: "Домашний интернет 50 Мбит/с", manager: "Айгуль К.", status: "завершена", comment: "", date: "2026-08-23", isDuplicate: false },
|
||
{ id: 14, name: "Омаров Ерлан", phone: "+7 701 456 78 90", address: "г. Алматы, ул. Панфилова 90, кв. 15", tariff: "ТВ + Интернет", manager: "", status: "спорная", comment: "Неправильный адрес, уточняется", date: "2026-08-25", isDuplicate: false },
|
||
{ id: 15, name: "Петрова Елена Сергеевна", phone: "+7 778 567 89 01", address: "г. Алматы, пр. Абая 50, кв. 100", tariff: "Домашний интернет 500 Мбит/с", manager: "Бауыржан Т.", status: "в работе", comment: "", date: "2026-08-28", isDuplicate: false },
|
||
{ id: 16, name: "Рахимов Берик", phone: "+7 702 678 90 12", address: "г. Алматы, ул. Кабанбай батыра 70, кв. 45", tariff: "Домашний интернет 100 Мбит/с", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false },
|
||
{ id: 17, name: "Смирнова Ольга", phone: "+7 777 789 01 23", address: "г. Алматы, мкр. Орбита-2, д. 55, кв. 8", tariff: "Тройной пакет", manager: "Серик Ж.", status: "на проверке", comment: "", date: "2026-08-26", isDuplicate: false },
|
||
{ id: 18, name: "Турсунов Марат", phone: "+7 701 890 12 34", address: "г. Алматы, ул. Пушкина 25, офис 12", tariff: "Бизнес тариф", manager: "Айгуль К.", status: "завершена", comment: "", date: "2026-08-22", isDuplicate: false },
|
||
{ id: 19, name: "Уалиева Жанар", phone: "+7 778 901 23 45", address: "г. Алматы, пр. Достык 100, кв. 60", tariff: "Домашний интернет 50 Мбит/с", manager: "", status: "новая", comment: "Заявка с сайта", date: "2026-08-28", isDuplicate: false },
|
||
{ id: 20, name: "Федоров Виктор", phone: "+7 702 012 34 56", address: "г. Алматы, ул. Богенбай батыра 40, кв. 20", tariff: "ТВ + Интернет", manager: "Бауыржан Т.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false },
|
||
{ id: 21, name: "Хасанова Мадина", phone: "+7 777 123 45 67", address: "г. Алматы, мкр. Нуркент, д. 12, кв. 35", tariff: "Домашний интернет 100 Мбит/с", manager: "", status: "спорная", comment: "Дубль? Тот же телефон что у Алиева А.", date: "2026-08-27", isDuplicate: true },
|
||
{ id: 22, name: "Иванов Петр", phone: "+7 701 333 22 11", address: "г. Алматы, ул. Тимирязева 80, кв. 50", tariff: "Домашний интернет 500 Мбит/с", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-28", isDuplicate: false },
|
||
{ id: 23, name: "Козлов Андрей", phone: "+7 778 444 33 22", address: "г. Алматы, пр. Абая 200, кв. 120", tariff: "Бизнес тариф", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false },
|
||
{ id: 24, name: "Алиев Арман Берикович", phone: "+7 777 123 45 67", address: "г. Алматы, ул. Абая 10, кв. 5", tariff: "Домашний интернет 100 Мбит/с", manager: "", status: "спорная", comment: "ВОЗМОЖНЫЙ ДУБЛЬ - заявка #1", date: "2026-08-28", isDuplicate: true },
|
||
{ id: 25, name: "Шарипова Гульмира", phone: "+7 702 555 44 33", address: "г. Алматы, ул. Сейфуллина 150, кв. 70", tariff: "Тройной пакет", manager: "Айгуль К.", status: "на проверке", comment: "", date: "2026-08-26", isDuplicate: false },
|
||
{ id: 26, name: "Яковлев Михаил", phone: "+7 777 666 55 44", address: "г. Алматы, мкр. Самал-2, д. 30, кв. 15", tariff: "Домашний интернет 50 Мбит/с", manager: "", status: "новая", comment: "Клиент из рекламы", date: "2026-08-28", isDuplicate: false },
|
||
{ id: 27, name: "Есенбаева Айгуль", phone: "+7 701 777 66 55", address: "г. Алматы, ул. Масанчи 85, кв. 40", tariff: "ТВ + Интернет", manager: "Бауыржан Т.", status: "завершена", comment: "", date: "2026-08-21", isDuplicate: false },
|
||
{ id: 28, name: "Морозов Игорь", phone: "+7 778 888 77 66", address: "г. Алматы, пр. Райымбека 300, офис 20", tariff: "Бизнес тариф", manager: "Серик Ж.", status: "в работе", comment: "", date: "2026-08-27", isDuplicate: false },
|
||
{ id: 29, name: "Кенжебаева Сауле", phone: "+7 702 999 88 77", address: "г. Алматы, мкр. Айнабулак, д. 5, кв. 25", tariff: "Домашний интернет 100 Мбит/с", manager: "", status: "новая", comment: "", date: "2026-08-28", isDuplicate: false },
|
||
{ id: 30, name: "Ткаченко Владимир", phone: "+7 777 000 99 88", address: "г. Алматы, ул. Курмангазы 60, кв. 90", tariff: "Домашний интернет 500 Мбит/с", manager: "Айгуль К.", status: "просрочка", comment: "Клиент не отвечает", date: "2026-08-20", isDuplicate: false }
|
||
];
|
||
|
||
let requests = [];
|
||
let currentFilter = 'all';
|
||
let duplicateCheckId = null;
|
||
|
||
// Инициализация
|
||
function init() {
|
||
const stored = localStorage.getItem('uchet_requests');
|
||
if (stored) {
|
||
requests = JSON.parse(stored);
|
||
} else {
|
||
requests = [...DEMO_DATA];
|
||
saveToStorage();
|
||
}
|
||
renderDashboard();
|
||
renderTable();
|
||
}
|
||
|
||
function saveToStorage() {
|
||
localStorage.setItem('uchet_requests', JSON.stringify(requests));
|
||
}
|
||
|
||
// Дашборд
|
||
function renderDashboard() {
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
const counts = {
|
||
'новая': 0,
|
||
'в работе': 0,
|
||
'на проверке': 0,
|
||
'спорная': 0,
|
||
'завершена': 0,
|
||
'просрочка': 0,
|
||
'total': requests.length
|
||
};
|
||
|
||
requests.forEach(r => {
|
||
if (counts[r.status] !== undefined) counts[r.status]++;
|
||
// Просрочка: старше 3 дней и не завершена
|
||
if (r.status !== 'завершена' && r.status !== 'просрочка') {
|
||
const daysOld = daysBetween(r.date, today);
|
||
if (daysOld > 3) {
|
||
counts['просрочка']++;
|
||
}
|
||
}
|
||
});
|
||
|
||
document.getElementById('count-new').textContent = counts['новая'];
|
||
document.getElementById('count-work').textContent = counts['в работе'];
|
||
document.getElementById('count-duplicate').textContent = requests.filter(r => r.isDuplicate).length;
|
||
document.getElementById('count-overdue').textContent = counts['просрочка'];
|
||
document.getElementById('count-total').textContent = counts.total;
|
||
}
|
||
|
||
function daysBetween(date1, date2) {
|
||
const d1 = new Date(date1);
|
||
const d2 = new Date(date2);
|
||
return Math.round((d2 - d1) / (1000 * 60 * 60 * 24));
|
||
}
|
||
|
||
// Фильтры
|
||
function setFilter(status) {
|
||
currentFilter = status;
|
||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.status === status);
|
||
});
|
||
renderTable();
|
||
}
|
||
|
||
// Таблица
|
||
function renderTable() {
|
||
const tbody = document.getElementById('table-body');
|
||
const search = document.getElementById('search').value.toLowerCase();
|
||
|
||
let filtered = requests.filter(r => {
|
||
const matchesSearch = r.name.toLowerCase().includes(search) ||
|
||
r.phone.toLowerCase().includes(search) ||
|
||
r.address.toLowerCase().includes(search) ||
|
||
(r.manager && r.manager.toLowerCase().includes(search));
|
||
const matchesStatus = currentFilter === 'all' || r.status === currentFilter;
|
||
return matchesSearch && matchesStatus;
|
||
});
|
||
|
||
// Сортировка: новые сверху
|
||
filtered.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||
|
||
if (filtered.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="8" class="empty-state">
|
||
<div class="empty-state-icon">📭</div>
|
||
<p>Нет заявок по выбранным фильтрам</p>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = filtered.map(r => `
|
||
<tr>
|
||
<td>${formatDate(r.date)}</td>
|
||
<td>
|
||
${escapeHtml(r.name)}
|
||
${r.isDuplicate ? '<span class="duplicate-flag">ДУБЛЬ</span>' : ''}
|
||
</td>
|
||
<td>${escapeHtml(r.phone)}</td>
|
||
<td>${escapeHtml(r.address)}</td>
|
||
<td>${escapeHtml(r.tariff)}</td>
|
||
<td>${r.manager || '—'}</td>
|
||
<td><span class="status-badge status-${r.status}">${r.status}</span></td>
|
||
<td class="actions">
|
||
<button class="btn btn-sm btn-secondary" onclick="editRequest(${r.id})">✏️</button>
|
||
${r.status === 'новая' ? `<button class="btn btn-sm btn-primary" onclick="takeToWork(${r.id})">В работу</button>` : ''}
|
||
${!r.isDuplicate && r.status !== 'завершена' ? `<button class="btn btn-sm btn-warning" onclick="checkDuplicate(${r.id})">Дубль?</button>` : ''}
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function formatDate(dateStr) {
|
||
const [y, m, d] = dateStr.split('-');
|
||
return `${d}.${m}.${y}`;
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// Модальное окно
|
||
function openModal() {
|
||
document.getElementById('modal-title').textContent = 'Новая заявка';
|
||
document.getElementById('request-form').reset();
|
||
document.getElementById('req-id').value = '';
|
||
document.getElementById('modal').classList.add('active');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('modal').classList.remove('active');
|
||
}
|
||
|
||
function editRequest(id) {
|
||
const r = requests.find(x => x.id === id);
|
||
if (!r) return;
|
||
|
||
document.getElementById('modal-title').textContent = 'Редактирование заявки';
|
||
document.getElementById('req-id').value = r.id;
|
||
document.getElementById('req-name').value = r.name;
|
||
document.getElementById('req-phone').value = r.phone;
|
||
document.getElementById('req-address').value = r.address;
|
||
document.getElementById('req-tariff').value = r.tariff;
|
||
document.getElementById('req-manager').value = r.manager || '';
|
||
document.getElementById('req-status').value = r.status;
|
||
document.getElementById('req-comment').value = r.comment || '';
|
||
document.getElementById('modal').classList.add('active');
|
||
}
|
||
|
||
function saveRequest(e) {
|
||
e.preventDefault();
|
||
const id = document.getElementById('req-id').value;
|
||
|
||
const data = {
|
||
name: document.getElementById('req-name').value.trim(),
|
||
phone: document.getElementById('req-phone').value.trim(),
|
||
address: document.getElementById('req-address').value.trim(),
|
||
tariff: document.getElementById('req-tariff').value,
|
||
manager: document.getElementById('req-manager').value.trim(),
|
||
status: document.getElementById('req-status').value,
|
||
comment: document.getElementById('req-comment').value.trim(),
|
||
date: new Date().toISOString().slice(0, 10)
|
||
};
|
||
|
||
if (id) {
|
||
const idx = requests.findIndex(r => r.id == id);
|
||
if (idx !== -1) {
|
||
requests[idx] = { ...requests[idx], ...data };
|
||
}
|
||
} else {
|
||
const newId = Math.max(...requests.map(r => r.id), 0) + 1;
|
||
requests.unshift({ id: newId, ...data, isDuplicate: false });
|
||
}
|
||
|
||
saveToStorage();
|
||
closeModal();
|
||
renderDashboard();
|
||
renderTable();
|
||
}
|
||
|
||
// Действия
|
||
function takeToWork(id) {
|
||
const r = requests.find(x => x.id === id);
|
||
if (r) {
|
||
r.status = 'в работе';
|
||
if (!r.manager) r.manager = prompt('Введите ФИО менеджера:');
|
||
saveToStorage();
|
||
renderDashboard();
|
||
renderTable();
|
||
}
|
||
}
|
||
|
||
function checkDuplicate(id) {
|
||
const r = requests.find(x => x.id === id);
|
||
if (!r) return;
|
||
|
||
// Ищем дубли по телефону или ФИО
|
||
const duplicates = requests.filter(x =>
|
||
x.id !== id &&
|
||
(x.phone === r.phone || normalizeName(x.name) === normalizeName(r.name))
|
||
);
|
||
|
||
if (duplicates.length > 0) {
|
||
duplicateCheckId = id;
|
||
const dup = duplicates[0];
|
||
document.getElementById('duplicate-content').innerHTML = `
|
||
<p>Найдено совпадение с заявкой #${dup.id}:</p>
|
||
<div class="duplicate-card">
|
||
<strong>${escapeHtml(dup.name)}</strong><br>
|
||
Тел: ${escapeHtml(dup.phone)}<br>
|
||
Адрес: ${escapeHtml(dup.address)}<br>
|
||
Статус: ${dup.status}<br>
|
||
Дата: ${formatDate(dup.date)}
|
||
</div>
|
||
<p>Пометить текущую заявку как дубль?</p>
|
||
`;
|
||
document.getElementById('duplicate-modal').classList.add('active');
|
||
} else {
|
||
alert('Дубли не найдены');
|
||
}
|
||
}
|
||
|
||
function normalizeName(name) {
|
||
return name.toLowerCase().replace(/[^а-яёa-z]/g, '').trim();
|
||
}
|
||
|
||
function confirmDuplicate() {
|
||
if (duplicateCheckId) {
|
||
const r = requests.find(x => x.id === duplicateCheckId);
|
||
if (r) {
|
||
r.isDuplicate = true;
|
||
r.status = 'спорная';
|
||
r.comment = (r.comment || '') + ' Помечена как дубль вручную.';
|
||
saveToStorage();
|
||
renderDashboard();
|
||
renderTable();
|
||
}
|
||
closeDuplicateModal();
|
||
}
|
||
}
|
||
|
||
function keepBoth() {
|
||
closeDuplicateModal();
|
||
}
|
||
|
||
function closeDuplicateModal() {
|
||
document.getElementById('duplicate-modal').classList.remove('active');
|
||
duplicateCheckId = null;
|
||
}
|
||
|
||
// Telegram сводка
|
||
function showTelegramSummary() {
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
const counts = {
|
||
'новая': requests.filter(r => r.status === 'новая').length,
|
||
'в работе': requests.filter(r => r.status === 'в работе').length,
|
||
'на проверке': requests.filter(r => r.status === 'на проверке').length,
|
||
'спорная': requests.filter(r => r.status === 'спорная').length,
|
||
'завершена': requests.filter(r => r.status === 'завершена').length,
|
||
'дубли': requests.filter(r => r.isDuplicate).length
|
||
};
|
||
|
||
const managers = {};
|
||
requests.filter(r => r.manager && r.status !== 'завершена').forEach(r => {
|
||
managers[r.manager] = (managers[r.manager] || 0) + 1;
|
||
});
|
||
|
||
const text = `📊 *Сводка по заявкам на ${formatDate(today)}*
|
||
|
||
🔹 Новые: ${counts['новая']}
|
||
🔹 В работе: ${counts['в работе']}
|
||
🔹 На проверке: ${counts['на проверке']}
|
||
🔹 Спорные: ${counts['спорная']}
|
||
🔹 Дубли: ${counts['дубли']}
|
||
🔹 Завершено: ${counts['завершена']}
|
||
|
||
📈 *Всего в работе:* ${counts['новая'] + counts['в работе'] + counts['на проверке'] + counts['спорная']}
|
||
|
||
👥 *По менеджерам:*
|
||
${Object.entries(managers).map(([m, c]) => `${m}: ${c} заяв.`).join('\n') || '—'}
|
||
|
||
⚠️ *Просрочка:* ${requests.filter(r => r.status === 'просрочка').length} заявок старше 3 дней`;
|
||
|
||
document.getElementById('telegram-content').textContent = text;
|
||
document.getElementById('telegram-modal').classList.add('active');
|
||
}
|
||
|
||
function closeTelegramModal() {
|
||
document.getElementById('telegram-modal').classList.remove('active');
|
||
}
|
||
|
||
function copyTelegram() {
|
||
const text = document.getElementById('telegram-content').textContent;
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
alert('Сводка скопирована в буфер обмена!');
|
||
});
|
||
}
|
||
|
||
// Экспорт в Excel (CSV с BOM)
|
||
function exportToExcel() {
|
||
const headers = ['ID', 'Дата', 'ФИО', 'Телефон', 'Адрес', 'Тариф', 'Менеджер', 'Статус', 'Комментарий', 'Дубль'];
|
||
const rows = requests.map(r => [
|
||
r.id,
|
||
r.date,
|
||
`"${r.name.replace(/"/g, '""')}"`,
|
||
`"${r.phone.replace(/"/g, '""')}"`,
|
||
`"${r.address.replace(/"/g, '""')}"`,
|
||
`"${r.tariff.replace(/"/g, '""')}"`,
|
||
`"${(r.manager || '').replace(/"/g, '""')}"`,
|
||
r.status,
|
||
`"${(r.comment || '').replace(/"/g, '""')}"`,
|
||
r.isDuplicate ? 'Да' : 'Нет'
|
||
]);
|
||
|
||
const csv = '\uFEFF' + [headers.join(';'), ...rows.map(r => r.join(';'))].join('\n');
|
||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `zayavki_${new Date().toISOString().slice(0, 10)}.csv`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
// Старт
|
||
init();
|