Files
pleno-vue/src/components/session/token/SessionUser/Objects/DepartmentDailyReportComplaints.vue
T

601 lines
20 KiB
Vue

<script>
import Swal from "sweetalert2";
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import i18n from "@/i18n";
import {
COMPLAINT_CUSTOMER_SEARCH_DEBOUNCE_MS,
COMPLAINT_CUSTOMER_SEARCH_MIN_LENGTH,
formatComplaintCustomerLabel,
normalizeComplaintCustomer,
searchComplaintCustomers,
} from "@/services/departmentDailyReportComplaintCustomers.js";
import {
DEPARTMENT_DAILY_REPORT_COMPLAINT_CATEGORY_OPTIONS,
isDepartmentDailyReportComplaintCategory,
} from "@/services/departmentDailyReportComplaintCategories.js";
const t = (key) => i18n.global.t(key);
const ENDPOINT = "/departments/daily-reports/complaints";
const EDIT_COMPLAINT_CUSTOMER_SEARCH_INPUT_ID = "superuser-complaint-customer-search";
const EDIT_COMPLAINT_CUSTOMER_SEARCH_CONTROL_ID = "superuser-complaint-customer-search-control";
const EDIT_COMPLAINT_CUSTOMER_RESULTS_WRAPPER_ID = "superuser-complaint-customer-results-wrapper";
const EDIT_COMPLAINT_CUSTOMER_RESULTS_ID = "superuser-complaint-customer-results";
const EDIT_COMPLAINT_CUSTOMER_HELP_ID = "superuser-complaint-customer-help";
const EDIT_COMPLAINT_CUSTOMER_CLEAR_ID = "superuser-complaint-customer-clear";
const EDIT_COMPLAINT_CUSTOMER_SEARCH_WRAPPER_STYLE = "position: relative;";
const EDIT_COMPLAINT_CUSTOMER_RESULTS_WRAPPER_STYLE = "position: absolute; top: calc(100% + 4px); left: 0; right: 0; width: 100%; z-index: 30;";
const EDIT_COMPLAINT_CUSTOMER_RESULTS_MENU_STYLE = "display: block; position: static; width: 100%; padding-top: 0;";
const EDIT_COMPLAINT_CUSTOMER_RESULTS_CONTENT_STYLE = "max-height: 14rem; overflow-y: auto;";
const EDIT_COMPLAINT_CUSTOMER_OPTION_STYLE = "width: 100%; justify-content: flex-start; border: 0; border-radius: 0;";
const escapeHtml = (value) =>
String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
const buildDepartmentOptions = (departments, selectedDepartmentId) => {
return departments
.map((department) => {
const selected = Number(department.id) === Number(selectedDepartmentId) ? " selected" : "";
return `<option value="${escapeHtml(department.id)}"${selected}>${escapeHtml(department.name)}</option>`;
})
.join("");
};
const buildComplaintCategoryOptions = (selectedCategory) => {
const normalizedSelectedCategory = String(selectedCategory ?? "").trim();
const options = [
`<option value="">${escapeHtml("Vaelg kategori")}</option>`,
...DEPARTMENT_DAILY_REPORT_COMPLAINT_CATEGORY_OPTIONS.map((category) => {
const selected = category.value === normalizedSelectedCategory ? " selected" : "";
return `<option value="${escapeHtml(category.value)}"${selected}>${escapeHtml(category.label)}</option>`;
}),
];
return options.join("");
};
const initialComplaintCustomerSelection = (complaint) => normalizeComplaintCustomer({
customer_number: complaint.customer_number ?? null,
customer_name: complaint.customer_name ?? null,
});
const initialComplaintWashDate = (complaint) => String(complaint?.wash_date ?? "").trim();
const initialComplaintCategory = (complaint) => String(complaint?.category ?? "").trim();
const buildEditComplaintForm = (complaint, departments) => {
const selectedCustomer = initialComplaintCustomerSelection(complaint);
const selectedCustomerLabel = formatComplaintCustomerLabel(selectedCustomer);
const clearButtonStyle = selectedCustomer ? "" : "visibility: hidden; pointer-events: none;";
const washDate = initialComplaintWashDate(complaint);
const category = initialComplaintCategory(complaint);
return `
<div class="field">
<label class="label has-text-black" for="superuser-complaint-department">Afdeling</label>
<div class="control">
<div class="select is-fullwidth">
<select
id="superuser-complaint-department"
data-testid="superuser-complaint-edit-department"
class="has-background-light has-text-black"
>
${buildDepartmentOptions(departments, complaint.department_id)}
</select>
</div>
</div>
</div>
<div class="field">
<label class="label has-text-black" for="${EDIT_COMPLAINT_CUSTOMER_SEARCH_INPUT_ID}">Kunde (valgfri)</label>
<div style="${EDIT_COMPLAINT_CUSTOMER_SEARCH_WRAPPER_STYLE}">
<div
class="control"
id="${EDIT_COMPLAINT_CUSTOMER_SEARCH_CONTROL_ID}"
>
<input
id="${EDIT_COMPLAINT_CUSTOMER_SEARCH_INPUT_ID}"
data-testid="superuser-complaint-edit-customer-search"
class="input has-background-light has-text-black"
type="text"
autocomplete="off"
placeholder="Søg kunde eller kundenummer"
value="${escapeHtml(selectedCustomerLabel)}"
>
</div>
<div
id="${EDIT_COMPLAINT_CUSTOMER_RESULTS_WRAPPER_ID}"
class="dropdown is-fullwidth complaint-customer-dropdown is-hidden"
style="${EDIT_COMPLAINT_CUSTOMER_RESULTS_WRAPPER_STYLE}"
>
<div class="dropdown-menu" style="${EDIT_COMPLAINT_CUSTOMER_RESULTS_MENU_STYLE}">
<div
id="${EDIT_COMPLAINT_CUSTOMER_RESULTS_ID}"
data-testid="superuser-complaint-edit-customer-results"
class="dropdown-content"
style="${EDIT_COMPLAINT_CUSTOMER_RESULTS_CONTENT_STYLE}"
></div>
</div>
</div>
</div>
<button
id="${EDIT_COMPLAINT_CUSTOMER_CLEAR_ID}"
data-testid="superuser-complaint-edit-customer-clear"
type="button"
class="button is-small mt-2"
style="${clearButtonStyle}"
>
Ryd
</button>
<p
id="${EDIT_COMPLAINT_CUSTOMER_HELP_ID}"
class="help is-danger is-hidden"
></p>
</div>
<div class="field">
<label class="label has-text-black" for="superuser-complaint-wash-date">Dato for vask</label>
<div class="control">
<input
id="superuser-complaint-wash-date"
data-testid="superuser-complaint-edit-wash-date"
class="input has-background-light has-text-black"
type="date"
value="${escapeHtml(washDate)}"
>
</div>
</div>
<div class="field">
<label class="label has-text-black" for="superuser-complaint-category">Kategori</label>
<div class="control">
<div class="select is-fullwidth">
<select
id="superuser-complaint-category"
data-testid="superuser-complaint-edit-category"
class="has-background-light has-text-black"
>
${buildComplaintCategoryOptions(category)}
</select>
</div>
</div>
</div>
<div class="field">
<label class="label has-text-black" for="superuser-complaint-description">Beskrivelse</label>
<div class="control">
<textarea
id="superuser-complaint-description"
data-testid="superuser-complaint-edit-description"
class="textarea has-background-light has-text-black"
rows="6"
>${escapeHtml(complaint.description ?? "")}</textarea>
</div>
</div>
`;
};
const createComplaintCustomerLookupState = (complaint) => ({
selectedCustomer: initialComplaintCustomerSelection(complaint),
results: [],
query: "",
hasSearched: false,
lookupError: "",
searchTimer: null,
closeTimer: null,
requestId: 0,
});
const setComplaintCustomerHelpMessage = (helpElement, message = "") => {
if (!helpElement) {
return;
}
helpElement.textContent = message;
helpElement.classList.toggle("is-hidden", message === "");
};
const hideComplaintCustomerResults = (wrapperElement, resultsElement) => {
if (resultsElement) {
resultsElement.innerHTML = "";
}
if (!wrapperElement) {
return;
}
wrapperElement.classList.add("is-hidden");
wrapperElement.classList.remove("is-active");
};
const showComplaintCustomerResults = (wrapperElement) => {
if (!wrapperElement) {
return;
}
wrapperElement.classList.remove("is-hidden");
wrapperElement.classList.add("is-active");
};
const resolveComplaintCustomerLookupErrorMessage = (error) => (
SessionUser.functions.parseErrorMessage(error)
|| error?.response?.data?.data?.message
|| error?.response?.data?.message
|| error?.message
|| "Kundeopslag mislykkedes."
);
const resolveComplaintCustomerNumberFromLookup = (lookupState) => {
const searchInput = document.getElementById(EDIT_COMPLAINT_CUSTOMER_SEARCH_INPUT_ID);
const typedValue = String(searchInput?.value || "").trim();
if (typedValue === "") {
return null;
}
if (lookupState.selectedCustomer) {
return lookupState.selectedCustomer.customer_number;
}
throw new Error("Vaelg en kunde fra listen eller ryd feltet.");
};
const setupComplaintCustomerLookupField = (lookupState) => {
const searchInput = document.getElementById(EDIT_COMPLAINT_CUSTOMER_SEARCH_INPUT_ID);
const searchControl = document.getElementById(EDIT_COMPLAINT_CUSTOMER_SEARCH_CONTROL_ID);
const resultsWrapper = document.getElementById(EDIT_COMPLAINT_CUSTOMER_RESULTS_WRAPPER_ID);
const resultsElement = document.getElementById(EDIT_COMPLAINT_CUSTOMER_RESULTS_ID);
const helpElement = document.getElementById(EDIT_COMPLAINT_CUSTOMER_HELP_ID);
const clearButton = document.getElementById(EDIT_COMPLAINT_CUSTOMER_CLEAR_ID);
if (!searchInput || !searchControl || !resultsWrapper || !resultsElement || !helpElement || !clearButton) {
return () => {};
}
const clearSearchTimer = () => {
if (lookupState.searchTimer !== null) {
clearTimeout(lookupState.searchTimer);
lookupState.searchTimer = null;
}
};
const clearCloseTimer = () => {
if (lookupState.closeTimer !== null) {
clearTimeout(lookupState.closeTimer);
lookupState.closeTimer = null;
}
};
const setSearching = (isSearching) => {
searchControl.classList.toggle("is-loading", Boolean(isSearching));
};
const syncSelectedCustomer = () => {
clearButton.style.visibility = lookupState.selectedCustomer ? "visible" : "hidden";
clearButton.style.pointerEvents = lookupState.selectedCustomer ? "auto" : "none";
if (lookupState.selectedCustomer) {
searchInput.value = formatComplaintCustomerLabel(lookupState.selectedCustomer);
}
};
const selectCustomer = (customer) => {
const normalizedCustomer = normalizeComplaintCustomer(customer);
if (!normalizedCustomer) {
return;
}
lookupState.selectedCustomer = normalizedCustomer;
lookupState.results = [];
lookupState.query = "";
lookupState.hasSearched = false;
lookupState.lookupError = "";
setComplaintCustomerHelpMessage(helpElement, "");
syncSelectedCustomer();
hideComplaintCustomerResults(resultsWrapper, resultsElement);
};
const renderResults = () => {
resultsElement.innerHTML = "";
if (lookupState.results.length === 0) {
if (
!lookupState.hasSearched
|| lookupState.lookupError !== ""
|| lookupState.query.trim().length < COMPLAINT_CUSTOMER_SEARCH_MIN_LENGTH
) {
hideComplaintCustomerResults(resultsWrapper, resultsElement);
return;
}
const emptyState = document.createElement("div");
emptyState.className = "dropdown-item has-text-grey";
emptyState.setAttribute("data-testid", "superuser-complaint-edit-customer-no-results");
emptyState.textContent = "Ingen kunder fundet.";
resultsElement.appendChild(emptyState);
showComplaintCustomerResults(resultsWrapper);
return;
}
lookupState.results.forEach((customer) => {
const option = document.createElement("button");
option.type = "button";
option.className = "dropdown-item button is-white complaint-customer-option";
option.setAttribute("data-testid", `superuser-complaint-edit-customer-option-${customer.customer_number}`);
option.setAttribute("style", EDIT_COMPLAINT_CUSTOMER_OPTION_STYLE);
option.textContent = formatComplaintCustomerLabel(customer);
option.addEventListener("mousedown", (event) => {
event.preventDefault();
clearCloseTimer();
selectCustomer(customer);
});
resultsElement.appendChild(option);
});
showComplaintCustomerResults(resultsWrapper);
};
const runSearch = async (query, requestId) => {
lookupState.hasSearched = true;
lookupState.lookupError = "";
setComplaintCustomerHelpMessage(helpElement, "");
setSearching(true);
try {
const results = await searchComplaintCustomers(query);
if (requestId !== lookupState.requestId) {
return;
}
lookupState.results = results;
renderResults();
} catch (error) {
if (requestId !== lookupState.requestId) {
return;
}
lookupState.results = [];
lookupState.lookupError = resolveComplaintCustomerLookupErrorMessage(error);
setComplaintCustomerHelpMessage(helpElement, lookupState.lookupError);
hideComplaintCustomerResults(resultsWrapper, resultsElement);
} finally {
if (requestId === lookupState.requestId) {
setSearching(false);
}
}
};
const onSearchInput = () => {
clearCloseTimer();
clearSearchTimer();
lookupState.requestId += 1;
lookupState.query = String(searchInput.value || "");
lookupState.hasSearched = false;
lookupState.lookupError = "";
lookupState.results = [];
setComplaintCustomerHelpMessage(helpElement, "");
if (
lookupState.selectedCustomer
&& searchInput.value.trim() === formatComplaintCustomerLabel(lookupState.selectedCustomer)
) {
return;
}
lookupState.selectedCustomer = null;
syncSelectedCustomer();
const trimmedQuery = lookupState.query.trim();
if (trimmedQuery === "" || trimmedQuery.length < COMPLAINT_CUSTOMER_SEARCH_MIN_LENGTH) {
setSearching(false);
hideComplaintCustomerResults(resultsWrapper, resultsElement);
return;
}
const requestId = lookupState.requestId;
lookupState.searchTimer = setTimeout(() => {
runSearch(trimmedQuery, requestId);
}, COMPLAINT_CUSTOMER_SEARCH_DEBOUNCE_MS);
};
const onSearchFocus = () => {
clearCloseTimer();
if (lookupState.selectedCustomer) {
return;
}
if (
lookupState.results.length > 0
|| (
lookupState.hasSearched
&& lookupState.lookupError === ""
&& lookupState.query.trim().length >= COMPLAINT_CUSTOMER_SEARCH_MIN_LENGTH
)
) {
renderResults();
}
};
const onSearchBlur = () => {
clearCloseTimer();
lookupState.closeTimer = setTimeout(() => {
hideComplaintCustomerResults(resultsWrapper, resultsElement);
}, 150);
};
const onClearCustomer = (event) => {
event.preventDefault();
clearSearchTimer();
clearCloseTimer();
lookupState.requestId += 1;
lookupState.selectedCustomer = null;
lookupState.results = [];
lookupState.query = "";
lookupState.hasSearched = false;
lookupState.lookupError = "";
searchInput.value = "";
setSearching(false);
setComplaintCustomerHelpMessage(helpElement, "");
syncSelectedCustomer();
hideComplaintCustomerResults(resultsWrapper, resultsElement);
searchInput.focus();
};
searchInput.addEventListener("input", onSearchInput);
searchInput.addEventListener("focus", onSearchFocus);
searchInput.addEventListener("blur", onSearchBlur);
clearButton.addEventListener("click", onClearCustomer);
syncSelectedCustomer();
setComplaintCustomerHelpMessage(helpElement, "");
return () => {
clearSearchTimer();
clearCloseTimer();
setSearching(false);
searchInput.removeEventListener("input", onSearchInput);
searchInput.removeEventListener("focus", onSearchFocus);
searchInput.removeEventListener("blur", onSearchBlur);
clearButton.removeEventListener("click", onClearCustomer);
};
};
export const DepartmentDailyReportComplaints = {
get meta() {
return {
title: t("superuser.pages.complaints.title"),
icon: "fas fa-comment-alt",
description: t("superuser.pages.complaints.subtitle"),
endpoint: ENDPOINT,
labels: {
single: t("superuser.pages.complaints.single"),
multiple: t("superuser.pages.complaints.title"),
},
};
},
get: {
all: async (params = {}) => {
return authenticatedRequest(ENDPOINT, "GET", params).then((response) => response.data.data);
},
single: async (id) => {
return authenticatedRequest(ENDPOINT, "GET", { id }).then((response) => response.data.data);
},
},
update: async (id, payload) => {
return authenticatedRequest(ENDPOINT, "PUT", {
id,
...payload,
});
},
delete: {
single: async (id) => {
return authenticatedRequest(`${ENDPOINT}?id=${id}`, "DELETE");
},
},
functions: {
showEditForm: async (complaint, onAfterSubmit = null) => {
const departments = await SessionUser.objects.departments.get.all().then((result) =>
Array.isArray(result) ? result : []
);
const customerLookupState = createComplaintCustomerLookupState(complaint);
let cleanupCustomerLookup = null;
return Swal.fire({
title: t("superuser.pages.complaints.edit_title"),
html: buildEditComplaintForm(complaint, departments),
showCancelButton: true,
confirmButtonText: t("common.save"),
cancelButtonText: t("common.cancel"),
focusConfirm: false,
didOpen: () => {
cleanupCustomerLookup = setupComplaintCustomerLookupField(customerLookupState);
},
willClose: () => {
if (typeof cleanupCustomerLookup === "function") {
cleanupCustomerLookup();
cleanupCustomerLookup = null;
}
},
preConfirm: async () => {
try {
const departmentId = Number.parseInt(
document.getElementById("superuser-complaint-department")?.value || "",
10
);
if (!Number.isFinite(departmentId) || departmentId <= 0) {
throw new Error("Afdeling er paakraevet.");
}
const washDate = (
document.getElementById("superuser-complaint-wash-date")?.value || ""
).trim();
if (washDate === "") {
throw new Error("Dato for vask er paakraevet.");
}
const category = (
document.getElementById("superuser-complaint-category")?.value || ""
).trim();
if (!isDepartmentDailyReportComplaintCategory(category)) {
throw new Error("Kategori er paakraevet.");
}
const description = (
document.getElementById("superuser-complaint-description")?.value || ""
).trim();
if (description === "") {
throw new Error("Beskrivelse er paakraevet.");
}
const customerNumber = resolveComplaintCustomerNumberFromLookup(customerLookupState);
await DepartmentDailyReportComplaints.update(complaint.id, {
department_id: departmentId,
customer_number: customerNumber,
wash_date: washDate,
category,
description,
});
if (typeof onAfterSubmit === "function") {
await onAfterSubmit();
}
} catch (error) {
Swal.showValidationMessage(
SessionUser.functions.parseErrorMessage(error) || error?.message || String(error)
);
return false;
}
return true;
},
});
},
showDeleteConfirmationModal: async (id, onAfterSubmit = null) => {
return Swal.fire({
title: t("superuser.pages.complaints.delete_title"),
text: t("superuser.pages.complaints.delete_message"),
showCancelButton: true,
confirmButtonText: t("global.confirm_delete"),
cancelButtonText: t("common.cancel"),
showLoaderOnConfirm: true,
preConfirm: async () => {
try {
await DepartmentDailyReportComplaints.delete.single(id);
if (typeof onAfterSubmit === "function") {
await onAfterSubmit();
}
} catch (error) {
Swal.showValidationMessage(
SessionUser.functions.parseErrorMessage(error) || error?.message || String(error)
);
return false;
}
return true;
},
});
},
},
};
</script>