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

329 lines
13 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>
<div id="editNameRow" class="card" style="display:none; margin-bottom: 1rem;">
<div class="form-group" style="display:flex;gap:0.5rem;align-items:center;">
<label for="editNameInput" style="min-width:120px;">Имя клиента</label>
<input id="editNameInput" type="text" value="" style="flex:1;">
</div>
<div style="display:flex;gap:0.75rem;justify-content:flex-end; margin-top:0.5rem;">
<button id="saveNameBtn" class="btn-secondary">Сохранить</button>
<button id="cancelEditNameBtn" class="btn-secondary">Отмена</button>
</div>
</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);"><span data-current-name>${escapeHtml(data.client.name) || '<span style="color:var(--text-muted);">не указано</span>'}</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>
<div style="margin-top:0.5rem;">
<button id="startEditName" class="btn-secondary">Изменить имя</button>
</div>
`;
// Pre-fill edit field if present after rendering
const editNameInput = document.getElementById('editNameInput');
if (editNameInput) {
editNameInput.value = data.client.name || '';
}
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();
// Name editing UI for client
document.addEventListener('click', function(e) {
if (e.target && e.target.id === 'startEditName') {
document.getElementById('editNameRow').style.display = 'block';
// Pre-fill with current name from display
const nameEl = document.querySelector('#clientInfo [data-current-name]');
const currentName = nameEl ? nameEl.textContent.trim() : '';
document.getElementById('editNameInput').value = currentName;
}
});
document.getElementById('cancelEditNameBtn')?.addEventListener('click', () => {
document.getElementById('editNameRow').style.display = 'none';
});
document.getElementById('saveNameBtn')?.addEventListener('click', async (ev) => {
ev.preventDefault();
const newName = document.getElementById('editNameInput').value.trim();
if (newName.length === 0) return;
try {
const csrfToken = await getCsrfToken();
const res = await fetch(`/api/clients/${clientId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
body: JSON.stringify({ name: newName })
});
if (res.ok) {
document.getElementById('editNameRow').style.display = 'none';
loadClient();
} else {
const err = await res.json();
showToast(err.error || 'Ошибка обновления', 'error');
}
} catch (err) {
showToast('Ошибка соединения', 'error');
}
});
</script>
</body>
</html>