neobhodimo-sozdat-monitoring/index.html

330 lines
15 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Мониторинг коммунальных платежей</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f7fa; color: #1a1a1a; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 10px; color: #1a1a1a; }
.subtitle { color: #666; margin-bottom: 30px; }
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 30px; }
.summary-card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.summary-card h3 { font-size: 14px; color: #666; margin-bottom: 8px; }
.summary-card .value { font-size: 24px; font-weight: bold; color: #1a1a1a; }
.summary-card .value.warning { color: #f59e0b; }
.summary-card .value.success { color: #10b981; }
.apartments { display: grid; gap: 20px; }
.apartment { background: white; border-radius: 12px; padding: 25px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.apartment-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 15px; }
.apartment-header h2 { font-size: 20px; color: #1a1a1a; }
.apartment-header .tenant { color: #666; font-size: 14px; }
.status { padding: 6px 14px; border-radius: 20px; font-size: 13px; font-weight: 500; }
.status.not-paid { background: #fee2e2; color: #dc2626; }
.status.paid { background: #d1fae5; color: #059669; }
.readings-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; }
.reading-item { background: #f8fafc; padding: 15px; border-radius: 8px; }
.reading-item label { display: block; font-size: 13px; color: #64748b; margin-bottom: 8px; }
.reading-item .reading-row { display: flex; gap: 10px; align-items: center; }
.reading-item input { width: 100%; padding: 8px 12px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 14px; }
.reading-item .diff { font-size: 12px; color: #64748b; white-space: nowrap; }
.costs-section { background: #f8fafc; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
.costs-section h3 { font-size: 16px; margin-bottom: 12px; color: #1a1a1a; }
.cost-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #e2e8f0; font-size: 14px; }
.cost-row:last-child { border-bottom: none; font-weight: bold; }
.cost-row .label { color: #64748b; }
.cost-row .value { font-weight: 500; }
.cost-row.total { font-size: 16px; color: #1a1a1a; margin-top: 10px; padding-top: 10px; border-top: 2px solid #1a1a1a; }
.actions { display: flex; gap: 10px; flex-wrap: wrap; }
button { padding: 10px 20px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
.btn-primary { background: #3b82f6; color: white; }
.btn-primary:hover { background: #2563eb; }
.btn-success { background: #10b981; color: white; }
.btn-success:hover { background: #059669; }
.btn-secondary { background: #f1f5f9; color: #475569; }
.btn-secondary:hover { background: #e2e8f0; }
.payment-info { background: #fef3c7; border: 1px solid #f59e0b; padding: 15px; border-radius: 8px; margin-top: 20px; }
.payment-info h3 { font-size: 15px; color: #92400e; margin-bottom: 8px; }
.payment-info p { font-size: 13px; color: #92400e; }
.notification { position: fixed; top: 20px; right: 20px; background: #10b981; color: white; padding: 15px 25px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); transform: translateX(400px); transition: transform 0.3s; z-index: 1000; }
.notification.show { transform: translateX(0); }
.notification.error { background: #ef4444; }
@media (max-width: 768px) {
.apartment-header { flex-direction: column; align-items: flex-start; }
.readings-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="container">
<h1>🏠 Мониторинг коммунальных платежей</h1>
<p class="subtitle">Учёт оплаты аренды и коммунальных услуг</p>
<div class="summary" id="summary"></div>
<div class="apartments" id="apartments"></div>
</div>
<div class="notification" id="notification"></div>
<script>
let data = { apartments: [], log: [] };
async function loadData() {
const resp = await fetch("/api/data");
data = await resp.json();
render();
}
function calcConsumption(last, current) {
return Math.max(0, current - last);
}
function calcCost(consumption, rate) {
return Math.round(consumption * rate);
}
function render() {
const now = new Date();
const currentMonth = now.getMonth();
const currentYear = now.getFullYear();
let totalUnpaid = 0;
let totalApartments = data.apartments.length;
let paidCount = 0;
let overdueCount = 0;
const summaryEl = document.getElementById("summary");
const aptsEl = document.getElementById("apartments");
let summaryHTML = "";
let aptsHTML = "";
data.apartments.forEach(apt => {
const electricityCost = calcCost(
calcConsumption(apt.lastReadings.electricity, apt.currentReadings.electricity),
apt.meterRates.electricity
);
const coldWaterCost = calcCost(
calcConsumption(apt.lastReadings.coldWater, apt.currentReadings.coldWater),
apt.meterRates.coldWater
);
const hotWaterCost = calcCost(
calcConsumption(apt.lastReadings.hotWater, apt.currentReadings.hotWater),
apt.meterRates.hotWater
);
const gasCost = calcCost(
calcConsumption(apt.lastReadings.gas, apt.currentReadings.gas),
apt.meterRates.gas
);
const variableCosts = electricityCost + coldWaterCost + hotWaterCost + gasCost;
const totalCost = variableCosts + apt.fixedCosts;
if (apt.status === "оплачено") {
paidCount++;
} else {
totalUnpaid += totalCost;
if (now.getDate() > apt.paymentDay) {
overdueCount++;
}
}
const isPaid = apt.status === "оплачено";
const isOverdue = now.getDate() > apt.paymentDay && !isPaid;
aptsHTML += `
<div class="apartment">
<div class="apartment-header">
<div>
<h2>${apt.name}</h2>
<p class="tenant">Арендатор: ${apt.tenant} | День оплаты: ${apt.paymentDay}-е число</p>
</div>
<span class="status ${isPaid ? 'paid' : (isOverdue ? 'not-paid' : 'not-paid')}">
${isPaid ? '✓ Оплачено' : (isOverdue ? '⚠ Просрочено' : '⏳ Не оплачено')}
</span>
</div>
<div class="readings-grid">
<div class="reading-item">
<label>⚡ Электроэнергия (тг/кВт·ч: ${apt.meterRates.electricity})</label>
<div class="reading-row">
<input type="number" id="elec-${apt.id}" value="${apt.currentReadings.electricity}" placeholder="Текущие">
<span class="diff">+${calcConsumption(apt.lastReadings.electricity, apt.currentReadings.electricity)} кВт·ч</span>
</div>
</div>
<div class="reading-item">
<label>💧 Холодная вода (тг/м³: ${apt.meterRates.coldWater})</label>
<div class="reading-row">
<input type="number" id="water-${apt.id}" value="${apt.currentReadings.coldWater}" placeholder="Текущие">
<span class="diff">+${calcConsumption(apt.lastReadings.coldWater, apt.currentReadings.coldWater)} м³</span>
</div>
</div>
<div class="reading-item">
<label>🔥 Горячая вода (тг/м³: ${apt.meterRates.hotWater})</label>
<div class="reading-row">
<input type="number" id="hot-${apt.id}" value="${apt.currentReadings.hotWater}" placeholder="Текущие">
<span class="diff">+${calcConsumption(apt.lastReadings.hotWater, apt.currentReadings.hotWater)} м³</span>
</div>
</div>
<div class="reading-item">
<label>🔥 Газ (тг/м³: ${apt.meterRates.gas})</label>
<div class="reading-row">
<input type="number" id="gas-${apt.id}" value="${apt.currentReadings.gas}" placeholder="Текущие">
<span class="diff">+${calcConsumption(apt.lastReadings.gas, apt.currentReadings.gas)} м³</span>
</div>
</div>
</div>
<div class="costs-section">
<h3>Расчёт стоимости</h3>
<div class="cost-row">
<span class="label">Электроэнергия</span>
<span class="value">${electricityCost} ₸</span>
</div>
<div class="cost-row">
<span class="label">Холодная вода</span>
<span class="value">${coldWaterCost} ₸</span>
</div>
<div class="cost-row">
<span class="label">Горячая вода</span>
<span class="value">${hotWaterCost} ₸</span>
</div>
<div class="cost-row">
<span class="label">Газоснабжение</span>
<span class="value">${gasCost} ₸</span>
</div>
<div class="cost-row">
<span class="label">Постоянные расходы</span>
<span class="value">${apt.fixedCosts} ₸</span>
</div>
<div class="cost-row total">
<span class="label">Итого к оплате</span>
<span class="value" style="color: ${isPaid ? '#059669' : '#dc2626'}">${totalCost} ₸</span>
</div>
</div>
<div class="actions">
<button class="btn-primary" onclick="updateReadings(${apt.id})">Сохранить показания</button>
${!isPaid ? `<button class="btn-success" onclick="markPaid(${apt.id})">✓ Отметить оплату</button>` : ''}
<button class="btn-secondary" onclick="exportCsv()">📊 Выгрузить в Excel</button>
</div>
<div class="payment-info">
<h3>💳 Оплата до ${apt.paymentDay}-го числа</h3>
<p>Оплата производится на банковскую карту (Forte Bank). После оплаты нажмите "Отметить оплату".</p>
</div>
</div>
`;
});
summaryHTML = `
<div class="summary-card">
<h3>Всего квартир</h3>
<div class="value">${totalApartments}</div>
</div>
<div class="summary-card">
<h3>Оплачено</h3>
<div class="value success">${paidCount}</div>
</div>
<div class="summary-card">
<h3>Не оплачено</h3>
<div class="value warning">${totalApartments - paidCount}</div>
</div>
<div class="summary-card">
<h3>Просрочено</h3>
<div class="value" style="color: #dc2626">${overdueCount}</div>
</div>
<div class="summary-card">
<h3>Общая сумма к оплате</h3>
<div class="value" style="color: #dc2626">${totalUnpaid.toLocaleString()} ₸</div>
</div>
`;
summaryEl.innerHTML = summaryHTML;
aptsEl.innerHTML = aptsHTML;
}
async function updateReadings(apartmentId) {
const electricity = parseFloat(document.getElementById(`elec-${apartmentId}`).value) || 0;
const coldWater = parseFloat(document.getElementById(`water-${apartmentId}`).value) || 0;
const hotWater = parseFloat(document.getElementById(`hot-${apartmentId}`).value) || 0;
const gas = parseFloat(document.getElementById(`gas-${apartmentId}`).value) || 0;
await fetch("/api/update-readings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
apartmentId,
readings: { electricity, coldWater, hotWater, gas }
})
});
showNotification("Показания сохранены!");
loadData();
}
async function markPaid(apartmentId) {
await fetch("/api/mark-paid", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ apartmentId })
});
showNotification("Оплата отмечена!");
loadData();
}
function showNotification(message, isError = false) {
const notif = document.getElementById("notification");
notif.textContent = message;
notif.className = "notification show" + (isError ? " error" : "");
setTimeout(() => notif.classList.remove("show"), 3000);
}
function exportCsv() {
const now = new Date();
const rows = [["Квартира", "Арендатор", "День оплаты", "Электроэнергия", "Хол. вода", "Гор. вода", "Газ", "Постоянные", "Итого", "Статус"]];
data.apartments.forEach(apt => {
const electricityCost = Math.round(
Math.max(0, apt.currentReadings.electricity - apt.lastReadings.electricity) * apt.meterRates.electricity
);
const coldWaterCost = Math.round(
Math.max(0, apt.currentReadings.coldWater - apt.lastReadings.coldWater) * apt.meterRates.coldWater
);
const hotWaterCost = Math.round(
Math.max(0, apt.currentReadings.hotWater - apt.lastReadings.hotWater) * apt.meterRates.hotWater
);
const gasCost = Math.round(
Math.max(0, apt.currentReadings.gas - apt.lastReadings.gas) * apt.meterRates.gas
);
const total = electricityCost + coldWaterCost + hotWaterCost + gasCost + apt.fixedCosts;
rows.push([
apt.name,
apt.tenant,
apt.paymentDay,
electricityCost,
coldWaterCost,
hotWaterCost,
gasCost,
apt.fixedCosts,
total,
apt.status
]);
});
const csv = "\uFEFF" + rows.map(r => r.map(v => '"' + String(v ?? "").replace(/"/g, '""') + '"').join(";")).join("\n");
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
a.download = `komunalnye_${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}.csv`;
a.click();
}
loadData();
</script>
</body>
</html>