tos / index.html
arkleinberg's picture
Add 3 files
fc2fbc5 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Santo Domingo Port Management 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">
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
#viewport-3d {
position: absolute;
width: 100%;
height: 100%;
}
.overlay {
position: absolute;
background: rgba(15, 23, 42, 0.85);
backdrop-filter: blur(10px);
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
color: white;
z-index: 100;
border: 1px solid rgba(56, 189, 248, 0.3);
}
.side-right {
right: 20px;
top: 50%;
transform: translateY(-50%);
padding: 15px 10px;
}
.top-right {
right: 20px;
top: 20px;
padding: 12px 20px;
}
.bottom {
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 40%;
min-width: 400px;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
object-fit: cover;
border: 2px solid #38bdf8;
}
#chat-messages {
height: 200px;
overflow-y: auto;
padding: 10px;
margin-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.chat-message {
margin-bottom: 10px;
padding: 8px 12px;
border-radius: 8px;
max-width: 80%;
}
.user-message {
background: rgba(56, 189, 248, 0.2);
margin-left: auto;
border-top-right-radius: 0;
}
.ai-message {
background: rgba(30, 41, 59, 0.8);
margin-right: auto;
border-top-left-radius: 0;
}
.menu-btn {
width: 50px;
height: 50px;
margin: 10px 0;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
position: relative;
}
.menu-btn:hover {
background: rgba(56, 189, 248, 0.3);
transform: translateY(-3px);
}
.menu-btn.active {
background: rgba(56, 189, 248, 0.5);
}
.menu-btn::after {
content: attr(title);
position: absolute;
left: -150px;
background: rgba(15, 23, 42, 0.9);
padding: 5px 10px;
border-radius: 5px;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
width: 140px;
text-align: right;
}
.menu-btn:hover::after {
opacity: 1;
}
#minimap {
width: 220px;
height: 220px;
overflow: hidden;
border: 2px solid #38bdf8;
}
#minimap img {
width: 100%;
height: 100%;
object-fit: cover;
}
#context-menu {
display: none;
width: 300px;
padding: 15px;
}
.context-item {
padding: 8px 12px;
border-radius: 6px;
margin-bottom: 5px;
cursor: pointer;
transition: background 0.2s;
}
.context-item:hover {
background: rgba(56, 189, 248, 0.3);
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(56, 189, 248, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(56, 189, 248, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(56, 189, 248, 0);
}
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.status-open {
background-color: #10b981;
}
.status-closed {
background-color: #ef4444;
}
.status-warning {
background-color: #f59e0b;
}
.progress-bar {
height: 8px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 4px;
margin-top: 5px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #38bdf8;
border-radius: 4px;
transition: width 0.3s ease;
}
.vessel-marker {
position: absolute;
width: 12px;
height: 12px;
background-color: #38bdf8;
border-radius: 50%;
border: 2px solid white;
transform: translate(-50%, -50%);
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Minimap position indicators */
.minimap-indicator {
position: absolute;
width: 6px;
height: 6px;
background-color: #38bdf8;
border-radius: 50%;
transform: translate(-50%, -50%);
}
.camera-indicator {
position: absolute;
width: 10px;
height: 10px;
background-color: #ef4444;
border-radius: 50%;
transform: translate(-50%, -50%);
}
.camera-direction {
position: absolute;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 12px solid #ef4444;
transform: translate(-50%, -50%) rotate(var(--rotation));
}
</style>
</head>
<body class="bg-slate-900 text-white overflow-hidden">
<!-- 3D Viewport Container -->
<div id="viewport-3d"></div>
<!-- Minimap -->
<div id="minimap" class="overlay top-right">
<img src="https://maps.googleapis.com/maps/api/staticmap?center=18.4719,-69.8923&zoom=15&size=400x400&maptype=satellite&key=YOUR_API_KEY" alt="Santo Domingo Port Map">
<div class="absolute bottom-2 right-2 bg-black bg-opacity-70 px-2 py-1 rounded text-xs">
<span class="text-blue-300">Live View</span>
</div>
<!-- Vessel markers will be added here by JS -->
</div>
<!-- Main Menu -->
<nav id="main-menu" class="overlay side-right">
<ul>
<li>
<button id="btn-berth" class="menu-btn pulse" title="Gestion des postes à quai">
<i class="fas fa-anchor text-2xl text-blue-300"></i>
</button>
</li>
<li>
<button id="btn-yard" class="menu-btn" title="Gestion du yard">
<i class="fas fa-boxes text-2xl text-blue-300"></i>
</button>
</li>
<li>
<button id="btn-gate" class="menu-btn" title="Gestion des accès">
<i class="fas fa-truck-moving text-2xl text-blue-300"></i>
</button>
</li>
<li>
<button id="btn-schedule" class="menu-btn" title="Planification équipements/personnel">
<i class="fas fa-calendar-alt text-2xl text-blue-300"></i>
</button>
</li>
<li>
<button id="btn-billing" class="menu-btn" title="Facturation & rapports">
<i class="fas fa-file-invoice-dollar text-2xl text-blue-300"></i>
</button>
</li>
</ul>
</nav>
<!-- AI Chat Panel -->
<div id="chat-panel" class="overlay bottom">
<div id="chat-header" class="flex items-center justify-between px-4 py-2 border-b border-slate-700">
<h3 class="font-semibold text-blue-300 flex items-center">
<img src="https://via.placeholder.com/24/38bdf8/ffffff?text=M2H" alt="M2H Majestic" class="mr-2 rounded">
M2H Majestic Assistant
</h3>
<button id="minimize-chat" class="text-slate-400 hover:text-white">
<i class="fas fa-minus"></i>
</button>
</div>
<div id="chat-messages">
<div class="chat-message ai-message">
<p>Bonjour! Je suis votre assistant M2H Majestic pour la gestion du port de Santo Domingo. Comment puis-je vous aider aujourd'hui?</p>
<p class="text-xs text-slate-400 mt-1">Maintenant</p>
</div>
<div class="chat-message ai-message">
<p>Je peux vous fournir des informations sur les navires en approche, l'état des quais, la capacité du yard et plus encore.</p>
<p class="text-xs text-slate-400 mt-1">Maintenant</p>
</div>
</div>
<div id="chat-input" class="flex p-3">
<input type="text" placeholder="Demandez des informations sur les navires, les quais..."
class="flex-1 bg-slate-800 rounded-l-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<button id="send-btn" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-r-lg transition">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
<!-- User Info -->
<div id="user-info" class="overlay top-right flex items-center">
<img src="https://randomuser.me/api/portraits/men/32.jpg" alt="Avatar" class="avatar">
<div>
<p class="font-medium">Jean Dupont</p>
<p class="text-xs text-blue-300">Yield Manager</p>
</div>
<button class="ml-4 text-slate-400 hover:text-white">
<i class="fas fa-cog"></i>
</button>
</div>
<!-- Context Menu (hidden by default) -->
<div id="context-menu" class="overlay">
<h3 class="font-semibold mb-3 text-blue-300 border-b border-slate-700 pb-2 flex items-center">
<i class="fas fa-info-circle mr-2"></i>
<span id="context-title">Quai 1 - Terminal Santo Domingo</span>
</h3>
<div class="mb-3">
<div class="flex items-center">
<span class="status-indicator status-open"></span>
<span>Statut: <strong>Occupé</strong></span>
</div>
<div class="mt-1">
<span>Navire: <strong>MSC Caribe</strong> (Porte-conteneurs)</span>
</div>
<div class="mt-1">
<span>ETA: <strong>14:30</strong> (dans 2h15)</span>
</div>
</div>
<div class="mb-3">
<div class="flex justify-between text-sm">
<span>Progression déchargement:</span>
<span>65%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 65%"></div>
</div>
</div>
<div class="context-item flex items-center">
<i class="fas fa-info-circle mr-3 text-blue-300"></i>
<span>Voir les détails complets</span>
</div>
<div class="context-item flex items-center">
<i class="fas fa-calendar-check mr-3 text-blue-300"></i>
<span>Planifier l'accostage</span>
</div>
<div class="context-item flex items-center">
<i class="fas fa-truck-loading mr-3 text-blue-300"></i>
<span>Assigner des grues</span>
</div>
<div class="context-item flex items-center">
<i class="fas fa-file-invoice mr-3 text-blue-300"></i>
<span>Générer un rapport</span>
</div>
</div>
<!-- Notification Toast -->
<div id="notification" class="fixed top-5 left-1/2 transform -translate-x-1/2 bg-green-600 text-white px-6 py-3 rounded-lg shadow-lg hidden flex items-center">
<i class="fas fa-check-circle mr-2"></i>
<span>Berth allocation updated successfully!</span>
</div>
<script>
// Initialize Three.js scene
let scene, camera, renderer, controls;
let portObjects = {};
let selectedObject = null;
function initThreeJS() {
// Create scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x1e3c72);
scene.fog = new THREE.FogExp2(0x1e3c72, 0.001);
// Create camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 150, 300);
// Create renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.getElementById('viewport-3d').appendChild(renderer.domElement);
// Add controls
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 50;
controls.maxDistance = 500;
controls.maxPolarAngle = Math.PI * 0.9;
// Add lights
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(100, 300, 100);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
// Add water
const waterGeometry = new THREE.PlaneGeometry(1000, 1000);
const waterMaterial = new THREE.MeshStandardMaterial({
color: 0x1e88e5,
roughness: 0.1,
metalness: 0.5
});
const water = new THREE.Mesh(waterGeometry, waterMaterial);
water.rotation.x = -Math.PI / 2;
water.position.y = -5;
scene.add(water);
// Add ground
const groundGeometry = new THREE.PlaneGeometry(1000, 1000);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x4b5563,
roughness: 0.8
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -5;
ground.receiveShadow = true;
scene.add(ground);
// Create port infrastructure
createPortInfrastructure();
// Handle window resize
window.addEventListener('resize', onWindowResize);
// Start animation loop
animate();
}
function createPortInfrastructure() {
// Create terminals (Santo Domingo, Don Diego, Sans Souci)
createTerminal('Santo Domingo', -200, 0, 0, 300, 200, [
{ id: 1, length: 150, width: 30, occupied: true },
{ id: 2, length: 180, width: 30, occupied: false },
{ id: 3, length: 160, width: 30, occupied: true },
{ id: 4, length: 140, width: 30, occupied: false },
{ id: 5, length: 170, width: 30, occupied: true }
]);
createTerminal('Don Diego', 200, 0, 0, 250, 150, [
{ id: 6, length: 120, width: 25, occupied: false },
{ id: 7, length: 130, width: 25, occupied: true },
{ id: 8, length: 140, width: 25, occupied: false }
]);
createTerminal('Sans Souci', 0, 0, -200, 250, 250, [
{ id: 9, length: 250, width: 35, occupied: true }
]);
// Create storage areas
createStorageArea('Vehicle Storage', -100, 0, 150, 200, 150, 7000);
createStorageArea('Warehouse A', -300, 0, 50, 100, 80);
createStorageArea('Warehouse B', -350, 0, -50, 100, 80);
// Create gates
createGate('Main Gate', -400, 0, 0);
createGate('Truck Gate', -450, 0, 100);
createGate('Secondary Gate', -450, 0, -100);
}
function createTerminal(name, x, y, z, width, depth, berths) {
// Create terminal base
const terminalGeometry = new THREE.BoxGeometry(width, 5, depth);
const terminalMaterial = new THREE.MeshStandardMaterial({
color: 0x6b7280,
roughness: 0.7
});
const terminal = new THREE.Mesh(terminalGeometry, terminalMaterial);
terminal.position.set(x, y - 2.5, z);
terminal.receiveShadow = true;
terminal.userData = { type: 'terminal', name: name };
scene.add(terminal);
portObjects[`terminal_${name}`] = terminal;
// Create berths
berths.forEach((berth, index) => {
const berthX = x + (index - (berths.length - 1) / 2) * (width / berths.length);
const berthZ = z + depth / 2;
const berthGeometry = new THREE.BoxGeometry(berth.length, 10, berth.width);
const berthMaterial = new THREE.MeshStandardMaterial({
color: berth.occupied ? 0xef4444 : 0x10b981,
roughness: 0.6
});
const berthMesh = new THREE.Mesh(berthGeometry, berthMaterial);
berthMesh.position.set(berthX, y, berthZ);
berthMesh.userData = {
type: 'berth',
id: berth.id,
terminal: name,
occupied: berth.occupied,
vessel: berth.occupied ? `Vessel ${Math.floor(Math.random() * 1000)}` : null
};
scene.add(berthMesh);
portObjects[`berth_${name}_${berth.id}`] = berthMesh;
// Add vessel if occupied
if (berth.occupied) {
const vesselGeometry = new THREE.BoxGeometry(berth.length * 0.8, 20, berth.width * 2);
const vesselMaterial = new THREE.MeshStandardMaterial({
color: 0x3b82f6,
roughness: 0.5
});
const vessel = new THREE.Mesh(vesselGeometry, vesselMaterial);
vessel.position.set(berthX, y + 15, berthZ + berth.width);
vessel.userData = { type: 'vessel', berth: berth.id };
scene.add(vessel);
portObjects[`vessel_${name}_${berth.id}`] = vessel;
}
});
// Add terminal building
const buildingGeometry = new THREE.BoxGeometry(width * 0.6, 30, depth * 0.3);
const buildingMaterial = new THREE.MeshStandardMaterial({
color: 0x9ca3af,
roughness: 0.8
});
const building = new THREE.Mesh(buildingGeometry, buildingMaterial);
building.position.set(x, y + 15, z - depth * 0.35);
building.castShadow = true;
scene.add(building);
}
function createStorageArea(name, x, y, z, width, depth, capacity) {
const areaGeometry = new THREE.BoxGeometry(width, 5, depth);
const areaMaterial = new THREE.MeshStandardMaterial({
color: 0x4b5563,
roughness: 0.8
});
const area = new THREE.Mesh(areaGeometry, areaMaterial);
area.position.set(x, y - 2.5, z);
area.userData = { type: 'storage', name: name, capacity: capacity };
scene.add(area);
portObjects[`storage_${name}`] = area;
// Add containers if it's a container yard
if (name.includes('Vehicle')) {
for (let i = 0; i < 20; i++) {
const containerX = x + (Math.random() - 0.5) * width * 0.8;
const containerZ = z + (Math.random() - 0.5) * depth * 0.8;
const containerGeometry = new THREE.BoxGeometry(5, 3, 2);
const containerMaterial = new THREE.MeshStandardMaterial({
color: 0x3b82f6,
roughness: 0.7
});
const container = new THREE.Mesh(containerGeometry, containerMaterial);
container.position.set(containerX, y + 1.5, containerZ);
container.rotation.y = Math.random() * Math.PI;
scene.add(container);
}
}
}
function createGate(name, x, y, z) {
const gateGeometry = new THREE.BoxGeometry(10, 20, 5);
const gateMaterial = new THREE.MeshStandardMaterial({
color: 0xf59e0b,
roughness: 0.6
});
const gate = new THREE.Mesh(gateGeometry, gateMaterial);
gate.position.set(x, y + 10, z);
gate.userData = { type: 'gate', name: name, status: Math.random() > 0.5 ? 'open' : 'closed' };
scene.add(gate);
portObjects[`gate_${name}`] = gate;
// Add road
const roadGeometry = new THREE.PlaneGeometry(30, 100);
const roadMaterial = new THREE.MeshStandardMaterial({
color: 0x6b7280,
roughness: 0.9
});
const road = new THREE.Mesh(roadGeometry, roadMaterial);
road.rotation.x = -Math.PI / 2;
road.position.set(x, y, z);
scene.add(road);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
// Update minimap indicators
updateMinimap();
}
function updateMinimap() {
// Clear existing indicators
document.querySelectorAll('.vessel-marker, .minimap-indicator, .camera-indicator, .camera-direction').forEach(el => el.remove());
// Add vessel markers
Object.keys(portObjects).forEach(key => {
const obj = portObjects[key];
if (obj.userData.type === 'vessel' || obj.userData.type === 'gate') {
const position = obj.position.clone().project(camera);
// Convert normalized device coordinates to minimap coordinates
const x = ((position.x * 0.5 + 0.5) * 220) - 110;
const y = ((-position.y * 0.5 + 0.5) * 220) - 110;
if (x >= 0 && x <= 220 && y >= 0 && y <= 220) {
const marker = document.createElement('div');
marker.className = obj.userData.type === 'vessel' ? 'vessel-marker' : 'minimap-indicator';
marker.style.left = `${x}px`;
marker.style.top = `${y}px`;
document.getElementById('minimap').appendChild(marker);
}
}
});
// Add camera indicator
const cameraPosition = camera.position.clone().project(camera);
const camX = ((cameraPosition.x * 0.5 + 0.5) * 220) - 110;
const camY = ((-cameraPosition.y * 0.5 + 0.5) * 220) - 110;
if (camX >= 0 && camX <= 220 && camY >= 0 && camY <= 220) {
const camMarker = document.createElement('div');
camMarker.className = 'camera-indicator';
camMarker.style.left = `${camX}px`;
camMarker.style.top = `${camY}px`;
document.getElementById('minimap').appendChild(camMarker);
// Add camera direction indicator
const direction = document.createElement('div');
direction.className = 'camera-direction';
direction.style.left = `${camX}px`;
direction.style.top = `${camY}px`;
// Calculate rotation based on camera direction
const target = new THREE.Vector3(0, 0, -1);
target.applyQuaternion(camera.quaternion);
const angle = Math.atan2(target.x, target.z);
direction.style.setProperty('--rotation', `${angle}rad`);
document.getElementById('minimap').appendChild(direction);
}
}
// Initialize Three.js when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
initThreeJS();
// Set up raycasting for object selection
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseClick(event) {
// Calculate mouse position in normalized device coordinates
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// Update the raycaster
raycaster.setFromCamera(mouse, camera);
// Calculate objects intersecting the ray
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
const object = intersects[0].object;
// Check if it's a port object we care about
if (object.userData && (object.userData.type === 'berth' || object.userData.type === 'gate')) {
selectedObject = object;
showContextMenu(object, event.clientX, event.clientY);
// Move camera to focus on the object
moveCameraToObject(object);
}
} else {
// Clicked on empty space, hide context menu
document.getElementById('context-menu').style.display = 'none';
}
}
function moveCameraToObject(object) {
// Calculate target position for camera
const targetPosition = object.position.clone();
targetPosition.y += 50; // Elevate camera
targetPosition.z -= 100; // Move camera back
// Animate camera movement
const duration = 1000; // ms
const startPosition = camera.position.clone();
const startTime = Date.now();
function animateCamera() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
camera.position.lerpVectors(startPosition, targetPosition, progress);
if (progress < 1) {
requestAnimationFrame(animateCamera);
} else {
// Camera arrived, update controls target
controls.target.copy(object.position);
}
}
animateCamera();
}
function showContextMenu(object, x, y) {
const contextMenu = document.getElementById('context-menu');
const title = document.getElementById('context-title');
if (object.userData.type === 'berth') {
title.innerHTML = `<i class="fas fa-anchor mr-2"></i>Quai ${object.userData.id} - Terminal ${object.userData.terminal}`;
// Update status indicator
const statusIndicator = contextMenu.querySelector('.status-indicator');
statusIndicator.className = 'status-indicator ' +
(object.userData.occupied ? 'status-open' : 'status-closed');
// Update status text
contextMenu.querySelector('.status-indicator').nextElementSibling.innerHTML =
`Statut: <strong>${object.userData.occupied ? 'Occupé' : 'Libre'}</strong>`;
// Update vessel info if occupied
if (object.userData.occupied) {
contextMenu.querySelector('.mt-1 > span').innerHTML =
`Navire: <strong>${object.userData.vessel}</strong> (Porte-conteneurs)`;
} else {
contextMenu.querySelector('.mt-1 > span').innerHTML =
`Navire: <strong>Aucun</strong>`;
}
// Random ETA for demo
const hours = Math.floor(Math.random() * 24);
const minutes = Math.floor(Math.random() * 60);
const etaTime = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
contextMenu.querySelectorAll('.mt-1')[1].innerHTML =
`ETA: <strong>${etaTime}</strong> (dans ${Math.floor(Math.random() * 6) + 1}h${Math.floor(Math.random() * 60)})`;
// Random progress for demo
const progress = object.userData.occupied ? Math.floor(Math.random() * 100) : 0;
contextMenu.querySelector('.progress-fill').style.width = `${progress}%`;
contextMenu.querySelectorAll('.text-sm span')[1].textContent = `${progress}%`;
} else if (object.userData.type === 'gate') {
title.innerHTML = `<i class="fas fa-truck-moving mr-2"></i>${object.userData.name}`;
// Update status indicator
const statusIndicator = contextMenu.querySelector('.status-indicator');
statusIndicator.className = 'status-indicator ' +
(object.userData.status === 'open' ? 'status-open' : 'status-closed');
// Update status text
contextMenu.querySelector('.status-indicator').nextElementSibling.innerHTML =
`Statut: <strong>${object.userData.status === 'open' ? 'Ouvert' : 'Fermé'}</strong>`;
// Update other info
contextMenu.querySelector('.mt-1 > span').innerHTML =
`Trafic: <strong>${Math.floor(Math.random() * 100)} camions/h</strong>`;
contextMenu.querySelectorAll('.mt-1')[1].innerHTML =
`Prochain: <strong>Camion ${Math.floor(Math.random() * 1000)}</strong>`;
// For gates, we'll show queue progress instead of loading
const queue = Math.floor(Math.random() * 100);
contextMenu.querySelector('.progress-fill').style.width = `${queue}%`;
contextMenu.querySelectorAll('.text-sm span')[1].textContent = `${queue}%`;
contextMenu.querySelectorAll('.text-sm span')[0].textContent = 'File d\'attente:';
}
// Position and show the context menu
contextMenu.style.left = `${x}px`;
contextMenu.style.top = `${y}px`;
contextMenu.style.display = 'block';
}
// Add click event listener
window.addEventListener('click', onMouseClick);
// Menu button interactions
const menuButtons = document.querySelectorAll('#main-menu .menu-btn');
menuButtons.forEach(btn => {
btn.addEventListener('click', function() {
menuButtons.forEach(b => b.classList.remove('active'));
this.classList.add('active');
// Show notification
const notification = document.getElementById('notification');
notification.textContent = `${this.getAttribute('title')} panel opened`;
notification.classList.remove('hidden');
notification.classList.remove('bg-green-600', 'bg-red-600', 'bg-blue-600');
// Change color based on which button was clicked
if (this.id === 'btn-billing' || this.id === 'btn-schedule') {
notification.classList.add('bg-blue-600');
} else if (this.id === 'btn-gate') {
notification.classList.add('bg-yellow-600');
} else {
notification.classList.add('bg-green-600');
}
setTimeout(() => {
notification.classList.add('hidden');
}, 3000);
});
});
// Simulate chat interaction
document.getElementById('send-btn').addEventListener('click', sendMessage);
document.querySelector('#chat-input input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') sendMessage();
});
function sendMessage() {
const input = document.querySelector('#chat-input input');
const message = input.value.trim();
if (message) {
// Add user message
const chatContainer = document.getElementById('chat-messages');
const userMsg = document.createElement('div');
userMsg.className = 'chat-message user-message';
userMsg.innerHTML = `
<p>${message}</p>
<p class="text-xs text-slate-400 mt-1">Maintenant</p>
`;
chatContainer.appendChild(userMsg);
// Clear input
input.value = '';
// Scroll to bottom
chatContainer.scrollTop = chatContainer.scrollHeight;
// Simulate AI response after a delay
setTimeout(() => {
const responses = [
"J'ai vérifié le système. Il y a 3 postes à quai disponibles qui peuvent accueillir ce navire.",
"Le yard a actuellement une capacité restante de 45% dans la Zone C.",
"La porte 2 connaît des retards en raison d'un trafic accru. Envisagez de rediriger vers la porte 3.",
"J'ai programmé 2 grues pour le navire MSC Oscar, comme demandé.",
"Le rapport de facturation pour le Q3 a été généré et envoyé à votre email."
];
const aiMsg = document.createElement('div');
aiMsg.className = 'chat-message ai-message';
aiMsg.innerHTML = `
<p>${responses[Math.floor(Math.random() * responses.length)]}</p>
<p class="text-xs text-slate-400 mt-1">Maintenant</p>
`;
chatContainer.appendChild(aiMsg);
chatContainer.scrollTop = chatContainer.scrollHeight;
}, 1000 + Math.random() * 2000);
}
}
// Minimize chat
document.getElementById('minimize-chat').addEventListener('click', function() {
const chatPanel = document.getElementById('chat-panel');
const messages = document.getElementById('chat-messages');
const input = document.getElementById('chat-input');
if (messages.style.display !== 'none') {
messages.style.display = 'none';
input.style.display = 'none';
this.innerHTML = '<i class="fas fa-plus"></i>';
chatPanel.style.transform = 'translateX(-50%) translateY(calc(100% - 40px))';
} else {
messages.style.display = 'block';
input.style.display = 'flex';
this.innerHTML = '<i class="fas fa-minus"></i>';
chatPanel.style.transform = 'translateX(-50%)';
}
});
// Simulate initial notification
setTimeout(() => {
const notification = document.getElementById('notification');
notification.textContent = 'Navire MSC Lucy ETA mis à jour à 14:30';
notification.classList.remove('hidden');
setTimeout(() => {
notification.classList.add('hidden');
}, 3000);
}, 1500);
});
</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=arkleinberg/tos" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>