605 lines
22 KiB
Vue
605 lines
22 KiB
Vue
<script setup>
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { SessionUser } from '@/components/session/token/SessionUser.vue';
|
|
import InvoiceDistributionCard from '@/views/dashboards/superUserDashboard/invoiceDistribution/components/InvoiceDistributionCard.vue';
|
|
import InvoiceDistributionTrendChart from '@/views/dashboards/superUserDashboard/invoiceDistribution/components/InvoiceDistributionTrendChart.vue';
|
|
import InvoiceDistributionSkeleton from '@/views/dashboards/superUserDashboard/invoiceDistribution/components/InvoiceDistributionSkeleton.vue';
|
|
import {
|
|
fetchDistributionOverviewMonths,
|
|
fetchFirstOrderDate,
|
|
} from '@/views/dashboards/superUserDashboard/invoiceDistribution/imports/invoiceDistributionApi.js';
|
|
import {
|
|
buildSourceComposition,
|
|
getYearMonthLabel,
|
|
sortMonthSummaries,
|
|
toNumber,
|
|
} from '@/views/dashboards/superUserDashboard/invoiceDistribution/imports/invoiceDistributionCalculations.js';
|
|
|
|
const { t, locale } = useI18n();
|
|
|
|
const monthSummaries = ref([]);
|
|
const loading = ref(false);
|
|
const errorMessage = ref('');
|
|
const loadProgress = ref({ loaded: 0, total: 0 });
|
|
|
|
const search = ref('');
|
|
const yearFilter = ref('all');
|
|
const sortKey = ref('month');
|
|
const sortDirection = ref('desc');
|
|
|
|
const availableYears = computed(() => {
|
|
const years = new Set(monthSummaries.value.map((summary) => String(summary.year)));
|
|
return ['all', ...Array.from(years).sort((a, b) => Number(b) - Number(a))];
|
|
});
|
|
|
|
const filteredMonthSummaries = computed(() => {
|
|
const normalizedSearch = String(search.value || '').trim().toLowerCase();
|
|
return monthSummaries.value.filter((summary) => {
|
|
const monthLabel = getYearMonthLabel(summary.year, summary.month, locale.value);
|
|
const searchMatch = !normalizedSearch || monthLabel.toLowerCase().includes(normalizedSearch);
|
|
const yearMatch = yearFilter.value === 'all' || String(summary.year) === yearFilter.value;
|
|
return searchMatch && yearMatch;
|
|
});
|
|
});
|
|
|
|
const sortedMonthSummaries = computed(() => {
|
|
return sortMonthSummaries(filteredMonthSummaries.value, sortKey.value, sortDirection.value);
|
|
});
|
|
|
|
const totals = computed(() => {
|
|
return sortedMonthSummaries.value.reduce((acc, summary) => {
|
|
acc.bookedAmount += toNumber(summary.bookedAmount);
|
|
acc.distributionAmount += toNumber(summary.distributionAmount);
|
|
acc.fixedPricingAmount += toNumber(summary.fixedPricingAmount);
|
|
acc.subscriptionAmount += toNumber(summary.subscriptionAmount);
|
|
acc.customerPriceAmount += toNumber(summary.customerPriceAmount);
|
|
acc.totalAmount += toNumber(summary.totalAmount);
|
|
return acc;
|
|
}, {
|
|
bookedAmount: 0,
|
|
distributionAmount: 0,
|
|
fixedPricingAmount: 0,
|
|
subscriptionAmount: 0,
|
|
customerPriceAmount: 0,
|
|
totalAmount: 0,
|
|
});
|
|
});
|
|
|
|
const totalComposition = computed(() => buildSourceComposition(totals.value));
|
|
const monthsUsingLegacyDistribution = computed(() => {
|
|
return sortedMonthSummaries.value.filter((summary) => summary?.fallback?.usedLegacyDistribution);
|
|
});
|
|
const latestDistributionFallbackReason = computed(() => {
|
|
const last = monthsUsingLegacyDistribution.value[0];
|
|
return last?.fallback?.distributionFallbackReason || '';
|
|
});
|
|
|
|
const topMonths = computed(() => {
|
|
return sortMonthSummaries(sortedMonthSummaries.value, 'total', 'desc').slice(0, 3);
|
|
});
|
|
|
|
const formatCurrency = (value) => SessionUser.functions.currency.toLocal(toNumber(value));
|
|
const formatPercent = (value) => `${Math.round(toNumber(value))}%`;
|
|
|
|
const getMonthLabel = (summary) => {
|
|
return getYearMonthLabel(summary.year, summary.month, locale.value);
|
|
};
|
|
|
|
const getSortIcon = (column) => {
|
|
if (sortKey.value !== column) {
|
|
return 'fa-sort';
|
|
}
|
|
return sortDirection.value === 'asc' ? 'fa-sort-up' : 'fa-sort-down';
|
|
};
|
|
|
|
const getAriaSort = (column) => {
|
|
if (sortKey.value !== column) {
|
|
return 'none';
|
|
}
|
|
return sortDirection.value === 'asc' ? 'ascending' : 'descending';
|
|
};
|
|
|
|
const setSort = (column) => {
|
|
if (sortKey.value === column) {
|
|
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc';
|
|
return;
|
|
}
|
|
sortKey.value = column;
|
|
sortDirection.value = 'desc';
|
|
};
|
|
|
|
const openMonthDetails = (summary) => {
|
|
window.open(`/superuser/invoices/distribution/${summary.year}/${summary.month}`, '_blank');
|
|
};
|
|
|
|
const resetFilters = () => {
|
|
search.value = '';
|
|
yearFilter.value = 'all';
|
|
sortKey.value = 'month';
|
|
sortDirection.value = 'desc';
|
|
};
|
|
|
|
const loadOverview = async () => {
|
|
loading.value = true;
|
|
errorMessage.value = '';
|
|
loadProgress.value = { loaded: 0, total: 0 };
|
|
|
|
try {
|
|
const firstOrderDate = await fetchFirstOrderDate();
|
|
const summaries = await fetchDistributionOverviewMonths({
|
|
fromDate: firstOrderDate,
|
|
onProgress: (progress) => {
|
|
loadProgress.value = progress;
|
|
},
|
|
});
|
|
monthSummaries.value = summaries;
|
|
} catch (error) {
|
|
errorMessage.value = SessionUser.functions.parseErrorMessage(error) || String(error?.message || error);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
loadOverview();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="distribution-overview distribution-overview-v2" data-testid="distribution-overview-page">
|
|
<InvoiceDistributionCard class="overview-header hero-surface">
|
|
<div class="overview-header__top hero-row">
|
|
<div>
|
|
<p class="eyebrow">Superuser • Revenue Intelligence</p>
|
|
<h2 class="title is-4 mb-1">{{ t('superuser_invoice_distribution.overview.title') }}</h2>
|
|
<p class="subtitle is-6 mb-0">{{ t('superuser_invoice_distribution.overview.subtitle') }}</p>
|
|
</div>
|
|
<b-button
|
|
type="is-dark"
|
|
class="control-button"
|
|
:loading="loading"
|
|
:aria-label="t('superuser_invoice_distribution.actions.reload')"
|
|
@click="loadOverview"
|
|
icon-pack="fas"
|
|
icon-left="sync-alt"
|
|
data-testid="distribution-overview-refresh"
|
|
>
|
|
{{ t('superuser_invoice_distribution.actions.reload') }}
|
|
</b-button>
|
|
</div>
|
|
|
|
<div class="control-toolbar mt-4 overview-dock">
|
|
<b-field :label="t('superuser_invoice_distribution.filters.search')" class="toolbar-field">
|
|
<b-input
|
|
v-model="search"
|
|
:placeholder="t('superuser_invoice_distribution.filters.search_placeholder')"
|
|
icon="search"
|
|
icon-pack="fas"
|
|
:aria-label="t('superuser_invoice_distribution.filters.search')"
|
|
/>
|
|
</b-field>
|
|
<b-field :label="t('superuser_invoice_distribution.filters.year')" class="toolbar-field">
|
|
<b-select v-model="yearFilter" expanded :aria-label="t('superuser_invoice_distribution.filters.year')">
|
|
<option v-for="yearOption in availableYears" :key="yearOption" :value="yearOption">
|
|
{{ yearOption === 'all' ? t('superuser_invoice_distribution.filters.all_years') : yearOption }}
|
|
</option>
|
|
</b-select>
|
|
</b-field>
|
|
<div class="toolbar-hint" role="status" aria-live="polite">
|
|
<p class="toolbar-hint__title">{{ t('superuser_invoice_distribution.overview.loaded_months', { loaded: sortedMonthSummaries.length }) }}</p>
|
|
<p class="toolbar-hint__subtitle">{{ t('superuser_invoice_distribution.tooltips.summary') }}</p>
|
|
</div>
|
|
</div>
|
|
</InvoiceDistributionCard>
|
|
|
|
<b-message v-if="errorMessage" type="is-danger" has-icon icon-pack="fas" class="mb-0">
|
|
<div class="error-state__content">
|
|
<p>{{ t('superuser_invoice_distribution.errors.overview_load_failed') }}: {{ errorMessage }}</p>
|
|
<b-button type="is-danger" class="control-button is-light" @click="loadOverview">
|
|
{{ t('superuser_invoice_distribution.actions.try_again') }}
|
|
</b-button>
|
|
</div>
|
|
</b-message>
|
|
|
|
<b-message
|
|
v-if="!loading && monthsUsingLegacyDistribution.length"
|
|
type="is-warning"
|
|
has-icon
|
|
icon-pack="fas"
|
|
class="mb-0"
|
|
>
|
|
<div class="fallback-note">
|
|
<p>{{ t('superuser_invoice_distribution.warnings.legacy_distribution_fallback', { count: monthsUsingLegacyDistribution.length }) }}</p>
|
|
<p v-if="latestDistributionFallbackReason" class="is-size-7">{{ latestDistributionFallbackReason }}</p>
|
|
</div>
|
|
</b-message>
|
|
|
|
<InvoiceDistributionCard v-if="loading">
|
|
<div class="progress-meta mb-3">
|
|
<span>{{ t('superuser_invoice_distribution.loading.loading_months', { loaded: loadProgress.loaded, total: loadProgress.total }) }}</span>
|
|
</div>
|
|
<b-progress
|
|
type="is-primary"
|
|
size="is-small"
|
|
:value="loadProgress.loaded"
|
|
:max="Math.max(loadProgress.total, 1)"
|
|
aria-label="Loading distribution overview"
|
|
/>
|
|
<InvoiceDistributionSkeleton :rows="7" />
|
|
</InvoiceDistributionCard>
|
|
|
|
<template v-else>
|
|
<section class="kpi-strip" aria-label="KPI summary">
|
|
<InvoiceDistributionCard class="metric-card metric-total">
|
|
<p class="metric-title">{{ t('superuser_invoice_distribution.metrics.total') }}</p>
|
|
<p class="metric-value">{{ formatCurrency(totals.totalAmount) }}</p>
|
|
</InvoiceDistributionCard>
|
|
<InvoiceDistributionCard class="metric-card metric-booked">
|
|
<p class="metric-title">{{ t('superuser_invoice_distribution.metrics.booked') }}</p>
|
|
<p class="metric-value">{{ formatCurrency(totals.bookedAmount) }}</p>
|
|
</InvoiceDistributionCard>
|
|
<InvoiceDistributionCard class="metric-card metric-distribution">
|
|
<p class="metric-title">{{ t('superuser_invoice_distribution.metrics.distribution') }}</p>
|
|
<p class="metric-value">{{ formatCurrency(totals.distributionAmount) }}</p>
|
|
</InvoiceDistributionCard>
|
|
<InvoiceDistributionCard class="metric-card metric-customer-prices">
|
|
<p class="metric-title">{{ t('superuser_invoice_distribution.metrics.customer_prices') }}</p>
|
|
<p class="metric-value">{{ formatCurrency(totals.customerPriceAmount) }}</p>
|
|
</InvoiceDistributionCard>
|
|
<InvoiceDistributionCard class="metric-card metric-months">
|
|
<p class="metric-title">{{ t('superuser_invoice_distribution.metrics.months') }}</p>
|
|
<p class="metric-value">{{ sortedMonthSummaries.length }}</p>
|
|
</InvoiceDistributionCard>
|
|
</section>
|
|
|
|
<InvoiceDistributionCard class="chart-section">
|
|
<div class="section-header">
|
|
<h3 class="title is-6 mb-0">{{ t('superuser_invoice_distribution.sections.trend') }}</h3>
|
|
</div>
|
|
<InvoiceDistributionTrendChart
|
|
:months="sortedMonthSummaries"
|
|
:locale="locale"
|
|
:aria-label="t('superuser_invoice_distribution.aria.trend_chart')"
|
|
:labels="{
|
|
total: t('superuser_invoice_distribution.metrics.total'),
|
|
booked: t('superuser_invoice_distribution.metrics.booked'),
|
|
distribution: t('superuser_invoice_distribution.metrics.distribution'),
|
|
customerPrices: t('superuser_invoice_distribution.metrics.customer_prices'),
|
|
}"
|
|
/>
|
|
</InvoiceDistributionCard>
|
|
|
|
<InvoiceDistributionCard>
|
|
<div class="section-header">
|
|
<h3 class="title is-6 mb-0">{{ t('superuser_invoice_distribution.sections.month_table') }}</h3>
|
|
<b-button size="is-small" type="is-light" class="control-button" @click="resetFilters">
|
|
{{ t('superuser_invoice_distribution.actions.reset_filters') }}
|
|
</b-button>
|
|
</div>
|
|
|
|
<div v-if="!sortedMonthSummaries.length" class="empty-state has-text-centered">
|
|
<p class="has-text-grey mb-3">{{ t('superuser_invoice_distribution.empty.no_months') }}</p>
|
|
<b-button type="is-link" class="control-button is-light" @click="loadOverview">
|
|
{{ t('common.retry') }}
|
|
</b-button>
|
|
</div>
|
|
|
|
<div class="table-container table-container--scroll" v-else>
|
|
<table class="table is-fullwidth is-hoverable is-striped distribution-table">
|
|
<caption>{{ t('superuser_invoice_distribution.aria.month_table_caption') }}</caption>
|
|
<thead>
|
|
<tr>
|
|
<th scope="col" :aria-sort="getAriaSort('month')">
|
|
<b-button
|
|
type="is-text"
|
|
size="is-small"
|
|
class="table-sort"
|
|
@click="setSort('month')"
|
|
:aria-label="t('superuser_invoice_distribution.aria.sort_month')"
|
|
>
|
|
<span>{{ t('superuser_invoice_distribution.table.month') }}</span>
|
|
<span class="icon is-small"><i class="fas" :class="getSortIcon('month')"></i></span>
|
|
</b-button>
|
|
</th>
|
|
<th scope="col" class="has-text-right">{{ t('superuser_invoice_distribution.table.booked') }}</th>
|
|
<th scope="col" class="has-text-right">{{ t('superuser_invoice_distribution.table.fixed_pricing') }}</th>
|
|
<th scope="col" class="has-text-right">{{ t('superuser_invoice_distribution.table.subscriptions') }}</th>
|
|
<th scope="col" class="has-text-right">{{ t('superuser_invoice_distribution.table.customer_prices') }}</th>
|
|
<th scope="col" class="has-text-right">{{ t('superuser_invoice_distribution.table.distribution') }}</th>
|
|
<th scope="col" :aria-sort="getAriaSort('total')" class="has-text-right">
|
|
<b-button
|
|
type="is-text"
|
|
size="is-small"
|
|
class="table-sort table-sort--right"
|
|
@click="setSort('total')"
|
|
:aria-label="t('superuser_invoice_distribution.aria.sort_total')"
|
|
>
|
|
<span>{{ t('superuser_invoice_distribution.table.total') }}</span>
|
|
<span class="icon is-small"><i class="fas" :class="getSortIcon('total')"></i></span>
|
|
</b-button>
|
|
</th>
|
|
<th scope="col">{{ t('superuser_invoice_distribution.table.source_mix') }}</th>
|
|
<th scope="col">{{ t('superuser_invoice_distribution.table.actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="summary in sortedMonthSummaries" :key="summary.key">
|
|
<th scope="row">{{ getMonthLabel(summary) }}</th>
|
|
<td class="has-text-right is-family-monospace">{{ formatCurrency(summary.bookedAmount) }}</td>
|
|
<td class="has-text-right is-family-monospace">{{ formatCurrency(summary.fixedPricingAmount) }}</td>
|
|
<td class="has-text-right is-family-monospace">{{ formatCurrency(summary.subscriptionAmount) }}</td>
|
|
<td class="has-text-right is-family-monospace">{{ formatCurrency(summary.customerPriceAmount) }}</td>
|
|
<td class="has-text-right is-family-monospace">{{ formatCurrency(summary.distributionAmount) }}</td>
|
|
<td class="has-text-right is-family-monospace">{{ formatCurrency(summary.totalAmount) }}</td>
|
|
<td>
|
|
<div class="source-composition">
|
|
<div class="source-composition__bar" :aria-label="t('superuser_invoice_distribution.aria.source_mix_row', { month: getMonthLabel(summary) })">
|
|
<span
|
|
class="source-composition__segment source-composition__segment--fixed"
|
|
:style="{ width: `${buildSourceComposition(summary).fixedPercent}%` }"
|
|
></span>
|
|
<span
|
|
class="source-composition__segment source-composition__segment--subscription"
|
|
:style="{ width: `${buildSourceComposition(summary).subscriptionPercent}%` }"
|
|
></span>
|
|
<span
|
|
class="source-composition__segment source-composition__segment--customer-prices"
|
|
:style="{ width: `${buildSourceComposition(summary).customerPricePercent}%` }"
|
|
></span>
|
|
</div>
|
|
<span class="is-size-7 has-text-grey">
|
|
{{ formatPercent(buildSourceComposition(summary).fixedPercent) }}
|
|
/
|
|
{{ formatPercent(buildSourceComposition(summary).subscriptionPercent) }}
|
|
/
|
|
{{ formatPercent(buildSourceComposition(summary).customerPricePercent) }}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<b-button
|
|
size="is-small"
|
|
type="is-link"
|
|
class="control-button is-light"
|
|
@click="openMonthDetails(summary)"
|
|
:aria-label="t('superuser_invoice_distribution.aria.open_month', { month: getMonthLabel(summary) })"
|
|
icon-pack="fas"
|
|
icon-left="external-link-alt"
|
|
data-testid="distribution-overview-open-month"
|
|
>
|
|
{{ t('superuser_invoice_distribution.actions.open_month') }}
|
|
</b-button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</InvoiceDistributionCard>
|
|
|
|
<section class="insights-grid" aria-label="Secondary insights">
|
|
<InvoiceDistributionCard class="insight-card">
|
|
<h3 class="title is-6 mb-3">{{ t('superuser_invoice_distribution.sections.source_blend') }}</h3>
|
|
<div class="insight-row">
|
|
<span>{{ t('superuser_invoice_distribution.metrics.fixed_pricing') }}</span>
|
|
<strong>{{ formatCurrency(totalComposition.fixedAmount) }} ({{ formatPercent(totalComposition.fixedPercent) }})</strong>
|
|
</div>
|
|
<div class="insight-row">
|
|
<span>{{ t('superuser_invoice_distribution.metrics.subscriptions') }}</span>
|
|
<strong>{{ formatCurrency(totalComposition.subscriptionAmount) }} ({{ formatPercent(totalComposition.subscriptionPercent) }})</strong>
|
|
</div>
|
|
<div class="insight-row">
|
|
<span>{{ t('superuser_invoice_distribution.metrics.customer_prices') }}</span>
|
|
<strong>{{ formatCurrency(totalComposition.customerPriceAmount) }} ({{ formatPercent(totalComposition.customerPricePercent) }})</strong>
|
|
</div>
|
|
</InvoiceDistributionCard>
|
|
|
|
<InvoiceDistributionCard class="insight-card">
|
|
<h3 class="title is-6 mb-3">{{ t('superuser_invoice_distribution.sections.top_months') }}</h3>
|
|
<div v-if="topMonths.length" class="top-month-list">
|
|
<b-button
|
|
v-for="summary in topMonths"
|
|
:key="summary.key"
|
|
size="is-small"
|
|
type="is-light"
|
|
class="top-month-button control-button"
|
|
@click="openMonthDetails(summary)"
|
|
>
|
|
<span>{{ getMonthLabel(summary) }}</span>
|
|
<span class="has-text-weight-semibold">{{ formatCurrency(summary.totalAmount) }}</span>
|
|
</b-button>
|
|
</div>
|
|
<p v-else class="has-text-grey">{{ t('superuser_invoice_distribution.empty.no_months') }}</p>
|
|
</InvoiceDistributionCard>
|
|
</section>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
:root {
|
|
--distribution-v2-accent: #0f6b86;
|
|
}
|
|
|
|
:deep(.card) {
|
|
border-radius: 16px;
|
|
}
|
|
|
|
.distribution-overview-v2 {
|
|
display: grid;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.hero-surface {
|
|
background:
|
|
radial-gradient(circle at top right, rgba(15, 107, 134, 0.2), transparent 55%),
|
|
linear-gradient(135deg, #f5fbff 0%, #f7f8fc 100%);
|
|
}
|
|
|
|
.hero-row {
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.eyebrow {
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
font-weight: 700;
|
|
font-size: 0.7rem;
|
|
margin-bottom: 0.4rem;
|
|
color: #38576a;
|
|
}
|
|
|
|
.overview-dock {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr minmax(220px, 1fr);
|
|
gap: 0.9rem;
|
|
align-items: end;
|
|
}
|
|
|
|
.toolbar-hint {
|
|
padding: 0.75rem 0.9rem;
|
|
background: #ffffff;
|
|
border: 1px solid #d8e5ee;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.toolbar-hint__title {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.toolbar-hint__subtitle {
|
|
color: #617986;
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.fallback-note p + p {
|
|
margin-top: 0.3rem;
|
|
}
|
|
|
|
.kpi-strip {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
gap: 0.8rem;
|
|
}
|
|
|
|
.metric-card {
|
|
background: #ffffff;
|
|
border: 1px solid #d8e5ee;
|
|
}
|
|
|
|
.metric-title {
|
|
color: #617986;
|
|
font-size: 0.78rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.metric-value {
|
|
font-size: 1.3rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.metric-customer-prices {
|
|
border-color: #f0d6c8;
|
|
background: #fffaf7;
|
|
}
|
|
|
|
.chart-section {
|
|
border: 1px solid #d8e5ee;
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.distribution-table {
|
|
font-size: 0.92rem;
|
|
}
|
|
|
|
.source-composition__bar {
|
|
width: 100%;
|
|
max-width: 160px;
|
|
height: 8px;
|
|
border-radius: 999px;
|
|
overflow: hidden;
|
|
background: #dde8ef;
|
|
display: flex;
|
|
}
|
|
|
|
.source-composition__segment {
|
|
display: inline-block;
|
|
height: 100%;
|
|
}
|
|
|
|
.source-composition__segment--fixed {
|
|
background: #1f77b4;
|
|
}
|
|
|
|
.source-composition__segment--subscription {
|
|
background: #ff7f0e;
|
|
}
|
|
|
|
.source-composition__segment--customer-prices {
|
|
background: #c44536;
|
|
}
|
|
|
|
.insights-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 0.8rem;
|
|
}
|
|
|
|
.insight-card {
|
|
border: 1px solid #d8e5ee;
|
|
}
|
|
|
|
.insight-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
padding: 0.35rem 0;
|
|
}
|
|
|
|
.top-month-list {
|
|
display: grid;
|
|
gap: 0.45rem;
|
|
}
|
|
|
|
.top-month-button {
|
|
justify-content: space-between;
|
|
}
|
|
|
|
@media (max-width: 1100px) {
|
|
.overview-dock {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.kpi-strip {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.kpi-strip {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.insights-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
:deep(.button:focus-visible),
|
|
:deep(.input:focus-visible),
|
|
:deep(.select select:focus-visible),
|
|
:deep(.textarea:focus-visible) {
|
|
outline: 2px solid rgba(15, 107, 134, 0.55);
|
|
outline-offset: 2px;
|
|
}
|
|
</style>
|
|
|