## Why
In the superuser fakturaer-periode selvvask view, XL vask rows were
missing usable controls. Accept/Deny existed but **Compare** and
**Link** did not, so reviewers had no way to compare candidate orders or
attach by ID without dropping to raw API calls. Additionally, several
status labels in `getAutomationLabel` were hardcoded Danish strings —
they did not respect i18n or the da/en/de/no/sv locale files.
A legacy stub in `XLVaskUsageLog.vue` (`<template v-if="usage.WashItems
&& 1 === 2">`) permanently disabled the per-row wash items display.
## What changed
`src/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue`:
- New **Compare** button — `<b-modal>` side-by-side price view using
existing `duplicates` + `doesObjectHaveExactMatch`. Disabled when no
duplicates. Test IDs `xlvask-compare-{id}` and `xlvask-compare-modal`.
- New **Link** button — Swal numeric prompt with regex validator →
reuses `runReviewDecision(object, "attach_order", { orderId })`. Test ID
`xlvask-automation-link-{id}`.
- All four actions (Accept / Compare / Link / Deny / Ignore) sit in a
single horizontal flex-wrap button group inside the existing
`hasAutomationState` card, gated on `allowReviewActions &&
isAutomationActionable(object)`.
- Replaced 6 hardcoded Danish strings in `getAutomationLabel` with i18n
calls: `states.suggested_*`, `states.auto_accepted_*`,
`states.accepted_*`.
`src/i18n/source/global/shared/invoicing_period/xlvask_autopilot.json`
(and the 5 locale overrides) — added:
- `actions.compare`, `actions.link`
- `actions.compare_modal_title`, `actions.compare_modal_close`
- `actions.link_prompt_title`, `actions.link_prompt_label`,
`actions.link_prompt_invalid`
- `states.suggested_create_order`, `states.suggested_attach_order`,
`states.auto_accepted_create`, `states.auto_accepted_attach`,
`states.accepted_create`, `states.accepted_attach`
Regenerated the i18n bundle (`src/i18n/generated/*-v2.json`).
`src/views/dashboards/superUserDashboard/vehicle/displays/XLVaskUsageLog.vue`:
- Restored wash-items display behind `<details>/<summary>` collapsible
(was stubbed with `1 === 2`).
## Verification
- `npx eslint` — clean.
- `npm run i18n:v2:check` — all 4 sub-checks green.
Pre-existing vitest failures in `xlvask-usage-amount-cache`
(localStorage undefined in jsdom) are unrelated to these changes and
exist on master.
## Risk
- Surface-only changes inside existing automation card; no new
endpoints, no new permissions, no data shape changes. Backwards
compatible.
🤖 Generated with [OpenClaw](https://openclaw.ai)
---------
Co-authored-by: XL Vask Subagent <agent@truckwash.dk>
Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
1055 lines
42 KiB
Vue
1055 lines
42 KiB
Vue
<script setup>
|
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
|
const props = defineProps({
|
|
autoLoad: {
|
|
type: Boolean, default: true
|
|
}, setCustomerFilter: {
|
|
type: Number, default: 0
|
|
}, hideSearch: {
|
|
type: Boolean, default: false
|
|
}, title: {
|
|
type: String, default: ""
|
|
}, initialDateFrom: {
|
|
type: String, default: ""
|
|
}, initialDateTo: {
|
|
type: String, default: ""
|
|
}, inheritPeriodFilters: {
|
|
type: Boolean, default: false
|
|
}, loadAllAtOnce: {
|
|
type: Boolean, default: false
|
|
}, highlightUsageLogId: {
|
|
type: Number, default: 0
|
|
}, automationWorkspace: {
|
|
type: Boolean, default: false
|
|
}
|
|
})
|
|
import {computed, onMounted, onUnmounted, provide, ref, watch } from "vue";
|
|
import { useRouter } from "vue-router";
|
|
import {
|
|
PaginatedListKey,
|
|
usePaginatedList,
|
|
} from "@/components/pagination/paginatedList.vue";
|
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
|
|
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
|
import XlvaskUsageOrdersTable from "@/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue";
|
|
import PaginationDisplayTemplateDates
|
|
from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
|
|
import ShowErrorField from "@/components/global/ShowErrorField.vue";
|
|
import { useI18n } from 'vue-i18n'
|
|
import { BSwitch } from "buefy";
|
|
import Swal from "sweetalert2";
|
|
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
|
|
import { SELFWASH_PERIOD_ALL_LIMIT } from "@/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js";
|
|
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
|
|
import {
|
|
emptyXlvaskAutopilotSummary,
|
|
emptyXlvaskAutomationCapabilities,
|
|
emptyXlvaskAutomationReadiness,
|
|
isXlvaskAutopilotRunActive,
|
|
normalizeXlvaskAutomationCapabilities,
|
|
normalizeXlvaskAutomationReadiness,
|
|
normalizeXlvaskAutopilotRun,
|
|
normalizeXlvaskAutopilotSummary,
|
|
xlvaskAutopilotRunProgress,
|
|
} from "@/components/displays/department/pos/sync/xlvaskAutopilotUi.js";
|
|
|
|
const { t } = useI18n()
|
|
const router = useRouter();
|
|
const paginatedList = usePaginatedList();
|
|
provide(PaginatedListKey, paginatedList);
|
|
|
|
const {
|
|
isLoading,
|
|
list,
|
|
loadList,
|
|
metaCurrentPage,
|
|
metaItemsPerPage,
|
|
metaTotalItems,
|
|
setEndpoint,
|
|
setMetaItemsPerPage,
|
|
setPage,
|
|
search,
|
|
setFilter,
|
|
setOrder,
|
|
hideSearchField,
|
|
setHideSearchField,
|
|
} = paginatedList;
|
|
|
|
setEndpoint("/modules/xlvask/services/usage/orders", false);
|
|
if (props.loadAllAtOnce) {
|
|
setMetaItemsPerPage(SELFWASH_PERIOD_ALL_LIMIT, false);
|
|
}
|
|
const parseInitialDate = (value) => {
|
|
if (!value) {
|
|
return new Date();
|
|
}
|
|
|
|
const parsed = parseLocalDateOnly(value);
|
|
return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
|
|
};
|
|
|
|
const applyInitialPeriodFilters = () => {
|
|
if (props.initialDateFrom) {
|
|
setFilter("StartTime-date_from", props.initialDateFrom, false);
|
|
}
|
|
|
|
if (props.initialDateTo) {
|
|
setFilter("StartTime-date_to", props.initialDateTo, false);
|
|
}
|
|
};
|
|
|
|
const shouldShowLocalFilters = computed(() => !props.inheritPeriodFilters);
|
|
|
|
const titleText = computed(() => props.title || SessionUser.objects.orders.meta.title);
|
|
// If the customer filter is set, filter the orders by the customer number
|
|
if (props.setCustomerFilter > 0) {
|
|
setFilter("customer_number", props.setCustomerFilter);
|
|
}
|
|
|
|
// Hide the search field
|
|
if (props.hideSearch) {
|
|
setHideSearchField(true);
|
|
} else {
|
|
setHideSearchField(false);
|
|
}
|
|
|
|
// If the departmentId is set, in the route, filter the orders by the departmentId
|
|
if (router.currentRoute.value.params.departmentId) {
|
|
setOrder("StartTime", "desc");
|
|
//setFilter("department", router.currentRoute.value.params.departmentId, false);
|
|
} else {
|
|
setOrder("StartTime", "desc");
|
|
}
|
|
const isImportLoading = ref(false);
|
|
const summary = ref(emptyXlvaskAutopilotSummary());
|
|
const summaryLoading = ref(false);
|
|
const summaryError = ref("");
|
|
let summaryRequestSequence = 0;
|
|
const autopilotRun = ref(null);
|
|
const runError = ref("");
|
|
const capabilities = ref(emptyXlvaskAutomationCapabilities());
|
|
const readiness = ref(emptyXlvaskAutomationReadiness());
|
|
const controlStateLoading = ref(false);
|
|
const controlStateError = ref("");
|
|
const adminReadinessError = ref("");
|
|
const policyActionLoading = ref(false);
|
|
const activeRunCheckComplete = ref(false);
|
|
let controlStateRequestSequence = 0;
|
|
let activeRunRecoverySequence = 0;
|
|
let periodScopeSequence = 0;
|
|
let currentRunIdempotencyKey = "";
|
|
let currentRunFingerprint = "";
|
|
let runPollTimer = null;
|
|
let runPollSequence = 0;
|
|
let runPollingStartedAt = 0;
|
|
const runPollingPaused = ref(false);
|
|
const RUN_POLL_INTERVAL_MS = 2_000;
|
|
const RUN_POLL_MAX_DURATION_MS = 5 * 60 * 1_000;
|
|
|
|
const extractResponseData = (response) => response?.data?.data ?? response?.data ?? {};
|
|
|
|
const loadSummary = async () => {
|
|
const sequence = ++summaryRequestSequence;
|
|
const requestedFrom = props.initialDateFrom || formatLocalDateOnly(dateFrom.value);
|
|
const requestedTo = props.initialDateTo || formatLocalDateOnly(dateTo.value);
|
|
summaryLoading.value = true;
|
|
summaryError.value = "";
|
|
try {
|
|
const response = await SessionUser.request(
|
|
"/modules/xlvask/services/usage/orders/summary",
|
|
"GET",
|
|
{ dateFrom: requestedFrom, dateTo: requestedTo },
|
|
);
|
|
if (sequence !== summaryRequestSequence) return;
|
|
const data = extractResponseData(response);
|
|
summary.value = normalizeXlvaskAutopilotSummary(data.summary);
|
|
} catch (error) {
|
|
if (sequence !== summaryRequestSequence) return;
|
|
summary.value = emptyXlvaskAutopilotSummary();
|
|
summaryError.value = t("invoicing_period.xlvask_autopilot.summary_error");
|
|
console.error("Failed to load XL-Vask autopilot summary.", error);
|
|
} finally {
|
|
if (sequence === summaryRequestSequence) summaryLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const loadAutomationControlState = async () => {
|
|
if (!props.automationWorkspace) return;
|
|
const sequence = ++controlStateRequestSequence;
|
|
controlStateLoading.value = true;
|
|
controlStateError.value = "";
|
|
adminReadinessError.value = "";
|
|
try {
|
|
const capabilityResponse = await SessionUser.request(
|
|
"/modules/xlvask/services/usage/automation/capabilities",
|
|
"GET",
|
|
buildImportUsageParams(),
|
|
);
|
|
if (sequence !== controlStateRequestSequence) return;
|
|
const capabilityData = extractResponseData(capabilityResponse);
|
|
capabilities.value = normalizeXlvaskAutomationCapabilities(capabilityData);
|
|
readiness.value = normalizeXlvaskAutomationReadiness(capabilityData.readiness || capabilityData);
|
|
if (!capabilities.value.can_view) return;
|
|
if (capabilities.value.can_manage_policy) {
|
|
try {
|
|
const readinessResponse = await SessionUser.request(
|
|
"/modules/xlvask/services/usage/automation/admin/readiness",
|
|
"GET",
|
|
buildImportUsageParams(),
|
|
);
|
|
if (sequence !== controlStateRequestSequence) return;
|
|
readiness.value = normalizeXlvaskAutomationReadiness(extractResponseData(readinessResponse));
|
|
} catch (error) {
|
|
if (sequence !== controlStateRequestSequence) return;
|
|
adminReadinessError.value = t("invoicing_period.xlvask_autopilot.controls.readiness_error");
|
|
console.error("Failed to load XL-Vask administrator readiness.", error);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (sequence !== controlStateRequestSequence) return;
|
|
capabilities.value = emptyXlvaskAutomationCapabilities();
|
|
readiness.value = emptyXlvaskAutomationReadiness();
|
|
controlStateError.value = t("invoicing_period.xlvask_autopilot.controls.load_error");
|
|
console.error("Failed to load XL-Vask automation control state.", error);
|
|
} finally {
|
|
if (sequence === controlStateRequestSequence) controlStateLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const buildImportUsageParams = () => {
|
|
if (!props.inheritPeriodFilters) {
|
|
return {
|
|
dateFrom: formatLocalDateOnly(dateFrom.value),
|
|
dateTo: formatLocalDateOnly(dateTo.value),
|
|
};
|
|
}
|
|
|
|
return {
|
|
...(props.initialDateFrom ? { dateFrom: props.initialDateFrom } : {}),
|
|
...(props.initialDateTo ? { dateTo: props.initialDateTo } : {}),
|
|
};
|
|
};
|
|
|
|
const refreshAutopilotSurface = async () => {
|
|
await Promise.all([loadList(), loadSummary()]);
|
|
window.dispatchEvent(new CustomEvent("xlvask-usage-order-updated", {
|
|
detail: { runId: autopilotRun.value?.id ?? null, source: "autopilot" },
|
|
}));
|
|
};
|
|
|
|
const clearRunPollTimer = () => {
|
|
if (runPollTimer !== null) {
|
|
clearTimeout(runPollTimer);
|
|
runPollTimer = null;
|
|
}
|
|
};
|
|
|
|
const invalidateAutopilotScope = () => {
|
|
periodScopeSequence += 1;
|
|
runPollSequence += 1;
|
|
activeRunRecoverySequence += 1;
|
|
controlStateRequestSequence += 1;
|
|
activeRunCheckComplete.value = false;
|
|
clearRunPollTimer();
|
|
autopilotRun.value = null;
|
|
isImportLoading.value = false;
|
|
runPollingPaused.value = false;
|
|
runPollingStartedAt = 0;
|
|
runError.value = "";
|
|
currentRunIdempotencyKey = "";
|
|
currentRunFingerprint = "";
|
|
};
|
|
|
|
const pollAutopilotRun = async (runId, sequence) => {
|
|
if (!runId || sequence !== runPollSequence) return;
|
|
if (document.hidden) {
|
|
runPollingPaused.value = true;
|
|
return;
|
|
}
|
|
if (runPollingStartedAt > 0 && Date.now() - runPollingStartedAt >= RUN_POLL_MAX_DURATION_MS) {
|
|
runPollingPaused.value = true;
|
|
return;
|
|
}
|
|
try {
|
|
const response = await SessionUser.request(
|
|
`/modules/xlvask/services/usage/autopilot-runs/${runId}`,
|
|
"GET",
|
|
);
|
|
if (sequence !== runPollSequence) return;
|
|
const data = extractResponseData(response);
|
|
autopilotRun.value = normalizeXlvaskAutopilotRun(data.run);
|
|
if (isXlvaskAutopilotRunActive(autopilotRun.value)) {
|
|
runPollTimer = setTimeout(() => pollAutopilotRun(runId, sequence), RUN_POLL_INTERVAL_MS);
|
|
return;
|
|
}
|
|
isImportLoading.value = false;
|
|
runPollingPaused.value = false;
|
|
runPollingStartedAt = 0;
|
|
currentRunIdempotencyKey = "";
|
|
currentRunFingerprint = "";
|
|
await refreshAutopilotSurface();
|
|
await loadAutomationControlState();
|
|
} catch (error) {
|
|
if (sequence !== runPollSequence) return;
|
|
runPollingPaused.value = true;
|
|
runError.value = t("invoicing_period.xlvask_autopilot.run_status_error");
|
|
console.error("Failed to load XL-Vask autopilot run.", error);
|
|
}
|
|
};
|
|
|
|
const resumeAutopilotPolling = ({ resetWindow = true } = {}) => {
|
|
if (!isXlvaskAutopilotRunActive(autopilotRun.value) || document.hidden) return;
|
|
clearRunPollTimer();
|
|
if (resetWindow) runPollingStartedAt = Date.now();
|
|
runPollingPaused.value = false;
|
|
void pollAutopilotRun(autopilotRun.value.id, runPollSequence);
|
|
};
|
|
|
|
const handleVisibilityChange = () => {
|
|
if (!document.hidden && runPollingPaused.value && runPollingStartedAt > 0
|
|
&& Date.now() - runPollingStartedAt < RUN_POLL_MAX_DURATION_MS) {
|
|
resumeAutopilotPolling({ resetWindow: false });
|
|
}
|
|
};
|
|
|
|
const handleUsageOrderUpdated = (event) => {
|
|
if (event?.detail?.source === "adjudication") {
|
|
void Promise.all([loadSummary(), loadAutomationControlState()]);
|
|
return;
|
|
}
|
|
if (event?.detail?.source !== "autopilot") {
|
|
void loadSummary();
|
|
}
|
|
};
|
|
|
|
const createIdempotencyKey = () => {
|
|
if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID();
|
|
return `xlvask-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
};
|
|
|
|
const getRunIdempotencyKey = (mode, params) => {
|
|
const fingerprint = JSON.stringify({ mode, ...params });
|
|
if (fingerprint !== currentRunFingerprint || !currentRunIdempotencyKey) {
|
|
currentRunFingerprint = fingerprint;
|
|
currentRunIdempotencyKey = createIdempotencyKey();
|
|
}
|
|
return currentRunIdempotencyKey;
|
|
};
|
|
|
|
const buildExecutionReadinessSnapshot = () => ({
|
|
ready: readiness.value.ready,
|
|
worker_healthy: readiness.value.worker_healthy,
|
|
blocked_reasons: [...readiness.value.blocked_reasons],
|
|
can_execute: capabilities.value.can_execute,
|
|
allowed_modes: [...capabilities.value.allowed_modes],
|
|
policy_version: readiness.value.policy_version,
|
|
effective_stage: readiness.value.effective_stage,
|
|
eligible: readiness.value.eligible_counts.total,
|
|
attach_global: readiness.value.budgets.attach_order.remaining_global,
|
|
attach_hall_minimum: readiness.value.budgets.attach_order.remaining_hall,
|
|
create_global: readiness.value.budgets.create_order.remaining_global,
|
|
create_hall_minimum: readiness.value.budgets.create_order.remaining_hall,
|
|
sources: [...capabilities.value.effective_action_sources],
|
|
});
|
|
|
|
const canStartDryRun = computed(() => props.automationWorkspace
|
|
&& capabilities.value.can_dry_run
|
|
&& capabilities.value.allowed_modes.includes("dry_run")
|
|
&& activeRunCheckComplete.value
|
|
&& !isXlvaskAutopilotRunActive(autopilotRun.value));
|
|
|
|
const canStartExecute = computed(() => props.automationWorkspace
|
|
&& capabilities.value.can_execute
|
|
&& readiness.value.ready
|
|
&& capabilities.value.allowed_modes.includes("execute")
|
|
&& activeRunCheckComplete.value
|
|
&& !isXlvaskAutopilotRunActive(autopilotRun.value));
|
|
|
|
const startAutopilotRun = async (mode = "dry_run") => {
|
|
if (mode === "dry_run" ? !canStartDryRun.value : !canStartExecute.value) return;
|
|
const requestScopeSequence = periodScopeSequence;
|
|
const runParams = buildImportUsageParams();
|
|
const readinessSnapshot = buildExecutionReadinessSnapshot();
|
|
const readinessFingerprint = JSON.stringify(readinessSnapshot);
|
|
if (mode === "execute") {
|
|
const confirmationPhrase = t("invoicing_period.xlvask_autopilot.controls.execute_phrase");
|
|
const confirmation = await Swal.fire({
|
|
icon: "warning",
|
|
title: t("invoicing_period.xlvask_autopilot.controls.execute_title"),
|
|
text: t("invoicing_period.xlvask_autopilot.controls.execute_description", {
|
|
from: runParams.dateFrom || "—",
|
|
to: runParams.dateTo || "—",
|
|
eligible: readinessSnapshot.eligible,
|
|
attachGlobal: readinessSnapshot.attach_global,
|
|
attachHall: readinessSnapshot.attach_hall_minimum,
|
|
createGlobal: readinessSnapshot.create_global,
|
|
createHall: readinessSnapshot.create_hall_minimum,
|
|
sources: readinessSnapshot.sources.join(", ") || "—",
|
|
}),
|
|
input: "text",
|
|
inputLabel: t("invoicing_period.xlvask_autopilot.preview.confirmation_label", { phrase: confirmationPhrase }),
|
|
inputPlaceholder: confirmationPhrase,
|
|
showCancelButton: true,
|
|
confirmButtonText: t("invoicing_period.xlvask_autopilot.controls.execute"),
|
|
cancelButtonText: t("common.cancel"),
|
|
inputValidator: (value) => String(value ?? "").trim() === confirmationPhrase
|
|
? undefined
|
|
: t("invoicing_period.xlvask_autopilot.preview.confirmation_mismatch", { phrase: confirmationPhrase }),
|
|
});
|
|
if (!confirmation.isConfirmed) return;
|
|
if (requestScopeSequence !== periodScopeSequence) return;
|
|
if (readinessFingerprint !== JSON.stringify(buildExecutionReadinessSnapshot())) return;
|
|
if (!canStartExecute.value) return;
|
|
}
|
|
|
|
isImportLoading.value = true;
|
|
runError.value = "";
|
|
clearRunPollTimer();
|
|
const sequence = ++runPollSequence;
|
|
runPollingStartedAt = Date.now();
|
|
runPollingPaused.value = false;
|
|
try {
|
|
const idempotencyKey = getRunIdempotencyKey(mode, runParams);
|
|
const response = await SessionUser.request(
|
|
"/modules/xlvask/services/usage/autopilot-runs",
|
|
"POST",
|
|
{
|
|
...runParams,
|
|
mode,
|
|
forceRefetch: true,
|
|
idempotency_key: idempotencyKey,
|
|
},
|
|
);
|
|
if (requestScopeSequence !== periodScopeSequence) return;
|
|
const data = extractResponseData(response);
|
|
autopilotRun.value = normalizeXlvaskAutopilotRun(data.run);
|
|
if (!autopilotRun.value?.id) {
|
|
throw new Error("Autopilot run response did not contain a run id.");
|
|
}
|
|
if (isXlvaskAutopilotRunActive(autopilotRun.value)) {
|
|
await pollAutopilotRun(autopilotRun.value.id, sequence);
|
|
} else {
|
|
await refreshAutopilotSurface();
|
|
isImportLoading.value = false;
|
|
currentRunIdempotencyKey = "";
|
|
currentRunFingerprint = "";
|
|
}
|
|
} catch (error) {
|
|
if (requestScopeSequence !== periodScopeSequence) return;
|
|
runError.value = t("invoicing_period.xlvask_autopilot.run_start_error");
|
|
console.error("Failed to start XL-Vask autopilot.", error);
|
|
isImportLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const recoverActiveAutopilotRun = async () => {
|
|
if (!props.automationWorkspace || !capabilities.value.can_view) return;
|
|
if (!capabilities.value.can_dry_run && !capabilities.value.can_execute && !capabilities.value.can_review) {
|
|
activeRunCheckComplete.value = true;
|
|
return;
|
|
}
|
|
const recoverySequence = ++activeRunRecoverySequence;
|
|
activeRunCheckComplete.value = false;
|
|
try {
|
|
const response = await SessionUser.request(
|
|
"/modules/xlvask/services/usage/autopilot-runs/active",
|
|
"GET",
|
|
buildImportUsageParams(),
|
|
);
|
|
if (recoverySequence !== activeRunRecoverySequence) return;
|
|
const data = extractResponseData(response);
|
|
const run = normalizeXlvaskAutopilotRun(data.run);
|
|
activeRunCheckComplete.value = true;
|
|
if (!isXlvaskAutopilotRunActive(run)) return;
|
|
autopilotRun.value = run;
|
|
isImportLoading.value = true;
|
|
runPollSequence += 1;
|
|
runPollingStartedAt = Date.now();
|
|
resumeAutopilotPolling();
|
|
} catch (error) {
|
|
if (recoverySequence !== activeRunRecoverySequence) return;
|
|
activeRunCheckComplete.value = false;
|
|
controlStateError.value = t("invoicing_period.xlvask_autopilot.controls.active_run_error");
|
|
console.error("Failed to recover the active XL-Vask automation run.", error);
|
|
}
|
|
};
|
|
|
|
const NEXT_POLICY_STAGE = {
|
|
off: "advisory",
|
|
advisory: "ai_attach_canary",
|
|
ai_attach_canary: "ai_attach_verified",
|
|
ai_attach_verified: "ai_create_canary",
|
|
ai_create_canary: "verified_capped",
|
|
halted: "advisory",
|
|
};
|
|
|
|
const nextPolicyStage = computed(() => NEXT_POLICY_STAGE[readiness.value.effective_stage] || "");
|
|
const canAdvancePolicy = computed(() => capabilities.value.can_manage_policy
|
|
&& !adminReadinessError.value
|
|
&& Boolean(nextPolicyStage.value)
|
|
&& (readiness.value.ready || readiness.value.effective_stage === "halted"));
|
|
|
|
const applyPolicyPreview = async () => {
|
|
if (!canAdvancePolicy.value || policyActionLoading.value) return;
|
|
policyActionLoading.value = true;
|
|
try {
|
|
const reasonResult = await Swal.fire({
|
|
title: t("invoicing_period.xlvask_autopilot.controls.policy_reason_title"),
|
|
input: "textarea",
|
|
inputLabel: t("invoicing_period.xlvask_autopilot.preview.reason_label"),
|
|
showCancelButton: true,
|
|
confirmButtonText: t("common.continue"),
|
|
cancelButtonText: t("common.cancel"),
|
|
inputValidator: (value) => String(value ?? "").trim()
|
|
? undefined
|
|
: t("invoicing_period.xlvask_autopilot.preview.reason_required"),
|
|
});
|
|
if (!reasonResult.isConfirmed) return;
|
|
const previewResponse = await SessionUser.request(
|
|
"/modules/xlvask/services/usage/automation/admin/policy/previews",
|
|
"POST",
|
|
{
|
|
target_stage: nextPolicyStage.value,
|
|
expected_policy_version: readiness.value.policy_version,
|
|
reason: String(reasonResult.value).trim(),
|
|
},
|
|
);
|
|
const preview = extractResponseData(previewResponse).preview;
|
|
if (!preview?.id || !preview?.selection_hash || preview.expected_policy_version === undefined) {
|
|
throw new Error("Policy preview is incomplete.");
|
|
}
|
|
const phrase = String(preview.confirmation_phrase || t("invoicing_period.xlvask_autopilot.controls.policy_phrase")).trim();
|
|
const confirmation = await Swal.fire({
|
|
icon: "warning",
|
|
title: t("invoicing_period.xlvask_autopilot.controls.policy_title"),
|
|
text: t("invoicing_period.xlvask_autopilot.controls.policy_description", { stage: nextPolicyStage.value }),
|
|
input: preview.requires_confirmation === false ? undefined : "text",
|
|
inputLabel: preview.requires_confirmation === false
|
|
? undefined
|
|
: t("invoicing_period.xlvask_autopilot.preview.confirmation_label", { phrase }),
|
|
inputPlaceholder: phrase,
|
|
showCancelButton: true,
|
|
confirmButtonText: t("invoicing_period.xlvask_autopilot.controls.policy_apply"),
|
|
cancelButtonText: t("common.cancel"),
|
|
inputValidator: preview.requires_confirmation === false
|
|
? undefined
|
|
: (value) => String(value ?? "").trim() === phrase
|
|
? undefined
|
|
: t("invoicing_period.xlvask_autopilot.preview.confirmation_mismatch", { phrase }),
|
|
});
|
|
if (!confirmation.isConfirmed) return;
|
|
const response = await SessionUser.request(
|
|
"/modules/xlvask/services/usage/automation/admin/policy/apply",
|
|
"POST",
|
|
{
|
|
preview_id: preview.id,
|
|
selection_hash: preview.selection_hash,
|
|
...(preview.requires_confirmation === false ? {} : { confirmation_text: confirmation.value }),
|
|
expected_policy_version: preview.expected_policy_version,
|
|
},
|
|
);
|
|
const data = extractResponseData(response);
|
|
readiness.value = normalizeXlvaskAutomationReadiness(data.readiness || data);
|
|
await loadAutomationControlState();
|
|
} catch (error) {
|
|
await Swal.fire({
|
|
icon: "error",
|
|
title: t("invoicing_period.xlvask_autopilot.preview.error_title"),
|
|
text: SessionUser.functions.parseErrorMessage?.(error) || String(error),
|
|
});
|
|
} finally {
|
|
policyActionLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const haltAutomation = async () => {
|
|
if (!capabilities.value.can_halt || policyActionLoading.value) return;
|
|
const result = await Swal.fire({
|
|
icon: "warning",
|
|
title: t("invoicing_period.xlvask_autopilot.controls.halt_title"),
|
|
input: "textarea",
|
|
inputLabel: t("invoicing_period.xlvask_autopilot.preview.reason_label"),
|
|
showCancelButton: true,
|
|
confirmButtonText: t("invoicing_period.xlvask_autopilot.controls.halt"),
|
|
cancelButtonText: t("common.cancel"),
|
|
inputValidator: (value) => String(value ?? "").trim()
|
|
? undefined
|
|
: t("invoicing_period.xlvask_autopilot.preview.reason_required"),
|
|
});
|
|
if (!result.isConfirmed) return;
|
|
policyActionLoading.value = true;
|
|
try {
|
|
const response = await SessionUser.request(
|
|
"/modules/xlvask/services/usage/automation/admin/halt",
|
|
"POST",
|
|
{ reason: String(result.value).trim() },
|
|
);
|
|
const data = extractResponseData(response);
|
|
readiness.value = normalizeXlvaskAutomationReadiness(data.readiness || data);
|
|
await loadAutomationControlState();
|
|
} catch (error) {
|
|
await Swal.fire({
|
|
icon: "error",
|
|
title: t("invoicing_period.xlvask_autopilot.preview.error_title"),
|
|
text: SessionUser.functions.parseErrorMessage?.(error) || String(error),
|
|
});
|
|
} finally {
|
|
policyActionLoading.value = false;
|
|
}
|
|
};
|
|
const dateFrom = ref(parseInitialDate(props.initialDateFrom));
|
|
const dateTo = ref(parseInitialDate(props.initialDateTo));
|
|
|
|
const parsedDate = (date) => {
|
|
return parseLocalDateOnly(date);
|
|
};
|
|
|
|
const reloadScheduled = ref(false);
|
|
const scheduleReload = () => {
|
|
if (!reloadScheduled.value) {
|
|
reloadScheduled.value = true;
|
|
setTimeout(() => {
|
|
reloadScheduled.value = false;
|
|
loadList();
|
|
}, 20);
|
|
}
|
|
};
|
|
|
|
const actions = {
|
|
date: {
|
|
from: {
|
|
select: (date) => {
|
|
dateFrom.value = parsedDate(date);
|
|
setFilter("StartTime-date_from", formatLocalDateOnly(date), false);
|
|
scheduleReload();
|
|
}
|
|
},
|
|
to: {
|
|
select: (date) => {
|
|
dateTo.value = parsedDate(date);
|
|
setFilter("StartTime-date_to", formatLocalDateOnly(date), false);
|
|
scheduleReload();
|
|
}
|
|
},
|
|
}
|
|
};
|
|
|
|
applyInitialPeriodFilters();
|
|
// Load the list automatically if the autoLoad prop is set
|
|
if (props.autoLoad) {
|
|
loadList();
|
|
loadSummary();
|
|
}
|
|
|
|
watch(
|
|
() => [props.initialDateFrom, props.initialDateTo],
|
|
async ([nextDateFrom, nextDateTo], [previousDateFrom, previousDateTo]) => {
|
|
if (nextDateFrom === previousDateFrom && nextDateTo === previousDateTo) {
|
|
return;
|
|
}
|
|
|
|
invalidateAutopilotScope();
|
|
const scopeSequence = periodScopeSequence;
|
|
dateFrom.value = parseInitialDate(nextDateFrom);
|
|
dateTo.value = parseInitialDate(nextDateTo);
|
|
currentRunIdempotencyKey = "";
|
|
currentRunFingerprint = "";
|
|
applyInitialPeriodFilters();
|
|
|
|
if (props.autoLoad) {
|
|
loadList();
|
|
loadSummary();
|
|
}
|
|
if (props.automationWorkspace) {
|
|
await loadAutomationControlState();
|
|
if (scopeSequence !== periodScopeSequence) return;
|
|
await recoverActiveAutopilotRun();
|
|
}
|
|
}
|
|
);
|
|
|
|
onUnmounted(() => {
|
|
runPollSequence += 1;
|
|
clearRunPollTimer();
|
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
window.removeEventListener("xlvask-usage-order-updated", handleUsageOrderUpdated);
|
|
});
|
|
|
|
onMounted(async () => {
|
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
window.addEventListener("xlvask-usage-order-updated", handleUsageOrderUpdated);
|
|
if (props.automationWorkspace) {
|
|
const scopeSequence = periodScopeSequence;
|
|
await loadAutomationControlState();
|
|
if (scopeSequence !== periodScopeSequence) return;
|
|
await recoverActiveAutopilotRun();
|
|
}
|
|
});
|
|
|
|
const filterValues = ref({
|
|
import_state: "",
|
|
resolution_state: "",
|
|
certainty: "",
|
|
planned_action: "",
|
|
});
|
|
|
|
const applyAutopilotFilter = (key, value) => {
|
|
filterValues.value[key] = value;
|
|
setFilter(key, value, false);
|
|
setPage(1);
|
|
loadList();
|
|
};
|
|
|
|
const clearAutopilotFilters = () => {
|
|
Object.keys(filterValues.value).forEach((key) => {
|
|
filterValues.value[key] = "";
|
|
setFilter(key, "", false);
|
|
});
|
|
showOnlyUnattachedVehicle.value = false;
|
|
setPage(1);
|
|
loadList();
|
|
};
|
|
|
|
const summaryCards = computed(() => [
|
|
{ key: "new", count: summary.value.new, tone: "is-info" },
|
|
{ key: "updated", count: summary.value.updated, tone: "is-info" },
|
|
{ key: "unchanged", count: summary.value.unchanged, tone: "is-light" },
|
|
{ key: "already_linked", count: summary.value.already_linked, tone: "is-success" },
|
|
{ key: "auto_linked", count: summary.value.auto_linked, tone: "is-success" },
|
|
{ key: "auto_created", count: summary.value.auto_created, tone: "is-success" },
|
|
{ key: "certain", count: summary.value.certain, tone: "is-success" },
|
|
{ key: "uncertain", count: summary.value.uncertain, tone: "is-warning" },
|
|
{ key: "needs_review", count: summary.value.needs_review, tone: "is-warning" },
|
|
{ key: "blocked", count: summary.value.blocked, tone: "is-danger" },
|
|
{ key: "invalid", count: summary.value.invalid, tone: "is-danger" },
|
|
{ key: "ignored", count: summary.value.ignored, tone: "is-light" },
|
|
{ key: "failed", count: summary.value.failed, tone: "is-danger" },
|
|
]);
|
|
|
|
const runProgress = computed(() => xlvaskAutopilotRunProgress(autopilotRun.value));
|
|
const runStatusLabel = computed(() => {
|
|
if (!autopilotRun.value) return "";
|
|
const phase = autopilotRun.value.phase || autopilotRun.value.status;
|
|
return t(`invoicing_period.xlvask_autopilot.run_phases.${phase}`, phase);
|
|
});
|
|
|
|
const reviewProgressPercent = (action) => {
|
|
const progress = readiness.value.review_progress[action];
|
|
if (!progress?.target) return 0;
|
|
return Math.min(100, Math.round((progress.reviewed / progress.target) * 100));
|
|
};
|
|
|
|
const showOnlyUnattachedVehicle = ref(false);
|
|
const visibleObjectsCount = computed(() => {
|
|
const currentList = Array.isArray(list.value) ? list.value : [];
|
|
if (!showOnlyUnattachedVehicle.value) {
|
|
return currentList.length;
|
|
}
|
|
|
|
return currentList.filter((object) => !isUsageOrderAttachedToOrder(object)).length;
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="level mb-3">
|
|
<div class="level-left">
|
|
<div class="level-item">
|
|
<h1 class="title is-4">{{ titleText }} ({{SessionUser.objects.global.language.synchronized}})</h1>
|
|
</div>
|
|
<div class="level-item">
|
|
<h2 class="subtitle is-6">{{SessionUser.objects.global.language.showing}} {{ visibleObjectsCount }} {{SessionUser.objects.global.language.showing_of_separator}} {{ metaTotalItems }} {{SessionUser.objects.orders.meta.title.toLowerCase()}}</h2>
|
|
</div>
|
|
</div>
|
|
<div class="level-right">
|
|
<div v-if="props.automationWorkspace && capabilities.can_dry_run" class="level-item">
|
|
<LoadButtonWhileAwait
|
|
class="is-dark"
|
|
:isLoading="isImportLoading"
|
|
:disabled="!canStartDryRun"
|
|
:loadFunction="() => startAutopilotRun('dry_run')"
|
|
icon="fas fa-search"
|
|
data-testid="xlvask-autopilot-dry-run"
|
|
>{{ t('invoicing_period.xlvask_autopilot.controls.analyze') }}</LoadButtonWhileAwait>
|
|
</div>
|
|
<div v-if="props.automationWorkspace && capabilities.can_execute" class="level-item">
|
|
<LoadButtonWhileAwait
|
|
class="is-danger"
|
|
:isLoading="isImportLoading"
|
|
:disabled="!canStartExecute"
|
|
:loadFunction="() => startAutopilotRun('execute')"
|
|
icon="fas fa-robot"
|
|
data-testid="xlvask-autopilot-execute"
|
|
>{{ t('invoicing_period.xlvask_autopilot.controls.execute') }}</LoadButtonWhileAwait>
|
|
</div>
|
|
<div class="level-item">
|
|
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">{{SessionUser.objects.global.language.reload}}</LoadButtonWhileAwait>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<section
|
|
v-if="props.automationWorkspace"
|
|
class="box xlvask-automation-controls mb-4"
|
|
data-testid="xlvask-automation-controls"
|
|
aria-labelledby="xlvask-automation-controls-title"
|
|
>
|
|
<div class="is-flex is-justify-content-space-between is-align-items-flex-start is-flex-wrap-wrap mb-3">
|
|
<div>
|
|
<h2 id="xlvask-automation-controls-title" class="title is-6 mb-1">
|
|
{{ t('invoicing_period.xlvask_autopilot.controls.title') }}
|
|
</h2>
|
|
<p class="is-size-7 has-text-grey mb-0">
|
|
{{ t('invoicing_period.xlvask_autopilot.controls.stage', { stage: readiness.effective_stage }) }}
|
|
</p>
|
|
</div>
|
|
<span v-if="controlStateLoading" class="icon has-text-grey" role="status" :aria-label="t('common.loading')">
|
|
<i class="fas fa-spinner fa-spin" aria-hidden="true"></i>
|
|
</span>
|
|
<div v-else class="buttons mb-0">
|
|
<button
|
|
v-if="capabilities.can_manage_policy && nextPolicyStage"
|
|
type="button"
|
|
class="button is-small is-link is-light"
|
|
:class="{ 'is-loading': policyActionLoading }"
|
|
:disabled="policyActionLoading || !canAdvancePolicy"
|
|
data-testid="xlvask-policy-advance"
|
|
@click="applyPolicyPreview"
|
|
>{{ t('invoicing_period.xlvask_autopilot.controls.policy_advance', { stage: nextPolicyStage }) }}</button>
|
|
<button
|
|
v-if="capabilities.can_halt"
|
|
type="button"
|
|
class="button is-small is-danger"
|
|
:class="{ 'is-loading': policyActionLoading }"
|
|
:disabled="policyActionLoading"
|
|
data-testid="xlvask-policy-halt"
|
|
@click="haltAutomation"
|
|
>{{ t('invoicing_period.xlvask_autopilot.controls.halt') }}</button>
|
|
</div>
|
|
</div>
|
|
<p v-if="controlStateError" class="notification is-danger is-light py-2 px-3" role="alert">{{ controlStateError }}</p>
|
|
<template v-else-if="capabilities.can_view">
|
|
<p v-if="adminReadinessError" class="notification is-warning is-light py-2 px-3" role="alert">
|
|
{{ adminReadinessError }}
|
|
</p>
|
|
<div class="tags mb-3" data-testid="xlvask-automation-readiness">
|
|
<span class="tag" :class="readiness.ready ? 'is-success' : 'is-danger'">
|
|
{{ readiness.ready ? t('invoicing_period.xlvask_autopilot.controls.ready') : t('invoicing_period.xlvask_autopilot.controls.not_ready') }}
|
|
</span>
|
|
<span class="tag" :class="readiness.worker_healthy ? 'is-success is-light' : 'is-warning is-light'">
|
|
{{ readiness.worker_healthy ? t('invoicing_period.xlvask_autopilot.controls.worker_healthy') : t('invoicing_period.xlvask_autopilot.controls.worker_unhealthy') }}
|
|
</span>
|
|
<span v-if="readiness.model" class="tag is-light">{{ t('invoicing_period.xlvask_autopilot.model') }}: {{ readiness.model }}</span>
|
|
<span v-if="readiness.policy_version" class="tag is-light">{{ t('invoicing_period.xlvask_autopilot.policy_version') }}: {{ readiness.policy_version }}</span>
|
|
</div>
|
|
<ul v-if="readiness.blocked_reasons.length || capabilities.blocked_reasons.length" class="notification is-warning is-light py-2 px-4" role="status">
|
|
<li v-for="reason in [...readiness.blocked_reasons, ...capabilities.blocked_reasons]" :key="reason">{{ reason }}</li>
|
|
</ul>
|
|
<div class="columns is-multiline is-variable is-2">
|
|
<div v-for="action in ['attach_order', 'create_order']" :key="action" class="column is-12-tablet is-6-desktop">
|
|
<div class="xlvask-review-progress">
|
|
<div class="is-flex is-justify-content-space-between is-size-7 mb-1">
|
|
<strong>{{ t(`invoicing_period.xlvask_autopilot.actions.${action}`) }}</strong>
|
|
<span>{{ readiness.review_progress[action].reviewed }}/{{ readiness.review_progress[action].target }}</span>
|
|
</div>
|
|
<progress class="progress is-small is-info mb-1" :value="reviewProgressPercent(action)" max="100">
|
|
{{ reviewProgressPercent(action) }}%
|
|
</progress>
|
|
<p class="is-size-7 has-text-grey mb-0">
|
|
{{ t('invoicing_period.xlvask_autopilot.controls.budget', {
|
|
global: readiness.budgets[action].remaining_global,
|
|
hall: readiness.budgets[action].remaining_hall,
|
|
}) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<p v-else-if="!controlStateLoading" class="notification is-warning is-light py-2 px-3" role="status">
|
|
{{ t('invoicing_period.xlvask_autopilot.controls.no_access') }}
|
|
</p>
|
|
</section>
|
|
<article
|
|
v-if="props.automationWorkspace && (autopilotRun || runError)"
|
|
class="message xlvask-autopilot-run-status"
|
|
:class="{ 'is-info': isXlvaskAutopilotRunActive(autopilotRun), 'is-success': autopilotRun?.status === 'completed', 'is-warning': autopilotRun?.status === 'completed_with_warnings', 'is-danger': runError || autopilotRun?.status === 'failed' }"
|
|
data-testid="xlvask-autopilot-run-status"
|
|
>
|
|
<div class="message-body">
|
|
<div class="is-flex is-justify-content-space-between is-flex-wrap-wrap mb-2">
|
|
<strong>{{ runError || runStatusLabel }}</strong>
|
|
<span v-if="autopilotRun?.id" class="is-size-7 has-text-grey">#{{ autopilotRun.id }}</span>
|
|
</div>
|
|
<progress
|
|
v-if="isXlvaskAutopilotRunActive(autopilotRun)"
|
|
class="progress is-info is-small mb-2"
|
|
:value="runProgress"
|
|
max="100"
|
|
>{{ runProgress }}%</progress>
|
|
<p v-if="autopilotRun?.total" class="is-size-7 mb-0">
|
|
{{ t('invoicing_period.xlvask_autopilot.run_progress', { processed: autopilotRun.processed, total: autopilotRun.total }) }}
|
|
</p>
|
|
<button
|
|
v-if="runPollingPaused && isXlvaskAutopilotRunActive(autopilotRun)"
|
|
type="button"
|
|
class="button is-small is-info is-light mt-2"
|
|
data-testid="xlvask-autopilot-resume"
|
|
@click="resumeAutopilotPolling()"
|
|
>{{ t('invoicing_period.xlvask_autopilot.resume_status') }}</button>
|
|
<p v-if="autopilotRun?.warning" class="has-text-warning-dark mt-2 mb-0">{{ autopilotRun.warning }}</p>
|
|
<p v-if="autopilotRun?.error" class="has-text-danger mt-2 mb-0">{{ autopilotRun.error }}</p>
|
|
</div>
|
|
</article>
|
|
<section class="xlvask-autopilot-summary mb-4" aria-live="polite" data-testid="xlvask-autopilot-summary">
|
|
<div class="is-flex is-justify-content-space-between is-align-items-center mb-2">
|
|
<h2 class="title is-6 mb-0">{{ t('invoicing_period.xlvask_autopilot.summary_title') }}</h2>
|
|
<span v-if="summaryLoading" class="icon has-text-grey"><i class="fas fa-spinner fa-spin"></i></span>
|
|
</div>
|
|
<p v-if="summaryError" class="help is-danger mb-2">{{ summaryError }}</p>
|
|
<div class="tags">
|
|
<span
|
|
v-for="card in summaryCards"
|
|
:key="card.key"
|
|
class="tag is-light xlvask-autopilot-summary-chip"
|
|
:class="card.tone"
|
|
:data-testid="'xlvask-summary-' + card.key"
|
|
>
|
|
{{ t(`invoicing_period.xlvask_autopilot.states.${card.key}`) }}: {{ card.count }}
|
|
</span>
|
|
</div>
|
|
</section>
|
|
<section class="box xlvask-autopilot-filters mb-4" data-testid="xlvask-autopilot-filters">
|
|
<div class="columns is-multiline is-variable is-2">
|
|
<div class="column is-6-tablet is-3-desktop">
|
|
<label class="label is-small" for="xlvask-import-state-filter">{{ t('invoicing_period.xlvask_autopilot.filters.import_state') }}</label>
|
|
<div class="select is-small is-fullwidth">
|
|
<select id="xlvask-import-state-filter" :value="filterValues.import_state" @change="applyAutopilotFilter('import_state', $event.target.value)">
|
|
<option value="">{{ t('invoicing_period.xlvask_autopilot.filters.all') }}</option>
|
|
<option v-for="state in ['new', 'updated', 'unchanged', 'invalid']" :key="state" :value="state">{{ t(`invoicing_period.xlvask_autopilot.states.${state}`) }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="column is-6-tablet is-3-desktop">
|
|
<label class="label is-small" for="xlvask-resolution-state-filter">{{ t('invoicing_period.xlvask_autopilot.filters.resolution_state') }}</label>
|
|
<div class="select is-small is-fullwidth">
|
|
<select id="xlvask-resolution-state-filter" :value="filterValues.resolution_state" @change="applyAutopilotFilter('resolution_state', $event.target.value)">
|
|
<option value="">{{ t('invoicing_period.xlvask_autopilot.filters.all') }}</option>
|
|
<option v-for="state in ['already_linked', 'auto_linked', 'auto_created', 'needs_review', 'blocked', 'ignored', 'failed']" :key="state" :value="state">{{ t(`invoicing_period.xlvask_autopilot.states.${state}`) }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="column is-6-tablet is-3-desktop">
|
|
<label class="label is-small" for="xlvask-certainty-filter">{{ t('invoicing_period.xlvask_autopilot.filters.certainty') }}</label>
|
|
<div class="select is-small is-fullwidth">
|
|
<select id="xlvask-certainty-filter" :value="filterValues.certainty" @change="applyAutopilotFilter('certainty', $event.target.value)">
|
|
<option value="">{{ t('invoicing_period.xlvask_autopilot.filters.all') }}</option>
|
|
<option v-for="state in ['certain', 'uncertain', 'none']" :key="state" :value="state">{{ t(`invoicing_period.xlvask_autopilot.states.${state}`) }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="column is-6-tablet is-3-desktop">
|
|
<label class="label is-small" for="xlvask-action-filter">{{ t('invoicing_period.xlvask_autopilot.filters.planned_action') }}</label>
|
|
<div class="select is-small is-fullwidth">
|
|
<select id="xlvask-action-filter" :value="filterValues.planned_action" @change="applyAutopilotFilter('planned_action', $event.target.value)">
|
|
<option value="">{{ t('invoicing_period.xlvask_autopilot.filters.all') }}</option>
|
|
<option v-for="action in ['attach_order', 'create_order', 'resolve_mapping', 'recheck', 'ignore', 'none']" :key="action" :value="action">{{ t(`invoicing_period.xlvask_autopilot.actions.${action}`) }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button class="button is-small is-light" type="button" @click="clearAutopilotFilters">
|
|
{{ t('invoicing_period.xlvask_autopilot.filters.clear') }}
|
|
</button>
|
|
</section>
|
|
<input @input="search($event.target.value)" class="input" type="text" :placeholder="$t('global.search_transactions')" v-if="!hideSearchField && shouldShowLocalFilters"/>
|
|
<PaginationDisplay v-if="shouldShowLocalFilters" :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage">
|
|
<template #paginationColumns>
|
|
<!-- Sort by created_at -->
|
|
<div class="column is-narrow my-3">
|
|
<label class="label">{{ t('pagination.order_direction') }}</label>
|
|
<div class="control">
|
|
<div class="select">
|
|
<select @change="setOrder('created_at', $event.target.value); loadList();">
|
|
<option value="asc">{{ t('pagination.ascending') }}</option>
|
|
<option value="desc" selected>{{ t('pagination.descending') }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<PaginationDisplayTemplateDates
|
|
v-bind:end-date="dateTo"
|
|
v-bind:start-date="dateFrom"
|
|
@update:startDate="actions.date.from.select"
|
|
@update:endDate="actions.date.to.select"
|
|
/>
|
|
<div class="column is-narrow my-3">
|
|
<label class="label">{{ t('invoicing_period.xlvask_autopilot.filters.title') }}</label>
|
|
<div class="control">
|
|
<b-switch
|
|
size="is-small"
|
|
type="is-link"
|
|
v-model="showOnlyUnattachedVehicle"
|
|
>
|
|
{{ t('invoicing_period.xlvask_autopilot.filters.unattached_only') }}
|
|
</b-switch>
|
|
</div>
|
|
</div>
|
|
<!--
|
|
<PaginationDisplayTemplateDate
|
|
:label="SessionUser.objects.global.language.date_from"
|
|
@update:date="actions.date.from.select"
|
|
v-bind:model-value="dateFrom"
|
|
/>
|
|
<PaginationDisplayTemplateDate
|
|
:label="SessionUser.objects.global.language.date_to"
|
|
@update:date="actions.date.to.select"
|
|
v-bind:model-value="dateTo"
|
|
/>
|
|
-->
|
|
</template>
|
|
</PaginationDisplay>
|
|
<ShowErrorField v-else error="paginatedGetRequest"/>
|
|
<XlvaskUsageOrdersTable
|
|
:objects="list"
|
|
:show-only-unattached-vehicle="showOnlyUnattachedVehicle"
|
|
:highlight-usage-log-id="props.highlightUsageLogId"
|
|
:allow-select-multiple="props.automationWorkspace && capabilities.can_review"
|
|
:allow-review-actions="props.automationWorkspace && capabilities.can_review"
|
|
:allow-adjudication-actions="props.automationWorkspace && capabilities.can_manage_policy"
|
|
/>
|
|
<PaginationNavigation
|
|
v-if="!props.loadAllAtOnce"
|
|
:currentPage="metaCurrentPage"
|
|
:totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)"
|
|
:loadFunction="loadList"
|
|
:setPage="setPage"
|
|
:isLoading="isLoading"
|
|
/>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.xlvask-autopilot-summary .tags {
|
|
gap: 0.35rem;
|
|
}
|
|
|
|
.xlvask-autopilot-summary-chip {
|
|
height: auto;
|
|
min-height: 1.75rem;
|
|
white-space: normal;
|
|
}
|
|
|
|
.xlvask-autopilot-filters {
|
|
padding: 0.9rem;
|
|
}
|
|
|
|
.xlvask-autopilot-run-status .message-body {
|
|
padding: 0.85rem 1rem;
|
|
}
|
|
|
|
.xlvask-review-progress {
|
|
border: 1px solid #e7e7e7;
|
|
border-radius: 6px;
|
|
padding: 0.75rem;
|
|
}
|
|
</style>
|