reestr-dogovorov-arendy/index.html

560 lines
24 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>
:root {
--bg: #f5f7fa;
--card: #ffffff;
--text: #1f2937;
--muted: #6b7280;
--border: #e5e7eb;
--primary: #2563eb;
--primary-hover: #1d4ed8;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--info: #3b82f6;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); line-height: 1.5; padding: 24px; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 28px; font-weight: 700; margin-bottom: 24px; color: var(--text); }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: var(--card); border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border: 1px solid var(--border); }
.stat-value { font-size: 32px; font-weight: 700; color: var(--text); }
.stat-label { font-size: 14px; color: var(--muted); margin-top: 4px; }
.controls { background: var(--card); border-radius: 12px; padding: 20px; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border: 1px solid var(--border); }
.search-row { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
.search-input { flex: 1; min-width: 280px; padding: 12px 16px; border: 1px solid var(--border); border-radius: 8px; font-size: 15px; outline: none; transition: border-color 0.2s; }
.search-input:focus { border-color: var(--primary); }
.btn { padding: 12px 20px; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover { background: var(--primary-hover); }
.btn-secondary { background: #f3f4f6; color: var(--text); border: 1px solid var(--border); }
.btn-secondary:hover { background: #e5e7eb; }
.btn-danger { background: var(--danger); color: white; }
.btn-danger:hover { background: #dc2626; }
.btn-sm { padding: 6px 12px; font-size: 13px; }
.chips { display: flex; gap: 8px; flex-wrap: wrap; }
.chip { padding: 8px 16px; border-radius: 20px; font-size: 14px; cursor: pointer; border: 1px solid var(--border); background: var(--card); transition: all 0.2s; }
.chip:hover { border-color: var(--primary); }
.chip.active { background: var(--primary); color: white; border-color: var(--primary); }
.table-container { background: var(--card); border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border: 1px solid var(--border); margin-bottom: 24px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 14px 16px; text-align: left; border-bottom: 1px solid var(--border); }
th { background: #f9fafb; font-weight: 600; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--muted); }
tr { transition: background 0.15s; }
tr:hover { background: #f9fafb; }
tr.expiring-soon { background: #fef3c7; }
tr.expired { background: #fee2e2; }
tr.suspended { background: #f3f4f6; }
.status-badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; text-transform: uppercase; }
.status-active { background: #d1fae5; color: #065f46; }
.status-expiring { background: #fef3c7; color: #92400e; }
.status-expired { background: #fee2e2; color: #991b1b; }
.status-suspended { background: #e5e7eb; color: #374151; }
.actions { display: flex; gap: 8px; }
.action-btn { padding: 6px 10px; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.15s; }
.action-btn-edit { background: #dbeafe; color: var(--primary); }
.action-btn-edit:hover { background: #bfdbfe; }
.action-btn-delete { background: #fee2e2; color: var(--danger); }
.action-btn-delete:hover { background: #fecaca; }
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
.modal-overlay.open { display: flex; }
.modal { background: var(--card); border-radius: 16px; padding: 32px; width: 100%; max-width: 520px; max-height: 90vh; overflow-y: auto; }
.modal h2 { font-size: 22px; margin-bottom: 24px; }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; font-size: 14px; font-weight: 600; margin-bottom: 6px; color: var(--text); }
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; font-size: 15px; outline: none; }
.form-group input:focus, .form-group select:focus { border-color: var(--primary); }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.form-actions { display: flex; gap: 12px; margin-top: 24px; }
.tabs { display: flex; gap: 8px; margin-bottom: 16px; }
.tab { padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: 600; border: 1px solid transparent; }
.tab.active { background: var(--primary); color: white; }
.tab:hover:not(.active) { background: #f3f4f6; }
.log-container { background: var(--card); border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border: 1px solid var(--border); }
.log-entry { padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 14px; }
.log-entry:last-child { border-bottom: none; }
.log-time { color: var(--muted); font-size: 12px; }
.log-action { font-weight: 600; }
.log-action.create { color: var(--success); }
.log-action.edit { color: var(--info); }
.log-action.delete { color: var(--danger); }
.empty-state { text-align: center; padding: 60px 20px; color: var(--muted); }
.empty-state p { font-size: 16px; }
@media (max-width: 768px) {
body { padding: 16px; }
.form-row { grid-template-columns: 1fr; }
table { font-size: 13px; }
th, td { padding: 10px 12px; }
}
</style>
</head>
<body>
<div class="container">
<h1>Реестр договоров аренды</h1>
<div class="stats" id="stats"></div>
<div class="controls">
<div class="search-row">
<input type="text" class="search-input" id="searchInput" placeholder="Поиск по контрагенту, объекту, ответственному или статусу...">
<button class="btn btn-primary" onclick="openModal()">+ Добавить договор</button>
<button class="btn btn-secondary" onclick="exportCSV()">Выгрузить CSV</button>
</div>
<div class="chips" id="statusChips"></div>
</div>
<div class="tabs">
<div class="tab active" data-tab="registry" onclick="switchTab('registry')">Реестр</div>
<div class="tab" data-tab="log" onclick="switchTab('log')">Журнал изменений</div>
</div>
<div id="registryTab">
<div class="table-container">
<table>
<thead>
<tr>
<th>Контрагент</th>
<th>Объект</th>
<th>Сумма (₸)</th>
<th>Срок действия</th>
<th>Ответственный</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
<div class="empty-state" id="emptyState" style="display: none;">
<p>Нет договоров для отображения</p>
</div>
</div>
</div>
<div id="logTab" style="display: none;">
<div class="log-container" id="logContainer"></div>
</div>
</div>
<div class="modal-overlay" id="modalOverlay">
<div class="modal">
<h2 id="modalTitle">Добавить договор</h2>
<form id="contractForm">
<input type="hidden" id="contractId">
<div class="form-group">
<label for="counterparty">Контрагент *</label>
<input type="text" id="counterparty" required placeholder="Например: ТОО «Алатау Сервис»">
</div>
<div class="form-group">
<label for="object">Объект аренды *</label>
<input type="text" id="object" required placeholder="Например: офис в Алматы">
</div>
<div class="form-row">
<div class="form-group">
<label for="amount">Сумма (₸) *</label>
<input type="number" id="amount" required placeholder="150000">
</div>
<div class="form-group">
<label for="responsible">Ответственный *</label>
<input type="text" id="responsible" required placeholder="ФИО">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="startDate">Дата начала *</label>
<input type="date" id="startDate" required>
</div>
<div class="form-group">
<label for="endDate">Дата окончания *</label>
<input type="date" id="endDate" required>
</div>
</div>
<div class="form-group">
<label for="status">Статус *</label>
<select id="status" required>
<option value="active">Действующий</option>
<option value="expiring">Скоро истекает</option>
<option value="expired">Истёк</option>
<option value="suspended">Приостановлен</option>
</select>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Сохранить</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Отмена</button>
</div>
</form>
</div>
</div>
<script>
const STORAGE_KEY = 'lease_contracts';
const LOG_KEY = 'lease_log';
const EXPIRING_DAYS = 30;
const demoData = [
{ id: 1, counterparty: 'ТОО «Алатау Сервис»', object: 'Офис, Алматы, ул. Абая 150', amount: 450000, startDate: '2025-01-15', endDate: '2026-01-14', responsible: 'Иванов А.С.', status: 'active' },
{ id: 2, counterparty: 'ТОО «Qazaq Office»', object: 'Склад, Астана, пр. Сарыарка 22', amount: 680000, startDate: '2025-06-01', endDate: '2026-05-31', responsible: 'Петрова М.К.', status: 'active' },
{ id: 3, counterparty: 'ИП Садыкова Г.Т.', object: 'Помещение, Шымкент, ул. Толе би 45', amount: 220000, startDate: '2024-03-01', endDate: '2025-08-31', responsible: 'Алиев Н.Р.', status: 'expiring' },
{ id: 4, counterparty: 'ТОО «Steppe Trade»', object: 'Торговая точка, Караганда, ул. Ленина 88', amount: 350000, startDate: '2025-09-01', endDate: '2026-08-31', responsible: 'Смирнов Д.В.', status: 'active' },
{ id: 5, counterparty: 'ТОО «Байтау»', object: 'Офис, Алматы, пр. Достык 100', amount: 520000, startDate: '2023-01-01', endDate: '2024-12-31', responsible: 'Иванов А.С.', status: 'expired' },
{ id: 6, counterparty: 'ИП Нурланов', object: 'Склад, Астана, ул. Кенесары 55', amount: 380000, startDate: '2025-02-01', endDate: '2026-01-31', responsible: 'Петрова М.К.', status: 'active' },
{ id: 7, counterparty: 'ТОО «KazRent»', object: 'Помещение, Алматы, ул. Сатпаева 30', amount: 750000, startDate: '2024-06-01', endDate: '2025-05-31', responsible: 'Алиев Н.Р.', status: 'suspended' },
{ id: 8, counterparty: 'ТОО «Алматы Строй»', object: 'Офис, Алматы, пр. Абая 200', amount: 480000, startDate: '2025-10-01', endDate: '2025-09-15', responsible: 'Смирнов Д.В.', status: 'expiring' }
];
let contracts = [];
let log = [];
let currentFilter = 'all';
let searchQuery = '';
function loadData() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
contracts = JSON.parse(stored);
} else {
contracts = [...demoData];
saveData();
}
const storedLog = localStorage.getItem(LOG_KEY);
if (storedLog) {
log = JSON.parse(storedLog);
} else {
log = [
{ time: new Date().toISOString(), action: 'create', what: 'Инициализация реестра', who: 'Система' }
];
saveLog();
}
}
function saveData() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(contracts));
}
function saveLog() {
localStorage.setItem(LOG_KEY, JSON.stringify(log));
}
function addLogEntry(action, what, who = 'Пользователь') {
log.unshift({
time: new Date().toISOString(),
action,
what,
who
});
saveLog();
}
function getStatusLabel(status) {
const labels = {
active: 'Действующий',
expiring: 'Скоро истекает',
expired: 'Истёк',
suspended: 'Приостановлен'
};
return labels[status] || status;
}
function getStatusClass(status) {
return 'status-' + status;
}
function formatDate(dateStr) {
const d = new Date(dateStr);
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
return `${day}.${month}.${year}`;
}
function getRowClass(contract) {
if (contract.status === 'expired') return 'expired';
if (contract.status === 'suspended') return 'suspended';
if (contract.status === 'expiring') return 'expiring-soon';
return '';
}
function calculateStats() {
const stats = { total: contracts.length, active: 0, expiring: 0, expired: 0, suspended: 0 };
contracts.forEach(c => {
if (stats[c.status] !== undefined) stats[c.status]++;
});
return stats;
}
function renderStats() {
const stats = calculateStats();
const container = document.getElementById('stats');
container.innerHTML = `
<div class="stat-card">
<div class="stat-value">${stats.total}</div>
<div class="stat-label">Всего договоров</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.active}</div>
<div class="stat-label">Действующий</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.expiring}</div>
<div class="stat-label">Скоро истекает</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.expired}</div>
<div class="stat-label">Истёк</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.suspended}</div>
<div class="stat-label">Приостановлен</div>
</div>
`;
}
function renderChips() {
const chips = [
{ value: 'all', label: 'Все' },
{ value: 'active', label: 'Действующий' },
{ value: 'expiring', label: 'Скоро истекает' },
{ value: 'expired', label: 'Истёк' },
{ value: 'suspended', label: 'Приостановлен' }
];
const container = document.getElementById('statusChips');
container.innerHTML = chips.map(chip => `
<div class="chip ${currentFilter === chip.value ? 'active' : ''}"
onclick="setFilter('${chip.value}')">${chip.label}</div>
`).join('');
}
function setFilter(value) {
currentFilter = value;
renderChips();
renderTable();
}
function filterContracts() {
let filtered = [...contracts];
if (currentFilter !== 'all') {
filtered = filtered.filter(c => c.status === currentFilter);
}
if (searchQuery) {
const q = searchQuery.toLowerCase();
filtered = filtered.filter(c =>
c.counterparty.toLowerCase().includes(q) ||
c.object.toLowerCase().includes(q) ||
c.responsible.toLowerCase().includes(q) ||
getStatusLabel(c.status).toLowerCase().includes(q)
);
}
return filtered;
}
function renderTable() {
const filtered = filterContracts();
const tbody = document.getElementById('tableBody');
const emptyState = document.getElementById('emptyState');
if (filtered.length === 0) {
tbody.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
tbody.innerHTML = filtered.map(c => `
<tr class="${getRowClass(c)}">
<td>${escapeHtml(c.counterparty)}</td>
<td>${escapeHtml(c.object)}</td>
<td>${formatNumber(c.amount)}</td>
<td>${formatDate(c.startDate)} — ${formatDate(c.endDate)}</td>
<td>${escapeHtml(c.responsible)}</td>
<td><span class="status-badge ${getStatusClass(c)}">${getStatusLabel(c.status)}</span></td>
<td>
<div class="actions">
<button class="action-btn action-btn-edit" onclick="editContract(${c.id})">Ред.</button>
<button class="action-btn action-btn-delete" onclick="deleteContract(${c.id})">Удалить</button>
</div>
</td>
</tr>
`).join('');
}
function renderLog() {
const container = document.getElementById('logContainer');
container.innerHTML = log.map(entry => `
<div class="log-entry">
<span class="log-time">${new Date(entry.time).toLocaleString('ru-RU')}</span> —
<span class="log-action ${entry.action}">${entry.action === 'create' ? 'Создание' : entry.action === 'edit' ? 'Редактирование' : 'Удаление'}</span>:
${escapeHtml(entry.what)} (Ответственный: ${escapeHtml(entry.who)})
</div>
`).join('');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatNumber(num) {
return new Intl.NumberFormat('ru-RU').format(num);
}
function openModal(contractId = null) {
const overlay = document.getElementById('modalOverlay');
const title = document.getElementById('modalTitle');
const form = document.getElementById('contractForm');
if (contractId) {
const contract = contracts.find(c => c.id === contractId);
if (!contract) return;
title.textContent = 'Редактировать договор';
document.getElementById('contractId').value = contract.id;
document.getElementById('counterparty').value = contract.counterparty;
document.getElementById('object').value = contract.object;
document.getElementById('amount').value = contract.amount;
document.getElementById('responsible').value = contract.responsible;
document.getElementById('startDate').value = contract.startDate;
document.getElementById('endDate').value = contract.endDate;
document.getElementById('status').value = contract.status;
} else {
title.textContent = 'Добавить договор';
form.reset();
document.getElementById('contractId').value = '';
}
overlay.classList.add('open');
}
function closeModal() {
document.getElementById('modalOverlay').classList.remove('open');
}
function editContract(id) {
openModal(id);
}
function deleteContract(id) {
const contract = contracts.find(c => c.id === id);
if (!contract) return;
if (!confirm(`Вы уверены, что хотите удалить договор с контрагентом "${contract.counterparty}"?`)) {
return;
}
addLogEntry('delete', `Удалён договор: ${contract.counterparty} (${contract.object})`, contract.responsible);
contracts = contracts.filter(c => c.id !== id);
saveData();
renderAll();
}
function exportCSV() {
const headers = ['Контрагент', 'Объект', 'Сумма', 'Дата начала', 'Дата окончания', 'Ответственный', 'Статус'];
const rows = filterContracts().map(c => [
c.counterparty,
c.object,
c.amount.toString(),
formatDate(c.startDate),
formatDate(c.endDate),
c.responsible,
getStatusLabel(c.status)
]);
const csvContent = '\uFEFF' + [headers.join(';'), ...rows.map(r => r.join(';'))].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'reestr_dogovorov.csv';
a.click();
URL.revokeObjectURL(url);
}
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelector(`[data-tab="${tab}"]`).classList.add('active');
if (tab === 'registry') {
document.getElementById('registryTab').style.display = 'block';
document.getElementById('logTab').style.display = 'none';
} else {
document.getElementById('registryTab').style.display = 'none';
document.getElementById('logTab').style.display = 'block';
renderLog();
}
}
function renderAll() {
renderStats();
renderChips();
renderTable();
}
document.getElementById('contractForm').addEventListener('submit', function(e) {
e.preventDefault();
const id = document.getElementById('contractId').value;
const contractData = {
counterparty: document.getElementById('counterparty').value,
object: document.getElementById('object').value,
amount: parseFloat(document.getElementById('amount').value),
responsible: document.getElementById('responsible').value,
startDate: document.getElementById('startDate').value,
endDate: document.getElementById('endDate').value,
status: document.getElementById('status').value
};
if (id) {
const index = contracts.findIndex(c => c.id == id);
if (index !== -1) {
const old = contracts[index];
contractData.id = old.id;
contracts[index] = contractData;
addLogEntry('edit', `Изменён договор: ${contractData.counterparty} (${contractData.object})`, contractData.responsible);
}
} else {
const newId = Date.now();
contracts.push({ id: newId, ...contractData });
addLogEntry('create', `Создан договор: ${contractData.counterparty} (${contractData.object})`, contractData.responsible);
}
saveData();
closeModal();
renderAll();
});
document.getElementById('searchInput').addEventListener('input', function(e) {
searchQuery = e.target.value.trim();
renderTable();
});
document.getElementById('modalOverlay').addEventListener('click', function(e) {
if (e.target === e.currentTarget) {
closeModal();
}
});
loadData();
renderAll();
</script>
</body>
</html>