Files
alro65 5b7b41aa50 Initial commit: Fleet Management app with security hardening and background launcher
- Flask app with SQLAlchemy, Flask-Login, Flask-Mail
- Admin/owner roles, vessel management, charters, work orders
- Background launcher (Iniciar.vbs) runs server without terminal window
- Root redirect fixed: / → /login
- debug=False, use_reloader=False for pythonw.exe compatibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 02:54:10 -04:00

454 lines
23 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Companies & Users — Fleet Management</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f0f2f5; }
.header { background: #0a2a3a; color: white; padding: 15px 30px; display: flex; justify-content: space-between; align-items: center; border-bottom: 3px solid #c4a747; }
.header h1 { font-size: 22px; }
.header h1 span { color: #c4a747; }
.header-actions { display: flex; gap: 12px; align-items: center; }
.back-btn { background: transparent; color: #c4a747; padding: 8px 18px; text-decoration: none; border-radius: 5px; font-weight: bold; font-size: 14px; border: 1px solid #c4a747; }
.back-btn:hover { background: #c4a747; color: #0a2a3a; }
.logout-btn { background: #c4a747; color: #0a2a3a; padding: 8px 18px; text-decoration: none; border-radius: 5px; font-weight: bold; font-size: 14px; }
.logout-btn:hover { background: #d4b757; }
.container { padding: 25px 30px; max-width: 1100px; margin: 0 auto; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 22px; flex-wrap: wrap; gap: 10px; }
.page-header h2 { color: #0a2a3a; font-size: 22px; }
.quota-badge { background: white; border: 2px solid #c4a747; border-radius: 20px; padding: 6px 16px; font-size: 13px; font-weight: 700; color: #0a2a3a; }
.quota-badge span { color: #c4a747; }
.company-block { background: white; border-radius: 10px; margin-bottom: 20px; box-shadow: 0 1px 6px rgba(0,0,0,0.08); overflow: hidden; }
.company-header { background: #0a2a3a; color: white; padding: 14px 20px; display: flex; justify-content: space-between; align-items: center; }
.company-header h3 { font-size: 16px; }
.company-header h3 .company-email { font-size: 12px; color: #aac4d0; font-weight: 400; margin-left: 10px; }
.company-meta { font-size: 12px; color: #aac4d0; margin-top: 2px; }
.company-actions { display: flex; gap: 8px; }
.users-section { padding: 18px 20px; }
.users-section h4 { font-size: 13px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; font-weight: 600; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th { background: #f4f6f8; color: #0a2a3a; padding: 9px 12px; text-align: left; font-size: 12px; text-transform: uppercase; letter-spacing: 0.4px; border-bottom: 2px solid #e8e8e8; }
td { padding: 10px 12px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: #fafaf8; }
.btn { background: #0a2a3a; color: white; padding: 7px 14px; border: none; border-radius: 5px; cursor: pointer; font-size: 12px; font-weight: 600; text-decoration: none; display: inline-block; transition: background 0.2s; }
.btn:hover { background: #c4a747; color: #0a2a3a; }
.btn-sm { padding: 4px 10px; font-size: 11px; }
.btn-gold { background: #c4a747; color: #0a2a3a; }
.btn-gold:hover { background: #d4b757; }
.btn-danger { background: #e74c3c; color: white; }
.btn-danger:hover { background: #c0392b; }
.btn-success { background: #27ae60; color: white; }
.btn-success:hover { background: #219a52; }
.badge { display: inline-block; padding: 2px 9px; border-radius: 10px; font-size: 11px; font-weight: 700; }
.badge-super { background: #c4a747; color: #0a2a3a; }
.badge-admin { background: #d4edda; color: #155724; }
.badge-active { background: #d4edda; color: #155724; }
.badge-inactive { background: #f8d7da; color: #721c24; }
.vessel-count { background: #f0e8cc; color: #0a2a3a; border-radius: 10px; padding: 2px 10px; font-size: 12px; font-weight: 700; display: inline-block; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.55); justify-content: center; align-items: flex-start; z-index: 1000; padding-top: 60px; overflow-y: auto; }
.modal.open { display: flex; }
.modal-content { background: white; padding: 28px; border-radius: 10px; width: 520px; max-width: 95%; margin-bottom: 40px; }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 2px solid #f0e8cc; padding-bottom: 12px; }
.modal-header h3 { color: #0a2a3a; font-size: 17px; }
.close-btn { cursor: pointer; font-size: 26px; color: #999; line-height: 1; background: none; border: none; }
.close-btn:hover { color: #0a2a3a; }
.form-group { margin-bottom: 14px; }
.form-group label { display: block; margin-bottom: 5px; color: #0a2a3a; font-weight: 600; font-size: 13px; }
.form-group input, .form-group select { width: 100%; padding: 9px 12px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: #c4a747; }
.hint { font-size: 11px; color: #888; margin-top: 3px; }
.section-sep { font-size: 11px; font-weight: 700; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin: 16px 0 10px; padding-bottom: 5px; border-bottom: 1px dashed #e0d8b0; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.msg-bar { padding: 10px 16px; border-radius: 6px; font-size: 14px; font-weight: 600; margin-bottom: 16px; display: none; }
.msg-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; display: block; }
.msg-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; display: block; }
.empty-users { color: #999; font-size: 13px; font-style: italic; padding: 10px 0; }
</style>
</head>
<body>
<div class="header">
<h1>Fleet <span>Management</span></h1>
<div class="header-actions">
<a href="/admin/dashboard" class="back-btn">&#8592; Dashboard</a>
<a href="{{ url_for('auth.logout') }}" class="logout-btn">Cerrar Sesion</a>
</div>
</div>
<div class="container">
<div class="page-header">
<h2>&#9776; Companies &amp; Users</h2>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
<div class="quota-badge">Companies: <span>{{ companies|length }}</span> / 10</div>
{% if companies|length < 10 %}
<button class="btn btn-gold" onclick="openCreateCompany()">+ New Company</button>
{% endif %}
</div>
</div>
<div id="msgBar" class="msg-bar"></div>
{% for c in companies %}
{% set vessel_count = c.managed_vessels | length %}
<div class="company-block">
<div class="company-header">
<div>
<h3>
{{ c.name }}
<span class="company-email">{{ c.email or '' }}</span>
</h3>
<div class="company-meta">
{% if c.phone %}&#128222; {{ c.phone }} &nbsp;&bull;&nbsp;{% endif %}
<span class="vessel-count">{{ vessel_count }} vessel{{ 's' if vessel_count != 1 else '' }}</span>
&nbsp;&bull;&nbsp; {{ c.users | length }} user{{ 's' if c.users|length != 1 else '' }}
</div>
</div>
<div class="company-actions">
<button class="btn btn-sm" onclick="openAddUser({{ c.id }}, {{ c.name | tojson }})">+ Add User</button>
<button class="btn btn-sm btn-gold" onclick='openEditCompany({{ c.id }}, {{ c.name|tojson }}, {{ (c.email or "")|tojson }}, {{ (c.phone or "")|tojson }})'>Edit</button>
</div>
</div>
<div class="users-section">
<h4>Users</h4>
{% if c.users %}
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for u in c.users %}
<tr>
<td>
<strong>{{ u.name }}</strong>
{% if u.is_super_admin %}<span class="badge badge-super">&#9733; Super Admin</span>{% endif %}
</td>
<td>{{ u.email }}</td>
<td><span class="badge badge-admin">{{ u.role | capitalize }}</span></td>
<td>
{% if u.is_active %}
<span class="badge badge-active">Active</span>
{% else %}
<span class="badge badge-inactive">Inactive</span>
{% endif %}
</td>
<td>
<button class="btn btn-sm" onclick='openEditUser({{ u.id }}, {{ c.id }}, {{ u.name|tojson }}, {{ u.email|tojson }}, {{ u.role|tojson }}, {{ u.is_super_admin|tojson }}, {{ u.is_active|tojson }})'>Edit</button>
{% if not u.is_super_admin %}
<button class="btn btn-sm btn-danger" onclick="deleteUser({{ u.id }}, {{ c.id }})">Delete</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-users">No users assigned to this company yet. Click "+ Add User" to create one.</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<!-- MODAL: New Company -->
<div id="createCompanyModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>New Management Company</h3>
<button class="close-btn" onclick="closeModal('createCompanyModal')">&times;</button>
</div>
<div class="section-sep">Company Info</div>
<div class="form-group"><label>Company Name *</label><input type="text" id="cc_name" placeholder="Miami Marine Management LLC"></div>
<div class="form-row">
<div class="form-group"><label>Company Email</label><input type="email" id="cc_email" placeholder="info@company.com"></div>
<div class="form-group"><label>Phone</label><input type="text" id="cc_phone" placeholder="305-000-0000"></div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:18px;">
<button class="btn" onclick="closeModal('createCompanyModal')" style="background:#ccc;color:#333;">Cancel</button>
<button class="btn btn-gold" onclick="createCompany()">Create Company</button>
</div>
</div>
</div>
<!-- MODAL: Edit Company -->
<div id="editCompanyModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Edit Company</h3>
<button class="close-btn" onclick="closeModal('editCompanyModal')">&times;</button>
</div>
<input type="hidden" id="ec_id">
<div class="form-group"><label>Company Name *</label><input type="text" id="ec_name"></div>
<div class="form-row">
<div class="form-group"><label>Company Email</label><input type="email" id="ec_email"></div>
<div class="form-group"><label>Phone</label><input type="text" id="ec_phone"></div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:18px;">
<button class="btn" onclick="closeModal('editCompanyModal')" style="background:#ccc;color:#333;">Cancel</button>
<button class="btn btn-gold" onclick="updateCompany()">Save Changes</button>
</div>
</div>
</div>
<!-- MODAL: Add User -->
<div id="addUserModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Add User to <span id="addUserCompanyName" style="color:#c4a747;"></span></h3>
<button class="close-btn" onclick="closeModal('addUserModal')">&times;</button>
</div>
<input type="hidden" id="au_company_id">
<div class="form-row">
<div class="form-group"><label>Full Name *</label><input type="text" id="au_name" placeholder="John Smith"></div>
<div class="form-group"><label>Email *</label><input type="email" id="au_email" placeholder="user@company.com"></div>
</div>
<div class="form-row">
<div class="form-group">
<label>Password *</label>
<input type="password" id="au_password" placeholder="Min. 6 characters">
</div>
<div class="form-group">
<label>Role</label>
<select id="au_role">
<option value="admin">Admin</option>
<option value="operator">Operator</option>
</select>
</div>
</div>
<div class="form-group">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400;">
<input type="checkbox" id="au_super" style="width:auto;">
Grant Super Admin privileges (can manage all companies and users)
</label>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:18px;">
<button class="btn" onclick="closeModal('addUserModal')" style="background:#ccc;color:#333;">Cancel</button>
<button class="btn btn-success" onclick="addUser()">Add User</button>
</div>
</div>
</div>
<!-- MODAL: Edit User -->
<div id="editUserModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Edit User</h3>
<button class="close-btn" onclick="closeModal('editUserModal')">&times;</button>
</div>
<input type="hidden" id="eu_id">
<input type="hidden" id="eu_company_id">
<div class="form-row">
<div class="form-group"><label>Full Name *</label><input type="text" id="eu_name"></div>
<div class="form-group"><label>Email *</label><input type="email" id="eu_email"></div>
</div>
<div class="form-row">
<div class="form-group">
<label>New Password <span style="font-weight:400;color:#888;">(leave blank to keep)</span></label>
<input type="password" id="eu_password" placeholder="New password...">
</div>
<div class="form-group">
<label>Role</label>
<select id="eu_role">
<option value="admin">Admin</option>
<option value="operator">Operator</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400;">
<input type="checkbox" id="eu_super" style="width:auto;">
Super Admin privileges
</label>
</div>
<div class="form-group">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400;">
<input type="checkbox" id="eu_active" style="width:auto;" checked>
Active account
</label>
</div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:18px;">
<button class="btn" onclick="closeModal('editUserModal')" style="background:#ccc;color:#333;">Cancel</button>
<button class="btn btn-gold" onclick="updateUser()">Save Changes</button>
</div>
</div>
</div>
<script>
function showMsg(msg, type) {
const bar = document.getElementById('msgBar');
bar.textContent = msg;
bar.className = 'msg-bar msg-' + type;
bar.scrollIntoView({behavior:'smooth', block:'nearest'});
setTimeout(() => { bar.className = 'msg-bar'; }, 4000);
}
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
document.querySelectorAll('.modal').forEach(m => {
m.addEventListener('click', e => { if (e.target === m) m.classList.remove('open'); });
});
// ---- COMPANY ----
function openCreateCompany() {
['cc_name','cc_email','cc_phone'].forEach(id => document.getElementById(id).value = '');
document.getElementById('createCompanyModal').classList.add('open');
}
function openEditCompany(id, name, email, phone) {
document.getElementById('ec_id').value = id;
document.getElementById('ec_name').value = name;
document.getElementById('ec_email').value = email;
document.getElementById('ec_phone').value = phone;
document.getElementById('editCompanyModal').classList.add('open');
}
async function createCompany() {
const name = document.getElementById('cc_name').value.trim();
if (!name) { showMsg('Company name is required.', 'error'); return; }
const res = await fetch('/api/management-companies', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({
name,
company_email: document.getElementById('cc_email').value.trim(),
phone: document.getElementById('cc_phone').value.trim()
})
});
const data = await res.json();
if (data.success) {
showMsg('Company created. Reloading...', 'success');
closeModal('createCompanyModal');
setTimeout(() => location.reload(), 1200);
} else {
showMsg(data.error || 'Error creating company', 'error');
}
}
async function updateCompany() {
const id = document.getElementById('ec_id').value;
const res = await fetch('/api/management-companies/' + id, {
method: 'PUT',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({
name: document.getElementById('ec_name').value.trim(),
company_email: document.getElementById('ec_email').value.trim(),
phone: document.getElementById('ec_phone').value.trim()
})
});
const data = await res.json();
if (data.success) {
showMsg('Company updated. Reloading...', 'success');
closeModal('editCompanyModal');
setTimeout(() => location.reload(), 1200);
} else {
showMsg(data.error || 'Error', 'error');
}
}
// ---- USERS ----
function openAddUser(companyId, companyName) {
document.getElementById('au_company_id').value = companyId;
document.getElementById('addUserCompanyName').textContent = companyName;
['au_name','au_email','au_password'].forEach(id => document.getElementById(id).value = '');
document.getElementById('au_role').value = 'admin';
document.getElementById('au_super').checked = false;
document.getElementById('addUserModal').classList.add('open');
}
function openEditUser(userId, companyId, name, email, role, isSuper, isActive) {
document.getElementById('eu_id').value = userId;
document.getElementById('eu_company_id').value = companyId;
document.getElementById('eu_name').value = name;
document.getElementById('eu_email').value = email;
document.getElementById('eu_password').value = '';
document.getElementById('eu_role').value = role;
document.getElementById('eu_super').checked = isSuper;
document.getElementById('eu_active').checked = isActive;
document.getElementById('editUserModal').classList.add('open');
}
async function addUser() {
const companyId = document.getElementById('au_company_id').value;
const name = document.getElementById('au_name').value.trim();
const email = document.getElementById('au_email').value.trim();
const pwd = document.getElementById('au_password').value;
if (!name || !email || !pwd) { showMsg('Name, email and password are required.', 'error'); return; }
const res = await fetch(`/api/management-companies/${companyId}/users`, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({
name, email, password: pwd,
role: document.getElementById('au_role').value,
is_super_admin: document.getElementById('au_super').checked
})
});
const data = await res.json();
if (data.success) {
showMsg('User added successfully. Reloading...', 'success');
closeModal('addUserModal');
setTimeout(() => location.reload(), 1200);
} else {
showMsg(data.error || 'Error adding user', 'error');
}
}
async function updateUser() {
const id = document.getElementById('eu_id').value;
const companyId = document.getElementById('eu_company_id').value;
const payload = {
name: document.getElementById('eu_name').value.trim(),
email: document.getElementById('eu_email').value.trim(),
role: document.getElementById('eu_role').value,
is_super_admin: document.getElementById('eu_super').checked,
is_active: document.getElementById('eu_active').checked
};
const pwd = document.getElementById('eu_password').value;
if (pwd) payload.password = pwd;
const res = await fetch(`/api/management-companies/${companyId}/users/${id}`, {
method: 'PUT',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.success) {
showMsg('User updated. Reloading...', 'success');
closeModal('editUserModal');
setTimeout(() => location.reload(), 1200);
} else {
showMsg(data.error || 'Error', 'error');
}
}
async function deleteUser(userId, companyId) {
if (!confirm('Delete this user? This action cannot be undone.')) return;
const res = await fetch(`/api/management-companies/${companyId}/users/${userId}`, {method: 'DELETE'});
const data = await res.json();
if (data.success) {
showMsg('User deleted. Reloading...', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showMsg(data.error || 'Error', 'error');
}
}
</script>
</body>
</html>