Spaces:
Running
Running
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>График ТО и Главная страница</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .toast { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| transform: translateY(100px); | |
| opacity: 0; | |
| transition: all 0.3s ease; | |
| } | |
| .toast.show { | |
| transform: translateY(0); | |
| opacity: 1; | |
| } | |
| .tab-content { | |
| display: none; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| .nav-tabs .active { | |
| border-bottom: 3px solid #3b82f6; | |
| color: #3b82f6; | |
| font-weight: 600; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <!-- Навигационные табы --> | |
| <div class="flex border-b mb-6 nav-tabs"> | |
| <button class="px-4 py-2 font-medium active" data-tab="main">Главная</button> | |
| <button class="px-4 py-2 font-medium" data-tab="to">График ТО</button> | |
| </div> | |
| <!-- Главная страница --> | |
| <div id="main" class="tab-content active"> | |
| <div class="bg-white rounded-lg shadow-md p-6 mb-6"> | |
| <h1 class="text-2xl font-bold text-gray-800 mb-4">Добро пожаловать в систему мониторинга ТО</h1> | |
| <p class="text-gray-600 mb-6">Здесь вы можете отслеживать состояние оборудования и планировать техническое обслуживание.</p> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> | |
| <div class="bg-blue-50 rounded-lg p-4 border border-blue-100"> | |
| <div class="flex items-center"> | |
| <div class="p-3 rounded-full bg-blue-100 text-blue-600 mr-4"> | |
| <i class="fas fa-check-circle text-xl"></i> | |
| </div> | |
| <div> | |
| <h3 class="font-semibold text-gray-800">Выполненные ТО</h3> | |
| <p class="text-2xl font-bold" id="completed-to">0</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-yellow-50 rounded-lg p-4 border border-yellow-100"> | |
| <div class="flex items-center"> | |
| <div class="p-3 rounded-full bg-yellow-100 text-yellow-600 mr-4"> | |
| <i class="fas fa-clock text-xl"></i> | |
| </div> | |
| <div> | |
| <h3 class="font-semibold text-gray-800">Запланированные ТО</h3> | |
| <p class="text-2xl font-bold" id="planned-to">0</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-red-50 rounded-lg p-4 border border-red-100"> | |
| <div class="flex items-center"> | |
| <div class="p-3 rounded-full bg-red-100 text-red-600 mr-4"> | |
| <i class="fas fa-exclamation-triangle text-xl"></i> | |
| </div> | |
| <div> | |
| <h3 class="font-semibold text-gray-800">Просроченные ТО</h3> | |
| <p class="text-2xl font-bold" id="overdue-to">0</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow-sm p-4 border border-gray-200"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Статус ТО</h2> | |
| <div class="h-64"> | |
| <canvas id="mainChart"></canvas> | |
| </div> | |
| </div> | |
| <div class="mt-6 bg-white rounded-lg shadow-sm p-4 border border-gray-200"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Ближайшие ТО</h2> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Оборудование</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Тип ТО</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Дата</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Статус</th> | |
| </tr> | |
| </thead> | |
| <tbody class="bg-white divide-y divide-gray-200" id="upcoming-to-list"> | |
| <!-- Сюда будут добавляться ближайшие ТО --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Страница графика ТО --> | |
| <div id="to" class="tab-content"> | |
| <div class="bg-white rounded-lg shadow-md p-6"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h1 class="text-2xl font-bold text-gray-800">График технического обслуживания</h1> | |
| <button id="add-to-btn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center"> | |
| <i class="fas fa-plus mr-2"></i> Добавить ТО | |
| </button> | |
| </div> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
| <div class="lg:col-span-2"> | |
| <div class="bg-white rounded-lg shadow-sm p-4 border border-gray-200 mb-6"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">График выполнения ТО</h2> | |
| <div class="h-96"> | |
| <canvas id="toChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="bg-white rounded-lg shadow-sm p-4 border border-gray-200"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Список ТО</h2> | |
| <div class="space-y-4" id="to-list"> | |
| <!-- Сюда будут добавляться элементы ТО --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Модальное окно добавления ТО --> | |
| <div id="to-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-white rounded-lg shadow-xl w-full max-w-md"> | |
| <div class="flex justify-between items-center border-b px-6 py-4"> | |
| <h3 class="text-lg font-semibold text-gray-800">Добавить техническое обслуживание</h3> | |
| <button id="close-modal" class="text-gray-400 hover:text-gray-600"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="p-6"> | |
| <form id="to-form"> | |
| <div class="mb-4"> | |
| <label for="equipment" class="block text-sm font-medium text-gray-700 mb-1">Оборудование</label> | |
| <input type="text" id="equipment" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required> | |
| </div> | |
| <div class="mb-4"> | |
| <label for="to-type" class="block text-sm font-medium text-gray-700 mb-1">Тип ТО</label> | |
| <select id="to-type" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required> | |
| <option value="">Выберите тип</option> | |
| <option value="Ежедневное">Ежедневное</option> | |
| <option value="Еженедельное">Еженедельное</option> | |
| <option value="Ежемесячное">Ежемесячное</option> | |
| <option value="Квартальное">Квартальное</option> | |
| <option value="Годовое">Годовое</option> | |
| </select> | |
| </div> | |
| <div class="mb-4"> | |
| <label for="to-date" class="block text-sm font-medium text-gray-700 mb-1">Дата ТО</label> | |
| <input type="date" id="to-date" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required> | |
| </div> | |
| <div class="mb-4"> | |
| <label for="to-status" class="block text-sm font-medium text-gray-700 mb-1">Статус</label> | |
| <select id="to-status" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required> | |
| <option value="planned">Запланировано</option> | |
| <option value="completed">Выполнено</option> | |
| <option value="overdue">Просрочено</option> | |
| </select> | |
| </div> | |
| <div class="flex justify-end space-x-3"> | |
| <button type="button" id="cancel-to" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">Отмена</button> | |
| <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">Сохранить</button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Уведомление --> | |
| <div id="toast" class="toast bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg"> | |
| <div class="flex items-center"> | |
| <i class="fas fa-check-circle mr-2"></i> | |
| <span id="toast-message">ТО успешно добавлено!</span> | |
| </div> | |
| </div> | |
| <script> | |
| // Инициализация данных | |
| let toData = JSON.parse(localStorage.getItem('toData')) || [ | |
| { id: 1, equipment: 'Компрессор №1', type: 'Ежемесячное', date: '2023-06-15', status: 'completed' }, | |
| { id: 2, equipment: 'Насосная станция', type: 'Квартальное', date: '2023-07-20', status: 'planned' }, | |
| { id: 3, equipment: 'Вентиляционная система', type: 'Годовое', date: '2023-05-10', status: 'overdue' }, | |
| { id: 4, equipment: 'Трансформатор Т-1', type: 'Еженедельное', date: '2023-06-25', status: 'planned' }, | |
| { id: 5, equipment: 'Генератор Г-2', type: 'Ежедневное', date: '2023-06-10', status: 'completed' } | |
| ]; | |
| // Сохраняем данные в localStorage | |
| function saveData() { | |
| localStorage.setItem('toData', JSON.stringify(toData)); | |
| updateMainPage(); | |
| updateTOPage(); | |
| } | |
| // Обновление главной страницы | |
| function updateMainPage() { | |
| // Обновляем счетчики | |
| const completed = toData.filter(item => item.status === 'completed').length; | |
| const planned = toData.filter(item => item.status === 'planned').length; | |
| const overdue = toData.filter(item => item.status === 'overdue').length; | |
| document.getElementById('completed-to').textContent = completed; | |
| document.getElementById('planned-to').textContent = planned; | |
| document.getElementById('overdue-to').textContent = overdue; | |
| // Обновляем график на главной | |
| updateMainChart(); | |
| // Обновляем список ближайших ТО | |
| updateUpcomingTOList(); | |
| } | |
| // Обновление страницы графика ТО | |
| function updateTOPage() { | |
| // Обновляем график ТО | |
| updateTOChart(); | |
| // Обновляем список ТО | |
| updateTOList(); | |
| } | |
| // Обновление графика на главной | |
| let mainChart; | |
| function updateMainChart() { | |
| const ctx = document.getElementById('mainChart').getContext('2d'); | |
| const completed = toData.filter(item => item.status === 'completed').length; | |
| const planned = toData.filter(item => item.status === 'planned').length; | |
| const overdue = toData.filter(item => item.status === 'overdue').length; | |
| if (mainChart) { | |
| mainChart.destroy(); | |
| } | |
| mainChart = new Chart(ctx, { | |
| type: 'doughnut', | |
| data: { | |
| labels: ['Выполнено', 'Запланировано', 'Просрочено'], | |
| datasets: [{ | |
| data: [completed, planned, overdue], | |
| backgroundColor: [ | |
| '#10B981', | |
| '#3B82F6', | |
| '#EF4444' | |
| ], | |
| borderWidth: 0 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| position: 'bottom' | |
| } | |
| }, | |
| cutout: '70%' | |
| } | |
| }); | |
| } | |
| // Обновление графика ТО | |
| let toChart; | |
| function updateTOChart() { | |
| const ctx = document.getElementById('toChart').getContext('2d'); | |
| // Группируем по месяцам | |
| const months = ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек']; | |
| const currentYear = new Date().getFullYear(); | |
| // Подготовка данных | |
| const monthlyData = months.map((month, index) => { | |
| const monthNum = index + 1; | |
| const monthStr = monthNum < 10 ? `0${monthNum}` : `${monthNum}`; | |
| const monthStart = `${currentYear}-${monthStr}-01`; | |
| return toData.filter(item => { | |
| const itemDate = new Date(item.date); | |
| return itemDate.getFullYear() === currentYear && | |
| itemDate.getMonth() === index; | |
| }).length; | |
| }); | |
| if (toChart) { | |
| toChart.destroy(); | |
| } | |
| toChart = new Chart(ctx, { | |
| type: 'bar', | |
| data: { | |
| labels: months, | |
| datasets: [{ | |
| label: 'Количество ТО', | |
| data: monthlyData, | |
| backgroundColor: '#3B82F6', | |
| borderRadius: 4 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| ticks: { | |
| stepSize: 1 | |
| } | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| display: false | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Обновление списка ближайших ТО на главной | |
| function updateUpcomingTOList() { | |
| const listElement = document.getElementById('upcoming-to-list'); | |
| listElement.innerHTML = ''; | |
| // Сортируем по дате и берем 5 ближайших | |
| const sorted = [...toData].sort((a, b) => new Date(a.date) - new Date(b.date)); | |
| const upcoming = sorted.slice(0, 5); | |
| upcoming.forEach(item => { | |
| const row = document.createElement('tr'); | |
| // Форматируем дату | |
| const dateObj = new Date(item.date); | |
| const formattedDate = dateObj.toLocaleDateString('ru-RU', { | |
| day: '2-digit', | |
| month: '2-digit', | |
| year: 'numeric' | |
| }); | |
| // Определяем цвет статуса | |
| let statusClass = ''; | |
| let statusText = ''; | |
| switch(item.status) { | |
| case 'completed': | |
| statusClass = 'bg-green-100 text-green-800'; | |
| statusText = 'Выполнено'; | |
| break; | |
| case 'planned': | |
| statusClass = 'bg-blue-100 text-blue-800'; | |
| statusText = 'Запланировано'; | |
| break; | |
| case 'overdue': | |
| statusClass = 'bg-red-100 text-red-800'; | |
| statusText = 'Просрочено'; | |
| break; | |
| } | |
| row.innerHTML = ` | |
| <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${item.equipment}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.type}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedDate}</td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${statusClass}"> | |
| ${statusText} | |
| </span> | |
| </td> | |
| `; | |
| listElement.appendChild(row); | |
| }); | |
| } | |
| // Обновление списка ТО на странице графика | |
| function updateTOList() { | |
| const listElement = document.getElementById('to-list'); | |
| listElement.innerHTML = ''; | |
| // Сортируем по дате | |
| const sorted = [...toData].sort((a, b) => new Date(a.date) - new Date(b.date)); | |
| sorted.forEach(item => { | |
| const itemElement = document.createElement('div'); | |
| itemElement.className = 'bg-gray-50 rounded-lg p-4 border border-gray-200'; | |
| // Форматируем дату | |
| const dateObj = new Date(item.date); | |
| const formattedDate = dateObj.toLocaleDateString('ru-RU', { | |
| day: '2-digit', | |
| month: '2-digit', | |
| year: 'numeric' | |
| }); | |
| // Определяем цвет статуса | |
| let statusClass = ''; | |
| let statusText = ''; | |
| switch(item.status) { | |
| case 'completed': | |
| statusClass = 'bg-green-100 text-green-800'; | |
| statusText = 'Выполнено'; | |
| break; | |
| case 'planned': | |
| statusClass = 'bg-blue-100 text-blue-800'; | |
| statusText = 'Запланировано'; | |
| break; | |
| case 'overdue': | |
| statusClass = 'bg-red-100 text-red-800'; | |
| statusText = 'Просрочено'; | |
| break; | |
| } | |
| itemElement.innerHTML = ` | |
| <div class="flex justify-between items-start mb-2"> | |
| <h3 class="font-medium text-gray-800">${item.equipment}</h3> | |
| <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${statusClass}"> | |
| ${statusText} | |
| </span> | |
| </div> | |
| <div class="text-sm text-gray-600 mb-1">Тип: ${item.type}</div> | |
| <div class="text-sm text-gray-600">Дата: ${formattedDate}</div> | |
| <div class="flex justify-end mt-3 space-x-2"> | |
| <button class="edit-to text-blue-600 hover:text-blue-800 text-sm" data-id="${item.id}"> | |
| <i class="fas fa-edit mr-1"></i> Изменить | |
| </button> | |
| <button class="delete-to text-red-600 hover:text-red-800 text-sm" data-id="${item.id}"> | |
| <i class="fas fa-trash-alt mr-1"></i> Удалить | |
| </button> | |
| </div> | |
| `; | |
| listElement.appendChild(itemElement); | |
| }); | |
| // Добавляем обработчики для кнопок редактирования и удаления | |
| document.querySelectorAll('.edit-to').forEach(button => { | |
| button.addEventListener('click', function() { | |
| const id = parseInt(this.getAttribute('data-id')); | |
| editTO(id); | |
| }); | |
| }); | |
| document.querySelectorAll('.delete-to').forEach(button => { | |
| button.addEventListener('click', function() { | |
| const id = parseInt(this.getAttribute('data-id')); | |
| deleteTO(id); | |
| }); | |
| }); | |
| } | |
| // Редактирование ТО | |
| function editTO(id) { | |
| const item = toData.find(item => item.id === id); | |
| if (!item) return; | |
| document.getElementById('equipment').value = item.equipment; | |
| document.getElementById('to-type').value = item.type; | |
| document.getElementById('to-date').value = item.date; | |
| document.getElementById('to-status').value = item.status; | |
| // Сохраняем ID редактируемого элемента | |
| document.getElementById('to-form').setAttribute('data-edit-id', id); | |
| // Открываем модальное окно | |
| document.getElementById('to-modal').classList.remove('hidden'); | |
| } | |
| // Удаление ТО | |
| function deleteTO(id) { | |
| if (confirm('Вы уверены, что хотите удалить это ТО?')) { | |
| toData = toData.filter(item => item.id !== id); | |
| saveData(); | |
| showToast('ТО успешно удалено!'); | |
| } | |
| } | |
| // Показ уведомления | |
| function showToast(message) { | |
| const toast = document.getElementById('toast'); | |
| document.getElementById('toast-message').textContent = message; | |
| toast.classList.add('show'); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 3000); | |
| } | |
| // Переключение табов | |
| document.querySelectorAll('[data-tab]').forEach(tab => { | |
| tab.addEventListener('click', function() { | |
| const tabId = this.getAttribute('data-tab'); | |
| // Убираем активный класс у всех табов и контента | |
| document.querySelectorAll('[data-tab]').forEach(t => t.classList.remove('active')); | |
| document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); | |
| // Добавляем активный класс текущему табу и контенту | |
| this.classList.add('active'); | |
| document.getElementById(tabId).classList.add('active'); | |
| }); | |
| }); | |
| // Обработка формы ТО | |
| document.getElementById('to-form').addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| const equipment = document.getElementById('equipment').value; | |
| const type = document.getElementById('to-type').value; | |
| const date = document.getElementById('to-date').value; | |
| const status = document.getElementById('to-status').value; | |
| const editId = this.getAttribute('data-edit-id'); | |
| if (editId) { | |
| // Редактирование существующего ТО | |
| const id = parseInt(editId); | |
| const index = toData.findIndex(item => item.id === id); | |
| if (index !== -1) { | |
| toData[index] = { | |
| id, | |
| equipment, | |
| type, | |
| date, | |
| status | |
| }; | |
| showToast('ТО успешно обновлено!'); | |
| } | |
| } else { | |
| // Добавление нового ТО | |
| const newId = toData.length > 0 ? Math.max(...toData.map(item => item.id)) + 1 : 1; | |
| toData.push({ | |
| id: newId, | |
| equipment, | |
| type, | |
| date, | |
| status | |
| }); | |
| showToast('ТО успешно добавлено!'); | |
| } | |
| // Сбрасываем форму и закрываем модальное окно | |
| this.reset(); | |
| this.removeAttribute('data-edit-id'); | |
| document.getElementById('to-modal').classList.add('hidden'); | |
| // Сохраняем данные и обновляем интерфейс | |
| saveData(); | |
| }); | |
| // Открытие модального окна | |
| document.getElementById('add-to-btn').addEventListener('click', function() { | |
| document.getElementById('to-form').reset(); | |
| document.getElementById('to-form').removeAttribute('data-edit-id'); | |
| document.getElementById('to-modal').classList.remove('hidden'); | |
| }); | |
| // Закрытие модального окна | |
| document.getElementById('close-modal').addEventListener('click', function() { | |
| document.getElementById('to-modal').classList.add('hidden'); | |
| }); | |
| document.getElementById('cancel-to').addEventListener('click', function() { | |
| document.getElementById('to-modal').classList.add('hidden'); | |
| }); | |
| // Инициализация при загрузке | |
| document.addEventListener('DOMContentLoaded', function() { | |
| saveData(); // Это обновит все данные и графики | |
| // Устанавливаем минимальную дату (сегодня) для поля даты | |
| const today = new Date().toISOString().split('T')[0]; | |
| document.getElementById('to-date').setAttribute('min', today); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Karmashek/project-to" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |