Files
pleno-vue/src/composables/useSelfServeLogic.js
T

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,
};
}