Compare commits

..
Author SHA1 Message Date
Jeppe Bundgaard d435c6d60a Fix local date selection handling 2026-07-06 14:46:14 +02:00
Jeppe BandJeppe Bundgaard 278298ecd2 Submit error reports without screenshots (#153)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 14:28:24 +02:00
Jeppe BandJeppe Bundgaard dec90bbbbf Block restricted mobile additional items (#148)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 14:07:04 +02:00
Jeppe BandJeppe Bundgaard a4b9d2f04d Add superuser vehicle creation flow (#152)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 13:59:40 +02:00
Jeppe BandJeppe Bundgaard 22f8b5f203 Improve superuser role permissions page (#149)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 12:43:03 +02:00
Jeppe B 987acc5b0f Add role permission help modal (#142)
Adds the Rolle help icon and grouped permissions modal for limited backoffice employees, plus i18n and Playwright coverage.
2026-07-06 12:25:01 +02:00
Jeppe BandJeppe Bundgaard cf1509e34f Fix POS step metadata persistence (#138)
* Fix POS step metadata persistence

* Preserve registration save payload formatting

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 10:20:13 +02:00
Jeppe BandJeppe Bundgaard ce2e671768 Fix department notification SMS table refresh
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 09:57:30 +02:00
Jeppe BandJeppe Bundgaard cf2add29dd Default vehicle subscriptions to false (#136)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-06 09:21:31 +02:00
103 changed files with 8198 additions and 599 deletions
+19 -1
View File
@@ -7,7 +7,7 @@ export const fallbackChangePatterns = [
/^vite\.config\.js$/u,
/^playwright(?:\..+)?\.config\.(?:js|ts)$/u,
/^playwright\.global-(?:setup|teardown)\.mjs$/u,
/^scripts\/run-playwright/u,
/^scripts\/run-playwright-(?:pr|ci-parallel|batched-chromium)\.mjs$/u,
/^tests\/e2e\/(?:support|fixtures)\//u,
];
@@ -24,6 +24,15 @@ export const sourceMappings = [
specs: ["tests/e2e/navigation.smoke.spec.js"],
projects: chromiumProjects,
},
{
name: "superuser-roles-permissions",
patterns: [
/^src\/views\/dashboards\/superUserDashboard\/roles\/SuperUserRolesPermissions\.vue$/u,
/^src\/views\/dashboards\/superUserDashboard\/roles\/rolePermissionCatalog\.js$/u,
],
specs: ["tests/e2e/superuser-roles-permissions.spec.ts"],
projects: chromiumProjects,
},
{
name: "booking",
patterns: [/bookings?/iu, /time-bookings/iu, /^src\/views\/guest\/book\//u],
@@ -52,6 +61,15 @@ export const sourceMappings = [
specs: ["tests/e2e/edge-gateways.smoke.spec.js", "tests/e2e/edge-gateways.routes.spec.js"],
projects: chromiumProjects,
},
{
name: "admin-department-notifications",
patterns: [
/^src\/components\/displays\/department\/notifications\//u,
/^src\/components\/displays\/pagination\/models\/DepartmentPos\/NotificationsPhonePagination\.vue$/u,
],
specs: ["tests/e2e/admin-department-notifications.spec.ts"],
projects: chromiumProjects,
},
{
name: "invoicing",
patterns: [/invoic/iu, /economic[-/]?queue/iu, /collected-order/iu],
+2
View File
@@ -57,6 +57,7 @@ export const ownedFilesByRole = {
admin: [
"admin-bookings-mobile.spec.ts",
"admin-daily-report.spec.ts",
"admin-department-notifications.spec.ts",
"admin-department-visibility.spec.ts",
"admin-overview-mobile.spec.ts",
"admin-overview-night-washes.spec.ts",
@@ -105,6 +106,7 @@ export const ownedFilesByRole = {
"superuser-departments-archive.spec.ts",
"superuser-drafts.spec.ts",
"superuser-products-layout.spec.ts",
"superuser-roles-permissions.spec.ts",
"superuser-system-status.smoke.spec.js",
"superuser-users.spec.ts",
"superuser-vehicles.smoke.spec.js",
@@ -5,6 +5,7 @@ import { useWindowSize } from "@vueuse/core";
import { BMessage } from "buefy";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
type DateRange = {
startDate: Date,
@@ -115,23 +116,11 @@ const availableYears = computed(() => (
));
const formatDateInputValue = (date: Date) => {
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
return "";
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
return formatLocalDateOnly(date);
};
const parseDateInputValue = (value: string) => {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
if (!match) {
return new Date(value);
}
return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
return parseLocalDateOnly(value);
};
const isSameDateInputValue = (left: Date, right: Date) => (
@@ -4,12 +4,15 @@ import { useI18n } from 'vue-i18n';
const { t } = useI18n();
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
import { loadList } from "@/components/pagination/paginatedList.vue";
const props = defineProps({
objects: {
type: Array,
required: true,
},
loadList: {
type: Function,
required: true,
},
});
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { ref } from 'vue';
@@ -22,6 +25,52 @@ const su_object = SessionUser.objects.department_notification_sms;
const hasUpdatePermission = () => {
return (SessionUser.hasPermission('department_notification_sms_update') || SessionUser.canAccessSuperUser());
};
const enabledSavingIds = ref(new Set());
const isEnabled = (value) => {
return value === true || value === 1 || value === "1" || value === "true";
};
const isEnabledSaving = (id) => {
return enabledSavingIds.value.has(Number(id));
};
const setEnabledSaving = (id, saving) => {
const nextIds = new Set(enabledSavingIds.value);
const normalizedId = Number(id);
if (saving) {
nextIds.add(normalizedId);
} else {
nextIds.delete(normalizedId);
}
enabledSavingIds.value = nextIds;
};
const toggleEnabled = async (obj, event) => {
const target = event?.target;
const previousValue = isEnabled(obj.enabled);
if (!target || !hasUpdatePermission() || isEnabledSaving(obj.id)) {
if (target) {
target.checked = previousValue;
}
return;
}
const nextValue = Boolean(target.checked);
setEnabledSaving(obj.id, true);
try {
await su_object.set.enabled(obj.id, nextValue);
await props.loadList();
} catch (error) {
target.checked = previousValue;
console.error('Unable to update department notification SMS active state', error);
} finally {
setEnabledSaving(obj.id, false);
}
};
</script>
<template>
@@ -42,7 +91,7 @@ const hasUpdatePermission = () => {
<!-- Label -->
<EditableTableColumn
:object="obj"
:loadList="loadList"
:loadList="props.loadList"
:editFunction="su_object.showEditObjectFieldForm"
column="label"
:permission-check-function="hasUpdatePermission"
@@ -50,7 +99,7 @@ const hasUpdatePermission = () => {
<!-- Phone country code -->
<EditableTableColumn
:object="obj"
:loadList="loadList"
:loadList="props.loadList"
:editFunction="su_object.showEditObjectFieldForm"
column="phone_country_code"
:permission-check-function="hasUpdatePermission"
@@ -58,22 +107,27 @@ const hasUpdatePermission = () => {
<!-- Phone -->
<EditableTableColumn
:object="obj"
:loadList="loadList"
:loadList="props.loadList"
:editFunction="su_object.showEditObjectFieldForm"
column="phone"
:permission-check-function="hasUpdatePermission"
/>
<!-- Enabled -->
<EditableTableColumn
:object="obj"
:loadList="loadList"
:editFunction="su_object.showEditObjectFieldForm"
column="enabled"
:parse-function="(value) => {
return value ? t('common.yes') : t('common.no');
}"
:permission-check-function="hasUpdatePermission"
/>
<td>
<input
:id="`department-notification-sms-enabled-${obj.id}`"
class="switch is-rounded is-small"
type="checkbox"
:checked="isEnabled(obj.enabled)"
:disabled="!hasUpdatePermission() || isEnabledSaving(obj.id)"
:data-testid="`department-notification-sms-enabled-toggle-${obj.id}`"
:aria-label="`${su_object.columns.enabled.label}: ${obj.label}`"
@change="toggleEnabled(obj, $event)"
/>
<label :for="`department-notification-sms-enabled-${obj.id}`">
{{ isEnabled(obj.enabled) ? t('common.yes') : t('common.no') }}
</label>
</td>
<!-- Actions -->
<td>
<ActionSettingsWheelButton class="is-float-right">
@@ -81,9 +135,10 @@ const hasUpdatePermission = () => {
<RequiresPermission :permission="'department_notification_sms_delete'">
<template #default>
<ActionSettingsWheelItem
@click="su_object.functions.showDeleteObjectForm(obj.id, loadList)"
@click="su_object.functions.showDeleteObjectForm(obj.id, props.loadList)"
:icon="SessionUser.objects.global.icons.delete"
:label="t('global.delete')"
:test-id="`department-notification-sms-delete-${obj.id}`"
template="danger"
/>
</template>
@@ -91,6 +146,7 @@ const hasUpdatePermission = () => {
<ActionSettingsWheelItem
:icon="SessionUser.objects.global.icons.delete"
:label="t('global.delete')"
:test-id="`department-notification-sms-delete-${obj.id}`"
template="danger"
disabled
/>
@@ -111,4 +167,4 @@ const hasUpdatePermission = () => {
<style scoped>
</style>
</style>
@@ -1,9 +1,12 @@
<script setup>
import { computed, nextTick, ref, watch } from "vue";
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js";
import { isBlankPosMetadataValue } from "@/components/shop/POSDepartmentProcess.vue";
import {
isBlankPosMetadataValue,
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
const props = defineProps({
order_id: {
@@ -123,7 +126,7 @@ const referenceField = createCustomerWishField({
key: "reference",
label: computed(() => t("common.reference")),
source: () => props.reference,
saveValue: (value) => SessionUser.objects.orders.set.reference(props.order_id, value),
saveValue: (value) => saveOrderMetadataField("reference", value, props.order_id),
testIdBase: "pos-order-customer-wishes-reference",
isRequired: () => props.referenceRequired,
warningStateWhenEmpty: "danger",
@@ -134,7 +137,7 @@ const poField = createCustomerWishField({
key: "po",
label: computed(() => t("admin.pos.po_number")),
source: () => props.po,
saveValue: (value) => SessionUser.objects.orders.set.po(props.order_id, value),
saveValue: (value) => saveOrderMetadataField("po", value, props.order_id),
testIdBase: "pos-order-customer-wishes-po",
isRequired: () => props.poRequired,
warningStateWhenEmpty: "warning",
@@ -145,7 +148,7 @@ const safetySealField = createCustomerWishField({
key: "safety_seal",
label: computed(() => t("admin.pos.safety_seal")),
source: () => props.safetySeal,
saveValue: (value) => SessionUser.objects.orders.set.safety_seal(props.order_id, value),
saveValue: (value) => saveOrderMetadataField("safety_seal", value, props.order_id),
testIdBase: "pos-order-customer-wishes-safety-seal",
isRequired: () => false,
warningStateWhenEmpty: null,
@@ -161,6 +164,15 @@ const safetySealField = createCustomerWishField({
const fields = computed(() => {
return props.showSafetySeal ? [referenceField, poField, safetySealField] : [referenceField, poField];
});
const flushCustomerWishFields = async () => {
await Promise.all(fields.value.map((field) => field.autosave.flushAndWait()));
};
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(flushCustomerWishFields);
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
});
</script>
<template>
@@ -1,8 +1,11 @@
<script setup>
import { computed, nextTick, ref, watch } from 'vue';
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js";
import {
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
const props = defineProps({
order_id: {
@@ -30,8 +33,7 @@ const isEditing = ref(false);
const autosave = useOrderMetadataAutosave({
source: () => props.note,
saveValue: async (value) => {
await SessionUser.objects.orders.set.notes(props.order_id, value);
return value;
return saveOrderMetadataField("notes", value, props.order_id);
},
onSaved: async (savedValue, requestedValue) => {
if (typeof props.onSaved === "function") {
@@ -68,6 +70,12 @@ const lostFocus = async () => {
isEditing.value = false;
await autosave.flush();
};
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(() => autosave.flushAndWait());
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
});
</script>
<template>
@@ -1,8 +1,11 @@
<script setup>
import { computed, nextTick, ref, watch } from "vue";
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js";
import {
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
const props = defineProps({
index: {
@@ -36,8 +39,7 @@ autosave = useOrderMetadataAutosave({
source: () => props.modelValue,
normalizeValue: normalizeRegistrationDraft,
saveValue: async (value) => {
await SessionUser.objects.orders.set[fieldKey.value](props.order_id, value);
return value;
return saveOrderMetadataField(fieldKey.value, value, props.order_id);
},
onSaved: async () => {
await props.loadOrder();
@@ -74,6 +76,12 @@ const closeEditor = async () => {
isEditing.value = false;
await autosave.flush();
};
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(() => autosave.flushAndWait());
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
});
</script>
<template>
@@ -30,6 +30,7 @@ import PosLastScannedLicensePlatesV2 from "@/components/displays/department/pos/
import PosDesktopOrderBookingSelectorModal from "@/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue";
import PosDesktopCustomerConflictModal from "@/components/displays/department/pos/steps/elements/PosDesktopCustomerConflictModal.vue";
import PosDesktopDuplicateWarning from "@/components/displays/department/pos/steps/elements/PosDesktopDuplicateWarning.vue";
import { todayLocalDateOnly } from "@/services/dateOnly.js";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { parsePosRouteSearch } from "@/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js";
import {
@@ -377,8 +378,8 @@ const fetchDuplicateOrdersForContext = async (context) => {
try {
const response = await SessionUser.request(SessionUser.objects.orders.meta.endpoint, "GET", {
filters: `reg_1:${normalizedContext.reg1},department_id:${department_id.value},created_at-date_from:${
new Date().toISOString().split("T")[0]
},created_at-date_to:${new Date().toISOString().split("T")[0]}`,
todayLocalDateOnly()
},created_at-date_to:${todayLocalDateOnly()}`,
limit: 5,
});
@@ -4,7 +4,7 @@ import {
setBackgroundColor,
backgroundColors,
} from "@/components/viewport/page/headers/ViewportHeaderSettings.vue";
import { onMounted, ref, watch, computed } from "vue";
import { onBeforeUnmount, onMounted, ref, watch, computed } from "vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import PosDepartmentStepMobile2Product from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Product.vue";
import PosDepartmentStepMobile2LastOrder from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2LastOrder.vue";
@@ -43,6 +43,8 @@ import {
customer_name,
isAddonRestricted,
canBuyAdditionalServices,
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
import { createOrderItem, getOrderItems, removeOrderItem } from "@/components/shop/OrdersItems.vue";
import { PosProduct } from "@/components/displays/department/pos/steps/mobile/objects/PosProduct.vue";
@@ -196,8 +198,7 @@ const applyPendingBookingFromSelection = async () => {
// Set PO number on the order if provided by the booking
if (booking.po && typeof booking.po === "string" && booking.po.trim() !== "") {
try {
await SessionUser.objects.orders.set.po(order_id.value, booking.po);
order_po.value = booking.po.trim();
order_po.value = await saveOrderMetadataField("po", booking.po, order_id.value);
} catch (e) {
console.error("Failed to set PO on order from booking", e);
}
@@ -217,9 +218,9 @@ const applyPendingBookingFromSelection = async () => {
const bookingReference = String(fullBookingData.reference ?? fullBookingData.reference_number ?? "").trim();
if (bookingReference !== "") {
try {
await SessionUser.objects.orders.set.reference(order_id.value, bookingReference);
persistedReference.value = bookingReference;
metadata.setReference(bookingReference);
const savedReference = await saveOrderMetadataField("reference", bookingReference, order_id.value);
persistedReference.value = savedReference;
metadata.setReference(savedReference);
hydrateReferenceFromSources();
} catch (setRefError) {
console.error("Failed to set reference on order from booking:", setRefError);
@@ -228,10 +229,10 @@ const applyPendingBookingFromSelection = async () => {
const bookingNotes = String(fullBookingData.notes ?? fullBookingData.note ?? "").trim();
if (bookingNotes !== "") {
try {
await SessionUser.objects.orders.set.notes(order_id.value, bookingNotes);
order_notes.value = bookingNotes;
notes.value = bookingNotes;
metadata.setNotes(bookingNotes);
const savedNotes = await saveOrderMetadataField("notes", bookingNotes, order_id.value);
order_notes.value = savedNotes;
notes.value = savedNotes;
metadata.setNotes(savedNotes);
} catch (setNotesError) {
console.error("Failed to set notes on order from booking:", setNotesError);
}
@@ -243,13 +244,11 @@ const applyPendingBookingFromSelection = async () => {
const currentReg2 = normalizeRegistrationValue(reg_2.value);
if (bookingReg1 && bookingReg1 !== currentReg1) {
reg_1.value = bookingReg1;
await SessionUser.objects.orders.set.reg_1(order_id.value, bookingReg1);
reg_1.value = await saveOrderMetadataField("reg_1", bookingReg1, order_id.value);
}
if (bookingReg2 !== currentReg2) {
reg_2.value = bookingReg2;
await SessionUser.objects.orders.set.reg_2(order_id.value, bookingReg2);
reg_2.value = await saveOrderMetadataField("reg_2", bookingReg2, order_id.value);
}
const bookingMatches = Array.isArray(vehicles.vehicle_1.value?.booking_matches)
@@ -538,8 +537,7 @@ const notesAutosave = useOrderMetadataAutosave({
return value;
}
await SessionUser.objects.orders.set.notes(normalizedOrderId, value);
return value;
return saveOrderMetadataField("notes", value, normalizedOrderId);
},
onSaved: async (value) => {
order_notes.value = value;
@@ -555,8 +553,7 @@ const referenceAutosave = useOrderMetadataAutosave({
return value;
}
await SessionUser.objects.orders.set.reference(normalizedOrderId, value);
return value;
return saveOrderMetadataField("reference", value, normalizedOrderId);
},
onSaved: async (value) => {
persistedReference.value = value;
@@ -572,8 +569,7 @@ const safetySealAutosave = useOrderMetadataAutosave({
return value;
}
await SessionUser.objects.orders.set.safety_seal(normalizedOrderId, value);
return value;
return saveOrderMetadataField("safety_seal", value, normalizedOrderId);
},
onSaved: async (value) => {
order_safety_seal.value = value;
@@ -585,6 +581,18 @@ const stepTwoNotesInput = notesAutosave.draft;
const stepTwoReferenceInput = referenceAutosave.draft;
const stepTwoSafetySealInput = safetySealAutosave.draft;
const showSafetySealField = computed(() => transactionItems.containsWashCertificate());
const flushStepTwoMetadataFields = async () => {
await Promise.all([
notesAutosave.flushAndWait(),
referenceAutosave.flushAndWait(),
safetySealAutosave.flushAndWait(),
]);
};
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(flushStepTwoMetadataFields);
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
});
const resolveReferenceFromSources = () => {
const orderReference = normalizeReferenceValue(persistedReference.value);
@@ -22,7 +22,7 @@ import PosDepartmentStep2MobileVehicleSelection
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
import PosDepartmentStepMobile2FloatingCart
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2FloatingCart.vue";
import { isAddonRestricted, canBuyAdditionalServices } from "@/components/shop/POSDepartmentProcess.vue";
import { isAddonRestricted, canBuyAdditionalServices, isProductRestricted } from "@/components/shop/POSDepartmentProcess.vue";
const props = defineProps({
label: {
@@ -46,6 +46,7 @@ const props = defineProps({
},
});
const canSelectAdditionalItems = computed(() => canBuyAdditionalServices());
const checked = ref(props.defaultChecked);
// Function to generate a summary from the last order
function generateSummary(order: PosOrder): string {
@@ -72,6 +73,12 @@ const displaySubtitle = computed(() => {
});
// Emit event on toggle
function onToggle(isOpen: boolean) {
if (isOpen && !canSelectAdditionalItems.value) {
checked.value = false;
pos.views.additionalItemSelection.value = false;
return;
}
checked.value = isOpen;
// Open the additional item selection view if toggled open
if (isOpen) {
@@ -118,17 +125,45 @@ const getAvailableAdditionalItems = () => {
}));
}
const availableAdditionalItems = ref<Addon[]>(getAvailableAdditionalItems());
const isAdditionalItemRestricted = (product: PosProduct) => {
if (!canSelectAdditionalItems.value) {
return true;
}
if (isProductRestricted(product)) {
return true;
}
return isAddonRestricted(convertProductToAddon(product));
}
watch(canSelectAdditionalItems, (canSelect) => {
if (!canSelect) {
checked.value = false;
pos.views.additionalItemSelection.value = false;
}
});
// Computed property to filter out restricted additional items based on customer attributes
const filteredAdditionalItems = computed(() => {
// If additional services are restricted, return empty array
if (!canBuyAdditionalServices()) {
if (!canSelectAdditionalItems.value) {
return [];
}
// Filter out individually restricted addons
return availableAdditionalItems.value.filter((addon: Addon) => !isAddonRestricted(addon));
return availableAdditionalItems.value.filter((addon: Addon) => {
if (isAddonRestricted(addon)) {
return false;
}
if (addon.product && isProductRestricted(addon.product)) {
return false;
}
return true;
});
});
const onClickAddOtherProduct = () => {
if (!canSelectAdditionalItems.value) {
pos.views.additionalItemSelection.value = false;
return;
}
pos.views.additionalItemSelection.value = !pos.views.additionalItemSelection.value;
}
@@ -159,7 +194,9 @@ watch(() => pos.transactionItems.additionalItems.value, (newVal) => {
}, { deep: true });
const onClickAddProduct = async (product: PosProduct) => {
// If the product requires note, open note input.
if (isAdditionalItemRestricted(product)) {
return;
}
pos.transactionItems.addAdditionalItem(product);
// If the view is fullscreen, close it after adding
//if (pos.views.additionalItemSelection.value) {
@@ -171,7 +208,7 @@ const onClickAddProduct = async (product: PosProduct) => {
<template>
<div data-testid="pos-mobile-additional-items">
<!-- Minimal view, when not set as fullscreen view -->
<WhiteBoxCard :toggleable="pos.transactionItems.additionalItems.value && pos.transactionItems.additionalItems.value.length == 0"
<WhiteBoxCard :toggleable="canSelectAdditionalItems && pos.transactionItems.additionalItems.value && pos.transactionItems.additionalItems.value.length == 0"
:defaultOpen="pos.transactionItems.additionalItems.value && pos.transactionItems.additionalItems.value.length > 0"
@toggle="onToggle"
:forceState="(pos.transactionItems.additionalItems.value && pos.transactionItems.additionalItems.value.length > 0) ? true : (pos.views.additionalItemSelection.value)"
@@ -201,7 +238,7 @@ const onClickAddProduct = async (product: PosProduct) => {
</div>
</template>
<!-- Footer -->
<template #footer>
<template #footer v-if="canSelectAdditionalItems">
<!-- Select other product button -->
<a class="card-footer-item" data-testid="pos-mobile-additional-items-open" @click="onClickAddOtherProduct">
<span class="icon">
@@ -212,7 +249,7 @@ const onClickAddProduct = async (product: PosProduct) => {
</template>
</WhiteBoxCard>
<!-- Fullscreen view, when selecting other products -->
<template v-else>
<template v-else-if="canSelectAdditionalItems">
<div data-testid="pos-mobile-additional-items-selection">
<!-- Categories of products -->
<PosDepartmentStep2MobileVehicleSelection :onAddProduct="onClickAddProduct" :onSearchClick="() => console.warn('AdditionalItem Search Clicked')"/><!-- :asAddons="true" :addons="availableAdditionalItems" @update:addons="availableAdditionalItems = $event"/>-->
@@ -20,6 +20,7 @@ import {
hasAttribute,
uploadAttachment,
restoreStoredPosOrderId,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
import * as POSDepartmentProcess from "@/components/shop/POSDepartmentProcess.vue";
import { errors } from "@/components/request/HandleGlobalError.vue";
@@ -96,17 +97,30 @@ const props = defineProps({
},
});
const updateReference = () => {
if (!!order_id.value && order_id.value > 0) SessionUser.objects.orders.set.reference(order_id.value, reference.value);
const getActiveOrderId = () => {
const parsedOrderId = Number.parseInt(String(order_id.value ?? ""), 10);
return Number.isInteger(parsedOrderId) && parsedOrderId > 0 ? parsedOrderId : null;
};
const updateNotes = () => {
if (!!order_id.value && order_id.value > 0) SessionUser.objects.orders.set.notes(order_id.value, order_notes.value);
const updateReference = async () => {
const activeOrderId = getActiveOrderId();
if (activeOrderId) {
await saveOrderMetadataField("reference", reference.value, activeOrderId);
}
};
const updateSafetySeal = () => {
if (!!order_id.value && order_id.value > 0)
SessionUser.objects.orders.set.safety_seal(order_id.value, order_safety_seal.value);
const updateNotes = async () => {
const activeOrderId = getActiveOrderId();
if (activeOrderId) {
await saveOrderMetadataField("notes", order_notes.value, activeOrderId);
}
};
const updateSafetySeal = async () => {
const activeOrderId = getActiveOrderId();
if (activeOrderId) {
await saveOrderMetadataField("safety_seal", order_safety_seal.value, activeOrderId);
}
};
// Helpers: extracted for clarity and reuse
@@ -196,9 +210,9 @@ const step1 = async () => {
// Reset the vehicle selection
views.vehicleSelection.value = false;
// Merge references and notes
mergeReferences();
mergeNotes();
mergeSafetySeal();
await mergeReferences();
await mergeNotes();
await mergeSafetySeal();
// We only need to check if the customer requires reference, if the field is empty.
if (!isNonEmptyString(reference.value)) {
const isValid = await validateReferenceRequirements();
@@ -214,37 +228,39 @@ const mergeReferences = () => {
if (metadata.getReference() && metadata.getReference().length >= 1) {
// If there is no reference, set it to the registration number of vehicle 1
reference.value = metadata.getReference();
updateReference();
return updateReference();
} else if (reference.value && reference.value.length >= 1) {
// If there is a reference in the field, set it to the metadata
metadata.setReference(reference.value);
updateReference();
return updateReference();
} else if (vehicles.vehicle_1.value?.reference && vehicles.vehicle_1.value?.reference.length >= 1) {
// If there is no reference, set it to the registration number of vehicle 1
reference.value = vehicles.vehicle_1.value?.reference;
metadata.setReference(reference.value);
updateReference();
return updateReference();
}
return Promise.resolve();
};
const mergeNotes = () => {
if (metadata.getNotes() && metadata.getNotes().length >= 1) {
order_notes.value = metadata.getNotes();
updateNotes();
return updateNotes();
} else if (order_notes.value && order_notes.value.length >= 1) {
metadata.setNotes(order_notes.value);
updateNotes();
return updateNotes();
}
return Promise.resolve();
};
const mergeSafetySeal = () => {
const resolvedSafetySeal = getResolvedMobileSafetySeal();
order_safety_seal.value = resolvedSafetySeal;
syncMobileSafetySealState(resolvedSafetySeal);
updateSafetySeal();
return updateSafetySeal();
};
const finalizeStep1 = async () => {
mergeReferences();
mergeNotes();
mergeSafetySeal();
await mergeReferences();
await mergeNotes();
await mergeSafetySeal();
// Upload the attachments if there are any (And the order id has been created)
if (attachments.getBase64().length > 0 && order_id.value > 0) {
const uploadResults = await Promise.all(
@@ -5,6 +5,7 @@ import {
order_safety_seal,
reference,
clearStoredPosOrderId,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
import { metadata } from "./PosDepartmentStepMobileFlow.vue";
@@ -52,9 +53,9 @@ export const persistCurrentMobileOrderMetadata = async () => {
order_notes.value = normalizedNotes;
await Promise.all([
SessionUser.objects.orders.set.reference(normalizedOrderId, normalizedReference),
SessionUser.objects.orders.set.notes(normalizedOrderId, normalizedNotes),
SessionUser.objects.orders.set.safety_seal(normalizedOrderId, normalizedSafetySeal),
saveOrderMetadataField("reference", normalizedReference, normalizedOrderId),
saveOrderMetadataField("notes", normalizedNotes, normalizedOrderId),
saveOrderMetadataField("safety_seal", normalizedSafetySeal, normalizedOrderId),
]);
return normalizedOrderId;
@@ -1,6 +1,13 @@
<script setup lang="ts">
import { ref, onBeforeUnmount, onMounted, watch } from "vue";
import { order_id, reg_1, reg_2, reg_3, getOrderDetails } from "@/components/shop/POSDepartmentProcess.vue";
import {
order_id,
reg_1,
reg_2,
reg_3,
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
import PosDepartmentStepMobile1RegistrationNumberInputField from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1RegistrationNumberInputField.vue";
import BarcodeScanner from "@/components/viewport/elements/icons/BarcodeScanner.vue";
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
@@ -103,9 +110,7 @@ const createRegistrationAutosave = (vehicleIndex: number) =>
return value;
}
await SessionUser.objects.orders.set[`reg_${vehicleIndex}`](normalizedOrderId, value);
const refreshedOrder = await getOrderDetails(normalizedOrderId);
return refreshedOrder?.[`reg_${vehicleIndex}`] ?? value;
return saveOrderMetadataField(`reg_${vehicleIndex}`, value, normalizedOrderId);
},
onSaved: async (value) => {
setPersistedRegistrationValue(vehicleIndex, value);
@@ -194,6 +199,11 @@ const flushAllRegistrations = async () => {
await Promise.all([1, 2, 3].map((vehicleIndex) => registrationAutosaves[vehicleIndex].flush()));
};
const waitForAllRegistrationSaves = async () => {
await Promise.all([1, 2, 3].map((vehicleIndex) => registrationAutosaves[vehicleIndex].flushAndWait()));
};
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(waitForAllRegistrationSaves);
// Function to set a value if it is not null
const createVehicleSelectionFromSearchResult = (value: PosSearchResult) => ({
...value,
@@ -266,11 +276,12 @@ onMounted(() => {
});
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
void flushAllRegistrations();
});
const closeManualInput = async () => {
await flushAllRegistrations();
await waitForAllRegistrationSaves();
emit("close");
};
</script>
@@ -12,6 +12,7 @@ import PosDepartmentStepMobileButtonNextStep from "@/components/displays/departm
import SessionUser from "@/components/session/token/SessionUser.vue";
import { PosOrder } from "@/components/displays/department/pos/steps/mobile/objects/PosOrder.vue";
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
import { todayLocalDateOnly } from "@/services/dateOnly.js";
// Define the close event to emit when the component is closed
const emit = defineEmits(["close"]);
/** Display variables */
@@ -126,7 +127,7 @@ const liveTransactions = ref(null);
const isLoading = ref(true);
const syncListTransactionHistory = async () => {
let dateToday = new Date().toISOString().split("T")[0]; // Get today's date in YYYY-MM-DD format
let dateToday = todayLocalDateOnly(); // Get today's date in YYYY-MM-DD format
isLoading.value = true;
// Fetch the list of orders created today for the current department
SessionUser.objects.orders.get
@@ -1,6 +1,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { useI18n } from 'vue-i18n';
import { formatLocalDateOnly } from '@/services/dateOnly.js';
type startEndDate = {
start: Date; // The start date of the range
end: Date; // The end date of the range
@@ -27,7 +28,7 @@ const datePresetFunctions = {
month: {
// Get the first day of the month based on the provided date first day at 00:00:01
firstDayOfMonth: (date: Date): Date => {
return new Date(date.getFullYear(), date.getMonth(), 2, 0, 0, 1, 0); // Set to the first day of the month at 00:00:01
return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0); // Set to the first day of the month at 00:00:00
},
// Get the last day of the month based on the provided date last day at 23:59:59
lastDayOfMonth: (date: Date): Date => {
@@ -116,7 +117,7 @@ export const datePresets = <datePreset[]>[
*/
const convertToISO = (date: Date): string => {
return date.toISOString().split('T')[0]; // Convert to YYYY-MM-DD format
return formatLocalDateOnly(date); // Convert to YYYY-MM-DD format
};
export const dateFunctions = {
@@ -150,4 +151,4 @@ export default defineComponent({
},
});
</script>
<template></template>
<template></template>
@@ -47,7 +47,7 @@ if (router.currentRoute.value.params.departmentId) {
<span>{{ t('common.add') }} {{ SessionUser.objects.department_notification_sms.meta.labels.single }}</span>
</button>
</template>
<DepartmentNotificationsPhoneTable :objects="list" />
<DepartmentNotificationsPhoneTable :objects="list" :load-list="loadList" />
</TableLabeledPagination>
</template>
@@ -50,6 +50,7 @@ import ShowErrorField from "@/components/global/ShowErrorField.vue";
import { useI18n } from 'vue-i18n'
import { BSwitch } from "buefy";
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n()
const router = useRouter();
@@ -63,7 +64,7 @@ const parseInitialDate = (value) => {
return new Date();
}
const parsed = new Date(`${value}T00:00:00`);
const parsed = parseLocalDateOnly(value);
return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
};
@@ -129,7 +130,7 @@ const dateFrom = ref(parseInitialDate(props.initialDateFrom));
const dateTo = ref(parseInitialDate(props.initialDateTo));
const parsedDate = (date) => {
return new Date(date);
return parseLocalDateOnly(date);
};
const reloadScheduled = ref(false);
@@ -148,14 +149,14 @@ const actions = {
from: {
select: (date) => {
dateFrom.value = parsedDate(date);
setFilter("StartTime-date_from", date.toISOString().split("T")[0], false);
setFilter("StartTime-date_from", formatLocalDateOnly(date), false);
scheduleReload();
}
},
to: {
select: (date) => {
dateTo.value = parsedDate(date);
setFilter("StartTime-date_to", date.toISOString().split("T")[0], false);
setFilter("StartTime-date_to", formatLocalDateOnly(date), false);
scheduleReload();
}
},
@@ -109,8 +109,8 @@ import OrdersTable from "@/components/displays/department/pos/orders/ordersTable
import {SessionUser} from "@/components/session/token/SessionUser.vue";
import PaginationDisplayFilters from "@/components/displays/pagination/PaginationDisplayFilters.vue";
import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue";
import {now} from "@vueuse/core";
import { useI18n } from 'vue-i18n'
import { formatLocalDateOnly, parseLocalDateOnly, todayLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n()
const router = useRouter();
@@ -176,8 +176,8 @@ if (props.autoLoad) {
const date_from = ref(null);
const date_to = ref(null);
const onDateRangeSelected = (newSelectionStartDate, newSelectionToDate) => {
const formattedStartDate = newSelectionStartDate.toISOString().split("T")[0];
const formattedEndDate = newSelectionToDate.toISOString().split("T")[0];
const formattedStartDate = formatLocalDateOnly(newSelectionStartDate);
const formattedEndDate = formatLocalDateOnly(newSelectionToDate);
date_from.value = formattedStartDate;
date_to.value = formattedEndDate;
setFilter("created_at-date_from", formattedStartDate, true);
@@ -361,7 +361,7 @@ const doesEndpointMatch = (matcher) => {
<!-- Shortcuts for date filters -->
<DatePeriodSelector :on-selection-change="onDateRangeSelected"
:visibility="{ showDailySelector: false, showWeeklySelector: false, showMultipleMonthWarning: false, showUpdateButton: false, showMonthSelector: false, showStartDate: false, showEndDate: false, showSelectionValidity: false, showYearSelector: false, showToLabel: false }"
v-bind:selection="{ startDate: date_from ? new Date(date_from) : new Date(now()), endDate: date_to ? new Date(date_to) : new Date(now()) }"/>
v-bind:selection="{ startDate: parseLocalDateOnly(date_from || todayLocalDateOnly()), endDate: parseLocalDateOnly(date_to || todayLocalDateOnly()) }"/>
</div>
</template>
<template #default>
@@ -28,6 +28,7 @@ import {ref, watch} from "vue";
import { Colors } from "@/ThemeConfig.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useI18n } from 'vue-i18n'
import { todayLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n()
const router = useRouter();
@@ -52,7 +53,7 @@ watch(() => getFilter("status"), (value) => {
// If the route starts with /user, set the endpoint to /user/bookings
if (router.currentRoute.value.path.startsWith("/user")) {
setEndpoint("/user/bookings", false);
setFilter("date", new Date().toISOString().split("T")[0], false);
setFilter("date", todayLocalDateOnly(), false);
setOrder("created_at", "desc");
}
// If the route starts with /superuser, set the endpoint to /bookings
@@ -81,7 +82,7 @@ loadList();
const showingToday = ref(true);
watch(() => getFilter("date"), (value) => {
showingToday.value = value === new Date().toISOString().split("T")[0];
showingToday.value = value === todayLocalDateOnly();
});
// If the screen is mobile, set the is small variable to true
@@ -151,7 +152,7 @@ const showNewOrderBookingsPortal = () => {
<label class="label">{{ isSmall ? t('common.today') : t('pagination.show_only_today') }}</label>
<div class="control">
<div class="field">
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { setFilter('date', event.target.checked ? new Date().toISOString().split('T')[0] : '*') }" checked="checked" :class="{ 'is-link': showingToday }" />
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { setFilter('date', event.target.checked ? todayLocalDateOnly() : '*') }" checked="checked" :class="{ 'is-link': showingToday }" />
<label for="today"></label>
</div>
</div>
@@ -188,4 +189,4 @@ const showNewOrderBookingsPortal = () => {
<style scoped>
</style>
</style>
@@ -8,6 +8,7 @@ import OrderBookingsTable from "@/views/dashboards/userDashboard/bookings/displa
import { departments, getDepartments } from "@/components/pagination/departmentTabs.vue";
import { Colors } from "@/ThemeConfig.vue";
import { useI18n } from "vue-i18n";
import { endOfLocalDate, startOfLocalDate, todayLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n();
/**
@@ -52,11 +53,9 @@ const onOnlyTodayFilterChange = (event, autoLoadList = true) => {
setFilter("datetime-date_from", null, false);
setFilter("datetime-date_to", null, false);
} else {
const startOfDay = new Date().setHours(0, 0, 0, 0);
const endOfDay = new Date().setHours(23, 59, 59, 999);
//setFilter('datetime', null, false);
setFilter("datetime-date_from", new Date(startOfDay).toISOString(), false);
setFilter("datetime-date_to", new Date(endOfDay).toISOString(), false);
setFilter("datetime-date_from", startOfLocalDate(val).toISOString(), false);
setFilter("datetime-date_to", endOfLocalDate(val).toISOString(), false);
}
if (autoLoadList) {
loadList();
@@ -95,7 +94,7 @@ onMounted(() => {
departmentFilter.value = value;
} else if (key === "only_today" && value === true) {
setFilterKey = false; // Since the only_today filter is handled separately
onOnlyTodayFilterChange({ target: { value: new Date().toISOString().split("T")[0] } }, false);
onOnlyTodayFilterChange({ target: { value: todayLocalDateOnly() } }, false);
}
if (setFilterKey) {
setFilter(key, value, false);
@@ -170,7 +169,7 @@ onMounted(() => {
<div class="select">
<select :value="onlyTodayFilter" @change="onOnlyTodayFilterChange">
<option value="*">{{ t("common.all") }}</option>
<option :value="new Date().toISOString().split('T')[0]">{{ t("common.yes") }}</option>
<option :value="todayLocalDateOnly()">{{ t("common.yes") }}</option>
</select>
</div>
</div>
@@ -193,7 +192,7 @@ onMounted(() => {
@change="
(event) => {
onOnlyTodayFilterChange({
target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' },
target: { value: event.target.checked ? todayLocalDateOnly() : '*' },
});
}
"
@@ -226,7 +225,7 @@ onMounted(() => {
@change="
(event) => {
onOnlyTodayFilterChange(
{ target: { value: event.target.checked ? new Date().toISOString().split('T')[0] : '*' } },
{ target: { value: event.target.checked ? todayLocalDateOnly() : '*' } },
false
);
onOrderIdFilterChange({ target: { value: event.target.checked ? 'is null' : '*' } });
@@ -52,6 +52,10 @@ loadList();
:columnLabels="{ type: SessionUser.objects.vehicles.columns.type.label }"
/>
</div>
<div v-if="$slots.actions" class="vehicles-pagination__add-action">
<label class="label is-small">&nbsp;</label>
<slot name="actions" />
</div>
<!-- Create a new vehicle, if the route is /user -->
<div class="vehicles-pagination__add-action"
v-if="router.currentRoute.value.path.startsWith('/user')">
@@ -1,6 +1,7 @@
<script setup lang="ts">
import PaginationDisplayItemColumn from "@/components/displays/pagination/PaginationDisplayItemColumn.vue";
import { ref, computed } from "vue";
import { computed } from "vue";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
const props = defineProps({
label: {
type: String,
@@ -19,7 +20,7 @@ defineExpose({
});
const formattedDate = computed(() => {
return date.value.toISOString().split('T')[0];
return formatLocalDateOnly(date.value);
});
// This component is used to display a date input in a pagination display item column.
@@ -36,8 +37,7 @@ const formattedDate = computed(() => {
class="input"
:value="formattedDate"
@input="(e) => {
const newDate = new Date(e.target.value);
console.log('Selected date:', newDate);
const newDate = parseLocalDateOnly(e.target.value);
emit('update:date', newDate);
}"
/>
@@ -47,4 +47,4 @@ const formattedDate = computed(() => {
<style scoped>
</style>
</style>
@@ -1,5 +1,4 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { SessionUser } from '@/components/session/token/SessionUser.vue';
@@ -10,7 +9,7 @@ import PaginationDisplayTemplateButton
from "@/components/displays/pagination/templates/PaginationDisplayTemplateButton.vue";
import { datePresets , dateFunctions} from '@/components/displays/pagination/PaginationDisplayDates.vue';
const props = defineProps({
defineProps({
startDate: {
type: Date,
required: true,
@@ -46,6 +45,7 @@ function updateStartDate(date: Date) {
// Update the end date model when the date is changed
function updateEndDate(date: Date) {
endDateModel.value = date;
emits('update:endDate', date);
}
</script>
@@ -79,4 +79,4 @@ function updateEndDate(date: Date) {
<style scoped>
</style>
</style>
@@ -13,6 +13,7 @@ import { showDownloadWashCertificate } from "@/components/shop/DownloadWashCerti
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import Swal from 'sweetalert2';
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
import { todayLocalDateOnly, yesterdayLocalDateOnly } from "@/services/dateOnly.js";
const redirectBookingObjectPage = (objectId) => {
// Send the user to the object page
window.location.href = `/user/bookings/${objectId}`;
@@ -237,7 +238,8 @@ window.addEventListener('resize', () => {
* To organize the bookings, we want to show the bookings that are created today first, then yesterday, then all other days
* @type {string}
*/
const currentDate = new Date().toISOString().split("T")[0];
const currentDate = todayLocalDateOnly();
const previousDate = yesterdayLocalDateOnly();
/**
* Sort the bookings by date
@@ -362,7 +364,7 @@ const canUserEditObject = (object) => {
</span>
<span class="has-text-grey">
<!-- Human readable date (Today, Yesterday, etc.) -->
{{ object.date === currentDate ? $t('tables.bookings.today') : object.date === new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split("T")[0] ? $t('tables.bookings.yesterday') : object.date }}
{{ object.date === currentDate ? $t('tables.bookings.today') : object.date === previousDate ? $t('tables.bookings.yesterday') : object.date }}
<!-- Number of bookings on the date -->
({{ $t('tables.bookings.bookings_count', { count: objects.filter((booking) => booking.date === object.date).length }) }})
</span>
@@ -8,9 +8,9 @@ const props = defineProps(['label', 'isText']);
const label = computed(() => props.label || t('admin.pos.buttons.go_back'));
// Go to previous step
const goToPreviousStep = () => {
const goToPreviousStep = async () => {
// Get the department id
previousStep();
await previousStep();
}
const emptyIfText = (string) => props.isText ? '' : string;
@@ -32,4 +32,4 @@ const emptyIfText = (string) => props.isText ? '' : string;
<style scoped>
</style>
</style>
@@ -0,0 +1,241 @@
<script setup>
import { computed, nextTick, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import CustomerSearchSelect from "@/components/search/economic/CustomerSearchSelect.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
const emit = defineEmits(["close", "created"]);
const { t } = useI18n();
const selectedCustomer = ref(null);
const registrationNumber = ref("");
const vehicleType = ref("");
const washSubscription = ref(false);
const reference = ref("");
const vehicleTypeOptions = ref([]);
const isLoadingVehicleTypes = ref(false);
const isSubmitting = ref(false);
const errorMessage = ref("");
const vehicleTypeError = ref("");
const getCustomerNumber = (customer) => {
const parsedValue = Number.parseInt(
String(customer?.customerNumber ?? customer?.customer_number ?? customer?.customer_id ?? customer?.id ?? ""),
10
);
return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null;
};
const selectedCustomerNumber = computed(() => getCustomerNumber(selectedCustomer.value));
const normalizedVehicleType = computed(() => {
if (vehicleType.value === "") {
return null;
}
const parsedValue = Number.parseInt(String(vehicleType.value), 10);
return Number.isInteger(parsedValue) && parsedValue >= 0 ? parsedValue : null;
});
const normalizedRegistrationNumber = computed(() => {
return registrationNumber.value.trim().toUpperCase();
});
const canSubmit = computed(() => {
return (
!isSubmitting.value &&
!isLoadingVehicleTypes.value &&
selectedCustomerNumber.value !== null &&
normalizedRegistrationNumber.value.length > 0 &&
normalizedVehicleType.value !== null
);
});
const parseErrorMessage = (error) => {
return SessionUser.functions.parseErrorMessage(error) || t("vehicles.add_modal.error");
};
const loadVehicleTypes = async () => {
isLoadingVehicleTypes.value = true;
vehicleTypeError.value = "";
try {
vehicleTypeOptions.value = await SessionUser.objects.vehicles.columns.type.options();
} catch (error) {
vehicleTypeError.value = parseErrorMessage(error) || t("vehicles.add_modal.type_load_error");
} finally {
isLoadingVehicleTypes.value = false;
}
};
const closeModal = () => {
if (!isSubmitting.value) {
emit("close");
}
};
const submitVehicle = async () => {
if (!canSubmit.value) {
errorMessage.value = t("vehicles.add_modal.validation_error");
return;
}
isSubmitting.value = true;
errorMessage.value = "";
try {
const response = await SessionUser.objects.vehicles.add(
normalizedVehicleType.value,
normalizedRegistrationNumber.value,
washSubscription.value,
selectedCustomerNumber.value,
reference.value.trim() || null
);
emit("created", response);
} catch (error) {
errorMessage.value = parseErrorMessage(error);
} finally {
isSubmitting.value = false;
}
};
onMounted(async () => {
await loadVehicleTypes();
await nextTick();
document.getElementById("superuser-add-vehicle-customer-search")?.focus();
});
</script>
<template>
<div class="modal is-active" data-testid="superuser-add-vehicle-modal">
<div class="modal-background" @click="closeModal"></div>
<div class="modal-card superuser-add-vehicle-modal">
<header class="modal-card-head">
<p class="modal-card-title">{{ t("vehicles.add_modal.title") }}</p>
<button
class="delete"
type="button"
:aria-label="t('common.close')"
data-testid="superuser-add-vehicle-close"
@click="closeModal"
></button>
</header>
<section class="modal-card-body">
<div v-if="errorMessage" class="notification is-danger is-light" data-testid="superuser-add-vehicle-error">
{{ errorMessage }}
</div>
<CustomerSearchSelect
v-model="selectedCustomer"
input-id="superuser-add-vehicle-customer-search"
test-id-prefix="superuser-add-vehicle-customer"
:disabled="isSubmitting"
:placeholder="t('vehicles.add_modal.customer_placeholder')"
/>
<div class="field">
<label class="label" for="superuser-add-vehicle-registration">{{ t("vehicles.form.license_plate") }}</label>
<div class="control">
<input
id="superuser-add-vehicle-registration"
v-model="registrationNumber"
class="input"
type="text"
autocomplete="off"
:placeholder="t('vehicles.add_modal.registration_placeholder')"
:disabled="isSubmitting"
data-testid="superuser-add-vehicle-registration"
/>
</div>
</div>
<div class="field">
<label class="label" for="superuser-add-vehicle-type">{{ t("vehicles.form.type") }}</label>
<div class="control" :class="{ 'is-loading': isLoadingVehicleTypes }">
<div class="select is-fullwidth">
<select
id="superuser-add-vehicle-type"
v-model="vehicleType"
:disabled="isSubmitting || isLoadingVehicleTypes || vehicleTypeOptions.length === 0"
data-testid="superuser-add-vehicle-type"
>
<option disabled value="">{{ t("vehicles.add_modal.type_placeholder") }}</option>
<option v-for="option in vehicleTypeOptions" :key="option.id" :value="option.id">
{{ option.name }}
</option>
</select>
</div>
</div>
<p v-if="vehicleTypeError" class="help is-danger" data-testid="superuser-add-vehicle-type-error">
{{ vehicleTypeError }}
</p>
</div>
<div class="field">
<label class="checkbox">
<input
v-model="washSubscription"
type="checkbox"
:disabled="isSubmitting"
data-testid="superuser-add-vehicle-wash-subscription"
/>
{{ t("objects.vehicles.columns.wash_subscription") }}
</label>
</div>
<div class="field">
<label class="label" for="superuser-add-vehicle-reference">{{ t("common.reference") }}</label>
<div class="control">
<input
id="superuser-add-vehicle-reference"
v-model="reference"
class="input"
type="text"
autocomplete="off"
:placeholder="t('vehicles.add_modal.reference_placeholder')"
:disabled="isSubmitting"
data-testid="superuser-add-vehicle-reference"
/>
</div>
</div>
</section>
<footer class="modal-card-foot is-justify-content-flex-end">
<button
class="button"
type="button"
:disabled="isSubmitting"
data-testid="superuser-add-vehicle-cancel"
@click="closeModal"
>
{{ t("common.cancel") }}
</button>
<button
class="button is-link"
type="button"
:class="{ 'is-loading': isSubmitting }"
:disabled="!canSubmit"
data-testid="superuser-add-vehicle-submit"
@click="submitVehicle"
>
{{ t("vehicles.add_modal.submit") }}
</button>
</footer>
</div>
</div>
</template>
<style scoped>
.superuser-add-vehicle-modal {
max-width: min(44rem, calc(100vw - 2rem));
width: 100%;
}
.modal-card-body {
overflow: visible;
}
</style>
@@ -19,17 +19,17 @@ const SENSITIVE_QUERY_KEYS = /authorization|password|passwd|secret|token|api[_-]
const FALLBACK_LABELS = {
"error_report.button": "Report error",
"error_report.title": "Report error",
"error_report.subtitle": "Send the current screen and recent error details to support.",
"error_report.subtitle": "Send recent error details to support. A screenshot is attached when available.",
"error_report.before_error": "What were you doing before the error occurred?",
"error_report.expected": "What did you expect would happen?",
"error_report.actual": "What actually happened?",
"error_report.before_error_placeholder": "Describe the action you were taking, for example opening orders or selecting a customer.",
"error_report.expected_placeholder": "Describe the result you expected to see.",
"error_report.actual_placeholder": "Describe what you saw instead, including any error text.",
"error_report.consent": "I accept that the current app screen, recent request errors, Vue errors, browser details, and my answers are collected for troubleshooting.",
"error_report.consent": "I accept that recent request errors, Vue errors, browser details, my answers, and an app screenshot when available are collected for troubleshooting.",
"error_report.submit": "Submit report",
"error_report.submitted": "Error report submitted.",
"error_report.capture_failed": "The screen capture failed. Please try again.",
"error_report.capture_failed": "The screen capture failed. The report will be sent without a screenshot.",
"error_report.submit_failed": "The error report could not be submitted.",
"error_report.required": "All fields and data collection acceptance are required.",
};
@@ -277,15 +277,18 @@ const submit = async () => {
isSubmitting.value = true;
try {
let screenshot = "";
let screenshot = null;
try {
screenshot = await captureScreenshot();
} catch {
captureError.value = tr("error_report.capture_failed");
return;
}
const context = buildContext();
context.screenshot_attachment = {
status: screenshot ? "stored" : "capture_failed",
attached: Boolean(screenshot),
};
await submitErrorReport({
before_error: form.before_error.trim(),
expected: form.expected.trim(),
@@ -340,7 +343,7 @@ const submit = async () => {
<div v-if="submitted" class="notification is-success is-light" data-testid="error-report-submitted">
{{ tr("error_report.submitted") }}
</div>
<div v-if="captureError" class="notification is-danger is-light">
<div v-if="captureError" class="notification is-warning is-light" data-testid="error-report-capture-warning">
{{ captureError }}
</div>
<div v-if="submitError" class="notification is-danger is-light">
+4 -3
View File
@@ -4,6 +4,7 @@ import { ref, watch, onMounted } from "vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useRoute } from "vue-router";
import { isAccessibleVisibleNamedDepartment, sortByDepartmentPriorityOrder } from "@/services/departmentVisibility.js";
import { todayLocalDateOnly } from "@/services/dateOnly.js";
// Get the department ID from the URL
const route = useRoute();
const department_id = route.params.departmentId || SessionUser.functions.getDepartmentIdFromUrl() || null;
@@ -73,9 +74,9 @@ const emphasis = ref({
"department:" +
department_id +
",status:pending,date-date_from:" +
new Date().toISOString().split("T")[0] +
todayLocalDateOnly() +
",date-date_to:" +
new Date().toISOString().split("T")[0],
todayLocalDateOnly(),
limit: 1, // Limit to 1 booking, we only need to know if there are any bookings or not
page: 1,
})
@@ -97,7 +98,7 @@ const emphasis = ref({
* Check if the department has a daily report for today
*/
SessionUser.request(SessionUser.objects.department_daily_reports.meta.endpoint + "/get", "GET", {
date: new Date().toISOString().split("T")[0],
date: todayLocalDateOnly(),
id: department_id, // This refers to the department ID
})
.then((response) => {
@@ -0,0 +1,288 @@
<script setup>
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { isSearching, searchCustomer, searchCustomerResults } from "@/components/search/economic/customerSearch.vue";
const props = defineProps({
modelValue: {
type: Object,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
inputId: {
type: String,
default: "customer-search-select-input",
},
testIdPrefix: {
type: String,
default: "customer-search-select",
},
placeholder: {
type: String,
default: "",
},
});
const emit = defineEmits(["update:modelValue", "selected", "cleared"]);
const { t } = useI18n();
const searchQuery = ref("");
const showResults = ref(false);
const selectedResultIndex = ref(-1);
const getCustomerNumber = (customer) => {
const parsedValue = Number.parseInt(
String(customer?.customerNumber ?? customer?.customer_number ?? customer?.customer_id ?? customer?.id ?? ""),
10
);
return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null;
};
const getCustomerName = (customer) => {
return String(customer?.name ?? customer?.customer_name ?? customer?.customerName ?? "").trim();
};
const getCustomerCity = (customer) => {
return String(customer?.city ?? customer?.address_city ?? "").trim();
};
const formatCustomer = (customer) => {
if (!customer) {
return "";
}
const name = getCustomerName(customer);
const customerNumber = getCustomerNumber(customer);
return [name, customerNumber ? `#${customerNumber}` : null].filter(Boolean).join(" - ");
};
const selectedCustomerNumber = computed(() => getCustomerNumber(props.modelValue));
const selectedCustomerName = computed(() => getCustomerName(props.modelValue));
const selectedCustomerCity = computed(() => getCustomerCity(props.modelValue));
const hasResults = computed(() => searchCustomerResults.value.length > 0);
const placeholderText = computed(() => props.placeholder || t("vehicles.add_modal.customer_placeholder"));
const resetSearchResults = () => {
searchCustomer(null);
selectedResultIndex.value = -1;
};
const setSelectedCustomer = (customer) => {
emit("update:modelValue", customer);
emit("selected", customer);
searchQuery.value = formatCustomer(customer);
showResults.value = false;
selectedResultIndex.value = -1;
};
const clearSelectedCustomer = async () => {
emit("update:modelValue", null);
emit("cleared");
searchQuery.value = "";
showResults.value = false;
resetSearchResults();
await nextTick();
document.getElementById(props.inputId)?.focus();
};
const handleSearchInput = () => {
if (props.modelValue) {
emit("update:modelValue", null);
}
const query = searchQuery.value.trim();
selectedResultIndex.value = -1;
if (!query) {
showResults.value = false;
resetSearchResults();
return;
}
showResults.value = true;
searchCustomer(query);
};
const handleFocus = () => {
if (searchQuery.value.trim() && hasResults.value) {
showResults.value = true;
}
};
const handleBlur = () => {
window.setTimeout(() => {
showResults.value = false;
selectedResultIndex.value = -1;
}, 150);
};
const handleKeydown = (event) => {
if (!showResults.value || !hasResults.value) {
return;
}
if (event.key === "ArrowDown") {
event.preventDefault();
selectedResultIndex.value = Math.min(selectedResultIndex.value + 1, searchCustomerResults.value.length - 1);
} else if (event.key === "ArrowUp") {
event.preventDefault();
selectedResultIndex.value = Math.max(selectedResultIndex.value - 1, 0);
} else if (event.key === "Enter") {
event.preventDefault();
const selectedCustomer = searchCustomerResults.value[selectedResultIndex.value] || searchCustomerResults.value[0];
if (selectedCustomer) {
setSelectedCustomer(selectedCustomer);
}
} else if (event.key === "Escape") {
showResults.value = false;
selectedResultIndex.value = -1;
}
};
watch(
() => props.modelValue,
(customer) => {
if (customer) {
searchQuery.value = formatCustomer(customer);
} else if (!document.activeElement || document.activeElement.id !== props.inputId) {
searchQuery.value = "";
}
},
{ immediate: true }
);
onBeforeUnmount(() => {
resetSearchResults();
});
</script>
<template>
<div class="customer-search-select">
<div class="field">
<label class="label" :for="inputId">{{ t("vehicles.add_modal.customer_label") }}</label>
<div class="control has-icons-left" :class="{ 'is-loading': isSearching }">
<input
:id="inputId"
v-model="searchQuery"
class="input"
type="text"
autocomplete="off"
:placeholder="placeholderText"
:disabled="disabled"
:aria-expanded="showResults && hasResults"
:data-testid="`${testIdPrefix}-input`"
@input="handleSearchInput"
@focus="handleFocus"
@blur="handleBlur"
@keydown="handleKeydown"
/>
<span class="icon is-left">
<i class="fas fa-search"></i>
</span>
</div>
</div>
<div
v-if="showResults && hasResults"
class="dropdown is-active customer-search-select__dropdown"
:data-testid="`${testIdPrefix}-results`"
>
<div class="dropdown-menu customer-search-select__menu" role="listbox">
<div class="dropdown-content">
<button
v-for="(customer, index) in searchCustomerResults"
:key="getCustomerNumber(customer) || index"
type="button"
class="dropdown-item customer-search-select__option"
:class="{ 'is-active': selectedResultIndex === index }"
:data-testid="`${testIdPrefix}-option-${index}`"
@mousedown.prevent="setSelectedCustomer(customer)"
>
<span class="customer-search-select__option-main">{{ getCustomerName(customer) }}</span>
<span class="customer-search-select__option-meta">
#{{ getCustomerNumber(customer) }}
<template v-if="getCustomerCity(customer)"> · {{ getCustomerCity(customer) }}</template>
</span>
</button>
</div>
</div>
</div>
<div v-if="modelValue" class="customer-search-select__selected" :data-testid="`${testIdPrefix}-selected`">
<div>
<p class="has-text-weight-semibold">{{ t("vehicles.add_modal.selected_customer") }}</p>
<p>{{ selectedCustomerName }}</p>
<p class="is-size-7 has-text-grey">
#{{ selectedCustomerNumber }}
<span v-if="selectedCustomerCity"> · {{ selectedCustomerCity }}</span>
</p>
</div>
<button
type="button"
class="button is-small is-light"
:disabled="disabled"
:data-testid="`${testIdPrefix}-clear`"
@click="clearSelectedCustomer"
>
{{ t("common.clear") }}
</button>
</div>
</div>
</template>
<style scoped>
.customer-search-select {
position: relative;
}
.customer-search-select__dropdown,
.customer-search-select__menu {
width: 100%;
}
.customer-search-select__dropdown {
left: 0;
position: absolute;
right: 0;
top: 4.75rem;
z-index: 40;
}
.customer-search-select__option {
align-items: flex-start;
background: transparent;
border: 0;
display: flex;
flex-direction: column;
text-align: left;
white-space: normal;
width: 100%;
}
.customer-search-select__option-main {
font-weight: 600;
}
.customer-search-select__option-meta {
color: #6b7280;
font-size: 0.8rem;
}
.customer-search-select__selected {
align-items: flex-start;
background: #f5f8fc;
border: 1px solid #d8e2ef;
border-radius: 6px;
display: flex;
gap: 1rem;
justify-content: space-between;
margin-bottom: 1rem;
padding: 0.85rem 1rem;
}
</style>
@@ -5,6 +5,7 @@ import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/Ob
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {ref} from "vue";
import i18n from '@/i18n';
import { todayLocalDateOnly } from "@/services/dateOnly.js";
const t = (key) => i18n.global.t(key);
@@ -241,7 +242,7 @@ export const Bookings = {
'/bookings',
'GET',
{
filters: `department:${department},status:pending,date-date_from:${new Date().toISOString().split('T')[0]},date-date_to:${new Date().toISOString().split('T')[0]}`,
filters: `department:${department},status:pending,date-date_from:${todayLocalDateOnly()},date-date_to:${todayLocalDateOnly()}`,
limit: 100,
page: 1
}
@@ -289,4 +290,4 @@ export const Bookings = {
);
}
};
</script>
</script>
@@ -5,6 +5,7 @@ import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/Ob
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {ref} from "vue";
import i18n from '@/i18n';
import { todayLocalDateOnly } from "@/services/dateOnly.js";
const t = (key) => i18n.global.t(key);
@@ -80,7 +81,7 @@ const t = (key) => i18n.global.t(key);
water_usage_morning: parseInt(water_usage_morning),
water_usage: parseInt(water_usage),
notes: notes ? notes : 'Daily report',
date: date ? date : new Date().toISOString().split('T')[0]
date: date ? date : todayLocalDateOnly()
}
);
},
@@ -1,6 +1,7 @@
<script>
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { formatLocalDateOnly, todayLocalDateOnly } from "@/services/dateOnly.js";
/**
* The DepartmentTimeBookingsEntries object
@@ -230,10 +231,10 @@ export const DepartmentTimeBookingsEntries = {
if (!dateFrom) {
const lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
dateFrom = lastWeek.toISOString().split('T')[0]; // Format as YYYY-MM-DD
dateFrom = formatLocalDateOnly(lastWeek); // Format as YYYY-MM-DD
}
if (!dateTo) {
dateTo = new Date().toISOString().split('T')[0]; // Format as YYYY-MM-DD
dateTo = todayLocalDateOnly(); // Format as YYYY-MM-DD
}
console.log("Fetching public available times for department:", departmentId, "from", dateFrom, "to", dateTo);
// Make the request to fetch public available times for the department
@@ -279,4 +280,4 @@ export const DepartmentTimeBookingsEntries = {
);
}
};
</script>
</script>
@@ -620,6 +620,13 @@ export const ObjectsGlobal = {
function getLockedValue(column, defaultValue = '') {
return lockedValues[column] !== undefined ? lockedValues[column] : '';
}
function getCreationDefaultValue(column, defaultValue = '') {
return columns[column].creation.default !== undefined ? columns[column].creation.default : defaultValue;
}
function isBooleanDefault(column, value) {
const defaultValue = getCreationDefaultValue(column, true);
return String(defaultValue) === String(value);
}
// Add the form fields
for (const column in columns) {
if (columns[column].creation.required) {
@@ -647,8 +654,8 @@ export const ObjectsGlobal = {
<!-- Select -->
<div class="select is-fullwidth">
<select id="${column}" class="has-background-light has-text-black">
<option value="true">Ja</option>
<option value="false">Nej</option>
<option value="true" ${isBooleanDefault(column, true) ? 'selected' : ''}>Ja</option>
<option value="false" ${isBooleanDefault(column, false) ? 'selected' : ''}>Nej</option>
</select>
</div>
</div>`;
@@ -97,6 +97,7 @@ export const Vehicles = {
sortable: true,
creation: {
required: true,
default: false,
}
},
customer_id: {
@@ -117,7 +118,7 @@ export const Vehicles = {
},
};
},
add: async (type, reg, wash_subscription, customer_id, reference = null) => {
add: async (type, reg, wash_subscription = false, customer_id, reference = null) => {
return ObjectsGlobal.add.object(
Vehicles.meta.endpoint,
{
@@ -22,7 +22,12 @@ export const getUsersVehicles = async () => {
}
export const addVehicleUser = async (reg, type, notes) => {
return await authenticatedRequest("/user/vehicles", "POST", {reg, type, notes})
return await authenticatedRequest("/vehicles", "POST", {
reg,
type: parseInt(type),
wash_subscription: false,
...(notes ? { notes } : {}),
})
.then(
(response) => {
vehicles.value.push(response.data.data);
@@ -35,4 +40,4 @@ export const addVehicleUser = async (reg, type, notes) => {
);
}
</script>
</script>
+201 -59
View File
@@ -52,6 +52,42 @@ export const nextStepDelay = ref(0); // 2 seconds delay
export const isCreatingOrder = ref(false);
export const desktopStep1PreflightHandler = ref(null);
let createOrderRequest = null;
const posStepSaveBarriers = new Set();
let latestOrderDetailsRequestId = 0;
let latestOrderMetadataMutationSequence = 0;
let activeOrderMetadataMutationCount = 0;
export const registerPosStepSaveBarrier = (handler) => {
if (typeof handler !== "function") {
return () => {};
}
posStepSaveBarriers.add(handler);
return () => {
posStepSaveBarriers.delete(handler);
};
};
export const unregisterPosStepSaveBarrier = (handler) => {
posStepSaveBarriers.delete(handler);
};
export const flushPosStepSaveBarriers = async () => {
const barriers = Array.from(posStepSaveBarriers);
if (barriers.length === 0) {
return true;
}
const results = await Promise.allSettled(barriers.map((handler) => handler()));
const failedResult = results.find((result) => result.status === "rejected");
if (failedResult) {
parseError(failedResult.reason, "stepError");
return false;
}
return true;
};
export const setDesktopStep1PreflightHandler = (handler = null) => {
desktopStep1PreflightHandler.value = typeof handler === "function" ? handler : null;
@@ -109,6 +145,10 @@ export const nextStep = async (options = { isMobile: false, orderCreation: true
}
// Set the delay to 2 seconds
setNextStepDelay(2);
if (!(await flushPosStepSaveBarriers())) {
nextStepDelay.value = 0;
return;
}
// Clear the errors
clearErrors();
// If the current step is 1, create the order
@@ -188,8 +228,22 @@ export const nextStep = async (options = { isMobile: false, orderCreation: true
step.value++;
};
export const previousStep = () => {
step.value--;
export const previousStep = async () => {
if (nextStepDelay.value > 0) {
return;
}
if (!(await flushPosStepSaveBarriers())) {
return;
}
step.value = Math.max(1, step.value - 1);
if (order_id.value) {
pushPosRouteState(`id=${order_id.value}&customer_id=${customer_id.value}&step=${step.value}`);
} else {
pushPosRouteState(`step=${step.value}`);
}
};
export const getCurrentStep = () => {
@@ -784,6 +838,129 @@ watch([reg_1, reg_2, reg_3], () => {
reg_3.value = normalizeRegistrationValue(reg_3.value);
});
const applyOrderMetadataFieldToPosState = (field, value) => {
if (field === "reference") {
reference.value = normalizeOrderMetadataString(value);
return reference.value;
}
if (field === "notes") {
order_notes.value = normalizeOrderMetadataString(value);
return order_notes.value;
}
if (field === "po") {
order_po.value = normalizeOrderMetadataString(value);
return order_po.value;
}
if (field === "safety_seal") {
order_safety_seal.value = normalizeOrderMetadataString(value);
return order_safety_seal.value;
}
if (field === "reg_1") {
reg_1.value = normalizeRegistrationValue(value);
return reg_1.value;
}
if (field === "reg_2") {
reg_2.value = normalizeRegistrationValue(value);
return reg_2.value;
}
if (field === "reg_3") {
reg_3.value = normalizeRegistrationValue(value);
return reg_3.value;
}
return normalizeOrderMetadataString(value);
};
export const applyOrderDetailsToPosState = (orderData, includes = {}) => {
if (!orderData || typeof orderData !== "object") {
return null;
}
const includedCustomer = includes?.customer ?? null;
const normalizedOrderCustomerNumber = resolveCustomerNumber(orderData?.customer_id);
const selectedCustomerBeforeHydration = getSelectedCustomerNumber();
department_id.value = orderData.department_id;
applyOrderMetadataFieldToPosState("reference", orderData.reference);
applyOrderMetadataFieldToPosState("notes", orderData.notes);
applyOrderMetadataFieldToPosState("po", orderData.po);
applyOrderMetadataFieldToPosState("safety_seal", orderData.safety_seal);
applyOrderMetadataFieldToPosState("reg_1", orderData.reg_1);
applyOrderMetadataFieldToPosState("reg_2", orderData.reg_2);
applyOrderMetadataFieldToPosState("reg_3", orderData.reg_3);
invoiceCollectionId.value = orderData.invoice_collection_id || null;
completed_at.value = orderData.completed_at || null;
if (includedCustomer && typeof includedCustomer === "object") {
const customerSource =
includedCustomer.economic_customer && typeof includedCustomer.economic_customer === "object"
? includedCustomer.economic_customer
: includedCustomer;
selectCustomer(
normalizeCustomerRecord(customerSource, {
...includedCustomer,
customerNumber: normalizedOrderCustomerNumber ?? resolveCustomerNumber(includedCustomer),
name: includedCustomer.name ?? includedCustomer.customer_name ?? orderData.customer_name ?? customer_name.value,
customer_name:
includedCustomer.customer_name ?? includedCustomer.name ?? orderData.customer_name ?? customer_name.value,
})
);
} else if (normalizedOrderCustomerNumber) {
customer_id.value = normalizedOrderCustomerNumber;
if (
selectedCustomerBeforeHydration !== normalizedOrderCustomerNumber ||
customer_name.value === "" ||
!customer_data.value
) {
void searchAndSelectCustomer(normalizedOrderCustomerNumber, { forceRefresh: true });
}
} else {
clearSelectedCustomerState();
}
return orderData;
};
export const saveOrderMetadataField = async (field, value, targetOrderId = order_id.value) => {
const normalizedOrderId = toPositiveInteger(targetOrderId);
const setter = SessionUser.objects.orders.set?.[field];
if (!normalizedOrderId || typeof setter !== "function") {
throw new Error(`Unable to save order field: ${field}`);
}
const mutationSequence = latestOrderMetadataMutationSequence + 1;
latestOrderMetadataMutationSequence = mutationSequence;
activeOrderMetadataMutationCount += 1;
const isRegistrationField = field === "reg_1" || field === "reg_2" || field === "reg_3";
const requestValue = isRegistrationField
? String(value ?? "").toUpperCase()
: normalizeOrderMetadataString(value);
const normalizedValue = isRegistrationField ? normalizeRegistrationValue(value) : requestValue;
try {
const response = await setter(normalizedOrderId, requestValue);
const updatedOrder = response?.data?.data;
if (updatedOrder && toPositiveInteger(updatedOrder.id) === normalizedOrderId) {
if (mutationSequence === latestOrderMetadataMutationSequence && activeOrderMetadataMutationCount === 1) {
applyOrderDetailsToPosState(updatedOrder, response?.data?.includes ?? {});
}
return applyOrderMetadataFieldToPosState(field, updatedOrder[field] ?? normalizedValue);
}
return applyOrderMetadataFieldToPosState(field, normalizedValue);
} finally {
activeOrderMetadataMutationCount = Math.max(0, activeOrderMetadataMutationCount - 1);
}
};
/** Get the order details by order id */
export const getOrderDetails = async (id = null) => {
const token = localStorage.getItem("token");
@@ -791,6 +968,9 @@ export const getOrderDetails = async (id = null) => {
if (!token || !targetOrderId) {
return null;
}
const requestId = ++latestOrderDetailsRequestId;
const metadataMutationSequenceAtRequest = latestOrderMetadataMutationSequence;
const hadActiveMetadataMutationAtRequest = activeOrderMetadataMutationCount > 0;
return axios
.get(API_URL + "/order?id=" + targetOrderId, {
headers: {
@@ -799,54 +979,21 @@ export const getOrderDetails = async (id = null) => {
})
.then((response) => {
const order_data = response.data.data;
const includedCustomer = response?.data?.includes?.customer ?? null;
const normalizedOrderCustomerNumber = resolveCustomerNumber(order_data?.customer_id);
const selectedCustomerBeforeHydration = getSelectedCustomerNumber();
// Set the order details
department_id.value = order_data.department_id;
reference.value = order_data.reference || "";
order_notes.value = order_data.notes || "";
order_po.value = order_data.po || "";
order_safety_seal.value = normalizeOrderMetadataString(order_data.safety_seal);
reg_1.value = order_data.reg_1 || "";
reg_2.value = order_data.reg_2 || "";
reg_3.value = order_data.reg_3 || "";
invoiceCollectionId.value = order_data.invoice_collection_id || null;
completed_at.value = order_data.completed_at || null;
if (!order_data) {
return null;
}
if (includedCustomer && typeof includedCustomer === "object") {
const customerSource =
includedCustomer.economic_customer && typeof includedCustomer.economic_customer === "object"
? includedCustomer.economic_customer
: includedCustomer;
const normalizedResponseOrderId = toPositiveInteger(order_data.id ?? targetOrderId);
const normalizedCurrentOrderId = toPositiveInteger(order_id.value);
const isStaleForDifferentOrder =
requestId !== latestOrderDetailsRequestId &&
normalizedCurrentOrderId &&
normalizedResponseOrderId &&
normalizedCurrentOrderId !== normalizedResponseOrderId;
const isStaleBeforeMetadataMutation = metadataMutationSequenceAtRequest !== latestOrderMetadataMutationSequence;
selectCustomer(
normalizeCustomerRecord(customerSource, {
...includedCustomer,
customerNumber: normalizedOrderCustomerNumber ?? resolveCustomerNumber(includedCustomer),
name:
includedCustomer.name ??
includedCustomer.customer_name ??
order_data.customer_name ??
customer_name.value,
customer_name:
includedCustomer.customer_name ??
includedCustomer.name ??
order_data.customer_name ??
customer_name.value,
})
);
} else if (normalizedOrderCustomerNumber) {
customer_id.value = normalizedOrderCustomerNumber;
if (
selectedCustomerBeforeHydration !== normalizedOrderCustomerNumber ||
customer_name.value === "" ||
!customer_data.value
) {
void searchAndSelectCustomer(normalizedOrderCustomerNumber, { forceRefresh: true });
}
} else {
clearSelectedCustomerState();
if (!isStaleForDifferentOrder && !isStaleBeforeMetadataMutation && !hadActiveMetadataMutationAtRequest) {
applyOrderDetailsToPosState(order_data, response?.data?.includes ?? {});
}
// Return the order data
return order_data;
@@ -1338,7 +1485,7 @@ const copyLastWashReferenceToEmptyCurrentOrder = async (sourceReference) => {
const normalizedOrderId = toPositiveInteger(order_id.value);
if (normalizedOrderId) {
await SessionUser.objects.orders.set.reference(normalizedOrderId, normalizedSourceReference);
await saveOrderMetadataField("reference", normalizedSourceReference, normalizedOrderId);
}
};
@@ -2208,32 +2355,27 @@ export const hydrateSelectedOrderBookingForDesktop = async () => {
const bookingReference = getOrderBookingReferenceValue(booking);
if (bookingReference !== "" && bookingReference !== reference.value) {
await SessionUser.objects.orders.set.reference(normalizedOrderId, bookingReference);
reference.value = bookingReference;
reference.value = await saveOrderMetadataField("reference", bookingReference, normalizedOrderId);
}
const bookingReg1 = getOrderBookingReg1Value(booking);
if (bookingReg1 !== "" && bookingReg1 !== reg_1.value) {
await SessionUser.objects.orders.set.reg_1(normalizedOrderId, bookingReg1);
reg_1.value = bookingReg1;
reg_1.value = await saveOrderMetadataField("reg_1", bookingReg1, normalizedOrderId);
}
const bookingReg2 = getOrderBookingReg2Value(booking);
if (bookingReg2 !== reg_2.value) {
await SessionUser.objects.orders.set.reg_2(normalizedOrderId, bookingReg2);
reg_2.value = bookingReg2;
reg_2.value = await saveOrderMetadataField("reg_2", bookingReg2, normalizedOrderId);
}
const bookingNotes = getOrderBookingNotesValue(booking);
if (bookingNotes !== "" && bookingNotes !== order_notes.value) {
await SessionUser.objects.orders.set.notes(normalizedOrderId, bookingNotes);
order_notes.value = bookingNotes;
order_notes.value = await saveOrderMetadataField("notes", bookingNotes, normalizedOrderId);
}
const bookingPo = String(booking?.po ?? "").trim();
if (bookingPo !== "" && isBlankPosMetadataValue(order_po.value)) {
await SessionUser.objects.orders.set.po(normalizedOrderId, bookingPo);
order_po.value = bookingPo;
order_po.value = await saveOrderMetadataField("po", bookingPo, normalizedOrderId);
}
const currentOrderItems = await loadOrderItems();
+76 -32
View File
@@ -23,10 +23,12 @@ export const useOrderMetadataAutosave = ({
const draft = ref(normalizeValue(resolveSourceValue(source)));
const lastSavedValue = ref(normalizeValue(resolveSourceValue(source)));
const isSaving = ref(false);
const lastError = ref(null);
const isDirty = computed(() => normalizeValue(draft.value) !== normalizeValue(lastSavedValue.value));
let timerId = null;
let saveAgainAfterCurrentRequest = false;
let activeSavePromise = null;
let nextSequence = 0;
let lastAppliedSequence = 0;
@@ -66,45 +68,57 @@ export const useOrderMetadataAutosave = ({
const sequence = ++nextSequence;
let saveCompleted = false;
isSaving.value = true;
lastError.value = null;
try {
const result = await saveValue(requestedValue);
const savedValue = normalizeValue(result ?? requestedValue);
const run = (async () => {
try {
const result = await saveValue(requestedValue);
const savedValue = normalizeValue(result ?? requestedValue);
if (sequence >= lastAppliedSequence) {
lastAppliedSequence = sequence;
lastSavedValue.value = savedValue;
if (sequence >= lastAppliedSequence) {
lastAppliedSequence = sequence;
lastSavedValue.value = savedValue;
if (normalizeValue(draft.value) === requestedValue) {
draft.value = savedValue;
if (normalizeValue(draft.value) === requestedValue) {
draft.value = savedValue;
}
}
if (typeof onSaved === "function") {
await onSaved(savedValue, requestedValue);
}
saveCompleted = true;
return true;
} catch (error) {
lastError.value = error;
if (typeof onError === "function") {
onError(error);
} else {
console.error(error);
}
return false;
} finally {
isSaving.value = false;
const shouldSaveAgain = saveAgainAfterCurrentRequest;
saveAgainAfterCurrentRequest = false;
if (saveCompleted && (shouldSaveAgain || isDirty.value) && isDirty.value) {
void persist();
}
}
})();
if (typeof onSaved === "function") {
await onSaved(savedValue, requestedValue);
const trackedSavePromise = run.finally(() => {
if (activeSavePromise === trackedSavePromise) {
activeSavePromise = null;
}
saveCompleted = true;
return true;
} catch (error) {
if (typeof onError === "function") {
onError(error);
} else {
console.error(error);
}
return false;
} finally {
isSaving.value = false;
const shouldSaveAgain = saveAgainAfterCurrentRequest;
saveAgainAfterCurrentRequest = false;
if (saveCompleted && (shouldSaveAgain || isDirty.value) && isDirty.value) {
void persist();
}
}
});
activeSavePromise = trackedSavePromise;
return trackedSavePromise;
};
const scheduleSave = () => {
@@ -123,6 +137,34 @@ export const useOrderMetadataAutosave = ({
return persist();
};
const flushAndWait = async () => {
clearPendingSave();
while (isSaving.value && activeSavePromise) {
await activeSavePromise;
}
if (!isDirty.value) {
return false;
}
const didSave = await persist();
if (!didSave && isDirty.value) {
throw lastError.value || new Error("Unable to save order metadata");
}
while (isSaving.value && activeSavePromise) {
await activeSavePromise;
}
if (isDirty.value) {
return flushAndWait();
}
return didSave;
};
onBeforeUnmount(() => {
clearPendingSave();
@@ -139,10 +181,12 @@ export const useOrderMetadataAutosave = ({
return {
draft,
lastSavedValue,
lastError,
isDirty,
isSaving,
scheduleSave,
flush,
flushAndWait,
clearPendingSave,
syncFromSource,
};
+356 -3
View File
@@ -1239,6 +1239,130 @@
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}"
},
"role_permissions": {
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
"tooltip": "@:{'templates.generated.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'templates.generated.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'templates.generated.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'templates.generated.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'templates.generated.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'templates.generated.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
"limited_backoffice": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"view_department_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
},
"view_own_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -3354,17 +3478,17 @@
"error_report": {
"button": "Rapporter fejl",
"title": "Rapporter fejl",
"subtitle": "Send det aktuelle skærmbillede og de seneste fejldetaljer til support.",
"subtitle": "Send de seneste fejldetaljer til support. Et skærmbillede vedhæftes, når det er muligt.",
"before_error": "Hvad lavede du, før fejlen opstod?",
"expected": "Hvad forventede du, der ville ske?",
"actual": "Hvad skete der faktisk?",
"before_error_placeholder": "Beskriv handlingen, du var i gang med, for eksempel at åbne ordrer eller vælge en kunde.",
"expected_placeholder": "Beskriv det resultat, du forventede at se.",
"actual_placeholder": "Beskriv, hvad du så i stedet, inklusive eventuel fejltekst.",
"consent": "Jeg accepterer, at det aktuelle app-skærmbillede, de seneste request-fejl, Vue-fejl, browseroplysninger og mine svar indsamles til fejlfinding.",
"consent": "Jeg accepterer, at de seneste request-fejl, Vue-fejl, browseroplysninger, mine svar og et app-skærmbillede, når det er muligt, indsamles til fejlfinding.",
"submit": "Send rapport",
"submitted": "Fejlrapporten er sendt.",
"capture_failed": "Skærmbilledet kunne ikke oprettes. Prøv igen.",
"capture_failed": "Skærmbilledet kunne ikke oprettes. Rapporten sendes uden skærmbillede.",
"submit_failed": "Fejlrapporten kunne ikke sendes.",
"required": "Alle felter og accept af dataindsamling er påkrævet.",
"page_subtitle": "Rapporterede skærme, request-fejl og Vue-fejl",
@@ -4722,6 +4846,97 @@
"subtitle": "@.capitalize:{'words.generated.administreret'} @:{'words.replication.services.database'}-, @:{'words.replication.services.redis'}- @:{'words.generated.og'} @:{'words.replication.services.minio'}-@:{'words.generated.replikering'}"
},
"roles": {
"permissions": {
"access_scope": "Adgangsomrade",
"actions": {
"clear_all": "Ryd gruppe",
"reload": "Genindlaes",
"retry": "Prov igen",
"select_all": "Vaelg gruppe"
},
"columns": {
"enabled": "Aktiveret",
"permission": "Tilladelse",
"route": "Rute",
"status": "Status"
},
"department_access_description": "Giver adgang til {name}.",
"empty": "Der er ingen tilladelser tilgaengelige.",
"empty_group": "Denne gruppe har ingen tilladelser.",
"errors": {
"load": "Rolletilladelser kunne ikke indlaeses.",
"update": "En eller flere tilladelser kunne ikke opdateres. Rollens aktuelle tilladelser blev genindlaest."
},
"filtered_summary": "{count} matcher",
"groups": {
"access": {
"description": "Sidetype- og afdelingsadgang.",
"label": "Adgang"
},
"bookings": {
"description": "Tilladelser til bookinger, tidsbookinger og planlaegning.",
"label": "Bookinger"
},
"customers_users": {
"description": "Tilladelser til kunder, brugere, medarbejdere, roller og login.",
"label": "Kunder og brugere"
},
"departments_operations": {
"description": "Afdelingsdrift, baner, relaer, notifikationer, rapporter og mal.",
"label": "Afdelinger og drift"
},
"invoicing_economic": {
"description": "Tilladelser til fakturaer, faktureringsperioder, opkraevning og e-conomic.",
"label": "Fakturering og e-conomic"
},
"orders_pos": {
"description": "Tilladelser til ordrer, POS, betalinger og ordrelinjer.",
"label": "Ordrer og POS"
},
"other": {
"description": "Tilladelser, der ikke matcher en anden gruppe.",
"label": "Andet"
},
"products_pricing": {
"description": "Tilladelser til produkter, priser, rabatter og valuta.",
"label": "Produkter og priser"
},
"selfserve_edge": {
"description": "Tilladelser til selvbetjening, vask, scannere, gateways og edge-udstyr.",
"label": "Selvbetjening og edge"
},
"system_config": {
"description": "System, konfiguration, release, replikering og integrationer.",
"label": "System og konfiguration"
}
},
"no_description": "Der er ingen beskrivelse.",
"no_search_results": "Ingen tilladelser matcher sogningen.",
"page_access": {
"admin": {
"description": "Giver adgang til administrationssider.",
"label": "Admin"
},
"superuser": {
"description": "Giver adgang til superbrugersider.",
"label": "Superbruger"
},
"user": {
"description": "Giver adgang til brugersider.",
"label": "Bruger"
}
},
"search_label": "Sog i tilladelser",
"search_placeholder": "Sog pa label, nogle, beskrivelse, rute eller metode",
"selected_summary": "{selected} af {total} valgt",
"status": {
"disabled": "Deaktiveret",
"enabled": "Aktiveret"
},
"subtitle": "Rolletilladelser",
"title": "Tilladelser for rolle {id}",
"title_with_name": "Tilladelser for {name}"
},
"subtitle": "@.capitalize:{'words.generated.brugerroller'}"
},
"self_wash": {
@@ -5717,6 +5932,20 @@
},
"vehicles": {
"add": "@:{'words.generated.tilføj'} @:{'words.generated.køretøj'}",
"add_modal": {
"customer_label": "Kunde",
"customer_placeholder": "Søg efter kundenavn eller kundenummer",
"error": "Køretøjet kunne ikke tilføjes.",
"no_customer_results": "Ingen kunder fundet",
"reference_placeholder": "Valgfri reference",
"registration_placeholder": "Registreringsnummer",
"selected_customer": "Valgt kunde",
"submit": "Tilføj køretøj",
"title": "Tilføj køretøj",
"type_load_error": "Køretøjstyper kunne ikke indlæses.",
"type_placeholder": "Vælg køretøjstype",
"validation_error": "Vælg kunde, registreringsnummer og køretøjstype."
},
"brand": "@:{'templates.generated.compat.admin.pos.make'}",
"color": "Farve",
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
@@ -5844,6 +6073,130 @@
"load_employees": "@.capitalize:{'words.generated.medarbejdere'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.indlæses'}.",
"save_employee": "@.capitalize:{'words.generated.medarbejderen'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.gemmes'}.",
"deactivate_employee": "@.capitalize:{'words.generated.medarbejderen'} @:{'words.generated.kunne'} @:{'words.generated.ikke'} deaktiveres."
},
"role_permissions": {
"title": "Rolletilladelser",
"tooltip": "Vis rolletilladelser",
"intro": "Roller samler de præcise funktioner, der tildeles en medarbejder, når rollen gemmes.",
"scope": "Alle funktioner er begrænset til de afdelinger, der vælges for medarbejderen.",
"selected": "Valgt",
"close": "Luk",
"unavailable": "Tilladelsesdetaljer er ikke tilgængelige for denne rolle, før rollekataloget er opdateret.",
"groups": {
"account": "Konto",
"orders": "Ordrer",
"bookings": "Bookinger",
"time_bookings": "Tidsbookinger",
"reports": "Rapporter",
"limited_backoffice": "Backoffice",
"unknown": "Øvrigt"
},
"capabilities": {
"sign_in": {
"label": "Log ind",
"description": "Giver medarbejderen adgang til at logge ind i Pleno."
},
"view_own_permissions": {
"label": "Se egne tilladelser",
"description": "Viser medarbejderen, hvilke adgangsrettigheder kontoen har."
},
"view_orders": {
"label": "Se ordrer",
"description": "Kan åbne ordrelister og ordredetaljer for tildelte afdelinger."
},
"create_orders": {
"label": "Opret ordrer",
"description": "Kan oprette nye ordrer for tildelte afdelinger."
},
"edit_orders": {
"label": "Rediger ordrer",
"description": "Kan ændre ordredetaljer for tildelte afdelinger."
},
"delete_orders": {
"label": "Slet ordrer",
"description": "Kan slette ordrer i tildelte afdelinger."
},
"view_order_items": {
"label": "Se ordrelinjer",
"description": "Kan se produkter, ydelser og priser på ordrer."
},
"create_order_items": {
"label": "Opret ordrelinjer",
"description": "Kan tilføje produkter og ydelser til ordrer."
},
"update_order_lines": {
"label": "Rediger ordrelinjer",
"description": "Kan ændre produkter, antal og priser på ordrer."
},
"remove_order_lines": {
"label": "Slet ordrelinjer",
"description": "Kan fjerne produkter og ydelser fra ordrer."
},
"charge_orders": {
"label": "Opkræv ordrer",
"description": "Kan tage betaling eller opkræve en betalingsreservation for en ordre."
},
"view_department_bookings": {
"label": "Se afdelingsbookinger",
"description": "Kan se bookinger for tildelte afdelinger."
},
"view_own_bookings": {
"label": "Se egne bookinger",
"description": "Kan se bookingantal og bookinger knyttet til medarbejderkontoen."
},
"update_bookings": {
"label": "Rediger bookinger",
"description": "Kan ændre tilladte bookingdetaljer."
},
"create_bookings": {
"label": "Opret bookinger",
"description": "Kan oprette nye bookinger for tildelte afdelinger."
},
"mark_bookings_complete": {
"label": "Afslut bookinger",
"description": "Kan markere bookinger som afsluttet."
},
"send_booking_confirmations": {
"label": "Gensend bookingbekræftelser",
"description": "Kan gensende bookingbekræftelser og afslutningsmails."
},
"view_time_booking_entries": {
"label": "Se tidsbookinger",
"description": "Kan gennemse tidsbookingposter for tildelte afdelinger."
},
"create_time_booking_entries": {
"label": "Opret tidsbookinger",
"description": "Kan oprette nye tidsbookingposter."
},
"edit_time_booking_entries": {
"label": "Rediger tidsbookinger",
"description": "Kan redigere tidsbookingposter, når backend-ruten understøtter det."
},
"view_order_statistics": {
"label": "Se ordrestatistik",
"description": "Kan åbne statistikvisninger for ordrer i tildelte afdelinger."
},
"view_booking_statistics": {
"label": "Se bookingstatistik",
"description": "Kan åbne statistikvisninger for bookinger i tildelte afdelinger."
},
"open_limited_backoffice": {
"label": "Åbn begrænset backoffice",
"description": "Kan tilgå det begrænsede backofficeområde."
},
"manage_department_prices": {
"label": "Administrer afdelingspriser",
"description": "Kan redigere eksplicitte priser for tildelte afdelinger."
},
"manage_employee_access": {
"label": "Administrer medarbejderadgang",
"description": "Kan oprette, redigere og deaktivere begrænsede backofficemedarbejdere."
},
"unknown": {
"label": "Ekstra funktion",
"description": "Denne funktion kommer fra rollekataloget, men er endnu ikke beskrevet i grænsefladen."
}
}
}
}
}
+342 -3
View File
@@ -1350,6 +1350,130 @@
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}"
},
"role_permissions": {
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
"tooltip": "@:{'templates.generated.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'templates.generated.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'templates.generated.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'templates.generated.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'templates.generated.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'templates.generated.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
"limited_backoffice": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"view_department_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
},
"view_own_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -3465,17 +3589,17 @@
"error_report": {
"button": "Fehler melden",
"title": "Fehler melden",
"subtitle": "Sende den aktuellen Bildschirm und die letzten Fehlerdetails an den Support.",
"subtitle": "Sende die letzten Fehlerdetails an den Support. Ein Screenshot wird angehängt, wenn möglich.",
"before_error": "Was haben Sie getan, bevor der Fehler auftrat?",
"expected": "Was hatten Sie erwartet?",
"actual": "Was ist tatsächlich passiert?",
"before_error_placeholder": "Beschreiben Sie die Aktion, zum Beispiel Bestellungen öffnen oder einen Kunden auswählen.",
"expected_placeholder": "Beschreiben Sie das erwartete Ergebnis.",
"actual_placeholder": "Beschreiben Sie, was stattdessen zu sehen war, inklusive Fehlermeldung.",
"consent": "Ich akzeptiere, dass der aktuelle App-Bildschirm, letzte Request-Fehler, Vue-Fehler, Browserdetails und meine Antworten zur Fehlersuche erfasst werden.",
"consent": "Ich akzeptiere, dass letzte Request-Fehler, Vue-Fehler, Browserdetails, meine Antworten und ein App-Screenshot, wenn möglich, zur Fehlersuche erfasst werden.",
"submit": "Bericht senden",
"submitted": "Fehlerbericht gesendet.",
"capture_failed": "Der Screenshot konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
"capture_failed": "Der Screenshot konnte nicht erstellt werden. Der Bericht wird ohne Screenshot gesendet.",
"submit_failed": "Der Fehlerbericht konnte nicht gesendet werden.",
"required": "Alle Felder und die Zustimmung zur Datenerfassung sind erforderlich.",
"page_subtitle": "Gemeldete Bildschirme, Request-Fehler und Vue-Fehler",
@@ -4833,6 +4957,97 @@
"subtitle": "@.capitalize:{'words.generated.managed'} @:{'words.generated.database'} @:{'words.generated.and'} @:{'words.replication.services.redis'} @:{'words.generated.replication'}"
},
"roles": {
"permissions": {
"access_scope": "Zugriffsbereich",
"actions": {
"clear_all": "Gruppe leeren",
"reload": "Neu laden",
"retry": "Erneut versuchen",
"select_all": "Gruppe auswaehlen"
},
"columns": {
"enabled": "Aktiviert",
"permission": "Berechtigung",
"route": "Route",
"status": "Status"
},
"department_access_description": "Erlaubt Zugriff auf {name}.",
"empty": "Es sind keine Berechtigungen verfuegbar.",
"empty_group": "Diese Gruppe hat keine Berechtigungen.",
"errors": {
"load": "Rollenberechtigungen konnten nicht geladen werden.",
"update": "Eine oder mehrere Berechtigungen konnten nicht aktualisiert werden. Die aktuellen Rollenberechtigungen wurden neu geladen."
},
"filtered_summary": "{count} Treffer",
"groups": {
"access": {
"description": "Seitentyp- und Abteilungszugriff.",
"label": "Zugriff"
},
"bookings": {
"description": "Berechtigungen fuer Buchungen, Zeitbuchungen und Planung.",
"label": "Buchungen"
},
"customers_users": {
"description": "Berechtigungen fuer Kunden, Benutzer, Mitarbeiter, Rollen und Anmeldung.",
"label": "Kunden und Benutzer"
},
"departments_operations": {
"description": "Abteilungsbetrieb, Spuren, Relais, Benachrichtigungen, Berichte und Ziele.",
"label": "Abteilungen und Betrieb"
},
"invoicing_economic": {
"description": "Berechtigungen fuer Rechnungen, Abrechnungsperioden, Inkasso und e-conomic.",
"label": "Rechnungen und e-conomic"
},
"orders_pos": {
"description": "Berechtigungen fuer Auftraege, POS, Zahlungen und Auftragszeilen.",
"label": "Auftraege und POS"
},
"other": {
"description": "Berechtigungen, die keiner anderen Gruppe entsprechen.",
"label": "Sonstige"
},
"products_pricing": {
"description": "Berechtigungen fuer Produkte, Preise, Rabatte und Waehrungen.",
"label": "Produkte und Preise"
},
"selfserve_edge": {
"description": "Berechtigungen fuer Self-Service, Waschen, Scanner, Gateways und Edge-Hardware.",
"label": "Self-Service und Edge"
},
"system_config": {
"description": "System, Konfiguration, Release, Replikation und Integrationen.",
"label": "System und Konfiguration"
}
},
"no_description": "Keine Beschreibung verfuegbar.",
"no_search_results": "Keine Berechtigungen entsprechen der Suche.",
"page_access": {
"admin": {
"description": "Erlaubt Zugriff auf Admin-Dashboardseiten.",
"label": "Admin"
},
"superuser": {
"description": "Erlaubt Zugriff auf Superuser-Dashboardseiten.",
"label": "Superuser"
},
"user": {
"description": "Erlaubt Zugriff auf Benutzer-Dashboardseiten.",
"label": "Benutzer"
}
},
"search_label": "Berechtigungen suchen",
"search_placeholder": "Nach Label, Schluessel, Beschreibung, Route oder Methode suchen",
"selected_summary": "{selected} von {total} ausgewaehlt",
"status": {
"disabled": "Deaktiviert",
"enabled": "Aktiviert"
},
"subtitle": "Rollenberechtigungen",
"title": "Berechtigungen fuer Rolle {id}",
"title_with_name": "Berechtigungen fuer {name}"
},
"subtitle": "Benutzerrollen"
},
"self_wash": {
@@ -5955,6 +6170,130 @@
"load_employees": "@:{'words.generated.mitarbeiter'} @:{'words.generated.konnten'} @:{'words.generated.nicht'} @:{'words.generated.geladen'} @:{'words.generated.werden'}.",
"save_employee": "@:{'words.generated.mitarbeiter'} @:{'words.generated.konnte'} @:{'words.generated.nicht'} @:{'words.generated.gespeichert'} @:{'words.generated.werden'}.",
"deactivate_employee": "@:{'words.generated.mitarbeiter'} @:{'words.generated.konnte'} @:{'words.generated.nicht'} @:{'words.generated.deaktiviert'} @:{'words.generated.werden'}."
},
"role_permissions": {
"title": "Rollenberechtigungen",
"tooltip": "Rollenberechtigungen anzeigen",
"intro": "Rollen bündeln die genauen Funktionen, die einem Mitarbeiter beim Speichern der Rolle zugewiesen werden.",
"scope": "Jede Funktion ist auf die Abteilungen begrenzt, die für den Mitarbeiter ausgewählt sind.",
"selected": "Ausgewählt",
"close": "Schließen",
"unavailable": "Berechtigungsdetails sind für diese Rolle erst verfügbar, wenn der Rollenkatalog aktualisiert wurde.",
"groups": {
"account": "Konto",
"orders": "Aufträge",
"bookings": "Buchungen",
"time_bookings": "Zeitbuchungen",
"reports": "Berichte",
"limited_backoffice": "Backoffice",
"unknown": "Sonstiges"
},
"capabilities": {
"sign_in": {
"label": "Anmelden",
"description": "Erlaubt dem Mitarbeiter, sich bei Pleno anzumelden."
},
"view_own_permissions": {
"label": "Eigene Berechtigungen anzeigen",
"description": "Zeigt dem Mitarbeiter, welche Zugriffsrechte das Konto hat."
},
"view_orders": {
"label": "Aufträge anzeigen",
"description": "Kann Auftragslisten und Auftragsdetails für zugewiesene Abteilungen öffnen."
},
"create_orders": {
"label": "Aufträge erstellen",
"description": "Kann neue Aufträge für zugewiesene Abteilungen erstellen."
},
"edit_orders": {
"label": "Aufträge bearbeiten",
"description": "Kann Auftragsdetails für zugewiesene Abteilungen ändern."
},
"delete_orders": {
"label": "Aufträge löschen",
"description": "Kann Aufträge in zugewiesenen Abteilungen löschen."
},
"view_order_items": {
"label": "Auftragspositionen anzeigen",
"description": "Kann Produkte, Leistungen und Preise auf Aufträgen sehen."
},
"create_order_items": {
"label": "Auftragspositionen erstellen",
"description": "Kann Produkte und Leistungen zu Aufträgen hinzufügen."
},
"update_order_lines": {
"label": "Auftragspositionen bearbeiten",
"description": "Kann Produkte, Mengen und Preise auf Aufträgen ändern."
},
"remove_order_lines": {
"label": "Auftragspositionen löschen",
"description": "Kann Produkte und Leistungen aus Aufträgen entfernen."
},
"charge_orders": {
"label": "Aufträge abrechnen",
"description": "Kann Zahlungen annehmen oder eine Zahlungsreservierung für einen Auftrag belasten."
},
"view_department_bookings": {
"label": "Abteilungsbuchungen anzeigen",
"description": "Kann Buchungen für zugewiesene Abteilungen sehen."
},
"view_own_bookings": {
"label": "Eigene Buchungen anzeigen",
"description": "Kann Buchungszahlen und Buchungen sehen, die mit dem Mitarbeiterkonto verbunden sind."
},
"update_bookings": {
"label": "Buchungen bearbeiten",
"description": "Kann erlaubte Buchungsdetails ändern."
},
"create_bookings": {
"label": "Buchungen erstellen",
"description": "Kann neue Buchungen für zugewiesene Abteilungen erstellen."
},
"mark_bookings_complete": {
"label": "Buchungen abschließen",
"description": "Kann Buchungen als abgeschlossen markieren."
},
"send_booking_confirmations": {
"label": "Buchungsbestätigungen erneut senden",
"description": "Kann Buchungsbestätigungen und Abschluss-E-Mails erneut senden."
},
"view_time_booking_entries": {
"label": "Zeitbuchungen anzeigen",
"description": "Kann Zeitbuchungseinträge für zugewiesene Abteilungen prüfen."
},
"create_time_booking_entries": {
"label": "Zeitbuchungen erstellen",
"description": "Kann neue Zeitbuchungseinträge hinzufügen."
},
"edit_time_booking_entries": {
"label": "Zeitbuchungen bearbeiten",
"description": "Kann Zeitbuchungseinträge bearbeiten, wenn die Backend-Route dies unterstützt."
},
"view_order_statistics": {
"label": "Auftragsstatistik anzeigen",
"description": "Kann Auftragsstatistiken für zugewiesene Abteilungen öffnen."
},
"view_booking_statistics": {
"label": "Buchungsstatistik anzeigen",
"description": "Kann Buchungsstatistiken für zugewiesene Abteilungen öffnen."
},
"open_limited_backoffice": {
"label": "Begrenztes Backoffice öffnen",
"description": "Kann den begrenzten Backofficebereich öffnen."
},
"manage_department_prices": {
"label": "Abteilungspreise verwalten",
"description": "Kann explizite Preise für zugewiesene Abteilungen bearbeiten."
},
"manage_employee_access": {
"label": "Mitarbeiterzugang verwalten",
"description": "Kann begrenzte Backoffice-Mitarbeiter erstellen, bearbeiten und deaktivieren."
},
"unknown": {
"label": "Zusätzliche Funktion",
"description": "Diese Funktion kommt aus dem Rollenkatalog, ist aber in der Oberfläche noch nicht beschrieben."
}
}
}
}
}
+356 -3
View File
@@ -1071,6 +1071,130 @@
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}"
},
"role_permissions": {
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
"tooltip": "@:{'templates.generated.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'templates.generated.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'templates.generated.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'templates.generated.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'templates.generated.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'templates.generated.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
"limited_backoffice": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"view_department_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
},
"view_own_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -3186,17 +3310,17 @@
"error_report": {
"button": "Report error",
"title": "Report error",
"subtitle": "Send the current screen and recent error details to support.",
"subtitle": "Send recent error details to support. A screenshot is attached when available.",
"before_error": "What were you doing before the error occurred?",
"expected": "What did you expect would happen?",
"actual": "What actually happened?",
"before_error_placeholder": "Describe the action you were taking, for example opening orders or selecting a customer.",
"expected_placeholder": "Describe the result you expected to see.",
"actual_placeholder": "Describe what you saw instead, including any error text.",
"consent": "I accept that the current app screen, recent request errors, Vue errors, browser details, and my answers are collected for troubleshooting.",
"consent": "I accept that recent request errors, Vue errors, browser details, my answers, and an app screenshot when available are collected for troubleshooting.",
"submit": "Submit report",
"submitted": "Error report submitted.",
"capture_failed": "The screen capture failed. Please try again.",
"capture_failed": "The screen capture failed. The report will be sent without a screenshot.",
"submit_failed": "The error report could not be submitted.",
"required": "All fields and data collection acceptance are required.",
"page_subtitle": "Reported screens, request failures, and Vue errors",
@@ -4554,6 +4678,97 @@
"subtitle": "@.capitalize:{'words.generated.managed'} @:{'words.replication.services.database'}, @:{'words.replication.services.redis'}, @:{'words.generated.and'} @:{'words.replication.services.minio'} @:{'words.generated.replication'}"
},
"roles": {
"permissions": {
"access_scope": "Access scope",
"actions": {
"clear_all": "Clear group",
"reload": "Reload",
"retry": "Try again",
"select_all": "Select group"
},
"columns": {
"enabled": "Enabled",
"permission": "Permission",
"route": "Route",
"status": "Status"
},
"department_access_description": "Allows access to {name}.",
"empty": "No permissions are available.",
"empty_group": "This group has no permissions.",
"errors": {
"load": "Could not load role permissions.",
"update": "Could not update one or more permissions. The current role permissions were reloaded."
},
"filtered_summary": "{count} matching",
"groups": {
"access": {
"description": "Page type and department access permissions.",
"label": "Access"
},
"bookings": {
"description": "Booking, time booking, and planning permissions.",
"label": "Bookings"
},
"customers_users": {
"description": "Customer, user, employee, role, and authentication permissions.",
"label": "Customers and users"
},
"departments_operations": {
"description": "Department operations, lanes, relays, notifications, reports, and goals.",
"label": "Departments and operations"
},
"invoicing_economic": {
"description": "Invoice, billing period, collection, and e-conomic permissions.",
"label": "Invoicing and e-conomic"
},
"orders_pos": {
"description": "Order, POS, payment, and order line permissions.",
"label": "Orders and POS"
},
"other": {
"description": "Permissions that do not match another group.",
"label": "Other"
},
"products_pricing": {
"description": "Product, price, discount, and currency permissions.",
"label": "Products and pricing"
},
"selfserve_edge": {
"description": "Self-serve, wash, scanner, gateway, and edge hardware permissions.",
"label": "Self-serve and edge"
},
"system_config": {
"description": "System, configuration, release, replication, and integration permissions.",
"label": "System and configuration"
}
},
"no_description": "No description is available.",
"no_search_results": "No permissions match the current search.",
"page_access": {
"admin": {
"description": "Allows access to admin dashboard pages.",
"label": "Admin"
},
"superuser": {
"description": "Allows access to superuser dashboard pages.",
"label": "Superuser"
},
"user": {
"description": "Allows access to user dashboard pages.",
"label": "User"
}
},
"search_label": "Search permissions",
"search_placeholder": "Search by label, key, description, route, or method",
"selected_summary": "{selected} of {total} selected",
"status": {
"disabled": "Disabled",
"enabled": "Enabled"
},
"subtitle": "Role permissions",
"title": "Role {id} permissions",
"title_with_name": "{name} permissions"
},
"subtitle": "@.capitalize:{'words.generated.user'} @:{'words.generated.roles'}"
},
"self_wash": {
@@ -5549,6 +5764,20 @@
},
"vehicles": {
"add": "@.capitalize:{'words.generated.add'} @:{'words.generated.vehicle'}",
"add_modal": {
"customer_label": "Customer",
"customer_placeholder": "Search by customer name or number",
"error": "Unable to add vehicle.",
"no_customer_results": "No customers found",
"reference_placeholder": "Optional reference",
"registration_placeholder": "Registration number",
"selected_customer": "Selected customer",
"submit": "Add vehicle",
"title": "Add vehicle",
"type_load_error": "Unable to load vehicle types.",
"type_placeholder": "Select vehicle type",
"validation_error": "Select a customer, registration number, and vehicle type."
},
"brand": "Brand",
"color": "Color",
"delete_vehicle": "@:{'templates.generated.compat.vehicles.delete'}",
@@ -5676,6 +5905,130 @@
"load_employees": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.load'} @:{'words.generated.employees'}.",
"save_employee": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.save'} @:{'words.generated.employee'}.",
"deactivate_employee": "@.capitalize:{'words.generated.could'} @:{'words.generated.not'} @:{'words.generated.deactivate'} @:{'words.generated.employee'}."
},
"role_permissions": {
"title": "Role permissions",
"tooltip": "View role permissions",
"intro": "Roles combine the exact capabilities assigned to an employee when the role is saved.",
"scope": "Every capability is limited to the departments selected for the employee.",
"selected": "Selected",
"close": "Close",
"unavailable": "Permission details are unavailable for this role until the role catalog has been updated.",
"groups": {
"account": "Account",
"orders": "Orders",
"bookings": "Bookings",
"time_bookings": "Time bookings",
"reports": "Reports",
"limited_backoffice": "Backoffice",
"unknown": "Other"
},
"capabilities": {
"sign_in": {
"label": "Sign in",
"description": "Allows the employee to sign in to Pleno."
},
"view_own_permissions": {
"label": "View own permissions",
"description": "Shows the employee which access rights their account has."
},
"view_orders": {
"label": "View orders",
"description": "Can open order lists and order details for assigned departments."
},
"create_orders": {
"label": "Create orders",
"description": "Can create new orders for assigned departments."
},
"edit_orders": {
"label": "Edit orders",
"description": "Can update order details for assigned departments."
},
"delete_orders": {
"label": "Delete orders",
"description": "Can delete orders in assigned departments."
},
"view_order_items": {
"label": "View order lines",
"description": "Can see products, services, and prices on orders."
},
"create_order_items": {
"label": "Create order lines",
"description": "Can add products and services to orders."
},
"update_order_lines": {
"label": "Edit order lines",
"description": "Can change products, quantities, and prices on orders."
},
"remove_order_lines": {
"label": "Delete order lines",
"description": "Can remove products and services from orders."
},
"charge_orders": {
"label": "Charge orders",
"description": "Can take payment or charge a payment intent for an order."
},
"view_department_bookings": {
"label": "View department bookings",
"description": "Can see bookings for assigned departments."
},
"view_own_bookings": {
"label": "View own bookings",
"description": "Can see booking counts and bookings tied to the employee account."
},
"update_bookings": {
"label": "Edit bookings",
"description": "Can change permitted booking details."
},
"create_bookings": {
"label": "Create bookings",
"description": "Can create new bookings for assigned departments."
},
"mark_bookings_complete": {
"label": "Complete bookings",
"description": "Can mark bookings as completed."
},
"send_booking_confirmations": {
"label": "Resend booking confirmations",
"description": "Can resend booking confirmation and completion emails."
},
"view_time_booking_entries": {
"label": "View time booking entries",
"description": "Can inspect time-booking entries for assigned departments."
},
"create_time_booking_entries": {
"label": "Create time booking entries",
"description": "Can add new time-booking entries."
},
"edit_time_booking_entries": {
"label": "Edit time booking entries",
"description": "Can edit time-booking entries when the backend route supports it."
},
"view_order_statistics": {
"label": "View order statistics",
"description": "Can open order statistics views for assigned departments."
},
"view_booking_statistics": {
"label": "View booking statistics",
"description": "Can open booking statistics views for assigned departments."
},
"open_limited_backoffice": {
"label": "Open limited backoffice",
"description": "Can access the limited backoffice area."
},
"manage_department_prices": {
"label": "Manage department prices",
"description": "Can edit explicit prices for assigned departments."
},
"manage_employee_access": {
"label": "Manage employee access",
"description": "Can create, edit, and deactivate limited backoffice employees."
},
"unknown": {
"label": "Additional capability",
"description": "This capability is provided by the role catalog but is not yet described in the interface."
}
}
}
}
}
+105
View File
@@ -4255,6 +4255,97 @@
"title": "@:replication.labels.replication"
},
"roles": {
"permissions": {
"access_scope": "@:{'templates.generated.compat.roles.permissions.access_scope'}",
"actions": {
"clear_all": "@:{'templates.generated.compat.roles.permissions.actions.clear_all'}",
"reload": "@:{'templates.generated.compat.roles.permissions.actions.reload'}",
"retry": "@:{'templates.generated.compat.roles.permissions.actions.retry'}",
"select_all": "@:{'templates.generated.compat.roles.permissions.actions.select_all'}"
},
"columns": {
"enabled": "@:{'templates.generated.compat.roles.permissions.columns.enabled'}",
"permission": "@:{'templates.generated.compat.roles.permissions.columns.permission'}",
"route": "@:{'templates.generated.compat.roles.permissions.columns.route'}",
"status": "@:{'templates.generated.compat.roles.permissions.columns.status'}"
},
"department_access_description": "@:{'templates.generated.compat.roles.permissions.department_access_description'}",
"empty": "@:{'templates.generated.compat.roles.permissions.empty'}",
"empty_group": "@:{'templates.generated.compat.roles.permissions.empty_group'}",
"errors": {
"load": "@:{'templates.generated.compat.roles.permissions.errors.load'}",
"update": "@:{'templates.generated.compat.roles.permissions.errors.update'}"
},
"filtered_summary": "@:{'templates.generated.compat.roles.permissions.filtered_summary'}",
"groups": {
"access": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.access.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.access.label'}"
},
"bookings": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.bookings.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.bookings.label'}"
},
"customers_users": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.customers_users.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.customers_users.label'}"
},
"departments_operations": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.departments_operations.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.departments_operations.label'}"
},
"invoicing_economic": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.invoicing_economic.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.invoicing_economic.label'}"
},
"orders_pos": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.orders_pos.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.orders_pos.label'}"
},
"other": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.other.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.other.label'}"
},
"products_pricing": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.products_pricing.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.products_pricing.label'}"
},
"selfserve_edge": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.selfserve_edge.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.selfserve_edge.label'}"
},
"system_config": {
"description": "@:{'templates.generated.compat.roles.permissions.groups.system_config.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.groups.system_config.label'}"
}
},
"no_description": "@:{'templates.generated.compat.roles.permissions.no_description'}",
"no_search_results": "@:{'templates.generated.compat.roles.permissions.no_search_results'}",
"page_access": {
"admin": {
"description": "@:{'templates.generated.compat.roles.permissions.page_access.admin.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.page_access.admin.label'}"
},
"superuser": {
"description": "@:{'templates.generated.compat.roles.permissions.page_access.superuser.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.page_access.superuser.label'}"
},
"user": {
"description": "@:{'templates.generated.compat.roles.permissions.page_access.user.description'}",
"label": "@:{'templates.generated.compat.roles.permissions.page_access.user.label'}"
}
},
"search_label": "@:{'templates.generated.compat.roles.permissions.search_label'}",
"search_placeholder": "@:{'templates.generated.compat.roles.permissions.search_placeholder'}",
"selected_summary": "@:{'templates.generated.compat.roles.permissions.selected_summary'}",
"status": {
"disabled": "@:{'templates.generated.compat.roles.permissions.status.disabled'}",
"enabled": "@:{'templates.generated.compat.roles.permissions.status.enabled'}"
},
"subtitle": "@:{'templates.generated.compat.roles.permissions.subtitle'}",
"title": "@:{'templates.generated.compat.roles.permissions.title'}",
"title_with_name": "@:{'templates.generated.compat.roles.permissions.title_with_name'}"
},
"subtitle": "@:{'templates.generated.compat.roles.subtitle'}",
"title": "@:{'templates.generated.compat.common.roles'}"
},
@@ -5743,6 +5834,20 @@
"vehicles": {
"actions": "@:common.actions",
"add": "@:{'templates.generated.compat.vehicles.add'}",
"add_modal": {
"customer_label": "@:{'templates.generated.compat.vehicles.add_modal.customer_label'}",
"customer_placeholder": "@:{'templates.generated.compat.vehicles.add_modal.customer_placeholder'}",
"error": "@:{'templates.generated.compat.vehicles.add_modal.error'}",
"no_customer_results": "@:{'templates.generated.compat.vehicles.add_modal.no_customer_results'}",
"reference_placeholder": "@:{'templates.generated.compat.vehicles.add_modal.reference_placeholder'}",
"registration_placeholder": "@:{'templates.generated.compat.vehicles.add_modal.registration_placeholder'}",
"selected_customer": "@:{'templates.generated.compat.vehicles.add_modal.selected_customer'}",
"submit": "@:{'templates.generated.compat.vehicles.add_modal.submit'}",
"title": "@:{'templates.generated.compat.vehicles.add_modal.title'}",
"type_load_error": "@:{'templates.generated.compat.vehicles.add_modal.type_load_error'}",
"type_placeholder": "@:{'templates.generated.compat.vehicles.add_modal.type_placeholder'}",
"validation_error": "@:{'templates.generated.compat.vehicles.add_modal.validation_error'}"
},
"brand": "@:{'templates.generated.compat.vehicles.brand'}",
"color": "@:{'templates.generated.compat.vehicles.color'}",
"created_at": "@:{'templates.generated.compat.global.generated'}",
+342 -3
View File
@@ -1353,6 +1353,130 @@
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}"
},
"role_permissions": {
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
"tooltip": "@:{'templates.generated.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'templates.generated.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'templates.generated.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'templates.generated.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'templates.generated.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'templates.generated.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
"limited_backoffice": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"view_department_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
},
"view_own_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -3468,17 +3592,17 @@
"error_report": {
"button": "Rapporter feil",
"title": "Rapporter feil",
"subtitle": "Send gjeldende skjermbilde og de siste feildetaljene til support.",
"subtitle": "Send de siste feildetaljene til support. Et skjermbilde legges ved når det er mulig.",
"before_error": "Hva gjorde du før feilen oppstod?",
"expected": "Hva forventet du at skulle skje?",
"actual": "Hva skjedde faktisk?",
"before_error_placeholder": "Beskriv handlingen du utførte, for eksempel å åpne ordre eller velge en kunde.",
"expected_placeholder": "Beskriv resultatet du forventet å se.",
"actual_placeholder": "Beskriv hva du så i stedet, inkludert eventuell feiltekst.",
"consent": "Jeg godtar at gjeldende app-skjermbilde, siste request-feil, Vue-feil, nettleserdetaljer og svarene mine samles inn for feilsøking.",
"consent": "Jeg godtar at siste request-feil, Vue-feil, nettleserdetaljer, svarene mine og et app-skjermbilde når det er mulig, samles inn for feilsøking.",
"submit": "Send rapport",
"submitted": "Feilrapport sendt.",
"capture_failed": "Skjermbildet kunne ikke tas. Prøv igjen.",
"capture_failed": "Skjermbildet kunne ikke tas. Rapporten sendes uten skjermbilde.",
"submit_failed": "Feilrapporten kunne ikke sendes.",
"required": "Alle felter og godkjenning av datainnsamling er påkrevd.",
"page_subtitle": "Rapporterte skjermer, request-feil og Vue-feil",
@@ -4836,6 +4960,97 @@
"subtitle": "@.capitalize:{'words.generated.managed'} @:{'words.replication.services.database'} @:{'words.generated.and'} @:{'words.replication.services.redis'} @:{'words.generated.replication'}"
},
"roles": {
"permissions": {
"access_scope": "Tilgangsomrade",
"actions": {
"clear_all": "Tomm gruppe",
"reload": "Last inn pa nytt",
"retry": "Prov igjen",
"select_all": "Velg gruppe"
},
"columns": {
"enabled": "Aktivert",
"permission": "Tillatelse",
"route": "Rute",
"status": "Status"
},
"department_access_description": "Gir tilgang til {name}.",
"empty": "Ingen tillatelser er tilgjengelige.",
"empty_group": "Denne gruppen har ingen tillatelser.",
"errors": {
"load": "Kunne ikke laste rolletillatelser.",
"update": "En eller flere tillatelser kunne ikke oppdateres. De gjeldende rolletillatelsene ble lastet inn pa nytt."
},
"filtered_summary": "{count} treff",
"groups": {
"access": {
"description": "Sidetype- og avdelingstilgang.",
"label": "Tilgang"
},
"bookings": {
"description": "Tillatelser for bookinger, tidsbookinger og planlegging.",
"label": "Bookinger"
},
"customers_users": {
"description": "Tillatelser for kunder, brukere, ansatte, roller og innlogging.",
"label": "Kunder og brukere"
},
"departments_operations": {
"description": "Avdelingsdrift, baner, releer, varsler, rapporter og mal.",
"label": "Avdelinger og drift"
},
"invoicing_economic": {
"description": "Tillatelser for fakturaer, faktureringsperioder, innkreving og e-conomic.",
"label": "Fakturering og e-conomic"
},
"orders_pos": {
"description": "Tillatelser for ordrer, POS, betalinger og ordrelinjer.",
"label": "Ordrer og POS"
},
"other": {
"description": "Tillatelser som ikke matcher en annen gruppe.",
"label": "Annet"
},
"products_pricing": {
"description": "Tillatelser for produkter, priser, rabatter og valuta.",
"label": "Produkter og priser"
},
"selfserve_edge": {
"description": "Tillatelser for selvbetjening, vask, skannere, gatewayer og edge-utstyr.",
"label": "Selvbetjening og edge"
},
"system_config": {
"description": "System, konfigurasjon, release, replikering og integrasjoner.",
"label": "System og konfigurasjon"
}
},
"no_description": "Ingen beskrivelse er tilgjengelig.",
"no_search_results": "Ingen tillatelser matcher soket.",
"page_access": {
"admin": {
"description": "Gir tilgang til administrasjonssider.",
"label": "Admin"
},
"superuser": {
"description": "Gir tilgang til superbrukersider.",
"label": "Superbruker"
},
"user": {
"description": "Gir tilgang til brukersider.",
"label": "Bruker"
}
},
"search_label": "Sok i tillatelser",
"search_placeholder": "Sok pa label, nokkel, beskrivelse, rute eller metode",
"selected_summary": "{selected} av {total} valgt",
"status": {
"disabled": "Deaktivert",
"enabled": "Aktivert"
},
"subtitle": "Rolletillatelser",
"title": "Tillatelser for rolle {id}",
"title_with_name": "Tillatelser for {name}"
},
"subtitle": "@.capitalize:{'words.generated.user'} roles"
},
"self_wash": {
@@ -5958,6 +6173,130 @@
"load_employees": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.laste'} @:{'words.generated.medarbeidere'}.",
"save_employee": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} @:{'words.generated.lagre'} @:{'words.generated.medarbeideren'}.",
"deactivate_employee": "@.capitalize:{'words.generated.kunne'} @:{'words.generated.ikke'} deaktivere @:{'words.generated.medarbeideren'}."
},
"role_permissions": {
"title": "Rolletillatelser",
"tooltip": "Vis rolletillatelser",
"intro": "Roller samler de nøyaktige funksjonene som tildeles en ansatt når rollen lagres.",
"scope": "Alle funksjoner er begrenset til avdelingene som velges for den ansatte.",
"selected": "Valgt",
"close": "Lukk",
"unavailable": "Tillatelsesdetaljer er ikke tilgjengelige for denne rollen før rollekatalogen er oppdatert.",
"groups": {
"account": "Konto",
"orders": "Ordrer",
"bookings": "Bookinger",
"time_bookings": "Tidsbookinger",
"reports": "Rapporter",
"limited_backoffice": "Backoffice",
"unknown": "Annet"
},
"capabilities": {
"sign_in": {
"label": "Logg inn",
"description": "Lar den ansatte logge inn i Pleno."
},
"view_own_permissions": {
"label": "Se egne tillatelser",
"description": "Viser den ansatte hvilke tilgangsrettigheter kontoen har."
},
"view_orders": {
"label": "Se ordrer",
"description": "Kan åpne ordrelister og ordredetaljer for tildelte avdelinger."
},
"create_orders": {
"label": "Opprett ordrer",
"description": "Kan opprette nye ordrer for tildelte avdelinger."
},
"edit_orders": {
"label": "Rediger ordrer",
"description": "Kan endre ordredetaljer for tildelte avdelinger."
},
"delete_orders": {
"label": "Slett ordrer",
"description": "Kan slette ordrer i tildelte avdelinger."
},
"view_order_items": {
"label": "Se ordrelinjer",
"description": "Kan se produkter, tjenester og priser på ordrer."
},
"create_order_items": {
"label": "Opprett ordrelinjer",
"description": "Kan legge produkter og tjenester til ordrer."
},
"update_order_lines": {
"label": "Rediger ordrelinjer",
"description": "Kan endre produkter, antall og priser på ordrer."
},
"remove_order_lines": {
"label": "Slett ordrelinjer",
"description": "Kan fjerne produkter og tjenester fra ordrer."
},
"charge_orders": {
"label": "Belast ordrer",
"description": "Kan ta betaling eller belaste en betalingsreservasjon for en ordre."
},
"view_department_bookings": {
"label": "Se avdelingsbookinger",
"description": "Kan se bookinger for tildelte avdelinger."
},
"view_own_bookings": {
"label": "Se egne bookinger",
"description": "Kan se bookingantall og bookinger knyttet til ansattkontoen."
},
"update_bookings": {
"label": "Rediger bookinger",
"description": "Kan endre tillatte bookingdetaljer."
},
"create_bookings": {
"label": "Opprett bookinger",
"description": "Kan opprette nye bookinger for tildelte avdelinger."
},
"mark_bookings_complete": {
"label": "Fullfør bookinger",
"description": "Kan markere bookinger som fullført."
},
"send_booking_confirmations": {
"label": "Send bookingbekreftelser på nytt",
"description": "Kan sende bookingbekreftelser og fullførings-e-poster på nytt."
},
"view_time_booking_entries": {
"label": "Se tidsbookinger",
"description": "Kan gå gjennom tidsbookingoppføringer for tildelte avdelinger."
},
"create_time_booking_entries": {
"label": "Opprett tidsbookinger",
"description": "Kan legge til nye tidsbookingoppføringer."
},
"edit_time_booking_entries": {
"label": "Rediger tidsbookinger",
"description": "Kan redigere tidsbookingoppføringer når backend-ruten støtter det."
},
"view_order_statistics": {
"label": "Se ordrestatistikk",
"description": "Kan åpne ordrestatistikk for tildelte avdelinger."
},
"view_booking_statistics": {
"label": "Se bookingstatistikk",
"description": "Kan åpne bookingstatistikk for tildelte avdelinger."
},
"open_limited_backoffice": {
"label": "Åpne begrenset backoffice",
"description": "Kan åpne det begrensede backofficeområdet."
},
"manage_department_prices": {
"label": "Administrer avdelingspriser",
"description": "Kan redigere eksplisitte priser for tildelte avdelinger."
},
"manage_employee_access": {
"label": "Administrer ansattilgang",
"description": "Kan opprette, redigere og deaktivere begrensede backofficeansatte."
},
"unknown": {
"label": "Ekstra funksjon",
"description": "Denne funksjonen kommer fra rollekatalogen, men er ennå ikke beskrevet i grensesnittet."
}
}
}
}
}
+342 -3
View File
@@ -1403,6 +1403,130 @@
"load_employees": "@:{'templates.generated.compat.limited_backoffice.errors.load_employees'}",
"save_employee": "@:{'templates.generated.compat.limited_backoffice.errors.save_employee'}",
"deactivate_employee": "@:{'templates.generated.compat.limited_backoffice.errors.deactivate_employee'}"
},
"role_permissions": {
"title": "@:{'templates.generated.compat.limited_backoffice.role_permissions.title'}",
"tooltip": "@:{'templates.generated.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'templates.generated.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'templates.generated.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'templates.generated.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'templates.generated.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'templates.generated.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.orders'}",
"bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.reports'}",
"limited_backoffice": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'templates.generated.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"view_department_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
},
"view_own_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'templates.generated.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -3518,17 +3642,17 @@
"error_report": {
"button": "Rapportera fel",
"title": "Rapportera fel",
"subtitle": "Skicka aktuell skärm och de senaste feldetaljerna till support.",
"subtitle": "Skicka de senaste feldetaljerna till support. En skärmbild bifogas när det är möjligt.",
"before_error": "Vad gjorde du innan felet uppstod?",
"expected": "Vad förväntade du dig skulle hända?",
"actual": "Vad hände faktiskt?",
"before_error_placeholder": "Beskriv åtgärden du gjorde, till exempel att öppna ordrar eller välja en kund.",
"expected_placeholder": "Beskriv resultatet du förväntade dig att se.",
"actual_placeholder": "Beskriv vad du såg i stället, inklusive eventuell feltext.",
"consent": "Jag accepterar att aktuell app-skärm, senaste request-fel, Vue-fel, webbläsardetaljer och mina svar samlas in för felsökning.",
"consent": "Jag accepterar att senaste request-fel, Vue-fel, webbläsardetaljer, mina svar och en app-skärmbild när det är möjligt samlas in för felsökning.",
"submit": "Skicka rapport",
"submitted": "Felrapport skickad.",
"capture_failed": "Skärmbilden kunde inte tas. Försök igen.",
"capture_failed": "Skärmbilden kunde inte tas. Rapporten skickas utan skärmbild.",
"submit_failed": "Felrapporten kunde inte skickas.",
"required": "Alla fält och godkännande av datainsamling krävs.",
"page_subtitle": "Rapporterade skärmar, request-fel och Vue-fel",
@@ -4886,6 +5010,97 @@
"subtitle": "@.capitalize:{'words.generated.managed'} @:{'words.generated.database'} @:{'words.generated.and'} @:{'words.replication.services.redis'} @:{'words.generated.replication'}"
},
"roles": {
"permissions": {
"access_scope": "Atkomstomrade",
"actions": {
"clear_all": "Rensa grupp",
"reload": "Ladda om",
"retry": "Forsok igen",
"select_all": "Valj grupp"
},
"columns": {
"enabled": "Aktiverad",
"permission": "Behorighet",
"route": "Rutt",
"status": "Status"
},
"department_access_description": "Ger atkomst till {name}.",
"empty": "Inga behorigheter ar tillgangliga.",
"empty_group": "Den har gruppen har inga behorigheter.",
"errors": {
"load": "Kunde inte lasa in rollbehorigheter.",
"update": "En eller flera behorigheter kunde inte uppdateras. Rollens aktuella behorigheter laddades om."
},
"filtered_summary": "{count} traffar",
"groups": {
"access": {
"description": "Sidtyp- och avdelningsatkomst.",
"label": "Atkomst"
},
"bookings": {
"description": "Behorigheter for bokningar, tidsbokningar och planering.",
"label": "Bokningar"
},
"customers_users": {
"description": "Behorigheter for kunder, anvandare, medarbetare, roller och inloggning.",
"label": "Kunder och anvandare"
},
"departments_operations": {
"description": "Avdelningsdrift, banor, relan, aviseringar, rapporter och mal.",
"label": "Avdelningar och drift"
},
"invoicing_economic": {
"description": "Behorigheter for fakturor, faktureringsperioder, inkasso och e-conomic.",
"label": "Fakturering och e-conomic"
},
"orders_pos": {
"description": "Behorigheter for ordrar, POS, betalningar och orderrader.",
"label": "Ordrar och POS"
},
"other": {
"description": "Behorigheter som inte matchar nagon annan grupp.",
"label": "Ovrigt"
},
"products_pricing": {
"description": "Behorigheter for produkter, priser, rabatter och valuta.",
"label": "Produkter och priser"
},
"selfserve_edge": {
"description": "Behorigheter for sjalvbetjaning, tvatt, skannrar, gateways och edge-utrustning.",
"label": "Sjalvbetjaning och edge"
},
"system_config": {
"description": "System, konfiguration, release, replikering och integrationer.",
"label": "System och konfiguration"
}
},
"no_description": "Ingen beskrivning ar tillganglig.",
"no_search_results": "Inga behorigheter matchar aktuell sokning.",
"page_access": {
"admin": {
"description": "Ger atkomst till administrationssidor.",
"label": "Admin"
},
"superuser": {
"description": "Ger atkomst till superuser-sidor.",
"label": "Superuser"
},
"user": {
"description": "Ger atkomst till anvandarsidor.",
"label": "Anvandare"
}
},
"search_label": "Sok behorigheter",
"search_placeholder": "Sok pa label, nyckel, beskrivning, rutt eller metod",
"selected_summary": "{selected} av {total} valda",
"status": {
"disabled": "Inaktiverad",
"enabled": "Aktiverad"
},
"subtitle": "Rollbehorigheter",
"title": "Behorigheter for roll {id}",
"title_with_name": "Behorigheter for {name}"
},
"subtitle": "@.capitalize:{'words.generated.user'} @:{'words.generated.roles'}"
},
"self_wash": {
@@ -6008,6 +6223,130 @@
"load_employees": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.ladda'} @:{'words.generated.medarbetare'}.",
"save_employee": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.spara'} @:{'words.generated.medarbetaren'}.",
"deactivate_employee": "@.capitalize:{'words.generated.kunde'} @:{'words.generated.inte'} @:{'words.generated.inaktivera'} @:{'words.generated.medarbetaren'}."
},
"role_permissions": {
"title": "Rollbehörigheter",
"tooltip": "Visa rollbehörigheter",
"intro": "Roller samlar de exakta funktioner som tilldelas en medarbetare när rollen sparas.",
"scope": "Alla funktioner är begränsade till de avdelningar som väljs för medarbetaren.",
"selected": "Vald",
"close": "Stäng",
"unavailable": "Behörighetsdetaljer är inte tillgängliga för den här rollen förrän rollkatalogen har uppdaterats.",
"groups": {
"account": "Konto",
"orders": "Order",
"bookings": "Bokningar",
"time_bookings": "Tidsbokningar",
"reports": "Rapporter",
"limited_backoffice": "Backoffice",
"unknown": "Övrigt"
},
"capabilities": {
"sign_in": {
"label": "Logga in",
"description": "Ger medarbetaren möjlighet att logga in i Pleno."
},
"view_own_permissions": {
"label": "Se egna behörigheter",
"description": "Visar medarbetaren vilka åtkomsträttigheter kontot har."
},
"view_orders": {
"label": "Se order",
"description": "Kan öppna orderlistor och orderdetaljer för tilldelade avdelningar."
},
"create_orders": {
"label": "Skapa order",
"description": "Kan skapa nya order för tilldelade avdelningar."
},
"edit_orders": {
"label": "Redigera order",
"description": "Kan ändra orderdetaljer för tilldelade avdelningar."
},
"delete_orders": {
"label": "Ta bort order",
"description": "Kan ta bort order i tilldelade avdelningar."
},
"view_order_items": {
"label": "Se orderrader",
"description": "Kan se produkter, tjänster och priser på order."
},
"create_order_items": {
"label": "Skapa orderrader",
"description": "Kan lägga till produkter och tjänster på order."
},
"update_order_lines": {
"label": "Redigera orderrader",
"description": "Kan ändra produkter, antal och priser på order."
},
"remove_order_lines": {
"label": "Ta bort orderrader",
"description": "Kan ta bort produkter och tjänster från order."
},
"charge_orders": {
"label": "Debitera order",
"description": "Kan ta betalt eller debitera en betalningsreservation för en order."
},
"view_department_bookings": {
"label": "Se avdelningsbokningar",
"description": "Kan se bokningar för tilldelade avdelningar."
},
"view_own_bookings": {
"label": "Se egna bokningar",
"description": "Kan se bokningsantal och bokningar kopplade till medarbetarkontot."
},
"update_bookings": {
"label": "Redigera bokningar",
"description": "Kan ändra tillåtna bokningsdetaljer."
},
"create_bookings": {
"label": "Skapa bokningar",
"description": "Kan skapa nya bokningar för tilldelade avdelningar."
},
"mark_bookings_complete": {
"label": "Slutför bokningar",
"description": "Kan markera bokningar som slutförda."
},
"send_booking_confirmations": {
"label": "Skicka bokningsbekräftelser igen",
"description": "Kan skicka bokningsbekräftelser och slutförandemejl igen."
},
"view_time_booking_entries": {
"label": "Se tidsbokningar",
"description": "Kan granska tidsbokningsposter för tilldelade avdelningar."
},
"create_time_booking_entries": {
"label": "Skapa tidsbokningar",
"description": "Kan lägga till nya tidsbokningsposter."
},
"edit_time_booking_entries": {
"label": "Redigera tidsbokningar",
"description": "Kan redigera tidsbokningsposter när backend-rutten stöder det."
},
"view_order_statistics": {
"label": "Se orderstatistik",
"description": "Kan öppna orderstatistik för tilldelade avdelningar."
},
"view_booking_statistics": {
"label": "Se bokningsstatistik",
"description": "Kan öppna bokningsstatistik för tilldelade avdelningar."
},
"open_limited_backoffice": {
"label": "Öppna begränsat backoffice",
"description": "Kan öppna det begränsade backofficeområdet."
},
"manage_department_prices": {
"label": "Hantera avdelningspriser",
"description": "Kan redigera uttryckliga priser för tilldelade avdelningar."
},
"manage_employee_access": {
"label": "Hantera medarbetaråtkomst",
"description": "Kan skapa, redigera och inaktivera begränsade backofficemedarbetare."
},
"unknown": {
"label": "Extra funktion",
"description": "Den här funktionen kommer från rollkatalogen men är ännu inte beskriven i gränssnittet."
}
}
}
}
}
@@ -3,17 +3,17 @@
"error_report": {
"button": "Rapporter fejl",
"title": "Rapporter fejl",
"subtitle": "Send det aktuelle skærmbillede og de seneste fejldetaljer til support.",
"subtitle": "Send de seneste fejldetaljer til support. Et skærmbillede vedhæftes, når det er muligt.",
"before_error": "Hvad lavede du, før fejlen opstod?",
"expected": "Hvad forventede du, der ville ske?",
"actual": "Hvad skete der faktisk?",
"before_error_placeholder": "Beskriv handlingen, du var i gang med, for eksempel at åbne ordrer eller vælge en kunde.",
"expected_placeholder": "Beskriv det resultat, du forventede at se.",
"actual_placeholder": "Beskriv, hvad du så i stedet, inklusive eventuel fejltekst.",
"consent": "Jeg accepterer, at det aktuelle app-skærmbillede, de seneste request-fejl, Vue-fejl, browseroplysninger og mine svar indsamles til fejlfinding.",
"consent": "Jeg accepterer, at de seneste request-fejl, Vue-fejl, browseroplysninger, mine svar og et app-skærmbillede, når det er muligt, indsamles til fejlfinding.",
"submit": "Send rapport",
"submitted": "Fejlrapporten er sendt.",
"capture_failed": "Skærmbilledet kunne ikke oprettes. Prøv igen.",
"capture_failed": "Skærmbilledet kunne ikke oprettes. Rapporten sendes uden skærmbillede.",
"submit_failed": "Fejlrapporten kunne ikke sendes.",
"required": "Alle felter og accept af dataindsamling er påkrævet.",
"page_subtitle": "Rapporterede skærme, request-fejl og Vue-fejl",
@@ -0,0 +1,97 @@
{
"compat": {
"roles": {
"permissions": {
"access_scope": "Adgangsomrade",
"actions": {
"clear_all": "Ryd gruppe",
"reload": "Genindlaes",
"retry": "Prov igen",
"select_all": "Vaelg gruppe"
},
"columns": {
"enabled": "Aktiveret",
"permission": "Tilladelse",
"route": "Rute",
"status": "Status"
},
"department_access_description": "Giver adgang til {name}.",
"empty": "Der er ingen tilladelser tilgaengelige.",
"empty_group": "Denne gruppe har ingen tilladelser.",
"errors": {
"load": "Rolletilladelser kunne ikke indlaeses.",
"update": "En eller flere tilladelser kunne ikke opdateres. Rollens aktuelle tilladelser blev genindlaest."
},
"filtered_summary": "{count} matcher",
"groups": {
"access": {
"description": "Sidetype- og afdelingsadgang.",
"label": "Adgang"
},
"bookings": {
"description": "Tilladelser til bookinger, tidsbookinger og planlaegning.",
"label": "Bookinger"
},
"customers_users": {
"description": "Tilladelser til kunder, brugere, medarbejdere, roller og login.",
"label": "Kunder og brugere"
},
"departments_operations": {
"description": "Afdelingsdrift, baner, relaer, notifikationer, rapporter og mal.",
"label": "Afdelinger og drift"
},
"invoicing_economic": {
"description": "Tilladelser til fakturaer, faktureringsperioder, opkraevning og e-conomic.",
"label": "Fakturering og e-conomic"
},
"orders_pos": {
"description": "Tilladelser til ordrer, POS, betalinger og ordrelinjer.",
"label": "Ordrer og POS"
},
"other": {
"description": "Tilladelser, der ikke matcher en anden gruppe.",
"label": "Andet"
},
"products_pricing": {
"description": "Tilladelser til produkter, priser, rabatter og valuta.",
"label": "Produkter og priser"
},
"selfserve_edge": {
"description": "Tilladelser til selvbetjening, vask, scannere, gateways og edge-udstyr.",
"label": "Selvbetjening og edge"
},
"system_config": {
"description": "System, konfiguration, release, replikering og integrationer.",
"label": "System og konfiguration"
}
},
"no_description": "Der er ingen beskrivelse.",
"no_search_results": "Ingen tilladelser matcher sogningen.",
"page_access": {
"admin": {
"description": "Giver adgang til administrationssider.",
"label": "Admin"
},
"superuser": {
"description": "Giver adgang til superbrugersider.",
"label": "Superbruger"
},
"user": {
"description": "Giver adgang til brugersider.",
"label": "Bruger"
}
},
"search_label": "Sog i tilladelser",
"search_placeholder": "Sog pa label, nogle, beskrivelse, rute eller metode",
"selected_summary": "{selected} af {total} valgt",
"status": {
"disabled": "Deaktiveret",
"enabled": "Aktiveret"
},
"subtitle": "Rolletilladelser",
"title": "Tilladelser for rolle {id}",
"title_with_name": "Tilladelser for {name}"
}
}
}
}
@@ -2,6 +2,20 @@
"compat": {
"vehicles": {
"add": "@:{'terms.glossary.tilføj'} @:{'terms.glossary.køretøj'}",
"add_modal": {
"customer_label": "Kunde",
"customer_placeholder": "Søg efter kundenavn eller kundenummer",
"error": "Køretøjet kunne ikke tilføjes.",
"no_customer_results": "Ingen kunder fundet",
"reference_placeholder": "Valgfri reference",
"registration_placeholder": "Registreringsnummer",
"selected_customer": "Valgt kunde",
"submit": "Tilføj køretøj",
"title": "Tilføj køretøj",
"type_load_error": "Køretøjstyper kunne ikke indlæses.",
"type_placeholder": "Vælg køretøjstype",
"validation_error": "Vælg kunde, registreringsnummer og køretøjstype."
},
"brand": "@:{'phrases.compat.admin.pos.make'}",
"color": "Farve",
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
+248
View File
@@ -95,6 +95,130 @@
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}"
},
"role_permissions": {
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
"tooltip": "@:{'phrases.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'phrases.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'phrases.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'phrases.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'phrases.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'phrases.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
"bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.reports'}",
"limited_backoffice": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"view_department_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
},
"view_own_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -204,6 +328,130 @@
"load_employees": "@.capitalize:{'terms.glossary.medarbejdere'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.indlæses'}.",
"save_employee": "@.capitalize:{'terms.glossary.medarbejderen'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.gemmes'}.",
"deactivate_employee": "@.capitalize:{'terms.glossary.medarbejderen'} @:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} deaktiveres."
},
"role_permissions": {
"title": "Rolletilladelser",
"tooltip": "Vis rolletilladelser",
"intro": "Roller samler de præcise funktioner, der tildeles en medarbejder, når rollen gemmes.",
"scope": "Alle funktioner er begrænset til de afdelinger, der vælges for medarbejderen.",
"selected": "Valgt",
"close": "Luk",
"unavailable": "Tilladelsesdetaljer er ikke tilgængelige for denne rolle, før rollekataloget er opdateret.",
"groups": {
"account": "Konto",
"orders": "Ordrer",
"bookings": "Bookinger",
"time_bookings": "Tidsbookinger",
"reports": "Rapporter",
"limited_backoffice": "Backoffice",
"unknown": "Øvrigt"
},
"capabilities": {
"sign_in": {
"label": "Log ind",
"description": "Giver medarbejderen adgang til at logge ind i Pleno."
},
"view_own_permissions": {
"label": "Se egne tilladelser",
"description": "Viser medarbejderen, hvilke adgangsrettigheder kontoen har."
},
"view_orders": {
"label": "Se ordrer",
"description": "Kan åbne ordrelister og ordredetaljer for tildelte afdelinger."
},
"create_orders": {
"label": "Opret ordrer",
"description": "Kan oprette nye ordrer for tildelte afdelinger."
},
"edit_orders": {
"label": "Rediger ordrer",
"description": "Kan ændre ordredetaljer for tildelte afdelinger."
},
"delete_orders": {
"label": "Slet ordrer",
"description": "Kan slette ordrer i tildelte afdelinger."
},
"view_order_items": {
"label": "Se ordrelinjer",
"description": "Kan se produkter, ydelser og priser på ordrer."
},
"create_order_items": {
"label": "Opret ordrelinjer",
"description": "Kan tilføje produkter og ydelser til ordrer."
},
"update_order_lines": {
"label": "Rediger ordrelinjer",
"description": "Kan ændre produkter, antal og priser på ordrer."
},
"remove_order_lines": {
"label": "Slet ordrelinjer",
"description": "Kan fjerne produkter og ydelser fra ordrer."
},
"charge_orders": {
"label": "Opkræv ordrer",
"description": "Kan tage betaling eller opkræve en betalingsreservation for en ordre."
},
"view_department_bookings": {
"label": "Se afdelingsbookinger",
"description": "Kan se bookinger for tildelte afdelinger."
},
"view_own_bookings": {
"label": "Se egne bookinger",
"description": "Kan se bookingantal og bookinger knyttet til medarbejderkontoen."
},
"update_bookings": {
"label": "Rediger bookinger",
"description": "Kan ændre tilladte bookingdetaljer."
},
"create_bookings": {
"label": "Opret bookinger",
"description": "Kan oprette nye bookinger for tildelte afdelinger."
},
"mark_bookings_complete": {
"label": "Afslut bookinger",
"description": "Kan markere bookinger som afsluttet."
},
"send_booking_confirmations": {
"label": "Gensend bookingbekræftelser",
"description": "Kan gensende bookingbekræftelser og afslutningsmails."
},
"view_time_booking_entries": {
"label": "Se tidsbookinger",
"description": "Kan gennemse tidsbookingposter for tildelte afdelinger."
},
"create_time_booking_entries": {
"label": "Opret tidsbookinger",
"description": "Kan oprette nye tidsbookingposter."
},
"edit_time_booking_entries": {
"label": "Rediger tidsbookinger",
"description": "Kan redigere tidsbookingposter, når backend-ruten understøtter det."
},
"view_order_statistics": {
"label": "Se ordrestatistik",
"description": "Kan åbne statistikvisninger for ordrer i tildelte afdelinger."
},
"view_booking_statistics": {
"label": "Se bookingstatistik",
"description": "Kan åbne statistikvisninger for bookinger i tildelte afdelinger."
},
"open_limited_backoffice": {
"label": "Åbn begrænset backoffice",
"description": "Kan tilgå det begrænsede backofficeområde."
},
"manage_department_prices": {
"label": "Administrer afdelingspriser",
"description": "Kan redigere eksplicitte priser for tildelte afdelinger."
},
"manage_employee_access": {
"label": "Administrer medarbejderadgang",
"description": "Kan oprette, redigere og deaktivere begrænsede backofficemedarbejdere."
},
"unknown": {
"label": "Ekstra funktion",
"description": "Denne funktion kommer fra rollekataloget, men er endnu ikke beskrevet i grænsefladen."
}
}
}
}
}
@@ -3,17 +3,17 @@
"error_report": {
"button": "Fehler melden",
"title": "Fehler melden",
"subtitle": "Sende den aktuellen Bildschirm und die letzten Fehlerdetails an den Support.",
"subtitle": "Sende die letzten Fehlerdetails an den Support. Ein Screenshot wird angehängt, wenn möglich.",
"before_error": "Was haben Sie getan, bevor der Fehler auftrat?",
"expected": "Was hatten Sie erwartet?",
"actual": "Was ist tatsächlich passiert?",
"before_error_placeholder": "Beschreiben Sie die Aktion, zum Beispiel Bestellungen öffnen oder einen Kunden auswählen.",
"expected_placeholder": "Beschreiben Sie das erwartete Ergebnis.",
"actual_placeholder": "Beschreiben Sie, was stattdessen zu sehen war, inklusive Fehlermeldung.",
"consent": "Ich akzeptiere, dass der aktuelle App-Bildschirm, letzte Request-Fehler, Vue-Fehler, Browserdetails und meine Antworten zur Fehlersuche erfasst werden.",
"consent": "Ich akzeptiere, dass letzte Request-Fehler, Vue-Fehler, Browserdetails, meine Antworten und ein App-Screenshot, wenn möglich, zur Fehlersuche erfasst werden.",
"submit": "Bericht senden",
"submitted": "Fehlerbericht gesendet.",
"capture_failed": "Der Screenshot konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
"capture_failed": "Der Screenshot konnte nicht erstellt werden. Der Bericht wird ohne Screenshot gesendet.",
"submit_failed": "Der Fehlerbericht konnte nicht gesendet werden.",
"required": "Alle Felder und die Zustimmung zur Datenerfassung sind erforderlich.",
"page_subtitle": "Gemeldete Bildschirme, Request-Fehler und Vue-Fehler",
@@ -0,0 +1,97 @@
{
"compat": {
"roles": {
"permissions": {
"access_scope": "Zugriffsbereich",
"actions": {
"clear_all": "Gruppe leeren",
"reload": "Neu laden",
"retry": "Erneut versuchen",
"select_all": "Gruppe auswaehlen"
},
"columns": {
"enabled": "Aktiviert",
"permission": "Berechtigung",
"route": "Route",
"status": "Status"
},
"department_access_description": "Erlaubt Zugriff auf {name}.",
"empty": "Es sind keine Berechtigungen verfuegbar.",
"empty_group": "Diese Gruppe hat keine Berechtigungen.",
"errors": {
"load": "Rollenberechtigungen konnten nicht geladen werden.",
"update": "Eine oder mehrere Berechtigungen konnten nicht aktualisiert werden. Die aktuellen Rollenberechtigungen wurden neu geladen."
},
"filtered_summary": "{count} Treffer",
"groups": {
"access": {
"description": "Seitentyp- und Abteilungszugriff.",
"label": "Zugriff"
},
"bookings": {
"description": "Berechtigungen fuer Buchungen, Zeitbuchungen und Planung.",
"label": "Buchungen"
},
"customers_users": {
"description": "Berechtigungen fuer Kunden, Benutzer, Mitarbeiter, Rollen und Anmeldung.",
"label": "Kunden und Benutzer"
},
"departments_operations": {
"description": "Abteilungsbetrieb, Spuren, Relais, Benachrichtigungen, Berichte und Ziele.",
"label": "Abteilungen und Betrieb"
},
"invoicing_economic": {
"description": "Berechtigungen fuer Rechnungen, Abrechnungsperioden, Inkasso und e-conomic.",
"label": "Rechnungen und e-conomic"
},
"orders_pos": {
"description": "Berechtigungen fuer Auftraege, POS, Zahlungen und Auftragszeilen.",
"label": "Auftraege und POS"
},
"other": {
"description": "Berechtigungen, die keiner anderen Gruppe entsprechen.",
"label": "Sonstige"
},
"products_pricing": {
"description": "Berechtigungen fuer Produkte, Preise, Rabatte und Waehrungen.",
"label": "Produkte und Preise"
},
"selfserve_edge": {
"description": "Berechtigungen fuer Self-Service, Waschen, Scanner, Gateways und Edge-Hardware.",
"label": "Self-Service und Edge"
},
"system_config": {
"description": "System, Konfiguration, Release, Replikation und Integrationen.",
"label": "System und Konfiguration"
}
},
"no_description": "Keine Beschreibung verfuegbar.",
"no_search_results": "Keine Berechtigungen entsprechen der Suche.",
"page_access": {
"admin": {
"description": "Erlaubt Zugriff auf Admin-Dashboardseiten.",
"label": "Admin"
},
"superuser": {
"description": "Erlaubt Zugriff auf Superuser-Dashboardseiten.",
"label": "Superuser"
},
"user": {
"description": "Erlaubt Zugriff auf Benutzer-Dashboardseiten.",
"label": "Benutzer"
}
},
"search_label": "Berechtigungen suchen",
"search_placeholder": "Nach Label, Schluessel, Beschreibung, Route oder Methode suchen",
"selected_summary": "{selected} von {total} ausgewaehlt",
"status": {
"disabled": "Deaktiviert",
"enabled": "Aktiviert"
},
"subtitle": "Rollenberechtigungen",
"title": "Berechtigungen fuer Rolle {id}",
"title_with_name": "Berechtigungen fuer {name}"
}
}
}
}
+248
View File
@@ -95,6 +95,130 @@
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}"
},
"role_permissions": {
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
"tooltip": "@:{'phrases.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'phrases.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'phrases.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'phrases.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'phrases.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'phrases.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
"bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.reports'}",
"limited_backoffice": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"view_department_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
},
"view_own_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -204,6 +328,130 @@
"load_employees": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.konnten'} @:{'terms.glossary.nicht'} @:{'terms.glossary.geladen'} @:{'terms.glossary.werden'}.",
"save_employee": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.konnte'} @:{'terms.glossary.nicht'} @:{'terms.glossary.gespeichert'} @:{'terms.glossary.werden'}.",
"deactivate_employee": "@:{'terms.glossary.mitarbeiter'} @:{'terms.glossary.konnte'} @:{'terms.glossary.nicht'} @:{'terms.glossary.deaktiviert'} @:{'terms.glossary.werden'}."
},
"role_permissions": {
"title": "Rollenberechtigungen",
"tooltip": "Rollenberechtigungen anzeigen",
"intro": "Rollen bündeln die genauen Funktionen, die einem Mitarbeiter beim Speichern der Rolle zugewiesen werden.",
"scope": "Jede Funktion ist auf die Abteilungen begrenzt, die für den Mitarbeiter ausgewählt sind.",
"selected": "Ausgewählt",
"close": "Schließen",
"unavailable": "Berechtigungsdetails sind für diese Rolle erst verfügbar, wenn der Rollenkatalog aktualisiert wurde.",
"groups": {
"account": "Konto",
"orders": "Aufträge",
"bookings": "Buchungen",
"time_bookings": "Zeitbuchungen",
"reports": "Berichte",
"limited_backoffice": "Backoffice",
"unknown": "Sonstiges"
},
"capabilities": {
"sign_in": {
"label": "Anmelden",
"description": "Erlaubt dem Mitarbeiter, sich bei Pleno anzumelden."
},
"view_own_permissions": {
"label": "Eigene Berechtigungen anzeigen",
"description": "Zeigt dem Mitarbeiter, welche Zugriffsrechte das Konto hat."
},
"view_orders": {
"label": "Aufträge anzeigen",
"description": "Kann Auftragslisten und Auftragsdetails für zugewiesene Abteilungen öffnen."
},
"create_orders": {
"label": "Aufträge erstellen",
"description": "Kann neue Aufträge für zugewiesene Abteilungen erstellen."
},
"edit_orders": {
"label": "Aufträge bearbeiten",
"description": "Kann Auftragsdetails für zugewiesene Abteilungen ändern."
},
"delete_orders": {
"label": "Aufträge löschen",
"description": "Kann Aufträge in zugewiesenen Abteilungen löschen."
},
"view_order_items": {
"label": "Auftragspositionen anzeigen",
"description": "Kann Produkte, Leistungen und Preise auf Aufträgen sehen."
},
"create_order_items": {
"label": "Auftragspositionen erstellen",
"description": "Kann Produkte und Leistungen zu Aufträgen hinzufügen."
},
"update_order_lines": {
"label": "Auftragspositionen bearbeiten",
"description": "Kann Produkte, Mengen und Preise auf Aufträgen ändern."
},
"remove_order_lines": {
"label": "Auftragspositionen löschen",
"description": "Kann Produkte und Leistungen aus Aufträgen entfernen."
},
"charge_orders": {
"label": "Aufträge abrechnen",
"description": "Kann Zahlungen annehmen oder eine Zahlungsreservierung für einen Auftrag belasten."
},
"view_department_bookings": {
"label": "Abteilungsbuchungen anzeigen",
"description": "Kann Buchungen für zugewiesene Abteilungen sehen."
},
"view_own_bookings": {
"label": "Eigene Buchungen anzeigen",
"description": "Kann Buchungszahlen und Buchungen sehen, die mit dem Mitarbeiterkonto verbunden sind."
},
"update_bookings": {
"label": "Buchungen bearbeiten",
"description": "Kann erlaubte Buchungsdetails ändern."
},
"create_bookings": {
"label": "Buchungen erstellen",
"description": "Kann neue Buchungen für zugewiesene Abteilungen erstellen."
},
"mark_bookings_complete": {
"label": "Buchungen abschließen",
"description": "Kann Buchungen als abgeschlossen markieren."
},
"send_booking_confirmations": {
"label": "Buchungsbestätigungen erneut senden",
"description": "Kann Buchungsbestätigungen und Abschluss-E-Mails erneut senden."
},
"view_time_booking_entries": {
"label": "Zeitbuchungen anzeigen",
"description": "Kann Zeitbuchungseinträge für zugewiesene Abteilungen prüfen."
},
"create_time_booking_entries": {
"label": "Zeitbuchungen erstellen",
"description": "Kann neue Zeitbuchungseinträge hinzufügen."
},
"edit_time_booking_entries": {
"label": "Zeitbuchungen bearbeiten",
"description": "Kann Zeitbuchungseinträge bearbeiten, wenn die Backend-Route dies unterstützt."
},
"view_order_statistics": {
"label": "Auftragsstatistik anzeigen",
"description": "Kann Auftragsstatistiken für zugewiesene Abteilungen öffnen."
},
"view_booking_statistics": {
"label": "Buchungsstatistik anzeigen",
"description": "Kann Buchungsstatistiken für zugewiesene Abteilungen öffnen."
},
"open_limited_backoffice": {
"label": "Begrenztes Backoffice öffnen",
"description": "Kann den begrenzten Backofficebereich öffnen."
},
"manage_department_prices": {
"label": "Abteilungspreise verwalten",
"description": "Kann explizite Preise für zugewiesene Abteilungen bearbeiten."
},
"manage_employee_access": {
"label": "Mitarbeiterzugang verwalten",
"description": "Kann begrenzte Backoffice-Mitarbeiter erstellen, bearbeiten und deaktivieren."
},
"unknown": {
"label": "Zusätzliche Funktion",
"description": "Diese Funktion kommt aus dem Rollenkatalog, ist aber in der Oberfläche noch nicht beschrieben."
}
}
}
}
}
@@ -3,17 +3,17 @@
"error_report": {
"button": "Report error",
"title": "Report error",
"subtitle": "Send the current screen and recent error details to support.",
"subtitle": "Send recent error details to support. A screenshot is attached when available.",
"before_error": "What were you doing before the error occurred?",
"expected": "What did you expect would happen?",
"actual": "What actually happened?",
"before_error_placeholder": "Describe the action you were taking, for example opening orders or selecting a customer.",
"expected_placeholder": "Describe the result you expected to see.",
"actual_placeholder": "Describe what you saw instead, including any error text.",
"consent": "I accept that the current app screen, recent request errors, Vue errors, browser details, and my answers are collected for troubleshooting.",
"consent": "I accept that recent request errors, Vue errors, browser details, my answers, and an app screenshot when available are collected for troubleshooting.",
"submit": "Submit report",
"submitted": "Error report submitted.",
"capture_failed": "The screen capture failed. Please try again.",
"capture_failed": "The screen capture failed. The report will be sent without a screenshot.",
"submit_failed": "The error report could not be submitted.",
"required": "All fields and data collection acceptance are required.",
"page_subtitle": "Reported screens, request failures, and Vue errors",
@@ -0,0 +1,97 @@
{
"compat": {
"roles": {
"permissions": {
"access_scope": "Access scope",
"actions": {
"clear_all": "Clear group",
"reload": "Reload",
"retry": "Try again",
"select_all": "Select group"
},
"columns": {
"enabled": "Enabled",
"permission": "Permission",
"route": "Route",
"status": "Status"
},
"department_access_description": "Allows access to {name}.",
"empty": "No permissions are available.",
"empty_group": "This group has no permissions.",
"errors": {
"load": "Could not load role permissions.",
"update": "Could not update one or more permissions. The current role permissions were reloaded."
},
"filtered_summary": "{count} matching",
"groups": {
"access": {
"description": "Page type and department access permissions.",
"label": "Access"
},
"bookings": {
"description": "Booking, time booking, and planning permissions.",
"label": "Bookings"
},
"customers_users": {
"description": "Customer, user, employee, role, and authentication permissions.",
"label": "Customers and users"
},
"departments_operations": {
"description": "Department operations, lanes, relays, notifications, reports, and goals.",
"label": "Departments and operations"
},
"invoicing_economic": {
"description": "Invoice, billing period, collection, and e-conomic permissions.",
"label": "Invoicing and e-conomic"
},
"orders_pos": {
"description": "Order, POS, payment, and order line permissions.",
"label": "Orders and POS"
},
"other": {
"description": "Permissions that do not match another group.",
"label": "Other"
},
"products_pricing": {
"description": "Product, price, discount, and currency permissions.",
"label": "Products and pricing"
},
"selfserve_edge": {
"description": "Self-serve, wash, scanner, gateway, and edge hardware permissions.",
"label": "Self-serve and edge"
},
"system_config": {
"description": "System, configuration, release, replication, and integration permissions.",
"label": "System and configuration"
}
},
"no_description": "No description is available.",
"no_search_results": "No permissions match the current search.",
"page_access": {
"admin": {
"description": "Allows access to admin dashboard pages.",
"label": "Admin"
},
"superuser": {
"description": "Allows access to superuser dashboard pages.",
"label": "Superuser"
},
"user": {
"description": "Allows access to user dashboard pages.",
"label": "User"
}
},
"search_label": "Search permissions",
"search_placeholder": "Search by label, key, description, route, or method",
"selected_summary": "{selected} of {total} selected",
"status": {
"disabled": "Disabled",
"enabled": "Enabled"
},
"subtitle": "Role permissions",
"title": "Role {id} permissions",
"title_with_name": "{name} permissions"
}
}
}
}
@@ -2,6 +2,20 @@
"compat": {
"vehicles": {
"add": "@.capitalize:{'terms.glossary.add'} @:{'terms.glossary.vehicle'}",
"add_modal": {
"customer_label": "Customer",
"customer_placeholder": "Search by customer name or number",
"error": "Unable to add vehicle.",
"no_customer_results": "No customers found",
"reference_placeholder": "Optional reference",
"registration_placeholder": "Registration number",
"selected_customer": "Selected customer",
"submit": "Add vehicle",
"title": "Add vehicle",
"type_load_error": "Unable to load vehicle types.",
"type_placeholder": "Select vehicle type",
"validation_error": "Select a customer, registration number, and vehicle type."
},
"brand": "Brand",
"color": "Color",
"delete_vehicle": "@:{'phrases.compat.vehicles.delete'}",
+248
View File
@@ -95,6 +95,130 @@
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}"
},
"role_permissions": {
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
"tooltip": "@:{'phrases.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'phrases.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'phrases.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'phrases.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'phrases.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'phrases.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
"bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.reports'}",
"limited_backoffice": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"view_department_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
},
"view_own_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -204,6 +328,130 @@
"load_employees": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.load'} @:{'terms.glossary.employees'}.",
"save_employee": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.save'} @:{'terms.glossary.employee'}.",
"deactivate_employee": "@.capitalize:{'terms.glossary.could'} @:{'terms.glossary.not'} @:{'terms.glossary.deactivate'} @:{'terms.glossary.employee'}."
},
"role_permissions": {
"title": "Role permissions",
"tooltip": "View role permissions",
"intro": "Roles combine the exact capabilities assigned to an employee when the role is saved.",
"scope": "Every capability is limited to the departments selected for the employee.",
"selected": "Selected",
"close": "Close",
"unavailable": "Permission details are unavailable for this role until the role catalog has been updated.",
"groups": {
"account": "Account",
"orders": "Orders",
"bookings": "Bookings",
"time_bookings": "Time bookings",
"reports": "Reports",
"limited_backoffice": "Backoffice",
"unknown": "Other"
},
"capabilities": {
"sign_in": {
"label": "Sign in",
"description": "Allows the employee to sign in to Pleno."
},
"view_own_permissions": {
"label": "View own permissions",
"description": "Shows the employee which access rights their account has."
},
"view_orders": {
"label": "View orders",
"description": "Can open order lists and order details for assigned departments."
},
"create_orders": {
"label": "Create orders",
"description": "Can create new orders for assigned departments."
},
"edit_orders": {
"label": "Edit orders",
"description": "Can update order details for assigned departments."
},
"delete_orders": {
"label": "Delete orders",
"description": "Can delete orders in assigned departments."
},
"view_order_items": {
"label": "View order lines",
"description": "Can see products, services, and prices on orders."
},
"create_order_items": {
"label": "Create order lines",
"description": "Can add products and services to orders."
},
"update_order_lines": {
"label": "Edit order lines",
"description": "Can change products, quantities, and prices on orders."
},
"remove_order_lines": {
"label": "Delete order lines",
"description": "Can remove products and services from orders."
},
"charge_orders": {
"label": "Charge orders",
"description": "Can take payment or charge a payment intent for an order."
},
"view_department_bookings": {
"label": "View department bookings",
"description": "Can see bookings for assigned departments."
},
"view_own_bookings": {
"label": "View own bookings",
"description": "Can see booking counts and bookings tied to the employee account."
},
"update_bookings": {
"label": "Edit bookings",
"description": "Can change permitted booking details."
},
"create_bookings": {
"label": "Create bookings",
"description": "Can create new bookings for assigned departments."
},
"mark_bookings_complete": {
"label": "Complete bookings",
"description": "Can mark bookings as completed."
},
"send_booking_confirmations": {
"label": "Resend booking confirmations",
"description": "Can resend booking confirmation and completion emails."
},
"view_time_booking_entries": {
"label": "View time booking entries",
"description": "Can inspect time-booking entries for assigned departments."
},
"create_time_booking_entries": {
"label": "Create time booking entries",
"description": "Can add new time-booking entries."
},
"edit_time_booking_entries": {
"label": "Edit time booking entries",
"description": "Can edit time-booking entries when the backend route supports it."
},
"view_order_statistics": {
"label": "View order statistics",
"description": "Can open order statistics views for assigned departments."
},
"view_booking_statistics": {
"label": "View booking statistics",
"description": "Can open booking statistics views for assigned departments."
},
"open_limited_backoffice": {
"label": "Open limited backoffice",
"description": "Can access the limited backoffice area."
},
"manage_department_prices": {
"label": "Manage department prices",
"description": "Can edit explicit prices for assigned departments."
},
"manage_employee_access": {
"label": "Manage employee access",
"description": "Can create, edit, and deactivate limited backoffice employees."
},
"unknown": {
"label": "Additional capability",
"description": "This capability is provided by the role catalog but is not yet described in the interface."
}
}
}
}
}
@@ -1,5 +1,96 @@
{
"roles": {
"permissions": {
"access_scope": "@:{'phrases.compat.roles.permissions.access_scope'}",
"actions": {
"clear_all": "@:{'phrases.compat.roles.permissions.actions.clear_all'}",
"reload": "@:{'phrases.compat.roles.permissions.actions.reload'}",
"retry": "@:{'phrases.compat.roles.permissions.actions.retry'}",
"select_all": "@:{'phrases.compat.roles.permissions.actions.select_all'}"
},
"columns": {
"enabled": "@:{'phrases.compat.roles.permissions.columns.enabled'}",
"permission": "@:{'phrases.compat.roles.permissions.columns.permission'}",
"route": "@:{'phrases.compat.roles.permissions.columns.route'}",
"status": "@:{'phrases.compat.roles.permissions.columns.status'}"
},
"department_access_description": "@:{'phrases.compat.roles.permissions.department_access_description'}",
"empty": "@:{'phrases.compat.roles.permissions.empty'}",
"empty_group": "@:{'phrases.compat.roles.permissions.empty_group'}",
"errors": {
"load": "@:{'phrases.compat.roles.permissions.errors.load'}",
"update": "@:{'phrases.compat.roles.permissions.errors.update'}"
},
"filtered_summary": "@:{'phrases.compat.roles.permissions.filtered_summary'}",
"groups": {
"access": {
"description": "@:{'phrases.compat.roles.permissions.groups.access.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.access.label'}"
},
"bookings": {
"description": "@:{'phrases.compat.roles.permissions.groups.bookings.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.bookings.label'}"
},
"customers_users": {
"description": "@:{'phrases.compat.roles.permissions.groups.customers_users.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.customers_users.label'}"
},
"departments_operations": {
"description": "@:{'phrases.compat.roles.permissions.groups.departments_operations.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.departments_operations.label'}"
},
"invoicing_economic": {
"description": "@:{'phrases.compat.roles.permissions.groups.invoicing_economic.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.invoicing_economic.label'}"
},
"orders_pos": {
"description": "@:{'phrases.compat.roles.permissions.groups.orders_pos.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.orders_pos.label'}"
},
"other": {
"description": "@:{'phrases.compat.roles.permissions.groups.other.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.other.label'}"
},
"products_pricing": {
"description": "@:{'phrases.compat.roles.permissions.groups.products_pricing.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.products_pricing.label'}"
},
"selfserve_edge": {
"description": "@:{'phrases.compat.roles.permissions.groups.selfserve_edge.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.selfserve_edge.label'}"
},
"system_config": {
"description": "@:{'phrases.compat.roles.permissions.groups.system_config.description'}",
"label": "@:{'phrases.compat.roles.permissions.groups.system_config.label'}"
}
},
"no_description": "@:{'phrases.compat.roles.permissions.no_description'}",
"no_search_results": "@:{'phrases.compat.roles.permissions.no_search_results'}",
"page_access": {
"admin": {
"description": "@:{'phrases.compat.roles.permissions.page_access.admin.description'}",
"label": "@:{'phrases.compat.roles.permissions.page_access.admin.label'}"
},
"superuser": {
"description": "@:{'phrases.compat.roles.permissions.page_access.superuser.description'}",
"label": "@:{'phrases.compat.roles.permissions.page_access.superuser.label'}"
},
"user": {
"description": "@:{'phrases.compat.roles.permissions.page_access.user.description'}",
"label": "@:{'phrases.compat.roles.permissions.page_access.user.label'}"
}
},
"search_label": "@:{'phrases.compat.roles.permissions.search_label'}",
"search_placeholder": "@:{'phrases.compat.roles.permissions.search_placeholder'}",
"selected_summary": "@:{'phrases.compat.roles.permissions.selected_summary'}",
"status": {
"disabled": "@:{'phrases.compat.roles.permissions.status.disabled'}",
"enabled": "@:{'phrases.compat.roles.permissions.status.enabled'}"
},
"subtitle": "@:{'phrases.compat.roles.permissions.subtitle'}",
"title": "@:{'phrases.compat.roles.permissions.title'}",
"title_with_name": "@:{'phrases.compat.roles.permissions.title_with_name'}"
},
"subtitle": "@:{'phrases.compat.roles.subtitle'}",
"title": "@:{'phrases.compat.common.roles'}"
}
@@ -2,6 +2,20 @@
"vehicles": {
"actions": "@:common.actions",
"add": "@:{'phrases.compat.vehicles.add'}",
"add_modal": {
"customer_label": "@:{'phrases.compat.vehicles.add_modal.customer_label'}",
"customer_placeholder": "@:{'phrases.compat.vehicles.add_modal.customer_placeholder'}",
"error": "@:{'phrases.compat.vehicles.add_modal.error'}",
"no_customer_results": "@:{'phrases.compat.vehicles.add_modal.no_customer_results'}",
"reference_placeholder": "@:{'phrases.compat.vehicles.add_modal.reference_placeholder'}",
"registration_placeholder": "@:{'phrases.compat.vehicles.add_modal.registration_placeholder'}",
"selected_customer": "@:{'phrases.compat.vehicles.add_modal.selected_customer'}",
"submit": "@:{'phrases.compat.vehicles.add_modal.submit'}",
"title": "@:{'phrases.compat.vehicles.add_modal.title'}",
"type_load_error": "@:{'phrases.compat.vehicles.add_modal.type_load_error'}",
"type_placeholder": "@:{'phrases.compat.vehicles.add_modal.type_placeholder'}",
"validation_error": "@:{'phrases.compat.vehicles.add_modal.validation_error'}"
},
"brand": "@:{'phrases.compat.vehicles.brand'}",
"color": "@:{'phrases.compat.vehicles.color'}",
"created_at": "@:{'phrases.compat.global.generated'}",
@@ -3,17 +3,17 @@
"error_report": {
"button": "Rapporter feil",
"title": "Rapporter feil",
"subtitle": "Send gjeldende skjermbilde og de siste feildetaljene til support.",
"subtitle": "Send de siste feildetaljene til support. Et skjermbilde legges ved når det er mulig.",
"before_error": "Hva gjorde du før feilen oppstod?",
"expected": "Hva forventet du at skulle skje?",
"actual": "Hva skjedde faktisk?",
"before_error_placeholder": "Beskriv handlingen du utførte, for eksempel å åpne ordre eller velge en kunde.",
"expected_placeholder": "Beskriv resultatet du forventet å se.",
"actual_placeholder": "Beskriv hva du så i stedet, inkludert eventuell feiltekst.",
"consent": "Jeg godtar at gjeldende app-skjermbilde, siste request-feil, Vue-feil, nettleserdetaljer og svarene mine samles inn for feilsøking.",
"consent": "Jeg godtar at siste request-feil, Vue-feil, nettleserdetaljer, svarene mine og et app-skjermbilde når det er mulig, samles inn for feilsøking.",
"submit": "Send rapport",
"submitted": "Feilrapport sendt.",
"capture_failed": "Skjermbildet kunne ikke tas. Prøv igjen.",
"capture_failed": "Skjermbildet kunne ikke tas. Rapporten sendes uten skjermbilde.",
"submit_failed": "Feilrapporten kunne ikke sendes.",
"required": "Alle felter og godkjenning av datainnsamling er påkrevd.",
"page_subtitle": "Rapporterte skjermer, request-feil og Vue-feil",
@@ -0,0 +1,97 @@
{
"compat": {
"roles": {
"permissions": {
"access_scope": "Tilgangsomrade",
"actions": {
"clear_all": "Tomm gruppe",
"reload": "Last inn pa nytt",
"retry": "Prov igjen",
"select_all": "Velg gruppe"
},
"columns": {
"enabled": "Aktivert",
"permission": "Tillatelse",
"route": "Rute",
"status": "Status"
},
"department_access_description": "Gir tilgang til {name}.",
"empty": "Ingen tillatelser er tilgjengelige.",
"empty_group": "Denne gruppen har ingen tillatelser.",
"errors": {
"load": "Kunne ikke laste rolletillatelser.",
"update": "En eller flere tillatelser kunne ikke oppdateres. De gjeldende rolletillatelsene ble lastet inn pa nytt."
},
"filtered_summary": "{count} treff",
"groups": {
"access": {
"description": "Sidetype- og avdelingstilgang.",
"label": "Tilgang"
},
"bookings": {
"description": "Tillatelser for bookinger, tidsbookinger og planlegging.",
"label": "Bookinger"
},
"customers_users": {
"description": "Tillatelser for kunder, brukere, ansatte, roller og innlogging.",
"label": "Kunder og brukere"
},
"departments_operations": {
"description": "Avdelingsdrift, baner, releer, varsler, rapporter og mal.",
"label": "Avdelinger og drift"
},
"invoicing_economic": {
"description": "Tillatelser for fakturaer, faktureringsperioder, innkreving og e-conomic.",
"label": "Fakturering og e-conomic"
},
"orders_pos": {
"description": "Tillatelser for ordrer, POS, betalinger og ordrelinjer.",
"label": "Ordrer og POS"
},
"other": {
"description": "Tillatelser som ikke matcher en annen gruppe.",
"label": "Annet"
},
"products_pricing": {
"description": "Tillatelser for produkter, priser, rabatter og valuta.",
"label": "Produkter og priser"
},
"selfserve_edge": {
"description": "Tillatelser for selvbetjening, vask, skannere, gatewayer og edge-utstyr.",
"label": "Selvbetjening og edge"
},
"system_config": {
"description": "System, konfigurasjon, release, replikering og integrasjoner.",
"label": "System og konfigurasjon"
}
},
"no_description": "Ingen beskrivelse er tilgjengelig.",
"no_search_results": "Ingen tillatelser matcher soket.",
"page_access": {
"admin": {
"description": "Gir tilgang til administrasjonssider.",
"label": "Admin"
},
"superuser": {
"description": "Gir tilgang til superbrukersider.",
"label": "Superbruker"
},
"user": {
"description": "Gir tilgang til brukersider.",
"label": "Bruker"
}
},
"search_label": "Sok i tillatelser",
"search_placeholder": "Sok pa label, nokkel, beskrivelse, rute eller metode",
"selected_summary": "{selected} av {total} valgt",
"status": {
"disabled": "Deaktivert",
"enabled": "Aktivert"
},
"subtitle": "Rolletillatelser",
"title": "Tillatelser for rolle {id}",
"title_with_name": "Tillatelser for {name}"
}
}
}
}
+248
View File
@@ -95,6 +95,130 @@
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}"
},
"role_permissions": {
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
"tooltip": "@:{'phrases.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'phrases.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'phrases.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'phrases.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'phrases.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'phrases.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
"bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.reports'}",
"limited_backoffice": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"view_department_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
},
"view_own_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -204,6 +328,130 @@
"load_employees": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.laste'} @:{'terms.glossary.medarbeidere'}.",
"save_employee": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} @:{'terms.glossary.lagre'} @:{'terms.glossary.medarbeideren'}.",
"deactivate_employee": "@.capitalize:{'terms.glossary.kunne'} @:{'terms.glossary.ikke'} deaktivere @:{'terms.glossary.medarbeideren'}."
},
"role_permissions": {
"title": "Rolletillatelser",
"tooltip": "Vis rolletillatelser",
"intro": "Roller samler de nøyaktige funksjonene som tildeles en ansatt når rollen lagres.",
"scope": "Alle funksjoner er begrenset til avdelingene som velges for den ansatte.",
"selected": "Valgt",
"close": "Lukk",
"unavailable": "Tillatelsesdetaljer er ikke tilgjengelige for denne rollen før rollekatalogen er oppdatert.",
"groups": {
"account": "Konto",
"orders": "Ordrer",
"bookings": "Bookinger",
"time_bookings": "Tidsbookinger",
"reports": "Rapporter",
"limited_backoffice": "Backoffice",
"unknown": "Annet"
},
"capabilities": {
"sign_in": {
"label": "Logg inn",
"description": "Lar den ansatte logge inn i Pleno."
},
"view_own_permissions": {
"label": "Se egne tillatelser",
"description": "Viser den ansatte hvilke tilgangsrettigheter kontoen har."
},
"view_orders": {
"label": "Se ordrer",
"description": "Kan åpne ordrelister og ordredetaljer for tildelte avdelinger."
},
"create_orders": {
"label": "Opprett ordrer",
"description": "Kan opprette nye ordrer for tildelte avdelinger."
},
"edit_orders": {
"label": "Rediger ordrer",
"description": "Kan endre ordredetaljer for tildelte avdelinger."
},
"delete_orders": {
"label": "Slett ordrer",
"description": "Kan slette ordrer i tildelte avdelinger."
},
"view_order_items": {
"label": "Se ordrelinjer",
"description": "Kan se produkter, tjenester og priser på ordrer."
},
"create_order_items": {
"label": "Opprett ordrelinjer",
"description": "Kan legge produkter og tjenester til ordrer."
},
"update_order_lines": {
"label": "Rediger ordrelinjer",
"description": "Kan endre produkter, antall og priser på ordrer."
},
"remove_order_lines": {
"label": "Slett ordrelinjer",
"description": "Kan fjerne produkter og tjenester fra ordrer."
},
"charge_orders": {
"label": "Belast ordrer",
"description": "Kan ta betaling eller belaste en betalingsreservasjon for en ordre."
},
"view_department_bookings": {
"label": "Se avdelingsbookinger",
"description": "Kan se bookinger for tildelte avdelinger."
},
"view_own_bookings": {
"label": "Se egne bookinger",
"description": "Kan se bookingantall og bookinger knyttet til ansattkontoen."
},
"update_bookings": {
"label": "Rediger bookinger",
"description": "Kan endre tillatte bookingdetaljer."
},
"create_bookings": {
"label": "Opprett bookinger",
"description": "Kan opprette nye bookinger for tildelte avdelinger."
},
"mark_bookings_complete": {
"label": "Fullfør bookinger",
"description": "Kan markere bookinger som fullført."
},
"send_booking_confirmations": {
"label": "Send bookingbekreftelser på nytt",
"description": "Kan sende bookingbekreftelser og fullførings-e-poster på nytt."
},
"view_time_booking_entries": {
"label": "Se tidsbookinger",
"description": "Kan gå gjennom tidsbookingoppføringer for tildelte avdelinger."
},
"create_time_booking_entries": {
"label": "Opprett tidsbookinger",
"description": "Kan legge til nye tidsbookingoppføringer."
},
"edit_time_booking_entries": {
"label": "Rediger tidsbookinger",
"description": "Kan redigere tidsbookingoppføringer når backend-ruten støtter det."
},
"view_order_statistics": {
"label": "Se ordrestatistikk",
"description": "Kan åpne ordrestatistikk for tildelte avdelinger."
},
"view_booking_statistics": {
"label": "Se bookingstatistikk",
"description": "Kan åpne bookingstatistikk for tildelte avdelinger."
},
"open_limited_backoffice": {
"label": "Åpne begrenset backoffice",
"description": "Kan åpne det begrensede backofficeområdet."
},
"manage_department_prices": {
"label": "Administrer avdelingspriser",
"description": "Kan redigere eksplisitte priser for tildelte avdelinger."
},
"manage_employee_access": {
"label": "Administrer ansattilgang",
"description": "Kan opprette, redigere og deaktivere begrensede backofficeansatte."
},
"unknown": {
"label": "Ekstra funksjon",
"description": "Denne funksjonen kommer fra rollekatalogen, men er ennå ikke beskrevet i grensesnittet."
}
}
}
}
}
@@ -3,17 +3,17 @@
"error_report": {
"button": "Rapportera fel",
"title": "Rapportera fel",
"subtitle": "Skicka aktuell skärm och de senaste feldetaljerna till support.",
"subtitle": "Skicka de senaste feldetaljerna till support. En skärmbild bifogas när det är möjligt.",
"before_error": "Vad gjorde du innan felet uppstod?",
"expected": "Vad förväntade du dig skulle hända?",
"actual": "Vad hände faktiskt?",
"before_error_placeholder": "Beskriv åtgärden du gjorde, till exempel att öppna ordrar eller välja en kund.",
"expected_placeholder": "Beskriv resultatet du förväntade dig att se.",
"actual_placeholder": "Beskriv vad du såg i stället, inklusive eventuell feltext.",
"consent": "Jag accepterar att aktuell app-skärm, senaste request-fel, Vue-fel, webbläsardetaljer och mina svar samlas in för felsökning.",
"consent": "Jag accepterar att senaste request-fel, Vue-fel, webbläsardetaljer, mina svar och en app-skärmbild när det är möjligt samlas in för felsökning.",
"submit": "Skicka rapport",
"submitted": "Felrapport skickad.",
"capture_failed": "Skärmbilden kunde inte tas. Försök igen.",
"capture_failed": "Skärmbilden kunde inte tas. Rapporten skickas utan skärmbild.",
"submit_failed": "Felrapporten kunde inte skickas.",
"required": "Alla fält och godkännande av datainsamling krävs.",
"page_subtitle": "Rapporterade skärmar, request-fel och Vue-fel",
@@ -0,0 +1,97 @@
{
"compat": {
"roles": {
"permissions": {
"access_scope": "Atkomstomrade",
"actions": {
"clear_all": "Rensa grupp",
"reload": "Ladda om",
"retry": "Forsok igen",
"select_all": "Valj grupp"
},
"columns": {
"enabled": "Aktiverad",
"permission": "Behorighet",
"route": "Rutt",
"status": "Status"
},
"department_access_description": "Ger atkomst till {name}.",
"empty": "Inga behorigheter ar tillgangliga.",
"empty_group": "Den har gruppen har inga behorigheter.",
"errors": {
"load": "Kunde inte lasa in rollbehorigheter.",
"update": "En eller flera behorigheter kunde inte uppdateras. Rollens aktuella behorigheter laddades om."
},
"filtered_summary": "{count} traffar",
"groups": {
"access": {
"description": "Sidtyp- och avdelningsatkomst.",
"label": "Atkomst"
},
"bookings": {
"description": "Behorigheter for bokningar, tidsbokningar och planering.",
"label": "Bokningar"
},
"customers_users": {
"description": "Behorigheter for kunder, anvandare, medarbetare, roller och inloggning.",
"label": "Kunder och anvandare"
},
"departments_operations": {
"description": "Avdelningsdrift, banor, relan, aviseringar, rapporter och mal.",
"label": "Avdelningar och drift"
},
"invoicing_economic": {
"description": "Behorigheter for fakturor, faktureringsperioder, inkasso och e-conomic.",
"label": "Fakturering och e-conomic"
},
"orders_pos": {
"description": "Behorigheter for ordrar, POS, betalningar och orderrader.",
"label": "Ordrar och POS"
},
"other": {
"description": "Behorigheter som inte matchar nagon annan grupp.",
"label": "Ovrigt"
},
"products_pricing": {
"description": "Behorigheter for produkter, priser, rabatter och valuta.",
"label": "Produkter och priser"
},
"selfserve_edge": {
"description": "Behorigheter for sjalvbetjaning, tvatt, skannrar, gateways och edge-utrustning.",
"label": "Sjalvbetjaning och edge"
},
"system_config": {
"description": "System, konfiguration, release, replikering och integrationer.",
"label": "System och konfiguration"
}
},
"no_description": "Ingen beskrivning ar tillganglig.",
"no_search_results": "Inga behorigheter matchar aktuell sokning.",
"page_access": {
"admin": {
"description": "Ger atkomst till administrationssidor.",
"label": "Admin"
},
"superuser": {
"description": "Ger atkomst till superuser-sidor.",
"label": "Superuser"
},
"user": {
"description": "Ger atkomst till anvandarsidor.",
"label": "Anvandare"
}
},
"search_label": "Sok behorigheter",
"search_placeholder": "Sok pa label, nyckel, beskrivning, rutt eller metod",
"selected_summary": "{selected} av {total} valda",
"status": {
"disabled": "Inaktiverad",
"enabled": "Aktiverad"
},
"subtitle": "Rollbehorigheter",
"title": "Behorigheter for roll {id}",
"title_with_name": "Behorigheter for {name}"
}
}
}
}
+248
View File
@@ -95,6 +95,130 @@
"load_employees": "@:{'phrases.compat.limited_backoffice.errors.load_employees'}",
"save_employee": "@:{'phrases.compat.limited_backoffice.errors.save_employee'}",
"deactivate_employee": "@:{'phrases.compat.limited_backoffice.errors.deactivate_employee'}"
},
"role_permissions": {
"title": "@:{'phrases.compat.limited_backoffice.role_permissions.title'}",
"tooltip": "@:{'phrases.compat.limited_backoffice.role_permissions.tooltip'}",
"intro": "@:{'phrases.compat.limited_backoffice.role_permissions.intro'}",
"scope": "@:{'phrases.compat.limited_backoffice.role_permissions.scope'}",
"selected": "@:{'phrases.compat.limited_backoffice.role_permissions.selected'}",
"close": "@:{'phrases.compat.limited_backoffice.role_permissions.close'}",
"unavailable": "@:{'phrases.compat.limited_backoffice.role_permissions.unavailable'}",
"groups": {
"account": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.account'}",
"orders": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.orders'}",
"bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.bookings'}",
"time_bookings": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.time_bookings'}",
"reports": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.reports'}",
"limited_backoffice": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.limited_backoffice'}",
"unknown": "@:{'phrases.compat.limited_backoffice.role_permissions.groups.unknown'}"
},
"capabilities": {
"sign_in": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.sign_in.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.sign_in.description'}"
},
"view_own_permissions": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_permissions.description'}"
},
"view_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_orders.description'}"
},
"create_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_orders.description'}"
},
"edit_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_orders.description'}"
},
"delete_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.delete_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.delete_orders.description'}"
},
"view_order_items": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_items.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_items.description'}"
},
"create_order_items": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_order_items.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_order_items.description'}"
},
"update_order_lines": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_order_lines.description'}"
},
"remove_order_lines": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.remove_order_lines.description'}"
},
"charge_orders": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.charge_orders.description'}"
},
"view_department_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_department_bookings.description'}"
},
"view_own_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_own_bookings.description'}"
},
"update_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.update_bookings.description'}"
},
"create_bookings": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_bookings.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_bookings.description'}"
},
"mark_bookings_complete": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description'}"
},
"send_booking_confirmations": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description'}"
},
"view_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description'}"
},
"create_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description'}"
},
"edit_time_booking_entries": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description'}"
},
"view_order_statistics": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_order_statistics.description'}"
},
"view_booking_statistics": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description'}"
},
"open_limited_backoffice": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description'}"
},
"manage_department_prices": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_department_prices.description'}"
},
"manage_employee_access": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.manage_employee_access.description'}"
},
"unknown": {
"label": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.unknown.label'}",
"description": "@:{'phrases.compat.limited_backoffice.role_permissions.capabilities.unknown.description'}"
}
}
}
},
"redirect": {
@@ -204,6 +328,130 @@
"load_employees": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.ladda'} @:{'terms.glossary.medarbetare'}.",
"save_employee": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.spara'} @:{'terms.glossary.medarbetaren'}.",
"deactivate_employee": "@.capitalize:{'terms.glossary.kunde'} @:{'terms.glossary.inte'} @:{'terms.glossary.inaktivera'} @:{'terms.glossary.medarbetaren'}."
},
"role_permissions": {
"title": "Rollbehörigheter",
"tooltip": "Visa rollbehörigheter",
"intro": "Roller samlar de exakta funktioner som tilldelas en medarbetare när rollen sparas.",
"scope": "Alla funktioner är begränsade till de avdelningar som väljs för medarbetaren.",
"selected": "Vald",
"close": "Stäng",
"unavailable": "Behörighetsdetaljer är inte tillgängliga för den här rollen förrän rollkatalogen har uppdaterats.",
"groups": {
"account": "Konto",
"orders": "Order",
"bookings": "Bokningar",
"time_bookings": "Tidsbokningar",
"reports": "Rapporter",
"limited_backoffice": "Backoffice",
"unknown": "Övrigt"
},
"capabilities": {
"sign_in": {
"label": "Logga in",
"description": "Ger medarbetaren möjlighet att logga in i Pleno."
},
"view_own_permissions": {
"label": "Se egna behörigheter",
"description": "Visar medarbetaren vilka åtkomsträttigheter kontot har."
},
"view_orders": {
"label": "Se order",
"description": "Kan öppna orderlistor och orderdetaljer för tilldelade avdelningar."
},
"create_orders": {
"label": "Skapa order",
"description": "Kan skapa nya order för tilldelade avdelningar."
},
"edit_orders": {
"label": "Redigera order",
"description": "Kan ändra orderdetaljer för tilldelade avdelningar."
},
"delete_orders": {
"label": "Ta bort order",
"description": "Kan ta bort order i tilldelade avdelningar."
},
"view_order_items": {
"label": "Se orderrader",
"description": "Kan se produkter, tjänster och priser på order."
},
"create_order_items": {
"label": "Skapa orderrader",
"description": "Kan lägga till produkter och tjänster på order."
},
"update_order_lines": {
"label": "Redigera orderrader",
"description": "Kan ändra produkter, antal och priser på order."
},
"remove_order_lines": {
"label": "Ta bort orderrader",
"description": "Kan ta bort produkter och tjänster från order."
},
"charge_orders": {
"label": "Debitera order",
"description": "Kan ta betalt eller debitera en betalningsreservation för en order."
},
"view_department_bookings": {
"label": "Se avdelningsbokningar",
"description": "Kan se bokningar för tilldelade avdelningar."
},
"view_own_bookings": {
"label": "Se egna bokningar",
"description": "Kan se bokningsantal och bokningar kopplade till medarbetarkontot."
},
"update_bookings": {
"label": "Redigera bokningar",
"description": "Kan ändra tillåtna bokningsdetaljer."
},
"create_bookings": {
"label": "Skapa bokningar",
"description": "Kan skapa nya bokningar för tilldelade avdelningar."
},
"mark_bookings_complete": {
"label": "Slutför bokningar",
"description": "Kan markera bokningar som slutförda."
},
"send_booking_confirmations": {
"label": "Skicka bokningsbekräftelser igen",
"description": "Kan skicka bokningsbekräftelser och slutförandemejl igen."
},
"view_time_booking_entries": {
"label": "Se tidsbokningar",
"description": "Kan granska tidsbokningsposter för tilldelade avdelningar."
},
"create_time_booking_entries": {
"label": "Skapa tidsbokningar",
"description": "Kan lägga till nya tidsbokningsposter."
},
"edit_time_booking_entries": {
"label": "Redigera tidsbokningar",
"description": "Kan redigera tidsbokningsposter när backend-rutten stöder det."
},
"view_order_statistics": {
"label": "Se orderstatistik",
"description": "Kan öppna orderstatistik för tilldelade avdelningar."
},
"view_booking_statistics": {
"label": "Se bokningsstatistik",
"description": "Kan öppna bokningsstatistik för tilldelade avdelningar."
},
"open_limited_backoffice": {
"label": "Öppna begränsat backoffice",
"description": "Kan öppna det begränsade backofficeområdet."
},
"manage_department_prices": {
"label": "Hantera avdelningspriser",
"description": "Kan redigera uttryckliga priser för tilldelade avdelningar."
},
"manage_employee_access": {
"label": "Hantera medarbetaråtkomst",
"description": "Kan skapa, redigera och inaktivera begränsade backofficemedarbetare."
},
"unknown": {
"label": "Extra funktion",
"description": "Den här funktionen kommer från rollkatalogen men är ännu inte beskriven i gränssnittet."
}
}
}
}
}
+84
View File
@@ -0,0 +1,84 @@
const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
const DATE_ONLY_PREFIX_PATTERN = /^(\d{4}-\d{2}-\d{2})/;
const padDatePart = (value) => String(value).padStart(2, "0");
export const isValidDate = (value) => value instanceof Date && !Number.isNaN(value.getTime());
export const formatLocalDateOnly = (value = new Date()) => {
if (typeof value === "string") {
const directMatch = value.trim().match(DATE_ONLY_PREFIX_PATTERN);
if (directMatch) {
return directMatch[1];
}
}
const date = value instanceof Date ? value : new Date(value);
if (!isValidDate(date)) {
return "";
}
return [
date.getFullYear(),
padDatePart(date.getMonth() + 1),
padDatePart(date.getDate()),
].join("-");
};
export const parseLocalDateOnly = (value) => {
if (value instanceof Date) {
return isValidDate(value)
? new Date(value.getFullYear(), value.getMonth(), value.getDate())
: new Date(Number.NaN);
}
const stringValue = String(value ?? "").trim();
const match = DATE_ONLY_PATTERN.exec(stringValue);
if (match) {
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const parsed = new Date(year, month - 1, day);
if (
parsed.getFullYear() === year
&& parsed.getMonth() === month - 1
&& parsed.getDate() === day
) {
return parsed;
}
return new Date(Number.NaN);
}
const parsed = new Date(stringValue);
return isValidDate(parsed)
? new Date(parsed.getFullYear(), parsed.getMonth(), parsed.getDate())
: parsed;
};
export const todayLocalDateOnly = () => formatLocalDateOnly(new Date());
export const yesterdayLocalDateOnly = () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return formatLocalDateOnly(yesterday);
};
export const startOfLocalDate = (value) => {
const date = parseLocalDateOnly(value);
if (!isValidDate(date)) {
return date;
}
date.setHours(0, 0, 0, 0);
return date;
};
export const endOfLocalDate = (value) => {
const date = parseLocalDateOnly(value);
if (!isValidDate(date)) {
return date;
}
date.setHours(23, 59, 59, 999);
return date;
};
@@ -28,6 +28,7 @@ const errorMessage = ref("");
const successMessage = ref("");
const editingEmployee = ref(null);
const formAttempted = ref(false);
const showRolePermissions = ref(false);
const form = ref({
display_name: "",
email: "",
@@ -59,9 +60,155 @@ const roleMessages = computed(() => ({
},
}));
const rolePermissionGroupMessages = computed(() => ({
account: {
label: t("templates.limited_backoffice.role_permissions.groups.account"),
},
orders: {
label: t("templates.limited_backoffice.role_permissions.groups.orders"),
},
bookings: {
label: t("templates.limited_backoffice.role_permissions.groups.bookings"),
},
time_bookings: {
label: t("templates.limited_backoffice.role_permissions.groups.time_bookings"),
},
reports: {
label: t("templates.limited_backoffice.role_permissions.groups.reports"),
},
limited_backoffice: {
label: t("templates.limited_backoffice.role_permissions.groups.limited_backoffice"),
},
unknown: {
label: t("templates.limited_backoffice.role_permissions.groups.unknown"),
},
}));
const roleCapabilityMessages = computed(() => ({
sign_in: {
label: t("templates.limited_backoffice.role_permissions.capabilities.sign_in.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.sign_in.description"),
},
view_own_permissions: {
label: t("templates.limited_backoffice.role_permissions.capabilities.view_own_permissions.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.view_own_permissions.description"),
},
view_orders: {
label: t("templates.limited_backoffice.role_permissions.capabilities.view_orders.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.view_orders.description"),
},
create_orders: {
label: t("templates.limited_backoffice.role_permissions.capabilities.create_orders.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.create_orders.description"),
},
edit_orders: {
label: t("templates.limited_backoffice.role_permissions.capabilities.edit_orders.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.edit_orders.description"),
},
delete_orders: {
label: t("templates.limited_backoffice.role_permissions.capabilities.delete_orders.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.delete_orders.description"),
},
view_order_items: {
label: t("templates.limited_backoffice.role_permissions.capabilities.view_order_items.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.view_order_items.description"),
},
create_order_items: {
label: t("templates.limited_backoffice.role_permissions.capabilities.create_order_items.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.create_order_items.description"),
},
update_order_lines: {
label: t("templates.limited_backoffice.role_permissions.capabilities.update_order_lines.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.update_order_lines.description"),
},
remove_order_lines: {
label: t("templates.limited_backoffice.role_permissions.capabilities.remove_order_lines.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.remove_order_lines.description"),
},
charge_orders: {
label: t("templates.limited_backoffice.role_permissions.capabilities.charge_orders.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.charge_orders.description"),
},
view_department_bookings: {
label: t("templates.limited_backoffice.role_permissions.capabilities.view_department_bookings.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.view_department_bookings.description"),
},
view_own_bookings: {
label: t("templates.limited_backoffice.role_permissions.capabilities.view_own_bookings.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.view_own_bookings.description"),
},
update_bookings: {
label: t("templates.limited_backoffice.role_permissions.capabilities.update_bookings.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.update_bookings.description"),
},
create_bookings: {
label: t("templates.limited_backoffice.role_permissions.capabilities.create_bookings.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.create_bookings.description"),
},
mark_bookings_complete: {
label: t("templates.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.mark_bookings_complete.description"),
},
send_booking_confirmations: {
label: t("templates.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.send_booking_confirmations.description"),
},
view_time_booking_entries: {
label: t("templates.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.view_time_booking_entries.description"),
},
create_time_booking_entries: {
label: t("templates.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.create_time_booking_entries.description"),
},
edit_time_booking_entries: {
label: t("templates.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.edit_time_booking_entries.description"),
},
view_order_statistics: {
label: t("templates.limited_backoffice.role_permissions.capabilities.view_order_statistics.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.view_order_statistics.description"),
},
view_booking_statistics: {
label: t("templates.limited_backoffice.role_permissions.capabilities.view_booking_statistics.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.view_booking_statistics.description"),
},
open_limited_backoffice: {
label: t("templates.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.open_limited_backoffice.description"),
},
manage_department_prices: {
label: t("templates.limited_backoffice.role_permissions.capabilities.manage_department_prices.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.manage_department_prices.description"),
},
manage_employee_access: {
label: t("templates.limited_backoffice.role_permissions.capabilities.manage_employee_access.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.manage_employee_access.description"),
},
unknown: {
label: t("templates.limited_backoffice.role_permissions.capabilities.unknown.label"),
description: t("templates.limited_backoffice.role_permissions.capabilities.unknown.description"),
},
}));
const roleLabel = (role) => roleMessages.value[role?.key]?.label || role?.label || "";
const roleDescription = (role) => roleMessages.value[role?.key]?.description || role?.description || "";
const roleByKey = (key) => roles.value.find((role) => role.key === key) || { key };
const rolePermissionGroupLabel = (groupKey) =>
rolePermissionGroupMessages.value[groupKey]?.label || rolePermissionGroupMessages.value.unknown.label;
const roleCapabilityLabel = (capabilityKey) =>
roleCapabilityMessages.value[capabilityKey]?.label || roleCapabilityMessages.value.unknown.label;
const roleCapabilityDescription = (capabilityKey) =>
roleCapabilityMessages.value[capabilityKey]?.description || roleCapabilityMessages.value.unknown.description;
const rolePermissionGroups = (role) =>
(Array.isArray(role?.permission_groups) ? role.permission_groups : [])
.map((group) => ({
key: String(group?.key || ""),
capabilities: (Array.isArray(group?.capabilities) ? group.capabilities : [])
.map((capability) => String(capability || ""))
.filter(Boolean),
}))
.filter((group) => group.key && group.capabilities.length > 0);
const visibleRoles = computed(() => roles.value.filter((role) => ROLE_KEYS.includes(role.key)));
const isEditing = computed(() => Boolean(editingEmployee.value));
@@ -301,9 +448,29 @@ onMounted(() => {
</div>
<div class="field">
<label class="label" for="limited-employee-role">
{{ t("templates.limited_backoffice.employees.role") }}
</label>
<div class="limited-employee-role-label-row">
<label class="label mb-0" for="limited-employee-role">
{{ t("templates.limited_backoffice.employees.role") }}
</label>
<b-tooltip
:label="t('templates.limited_backoffice.role_permissions.tooltip')"
position="is-left"
type="is-dark"
>
<button
type="button"
class="button is-white is-small limited-role-help-button"
:aria-label="t('templates.limited_backoffice.role_permissions.tooltip')"
:title="t('templates.limited_backoffice.role_permissions.tooltip')"
data-testid="limited-employee-role-help"
@click="showRolePermissions = true"
>
<span class="icon is-small">
<i class="fas fa-question-circle" aria-hidden="true"></i>
</span>
</button>
</b-tooltip>
</div>
<div class="control">
<div class="select is-fullwidth">
<select id="limited-employee-role" v-model="form.role_key" data-testid="limited-employee-role">
@@ -464,6 +631,94 @@ onMounted(() => {
</div>
</div>
</div>
<b-modal
v-model="showRolePermissions"
:width="880"
:can-cancel="['escape', 'x', 'outside']"
trap-focus
aria-role="dialog"
aria-modal
>
<div class="modal-card limited-role-permissions-modal" data-testid="limited-role-permissions-modal">
<header class="modal-card-head">
<p class="modal-card-title">{{ t("templates.limited_backoffice.role_permissions.title") }}</p>
<button
type="button"
class="delete"
:aria-label="t('templates.limited_backoffice.role_permissions.close')"
data-testid="limited-role-permissions-close"
@click="showRolePermissions = false"
></button>
</header>
<section class="modal-card-body">
<p class="limited-role-permissions-modal__intro" data-testid="limited-role-permissions-intro">
{{ t("templates.limited_backoffice.role_permissions.intro") }}
</p>
<p class="limited-role-permissions-modal__scope">
{{ t("templates.limited_backoffice.role_permissions.scope") }}
</p>
<div class="limited-role-permissions-list">
<section
v-for="role in visibleRoles"
:key="role.key"
class="limited-role-permissions-role"
:class="{ 'limited-role-permissions-role--selected': role.key === form.role_key }"
:data-testid="`limited-role-permissions-role-${role.key}`"
>
<div class="limited-role-permissions-role__header">
<div>
<h3 class="title is-6 mb-1">{{ roleLabel(role) }}</h3>
<p class="is-size-7 has-text-grey">{{ roleDescription(role) }}</p>
</div>
<span v-if="role.key === form.role_key" class="tag is-info is-light">
{{ t("templates.limited_backoffice.role_permissions.selected") }}
</span>
</div>
<div
v-if="rolePermissionGroups(role).length === 0"
class="notification is-warning is-light limited-role-permissions-role__empty"
>
{{ t("templates.limited_backoffice.role_permissions.unavailable") }}
</div>
<div
v-for="group in rolePermissionGroups(role)"
:key="`${role.key}-${group.key}`"
class="limited-role-permissions-group"
:data-testid="`limited-role-permissions-group-${role.key}-${group.key}`"
>
<h4 class="limited-role-permissions-group__title">
{{ rolePermissionGroupLabel(group.key) }}
</h4>
<ul class="limited-role-permissions-capabilities">
<li
v-for="capability in group.capabilities"
:key="`${role.key}-${group.key}-${capability}`"
class="limited-role-permissions-capability"
>
<strong>{{ roleCapabilityLabel(capability) }}</strong>
<span>{{ roleCapabilityDescription(capability) }}</span>
</li>
</ul>
</div>
</section>
</div>
</section>
<footer class="modal-card-foot is-justify-content-flex-end">
<button
type="button"
class="button is-dark"
data-testid="limited-role-permissions-close-footer"
@click="showRolePermissions = false"
>
{{ t("templates.limited_backoffice.role_permissions.close") }}
</button>
</footer>
</div>
</b-modal>
</LimitedBackofficeLayout>
</RestrictedPageWrapper>
</template>
@@ -484,6 +739,28 @@ onMounted(() => {
gap: 0.5rem;
}
.limited-employee-role-label-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.limited-role-help-button {
flex: 0 0 auto;
width: 2rem;
height: 2rem;
color: #485fc7;
border-radius: 999px;
}
.limited-role-help-button:focus,
.limited-role-help-button:hover {
color: #363636;
background: #f5f5f5;
}
.limited-employees-header {
display: flex;
justify-content: space-between;
@@ -509,6 +786,78 @@ onMounted(() => {
width: 190px;
}
.limited-role-permissions-modal {
width: 100%;
max-width: 880px;
}
.limited-role-permissions-modal__intro {
margin-bottom: 0.5rem;
}
.limited-role-permissions-modal__scope {
margin-bottom: 1rem;
color: #4a4a4a;
}
.limited-role-permissions-list {
display: grid;
gap: 1rem;
}
.limited-role-permissions-role {
border: 1px solid #dbdbdb;
border-radius: 8px;
padding: 1rem;
background: #fff;
}
.limited-role-permissions-role--selected {
border-color: #485fc7;
box-shadow: inset 3px 0 0 #485fc7;
}
.limited-role-permissions-role__header {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.limited-role-permissions-role__empty {
margin-bottom: 0;
}
.limited-role-permissions-group + .limited-role-permissions-group {
margin-top: 0.85rem;
}
.limited-role-permissions-group__title {
margin-bottom: 0.4rem;
font-size: 0.85rem;
font-weight: 700;
color: #363636;
text-transform: uppercase;
}
.limited-role-permissions-capabilities {
display: grid;
gap: 0.45rem;
margin: 0;
}
.limited-role-permissions-capability {
display: grid;
gap: 0.1rem;
padding-left: 0.75rem;
border-left: 2px solid #dbdbdb;
}
.limited-role-permissions-capability span {
color: #4a4a4a;
}
@media screen and (max-width: 1024px) {
.limited-employees-header {
display: block;
@@ -518,4 +867,14 @@ onMounted(() => {
margin-top: 0.5rem;
}
}
@media screen and (max-width: 768px) {
.limited-role-permissions-role__header {
display: block;
}
.limited-role-permissions-role__header .tag {
margin-top: 0.5rem;
}
}
</style>
@@ -19,6 +19,7 @@ import DepartmentDashboardDailyReportNavigation from "@/views/dashboards/departm
import DepartmentDashboardDailyReportProductSales from "@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportProductSales.vue";
import DepartmentDashboardDailyReportTodayForm from "@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportTodayForm.vue";
import DepartmentWeather from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentWeather.vue";
import { parseLocalDateOnly, todayLocalDateOnly } from "@/services/dateOnly.js";
import {
complaints_metric_state,
count_bookings,
@@ -131,7 +132,7 @@ onMounted(() => {
return [...accessibleDepartmentIds.value];
})();
const initialDateFrom = typeof query.dateFrom === "string" ? query.dateFrom : new Date().toISOString().split("T")[0];
const initialDateFrom = typeof query.dateFrom === "string" ? query.dateFrom : todayLocalDateOnly();
const initialDateTo = typeof query.dateTo === "string" ? query.dateTo : initialDateFrom;
initializeDailyReportFilters({
@@ -170,7 +171,7 @@ watch(
);
const formatDateShort = (dateString) => {
const date = new Date(dateString);
const date = parseLocalDateOnly(dateString);
const dayName = date.toLocaleDateString("da-DK", { weekday: "long" });
const dateValue = date.toLocaleDateString("da-DK", { day: "2-digit", month: "2-digit", year: "2-digit" });
return `${dayName} d. ${dateValue}`;
@@ -178,7 +179,7 @@ const formatDateShort = (dateString) => {
const formatDateRangeSubtitle = (startDateString, endDateString) => {
const dateOptions = { day: "2-digit", month: "2-digit", year: "2-digit" };
return `${new Date(startDateString).toLocaleDateString("da-DK", dateOptions)} - ${new Date(endDateString).toLocaleDateString("da-DK", dateOptions)}`;
return `${parseLocalDateOnly(startDateString).toLocaleDateString("da-DK", dateOptions)} - ${parseLocalDateOnly(endDateString).toLocaleDateString("da-DK", dateOptions)}`;
};
const formatSubtitle = (subtitle) => {
@@ -2,18 +2,16 @@
import { computed, ref, watch } from "vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { createEmptyOutsideHours, normalizeOutsideHours } from "@/views/dashboards/departmentDashboard/modules/daily-report/outsideHours.js";
import { todayLocalDateOnly, yesterdayLocalDateOnly } from "@/services/dateOnly.js";
const GENERATE_UNIQUE_ID = () => Math.random().toString(36).slice(2, 11);
const DATE_TODAY = () => {
const date = new Date();
return date.toISOString().split("T")[0];
return todayLocalDateOnly();
};
const DATE_YESTERDAY = () => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date.toISOString().split("T")[0];
return yesterdayLocalDateOnly();
};
const date_shortcuts = ref({
@@ -17,6 +17,7 @@ import {
isLoading as departmentsStoreLoading,
} from "@/components/pagination/departmentTabs.vue";
import { isAccessibleVisibleDepartment, sortByDepartmentPriorityOrder } from "@/services/departmentVisibility.js";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
/**
* Props:
@@ -141,14 +142,7 @@ const toggleAllDepartments = () => {
};
const formatDateSelectionValue = (date) => {
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
return "";
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
return formatLocalDateOnly(date);
};
const onDateSelectionChange = (startDate, endDate) => {
@@ -161,7 +155,7 @@ const onDateSelectionChange = (startDate, endDate) => {
<div class="column is-12" data-testid="daily-report-date-controls">
<DatePeriodSelector
:on-selection-change="onDateSelectionChange"
:selection="{ startDate: new Date(selected_date), endDate: new Date(selected_date_to) }"
:selection="{ startDate: parseLocalDateOnly(selected_date), endDate: parseLocalDateOnly(selected_date_to) }"
:visibility="{
showDailySelector: true,
showWeeklySelector: true,
@@ -5,6 +5,7 @@ import { parseError, getError, clearErrors} from "@/components/request/HandleGlo
import ShowErrorField from "@/components/global/ShowErrorField.vue";
import { selected_date, onChangeCall, selected_department_id} from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue";
import Swal from "sweetalert2";
import { parseLocalDateOnly, yesterdayLocalDateOnly } from "@/services/dateOnly.js";
const props = defineProps({
department_id: Number,
@@ -23,7 +24,7 @@ const created_at = ref(null);
const id = ref(null);
const message = ref('');
const yesterday = ref({
date: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split('T')[0],
date: yesterdayLocalDateOnly(),
water_usage: 0,
water_usage_morning: 0,
notes: null,
@@ -104,7 +105,7 @@ const getDailyReport = async () => {
// Check if the date is today
const created_at_val = new Date(response.data.data.created_at);
const today = new Date();
if (created_at_val.toDateString() === (selected_date.value ? new Date(selected_date.value) : today).toDateString()) {
if (created_at_val.toDateString() === (selected_date.value ? parseLocalDateOnly(selected_date.value) : today).toDateString()) {
// The daily report is today
// So we can set the values
water_usage.value = parseInt(response.data.data.water_usage);
@@ -212,4 +213,4 @@ onChangeCall(() => {
<style scoped>
</style>
</style>
@@ -1,10 +1,11 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { timeBookingsNewScheduler } from '@/views/dashboards/departmentDashboard/modules/time-bookings/book/TimeBookingsNewScheduler.vue';
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
const date = ref();
// Set the initial date to the current date YYYY-MM-DD format
const formatDate = (date_input) => {
return date_input.toISOString().split('T')[0];
return formatLocalDateOnly(date_input);
};
date.value = formatDate(new Date());
@@ -15,7 +16,7 @@ watch(timeBookingsNewScheduler.date.selected, (newDate) => {
watch(date, (newDate) => {
// Update the selected date in the scheduler when the date input changes
timeBookingsNewScheduler.date.selected.value = new Date(newDate);
timeBookingsNewScheduler.date.selected.value = parseLocalDateOnly(newDate);
});
</script>
@@ -63,4 +64,4 @@ watch(date, (newDate) => {
<style scoped>
</style>
</style>
@@ -1,6 +1,7 @@
<script setup>
import { ref, watch } from 'vue';
import { SessionUser } from '@/components/session/token/SessionUser.vue';
import { todayLocalDateOnly } from "@/services/dateOnly.js";
const props = defineProps({
department_id: {
@@ -68,8 +69,7 @@ const getBookings = async () => {
};
const getDate = () => {
const date = new Date();
return date.toISOString().split('T')[0];
return todayLocalDateOnly();
};
// Get the department
@@ -2,6 +2,7 @@
import { selected_date, selected_date_to, selectDate as selectDailyReportDate } from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue";
import DatePeriodSelector from "@/components/displays/buttons/DatePeriodSelector.vue";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
// Emits for parents that want to react to date changes (non-breaking if unused)
const emit = defineEmits(['date-change']);
@@ -16,12 +17,12 @@ const selectDate = (startIso, endIso) => {
<template>
<DatePeriodSelector
:on-selection-change="(startDate, endDate) => selectDate(startDate.toISOString().split('T')[0], endDate.toISOString().split('T')[0])"
:selection="{ startDate: new Date(selected_date), endDate: new Date(selected_date_to) }"
:on-selection-change="(startDate, endDate) => selectDate(formatLocalDateOnly(startDate), formatLocalDateOnly(endDate))"
:selection="{ startDate: parseLocalDateOnly(selected_date), endDate: parseLocalDateOnly(selected_date_to) }"
:visibility="{ showDailySelector: true, showWeeklySelector: true, showMultipleMonthWarning: false, showUpdateButton: false }"
:reverse-level-order="true"
@update:selection="(newSelection) => {
selectDate(newSelection.startDate.toISOString().split('T')[0], newSelection.endDate.toISOString().split('T')[0]);
selectDate(formatLocalDateOnly(newSelection.startDate), formatLocalDateOnly(newSelection.endDate));
}"
/>
</template>
@@ -1,5 +1,6 @@
<script lang="ts">
import {defineComponent, ref, watch} from 'vue';
import { todayLocalDateOnly } from "@/services/dateOnly.js";
type fetch = {
@@ -11,8 +12,8 @@ type fetch = {
};
/** Define the variables */
export const show_this_week = ref(false);
export const selected_date = ref(new Date().toISOString().split('T')[0]);
export const selected_date_to = ref(new Date().toISOString().split('T')[0]);
export const selected_date = ref(todayLocalDateOnly());
export const selected_date_to = ref(todayLocalDateOnly());
export const last_fetch_id = ref(0);
export const fetches = ref<fetch[]>([]);
/** Define the functions */
@@ -53,4 +54,4 @@ watch([selected_date, selected_date_to], () => {
export default defineComponent({
name: 'DepartmentsOverviewObject'
});
</script>
</script>
@@ -1,21 +1,53 @@
<script setup>
import { getOrders } from "@/components/shop/Orders.vue";
import { showCreateOrderForm } from "@/components/forms/superUser/createOrderForm.vue";
import { ref } from "vue";
import SuperUserDashboardNavigation from "@/views/dashboards/superUserDashboard/SuperUserDashboardNavigation.vue";
import PageTitle from "@/components/global/PageTitle.vue";
import Orders from "@/components/displays/Orders.vue";
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import VehiclesPagination from "@/components/displays/pagination/models/UserDashboard/VehiclesPagination.vue";
import SuperuserAddVehicleModal from "@/components/forms/superUser/SuperuserAddVehicleModal.vue";
import { loadList } from "@/components/pagination/paginatedList.vue";
const isAddVehicleModalOpen = ref(false);
const openAddVehicleModal = () => {
isAddVehicleModalOpen.value = true;
};
const closeAddVehicleModal = () => {
isAddVehicleModalOpen.value = false;
};
const handleVehicleCreated = async () => {
closeAddVehicleModal();
await loadList();
};
</script>
<template>
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessSuperUser()">
<SuperUserDashboardNavigation />
<VehiclesPagination />
<VehiclesPagination>
<template #actions>
<button
class="button is-link button-same-width superuser-vehicles__add-button"
type="button"
data-testid="superuser-vehicles-add"
@click="openAddVehicleModal"
>
{{ $t("vehicles.add") }}
</button>
</template>
</VehiclesPagination>
<SuperuserAddVehicleModal
v-if="isAddVehicleModalOpen"
@close="closeAddVehicleModal"
@created="handleVehicleCreated"
/>
</RestrictedPageWrapper>
</template>
<style scoped>
</style>
.superuser-vehicles__add-button {
white-space: nowrap;
}
</style>
@@ -1,286 +1,572 @@
<script setup>
import { computed, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import PageTitle from "@/components/global/PageTitle.vue";
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
import {SessionUser} from "@/components/session/token/SessionUser.vue";
import {useRouter} from 'vue-router'
import {ref, watch} from "vue";
import {loadList, setEndpoint, setOrder} from "@/components/pagination/paginatedList.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import RolesSubPageWrapper from "@/views/dashboards/superUserDashboard/roles/RolesSubPageWrapper.vue";
import {
filterPermissionRecords,
getPermissionDelta,
groupPermissionRecords,
normalizePermissionCatalog,
pageAccessPermissionKeys,
permissionGroupDefinitions,
sanitizePermissionDomId,
} from "@/views/dashboards/superUserDashboard/roles/rolePermissionCatalog.js";
// Lists
const permissions = ref([]);
const permissionsList = ref([]);
const rolePermissions = ref([]);
const { t } = useI18n();
const route = useRoute();
const role = ref(null);
const departments = ref([]);
const permissionRecords = ref([]);
const selectedPermissions = ref(new Set());
const busyPermissions = ref(new Set());
const busyGroups = ref(new Set());
const isLoading = ref(true);
const loadError = ref("");
const mutationError = ref("");
const search = ref("");
const activeGroup = ref("access");
// Loaded state
const isLoaded = ref(false);
// Individually loaded states
const isDepartmentsLoaded = ref(false);
const isPermissionsLoaded = ref(false);
const isRolePermissionsLoaded = ref(false);
const isPermissionsListLoaded = ref(false);
const roleId = computed(() => Number.parseInt(String(route.params.roleId || ""), 10));
const roleTitle = computed(() =>
role.value?.name
? t("roles.permissions.title_with_name", { name: role.value.name })
: t("roles.permissions.title", { id: Number.isFinite(roleId.value) ? roleId.value : "" })
);
watch(() => departments.value, () => {
console.log('Departments loaded');
isDepartmentsLoaded.value = true;
});
const buildPermissionOverrides = (departmentList) => {
const overrides = {};
watch(() => permissionsList.value, () => {
console.log('Permissions list loaded');
isPermissionsListLoaded.value = true;
});
for (const permission of pageAccessPermissionKeys) {
overrides[permission] = {
label: t(`roles.permissions.page_access.${permission}.label`),
description: t(`roles.permissions.page_access.${permission}.description`),
groupKey: "access",
isVirtual: true,
};
}
watch(() => permissions.value, () => {
console.log('Permissions loaded');
isPermissionsLoaded.value = true;
});
watch(() => rolePermissions.value, () => {
console.log('Role permissions loaded');
isRolePermissionsLoaded.value = true;
});
// Get the department from the route
const router = useRouter()
const roleId = ref(parseInt(router.currentRoute.value.params.roleId));
// Set the endpoint
setEndpoint(SessionUser.objects.department_categories.meta.endpoint + `?id=${roleId.value}`);
// Set the order
setOrder("category_id", "asc");
// Load the list
loadList();
const loadPermissionsList = async () => {
const response = await SessionUser.objects.permissions.get.all().then((response) => {
const tmpPermissionsList = [];
const tmpPermissions = [];
for (const i in response) {
const endpoint = i;
const methods = response[i];
for (const method in methods) {
const permissions = methods[method];
for (const permission in permissions) {
tmpPermissionsList.push(permission);
tmpPermissions[endpoint] = tmpPermissions[endpoint] || {}; // Create the endpoint if it doesn't exist
tmpPermissions[endpoint][method] = tmpPermissions[endpoint][method] || {}; // Create the method if it doesn't exist
tmpPermissions[endpoint][method][permission] = permissions[permission];
}
}
for (const department of departmentList) {
const departmentId = Number.parseInt(String(department?.id ?? ""), 10);
if (!Number.isFinite(departmentId)) {
continue;
}
permissionsList.value = tmpPermissionsList;
permissions.value = tmpPermissions;
});
}
// Get all available permissions
loadPermissionsList();
const loadRolePermissions = async () => {
await SessionUser.objects.roles.get.single(roleId.value).then((response) => {
rolePermissions.value = response.permissions;
});
}
// Get the permissions for the role
loadRolePermissions();
const convertRouteToHumanReadable = (route) => {
let arrow = " -> ";
// Replace / with space
let humanReadable = route.replace(/\//g, arrow);
// Replace _ with space
humanReadable = humanReadable.replace(/_/g, " ");
// Capitalize the first letter
humanReadable = humanReadable.charAt(0).toUpperCase() + humanReadable.slice(1);
// If the route ends with an arrow, remove it
if (humanReadable.endsWith(arrow)) {
humanReadable = humanReadable.slice(0, -arrow.length);
const name = String(department?.name || departmentId);
overrides[`department_access_${departmentId}`] = {
label: name,
description: t("roles.permissions.department_access_description", { name }),
groupKey: "access",
isVirtual: true,
departmentId,
};
}
// If the route starts with an arrow, remove it
if (humanReadable.startsWith(arrow)) {
humanReadable = humanReadable.slice(arrow.length);
}
return humanReadable;
}
const endpointMethodColors = {
GET: "is-success",
POST: "is-info",
PUT: "is-warning",
PATCH: "is-warning",
DELETE: "is-danger",
OPTIONS: "is-light",
}
return overrides;
};
const hasPermission = (permission) => {
return rolePermissions.value.includes(permission);
}
const togglePermission = (permission) => {
if (hasPermission(permission)) {
SessionUser.objects.roles.functions.permissions.remove(roleId.value, permission).then((response) => {
// Remove the permission
rolePermissions.value = rolePermissions.value.filter((value) => value !== permission);
});
const replaceSetValue = (targetRef, value, shouldAdd) => {
const next = new Set(targetRef.value);
if (shouldAdd) {
next.add(value);
} else {
SessionUser.objects.roles.functions.permissions.add(roleId.value, permission).then((response) => {
// Add the permission
rolePermissions.value.push(permission);
});
next.delete(value);
}
}
targetRef.value = next;
};
const setPermissionBusy = (permission, isBusy) => {
replaceSetValue(busyPermissions, permission, isBusy);
};
const loadDepartments = async () => {
const response = await SessionUser.objects.departments.get.all().then((response) => {
departments.value = response;
const setGroupBusy = (groupKey, isBusy) => {
replaceSetValue(busyGroups, groupKey, isBusy);
};
const hasPermission = (permission) => selectedPermissions.value.has(permission);
const isPermissionBusy = (permission) => busyPermissions.value.has(permission);
const isGroupBusy = (groupKey) => busyGroups.value.has(groupKey);
const permissionDomId = (permission) => `role-permission-${sanitizePermissionDomId(permission)}`;
const permissionTestId = (permission) => `role-permission-row-${sanitizePermissionDomId(permission)}`;
const allRecordsByGroup = computed(() => groupPermissionRecords(permissionRecords.value));
const filteredPermissionRecords = computed(() => filterPermissionRecords(permissionRecords.value, search.value));
const filteredRecordsByGroup = computed(() => groupPermissionRecords(filteredPermissionRecords.value));
const groupTabs = computed(() =>
permissionGroupDefinitions
.map((group) => {
const allRecords = allRecordsByGroup.value.get(group.key) || [];
const filteredRecords = filteredRecordsByGroup.value.get(group.key) || [];
return {
...group,
label: t(`roles.permissions.groups.${group.key}.label`),
description: t(`roles.permissions.groups.${group.key}.description`),
totalCount: allRecords.length,
filteredCount: filteredRecords.length,
};
})
.filter((group) => group.totalCount > 0)
);
const totalCount = computed(() => permissionRecords.value.length);
const filteredCount = computed(() => filteredPermissionRecords.value.length);
const selectedCount = computed(() => selectedPermissions.value.size);
const getAllGroupRecords = (groupKey) => allRecordsByGroup.value.get(groupKey) || [];
const getFilteredGroupRecords = (groupKey) => filteredRecordsByGroup.value.get(groupKey) || [];
const isGroupFullySelected = (groupKey) => {
const records = getAllGroupRecords(groupKey);
return records.length > 0 && records.every((record) => selectedPermissions.value.has(record.permission));
};
const isGroupFullyCleared = (groupKey) => {
const records = getAllGroupRecords(groupKey);
return records.length > 0 && records.every((record) => !selectedPermissions.value.has(record.permission));
};
watch(
groupTabs,
(tabs) => {
if (tabs.length === 0) {
return;
}
if (!tabs.some((tab) => tab.key === activeGroup.value)) {
activeGroup.value = tabs[0].key;
}
},
{ immediate: true }
);
const loadRolePermissions = async () => {
ObjectsGlobal.clearCache();
const selectedRole = await SessionUser.objects.roles.get.single(roleId.value);
role.value = selectedRole || null;
selectedPermissions.value = new Set(Array.isArray(selectedRole?.permissions) ? selectedRole.permissions : []);
};
const loadPageData = async () => {
isLoading.value = true;
loadError.value = "";
mutationError.value = "";
try {
if (!Number.isFinite(roleId.value)) {
throw new Error("Invalid role id");
}
ObjectsGlobal.clearCache();
const [permissionCatalog, selectedRole, departmentList] = await Promise.all([
SessionUser.objects.permissions.get.all(),
SessionUser.objects.roles.get.single(roleId.value),
SessionUser.objects.departments.get.all(),
]);
const safeDepartments = Array.isArray(departmentList) ? departmentList : [];
role.value = selectedRole || null;
departments.value = safeDepartments;
selectedPermissions.value = new Set(Array.isArray(selectedRole?.permissions) ? selectedRole.permissions : []);
permissionRecords.value = normalizePermissionCatalog(permissionCatalog || {}, {
departments: safeDepartments,
permissionOverrides: buildPermissionOverrides(safeDepartments),
});
} catch (error) {
console.error("Failed to load role permissions page", error);
loadError.value = t("roles.permissions.errors.load");
} finally {
isLoading.value = false;
}
};
const mutatePermissions = async ({ add = [], remove = [], groupKey = null }) => {
const additions = add.filter((permission) => !selectedPermissions.value.has(permission) && !isPermissionBusy(permission));
const removals = remove.filter((permission) => selectedPermissions.value.has(permission) && !isPermissionBusy(permission));
const affectedPermissions = [...additions, ...removals];
if (affectedPermissions.length === 0) {
return;
}
mutationError.value = "";
if (groupKey) {
setGroupBusy(groupKey, true);
}
for (const permission of affectedPermissions) {
setPermissionBusy(permission, true);
}
const optimisticPermissions = new Set(selectedPermissions.value);
for (const permission of additions) {
optimisticPermissions.add(permission);
}
for (const permission of removals) {
optimisticPermissions.delete(permission);
}
selectedPermissions.value = optimisticPermissions;
try {
const operations = [
...additions.map((permission) =>
SessionUser.objects.roles.functions.permissions.add(roleId.value, permission).then(() => ({
permission,
action: "add",
}))
),
...removals.map((permission) =>
SessionUser.objects.roles.functions.permissions.remove(roleId.value, permission).then(() => ({
permission,
action: "remove",
}))
),
];
const results = await Promise.allSettled(operations);
const failedResult = results.find((result) => result.status === "rejected");
if (failedResult) {
throw failedResult.reason;
}
ObjectsGlobal.clearCache();
const nextPermissions = new Set(selectedPermissions.value);
for (const permission of additions) {
nextPermissions.add(permission);
}
for (const permission of removals) {
nextPermissions.delete(permission);
}
selectedPermissions.value = nextPermissions;
} catch (error) {
console.error("Failed to update role permissions", error);
mutationError.value = t("roles.permissions.errors.update");
await loadRolePermissions().catch((reloadError) => {
console.error("Failed to reload role permissions after mutation error", reloadError);
});
} finally {
for (const permission of affectedPermissions) {
setPermissionBusy(permission, false);
}
if (groupKey) {
setGroupBusy(groupKey, false);
}
}
};
const togglePermission = async (record) => {
if (!record?.permission || isPermissionBusy(record.permission)) {
return;
}
const shouldAdd = !selectedPermissions.value.has(record.permission);
await mutatePermissions({
add: shouldAdd ? [record.permission] : [],
remove: shouldAdd ? [] : [record.permission],
});
}
};
loadDepartments();
const applyGroupSelection = async (groupKey, shouldSelect) => {
if (isGroupBusy(groupKey)) {
return;
}
watch(() => isDepartmentsLoaded.value && isPermissionsListLoaded.value && isPermissionsLoaded.value && isRolePermissionsLoaded.value, () => {
console.log('Loaded:', 'Departments:', departments.value.length, 'Permissions:', permissionsList.value.length, 'Role permissions:', rolePermissions.value.length);
isLoaded.value = true;
});
const delta = getPermissionDelta(getAllGroupRecords(groupKey), selectedPermissions.value, shouldSelect);
await mutatePermissions({
...delta,
groupKey,
});
};
onMounted(loadPageData);
</script>
<template>
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessSuperUser()">
<RolesSubPageWrapper>
<template #title>
<PageTitle :title="`Role ${roleId}`" subtitle="Role permissions">
<PageTitle :title="roleTitle" :subtitle="t('roles.permissions.subtitle')">
<template #buttons>
<button class="button is-dark" type="button" :disabled="isLoading" @click="loadPageData">
<span class="icon">
<i class="fas fa-sync-alt"></i>
</span>
<span>{{ t("roles.permissions.actions.reload") }}</span>
</button>
</template>
</PageTitle>
</template>
<!-- Department access permissions -->
<div class="box" v-if="isLoaded">
<p class="is-pulled-right">
<span class="tag is-dark is-large">
Department access
</span>
</p>
<p class="mt-2"> Department dependent permissions applies to:</p>
<div class="columns is-multiline">
<div v-for="department in departments" :key="department.id" class="column is-3">
<span class="tag is-dark">
{{ department.name }}
</span>
<!-- Enable switch -->
<div class="field">
<input
:id="'enable_department_' + department.id + '_access'"
type="checkbox"
class="switch is-rounded"
@change="togglePermission('department_access_' + department.id)"
:checked="hasPermission('department_access_' + department.id)"
/>
<label :for="'enable_department_' + department.id + '_access'"></label>
</div>
</div>
<section class="role-permissions-page" data-testid="role-permissions-page">
<div v-if="loadError" class="notification is-danger is-light" data-testid="role-permissions-load-error">
<p>{{ loadError }}</p>
<button class="button is-danger is-light mt-3" type="button" @click="loadPageData">
{{ t("roles.permissions.actions.retry") }}
</button>
</div>
</div>
<!-- Page type permissions -->
<div class="box">
<p class="is-pulled-right">
<span class="tag is-dark is-large">
Page type access
</span>
</p>
<p class="mt-2"> Page type dependent permissions applies to:</p>
<div class="columns is-multiline">
<!-- User -->
<div class="column is-3">
<span class="tag is-dark">
User
</span>
<!-- Enable switch -->
<div class="field">
<input
id="enable_user"
type="checkbox"
class="switch is-rounded"
@change="togglePermission('user')"
:checked="hasPermission('user')"
/>
<label for="enable_user"></label>
</div>
</div>
<!-- Admin -->
<div class="column is-3">
<span class="tag is-dark">
Admin
</span>
<!-- Enable switch -->
<div class="field">
<input
id="enable_admin"
type="checkbox"
class="switch is-rounded"
@change="togglePermission('admin')"
:checked="hasPermission('admin')"
/>
<label for="enable_admin"></label>
</div>
</div>
<!-- Super user -->
<div class="column is-3">
<span class="tag is-dark">
Super user
</span>
<!-- Enable switch -->
<div class="field">
<input
id="enable_superuser"
type="checkbox"
class="switch is-rounded"
@change="togglePermission('superuser')"
:checked="hasPermission('superuser')"
/>
<label for="enable_superuser"></label>
</div>
<div v-else-if="isLoading" class="role-permissions-loading" data-testid="role-permissions-loading">
<progress class="progress is-small is-dark" max="100"></progress>
</div>
</div>
</div>
<!-- Permissions -->
<div v-for="([endpoint, permission]) of Object.entries(permissions).sort((a, b) => a[0].localeCompare(b[0]))" class="box" v-if="isLoaded">
<p class="is-pulled-right">
<span class="tag is-dark is-large">
{{ endpoint }}
</span>
</p>
<!-- Permissions -->
<div class="mt-2" v-for="(permission, method) in permission" :key="method">
<span class="tag mt-3" :class="endpointMethodColors[method]">
<strong class="has-text-black">{{ method }}</strong>
</span>
<strong>&nbsp;{{ endpoint}}</strong> {{ convertRouteToHumanReadable(endpoint) }}
<!--{{ permission }} -->
<p class="mt-1"> Permissions </p>
<div v-for="(permission, permission_node) in permission" :key="permission_node">
<span class="tag is-dark">
{{ permission_node }}
</span>
{{ permission }}
<!-- Enable switch -->
<div class="field">
<input
:id="'enable_' + permission_node"
type="checkbox"
class="switch is-rounded"
:checked="hasPermission(permission_node)"
@change="togglePermission(permission_node)"
/>
<label :for="'enable_' + permission_node"></label>
<template v-else>
<div v-if="mutationError" class="notification is-danger is-light" data-testid="role-permission-error">
{{ mutationError }}
</div>
<div class="role-permissions-toolbar">
<div class="field role-permissions-search">
<label class="label" for="role-permissions-search">
{{ t("roles.permissions.search_label") }}
</label>
<div class="control has-icons-left">
<input
id="role-permissions-search"
v-model="search"
class="input"
type="search"
:placeholder="t('roles.permissions.search_placeholder')"
data-testid="role-permissions-search"
/>
<span class="icon is-left">
<i class="fas fa-search"></i>
</span>
</div>
</div>
<div class="role-permissions-summary" aria-live="polite">
<span class="tag is-dark is-light" data-testid="role-permissions-selected-count">
{{ t("roles.permissions.selected_summary", { selected: selectedCount, total: totalCount }) }}
</span>
<span v-if="search" class="tag is-info is-light" data-testid="role-permissions-filtered-count">
{{ t("roles.permissions.filtered_summary", { count: filteredCount }) }}
</span>
</div>
</div>
</div>
<!--<pre>{{ permission }}</pre> -->
</div>
<b-tabs
v-if="groupTabs.length"
v-model="activeGroup"
expanded
type="is-boxed"
data-testid="role-permissions-tabs"
>
<b-tab-item
v-for="group in groupTabs"
:key="group.key"
:value="group.key"
:label="`${group.label} (${group.filteredCount})`"
:icon="group.icon"
icon-pack="fas"
>
<section class="role-permission-group" :data-testid="`role-permissions-group-${group.key}`">
<div class="role-permission-group-header">
<div>
<h2 class="title is-5">{{ group.label }}</h2>
<p class="role-permission-group-description">{{ group.description }}</p>
</div>
<div class="buttons role-permission-group-actions">
<button
class="button is-dark is-small"
type="button"
:disabled="isGroupBusy(group.key) || isGroupFullySelected(group.key)"
:data-testid="`role-permissions-select-all-${group.key}`"
@click="applyGroupSelection(group.key, true)"
>
<span class="icon">
<i class="fas fa-check-square"></i>
</span>
<span>{{ t("roles.permissions.actions.select_all") }}</span>
</button>
<button
class="button is-light is-small"
type="button"
:disabled="isGroupBusy(group.key) || isGroupFullyCleared(group.key)"
:data-testid="`role-permissions-clear-all-${group.key}`"
@click="applyGroupSelection(group.key, false)"
>
<span class="icon">
<i class="fas fa-minus-square"></i>
</span>
<span>{{ t("roles.permissions.actions.clear_all") }}</span>
</button>
</div>
</div>
<div
v-if="getFilteredGroupRecords(group.key).length === 0"
class="notification is-light"
:data-testid="`role-permissions-empty-${group.key}`"
>
{{ search ? t("roles.permissions.no_search_results") : t("roles.permissions.empty_group") }}
</div>
<div v-else class="table-container">
<table class="table is-fullwidth is-hoverable role-permissions-table">
<thead>
<tr>
<th>{{ t("roles.permissions.columns.enabled") }}</th>
<th>{{ t("roles.permissions.columns.permission") }}</th>
<th>{{ t("roles.permissions.columns.route") }}</th>
<th>{{ t("roles.permissions.columns.status") }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="record in getFilteredGroupRecords(group.key)"
:key="record.permission"
:class="{ 'role-permission-selected': hasPermission(record.permission) }"
:data-testid="permissionTestId(record.permission)"
>
<td class="role-permission-toggle-cell">
<div class="field">
<input
:id="permissionDomId(record.permission)"
class="switch is-rounded"
type="checkbox"
:checked="hasPermission(record.permission)"
:disabled="isPermissionBusy(record.permission)"
:data-testid="`role-permission-toggle-${sanitizePermissionDomId(record.permission)}`"
@change="togglePermission(record)"
/>
<label :for="permissionDomId(record.permission)"></label>
</div>
</td>
<td>
<strong>{{ record.label }}</strong>
<p class="role-permission-description">
{{ record.description || t("roles.permissions.no_description") }}
</p>
<code>{{ record.permission }}</code>
</td>
<td>
<div class="role-permission-route">
<span>{{ record.endpointLabel || t("roles.permissions.access_scope") }}</span>
<span v-if="record.method" class="tag is-small" :class="record.methodColor">
{{ record.method }}
</span>
</div>
<code v-if="record.endpoint">{{ record.endpoint }}</code>
</td>
<td>
<span
class="tag"
:class="hasPermission(record.permission) ? 'is-success is-light' : 'is-light'"
>
{{
hasPermission(record.permission)
? t("roles.permissions.status.enabled")
: t("roles.permissions.status.disabled")
}}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</b-tab-item>
</b-tabs>
<div v-else class="notification is-light" data-testid="role-permissions-empty">
{{ t("roles.permissions.empty") }}
</div>
</template>
</section>
</RolesSubPageWrapper>
</RestrictedPageWrapper>
</template>
<style scoped>
.role-permissions-page {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>
.role-permissions-loading {
padding: 1rem 0;
}
.role-permissions-toolbar {
align-items: flex-end;
display: flex;
gap: 1rem;
justify-content: space-between;
}
.role-permissions-search {
flex: 1 1 28rem;
max-width: 42rem;
}
.role-permissions-summary {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: flex-end;
padding-bottom: 0.25rem;
}
.role-permission-group {
padding-top: 1rem;
}
.role-permission-group-header {
align-items: flex-start;
display: flex;
gap: 1rem;
justify-content: space-between;
margin-bottom: 1rem;
}
.role-permission-group-description,
.role-permission-description {
color: #5f6672;
}
.role-permission-group-actions {
flex: 0 0 auto;
justify-content: flex-end;
}
.role-permissions-table th:first-child,
.role-permission-toggle-cell {
width: 6rem;
}
.role-permission-route {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.role-permission-selected {
background: #f2fbf5;
}
@media screen and (max-width: 768px) {
.role-permissions-toolbar,
.role-permission-group-header {
align-items: stretch;
flex-direction: column;
}
.role-permissions-search {
max-width: none;
}
.role-permissions-summary,
.role-permission-group-actions {
justify-content: flex-start;
}
}
</style>
@@ -0,0 +1,297 @@
export const endpointMethodColors = {
GET: "is-success",
POST: "is-info",
PUT: "is-warning",
PATCH: "is-warning",
DELETE: "is-danger",
OPTIONS: "is-light",
};
export const pageAccessPermissionKeys = ["user", "admin", "superuser"];
export const permissionGroupDefinitions = [
{
key: "access",
icon: "unlock",
patterns: [/^(user|admin|superuser)$/u, /^department_access_\d+$/u],
},
{
key: "orders_pos",
icon: "cash-register",
patterns: [/order/iu, /pos/iu, /stripe/iu, /payment/iu, /xlvask/iu],
},
{
key: "bookings",
icon: "calendar-alt",
patterns: [/booking/iu, /time[-_]?booking/iu],
},
{
key: "customers_users",
icon: "users",
patterns: [/customer/iu, /user/iu, /subuser/iu, /employee/iu, /auth/iu, /role/iu, /permission/iu],
},
{
key: "departments_operations",
icon: "building",
patterns: [/department/iu, /lane/iu, /relay/iu, /daily[-_]?report/iu, /complaint/iu, /goal/iu, /notification/iu],
},
{
key: "products_pricing",
icon: "tags",
patterns: [/product/iu, /price/iu, /pricing/iu, /discount/iu, /currency/iu],
},
{
key: "invoicing_economic",
icon: "file-invoice",
patterns: [/invoice/iu, /economic/iu, /billing/iu, /collection/iu],
},
{
key: "selfserve_edge",
icon: "network-wired",
patterns: [/self[-_]?serve/iu, /selfserve/iu, /edge/iu, /gateway/iu, /scanner/iu, /wash/iu, /shelly/iu],
},
{
key: "system_config",
icon: "cogs",
patterns: [/system/iu, /config/iu, /configuration/iu, /replication/iu, /release/iu, /coolify/iu, /module/iu, /backup/iu, /workfeed/iu, /error[-_]?report/iu],
},
{
key: "other",
icon: "key",
patterns: [],
},
];
const permissionGroupByKey = new Map(permissionGroupDefinitions.map((group) => [group.key, group]));
const permissionGroupMatchPriority = [
"access",
"orders_pos",
"bookings",
"departments_operations",
"products_pricing",
"invoicing_economic",
"selfserve_edge",
"system_config",
"customers_users",
];
const permissionGroupsForMatching = permissionGroupDefinitions
.filter((group) => group.key !== "other")
.sort(
(left, right) =>
permissionGroupMatchPriority.indexOf(left.key) - permissionGroupMatchPriority.indexOf(right.key)
);
const normalizeText = (value) =>
String(value ?? "")
.toLowerCase()
.normalize("NFKD")
.replace(/[\u0300-\u036f]/gu, "")
.trim();
export const sanitizePermissionDomId = (value) =>
String(value ?? "")
.replace(/[^a-zA-Z0-9_-]+/g, "-")
.replace(/^-+|-+$/g, "")
.toLowerCase() || "permission";
const splitHumanWords = (value) =>
String(value ?? "")
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/[_./:-]+/g, " ")
.replace(/\s+/g, " ")
.trim();
export const humanizePermissionKey = (permission) => {
const words = splitHumanWords(permission);
if (!words) {
return "";
}
return words.charAt(0).toUpperCase() + words.slice(1);
};
export const humanizeEndpoint = (endpoint) => {
const words = splitHumanWords(String(endpoint ?? "").replace(/^\/+|\/+$/g, ""));
if (!words) {
return "";
}
return words.charAt(0).toUpperCase() + words.slice(1);
};
export const getPermissionGroupKey = ({ permission = "", endpoint = "", description = "" } = {}) => {
const haystack = `${permission} ${endpoint} ${description}`;
const match = permissionGroupsForMatching.find((group) =>
group.patterns.some((pattern) => pattern.test(haystack))
);
return match?.key || "other";
};
const buildSearchText = (record) =>
normalizeText(
[
record.permission,
record.label,
record.description,
record.endpoint,
record.endpointLabel,
record.method,
record.groupKey,
].join(" ")
);
const createPermissionRecord = ({
permission,
description = "",
endpoint = "",
method = "",
permissionOverrides = {},
}) => {
const key = String(permission || "").trim();
const override = permissionOverrides[key] || {};
const record = {
id: key,
permission: key,
label: override.label || humanizePermissionKey(key),
description: override.description || String(description || ""),
endpoint: String(endpoint || ""),
endpointLabel: humanizeEndpoint(endpoint),
method: String(method || ""),
methodColor: endpointMethodColors[String(method || "").toUpperCase()] || "is-light",
groupKey: override.groupKey || getPermissionGroupKey({ permission: key, endpoint, description }),
isVirtual: Boolean(override.isVirtual),
departmentId: override.departmentId || null,
};
record.searchText = buildSearchText(record);
return record;
};
const createAccessPermissionRecords = ({ departments = [], permissionOverrides = {} }) => {
const pageRecords = pageAccessPermissionKeys.map((permission) =>
createPermissionRecord({
permission,
permissionOverrides: {
...permissionOverrides,
[permission]: {
...(permissionOverrides[permission] || {}),
groupKey: "access",
isVirtual: true,
},
},
})
);
const departmentRecords = departments.map((department) => {
const departmentId = Number.parseInt(String(department?.id ?? ""), 10);
const permission = `department_access_${departmentId}`;
const override = permissionOverrides[permission] || {};
return createPermissionRecord({
permission,
permissionOverrides: {
...permissionOverrides,
[permission]: {
label: override.label || String(department?.name || permission),
description: override.description || "",
groupKey: "access",
isVirtual: true,
departmentId,
},
},
});
});
return [...pageRecords, ...departmentRecords].filter((record) => record.permission && record.permission !== "department_access_NaN");
};
export const normalizePermissionCatalog = (catalog = {}, options = {}) => {
const permissionOverrides = options.permissionOverrides || {};
const recordsByPermission = new Map();
for (const record of createAccessPermissionRecords({
departments: options.departments || [],
permissionOverrides,
})) {
recordsByPermission.set(record.permission, record);
}
for (const [endpoint, methods] of Object.entries(catalog || {})) {
if (!methods || typeof methods !== "object") {
continue;
}
for (const [method, permissions] of Object.entries(methods)) {
if (!permissions || typeof permissions !== "object") {
continue;
}
for (const [permission, description] of Object.entries(permissions)) {
const key = String(permission || "").trim();
if (!key || recordsByPermission.has(key)) {
continue;
}
recordsByPermission.set(
key,
createPermissionRecord({
permission: key,
description,
endpoint,
method,
permissionOverrides,
})
);
}
}
}
return [...recordsByPermission.values()].sort((left, right) => {
const groupCompare =
permissionGroupDefinitions.findIndex((group) => group.key === left.groupKey) -
permissionGroupDefinitions.findIndex((group) => group.key === right.groupKey);
if (groupCompare !== 0) {
return groupCompare;
}
return left.label.localeCompare(right.label, undefined, { sensitivity: "base" });
});
};
export const filterPermissionRecords = (records = [], search = "") => {
const normalizedSearch = normalizeText(search);
if (!normalizedSearch) {
return [...records];
}
return records.filter((record) => record.searchText.includes(normalizedSearch));
};
export const groupPermissionRecords = (records = []) => {
const groups = new Map(permissionGroupDefinitions.map((group) => [group.key, []]));
for (const record of records) {
const groupKey = permissionGroupByKey.has(record.groupKey) ? record.groupKey : "other";
groups.get(groupKey).push(record);
}
return groups;
};
export const getPermissionDelta = (records = [], selectedPermissions = [], shouldSelect = true) => {
const selectedSet = selectedPermissions instanceof Set ? selectedPermissions : new Set(selectedPermissions);
const uniquePermissions = [...new Set(records.map((record) => record.permission).filter(Boolean))];
if (shouldSelect) {
return {
add: uniquePermissions.filter((permission) => !selectedSet.has(permission)),
remove: [],
};
}
return {
add: [],
remove: uniquePermissions.filter((permission) => selectedSet.has(permission)),
};
};
@@ -13,6 +13,7 @@ import StatisticsDepartmentGoal
from "@/views/dashboards/superUserDashboard/statistics/displays/overview/StatisticsDepartmentGoal.vue";
import {departments, getDepartments} from "@/components/pagination/departmentTabs.vue";
import { useI18n } from 'vue-i18n';
import { todayLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n();
@@ -70,7 +71,8 @@ getDepartments()
// Get the statistics
const getStatistics = async () => {
// Get the statistics
const response = await authenticatedRequest('/statistics/income/departments?start_date=' + (new Date()).toISOString().split('T')[0] + '&end_date=' + (new Date()).toISOString().split('T')[0])
const today = todayLocalDateOnly();
const response = await authenticatedRequest('/statistics/income/departments?start_date=' + today + '&end_date=' + today)
// Get the sent data
statistics_department_today.value = response.data.data
// Log the data
@@ -131,4 +133,4 @@ getStatistics()
<style scoped>
</style>
</style>
@@ -4,6 +4,7 @@ import {ref} from 'vue';
import { Bar } from 'vue-chartjs';
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale } from 'chart.js'
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { todayLocalDateOnly } from "@/services/dateOnly.js";
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
@@ -16,7 +17,7 @@ const chartOptions = {
// Shortcut to statistics
// Set the time for the statistics to today
SessionUser.adminUser.statistics.set_time((new Date()).toISOString().split('T')[0], (new Date()).toISOString().split('T')[0])
SessionUser.adminUser.statistics.set_time(todayLocalDateOnly(), todayLocalDateOnly())
@@ -33,4 +34,4 @@ SessionUser.adminUser.statistics.set_time((new Date()).toISOString().split('T')[
}"
/>
</div>
</template>
</template>
@@ -1340,6 +1340,10 @@ const clearRecentCompletedRefreshInterval = () => {
};
const scheduleRecentCompletedWashRefresh = (attempt = 0) => {
if (isMyWashStartUnmounted.value) {
return;
}
clearRecentCompletedRefreshTimeout();
if (refreshRecentlyCompletedWashState() || attempt >= 20 || isMyWashStartUnmounted.value) {
@@ -1347,6 +1351,10 @@ const scheduleRecentCompletedWashRefresh = (attempt = 0) => {
}
recentCompletedRefreshTimeout.value = window.setTimeout(() => {
if (isMyWashStartUnmounted.value) {
return;
}
scheduleRecentCompletedWashRefresh(attempt + 1);
}, 250);
};
@@ -0,0 +1,165 @@
import { expect, Page, test } from "@playwright/test";
import { apiPathPattern, mockApi, seedAuthenticatedState } from "./support/network.js";
type NotificationSmsRow = {
id: number;
department: number;
label: string;
phone_country_code: number;
phone: number;
enabled: boolean;
created_at: string;
};
const json = (body: unknown, status = 200) => ({
status,
contentType: "application/json",
body: JSON.stringify(body),
});
const setupNotificationSmsApi = async (page: Page) => {
let listRequestCount = 0;
const mutations: Array<{ method: string; body?: Record<string, unknown>; id?: number }> = [];
let rows: NotificationSmsRow[] = [
{
id: 501,
department: 1,
label: "Dispatch line",
phone_country_code: 45,
phone: 12345678,
enabled: true,
created_at: "2026-07-06 12:00:00",
},
];
await page.route(apiPathPattern("/department/notification/sms"), async (route) => {
const request = route.request();
const method = request.method();
if (method === "GET") {
listRequestCount += 1;
await route.fulfill(
json({
data: rows,
meta: {
pagination: {
page: 1,
per_page: 100,
total: rows.length,
},
},
})
);
return;
}
if (method === "PUT") {
const body = request.postDataJSON() as Record<string, unknown>;
mutations.push({ method, body });
rows = rows.map((row) =>
Number(row.id) === Number(body.id)
? {
...row,
enabled: body.enabled === true,
}
: row
);
await route.fulfill(json({ data: { message: "Department notification SMS updated" } }));
return;
}
if (method === "DELETE") {
const id = Number(new URL(request.url()).searchParams.get("id") || 0);
mutations.push({ method, id });
rows = rows.filter((row) => Number(row.id) !== id);
await route.fulfill(json({ data: { message: "Department notification SMS deleted" } }));
return;
}
await route.fulfill(json({ message: "Unsupported notification SMS method" }, 405));
});
return {
mutations,
get listRequestCount() {
return listRequestCount;
},
};
};
test("@smoke department notification SMS active toggle and delete refresh the table", async ({ page }) => {
const notificationErrors: Array<{ status: number; url: string }> = [];
page.on("response", (response) => {
if (response.url().includes("/department/notification/sms") && response.status() >= 400) {
notificationErrors.push({
status: response.status(),
url: response.url(),
});
}
});
await seedAuthenticatedState(page, "admin-department-notifications-token");
await mockApi(page, {
authenticated: true,
permissions: [
"admin",
"department_access_1",
"department_notification_sms_get",
"department_notification_sms_update",
"department_notification_sms_delete",
"department_notification_sms_add",
],
});
const notificationSmsApi = await setupNotificationSmsApi(page);
await page.goto("/admin/1/modules/notifications", { waitUntil: "domcontentloaded" });
const row = page.locator("tr", { hasText: "Dispatch line" });
await expect(row).toBeVisible();
const enabledToggle = page.getByTestId("department-notification-sms-enabled-toggle-501");
await expect(enabledToggle).toBeChecked();
const listRequestsBeforeToggle = notificationSmsApi.listRequestCount;
const updateRequestPromise = page.waitForRequest(
(request) => request.method() === "PUT" && request.url().includes("/department/notification/sms")
);
await enabledToggle.uncheck({ force: true });
const updateRequest = await updateRequestPromise;
expect(updateRequest.postDataJSON()).toMatchObject({
id: 501,
enabled: false,
});
await expect.poll(() => notificationSmsApi.listRequestCount).toBeGreaterThan(listRequestsBeforeToggle);
await expect(enabledToggle).not.toBeChecked();
const listRequestsBeforeDelete = notificationSmsApi.listRequestCount;
await row.locator('button[aria-haspopup="true"]').click();
const deleteRequestPromise = page.waitForRequest(
(request) => request.method() === "DELETE" && request.url().includes("/department/notification/sms")
);
await page.getByTestId("department-notification-sms-delete-501").click();
await expect(page.locator(".swal2-popup")).toBeVisible();
await page.locator(".swal2-confirm").click();
const deleteRequest = await deleteRequestPromise;
expect(new URL(deleteRequest.url()).searchParams.get("id")).toBe("501");
await expect.poll(() => notificationSmsApi.listRequestCount).toBeGreaterThan(listRequestsBeforeDelete);
await expect(page.locator("tr", { hasText: "Dispatch line" })).toHaveCount(0);
expect(notificationErrors).toEqual([]);
expect(notificationSmsApi.mutations).toEqual([
{
method: "PUT",
body: {
id: 501,
enabled: false,
},
},
{
method: "DELETE",
id: 501,
},
]);
});
+152 -4
View File
@@ -15,16 +15,20 @@ function isApiUrl(url: string) {
);
}
async function mockHtml2Canvas(page) {
await page.route(/\/node_modules\/html2canvas\/dist\/html2canvas\.esm\.js(?:\?.*)?$/i, async (route) => {
const html2CanvasModulePattern = /\/(?:node_modules\/.*)?html2canvas(?:\.[\w-]+)?\.js(?:\?.*)?$/i;
async function mockHtml2Canvas(page, { alwaysFail = false } = {}) {
await page.unroute(html2CanvasModulePattern).catch(() => {});
await page.route(html2CanvasModulePattern, async (route) => {
await route.fulfill({
contentType: "text/javascript",
body: `
const alwaysFail = ${alwaysFail ? "true" : "false"};
let html2canvasAttempts = 0;
export default async function html2canvas() {
html2canvasAttempts += 1;
if (html2canvasAttempts === 1) {
throw new Error("Simulated first capture failure");
if (alwaysFail || html2canvasAttempts === 1) {
throw new Error(alwaysFail ? "Simulated permanent capture failure" : "Simulated first capture failure");
}
const canvas = document.createElement("canvas");
canvas.width = 8;
@@ -163,10 +167,68 @@ test.describe("Authenticated error reports", () => {
expect(Array.isArray(body.vue_errors)).toBe(true);
expect(body.context).toMatchObject({
data_collection_policy_version: "error-report-v1",
screenshot_attachment: {
status: "stored",
attached: true,
},
});
await expect(page.getByTestId("error-report-submitted")).toBeVisible();
});
test("submits without a screenshot when capture never succeeds", async ({ page }) => {
await mockHtml2Canvas(page, { alwaysFail: true });
const submittedBodies: Array<Record<string, unknown>> = [];
await page.route(/\/error-reports(?:\?.*)?$/i, async (route) => {
if (!isApiUrl(route.request().url())) {
await route.fallback();
return;
}
if (route.request().method().toUpperCase() !== "POST") {
await route.fallback();
return;
}
const body = route.request().postDataJSON() as Record<string, unknown>;
submittedBodies.push(body);
await route.fulfill(
json(
{
success: true,
data: {
id: 100,
status: "open",
screenshot: null,
},
},
201
)
);
});
await primeMockSession(page, { token: "error-report-user-token", bootPath: "/admin" });
await openErrorReportLauncher(page);
await page.getByLabel("What were you doing before the error occurred?").fill("Opening the orders page");
await page.getByLabel("What did you expect would happen?").fill("The orders should load");
await page.getByLabel("What actually happened?").fill("The page showed an error");
await page.getByTestId("error-report-consent").check();
await page.getByTestId("error-report-submit").click();
await expect.poll(() => submittedBodies.length).toBe(1);
const body = submittedBodies[0];
expect(body.screenshot).toBeNull();
expect(body.context).toMatchObject({
screenshot_attachment: {
status: "capture_failed",
attached: false,
},
});
await expect(page.getByTestId("error-report-capture-warning")).toContainText(
"The screen capture failed. The report will be sent without a screenshot."
);
await expect(page.getByTestId("error-report-submitted")).toBeVisible();
});
test("lets superusers inspect and resolve submitted reports", async ({ page }) => {
const report: Record<string, any> = {
id: 12,
@@ -277,4 +339,90 @@ test.describe("Authenticated error reports", () => {
]);
await expect(page.getByTestId("error-report-detail")).toContainText("resolved");
});
test("lets superusers inspect submitted reports without screenshots", async ({ page }) => {
const report: Record<string, any> = {
id: 13,
status: "open",
reporter: {
type: "user",
user_id: 7,
customer_number: 12345,
name: "Error Reporter",
email: "reporter@example.test",
},
route_path: "/user/orders",
page_url: "https://app.example.test/user/orders",
release_trace_id: "trace-no-screenshot",
frontend_version: "front-1",
api_version: "api-1",
screenshot: null,
answers: {
before_error: "Opening the orders page",
expected: "Orders should load",
actual: "The table stayed empty",
},
request_error_count: 0,
vue_error_count: 0,
request_errors: [],
vue_errors: [],
runtime_context: {
screenshot_attachment: {
status: "capture_failed",
attached: false,
mime_type: null,
size_bytes: 0,
},
},
created_at: "2026-05-19 08:00:00",
updated_at: "2026-05-19 08:00:00",
resolved_at: null,
resolved_by_user_id: null,
resolution_note: null,
};
await page.route(/\/superuser\/error-reports(?:\?.*)?$/i, async (route) => {
if (!isApiUrl(route.request().url())) {
await route.fallback();
return;
}
if (route.request().method().toUpperCase() !== "GET") {
await route.fallback();
return;
}
await route.fulfill(
json({
success: true,
data: {
items: [report],
counts: {
open: 1,
resolved: 0,
all: 1,
},
limit: 100,
offset: 0,
},
})
);
});
await page.route(/\/superuser\/error-reports\/13$/i, async (route) => {
if (!isApiUrl(route.request().url())) {
await route.fallback();
return;
}
await route.fulfill(json({ success: true, data: report }));
});
await primeMockSession(page, { token: "error-report-superuser-token", bootPath: "/superuser/error-reports" });
await expect(page.getByTestId("error-reports-page")).toBeVisible();
await expect(page.getByTestId("error-report-list")).toContainText("/user/orders");
await page.getByTestId("error-report-view").click();
const detail = page.getByTestId("error-report-detail");
await expect(detail).toContainText("Opening the orders page");
await expect(detail).toContainText("trace-no-screenshot");
await expect(detail.locator("img")).toHaveCount(0);
});
});
+170 -5
View File
@@ -82,12 +82,128 @@ const pricePayload = {
],
};
const rolePermissionGroups = {
viewer: [{ key: "account", capabilities: ["sign_in", "view_own_permissions"] }],
cashier: [
{ key: "account", capabilities: ["sign_in", "view_own_permissions"] },
{
key: "orders",
capabilities: [
"view_orders",
"create_orders",
"edit_orders",
"view_order_items",
"create_order_items",
"update_order_lines",
"remove_order_lines",
"charge_orders",
],
},
],
booking_coordinator: [
{ key: "account", capabilities: ["sign_in", "view_own_permissions"] },
{ key: "orders", capabilities: ["view_orders"] },
{
key: "bookings",
capabilities: [
"view_department_bookings",
"view_own_bookings",
"update_bookings",
"create_bookings",
"mark_bookings_complete",
"send_booking_confirmations",
],
},
{
key: "time_bookings",
capabilities: ["view_time_booking_entries", "create_time_booking_entries", "edit_time_booking_entries"],
},
],
operations_lead: [
{ key: "account", capabilities: ["sign_in", "view_own_permissions"] },
{
key: "orders",
capabilities: [
"view_orders",
"create_orders",
"edit_orders",
"delete_orders",
"view_order_items",
"create_order_items",
"update_order_lines",
"remove_order_lines",
"charge_orders",
],
},
{
key: "bookings",
capabilities: [
"view_department_bookings",
"view_own_bookings",
"update_bookings",
"create_bookings",
"mark_bookings_complete",
"send_booking_confirmations",
],
},
{ key: "reports", capabilities: ["view_order_statistics", "view_booking_statistics"] },
],
department_admin: [
{ key: "account", capabilities: ["sign_in", "view_own_permissions"] },
{
key: "orders",
capabilities: [
"view_orders",
"create_orders",
"edit_orders",
"delete_orders",
"view_order_items",
"create_order_items",
"update_order_lines",
"remove_order_lines",
"charge_orders",
],
},
{
key: "bookings",
capabilities: [
"view_department_bookings",
"view_own_bookings",
"update_bookings",
"create_bookings",
"mark_bookings_complete",
"send_booking_confirmations",
],
},
{ key: "reports", capabilities: ["view_order_statistics", "view_booking_statistics"] },
{
key: "limited_backoffice",
capabilities: ["open_limited_backoffice", "manage_department_prices", "manage_employee_access"],
},
],
};
const rolesPayload = [
{ key: "viewer", label: "Viewer", description: "Can view." },
{ key: "cashier", label: "Cashier", description: "Can sell." },
{ key: "booking_coordinator", label: "Booking coordinator", description: "Can coordinate." },
{ key: "operations_lead", label: "Operations lead", description: "Can coordinate operations." },
{ key: "department_admin", label: "Department admin", description: "Can administer." },
{ key: "viewer", label: "Viewer", description: "Can view.", permission_groups: rolePermissionGroups.viewer },
{ key: "cashier", label: "Cashier", description: "Can sell.", permission_groups: rolePermissionGroups.cashier },
{
key: "booking_coordinator",
label: "Booking coordinator",
description: "Can coordinate.",
permission_groups: rolePermissionGroups.booking_coordinator,
},
{
key: "operations_lead",
label: "Operations lead",
description: "Can coordinate operations.",
permission_groups: rolePermissionGroups.operations_lead,
},
{
key: "department_admin",
label: "Department admin",
description: "Can administer.",
permission_groups: rolePermissionGroups.department_admin,
},
{ key: "superuser", label: "Superuser", description: "Must not render." },
];
@@ -271,6 +387,55 @@ test.describe("Limited backoffice", () => {
expect(api.forbiddenCalls).toEqual([]);
});
test("shows grouped human-readable role permission help without raw permission keys", async ({ page }, testInfo) => {
await seedLimitedBackofficeSession(page);
await mockLimitedBackofficeApi(page);
await page.goto("/backoffice/employees");
const helpButton = page.getByTestId("limited-employee-role-help");
await expect(helpButton).toBeVisible();
await expect(helpButton).toHaveAttribute("title", "View role permissions");
if (isDesktopProject(testInfo)) {
await helpButton.hover();
await expect(page.locator(".tooltip-content").filter({ hasText: "View role permissions" })).toBeVisible();
}
await helpButton.click();
const modal = page.getByTestId("limited-role-permissions-modal");
await expect(modal).toBeVisible();
await expect(modal).toContainText("Role permissions");
await expect(modal).toContainText("Every capability is limited to the departments selected for the employee.");
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Cashier");
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Orders");
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("View orders");
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Create order lines");
await expect(page.getByTestId("limited-role-permissions-role-viewer")).toContainText("Selected");
for (const rawPermission of [
"list_orders",
"add_order",
"edit_order_items",
"delete_order_items",
"department_timebookings_entries_get",
"limited_backoffice_employees_manage",
"department_access_1",
"raw_permissions",
]) {
await expect(page.locator("body")).not.toContainText(rawPermission);
}
await page.getByTestId("limited-role-permissions-close-footer").click();
await expect(page.getByTestId("limited-role-permissions-modal")).toHaveCount(0);
await helpButton.click();
await expect(page.getByTestId("limited-role-permissions-modal")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByTestId("limited-role-permissions-modal")).toHaveCount(0);
});
test("does not render data for a department outside the manager scope", async ({ page }, testInfo) => {
test.skip(!isDesktopProject(testInfo), "Desktop only");
@@ -50,6 +50,8 @@ function createCardOrderFixture(overrides = {}) {
customer_id: CARD_CUSTOMER_ID,
department_id: DEPARTMENT_ID,
reference: "CARD-REF-54518",
po: "",
safety_seal: "",
notes: "Paid at terminal",
reg_1: "ZZ99999",
reg_2: "",
@@ -236,6 +238,61 @@ test.describe("POS desktop card payments", () => {
await expect(stepThree.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
});
test("step 2 customer wishes stay visible after moving to card payment step 3", async ({ page }) => {
const stepTwoReference = "CARD-STEP-2-REF";
const stepTwoPo = "PO-CARD-STEP-2";
const fixture = createCardOrderFixture({
ordersById: {
[ORDER_ID]: {
...createCardOrderFixture().ordersById[ORDER_ID],
reference: "",
po: "",
},
},
});
await mockApi(page, {
authenticated: true,
permissions: DESKTOP_POS_PERMISSIONS,
edgeGateways: false,
pos: fixture,
});
await primeSession(page, `desktop-card-step2-${Date.now()}`);
await page.goto(`/admin/${DEPARTMENT_ID}/modules/pos?id=${ORDER_ID}&customer_id=${CARD_CUSTOMER_ID}&step=2`);
const stepTwo = page.getByTestId("pos-step-2");
await expect(stepTwo).toBeVisible({ timeout: 10_000 });
await expect(stepTwo.getByTestId("pos-order-metadata-grid")).toBeVisible({ timeout: 10_000 });
await stepTwo.getByTestId("pos-order-customer-wishes-reference").click();
await stepTwo.getByTestId("pos-order-customer-wishes-reference-input").fill(stepTwoReference);
await stepTwo.getByTestId("pos-order-customer-wishes-po").click();
await stepTwo.getByTestId("pos-order-customer-wishes-po-input").fill(stepTwoPo);
await stepTwo.getByTestId("pos-next-step").click();
const stepThree = page.getByTestId("pos-step-3");
await expect(stepThree).toBeVisible({ timeout: 10_000 });
await expect(stepThree.getByTestId("pos-order-customer-wishes-reference")).toContainText(stepTwoReference, {
timeout: 10_000,
});
await expect(stepThree.getByTestId("pos-order-customer-wishes-po")).toContainText(stepTwoPo, {
timeout: 10_000,
});
await expect.poll(() => fixture.ordersById[ORDER_ID].reference, { timeout: 10_000 }).toBe(stepTwoReference);
await expect.poll(() => fixture.ordersById[ORDER_ID].po, { timeout: 10_000 }).toBe(stepTwoPo);
await page.reload();
const reloadedStepThree = page.getByTestId("pos-step-3");
await expect(reloadedStepThree).toBeVisible({ timeout: 10_000 });
await expect(reloadedStepThree.getByTestId("pos-order-customer-wishes-reference")).toContainText(stepTwoReference, {
timeout: 10_000,
});
await expect(reloadedStepThree.getByTestId("pos-order-customer-wishes-po")).toContainText(stepTwoPo, {
timeout: 10_000,
});
});
test("idle state renders grouped terminal statuses and only keeps ready terminals selectable", async ({ page }) => {
const fixture = createCardOrderFixture({
readers: [
+60
View File
@@ -3272,6 +3272,66 @@ test.describe("POS mobile order flow", () => {
expect(createdProductIds).toEqual([53, 71, 91]);
});
test("manual step 2 blocks additional items when customer restricts additional services", async ({ page }) => {
const orderId = 9414;
const fixture = createMobilePosFixture({
customerAttributesByNumber: {
[REGULAR_CUSTOMER_ID]: [
{
id: 941401,
customer_number: REGULAR_CUSTOMER_ID,
attribute: "restrictAdditionalServices",
},
],
},
ordersById: {
[orderId]: buildRegularOrder(orderId, {
reference: "STEP2-RESTRICT-ADDITIONAL",
reg_1: "ZZ00000",
}),
},
orderItemsByOrderId: {
[orderId]: [],
},
});
await setupMobilePosPage(page, fixture, {
token: "mobile-step2-restrict-additional-token",
seedState: {
customerId: REGULAR_CUSTOMER_ID,
reg: "ZZ00000",
reference: "STEP2-RESTRICT-ADDITIONAL",
includePrimaryItem: false,
vehicleType: null,
lastOrderId: null,
},
route: {
step: 2,
orderId,
customerId: REGULAR_CUSTOMER_ID,
},
});
await expect.poll(() => fixture.requestCounters.customerAttributesGet, { timeout: 10_000 }).toBeGreaterThan(0);
await selectPrimaryProduct(page, 53);
await expect(page.getByTestId("pos-mobile-additional-items-open")).toHaveCount(1);
await page.getByTestId("pos-mobile-additional-items-open").click();
await expect(page.getByTestId("pos-mobile-additional-items-selection")).toHaveCount(0);
await longPressAdditionalItems(page);
await expect(page.getByTestId("pos-mobile-additional-items-selection")).toHaveCount(0);
await expect
.poll(
async () => {
const snapshot = await getStoredPosSnapshot(page);
return snapshot?.transactionItems?.additionalItems || [];
},
{ timeout: 10_000 }
)
.toEqual([]);
});
test("clear all deletes the order and returns to the scanner", async ({ page }) => {
const orderId = 9403;
const fixture = createMobilePosFixture({
+2 -2
View File
@@ -223,7 +223,7 @@ test.describe("Self-serve wash", () => {
await page.goto("/user/wash");
await expect(page.getByRole("link", { name: "Start vask" })).toBeVisible({ timeout: 15_000 });
await expect(page.getByRole("link", { name: "Start vask" })).toBeVisible({ timeout: 30_000 });
await expect(page.getByText("Roskilde").first()).toBeVisible();
const orderedDepartmentNames = await page
@@ -268,7 +268,7 @@ test.describe("Self-serve wash", () => {
});
await page.reload({ waitUntil: "domcontentloaded" });
await expect(page.getByTestId("self-serve-live-elapsed")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("self-serve-inline-progress")).toContainText("ZZ00000", { timeout: 20_000 });
await advanceGuidedWashToLastStep(page);
await expect(page.getByTestId("self-serve-guided-next")).toBeHidden();
@@ -0,0 +1,207 @@
import { expect, test, type Page, type Route } from "@playwright/test";
import { apiPathPattern, mockApi, seedAuthenticatedState } from "./support/network.js";
// This route is sensitive to stale cached Vue modules when the PWA service worker is active in mobile runs.
test.use({ serviceWorkers: "block" });
const json = (body: unknown, status = 200) => ({
status,
contentType: "application/json",
body: JSON.stringify(body),
});
const permissionCatalog = {
"/orders": {
GET: {
list_orders: "List orders",
},
POST: {
add_order: "Create orders",
},
},
"/admin/bookings": {
POST: {
create_bookings: "Create bookings",
},
},
"/superuser/system/replication": {
GET: {
superuser_replication_view: "View replication status",
},
},
};
const departments = [
{
id: 1,
name: "Copenhagen",
},
{
id: 2,
name: "Odense",
},
];
type PermissionCall = {
action: "add" | "remove";
permission: string;
};
const openGroupTab = async (page: Page, label: RegExp) => {
await page.locator(".tabs").getByText(label).click();
};
const clickPermissionSwitch = async (page: Page, permission: string) => {
await page.locator(`label[for="role-permission-${permission}"]`).click();
};
const installRolePermissionRoutes = async (
page: Page,
options: {
failAction?: "add" | "remove";
failPermission?: string;
} = {}
) => {
let rolePermissions = ["user", "superuser", "department_access_1", "list_orders"];
const calls: PermissionCall[] = [];
const rolePayload = () => ({
id: 2,
name: "Operations manager",
description: "Can manage operations.",
created_at: "2026-07-06T08:00:00.000Z",
permissions: [...rolePermissions],
});
await page.route(apiPathPattern("/permissions"), async (route: Route) => {
await route.fulfill(json({ data: permissionCatalog }));
});
await page.route(apiPathPattern("/departments"), async (route: Route) => {
await route.fulfill(json({ data: departments }));
});
await page.route(apiPathPattern("/roles"), async (route: Route) => {
await route.fulfill(json({ data: rolePayload() }));
});
await page.route(apiPathPattern("/roles/permissions"), async (route: Route) => {
const request = route.request();
const method = request.method();
const url = new URL(request.url());
const body = method === "POST" ? request.postDataJSON() : {};
const permission =
method === "POST" ? String(body.permission_id || "") : String(url.searchParams.get("permission_id") || "");
const action = method === "POST" ? "add" : "remove";
calls.push({ action, permission });
if (options.failAction === action && options.failPermission === permission) {
await route.fulfill(json({ data: { message: "Mutation failed" } }, 500));
return;
}
if (action === "add" && !rolePermissions.includes(permission)) {
rolePermissions = [...rolePermissions, permission];
}
if (action === "remove") {
rolePermissions = rolePermissions.filter((value) => value !== permission);
}
await route.fulfill(json({ data: rolePayload() }));
});
return {
calls,
getRolePermissions: () => [...rolePermissions],
};
};
const bootRolePermissionsPage = async (page: Page, routeOptions = {}) => {
await page.addInitScript(() => {
window.localStorage.setItem("locale", "en");
});
await seedAuthenticatedState(page, "superuser-role-permissions-token");
await mockApi(page, {
authenticated: true,
permissions: [
"superuser",
"user",
"permissions_list",
"list_roles",
"add_role_permission",
"delete_role_permission",
],
sessionData: {
group_id: 1,
},
});
const harness = await installRolePermissionRoutes(page, routeOptions);
await page.goto("/superuser/roles/2", { waitUntil: "domcontentloaded" });
await expect(page.getByTestId("role-permissions-page")).toBeVisible();
await expect(page.getByTestId("role-permissions-loading")).toHaveCount(0);
return harness;
};
test.describe("Superuser role permissions", () => {
test("@smoke @pr loads grouped permissions, searches, and toggles without a blank page", async ({ page }) => {
const { calls } = await bootRolePermissionsPage(page);
await expect(page.getByText("Operations manager permissions")).toBeVisible();
await expect(page.getByTestId("role-permissions-selected-count")).toContainText("4 of 9 selected");
await expect(page.getByRole("heading", { name: "Access" })).toBeVisible();
await openGroupTab(page, /Orders and POS/);
await expect(page.getByTestId("role-permission-row-list_orders")).toContainText("List orders");
await clickPermissionSwitch(page, "list_orders");
await expect.poll(() => calls).toContainEqual({ action: "remove", permission: "list_orders" });
await expect(page.getByTestId("role-permissions-page")).toBeVisible();
await expect(page.locator("body")).not.toContainText(/white screen|TypeError|Unhandled/i);
await page.getByTestId("role-permissions-search").fill("booking");
await openGroupTab(page, /Bookings/);
await expect(page.getByTestId("role-permission-row-create_bookings")).toContainText("Create bookings");
await openGroupTab(page, /Orders and POS/);
await expect(page.getByTestId("role-permissions-empty-orders_pos")).toBeVisible();
});
test("selects and clears all permissions in a group without no-op requests", async ({ page }) => {
const { calls, getRolePermissions } = await bootRolePermissionsPage(page);
await openGroupTab(page, /Orders and POS/);
await page.getByTestId("role-permissions-clear-all-orders_pos").click();
await expect.poll(() => calls).toEqual([{ action: "remove", permission: "list_orders" }]);
expect(getRolePermissions()).not.toContain("list_orders");
expect(getRolePermissions()).not.toContain("add_order");
await page.getByTestId("role-permissions-select-all-orders_pos").click();
await expect
.poll(() => calls)
.toEqual([
{ action: "remove", permission: "list_orders" },
{ action: "add", permission: "add_order" },
{ action: "add", permission: "list_orders" },
]);
expect(getRolePermissions()).toEqual(expect.arrayContaining(["list_orders", "add_order"]));
});
test("keeps the page rendered and reloads role permissions when a mutation fails", async ({ page }) => {
const { calls, getRolePermissions } = await bootRolePermissionsPage(page, {
failAction: "add",
failPermission: "add_order",
});
await openGroupTab(page, /Orders and POS/);
await clickPermissionSwitch(page, "add_order");
await expect.poll(() => calls).toContainEqual({ action: "add", permission: "add_order" });
await expect(page.getByTestId("role-permission-error")).toBeVisible();
await expect(page.getByTestId("role-permissions-page")).toBeVisible();
await expect(page.getByTestId("role-permission-toggle-add_order")).not.toBeChecked();
expect(getRolePermissions()).not.toContain("add_order");
});
});
@@ -7,10 +7,13 @@ async function primeSuperuserSession(page) {
}
test.describe("Superuser vehicles smoke", () => {
test.describe.configure({ mode: "serial" });
test.beforeEach(async ({ page }) => {
await mockApi(page, {
authenticated: true,
permissions: ["superuser", "user"],
pos: true,
});
await primeSuperuserSession(page);
});
@@ -23,4 +26,51 @@ test.describe("Superuser vehicles smoke", () => {
await expect(page.locator("body")).toContainText(/registrerede|registered/i);
await expect(page.locator("body")).not.toContainText(/Order ID is required/i);
});
test("superuser can add a vehicle after selecting a customer from searchable results", async ({ page }) => {
const createVehiclePayloads = [];
page.on("request", (request) => {
const url = new URL(request.url());
if (url.pathname.endsWith("/vehicles") && request.method() === "POST") {
createVehiclePayloads.push(request.postDataJSON());
}
});
await page.goto("/superuser/vehicles");
await page.getByTestId("superuser-vehicles-add").click();
await expect(page.getByTestId("superuser-add-vehicle-modal")).toBeVisible();
await page.getByTestId("superuser-add-vehicle-customer-input").fill("12345679");
await expect(page.getByTestId("superuser-add-vehicle-customer-option-0")).toBeVisible();
await page.getByTestId("superuser-add-vehicle-customer-option-0").click();
await expect(page.getByTestId("superuser-add-vehicle-customer-selected")).toContainText("#12345679");
await page.getByTestId("superuser-add-vehicle-registration").fill("ab12345");
await expect(page.getByTestId("superuser-add-vehicle-type")).toBeEnabled();
await page.getByTestId("superuser-add-vehicle-type").selectOption("53");
await page.getByTestId("superuser-add-vehicle-wash-subscription").check();
await page.getByTestId("superuser-add-vehicle-reference").fill("Fleet reference");
await Promise.all([
page.waitForResponse(
(response) => response.url().includes("/vehicles") && response.request().method() === "POST"
),
page.getByTestId("superuser-add-vehicle-submit").click(),
]);
expect(createVehiclePayloads).toEqual([
{
type: 53,
reg: "AB12345",
wash_subscription: true,
customer_id: 12345679,
reference: "Fleet reference",
},
]);
await expect(page.getByTestId("superuser-add-vehicle-modal")).toBeHidden();
await expect(page.getByTestId("user-vehicles-table")).toContainText("AB12345");
});
});
+25
View File
@@ -2736,6 +2736,7 @@ export function createPosFixture(overrides = {}) {
last_order_id: 54518,
},
],
nextVehicleId: 7002,
unknownVehicles: [],
orderBookings: [],
bookingOrderAssignments: [],
@@ -3278,6 +3279,30 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
return true;
}
if (pathname.endsWith("/vehicles") && method === "POST") {
const body = request.postDataJSON?.() || {};
const customerId = Number(body.customer_id || 0);
const customer = posFixture.customersByNumber[customerId] || null;
const vehicle = {
id: posFixture.nextVehicleId || 9001,
reg: String(body.reg || "").toUpperCase(),
customer_id: customerId,
customer_name: customer?.name || "",
type: Number(body.type || 0),
status: "verified",
barred: false,
wash_subscription: Boolean(body.wash_subscription),
addons: { enabled: 0, available: 0, list: [] },
reference: body.reference || null,
};
posFixture.nextVehicleId = vehicle.id + 1;
posFixture.vehicles = [vehicle, ...(posFixture.vehicles || [])];
await route.fulfill(json({ success: true, data: vehicle }));
return true;
}
if (pathname.endsWith("/department/vehicles/unknown-customer") && method === "GET") {
await route.fulfill(json({ success: true, data: posFixture.unknownVehicles || [] }));
return true;
+1
View File
@@ -42,6 +42,7 @@ test("[PAGES][User][/user/vehicles] opens the add-vehicle dialog", async ({ page
await expect(dialog).toBeVisible();
await expect(dialog.getByRole("heading", { name: /opret/i })).toBeVisible();
await expect(dialog.locator("label").filter({ hasText: /^Type$/ })).toBeVisible();
await expect(dialog.locator("select#wash_subscription")).toHaveValue("false");
await expect(dialog.getByRole("button", { name: /gem/i })).toBeVisible();
await expect(dialog.getByRole("button", { name: /annuller/i })).toBeVisible();
});
+23 -14
View File
@@ -6,7 +6,12 @@ async function primeSuperuserSession(page) {
await primeMockSession(page, { token });
}
async function runDiagnostic(page, testId, inputValue = "") {
async function expectDiagnosticResult(page, expectedText) {
const result = page.locator(".swal2-popup:visible pre").first();
await expect(result).toContainText(expectedText);
}
async function runDiagnostic(page, testId, { inputValue = "", expectedText } = {}) {
await page.locator(`[data-testid="${testId}"]:visible`).first().click();
await expect(page.locator(".swal2-popup")).toBeVisible();
const input = page.locator(".swal2-input");
@@ -14,10 +19,10 @@ async function runDiagnostic(page, testId, inputValue = "") {
await input.fill(inputValue);
}
await page.locator(".swal2-confirm").click();
await expect(page.locator(".swal2-popup pre")).toBeVisible();
await expectDiagnosticResult(page, expectedText);
}
async function runShiftDiagnostic(page, { startFrom, startTo, employeeID = "", released = "" }) {
async function runShiftDiagnostic(page, { startFrom, startTo, employeeID = "", released = "", expectedText }) {
await page.locator('[data-testid="workfeed-diagnostics-list-shifts"]:visible').first().click();
await expect(page.locator(".swal2-popup")).toBeVisible();
await page.locator("#workfeed-start-from").fill(startFrom);
@@ -29,11 +34,13 @@ async function runShiftDiagnostic(page, { startFrom, startTo, employeeID = "", r
await page.locator("#workfeed-released").selectOption(released);
}
await page.locator(".swal2-confirm").click();
await expect(page.locator(".swal2-popup pre")).toBeVisible();
await expectDiagnosticResult(page, expectedText);
}
async function closeResultModal(page) {
await page.locator(".swal2-confirm").click();
const confirmButton = page.locator(".swal2-popup:visible .swal2-confirm").first();
await expect(confirmButton).toBeVisible();
await confirmButton.evaluate((button) => button.click());
await expect(page.locator(".swal2-popup")).toBeHidden();
}
@@ -67,12 +74,10 @@ test.describe("Workfeed configuration smoke", () => {
await expect(page.locator('[data-testid="workfeed-diagnostics-get-employee"]:visible').first()).toBeVisible();
await expect(page.locator('[data-testid="workfeed-diagnostics-get-shift"]:visible').first()).toBeVisible();
await runDiagnostic(page, "workfeed-diagnostics-list-departments");
await expect(page.locator(".swal2-popup pre")).toContainText("North Facility");
await runDiagnostic(page, "workfeed-diagnostics-list-departments", { expectedText: "North Facility" });
await closeResultModal(page);
await runDiagnostic(page, "workfeed-diagnostics-list-employees");
await expect(page.locator(".swal2-popup pre")).toContainText("Anne Nielsen");
await runDiagnostic(page, "workfeed-diagnostics-list-employees", { expectedText: "Anne Nielsen" });
await closeResultModal(page);
await runShiftDiagnostic(page, {
@@ -80,16 +85,20 @@ test.describe("Workfeed configuration smoke", () => {
startTo: "2026-03-25T00:00",
employeeID: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P",
released: "true",
expectedText: "Morning Shift",
});
await expect(page.locator(".swal2-popup pre")).toContainText("Morning Shift");
await closeResultModal(page);
await runDiagnostic(page, "workfeed-diagnostics-get-employee", "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P");
await expect(page.locator(".swal2-popup pre")).toContainText("Anne Nielsen");
await runDiagnostic(page, "workfeed-diagnostics-get-employee", {
inputValue: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P",
expectedText: "Anne Nielsen",
});
await closeResultModal(page);
await runDiagnostic(page, "workfeed-diagnostics-get-shift", "shf_01J5P3X9D35R9C6BDZ1S0R4V6Q");
await expect(page.locator(".swal2-popup pre")).toContainText("Morning Shift");
await runDiagnostic(page, "workfeed-diagnostics-get-shift", {
inputValue: "shf_01J5P3X9D35R9C6BDZ1S0R4V6Q",
expectedText: "Morning Shift",
});
await closeResultModal(page);
});
});
+115
View File
@@ -0,0 +1,115 @@
// @vitest-environment jsdom
import { nextTick } from "vue";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { mountWithApp } from "./helpers/mountWithApp.js";
const searchMocks = vi.hoisted(() => ({
isSearchingRef: null,
resultsRef: null,
searchCustomer: vi.fn(),
}));
vi.mock("@/components/search/economic/customerSearch.vue", async () => {
const { ref } = await vi.importActual("vue");
searchMocks.isSearchingRef = ref(false);
searchMocks.resultsRef = ref([]);
return {
isSearching: searchMocks.isSearchingRef,
searchCustomerResults: searchMocks.resultsRef,
searchCustomer: searchMocks.searchCustomer,
};
});
import CustomerSearchSelect from "@/components/search/economic/CustomerSearchSelect.vue";
const customers = [
{
customerNumber: 12345679,
name: "Acme Transport",
city: "Taastrup",
},
{
customerNumber: 87654321,
name: "Nordic Wash",
city: "Copenhagen",
},
];
const messages = {
en: {
vehicles: {
add_modal: {
customer_label: "Customer",
customer_placeholder: "Search customers",
selected_customer: "Selected customer",
},
},
common: {
clear: "Clear",
},
},
};
const mountComponent = (props = {}) =>
mountWithApp(CustomerSearchSelect, {
props: {
inputId: "test-customer-search",
testIdPrefix: "test-customer",
...props,
},
messages,
});
describe("CustomerSearchSelect", () => {
beforeEach(() => {
vi.clearAllMocks();
searchMocks.isSearchingRef.value = false;
searchMocks.resultsRef.value = [];
searchMocks.searchCustomer.mockImplementation((query) => {
searchMocks.resultsRef.value = query ? customers : [];
return Promise.resolve(searchMocks.resultsRef.value);
});
});
it("searches customers and emits the selected customer", async () => {
const wrapper = mountComponent();
await wrapper.get('[data-testid="test-customer-input"]').setValue("acme");
await nextTick();
expect(searchMocks.searchCustomer).toHaveBeenLastCalledWith("acme");
expect(wrapper.get('[data-testid="test-customer-option-0"]').text()).toContain("Acme Transport");
await wrapper.get('[data-testid="test-customer-option-0"]').trigger("mousedown");
await nextTick();
expect(wrapper.emitted("update:modelValue").at(-1)).toEqual([customers[0]]);
expect(wrapper.emitted("selected").at(-1)).toEqual([customers[0]]);
await wrapper.setProps({ modelValue: customers[0] });
await nextTick();
expect(wrapper.get('[data-testid="test-customer-selected"]').text()).toContain("Acme Transport");
});
it("supports keyboard selection and clearing", async () => {
const wrapper = mountComponent();
await wrapper.get('[data-testid="test-customer-input"]').setValue("nordic");
await wrapper.get('[data-testid="test-customer-input"]').trigger("keydown", { key: "ArrowDown" });
await wrapper.get('[data-testid="test-customer-input"]').trigger("keydown", { key: "ArrowDown" });
await wrapper.get('[data-testid="test-customer-input"]').trigger("keydown", { key: "Enter" });
await nextTick();
expect(wrapper.emitted("update:modelValue").at(-1)).toEqual([customers[1]]);
await wrapper.setProps({ modelValue: customers[1] });
await nextTick();
await wrapper.get('[data-testid="test-customer-clear"]').trigger("click");
await nextTick();
expect(wrapper.emitted("update:modelValue").at(-1)).toEqual([null]);
expect(wrapper.emitted("cleared")).toHaveLength(1);
});
});
+34
View File
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { endOfLocalDate, formatLocalDateOnly, parseLocalDateOnly, startOfLocalDate } from "@/services/dateOnly.js";
describe("date-only helpers", () => {
it("formats Date objects from their local calendar date", () => {
expect(formatLocalDateOnly(new Date(2026, 2, 31, 0, 0, 0, 0))).toBe("2026-03-31");
});
it("parses date input values as local calendar dates", () => {
const parsed = parseLocalDateOnly("2026-03-31");
expect(parsed.getFullYear()).toBe(2026);
expect(parsed.getMonth()).toBe(2);
expect(parsed.getDate()).toBe(31);
expect(formatLocalDateOnly(parsed)).toBe("2026-03-31");
});
it("keeps date-only prefixes from API timestamps without timezone reinterpretation", () => {
expect(formatLocalDateOnly("2026-03-31T23:30:00.000Z")).toBe("2026-03-31");
});
it("rejects impossible date input values", () => {
expect(Number.isNaN(parseLocalDateOnly("2026-02-31").getTime())).toBe(true);
});
it("builds local day boundaries from the selected date", () => {
expect(startOfLocalDate("2026-03-31").getHours()).toBe(0);
expect(startOfLocalDate("2026-03-31").getMinutes()).toBe(0);
expect(endOfLocalDate("2026-03-31").getHours()).toBe(23);
expect(endOfLocalDate("2026-03-31").getMinutes()).toBe(59);
expect(formatLocalDateOnly(endOfLocalDate("2026-03-31"))).toBe("2026-03-31");
});
});
+26
View File
@@ -122,6 +122,32 @@ describe("DatePeriodSelector mobile layout", () => {
expect(formatLocalDate(onSelectionChange.mock.calls[0][0])).toBe("2026-03-01");
expect(formatLocalDate(onSelectionChange.mock.calls[0][1])).toBe("2026-03-05");
});
it("emits the exact clicked date for start and end inputs", async () => {
sharedState.width.value = 1024;
const onSelectionChange = vi.fn();
const wrapper = mount(DatePeriodSelector, {
props: {
selection: {
startDate: new Date(2026, 2, 1, 0, 0, 0, 0),
endDate: new Date(2026, 2, 3, 23, 59, 59, 999),
},
onSelectionChange,
visibility: {
showUpdateButton: false,
},
},
});
await wrapper.get("[data-testid='date-period-start']").setValue("2026-03-31");
await wrapper.get("[data-testid='date-period-end']").setValue("2026-04-02");
const emittedSelection = wrapper.emitted("update:selection")?.at(-1)?.[0];
expect(formatLocalDate(emittedSelection.startDate)).toBe("2026-03-31");
expect(formatLocalDate(emittedSelection.endDate)).toBe("2026-04-02");
expect(formatLocalDate(onSelectionChange.mock.calls.at(-1)[0])).toBe("2026-03-31");
expect(formatLocalDate(onSelectionChange.mock.calls.at(-1)[1])).toBe("2026-04-02");
});
});
describe("DatePeriodSelector month warning", () => {
@@ -0,0 +1,178 @@
// @vitest-environment jsdom
import { flushPromises, mount } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
setEnabled: vi.fn(),
showDeleteObjectForm: vi.fn(),
showEditObjectFieldForm: vi.fn(),
hasPermission: vi.fn(),
canAccessSuperUser: vi.fn(),
}));
vi.mock("vue-i18n", () => ({
useI18n: () => ({
t: (key) =>
({
"common.yes": "Yes",
"common.no": "No",
"global.delete": "Delete",
"tables.showing": "Showing",
}[key] ?? key),
}),
}));
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
hasPermission: mocks.hasPermission,
canAccessSuperUser: mocks.canAccessSuperUser,
objects: {
global: {
icons: {
delete: "fas fa-trash",
},
},
department_notification_sms: {
meta: {
labels: {
multiple: "notification phone numbers",
},
},
columns: {
id: { label: "ID" },
label: { label: "Label" },
phone_country_code: { label: "Country code" },
phone: { label: "Phone" },
enabled: { label: "Enabled" },
},
set: {
enabled: mocks.setEnabled,
},
showEditObjectFieldForm: mocks.showEditObjectFieldForm,
functions: {
showDeleteObjectForm: mocks.showDeleteObjectForm,
},
},
},
},
}));
vi.mock("@/components/displays/buttons/EditableTableColumn.vue", () => ({
default: {
name: "EditableTableColumn",
props: {
object: Object,
column: String,
loadList: Function,
},
template: `<td :data-testid="\`editable-\${column}-\${object.id}\`">{{ object[column] }}</td>`,
},
}));
vi.mock("@/components/displays/buttons/ActionSettingsWheelButton.vue", () => ({
default: {
name: "ActionSettingsWheelButton",
template: `<div><slot name="actions" /></div>`,
},
}));
vi.mock("@/components/displays/buttons/ActionSettingsWheelItem.vue", () => ({
default: {
name: "ActionSettingsWheelItem",
props: {
testId: String,
label: String,
disabled: Boolean,
},
emits: ["click"],
template: `<button type="button" :data-testid="testId" :disabled="disabled" @click="$emit('click')">{{ label }}</button>`,
},
}));
vi.mock("@/components/displays/permissionbased/RequiresPermission.vue", () => ({
default: {
name: "RequiresPermission",
props: {
permission: String,
},
template: `<slot />`,
},
}));
import DepartmentNotificationsPhoneTable from "@/components/displays/department/notifications/departmentNotificationsPhoneTable.vue";
const mountTable = (props = {}) =>
mount(DepartmentNotificationsPhoneTable, {
props: {
objects: [
{
id: 10,
label: "Dispatch line",
phone_country_code: 45,
phone: 12345678,
enabled: true,
},
],
loadList: vi.fn().mockResolvedValue({}),
...props,
},
global: {
mocks: {
$t: (key) =>
({
"tables.showing": "Showing",
}[key] ?? key),
},
},
});
describe("DepartmentNotificationsPhoneTable", () => {
beforeEach(() => {
vi.restoreAllMocks();
mocks.setEnabled.mockReset();
mocks.showDeleteObjectForm.mockReset();
mocks.showEditObjectFieldForm.mockReset();
mocks.hasPermission.mockReset();
mocks.canAccessSuperUser.mockReset();
mocks.setEnabled.mockResolvedValue({});
mocks.hasPermission.mockReturnValue(true);
mocks.canAccessSuperUser.mockReturnValue(false);
});
it("updates enabled state and reloads the provided paginated list", async () => {
const loadList = vi.fn().mockResolvedValue("reloaded");
const wrapper = mountTable({ loadList });
await wrapper.get('[data-testid="department-notification-sms-enabled-toggle-10"]').setValue(false);
await flushPromises();
expect(mocks.setEnabled).toHaveBeenCalledWith(10, false);
expect(loadList).toHaveBeenCalledOnce();
});
it("rolls back the switch and skips reload when the enabled update fails", async () => {
const error = new Error("Not Found");
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
mocks.setEnabled.mockRejectedValueOnce(error);
const loadList = vi.fn().mockResolvedValue("reloaded");
const wrapper = mountTable({ loadList });
const toggle = wrapper.get('[data-testid="department-notification-sms-enabled-toggle-10"]');
await toggle.setValue(false);
await flushPromises();
expect(toggle.element.checked).toBe(true);
expect(loadList).not.toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledWith("Unable to update department notification SMS active state", error);
});
it("passes the provided paginated list reload callback to delete", async () => {
const loadList = vi.fn().mockResolvedValue("reloaded");
const wrapper = mountTable({ loadList });
await wrapper.get('[data-testid="department-notification-sms-delete-10"]').trigger("click");
expect(mocks.showDeleteObjectForm).toHaveBeenCalledWith(10, loadList);
});
});
@@ -2,6 +2,7 @@
import { defineComponent, nextTick } from "vue";
import { mount } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { formatLocalDateOnly } from "@/services/dateOnly.js";
const selectDateMock = vi.hoisted(() => vi.fn());
const sharedDateState = vi.hoisted(() => ({
@@ -39,10 +40,10 @@ const DatePeriodSelectorStub = defineComponent({
emits: ["update:selection"],
template: `
<div>
<button class="trigger-on-selection-change" @click="onSelectionChange(new Date('2026-03-24T00:00:00.000Z'), new Date('2026-03-29T00:00:00.000Z'))">
<button class="trigger-on-selection-change" @click="onSelectionChange(new Date(2026, 2, 24), new Date(2026, 2, 29))">
onSelectionChange
</button>
<button class="trigger-update-selection" @click="$emit('update:selection', { startDate: new Date('2026-04-01T00:00:00.000Z'), endDate: new Date('2026-04-30T00:00:00.000Z') })">
<button class="trigger-update-selection" @click="$emit('update:selection', { startDate: new Date(2026, 3, 1), endDate: new Date(2026, 3, 30) })">
update:selection
</button>
</div>
@@ -105,7 +106,7 @@ describe("DepartmentDashboardOverviewNavigation", () => {
});
const selector = wrapper.findComponent(DatePeriodSelectorStub);
expect(selector.props("selection").startDate.toISOString().split("T")[0]).toBe("2026-05-05");
expect(selector.props("selection").endDate.toISOString().split("T")[0]).toBe("2026-05-11");
expect(formatLocalDateOnly(selector.props("selection").startDate)).toBe("2026-05-05");
expect(formatLocalDateOnly(selector.props("selection").endDate)).toBe("2026-05-11");
});
});
@@ -63,6 +63,26 @@ describe("ObjectsGlobal select editor escaping", () => {
});
describe("ObjectsGlobal create form locked values", () => {
it("renders boolean create fields with an explicit false default selected", async () => {
const html = await ObjectsGlobal.generateCreateObjectForm(
{
wash_subscription: {
label: "Wash subscription",
type: "boolean",
creation: {
required: true,
default: false,
},
},
},
{}
);
document.body.innerHTML = html;
expect(document.querySelector("#wash_subscription")?.value).toBe("false");
});
it("submits required locked values without reading them back from the DOM", async () => {
const add = vi.fn().mockResolvedValue({});
const object = {
@@ -0,0 +1,110 @@
// @vitest-environment jsdom
import { defineComponent } from "vue";
import { mount } from "@vue/test-utils";
import { describe, expect, it, vi } from "vitest";
vi.mock("vue-i18n", async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
useI18n: () => ({
t: (value) => value,
}),
};
});
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
objects: {
global: {
language: {
date_from: "From",
date_to: "To",
},
},
},
},
}));
import { formatLocalDateOnly } from "@/services/dateOnly.js";
import PaginationDisplayTemplateDate from "@/components/displays/pagination/templates/PaginationDisplayTemplateDate.vue";
import PaginationDisplayTemplateDates from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
import { dateFunctions } from "@/components/displays/pagination/PaginationDisplayDates.vue";
const PaginationDisplayItemColumnStub = defineComponent({
name: "PaginationDisplayItemColumn",
props: {
label: {
type: String,
required: true,
},
},
template: "<div><slot name='control' /></div>",
});
describe("pagination date selection", () => {
it("renders Date model values as the same local date", () => {
const wrapper = mount(PaginationDisplayTemplateDate, {
props: {
label: "Date",
modelValue: new Date(2026, 2, 31, 0, 0, 0, 0),
},
global: {
stubs: {
PaginationDisplayItemColumn: PaginationDisplayItemColumnStub,
},
},
});
expect(wrapper.get("input[type='date']").element.value).toBe("2026-03-31");
});
it("emits the clicked input date as the same local date", async () => {
const wrapper = mount(PaginationDisplayTemplateDate, {
props: {
label: "Date",
modelValue: new Date(2026, 2, 1, 0, 0, 0, 0),
},
global: {
stubs: {
PaginationDisplayItemColumn: PaginationDisplayItemColumnStub,
},
},
});
await wrapper.get("input[type='date']").setValue("2026-03-31");
const emittedDate = wrapper.emitted("update:date")?.at(-1)?.[0];
expect(formatLocalDateOnly(emittedDate)).toBe("2026-03-31");
});
it("uses the real first day of the month for month presets", () => {
const firstDay = dateFunctions.datePresetFunctions.month.firstDayOfMonth(new Date(2026, 4, 15, 12, 0, 0, 0));
expect(formatLocalDateOnly(firstDay)).toBe("2026-05-01");
expect(firstDay.getHours()).toBe(0);
expect(firstDay.getMinutes()).toBe(0);
});
it("emits both start and end updates from the paired date selector", async () => {
const wrapper = mount(PaginationDisplayTemplateDates, {
props: {
startDate: new Date(2026, 2, 1, 0, 0, 0, 0),
endDate: new Date(2026, 2, 31, 0, 0, 0, 0),
},
global: {
stubs: {
PaginationDisplayItemColumn: PaginationDisplayItemColumnStub,
},
},
});
const inputs = wrapper.findAll("input[type='date']");
await inputs[0].setValue("2026-04-01");
await inputs[1].setValue("2026-04-30");
expect(formatLocalDateOnly(wrapper.emitted("update:startDate")?.at(-1)?.[0])).toBe("2026-04-01");
expect(formatLocalDateOnly(wrapper.emitted("update:endDate")?.at(-1)?.[0])).toBe("2026-04-30");
});
});
+27 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { sourceMappings } from "../../scripts/playwright-pr-mapping.mjs";
import { fallbackChangePatterns, sourceMappings } from "../../scripts/playwright-pr-mapping.mjs";
const specsFor = (file) =>
sourceMappings
@@ -7,9 +7,35 @@ const specsFor = (file) =>
.flatMap((mapping) => mapping.specs);
describe("Playwright PR mapping", () => {
const triggersFallback = (file) => fallbackChangePatterns.some((pattern) => pattern.test(file));
it("maps user vehicle table changes to the user vehicles E2E coverage", () => {
expect(specsFor("src/components/displays/user/vehicles/vehiclesTable.vue")).toContain(
"tests/e2e/userVehicles.spec.ts"
);
});
it("maps department notification table changes to the admin notification E2E coverage", () => {
expect(
specsFor("src/components/displays/department/notifications/departmentNotificationsPhoneTable.vue")
).toContain("tests/e2e/admin-department-notifications.spec.ts");
expect(
specsFor("src/components/displays/pagination/models/DepartmentPos/NotificationsPhonePagination.vue")
).toContain("tests/e2e/admin-department-notifications.spec.ts");
});
it("maps superuser role permission page changes to role permissions E2E coverage", () => {
expect(specsFor("src/views/dashboards/superUserDashboard/roles/SuperUserRolesPermissions.vue")).toContain(
"tests/e2e/superuser-roles-permissions.spec.ts"
);
expect(specsFor("src/views/dashboards/superUserDashboard/roles/rolePermissionCatalog.js")).toContain(
"tests/e2e/superuser-roles-permissions.spec.ts"
);
});
it("keeps full-slice ownership metadata out of broad PR smoke fallback", () => {
expect(triggersFallback("scripts/run-playwright-pr.mjs")).toBe(true);
expect(triggersFallback("scripts/run-playwright-ci-parallel.mjs")).toBe(true);
expect(triggersFallback("scripts/run-playwright-full-slice.mjs")).toBe(false);
});
});
+123
View File
@@ -93,6 +93,8 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
reference: vi.fn(),
reg_1: vi.fn(),
reg_2: vi.fn(),
reg_3: vi.fn(),
safety_seal: vi.fn(),
},
},
order_bookings: {
@@ -152,6 +154,8 @@ import {
getVehiclePlateBookings,
invoiceCollectionId,
loadOrderItems,
nextStep,
nextStepDelay,
order_id,
order_items,
hydrateSelectedOrderBookingForDesktop,
@@ -163,7 +167,10 @@ import {
reg_2,
reg_3,
pendingBookings,
previousStep,
registerPosStepSaveBarrier,
restoreStoredPosOrderId,
saveOrderMetadataField,
searchAndSelectCustomer,
setOrderId,
scan_data,
@@ -172,6 +179,122 @@ import {
step,
} from "@/components/shop/POSDepartmentProcess.vue";
describe("POSDepartmentProcess order metadata persistence", () => {
beforeEach(() => {
window.history.replaceState({}, "", "/admin/12/modules/pos?step=2");
clearActivePosOrderContext({ clearStep: true });
localStorage.setItem("token", "test-token");
nextStepDelay.value = 0;
order_id.value = 54518;
customer_id.value = 999;
customer_name.value = "Card Terminal Customer";
customer_data.value = {};
department_id.value = 12;
step.value = 2;
Object.values(SessionUser.objects.orders.set).forEach((setter) => setter.mockReset?.());
});
it("applies saved metadata responses to shared POS state", async () => {
SessionUser.objects.orders.set.reference.mockResolvedValueOnce({
data: {
data: {
id: 54518,
customer_id: 999,
department_id: 12,
reference: "STEP-2-REF",
notes: "Visible note",
po: "PO-STEP-2",
safety_seal: "SEAL-1",
reg_1: "aa 12345",
reg_2: "",
reg_3: "",
invoice_collection_id: 200,
completed_at: null,
},
},
});
await expect(saveOrderMetadataField("reference", "STEP-2-REF", 54518)).resolves.toBe("STEP-2-REF");
expect(SessionUser.objects.orders.set.reference).toHaveBeenCalledWith(54518, "STEP-2-REF");
expect(reference.value).toBe("STEP-2-REF");
expect(order_notes.value).toBe("Visible note");
expect(order_po.value).toBe("PO-STEP-2");
expect(order_safety_seal.value).toBe("SEAL-1");
expect(reg_1.value).toBe("AA12345");
});
it("preserves registration input formatting for the save request and normalizes shared state", async () => {
SessionUser.objects.orders.set.reg_1.mockResolvedValueOnce({
data: {
data: {
id: 54518,
reg_1: "CD1234",
},
},
});
await expect(saveOrderMetadataField("reg_1", "cd-12 34", 54518)).resolves.toBe("CD1234");
expect(SessionUser.objects.orders.set.reg_1).toHaveBeenCalledWith(54518, "CD-12 34");
expect(reg_1.value).toBe("CD1234");
});
it("waits for registered field saves before moving from step 2 to step 3", async () => {
let resolveBarrier;
const barrier = vi.fn(
() =>
new Promise((resolve) => {
resolveBarrier = () => {
reference.value = "BARRIER-SAVED";
resolve();
};
})
);
const unregister = registerPosStepSaveBarrier(barrier);
try {
const moveToNextStep = nextStep({ isMobile: false, orderCreation: false });
await Promise.resolve();
expect(barrier).toHaveBeenCalledTimes(1);
expect(window.location.search).toBe("?step=2");
resolveBarrier();
await moveToNextStep;
expect(reference.value).toBe("BARRIER-SAVED");
expect(window.location.search).toContain("step=3");
expect(window.location.search).toContain("id=54518");
expect(window.location.search).toContain("customer_id=999");
} finally {
unregister();
nextStepDelay.value = 0;
}
});
it("waits for registered field saves before moving backward", async () => {
step.value = 3;
window.history.replaceState({}, "", "/admin/12/modules/pos?id=54518&customer_id=999&step=3");
const barrier = vi.fn(async () => {
order_po.value = "PO-BACK-SAVED";
});
const unregister = registerPosStepSaveBarrier(barrier);
try {
await previousStep();
expect(barrier).toHaveBeenCalledTimes(1);
expect(order_po.value).toBe("PO-BACK-SAVED");
expect(step.value).toBe(2);
expect(window.location.search).toContain("step=2");
} finally {
unregister();
nextStepDelay.value = 0;
}
});
});
describe("POSDepartmentProcess.ensureVehiclePlateBookingsLoaded", () => {
beforeEach(() => {
department_id.value = 531;
+160
View File
@@ -0,0 +1,160 @@
import { describe, expect, it } from "vitest";
import {
filterPermissionRecords,
getPermissionDelta,
groupPermissionRecords,
humanizeEndpoint,
humanizePermissionKey,
normalizePermissionCatalog,
sanitizePermissionDomId,
} from "../../src/views/dashboards/superUserDashboard/roles/rolePermissionCatalog.js";
const catalog = {
"/orders": {
GET: {
list_orders: "List orders",
},
POST: {
add_order: "Add an order",
},
},
"/admin/bookings": {
POST: {
create_bookings: "Create bookings",
},
},
"/superuser/system/replication": {
GET: {
superuser_replication_view: "View replication status",
},
},
};
const departments = [
{
id: 7,
name: "Copenhagen",
},
];
const permissionOverrides = {
user: {
label: "User",
description: "Allows access to user dashboard pages.",
groupKey: "access",
},
admin: {
label: "Admin",
description: "Allows access to admin dashboard pages.",
groupKey: "access",
},
superuser: {
label: "Superuser",
description: "Allows access to superuser dashboard pages.",
groupKey: "access",
},
department_access_7: {
label: "Copenhagen",
description: "Allows access to Copenhagen.",
groupKey: "access",
departmentId: 7,
},
};
describe("role permission catalog helpers", () => {
it("normalizes route permissions and virtual access permissions", () => {
const records = normalizePermissionCatalog(catalog, {
departments,
permissionOverrides,
});
expect(records.map((record) => record.permission)).toEqual(
expect.arrayContaining([
"user",
"admin",
"superuser",
"department_access_7",
"list_orders",
"add_order",
"create_bookings",
"superuser_replication_view",
])
);
expect(records.find((record) => record.permission === "department_access_7")).toMatchObject({
groupKey: "access",
label: "Copenhagen",
description: "Allows access to Copenhagen.",
isVirtual: true,
});
expect(records.find((record) => record.permission === "list_orders")).toMatchObject({
groupKey: "orders_pos",
label: "List orders",
description: "List orders",
endpoint: "/orders",
endpointLabel: "Orders",
method: "GET",
});
expect(records.find((record) => record.permission === "create_bookings")).toMatchObject({
groupKey: "bookings",
});
expect(records.find((record) => record.permission === "superuser_replication_view")).toMatchObject({
groupKey: "system_config",
});
});
it("searches labels, descriptions, keys, endpoints, and methods", () => {
const records = normalizePermissionCatalog(catalog, {
departments,
permissionOverrides,
});
expect(filterPermissionRecords(records, "copenhagen").map((record) => record.permission)).toEqual([
"department_access_7",
]);
expect(filterPermissionRecords(records, "POST").map((record) => record.permission)).toEqual(
expect.arrayContaining(["add_order", "create_bookings"])
);
expect(filterPermissionRecords(records, "replication status").map((record) => record.permission)).toEqual([
"superuser_replication_view",
]);
});
it("groups records by stable group key", () => {
const grouped = groupPermissionRecords(
normalizePermissionCatalog(catalog, {
departments,
permissionOverrides,
})
);
expect(grouped.get("access").map((record) => record.permission)).toEqual(
expect.arrayContaining(["user", "admin", "superuser", "department_access_7"])
);
expect(grouped.get("orders_pos").map((record) => record.permission)).toEqual(
expect.arrayContaining(["list_orders", "add_order"])
);
expect(grouped.get("bookings").map((record) => record.permission)).toEqual(["create_bookings"]);
});
it("calculates bulk add and remove deltas without no-op requests", () => {
const records = normalizePermissionCatalog(catalog, {
departments,
permissionOverrides,
}).filter((record) => ["list_orders", "add_order"].includes(record.permission));
expect(getPermissionDelta(records, new Set(["list_orders"]), true)).toEqual({
add: ["add_order"],
remove: [],
});
expect(getPermissionDelta(records, new Set(["list_orders"]), false)).toEqual({
add: [],
remove: ["list_orders"],
});
});
it("humanizes and sanitizes permission display values", () => {
expect(humanizePermissionKey("superuser_replication_view")).toBe("Superuser replication view");
expect(humanizeEndpoint("/superuser/system/replication")).toBe("Superuser system replication");
expect(sanitizePermissionDomId("department/access 7")).toBe("department-access-7");
});
});
@@ -0,0 +1,152 @@
// @vitest-environment jsdom
import { nextTick } from "vue";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { mountWithApp } from "./helpers/mountWithApp.js";
const sessionMocks = vi.hoisted(() => ({
addVehicle: vi.fn(),
parseErrorMessage: vi.fn(),
vehicleTypeOptions: vi.fn(),
}));
vi.mock("@/components/session/token/SessionUser.vue", () => {
const sessionUser = {
functions: {
parseErrorMessage: sessionMocks.parseErrorMessage,
},
objects: {
vehicles: {
add: sessionMocks.addVehicle,
columns: {
type: {
options: sessionMocks.vehicleTypeOptions,
},
},
},
},
};
return {
SessionUser: sessionUser,
default: sessionUser,
};
});
import SuperuserAddVehicleModal from "@/components/forms/superUser/SuperuserAddVehicleModal.vue";
const CustomerSearchSelectStub = {
props: ["modelValue"],
emits: ["update:modelValue"],
template: `
<button
type="button"
data-testid="customer-select-stub"
@click="$emit('update:modelValue', { customerNumber: 12345679, name: 'Acme Transport' })"
>
Select customer
</button>
`,
};
const messages = {
en: {
vehicles: {
add_modal: {
customer_label: "Customer",
customer_placeholder: "Search customers",
error: "Unable to add vehicle.",
reference_placeholder: "Optional reference",
registration_placeholder: "Registration number",
selected_customer: "Selected customer",
submit: "Add vehicle",
title: "Add vehicle",
type_load_error: "Unable to load vehicle types.",
type_placeholder: "Select vehicle type",
validation_error: "Select required fields.",
},
form: {
license_plate: "Registration",
type: "Type",
},
},
objects: {
vehicles: {
columns: {
wash_subscription: "Wash subscription",
},
},
},
common: {
cancel: "Cancel",
close: "Close",
reference: "Reference",
},
},
};
const flushAll = async () => {
await Promise.resolve();
await nextTick();
await Promise.resolve();
await nextTick();
};
const mountComponent = () =>
mountWithApp(SuperuserAddVehicleModal, {
messages,
global: {
stubs: {
CustomerSearchSelect: CustomerSearchSelectStub,
},
},
});
describe("SuperuserAddVehicleModal", () => {
beforeEach(() => {
vi.clearAllMocks();
sessionMocks.vehicleTypeOptions.mockResolvedValue([
{
id: 53,
name: "Forvogn",
},
]);
sessionMocks.addVehicle.mockResolvedValue({
data: {
data: {
id: 7002,
},
},
});
sessionMocks.parseErrorMessage.mockReturnValue(null);
});
it("submits the selected customer and vehicle fields through the vehicle API", async () => {
const wrapper = mountComponent();
await flushAll();
await wrapper.get('[data-testid="customer-select-stub"]').trigger("click");
await wrapper.get('[data-testid="superuser-add-vehicle-registration"]').setValue("ab12345");
await wrapper.get('[data-testid="superuser-add-vehicle-type"]').setValue("53");
await wrapper.get('[data-testid="superuser-add-vehicle-wash-subscription"]').setValue(true);
await wrapper.get('[data-testid="superuser-add-vehicle-reference"]').setValue("Fleet ref");
await wrapper.get('[data-testid="superuser-add-vehicle-submit"]').trigger("click");
await flushAll();
expect(sessionMocks.addVehicle).toHaveBeenCalledWith(53, "AB12345", true, 12345679, "Fleet ref");
expect(wrapper.emitted("created")).toHaveLength(1);
});
it("keeps submit disabled until required fields are present", async () => {
const wrapper = mountComponent();
await flushAll();
expect(wrapper.get('[data-testid="superuser-add-vehicle-submit"]').attributes("disabled")).toBeDefined();
await wrapper.get('[data-testid="customer-select-stub"]').trigger("click");
await wrapper.get('[data-testid="superuser-add-vehicle-registration"]').setValue("AB12345");
await wrapper.get('[data-testid="superuser-add-vehicle-type"]').setValue("53");
await nextTick();
expect(wrapper.get('[data-testid="superuser-add-vehicle-submit"]').attributes("disabled")).toBeUndefined();
});
});

Some files were not shown because too many files have changed in this diff Show More