1
This commit is contained in:
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
# Устанавливаем git
|
||||||
|
RUN apk update --no-cache
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
RUN git clone https://git.dadehard.ru/kalugin66/hotel777-manager.git .
|
||||||
|
COPY .env . || true
|
||||||
|
RUN npm i
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["npm", "start"]
|
||||||
31
docker-compose.yml
Normal file
31
docker-compose.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
hotel777-manager:
|
||||||
|
build: .
|
||||||
|
image: kalugin66/hotel777-manager
|
||||||
|
container_name: hotel777-manager
|
||||||
|
# ports:
|
||||||
|
# - "3000:3000"
|
||||||
|
networks:
|
||||||
|
- applications
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- /docker/hotel777-manager/data:/app/data:rw
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Yekaterinburg
|
||||||
|
- HOTEL777KEY="secretkey"
|
||||||
|
- hotelName="Hotel 777"
|
||||||
|
- hotelAddress="Абхазтя золотой берег"
|
||||||
|
- hotelPhone="+79400000000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "http://localhost:3000"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
applications:
|
||||||
|
external: true
|
||||||
|
# docker network create applications
|
||||||
|
# docker compose up -d
|
||||||
|
# docker compose up -d --build
|
||||||
|
# docker compose build --no-cache && docker compose up -d
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
<!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">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header></header>
|
|
||||||
<main>
|
|
||||||
<h1>Заявки на заселение</h1>
|
|
||||||
<div>
|
|
||||||
<label>Фильтр по статусу:
|
|
||||||
<select id="statusFilter">
|
|
||||||
<option value="">Все</option>
|
|
||||||
<option value="Новая">Новая</option>
|
|
||||||
<option value="В работе">В работе</option>
|
|
||||||
<option value="Подтверждена">Подтверждена</option>
|
|
||||||
<option value="Заселение">Заселение</option>
|
|
||||||
<option value="Завершена">Завершена</option>
|
|
||||||
<option value="Отменена">Отменена</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<input type="text" id="searchInput" placeholder="Поиск по имени, телефону или комментарию">
|
|
||||||
<button id="searchBtn">Найти</button>
|
|
||||||
</div>
|
|
||||||
<table id="bookingsTable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID внеш.</th>
|
|
||||||
<th>Имя</th>
|
|
||||||
<th>Телефон</th>
|
|
||||||
<th>Даты</th>
|
|
||||||
<th>Статус</th>
|
|
||||||
<th>Действия</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- Модальное окно редактирования -->
|
|
||||||
<div id="editModal" class="modal" style="display:none;">
|
|
||||||
<div class="modal-content">
|
|
||||||
<h2>Редактировать заявку</h2>
|
|
||||||
<form id="editForm">
|
|
||||||
<input type="hidden" id="editId">
|
|
||||||
<label>Статус</label>
|
|
||||||
<select id="editStatus">
|
|
||||||
<option value="Новая">Новая</option>
|
|
||||||
<option value="В работе">В работе</option>
|
|
||||||
<option value="Подтверждена">Подтверждена</option>
|
|
||||||
<option value="Заселение">Заселение</option>
|
|
||||||
<option value="Завершена">Завершена</option>
|
|
||||||
<option value="Отменена">Отменена</option>
|
|
||||||
</select>
|
|
||||||
<label>Комментарий</label>
|
|
||||||
<textarea id="editComments" rows="3"></textarea>
|
|
||||||
<label>Переназначить на карточку клиента (по телефону)</label>
|
|
||||||
<input type="text" id="editPhone" placeholder="Телефон (любой формат)">
|
|
||||||
<button type="submit">Сохранить</button>
|
|
||||||
<button type="button" id="closeModal">Отмена</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<script src="nav.js"></script>
|
|
||||||
<script src="seo.js"></script>
|
|
||||||
<script>
|
|
||||||
// Проверка авторизации
|
|
||||||
fetch('/api/me').then(r => r.json()).then(data => {
|
|
||||||
if (!data.isAdmin) window.location.href = '/login.html';
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableBody = document.querySelector('#bookingsTable tbody');
|
|
||||||
const modal = document.getElementById('editModal');
|
|
||||||
let currentBookings = [];
|
|
||||||
|
|
||||||
async function loadBookings() {
|
|
||||||
const status = document.getElementById('statusFilter').value;
|
|
||||||
const search = document.getElementById('searchInput').value;
|
|
||||||
let url = '/api/bookings?';
|
|
||||||
if (status) url += `status=${encodeURIComponent(status)}&`;
|
|
||||||
if (search) url += `search=${encodeURIComponent(search)}&`;
|
|
||||||
const res = await fetch(url);
|
|
||||||
currentBookings = await res.json();
|
|
||||||
renderTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTable() {
|
|
||||||
tableBody.innerHTML = '';
|
|
||||||
currentBookings.forEach(b => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
row.innerHTML = `
|
|
||||||
<td>${b.external_id || '-'}</td>
|
|
||||||
<td>${b.name}</td>
|
|
||||||
<td>${b.phone_raw}</td>
|
|
||||||
<td>${b.checkin_date} – ${b.checkout_date}</td>
|
|
||||||
<td>${b.status}</td>
|
|
||||||
<td><button class="editBtn" data-id="${b.id}">Изменить</button></td>
|
|
||||||
`;
|
|
||||||
tableBody.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('searchBtn').addEventListener('click', loadBookings);
|
|
||||||
document.getElementById('statusFilter').addEventListener('change', loadBookings);
|
|
||||||
|
|
||||||
// Редактирование
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (e.target.classList.contains('editBtn')) {
|
|
||||||
const id = e.target.dataset.id;
|
|
||||||
const booking = currentBookings.find(b => b.id == id);
|
|
||||||
if (!booking) return;
|
|
||||||
document.getElementById('editId').value = booking.id;
|
|
||||||
document.getElementById('editStatus').value = booking.status;
|
|
||||||
document.getElementById('editComments').value = booking.comments || '';
|
|
||||||
document.getElementById('editPhone').value = '';
|
|
||||||
modal.style.display = 'flex';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('closeModal').addEventListener('click', () => {
|
|
||||||
modal.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('editForm').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const id = document.getElementById('editId').value;
|
|
||||||
const status = document.getElementById('editStatus').value;
|
|
||||||
const comments = document.getElementById('editComments').value;
|
|
||||||
const phone = document.getElementById('editPhone').value.trim();
|
|
||||||
|
|
||||||
const body = {};
|
|
||||||
if (status) body.status = status;
|
|
||||||
body.comments = comments;
|
|
||||||
if (phone) body.phone = phone;
|
|
||||||
|
|
||||||
const res = await fetch(`/api/bookings/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
modal.style.display = 'none';
|
|
||||||
loadBookings();
|
|
||||||
} else {
|
|
||||||
alert('Ошибка сохранения');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Первоначальная загрузка
|
|
||||||
loadBookings();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ru" data-title="Добро пожаловать" data-description="Гостиница Солнечная – бронируйте номера онлайн">
|
<html lang="ru" data-title="Заявки на заселение" data-description="Управление заявками гостиницы">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -8,10 +8,148 @@
|
|||||||
<body>
|
<body>
|
||||||
<header></header>
|
<header></header>
|
||||||
<main>
|
<main>
|
||||||
<h1>Добро пожаловать в гостиницу "Солнечная"</h1>
|
<h1>Заявки на заселение</h1>
|
||||||
<p>Сервис управления заявками на заселение. Для доступа в панель управления <a href="/login.html">войдите</a>.</p>
|
<div>
|
||||||
|
<label>Фильтр по статусу:
|
||||||
|
<select id="statusFilter">
|
||||||
|
<option value="">Все</option>
|
||||||
|
<option value="Новая">Новая</option>
|
||||||
|
<option value="В работе">В работе</option>
|
||||||
|
<option value="Подтверждена">Подтверждена</option>
|
||||||
|
<option value="Заселение">Заселение</option>
|
||||||
|
<option value="Завершена">Завершена</option>
|
||||||
|
<option value="Отменена">Отменена</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="searchInput" placeholder="Поиск по имени, телефону или комментарию">
|
||||||
|
<button id="searchBtn">Найти</button>
|
||||||
|
</div>
|
||||||
|
<table id="bookingsTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID внеш.</th>
|
||||||
|
<th>Имя</th>
|
||||||
|
<th>Телефон</th>
|
||||||
|
<th>Даты</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Модальное окно редактирования -->
|
||||||
|
<div id="editModal" class="modal" style="display:none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h2>Редактировать заявку</h2>
|
||||||
|
<form id="editForm">
|
||||||
|
<input type="hidden" id="editId">
|
||||||
|
<label>Статус</label>
|
||||||
|
<select id="editStatus">
|
||||||
|
<option value="Новая">Новая</option>
|
||||||
|
<option value="В работе">В работе</option>
|
||||||
|
<option value="Подтверждена">Подтверждена</option>
|
||||||
|
<option value="Заселение">Заселение</option>
|
||||||
|
<option value="Завершена">Завершена</option>
|
||||||
|
<option value="Отменена">Отменена</option>
|
||||||
|
</select>
|
||||||
|
<label>Комментарий</label>
|
||||||
|
<textarea id="editComments" rows="3"></textarea>
|
||||||
|
<label>Переназначить на карточку клиента (по телефону)</label>
|
||||||
|
<input type="text" id="editPhone" placeholder="Телефон (любой формат)">
|
||||||
|
<button type="submit">Сохранить</button>
|
||||||
|
<button type="button" id="closeModal">Отмена</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script src="nav.js"></script>
|
<script src="nav.js"></script>
|
||||||
<script src="seo.js"></script>
|
<script src="seo.js"></script>
|
||||||
|
<script>
|
||||||
|
// Проверка авторизации
|
||||||
|
fetch('/api/me').then(r => r.json()).then(data => {
|
||||||
|
if (!data.isAdmin) window.location.href = '/login.html';
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableBody = document.querySelector('#bookingsTable tbody');
|
||||||
|
const modal = document.getElementById('editModal');
|
||||||
|
let currentBookings = [];
|
||||||
|
|
||||||
|
async function loadBookings() {
|
||||||
|
const status = document.getElementById('statusFilter').value;
|
||||||
|
const search = document.getElementById('searchInput').value;
|
||||||
|
let url = '/api/bookings?';
|
||||||
|
if (status) url += `status=${encodeURIComponent(status)}&`;
|
||||||
|
if (search) url += `search=${encodeURIComponent(search)}&`;
|
||||||
|
const res = await fetch(url);
|
||||||
|
currentBookings = await res.json();
|
||||||
|
renderTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable() {
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
currentBookings.forEach(b => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${b.external_id || '-'}</td>
|
||||||
|
<td>${b.name}</td>
|
||||||
|
<td>${b.phone_raw}</td>
|
||||||
|
<td>${b.checkin_date} – ${b.checkout_date}</td>
|
||||||
|
<td>${b.status}</td>
|
||||||
|
<td><button class="editBtn" data-id="${b.id}">Изменить</button></td>
|
||||||
|
`;
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('searchBtn').addEventListener('click', loadBookings);
|
||||||
|
document.getElementById('statusFilter').addEventListener('change', loadBookings);
|
||||||
|
|
||||||
|
// Редактирование
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('editBtn')) {
|
||||||
|
const id = e.target.dataset.id;
|
||||||
|
const booking = currentBookings.find(b => b.id == id);
|
||||||
|
if (!booking) return;
|
||||||
|
document.getElementById('editId').value = booking.id;
|
||||||
|
document.getElementById('editStatus').value = booking.status;
|
||||||
|
document.getElementById('editComments').value = booking.comments || '';
|
||||||
|
document.getElementById('editPhone').value = '';
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('closeModal').addEventListener('click', () => {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('editForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const id = document.getElementById('editId').value;
|
||||||
|
const status = document.getElementById('editStatus').value;
|
||||||
|
const comments = document.getElementById('editComments').value;
|
||||||
|
const phone = document.getElementById('editPhone').value.trim();
|
||||||
|
|
||||||
|
const body = {};
|
||||||
|
if (status) body.status = status;
|
||||||
|
body.comments = comments;
|
||||||
|
if (phone) body.phone = phone;
|
||||||
|
|
||||||
|
const res = await fetch(`/api/bookings/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
loadBookings();
|
||||||
|
} else {
|
||||||
|
alert('Ошибка сохранения');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Первоначальная загрузка
|
||||||
|
loadBookings();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -2,9 +2,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const navHTML = `
|
const navHTML = `
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/">Главная</a>
|
<a href="/">Главная</a>
|
||||||
<a href="/admin.html">Панель управления</a>
|
|
||||||
<a href="/bookings.html">Заявки</a>
|
|
||||||
<a href="/clients.html">Клиенты</a>
|
<a href="/clients.html">Клиенты</a>
|
||||||
|
<a href="/admin.html">Панель управления</a>
|
||||||
<a id="logoutLink" href="#" style="display:none;">Выход</a>
|
<a id="logoutLink" href="#" style="display:none;">Выход</a>
|
||||||
</nav>
|
</nav>
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user