939 lines
32 KiB
JavaScript
939 lines
32 KiB
JavaScript
import { computed, ref } from "vue";
|
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
|
|
|
const normalizeBooleanAnswer = (value) => {
|
|
if (value === true || value === false) {
|
|
return value;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const normalizeQuestion = (question) => ({
|
|
id: parseInt(question?.id ?? question?.question_id ?? 0),
|
|
question: question?.question ?? "",
|
|
description: question?.description ?? "",
|
|
condition_id: question?.condition_id ?? null,
|
|
order_priority: parseInt(question?.order_priority ?? 0),
|
|
answer: normalizeBooleanAnswer(question?.answer),
|
|
answered_at: question?.answered_at ?? null,
|
|
});
|
|
|
|
const normalizeCondition = (condition) => ({
|
|
id: parseInt(condition?.id ?? condition?.condition_id ?? 0),
|
|
condition_id: condition?.condition_id ? parseInt(condition.condition_id) : null,
|
|
name: condition?.name ?? "",
|
|
description: condition?.description ?? "",
|
|
});
|
|
|
|
const normalizeRule = (rule) => ({
|
|
id: parseInt(rule?.id ?? 0),
|
|
condition_id: parseInt(rule?.condition_id ?? 0),
|
|
object_type: rule?.object_type ?? "question",
|
|
object_id: parseInt(rule?.object_id ?? 0),
|
|
type: rule?.type ?? "",
|
|
name: rule?.name ?? "",
|
|
description: rule?.description ?? "",
|
|
});
|
|
|
|
const normalizeTask = (task) => ({
|
|
id: parseInt(task?.id ?? task?.task_id ?? 0) || null,
|
|
task_id: parseInt(task?.task_id ?? task?.id ?? 0) || null,
|
|
task: task?.task ?? "",
|
|
description: task?.description ?? "",
|
|
condition_id: task?.condition_id ?? null,
|
|
gate_type: task?.gate_type ?? null,
|
|
gate_ref_id: task?.gate_ref_id ?? null,
|
|
order_priority: parseInt(task?.order_priority ?? 0),
|
|
services: Array.isArray(task?.services) ? task.services : [],
|
|
buttons: Array.isArray(task?.buttons) ? task.buttons : [],
|
|
attachments: Array.isArray(task?.attachments) ? task.attachments : [],
|
|
dynamic_images_vehicle_type: task?.dynamic_images_vehicle_type ?? null,
|
|
});
|
|
|
|
const extractTaskId = (task) => parseInt(task?.task_id ?? task?.id ?? 0) || null;
|
|
const extractVehicleConditionQuestionId = (condition) => (
|
|
parseInt(condition?.question?.id ?? condition?.question_id ?? condition?.question ?? 0) || null
|
|
);
|
|
|
|
const buildAnswersMap = (questions) => questions.reduce((accumulator, question) => {
|
|
accumulator[question.id] = question.answer;
|
|
return accumulator;
|
|
}, {});
|
|
|
|
const hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object || {}, key);
|
|
|
|
const isBooleanAnswer = (value) => value === true || value === false;
|
|
|
|
const sortByOrderPriority = (list) => [...list].sort((a, b) => {
|
|
const priorityDiff = parseInt(a?.order_priority ?? 0) - parseInt(b?.order_priority ?? 0);
|
|
if (priorityDiff !== 0) {
|
|
return priorityDiff;
|
|
}
|
|
return parseInt(a?.id ?? 0) - parseInt(b?.id ?? 0);
|
|
});
|
|
|
|
const normalizePositiveInt = (value) => {
|
|
const parsed = parseInt(value);
|
|
return !Number.isNaN(parsed) && parsed > 0 ? parsed : null;
|
|
};
|
|
|
|
const extractErrorMessage = (error, fallback = "Kunne ikke hente selvvaskdata. Prov igen.") => {
|
|
const candidates = [
|
|
error?.response?.data?.data?.message,
|
|
error?.response?.data?.message,
|
|
error?.response?.data?.error,
|
|
error?.data?.data?.message,
|
|
error?.data?.message,
|
|
error?.message,
|
|
];
|
|
|
|
const message = candidates.find((candidate) => (
|
|
typeof candidate === "string" && candidate.trim() !== ""
|
|
));
|
|
|
|
return message || fallback;
|
|
};
|
|
|
|
const extractResolvedVehicleTypeId = (payload) => {
|
|
if (!payload || typeof payload !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const candidates = [
|
|
payload?.vehicle_type_id,
|
|
payload?.session?.vehicle_type_id,
|
|
payload?.vehicle?.vehicle_type_id,
|
|
payload?.vehicle?.type_id,
|
|
payload?.vehicle?.type?.id,
|
|
payload?.vehicle?.type,
|
|
];
|
|
|
|
for (const candidate of candidates) {
|
|
const normalizedCandidate = normalizePositiveInt(candidate);
|
|
if (normalizedCandidate !== null) {
|
|
return normalizedCandidate;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const mergeQuestion = (existingQuestion, incomingQuestion, incomingRaw = {}) => {
|
|
if (!existingQuestion) {
|
|
return incomingQuestion;
|
|
}
|
|
|
|
const shouldUseIncomingAnswer = hasOwn(incomingRaw, "answer")
|
|
&& (isBooleanAnswer(incomingQuestion.answer) || !isBooleanAnswer(existingQuestion.answer));
|
|
|
|
return {
|
|
...existingQuestion,
|
|
...incomingQuestion,
|
|
question: hasOwn(incomingRaw, "question") ? incomingQuestion.question : existingQuestion.question,
|
|
description: hasOwn(incomingRaw, "description") ? incomingQuestion.description : existingQuestion.description,
|
|
condition_id: hasOwn(incomingRaw, "condition_id") ? incomingQuestion.condition_id : existingQuestion.condition_id,
|
|
order_priority: hasOwn(incomingRaw, "order_priority") ? incomingQuestion.order_priority : existingQuestion.order_priority,
|
|
answer: shouldUseIncomingAnswer ? incomingQuestion.answer : existingQuestion.answer,
|
|
answered_at: hasOwn(incomingRaw, "answered_at") ? incomingQuestion.answered_at : existingQuestion.answered_at,
|
|
};
|
|
};
|
|
|
|
const mergeQuestionSets = (existingQuestions, incomingQuestions, incomingRawQuestions = []) => {
|
|
if (!Array.isArray(existingQuestions) || existingQuestions.length === 0) {
|
|
return sortByOrderPriority(incomingQuestions);
|
|
}
|
|
|
|
if (!Array.isArray(incomingQuestions) || incomingQuestions.length === 0) {
|
|
return sortByOrderPriority(existingQuestions);
|
|
}
|
|
|
|
const incomingMap = incomingQuestions.reduce((accumulator, question, index) => {
|
|
accumulator[question.id] = {
|
|
question,
|
|
raw: incomingRawQuestions[index] || {},
|
|
};
|
|
return accumulator;
|
|
}, {});
|
|
|
|
const merged = existingQuestions.map((question) => {
|
|
const incomingEntry = incomingMap[question.id];
|
|
if (!incomingEntry) {
|
|
return question;
|
|
}
|
|
|
|
return mergeQuestion(question, incomingEntry.question, incomingEntry.raw);
|
|
});
|
|
|
|
incomingQuestions.forEach((question, index) => {
|
|
const exists = merged.some((entry) => entry.id === question.id);
|
|
if (!exists) {
|
|
merged.push(mergeQuestion(null, question, incomingRawQuestions[index] || {}));
|
|
}
|
|
});
|
|
|
|
return sortByOrderPriority(merged);
|
|
};
|
|
|
|
const mergeByNumericId = (existingItems, incomingItems) => {
|
|
if (!Array.isArray(existingItems) || existingItems.length === 0) {
|
|
return Array.isArray(incomingItems) ? [...incomingItems] : [];
|
|
}
|
|
|
|
if (!Array.isArray(incomingItems) || incomingItems.length === 0) {
|
|
return [...existingItems];
|
|
}
|
|
|
|
const merged = [...existingItems];
|
|
incomingItems.forEach((incomingItem) => {
|
|
const incomingId = parseInt(incomingItem?.id ?? 0);
|
|
if (!incomingId) {
|
|
merged.push(incomingItem);
|
|
return;
|
|
}
|
|
|
|
const existingIndex = merged.findIndex((item) => parseInt(item?.id ?? 0) === incomingId);
|
|
if (existingIndex === -1) {
|
|
merged.push(incomingItem);
|
|
return;
|
|
}
|
|
|
|
merged[existingIndex] = {
|
|
...merged[existingIndex],
|
|
...incomingItem,
|
|
};
|
|
});
|
|
|
|
return merged;
|
|
};
|
|
|
|
export function useSelfServeLogic() {
|
|
const loading = ref(false);
|
|
const requestError = ref(null);
|
|
const preview = ref(null);
|
|
const summary = ref(null);
|
|
const lane = ref(null);
|
|
const machineType = ref(null);
|
|
const vehicle = ref(null);
|
|
const session = ref(null);
|
|
const events = ref([]);
|
|
const questions = ref([]);
|
|
const conditions = ref([]);
|
|
const rules = ref([]);
|
|
const tasks = ref([]);
|
|
const answers = ref({});
|
|
const completedTasks = ref({});
|
|
const allowedServices = ref([]);
|
|
const configVersionId = ref(null);
|
|
const evaluationTrace = ref(null);
|
|
const lastPreviewContextKey = ref(null);
|
|
const lastVehicleTypeOverride = ref(null);
|
|
const lastResolvedVehicleTypeId = ref(null);
|
|
const summaryVisibleQuestionIds = ref([]);
|
|
const summaryQuestionOrder = ref({});
|
|
const latestFetchRequestId = ref(0);
|
|
|
|
const beginFetchRequest = () => {
|
|
latestFetchRequestId.value += 1;
|
|
return latestFetchRequestId.value;
|
|
};
|
|
|
|
const isFetchRequestActive = (requestId) => (
|
|
requestId === null
|
|
|| requestId === undefined
|
|
|| requestId === latestFetchRequestId.value
|
|
);
|
|
|
|
const setSummaryVisibleQuestions = (questionList = []) => {
|
|
const normalizedIds = (Array.isArray(questionList) ? questionList : [])
|
|
.map((question) => parseInt(question?.id ?? 0))
|
|
.filter((id) => id > 0);
|
|
|
|
summaryVisibleQuestionIds.value = normalizedIds;
|
|
summaryQuestionOrder.value = normalizedIds.reduce((accumulator, id, index) => {
|
|
accumulator[id] = index;
|
|
return accumulator;
|
|
}, {});
|
|
};
|
|
|
|
const updateResolvedVehicleTypeId = (...sources) => {
|
|
for (const source of sources) {
|
|
const resolvedVehicleTypeId = extractResolvedVehicleTypeId(source);
|
|
if (resolvedVehicleTypeId !== null) {
|
|
lastResolvedVehicleTypeId.value = resolvedVehicleTypeId;
|
|
return resolvedVehicleTypeId;
|
|
}
|
|
}
|
|
|
|
return lastResolvedVehicleTypeId.value;
|
|
};
|
|
|
|
const isImage = (attachment) => {
|
|
const filename = attachment?.content?.other || "";
|
|
const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg"];
|
|
const extension = filename.split(".").pop()?.toLowerCase();
|
|
return imageExtensions.includes(extension);
|
|
};
|
|
|
|
const hydrateTaskAttachments = async (taskList) => {
|
|
return Promise.all(taskList.map(async (task) => {
|
|
const taskId = extractTaskId(task);
|
|
|
|
if (!taskId) {
|
|
return {
|
|
...task,
|
|
attachments: Array.isArray(task.attachments) ? task.attachments : [],
|
|
};
|
|
}
|
|
|
|
try {
|
|
const response = await SessionUser.objects.self_serve_tasks.attachments.list(taskId);
|
|
const attachmentList = Array.isArray(response?.data) ? response.data : Array.isArray(response) ? response : [];
|
|
|
|
const attachments = await Promise.all(attachmentList.map(async (attachment) => {
|
|
if (!isImage(attachment)) {
|
|
return attachment;
|
|
}
|
|
|
|
try {
|
|
const downloadResponse = await SessionUser.objects.self_serve_tasks.attachments.download(taskId, attachment.id);
|
|
return {
|
|
...attachment,
|
|
download_link: downloadResponse?.data?.download_link || downloadResponse?.download_link || null,
|
|
};
|
|
} catch (error) {
|
|
console.error(`Error fetching attachment download link for task ${taskId}:`, error);
|
|
return attachment;
|
|
}
|
|
}));
|
|
|
|
return {
|
|
...task,
|
|
attachments,
|
|
};
|
|
} catch (error) {
|
|
console.error(`Error loading attachments for task ${taskId}:`, error);
|
|
return {
|
|
...task,
|
|
attachments: [],
|
|
};
|
|
}
|
|
}));
|
|
};
|
|
|
|
const applyPreviewData = async (previewData, options = {}) => {
|
|
const { mergeQuestions = false, requestId = null } = options;
|
|
if (!isFetchRequestActive(requestId)) {
|
|
return false;
|
|
}
|
|
|
|
preview.value = previewData || null;
|
|
lane.value = previewData?.lane || lane.value;
|
|
machineType.value = previewData?.machine_type || machineType.value;
|
|
vehicle.value = previewData?.vehicle || null;
|
|
session.value = previewData?.session || session.value;
|
|
if (Object.prototype.hasOwnProperty.call(previewData || {}, "config_version_id")) {
|
|
configVersionId.value = previewData?.config_version_id ?? null;
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(previewData || {}, "evaluation_trace")) {
|
|
evaluationTrace.value = previewData?.evaluation_trace ?? null;
|
|
}
|
|
|
|
const previewQuestionsRaw = Array.isArray(previewData?.questions) ? previewData.questions : [];
|
|
const normalizedQuestions = previewQuestionsRaw.length > 0
|
|
? previewQuestionsRaw.map(normalizeQuestion)
|
|
: [];
|
|
|
|
const nextQuestions = mergeQuestions
|
|
? mergeQuestionSets(questions.value, normalizedQuestions, previewQuestionsRaw)
|
|
: normalizedQuestions;
|
|
|
|
questions.value = nextQuestions;
|
|
answers.value = buildAnswersMap(nextQuestions);
|
|
|
|
const normalizedConditions = Array.isArray(previewData?.conditions)
|
|
? previewData.conditions.map(normalizeCondition)
|
|
: [];
|
|
const normalizedRules = Array.isArray(previewData?.rules)
|
|
? previewData.rules.map(normalizeRule)
|
|
: [];
|
|
|
|
conditions.value = mergeQuestions
|
|
? mergeByNumericId(conditions.value, normalizedConditions)
|
|
: normalizedConditions;
|
|
rules.value = mergeQuestions
|
|
? mergeByNumericId(rules.value, normalizedRules)
|
|
: normalizedRules;
|
|
|
|
const normalizedTasks = Array.isArray(previewData?.tasks)
|
|
? previewData.tasks.map(normalizeTask).sort((a, b) => a.order_priority - b.order_priority)
|
|
: [];
|
|
|
|
const hydratedTasks = await hydrateTaskAttachments(normalizedTasks);
|
|
if (!isFetchRequestActive(requestId)) {
|
|
return false;
|
|
}
|
|
|
|
tasks.value = hydratedTasks;
|
|
allowedServices.value = Array.isArray(previewData?.allowed_services)
|
|
? previewData.allowed_services
|
|
: [];
|
|
return true;
|
|
};
|
|
|
|
const applySummaryData = async (summaryData, options = {}) => {
|
|
const { requestId = null } = options;
|
|
if (!isFetchRequestActive(requestId)) {
|
|
return false;
|
|
}
|
|
|
|
summary.value = summaryData || null;
|
|
session.value = summaryData?.session || session.value;
|
|
lane.value = summaryData?.lane || lane.value;
|
|
machineType.value = summaryData?.machine_type || machineType.value;
|
|
events.value = Array.isArray(summaryData?.events) ? summaryData.events : [];
|
|
if (Object.prototype.hasOwnProperty.call(summaryData || {}, "config_version_id")) {
|
|
configVersionId.value = summaryData?.config_version_id ?? null;
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(summaryData || {}, "evaluation_trace")) {
|
|
evaluationTrace.value = summaryData?.evaluation_trace ?? null;
|
|
}
|
|
|
|
if (Array.isArray(summaryData?.questions)) {
|
|
const summaryQuestionsRaw = summaryData.questions;
|
|
const normalizedQuestions = summaryQuestionsRaw.map(normalizeQuestion);
|
|
const nextQuestions = mergeQuestionSets(questions.value, normalizedQuestions, summaryQuestionsRaw);
|
|
questions.value = nextQuestions;
|
|
answers.value = buildAnswersMap(nextQuestions);
|
|
setSummaryVisibleQuestions(normalizedQuestions);
|
|
} else {
|
|
setSummaryVisibleQuestions([]);
|
|
}
|
|
|
|
if (Array.isArray(summaryData?.conditions)) {
|
|
conditions.value = mergeByNumericId(conditions.value, summaryData.conditions.map(normalizeCondition));
|
|
}
|
|
|
|
if (Array.isArray(summaryData?.rules)) {
|
|
rules.value = mergeByNumericId(rules.value, summaryData.rules.map(normalizeRule));
|
|
}
|
|
|
|
if (Array.isArray(summaryData?.tasks)) {
|
|
const normalizedTasks = summaryData.tasks
|
|
.map(normalizeTask)
|
|
.sort((a, b) => a.order_priority - b.order_priority);
|
|
|
|
const hydratedTasks = await hydrateTaskAttachments(normalizedTasks);
|
|
if (!isFetchRequestActive(requestId)) {
|
|
return false;
|
|
}
|
|
|
|
tasks.value = hydratedTasks;
|
|
allowedServices.value = Array.from(new Set(normalizedTasks.flatMap((task) => task.services || [])));
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const downloadAttachment = async (taskId, attachmentId) => {
|
|
try {
|
|
const response = await SessionUser.objects.self_serve_tasks.attachments.download(taskId, attachmentId);
|
|
if (response?.data?.download_link) {
|
|
window.open(response.data.download_link, "_blank");
|
|
} else if (response?.download_link) {
|
|
window.open(response.download_link, "_blank");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error downloading attachment:", error);
|
|
}
|
|
};
|
|
|
|
const fetchWashSummary = async (params = {}, manageLoading = true, options = {}) => {
|
|
const { requestId = null } = options;
|
|
if (!params.session_id && !(params.lane_id && params.reg)) {
|
|
return null;
|
|
}
|
|
|
|
if (manageLoading) {
|
|
loading.value = true;
|
|
}
|
|
|
|
try {
|
|
const summaryData = await SessionUser.objects.self_serve_vehicle_conditions.get.washSummary(params);
|
|
const didApply = await applySummaryData(summaryData, { requestId });
|
|
if (!didApply) {
|
|
return null;
|
|
}
|
|
requestError.value = null;
|
|
return summaryData;
|
|
} catch (error) {
|
|
console.error("Error fetching self-serve summary:", error);
|
|
if (isFetchRequestActive(requestId)) {
|
|
requestError.value = extractErrorMessage(error, "Kunne ikke hente vaskestatus. Prov igen.");
|
|
}
|
|
return null;
|
|
} finally {
|
|
if (manageLoading) {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
const fetchSelfServeData = async (_departmentId, _vehicleTypeId, laneId = null, reg = null) => {
|
|
const requestId = beginFetchRequest();
|
|
|
|
if (!laneId || !reg || reg.trim().length < 2) {
|
|
preview.value = null;
|
|
summary.value = null;
|
|
session.value = null;
|
|
lane.value = null;
|
|
machineType.value = null;
|
|
vehicle.value = null;
|
|
events.value = [];
|
|
questions.value = [];
|
|
conditions.value = [];
|
|
rules.value = [];
|
|
tasks.value = [];
|
|
allowedServices.value = [];
|
|
answers.value = {};
|
|
configVersionId.value = null;
|
|
evaluationTrace.value = null;
|
|
lastPreviewContextKey.value = null;
|
|
lastVehicleTypeOverride.value = null;
|
|
lastResolvedVehicleTypeId.value = null;
|
|
summaryVisibleQuestionIds.value = [];
|
|
summaryQuestionOrder.value = {};
|
|
loading.value = false;
|
|
return null;
|
|
}
|
|
|
|
loading.value = true;
|
|
requestError.value = null;
|
|
try {
|
|
const normalizedReg = reg.trim().toUpperCase();
|
|
const normalizedVehicleTypeId = parseInt(_vehicleTypeId);
|
|
const hasVehicleTypeOverride = !Number.isNaN(normalizedVehicleTypeId) && normalizedVehicleTypeId > 0;
|
|
lastVehicleTypeOverride.value = hasVehicleTypeOverride ? normalizedVehicleTypeId : null;
|
|
if (hasVehicleTypeOverride) {
|
|
lastResolvedVehicleTypeId.value = normalizedVehicleTypeId;
|
|
}
|
|
const contextVehicleTypeKey = hasVehicleTypeOverride
|
|
? normalizedVehicleTypeId
|
|
: (lastResolvedVehicleTypeId.value ?? "auto");
|
|
const contextKey = `${parseInt(laneId)}:${normalizedReg}:${contextVehicleTypeKey}`;
|
|
const shouldMergeQuestions = lastPreviewContextKey.value === contextKey;
|
|
const previewData = hasVehicleTypeOverride
|
|
? await SessionUser.objects.self_serve_vehicle_conditions.get.previewAllowed(laneId, normalizedReg, normalizedVehicleTypeId)
|
|
: await SessionUser.objects.self_serve_vehicle_conditions.get.previewAllowed(laneId, normalizedReg);
|
|
|
|
if (!isFetchRequestActive(requestId)) {
|
|
return null;
|
|
}
|
|
|
|
const didApplyPreview = await applyPreviewData(previewData, {
|
|
mergeQuestions: shouldMergeQuestions,
|
|
requestId,
|
|
});
|
|
if (!didApplyPreview || !isFetchRequestActive(requestId)) {
|
|
return null;
|
|
}
|
|
|
|
updateResolvedVehicleTypeId(previewData, previewData?.session, previewData?.vehicle);
|
|
const resolvedContextVehicleTypeKey = hasVehicleTypeOverride
|
|
? normalizedVehicleTypeId
|
|
: (lastResolvedVehicleTypeId.value ?? "auto");
|
|
lastPreviewContextKey.value = `${parseInt(laneId)}:${normalizedReg}:${resolvedContextVehicleTypeKey}`;
|
|
|
|
let summaryData = null;
|
|
if (previewData?.session?.id) {
|
|
summaryData = await fetchWashSummary(
|
|
{ session_id: previewData.session.id },
|
|
false,
|
|
{ requestId }
|
|
);
|
|
}
|
|
|
|
if (!isFetchRequestActive(requestId)) {
|
|
return null;
|
|
}
|
|
|
|
updateResolvedVehicleTypeId(summaryData, summaryData?.session);
|
|
let hasSummaryQuestions = Array.isArray(summaryData?.questions) && summaryData.questions.length > 0;
|
|
const shouldRunFallbackSummary = !!previewData?.session?.id
|
|
&& (!hasSummaryQuestions || !hasVehicleTypeOverride);
|
|
if (shouldRunFallbackSummary) {
|
|
const fallbackSummaryParams = {
|
|
lane_id: parseInt(laneId),
|
|
reg: normalizedReg,
|
|
};
|
|
const fallbackVehicleTypeId = hasVehicleTypeOverride
|
|
? normalizedVehicleTypeId
|
|
: updateResolvedVehicleTypeId(previewData, summaryData, session.value, vehicle.value);
|
|
if (fallbackVehicleTypeId !== null) {
|
|
fallbackSummaryParams.vehicle_type = fallbackVehicleTypeId;
|
|
}
|
|
const fallbackSummary = await fetchWashSummary(
|
|
fallbackSummaryParams,
|
|
false,
|
|
{ requestId }
|
|
);
|
|
if (!isFetchRequestActive(requestId)) {
|
|
return null;
|
|
}
|
|
|
|
updateResolvedVehicleTypeId(fallbackSummary, fallbackSummary?.session);
|
|
hasSummaryQuestions = Array.isArray(fallbackSummary?.questions) && fallbackSummary.questions.length > 0;
|
|
}
|
|
|
|
// If summary is not available yet, keep the preview list visible so the Questions step is not empty.
|
|
if (!hasSummaryQuestions && isFetchRequestActive(requestId)) {
|
|
setSummaryVisibleQuestions(questions.value);
|
|
}
|
|
|
|
return previewData;
|
|
} catch (error) {
|
|
console.error("Error fetching self-serve preview:", error);
|
|
if (isFetchRequestActive(requestId)) {
|
|
requestError.value = extractErrorMessage(error);
|
|
}
|
|
return null;
|
|
} finally {
|
|
if (isFetchRequestActive(requestId)) {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
const syncVehicleAnswer = async ({ departmentId, laneId, customerNumber = null, reg, questionId, value, vehicleTypeId = undefined }) => {
|
|
if (!departmentId || !laneId || !reg || !questionId) {
|
|
return null;
|
|
}
|
|
|
|
loading.value = true;
|
|
try {
|
|
const normalizedReg = reg.trim().toUpperCase();
|
|
const response = await SessionUser.objects.self_serve_vehicle_conditions.add(
|
|
departmentId,
|
|
laneId,
|
|
customerNumber || 0,
|
|
normalizedReg,
|
|
questionId,
|
|
value
|
|
);
|
|
|
|
const payload = response?.data?.data || response?.data || response || {};
|
|
const responseSummary = payload?.selfserve || null;
|
|
|
|
answers.value = {
|
|
...answers.value,
|
|
[parseInt(questionId)]: value,
|
|
};
|
|
|
|
const refreshVehicleTypeCandidate = vehicleTypeId === undefined || vehicleTypeId === null
|
|
? (lastVehicleTypeOverride.value ?? lastResolvedVehicleTypeId.value)
|
|
: vehicleTypeId;
|
|
const normalizedRefreshVehicleTypeId = parseInt(refreshVehicleTypeCandidate);
|
|
const refreshVehicleTypeId = !Number.isNaN(normalizedRefreshVehicleTypeId) && normalizedRefreshVehicleTypeId > 0
|
|
? normalizedRefreshVehicleTypeId
|
|
: null;
|
|
|
|
if (responseSummary) {
|
|
await applySummaryData(responseSummary);
|
|
updateResolvedVehicleTypeId(responseSummary, responseSummary?.session);
|
|
}
|
|
|
|
await fetchSelfServeData(departmentId, refreshVehicleTypeId, laneId, normalizedReg);
|
|
answers.value = {
|
|
...answers.value,
|
|
[parseInt(questionId)]: value,
|
|
};
|
|
return payload;
|
|
} catch (error) {
|
|
console.error("Error synchronizing vehicle answer:", error);
|
|
requestError.value = extractErrorMessage(error, "Kunne ikke gemme svaret. Prov igen.");
|
|
throw error;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const clearVehicleAnswers = async ({ departmentId = null, laneId, reg, questionIds = [], vehicleTypeId = null }) => {
|
|
if (!laneId || !reg || !reg.trim()) {
|
|
return { deletedCount: 0 };
|
|
}
|
|
|
|
loading.value = true;
|
|
try {
|
|
const normalizedReg = reg.trim().toUpperCase();
|
|
const filters = {
|
|
lane: parseInt(laneId),
|
|
reg: normalizedReg,
|
|
};
|
|
|
|
if (departmentId !== null && departmentId !== undefined && !Number.isNaN(parseInt(departmentId))) {
|
|
filters.department = parseInt(departmentId);
|
|
}
|
|
|
|
const conditions = await SessionUser.objects.self_serve_vehicle_conditions.get.all(filters);
|
|
const normalizedQuestionIds = new Set(
|
|
(Array.isArray(questionIds) ? questionIds : [])
|
|
.map((id) => parseInt(id))
|
|
.filter((id) => !Number.isNaN(id) && id > 0)
|
|
);
|
|
|
|
const conditionIdsToDelete = (Array.isArray(conditions) ? conditions : [])
|
|
.filter((condition) => {
|
|
if (normalizedQuestionIds.size === 0) {
|
|
return true;
|
|
}
|
|
|
|
const conditionQuestionId = extractVehicleConditionQuestionId(condition);
|
|
return conditionQuestionId !== null && normalizedQuestionIds.has(conditionQuestionId);
|
|
})
|
|
.map((condition) => parseInt(condition?.id ?? 0))
|
|
.filter((id) => !Number.isNaN(id) && id > 0);
|
|
|
|
await Promise.all(conditionIdsToDelete.map((conditionId) => (
|
|
SessionUser.objects.self_serve_vehicle_conditions.delete(conditionId)
|
|
)));
|
|
|
|
// Force a non-merge refresh so cleared answers are not kept from local state.
|
|
lastPreviewContextKey.value = null;
|
|
const refreshVehicleTypeCandidate = vehicleTypeId !== null && vehicleTypeId !== undefined
|
|
? vehicleTypeId
|
|
: (lastVehicleTypeOverride.value ?? lastResolvedVehicleTypeId.value);
|
|
const normalizedRefreshVehicleTypeId = parseInt(refreshVehicleTypeCandidate);
|
|
const refreshVehicleTypeId = !Number.isNaN(normalizedRefreshVehicleTypeId) && normalizedRefreshVehicleTypeId > 0
|
|
? normalizedRefreshVehicleTypeId
|
|
: null;
|
|
|
|
await fetchSelfServeData(departmentId, refreshVehicleTypeId, parseInt(laneId), normalizedReg);
|
|
|
|
return { deletedCount: conditionIdsToDelete.length };
|
|
} catch (error) {
|
|
console.error("Error clearing self-serve answers:", error);
|
|
requestError.value = extractErrorMessage(error, "Kunne ikke nulstille svar. Prov igen.");
|
|
throw error;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const reset = () => {
|
|
completedTasks.value = {};
|
|
};
|
|
|
|
const answerQuestion = (questionId, value) => {
|
|
answers.value = {
|
|
...answers.value,
|
|
[questionId]: value,
|
|
};
|
|
};
|
|
|
|
const removeAnswer = (questionId) => {
|
|
const updatedAnswers = { ...answers.value };
|
|
delete updatedAnswers[questionId];
|
|
answers.value = updatedAnswers;
|
|
};
|
|
|
|
const visibleQuestions = computed(() => {
|
|
const allowedQuestionIds = new Set(Array.isArray(summaryVisibleQuestionIds.value) ? summaryVisibleQuestionIds.value : []);
|
|
const hasSummarySnapshot = summary.value !== null;
|
|
|
|
if (hasSummarySnapshot && allowedQuestionIds.size === 0) {
|
|
return [...questions.value]
|
|
.filter((question) => answers.value[question.id] === true || answers.value[question.id] === false)
|
|
.sort((a, b) => parseInt(a?.order_priority ?? 0) - parseInt(b?.order_priority ?? 0));
|
|
}
|
|
|
|
return [...questions.value]
|
|
.filter((question) => allowedQuestionIds.has(question.id))
|
|
.sort((a, b) => {
|
|
const indexA = summaryQuestionOrder.value[a.id];
|
|
const indexB = summaryQuestionOrder.value[b.id];
|
|
if (indexA !== undefined || indexB !== undefined) {
|
|
if (indexA === undefined) {
|
|
return 1;
|
|
}
|
|
if (indexB === undefined) {
|
|
return -1;
|
|
}
|
|
return indexA - indexB;
|
|
}
|
|
return parseInt(a?.order_priority ?? 0) - parseInt(b?.order_priority ?? 0);
|
|
});
|
|
});
|
|
|
|
const activeTasks = computed(() => {
|
|
return [...tasks.value]
|
|
.filter((task) => {
|
|
if (!task.condition_id || parseInt(task.condition_id) === 0) {
|
|
return true;
|
|
}
|
|
const hasRulesForCondition = rules.value.some((rule) => parseInt(rule.condition_id) === parseInt(task.condition_id));
|
|
if (!hasRulesForCondition) {
|
|
return true;
|
|
}
|
|
return evaluateCondition(task.condition_id);
|
|
})
|
|
.sort((a, b) => a.order_priority - b.order_priority);
|
|
});
|
|
|
|
const activeTaskServices = computed(() => {
|
|
if (allowedServices.value.length > 0) {
|
|
return allowedServices.value;
|
|
}
|
|
|
|
return Array.from(new Set(activeTasks.value.flatMap((task) => task.services || [])));
|
|
});
|
|
|
|
const currentQuestion = computed(() => {
|
|
return visibleQuestions.value.find((question) => answers.value[question.id] === null || typeof answers.value[question.id] === "undefined");
|
|
});
|
|
|
|
const allVisibleQuestionsAnswered = computed(() => visibleQuestions.value.every((question) => answers.value[question.id] === true || answers.value[question.id] === false));
|
|
const machineAvailable = computed(() => preview.value?.machine_available === true);
|
|
const allowed = computed(() => preview.value?.allowed === true || session.value?.allowed === true);
|
|
|
|
const evaluateCondition = (conditionId, visited = new Set()) => {
|
|
const normalizedConditionId = parseInt(conditionId);
|
|
if (!normalizedConditionId) {
|
|
return true;
|
|
}
|
|
if (visited.has(normalizedConditionId)) {
|
|
return false;
|
|
}
|
|
|
|
const conditionRules = rules.value.filter((rule) => parseInt(rule.condition_id) === normalizedConditionId);
|
|
if (conditionRules.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
const nextVisited = new Set(visited);
|
|
nextVisited.add(normalizedConditionId);
|
|
return conditionRules.every((rule) => evaluateRule(rule, nextVisited));
|
|
};
|
|
|
|
const evaluateRule = (rule, visited = new Set()) => {
|
|
const objectId = parseInt(rule?.object_id ?? 0);
|
|
let targetValue = null;
|
|
|
|
if (rule?.object_type === "condition") {
|
|
targetValue = evaluateCondition(objectId, visited);
|
|
} else {
|
|
targetValue = answers.value[objectId];
|
|
}
|
|
|
|
switch (rule?.type) {
|
|
case "IS_TRUE":
|
|
return targetValue === true;
|
|
case "IS_FALSE":
|
|
return targetValue === false;
|
|
case "IS_TRUE_OR_NOT_SET":
|
|
return targetValue === true || targetValue === null || typeof targetValue === "undefined";
|
|
case "IS_FALSE_OR_NOT_SET":
|
|
return targetValue === false || targetValue === null || typeof targetValue === "undefined";
|
|
case "IS_SET":
|
|
return targetValue === true || targetValue === false;
|
|
case "IS_TRUE_OR_ANY_TRUE":
|
|
if (targetValue === true) {
|
|
return true;
|
|
}
|
|
if (rule?.object_type === "condition") {
|
|
const nestedRules = rules.value.filter((entry) => parseInt(entry.condition_id) === objectId);
|
|
return nestedRules.some((entry) => evaluateRule(entry, new Set(visited)));
|
|
}
|
|
return false;
|
|
default:
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const isQuestionVisible = (question) => visibleQuestions.value.some((entry) => entry.id === question.id);
|
|
const isTaskActive = (taskId) => activeTasks.value.some((task) => extractTaskId(task) === parseInt(taskId));
|
|
|
|
const isServiceAllowed = (serviceName) => {
|
|
return activeTaskServices.value.includes(serviceName);
|
|
};
|
|
|
|
const updateLaneAllowedServices = async (laneId) => {
|
|
if (!laneId) return;
|
|
const taskIds = activeTasks.value.map((task) => extractTaskId(task)).filter(Boolean);
|
|
|
|
try {
|
|
const response = await SessionUser.request('/modules/self-serve/lane/services/allowed', 'post', {
|
|
lane_id: parseInt(laneId),
|
|
task_ids: taskIds,
|
|
});
|
|
|
|
allowedServices.value = response?.data?.data?.allowed_services || response?.data?.allowed_services || activeTaskServices.value;
|
|
return response;
|
|
} catch (error) {
|
|
console.error("Error updating lane allowed services:", error);
|
|
requestError.value = extractErrorMessage(error, "Kunne ikke opdatere vaskebanens tjenester. Prov igen.");
|
|
allowedServices.value = activeTaskServices.value;
|
|
}
|
|
};
|
|
|
|
const enableMachineRelay = async (laneId, duration = null) => {
|
|
if (!laneId) return;
|
|
try {
|
|
const payload = {
|
|
lane_id: parseInt(laneId),
|
|
};
|
|
if (duration !== null) {
|
|
payload.duration = parseInt(duration);
|
|
}
|
|
return await SessionUser.request('/modules/self-serve/lane/relay/machine/enable', 'post', payload);
|
|
} catch (error) {
|
|
console.error("Error enabling machine relay:", error);
|
|
requestError.value = extractErrorMessage(error, "Kunne ikke starte maskinen. Prov igen.");
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
return {
|
|
loading,
|
|
error: requestError,
|
|
preview,
|
|
summary,
|
|
lane,
|
|
machineType,
|
|
vehicle,
|
|
session,
|
|
events,
|
|
questions,
|
|
conditions,
|
|
rules,
|
|
tasks,
|
|
answers,
|
|
completedTasks,
|
|
allowedServices,
|
|
configVersionId,
|
|
evaluationTrace,
|
|
visibleQuestions,
|
|
activeTasks,
|
|
activeTaskServices,
|
|
currentQuestion,
|
|
allVisibleQuestionsAnswered,
|
|
machineAvailable,
|
|
allowed,
|
|
fetchSelfServeData,
|
|
fetchWashSummary,
|
|
syncVehicleAnswer,
|
|
clearVehicleAnswers,
|
|
evaluateRule,
|
|
evaluateCondition,
|
|
isQuestionVisible,
|
|
isTaskActive,
|
|
isServiceAllowed,
|
|
updateLaneAllowedServices,
|
|
enableMachineRelay,
|
|
isImage,
|
|
downloadAttachment,
|
|
reset,
|
|
answerQuestion,
|
|
removeAnswer,
|
|
};
|
|
}
|