Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a78e523b2 | ||
|
|
7f68471b85 |
@@ -101,7 +101,7 @@ jobs:
|
||||
if: github.event_name != 'schedule'
|
||||
needs: build-and-unit
|
||||
name: E2E-pr-${{ matrix.suite }}-${{ matrix.project }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, Linux, X64, pleno, frontend, docker]
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -238,6 +238,7 @@ jobs:
|
||||
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
|
||||
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
|
||||
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
|
||||
--env PLAYWRIGHT_WORKERS="${PLAYWRIGHT_WORKERS:-1}" \
|
||||
--env MATRIX_SUITE="$MATRIX_SUITE" \
|
||||
--env MATRIX_PROJECT="$MATRIX_PROJECT" \
|
||||
--env DIFF_BASE_REF="$DIFF_BASE_REF" \
|
||||
|
||||
@@ -43,12 +43,6 @@ export const sourceMappings = [
|
||||
specs: ["tests/e2e/superuser-roles-permissions.spec.ts"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "superuser-users",
|
||||
patterns: [/^src\/views\/dashboards\/superUserDashboard\/user\//u],
|
||||
specs: ["tests/e2e/superuser-users.spec.ts"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "superuser-dashboard",
|
||||
patterns: [
|
||||
|
||||
+4
-20
@@ -24,24 +24,8 @@ const onCustomerChange = () => {
|
||||
isCustomerSelected.value = false;
|
||||
}
|
||||
|
||||
const parseFixedPriceValue = (value) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(String(value), 10);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
const formatNumber = (value) => Number.isInteger(value) ? String(value) : value.toFixed(2);
|
||||
|
||||
const parseValue = (discount) => {
|
||||
const fixedPrice = parseFixedPriceValue(discount.fixed_price);
|
||||
if (fixedPrice !== null) {
|
||||
return `${formatNumber(fixedPrice)} Kr.`;
|
||||
}
|
||||
|
||||
let tmp_value = parseFloat(discount.percentage).toFixed(2);
|
||||
const parseValue = (value) => {
|
||||
let tmp_value = parseFloat(value).toFixed(2);
|
||||
// Add the percentage sign
|
||||
return `${tmp_value}%`;
|
||||
}
|
||||
@@ -94,7 +78,7 @@ watch(customer_id, onCustomerChange, { immediate: true });
|
||||
<!--<span class="icon is-small mr-1">
|
||||
<i class="fas fa-percent" aria-hidden="true"></i>
|
||||
</span> -->
|
||||
<span>{{ parseValue(discount) }}</span>
|
||||
<span>{{ parseValue(discount.percentage) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,4 +86,4 @@ watch(customer_id, onCustomerChange, { immediate: true });
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
@@ -29,8 +29,6 @@ const discount_product = ref(null);
|
||||
const discount_category = ref(null);
|
||||
// The global discount (if any)
|
||||
const discount_global = ref(null);
|
||||
// The fixed product price (if any)
|
||||
const fixed_product_price = ref(null);
|
||||
|
||||
// Show the discounts dropdown
|
||||
const showDiscountsDropdown = ref(false);
|
||||
@@ -51,19 +49,6 @@ const hasGlobalDiscount = () => {
|
||||
return discount_global.value > 0;
|
||||
}
|
||||
|
||||
const hasFixedProductPrice = () => {
|
||||
return fixed_product_price.value !== null;
|
||||
}
|
||||
|
||||
const parseFixedPriceValue = (value) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(String(value), 10);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
// Set the product discount
|
||||
const setProductDiscount = (percentage) => {
|
||||
// Check if the percentage is a number
|
||||
@@ -98,16 +83,6 @@ const setGlobalDiscount = (percentage) => {
|
||||
discount_global.value = percentage;
|
||||
}
|
||||
|
||||
const setFixedProductPrice = (fixedPrice) => {
|
||||
const parsedFixedPrice = parseFixedPriceValue(fixedPrice);
|
||||
if (parsedFixedPrice === null) {
|
||||
fixed_product_price.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
fixed_product_price.value = parsedFixedPrice;
|
||||
}
|
||||
|
||||
|
||||
// Debugging function
|
||||
const getDiscountDebug = () => {
|
||||
@@ -126,15 +101,9 @@ const getDiscountDebug = () => {
|
||||
|
||||
// Parse the customer discounts
|
||||
const parseCustomerDiscounts = () => {
|
||||
discount_product.value = null;
|
||||
discount_category.value = null;
|
||||
discount_global.value = null;
|
||||
fixed_product_price.value = null;
|
||||
|
||||
const customerDiscounts = Array.isArray(props.customer_discounts) ? props.customer_discounts : [];
|
||||
const tmp_discount_product = customerDiscounts.find((discount) => discount.product_or_category_id == props.product.id && Number(discount.is_category) === 0)
|
||||
const tmp_discount_category = customerDiscounts.find((discount) => discount.product_or_category_id == props.product.category && Number(discount.is_category) === 1);
|
||||
const tmp_discount_global = customerDiscounts.find((discount) => discount.id === 999999 && Number(discount.is_category) === 1);
|
||||
const tmp_discount_product = props.customer_discounts.find((discount) => discount.product_or_category_id === props.product.id && !discount.is_category)
|
||||
const tmp_discount_category = props.customer_discounts.find((discount) => discount.product_or_category_id === props.product.category && discount.is_category);
|
||||
const tmp_discount_global = props.customer_discounts.find((discount) => discount.id === 999999 && discount.is_category);
|
||||
|
||||
//console.log("Product discount: ", tmp_discount_product);
|
||||
//console.log("Category discount: ", tmp_discount_category);
|
||||
@@ -143,7 +112,6 @@ const parseCustomerDiscounts = () => {
|
||||
// Set the product discount
|
||||
if (tmp_discount_product) {
|
||||
setProductDiscount(tmp_discount_product.percentage);
|
||||
setFixedProductPrice(tmp_discount_product.fixed_price);
|
||||
}
|
||||
// Set the category discount
|
||||
if (tmp_discount_category) {
|
||||
@@ -157,10 +125,6 @@ const parseCustomerDiscounts = () => {
|
||||
|
||||
// Get the best discount for the customer
|
||||
const getBestDiscount = () => {
|
||||
if (hasFixedProductPrice()) {
|
||||
highestEligibleDiscount.value = 0;
|
||||
return 0;
|
||||
}
|
||||
// Set the best discount to 0
|
||||
let tmp_best_discount = 0;
|
||||
// Check if the product discount is higher than the current best discount
|
||||
@@ -218,32 +182,13 @@ watch(() => props.customer_discounts, () => {
|
||||
<span
|
||||
aria-haspopup="true"
|
||||
aria-controls="dropdown-menu"
|
||||
v-if="hasFixedProductPrice()"
|
||||
class="tag is-success is-light is-text text-can-not-select"
|
||||
>{{ fixed_product_price }} Kr.</span>
|
||||
<span
|
||||
aria-haspopup="true"
|
||||
aria-controls="dropdown-menu"
|
||||
v-else-if="highestEligibleDiscount > 0"
|
||||
v-if="highestEligibleDiscount > 0"
|
||||
class="tag is-warning is-light is-text text-can-not-select"
|
||||
> -{{ highestEligibleDiscount }}%</span>
|
||||
</div>
|
||||
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content py-0">
|
||||
<div class="list has-overflow-ellipsis" style="width: 340px">
|
||||
<template v-if="hasFixedProductPrice()">
|
||||
<a class="list-item">
|
||||
<div class="list-item-content">
|
||||
<div class="list-item-title">Fast pris</div>
|
||||
</div>
|
||||
<div class="list-item-controls list-item-controls-force-visible">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-success is-light">{{ fixed_product_price }} Kr.</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Best discount -->
|
||||
<a class="list-item">
|
||||
<div class="list-item-content">
|
||||
@@ -294,7 +239,6 @@ watch(() => props.customer_discounts, () => {
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -310,4 +254,4 @@ watch(() => props.customer_discounts, () => {
|
||||
.text-can-not-select {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
productCategoryAllowed,
|
||||
isProductRestricted,
|
||||
setProductsCategory,
|
||||
getUserProductPrice,
|
||||
getUserProductDiscount,
|
||||
showFakeCreateOrderItem,
|
||||
department_id,
|
||||
user_discounts,
|
||||
@@ -555,9 +555,10 @@ const addPreviousOrderToCurrent = async (previousOrder) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
const previousOrderProduct = getPreviousOrderProduct(orderItem) || { id: productId, price: getPreviousOrderProductPrice(orderItem) };
|
||||
const previousOrderProduct = getPreviousOrderProduct(orderItem);
|
||||
const discount = getUserProductDiscount(previousOrderProduct || {});
|
||||
const basePrice = getPreviousOrderProductPrice(orderItem);
|
||||
const discountedPrice = String(getUserProductPrice({ ...previousOrderProduct, price: basePrice }));
|
||||
const discountedPrice = (basePrice - (basePrice * (discount / 100))).toFixed(0);
|
||||
|
||||
showFakeCreateOrderItem(productId, quantity, discountedPrice);
|
||||
await createOrderItem(orderId, productId, quantity);
|
||||
@@ -591,7 +592,9 @@ const getRecommendedProductPrice = (productId) => {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return String(getUserProductPrice(product));
|
||||
const discount = getUserProductDiscount(product);
|
||||
const price = Number(product.price ?? 0);
|
||||
return (price - (price * (discount / 100))).toFixed(0);
|
||||
};
|
||||
|
||||
const isRecommendedProductCategoryAllowed = (productId) => {
|
||||
|
||||
@@ -1802,30 +1802,13 @@ export const getUserDiscounts = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const ensureUserDiscountsLoaded = () => {
|
||||
/** Get user product discount */
|
||||
export const getUserProductDiscount = (product, allowCategory = null, onlyCategory = null) => {
|
||||
// Check if the user discounts are loaded
|
||||
if (!has_user_discounts_loaded.value) {
|
||||
has_user_discounts_loaded.value = true;
|
||||
getUserDiscounts().then(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
const isDirectCustomerProductDiscount = (discount, productId) => {
|
||||
return discount?.product_or_category_id == productId && Number(discount?.is_category) === 0;
|
||||
};
|
||||
|
||||
const parseFixedPriceValue = (value) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(String(value), 10);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
/** Get user product discount */
|
||||
export const getUserProductDiscount = (product, allowCategory = null, onlyCategory = null) => {
|
||||
// Check if the user discounts are loaded
|
||||
ensureUserDiscountsLoaded();
|
||||
|
||||
// Get the product's id
|
||||
const productId = product.id;
|
||||
@@ -1923,36 +1906,6 @@ export const getUserProductDiscount = (product, allowCategory = null, onlyCatego
|
||||
return appliedDiscount;
|
||||
};
|
||||
|
||||
export const getUserProductFixedPrice = (product) => {
|
||||
ensureUserDiscountsLoaded();
|
||||
|
||||
const productId = product?.id;
|
||||
if (productId === null || productId === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fixedPriceDiscount = (Array.isArray(user_discounts.value) ? user_discounts.value : []).find((discount) => (
|
||||
isDirectCustomerProductDiscount(discount, productId) && parseFixedPriceValue(discount.fixed_price) !== null
|
||||
));
|
||||
|
||||
return fixedPriceDiscount ? parseFixedPriceValue(fixedPriceDiscount.fixed_price) : null;
|
||||
};
|
||||
|
||||
export const getUserProductPrice = (product) => {
|
||||
const fixedPrice = getUserProductFixedPrice(product);
|
||||
if (fixedPrice !== null) {
|
||||
return fixedPrice;
|
||||
}
|
||||
|
||||
const price = Number(product?.price ?? 0);
|
||||
const discount = Number(getUserProductDiscount(product) || 0);
|
||||
if (!Number.isFinite(price)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number((price - (price * (discount / 100))).toFixed(0));
|
||||
};
|
||||
|
||||
export const showFakeCreateOrderItem = (product, quantity, price) => {
|
||||
order_items.value.push({
|
||||
id: Math.floor(Math.random() * 1000),
|
||||
|
||||
@@ -189,7 +189,7 @@ const descriptionKeyOverrides: Partial<Record<EntityType, string[]>> = {
|
||||
xlvask_customers: ['email', 'city', 'address', 'vatnumber'],
|
||||
motorapi_lookups: ['endpoint', 'license_plate', 'result'],
|
||||
fxratesapi_conversion_rates: ['base', 'target', 'endpoint', 'result'],
|
||||
customer_discounts: ['customer_number', 'fixed_price', 'percentage'],
|
||||
customer_discounts: ['customer_number', 'percentage'],
|
||||
module_config: ['description', 'value'],
|
||||
department_goals: ['criteria.type', 'criteria.progress_alert_frequency', 'criteria.progress_alert_destination']
|
||||
};
|
||||
@@ -251,7 +251,6 @@ const keyFieldOverrides: Partial<Record<EntityType, FieldSelector[]>> = {
|
||||
),
|
||||
customer_discounts: fieldList(
|
||||
field('Discount', 'discount', 'percentage'),
|
||||
field('Fixed price', 'fixed_price'),
|
||||
field('Target', 'product_or_category_id', 'object_id'),
|
||||
field('Category', 'is_category'),
|
||||
field('Customer #', 'customer_number'),
|
||||
|
||||
@@ -5764,31 +5764,6 @@
|
||||
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
|
||||
},
|
||||
"other": "@:{'templates.generated.compat.global.other'}",
|
||||
"overview": {
|
||||
"active_vehicles": "Aktive køretøjer",
|
||||
"attributes": "Attributter",
|
||||
"connected": "Forbundet",
|
||||
"customer_management": "Kundestyring",
|
||||
"customer_number_required": "Et kundenummer er påkrævet før kundeindstillinger kan administreres.",
|
||||
"customer_rules": "Kunderegler",
|
||||
"effective_access": "Effektiv adgang",
|
||||
"loaded": "Indlæst",
|
||||
"load_failed": "Brugerdata kunne ikke indlæses",
|
||||
"loading_user": "Indlæser bruger",
|
||||
"management_hub": "Administrationshub",
|
||||
"missing": "Mangler",
|
||||
"more_permissions": "+{count} flere tilladelser",
|
||||
"no_subscription_transactions": "Der er ikke registreret vaskeabonnementstransaktioner for kunden.",
|
||||
"not_invoiced": "Ikke faktureret",
|
||||
"open_draft": "Åben kladde",
|
||||
"open_vehicles": "Åbn køretøjer",
|
||||
"price_overrides": "Prisoverstyringer",
|
||||
"pricing": "Priser",
|
||||
"subscription_invoicing": "Abonnementsfakturering",
|
||||
"subtitle": "Overblik over kundedata, adgang, priser, køretøjer og abonnementsfakturering.",
|
||||
"vehicles_load_failed": "Køretøjer kunne ikke indlæses.",
|
||||
"workspace_shortcuts": "Arbejdsgange"
|
||||
},
|
||||
"permissions": "@.capitalize:{'words.generated.tilladelser'}",
|
||||
"user_data": "@.capitalize:{'words.generated.brugerdata'}",
|
||||
"user_id": "@.capitalize:{'words.generated.bruger'} @.upper:{'words.generated.id'}",
|
||||
|
||||
@@ -5867,31 +5867,6 @@
|
||||
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
|
||||
},
|
||||
"other": "@.capitalize:{'words.generated.andere'}",
|
||||
"overview": {
|
||||
"active_vehicles": "Aktive Fahrzeuge",
|
||||
"attributes": "Attribute",
|
||||
"connected": "Verbunden",
|
||||
"customer_management": "Kundenverwaltung",
|
||||
"customer_number_required": "Eine Kundennummer ist erforderlich, bevor Kundeneinstellungen verwaltet werden können.",
|
||||
"customer_rules": "Kundenregeln",
|
||||
"effective_access": "Effektiver Zugriff",
|
||||
"loaded": "Geladen",
|
||||
"load_failed": "Benutzerdaten konnten nicht geladen werden",
|
||||
"loading_user": "Benutzer wird geladen",
|
||||
"management_hub": "Verwaltung",
|
||||
"missing": "Fehlt",
|
||||
"more_permissions": "+{count} weitere Berechtigungen",
|
||||
"no_subscription_transactions": "Für diesen Kunden sind keine Waschabonnement-Transaktionen registriert.",
|
||||
"not_invoiced": "Nicht fakturiert",
|
||||
"open_draft": "Offener Entwurf",
|
||||
"open_vehicles": "Fahrzeuge öffnen",
|
||||
"price_overrides": "Preisüberschreibungen",
|
||||
"pricing": "Preise",
|
||||
"subscription_invoicing": "Abonnementabrechnung",
|
||||
"subtitle": "Überblick über Kundendaten, Zugriff, Preise, Fahrzeuge und Abonnementabrechnung.",
|
||||
"vehicles_load_failed": "Fahrzeuge konnten nicht geladen werden.",
|
||||
"workspace_shortcuts": "Arbeitsbereich"
|
||||
},
|
||||
"permissions": "Tilladelser",
|
||||
"user_data": "Brugerdata",
|
||||
"user_id": "@:{'words.generated.benutzer'}-@.upper:{'words.generated.id'}",
|
||||
|
||||
@@ -5596,31 +5596,6 @@
|
||||
"transaction_history": "@.capitalize:{'words.generated.transaction'} @:{'words.generated.history'}"
|
||||
},
|
||||
"other": "@:{'templates.generated.compat.global.other'}",
|
||||
"overview": {
|
||||
"active_vehicles": "Active vehicles",
|
||||
"attributes": "Attributes",
|
||||
"connected": "Connected",
|
||||
"customer_management": "Customer management",
|
||||
"customer_number_required": "A customer number is required before customer settings can be managed.",
|
||||
"customer_rules": "Customer rules",
|
||||
"effective_access": "Effective access",
|
||||
"loaded": "Loaded",
|
||||
"load_failed": "User data could not be loaded",
|
||||
"loading_user": "Loading user",
|
||||
"management_hub": "Management hub",
|
||||
"missing": "Missing",
|
||||
"more_permissions": "+{count} more permissions",
|
||||
"no_subscription_transactions": "No wash subscription transactions are registered for this customer.",
|
||||
"not_invoiced": "Not invoiced",
|
||||
"open_draft": "Open draft",
|
||||
"open_vehicles": "Open vehicles",
|
||||
"price_overrides": "Price overrides",
|
||||
"pricing": "Pricing",
|
||||
"subscription_invoicing": "Subscription invoicing",
|
||||
"subtitle": "Overview of customer data, access, pricing, vehicles, and subscription invoicing.",
|
||||
"vehicles_load_failed": "Vehicles could not be loaded.",
|
||||
"workspace_shortcuts": "Workspace shortcuts"
|
||||
},
|
||||
"permissions": "@.capitalize:{'words.generated.permissions'}",
|
||||
"user_data": "@.capitalize:{'words.generated.user'} @:{'words.generated.data'}",
|
||||
"user_id": "@.capitalize:{'words.generated.user'} @.upper:{'words.generated.id'}",
|
||||
|
||||
@@ -5632,31 +5632,6 @@
|
||||
"transaction_history": "@:{'templates.generated.compat.user_admin.orders.transaction_history'}"
|
||||
},
|
||||
"other": "@:{'templates.generated.compat.user_admin.other'}",
|
||||
"overview": {
|
||||
"active_vehicles": "@:{'templates.generated.compat.user_admin.overview.active_vehicles'}",
|
||||
"attributes": "@:{'templates.generated.compat.user_admin.overview.attributes'}",
|
||||
"connected": "@:{'templates.generated.compat.user_admin.overview.connected'}",
|
||||
"customer_management": "@:{'templates.generated.compat.user_admin.overview.customer_management'}",
|
||||
"customer_number_required": "@:{'templates.generated.compat.user_admin.overview.customer_number_required'}",
|
||||
"customer_rules": "@:{'templates.generated.compat.user_admin.overview.customer_rules'}",
|
||||
"effective_access": "@:{'templates.generated.compat.user_admin.overview.effective_access'}",
|
||||
"loaded": "@:{'templates.generated.compat.user_admin.overview.loaded'}",
|
||||
"load_failed": "@:{'templates.generated.compat.user_admin.overview.load_failed'}",
|
||||
"loading_user": "@:{'templates.generated.compat.user_admin.overview.loading_user'}",
|
||||
"management_hub": "@:{'templates.generated.compat.user_admin.overview.management_hub'}",
|
||||
"missing": "@:{'templates.generated.compat.user_admin.overview.missing'}",
|
||||
"more_permissions": "@:{'templates.generated.compat.user_admin.overview.more_permissions'}",
|
||||
"no_subscription_transactions": "@:{'templates.generated.compat.user_admin.overview.no_subscription_transactions'}",
|
||||
"not_invoiced": "@:{'templates.generated.compat.user_admin.overview.not_invoiced'}",
|
||||
"open_draft": "@:{'templates.generated.compat.user_admin.overview.open_draft'}",
|
||||
"open_vehicles": "@:{'templates.generated.compat.user_admin.overview.open_vehicles'}",
|
||||
"price_overrides": "@:{'templates.generated.compat.user_admin.overview.price_overrides'}",
|
||||
"pricing": "@:{'templates.generated.compat.user_admin.overview.pricing'}",
|
||||
"subscription_invoicing": "@:{'templates.generated.compat.user_admin.overview.subscription_invoicing'}",
|
||||
"subtitle": "@:{'templates.generated.compat.user_admin.overview.subtitle'}",
|
||||
"vehicles_load_failed": "@:{'templates.generated.compat.user_admin.overview.vehicles_load_failed'}",
|
||||
"workspace_shortcuts": "@:{'templates.generated.compat.user_admin.overview.workspace_shortcuts'}"
|
||||
},
|
||||
"permissions": "@:{'templates.generated.compat.user_admin.permissions'}",
|
||||
"subtitle": "@:user_admin.user_data",
|
||||
"title": "@:{'templates.generated.compat.common.user'}",
|
||||
|
||||
@@ -5870,31 +5870,6 @@
|
||||
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
|
||||
},
|
||||
"other": "@:{'templates.generated.compat.global.other'}",
|
||||
"overview": {
|
||||
"active_vehicles": "Aktive kjøretøy",
|
||||
"attributes": "Attributter",
|
||||
"connected": "Tilkoblet",
|
||||
"customer_management": "Kundestyring",
|
||||
"customer_number_required": "Et kundenummer kreves før kundeinnstillinger kan administreres.",
|
||||
"customer_rules": "Kunderegler",
|
||||
"effective_access": "Effektiv tilgang",
|
||||
"loaded": "Lastet",
|
||||
"load_failed": "Brukerdata kunne ikke lastes",
|
||||
"loading_user": "Laster bruker",
|
||||
"management_hub": "Administrasjon",
|
||||
"missing": "Mangler",
|
||||
"more_permissions": "+{count} flere tillatelser",
|
||||
"no_subscription_transactions": "Ingen vaskeabonnementstransaksjoner er registrert for kunden.",
|
||||
"not_invoiced": "Ikke fakturert",
|
||||
"open_draft": "Åpent utkast",
|
||||
"open_vehicles": "Åpne kjøretøy",
|
||||
"price_overrides": "Prisoverstyringer",
|
||||
"pricing": "Priser",
|
||||
"subscription_invoicing": "Abonnementsfakturering",
|
||||
"subtitle": "Oversikt over kundedata, tilgang, priser, kjøretøy og abonnementsfakturering.",
|
||||
"vehicles_load_failed": "Kjøretøy kunne ikke lastes.",
|
||||
"workspace_shortcuts": "Arbeidsområde"
|
||||
},
|
||||
"permissions": "@.capitalize:{'words.generated.tillatelser'}",
|
||||
"user_data": "@.capitalize:{'words.generated.brukerdata'}",
|
||||
"user_id": "@.capitalize:{'words.generated.bruker'}-@.upper:{'words.generated.id'}",
|
||||
|
||||
@@ -5920,31 +5920,6 @@
|
||||
"transaction_history": "@:{'templates.generated.compat.superuser.nav.orders'}"
|
||||
},
|
||||
"other": "Andet",
|
||||
"overview": {
|
||||
"active_vehicles": "Aktiva fordon",
|
||||
"attributes": "Attribut",
|
||||
"connected": "Ansluten",
|
||||
"customer_management": "Kundhantering",
|
||||
"customer_number_required": "Ett kundnummer krävs innan kundinställningar kan hanteras.",
|
||||
"customer_rules": "Kundregler",
|
||||
"effective_access": "Effektiv åtkomst",
|
||||
"loaded": "Inläst",
|
||||
"load_failed": "Användardata kunde inte läsas in",
|
||||
"loading_user": "Läser in användare",
|
||||
"management_hub": "Administration",
|
||||
"missing": "Saknas",
|
||||
"more_permissions": "+{count} fler behörigheter",
|
||||
"no_subscription_transactions": "Inga tvättabonnemangstransaktioner är registrerade för kunden.",
|
||||
"not_invoiced": "Ej fakturerat",
|
||||
"open_draft": "Öppet utkast",
|
||||
"open_vehicles": "Öppna fordon",
|
||||
"price_overrides": "Prisöverskrivningar",
|
||||
"pricing": "Priser",
|
||||
"subscription_invoicing": "Abonnemangsfakturering",
|
||||
"subtitle": "Översikt över kunddata, åtkomst, priser, fordon och abonnemangsfakturering.",
|
||||
"vehicles_load_failed": "Fordon kunde inte läsas in.",
|
||||
"workspace_shortcuts": "Arbetsyta"
|
||||
},
|
||||
"permissions": "Tilladelser",
|
||||
"user_data": "Brugerdata",
|
||||
"user_id": "Användar-@.upper:{'words.generated.id'}",
|
||||
|
||||
@@ -14,31 +14,6 @@
|
||||
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
|
||||
},
|
||||
"other": "@:{'phrases.compat.global.other'}",
|
||||
"overview": {
|
||||
"active_vehicles": "Aktive køretøjer",
|
||||
"attributes": "Attributter",
|
||||
"connected": "Forbundet",
|
||||
"customer_management": "Kundestyring",
|
||||
"customer_number_required": "Et kundenummer er påkrævet før kundeindstillinger kan administreres.",
|
||||
"customer_rules": "Kunderegler",
|
||||
"effective_access": "Effektiv adgang",
|
||||
"loaded": "Indlæst",
|
||||
"load_failed": "Brugerdata kunne ikke indlæses",
|
||||
"loading_user": "Indlæser bruger",
|
||||
"management_hub": "Administrationshub",
|
||||
"missing": "Mangler",
|
||||
"more_permissions": "+{count} flere tilladelser",
|
||||
"no_subscription_transactions": "Der er ikke registreret vaskeabonnementstransaktioner for kunden.",
|
||||
"not_invoiced": "Ikke faktureret",
|
||||
"open_draft": "Åben kladde",
|
||||
"open_vehicles": "Åbn køretøjer",
|
||||
"price_overrides": "Prisoverstyringer",
|
||||
"pricing": "Priser",
|
||||
"subscription_invoicing": "Abonnementsfakturering",
|
||||
"subtitle": "Overblik over kundedata, adgang, priser, køretøjer og abonnementsfakturering.",
|
||||
"vehicles_load_failed": "Køretøjer kunne ikke indlæses.",
|
||||
"workspace_shortcuts": "Arbejdsgange"
|
||||
},
|
||||
"permissions": "@.capitalize:{'terms.glossary.tilladelser'}",
|
||||
"user_data": "@.capitalize:{'terms.glossary.brugerdata'}",
|
||||
"user_id": "@.capitalize:{'terms.glossary.bruger'} @.upper:{'terms.glossary.id'}",
|
||||
|
||||
@@ -14,31 +14,6 @@
|
||||
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
|
||||
},
|
||||
"other": "@.capitalize:{'terms.glossary.andere'}",
|
||||
"overview": {
|
||||
"active_vehicles": "Aktive Fahrzeuge",
|
||||
"attributes": "Attribute",
|
||||
"connected": "Verbunden",
|
||||
"customer_management": "Kundenverwaltung",
|
||||
"customer_number_required": "Eine Kundennummer ist erforderlich, bevor Kundeneinstellungen verwaltet werden können.",
|
||||
"customer_rules": "Kundenregeln",
|
||||
"effective_access": "Effektiver Zugriff",
|
||||
"loaded": "Geladen",
|
||||
"load_failed": "Benutzerdaten konnten nicht geladen werden",
|
||||
"loading_user": "Benutzer wird geladen",
|
||||
"management_hub": "Verwaltung",
|
||||
"missing": "Fehlt",
|
||||
"more_permissions": "+{count} weitere Berechtigungen",
|
||||
"no_subscription_transactions": "Für diesen Kunden sind keine Waschabonnement-Transaktionen registriert.",
|
||||
"not_invoiced": "Nicht fakturiert",
|
||||
"open_draft": "Offener Entwurf",
|
||||
"open_vehicles": "Fahrzeuge öffnen",
|
||||
"price_overrides": "Preisüberschreibungen",
|
||||
"pricing": "Preise",
|
||||
"subscription_invoicing": "Abonnementabrechnung",
|
||||
"subtitle": "Überblick über Kundendaten, Zugriff, Preise, Fahrzeuge und Abonnementabrechnung.",
|
||||
"vehicles_load_failed": "Fahrzeuge konnten nicht geladen werden.",
|
||||
"workspace_shortcuts": "Arbeitsbereich"
|
||||
},
|
||||
"permissions": "Tilladelser",
|
||||
"user_data": "Brugerdata",
|
||||
"user_id": "@:{'terms.glossary.benutzer'}-@.upper:{'terms.glossary.id'}",
|
||||
|
||||
@@ -14,31 +14,6 @@
|
||||
"transaction_history": "@.capitalize:{'terms.glossary.transaction'} @:{'terms.glossary.history'}"
|
||||
},
|
||||
"other": "@:{'phrases.compat.global.other'}",
|
||||
"overview": {
|
||||
"active_vehicles": "Active vehicles",
|
||||
"attributes": "Attributes",
|
||||
"connected": "Connected",
|
||||
"customer_management": "Customer management",
|
||||
"customer_number_required": "A customer number is required before customer settings can be managed.",
|
||||
"customer_rules": "Customer rules",
|
||||
"effective_access": "Effective access",
|
||||
"loaded": "Loaded",
|
||||
"load_failed": "User data could not be loaded",
|
||||
"loading_user": "Loading user",
|
||||
"management_hub": "Management hub",
|
||||
"missing": "Missing",
|
||||
"more_permissions": "+{count} more permissions",
|
||||
"no_subscription_transactions": "No wash subscription transactions are registered for this customer.",
|
||||
"not_invoiced": "Not invoiced",
|
||||
"open_draft": "Open draft",
|
||||
"open_vehicles": "Open vehicles",
|
||||
"price_overrides": "Price overrides",
|
||||
"pricing": "Pricing",
|
||||
"subscription_invoicing": "Subscription invoicing",
|
||||
"subtitle": "Overview of customer data, access, pricing, vehicles, and subscription invoicing.",
|
||||
"vehicles_load_failed": "Vehicles could not be loaded.",
|
||||
"workspace_shortcuts": "Workspace shortcuts"
|
||||
},
|
||||
"permissions": "@.capitalize:{'terms.glossary.permissions'}",
|
||||
"user_data": "@.capitalize:{'terms.glossary.user'} @:{'terms.glossary.data'}",
|
||||
"user_id": "@.capitalize:{'terms.glossary.user'} @.upper:{'terms.glossary.id'}",
|
||||
|
||||
@@ -22,31 +22,6 @@
|
||||
"transaction_history": "@:{'phrases.compat.user_admin.orders.transaction_history'}"
|
||||
},
|
||||
"other": "@:{'phrases.compat.user_admin.other'}",
|
||||
"overview": {
|
||||
"active_vehicles": "@:{'phrases.compat.user_admin.overview.active_vehicles'}",
|
||||
"attributes": "@:{'phrases.compat.user_admin.overview.attributes'}",
|
||||
"connected": "@:{'phrases.compat.user_admin.overview.connected'}",
|
||||
"customer_management": "@:{'phrases.compat.user_admin.overview.customer_management'}",
|
||||
"customer_number_required": "@:{'phrases.compat.user_admin.overview.customer_number_required'}",
|
||||
"customer_rules": "@:{'phrases.compat.user_admin.overview.customer_rules'}",
|
||||
"effective_access": "@:{'phrases.compat.user_admin.overview.effective_access'}",
|
||||
"loaded": "@:{'phrases.compat.user_admin.overview.loaded'}",
|
||||
"load_failed": "@:{'phrases.compat.user_admin.overview.load_failed'}",
|
||||
"loading_user": "@:{'phrases.compat.user_admin.overview.loading_user'}",
|
||||
"management_hub": "@:{'phrases.compat.user_admin.overview.management_hub'}",
|
||||
"missing": "@:{'phrases.compat.user_admin.overview.missing'}",
|
||||
"more_permissions": "@:{'phrases.compat.user_admin.overview.more_permissions'}",
|
||||
"no_subscription_transactions": "@:{'phrases.compat.user_admin.overview.no_subscription_transactions'}",
|
||||
"not_invoiced": "@:{'phrases.compat.user_admin.overview.not_invoiced'}",
|
||||
"open_draft": "@:{'phrases.compat.user_admin.overview.open_draft'}",
|
||||
"open_vehicles": "@:{'phrases.compat.user_admin.overview.open_vehicles'}",
|
||||
"price_overrides": "@:{'phrases.compat.user_admin.overview.price_overrides'}",
|
||||
"pricing": "@:{'phrases.compat.user_admin.overview.pricing'}",
|
||||
"subscription_invoicing": "@:{'phrases.compat.user_admin.overview.subscription_invoicing'}",
|
||||
"subtitle": "@:{'phrases.compat.user_admin.overview.subtitle'}",
|
||||
"vehicles_load_failed": "@:{'phrases.compat.user_admin.overview.vehicles_load_failed'}",
|
||||
"workspace_shortcuts": "@:{'phrases.compat.user_admin.overview.workspace_shortcuts'}"
|
||||
},
|
||||
"permissions": "@:{'phrases.compat.user_admin.permissions'}",
|
||||
"subtitle": "@:user_admin.user_data",
|
||||
"title": "@:{'phrases.compat.common.user'}",
|
||||
|
||||
@@ -14,31 +14,6 @@
|
||||
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
|
||||
},
|
||||
"other": "@:{'phrases.compat.global.other'}",
|
||||
"overview": {
|
||||
"active_vehicles": "Aktive kjøretøy",
|
||||
"attributes": "Attributter",
|
||||
"connected": "Tilkoblet",
|
||||
"customer_management": "Kundestyring",
|
||||
"customer_number_required": "Et kundenummer kreves før kundeinnstillinger kan administreres.",
|
||||
"customer_rules": "Kunderegler",
|
||||
"effective_access": "Effektiv tilgang",
|
||||
"loaded": "Lastet",
|
||||
"load_failed": "Brukerdata kunne ikke lastes",
|
||||
"loading_user": "Laster bruker",
|
||||
"management_hub": "Administrasjon",
|
||||
"missing": "Mangler",
|
||||
"more_permissions": "+{count} flere tillatelser",
|
||||
"no_subscription_transactions": "Ingen vaskeabonnementstransaksjoner er registrert for kunden.",
|
||||
"not_invoiced": "Ikke fakturert",
|
||||
"open_draft": "Åpent utkast",
|
||||
"open_vehicles": "Åpne kjøretøy",
|
||||
"price_overrides": "Prisoverstyringer",
|
||||
"pricing": "Priser",
|
||||
"subscription_invoicing": "Abonnementsfakturering",
|
||||
"subtitle": "Oversikt over kundedata, tilgang, priser, kjøretøy og abonnementsfakturering.",
|
||||
"vehicles_load_failed": "Kjøretøy kunne ikke lastes.",
|
||||
"workspace_shortcuts": "Arbeidsområde"
|
||||
},
|
||||
"permissions": "@.capitalize:{'terms.glossary.tillatelser'}",
|
||||
"user_data": "@.capitalize:{'terms.glossary.brukerdata'}",
|
||||
"user_id": "@.capitalize:{'terms.glossary.bruker'}-@.upper:{'terms.glossary.id'}",
|
||||
|
||||
@@ -14,31 +14,6 @@
|
||||
"transaction_history": "@:{'phrases.compat.superuser.nav.orders'}"
|
||||
},
|
||||
"other": "Andet",
|
||||
"overview": {
|
||||
"active_vehicles": "Aktiva fordon",
|
||||
"attributes": "Attribut",
|
||||
"connected": "Ansluten",
|
||||
"customer_management": "Kundhantering",
|
||||
"customer_number_required": "Ett kundnummer krävs innan kundinställningar kan hanteras.",
|
||||
"customer_rules": "Kundregler",
|
||||
"effective_access": "Effektiv åtkomst",
|
||||
"loaded": "Inläst",
|
||||
"load_failed": "Användardata kunde inte läsas in",
|
||||
"loading_user": "Läser in användare",
|
||||
"management_hub": "Administration",
|
||||
"missing": "Saknas",
|
||||
"more_permissions": "+{count} fler behörigheter",
|
||||
"no_subscription_transactions": "Inga tvättabonnemangstransaktioner är registrerade för kunden.",
|
||||
"not_invoiced": "Ej fakturerat",
|
||||
"open_draft": "Öppet utkast",
|
||||
"open_vehicles": "Öppna fordon",
|
||||
"price_overrides": "Prisöverskrivningar",
|
||||
"pricing": "Priser",
|
||||
"subscription_invoicing": "Abonnemangsfakturering",
|
||||
"subtitle": "Översikt över kunddata, åtkomst, priser, fordon och abonnemangsfakturering.",
|
||||
"vehicles_load_failed": "Fordon kunde inte läsas in.",
|
||||
"workspace_shortcuts": "Arbetsyta"
|
||||
},
|
||||
"permissions": "Tilladelser",
|
||||
"user_data": "Brugerdata",
|
||||
"user_id": "Användar-@.upper:{'terms.glossary.id'}",
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter} from "vue-router";
|
||||
import { ref } from 'vue';
|
||||
import { showAuthSignOutForm } from '@/components/forms/auth/authSignOutForm.vue';
|
||||
import UserNavigation from "@/components/global/UserNavigation.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
const router = useRouter();
|
||||
// Get the user from the route
|
||||
const userId = ref(router.currentRoute.value.params.userId)
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const tabs = [
|
||||
{ name: 'Overview', path: '/superuser/users/' + userId.value },
|
||||
{ name: 'Orders', path: '/superuser/users/' + userId.value + '/orders' },
|
||||
{ name: 'Pricing', path: '/superuser/users/' + userId.value + '/pricing' },
|
||||
{ name: 'Other', path: '/superuser/users/' + userId.value + '/other' },
|
||||
{ name: 'Vehicles', path: '/superuser/users/' + userId.value + '/vehicles' },
|
||||
{ name: SessionUser.superUser.modules.xlvask.meta.title, path: '/superuser/users/' + userId.value + '/xlvask' },
|
||||
];
|
||||
|
||||
const userId = computed(() => route.params.userId);
|
||||
const basePath = computed(() => `/superuser/users/${userId.value}`);
|
||||
// Get the current path
|
||||
const currentPath = ref(router.currentRoute.value.path);
|
||||
|
||||
const tabs = computed(() => [
|
||||
{ key: "overview", name: t("nav.overview"), path: basePath.value },
|
||||
{ key: "orders", name: t("superuser.nav.orders"), path: `${basePath.value}/orders` },
|
||||
{ key: "pricing", name: t("user_admin.overview.pricing"), path: `${basePath.value}/pricing` },
|
||||
{ key: "other", name: t("user_admin.other"), path: `${basePath.value}/other` },
|
||||
{ key: "vehicles", name: t("common.vehicles"), path: `${basePath.value}/vehicles` },
|
||||
{ key: "xlvask", name: SessionUser.superUser.modules.xlvask.meta.title, path: `${basePath.value}/xlvask` },
|
||||
]);
|
||||
// Get the index of the active tab
|
||||
const activeTab = tabs.findIndex(tab => tab.path === currentPath.value);
|
||||
|
||||
// Change the tab
|
||||
const changeTab = (index) => {
|
||||
router.push(tabs[index].path);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="user-detail-tabs" data-testid="superuser-user-tabs">
|
||||
<div class="tabs is-right is-boxed">
|
||||
<div>
|
||||
<div class="tabs is-right">
|
||||
<ul>
|
||||
<li v-for="tab in tabs" :key="tab.path" :class="{ 'is-active': route.path === tab.path }">
|
||||
<router-link :to="tab.path" :data-testid="`superuser-user-tab-${tab.key}`">
|
||||
{{ tab.name }}
|
||||
</router-link>
|
||||
<li v-for="(tab, index) in tabs" :key="index" :class="{'is-active': activeTab === index}" @click="changeTab(index)">
|
||||
<a>{{ tab.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.user-detail-tabs {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tabs ul {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,91 +1,15 @@
|
||||
<script>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
import {ref, watch} from "vue";
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
|
||||
// Get the user from the route
|
||||
export const userId = ref(0);
|
||||
export const isUserLoading = ref(false);
|
||||
export const userLoadError = ref(null);
|
||||
export const selectedUserLoaded = ref(false);
|
||||
|
||||
let activeLoadPromise = null;
|
||||
let activeLoadUserId = null;
|
||||
let loadRequestSequence = 0;
|
||||
|
||||
const normalizeUserId = (id) => {
|
||||
const parsed = Number.parseInt(String(id ?? ""), 10);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
||||
};
|
||||
|
||||
const defaultKeys = () => ({
|
||||
open_invoice_draft: null,
|
||||
});
|
||||
|
||||
const economicFields = {
|
||||
customerNumber: null,
|
||||
name: null,
|
||||
address: null,
|
||||
zip: null,
|
||||
city: null,
|
||||
mobilePhone: null,
|
||||
email: null,
|
||||
cvr: null,
|
||||
currency: null,
|
||||
country: null,
|
||||
barred: null,
|
||||
};
|
||||
|
||||
const setEconomicData = (economicCustomer = {}) => {
|
||||
user.economicData.customerNumber.value = economicCustomer?.customerNumber ?? null;
|
||||
user.economicData.name.value = economicCustomer?.name ?? null;
|
||||
user.economicData.address.value = economicCustomer?.address ?? null;
|
||||
user.economicData.zip.value = economicCustomer?.zip ?? null;
|
||||
user.economicData.city.value = economicCustomer?.city ?? null;
|
||||
user.economicData.mobilePhone.value = economicCustomer?.mobilePhone ?? null;
|
||||
user.economicData.email.value = economicCustomer?.email ?? null;
|
||||
user.economicData.cvr.value = economicCustomer?.corporateIdentificationNumber ?? economicCustomer?.cvr ?? null;
|
||||
user.economicData.currency.value = economicCustomer?.currency ?? null;
|
||||
user.economicData.country.value = economicCustomer?.country ?? null;
|
||||
user.economicData.barred.value = economicCustomer?.barred ?? false;
|
||||
};
|
||||
|
||||
export const resetUser = ({ preserveUserId = false } = {}) => {
|
||||
if (!preserveUserId) {
|
||||
userId.value = 0;
|
||||
}
|
||||
|
||||
user.id.value = "";
|
||||
user.customer_number.value = "";
|
||||
user.display_name.value = "";
|
||||
user.email.value = "";
|
||||
user.phone.value = null;
|
||||
user.group_id.value = "";
|
||||
user.created_at.value = "";
|
||||
user.updated_at.value = "";
|
||||
setEconomicData(economicFields);
|
||||
user.permissions.value = [];
|
||||
user.attributes.value = [];
|
||||
user.discounts.value = [];
|
||||
user.orders_not_invoiced.value = [];
|
||||
user.keys.value = defaultKeys();
|
||||
user.wash_subscription_transactions.value = [];
|
||||
selectedUserLoaded.value = false;
|
||||
};
|
||||
|
||||
export const setUser = (id) => {
|
||||
const normalizedId = normalizeUserId(id);
|
||||
if (!normalizedId) {
|
||||
userLoadError.value = new Error("Invalid user id");
|
||||
resetUser();
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (userId.value !== normalizedId) {
|
||||
resetUser();
|
||||
userId.value = normalizedId;
|
||||
}
|
||||
|
||||
return getUserData(normalizedId);
|
||||
};
|
||||
userId.value = id;
|
||||
// Load the user data
|
||||
getUserData();
|
||||
}
|
||||
|
||||
export const onUserChange = (callback) => {
|
||||
watch(userId, (newValue) => {
|
||||
@@ -93,89 +17,48 @@ export const onUserChange = (callback) => {
|
||||
callback(newValue);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getUserCustomerNumber = () => {
|
||||
return user.customer_number.value;
|
||||
};
|
||||
|
||||
const applyUserData = (data = {}) => {
|
||||
user.id.value = data.id ?? "";
|
||||
user.customer_number.value = data.customer_number ?? "";
|
||||
user.display_name.value = data.display_name ?? "";
|
||||
user.email.value = data.email ?? "";
|
||||
user.phone.value = data.phone ?? null;
|
||||
user.group_id.value = data.group_id ?? "";
|
||||
user.created_at.value = data.created_at ?? "";
|
||||
user.updated_at.value = data.updated_at ?? "";
|
||||
setEconomicData(data.economic_customer ?? {});
|
||||
user.permissions.value = Array.isArray(data.permissions) ? data.permissions : [];
|
||||
user.attributes.value = Array.isArray(data.attributes) ? data.attributes : [];
|
||||
user.discounts.value = Array.isArray(data.discounts) ? data.discounts : [];
|
||||
user.orders_not_invoiced.value = Array.isArray(data.orders_not_invoiced) ? data.orders_not_invoiced : [];
|
||||
user.keys.value = data.keys && typeof data.keys === "object" ? { ...defaultKeys(), ...data.keys } : defaultKeys();
|
||||
user.wash_subscription_transactions.value = Array.isArray(data.wash_subscription_transactions)
|
||||
? data.wash_subscription_transactions
|
||||
: [];
|
||||
selectedUserLoaded.value = true;
|
||||
};
|
||||
}
|
||||
|
||||
// Get the user data
|
||||
export const getUserData = async (id = userId.value) => {
|
||||
const normalizedId = normalizeUserId(id);
|
||||
if (!normalizedId) {
|
||||
userLoadError.value = new Error("Invalid user id");
|
||||
resetUser();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (activeLoadPromise && activeLoadUserId === normalizedId) {
|
||||
return activeLoadPromise;
|
||||
}
|
||||
|
||||
const requestId = ++loadRequestSequence;
|
||||
activeLoadUserId = normalizedId;
|
||||
isUserLoading.value = true;
|
||||
userLoadError.value = null;
|
||||
|
||||
activeLoadPromise = authenticatedRequest(`/superuser/user?user_id=${normalizedId}`, "GET")
|
||||
export const getUserData = async () => {
|
||||
return await authenticatedRequest(`/superuser/user?user_id=${userId.value}`, "GET")
|
||||
.then((response) => {
|
||||
if (requestId !== loadRequestSequence) {
|
||||
return user;
|
||||
user.id.value = response.data.data.id;
|
||||
user.customer_number.value = response.data.data.customer_number;
|
||||
user.group_id.value = response.data.data.group_id;
|
||||
user.created_at.value = response.data.data.created_at;
|
||||
user.updated_at.value = response.data.data.updated_at;
|
||||
if (response.data.data.economic_customer.name) {
|
||||
user.economicData.customerNumber.value = response.data.data.economic_customer.customerNumber;
|
||||
user.economicData.name.value = response.data.data.economic_customer.name;
|
||||
user.economicData.address.value = response.data.data.economic_customer.address;
|
||||
user.economicData.zip.value = response.data.data.economic_customer.zip;
|
||||
user.economicData.city.value = response.data.data.economic_customer.city;
|
||||
user.economicData.mobilePhone.value = response.data.data.economic_customer.mobilePhone;
|
||||
user.economicData.email.value = response.data.data.economic_customer.email;
|
||||
user.economicData.cvr.value = response.data.data.economic_customer.corporateIdentificationNumber;
|
||||
user.economicData.currency.value = response.data.data.economic_customer.currency;
|
||||
user.economicData.country.value = response.data.data.economic_customer.country;
|
||||
user.economicData.barred.value = response.data.data.economic_customer.barred ?? false;
|
||||
}
|
||||
|
||||
applyUserData(response?.data?.data ?? {});
|
||||
return user;
|
||||
user.permissions.value = response.data.data.permissions;
|
||||
user.attributes.value = response.data.data.attributes;
|
||||
user.discounts.value = response.data.data.discounts;
|
||||
user.orders_not_invoiced.value = response.data.data.orders_not_invoiced || null;
|
||||
user.keys.value = response.data.data.keys;
|
||||
user.wash_subscription_transactions.value = response.data.data.wash_subscription_transactions || null;
|
||||
console.log(user.discounts.value);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (requestId === loadRequestSequence) {
|
||||
resetUser({ preserveUserId: true });
|
||||
userLoadError.value = error;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.finally(() => {
|
||||
if (requestId === loadRequestSequence) {
|
||||
isUserLoading.value = false;
|
||||
activeLoadPromise = null;
|
||||
activeLoadUserId = null;
|
||||
}
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
return activeLoadPromise;
|
||||
};
|
||||
|
||||
const isDirectProductDiscount = (discount, productId) => {
|
||||
return discount?.product_or_category_id == productId && Number(discount?.is_category) === 0;
|
||||
};
|
||||
|
||||
const parseFixedPriceValue = (value) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(String(value), 10);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
/** Get user product discount */
|
||||
@@ -250,25 +133,12 @@ export const getUserProductOnlyDiscount = (product) => {
|
||||
// Find the discount for the product
|
||||
let discount = null;
|
||||
try {
|
||||
discount = user.discounts.value.find(discount => isDirectProductDiscount(discount, productId)) || null;
|
||||
discount = user.discounts.value.find(discount => discount.product_or_category_id == productId && !discount.is_category) || null;
|
||||
} catch (e) {}
|
||||
// If the discount is not found, return 0
|
||||
return discount ? discount.percentage : 0;
|
||||
};
|
||||
|
||||
export const getUserProductFixedPrice = (product) => {
|
||||
const productId = product.id;
|
||||
let fixedPriceDiscount = null;
|
||||
|
||||
try {
|
||||
fixedPriceDiscount = user.discounts.value.find((discount) => (
|
||||
isDirectProductDiscount(discount, productId) && parseFixedPriceValue(discount.fixed_price) !== null
|
||||
)) || null;
|
||||
} catch (e) {}
|
||||
|
||||
return fixedPriceDiscount ? parseFixedPriceValue(fixedPriceDiscount.fixed_price) : null;
|
||||
};
|
||||
|
||||
export const isProductAllowedToApplyCategoryDiscounts = (product) => {
|
||||
// Check if the product is allowed to apply category discounts
|
||||
return parseInt(product.apply_category_discount) === 1;
|
||||
@@ -296,27 +166,9 @@ export const getProductBestApplicableDiscount = (product) => {
|
||||
return Math.max(productDiscount, categoryDiscount, globalDiscount);
|
||||
}
|
||||
|
||||
export const getProductEffectivePrice = (product) => {
|
||||
const fixedPrice = getUserProductFixedPrice(product);
|
||||
if (fixedPrice !== null) {
|
||||
return fixedPrice;
|
||||
}
|
||||
|
||||
const basePrice = Number(product.price ?? 0);
|
||||
const discount = Number(getProductBestApplicableDiscount(product) || 0);
|
||||
if (!Number.isFinite(basePrice)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number((basePrice - (basePrice * (discount / 100))).toFixed(0));
|
||||
}
|
||||
|
||||
export const user = {
|
||||
id: ref(''),
|
||||
customer_number: ref(''),
|
||||
display_name: ref(''),
|
||||
email: ref(''),
|
||||
phone: ref(null),
|
||||
group_id: ref(''),
|
||||
created_at: ref(''),
|
||||
updated_at: ref(''),
|
||||
@@ -333,23 +185,23 @@ export const user = {
|
||||
country: ref(null),
|
||||
barred: ref(null),
|
||||
},
|
||||
permissions: ref([]),
|
||||
attributes: ref([]),
|
||||
discounts: ref([]),
|
||||
orders_not_invoiced: ref([]),
|
||||
keys: ref(defaultKeys()),
|
||||
wash_subscription_transactions: ref([]),
|
||||
permissions: ref(null),
|
||||
attributes: ref(null),
|
||||
discounts: ref(null),
|
||||
orders_not_invoiced: ref(null),
|
||||
keys: ref({
|
||||
open_invoice_draft: ref(null),
|
||||
}),
|
||||
wash_subscription_transactions: ref(null),
|
||||
functions: {
|
||||
getUserData: getUserData,
|
||||
getUserProductDiscount: getUserProductDiscount,
|
||||
getUserCategoryDiscount: getUserCategoryDiscount,
|
||||
getUserGlobalDiscount: getUserGlobalDiscount,
|
||||
getUserProductOnlyDiscount: getUserProductOnlyDiscount,
|
||||
getUserProductFixedPrice: getUserProductFixedPrice,
|
||||
isProductAllowedToApplyCategoryDiscounts: isProductAllowedToApplyCategoryDiscounts,
|
||||
getProductCategory: getProductCategory,
|
||||
getProductBestApplicableDiscount: getProductBestApplicableDiscount,
|
||||
getProductEffectivePrice: getProductEffectivePrice,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,506 +1,133 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||
import PageTitle from "@/components/global/PageTitle.vue";
|
||||
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { useRouter } from 'vue-router'
|
||||
import UserSubPageWrapper from "@/views/dashboards/superUserDashboard/user/UserSubPageWrapper.vue";
|
||||
import {
|
||||
isUserLoading,
|
||||
selectedUserLoaded,
|
||||
setUser,
|
||||
user,
|
||||
userId,
|
||||
userLoadError,
|
||||
} from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue";
|
||||
import UserDefaultDepartment from "@/views/dashboards/superUserDashboard/user/displays/UserDefaultDepartment.vue";
|
||||
// Get the user from the route
|
||||
const router = useRouter()
|
||||
import {user, getUserData, userId, setUser} from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue";
|
||||
import WhiteBox from "@/components/displays/boxes/WhiteBox.vue";
|
||||
import UserVehicleSubscriptionsDisplay
|
||||
from "@/views/dashboards/superUserDashboard/user/displays/vehicles/UserVehicleSubscriptionsDisplay.vue";
|
||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||
import UserFixedPricing from "@/views/dashboards/superUserDashboard/user/displays/UserFixedPricing.vue";
|
||||
import UserOtherSpecialArrangement from "@/views/dashboards/superUserDashboard/user/displays/other/UserOtherSpecialArrangement.vue";
|
||||
import UserOtherVaskeabonnement from "@/views/dashboards/superUserDashboard/user/displays/other/UserOtherVaskeabonnement.vue";
|
||||
import UserVehicleSubscriptionsDisplay from "@/views/dashboards/superUserDashboard/user/displays/vehicles/UserVehicleSubscriptionsDisplay.vue";
|
||||
import UserDefaultDepartment from "@/views/dashboards/superUserDashboard/user/displays/UserDefaultDepartment.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
// Set the user
|
||||
setUser(router.currentRoute.value.params.userId);
|
||||
|
||||
const vehicles = ref([]);
|
||||
const vehiclesLoading = ref(false);
|
||||
const vehiclesError = ref(null);
|
||||
|
||||
const customerNumber = computed(() => Number.parseInt(String(user.customer_number.value || 0), 10) || 0);
|
||||
const basePath = computed(() => `/superuser/users/${userId.value || route.params.userId}`);
|
||||
const permissions = computed(() => (Array.isArray(user.permissions.value) ? user.permissions.value : []));
|
||||
const attributes = computed(() => (Array.isArray(user.attributes.value) ? user.attributes.value : []));
|
||||
const discounts = computed(() => (Array.isArray(user.discounts.value) ? user.discounts.value : []));
|
||||
const ordersNotInvoiced = computed(() =>
|
||||
Array.isArray(user.orders_not_invoiced.value) ? user.orders_not_invoiced.value : []
|
||||
);
|
||||
const washSubscriptionTransactions = computed(() =>
|
||||
Array.isArray(user.wash_subscription_transactions.value) ? user.wash_subscription_transactions.value : []
|
||||
);
|
||||
|
||||
const displayName = computed(() => {
|
||||
return (
|
||||
user.economicData.name.value ||
|
||||
user.display_name.value ||
|
||||
(userId.value ? `${t("user_admin.user_id")} ${userId.value}` : t("user_admin.overview.loading_user"))
|
||||
);
|
||||
});
|
||||
const hasCustomerNumber = computed(() => customerNumber.value > 0);
|
||||
const hasEconomicData = computed(() => Boolean(user.economicData.name.value || user.economicData.customerNumber.value));
|
||||
const isEconomicBarred = computed(() => [true, 1, "1", "true"].includes(user.economicData.barred.value));
|
||||
const activeVehicleSubscriptions = computed(() => vehicles.value.filter((vehicle) => vehicle?.wash_subscription).length);
|
||||
const selfServiceVehicles = computed(() => vehicles.value.filter((vehicle) => vehicle?.xlvask).length);
|
||||
const visibleVehicles = computed(() => vehicles.value.slice(0, 8));
|
||||
const visiblePermissions = computed(() => permissions.value.slice(0, 12));
|
||||
const hiddenPermissionCount = computed(() => Math.max(permissions.value.length - visiblePermissions.value.length, 0));
|
||||
|
||||
const userDetails = computed(() => [
|
||||
{ label: t("user_admin.user_id"), value: userId.value },
|
||||
{ label: t("user_admin.customer_number"), value: customerNumber.value || null },
|
||||
{ label: t("user_admin.group_id"), value: user.group_id.value },
|
||||
{ label: t("common.email"), value: user.email?.value || user.economicData.email.value },
|
||||
{ label: t("common.created"), value: user.created_at.value },
|
||||
{ label: t("user_admin.updated_at"), value: user.updated_at.value },
|
||||
]);
|
||||
|
||||
const economicDetails = computed(() => [
|
||||
{ label: t("user_admin.customer_number"), value: user.economicData.customerNumber.value },
|
||||
{ label: t("common.name"), value: user.economicData.name.value },
|
||||
{ label: t("common.address"), value: user.economicData.address.value },
|
||||
{ label: t("user_admin.zip"), value: user.economicData.zip.value },
|
||||
{ label: t("user_admin.city"), value: user.economicData.city.value },
|
||||
{ label: t("user_admin.mobile_phone"), value: user.economicData.mobilePhone.value },
|
||||
{ label: t("common.email"), value: user.economicData.email.value },
|
||||
{ label: t("user_admin.cvr"), value: user.economicData.cvr.value },
|
||||
{ label: t("user_admin.currency"), value: user.economicData.currency.value },
|
||||
{ label: t("common.country"), value: user.economicData.country.value },
|
||||
{ label: t("user_admin.barred"), value: isEconomicBarred.value ? t("common.yes") : t("common.no") },
|
||||
]);
|
||||
|
||||
const overviewMetrics = computed(() => [
|
||||
{
|
||||
key: "customer",
|
||||
icon: "fas fa-id-card",
|
||||
label: t("user_admin.customer_number"),
|
||||
value: customerNumber.value || t("global.no_data"),
|
||||
status: hasCustomerNumber.value ? t("common.active") : t("user_admin.overview.missing"),
|
||||
tone: hasCustomerNumber.value ? "is-success" : "is-warning",
|
||||
},
|
||||
{
|
||||
key: "economic",
|
||||
icon: "fas fa-building",
|
||||
label: t("user_admin.economic_data"),
|
||||
value: hasEconomicData.value ? t("user_admin.overview.connected") : t("global.no_data"),
|
||||
status: isEconomicBarred.value ? t("user_admin.barred") : t("common.active"),
|
||||
tone: isEconomicBarred.value ? "is-danger" : "is-success",
|
||||
},
|
||||
{
|
||||
key: "vehicles",
|
||||
icon: "fas fa-car",
|
||||
label: t("common.vehicles"),
|
||||
value: vehicles.value.length,
|
||||
status: vehiclesLoading.value ? t("common.loading") : t("user_admin.overview.loaded"),
|
||||
tone: vehiclesError.value ? "is-warning" : "is-info",
|
||||
},
|
||||
{
|
||||
key: "subscriptions",
|
||||
icon: "fas fa-car-side",
|
||||
label: t("user_admin.wash_subscriptions"),
|
||||
value: activeVehicleSubscriptions.value,
|
||||
status: t("user_admin.overview.active_vehicles"),
|
||||
tone: activeVehicleSubscriptions.value > 0 ? "is-success" : "is-light",
|
||||
},
|
||||
{
|
||||
key: "orders",
|
||||
icon: "fas fa-file-invoice-dollar",
|
||||
label: t("user_admin.overview.not_invoiced"),
|
||||
value: ordersNotInvoiced.value.length,
|
||||
status: user.keys.value?.open_invoice_draft ? t("user_admin.overview.open_draft") : t("global.no_data"),
|
||||
tone: ordersNotInvoiced.value.length > 0 ? "is-warning" : "is-success",
|
||||
},
|
||||
{
|
||||
key: "discounts",
|
||||
icon: "fas fa-tags",
|
||||
label: t("common.discount"),
|
||||
value: discounts.value.length,
|
||||
status: t("user_admin.overview.price_overrides"),
|
||||
tone: discounts.value.length > 0 ? "is-link" : "is-light",
|
||||
},
|
||||
{
|
||||
key: "attributes",
|
||||
icon: "fas fa-sliders-h",
|
||||
label: t("user_admin.overview.attributes"),
|
||||
value: attributes.value.length,
|
||||
status: t("user_admin.overview.customer_rules"),
|
||||
tone: attributes.value.length > 0 ? "is-info" : "is-light",
|
||||
},
|
||||
{
|
||||
key: "permissions",
|
||||
icon: "fas fa-shield-alt",
|
||||
label: t("user_admin.permissions"),
|
||||
value: permissions.value.length,
|
||||
status: t("user_admin.overview.effective_access"),
|
||||
tone: permissions.value.length > 0 ? "is-info" : "is-warning",
|
||||
},
|
||||
]);
|
||||
|
||||
const hubLinks = computed(() => [
|
||||
{ key: "orders", icon: "fas fa-file-alt", label: t("superuser.nav.orders"), to: `${basePath.value}/orders` },
|
||||
{ key: "pricing", icon: "fas fa-tags", label: t("user_admin.overview.pricing"), to: `${basePath.value}/pricing` },
|
||||
{ key: "other", icon: "fas fa-sliders-h", label: t("user_admin.other"), to: `${basePath.value}/other` },
|
||||
{ key: "vehicles", icon: "fas fa-car", label: t("common.vehicles"), to: `${basePath.value}/vehicles` },
|
||||
{
|
||||
key: "xlvask",
|
||||
icon: "fas fa-water",
|
||||
label: SessionUser.superUser.modules.xlvask.meta.title,
|
||||
to: `${basePath.value}/xlvask`,
|
||||
},
|
||||
]);
|
||||
|
||||
const valueOrEmpty = (value) => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return t("global.no_data");
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const loadVehicles = async () => {
|
||||
if (!hasCustomerNumber.value) {
|
||||
vehicles.value = [];
|
||||
vehiclesError.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
vehiclesLoading.value = true;
|
||||
vehiclesError.value = null;
|
||||
|
||||
try {
|
||||
const response = await SessionUser.objects.vehicles.get.user(customerNumber.value);
|
||||
vehicles.value = Array.isArray(response) ? response : [];
|
||||
} catch (error) {
|
||||
vehicles.value = [];
|
||||
vehiclesError.value = error;
|
||||
} finally {
|
||||
vehiclesLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const reloadOverview = async () => {
|
||||
await setUser(route.params.userId);
|
||||
await loadVehicles();
|
||||
};
|
||||
|
||||
watch(
|
||||
customerNumber,
|
||||
() => {
|
||||
loadVehicles();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessSuperUser()">
|
||||
<UserSubPageWrapper>
|
||||
<template #title>
|
||||
<PageTitle :title="$t('user_admin.title')" :subtitle="$t('user_admin.overview.subtitle')" />
|
||||
<PageTitle :title="$t('user_admin.title')" :subtitle="$t('user_admin.subtitle')" />
|
||||
</template>
|
||||
|
||||
<section class="user-overview" data-testid="superuser-user-overview-page">
|
||||
<div v-if="isUserLoading && !selectedUserLoaded" class="box" data-testid="superuser-user-overview-loading">
|
||||
<p class="has-text-weight-semibold">{{ $t("user_admin.overview.loading_user") }}</p>
|
||||
<p class="has-text-grey">{{ $t("user_admin.orders.loading_subtitle") }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="userLoadError" class="notification is-danger" data-testid="superuser-user-overview-error">
|
||||
<p class="has-text-weight-semibold">{{ $t("user_admin.overview.load_failed") }}</p>
|
||||
<p>{{ userLoadError?.response?.data?.message || userLoadError?.message || $t("common.unknown_error") }}</p>
|
||||
<button class="button is-light mt-3" type="button" data-testid="superuser-user-overview-retry" @click="reloadOverview">
|
||||
{{ $t("common.retry") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-else-if="selectedUserLoaded">
|
||||
<div class="user-overview__header box" data-testid="superuser-user-overview-header">
|
||||
<div>
|
||||
<p class="heading">{{ $t("user_admin.overview.management_hub") }}</p>
|
||||
<h2 class="title is-3 mb-2">{{ displayName }}</h2>
|
||||
<p class="subtitle is-6 mb-0">
|
||||
{{ $t("user_admin.user_id") }} {{ userId }}
|
||||
<span v-if="hasCustomerNumber">· {{ $t("user_admin.customer_number") }} {{ customerNumber }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="user-overview__header-actions">
|
||||
<ActionSettingsWheelButton
|
||||
:user_id="userId"
|
||||
:customer_number="customerNumber || null"
|
||||
data-testid="superuser-user-overview-actions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-overview__metrics columns is-multiline" data-testid="superuser-user-overview-metrics">
|
||||
<div v-for="metric in overviewMetrics" :key="metric.key" class="column is-3-desktop is-6-tablet">
|
||||
<div class="box user-overview__metric" :data-testid="`superuser-user-overview-metric-${metric.key}`">
|
||||
<div class="user-overview__metric-top">
|
||||
<span class="icon"><i :class="metric.icon"></i></span>
|
||||
<span class="tag is-light" :class="metric.tone">{{ metric.status }}</span>
|
||||
</div>
|
||||
<p class="heading">{{ metric.label }}</p>
|
||||
<p class="title is-4">{{ metric.value }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box" data-testid="superuser-user-overview-shortcuts">
|
||||
<div class="level is-mobile user-overview__section-title">
|
||||
<div class="level-left">
|
||||
<h3 class="title is-5 mb-0">{{ $t("user_admin.overview.workspace_shortcuts") }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<router-link
|
||||
v-for="link in hubLinks"
|
||||
:key="link.key"
|
||||
class="button is-light"
|
||||
:to="link.to"
|
||||
:data-testid="`superuser-user-overview-link-${link.key}`"
|
||||
>
|
||||
<span class="icon"><i :class="link.icon"></i></span>
|
||||
<span>{{ link.label }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-6-desktop">
|
||||
<section class="box" data-testid="superuser-user-overview-account">
|
||||
<h3 class="title is-5">{{ $t("user_admin.user_data") }}</h3>
|
||||
<dl class="user-overview__details">
|
||||
<template v-for="detail in userDetails" :key="detail.label">
|
||||
<dt>{{ detail.label }}</dt>
|
||||
<dd>{{ valueOrEmpty(detail.value) }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-desktop">
|
||||
<section class="box" data-testid="superuser-user-overview-economic">
|
||||
<h3 class="title is-5">{{ $t("user_admin.economic_data") }}</h3>
|
||||
<dl class="user-overview__details">
|
||||
<template v-for="detail in economicDetails" :key="detail.label">
|
||||
<dt>{{ detail.label }}</dt>
|
||||
<dd>{{ valueOrEmpty(detail.value) }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-6-desktop">
|
||||
<section class="box" data-testid="superuser-user-overview-access">
|
||||
<h3 class="title is-5">{{ $t("user_admin.permissions") }}</h3>
|
||||
<div v-if="visiblePermissions.length" class="tags">
|
||||
<span
|
||||
v-for="permission in visiblePermissions"
|
||||
:key="permission"
|
||||
class="tag is-info is-light"
|
||||
:data-testid="`superuser-user-overview-permission-${permission}`"
|
||||
>
|
||||
{{ permission }}
|
||||
</span>
|
||||
<span v-if="hiddenPermissionCount > 0" class="tag">
|
||||
{{ $t("user_admin.overview.more_permissions", { count: hiddenPermissionCount }) }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-else class="has-text-grey">{{ $t("global.no_data") }}</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-desktop">
|
||||
<section class="box" data-testid="superuser-user-overview-rules">
|
||||
<h3 class="title is-5">{{ $t("user_admin.overview.customer_rules") }}</h3>
|
||||
<ActionSettingsWheelButton
|
||||
v-if="hasCustomerNumber"
|
||||
:user_id="userId"
|
||||
:customer_number="customerNumber"
|
||||
:display-actions-directly="true"
|
||||
data-testid="superuser-user-overview-direct-actions"
|
||||
/>
|
||||
<p v-else class="has-text-grey">{{ $t("user_admin.overview.customer_number_required") }}</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="hasCustomerNumber" class="box" data-testid="superuser-user-overview-customer-management">
|
||||
<h3 class="title is-5">{{ $t("user_admin.overview.customer_management") }}</h3>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-6-desktop">
|
||||
<UserFixedPricing :key="`fixed-${customerNumber}`" :customer_number="customerNumber" />
|
||||
</div>
|
||||
<div class="column is-6-desktop">
|
||||
<UserDefaultDepartment :key="`department-${customerNumber}`" :customer_number="customerNumber" />
|
||||
</div>
|
||||
<div class="column is-6-desktop">
|
||||
<UserOtherSpecialArrangement :key="`special-${userId}`" :user_id="userId" />
|
||||
</div>
|
||||
<div class="column is-6-desktop">
|
||||
<UserOtherVaskeabonnement :key="`subscription-note-${userId}`" :user_id="userId" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else class="notification is-warning" data-testid="superuser-user-overview-no-customer-number">
|
||||
{{ $t("user_admin.overview.customer_number_required") }}
|
||||
</section>
|
||||
|
||||
<section v-if="hasCustomerNumber" class="box" data-testid="superuser-user-overview-vehicles">
|
||||
<div class="level is-mobile user-overview__section-title">
|
||||
<div class="level-left">
|
||||
<h3 class="title is-5 mb-0">{{ $t("common.vehicles") }}</h3>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<button
|
||||
class="button is-small is-light"
|
||||
type="button"
|
||||
:disabled="vehiclesLoading"
|
||||
data-testid="superuser-user-overview-vehicles-reload"
|
||||
@click="loadVehicles"
|
||||
>
|
||||
{{ $t("global.reload") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="vehiclesError" class="notification is-warning" data-testid="superuser-user-overview-vehicles-error">
|
||||
{{ $t("user_admin.overview.vehicles_load_failed") }}
|
||||
</div>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-4">
|
||||
<div class="box user-overview__compact-stat">
|
||||
<p class="heading">{{ $t("common.vehicles") }}</p>
|
||||
<p class="title is-4">{{ vehicles.length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="box user-overview__compact-stat">
|
||||
<p class="heading">{{ $t("user_admin.wash_subscriptions") }}</p>
|
||||
<p class="title is-4">{{ activeVehicleSubscriptions }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="box user-overview__compact-stat">
|
||||
<p class="heading">{{ SessionUser.superUser.modules.xlvask.meta.title }}</p>
|
||||
<p class="title is-4">{{ selfServiceVehicles }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="visibleVehicles.length" class="tags" data-testid="superuser-user-overview-vehicle-preview">
|
||||
<span
|
||||
v-for="vehicle in visibleVehicles"
|
||||
:key="vehicle.id || vehicle.reg"
|
||||
class="tag is-light"
|
||||
:data-testid="`superuser-user-overview-vehicle-${vehicle.id || vehicle.reg}`"
|
||||
>
|
||||
{{ vehicle.reg || vehicle.reference || `#${vehicle.id}` }}
|
||||
<div class="columns is-multiline">
|
||||
<!-- Customer small -->
|
||||
<div class="column is-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<!-- User icon -->
|
||||
<div class="card-header-icon">
|
||||
<span class="icon">
|
||||
<i class="fas fa-user"></i>
|
||||
</span>
|
||||
</div>
|
||||
<p v-else-if="!vehiclesLoading" class="has-text-grey">{{ $t("global.no_data") }}</p>
|
||||
<div class="buttons">
|
||||
<router-link class="button is-light" :to="`${basePath}/vehicles`" data-testid="superuser-user-overview-open-vehicles">
|
||||
<span class="icon"><i class="fas fa-list"></i></span>
|
||||
<span>{{ $t("user_admin.overview.open_vehicles") }}</span>
|
||||
</router-link>
|
||||
<button
|
||||
class="button is-light"
|
||||
type="button"
|
||||
data-testid="superuser-user-overview-add-vehicle"
|
||||
@click="SessionUser.objects.vehicles.functions.showCreateObjectForCustomerForm(customerNumber, loadVehicles)"
|
||||
>
|
||||
<span class="icon"><i class="fas fa-plus"></i></span>
|
||||
<span>{{ $t("user_vehicles.add_vehicle") }}</span>
|
||||
</button>
|
||||
<!-- User name -->
|
||||
<div class="card-header-title">
|
||||
{{ user.economicData.name }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="hasCustomerNumber" class="box" data-testid="superuser-user-overview-subscriptions">
|
||||
<h3 class="title is-5">{{ $t("user_admin.overview.subscription_invoicing") }}</h3>
|
||||
<UserVehicleSubscriptionsDisplay :key="`subscriptions-${customerNumber}`" :user="user" />
|
||||
<p v-if="washSubscriptionTransactions.length === 0" class="has-text-grey">
|
||||
{{ $t("user_admin.overview.no_subscription_transactions") }}
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
</section>
|
||||
<!-- User settings wheel -->
|
||||
<div class="card-header-icon">
|
||||
<ActionSettingsWheelButton
|
||||
:user_id="userId"
|
||||
>
|
||||
<template #actions>
|
||||
<!-- Since there's no additional actions, we can use the default slot -->
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<div class="box">
|
||||
<!-- User fixed pricing -->
|
||||
<div class="mb-2">
|
||||
<UserFixedPricing v-if="user.customer_number.value && user.customer_number.value > 0" v-bind:customer_number="user.customer_number.value" />
|
||||
</div>
|
||||
<!-- Default department -->
|
||||
<div class="mb-2">
|
||||
<UserDefaultDepartment v-if="user.customer_number.value && user.customer_number.value > 0" v-bind:customer_number="user.customer_number.value" />
|
||||
</div>
|
||||
<!-- Vehicle subscriptions -->
|
||||
<UserVehicleSubscriptionsDisplay
|
||||
v-if="user.customer_number.value && user.customer_number.value > 0"
|
||||
:user="user"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<h3 class="title is-4">{{ $t('user_admin.user_data') }}</h3>
|
||||
<div class="content">
|
||||
<p>{{ $t('user_admin.user_id') }}: {{ userId }}</p>
|
||||
<p>{{ $t('user_admin.customer_number') }}: {{ user.customer_number }}</p>
|
||||
<p>{{ $t('user_admin.group_id') }}: {{ user.group_id }}</p>
|
||||
<p>{{ $t('common.created') }}: {{ user.created_at }}</p>
|
||||
<p>{{ $t('user_admin.updated_at') }}: {{ user.updated_at }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Economic data -->
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<h3 class="title is-4">{{ $t('user_admin.economic_data') }} ({{ user.economicData.name ? user.economicData.name : $t('user_admin.no_data') }})</h3>
|
||||
<div class="content">
|
||||
<p>{{ $t('user_admin.customer_number') }}: {{ user.economicData.customerNumber }}</p>
|
||||
<p>{{ $t('common.name') }}: {{ user.economicData.name }}</p>
|
||||
<p>{{ $t('common.address') }}: {{ user.economicData.address }}</p>
|
||||
<p>{{ $t('user_admin.zip') }}: {{ user.economicData.zip }}</p>
|
||||
<p>{{ $t('user_admin.city') }}: {{ user.economicData.city }}</p>
|
||||
<p>{{ $t('user_admin.mobile_phone') }}: {{ user.economicData.mobilePhone }}</p>
|
||||
<p>{{ $t('common.email') }}: {{ user.economicData.email }}</p>
|
||||
<p>{{ $t('user_admin.cvr') }}: {{ user.economicData.cvr }}</p>
|
||||
<p>{{ $t('user_admin.currency') }}: {{ user.economicData.currency }}</p>
|
||||
<p>{{ $t('common.country') }}: {{ user.economicData.country }}</p>
|
||||
<p>{{ $t('user_admin.barred') }}: {{user.economicData.barred }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Permissions -->
|
||||
<div class="column is-12">
|
||||
<div class="box">
|
||||
<h3 class="title is-4">{{ $t('user_admin.permissions') }}</h3>
|
||||
<div class="content">
|
||||
<ul>
|
||||
<li v-for="permission in user.permissions.value" :key="permission.id">{{ permission }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Variables -->
|
||||
<div class="column is-12">
|
||||
<div class="box">
|
||||
<h3 class="title is-4">{{ $t('user_admin.variables') }}</h3>
|
||||
<div class="content">
|
||||
<p>SessionUser: {{ SessionUser.valueOf() }}</p>
|
||||
<p>userId: {{ userId }}</p>
|
||||
<p>user: {{ user.valueOf() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UserSubPageWrapper>
|
||||
</RestrictedPageWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.user-overview {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.user-overview__header {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.user-overview__header-actions {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.user-overview__metric,
|
||||
.user-overview__compact-stat {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.user-overview__metric-top {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.user-overview__details {
|
||||
display: grid;
|
||||
gap: 0.5rem 1rem;
|
||||
grid-template-columns: minmax(8rem, 38%) 1fr;
|
||||
}
|
||||
|
||||
.user-overview__details dt {
|
||||
color: #6b7280;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-overview__details dd {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.user-overview__section-title {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.user-overview__header {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.user-overview__header-actions {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.user-overview__details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
import UserSubPageWrapper from "@/views/dashboards/superUserDashboard/user/UserSubPageWrapper.vue";
|
||||
import PageTitle from "@/components/global/PageTitle.vue";
|
||||
import { useRouter } from 'vue-router'
|
||||
import { user, getUserData, userId, setUser, getUserProductDiscount, getUserGlobalDiscount, getProductBestApplicableDiscount, getUserProductOnlyDiscount, getUserProductFixedPrice, getProductEffectivePrice, getProductCategory, getUserCategoryDiscount, isProductAllowedToApplyCategoryDiscounts } from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue";
|
||||
import { user, getUserData, userId, setUser, getUserProductDiscount, getUserGlobalDiscount, getProductBestApplicableDiscount, getUserProductOnlyDiscount, getProductCategory, getUserCategoryDiscount, isProductAllowedToApplyCategoryDiscounts } from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue";
|
||||
import { getProducts } from "@/components/shop/Products.vue";
|
||||
import { ref } from 'vue';
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
@@ -56,72 +56,6 @@ const editDiscount = (product, isCategory) => {
|
||||
allowOutsideClick: () => !Swal.isLoading()
|
||||
});
|
||||
}
|
||||
|
||||
const formatPrice = (price) => `${Number(price || 0).toFixed(0)} Kr.`;
|
||||
|
||||
const getProductPriceDisplay = (product) => {
|
||||
const fixedPrice = getUserProductFixedPrice(product);
|
||||
if (fixedPrice !== null) {
|
||||
return `${formatPrice(fixedPrice)} (fast pris)`;
|
||||
}
|
||||
|
||||
const discount = getProductBestApplicableDiscount(product);
|
||||
if (discount === 0) {
|
||||
return formatPrice(product.price);
|
||||
}
|
||||
|
||||
return `${formatPrice(getProductEffectivePrice(product))} (${discount}% rabat)`;
|
||||
};
|
||||
|
||||
const editFixedPrice = (product) => {
|
||||
const fixedPrice = getUserProductFixedPrice(product);
|
||||
Swal.fire({
|
||||
title: product.name + ' ( product: ' + product.id + ' )',
|
||||
input: 'number',
|
||||
inputValue: fixedPrice === null ? '' : fixedPrice,
|
||||
inputLabel: 'Fast pris (Kr.)',
|
||||
inputAttributes: {
|
||||
autocapitalize: 'off',
|
||||
min: 0,
|
||||
step: 1
|
||||
},
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Save',
|
||||
showLoaderOnConfirm: true,
|
||||
inputValidator: (value) => {
|
||||
const normalizedValue = String(value ?? '').trim();
|
||||
if (normalizedValue === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedValue = Number(normalizedValue);
|
||||
if (!Number.isInteger(parsedValue) || parsedValue < 0) {
|
||||
return 'Fast pris skal være et heltal eller tom.';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
preConfirm: (value) => {
|
||||
const normalizedValue = String(value ?? '').trim();
|
||||
const fixedPriceValue = normalizedValue === '' ? null : Number.parseInt(normalizedValue, 10);
|
||||
return authenticatedRequest(`/superuser/user/discounts`, "POST", {
|
||||
user_id: userId.value,
|
||||
object_id: product.id,
|
||||
discount: getUserProductOnlyDiscount(product),
|
||||
fixed_price: fixedPriceValue,
|
||||
is_category: false
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
getUserData();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
allowOutsideClick: () => !Swal.isLoading()
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -140,7 +74,6 @@ const editFixedPrice = (product) => {
|
||||
<th>{{ $t('tables.common.category') }}</th>
|
||||
<th>{{ $t('tables.common.product_name') }}</th>
|
||||
<th>{{ $t('tables.common.price') }}</th>
|
||||
<th>Fast pris</th>
|
||||
<th>{{ $t('tables.common.discount_item') }}</th>
|
||||
<th>{{ $t('tables.common.discount_category') }}</th>
|
||||
</tr>
|
||||
@@ -151,16 +84,8 @@ const editFixedPrice = (product) => {
|
||||
<td>{{ getProductCategory(product)}}</td>
|
||||
<td>{{ product.name }}</td>
|
||||
<!-- Price -->
|
||||
<td>{{ getProductPriceDisplay(product) }}</td>
|
||||
|
||||
<!-- Fixed price -->
|
||||
<td
|
||||
class="is-clickable"
|
||||
@click="editFixedPrice(product)"
|
||||
:data-testid="`customer-fixed-price-${product.id}`"
|
||||
><template v-if="getUserProductFixedPrice(product) !== null">{{ getUserProductFixedPrice(product) }} Kr.</template><template v-else>-</template>
|
||||
<i class="is-pulled-right fas fa-edit"></i>
|
||||
</td>
|
||||
<td v-if="getProductBestApplicableDiscount(product) === 0">{{ product.price }} Kr.</td>
|
||||
<td v-else>{{ (product.price - (product.price * (getProductBestApplicableDiscount(product) / 100))).toFixed(0) }} Kr. ({{ getProductBestApplicableDiscount(product) }}% rabat)</td>
|
||||
|
||||
<!-- Discount: Item -->
|
||||
<td
|
||||
@@ -197,4 +122,4 @@ const editFixedPrice = (product) => {
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,19 +1,12 @@
|
||||
<script setup>
|
||||
import { watch } from "vue";
|
||||
|
||||
import SuperUserDashboardUserNavigation from "@/views/dashboards/superUserDashboard/user/SuperUserDashboardUserNavigation.vue";
|
||||
import SuperUserDashboardNavigation from "@/views/dashboards/superUserDashboard/SuperUserDashboardNavigation.vue";
|
||||
import { setUser } from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import {user, getUserData, userId, setUser, getUserCustomerNumber} from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
const router = useRouter()
|
||||
setUser(router.currentRoute.value.params.userId);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
watch(
|
||||
() => route.params.userId,
|
||||
(nextUserId) => {
|
||||
setUser(nextUserId);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -28,4 +21,4 @@ watch(
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import ConfigurationCategory from "@/components/displays/superuser/configuration/ConfigurationCategory.vue";
|
||||
import ConfigurationInputNumber from "@/components/displays/superuser/configuration/ConfigurationInputNumber.vue";
|
||||
import ConfigurationInput from "@/components/displays/superuser/configuration/ConfigurationInput.vue";
|
||||
import Swal from "sweetalert2";
|
||||
import ConfigurationSelect from "@/components/displays/superuser/configuration/ConfigurationSelect.vue";
|
||||
@@ -25,6 +26,7 @@ const getDefaultDepartment = async () => {
|
||||
customer_number: props.customer_number,
|
||||
}).then((response) => {
|
||||
let responseData = response.data.data;
|
||||
console.log(response.data.data, 'Default department');
|
||||
department_value.value = responseData.department;
|
||||
hasDefaultDepartment.value = true;
|
||||
}).catch((error) => {
|
||||
@@ -38,7 +40,8 @@ const onClickSubmit = async () => {
|
||||
await SessionUser.request('/customer/department/default', 'POST', {
|
||||
customer_number: props.customer_number,
|
||||
department: department_value.value,
|
||||
}).then(() => {
|
||||
}).then((response) => {
|
||||
console.log(response.data.data, 'Created default department');
|
||||
// Get the newly created fixed pricing
|
||||
getDefaultDepartment();
|
||||
}).catch((error) => {
|
||||
@@ -72,7 +75,8 @@ const onDeleteConfirmed = async () => {
|
||||
// Delete the fixed pricing
|
||||
await SessionUser.request('/customer/department/default', 'DELETE', {
|
||||
customer_number: props.customer_number,
|
||||
}).then(() => {
|
||||
}).then((response) => {
|
||||
console.log(response.data.data, 'Deleted default department');
|
||||
// Get the newly created fixed pricing
|
||||
getDefaultDepartment();
|
||||
}).catch((error) => {
|
||||
@@ -159,4 +163,4 @@ getDepartmentOptions();
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import ConfigurationCategory from "@/components/displays/superuser/configuration/ConfigurationCategory.vue";
|
||||
import ConfigurationInputNumber from "@/components/displays/superuser/configuration/ConfigurationInputNumber.vue";
|
||||
import ConfigurationInput from "@/components/displays/superuser/configuration/ConfigurationInput.vue";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
@@ -26,6 +27,8 @@ const getFixedPricing = async () => {
|
||||
await SessionUser.request('/customer/pricing/fixed', 'GET', {
|
||||
customer_number: props.customer_number,
|
||||
}).then((response) => {
|
||||
let responseData = response.data.data;
|
||||
console.log(response.data.data, 'Fixed pricing');
|
||||
pricing.value = response.data.data.price;
|
||||
description.value = response.data.data.description;
|
||||
hasFixedPricing.value = true;
|
||||
@@ -41,7 +44,8 @@ const onClickSubmit = async () => {
|
||||
customer_number: props.customer_number,
|
||||
price: inputPrice.value,
|
||||
description: inputDescription.value,
|
||||
}).then(() => {
|
||||
}).then((response) => {
|
||||
console.log(response.data.data, 'Fixed pricing');
|
||||
// Get the newly created fixed pricing
|
||||
getFixedPricing();
|
||||
}).catch((error) => {
|
||||
@@ -77,7 +81,8 @@ const onDeleteConfirmed = async () => {
|
||||
// Delete the fixed pricing
|
||||
await SessionUser.request('/customer/pricing/fixed', 'DELETE', {
|
||||
customer_number: props.customer_number,
|
||||
}).then(() => {
|
||||
}).then((response) => {
|
||||
console.log(response.data.data, 'Deleted fixed pricing');
|
||||
// Get the newly created fixed pricing
|
||||
getFixedPricing();
|
||||
}).catch((error) => {
|
||||
@@ -162,4 +167,4 @@ getFixedPricing();
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
+58
-59
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { ref } from 'vue';
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import Swal from "sweetalert2";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -10,73 +10,79 @@ const props = defineProps({
|
||||
user_id: Number,
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
default: false
|
||||
},
|
||||
textOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const OtherSpecialArrangementValue = ref(null);
|
||||
const userKeys = ref({});
|
||||
|
||||
const showLoadError = () => {
|
||||
Swal.fire({
|
||||
title: t("common.error"),
|
||||
text: t("common.unable_to_load"),
|
||||
icon: "error",
|
||||
confirmButtonText: t("common.ok"),
|
||||
});
|
||||
};
|
||||
|
||||
const showSaveError = () => {
|
||||
Swal.fire({
|
||||
title: t("common.error"),
|
||||
text: t("common.unknown_error"),
|
||||
icon: "error",
|
||||
confirmButtonText: t("common.ok"),
|
||||
});
|
||||
};
|
||||
const userKeys = ref([{
|
||||
key: '',
|
||||
value: ''
|
||||
}]);
|
||||
|
||||
const getKeys = async () => {
|
||||
if (!props.user_id) {
|
||||
userKeys.value = {};
|
||||
OtherSpecialArrangementValue.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await SessionUser.request(`/superuser/user/keys?user_id=${props.user_id}`, "GET");
|
||||
userKeys.value = response?.data?.data || {};
|
||||
OtherSpecialArrangementValue.value = userKeys.value.OtherSpecialArrangement || null;
|
||||
} catch (error) {
|
||||
showLoadError();
|
||||
}
|
||||
// Get the special arrangement
|
||||
await SessionUser.request('/superuser/user/keys?user_id=' + props.user_id, 'GET').then((response) => {
|
||||
userKeys.value = response.data.data;
|
||||
console.log(userKeys.value);
|
||||
// Check if the key exists
|
||||
try {
|
||||
if (userKeys.value.OtherSpecialArrangement) {
|
||||
OtherSpecialArrangementValue.value = userKeys.value.OtherSpecialArrangement;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Key does not exist');
|
||||
}
|
||||
}).catch((error) => {
|
||||
Swal.fire({
|
||||
title: 'Fejl',
|
||||
html: '<p>Der skete en fejl ved hentning af dataen. <br>Prøv igen, eller kontakt support.</p>',
|
||||
icon: 'error',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const setKey = async (key, value) => {
|
||||
await SessionUser.request("/superuser/user/keys", "POST", {
|
||||
// Set the key
|
||||
await SessionUser.request('/superuser/user/keys', 'POST', {
|
||||
user_id: props.user_id,
|
||||
key: key,
|
||||
value: value,
|
||||
value: value
|
||||
}).then((response) => {
|
||||
console.log(response);
|
||||
getKeys();
|
||||
});
|
||||
};
|
||||
|
||||
getKeys();
|
||||
const saveForm = async () => {
|
||||
if (props.readOnly) {
|
||||
return;
|
||||
}
|
||||
// Save the form
|
||||
console.log('Save the form');
|
||||
await setKey('OtherSpecialArrangement', OtherSpecialArrangementValue.value ?? '').then((response) => {
|
||||
console.log(response);
|
||||
}).catch((error) => {
|
||||
// Show the error with a sweetalert
|
||||
Swal.fire({
|
||||
title: 'Fejl',
|
||||
html: '<p>Der skete en fejl ved gemningen af dataen. <br>Prøv igen, eller kontakt support.</p>',
|
||||
icon: 'error',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await setKey("OtherSpecialArrangement", OtherSpecialArrangementValue.value ?? "");
|
||||
await getKeys();
|
||||
} catch (error) {
|
||||
showSaveError();
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.user_id, getKeys, { immediate: true });
|
||||
const parseLineBreaks = (text) => {
|
||||
return text.replace(/\n/g, '<br>');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -84,12 +90,7 @@ watch(() => props.user_id, getKeys, { immediate: true });
|
||||
<div class="field">
|
||||
<label class="label is-size-3">{{ t('superuser.user.special_arrangement.label') }}</label>
|
||||
<div class="control">
|
||||
<textarea
|
||||
class="textarea"
|
||||
v-model="OtherSpecialArrangementValue"
|
||||
:placeholder="t('superuser.user.special_arrangement.label')"
|
||||
data-testid="superuser-user-special-arrangement-input"
|
||||
></textarea>
|
||||
<textarea class="textarea" v-model="OtherSpecialArrangementValue" placeholder="Udfyld beskrivelse, hvis der er en intern aftale"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" v-if="!props.readOnly">
|
||||
@@ -102,7 +103,7 @@ watch(() => props.user_id, getKeys, { immediate: true });
|
||||
<div class="message is-info" v-if="OtherSpecialArrangementValue">
|
||||
<div class="message-body">
|
||||
<span><strong>{{ t('superuser.user.special_arrangement.label') }}</strong><br></span>
|
||||
<span v-if="OtherSpecialArrangementValue" class="preserve-lines">{{ OtherSpecialArrangementValue }}</span>
|
||||
<span v-if="OtherSpecialArrangementValue" v-html="parseLineBreaks(OtherSpecialArrangementValue)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,7 +117,7 @@ watch(() => props.user_id, getKeys, { immediate: true });
|
||||
<div
|
||||
class="message-body"
|
||||
>
|
||||
<p v-if="OtherSpecialArrangementValue" class="preserve-lines">{{ OtherSpecialArrangementValue }}</p>
|
||||
<p v-if="OtherSpecialArrangementValue" v-html="parseLineBreaks(OtherSpecialArrangementValue)"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,7 +125,5 @@ watch(() => props.user_id, getKeys, { immediate: true });
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.preserve-lines {
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
|
||||
</style>
|
||||
+62
-59
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { ref, watch } from 'vue';
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import Swal from "sweetalert2";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -10,86 +10,91 @@ const props = defineProps({
|
||||
user_id: Number,
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
default: false
|
||||
},
|
||||
textOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const OtherVaskeabonnementValue = ref(null);
|
||||
const userKeys = ref({});
|
||||
|
||||
const showLoadError = () => {
|
||||
Swal.fire({
|
||||
title: t("common.error"),
|
||||
text: t("common.unable_to_load"),
|
||||
icon: "error",
|
||||
confirmButtonText: t("common.ok"),
|
||||
});
|
||||
};
|
||||
|
||||
const showSaveError = () => {
|
||||
Swal.fire({
|
||||
title: t("common.error"),
|
||||
text: t("common.unknown_error"),
|
||||
icon: "error",
|
||||
confirmButtonText: t("common.ok"),
|
||||
});
|
||||
};
|
||||
const userKeys = ref([{
|
||||
key: '',
|
||||
value: ''
|
||||
}]);
|
||||
|
||||
const getKeys = async () => {
|
||||
if (!props.user_id) {
|
||||
userKeys.value = {};
|
||||
OtherVaskeabonnementValue.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await SessionUser.request(`/superuser/user/keys?user_id=${props.user_id}`, "GET");
|
||||
userKeys.value = response?.data?.data || {};
|
||||
OtherVaskeabonnementValue.value = userKeys.value.OtherVaskeabonnement || null;
|
||||
} catch (error) {
|
||||
showLoadError();
|
||||
}
|
||||
// Get the special arrangement
|
||||
await SessionUser.request('/superuser/user/keys?user_id=' + props.user_id, 'GET').then((response) => {
|
||||
userKeys.value = response.data.data;
|
||||
console.log(userKeys.value);
|
||||
// Check if the key exists
|
||||
try {
|
||||
if (userKeys.value.OtherVaskeabonnement) {
|
||||
OtherVaskeabonnementValue.value = userKeys.value.OtherVaskeabonnement;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Key does not exist');
|
||||
}
|
||||
}).catch((error) => {
|
||||
Swal.fire({
|
||||
title: 'Fejl',
|
||||
html: '<p>Der skete en fejl ved hentning af dataen. <br>Prøv igen, eller kontakt support.</p>',
|
||||
icon: 'error',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const setKey = async (key, value) => {
|
||||
await SessionUser.request("/superuser/user/keys", "POST", {
|
||||
// Set the key
|
||||
await SessionUser.request('/superuser/user/keys', 'POST', {
|
||||
user_id: props.user_id,
|
||||
key: key,
|
||||
value: value,
|
||||
value: value
|
||||
}).then((response) => {
|
||||
console.log(response);
|
||||
getKeys();
|
||||
});
|
||||
};
|
||||
|
||||
getKeys();
|
||||
const saveForm = async () => {
|
||||
if (props.readOnly) {
|
||||
return;
|
||||
}
|
||||
// Save the form
|
||||
console.log('Save the form');
|
||||
await setKey('OtherVaskeabonnement', OtherVaskeabonnementValue.value ?? '').then((response) => {
|
||||
console.log(response);
|
||||
}).catch((error) => {
|
||||
// Show the error with a sweetalert
|
||||
Swal.fire({
|
||||
title: 'Fejl',
|
||||
html: '<p>Der skete en fejl ved gemningen af dataen. <br>Prøv igen, eller kontakt support.</p>',
|
||||
icon: 'error',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await setKey("OtherVaskeabonnement", OtherVaskeabonnementValue.value ?? "");
|
||||
await getKeys();
|
||||
} catch (error) {
|
||||
showSaveError();
|
||||
}
|
||||
};
|
||||
const parseLineBreaks = (text) => {
|
||||
return text.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
watch(() => props.user_id, getKeys, { immediate: true });
|
||||
watch(() => props.user_id, () => {
|
||||
getKeys();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="form" @submit.prevent="saveForm" v-if="!props.readOnly && !props.textOnly">
|
||||
<form class="form" @submit.prevent="saveForm" v-if="!readOnly && !textOnly">
|
||||
<div class="field">
|
||||
<label class="label is-size-3">{{ t('superuser.user.wash_subscription.label') }}</label>
|
||||
<div class="control">
|
||||
<textarea
|
||||
class="textarea"
|
||||
v-model="OtherVaskeabonnementValue"
|
||||
:placeholder="t('superuser.user.wash_subscription.label')"
|
||||
data-testid="superuser-user-wash-subscription-note-input"
|
||||
></textarea>
|
||||
<textarea class="textarea" v-model="OtherVaskeabonnementValue" placeholder="Udfyld beskrivelse, hvis der er en intern aftale"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
@@ -102,7 +107,7 @@ watch(() => props.user_id, getKeys, { immediate: true });
|
||||
<div class="message is-info" v-if="OtherVaskeabonnementValue">
|
||||
<div class="message-body">
|
||||
<span><strong>{{ t('superuser.user.wash_subscription.label') }}</strong><br></span>
|
||||
<span v-if="OtherVaskeabonnementValue" class="preserve-lines">{{ OtherVaskeabonnementValue }}</span>
|
||||
<span v-if="OtherVaskeabonnementValue" v-html="parseLineBreaks(OtherVaskeabonnementValue)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,7 +121,7 @@ watch(() => props.user_id, getKeys, { immediate: true });
|
||||
<div
|
||||
class="message-body"
|
||||
>
|
||||
<p v-if="OtherVaskeabonnementValue" class="preserve-lines">{{ OtherVaskeabonnementValue }}</p>
|
||||
<p v-if="OtherVaskeabonnementValue" v-html="parseLineBreaks(OtherVaskeabonnementValue)"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,7 +129,5 @@ watch(() => props.user_id, getKeys, { immediate: true });
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.preserve-lines {
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
|
||||
</style>
|
||||
+7
@@ -35,6 +35,7 @@ const washSubscriptionVehicles = ref(null);
|
||||
|
||||
const load = () => {
|
||||
if (!props.user.customer_number.value) {
|
||||
console.log('No customer number found');
|
||||
return;
|
||||
}
|
||||
// Load the vehicles
|
||||
@@ -141,8 +142,13 @@ const showAddSubscriptionInvoiceFormOtherMonth = () => {
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
const selected = result.value;
|
||||
console.log(selected);
|
||||
const monthNumber = selected.substring(5, 7);
|
||||
const yearNumber = selected.substring(0, 4);
|
||||
// Redirect to the invoice creation page
|
||||
console.log(
|
||||
'Month: ' + monthNumber + ', Year: ' + yearNumber
|
||||
);
|
||||
createVehicleSubscriptionInvoice(monthNumber, yearNumber);
|
||||
}
|
||||
});
|
||||
@@ -158,6 +164,7 @@ const createVehicleSubscriptionInvoice = (month, year) => {
|
||||
year: year,
|
||||
}
|
||||
).then((response) => {
|
||||
console.log(response);
|
||||
// Reload the data
|
||||
load();
|
||||
// Redirect to the invoice creation page
|
||||
|
||||
@@ -1369,7 +1369,6 @@ const scheduleRecentCompletedWashRefresh = (attempt = 0) => {
|
||||
}
|
||||
|
||||
recentCompletedRefreshTimeout.value = hostWindow.setTimeout(() => {
|
||||
recentCompletedRefreshTimeout.value = null;
|
||||
if (isMyWashStartUnmounted.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ async function suppressVueDevtoolsOverlay(page) {
|
||||
|
||||
async function primeSuperuserSession(page) {
|
||||
const token = "superuser-e2e-token";
|
||||
await primeMockSession(page, { token, bootPath: null });
|
||||
await primeMockSession(page, { token });
|
||||
}
|
||||
|
||||
async function prepareInvoiceDistributionPage(page, overrides = {}) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { apiPathPattern, mockApi, seedAuthenticatedState } from "./support/network.js";
|
||||
import { mockApi, seedAuthenticatedState } from "./support/network.js";
|
||||
import { isDesktopProject } from "./support/projects";
|
||||
|
||||
const json = (body: unknown, status = 200) => ({
|
||||
@@ -35,172 +35,6 @@ const userEnvelope = (rows: Array<Record<string, unknown>>) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const overviewUser = {
|
||||
id: 11,
|
||||
customer_number: 12345,
|
||||
display_name: "Anna Andersen",
|
||||
email: "anna@example.test",
|
||||
phone: {
|
||||
number: "12345678",
|
||||
country_code: 45,
|
||||
},
|
||||
group_id: 1,
|
||||
created_at: "2026-01-01 08:30:00",
|
||||
updated_at: "2026-02-01 09:45:00",
|
||||
economic_customer: {
|
||||
customerNumber: 12345,
|
||||
name: "Anna Transport",
|
||||
address: "Main Road 1",
|
||||
zip: "2100",
|
||||
city: "Copenhagen",
|
||||
mobilePhone: "12345678",
|
||||
email: "billing@example.test",
|
||||
corporateIdentificationNumber: "12345678",
|
||||
currency: "DKK",
|
||||
country: "Denmark",
|
||||
barred: false,
|
||||
},
|
||||
permissions: ["superuser", "user", "get_user", "set_custom_price"],
|
||||
attributes: [{ id: 1, attribute: "invoiceAllOrdersIndividually" }],
|
||||
discounts: [{ id: 999999, percentage: 10 }],
|
||||
orders_not_invoiced: [{ id: 501 }],
|
||||
keys: {
|
||||
open_invoice_draft: "DRAFT-1",
|
||||
OtherSpecialArrangement: "Night wash agreement",
|
||||
OtherVaskeabonnement: "Monthly wash subscription note",
|
||||
},
|
||||
wash_subscription_transactions: [
|
||||
{
|
||||
id: 6250,
|
||||
customer_id: 12345,
|
||||
cashier_id: 1857,
|
||||
reference: "Vaskeabonnementer",
|
||||
notes: "",
|
||||
department_id: 10,
|
||||
reg_1: "",
|
||||
reg_2: "",
|
||||
reg_3: "",
|
||||
completed_at: null,
|
||||
created_at: "2026-03-01 00:00:01",
|
||||
deleted_at: null,
|
||||
total_net_amount: 694,
|
||||
invoice_collection_id: 1634,
|
||||
booking_id: 0,
|
||||
closed_at: "2026-03-24 12:45:10",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const overviewVehicles = [
|
||||
{
|
||||
id: 301,
|
||||
customer_id: 12345,
|
||||
reg: "AA11223",
|
||||
type: 1,
|
||||
reference: "Truck 1",
|
||||
wash_subscription: true,
|
||||
xlvask: true,
|
||||
addons: {
|
||||
list: [{ product: { id: 91, name: "Interior cleaning" } }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 302,
|
||||
customer_id: 12345,
|
||||
reg: "BB44556",
|
||||
type: 1,
|
||||
reference: "Trailer",
|
||||
wash_subscription: false,
|
||||
xlvask: false,
|
||||
addons: {
|
||||
list: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const setupOverviewApi = async (page, options: { failDetailUntilEnabled?: boolean } = {}) => {
|
||||
let detailRequests = 0;
|
||||
let detailSuccessEnabled = !options.failDetailUntilEnabled;
|
||||
|
||||
await page.route(apiPathPattern("/superuser/user"), async (route) => {
|
||||
const request = route.request();
|
||||
|
||||
if (request.method() !== "GET") {
|
||||
await route.fulfill(json({ data: { message: "OK" } }));
|
||||
return;
|
||||
}
|
||||
|
||||
detailRequests += 1;
|
||||
if (!detailSuccessEnabled) {
|
||||
await route.fulfill(json({ message: "User detail unavailable" }, 500));
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill(json({ data: overviewUser }));
|
||||
});
|
||||
|
||||
await page.route(apiPathPattern("/superuser/user/keys"), async (route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
await route.fulfill(json({ data: { message: "OK" } }));
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: {
|
||||
OtherSpecialArrangement: overviewUser.keys.OtherSpecialArrangement,
|
||||
OtherVaskeabonnement: overviewUser.keys.OtherVaskeabonnement,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
await page.route(apiPathPattern("/customer/pricing/fixed"), async (route) => {
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: {
|
||||
price: 1234,
|
||||
description: "Fixed agreement",
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
await page.route(apiPathPattern("/customer/department/default"), async (route) => {
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: {
|
||||
department: 1,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
await page.route(apiPathPattern("/vehicles"), async (route) => {
|
||||
const request = route.request();
|
||||
|
||||
if (request.method() !== "GET") {
|
||||
await route.fulfill(json({ data: { message: "OK" } }));
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(request.url());
|
||||
const filters = url.searchParams.get("filters") || "";
|
||||
const rows = filters.includes("wash_subscription:1")
|
||||
? overviewVehicles.filter((vehicle) => vehicle.wash_subscription)
|
||||
: overviewVehicles;
|
||||
|
||||
await route.fulfill(json({ data: rows }));
|
||||
});
|
||||
|
||||
return {
|
||||
enableDetailSuccess: () => {
|
||||
detailSuccessEnabled = true;
|
||||
},
|
||||
detailRequests: () => detailRequests,
|
||||
};
|
||||
};
|
||||
|
||||
test.describe("Superuser employees list", () => {
|
||||
test("uses shared search, pagination reload, and action wheel controls", async ({ page }, testInfo) => {
|
||||
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
||||
@@ -259,65 +93,3 @@ test.describe("Superuser employees list", () => {
|
||||
expect(searchesSeen).toContain("Anna");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Superuser user overview", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await seedAuthenticatedState(page, "superuser-user-overview-token");
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: ["superuser", "user", "get_user", "set_custom_price"],
|
||||
pos: {
|
||||
vehicles: overviewVehicles,
|
||||
},
|
||||
sessionData: {
|
||||
group_id: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("loads the management hub without raw debug output", async ({ page }) => {
|
||||
await setupOverviewApi(page);
|
||||
|
||||
await page.goto("/superuser/users/11", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(page.getByTestId("superuser-user-overview-page")).toBeVisible();
|
||||
await expect(page.getByTestId("superuser-user-overview-header")).toContainText("Anna Transport");
|
||||
await expect(page.getByTestId("superuser-user-tab-overview")).toHaveAttribute("href", "/superuser/users/11");
|
||||
await expect(page.getByTestId("superuser-user-tab-pricing")).toHaveAttribute("href", "/superuser/users/11/pricing");
|
||||
await expect(page.getByTestId("superuser-user-overview-metric-customer")).toContainText("12345");
|
||||
await expect(page.getByTestId("superuser-user-overview-metric-vehicles")).toContainText("2");
|
||||
await expect(page.getByTestId("superuser-user-overview-metric-orders")).toContainText("1");
|
||||
await expect(page.getByTestId("superuser-user-overview-metric-discounts")).toContainText("1");
|
||||
await expect(page.getByTestId("superuser-user-overview-account")).toContainText("anna@example.test");
|
||||
await expect(page.getByTestId("superuser-user-overview-economic")).toContainText("billing@example.test");
|
||||
await expect(page.getByTestId("superuser-user-overview-access")).toContainText("set_custom_price");
|
||||
await expect(page.getByTestId("superuser-user-overview-rules")).toBeVisible();
|
||||
await expect(page.getByTestId("superuser-user-special-arrangement-input")).toHaveValue("Night wash agreement");
|
||||
await expect(page.getByTestId("superuser-user-wash-subscription-note-input")).toHaveValue(
|
||||
"Monthly wash subscription note"
|
||||
);
|
||||
await expect(page.getByTestId("superuser-user-overview-vehicles")).toContainText("AA11223");
|
||||
await expect(page.getByTestId("superuser-user-overview-subscriptions")).toContainText("694");
|
||||
await expect(page.getByTestId("superuser-user-overview-link-vehicles")).toHaveAttribute(
|
||||
"href",
|
||||
"/superuser/users/11/vehicles"
|
||||
);
|
||||
|
||||
const body = page.locator("body");
|
||||
await expect(body).not.toContainText("SessionUser:");
|
||||
await expect(body).not.toContainText("economicData:");
|
||||
});
|
||||
|
||||
test("shows a retryable error when the detail request fails", async ({ page }) => {
|
||||
const overviewApi = await setupOverviewApi(page, { failDetailUntilEnabled: true });
|
||||
|
||||
await page.goto("/superuser/users/11", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(page.getByTestId("superuser-user-overview-error")).toContainText("User detail unavailable");
|
||||
expect(overviewApi.detailRequests()).toBeGreaterThan(0);
|
||||
overviewApi.enableDetailSuccess();
|
||||
await page.getByTestId("superuser-user-overview-retry").click();
|
||||
await expect(page.getByTestId("superuser-user-overview-header")).toContainText("Anna Transport");
|
||||
await expect(page.getByTestId("superuser-user-overview-error")).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/components/session/authenticatedRequest.vue", () => ({
|
||||
authenticatedRequest: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||
SessionUser: {},
|
||||
}));
|
||||
|
||||
import {
|
||||
getProductBestApplicableDiscount,
|
||||
getProductEffectivePrice,
|
||||
getUserProductFixedPrice,
|
||||
user,
|
||||
} from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue";
|
||||
import CustomerProductDiscountDisplay from "@/components/displays/department/pos/displays/CustomerProductDiscountDisplay.vue";
|
||||
|
||||
const product = {
|
||||
id: 101,
|
||||
category: 9,
|
||||
category_id: 9,
|
||||
name: "Premium wash",
|
||||
price: 1000,
|
||||
apply_category_discount: 1,
|
||||
};
|
||||
|
||||
const globalDiscount = {
|
||||
id: 999999,
|
||||
product_or_category_id: "global",
|
||||
is_category: 1,
|
||||
percentage: 50,
|
||||
fixed_price: null,
|
||||
};
|
||||
|
||||
const categoryDiscount = {
|
||||
id: 200,
|
||||
product_or_category_id: 9,
|
||||
is_category: 1,
|
||||
percentage: 80,
|
||||
fixed_price: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
user.discounts.value = [];
|
||||
});
|
||||
|
||||
describe("customer product fixed prices", () => {
|
||||
it("uses a direct fixed product price as the effective price without changing discount selection", () => {
|
||||
user.discounts.value = [
|
||||
globalDiscount,
|
||||
categoryDiscount,
|
||||
{
|
||||
id: 300,
|
||||
product_or_category_id: 101,
|
||||
is_category: 0,
|
||||
percentage: 10,
|
||||
fixed_price: 350,
|
||||
},
|
||||
];
|
||||
|
||||
expect(getProductBestApplicableDiscount(product)).toBe(80);
|
||||
expect(getUserProductFixedPrice(product)).toBe(350);
|
||||
expect(getProductEffectivePrice(product)).toBe(350);
|
||||
});
|
||||
|
||||
it("falls back to the existing best-discount calculation when fixed price is not set", () => {
|
||||
user.discounts.value = [
|
||||
globalDiscount,
|
||||
{
|
||||
...categoryDiscount,
|
||||
percentage: 30,
|
||||
},
|
||||
{
|
||||
id: 300,
|
||||
product_or_category_id: 101,
|
||||
is_category: 0,
|
||||
percentage: 10,
|
||||
fixed_price: null,
|
||||
},
|
||||
];
|
||||
|
||||
expect(getUserProductFixedPrice(product)).toBeNull();
|
||||
expect(getProductBestApplicableDiscount(product)).toBe(50);
|
||||
expect(getProductEffectivePrice(product)).toBe(500);
|
||||
});
|
||||
|
||||
it("treats zero as a set fixed price", () => {
|
||||
user.discounts.value = [
|
||||
{
|
||||
id: 300,
|
||||
product_or_category_id: 101,
|
||||
is_category: 0,
|
||||
percentage: 10,
|
||||
fixed_price: 0,
|
||||
},
|
||||
];
|
||||
|
||||
expect(getUserProductFixedPrice(product)).toBe(0);
|
||||
expect(getProductEffectivePrice(product)).toBe(0);
|
||||
});
|
||||
|
||||
it("shows fixed price instead of discount percentage in the POS product badge", () => {
|
||||
const wrapper = mount(CustomerProductDiscountDisplay, {
|
||||
props: {
|
||||
product,
|
||||
customer_discounts: [
|
||||
categoryDiscount,
|
||||
{
|
||||
id: 300,
|
||||
product_or_category_id: 101,
|
||||
is_category: 0,
|
||||
percentage: 10,
|
||||
fixed_price: 350,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain("350 Kr.");
|
||||
expect(wrapper.text()).not.toContain("-80%");
|
||||
});
|
||||
});
|
||||
@@ -43,7 +43,6 @@ describe("Playwright full E2E workflow grouping", () => {
|
||||
it("keeps PR E2E runner pressure bounded and diagnosable", () => {
|
||||
const source = workflowSource();
|
||||
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?runs-on: ubuntu-latest/u);
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?max-parallel: 4/u);
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?PLAYWRIGHT_WORKERS: 1/u);
|
||||
expect(source).toMatch(/e2e-pr:[\s\S]*?PLAYWRIGHT_VIDEO_MODE: on-first-retry/u);
|
||||
|
||||
@@ -57,15 +57,6 @@ describe("Playwright PR mapping", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("maps superuser user detail changes to the superuser users E2E coverage", () => {
|
||||
expect(specsFor("src/views/dashboards/superUserDashboard/user/User.vue")).toContain(
|
||||
"tests/e2e/superuser-users.spec.ts"
|
||||
);
|
||||
expect(
|
||||
specsFor("src/views/dashboards/superUserDashboard/user/displays/other/UserOtherSpecialArrangement.vue")
|
||||
).toContain("tests/e2e/superuser-users.spec.ts");
|
||||
});
|
||||
|
||||
it("maps superuser dashboard shell changes to system status E2E coverage", () => {
|
||||
expect(specsFor("src/views/dashboards/superUserDashboard/SuperUserDashboardNavigation.vue")).toContain(
|
||||
"tests/e2e/superuser-system-status.smoke.spec.js"
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const readSource = (relativePath) =>
|
||||
readFileSync(fileURLToPath(new URL(`../../${relativePath}`, import.meta.url)), "utf8");
|
||||
|
||||
describe("Superuser user overview source", () => {
|
||||
const overviewWidgetSources = [
|
||||
"src/views/dashboards/superUserDashboard/user/User.vue",
|
||||
"src/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue",
|
||||
"src/views/dashboards/superUserDashboard/user/displays/UserDefaultDepartment.vue",
|
||||
"src/views/dashboards/superUserDashboard/user/displays/UserFixedPricing.vue",
|
||||
"src/views/dashboards/superUserDashboard/user/displays/other/UserOtherSpecialArrangement.vue",
|
||||
"src/views/dashboards/superUserDashboard/user/displays/other/UserOtherVaskeabonnement.vue",
|
||||
"src/views/dashboards/superUserDashboard/user/displays/vehicles/UserVehicleSubscriptionsDisplay.vue",
|
||||
];
|
||||
|
||||
it("keeps the overview page on the cleaned management hub implementation", () => {
|
||||
const source = readSource("src/views/dashboards/superUserDashboard/user/User.vue");
|
||||
|
||||
expect(source).toContain('data-testid="superuser-user-overview-page"');
|
||||
expect(source).toContain("superuser-user-overview-customer-management");
|
||||
expect(source).toContain("superuser-user-overview-subscriptions");
|
||||
expect(source).not.toContain("SessionUser.valueOf()");
|
||||
expect(source).not.toContain("JSON.stringify(user");
|
||||
});
|
||||
|
||||
it("keeps the selected user state loadable and retry-aware", () => {
|
||||
const source = readSource("src/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue");
|
||||
|
||||
expect(source).toContain("isUserLoading");
|
||||
expect(source).toContain("userLoadError");
|
||||
expect(source).toContain("activeLoadPromise");
|
||||
});
|
||||
|
||||
it("keeps the overview and directly embedded widgets free of debug logging", () => {
|
||||
for (const relativePath of overviewWidgetSources) {
|
||||
expect(readSource(relativePath), relativePath).not.toContain("console.log");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -356,7 +356,6 @@ describe("system search card view-model behavior", () => {
|
||||
id: 326,
|
||||
customer_number: 12345679,
|
||||
discount: 12.5,
|
||||
fixed_price: 350,
|
||||
object_id: "global",
|
||||
is_category: true,
|
||||
created_at: "2026-03-12T09:15:00Z",
|
||||
@@ -365,7 +364,6 @@ describe("system search card view-model behavior", () => {
|
||||
);
|
||||
|
||||
expect(vm.keyFields.some((entry) => entry.label === "Discount" && entry.value === "12.5%")).toBe(true);
|
||||
expect(vm.keyFields.some((entry) => entry.label === "Fixed price" && entry.value === "350")).toBe(true);
|
||||
expect(vm.keyFields.some((entry) => entry.label === "Target" && entry.value === "global")).toBe(true);
|
||||
expect(vm.keyFields.some((entry) => entry.label === "Category" && entry.value === "Yes")).toBe(true);
|
||||
expect(vm.keyFields.some((entry) => entry.label === "Created" && entry.value.includes("2026-03-12"))).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user