Enhance subuser management contact and action flows
This commit is contained in:
@@ -4,6 +4,7 @@ import { useSlots } from "vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||
import ActionSettingsWheelToggleItem from "@/components/displays/buttons/ActionSettingsWheelToggleItem.vue";
|
||||
import ActionSettingsWheelSelectItem from "@/components/displays/buttons/ActionSettingsWheelSelectItem.vue";
|
||||
import Swal from "sweetalert2";
|
||||
import { ref } from "vue";
|
||||
import CustomerModal from "@/components/displays/modals/CustomerModal.vue";
|
||||
@@ -143,6 +144,7 @@ const isDesktopFlyoutLayout = ref(false);
|
||||
const isFixedPosition = ref(false);
|
||||
const fixedPositionStyles = ref({});
|
||||
const activeDesktopFlyoutSectionKey = ref(null);
|
||||
const activeDesktopFlyoutSubsectionKey = ref(null);
|
||||
const activeAttachmentId = ref(null);
|
||||
const previewLoadingAttachmentId = ref(null);
|
||||
const deletingAttachmentId = ref(null);
|
||||
@@ -155,6 +157,7 @@ let dropdownLayoutUpdateId = 0;
|
||||
const desktopFlyoutMinViewportWidth = 1400;
|
||||
const desktopFlyoutRootPanelWidthRem = 15;
|
||||
const desktopFlyoutSubmenuWidthRem = 17;
|
||||
const desktopFlyoutNestedPanelWidthRem = 17;
|
||||
const desktopFlyoutPanelGapPx = 12;
|
||||
|
||||
const closeDropdown = () => {
|
||||
@@ -223,11 +226,22 @@ const dropdownMenuStyle = computed(() => {
|
||||
});
|
||||
const dropdownContentStyle = computed(() => {
|
||||
if (isFixedPosition.value) {
|
||||
return {
|
||||
const fixedStyles = {
|
||||
maxHeight: dropdownMaxHeight.value ? `${dropdownMaxHeight.value}px` : "95vh",
|
||||
overflowY: "auto",
|
||||
overscrollBehavior: "contain",
|
||||
};
|
||||
|
||||
if (isDesktopFlyoutLayout.value) {
|
||||
return {
|
||||
...fixedStyles,
|
||||
overflow: "visible",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...fixedStyles,
|
||||
overflowY: "auto",
|
||||
};
|
||||
}
|
||||
|
||||
if (!dropdownMaxHeight.value) {
|
||||
@@ -1605,12 +1619,26 @@ const buildMenuToggleAction = (key, config) => ({
|
||||
...config,
|
||||
});
|
||||
|
||||
const buildMenuSelectAction = (key, config) => ({
|
||||
key,
|
||||
disabled: false,
|
||||
options: [],
|
||||
type: "select",
|
||||
...config,
|
||||
});
|
||||
|
||||
const buildMenuLabel = (key, config) => ({
|
||||
key,
|
||||
type: "label",
|
||||
...config,
|
||||
});
|
||||
|
||||
const buildMenuSubsection = (key, config) => ({
|
||||
key,
|
||||
type: "subsection",
|
||||
...config,
|
||||
});
|
||||
|
||||
const buildMenuSection = (key, label, items) => {
|
||||
const visibleItems = items.filter(Boolean);
|
||||
if (visibleItems.length === 0) {
|
||||
@@ -1625,7 +1653,41 @@ const buildMenuSection = (key, label, items) => {
|
||||
};
|
||||
|
||||
const isToggleMenuItem = (item) => item?.type === "toggle";
|
||||
const isSelectMenuItem = (item) => item?.type === "select";
|
||||
const isLabelMenuItem = (item) => item?.type === "label";
|
||||
const isSubsectionMenuItem = (item) => item?.type === "subsection";
|
||||
const getSubsectionMenuItems = (section) => (section?.items || []).filter((item) => isSubsectionMenuItem(item));
|
||||
const hasSubsectionMenuItems = (section) => getSubsectionMenuItems(section).length > 0;
|
||||
|
||||
const normalizeExternalMenuItem = (item, itemKey) => {
|
||||
if (!item || item.visible === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isToggleMenuItem(item)) {
|
||||
return buildMenuToggleAction(itemKey, item);
|
||||
}
|
||||
|
||||
if (isSelectMenuItem(item)) {
|
||||
return buildMenuSelectAction(itemKey, item);
|
||||
}
|
||||
|
||||
if (isLabelMenuItem(item)) {
|
||||
return buildMenuLabel(itemKey, item);
|
||||
}
|
||||
|
||||
if (isSubsectionMenuItem(item)) {
|
||||
const children = Array.isArray(item.items) ? item.items : [];
|
||||
return buildMenuSubsection(itemKey, {
|
||||
...item,
|
||||
items: children
|
||||
.map((child, childIndex) => normalizeExternalMenuItem(child, String(child?.key || `${itemKey}-item-${childIndex}`)))
|
||||
.filter(Boolean),
|
||||
});
|
||||
}
|
||||
|
||||
return buildMenuAction(itemKey, item);
|
||||
};
|
||||
|
||||
const externalMenuSections = computed(() =>
|
||||
props.menuSections
|
||||
@@ -1637,21 +1699,8 @@ const externalMenuSections = computed(() =>
|
||||
sectionKey,
|
||||
section?.label || sectionKey,
|
||||
items.map((item, itemIndex) => {
|
||||
if (!item || item.visible === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const itemKey = String(item.key || `${sectionKey}-item-${itemIndex}`);
|
||||
|
||||
if (isToggleMenuItem(item)) {
|
||||
return buildMenuToggleAction(itemKey, item);
|
||||
}
|
||||
|
||||
if (isLabelMenuItem(item)) {
|
||||
return buildMenuLabel(itemKey, item);
|
||||
}
|
||||
|
||||
return buildMenuAction(itemKey, item);
|
||||
const itemKey = String(item?.key || `${sectionKey}-item-${itemIndex}`);
|
||||
return normalizeExternalMenuItem(item, itemKey);
|
||||
})
|
||||
);
|
||||
})
|
||||
@@ -2502,6 +2551,7 @@ const desktopFlyoutMenuSections = computed(() => {
|
||||
watch(
|
||||
[
|
||||
activeDesktopFlyoutSectionKey,
|
||||
activeDesktopFlyoutSubsectionKey,
|
||||
activeAttachmentId,
|
||||
isDesktopFlyoutLayout,
|
||||
desktopFlyoutMenuSections,
|
||||
@@ -2525,13 +2575,40 @@ const activeDesktopFlyoutSection = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const activeDesktopFlyoutSubsection = computed(() => {
|
||||
const subsectionItems = getSubsectionMenuItems(activeDesktopFlyoutSection.value);
|
||||
|
||||
if (subsectionItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
subsectionItems.find((item) => item.key === activeDesktopFlyoutSubsectionKey.value) ??
|
||||
subsectionItems[0] ??
|
||||
null
|
||||
);
|
||||
});
|
||||
const hasDesktopFlyoutSubsections = computed(() =>
|
||||
desktopFlyoutMenuSections.value.some((section) => hasSubsectionMenuItems(section))
|
||||
);
|
||||
|
||||
const hasDropdownContent = computed(
|
||||
() => hasCustomActionsSlot.value || flatBuiltInMenuSections.value.length > 0 || standaloneMenuActions.value.length > 0
|
||||
);
|
||||
|
||||
const getDesktopFlyoutEstimatedWidth = () => {
|
||||
const rootFontSize = Number.parseFloat(window.getComputedStyle(document.documentElement).fontSize || "16") || 16;
|
||||
return (desktopFlyoutRootPanelWidthRem + desktopFlyoutSubmenuWidthRem) * rootFontSize + desktopFlyoutPanelGapPx;
|
||||
const panelWidthRem =
|
||||
desktopFlyoutRootPanelWidthRem +
|
||||
desktopFlyoutSubmenuWidthRem +
|
||||
(hasDesktopFlyoutSubsections.value ? desktopFlyoutNestedPanelWidthRem : 0);
|
||||
const panelGapCount = hasDesktopFlyoutSubsections.value ? 2 : 1;
|
||||
|
||||
return panelWidthRem * rootFontSize + desktopFlyoutPanelGapPx * panelGapCount;
|
||||
};
|
||||
|
||||
const setActiveDesktopFlyoutSubsectionForSection = (section) => {
|
||||
activeDesktopFlyoutSubsectionKey.value = getSubsectionMenuItems(section)[0]?.key ?? null;
|
||||
};
|
||||
|
||||
const syncDesktopFlyoutState = (triggerRect) => {
|
||||
@@ -2554,6 +2631,7 @@ const syncDesktopFlyoutState = (triggerRect) => {
|
||||
|
||||
if (!nextDesktopFlyoutState) {
|
||||
activeDesktopFlyoutSectionKey.value = null;
|
||||
activeDesktopFlyoutSubsectionKey.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2563,17 +2641,25 @@ const syncDesktopFlyoutState = (triggerRect) => {
|
||||
|
||||
if (!currentSectionStillExists) {
|
||||
activeDesktopFlyoutSectionKey.value = desktopFlyoutMenuSections.value[0]?.key ?? null;
|
||||
setActiveDesktopFlyoutSubsectionForSection(desktopFlyoutMenuSections.value[0] ?? null);
|
||||
}
|
||||
};
|
||||
|
||||
const setActiveDesktopFlyoutSection = (sectionKey) => {
|
||||
activeDesktopFlyoutSectionKey.value = sectionKey;
|
||||
setActiveDesktopFlyoutSubsectionForSection(
|
||||
desktopFlyoutMenuSections.value.find((section) => section.key === sectionKey) ?? null
|
||||
);
|
||||
|
||||
if (sectionKey !== "attachments") {
|
||||
activeAttachmentId.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const setActiveDesktopFlyoutSubsection = (subsectionKey) => {
|
||||
activeDesktopFlyoutSubsectionKey.value = subsectionKey;
|
||||
};
|
||||
|
||||
const isOverflowClippingStyle = (style) =>
|
||||
["overflow", "overflowX", "overflowY"].some((property) => {
|
||||
const value = style[property];
|
||||
@@ -2694,10 +2780,60 @@ const syncDesktopFlyoutPosition = () => {
|
||||
<template v-for="section in directBuiltInMenuSections" :key="section.key">
|
||||
<ActionSettingsWheelItemLabel :label="section.label" />
|
||||
<template v-for="item in section.items" :key="item.key">
|
||||
<div
|
||||
v-if="isSubsectionMenuItem(item)"
|
||||
class="action-settings-wheel-subsection"
|
||||
:data-testid="item.testId || `action-settings-wheel-subsection-${item.key}`"
|
||||
>
|
||||
<p class="action-settings-wheel-subsection__title">{{ item.label }}</p>
|
||||
<p v-if="item.description" class="action-settings-wheel-subsection__description">{{ item.description }}</p>
|
||||
<template v-for="subItem in item.items || []" :key="subItem.key">
|
||||
<ActionSettingsWheelItemLabel
|
||||
v-if="isLabelMenuItem(subItem)"
|
||||
:icon="subItem.icon"
|
||||
:label="subItem.label"
|
||||
:detail-value="subItem.detailValue"
|
||||
:appearance="subItem.appearance"
|
||||
:template="subItem.template"
|
||||
:data-testid="subItem.testId || undefined"
|
||||
/>
|
||||
<ActionSettingsWheelToggleItem
|
||||
v-else-if="isToggleMenuItem(subItem)"
|
||||
:checked="subItem.value"
|
||||
:click-action="subItem.clickAction"
|
||||
:description="subItem.description"
|
||||
:disabled="subItem.disabled"
|
||||
:label="subItem.label"
|
||||
:test-id="subItem.testId"
|
||||
/>
|
||||
<ActionSettingsWheelSelectItem
|
||||
v-else-if="isSelectMenuItem(subItem)"
|
||||
:icon="subItem.icon"
|
||||
:label="subItem.label"
|
||||
:value="subItem.value"
|
||||
:options="subItem.options"
|
||||
:change-action="subItem.changeAction"
|
||||
:disabled="subItem.disabled"
|
||||
:test-id="subItem.testId"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
v-else
|
||||
:icon="subItem.icon"
|
||||
:label="subItem.label"
|
||||
:detail-value="subItem.detailValue"
|
||||
:template="subItem.template"
|
||||
:click-action="subItem.clickAction"
|
||||
:disabled="subItem.disabled"
|
||||
:test-id="subItem.testId"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<ActionSettingsWheelItemLabel
|
||||
v-if="isLabelMenuItem(item)"
|
||||
v-else-if="isLabelMenuItem(item)"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:detail-value="item.detailValue"
|
||||
:appearance="item.appearance"
|
||||
:template="item.template"
|
||||
:data-testid="item.testId || undefined"
|
||||
/>
|
||||
@@ -2722,10 +2858,21 @@ const syncDesktopFlyoutPosition = () => {
|
||||
</CustomerRuleTooltip>
|
||||
</template>
|
||||
</ActionSettingsWheelToggleItem>
|
||||
<ActionSettingsWheelSelectItem
|
||||
v-else-if="isSelectMenuItem(item)"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
:options="item.options"
|
||||
:change-action="item.changeAction"
|
||||
:disabled="item.disabled"
|
||||
:test-id="item.testId"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
v-else
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:detail-value="item.detailValue"
|
||||
:template="item.template"
|
||||
:click-action="item.clickAction"
|
||||
:disabled="item.disabled"
|
||||
@@ -2738,6 +2885,7 @@ const syncDesktopFlyoutPosition = () => {
|
||||
:key="item.key"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:detail-value="item.detailValue"
|
||||
:template="item.template"
|
||||
:click-action="item.clickAction"
|
||||
:disabled="item.disabled"
|
||||
@@ -2771,6 +2919,62 @@ const syncDesktopFlyoutPosition = () => {
|
||||
<template v-if="hasDropdownContent">
|
||||
<template v-if="isDesktopFlyoutLayout && desktopFlyoutMenuSections.length > 0">
|
||||
<div class="action-settings-wheel-flyout" data-testid="action-settings-wheel-flyout">
|
||||
<div
|
||||
v-if="activeDesktopFlyoutSubsection"
|
||||
class="action-settings-wheel-nested-panel"
|
||||
:data-testid="
|
||||
activeDesktopFlyoutSubsection.testId
|
||||
? `${activeDesktopFlyoutSubsection.testId}-panel`
|
||||
: `action-settings-wheel-subsection-panel-${activeDesktopFlyoutSubsection.key}`
|
||||
"
|
||||
>
|
||||
<p class="action-settings-wheel-nested-panel__title">{{ activeDesktopFlyoutSubsection.label }}</p>
|
||||
<p v-if="activeDesktopFlyoutSubsection.description" class="action-settings-wheel-nested-panel__description">
|
||||
{{ activeDesktopFlyoutSubsection.description }}
|
||||
</p>
|
||||
<div class="action-settings-wheel-nested-panel__items">
|
||||
<template v-for="subItem in activeDesktopFlyoutSubsection.items || []" :key="subItem.key">
|
||||
<ActionSettingsWheelItemLabel
|
||||
v-if="isLabelMenuItem(subItem)"
|
||||
:icon="subItem.icon"
|
||||
:label="subItem.label"
|
||||
:detail-value="subItem.detailValue"
|
||||
:appearance="subItem.appearance"
|
||||
:template="subItem.template"
|
||||
:data-testid="subItem.testId || undefined"
|
||||
/>
|
||||
<ActionSettingsWheelToggleItem
|
||||
v-else-if="isToggleMenuItem(subItem)"
|
||||
:checked="subItem.value"
|
||||
:click-action="subItem.clickAction"
|
||||
:description="subItem.description"
|
||||
:disabled="subItem.disabled"
|
||||
:label="subItem.label"
|
||||
:test-id="subItem.testId"
|
||||
/>
|
||||
<ActionSettingsWheelSelectItem
|
||||
v-else-if="isSelectMenuItem(subItem)"
|
||||
:icon="subItem.icon"
|
||||
:label="subItem.label"
|
||||
:value="subItem.value"
|
||||
:options="subItem.options"
|
||||
:change-action="subItem.changeAction"
|
||||
:disabled="subItem.disabled"
|
||||
:test-id="subItem.testId"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
v-else
|
||||
:icon="subItem.icon"
|
||||
:label="subItem.label"
|
||||
:detail-value="subItem.detailValue"
|
||||
:template="subItem.template"
|
||||
:click-action="subItem.clickAction"
|
||||
:disabled="subItem.disabled"
|
||||
:test-id="subItem.testId"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-settings-wheel-flyout__submenu-stack">
|
||||
<div
|
||||
v-for="section in desktopFlyoutMenuSections"
|
||||
@@ -2780,6 +2984,7 @@ const syncDesktopFlyoutPosition = () => {
|
||||
'is-active': activeDesktopFlyoutSection?.key === section.key,
|
||||
'action-settings-wheel-flyout__submenu--attachments': section.key === 'attachments',
|
||||
'action-settings-wheel-flyout__submenu--rules': section.key === 'rules',
|
||||
'action-settings-wheel-flyout__submenu--has-nested': hasSubsectionMenuItems(section),
|
||||
}"
|
||||
:data-testid="`action-settings-wheel-submenu-${section.key}`"
|
||||
>
|
||||
@@ -2811,53 +3016,86 @@ const syncDesktopFlyoutPosition = () => {
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-else>
|
||||
<template v-for="item in section.items" :key="item.key">
|
||||
<ActionSettingsWheelItemLabel
|
||||
v-if="isLabelMenuItem(item)"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:template="item.template"
|
||||
:data-testid="item.testId || undefined"
|
||||
/>
|
||||
<ActionSettingsWheelToggleItem
|
||||
v-else-if="isToggleMenuItem(item)"
|
||||
:checked="item.value"
|
||||
:click-action="item.clickAction"
|
||||
:description="item.description"
|
||||
:disabled="item.disabled"
|
||||
:label="item.label"
|
||||
:test-id="item.testId"
|
||||
>
|
||||
<template v-if="item.customerRuleAttribute" #label="{ label }">
|
||||
<CustomerRuleTooltip
|
||||
:attribute="item.customerRuleAttribute"
|
||||
:active="item.customerRuleActive"
|
||||
:customer-number="item.customerRuleCustomerNumber"
|
||||
:department-id="item.customerRuleDepartmentId"
|
||||
:test-id="`${item.testId}-tooltip`"
|
||||
>
|
||||
<span>{{ label }}</span>
|
||||
</CustomerRuleTooltip>
|
||||
</template>
|
||||
</ActionSettingsWheelToggleItem>
|
||||
<ActionSettingsWheelItem
|
||||
v-else
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:template="item.template"
|
||||
:click-action="item.clickAction"
|
||||
:disabled="item.disabled"
|
||||
:test-id="item.testId"
|
||||
/>
|
||||
<button
|
||||
v-if="isSubsectionMenuItem(item)"
|
||||
type="button"
|
||||
class="action-settings-wheel-section-trigger action-settings-wheel-section-trigger--nested"
|
||||
:class="{ 'is-active': activeDesktopFlyoutSubsection?.key === item.key }"
|
||||
:data-testid="item.testId || `action-settings-wheel-subsection-${item.key}`"
|
||||
@mouseenter="setActiveDesktopFlyoutSubsection(item.key)"
|
||||
@focus="setActiveDesktopFlyoutSubsection(item.key)"
|
||||
@click.stop.prevent="setActiveDesktopFlyoutSubsection(item.key)"
|
||||
>
|
||||
<span class="action-settings-wheel-section-trigger__arrow">
|
||||
<i class="fas fa-chevron-left" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="action-settings-wheel-section-trigger__text">
|
||||
<span class="action-settings-wheel-section-trigger__label">{{ item.label }}</span>
|
||||
<span v-if="item.description" class="action-settings-wheel-section-trigger__description">
|
||||
{{ item.description }}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<ActionSettingsWheelItemLabel
|
||||
v-else-if="isLabelMenuItem(item)"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:detail-value="item.detailValue"
|
||||
:appearance="item.appearance"
|
||||
:template="item.template"
|
||||
:data-testid="item.testId || undefined"
|
||||
/>
|
||||
<ActionSettingsWheelToggleItem
|
||||
v-else-if="isToggleMenuItem(item)"
|
||||
:checked="item.value"
|
||||
:click-action="item.clickAction"
|
||||
:description="item.description"
|
||||
:disabled="item.disabled"
|
||||
:label="item.label"
|
||||
:test-id="item.testId"
|
||||
>
|
||||
<template v-if="item.customerRuleAttribute" #label="{ label }">
|
||||
<CustomerRuleTooltip
|
||||
:attribute="item.customerRuleAttribute"
|
||||
:active="item.customerRuleActive"
|
||||
:customer-number="item.customerRuleCustomerNumber"
|
||||
:department-id="item.customerRuleDepartmentId"
|
||||
:test-id="`${item.testId}-tooltip`"
|
||||
>
|
||||
<span>{{ label }}</span>
|
||||
</CustomerRuleTooltip>
|
||||
</template>
|
||||
</ActionSettingsWheelToggleItem>
|
||||
<ActionSettingsWheelSelectItem
|
||||
v-else-if="isSelectMenuItem(item)"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
:options="item.options"
|
||||
:change-action="item.changeAction"
|
||||
:disabled="item.disabled"
|
||||
:test-id="item.testId"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
v-else
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:detail-value="item.detailValue"
|
||||
:template="item.template"
|
||||
:click-action="item.clickAction"
|
||||
:disabled="item.disabled"
|
||||
:test-id="item.testId"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="section.key === 'attachments' && activeDesktopFlyoutSection?.key === 'attachments' && activeAttachment"
|
||||
class="action-settings-wheel-attachment-panel"
|
||||
data-testid="action-settings-wheel-attachment-panel"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="section.key === 'attachments' && activeDesktopFlyoutSection?.key === 'attachments' && activeAttachment"
|
||||
class="action-settings-wheel-attachment-panel"
|
||||
data-testid="action-settings-wheel-attachment-panel"
|
||||
>
|
||||
<p class="action-settings-wheel-attachment-panel__title" :title="getAttachmentLabel(activeAttachment)">
|
||||
{{ getAttachmentLabel(activeAttachment) }}
|
||||
</p>
|
||||
@@ -2987,6 +3225,7 @@ const syncDesktopFlyoutPosition = () => {
|
||||
:key="item.key"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:detail-value="item.detailValue"
|
||||
:template="item.template"
|
||||
:click-action="item.clickAction"
|
||||
:disabled="item.disabled"
|
||||
@@ -3001,10 +3240,60 @@ const syncDesktopFlyoutPosition = () => {
|
||||
<template v-for="section in flatBuiltInMenuSections" :key="section.key">
|
||||
<ActionSettingsWheelItemLabel :label="section.label" :data-testid="`action-settings-wheel-section-${section.key}`" />
|
||||
<template v-for="item in section.items" :key="item.key">
|
||||
<div
|
||||
v-if="isSubsectionMenuItem(item)"
|
||||
class="action-settings-wheel-subsection"
|
||||
:data-testid="item.testId || `action-settings-wheel-subsection-${item.key}`"
|
||||
>
|
||||
<p class="action-settings-wheel-subsection__title">{{ item.label }}</p>
|
||||
<p v-if="item.description" class="action-settings-wheel-subsection__description">{{ item.description }}</p>
|
||||
<template v-for="subItem in item.items || []" :key="subItem.key">
|
||||
<ActionSettingsWheelItemLabel
|
||||
v-if="isLabelMenuItem(subItem)"
|
||||
:icon="subItem.icon"
|
||||
:label="subItem.label"
|
||||
:detail-value="subItem.detailValue"
|
||||
:appearance="subItem.appearance"
|
||||
:template="subItem.template"
|
||||
:data-testid="subItem.testId || undefined"
|
||||
/>
|
||||
<ActionSettingsWheelToggleItem
|
||||
v-else-if="isToggleMenuItem(subItem)"
|
||||
:checked="subItem.value"
|
||||
:click-action="subItem.clickAction"
|
||||
:description="subItem.description"
|
||||
:disabled="subItem.disabled"
|
||||
:label="subItem.label"
|
||||
:test-id="subItem.testId"
|
||||
/>
|
||||
<ActionSettingsWheelSelectItem
|
||||
v-else-if="isSelectMenuItem(subItem)"
|
||||
:icon="subItem.icon"
|
||||
:label="subItem.label"
|
||||
:value="subItem.value"
|
||||
:options="subItem.options"
|
||||
:change-action="subItem.changeAction"
|
||||
:disabled="subItem.disabled"
|
||||
:test-id="subItem.testId"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
v-else
|
||||
:icon="subItem.icon"
|
||||
:label="subItem.label"
|
||||
:detail-value="subItem.detailValue"
|
||||
:template="subItem.template"
|
||||
:click-action="subItem.clickAction"
|
||||
:disabled="subItem.disabled"
|
||||
:test-id="subItem.testId"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<ActionSettingsWheelItemLabel
|
||||
v-if="isLabelMenuItem(item)"
|
||||
v-else-if="isLabelMenuItem(item)"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:detail-value="item.detailValue"
|
||||
:appearance="item.appearance"
|
||||
:template="item.template"
|
||||
:data-testid="item.testId || undefined"
|
||||
/>
|
||||
@@ -3029,10 +3318,21 @@ const syncDesktopFlyoutPosition = () => {
|
||||
</CustomerRuleTooltip>
|
||||
</template>
|
||||
</ActionSettingsWheelToggleItem>
|
||||
<ActionSettingsWheelSelectItem
|
||||
v-else-if="isSelectMenuItem(item)"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
:options="item.options"
|
||||
:change-action="item.changeAction"
|
||||
:disabled="item.disabled"
|
||||
:test-id="item.testId"
|
||||
/>
|
||||
<ActionSettingsWheelItem
|
||||
v-else
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:detail-value="item.detailValue"
|
||||
:template="item.template"
|
||||
:click-action="item.clickAction"
|
||||
:disabled="item.disabled"
|
||||
@@ -3045,6 +3345,7 @@ const syncDesktopFlyoutPosition = () => {
|
||||
:key="item.key"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:detail-value="item.detailValue"
|
||||
:template="item.template"
|
||||
:click-action="item.clickAction"
|
||||
:disabled="item.disabled"
|
||||
@@ -3131,7 +3432,7 @@ const syncDesktopFlyoutPosition = () => {
|
||||
.action-settings-wheel-flyout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
gap: 0.75rem;
|
||||
min-width: 33rem;
|
||||
z-index: 4003;
|
||||
@@ -3194,10 +3495,50 @@ const syncDesktopFlyoutPosition = () => {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.action-settings-wheel-flyout__submenu--has-nested {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.action-settings-wheel-flyout__submenu--rules {
|
||||
min-width: 18.75rem;
|
||||
}
|
||||
|
||||
.action-settings-wheel-subsection {
|
||||
border-top: 1px solid #e4ebf3;
|
||||
margin-top: 0.25rem;
|
||||
padding-top: 0.35rem;
|
||||
}
|
||||
|
||||
.action-settings-wheel-subsection:first-child {
|
||||
border-top: 0;
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.action-settings-wheel-subsection__title {
|
||||
color: #627187;
|
||||
font-size: 0.73rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 0.45rem 0.75rem 0.2rem;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-settings-wheel-subsection__description {
|
||||
color: #7b8798;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
margin: -0.1rem 0 0.35rem;
|
||||
overflow: hidden;
|
||||
padding: 0 0.75rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-settings-wheel-flyout__standalone-actions {
|
||||
margin-top: 0.35rem;
|
||||
padding-top: 0.35rem;
|
||||
@@ -3249,6 +3590,18 @@ const syncDesktopFlyoutPosition = () => {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.action-settings-wheel-section-trigger--nested {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.action-settings-wheel-section-trigger__text {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.action-settings-wheel-section-trigger__label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -3259,6 +3612,55 @@ const syncDesktopFlyoutPosition = () => {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.action-settings-wheel-section-trigger__description {
|
||||
color: #718095;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-settings-wheel-nested-panel {
|
||||
align-self: stretch;
|
||||
flex: 0 0 17rem;
|
||||
width: 17rem;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d9e4ef;
|
||||
border-radius: 0.7rem;
|
||||
padding: 0.35rem;
|
||||
}
|
||||
|
||||
.action-settings-wheel-nested-panel__title {
|
||||
color: #132339;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 0.45rem 0.75rem 0.1rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-settings-wheel-nested-panel__description {
|
||||
color: #718095;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 0.75rem 0.45rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-settings-wheel-nested-panel__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.action-settings-wheel-attachment-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@@ -5,6 +5,10 @@ const props = defineProps({
|
||||
clickAction: Function,
|
||||
icon: String,
|
||||
label: String,
|
||||
detailValue: {
|
||||
type: [String, Number],
|
||||
default: "",
|
||||
},
|
||||
disabled: Boolean,
|
||||
testId: {
|
||||
type: String,
|
||||
@@ -107,6 +111,10 @@ const getLabel = () => {
|
||||
return props.label ?? 'Unavngivet handling';
|
||||
}
|
||||
|
||||
const hasDetailValue = () => {
|
||||
return props.detailValue !== undefined && props.detailValue !== null && String(props.detailValue).length > 0;
|
||||
}
|
||||
|
||||
const getStyle = () => {
|
||||
return styles[props.template] ?? styles.default;
|
||||
}
|
||||
@@ -140,18 +148,40 @@ const getLabelColor = () => {
|
||||
<span class="icon">
|
||||
<i :class="getIcon() + ' ' + getIconColor()"></i>
|
||||
</span>
|
||||
<span class="ml-1"
|
||||
<span class="dropdown-item-action__label ml-1"
|
||||
:class="getLabelColor()"
|
||||
>{{ getLabel() }}</span>
|
||||
<span v-if="hasDetailValue()" class="dropdown-item-action__detail">
|
||||
{{ props.detailValue }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dropdown-item-action {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown-item-action__label {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.dropdown-item-action__detail {
|
||||
color: #718095;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 600;
|
||||
margin-left: auto;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
padding-left: 0.75rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.is-disabled {
|
||||
|
||||
@@ -8,6 +8,14 @@ const props = defineProps({
|
||||
default: undefined,
|
||||
},
|
||||
label: String,
|
||||
detailValue: {
|
||||
type: [String, Number],
|
||||
default: "",
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: "default",
|
||||
},
|
||||
template: {
|
||||
type: String,
|
||||
default: 'default', // The style of the button (default, danger, success, warning, info, light)
|
||||
@@ -86,6 +94,10 @@ const getLabel = () => {
|
||||
return props.label ?? 'Unavngivet handling';
|
||||
}
|
||||
|
||||
const hasDetailValue = () => {
|
||||
return props.detailValue !== undefined && props.detailValue !== null && String(props.detailValue).length > 0;
|
||||
}
|
||||
|
||||
const getStyle = () => {
|
||||
return styles[props.template] ?? styles.default;
|
||||
}
|
||||
@@ -109,25 +121,55 @@ const getLabelColor = () => {
|
||||
const hasIcon = () => {
|
||||
return props.icon !== undefined && props.icon !== null;
|
||||
}
|
||||
|
||||
const isMetadataAppearance = () => props.appearance === "metadata";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a class="dropdown-item is-disabled">
|
||||
<span class="icon" v-if="hasIcon()">
|
||||
<i :class="getIcon() + ' ' + getIconColor()"></i>
|
||||
</span>
|
||||
<span class="ml-1"
|
||||
:class="getLabelColor()"
|
||||
>{{ SessionUser.functions.ucFirst(getLabel()) }}</span>
|
||||
<a
|
||||
class="dropdown-item dropdown-item-label is-disabled"
|
||||
:class="{ 'dropdown-item-label--metadata': isMetadataAppearance() }"
|
||||
>
|
||||
<span class="icon" v-if="hasIcon()">
|
||||
<i :class="getIcon() + ' ' + getIconColor()"></i>
|
||||
</span>
|
||||
<span class="dropdown-item-label__text ml-1" :class="getLabelColor()">
|
||||
{{ SessionUser.functions.ucFirst(getLabel()) }}
|
||||
</span>
|
||||
<span v-if="hasDetailValue()" class="dropdown-item-label__detail">
|
||||
{{ props.detailValue }}
|
||||
</span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.is-disabled {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
pointer-events: none;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dropdown-item-label__text {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dropdown-item-label__detail {
|
||||
color: #718095;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 600;
|
||||
margin-left: auto;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
padding-left: 0.75rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.is-disabled:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -135,4 +177,22 @@ const hasIcon = () => {
|
||||
.is-disabled .icon {
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
.dropdown-item-label--metadata {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dropdown-item-label--metadata .icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dropdown-item-label--metadata .dropdown-item-label__text {
|
||||
color: #4a5b73;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dropdown-item-label--metadata .dropdown-item-label__detail {
|
||||
color: #25344d;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
changeAction: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "fas fa-cog",
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
testId: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const root = ref(null);
|
||||
const normalizeSelectValue = (value) => String(value);
|
||||
const localValue = ref(normalizeSelectValue(props.value));
|
||||
const isProcessing = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(value) => {
|
||||
localValue.value = normalizeSelectValue(value);
|
||||
}
|
||||
);
|
||||
|
||||
const isDisabled = computed(() => props.disabled || isProcessing.value);
|
||||
|
||||
const optionKey = (option) => `${typeof option?.value}:${normalizeSelectValue(option?.value)}`;
|
||||
const getInputValue = (value) => value?.target?.value ?? value;
|
||||
|
||||
const onInput = async (value) => {
|
||||
const nextValue = normalizeSelectValue(getInputValue(value));
|
||||
|
||||
if (isDisabled.value || nextValue === normalizeSelectValue(props.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
isProcessing.value = true;
|
||||
try {
|
||||
if (props.changeAction) {
|
||||
await props.changeAction(nextValue);
|
||||
}
|
||||
root.value?.dispatchEvent(new CustomEvent("dropdown-action-selected", { bubbles: true }));
|
||||
} finally {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="root"
|
||||
class="dropdown-item action-settings-wheel-select-item"
|
||||
:class="{ 'is-disabled': isDisabled }"
|
||||
:data-testid="props.testId || undefined"
|
||||
>
|
||||
<span class="icon action-settings-wheel-select-item__icon">
|
||||
<i :class="props.icon"></i>
|
||||
</span>
|
||||
<span class="action-settings-wheel-select-item__label">
|
||||
{{ props.label }}
|
||||
</span>
|
||||
<b-select
|
||||
v-model="localValue"
|
||||
size="is-small"
|
||||
:disabled="isDisabled"
|
||||
:aria-label="props.label"
|
||||
:data-testid="props.testId ? `${props.testId}-select` : undefined"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
@update:modelValue="onInput"
|
||||
>
|
||||
<option v-for="option in props.options" :key="optionKey(option)" :value="normalizeSelectValue(option.value)">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</b-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.action-settings-wheel-select-item {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-settings-wheel-select-item__icon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.action-settings-wheel-select-item__label {
|
||||
color: #25344d;
|
||||
flex: 1 1 auto;
|
||||
font-weight: 500;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.action-settings-wheel-select-item :deep(.select),
|
||||
.action-settings-wheel-select-item :deep(select) {
|
||||
max-width: 9.5rem;
|
||||
}
|
||||
|
||||
.action-settings-wheel-select-item.is-disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
</style>
|
||||
@@ -111,7 +111,7 @@ onBeforeUnmount(() => {
|
||||
.pagination-other-filters {
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.pagination-other-filters__button {
|
||||
@@ -136,9 +136,10 @@ onBeforeUnmount(() => {
|
||||
padding: 0 0.3rem;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: -0.5rem;
|
||||
top: -0.25rem;
|
||||
z-index: 3;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transform: translate(50%, -50%);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.pagination-other-filters__menu {
|
||||
|
||||
+1
-1
@@ -587,7 +587,7 @@ watch(orderDateEventSignature, () => {
|
||||
justify-content: flex-end;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -278,8 +278,14 @@ const resetSessionState = () => {
|
||||
SessionUser.subuser.username.value = null;
|
||||
SessionUser.subuser.name.value = null;
|
||||
SessionUser.subuser.email.value = null;
|
||||
SessionUser.subuser.email_verified.value = false;
|
||||
SessionUser.subuser.email_verified_at.value = null;
|
||||
SessionUser.subuser.phone.country_code.value = null;
|
||||
SessionUser.subuser.phone.number.value = null;
|
||||
SessionUser.subuser.phone.verified.value = false;
|
||||
SessionUser.subuser.phone.verified_at.value = null;
|
||||
SessionUser.subuser.verification_state.value = null;
|
||||
SessionUser.subuser.verification.value = null;
|
||||
SessionUser.subuser.grants.value = [];
|
||||
SessionUser.subuser.created_at.value = null;
|
||||
SessionUser.subuser.updated_at.value = null;
|
||||
@@ -579,8 +585,14 @@ export const getSubuserSessionData = async () => {
|
||||
SessionUser.subuser.username.value = data.username;
|
||||
SessionUser.subuser.name.value = data.name;
|
||||
SessionUser.subuser.email.value = data.email;
|
||||
SessionUser.subuser.email_verified.value = Boolean(data.email_verified);
|
||||
SessionUser.subuser.email_verified_at.value = data.email_verified_at || null;
|
||||
SessionUser.subuser.phone.country_code.value = data.phone_country_code;
|
||||
SessionUser.subuser.phone.number.value = data.phone;
|
||||
SessionUser.subuser.phone.verified.value = Boolean(data.phone_verified);
|
||||
SessionUser.subuser.phone.verified_at.value = data.phone_verified_at || null;
|
||||
SessionUser.subuser.verification_state.value = data.verification_state || data.verification?.state || null;
|
||||
SessionUser.subuser.verification.value = data.verification || null;
|
||||
SessionUser.subuser.grants.value = data.grants || [];
|
||||
reconcileSelectedSubuserGrant(SessionUser.subuser.grants.value);
|
||||
SessionUser.subuser.created_at.value = data.created_at;
|
||||
@@ -819,10 +831,16 @@ export const SessionUser = {
|
||||
username: ref(null),
|
||||
name: ref(null),
|
||||
email: ref(null),
|
||||
email_verified: ref(false),
|
||||
email_verified_at: ref(null),
|
||||
phone: {
|
||||
country_code: ref(null),
|
||||
number: ref(null),
|
||||
verified: ref(false),
|
||||
verified_at: ref(null),
|
||||
},
|
||||
verification_state: ref(null),
|
||||
verification: ref(null),
|
||||
grants: ref([]),
|
||||
created_at: ref(null),
|
||||
updated_at: ref(null),
|
||||
|
||||
@@ -33,6 +33,31 @@ const absoluteLoginLink = (path) => {
|
||||
}
|
||||
};
|
||||
|
||||
const directLoginPayload = (context = {}) => ({
|
||||
...(context.customer_number ? { customer_number: context.customer_number } : {}),
|
||||
...(context.grant_id ? { grant_id: context.grant_id } : {}),
|
||||
});
|
||||
|
||||
const driverContactText = (key, values = undefined) =>
|
||||
t(`templates.generated.compat.superuser.driver_contact.${key}`, values);
|
||||
|
||||
const showDeliveryFeedback = async (response, options = {}) => {
|
||||
const payload = response?.data?.data || response?.data || {};
|
||||
const delivery = payload.delivery || {};
|
||||
const sent = delivery.status === "sent";
|
||||
|
||||
await Swal.fire({
|
||||
title: sent
|
||||
? (options.sentTitle || driverContactText("link_sent_title"))
|
||||
: (options.notSentTitle || driverContactText("link_not_sent_title")),
|
||||
text: delivery.message || (sent
|
||||
? driverContactText("link_sent_default")
|
||||
: driverContactText("link_not_sent_default")),
|
||||
icon: sent ? "success" : "warning",
|
||||
confirmButtonText: driverContactText("close"),
|
||||
});
|
||||
};
|
||||
|
||||
const copyText = async (text) => {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
try {
|
||||
@@ -315,10 +340,108 @@ export const Subusers = {
|
||||
return authenticatedRequest(`/superuser/subusers/${encodeURIComponent(subuserId)}/password`, "POST", { password });
|
||||
},
|
||||
async generateAdminLoginLink(subuserId, context = {}) {
|
||||
return authenticatedRequest(`/superuser/subusers/${encodeURIComponent(subuserId)}/login-link`, "POST", {
|
||||
...(context.customer_number ? { customer_number: context.customer_number } : {}),
|
||||
...(context.grant_id ? { grant_id: context.grant_id } : {}),
|
||||
});
|
||||
return authenticatedRequest(
|
||||
`/superuser/subusers/${encodeURIComponent(subuserId)}/login-link`,
|
||||
"POST",
|
||||
directLoginPayload(context)
|
||||
);
|
||||
},
|
||||
async setAdminVerificationState(subuserId, channel, verified) {
|
||||
try {
|
||||
return await authenticatedRequest(
|
||||
`/superuser/subusers/${encodeURIComponent(subuserId)}/verification/${encodeURIComponent(channel)}`,
|
||||
"PATCH",
|
||||
{ verified: Boolean(verified) }
|
||||
);
|
||||
} catch (error) {
|
||||
await Swal.fire({
|
||||
title: driverContactText("verification_update_failed_title"),
|
||||
text: apiErrorMessage(error, driverContactText("verification_update_failed")),
|
||||
icon: "error",
|
||||
confirmButtonText: driverContactText("close"),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async sendAdminPasswordGuideLink(subuserId, channel, context = {}) {
|
||||
try {
|
||||
const response = await authenticatedRequest(
|
||||
`/superuser/subusers/${encodeURIComponent(subuserId)}/password-guide/${encodeURIComponent(channel)}/send`,
|
||||
"POST",
|
||||
directLoginPayload(context)
|
||||
);
|
||||
await showDeliveryFeedback(response, {
|
||||
sentTitle: driverContactText("password_guide_sent_title"),
|
||||
notSentTitle: driverContactText("password_guide_not_sent_title"),
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
await Swal.fire({
|
||||
title: driverContactText("password_guide_failed_title"),
|
||||
text: apiErrorMessage(error, driverContactText("link_send_failed")),
|
||||
icon: "error",
|
||||
confirmButtonText: driverContactText("close"),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async sendAdminLoginLink(subuserId, channel, context = {}) {
|
||||
try {
|
||||
const response = await authenticatedRequest(
|
||||
`/superuser/subusers/${encodeURIComponent(subuserId)}/login-link/${encodeURIComponent(channel)}/send`,
|
||||
"POST",
|
||||
directLoginPayload(context)
|
||||
);
|
||||
await showDeliveryFeedback(response, {
|
||||
sentTitle: driverContactText("login_link_sent_title"),
|
||||
notSentTitle: driverContactText("login_link_not_sent_title"),
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
await Swal.fire({
|
||||
title: driverContactText("login_link_failed_title"),
|
||||
text: apiErrorMessage(error, driverContactText("link_send_failed")),
|
||||
icon: "error",
|
||||
confirmButtonText: driverContactText("close"),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async resendVerificationCode(subuser, channel, refreshCallback = null, options = {}) {
|
||||
const superuser = Boolean(options.superuser);
|
||||
const endpoint = superuser
|
||||
? `/superuser/subusers/${encodeURIComponent(subuser.id)}/verification/${encodeURIComponent(channel)}/send`
|
||||
: `/subusers/${encodeURIComponent(subuser.id)}/verification/${encodeURIComponent(channel)}/send`;
|
||||
|
||||
try {
|
||||
const response = await authenticatedRequest(endpoint, "POST");
|
||||
const payload = response?.data?.data || response?.data || {};
|
||||
const delivery = payload.delivery || {};
|
||||
const channelLabel = channel === "email" ? "e-mail" : "telefon";
|
||||
|
||||
await Swal.fire({
|
||||
title: delivery.status === "sent" ? "Kode sendt" : "Kode kunne ikke sendes",
|
||||
text: delivery.message || (delivery.status === "sent"
|
||||
? `Verifikationskode er sendt til chaufførens ${channelLabel}.`
|
||||
: `Verifikationskode kunne ikke sendes til chaufførens ${channelLabel}.`),
|
||||
icon: delivery.status === "sent" ? "success" : "warning",
|
||||
confirmButtonText: "Luk",
|
||||
});
|
||||
|
||||
if (typeof refreshCallback === "function") {
|
||||
await refreshCallback();
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
await Swal.fire({
|
||||
title: "Kode kunne ikke sendes",
|
||||
text: apiErrorMessage(error, "Der opstod en fejl."),
|
||||
icon: "error",
|
||||
confirmButtonText: "Luk",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async showEditNameForm(subuser, refreshCallback = null) {
|
||||
const result = await Swal.fire({
|
||||
|
||||
@@ -34,6 +34,14 @@ export const Config = {
|
||||
return Config.set("invoiceLayoutNumber", layout);
|
||||
},
|
||||
},
|
||||
invoiceDiscountLayoutNumber: {
|
||||
get: async () => {
|
||||
return Config.get("invoiceDiscountLayoutNumber");
|
||||
},
|
||||
set: async (layout) => {
|
||||
return Config.set("invoiceDiscountLayoutNumber", layout);
|
||||
},
|
||||
},
|
||||
},
|
||||
fees: {
|
||||
adminFeeMonthly: {
|
||||
|
||||
@@ -5626,6 +5626,38 @@
|
||||
"custom_summary": "Tilpasset: {summary}"
|
||||
}
|
||||
},
|
||||
"driver_contact": {
|
||||
"close": "Luk",
|
||||
"email_missing": "E-mail mangler",
|
||||
"link_not_sent_default": "Linket kunne ikke sendes.",
|
||||
"link_not_sent_title": "Link kunne ikke sendes",
|
||||
"link_send_failed": "Linket kunne ikke sendes.",
|
||||
"link_sent_default": "Linket er sendt.",
|
||||
"link_sent_title": "Link sendt",
|
||||
"login_link_failed_title": "Loginlink kunne ikke sendes",
|
||||
"login_link_not_sent_title": "Loginlink kunne ikke sendes",
|
||||
"login_link_sent_title": "Loginlink sendt",
|
||||
"missing": "Mangler",
|
||||
"password_guide_failed_title": "Guide kunne ikke sendes",
|
||||
"password_guide_not_sent_title": "Guide kunne ikke sendes",
|
||||
"password_guide_sent_title": "Guide sendt",
|
||||
"phone_missing": "Telefon mangler",
|
||||
"send_login_link": "Send forhåndsgodkendt loginlink",
|
||||
"send_password_guide": "Send guide til ny adgangskode",
|
||||
"status": "Status",
|
||||
"unverified": "Ikke verificeret",
|
||||
"verification_update_failed": "Verifikationsstatus kunne ikke opdateres.",
|
||||
"verification_update_failed_title": "Status kunne ikke opdateres",
|
||||
"verified": "Verificeret"
|
||||
},
|
||||
"driver_wheel": {
|
||||
"access_section": "Adgange",
|
||||
"account_section": "Konto",
|
||||
"contact_section": "Kontakt",
|
||||
"details_section": "Detaljer",
|
||||
"identity_subsection": "Identitet",
|
||||
"system_subsection": "System"
|
||||
},
|
||||
"invoicing_billing_period": {
|
||||
"dashboard_right": "@.capitalize:{'words.generated.dashboard'} @.capitalize:{'words.generated.højre'} @.capitalize:{'words.generated.visning'}",
|
||||
"fetch_period_data": "@:{'words.generated.hent'} periodedata"
|
||||
|
||||
@@ -6029,6 +6029,38 @@
|
||||
"permission_count": "{count} permissions",
|
||||
"custom_summary": "Custom: {summary}"
|
||||
}
|
||||
},
|
||||
"driver_contact": {
|
||||
"close": "Close",
|
||||
"email_missing": "E-mail missing",
|
||||
"link_not_sent_default": "The link could not be sent.",
|
||||
"link_not_sent_title": "Link could not be sent",
|
||||
"link_send_failed": "The link could not be sent.",
|
||||
"link_sent_default": "The link was sent.",
|
||||
"link_sent_title": "Link sent",
|
||||
"login_link_failed_title": "Login link could not be sent",
|
||||
"login_link_not_sent_title": "Login link could not be sent",
|
||||
"login_link_sent_title": "Login link sent",
|
||||
"missing": "Missing",
|
||||
"password_guide_failed_title": "Guide could not be sent",
|
||||
"password_guide_not_sent_title": "Guide could not be sent",
|
||||
"password_guide_sent_title": "Guide sent",
|
||||
"phone_missing": "Phone missing",
|
||||
"send_login_link": "Send pre-authorized login link",
|
||||
"send_password_guide": "Send new password guide",
|
||||
"status": "Status",
|
||||
"unverified": "Not verified",
|
||||
"verification_update_failed": "Verification status could not be updated.",
|
||||
"verification_update_failed_title": "Status could not be updated",
|
||||
"verified": "Verified"
|
||||
},
|
||||
"driver_wheel": {
|
||||
"access_section": "Access",
|
||||
"account_section": "Account",
|
||||
"contact_section": "Contact",
|
||||
"details_section": "Details",
|
||||
"identity_subsection": "Identity",
|
||||
"system_subsection": "System"
|
||||
}
|
||||
},
|
||||
"system_status": {
|
||||
|
||||
@@ -5505,6 +5505,38 @@
|
||||
"custom_summary": "Custom: {summary}"
|
||||
}
|
||||
},
|
||||
"driver_contact": {
|
||||
"close": "Close",
|
||||
"email_missing": "E-mail missing",
|
||||
"link_not_sent_default": "The link could not be sent.",
|
||||
"link_not_sent_title": "Link could not be sent",
|
||||
"link_send_failed": "The link could not be sent.",
|
||||
"link_sent_default": "The link was sent.",
|
||||
"link_sent_title": "Link sent",
|
||||
"login_link_failed_title": "Login link could not be sent",
|
||||
"login_link_not_sent_title": "Login link could not be sent",
|
||||
"login_link_sent_title": "Login link sent",
|
||||
"missing": "Missing",
|
||||
"password_guide_failed_title": "Guide could not be sent",
|
||||
"password_guide_not_sent_title": "Guide could not be sent",
|
||||
"password_guide_sent_title": "Guide sent",
|
||||
"phone_missing": "Phone missing",
|
||||
"send_login_link": "Send pre-authorized login link",
|
||||
"send_password_guide": "Send new password guide",
|
||||
"status": "Status",
|
||||
"unverified": "Not verified",
|
||||
"verification_update_failed": "Verification status could not be updated.",
|
||||
"verification_update_failed_title": "Status could not be updated",
|
||||
"verified": "Verified"
|
||||
},
|
||||
"driver_wheel": {
|
||||
"access_section": "Access",
|
||||
"account_section": "Account",
|
||||
"contact_section": "Contact",
|
||||
"details_section": "Details",
|
||||
"identity_subsection": "Identity",
|
||||
"system_subsection": "System"
|
||||
},
|
||||
"invoicing_billing_period": {
|
||||
"dashboard_right": "@.capitalize:{'words.generated.dashboard'} @.capitalize:{'words.generated.right'} @.capitalize:{'words.generated.display'}",
|
||||
"fetch_period_data": "@:{'words.generated.fetch'} @.capitalize:{'words.generated.period'} @.capitalize:{'words.generated.data'}"
|
||||
|
||||
@@ -5480,6 +5480,38 @@
|
||||
"permission_count": "@:{'templates.generated.compat.superuser.driver_access.summary.permission_count'}"
|
||||
}
|
||||
},
|
||||
"driver_contact": {
|
||||
"close": "@:{'templates.generated.compat.superuser.driver_contact.close'}",
|
||||
"email_missing": "@:{'templates.generated.compat.superuser.driver_contact.email_missing'}",
|
||||
"link_not_sent_default": "@:{'templates.generated.compat.superuser.driver_contact.link_not_sent_default'}",
|
||||
"link_not_sent_title": "@:{'templates.generated.compat.superuser.driver_contact.link_not_sent_title'}",
|
||||
"link_send_failed": "@:{'templates.generated.compat.superuser.driver_contact.link_send_failed'}",
|
||||
"link_sent_default": "@:{'templates.generated.compat.superuser.driver_contact.link_sent_default'}",
|
||||
"link_sent_title": "@:{'templates.generated.compat.superuser.driver_contact.link_sent_title'}",
|
||||
"login_link_failed_title": "@:{'templates.generated.compat.superuser.driver_contact.login_link_failed_title'}",
|
||||
"login_link_not_sent_title": "@:{'templates.generated.compat.superuser.driver_contact.login_link_not_sent_title'}",
|
||||
"login_link_sent_title": "@:{'templates.generated.compat.superuser.driver_contact.login_link_sent_title'}",
|
||||
"missing": "@:{'templates.generated.compat.superuser.driver_contact.missing'}",
|
||||
"password_guide_failed_title": "@:{'templates.generated.compat.superuser.driver_contact.password_guide_failed_title'}",
|
||||
"password_guide_not_sent_title": "@:{'templates.generated.compat.superuser.driver_contact.password_guide_not_sent_title'}",
|
||||
"password_guide_sent_title": "@:{'templates.generated.compat.superuser.driver_contact.password_guide_sent_title'}",
|
||||
"phone_missing": "@:{'templates.generated.compat.superuser.driver_contact.phone_missing'}",
|
||||
"send_login_link": "@:{'templates.generated.compat.superuser.driver_contact.send_login_link'}",
|
||||
"send_password_guide": "@:{'templates.generated.compat.superuser.driver_contact.send_password_guide'}",
|
||||
"status": "@:{'templates.generated.compat.superuser.driver_contact.status'}",
|
||||
"unverified": "@:{'templates.generated.compat.superuser.driver_contact.unverified'}",
|
||||
"verification_update_failed": "@:{'templates.generated.compat.superuser.driver_contact.verification_update_failed'}",
|
||||
"verification_update_failed_title": "@:{'templates.generated.compat.superuser.driver_contact.verification_update_failed_title'}",
|
||||
"verified": "@:{'templates.generated.compat.superuser.driver_contact.verified'}"
|
||||
},
|
||||
"driver_wheel": {
|
||||
"access_section": "@:{'templates.generated.compat.superuser.driver_wheel.access_section'}",
|
||||
"account_section": "@:{'templates.generated.compat.superuser.driver_wheel.account_section'}",
|
||||
"contact_section": "@:{'templates.generated.compat.superuser.driver_wheel.contact_section'}",
|
||||
"details_section": "@:{'templates.generated.compat.superuser.driver_wheel.details_section'}",
|
||||
"identity_subsection": "@:{'templates.generated.compat.superuser.driver_wheel.identity_subsection'}",
|
||||
"system_subsection": "@:{'templates.generated.compat.superuser.driver_wheel.system_subsection'}"
|
||||
},
|
||||
"form": {
|
||||
"locked": "@:{'templates.generated.compat.objects.columns.suspended'}",
|
||||
"required": "@:{'templates.generated.compat.global.required'}"
|
||||
|
||||
@@ -5984,6 +5984,38 @@
|
||||
"permission_count": "{count} permissions",
|
||||
"custom_summary": "Custom: {summary}"
|
||||
}
|
||||
},
|
||||
"driver_contact": {
|
||||
"close": "Close",
|
||||
"email_missing": "E-mail missing",
|
||||
"link_not_sent_default": "The link could not be sent.",
|
||||
"link_not_sent_title": "Link could not be sent",
|
||||
"link_send_failed": "The link could not be sent.",
|
||||
"link_sent_default": "The link was sent.",
|
||||
"link_sent_title": "Link sent",
|
||||
"login_link_failed_title": "Login link could not be sent",
|
||||
"login_link_not_sent_title": "Login link could not be sent",
|
||||
"login_link_sent_title": "Login link sent",
|
||||
"missing": "Missing",
|
||||
"password_guide_failed_title": "Guide could not be sent",
|
||||
"password_guide_not_sent_title": "Guide could not be sent",
|
||||
"password_guide_sent_title": "Guide sent",
|
||||
"phone_missing": "Phone missing",
|
||||
"send_login_link": "Send pre-authorized login link",
|
||||
"send_password_guide": "Send new password guide",
|
||||
"status": "Status",
|
||||
"unverified": "Not verified",
|
||||
"verification_update_failed": "Verification status could not be updated.",
|
||||
"verification_update_failed_title": "Status could not be updated",
|
||||
"verified": "Verified"
|
||||
},
|
||||
"driver_wheel": {
|
||||
"access_section": "Access",
|
||||
"account_section": "Account",
|
||||
"contact_section": "Contact",
|
||||
"details_section": "Details",
|
||||
"identity_subsection": "Identity",
|
||||
"system_subsection": "System"
|
||||
}
|
||||
},
|
||||
"system_status": {
|
||||
|
||||
@@ -6082,6 +6082,38 @@
|
||||
"permission_count": "{count} permissions",
|
||||
"custom_summary": "Custom: {summary}"
|
||||
}
|
||||
},
|
||||
"driver_contact": {
|
||||
"close": "Close",
|
||||
"email_missing": "E-mail missing",
|
||||
"link_not_sent_default": "The link could not be sent.",
|
||||
"link_not_sent_title": "Link could not be sent",
|
||||
"link_send_failed": "The link could not be sent.",
|
||||
"link_sent_default": "The link was sent.",
|
||||
"link_sent_title": "Link sent",
|
||||
"login_link_failed_title": "Login link could not be sent",
|
||||
"login_link_not_sent_title": "Login link could not be sent",
|
||||
"login_link_sent_title": "Login link sent",
|
||||
"missing": "Missing",
|
||||
"password_guide_failed_title": "Guide could not be sent",
|
||||
"password_guide_not_sent_title": "Guide could not be sent",
|
||||
"password_guide_sent_title": "Guide sent",
|
||||
"phone_missing": "Phone missing",
|
||||
"send_login_link": "Send pre-authorized login link",
|
||||
"send_password_guide": "Send new password guide",
|
||||
"status": "Status",
|
||||
"unverified": "Not verified",
|
||||
"verification_update_failed": "Verification status could not be updated.",
|
||||
"verification_update_failed_title": "Status could not be updated",
|
||||
"verified": "Verified"
|
||||
},
|
||||
"driver_wheel": {
|
||||
"access_section": "Access",
|
||||
"account_section": "Account",
|
||||
"contact_section": "Contact",
|
||||
"details_section": "Details",
|
||||
"identity_subsection": "Identity",
|
||||
"system_subsection": "System"
|
||||
}
|
||||
},
|
||||
"system_status": {
|
||||
|
||||
@@ -84,6 +84,38 @@
|
||||
"custom_summary": "Tilpasset: {summary}"
|
||||
}
|
||||
},
|
||||
"driver_contact": {
|
||||
"close": "Luk",
|
||||
"email_missing": "E-mail mangler",
|
||||
"link_not_sent_default": "Linket kunne ikke sendes.",
|
||||
"link_not_sent_title": "Link kunne ikke sendes",
|
||||
"link_send_failed": "Linket kunne ikke sendes.",
|
||||
"link_sent_default": "Linket er sendt.",
|
||||
"link_sent_title": "Link sendt",
|
||||
"login_link_failed_title": "Loginlink kunne ikke sendes",
|
||||
"login_link_not_sent_title": "Loginlink kunne ikke sendes",
|
||||
"login_link_sent_title": "Loginlink sendt",
|
||||
"missing": "Mangler",
|
||||
"password_guide_failed_title": "Guide kunne ikke sendes",
|
||||
"password_guide_not_sent_title": "Guide kunne ikke sendes",
|
||||
"password_guide_sent_title": "Guide sendt",
|
||||
"phone_missing": "Telefon mangler",
|
||||
"send_login_link": "Send forhåndsgodkendt loginlink",
|
||||
"send_password_guide": "Send guide til ny adgangskode",
|
||||
"status": "Status",
|
||||
"unverified": "Ikke verificeret",
|
||||
"verification_update_failed": "Verifikationsstatus kunne ikke opdateres.",
|
||||
"verification_update_failed_title": "Status kunne ikke opdateres",
|
||||
"verified": "Verificeret"
|
||||
},
|
||||
"driver_wheel": {
|
||||
"access_section": "Adgange",
|
||||
"account_section": "Konto",
|
||||
"contact_section": "Kontakt",
|
||||
"details_section": "Detaljer",
|
||||
"identity_subsection": "Identitet",
|
||||
"system_subsection": "System"
|
||||
},
|
||||
"invoicing_billing_period": {
|
||||
"dashboard_right": "@.capitalize:{'terms.glossary.dashboard'} @.capitalize:{'terms.glossary.højre'} @.capitalize:{'terms.glossary.visning'}",
|
||||
"fetch_period_data": "@:{'terms.glossary.hent'} periodedata"
|
||||
|
||||
@@ -328,6 +328,38 @@
|
||||
"permission_count": "{count} permissions",
|
||||
"custom_summary": "Custom: {summary}"
|
||||
}
|
||||
},
|
||||
"driver_contact": {
|
||||
"close": "Close",
|
||||
"email_missing": "E-mail missing",
|
||||
"link_not_sent_default": "The link could not be sent.",
|
||||
"link_not_sent_title": "Link could not be sent",
|
||||
"link_send_failed": "The link could not be sent.",
|
||||
"link_sent_default": "The link was sent.",
|
||||
"link_sent_title": "Link sent",
|
||||
"login_link_failed_title": "Login link could not be sent",
|
||||
"login_link_not_sent_title": "Login link could not be sent",
|
||||
"login_link_sent_title": "Login link sent",
|
||||
"missing": "Missing",
|
||||
"password_guide_failed_title": "Guide could not be sent",
|
||||
"password_guide_not_sent_title": "Guide could not be sent",
|
||||
"password_guide_sent_title": "Guide sent",
|
||||
"phone_missing": "Phone missing",
|
||||
"send_login_link": "Send pre-authorized login link",
|
||||
"send_password_guide": "Send new password guide",
|
||||
"status": "Status",
|
||||
"unverified": "Not verified",
|
||||
"verification_update_failed": "Verification status could not be updated.",
|
||||
"verification_update_failed_title": "Status could not be updated",
|
||||
"verified": "Verified"
|
||||
},
|
||||
"driver_wheel": {
|
||||
"access_section": "Access",
|
||||
"account_section": "Account",
|
||||
"contact_section": "Contact",
|
||||
"details_section": "Details",
|
||||
"identity_subsection": "Identity",
|
||||
"system_subsection": "System"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,38 @@
|
||||
"custom_summary": "Custom: {summary}"
|
||||
}
|
||||
},
|
||||
"driver_contact": {
|
||||
"close": "Close",
|
||||
"email_missing": "E-mail missing",
|
||||
"link_not_sent_default": "The link could not be sent.",
|
||||
"link_not_sent_title": "Link could not be sent",
|
||||
"link_send_failed": "The link could not be sent.",
|
||||
"link_sent_default": "The link was sent.",
|
||||
"link_sent_title": "Link sent",
|
||||
"login_link_failed_title": "Login link could not be sent",
|
||||
"login_link_not_sent_title": "Login link could not be sent",
|
||||
"login_link_sent_title": "Login link sent",
|
||||
"missing": "Missing",
|
||||
"password_guide_failed_title": "Guide could not be sent",
|
||||
"password_guide_not_sent_title": "Guide could not be sent",
|
||||
"password_guide_sent_title": "Guide sent",
|
||||
"phone_missing": "Phone missing",
|
||||
"send_login_link": "Send pre-authorized login link",
|
||||
"send_password_guide": "Send new password guide",
|
||||
"status": "Status",
|
||||
"unverified": "Not verified",
|
||||
"verification_update_failed": "Verification status could not be updated.",
|
||||
"verification_update_failed_title": "Status could not be updated",
|
||||
"verified": "Verified"
|
||||
},
|
||||
"driver_wheel": {
|
||||
"access_section": "Access",
|
||||
"account_section": "Account",
|
||||
"contact_section": "Contact",
|
||||
"details_section": "Details",
|
||||
"identity_subsection": "Identity",
|
||||
"system_subsection": "System"
|
||||
},
|
||||
"invoicing_billing_period": {
|
||||
"dashboard_right": "@.capitalize:{'terms.glossary.dashboard'} @.capitalize:{'terms.glossary.right'} @.capitalize:{'terms.glossary.display'}",
|
||||
"fetch_period_data": "@:{'terms.glossary.fetch'} @.capitalize:{'terms.glossary.period'} @.capitalize:{'terms.glossary.data'}"
|
||||
|
||||
@@ -84,6 +84,38 @@
|
||||
"permission_count": "@:{'phrases.compat.superuser.driver_access.summary.permission_count'}"
|
||||
}
|
||||
},
|
||||
"driver_contact": {
|
||||
"close": "@:{'phrases.compat.superuser.driver_contact.close'}",
|
||||
"email_missing": "@:{'phrases.compat.superuser.driver_contact.email_missing'}",
|
||||
"link_not_sent_default": "@:{'phrases.compat.superuser.driver_contact.link_not_sent_default'}",
|
||||
"link_not_sent_title": "@:{'phrases.compat.superuser.driver_contact.link_not_sent_title'}",
|
||||
"link_send_failed": "@:{'phrases.compat.superuser.driver_contact.link_send_failed'}",
|
||||
"link_sent_default": "@:{'phrases.compat.superuser.driver_contact.link_sent_default'}",
|
||||
"link_sent_title": "@:{'phrases.compat.superuser.driver_contact.link_sent_title'}",
|
||||
"login_link_failed_title": "@:{'phrases.compat.superuser.driver_contact.login_link_failed_title'}",
|
||||
"login_link_not_sent_title": "@:{'phrases.compat.superuser.driver_contact.login_link_not_sent_title'}",
|
||||
"login_link_sent_title": "@:{'phrases.compat.superuser.driver_contact.login_link_sent_title'}",
|
||||
"missing": "@:{'phrases.compat.superuser.driver_contact.missing'}",
|
||||
"password_guide_failed_title": "@:{'phrases.compat.superuser.driver_contact.password_guide_failed_title'}",
|
||||
"password_guide_not_sent_title": "@:{'phrases.compat.superuser.driver_contact.password_guide_not_sent_title'}",
|
||||
"password_guide_sent_title": "@:{'phrases.compat.superuser.driver_contact.password_guide_sent_title'}",
|
||||
"phone_missing": "@:{'phrases.compat.superuser.driver_contact.phone_missing'}",
|
||||
"send_login_link": "@:{'phrases.compat.superuser.driver_contact.send_login_link'}",
|
||||
"send_password_guide": "@:{'phrases.compat.superuser.driver_contact.send_password_guide'}",
|
||||
"status": "@:{'phrases.compat.superuser.driver_contact.status'}",
|
||||
"unverified": "@:{'phrases.compat.superuser.driver_contact.unverified'}",
|
||||
"verification_update_failed": "@:{'phrases.compat.superuser.driver_contact.verification_update_failed'}",
|
||||
"verification_update_failed_title": "@:{'phrases.compat.superuser.driver_contact.verification_update_failed_title'}",
|
||||
"verified": "@:{'phrases.compat.superuser.driver_contact.verified'}"
|
||||
},
|
||||
"driver_wheel": {
|
||||
"access_section": "@:{'phrases.compat.superuser.driver_wheel.access_section'}",
|
||||
"account_section": "@:{'phrases.compat.superuser.driver_wheel.account_section'}",
|
||||
"contact_section": "@:{'phrases.compat.superuser.driver_wheel.contact_section'}",
|
||||
"details_section": "@:{'phrases.compat.superuser.driver_wheel.details_section'}",
|
||||
"identity_subsection": "@:{'phrases.compat.superuser.driver_wheel.identity_subsection'}",
|
||||
"system_subsection": "@:{'phrases.compat.superuser.driver_wheel.system_subsection'}"
|
||||
},
|
||||
"form": {
|
||||
"locked": "@:{'phrases.compat.objects.columns.suspended'}",
|
||||
"required": "@:{'phrases.compat.global.required'}"
|
||||
|
||||
@@ -328,6 +328,38 @@
|
||||
"permission_count": "{count} permissions",
|
||||
"custom_summary": "Custom: {summary}"
|
||||
}
|
||||
},
|
||||
"driver_contact": {
|
||||
"close": "Close",
|
||||
"email_missing": "E-mail missing",
|
||||
"link_not_sent_default": "The link could not be sent.",
|
||||
"link_not_sent_title": "Link could not be sent",
|
||||
"link_send_failed": "The link could not be sent.",
|
||||
"link_sent_default": "The link was sent.",
|
||||
"link_sent_title": "Link sent",
|
||||
"login_link_failed_title": "Login link could not be sent",
|
||||
"login_link_not_sent_title": "Login link could not be sent",
|
||||
"login_link_sent_title": "Login link sent",
|
||||
"missing": "Missing",
|
||||
"password_guide_failed_title": "Guide could not be sent",
|
||||
"password_guide_not_sent_title": "Guide could not be sent",
|
||||
"password_guide_sent_title": "Guide sent",
|
||||
"phone_missing": "Phone missing",
|
||||
"send_login_link": "Send pre-authorized login link",
|
||||
"send_password_guide": "Send new password guide",
|
||||
"status": "Status",
|
||||
"unverified": "Not verified",
|
||||
"verification_update_failed": "Verification status could not be updated.",
|
||||
"verification_update_failed_title": "Status could not be updated",
|
||||
"verified": "Verified"
|
||||
},
|
||||
"driver_wheel": {
|
||||
"access_section": "Access",
|
||||
"account_section": "Account",
|
||||
"contact_section": "Contact",
|
||||
"details_section": "Details",
|
||||
"identity_subsection": "Identity",
|
||||
"system_subsection": "System"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +328,38 @@
|
||||
"permission_count": "{count} permissions",
|
||||
"custom_summary": "Custom: {summary}"
|
||||
}
|
||||
},
|
||||
"driver_contact": {
|
||||
"close": "Close",
|
||||
"email_missing": "E-mail missing",
|
||||
"link_not_sent_default": "The link could not be sent.",
|
||||
"link_not_sent_title": "Link could not be sent",
|
||||
"link_send_failed": "The link could not be sent.",
|
||||
"link_sent_default": "The link was sent.",
|
||||
"link_sent_title": "Link sent",
|
||||
"login_link_failed_title": "Login link could not be sent",
|
||||
"login_link_not_sent_title": "Login link could not be sent",
|
||||
"login_link_sent_title": "Login link sent",
|
||||
"missing": "Missing",
|
||||
"password_guide_failed_title": "Guide could not be sent",
|
||||
"password_guide_not_sent_title": "Guide could not be sent",
|
||||
"password_guide_sent_title": "Guide sent",
|
||||
"phone_missing": "Phone missing",
|
||||
"send_login_link": "Send pre-authorized login link",
|
||||
"send_password_guide": "Send new password guide",
|
||||
"status": "Status",
|
||||
"unverified": "Not verified",
|
||||
"verification_update_failed": "Verification status could not be updated.",
|
||||
"verification_update_failed_title": "Status could not be updated",
|
||||
"verified": "Verified"
|
||||
},
|
||||
"driver_wheel": {
|
||||
"access_section": "Access",
|
||||
"account_section": "Account",
|
||||
"contact_section": "Contact",
|
||||
"details_section": "Details",
|
||||
"identity_subsection": "Identity",
|
||||
"system_subsection": "System"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +196,13 @@ load();
|
||||
:value="getModuleConfigValue('invoiceLayoutNumber')"
|
||||
:on-select="SessionUser.superUser.modules.economic.config.layouts.invoiceLayoutNumber.set"
|
||||
/>
|
||||
<ConfigurationSelect
|
||||
:label="$t('configuration.economic.invoice_template') + ' - ' + $t('common.discount')"
|
||||
:description="$t('configuration.economic.invoice_template_desc')"
|
||||
:options="layouts"
|
||||
:value="getModuleConfigValue('invoiceDiscountLayoutNumber')"
|
||||
:on-select="SessionUser.superUser.modules.economic.config.layouts.invoiceDiscountLayoutNumber.set"
|
||||
/>
|
||||
<ConfigurationSelect
|
||||
:label="$t('configuration.economic.default_distribution_department_id')"
|
||||
:description="$t('configuration.economic.default_distribution_department_id_desc')"
|
||||
|
||||
@@ -19,6 +19,172 @@ import { setUseLargeTableHeaders, useLargeTableHeaders } from "@/services/tableH
|
||||
|
||||
const { t } = useI18n();
|
||||
const isLoading = ref(false);
|
||||
|
||||
const subuserVerificationStatus = (channel) => {
|
||||
if (channel === "email") {
|
||||
return {
|
||||
available: Boolean(SessionUser.subuser.email.value),
|
||||
verified: Boolean(SessionUser.subuser.email_verified.value),
|
||||
verifiedAt: SessionUser.subuser.email_verified_at.value,
|
||||
destination: SessionUser.subuser.email.value,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
available: Boolean(SessionUser.subuser.phone.country_code.value && SessionUser.subuser.phone.number.value),
|
||||
verified: Boolean(SessionUser.subuser.phone.verified.value),
|
||||
verifiedAt: SessionUser.subuser.phone.verified_at.value,
|
||||
destination: SessionUser.subuser.phone.country_code.value && SessionUser.subuser.phone.number.value
|
||||
? `+${SessionUser.subuser.phone.country_code.value} ${SessionUser.subuser.phone.number.value}`
|
||||
: "",
|
||||
};
|
||||
};
|
||||
|
||||
const verificationBadgeClass = (channel) => {
|
||||
const status = subuserVerificationStatus(channel);
|
||||
if (!status.available) {
|
||||
return "is-light";
|
||||
}
|
||||
|
||||
return status.verified ? "is-success is-light" : "is-warning is-light";
|
||||
};
|
||||
|
||||
const verificationBadgeText = (channel) => {
|
||||
const status = subuserVerificationStatus(channel);
|
||||
if (!status.available) {
|
||||
return "Mangler";
|
||||
}
|
||||
|
||||
return status.verified ? "Verificeret" : "Ikke verificeret";
|
||||
};
|
||||
|
||||
const applySubuserPayload = (subuser) => {
|
||||
if (!subuser || typeof subuser !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(subuser, "email")) {
|
||||
SessionUser.subuser.email.value = subuser.email;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(subuser, "email_verified")) {
|
||||
SessionUser.subuser.email_verified.value = Boolean(subuser.email_verified);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(subuser, "email_verified_at")) {
|
||||
SessionUser.subuser.email_verified_at.value = subuser.email_verified_at || null;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(subuser, "phone_verified")) {
|
||||
SessionUser.subuser.phone.verified.value = Boolean(subuser.phone_verified);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(subuser, "phone_verified_at")) {
|
||||
SessionUser.subuser.phone.verified_at.value = subuser.phone_verified_at || null;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(subuser, "verification_state")) {
|
||||
SessionUser.subuser.verification_state.value = subuser.verification_state || null;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(subuser, "verification")) {
|
||||
SessionUser.subuser.verification.value = subuser.verification || null;
|
||||
}
|
||||
};
|
||||
|
||||
const applyVerificationPayload = (verification) => {
|
||||
if (!verification || typeof verification !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
SessionUser.subuser.verification.value = verification;
|
||||
SessionUser.subuser.verification_state.value = verification.state || null;
|
||||
SessionUser.subuser.email_verified.value = Boolean(verification.email?.verified);
|
||||
SessionUser.subuser.email_verified_at.value = verification.email?.verified_at || null;
|
||||
SessionUser.subuser.phone.verified.value = Boolean(verification.phone?.verified);
|
||||
SessionUser.subuser.phone.verified_at.value = verification.phone?.verified_at || null;
|
||||
};
|
||||
|
||||
const verificationErrorMessage = (error, fallback) =>
|
||||
error?.response?.data?.data?.message || error?.response?.data?.message || error?.message || fallback;
|
||||
|
||||
const onClickVerifySubuserContact = async (channel) => {
|
||||
const status = subuserVerificationStatus(channel);
|
||||
const label = channel === "email" ? "e-mail" : "telefonnummer";
|
||||
|
||||
if (!status.available) {
|
||||
await Swal.fire({
|
||||
title: "Kontaktoplysning mangler",
|
||||
text: `Der er ikke angivet et ${label}, der kan verificeres.`,
|
||||
icon: "warning",
|
||||
confirmButtonText: "Luk",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const sendResponse = await SessionUser.request(`/subusers/me/verification/${channel}/send`, "POST");
|
||||
const payload = sendResponse?.data?.data || sendResponse?.data || {};
|
||||
const delivery = payload.delivery || {};
|
||||
applyVerificationPayload(payload.verification);
|
||||
|
||||
if (delivery.status !== "sent") {
|
||||
await Swal.fire({
|
||||
title: "Kode kunne ikke sendes",
|
||||
text: delivery.message || "Verifikationskoden kunne ikke sendes.",
|
||||
icon: "error",
|
||||
confirmButtonText: "Luk",
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
await Swal.fire({
|
||||
title: "Kode kunne ikke sendes",
|
||||
text: verificationErrorMessage(error, "Verifikationskoden kunne ikke sendes."),
|
||||
icon: "error",
|
||||
confirmButtonText: "Luk",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await Swal.fire({
|
||||
title: channel === "email" ? "Verificér e-mail" : "Verificér telefonnummer",
|
||||
text: `Indtast den 6-cifrede kode, der blev sendt til ${status.destination}.`,
|
||||
input: "text",
|
||||
inputAttributes: {
|
||||
inputmode: "numeric",
|
||||
maxlength: "6",
|
||||
autocomplete: "one-time-code",
|
||||
},
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Verificér",
|
||||
cancelButtonText: "Annuller",
|
||||
preConfirm: async (value) => {
|
||||
const code = String(value || "").trim();
|
||||
if (!/^[0-9]{6}$/.test(code)) {
|
||||
Swal.showValidationMessage("Koden skal være 6 cifre.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const verifyResponse = await SessionUser.request(`/subusers/me/verification/${channel}/verify`, "POST", {
|
||||
code,
|
||||
});
|
||||
const payload = verifyResponse?.data?.data || verifyResponse?.data || {};
|
||||
applyVerificationPayload(payload.verification);
|
||||
applySubuserPayload(payload.subuser);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
Swal.showValidationMessage(verificationErrorMessage(error, "Koden kunne ikke verificeres."));
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await Swal.fire({
|
||||
title: "Verificeret",
|
||||
text: channel === "email" ? "Din e-mailadresse er verificeret." : "Dit telefonnummer er verificeret.",
|
||||
icon: "success",
|
||||
confirmButtonText: "Luk",
|
||||
});
|
||||
await SessionUser.getSessionData();
|
||||
}
|
||||
};
|
||||
/** Email */
|
||||
const onClickSaveEmail = async () => {
|
||||
// Get the password from Swal
|
||||
@@ -451,14 +617,18 @@ const onClickSaveSubuserEmail = async () => {
|
||||
if (result.isConfirmed) {
|
||||
const email = result.value;
|
||||
SessionUser.request("/subusers/me", "PUT", { email: email })
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
if (response.status === 200) {
|
||||
Swal.fire({
|
||||
applySubuserPayload(response?.data?.data || response?.data || {});
|
||||
SessionUser.subuser.email.value = email;
|
||||
SessionUser.subuser.email_verified.value = false;
|
||||
SessionUser.subuser.email_verified_at.value = null;
|
||||
await Swal.fire({
|
||||
title: "E-mail gemt",
|
||||
text: "Din e-mailadresse er blevet opdateret.",
|
||||
text: "Din e-mailadresse er blevet opdateret. Den skal verificeres før den markeres som gyldig.",
|
||||
icon: "success",
|
||||
});
|
||||
SessionUser.subuser.email.value = email;
|
||||
await onClickVerifySubuserContact("email");
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Fejl",
|
||||
@@ -711,12 +881,35 @@ const onClickSaveWashCertificateEmail = async () => {
|
||||
:value="SessionUser.subuser.email.value"
|
||||
:disabled="true"
|
||||
/>
|
||||
<button class="button is-link is-small mt-2" @click="onClickSaveSubuserEmail" :disabled="isLoading">
|
||||
<span>
|
||||
<i class="fas fa-edit"></i>
|
||||
<span class="ml-2">{{ $t("user_dashboard.profile.driver.edit_email") }}</span>
|
||||
<div class="is-flex is-flex-wrap-wrap is-align-items-center gap-2 mt-2">
|
||||
<span
|
||||
class="tag"
|
||||
:class="verificationBadgeClass('email')"
|
||||
data-testid="subuser-email-verification-badge"
|
||||
>
|
||||
<i class="fas fa-check-circle mr-1" v-if="SessionUser.subuser.email_verified.value"></i>
|
||||
<i class="fas fa-exclamation-triangle mr-1" v-else></i>
|
||||
{{ verificationBadgeText("email") }}
|
||||
</span>
|
||||
</button>
|
||||
<button class="button is-link is-small" @click="onClickSaveSubuserEmail" :disabled="isLoading">
|
||||
<span>
|
||||
<i class="fas fa-edit"></i>
|
||||
<span class="ml-2">{{ $t("user_dashboard.profile.driver.edit_email") }}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="subuserVerificationStatus('email').available && !SessionUser.subuser.email_verified.value"
|
||||
class="button is-warning is-light is-small"
|
||||
@click="onClickVerifySubuserContact('email')"
|
||||
:disabled="isLoading"
|
||||
data-testid="subuser-email-verify-button"
|
||||
>
|
||||
<span>
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span class="ml-2">Verificér e-mail</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<hr class="my-4" />
|
||||
<!-- Phone number (read-only) -->
|
||||
<ConfigurationInput
|
||||
@@ -725,6 +918,29 @@ const onClickSaveWashCertificateEmail = async () => {
|
||||
:value="SessionUser.subuser.phone.number.value"
|
||||
:disabled="true"
|
||||
/>
|
||||
<div class="is-flex is-flex-wrap-wrap is-align-items-center gap-2 mt-2">
|
||||
<span
|
||||
class="tag"
|
||||
:class="verificationBadgeClass('phone')"
|
||||
data-testid="subuser-phone-verification-badge"
|
||||
>
|
||||
<i class="fas fa-check-circle mr-1" v-if="SessionUser.subuser.phone.verified.value"></i>
|
||||
<i class="fas fa-exclamation-triangle mr-1" v-else></i>
|
||||
{{ verificationBadgeText("phone") }}
|
||||
</span>
|
||||
<button
|
||||
v-if="subuserVerificationStatus('phone').available && !SessionUser.subuser.phone.verified.value"
|
||||
class="button is-warning is-light is-small"
|
||||
@click="onClickVerifySubuserContact('phone')"
|
||||
:disabled="isLoading"
|
||||
data-testid="subuser-phone-verify-button"
|
||||
>
|
||||
<span>
|
||||
<i class="fas fa-mobile-alt"></i>
|
||||
<span class="ml-2">Verificér telefon</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Country code (read-only) -->
|
||||
<ConfigurationInput
|
||||
:title="$t('user_dashboard.profile.driver.country_code')"
|
||||
|
||||
@@ -92,8 +92,34 @@ function createSubuserSessionData(overrides: Record<string, unknown> = {}) {
|
||||
username: "driver-user",
|
||||
name: "E2E Driver",
|
||||
email: "driver@example.com",
|
||||
email_verified: false,
|
||||
email_verified_at: null,
|
||||
phone_country_code: 45,
|
||||
phone: 12345678,
|
||||
phone_verified: false,
|
||||
phone_verified_at: null,
|
||||
verification_state: "unverified",
|
||||
verification: {
|
||||
state: "unverified",
|
||||
email: {
|
||||
channel: "email",
|
||||
value: "driver@example.com",
|
||||
masked_value: "dr****@example.com",
|
||||
available: true,
|
||||
verified: false,
|
||||
verified_at: null,
|
||||
},
|
||||
phone: {
|
||||
channel: "phone",
|
||||
country_code: 45,
|
||||
phone: 12345678,
|
||||
value: "4512345678",
|
||||
masked_value: "******5678",
|
||||
available: true,
|
||||
verified: false,
|
||||
verified_at: null,
|
||||
},
|
||||
},
|
||||
grants: [],
|
||||
created_at: "2026-01-01T00:00:00.000Z",
|
||||
updated_at: "2026-01-01T00:00:00.000Z",
|
||||
@@ -167,7 +193,43 @@ async function installMockSubuserAuthRoutes(
|
||||
phone_country_code:
|
||||
"phoneCountryCode" in credentials ? Number.parseInt(credentials.phoneCountryCode, 10) || 45 : 45,
|
||||
phone: "phone" in credentials ? Number.parseInt(credentials.phone, 10) || 12345678 : 12345678,
|
||||
});
|
||||
}) as Record<string, any>;
|
||||
const refreshSubuserVerification = () => {
|
||||
const emailAvailable = Boolean(sessionData.email);
|
||||
const phoneAvailable = Boolean(sessionData.phone_country_code && sessionData.phone);
|
||||
sessionData.verification = {
|
||||
state:
|
||||
emailAvailable && phoneAvailable && sessionData.email_verified && sessionData.phone_verified
|
||||
? "verified"
|
||||
: emailAvailable && phoneAvailable && (sessionData.email_verified || sessionData.phone_verified)
|
||||
? "partial"
|
||||
: emailAvailable && phoneAvailable
|
||||
? "unverified"
|
||||
: !emailAvailable
|
||||
? "missing_email"
|
||||
: "missing_phone",
|
||||
email: {
|
||||
channel: "email",
|
||||
value: sessionData.email,
|
||||
masked_value: sessionData.email ? "dr****@example.com" : null,
|
||||
available: emailAvailable,
|
||||
verified: Boolean(sessionData.email_verified),
|
||||
verified_at: sessionData.email_verified_at,
|
||||
},
|
||||
phone: {
|
||||
channel: "phone",
|
||||
country_code: sessionData.phone_country_code,
|
||||
phone: sessionData.phone,
|
||||
value: phoneAvailable ? `${sessionData.phone_country_code}${sessionData.phone}` : null,
|
||||
masked_value: phoneAvailable ? "******5678" : null,
|
||||
available: phoneAvailable,
|
||||
verified: Boolean(sessionData.phone_verified),
|
||||
verified_at: sessionData.phone_verified_at,
|
||||
},
|
||||
};
|
||||
sessionData.verification_state = sessionData.verification.state;
|
||||
};
|
||||
refreshSubuserVerification();
|
||||
|
||||
await mockApi(page, {
|
||||
authenticated: false,
|
||||
@@ -211,7 +273,12 @@ async function installMockSubuserAuthRoutes(
|
||||
|
||||
if (route.request().method() === "PUT") {
|
||||
const body = route.request().postDataJSON?.() || {};
|
||||
if (Object.prototype.hasOwnProperty.call(body, "email") && body.email !== sessionData.email) {
|
||||
sessionData.email_verified = false;
|
||||
sessionData.email_verified_at = null;
|
||||
}
|
||||
Object.assign(sessionData, body);
|
||||
refreshSubuserVerification();
|
||||
}
|
||||
|
||||
await fulfillJson(route, {
|
||||
@@ -219,6 +286,54 @@ async function installMockSubuserAuthRoutes(
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/subusers/me/verification/*/send", async (route) => {
|
||||
const pathParts = new URL(route.request().url()).pathname.split("/");
|
||||
const channel = pathParts[pathParts.length - 2];
|
||||
await fulfillJson(route, {
|
||||
data: {
|
||||
delivery: {
|
||||
channel,
|
||||
status: "sent",
|
||||
message: "Verification code sent.",
|
||||
expires_in: 600,
|
||||
},
|
||||
verification: sessionData.verification,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/subusers/me/verification/*/verify", async (route) => {
|
||||
const pathParts = new URL(route.request().url()).pathname.split("/");
|
||||
const channel = pathParts[pathParts.length - 2];
|
||||
const body = route.request().postDataJSON?.() || {};
|
||||
if (body.code !== "123456") {
|
||||
await fulfillJson(route, { data: { message: "Invalid verification code." } }, 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (channel === "email") {
|
||||
sessionData.email_verified = true;
|
||||
sessionData.email_verified_at = "2026-07-13 10:30:00";
|
||||
}
|
||||
if (channel === "phone") {
|
||||
sessionData.phone_verified = true;
|
||||
sessionData.phone_verified_at = "2026-07-13 10:30:00";
|
||||
}
|
||||
refreshSubuserVerification();
|
||||
|
||||
await fulfillJson(route, {
|
||||
data: {
|
||||
result: {
|
||||
channel,
|
||||
status: "verified",
|
||||
message: "Contact value verified.",
|
||||
},
|
||||
verification: sessionData.verification,
|
||||
subuser: sessionData,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/auth/session", async (route) => {
|
||||
const authorization = route.request().headers().authorization;
|
||||
if (authorization !== `Bearer ${MOCK_SUBUSER_TOKEN}`) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,12 @@ async function goToUserProfile(page) {
|
||||
await expect(page).toHaveURL(/\/user\/profile(?:[?#].*)?$/);
|
||||
}
|
||||
|
||||
async function openContactSection(page) {
|
||||
const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
|
||||
await contactHeader.click();
|
||||
await page.waitForTimeout(200);
|
||||
}
|
||||
|
||||
// Subuser contact information display tests
|
||||
test("[PROFILE][Subuser][Contact] should display contact information section", async ({ page }) => {
|
||||
await loginAsSubuserByPhone(page);
|
||||
@@ -20,10 +26,7 @@ test("[PROFILE][Subuser][Contact] should display email field", async ({ page })
|
||||
await loginAsSubuserByPhone(page);
|
||||
await goToUserProfile(page);
|
||||
|
||||
// Expand contact section (second Chauffør section with address-card icon)
|
||||
const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
|
||||
await contactHeader.click();
|
||||
await page.waitForTimeout(200);
|
||||
await openContactSection(page);
|
||||
|
||||
// Email field should be visible
|
||||
await expect(page.getByText("E-mail", { exact: true })).toBeVisible();
|
||||
@@ -33,10 +36,7 @@ test("[PROFILE][Subuser][Contact] should display edit email button", async ({ pa
|
||||
await loginAsSubuserByPhone(page);
|
||||
await goToUserProfile(page);
|
||||
|
||||
// Expand contact section
|
||||
const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
|
||||
await contactHeader.click();
|
||||
await page.waitForTimeout(200);
|
||||
await openContactSection(page);
|
||||
|
||||
// Edit email button should be visible for subuser
|
||||
const editEmailButton = page.locator('button:has-text("Rediger e-mail")');
|
||||
@@ -47,10 +47,7 @@ test("[PROFILE][Subuser][Contact] should open edit email dialog when clicking ed
|
||||
await loginAsSubuserByPhone(page);
|
||||
await goToUserProfile(page);
|
||||
|
||||
// Expand contact section
|
||||
const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
|
||||
await contactHeader.click();
|
||||
await page.waitForTimeout(200);
|
||||
await openContactSection(page);
|
||||
|
||||
// Click edit email button
|
||||
const editEmailButton = page.locator('button:has-text("Rediger e-mail")');
|
||||
@@ -65,10 +62,7 @@ test("[PROFILE][Subuser][Contact] should display phone number field (read-only)"
|
||||
await loginAsSubuserByPhone(page);
|
||||
await goToUserProfile(page);
|
||||
|
||||
// Expand contact section
|
||||
const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
|
||||
await contactHeader.click();
|
||||
await page.waitForTimeout(200);
|
||||
await openContactSection(page);
|
||||
|
||||
// Phone number field should be visible
|
||||
await expect(page.getByText("Telefonnummer", { exact: true }).first()).toBeVisible();
|
||||
@@ -78,25 +72,49 @@ test("[PROFILE][Subuser][Contact] should display country code field (read-only)"
|
||||
await loginAsSubuserByPhone(page);
|
||||
await goToUserProfile(page);
|
||||
|
||||
// Expand contact section
|
||||
const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
|
||||
await contactHeader.click();
|
||||
await page.waitForTimeout(200);
|
||||
await openContactSection(page);
|
||||
|
||||
// Country code field should be visible
|
||||
await expect(page.getByText("Landekode", { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("[PROFILE][Subuser][Contact] should verify email with a code", async ({ page }) => {
|
||||
await loginAsSubuserByPhone(page);
|
||||
await goToUserProfile(page);
|
||||
await openContactSection(page);
|
||||
|
||||
await expect(page.getByTestId("subuser-email-verification-badge")).toContainText("Ikke verificeret");
|
||||
await page.getByTestId("subuser-email-verify-button").click();
|
||||
await expect(page.locator(".swal2-title")).toContainText("Verificér e-mail");
|
||||
await page.fill(".swal2-input", "123456");
|
||||
await page.click(".swal2-confirm");
|
||||
await expect(page.locator(".swal2-title")).toContainText("Verificeret");
|
||||
await page.getByRole("button", { name: "Luk" }).click();
|
||||
await expect(page.getByTestId("subuser-email-verification-badge")).toContainText("Verificeret");
|
||||
});
|
||||
|
||||
test("[PROFILE][Subuser][Contact] should verify phone with a code", async ({ page }) => {
|
||||
await loginAsSubuserByPhone(page);
|
||||
await goToUserProfile(page);
|
||||
await openContactSection(page);
|
||||
|
||||
await expect(page.getByTestId("subuser-phone-verification-badge")).toContainText("Ikke verificeret");
|
||||
await page.getByTestId("subuser-phone-verify-button").click();
|
||||
await expect(page.locator(".swal2-title")).toContainText("Verificér telefonnummer");
|
||||
await page.fill(".swal2-input", "123456");
|
||||
await page.click(".swal2-confirm");
|
||||
await expect(page.locator(".swal2-title")).toContainText("Verificeret");
|
||||
await page.getByRole("button", { name: "Luk" }).click();
|
||||
await expect(page.getByTestId("subuser-phone-verification-badge")).toContainText("Verificeret");
|
||||
});
|
||||
|
||||
/** Subuser Email Validation Tests */
|
||||
|
||||
test("[PROFILE][Subuser][Contact] should show validation error when email is empty", async ({ page }) => {
|
||||
await loginAsSubuserByPhone(page);
|
||||
await goToUserProfile(page);
|
||||
|
||||
// Expand contact section
|
||||
const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
|
||||
await contactHeader.click();
|
||||
await page.waitForTimeout(200);
|
||||
await openContactSection(page);
|
||||
|
||||
// Click edit email button
|
||||
const editEmailButton = page.locator('button:has-text("Rediger e-mail")');
|
||||
@@ -114,10 +132,7 @@ test("[PROFILE][Subuser][Contact] should show validation error for invalid email
|
||||
await loginAsSubuserByPhone(page);
|
||||
await goToUserProfile(page);
|
||||
|
||||
// Expand contact section
|
||||
const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
|
||||
await contactHeader.click();
|
||||
await page.waitForTimeout(200);
|
||||
await openContactSection(page);
|
||||
|
||||
// Click edit email button
|
||||
const editEmailButton = page.locator('button:has-text("Rediger e-mail")');
|
||||
|
||||
@@ -109,17 +109,15 @@ test.describe("superuser order date filters", () => {
|
||||
const otherToAnytimeGap = shortcutBox.x - (buttonBox.x + buttonBox.width);
|
||||
const dateShortcutGap = todayShortcutBox.x - (shortcutBox.x + shortcutBox.width);
|
||||
const countRightProtrusion = countBox.x + countBox.width - (buttonBox.x + buttonBox.width);
|
||||
const doesCountProtrudeFromTopRight = countBox.y < buttonBox.y && countRightProtrusion >= 5;
|
||||
const doesCountProtrudeFromTopRight = countBox.y < buttonBox.y && countRightProtrusion >= 8;
|
||||
const hasCountTopClearance = countBox.y >= shortcutActionsBox.y + 2;
|
||||
const hasCountRightClearance = otherToAnytimeGap - countRightProtrusion >= 1.5;
|
||||
|
||||
return (
|
||||
Math.abs(buttonCenterY - shortcutCenterY) <= 4 &&
|
||||
Math.abs(buttonBox.height - shortcutBox.height) <= 4 &&
|
||||
Math.abs(otherToAnytimeGap - dateShortcutGap) <= 2 &&
|
||||
doesCountProtrudeFromTopRight &&
|
||||
hasCountTopClearance &&
|
||||
hasCountRightClearance
|
||||
hasCountTopClearance
|
||||
);
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
@@ -970,6 +970,37 @@ describe("ActionSettingsWheelButton", () => {
|
||||
geometrySpy.mockRestore();
|
||||
});
|
||||
|
||||
it("passes metadata appearance to label rows in external menu sections", async () => {
|
||||
const wrapper = mountFlatDropdownButton({
|
||||
order_id: null,
|
||||
menuSections: [
|
||||
{
|
||||
key: "driver-details",
|
||||
label: "Details",
|
||||
items: [
|
||||
{
|
||||
key: "driver-id",
|
||||
type: "label",
|
||||
icon: "fas fa-hashtag",
|
||||
label: "Driver ID",
|
||||
detailValue: "51",
|
||||
appearance: "metadata",
|
||||
testId: "driver-id-row",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await wrapper.find(".dropdown-trigger button").trigger("click");
|
||||
await flushMicrotasks();
|
||||
|
||||
const row = wrapper.get('[data-testid="driver-id-row"]');
|
||||
expect(row.classes()).toContain("dropdown-item-label--metadata");
|
||||
expect(row.text()).toContain("Driver ID");
|
||||
expect(row.text()).toContain("51");
|
||||
});
|
||||
|
||||
it("does not render standalone booking completion for unlinked bookings", async () => {
|
||||
const wrapper = mountFlatDropdownButton({
|
||||
order_id: null,
|
||||
|
||||
Reference in New Issue
Block a user