backupchecks/containers/backupchecks/src/templates/main/customers.html

506 lines
20 KiB
HTML

{% extends "layout/base.html" %}
{% block content %}
<h2 class="mb-3">Customers</h2>
{% if can_manage %}
<div class="d-flex align-items-center gap-2 mb-3">
<form method="post" action="{{ url_for('main.customers_create') }}" class="d-flex align-items-center gap-2 mb-0" autocomplete="off" data-1p-ignore="true" data-lpignore="true">
<input type="text" name="name" class="form-control form-control-sm" placeholder="New customer name" required style="max-width: 320px;" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-1p-ignore="true" data-lpignore="true" data-form-type="other" />
<div class="form-check form-check-inline mb-0">
<input class="form-check-input" type="checkbox" name="active" id="newCustomerActive" checked />
<label class="form-check-label small" for="newCustomerActive">Active</label>
</div>
<button type="submit" class="btn btn-primary btn-sm" style="white-space: nowrap;">Add</button>
</form>
<form method="post" action="{{ url_for('main.customers_import') }}" enctype="multipart/form-data" class="d-flex align-items-center gap-2 mb-0">
<input type="file" name="file" accept=".csv,text/csv" class="form-control form-control-sm" required style="max-width: 420px;" />
<button type="submit" class="btn btn-outline-secondary btn-sm" style="white-space: nowrap;">Import CSV</button>
</form>
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('main.customers_export') }}">Export CSV</a>
{% if autotask_enabled and autotask_configured %}
<button type="button" class="btn btn-outline-secondary btn-sm" id="autotaskRefreshAllMappingsBtn" style="white-space: nowrap;">Refresh all Autotask mappings</button>
<span class="small text-muted" id="autotaskRefreshAllMappingsMsg"></span>
{% endif %}
</div>
{% endif %}
<div class="table-responsive">
<table class="table table-sm table-hover align-middle">
<thead class="table-light">
<tr>
<th scope="col">Customer</th>
<th scope="col">Active</th>
<th scope="col">Number of jobs</th>
<th scope="col">Autotask company</th>
<th scope="col">Autotask mapping</th>
{% if can_manage %}
<th scope="col">Actions</th>
{% endif %}
</tr>
</thead>
<tbody>
{% if customers %}
{% for c in customers %}
<tr>
<td>{{ c.name }}</td>
<td>
{% if c.active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-secondary">Inactive</span>
{% endif %}
</td>
<td>
{% if c.job_count > 0 %}
{{ c.job_count }}
{% else %}
<span class="text-danger fw-bold">0</span>
{% endif %}
</td>
<td>
{% if c.autotask_company_id %}
<span class="fw-semibold">{{ c.autotask_company_name or 'Unknown' }}</span>
<div class="text-muted small">ID: {{ c.autotask_company_id }}</div>
{% else %}
<span class="text-muted">Not mapped</span>
{% endif %}
</td>
<td>
{% set st = (c.autotask_mapping_status or '').lower() %}
{% if not c.autotask_company_id %}
<span class="badge bg-secondary">Not mapped</span>
{% elif st == 'ok' %}
<span class="badge bg-success">OK</span>
{% elif st == 'renamed' %}
<span class="badge bg-warning text-dark">Renamed</span>
{% elif st == 'missing' %}
<span class="badge bg-warning text-dark">Missing</span>
{% elif st == 'invalid' %}
<span class="badge bg-danger">Invalid</span>
{% else %}
<span class="badge bg-secondary">Unknown</span>
{% endif %}
{% if c.autotask_last_sync_at %}
<div class="text-muted small">Checked: {{ c.autotask_last_sync_at }}</div>
{% endif %}
</td>
{% if can_manage %}
<td>
<button
type="button"
class="btn btn-sm btn-outline-primary me-1 customer-edit-btn"
data-bs-toggle="modal"
data-bs-target="#editCustomerModal"
data-id="{{ c.id }}"
data-name="{{ c.name }}"
data-active="{{ '1' if c.active else '0' }}"
data-autotask-company-id="{{ c.autotask_company_id or '' }}"
data-autotask-company-name="{{ c.autotask_company_name or '' }}"
data-autotask-mapping-status="{{ c.autotask_mapping_status or '' }}"
data-autotask-last-sync-at="{{ c.autotask_last_sync_at or '' }}"
>
Edit
</button>
<form
method="post"
action="{{ url_for('main.customers_delete', customer_id=c.id) }}"
class="d-inline"
onsubmit="return confirm('Are you sure you want to delete this customer? All related jobs and mails will be removed.');"
>
<button type="submit" class="btn btn-sm btn-outline-danger">
Delete
</button>
</form>
</td>
{% endif %}
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="{% if can_manage %}6{% else %}5{% endif %}" class="text-center text-muted py-3">
No customers found.
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{% if can_manage %}
<!-- Edit customer modal -->
<div class="modal fade" id="editCustomerModal" tabindex="-1" aria-labelledby="editCustomerModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" id="editCustomerForm" autocomplete="off" data-1p-ignore="true" data-lpignore="true">
<div class="modal-header">
<h5 class="modal-title" id="editCustomerModalLabel">Edit customer</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="edit_customer_name" class="form-label">Customer name</label>
<input
type="text"
class="form-control"
id="edit_customer_name"
name="name"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
data-1p-ignore="true"
data-lpignore="true"
data-form-type="other"
required
/>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="edit_customer_active"
name="active"
/>
<label class="form-check-label" for="edit_customer_active">
Active
</label>
</div>
<hr class="my-4" />
<h6 class="mb-2">Autotask mapping</h6>
{% if autotask_enabled and autotask_configured %}
<div class="mb-2">
<div class="small text-muted">Current mapping</div>
<div id="autotaskCurrentMapping" class="fw-semibold">Not mapped</div>
<div id="autotaskCurrentMappingMeta" class="text-muted small"></div>
</div>
<div class="input-group input-group-sm mb-2">
<input type="text" class="form-control" id="autotaskCompanySearch" placeholder="Search Autotask companies" autocomplete="off" />
<button class="btn btn-outline-secondary" type="button" id="autotaskCompanySearchBtn">Search</button>
</div>
<div id="autotaskCompanyResults" class="border rounded p-2" style="max-height: 220px; overflow:auto;"></div>
<div class="d-flex gap-2 mt-2">
<button type="button" class="btn btn-sm btn-outline-primary" id="autotaskSetMappingBtn" disabled>Set mapping</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="autotaskRefreshMappingBtn">Refresh status</button>
<button type="button" class="btn btn-sm btn-outline-danger" id="autotaskClearMappingBtn">Clear mapping</button>
</div>
<div id="autotaskMappingMsg" class="small text-muted mt-2"></div>
{% else %}
<div class="text-muted small">
Autotask integration is not available. Enable and configure it in Settings → Extensions &amp; Integrations → Autotask.
</div>
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save changes</button>
</div>
</form>
</div>
</div>
</div>
<script>
(function () {
document.addEventListener("DOMContentLoaded", function () {
var editModalEl = document.getElementById("editCustomerModal");
if (!editModalEl) {
return;
}
var editForm = document.getElementById("editCustomerForm");
var nameInput = document.getElementById("edit_customer_name");
var activeInput = document.getElementById("edit_customer_active");
// Top-level refresh-all (only present when integration is enabled/configured)
var refreshAllBtn = document.getElementById("autotaskRefreshAllMappingsBtn");
var refreshAllMsg = document.getElementById("autotaskRefreshAllMappingsMsg");
// Autotask mapping UI (only present when integration is enabled/configured)
var atCurrent = document.getElementById("autotaskCurrentMapping");
var atCurrentMeta = document.getElementById("autotaskCurrentMappingMeta");
var atSearchInput = document.getElementById("autotaskCompanySearch");
var atSearchBtn = document.getElementById("autotaskCompanySearchBtn");
var atResults = document.getElementById("autotaskCompanyResults");
var atMsg = document.getElementById("autotaskMappingMsg");
var atSetBtn = document.getElementById("autotaskSetMappingBtn");
var atRefreshBtn = document.getElementById("autotaskRefreshMappingBtn");
var atClearBtn = document.getElementById("autotaskClearMappingBtn");
var currentCustomerId = null;
var selectedCompanyId = null;
function setRefreshAllMsg(text, isError) {
if (!refreshAllMsg) {
return;
}
refreshAllMsg.textContent = text || "";
if (isError) {
refreshAllMsg.classList.remove("text-muted");
refreshAllMsg.classList.add("text-danger");
} else {
refreshAllMsg.classList.remove("text-danger");
refreshAllMsg.classList.add("text-muted");
}
}
function setMsg(text, isError) {
if (!atMsg) {
return;
}
atMsg.textContent = text || "";
if (isError) {
atMsg.classList.remove("text-muted");
atMsg.classList.add("text-danger");
} else {
atMsg.classList.remove("text-danger");
atMsg.classList.add("text-muted");
}
}
function renderCurrentMapping(companyId, companyName, mappingStatus, lastSyncAt) {
if (!atCurrent || !atCurrentMeta) {
return;
}
if (!companyId) {
atCurrent.textContent = "Not mapped";
atCurrentMeta.textContent = "";
return;
}
atCurrent.textContent = (companyName || "Unknown") + " (ID: " + companyId + ")";
var parts = [];
if (mappingStatus) {
parts.push("Status: " + mappingStatus);
}
if (lastSyncAt) {
parts.push("Checked: " + lastSyncAt);
}
atCurrentMeta.textContent = parts.join(" • ");
}
function clearResults() {
if (!atResults) {
return;
}
atResults.innerHTML = "<div class=\"text-muted small\">No results.</div>";
}
function setSelectedCompanyId(cid) {
selectedCompanyId = cid;
if (atSetBtn) {
atSetBtn.disabled = !selectedCompanyId;
}
}
async function postJson(url, body) {
var resp = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify(body || {}),
});
var data = null;
try {
data = await resp.json();
} catch (e) {
data = null;
}
if (!resp.ok) {
var msg = (data && data.message) ? data.message : ("Request failed (" + resp.status + ").");
throw new Error(msg);
}
return data;
}
if (refreshAllBtn) {
refreshAllBtn.addEventListener("click", async function () {
if (!confirm("Refresh mapping status for all mapped customers?")) {
return;
}
refreshAllBtn.disabled = true;
setRefreshAllMsg("Refreshing...", false);
try {
var data = await postJson("/api/customers/autotask-mapping/refresh-all", {});
var counts = (data && data.counts) ? data.counts : null;
if (counts) {
setRefreshAllMsg(
"Done. OK: " + (counts.ok || 0) + ", Renamed: " + (counts.renamed || 0) + ", Missing: " + (counts.missing || 0) + ", Invalid: " + (counts.invalid || 0) + ".",
false
);
} else {
setRefreshAllMsg("Done.", false);
}
window.location.reload();
} catch (e) {
setRefreshAllMsg(e && e.message ? e.message : "Refresh failed.", true);
refreshAllBtn.disabled = false;
}
});
}
var editButtons = document.querySelectorAll(".customer-edit-btn");
editButtons.forEach(function (btn) {
btn.addEventListener("click", function () {
var id = btn.getAttribute("data-id");
var name = btn.getAttribute("data-name") || "";
var active = btn.getAttribute("data-active") === "1";
nameInput.value = name;
activeInput.checked = active;
if (id) {
editForm.action = "{{ url_for('main.customers_edit', customer_id=0) }}".replace("0", id);
}
// Autotask: seed current mapping from row data attributes
currentCustomerId = id || null;
if (atResults) {
clearResults();
}
setSelectedCompanyId(null);
setMsg("", false);
if (atCurrent) {
var atCompanyId = btn.getAttribute("data-autotask-company-id") || "";
var atCompanyName = btn.getAttribute("data-autotask-company-name") || "";
var atStatus = btn.getAttribute("data-autotask-mapping-status") || "";
var atLast = btn.getAttribute("data-autotask-last-sync-at") || "";
renderCurrentMapping(atCompanyId, atCompanyName, atStatus, atLast);
}
});
});
if (atSearchBtn && atSearchInput && atResults) {
atSearchBtn.addEventListener("click", async function () {
var q = (atSearchInput.value || "").trim();
if (!q) {
setMsg("Enter a search term.", true);
return;
}
setMsg("Searching...", false);
setSelectedCompanyId(null);
atResults.innerHTML = "<div class=\"text-muted small\">Searching...</div>";
try {
var resp = await fetch("/api/autotask/companies/search?q=" + encodeURIComponent(q), {
method: "GET",
credentials: "same-origin",
});
var data = await resp.json();
if (!resp.ok || !data || data.status !== "ok") {
throw new Error((data && data.message) ? data.message : "Search failed.");
}
var items = data.items || [];
if (!items.length) {
atResults.innerHTML = "<div class=\"text-muted small\">No companies found.</div>";
setMsg("No companies found.", false);
return;
}
var html = "";
items.forEach(function (it) {
var cid = it.id;
var name = it.companyName || it.name || ("Company #" + cid);
var active = (it.isActive === false) ? " (inactive)" : "";
html +=
"<div class=\"form-check\">" +
"<input class=\"form-check-input\" type=\"radio\" name=\"autotaskCompanyPick\" id=\"at_company_" + cid + "\" value=\"" + cid + "\" />" +
"<label class=\"form-check-label\" for=\"at_company_" + cid + "\">" +
name.replace(/</g, "&lt;").replace(/>/g, "&gt;") +
" <span class=\"text-muted\">(ID: " + cid + ")</span>" +
"<span class=\"text-muted\">" + active + "</span>" +
"</label>" +
"</div>";
});
atResults.innerHTML = html;
var radios = atResults.querySelectorAll("input[name='autotaskCompanyPick']");
radios.forEach(function (r) {
r.addEventListener("change", function () {
setSelectedCompanyId(r.value);
setMsg("Selected company ID: " + r.value, false);
});
});
setMsg("Select a company and click Set mapping.", false);
} catch (e) {
atResults.innerHTML = "<div class=\"text-muted small\">No results.</div>";
setMsg(e && e.message ? e.message : "Search failed.", true);
}
});
}
if (atSetBtn) {
atSetBtn.addEventListener("click", async function () {
if (!currentCustomerId) {
setMsg("No customer selected.", true);
return;
}
if (!selectedCompanyId) {
setMsg("Select a company first.", true);
return;
}
atSetBtn.disabled = true;
setMsg("Saving mapping...", false);
try {
await postJson("/api/customers/" + currentCustomerId + "/autotask-mapping", { company_id: selectedCompanyId });
window.location.reload();
} catch (e) {
setMsg(e && e.message ? e.message : "Failed to set mapping.", true);
atSetBtn.disabled = false;
}
});
}
if (atRefreshBtn) {
atRefreshBtn.addEventListener("click", async function () {
if (!currentCustomerId) {
setMsg("No customer selected.", true);
return;
}
setMsg("Refreshing status...", false);
try {
await postJson("/api/customers/" + currentCustomerId + "/autotask-mapping/refresh", {});
window.location.reload();
} catch (e) {
setMsg(e && e.message ? e.message : "Refresh failed.", true);
}
});
}
if (atClearBtn) {
atClearBtn.addEventListener("click", async function () {
if (!currentCustomerId) {
setMsg("No customer selected.", true);
return;
}
if (!confirm("Clear Autotask mapping for this customer?")) {
return;
}
setMsg("Clearing mapping...", false);
try {
await postJson("/api/customers/" + currentCustomerId + "/autotask-mapping/clear", {});
window.location.reload();
} catch (e) {
setMsg(e && e.message ? e.message : "Clear failed.", true);
}
});
}
});
})();
</script>
{% endif %}
{% endblock %}