5b7b41aa50
- 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>
454 lines
23 KiB
HTML
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">← Dashboard</a>
|
|
<a href="{{ url_for('auth.logout') }}" class="logout-btn">Cerrar Sesion</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="page-header">
|
|
<h2>☰ Companies & 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 %}📞 {{ c.phone }} • {% endif %}
|
|
<span class="vessel-count">{{ vessel_count }} vessel{{ 's' if vessel_count != 1 else '' }}</span>
|
|
• {{ 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">★ 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')">×</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')">×</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')">×</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')">×</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>
|