prefix was optional with empty default, allowing accounts to be created with broken account_code (no prefix). Root fix: enforce prefix at input level rather than patching data after the fact. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
331 lines
14 KiB
HTML
331 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-TW">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<base href="/admin/">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>租戶管理 — VMIS Admin</title>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/2.0.3/css/dataTables.bootstrap5.min.css">
|
|
<link rel="stylesheet" href="css/style.css">
|
|
<link rel="icon" href="img/logo.png">
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Sidebar -->
|
|
<nav id="sidebar">
|
|
<a class="brand" href="index.html"><img src="img/logo.png" alt="logo" style="height:28px;object-fit:contain;vertical-align:middle;" class="me-2">VMIS Admin</a>
|
|
<div class="pt-2">
|
|
<div class="nav-section">租戶服務</div>
|
|
<a class="nav-link active" href="tenants.html"><i class="bi bi-building"></i> 租戶管理</a>
|
|
<a class="nav-link" href="accounts.html"><i class="bi bi-people"></i> 帳號管理</a>
|
|
<div class="nav-section">基礎設施</div>
|
|
<a class="nav-link" href="servers.html"><i class="bi bi-hdd-network"></i> 伺服器狀態</a>
|
|
<a class="nav-link" href="system-status.html"><i class="bi bi-activity"></i> 系統狀態</a>
|
|
<div class="nav-section">排程</div>
|
|
<a class="nav-link" href="schedules.html"><i class="bi bi-clock"></i> 排程管理</a>
|
|
<a class="nav-link" href="schedule-logs.html"><i class="bi bi-list-ul"></i> 執行紀錄</a>
|
|
<div class="nav-section">系統</div>
|
|
<a class="nav-link" href="settings.html"><i class="bi bi-gear"></i> 系統設定</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main -->
|
|
<div id="main">
|
|
<div id="topbar">
|
|
<span class="fw-semibold text-secondary"><i class="bi bi-building me-1"></i>租戶管理</span>
|
|
<div class="ms-auto">
|
|
<button class="btn btn-primary btn-sm" onclick="openCreate()">
|
|
<i class="bi bi-plus-lg me-1"></i>新增租戶
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="content">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body p-0">
|
|
<table id="tbl" class="table table-hover mb-0 w-100">
|
|
<thead>
|
|
<tr>
|
|
<th>租戶代碼</th>
|
|
<th>中文名稱</th>
|
|
<th>網域</th>
|
|
<th>狀態</th>
|
|
<th class="text-center">啟用</th>
|
|
<th class="text-center">SSO</th>
|
|
<th class="text-center">郵箱</th>
|
|
<th class="text-center">Drive</th>
|
|
<th class="text-center">Office</th>
|
|
<th class="text-center">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create/Edit Modal -->
|
|
<div class="modal fade" id="formModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="formTitle">新增租戶</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="f-id">
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">租戶代碼 <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="f-code" placeholder="porsche">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">前置碼 <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="f-prefix" placeholder="PC">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">狀態</label>
|
|
<select class="form-select" id="f-status">
|
|
<option value="trial">試用 (trial)</option>
|
|
<option value="active">正式 (active)</option>
|
|
<option value="inactive">停用 (inactive)</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">中文名稱 <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="f-name" placeholder="匠耘科技">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">英文名稱</label>
|
|
<input type="text" class="form-control" id="f-name-eng" placeholder="Jiang Yun Tech">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">網域 <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="f-domain" placeholder="porsche.lab.taipei">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">統一編號</label>
|
|
<input type="text" class="form-control" id="f-tax-id">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">每帳號配額 (GB)</label>
|
|
<input type="number" class="form-control" id="f-quota-user" value="20" min="1">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">租戶總配額 (GB)</label>
|
|
<input type="number" class="form-control" id="f-quota-total" value="200" min="1">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">員工數上限</label>
|
|
<input type="number" class="form-control" id="f-emp-limit" value="50" min="1">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">聯絡人</label>
|
|
<input type="text" class="form-control" id="f-contact">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">聯絡人郵件</label>
|
|
<input type="email" class="form-control" id="f-contact-email">
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label">備註</label>
|
|
<textarea class="form-control" id="f-note" rows="2"></textarea>
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="f-active" checked>
|
|
<label class="form-check-label" for="f-active">啟用</label>
|
|
</div>
|
|
<div class="form-check form-switch mt-2">
|
|
<input class="form-check-input" type="checkbox" id="f-manager">
|
|
<label class="form-check-label" for="f-manager">
|
|
管理租戶 <small class="text-muted">(系統維護用,全系統只能有一個)</small>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
|
<button type="button" class="btn btn-primary" onclick="saveForm()">儲存</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Confirm Modal -->
|
|
<div class="modal fade" id="confirmModal" tabindex="-1">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header"><h6 class="modal-title">確認</h6></div>
|
|
<div class="modal-body" id="confirm-body"></div>
|
|
<div class="modal-footer">
|
|
<button id="confirm-cancel" class="btn btn-secondary btn-sm">取消</button>
|
|
<button id="confirm-ok" class="btn btn-danger btn-sm">確定</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast -->
|
|
<div id="toast-container" class="position-fixed bottom-0 end-0 p-3" style="z-index:9999"></div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://cdn.datatables.net/2.0.3/js/dataTables.min.js"></script>
|
|
<script src="https://cdn.datatables.net/2.0.3/js/dataTables.bootstrap5.min.js"></script>
|
|
<script src="js/api.js"></script>
|
|
<script>
|
|
let dt;
|
|
|
|
const statusBadge = s => ({
|
|
trial: '<span class="badge bg-warning text-dark">試用</span>',
|
|
active: '<span class="badge bg-success">正式</span>',
|
|
inactive: '<span class="badge bg-secondary">停用</span>',
|
|
}[s] || s);
|
|
|
|
async function loadTable() {
|
|
let rows;
|
|
try { rows = await apiFetch('/tenants'); } catch (e) { toast('無法載入租戶資料:' + e.message, 'error'); return; }
|
|
const data = rows.map(t => {
|
|
const lights = t.lights || {};
|
|
return [
|
|
`<strong>${t.code}</strong>`,
|
|
t.name,
|
|
`<small class="text-muted">${t.domain}</small>`,
|
|
statusBadge(t.status),
|
|
t.is_active
|
|
? '<span class="badge bg-success-subtle text-success">啟用</span>'
|
|
: '<span class="badge bg-secondary-subtle text-secondary">停用</span>',
|
|
lightHtml(lights.sso_result),
|
|
lightHtml(lights.mailbox_result),
|
|
lightHtml(lights.nc_result),
|
|
lightHtml(lights.office_result),
|
|
`<button class="btn btn-outline-primary btn-sm me-1" onclick='editRow(${JSON.stringify(t)})'><i class="bi bi-pencil"></i></button>
|
|
<button class="btn btn-outline-danger btn-sm" onclick="deleteRow(${t.id},'${t.name}')"><i class="bi bi-trash"></i></button>`,
|
|
];
|
|
});
|
|
|
|
if (dt) {
|
|
dt.clear().rows.add(data).draw();
|
|
} else {
|
|
dt = $('#tbl').DataTable({
|
|
data,
|
|
columns: [
|
|
{}, {}, {}, {},
|
|
{ className: 'text-center' },
|
|
{ className: 'text-center', orderable: false },
|
|
{ className: 'text-center', orderable: false },
|
|
{ className: 'text-center', orderable: false },
|
|
{ className: 'text-center', orderable: false },
|
|
{ className: 'text-center', orderable: false },
|
|
],
|
|
language: { url: 'https://cdn.datatables.net/plug-ins/2.0.3/i18n/zh-HANT.json' },
|
|
pageLength: 25,
|
|
order: [[0, 'asc']],
|
|
});
|
|
}
|
|
}
|
|
|
|
function clearForm() {
|
|
['f-id','f-code','f-prefix','f-name','f-name-eng','f-domain','f-tax-id','f-contact','f-contact-email','f-note'].forEach(id => {
|
|
document.getElementById(id).value = '';
|
|
});
|
|
document.getElementById('f-quota-user').value = 20;
|
|
document.getElementById('f-quota-total').value = 200;
|
|
document.getElementById('f-emp-limit').value = 50;
|
|
document.getElementById('f-status').value = 'trial';
|
|
document.getElementById('f-active').checked = true;
|
|
document.getElementById('f-manager').checked = false;
|
|
}
|
|
|
|
function openCreate() {
|
|
clearForm();
|
|
document.getElementById('formTitle').textContent = '新增租戶';
|
|
new bootstrap.Modal(document.getElementById('formModal')).show();
|
|
}
|
|
|
|
function editRow(t) {
|
|
clearForm();
|
|
document.getElementById('formTitle').textContent = '編輯租戶';
|
|
document.getElementById('f-id').value = t.id;
|
|
document.getElementById('f-code').value = t.code;
|
|
document.getElementById('f-prefix').value = t.prefix;
|
|
document.getElementById('f-name').value = t.name;
|
|
document.getElementById('f-name-eng').value = t.name_eng || '';
|
|
document.getElementById('f-domain').value = t.domain;
|
|
document.getElementById('f-tax-id').value = t.tax_id || '';
|
|
document.getElementById('f-quota-user').value = t.quota_per_user;
|
|
document.getElementById('f-quota-total').value = t.total_quota;
|
|
document.getElementById('f-emp-limit').value = t.employee_limit || 50;
|
|
document.getElementById('f-contact').value = t.contact || '';
|
|
document.getElementById('f-contact-email').value = t.contact_email || '';
|
|
document.getElementById('f-note').value = t.note || '';
|
|
document.getElementById('f-status').value = t.status;
|
|
document.getElementById('f-active').checked = t.is_active;
|
|
document.getElementById('f-manager').checked = t.is_manager || false;
|
|
new bootstrap.Modal(document.getElementById('formModal')).show();
|
|
}
|
|
|
|
async function saveForm() {
|
|
const id = document.getElementById('f-id').value;
|
|
const code = document.getElementById('f-code').value.trim();
|
|
const prefix = document.getElementById('f-prefix').value.trim();
|
|
const name = document.getElementById('f-name').value.trim();
|
|
const domain = document.getElementById('f-domain').value.trim();
|
|
if (!code || !prefix || !name || !domain) {
|
|
toast('代碼、前置碼、名稱、網域為必填', 'error');
|
|
return;
|
|
}
|
|
const payload = {
|
|
code,
|
|
prefix,
|
|
name,
|
|
name_eng: document.getElementById('f-name-eng').value.trim() || null,
|
|
domain,
|
|
tax_id: document.getElementById('f-tax-id').value.trim() || null,
|
|
quota_per_user: parseInt(document.getElementById('f-quota-user').value),
|
|
total_quota: parseInt(document.getElementById('f-quota-total').value),
|
|
employee_limit: parseInt(document.getElementById('f-emp-limit').value),
|
|
contact: document.getElementById('f-contact').value.trim() || null,
|
|
contact_email: document.getElementById('f-contact-email').value.trim() || null,
|
|
note: document.getElementById('f-note').value.trim() || null,
|
|
status: document.getElementById('f-status').value,
|
|
is_active: document.getElementById('f-active').checked,
|
|
is_manager: document.getElementById('f-manager').checked,
|
|
};
|
|
try {
|
|
if (id) {
|
|
await apiFetch(`/tenants/${id}`, { method: 'PUT', body: JSON.stringify(payload) });
|
|
toast('租戶已更新');
|
|
} else {
|
|
await apiFetch('/tenants', { method: 'POST', body: JSON.stringify(payload) });
|
|
toast('租戶已新增');
|
|
}
|
|
bootstrap.Modal.getInstance(document.getElementById('formModal')).hide();
|
|
loadTable();
|
|
} catch (e) {
|
|
toast(e.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function deleteRow(id, name) {
|
|
const ok = await confirm(`確定要刪除租戶「${name}」?\n(關聯帳號將一併刪除)`);
|
|
if (!ok) return;
|
|
try {
|
|
await apiFetch(`/tenants/${id}`, { method: 'DELETE' });
|
|
toast('租戶已刪除');
|
|
loadTable();
|
|
} catch (e) {
|
|
toast(e.message, 'error');
|
|
}
|
|
}
|
|
|
|
loadTable();
|
|
</script>
|
|
</body>
|
|
</html>
|