Unify truckwash.dk Kundeoprettelse and QR traffic on the shared customer page, add protected registration UX, and complete the limited-backoffice demo flow.
665 lines
22 KiB
Vue
665 lines
22 KiB
Vue
<script setup>
|
|
import { computed, ref, watch } from "vue";
|
|
import { useI18n } from "vue-i18n";
|
|
import Swal from "sweetalert2";
|
|
|
|
import SuperuserOverviewPanel from "@/components/displays/superuser/overview/SuperuserOverviewPanel.vue";
|
|
import {
|
|
getLimitedBackofficeDepartmentCustomerPricing,
|
|
getSuperuserDepartmentCustomerPricing,
|
|
unwrapDepartmentCustomerPricingResponse,
|
|
updateLimitedBackofficeDepartmentCustomerPricing,
|
|
updateSuperuserDepartmentCustomerPricing,
|
|
} from "@/services/departmentCustomerPricing.js";
|
|
|
|
const props = defineProps({
|
|
scope: {
|
|
type: String,
|
|
required: true,
|
|
validator: (value) => ["superuser", "limited"].includes(value),
|
|
},
|
|
departmentId: {
|
|
type: [Number, String],
|
|
required: true,
|
|
},
|
|
customPricingEnabled: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
canRead: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
canEdit: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
initialCustomerNumber: {
|
|
type: [Number, String],
|
|
default: "",
|
|
},
|
|
presentation: {
|
|
type: String,
|
|
default: "plain",
|
|
validator: (value) => ["plain", "superuser-tiles"].includes(value),
|
|
},
|
|
});
|
|
|
|
const { t, locale } = useI18n({ useScope: "global" });
|
|
|
|
const customerNumber = ref(String(props.initialCustomerNumber || ""));
|
|
const pricingData = ref(null);
|
|
const loading = ref(false);
|
|
const saving = ref(false);
|
|
const errorMessage = ref("");
|
|
const pricingConflict = ref(false);
|
|
|
|
const departmentId = computed(() => {
|
|
const parsed = Number.parseInt(String(props.departmentId || ""), 10);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
});
|
|
|
|
const parsedCustomerNumber = computed(() => {
|
|
const parsed = Number.parseInt(String(customerNumber.value || "").trim(), 10);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
});
|
|
|
|
const canLoadPricing = computed(
|
|
() =>
|
|
props.canRead && props.customPricingEnabled && departmentId.value !== null && parsedCustomerNumber.value !== null
|
|
);
|
|
|
|
const categories = computed(() => (Array.isArray(pricingData.value?.categories) ? pricingData.value.categories : []));
|
|
const hasProducts = computed(() => categories.value.some((category) => (category.products || []).length > 0));
|
|
const customer = computed(() => pricingData.value?.customer || null);
|
|
const customerDisplay = computed(() => {
|
|
if (!customer.value) {
|
|
return "";
|
|
}
|
|
|
|
return [customer.value.display_name, customer.value.customer_number ? `#${customer.value.customer_number}` : null]
|
|
.filter(Boolean)
|
|
.join(" - ");
|
|
});
|
|
|
|
const formatPrice = (price) => {
|
|
if (price === null || price === undefined || price === "") {
|
|
return "-";
|
|
}
|
|
|
|
return new Intl.NumberFormat(locale.value || undefined, {
|
|
style: "currency",
|
|
currency: "DKK",
|
|
maximumFractionDigits: 0,
|
|
}).format(Number(price || 0));
|
|
};
|
|
|
|
const overrideKey = (isCategory, objectId) => `${isCategory ? 1 : 0}:${String(objectId)}`;
|
|
|
|
const overridesByKey = computed(() => {
|
|
const map = new Map();
|
|
(pricingData.value?.overrides || []).forEach((override) => {
|
|
map.set(overrideKey(Boolean(override.is_category), override.product_or_category_id), {
|
|
is_category: Boolean(override.is_category),
|
|
product_or_category_id: override.product_or_category_id,
|
|
percentage: Number.parseInt(String(override.percentage ?? 0), 10) || 0,
|
|
fixed_price:
|
|
override.fixed_price === null || override.fixed_price === undefined
|
|
? null
|
|
: Number.parseInt(String(override.fixed_price), 10),
|
|
});
|
|
});
|
|
return map;
|
|
});
|
|
|
|
const getOverride = (isCategory, objectId) =>
|
|
overridesByKey.value.get(overrideKey(Boolean(isCategory), objectId)) || {
|
|
is_category: Boolean(isCategory),
|
|
product_or_category_id: objectId,
|
|
percentage: 0,
|
|
fixed_price: null,
|
|
};
|
|
|
|
const getDiscountDisplay = (override) => (override.percentage > 0 ? `${override.percentage}%` : "-");
|
|
const getFixedPriceDisplay = (override) => (override.fixed_price !== null ? formatPrice(override.fixed_price) : "-");
|
|
const customerIdentifier = computed(() => ({ customerNumber: parsedCustomerNumber.value }));
|
|
const tilePresentation = computed(() => props.presentation === "superuser-tiles");
|
|
|
|
const requestLoad = () =>
|
|
props.scope === "limited"
|
|
? getLimitedBackofficeDepartmentCustomerPricing(departmentId.value, customerIdentifier.value)
|
|
: getSuperuserDepartmentCustomerPricing(departmentId.value, customerIdentifier.value);
|
|
|
|
const requestSave = (overrides) =>
|
|
props.scope === "limited"
|
|
? updateLimitedBackofficeDepartmentCustomerPricing(
|
|
departmentId.value,
|
|
customerIdentifier.value,
|
|
overrides,
|
|
pricingData.value?.revision ?? null
|
|
)
|
|
: updateSuperuserDepartmentCustomerPricing(
|
|
departmentId.value,
|
|
customerIdentifier.value,
|
|
overrides,
|
|
pricingData.value?.revision ?? null
|
|
);
|
|
|
|
const resetPricingData = () => {
|
|
pricingData.value = null;
|
|
errorMessage.value = "";
|
|
pricingConflict.value = false;
|
|
};
|
|
|
|
const loadPricing = async () => {
|
|
if (!canLoadPricing.value) {
|
|
resetPricingData();
|
|
return;
|
|
}
|
|
|
|
loading.value = true;
|
|
errorMessage.value = "";
|
|
pricingConflict.value = false;
|
|
|
|
try {
|
|
const response = await requestLoad();
|
|
pricingData.value = unwrapDepartmentCustomerPricingResponse(response);
|
|
} catch (error) {
|
|
pricingData.value = null;
|
|
errorMessage.value =
|
|
error?.response?.data?.data?.message ||
|
|
error?.response?.data?.message ||
|
|
error?.message ||
|
|
t("departments.customer_pricing.errors.load");
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const normalizedOverrides = (nextOverride) => {
|
|
const map = new Map(overridesByKey.value);
|
|
const key = overrideKey(nextOverride.is_category, nextOverride.product_or_category_id);
|
|
const percentage = Number.parseInt(String(nextOverride.percentage ?? 0), 10) || 0;
|
|
const fixedPrice =
|
|
nextOverride.fixed_price === null || nextOverride.fixed_price === undefined || nextOverride.fixed_price === ""
|
|
? null
|
|
: Number.parseInt(String(nextOverride.fixed_price), 10);
|
|
const normalized = {
|
|
is_category: Boolean(nextOverride.is_category),
|
|
product_or_category_id: nextOverride.product_or_category_id,
|
|
percentage: Math.min(100, Math.max(0, percentage)),
|
|
fixed_price: nextOverride.is_category ? null : fixedPrice,
|
|
};
|
|
|
|
if (normalized.percentage <= 0 && normalized.fixed_price === null) {
|
|
map.delete(key);
|
|
} else {
|
|
map.set(key, normalized);
|
|
}
|
|
|
|
return [...map.values()];
|
|
};
|
|
|
|
const saveOverride = async (nextOverride) => {
|
|
if (!props.canEdit || !canLoadPricing.value) {
|
|
return;
|
|
}
|
|
|
|
saving.value = true;
|
|
errorMessage.value = "";
|
|
pricingConflict.value = false;
|
|
|
|
try {
|
|
const response = await requestSave(normalizedOverrides(nextOverride));
|
|
pricingData.value = unwrapDepartmentCustomerPricingResponse(response);
|
|
} catch (error) {
|
|
const payload = error?.response?.data?.data || error?.response?.data || {};
|
|
pricingConflict.value = Number(error?.response?.status) === 409 && payload?.code === "pricing_revision_conflict";
|
|
errorMessage.value = pricingConflict.value
|
|
? t("templates.limited_backoffice.errors.pricing_conflict")
|
|
: payload?.message || error?.message || t("departments.customer_pricing.errors.save");
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
};
|
|
|
|
const promptInteger = async ({ title, inputLabel, inputValue, allowEmpty = false, min = 0, max = null }) => {
|
|
const result = await Swal.fire({
|
|
title,
|
|
input: "number",
|
|
inputLabel,
|
|
inputValue,
|
|
inputAttributes: {
|
|
min,
|
|
...(max === null ? {} : { max }),
|
|
step: 1,
|
|
autocapitalize: "off",
|
|
},
|
|
showCancelButton: true,
|
|
confirmButtonText: t("common.save"),
|
|
showLoaderOnConfirm: true,
|
|
inputValidator: (value) => {
|
|
const normalized = String(value ?? "").trim();
|
|
if (allowEmpty && normalized === "") {
|
|
return null;
|
|
}
|
|
|
|
const parsed = Number.parseInt(normalized, 10);
|
|
if (!Number.isInteger(parsed) || parsed < min || (max !== null && parsed > max)) {
|
|
return max === null
|
|
? t("departments.customer_pricing.fixed_price_validation")
|
|
: t("departments.customer_pricing.discount_validation");
|
|
}
|
|
|
|
return null;
|
|
},
|
|
});
|
|
|
|
if (!result.isConfirmed) {
|
|
return undefined;
|
|
}
|
|
|
|
const normalized = String(result.value ?? "").trim();
|
|
return allowEmpty && normalized === "" ? null : Number.parseInt(normalized, 10);
|
|
};
|
|
|
|
const editDiscount = async (isCategory, objectId, label) => {
|
|
const override = getOverride(isCategory, objectId);
|
|
const value = await promptInteger({
|
|
title: label,
|
|
inputLabel: t("departments.customer_pricing.discount"),
|
|
inputValue: override.percentage > 0 ? override.percentage : "",
|
|
allowEmpty: true,
|
|
max: 100,
|
|
});
|
|
|
|
if (value === undefined) {
|
|
return;
|
|
}
|
|
|
|
await saveOverride({
|
|
...override,
|
|
percentage: value ?? 0,
|
|
});
|
|
};
|
|
|
|
const editFixedPrice = async (product) => {
|
|
const override = getOverride(false, product.id);
|
|
const value = await promptInteger({
|
|
title: product.name,
|
|
inputLabel: t("departments.customer_pricing.fixed_price"),
|
|
inputValue: override.fixed_price === null ? "" : override.fixed_price,
|
|
allowEmpty: true,
|
|
max: null,
|
|
});
|
|
|
|
if (value === undefined) {
|
|
return;
|
|
}
|
|
|
|
await saveOverride({
|
|
...override,
|
|
fixed_price: value,
|
|
});
|
|
};
|
|
|
|
watch(
|
|
() => props.initialCustomerNumber,
|
|
(value) => {
|
|
customerNumber.value = String(value || "");
|
|
if (canLoadPricing.value) {
|
|
void loadPricing();
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
watch(
|
|
() => [props.departmentId, props.customPricingEnabled, props.canRead],
|
|
() => {
|
|
if (canLoadPricing.value) {
|
|
void loadPricing();
|
|
} else if (!props.customPricingEnabled || !props.canRead) {
|
|
resetPricingData();
|
|
}
|
|
}
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<section
|
|
class="department-customer-pricing"
|
|
:class="{ 'department-customer-pricing--tiles': tilePresentation }"
|
|
data-testid="department-customer-pricing-editor"
|
|
>
|
|
<div
|
|
v-if="!props.canRead"
|
|
class="notification is-danger is-light"
|
|
data-testid="department-customer-pricing-forbidden"
|
|
>
|
|
{{ t("departments.customer_pricing.no_permission") }}
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="!props.customPricingEnabled"
|
|
class="notification is-warning is-light"
|
|
data-testid="department-customer-pricing-disabled"
|
|
>
|
|
{{ t("departments.customer_pricing.not_enabled") }}
|
|
</div>
|
|
|
|
<template v-else>
|
|
<component
|
|
:is="tilePresentation ? SuperuserOverviewPanel : 'div'"
|
|
class="department-customer-pricing__toolbar-panel"
|
|
:title="tilePresentation ? t('departments.customer_pricing.title') : undefined"
|
|
:subtitle="tilePresentation ? t('departments.customer_pricing.open') : undefined"
|
|
data-testid="department-customer-pricing-lookup"
|
|
>
|
|
<div class="department-customer-pricing__toolbar">
|
|
<div class="field department-customer-pricing__customer-field">
|
|
<label class="label" for="department-customer-pricing-customer-number">
|
|
{{ t("departments.customer_pricing.customer_number") }}
|
|
</label>
|
|
<div class="field has-addons">
|
|
<div class="control is-expanded">
|
|
<input
|
|
id="department-customer-pricing-customer-number"
|
|
v-model="customerNumber"
|
|
class="input"
|
|
type="number"
|
|
min="1"
|
|
step="1"
|
|
inputmode="numeric"
|
|
:placeholder="t('departments.customer_pricing.customer_number')"
|
|
data-testid="department-customer-pricing-customer-number"
|
|
@keydown.enter.prevent="loadPricing"
|
|
/>
|
|
</div>
|
|
<div class="control">
|
|
<b-tooltip :label="t('departments.customer_pricing.load_customer')" position="is-bottom" type="is-dark">
|
|
<button
|
|
class="button is-info"
|
|
type="button"
|
|
:disabled="!canLoadPricing || loading"
|
|
data-testid="department-customer-pricing-load"
|
|
@click="loadPricing"
|
|
>
|
|
<span class="icon is-small">
|
|
<i class="fas" :class="loading ? 'fa-spinner fa-spin' : 'fa-search'" aria-hidden="true"></i>
|
|
</span>
|
|
</button>
|
|
</b-tooltip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</component>
|
|
|
|
<div v-if="loading" class="notification is-light" data-testid="department-customer-pricing-loading">
|
|
{{ t("common.loading") }}
|
|
</div>
|
|
|
|
<div v-if="errorMessage" class="notification is-danger is-light" data-testid="department-customer-pricing-error">
|
|
<span>{{ errorMessage }}</span>
|
|
<button
|
|
v-if="pricingConflict"
|
|
type="button"
|
|
class="button is-small is-danger is-light ml-3"
|
|
data-testid="department-customer-pricing-conflict-reload"
|
|
@click="loadPricing"
|
|
>
|
|
{{ t("templates.limited_backoffice.reload_pricing") }}
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="pricingData" class="department-customer-pricing__content">
|
|
<component
|
|
:is="tilePresentation ? SuperuserOverviewPanel : 'div'"
|
|
class="department-customer-pricing__summary-panel"
|
|
:title="tilePresentation ? customerDisplay : undefined"
|
|
:subtitle="tilePresentation ? pricingData.department?.name : undefined"
|
|
data-testid="department-customer-pricing-customer-panel"
|
|
>
|
|
<div class="level department-customer-pricing__summary">
|
|
<div class="level-left">
|
|
<div>
|
|
<h2 class="title is-5" data-testid="department-customer-pricing-customer">
|
|
{{ customerDisplay }}
|
|
</h2>
|
|
<p class="subtitle is-6">{{ pricingData.department?.name }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="level-right">
|
|
<b-tooltip
|
|
:label="
|
|
props.canEdit
|
|
? t('departments.customer_pricing.edit_global_discount')
|
|
: t('departments.customer_pricing.edit_disabled')
|
|
"
|
|
position="is-left"
|
|
type="is-dark"
|
|
>
|
|
<span>
|
|
<button
|
|
class="button is-small"
|
|
type="button"
|
|
:disabled="!props.canEdit || saving"
|
|
data-testid="department-customer-pricing-global-discount"
|
|
@click="editDiscount(true, 'global', t('departments.customer_pricing.global_discount'))"
|
|
>
|
|
<span class="icon is-small"><i class="fas fa-percent" aria-hidden="true"></i></span>
|
|
<span>{{ getDiscountDisplay(getOverride(true, "global")) }}</span>
|
|
</button>
|
|
</span>
|
|
</b-tooltip>
|
|
</div>
|
|
</div>
|
|
</component>
|
|
|
|
<div
|
|
v-if="saving"
|
|
class="notification is-light py-2"
|
|
role="status"
|
|
data-testid="department-customer-pricing-saving"
|
|
>
|
|
{{ t("departments.customer_pricing.saving") }}
|
|
</div>
|
|
|
|
<div v-if="!hasProducts" class="notification is-light">
|
|
{{ t("departments.customer_pricing.no_products") }}
|
|
</div>
|
|
|
|
<component
|
|
:is="tilePresentation ? SuperuserOverviewPanel : 'div'"
|
|
v-for="category in categories"
|
|
:key="category.id"
|
|
class="department-customer-pricing__category"
|
|
:title="tilePresentation ? category.name : undefined"
|
|
:subtitle="tilePresentation ? t('departments.customer_pricing.item_discount') : undefined"
|
|
>
|
|
<div class="department-customer-pricing__category-header">
|
|
<h3 class="title is-6">{{ category.name }}</h3>
|
|
<b-tooltip
|
|
:label="
|
|
props.canEdit
|
|
? t('departments.customer_pricing.edit_category_discount')
|
|
: t('departments.customer_pricing.edit_disabled')
|
|
"
|
|
position="is-left"
|
|
type="is-dark"
|
|
>
|
|
<span>
|
|
<button
|
|
class="button is-small"
|
|
type="button"
|
|
:disabled="!props.canEdit || saving"
|
|
:data-testid="`department-customer-pricing-category-discount-${category.id}`"
|
|
@click="editDiscount(true, category.id, category.name)"
|
|
>
|
|
<span class="icon is-small"><i class="fas fa-tags" aria-hidden="true"></i></span>
|
|
<span>{{ getDiscountDisplay(getOverride(true, category.id)) }}</span>
|
|
</button>
|
|
</span>
|
|
</b-tooltip>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table class="table is-fullwidth is-hoverable is-striped department-customer-pricing__table">
|
|
<thead>
|
|
<tr>
|
|
<th>{{ t("common.product") }}</th>
|
|
<th>{{ t("departments.customer_pricing.department_price") }}</th>
|
|
<th>{{ t("departments.customer_pricing.fixed_price") }}</th>
|
|
<th>{{ t("departments.customer_pricing.item_discount") }}</th>
|
|
<th>{{ t("departments.customer_pricing.effective_price") }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="product in category.products"
|
|
:key="product.id"
|
|
:data-testid="`department-customer-pricing-product-${product.id}`"
|
|
>
|
|
<td>
|
|
<strong>{{ product.name }}</strong>
|
|
<p v-if="product.description" class="is-size-7 has-text-grey">{{ product.description }}</p>
|
|
</td>
|
|
<td :class="{ 'has-text-danger has-text-weight-semibold': product.missing_department_price }">
|
|
{{ formatPrice(product.department_price) }}
|
|
</td>
|
|
<td>
|
|
<b-tooltip
|
|
:label="
|
|
props.canEdit
|
|
? t('departments.customer_pricing.edit_fixed_price')
|
|
: t('departments.customer_pricing.edit_disabled')
|
|
"
|
|
position="is-bottom"
|
|
type="is-dark"
|
|
>
|
|
<span>
|
|
<button
|
|
class="button is-small is-white department-customer-pricing__cell-button"
|
|
type="button"
|
|
:disabled="!props.canEdit || saving"
|
|
:data-testid="`department-customer-pricing-fixed-price-${product.id}`"
|
|
@click="editFixedPrice(product)"
|
|
>
|
|
<span>{{ getFixedPriceDisplay(getOverride(false, product.id)) }}</span>
|
|
<span class="icon is-small"><i class="fas fa-edit" aria-hidden="true"></i></span>
|
|
</button>
|
|
</span>
|
|
</b-tooltip>
|
|
</td>
|
|
<td>
|
|
<b-tooltip
|
|
:label="
|
|
props.canEdit
|
|
? t('departments.customer_pricing.edit_item_discount')
|
|
: t('departments.customer_pricing.edit_disabled')
|
|
"
|
|
position="is-bottom"
|
|
type="is-dark"
|
|
>
|
|
<span>
|
|
<button
|
|
class="button is-small is-white department-customer-pricing__cell-button"
|
|
type="button"
|
|
:disabled="!props.canEdit || saving"
|
|
:data-testid="`department-customer-pricing-item-discount-${product.id}`"
|
|
@click="editDiscount(false, product.id, product.name)"
|
|
>
|
|
<span>{{ getDiscountDisplay(getOverride(false, product.id)) }}</span>
|
|
<span class="icon is-small"><i class="fas fa-edit" aria-hidden="true"></i></span>
|
|
</button>
|
|
</span>
|
|
</b-tooltip>
|
|
</td>
|
|
<td>{{ formatPrice(product.effective_price) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</component>
|
|
</div>
|
|
</template>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.department-customer-pricing {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.department-customer-pricing__toolbar,
|
|
.department-customer-pricing__summary,
|
|
.department-customer-pricing__category-header {
|
|
align-items: flex-start;
|
|
display: flex;
|
|
gap: 1rem;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.department-customer-pricing__customer-field {
|
|
max-width: 420px;
|
|
width: 100%;
|
|
}
|
|
|
|
.department-customer-pricing__content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.department-customer-pricing__category {
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.department-customer-pricing--tiles .department-customer-pricing__category {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.department-customer-pricing--tiles .department-customer-pricing__summary-panel .department-customer-pricing__summary,
|
|
.department-customer-pricing--tiles .department-customer-pricing__toolbar-panel .department-customer-pricing__toolbar {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.department-customer-pricing--tiles .department-customer-pricing__category-header .title,
|
|
.department-customer-pricing--tiles .department-customer-pricing__summary .title,
|
|
.department-customer-pricing--tiles .department-customer-pricing__summary .subtitle {
|
|
display: none;
|
|
}
|
|
|
|
.department-customer-pricing__category-header {
|
|
align-items: center;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.department-customer-pricing__category-header .title {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.department-customer-pricing__table th,
|
|
.department-customer-pricing__table td {
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.department-customer-pricing__cell-button {
|
|
justify-content: space-between;
|
|
min-width: 7rem;
|
|
width: 100%;
|
|
}
|
|
|
|
@media screen and (max-width: 768px) {
|
|
.department-customer-pricing__toolbar,
|
|
.department-customer-pricing__summary,
|
|
.department-customer-pricing__category-header {
|
|
align-items: stretch;
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
</style>
|