Files
pleno-vue/src/components/displays/department/pos/orders/ordersTable.vue
T

2143 lines
87 KiB
Vue

<script setup>
import { useI18n } from "vue-i18n";
const { t } = useI18n();
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
import OrderAttachmentsActionButton from "@/components/displays/department/pos/orders/OrderAttachmentsActionButton.vue";
import AssignDraftOrderCustomerModal from "@/components/displays/modals/AssignDraftOrderCustomerModal.vue";
import InvoicingPeriodFlagList from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodFlagList.vue";
const props = defineProps({
orders: {
type: Object,
required: true,
},
invoiceView: {
type: Boolean,
default: false,
},
allowSelectMultiple: {
type: Boolean,
default: false,
},
isCustomerView: {
type: Boolean,
default: false,
},
groupInvoiceCollection: {
type: Boolean,
default: false,
},
showOnlyWithIds: {
type: Array,
default: () => [],
},
autoExpandAll: {
type: Boolean,
default: false,
},
excludedOrderIds: {
type: Array, // Any order ids that should be excluded from the list, will also be excluded from calculations (e.g. hidden orders).
default: () => [],
},
showDraftAssignmentActions: {
type: Boolean,
default: false,
},
invoicePeriodFlags: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["flagStatusChanged", "flagCreated"]);
import { computed, onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { departments, getDepartments, isLoading, getDepartmentName } from "@/components/pagination/departmentTabs.vue";
import { showPopper, removePopperIfOpen, popperBox } from "@/components/displays/PopperDefault.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
const { orderBy, orderDirection, setOrder, loadList } = usePaginatedListInstance();
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
import OrderContentTable from "@/components/displays/superuser/tables/OrderContentTable.vue";
import InvoiceMultipleCollectionsModal from "@/components/displays/modals/InvoiceMultipleCollectionsModal.vue";
import Swal from "sweetalert2";
import ColorIndicator from "@/components/displays/buttons/ColorIndicator.vue";
import InvoicingBillingPeriodInvoiceProgressBar from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/InvoicingBillingPeriodInvoiceProgressBar.vue";
import { invoiceQueue } from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportInvoiceQueue.vue";
import PosDepartmentStepMobileAttachment from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileAttachment.vue";
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
import ViewportResponsiveWrapper from "@/components/viewport/conditions/elements/ViewportResponsiveWrapper.vue";
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
import { dispatchNavigationCountRefresh } from "@/components/models/navigation/items/navigationCountEvents.js";
import { isSystemUserId } from "@/components/session/token/SessionUser/Objects/systemUserIds.js";
const route = useRoute();
const redirectDepartmentOrderPage = (orderId, departmentId, newTab = true) => {
// Check if the user has access to the department
if (!SessionUser.canAccessDepartment(departmentId)) {
window.open("/user/orders/" + orderId, newTab ? "_blank" : "_self");
} else {
// Send the user to the order page (In a new tab)
// `/admin/${departmentId}/modules/pos/orders/${orderId}`
window.open(`/admin/${departmentId}/modules/pos/orders/${orderId}`, newTab ? "_blank" : "_self");
}
};
const redirectSuperUserInvoiceCollectionPage = (invoiceCollectionId) => {
// Send the user to the invoice collection page (In a new tab)
window.open(`/superuser/invoices/${invoiceCollectionId}`, "_blank");
};
// Get the departments (If the departments are not already loaded)
if (departments.value.length === 0) {
getDepartments();
}
const getEconomicInvoiceModule = (order) => order?.economic_invoice_module ?? null;
const getStripeInvoiceModule = (order) => order?.stripe_invoice_module ?? null;
const getInvoiceCollection = (order) => order?.invoice_collection ?? null;
const isOrderInvoicedWithEconomic = (order) => {
const economicInvoiceModule = getEconomicInvoiceModule(order);
if (!economicInvoiceModule) {
return false;
}
return economicInvoiceModule.invoice_id !== null || economicInvoiceModule.invoice_draft_id !== null;
};
const isOrderEconomicInvoiceBooked = (order) => {
const economicInvoiceModule = getEconomicInvoiceModule(order);
if (!economicInvoiceModule) {
return false;
}
return economicInvoiceModule.invoice_id !== null;
};
const isOrderInvoicedWithStripe = (order) => {
return getStripeInvoiceModule(order) !== null;
};
const isOrderInvoiced = (order) => {
return isOrderInvoicedWithEconomic(order) || isOrderInvoicedWithStripe(order);
};
const doesOrderHaveInvoiceCollection = (order) => {
return getInvoiceCollection(order) !== null;
};
const isOrderInvoiceCollectionClosed = (order) => {
const invoiceCollection = getInvoiceCollection(order);
if (!invoiceCollection) {
return false;
}
return invoiceCollection.closed_at !== null;
};
const isOrderInvoiceCollectionBooked = (order) => {
const invoiceCollection = getInvoiceCollection(order);
if (!invoiceCollection) {
return false;
}
return invoiceCollection.booked_invoice_id !== null;
};
const isOrderStripeInvoicePaid = (order) => {
const stripeInvoiceModule = getStripeInvoiceModule(order);
if (stripeInvoiceModule) {
return stripeInvoiceModule.paid;
}
return false;
};
const doesOrderHaveErrorMessage = (order) => {
return order.error_message !== null && order.error_message !== undefined;
};
const getOrderInvoiceStatusBarColor = (order) => {
// Check the invoice status
let color_class = "has-text-danger";
if (isOrderInvoicedWithStripe(order)) {
isOrderStripeInvoicePaid(order) ? (color_class = "has-text-success") : (color_class = "has-text-warning");
}
if (isOrderInvoicedWithEconomic(order)) {
isOrderEconomicInvoiceBooked(order) ? (color_class = "has-text-success") : (color_class = "has-text-warning");
}
if (doesOrderHaveInvoiceCollection(order)) {
if (isPendingHandheld(order) && !order?.completed_at) {
return "has-text-warning"; // If the order is pending handheld, show a warning color
}
color_class = "has-text-grey";
// Check if the invoice collection is closed
if (isOrderInvoiceCollectionClosed(order)) {
color_class = "has-text-warning";
// Check if the invoice collection is booked
if (isOrderInvoiceCollectionBooked(order)) {
color_class = "has-text-success";
}
}
}
if (doesOrderHaveErrorMessage(order)) {
color_class = "has-text-danger";
}
// Return the color class
return color_class;
};
const getOrderInvoiceStatusContent = (order) => {
const content = [];
const economicInvoiceModule = getEconomicInvoiceModule(order);
const stripeInvoiceModule = getStripeInvoiceModule(order);
const invoiceCollection = getInvoiceCollection(order);
if (isOrderInvoiced(order)) {
// If the order is invoiced with e-conomic, show the invoice id
if (isOrderInvoicedWithEconomic(order)) {
isOrderEconomicInvoiceBooked(order)
? content.push("Bogført i e-conomic")
: content.push("Gemt som kladde i e-conomic");
isOrderEconomicInvoiceBooked(order)
? content.push("Faktura ID: <strong>" + (economicInvoiceModule?.invoice_id ?? "-") + "</strong>")
: content.push("Faktura Kladde ID: <strong>" + (economicInvoiceModule?.invoice_draft_id ?? "-") + "</strong>");
}
// If the order is invoiced with stripe, show the invoice id
if (isOrderInvoicedWithStripe(order)) {
isOrderStripeInvoicePaid(order)
? content.push("Fakturaen er <strong>betalt</strong>")
: content.push("Fakturaen er <strong>ikke betalt</strong>");
content.push("Faktura ID: <strong>" + (stripeInvoiceModule?.id ?? "-") + "</strong>");
}
content.push(""); // Add a line break
isOrderInvoicedWithEconomic(order)
? content.push('<span class="has-text-grey">Faktureret med e-conomic</span>')
: content.push('<span class="has-text-grey">Faktureret med stripe</span>');
} else {
// Check if the order has an invoice collection
if (doesOrderHaveInvoiceCollection(order)) {
// Check if the invoice collection is closed
if (isOrderInvoiceCollectionClosed(order)) {
content.push("Faktura samlingen er <strong>lukket</strong>");
// Check if the invoice collection is booked
if (isOrderInvoiceCollectionBooked(order)) {
content.push("Faktura samlingen er <strong>bogført</strong>");
}
}
content.push(""); // Add a line break
content.push("Faktura samling ID: <strong>" + (invoiceCollection?.id ?? "-") + "</strong>");
// Add the Error message if it exists
if (doesOrderHaveErrorMessage(order)) {
content.push('Fejl: <strong class="has-text-danger">' + order.error_message + "</strong>");
}
} else {
content.push(""); // Add a line break
content.push('<strong class="has-text-grey">Transaktionen er ikke faktureret</strong>');
}
}
// Return the list of content as a string, with line breaks between each item
return content.join("<br>");
};
const getOrderInvoiceStatusTitle = (order) => {
return isOrderInvoiced(order) ? "Faktura status" : "Transaktion status";
};
const clickUser = (customer_id) => {
if (!SessionUser.canAccessSuperUser()) {
return;
}
// Send the user to the user page
SessionUser.adminUser.customers.fromCustomerNumber
.getUserId(customer_id)
.then((response) => window.open("/superuser/users/" + response.data.data.user_id, "_blank"));
};
const truncateOrderField = (value, length = 25) => {
return SessionUser.functions.text.truncate(value ?? "", length);
};
const normalizeInvoiceCollectionId = (invoiceCollectionId) => {
const parsedInvoiceCollectionId = Number.parseInt(invoiceCollectionId, 10);
return Number.isInteger(parsedInvoiceCollectionId) && parsedInvoiceCollectionId > 0
? parsedInvoiceCollectionId
: null;
};
const getInvoiceCollectionResponseOrders = (response) => {
if (Array.isArray(response?.orders)) {
return response.orders;
}
if (Array.isArray(response?.data?.orders)) {
return response.data.orders;
}
if (Array.isArray(response?.data?.data?.orders)) {
return response.data.data.orders;
}
if (Array.isArray(response?.includes?.orders)) {
return response.includes.orders;
}
return [];
};
const isColumnCurrentlyBeingSortedBy = (column) => {
// Check if the column is currently being sorted by
return orderBy.value === column;
};
const isCurrentSortDirectionAscending = () => {
// Check if the current sort direction is ascending
return orderDirection.value === "asc";
};
const isCurrentSortDirectionDescending = () => {
// Check if the current sort direction is descending
return orderDirection.value === "desc";
};
const onTableHeaderClick = (column) => {
// Check if the column is already sorted
if (orderBy.value === column) {
// If the column is already sorted, toggle the direction
const direction = orderDirection.value === "asc" ? "desc" : "asc";
setOrder(column, direction);
} else {
// If the column is not sorted, set the order to the new column and default to ascending
const direction = "asc";
setOrder(column, direction);
}
// Load the list
loadList();
};
const tableHeaders = ref([
{
name: "id",
title: SessionUser.objects.orders.columns.id.label,
sortable: true,
},
...(props.isCustomerView
? []
: [
{
name: "customer_id",
title: SessionUser.objects.orders.columns.customer_id.label,
sortable: true,
},
]),
...(props.isCustomerView
? []
: [
{
name: "cashier_id",
title: SessionUser.objects.orders.columns.cashier_id.label,
sortable: true,
},
]),
{
name: "department_id",
title: SessionUser.objects.orders.columns.department_id.label,
sortable: true,
},
{
name: "reg_1",
title: SessionUser.functions.ucFirst(SessionUser.objects.vehicles.meta.labels.multiple),
sortable: true,
},
/**
* {
* name: 'reg_1',
* title: 'Reg. 1',
* sortable: true,
* },
* {
* name: 'reg_2',
* title: 'Reg. 2',
* sortable: true,
* },
* {
* name: 'reg_3',
* title: 'Reg. 3',
* sortable: true,
* },
*/
{
name: "reference",
title: SessionUser.objects.orders.columns.reference.label,
sortable: true,
},
{
name: "notes",
title: SessionUser.objects.orders.columns.notes.label,
sortable: true,
},
{
name: "po",
title: SessionUser.objects.orders.columns.po.label,
visible: (order) => {
return canSeePoField() || SessionUser.canAccessAdmin();
},
},
{
name: "created_at",
title: SessionUser.objects.orders.columns.created_at.label,
sortable: true,
},
{
name: "total_net_amount",
title: t("tables.orders.amount"),
sortable: false,
},
/**
* {
* name: 'completed_at',
* title: 'Gennemført',
* sortable: true,
* },
*/
{
name: "actions",
title: "",
sortable: false,
},
]);
const isTableHeaderVisible = (header) => {
return header.visible === undefined || header.visible(null) === true;
};
const invoiceCollectionSummaryColumnSpan = computed(() => {
const invoiceControlColumns =
(props.invoiceView && props.allowSelectMultiple ? 1 : 0) + (props.invoiceView ? 1 : 0);
const leadingOrderColumns = tableHeaders.value.filter(
(header) => isTableHeaderVisible(header) && !["total_net_amount", "actions"].includes(header.name)
).length;
return invoiceControlColumns + leadingOrderColumns;
});
const active_dropdown_object_ids = ref([]);
const toggleDropdown = (object) => {
// Check if the object is already in the active dropdown list
if (active_dropdown_object_ids.value.includes(object.id)) {
// If it is, remove it from the list
active_dropdown_object_ids.value = active_dropdown_object_ids.value.filter((id) => id !== object.id);
} else {
// If it is not, add it to the list
active_dropdown_object_ids.value.push(object.id);
}
};
const isDropdownActive = (object) => {
// If the expanded all prop is true, the active dropdown list should be exclusive instead of inclusive
// Check if the object is in the active dropdown list
const isObjectInList = active_dropdown_object_ids.value.includes(object.id);
return isAutoExpandAll() ? !isObjectInList : isObjectInList;
};
/**
* Invoice collection selection
*/
const selectedInvoiceCollections = ref([]);
const toggleInvoiceCollectionSelection = (invoiceCollectionId) => {
// Check if the invoice collection is already selected
if (selectedInvoiceCollections.value.includes(invoiceCollectionId)) {
// If it is, remove it from the list
selectedInvoiceCollections.value = selectedInvoiceCollections.value.filter((id) => id !== invoiceCollectionId);
} else {
// If it is not, add it to the list
selectedInvoiceCollections.value.push(invoiceCollectionId);
}
};
const isInvoiceCollectionSelected = (invoiceCollectionId) => {
// Check if the invoice collection is in the selected list
return selectedInvoiceCollections.value.includes(invoiceCollectionId);
};
const isInvoiceCollectionSelectedAll = () => {
// Check if all invoice collections are selected
return selectedInvoiceCollections.value.length === getUniqueInvoiceCollections().length;
};
const selectAllInvoiceCollections = () => {
// Check if all invoice collections are selected
if (isInvoiceCollectionSelectedAll()) {
// If they are, deselect all
selectedInvoiceCollections.value = [];
} else {
// If they are not, select all
selectedInvoiceCollections.value = getUniqueInvoiceCollections();
}
};
const getUniqueInvoiceCollections = () => {
// Check if there are no orders
if (props.orders ? props.orders.length === 0 : true) {
// If there are no orders, return an empty array
return [];
}
// Get a list of unique invoice collections from the orders
return [
...new Set(
props.orders
.map((order) => normalizeInvoiceCollectionId(order?.invoice_collection_id))
.filter((invoiceCollectionId) => invoiceCollectionId !== null)
),
];
};
const invoiceCollectionQueueInProgress = invoiceQueue.invoiceCollectionQueueInProgress;
const invoiceCollectionQueueFailed = invoiceQueue.invoiceCollectionQueueFailed;
const invoiceCollectionQueueSuccess = invoiceQueue.invoiceCollectionQueueSuccess;
const invoiceCollectionQueue = invoiceQueue.invoiceCollectionQueue;
const collectionQueueLog = invoiceQueue.collectionQueueLog;
const isInvoiceMultipleCollectionsModalOpen = ref(false);
const isSuperuserInvoicesRoute = computed(() => (
route.path === "/superuser/invoices" || route.path.startsWith("/superuser/invoices/")
));
const legacyInvoiceProgressCount = computed(() => (
invoiceCollectionQueueSuccess.value.length
+ invoiceCollectionQueueFailed.value.length
+ invoiceCollectionQueueInProgress.value.length
+ invoiceCollectionQueue.value.length
));
const shouldShowLegacyInvoiceProgressBar = computed(() => {
return !isSuperuserInvoicesRoute.value && legacyInvoiceProgressCount.value > 0;
});
const isInvoiceQueueBusy = computed(() => {
return invoiceCollectionQueue.value.length > 0 || invoiceCollectionQueueInProgress.value.length > 0;
});
const invoiceSelectedCollections = () => {
// Check if any invoice collections are selected
if (selectedInvoiceCollections.value.length === 0) {
return;
}
isInvoiceMultipleCollectionsModalOpen.value = true;
invoiceQueue.addInvoiceCollectionsToQueue(selectedInvoiceCollections.value);
invoiceQueue.processInvoiceCollectionQueue();
};
const retryInvoiceCollection = (invoiceCollectionId) => {
invoiceQueue.retryInvoiceCollection(parseInt(invoiceCollectionId));
};
const confirmCloseModal = () => {
Swal.fire({
title: "Er du sikker på at du vil lukke vinduet?",
text: "Du vil ikke kunne få den visualiseret igen!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Ja, luk vinduet!",
}).then((result) => {
if (result.isConfirmed) {
isInvoiceMultipleCollectionsModalOpen.value = false;
}
});
};
const ordersSortedByInvoiceCollection = computed(() => {
// Sort the orders by invoice collection id
return [...props.orders].sort((a, b) => {
const leftInvoiceCollectionId = normalizeInvoiceCollectionId(a?.invoice_collection_id) ?? Number.MAX_SAFE_INTEGER;
const rightInvoiceCollectionId = normalizeInvoiceCollectionId(b?.invoice_collection_id) ?? Number.MAX_SAFE_INTEGER;
return leftInvoiceCollectionId - rightInvoiceCollectionId;
});
});
const redirectDepartmentBookingPage = (bookingId, departmentId, newTab = true) => {
// Send the user to the booking page (In a new tab)
window.open(`/admin/${departmentId}/modules/pos/bookings/${bookingId}`, newTab ? "_blank" : "_self");
};
const getVisibleOrdersSortedByInvoiceCollection = computed(() => {
// Get the orders that are visible and sorted by invoice collection id
return ordersSortedByInvoiceCollection.value.filter((order) => isObjectVisible(order));
});
const getVisibleOrders = computed(() => {
// Get the orders that are visible
return props.orders.filter((order) => isObjectVisible(order));
});
const getOrdersByInvoiceCollection = (invoiceCollectionId) => {
const normalizedInvoiceCollectionId = normalizeInvoiceCollectionId(invoiceCollectionId);
if (normalizedInvoiceCollectionId === null) {
return [];
}
// If the showOnlyWithIds prop is set, filter the orders by the ids in the array
if (props.showOnlyWithIds.length > 0) {
return props.orders.filter(
(order) =>
normalizeInvoiceCollectionId(order?.invoice_collection_id) === normalizedInvoiceCollectionId &&
props.showOnlyWithIds.includes(order.id)
);
}
// Get the orders by invoice collection id
return props.orders.filter(
(order) => normalizeInvoiceCollectionId(order?.invoice_collection_id) === normalizedInvoiceCollectionId
);
};
const isFirstOrderInCollection = (invoiceCollectionId, order) => {
// Check if the order is the first in the collection
const ordersInCollection = getOrdersByInvoiceCollection(invoiceCollectionId);
// Get the first order in the collection
const firstOrderInCollection = ordersInCollection[0];
// Check if the current order is the first in the collection
return firstOrderInCollection?.id === order.id;
};
const invoiceCollectionDetails = ref({}); // Used to provide the invoice collection booked id in the list
const invoiceCollectionOrderCounts = ref({});
const shouldLoadInvoiceCollectionOrderCounts = computed(() => {
return (
props.groupInvoiceCollection &&
SessionUser.canAccessSuperUser() &&
typeof SessionUser.objects?.collectedOrderInvoices?.get?.single === "function"
);
});
const resetInvoiceCollectionLookups = () => {
invoiceCollectionDetails.value = {};
invoiceCollectionOrderCounts.value = {};
};
const loadInvoiceCollectionOrderCounts = async () => {
resetInvoiceCollectionLookups();
if (!shouldLoadInvoiceCollectionOrderCounts.value) {
return;
}
// Get all unique invoice collection ids
const uniqueInvoiceCollections = getUniqueInvoiceCollections();
if (uniqueInvoiceCollections.length === 0) {
return;
}
// Loop through each unique invoice collection id
await Promise.all(
uniqueInvoiceCollections.map(async (invoiceCollectionId) => {
try {
// Get the orders for the invoice collection
const response = await SessionUser.objects.collectedOrderInvoices.get.single(invoiceCollectionId);
// Get the orders from the response
const orders = getInvoiceCollectionResponseOrders(response).filter(
(order) => props.excludedOrderIds.includes(order.id) === false
);
// Set the count for the invoice collection id
invoiceCollectionOrderCounts.value[invoiceCollectionId] = orders.length;
invoiceCollectionDetails.value[invoiceCollectionId] = response ?? null;
} catch (error) {
console.error("Error loading invoice collection order counts for: " + invoiceCollectionId, error);
// Set the count for the invoice collection id to 0 if there was an error
invoiceCollectionOrderCounts.value[invoiceCollectionId] = 0;
invoiceCollectionDetails.value[invoiceCollectionId] = null;
}
})
);
};
const getInvoiceCollectionOrderCount = (invoiceCollectionId) => {
const normalizedInvoiceCollectionId = normalizeInvoiceCollectionId(invoiceCollectionId);
if (normalizedInvoiceCollectionId === null) {
return 0;
}
// Get the count for the invoice collection id
return invoiceCollectionOrderCounts.value[normalizedInvoiceCollectionId] || 0;
};
const getInvoiceCollectionDetails = (invoiceCollectionId) => {
const normalizedInvoiceCollectionId = normalizeInvoiceCollectionId(invoiceCollectionId);
if (normalizedInvoiceCollectionId !== null && invoiceCollectionDetails.value[normalizedInvoiceCollectionId]) {
return invoiceCollectionDetails.value[normalizedInvoiceCollectionId];
}
return null;
};
const getInvoiceCollectionDetailOrders = (invoiceCollectionId) => {
const details = getInvoiceCollectionDetails(invoiceCollectionId);
return details ? getInvoiceCollectionResponseOrders(details) : [];
};
const getHiddenOrdersByInvoiceCollection = (invoiceCollectionId) => {
const visibleOrderIds = new Set(getOrdersByInvoiceCollection(invoiceCollectionId).map((order) => Number(order?.id)));
return getInvoiceCollectionDetailOrders(invoiceCollectionId).filter((order) => !visibleOrderIds.has(Number(order?.id)));
};
const getHiddenOrderIdsByInvoiceCollection = (invoiceCollectionId) => (
getHiddenOrdersByInvoiceCollection(invoiceCollectionId).map((order) => Number(order?.id))
);
const getHiddenInvoiceCollectionFlags = (invoiceCollectionId) => {
const hiddenOrderIds = new Set(getHiddenOrderIdsByInvoiceCollection(invoiceCollectionId));
if (hiddenOrderIds.size === 0) {
return [];
}
return sortInvoicePeriodFlags((props.invoicePeriodFlags || []).filter((flag) => {
if (!isActiveInvoicePeriodFlag(flag)) {
return false;
}
const targetType = String(flag?.target_type || "");
if (!["order", "order_field", "order_item", "order_item_field"].includes(targetType)) {
return false;
}
const flagOrderId = Number(
flag?.order_id
|| flag?.context?.order_id
|| (["order", "order_field"].includes(targetType) ? flag?.target_id : 0)
);
return flagOrderId > 0 && hiddenOrderIds.has(flagOrderId);
}));
};
watch(
[() => props.orders, () => props.excludedOrderIds, () => props.groupInvoiceCollection],
() => {
// Load the invoice collection order counts when the orders change
loadInvoiceCollectionOrderCounts();
},
{ immediate: true }
);
const isObjectVisible = (object) => {
if (!SessionUser.canAccessSuperUser() && isSystemOrder(object)) {
return false; // Hide system orders for non-superusers
}
// Check if the object is visible based on the showOnlyWithIds prop
if (props.showOnlyWithIds.length === 0 && !props.excludedOrderIds.includes(object.id)) {
return true; // If no filter is applied, show all objects
} else if (props.excludedOrderIds.includes(object.id)) {
return false; // If the object is in the excluded list, hide it
}
// Check if the object id is in the showOnlyWithIds array
return props.showOnlyWithIds.includes(object.id);
};
const autoExpandAllToggled = ref(false); // When true, the prop.autoExpandAll value will be reversed. (This is used to toggle the auto expand all button in the UI)
const isAutoExpandAll = () => {
// Return the opposite of the autoExpandAll prop if the autoExpandAllToggled is true
return (props.autoExpandAll && !autoExpandAllToggled.value) || (!props.autoExpandAll && autoExpandAllToggled.value);
};
const toggleAutoExpandAll = () => {
// Toggle the auto expand all state
autoExpandAllToggled.value = !autoExpandAllToggled.value;
// Clear the active dropdown list
// This is to avoid confusion when toggling the auto expand all button
active_dropdown_object_ids.value = [];
};
const isSystemOrder = (order) => {
return isSystemUserId(order?.cashier_id);
};
const isPendingHandheld = (order) => {
return order.pending_handheld === true && order.completed_at === null;
};
const canSeePoField = () => {
return true;
//return (SessionUser.hasAttribute('usePONumbers') || SessionUser.hasPermission('department'))
};
const sortInvoicePeriodFlags = (flags = []) => [...flags].sort((left, right) => {
const sourceOrder = { manual: 0, automatic: 1 };
const leftSource = sourceOrder[left?.source] ?? 99;
const rightSource = sourceOrder[right?.source] ?? 99;
if (leftSource !== rightSource) {
return leftSource - rightSource;
}
return String(left?.created_at || left?.fingerprint || "").localeCompare(
String(right?.created_at || right?.fingerprint || "")
);
});
const isActiveInvoicePeriodFlag = (flag) => String(flag?.status || "active") === "active";
const getOrderLineFlags = (order) => sortInvoicePeriodFlags((props.invoicePeriodFlags || []).filter((flag) => {
if (!isActiveInvoicePeriodFlag(flag)) {
return false;
}
const targetType = String(flag?.target_type || "");
if (!["order", "order_field"].includes(targetType)) {
return false;
}
const flagOrderId = Number(flag?.order_id || flag?.target_id || 0);
return flagOrderId > 0 && flagOrderId === Number(order?.id || 0);
}));
const getOrderItemLineFlags = (order) => sortInvoicePeriodFlags((props.invoicePeriodFlags || []).filter((flag) => {
if (!isActiveInvoicePeriodFlag(flag)) {
return false;
}
const targetType = String(flag?.target_type || "");
if (!["order_item", "order_item_field"].includes(targetType)) {
return false;
}
return Number(flag?.order_id || flag?.context?.order_id || 0) === Number(order?.id || 0);
}));
const getOrderContainedFlags = (order) => sortInvoicePeriodFlags([
...getOrderLineFlags(order),
...getOrderItemLineFlags(order),
]);
const getOrderIndicatorIconClass = (order) => {
if (getOrderContainedFlags(order).length > 0) {
return "fas fa-flag";
}
return isPendingHandheld(order) ? "fas fa-mobile-alt" : "fas fa-circle";
};
const getOrderIndicatorColorClass = (order) => {
const flags = getOrderContainedFlags(order);
if (flags.length === 0) {
return getOrderInvoiceStatusBarColor(order);
}
return flags.some((flag) => flag?.source === "manual") ? "has-text-danger" : "has-text-warning";
};
const emitFlagStatusChanged = (flag) => emit("flagStatusChanged", flag);
const emitFlagCreated = (flag) => emit("flagCreated", flag);
/**
* Order actions modal
*/
const selectedOrderForActionsMenu = ref(null);
const isOrderActionsModalOpen = computed(() => {
return selectedOrderForActionsMenu.value !== null;
});
const closeOrderActionsModal = () => {
selectedOrderForActionsMenu.value = null;
};
const openOrderActionsModal = (order) => {
selectedOrderForActionsMenu.value = order;
};
const selectedOrderForDraftAssignment = ref(null);
const isDraftAssignmentModalOpen = computed(() => {
return selectedOrderForDraftAssignment.value !== null;
});
const canEditTransaction = (order) => {
return SessionUser.canAccessAdmin() || SessionUser.canAccessDepartment(order.department_id);
};
const currentRoutePath = computed(() => {
const resolvedRoutePath =
route && typeof route === "object" ? route.path || route.fullPath || route.name || "" : "";
if (resolvedRoutePath) {
return String(resolvedRoutePath);
}
if (typeof window !== "undefined") {
return String(window.location.pathname || "");
}
return "";
});
const shouldShowDraftAssignmentActions = computed(() => {
return Boolean(props.showDraftAssignmentActions) || currentRoutePath.value.includes("/modules/pos/drafts");
});
const canShowDraftAssignmentAction = () => {
return shouldShowDraftAssignmentActions.value;
};
const closeDraftAssignmentModal = () => {
selectedOrderForDraftAssignment.value = null;
};
const openDraftAssignmentModal = (order) => {
if (!canShowDraftAssignmentAction()) {
return;
}
closeOrderActionsModal();
selectedOrderForDraftAssignment.value = order;
};
const handleDraftAssignmentSuccess = async () => {
closeDraftAssignmentModal();
await loadList();
dispatchNavigationCountRefresh();
await Swal.fire({
icon: "success",
title: t("admin.pos.drafts_assignment.success"),
timer: 1800,
showConfirmButton: false,
});
};
/**
* Cashier name formatting
* This:
* - Writes "afdeling" if the cashiers name is the same as the department name.
* - Returns the first name, and first letter of the lastname of the cashier.
*/
const formatCashierName = (order) => {
if (getDepartmentName(order.department_id) === order.cashier_name) {
return SessionUser.functions.ucFirst(SessionUser.objects.departments.meta.labels.single);
}
return order.cashier_name;
};
</script>
<template>
<ViewportResponsiveWrapper>
<template #desktop>
<table class="table is-fullwidth">
<thead>
<tr v-if="props.invoiceView && props.allowSelectMultiple">
<td colspan="100%">
<div class="buttons">
<!-- Invoice selected collections -->
<button
class="button is-small"
@click="invoiceSelectedCollections()"
:disabled="selectedInvoiceCollections.length === 0 || isInvoiceQueueBusy"
>
{{ $t("common.invoice") }} {{ $t("global.selected_multiple") }} ({{
selectedInvoiceCollections.length
}})
</button>
<!-- Select / Unselect all invoice collections -->
<button
class="button is-small"
@click="selectAllInvoiceCollections()"
:class="{
'is-light': !isInvoiceCollectionSelectedAll(),
'is-dark': isInvoiceCollectionSelectedAll(),
}"
>
{{ isInvoiceCollectionSelectedAll() ? $t("global.unselect") : $t("common.select") }}
{{ $t("common.all").toLowerCase() }}
</button>
<!-- Expand / Collapse all invoice collections -->
<button
class="button is-small"
@click="toggleAutoExpandAll()"
:class="{
'is-light': !isAutoExpandAll(),
'is-dark': isAutoExpandAll(),
}"
>
{{ isAutoExpandAll() ? $t("global.collapse") : $t("global.expand") }}
{{ $t("common.all").toLowerCase() }}
</button>
</div>
</td>
</tr>
<tr>
<th class="is-narrow" v-if="props.invoiceView && props.allowSelectMultiple"></th>
<th class="is-narrow" v-if="props.invoiceView"></th>
<template v-for="tableHeaders in tableHeaders" :key="tableHeaders">
<template v-if="tableHeaders.visible === undefined || tableHeaders.visible(null) === true">
<th
v-if="tableHeaders.sortable"
:class="{
'is-clickable': tableHeaders.sortable,
'has-text-centered': tableHeaders.name === 'id',
}"
@click="onTableHeaderClick(tableHeaders.name)"
>
<span class="is-flex-wrap-nowrap">
<span class="has-text-weight-bold">{{ tableHeaders.title }}</span>
<span
v-if="isColumnCurrentlyBeingSortedBy(tableHeaders.name)"
:class="{
'fas fa-sort-up': isCurrentSortDirectionAscending(),
'fas fa-sort-down': isCurrentSortDirectionDescending(),
}"
class="icon is-small"
/>
</span>
</th>
<th
v-else
:class="{
'has-text-centered': tableHeaders.name === 'id',
}"
>
<span class="is-flex-wrap-nowrap">
<span class="has-text-weight-bold">{{ tableHeaders.title }}</span>
</span>
</th>
</template>
</template>
</tr>
</thead>
<tbody>
<template
v-for="order in props.groupInvoiceCollection ? getVisibleOrdersSortedByInvoiceCollection : getVisibleOrders"
:key="order.id"
>
<!-- If grouping by invoice collection is enabled, show the invoice collection id -->
<template
v-if="
props.groupInvoiceCollection &&
doesOrderHaveInvoiceCollection(order) &&
isFirstOrderInCollection(order.invoice_collection_id, order)
"
>
<tr :data-testid="`pos-order-invoice-collection-row-${order.invoice_collection_id}`">
<td
:colspan="invoiceCollectionSummaryColumnSpan"
:data-testid="`pos-order-invoice-collection-summary-${order.invoice_collection_id}`"
>
<div class="has-text-weight-bold">
Faktura samling ID: {{ order.invoice_collection_id }}
<span class="has-text-grey"
>( {{ SessionUser.objects.global.language.showing }}
{{ getOrdersByInvoiceCollection(order.invoice_collection_id).length }}
{{ SessionUser.objects.global.language.showing_of_separator }}
{{ getInvoiceCollectionOrderCount(order.invoice_collection_id) }}
{{ SessionUser.objects.orders.meta.labels.multiple.toLowerCase() }} )</span
>
</div>
</td>
<td
class="has-text-right"
:data-testid="`pos-order-invoice-collection-total-${order.invoice_collection_id}`"
>
<!-- Total net amount for the invoice collection -->
<span class="has-text-weight-bold">
{{
SessionUser.functions.currency.toLocal(
getOrdersByInvoiceCollection(order.invoice_collection_id)
.reduce((sum, order) => sum + order.total_net_amount, 0)
.toFixed(2)
)
}}
</span>
</td>
<!-- Actions for the invoice collection -->
<td class="is-narrow">
<ActionSettingsWheelButton v-bind:invoice_collection_id="order.invoice_collection_id">
<template #actions> </template>
</ActionSettingsWheelButton>
</td>
</tr>
<!-- Warning, if there's orders in the collection that are filtered out -->
<tr
v-if="
getInvoiceCollectionOrderCount(order.invoice_collection_id) >
getOrdersByInvoiceCollection(order.invoice_collection_id).length
"
>
<td colspan="100%">
<div class="message is-warning">
<div class="message-body">
<span class="has-text-weight-bold">Bemærk:</span> Der er ordrer i denne faktura samling, som ikke
er synlige pga. filtre.
<br />
<span class="has-text-weight-bold"
>Disse
{{
getInvoiceCollectionOrderCount(order.invoice_collection_id) -
getOrdersByInvoiceCollection(order.invoice_collection_id).length
}}
er ikke vist, men vil muligvis blive faktureret alligevel.</span
>
<InvoicingPeriodFlagList
v-if="getHiddenInvoiceCollectionFlags(order.invoice_collection_id).length > 0"
class="mt-3"
compact
:flags="getHiddenInvoiceCollectionFlags(order.invoice_collection_id)"
@statusChanged="emitFlagStatusChanged"
/>
</div>
</div>
</td>
</tr>
</template>
<tr v-if="isObjectVisible(order)" :class="{ 'has-background-info-light': isSystemOrder(order) }">
<template v-if="props.invoiceView && props.allowSelectMultiple">
<td class="is-narrow">
<button
class="button is-small"
@click="toggleInvoiceCollectionSelection(order.invoice_collection_id)"
:class="{
'is-light': !isInvoiceCollectionSelected(parseInt(order.invoice_collection_id)),
'is-dark': isInvoiceCollectionSelected(parseInt(order.invoice_collection_id)),
}"
>
{{
isInvoiceCollectionSelected(parseInt(order.invoice_collection_id))
? SessionUser.objects.global.language.unselect
: SessionUser.objects.global.language.select
}}
</button>
</td>
</template>
<template v-if="props.invoiceView">
<td class="is-narrow">
<button
class="button is-small"
:data-testid="`pos-order-expand-${order.id}`"
@click="toggleDropdown(order)"
>
<span
class="icon"
:class="isDropdownActive(order) ? 'has-text-link' : ''"
:style="isDropdownActive(order) ? 'transform: rotate(90deg);' : ''"
>
<!-- Dropdown icon -->
<i class="fas fa-angle-right"></i>
</span>
</button>
</td>
</template>
<td>
<ColorIndicator
v-bind:color_class="getOrderIndicatorColorClass(order)"
v-bind:icon_class="getOrderIndicatorIconClass(order)"
v-bind:is_narrow="true"
v-bind:label="{
text: order.id,
classes: [],
max_length: 7,
}"
v-bind:visibility="{
icon: true,
dropdown: true,
}"
v-bind:dropdown_content="{
title: null,
buttons_title: null,
content: [
// Transaction
{
text: SessionUser.functions.ucFirst(SessionUser.objects.orders.meta.labels.single),
action: () => {
redirectDepartmentOrderPage(order.id, order.department_id);
},
...(SessionUser.canAccessSuperUser() ? {} : { disabled: true }),
button_classes: ['no-underline-text', 'is-text'],
button: true,
button_text: order.id,
v_centered: true,
},
// Invoice collection
{
text:
SessionUser.objects.global.language.invoice +
' ' +
SessionUser.objects.global.language.collection.toLowerCase(),
action: () => {
redirectSuperUserInvoiceCollectionPage(order.invoice_collection_id);
},
...(SessionUser.canAccessSuperUser() ? {} : { disabled: true }),
button_classes: ['no-underline-text', 'is-text'],
button: true,
button_text: order.invoice_collection_id,
v_centered: true,
},
/**
* // Completed status
* {
* text: SessionUser.objects.orders.columns.completed_at.label,
* action: () => {
* redirectDepartmentOrderPage(order.id, order.department_id);
* },
* ...(SessionUser.canAccessSuperUser() ? {} : { disabled: true }),
* button_classes: ['no-underline-text', 'is-text', ...(order.completed_at !== null ? [] : ['has-text-warning'])],
* button: true,
* button_text: (order.completed_at !== null ? SessionUser.objects.global.language.yes : SessionUser.objects.global.language.no),
* v_centered: true,
* },
*/
// Handheld status
...(isPendingHandheld(order)
? [
{
text: SessionUser.objects.global.language.handheld_pending_order,
action: () => {},
button_classes: ['no-underline-text', 'is-text', 'has-text-warning'],
button: true,
button_text: SessionUser.objects.global.language.confirmation_needed,
v_centered: true,
},
]
: []),
],
buttons: [],
}"
/>
</td>
<td v-if="!props.isCustomerView">
<ColorIndicator
v-bind:label="{
text: order.customer_name,
classes: [],
max_length: 7,
}"
v-bind:visibility="{
icon: false,
dropdown: true,
}"
v-bind:dropdown_content="{
title: null,
buttons_title: null,
content: [
{
text: order.customer_name,
action: () => {
clickUser(order.customer_id);
},
...(SessionUser.canAccessSuperUser() ? {} : { disabled: true }),
button: true,
button_text: 'Vis kunde',
},
],
buttons: [],
}"
/>
</td>
<!--
<td>
<span
@mouseover="showPopper(
popperBox(
'Customer ID',
order.customer_id,
),
$event.target
)"
@mouseleave="removePopperIfOpen()"
@click="clickUser(order.customer_id)"
:class="{ 'is-clickable': SessionUser.canAccessSuperUser() }"
>
{{ order.customer_name }}
</span>
</td> -->
<td v-if="!props.isCustomerView">
<template v-if="!isSystemOrder(order)">{{ formatCashierName(order) }}</template>
<template v-else><span class="has-text-grey-light">System</span></template>
</td>
<td>{{ getDepartmentName(order.department_id) }}</td>
<!-- Vehicles -->
<td>
<p>
<EditableTableColumn
:componentWrapper="'div'"
:object="order"
:loadList="loadList"
column="reg_1"
:parse-function="(value) => value || t('global.no_data')"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permissionCheckFunction="(orderobj) => canEditTransaction(order)"
/>
</p>
<p>
<EditableTableColumn
:componentWrapper="'div'"
:object="order"
:loadList="loadList"
column="reg_2"
:parse-function="(value) => value || '-'"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permissionCheckFunction="(bookingobj) => canEditTransaction(order)"
/>
</p>
<p v-show="order.reg_3">
<EditableTableColumn
:componentWrapper="'div'"
:object="order"
:loadList="loadList"
column="reg_3"
:parse-function="(value) => value || '-'"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permissionCheckFunction="(bookingobj) => canEditTransaction(order)"
/>
</p>
</td>
<EditableTableColumn
:object="order"
:loadList="loadList"
column="reference"
:hover-text="order.reference"
:parse-function="(value) => truncateOrderField(value, 10)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => true"
/>
<EditableTableColumn
:object="order"
:loadList="loadList"
column="notes"
:parse-function="(value) => truncateOrderField(value, 10)"
:hover-text="order.notes"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => SessionUser.canAccessAdmin()"
/>
<EditableTableColumn
v-if="canSeePoField() || SessionUser.canAccessAdmin()"
:object="order"
:loadList="loadList"
column="po"
:hover-text="order.po"
:parse-function="(value) => truncateOrderField(value, 10)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => canSeePoField() || SessionUser.canAccessAdmin()"
/>
<td>{{ order.created_at }}</td>
<td>{{ SessionUser.functions.currency.toLocal(order.total_net_amount) }}</td>
<!--<td>{{ order.completed_at }}</td>-->
<td class="is-narrow">
<div class="buttons pos-order-list-actions">
<button
v-if="shouldShowDraftAssignmentActions"
class="button is-small is-link is-light pos-order-list-assign-customer-button"
:data-testid="`draft-order-assign-customer-button-${order.id}`"
@click="openDraftAssignmentModal(order)"
>
<span class="icon">
<i class="fas fa-user-check"></i>
</span>
<span>{{ t("admin.pos.drafts_assignment.button") }}</span>
</button>
<!-- Attachments -->
<OrderAttachmentsActionButton
:order="order"
:refresh-function="loadList"
:data-testid="`pos-order-list-attachments-${order.id}`"
/>
<ActionSettingsWheelButton
v-bind:user_id="order.user_id"
v-bind:order_id="order.id"
v-bind:invoice_collection_id="order.invoice_collection_id"
v-bind:customer_number="order.customer_id"
v-bind:department_id="order.department_id"
v-bind:reg_1="order.reg_1"
v-bind:reg_2="order.reg_2"
v-bind:reg_3="order.reg_3"
v-bind:order_booking_id="order.booking_id"
:refreshFunction="loadList"
@deleted="loadList()"
@flag-created="emitFlagCreated"
:data-testid="`pos-order-list-settings-${order.id}`"
>
<template v-slot:actions>
<!--
Go to the order page, in a new tab
<action-settings-wheel-item
label="Se transaktion i ny fane"
icon="fas fa-external-link-alt"
:click-action="() => redirectDepartmentOrderPage(order.id, order.department_id)"
:disabled="false"
/>
Show the invoice collection, in a new tab
<action-settings-wheel-item
label="Vis faktura samling i ny fane"
icon="fas fa-file-invoice-dollar"
:click-action="() => redirectSuperUserInvoiceCollectionPage(order.invoice_collection.id)"
:disabled="!doesOrderHaveInvoiceCollection(order)"
/>
-->
</template>
</ActionSettingsWheelButton>
</div>
</td>
</tr>
<tr
v-if="getOrderLineFlags(order).length > 0"
:data-testid="`pos-order-flags-row-${order.id}`"
>
<td colspan="100%">
<InvoicingPeriodFlagList
compact
:flags="getOrderLineFlags(order)"
@statusChanged="emitFlagStatusChanged"
/>
</td>
</tr>
<!-- Dropdown content (if the invoice view is enabled,and the dropdown is active) -->
<tr v-if="props.invoiceView && isDropdownActive(order)">
<!-- Order line items -->
<td colspan="100%">
<!-- Content -->
<div class="content">
<OrderContentTable
:order-id="order.id"
:invoice-period-flags="getOrderItemLineFlags(order)"
@flagStatusChanged="emitFlagStatusChanged"
/>
</div>
</td>
</tr>
</template>
</tbody>
<tfoot>
<tr>
<td colspan="100%">
{{ t("tables.showing") }} {{ getVisibleOrders.length }}
{{ SessionUser.objects.orders.meta.labels.entries.toLowerCase() }}
</td>
</tr>
</tfoot>
</table>
</template>
<!-- Treat tablet the same as mobile to ensure visibility on larger phones/tablets -->
<template #tablet>
<div class="columns is-multiline is-mobile">
<!-- Bulk actions when invoice view allows selection -->
<div class="column is-full" v-if="props.invoiceView && props.allowSelectMultiple">
<div class="buttons">
<button
class="button is-small"
@click="invoiceSelectedCollections()"
:disabled="selectedInvoiceCollections.length === 0 || isInvoiceQueueBusy"
>
{{ SessionUser.objects.global.language.invoice }}
{{ SessionUser.objects.global.language.selected_multiple }} ({{ selectedInvoiceCollections.length }})
</button>
<button
class="button is-small"
@click="selectAllInvoiceCollections()"
:class="{
'is-light': !isInvoiceCollectionSelectedAll(),
'is-dark': isInvoiceCollectionSelectedAll(),
}"
>
{{
isInvoiceCollectionSelectedAll()
? SessionUser.objects.global.language.unselect
: SessionUser.objects.global.language.select
}}
{{ SessionUser.objects.global.language.all.toLowerCase() }}
</button>
<button
class="button is-small"
@click="toggleAutoExpandAll()"
:class="{
'is-light': !isAutoExpandAll(),
'is-dark': isAutoExpandAll(),
}"
>
{{
isAutoExpandAll()
? SessionUser.objects.global.language.collapse
: SessionUser.objects.global.language.expand
}}
{{ SessionUser.objects.global.language.all.toLowerCase() }}
</button>
</div>
</div>
<!-- Orders as cards -->
<template v-for="order in getVisibleOrders" :key="order.id">
<div class="column is-full">
<WhiteBoxCard
:forceStateFooter="false"
:forceState="false"
:defaultOpen="false"
:toggleable="true"
class="is-clickable"
:has-hover-effect="true"
:hasSelectionStyle="false"
>
<template #header>
<div
class="card-header-title is-flex is-justify-content-space-between is-align-items-center"
style="width: 100%"
>
<div>
<div>
<small>
<!-- ID and status icon -->
<span class="icon mr-1" :class="getOrderInvoiceStatusBarColor(order)">
<i :class="isPendingHandheld(order) ? 'fas fa-mobile-alt' : 'fas fa-circle'"></i>
</span>
#{{ order.id }}
</small>
</div>
<div v-if="!props.isCustomerView">
<small>{{ order.customer_name }}</small>
</div>
</div>
<div class="is-pulled-right" style="width: fit-content">
<div
class="columns is-multiline is-mobile is-gapless is-justify-content-flex-end is-align-items-center"
>
<div class="column is-narrow has-text-right mb-2">
<span class="tag is-light is-small mr-1">
<small>{{ getDepartmentName(order.department_id) }}</small>
</span>
</div>
<div class="column is-12 has-text-right mb-2">
<span class="buttons is-right mr-1">
<button
v-if="props.invoiceView && props.allowSelectMultiple"
class="button is-light is-small"
@click="toggleInvoiceCollectionSelection(order.invoice_collection_id)"
:class="{ 'is-dark': isInvoiceCollectionSelected(parseInt(order.invoice_collection_id)) }"
>
{{
isInvoiceCollectionSelected(parseInt(order.invoice_collection_id))
? SessionUser.objects.global.language.unselect
: SessionUser.objects.global.language.select
}}
</button>
</span>
</div>
</div>
</div>
</div>
</template>
<template #default>
<div class="card-content">
<div class="columns is-mobile is-multiline">
<div class="column is-12">
<small
>{{ order.reg_1 }}<span v-if="order.reg_2">, {{ order.reg_2 }}</span
><span v-if="order.reg_3">, {{ order.reg_3 }}</span></small
>
</div>
<div class="column is-12">
<strong
><small>{{ order.created_at }}</small></strong
>
</div>
<div class="column is-12">
<small
>{{ SessionUser.objects.orders.columns.total_net_amount.label || "Total" }}:
{{ SessionUser.functions.currency.toLocal(order.total_net_amount) }}</small
>
</div>
<div class="column is-12" v-if="order.completed_at">
<small
>{{ SessionUser.objects.orders.columns.completed_at.label }}: {{ order.completed_at }}</small
>
</div>
<div class="column is-12" v-if="isPendingHandheld(order)">
<small class="has-text-warning"
>{{ SessionUser.objects.global.language.handheld_pending_order }} •
{{ SessionUser.objects.global.language.confirmation_needed }}</small
>
</div>
</div>
</div>
<InvoicingPeriodFlagList
compact
:flags="getOrderLineFlags(order)"
@statusChanged="emitFlagStatusChanged"
/>
</template>
<template #content>
<div>
<p v-show="!!order.po"><strong>PO:</strong> {{ order.po || "-" }}</p>
<p v-show="!!order.reference">
<strong>{{ SessionUser.objects.orders.columns.reference?.label || "Reference" }}:</strong>
{{ order.reference || "-" }}
</p>
<p v-show="!!order.notes">
<strong>{{ SessionUser.objects.orders.columns.notes?.label || "Noter" }}:</strong>
{{ order.notes || "-" }}
</p>
<hr />
<!-- Actions and attachments -->
<div class="buttons is-right">
<button
v-if="shouldShowDraftAssignmentActions"
class="button is-small is-link is-light pos-order-list-assign-customer-button"
:data-testid="`draft-order-assign-customer-button-${order.id}`"
@click="openDraftAssignmentModal(order)"
>
<span class="icon">
<i class="fas fa-user-check"></i>
</span>
<span>{{ t("admin.pos.drafts_assignment.button") }}</span>
</button>
<ActionSettingsWheelButton
v-bind:user_id="order.user_id"
v-bind:order_id="order.id"
v-bind:invoice_collection_id="order.invoice_collection_id"
v-bind:customer_number="order.customer_id"
v-bind:department_id="order.department_id"
v-bind:reg_1="order.reg_1"
v-bind:reg_2="order.reg_2"
v-bind:reg_3="order.reg_3"
v-bind:order_booking_id="order.booking_id"
:refreshFunction="loadList"
@deleted="loadList()"
@flag-created="emitFlagCreated"
:displayActionsDirectly="true"
>
<template #actions></template>
</ActionSettingsWheelButton>
<OrderAttachmentsActionButton
:order="order"
:refresh-function="loadList"
:data-testid="`pos-order-list-attachments-${order.id}`"
/>
</div>
<!-- Order line items -->
<div class="content" v-if="props.invoiceView && isAutoExpandAll()">
<OrderContentTable
:order-id="order.id"
:invoice-period-flags="getOrderItemLineFlags(order)"
@flagStatusChanged="emitFlagStatusChanged"
/>
</div>
</div>
</template>
</WhiteBoxCard>
</div>
</template>
<!-- Footer count -->
<div class="column is-full">
<div class="has-text-centered">
<small
>{{ t("tables.showing") }} {{ getVisibleOrders.length }}
{{ SessionUser.objects.orders.meta.labels.entries.toLowerCase() }}</small
>
</div>
</div>
</div>
</template>
<template #mobile>
<div class="columns is-multiline is-mobile">
<!-- Bulk actions when invoice view allows selection -->
<div class="column is-full" v-if="props.invoiceView && props.allowSelectMultiple">
<div class="buttons">
<button
class="button is-small"
@click="invoiceSelectedCollections()"
:disabled="selectedInvoiceCollections.length === 0 || isInvoiceQueueBusy"
>
{{ SessionUser.objects.global.language.invoice }}
{{ SessionUser.objects.global.language.selected_multiple }} ({{ selectedInvoiceCollections.length }})
</button>
<button
class="button is-small"
@click="selectAllInvoiceCollections()"
:class="{
'is-light': !isInvoiceCollectionSelectedAll(),
'is-dark': isInvoiceCollectionSelectedAll(),
}"
>
{{
isInvoiceCollectionSelectedAll()
? SessionUser.objects.global.language.unselect
: SessionUser.objects.global.language.select
}}
{{ SessionUser.objects.global.language.all.toLowerCase() }}
</button>
<button
class="button is-small"
@click="toggleAutoExpandAll()"
:class="{
'is-light': !isAutoExpandAll(),
'is-dark': isAutoExpandAll(),
}"
>
{{
isAutoExpandAll()
? SessionUser.objects.global.language.collapse
: SessionUser.objects.global.language.expand
}}
{{ SessionUser.objects.global.language.all.toLowerCase() }}
</button>
</div>
</div>
<!-- Orders as cards -->
<template
v-for="order in props.groupInvoiceCollection ? getVisibleOrdersSortedByInvoiceCollection : getVisibleOrders"
:key="order.id"
>
<!-- If grouping by invoice collection is enabled, show the invoice collection id -->
<template
v-if="
props.groupInvoiceCollection &&
doesOrderHaveInvoiceCollection(order) &&
isFirstOrderInCollection(order.invoice_collection_id, order)
"
>
<div
class="column is-full"
:data-testid="`pos-order-invoice-collection-row-${order.invoice_collection_id}`"
>
<div class="message is-light mb-0">
<div
class="message-body is-flex is-justify-content-space-between is-align-items-center p-2"
:data-testid="`pos-order-invoice-collection-summary-${order.invoice_collection_id}`"
>
<div class="has-text-weight-bold">
Faktura samling ID: {{ order.invoice_collection_id }}
</div>
<div class="has-text-weight-bold">
<span :data-testid="`pos-order-invoice-collection-total-${order.invoice_collection_id}`"
>{{
SessionUser.functions.currency.toLocal(
getOrdersByInvoiceCollection(order.invoice_collection_id)
.reduce((sum, order) => sum + order.total_net_amount, 0)
.toFixed(2)
)
}}
kr.</span
>
</div>
</div>
</div>
</div>
</template>
<div class="column is-full">
<WhiteBoxCard
:forceStateFooter="true"
:forceState="false"
:defaultOpen="false"
:toggleable="true"
class="is-clickable pos-orders-mobile-card"
:has-hover-effect="true"
:hasSelectionStyle="false"
:data-testid="`pos-order-list-card-${order.id}`"
:headerTestId="`pos-order-expand-${order.id}`"
>
<template #header>
<div
class="card-header-title is-flex is-justify-content-space-between is-align-items-center"
style="width: 100%"
>
<div>
<div>
<small>
<!-- ID and status icon -->
<ColorIndicator
class="mr-1"
style="display: inline-flex"
v-bind:color_class="getOrderIndicatorColorClass(order)"
v-bind:icon_class="getOrderIndicatorIconClass(order)"
v-bind:is_narrow="true"
v-bind:visibility="{
icon: true,
dropdown: false,
}"
/>
{{ order.reg_1 }}<span v-if="order.reg_2">, {{ order.reg_2 }}</span
><span v-if="order.reg_3">, {{ order.reg_3 }}</span>
<span class="has-text-grey-light">#{{ order.id }}</span></small
>
</div>
<div v-if="!props.isCustomerView">
<small>{{ order.customer_name }}</small>
</div>
</div>
<div class="is-pulled-right" style="width: fit-content">
<div
class="columns is-multiline is-mobile is-gapless is-justify-content-flex-end is-align-items-center"
>
<div class="column is-narrow has-text-right mb-2">
<span class="tag is-light is-small mr-1">
<small>{{ getDepartmentName(order.department_id) }}</small>
</span>
</div>
<div class="column is-12 has-text-right mb-2">
<span class="buttons is-right mr-1">
<button
v-if="props.invoiceView && props.allowSelectMultiple"
class="button is-light is-small"
@click="toggleInvoiceCollectionSelection(order.invoice_collection_id)"
:class="{ 'is-dark': isInvoiceCollectionSelected(parseInt(order.invoice_collection_id)) }"
>
{{
isInvoiceCollectionSelected(parseInt(order.invoice_collection_id))
? SessionUser.objects.global.language.unselect
: SessionUser.objects.global.language.select
}}
</button>
</span>
</div>
</div>
</div>
</div>
</template>
<template #default>
<div class="card-content">
<div class="columns is-mobile is-multiline">
<div class="column is-12">
<template v-if="!!order.reference">
<div class="columns is-mobile is-gapless is-align-items-center mb-1">
<div class="column is-narrow mr-1">
<label class="label is-small"
>{{ SessionUser.objects.orders.columns.reference?.label || "Reference" }}:</label
>
</div>
<div class="column is-flex-grow-1">
<small>
<editable-table-column
:componentWrapper="'div'"
:object="order"
:loadList="loadList"
column="reference"
:hover-text="order.reference"
:parse-function="(value) => truncateOrderField(value, 20)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => true"
/>
</small>
</div>
</div>
</template>
<template v-if="!!order.notes">
<div class="columns is-mobile is-gapless is-align-items-center mb-1">
<div class="column is-narrow mr-1">
<label class="label is-small" :class="{ 'has-text-grey': !order.notes }"
>{{ SessionUser.objects.orders.columns.notes?.label || "Noter" }}:</label
>
</div>
<div class="column is-flex-grow-1">
<small>
<editable-table-column
:componentWrapper="'div'"
:object="order"
:loadList="loadList"
column="notes"
:hover-text="order.notes"
:parse-function="(value) => truncateOrderField(value, 20)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => SessionUser.canAccessAdmin()"
/>
</small>
</div>
</div>
</template>
<template v-if="!!order.po">
<div class="columns is-mobile is-gapless is-align-items-center mb-1">
<div class="column is-narrow mr-1">
<label class="label is-small" :class="{ 'has-text-grey': !order.po }">PO:</label>
</div>
<div class="column is-flex-grow-1">
<small>
<editable-table-column
:componentWrapper="'div'"
:object="order"
:loadList="loadList"
column="po"
:hover-text="order.po"
:parse-function="(value) => truncateOrderField(value, 20)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => canSeePoField() || SessionUser.canAccessAdmin()"
/>
</small>
</div>
</div>
</template>
<!-- Order summary -->
<hr v-if="!!order.reference || !!order.notes || !!order.po" />
<order-content-table
:order-id="order.id"
:summaryMode="true"
:display-price="SessionUser.canAccessAdmin()"
:invoice-period-flags="getOrderItemLineFlags(order)"
@flagStatusChanged="emitFlagStatusChanged"
/>
</div>
<div class="column is-12 has-text-centered">
<hr />
<!-- Total net amount -->
<strong>{{ SessionUser.functions.currency.toLocal(order.total_net_amount) }}</strong>
<br />
<!-- Cashier • Time ago -->
<small class="has-text-grey"
>{{ order.cashier_name }} • {{ SessionUser.functions.date.toLocalTimeHours(order.created_at) }}</small
>
<!-- Reg 1 • Reg 2 • Reg 3 -->
<br />
<div class="has-text-grey">
<div class="columns is-mobile is-gapless is-justify-content-center is-align-items-center">
<div class="column is-narrow">
<template v-if="!!order.reg_1">
<div class="columns is-mobile is-gapless is-align-items-center">
<div class="column is-narrow mr-1">
<label class="label is-small">Reg 1:</label>
</div>
<div class="column is-flex-grow-1">
<small>
<editable-table-column
:componentWrapper="'div'"
:object="order"
:loadList="loadList"
column="reg_1"
:hover-text="order.reg_1"
:parse-function="(value) => truncateOrderField(value, 20)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => true"
/>
</small>
</div>
</div>
</template>
</div>
<template v-if="order.reg_2 || order.reg_3">
<div class="column">
<div class="columns is-mobile is-gapless is-align-items-center">
<div class="column">
<label class="label is-small has-text-grey">•</label>
</div>
</div>
</div>
</template>
<div class="column is-narrow" v-if="order.reg_2">
<template v-if="!!order.reg_2">
<div class="columns is-mobile is-gapless is-align-items-center">
<div class="column is-narrow mr-1">
<label class="label is-small">Reg 2:</label>
</div>
<div class="column is-flex-grow-1">
<small>
<editable-table-column
:componentWrapper="'div'"
:object="order"
:loadList="loadList"
column="reg_2"
:hover-text="order.reg_2"
:parse-function="(value) => truncateOrderField(value, 20)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => true"
/>
</small>
</div>
</div>
</template>
</div>
<template v-if="order.reg_2 && order.reg_3">
<div class="column">
<div class="columns is-mobile is-gapless is-align-items-center">
<div class="column">
<label class="label is-small has-text-grey">•</label>
</div>
</div>
</div>
</template>
<div class="column is-narrow" v-if="order.reg_3">
<template v-if="!!order.reg_3">
<div class="columns is-mobile is-gapless is-align-items-center">
<div class="column is-narrow mr-1">
<label class="label is-small">Reg 3:</label>
</div>
<div class="column is-flex-grow-1">
<small>
<editable-table-column
:componentWrapper="'div'"
:object="order"
:loadList="loadList"
column="reg_3"
:hover-text="order.reg_3"
:parse-function="(value) => truncateOrderField(value, 20)"
:edit-function="SessionUser.objects.orders.showEditObjectFieldForm"
:permission-check-function="() => true"
/>
</small>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</div>
<InvoicingPeriodFlagList
compact
:flags="getOrderLineFlags(order)"
@statusChanged="emitFlagStatusChanged"
/>
</template>
<template #content> </template>
<template #footer>
<!-- Delete
<a class="card-footer-item" @click="SessionUser.objects.orders.functions.showDeleteConfirmationModal(order, loadList)">
{{ SessionUser.objects.global.language.delete }}
</a>-->
<a
class="card-footer-item"
@click="openOrderActionsModal(order)"
:data-testid="`pos-order-list-actions-${order.id}`"
>
<span class="icon mr-1">
<i class="fas fa-cog"></i>
</span>
</a>
<!-- Go to booking (if any) -->
<a
class="card-footer-item"
v-if="order.booking_id"
@click="redirectDepartmentBookingPage(order.booking_id, order.department_id, false)"
>
<span class="icon mr-1">
<i class="fas fa-calendar-alt"></i>
</span>
<span>{{ SessionUser.objects.order_bookings.meta.labels.single }}</span>
</a>
<!-- Go to order page -->
<a class="card-footer-item" @click="redirectDepartmentOrderPage(order.id, order.department_id, false)">
<span class="icon mr-1">
<i class="fas fa-arrow-right"></i>
</span>
</a>
</template>
</WhiteBoxCard>
</div>
</template>
<!-- Footer count -->
<div class="column is-full">
<div class="has-text-centered">
<small
>{{ t("tables.showing") }} {{ getVisibleOrders.length }}
{{ SessionUser.objects.orders.meta.labels.entries.toLowerCase() }}</small
>
</div>
</div>
</div>
</template>
</ViewportResponsiveWrapper>
<InvoiceMultipleCollectionsModal
v-if="isInvoiceMultipleCollectionsModalOpen"
v-bind:in-progress="invoiceCollectionQueueInProgress"
v-bind:failed="invoiceCollectionQueueFailed"
v-bind:succeeded="invoiceCollectionQueueSuccess"
v-bind:queued="invoiceCollectionQueue"
v-bind:jobs="invoiceQueue.invoiceCollectionQueueJobs"
v-bind:log="collectionQueueLog"
@closeModal="confirmCloseModal()"
@retry="retryInvoiceCollection"
/>
<!-- bottom progress bar -->
<InvoicingBillingPeriodInvoiceProgressBar
v-if="shouldShowLegacyInvoiceProgressBar"
v-bind:max-progress="
invoiceCollectionQueueSuccess.length +
invoiceCollectionQueueFailed.length +
invoiceCollectionQueueInProgress.length +
invoiceCollectionQueue.length
"
v-bind:progress="invoiceCollectionQueueSuccess.length"
v-bind:failed="invoiceCollectionQueueFailed.length"
v-bind:in-progress="invoiceCollectionQueueInProgress.length"
v-bind:queued="invoiceCollectionQueue.length"
@dismissed="isInvoiceMultipleCollectionsModalOpen = true"
/>
<!-- Order actions menu modal -->
<div
class="modal"
:class="{ 'is-active': isOrderActionsModalOpen }"
v-if="selectedOrderForActionsMenu"
data-testid="pos-order-actions-modal"
>
<div class="modal-background" @click="closeOrderActionsModal"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">{{ SessionUser.objects.global.language.actions }}</p>
<button class="delete" aria-label="close" @click="closeOrderActionsModal"></button>
</header>
<section class="modal-card-body">
<button
v-if="shouldShowDraftAssignmentActions && selectedOrderForActionsMenu"
class="button is-link is-light is-fullwidth mb-4"
:data-testid="`draft-order-assign-customer-modal-button-${selectedOrderForActionsMenu.id}`"
@click="openDraftAssignmentModal(selectedOrderForActionsMenu)"
>
<span class="icon">
<i class="fas fa-user-check"></i>
</span>
<span>{{ t("admin.pos.drafts_assignment.button") }}</span>
</button>
<ActionSettingsWheelButton
v-bind:user_id="selectedOrderForActionsMenu.user_id"
v-bind:order_id="selectedOrderForActionsMenu.id"
v-bind:invoice_collection_id="selectedOrderForActionsMenu.invoice_collection_id"
v-bind:customer_number="selectedOrderForActionsMenu.customer_id"
v-bind:reg_1="selectedOrderForActionsMenu.reg_1"
v-bind:reg_2="selectedOrderForActionsMenu.reg_2"
v-bind:reg_3="selectedOrderForActionsMenu.reg_3"
v-bind:order_booking_id="selectedOrderForActionsMenu.booking_id"
v-bind:department_id="selectedOrderForActionsMenu.department_id"
:refreshFunction="loadList"
@deleted="loadList()"
@flag-created="emitFlagCreated"
:displayActionsDirectly="true"
>
<template #actions></template>
</ActionSettingsWheelButton>
</section>
<footer class="modal-card-foot">
<button class="button is-light is-link is-fullwidth" @click="closeOrderActionsModal">
{{ SessionUser.objects.global.language.close }}
</button>
</footer>
</div>
</div>
<AssignDraftOrderCustomerModal
v-if="isDraftAssignmentModalOpen && selectedOrderForDraftAssignment"
:order="selectedOrderForDraftAssignment"
@close="closeDraftAssignmentModal"
@assigned="handleDraftAssignmentSuccess"
/>
</template>
<style scoped>
.status-bar-table-header {
max-width: 5px;
padding-left: 0;
padding-right: 0;
}
.status-bar-table-row {
border-radius: 5px;
width: 5px;
padding-left: 0;
padding-right: 0;
}
.status-bar.has-text-danger {
border-color: #ff3860;
}
.status-bar.has-text-warning {
border-color: #ffdd57;
}
.status-bar.has-text-success {
border-color: #48c774;
}
.buttons.pos-order-list-actions {
flex-wrap: nowrap;
justify-content: flex-end;
gap: 0.5rem;
}
.buttons.pos-order-list-actions > * {
flex-shrink: 0;
}
.pos-order-list-assign-customer-button {
border-radius: 0.8rem;
padding-inline: 0.85rem;
}
@media screen and (max-width: 768px) {
.pos-orders-mobile-card {
border: 1px solid #d7dee8;
border-radius: 0.9rem;
box-shadow: 0 10px 24px rgba(19, 35, 57, 0.08);
overflow: hidden;
}
.pos-orders-mobile-card :deep(.card) {
background: #ffffff;
}
.pos-orders-mobile-card :deep(.card-header) {
border-bottom: 1px solid #e3e9f2;
}
.pos-orders-mobile-card :deep(.card-content) {
border-top: 1px solid #eef2f7;
}
.pos-orders-mobile-card :deep(.card-footer) {
border-top: 1px solid #e3e9f2;
background: #fbfcfe;
}
}
</style>