|
|
<!DOCTYPE html> |
|
|
<html lang="ru"> |
|
|
<head> |
|
|
<meta charset="UTF-8" /> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
|
|
<title>MES System</title> |
|
|
<script src="https://cdn.tailwindcss.com "></script> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css "/> |
|
|
<style> |
|
|
.notification-badge { |
|
|
position: absolute; |
|
|
top: 0; |
|
|
right: 0; |
|
|
font-size: 0.625rem; |
|
|
line-height: 1.25rem; |
|
|
width: 1.25rem; |
|
|
height: 1.25rem; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
border-radius: 9999px; |
|
|
} |
|
|
.has-event:hover { |
|
|
background-color: #BFDBFE !important; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-50 text-white font-sans"> |
|
|
|
|
|
<div id="app" class="flex h-screen overflow-hidden bg-gray-50"></div> |
|
|
|
|
|
<script type="module"> |
|
|
import { createRoot } from 'https://unpkg.com/react @18/umd/react.development.js'; |
|
|
import ReactDOM from 'https://unpkg.com/react-dom @18/umd/react-dom.development.js'; |
|
|
|
|
|
const root = ReactDOM.createRoot(document.getElementById('app')); |
|
|
|
|
|
|
|
|
const equipmentData = [ |
|
|
{ |
|
|
id: 1, |
|
|
name: "Токарный станок CNC-2000", |
|
|
type: "Станок", |
|
|
serial: "CNC2000-001", |
|
|
inventory: "INV-1001", |
|
|
status: "active", |
|
|
lastMaintenance: "2023-05-15", |
|
|
nextMaintenance: "2023-06-15", |
|
|
maintenanceInterval: 30, |
|
|
description: "Токарный станок с ЧПУ" |
|
|
}, |
|
|
{ |
|
|
id: 2, |
|
|
name: "Конвейерная линия A-12", |
|
|
type: "Конвейер", |
|
|
serial: "CONV-A12-045", |
|
|
inventory: "INV-1042", |
|
|
status: "maintenance", |
|
|
lastMaintenance: "2023-05-20", |
|
|
nextMaintenance: "2023-06-20", |
|
|
maintenanceInterval: 30, |
|
|
description: "Линия для сборки продукции" |
|
|
}, |
|
|
{ |
|
|
id: 3, |
|
|
name: "Воздушный компрессор V-50", |
|
|
type: "Компрессор", |
|
|
serial: "COMP-V50-112", |
|
|
inventory: "INV-1078", |
|
|
status: "repair", |
|
|
lastMaintenance: "2023-04-10", |
|
|
nextMaintenance: "2023-06-10", |
|
|
maintenanceInterval: 60, |
|
|
description: "Промышленный воздушный компрессор" |
|
|
} |
|
|
]; |
|
|
|
|
|
const users = [ |
|
|
{id: 1, name: "Иван Петров", role: "admin", email: "ivan@example.com"}, |
|
|
{id: 2, name: "Петр Смирнов", role: "engineer", email: "petr@example.com"}, |
|
|
{id: 3, name: "Анна Иванова", role: "viewer", email: "anna@example.com"} |
|
|
]; |
|
|
|
|
|
const maintenanceEvents = [ |
|
|
{equipmentId: 1, date: "2023-06-15", status: "planned", responsible: "Иванов А.П."}, |
|
|
{equipmentId: 2, date: "2023-06-20", status: "planned", responsible: "Петров И.С."}, |
|
|
{equipmentId: 3, date: "2023-06-10", status: "overdue", responsible: "Сидоров В.М."} |
|
|
]; |
|
|
|
|
|
function formatDate(dateStr) { |
|
|
if (!dateStr) return ""; |
|
|
const d = new Date(dateStr); |
|
|
return `${d.getDate()}.${d.getMonth()+1}.${d.getFullYear()}`; |
|
|
} |
|
|
|
|
|
function calculateNextMaintenanceDate(commissionDate, intervalDays) { |
|
|
if (!commissionDate) return ''; |
|
|
const nextDate = new Date(new Date(commissionDate).getTime() + parseInt(intervalDays) * 24 * 60 * 60 * 1000); |
|
|
return nextDate.toISOString().split('T')[0]; |
|
|
} |
|
|
|
|
|
function Dashboard({ setActiveTab }) { |
|
|
const activeCount = equipmentData.length; |
|
|
const completedCount = equipmentData.filter(e => e.lastMaintenance).length; |
|
|
const overdueCount = equipmentData.filter(e => new Date(e.nextMaintenance) < new Date()).length; |
|
|
const repairCount = equipmentData.filter(e => e.status === "repair").length; |
|
|
|
|
|
return ( |
|
|
<div class="p-6"> |
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"> |
|
|
<div class="bg-white rounded-lg shadow p-6 text-gray-800"> |
|
|
<div class="flex items-center"> |
|
|
<i class="fas fa-tools text-xl text-blue-500 mr-4"></i> |
|
|
<div> |
|
|
<p class="text-sm text-gray-500">Всего оборудования</p> |
|
|
<h3 class="text-2xl font-bold">{activeCount}</h3> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="bg-white rounded-lg shadow p-6 text-gray-800"> |
|
|
<div class="flex items-center"> |
|
|
<i class="fas fa-check-circle text-xl text-green-500 mr-4"></i> |
|
|
<div> |
|
|
<p class="text-sm text-gray-500">Выполнено ТО</p> |
|
|
<h3 class="text-2xl font-bold">{completedCount}</h3> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="bg-white rounded-lg shadow p-6 text-gray-800"> |
|
|
<div class="flex items-center"> |
|
|
<i class="fas fa-exclamation-triangle text-xl text-yellow-500 mr-4"></i> |
|
|
<div> |
|
|
<p class="text-sm text-gray-500">Просрочено ТО</p> |
|
|
<h3 class="text-2xl font-bold">{overdueCount}</h3> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="bg-white rounded-lg shadow p-6 text-gray-800"> |
|
|
<div class="flex items-center"> |
|
|
<i class="fas fa-wrench text-xl text-red-500 mr-4"></i> |
|
|
<div> |
|
|
<p class="text-sm text-gray-500">Текущие ремонты</p> |
|
|
<h3 class="text-2xl font-bold">{repairCount}</h3> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow overflow-hidden"> |
|
|
<div class="p-4 border-b flex justify-between items-center"> |
|
|
<h2 class="text-lg font-semibold">Оборудование</h2> |
|
|
<button onClick={() => setActiveTab("equipment")} 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="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">ID</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> |
|
|
<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"> |
|
|
{equipmentData.map(eq => ( |
|
|
<tr key={eq.id}> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{eq.id}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{eq.name}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{eq.type}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> |
|
|
<span class={`px-2 py-1 rounded-full text-xs font-semibold ${ |
|
|
eq.status === "active" ? "bg-green-100 text-green-800" : |
|
|
eq.status === "maintenance" ? "bg-yellow-100 text-yellow-800" : "bg-red-100 text-red-800" |
|
|
}`}> |
|
|
{eq.status === "active" ? "Активно" : eq.status === "maintenance" ? "ТО" : "Ремонт"} |
|
|
</span> |
|
|
</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{formatDate(eq.lastMaintenance)}</td> |
|
|
</tr> |
|
|
))} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
function Equipment({ setEquipmentList, setActiveTab }) { |
|
|
const [showModal, setShowModal] = React.useState(false); |
|
|
const [newEq, setNewEq] = React.useState({ |
|
|
name: "", |
|
|
type: "", |
|
|
serial: "", |
|
|
inventory: "", |
|
|
commissionDate: "", |
|
|
maintenanceInterval: 30, |
|
|
description: "" |
|
|
}); |
|
|
|
|
|
const handleAdd = () => { |
|
|
if (!newEq.name || !newEq.type || !newEq.serial || !newEq.inventory || !newEq.commissionDate) { |
|
|
alert("Заполните все обязательные поля"); |
|
|
return; |
|
|
} |
|
|
|
|
|
const id = equipmentData.length > 0 ? Math.max(...equipmentData.map(e => e.id)) + 1 : 1; |
|
|
equipmentData.push({ |
|
|
...newEq, |
|
|
id, |
|
|
status: "active", |
|
|
nextMaintenance: calculateNextMaintenanceDate(newEq.commissionDate, newEq.maintenanceInterval) |
|
|
}); |
|
|
setEquipmentList([...equipmentData]); |
|
|
setShowModal(false); |
|
|
alert("Оборудование добавлено"); |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div class="p-6"> |
|
|
<h2 class="text-2xl font-bold mb-6">Оборудование</h2> |
|
|
<div class="bg-white rounded-lg shadow overflow-hidden"> |
|
|
<div class="p-4 border-b flex justify-between items-center"> |
|
|
<h3 class="text-lg font-semibold">Список оборудования</h3> |
|
|
<button onClick={() => setShowModal(true)} 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="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">ID</th> |
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Название</th> |
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Тип</th> |
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Статус</th> |
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Последнее ТО</th> |
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Действия</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody class="bg-white divide-y divide-gray-200"> |
|
|
{equipmentData.map(eq => ( |
|
|
<tr key={eq.id}> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{eq.id}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{eq.name}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{eq.type}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> |
|
|
<span class={`px-2 py-1 rounded-full text-xs font-semibold ${ |
|
|
eq.status === "active" ? "bg-green-100 text-green-800" : |
|
|
eq.status === "maintenance" ? "bg-yellow-100 text-yellow-800" : "bg-red-100 text-red-800" |
|
|
}`}> |
|
|
{eq.status === "active" ? "Активно" : eq.status === "maintenance" ? "ТО" : "Ремонт"} |
|
|
</span> |
|
|
</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{formatDate(eq.lastMaintenance)}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> |
|
|
<button class="text-blue-600 hover:text-blue-900 mr-3"><i class="fas fa-edit"></i></button> |
|
|
<button class="text-red-600 hover:text-red-900"><i class="fas fa-trash"></i></button> |
|
|
</td> |
|
|
</tr> |
|
|
))} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{showModal && ( |
|
|
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> |
|
|
<div class="bg-white rounded-lg w-full max-w-md p-6"> |
|
|
<div class="flex justify-between items-center mb-4"> |
|
|
<h3 class="text-lg font-semibold">Добавить оборудование</h3> |
|
|
<button onClick={() => setShowModal(false)} class="text-gray-500">×</button> |
|
|
</div> |
|
|
<form class="space-y-4"> |
|
|
<input value={newEq.name} onInput={(e) => setNewEq({...newEq, name: e.target.value})} placeholder="Название" required class="w-full px-4 py-2 border rounded-lg"/> |
|
|
<select value={newEq.type} onChange={(e) => setNewEq({...newEq, type: e.target.value})} required class="w-full px-4 py-2 border rounded-lg"> |
|
|
<option value="">Выберите тип</option> |
|
|
<option>Станок</option> |
|
|
<option>Конвейер</option> |
|
|
<option>Компрессор</option> |
|
|
<option>Генератор</option> |
|
|
<option>Насос</option> |
|
|
</select> |
|
|
<input value={newEq.serial} onInput={(e) => setNewEq({...newEq, serial: e.target.value})} placeholder="Серийный номер" required class="w-full px-4 py-2 border rounded-lg"/> |
|
|
<input value={newEq.inventory} onInput={(e) => setNewEq({...newEq, inventory: e.target.value})} placeholder="Инвентарный номер" required class="w-full px-4 py-2 border rounded-lg"/> |
|
|
<input value={newEq.commissionDate} type="date" onInput={(e) => setNewEq({...newEq, commissionDate: e.target.value})} required class="w-full px-4 py-2 border rounded-lg"/> |
|
|
<input value={newEq.maintenanceInterval} type="number" min="1" onInput={(e) => setNewEq({...newEq, maintenanceInterval: e.target.value})} placeholder="Интервал ТО (дни)" required class="w-full px-4 py-2 border rounded-lg"/> |
|
|
<textarea value={newEq.description} onInput={(e) => setNewEq({...newEq, description: e.target.value})} rows="3" placeholder="Описание" class="w-full px-4 py-2 border rounded-lg"></textarea> |
|
|
</form> |
|
|
<div class="mt-4 flex justify-end space-x-2"> |
|
|
<button onClick={() => setShowModal(false)} class="px-4 py-2 border rounded-lg hover:bg-gray-100">Отмена</button> |
|
|
<button onClick={handleAdd} class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Сохранить</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
function Schedule() { |
|
|
return ( |
|
|
<div class="p-6"> |
|
|
<h2 class="text-2xl font-bold mb-6">График ТО</h2> |
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
|
|
<div class="lg:col-span-2 bg-white rounded-lg shadow p-4"> |
|
|
<h3 class="text-lg font-semibold mb-4">Календарь ТО</h3> |
|
|
<div class="flex justify-between items-center mb-4"> |
|
|
<h4 class="font-medium">Июнь 2023</h4> |
|
|
<div class="flex space-x-2"> |
|
|
<button class="p-2 rounded-full hover:bg-gray-100"><i class="fas fa-chevron-left"></i></button> |
|
|
<button class="p-2 rounded-full hover:bg-gray-100"><i class="fas fa-chevron-right"></i></button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="grid grid-cols-7 gap-1 mb-2"> |
|
|
{['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].map(day => ( |
|
|
<div class="text-center text-xs font-medium text-gray-500">{day}</div> |
|
|
))} |
|
|
</div> |
|
|
<div class="grid grid-cols-7 gap-1"> |
|
|
{[...Array(35)].map((_, i) => { |
|
|
const day = i - 4; |
|
|
const hasEvent = maintenanceEvents.some(event => new Date(event.date).getDate() === day && new Date(event.date).getMonth() === 5); |
|
|
return ( |
|
|
<div class={`h-10 flex items-center justify-center border rounded ${hasEvent ? 'bg-blue-50' : ''}>{day > 0 && day <= 30 ? day : ''}</div> |
|
|
); |
|
|
})} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
function Users() { |
|
|
return ( |
|
|
<div class="p-6"> |
|
|
<h2 class="text-2xl font-bold mb-6">Пользователи</h2> |
|
|
<div class="bg-white rounded-lg shadow overflow-hidden"> |
|
|
<div class="p-4 border-b"> |
|
|
<h3 class="text-lg font-semibold">Список пользователей</h3> |
|
|
</div> |
|
|
<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">Имя</th> |
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th> |
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Роль</th> |
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Действия</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody class="bg-white divide-y divide-gray-200"> |
|
|
{users.map(user => ( |
|
|
<tr key={user.id}> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{user.name}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{user.email}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> |
|
|
<span class={`px-2 py-1 rounded-full text-xs font-semibold ${ |
|
|
user.role === "admin" ? "bg-purple-100 text-purple-800" : |
|
|
user.role === "engineer" ? "bg-blue-100 text-blue-800" : "bg-gray-100 text-gray-800" |
|
|
}`}> |
|
|
{user.role === "admin" ? "Администратор" : user.role === "engineer" ? "Инженер" : "Просмотр"} |
|
|
</span> |
|
|
</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> |
|
|
<button class="text-blue-600 hover:text-blue-900 mr-3"><i class="fas fa-edit"></i></button> |
|
|
<button class="text-red-600 hover:text-red-900"><i class="fas fa-trash"></i></button> |
|
|
</td> |
|
|
</tr> |
|
|
))} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
function App() { |
|
|
const [activeTab, setActiveTab] = React.useState("dashboard"); |
|
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = React.useState(false); |
|
|
|
|
|
const renderContent = () => { |
|
|
switch (activeTab) { |
|
|
case "dashboard": return <Dashboard setActiveTab={setActiveTab}/>; |
|
|
case "schedule": return <Schedule />; |
|
|
case "equipment": return <Equipment setEquipmentList={setActiveTab} setActiveTab={setActiveTab}/>; |
|
|
case "users": return <Users />; |
|
|
default: return <Dashboard setActiveTab={setActiveTab}/>; |
|
|
} |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div class="flex h-screen overflow-hidden bg-gray-50"> |
|
|
{/* Sidebar */} |
|
|
<div class={`sidebar bg-blue-800 text-white transition-all duration-300 ${isSidebarCollapsed ? 'collapsed w-16' : 'w-64'} flex flex-col`}> |
|
|
<div class="p-4 flex items-center justify-between border-b border-blue-700"> |
|
|
<div class="flex items-center"> |
|
|
<i class="fas fa-cogs text-2xl mr-3"></i> |
|
|
{!isSidebarCollapsed && <span class="text-xl font-bold">MES System</span>} |
|
|
</div> |
|
|
<button onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)} class="hover:text-blue-200"> |
|
|
<i class="fas fa-bars"></i> |
|
|
</button> |
|
|
</div> |
|
|
<nav class="p-4 flex-1 overflow-y-auto"> |
|
|
<div class="mb-6"> |
|
|
<p class="uppercase text-xs font-semibold text-blue-300 mb-2">Основное</p> |
|
|
<button onClick={() => setActiveTab("dashboard")} class={`flex items-center py-2 px-3 rounded-lg w-full text-left ${activeTab === "dashboard" ? "bg-blue-700" : "hover:bg-blue-700"} mb-2`}> |
|
|
<i class="fas fa-tachometer-alt mr-3"></i> |
|
|
{!isSidebarCollapsed && <span>Главная</span>} |
|
|
</button> |
|
|
<button onClick={() => setActiveTab("schedule")} class={`flex items-center py-2 px-3 rounded-lg w-full text-left ${activeTab === "schedule" ? "bg-blue-700" : "hover:bg-blue-700"} mb-2`}> |
|
|
<i class="fas fa-calendar-alt mr-3"></i> |
|
|
{!isSidebarCollapsed && <span>График ТО</span>} |
|
|
</button> |
|
|
<button onClick={() => setActiveTab("equipment")} class={`flex items-center py-2 px-3 rounded-lg w-full text-left ${activeTab === "equipment" ? "bg-blue-700" : "hover:bg-blue-700"} mb-2`}> |
|
|
<i class="fas fa-tools mr-3"></i> |
|
|
{!isSidebarCollapsed && <span>Оборудование</span>} |
|
|
</button> |
|
|
</div> |
|
|
<div class="mb-6"> |
|
|
<p class="uppercase text-xs font-semibold text-blue-300 mb-2">Настройки</p> |
|
|
<button onClick={() => setActiveTab("users")} class={`flex items-center py-2 px-3 rounded-lg w-full text-left ${activeTab === "users" ? "bg-blue-700" : "hover:bg-blue-700"} mb-2`}> |
|
|
<i class="fas fa-users-cog mr-3"></i> |
|
|
{!isSidebarCollapsed && <span>Пользователи</span>} |
|
|
</button> |
|
|
</div> |
|
|
</nav> |
|
|
<div class="p-4 border-t border-blue-700 flex items-center"> |
|
|
<img src="https://placehold.co/40x40?text=User " alt="User" class="w-10 h-10 rounded-full mr-3"/> |
|
|
{!isSidebarCollapsed && ( |
|
|
<div> |
|
|
<p class="font-medium">Иван Петров</p> |
|
|
<p class="text-xs text-blue-300">Администратор</p> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* Main Content */} |
|
|
<div class="main-content flex-1 overflow-auto"> |
|
|
<header class="bg-white shadow-sm py-4 px-6 flex justify-between items-center"> |
|
|
<h1 class="text-2xl font-bold text-gray-800"> |
|
|
{activeTab === "dashboard" && "Главная панель"} |
|
|
{activeTab === "schedule" && "График ТО"} |
|
|
{activeTab === "equipment" && "Оборудование"} |
|
|
{activeTab === "users" && "Пользователи"} |
|
|
</h1> |
|
|
<div class="flex items-center space-x-4"> |
|
|
<button class="p-2 rounded-full hover:bg-gray-100 relative"> |
|
|
<i class="fas fa-bell text-gray-600"></i> |
|
|
<span class="notification-badge bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center absolute top-0 right-0">5</span> |
|
|
</button> |
|
|
<div class="relative"> |
|
|
<input type="text" placeholder="Поиск..." class="pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"/> |
|
|
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> |
|
|
</div> |
|
|
</div> |
|
|
</header> |
|
|
<div class="p-6"> |
|
|
{renderContent()} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
root.render(<App />); |
|
|
</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/bad1" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
|
</html> |
|
|
|