Files
hotel777-manager/public/client.html
2026-05-04 21:09:42 +05:00

274 lines
10 KiB
HTML
Raw 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" data-title="Карточка клиента" data-description="Профиль клиента и его заявки">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<title>Карточка клиента</title>
<style>
.booking-cards { display: none; }
.booking-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 1.25rem;
margin-bottom: 0.75rem;
box-shadow: var(--shadow-sm);
transition: all var(--transition-base);
animation: cardSlideIn 0.3s ease-out backwards;
}
.booking-card:hover { box-shadow: var(--shadow-md); }
.booking-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.booking-card-status-line {
margin-bottom: 0.5rem;
}
.booking-card-body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.booking-card-field {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.booking-card-label {
font-size: 0.6875rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
}
.booking-card-value {
font-size: 0.875rem;
color: var(--text-primary);
font-weight: 500;
}
.booking-card-comment {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--border-light);
font-size: 0.875rem;
color: var(--text-secondary);
}
@keyframes cardSlideIn {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.table-container { display: none !important; }
.booking-cards { display: block; }
.booking-card-body {
grid-template-columns: 1fr;
gap: 0.5rem;
}
}
</style>
</head>
<body>
<header></header>
<div class="toast-container" id="toastContainer"></div>
<main>
<a href="clients.html" class="btn btn-secondary btn-sm" style="display:inline-flex;align-items:center;gap:0.375rem;margin-bottom:1.5rem;text-decoration:none;">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/></svg>
Назад
</a>
<h1>Профиль клиента</h1>
<div id="clientInfo" class="card" style="margin-bottom:2rem;"></div>
<h2>История заявок</h2>
<div class="table-container">
<table id="clientBookingsTable">
<thead>
<tr>
<th>ID</th>
<th>Имя</th>
<th>Даты</th>
<th>Статус</th>
<th>Комментарий</th>
</tr>
</thead>
<tbody id="bookingsBody"></tbody>
</table>
</div>
<div id="bookingCards" class="booking-cards"></div>
<div id="emptyState" class="empty-state" style="display:none;">
<div class="empty-state-icon">
<svg width="64" height="64" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
</div>
<h3>Заявок нет</h3>
<p>У этого клиента пока нет заявок</p>
</div>
</main>
<script src="nav.js"></script>
<script src="seo.js"></script>
<script>
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function getStatusClass(status) {
const map = {
'Новая': 'status-new',
'В работе': 'status-in-progress',
'Подтверждена': 'status-confirmed',
'Заселение': 'status-checkin',
'Завершена': 'status-completed',
'Отменена': 'status-cancelled'
};
return map[status] || 'status-new';
}
function showToast(message, type = 'info') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.innerHTML = `<span>${escapeHtml(message)}</span>`;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('toast-exit');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
fetch('/api/me').then(r => r.json()).then(data => {
if (!data.isAdmin) window.location.href = '/login.html';
});
const params = new URLSearchParams(window.location.search);
const clientId = params.get('id');
if (!clientId) {
document.querySelector('main').innerHTML = '<h1>Не указан ID клиента</h1><a href="clients.html" class="btn">Вернуться к списку</a>';
throw new Error('No id');
}
async function loadClient() {
const clientInfoEl = document.getElementById('clientInfo');
const tbody = document.getElementById('bookingsBody');
const bookingCards = document.getElementById('bookingCards');
const emptyState = document.getElementById('emptyState');
const tableContainer = document.querySelector('#clientBookingsTable').parentElement;
clientInfoEl.innerHTML = '<div class="loading-spinner"></div>';
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;padding:2rem;"><div class="loading-spinner"></div></td></tr>';
bookingCards.innerHTML = '<div style="text-align:center;padding:2rem;"><div class="loading-spinner"></div></div>';
try {
const res = await fetch(`/api/clients/${clientId}`);
if (!res.ok) {
clientInfoEl.innerHTML = '<p style="color:var(--danger);">Клиент не найден</p>';
tbody.innerHTML = '';
bookingCards.innerHTML = '';
tableContainer.style.display = 'none';
return;
}
const data = await res.json();
clientInfoEl.innerHTML = `
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1.5rem;">
<div>
<div style="color:var(--text-muted);font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;font-weight:600;margin-bottom:0.25rem;">Телефон</div>
<div style="font-size:1.0625rem;font-weight:600;color:var(--text-primary);">${escapeHtml(data.client.phone)}</div>
</div>
<div>
<div style="color:var(--text-muted);font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;font-weight:600;margin-bottom:0.25rem;">Имя</div>
<div style="font-size:1.0625rem;font-weight:600;color:var(--text-primary);">${escapeHtml(data.client.name) || '<span style="color:var(--text-muted);">не указано</span>'}</div>
</div>
<div>
<div style="color:var(--text-muted);font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;font-weight:600;margin-bottom:0.25rem;">Заявок</div>
<div style="font-size:1.0625rem;font-weight:600;color:var(--text-primary);">${data.bookings.length}</div>
</div>
</div>
`;
tbody.innerHTML = '';
bookingCards.innerHTML = '';
if (data.bookings.length === 0) {
tableContainer.style.display = 'none';
emptyState.style.display = 'block';
return;
}
tableContainer.style.display = 'block';
emptyState.style.display = 'none';
data.bookings.forEach((b, i) => {
const tr = document.createElement('tr');
tr.style.animationDelay = `${i * 0.03}s`;
tr.innerHTML = `
<td>${escapeHtml(b.external_id) || '—'}</td>
<td>${escapeHtml(b.name)}</td>
<td>${escapeHtml(b.checkin_date)} ${escapeHtml(b.checkout_date)}</td>
<td><span class="status-badge ${getStatusClass(b.status)}">${escapeHtml(b.status)}</span></td>
<td>${escapeHtml(b.comments) || '<span style="color:var(--text-muted);">—</span>'}</td>
`;
tbody.appendChild(tr);
const card = document.createElement('div');
card.className = 'booking-card';
card.style.animationDelay = `${i * 0.03}s`;
let html = `
<div class="booking-card-header">
<div>
<div style="font-weight:600;color:var(--text-primary);">${escapeHtml(b.name)}</div>
<div style="font-size:0.75rem;color:var(--text-muted);">ID: ${escapeHtml(b.external_id) || '—'}</div>
</div>
<span class="status-badge ${getStatusClass(b.status)}">${escapeHtml(b.status)}</span>
</div>
<div class="booking-card-body">
<div class="booking-card-field">
<span class="booking-card-label">Даты</span>
<span class="booking-card-value">${escapeHtml(b.checkin_date)} ${escapeHtml(b.checkout_date)}</span>
</div>
</div>
`;
if (b.comments) {
html += `<div class="booking-card-comment">${escapeHtml(b.comments)}</div>`;
}
card.innerHTML = html;
bookingCards.appendChild(card);
});
} catch (err) {
showToast('Ошибка загрузки', 'error');
clientInfoEl.innerHTML = '';
tbody.innerHTML = '';
bookingCards.innerHTML = '';
}
}
loadClient();
</script>
</body>
</html>