|
|
@@ -85,10 +85,10 @@
|
|
|
</div>
|
|
|
|
|
|
<div class="dashboard-panels">
|
|
|
- <DashboardAvailabilityPanel
|
|
|
- :is-all-properties-selected="isAllPropertiesSelected"
|
|
|
+ <DashboardHorizontalBarPanel
|
|
|
+ title="Disponibilidade do Período"
|
|
|
:items="availabilityItems"
|
|
|
- :total-capacity-days="totalCapacityDays"
|
|
|
+ :total-label="availabilityTotalLabel"
|
|
|
/>
|
|
|
|
|
|
<DashboardRevenuePanel
|
|
|
@@ -96,7 +96,11 @@
|
|
|
:chart-options="revenueChartOptions"
|
|
|
/>
|
|
|
|
|
|
- <DashboardChannelsPanel :channels="channels" />
|
|
|
+ <DashboardHorizontalBarPanel
|
|
|
+ title="Canais de Aquisição"
|
|
|
+ :items="channelsBarItems"
|
|
|
+ :total-label="channelsTotalLabel"
|
|
|
+ />
|
|
|
|
|
|
<q-card flat class="panel-card panel-card--soft">
|
|
|
<div class="panel-title">Repasses por Unidade</div>
|
|
|
@@ -148,10 +152,9 @@ import {
|
|
|
} from "src/helpers/utils";
|
|
|
|
|
|
import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
|
-import DashboardAvailabilityPanel from "./components/DashboardAvailabilityPanel.vue";
|
|
|
import DashboardFiltersBar from "./components/DashboardFiltersBar.vue";
|
|
|
+import DashboardHorizontalBarPanel from "./components/DashboardHorizontalBarPanel.vue";
|
|
|
import DashboardMetricCard from "./components/DashboardMetricCard.vue";
|
|
|
-import DashboardChannelsPanel from "./components/DashboardChannelsPanel.vue";
|
|
|
import DashboardPayoutTable from "./components/DashboardPayoutTable.vue";
|
|
|
import DashboardRevenuePanel from "./components/DashboardRevenuePanel.vue";
|
|
|
|
|
|
@@ -221,12 +224,12 @@ const monthLabels = [
|
|
|
"Dezembro",
|
|
|
];
|
|
|
|
|
|
+//
|
|
|
+
|
|
|
const availableReferences = computed(
|
|
|
() => dashboard.value?.filters?.available_references ?? [],
|
|
|
);
|
|
|
|
|
|
-const channels = computed(() => dashboard.value?.channels ?? []);
|
|
|
-
|
|
|
const selectedFilters = computed(
|
|
|
() => dashboard.value?.filters?.selected ?? {},
|
|
|
);
|
|
|
@@ -247,12 +250,14 @@ const propertyOptions = computed(() => {
|
|
|
|
|
|
const hasProperties = computed(() => propertyOptions.value.length > 1);
|
|
|
|
|
|
-//
|
|
|
-
|
|
|
const selectedPropertyId = computed(() => {
|
|
|
return selectedPropertyOption.value?.id ?? null;
|
|
|
});
|
|
|
|
|
|
+const isAllPropertiesSelected = computed(
|
|
|
+ () => selectedPropertyId.value === null,
|
|
|
+);
|
|
|
+
|
|
|
const canExportReport = computed(
|
|
|
() =>
|
|
|
hasProperties.value &&
|
|
|
@@ -260,10 +265,6 @@ const canExportReport = computed(
|
|
|
selectedPropertyId.value !== null,
|
|
|
);
|
|
|
|
|
|
-const isAllPropertiesSelected = computed(
|
|
|
- () => selectedPropertyId.value === null,
|
|
|
-);
|
|
|
-
|
|
|
//
|
|
|
|
|
|
const payoutRows = computed(() => dashboard.value?.properties_breakdown ?? []);
|
|
|
@@ -318,17 +319,6 @@ const monthOptions = computed(() => {
|
|
|
|
|
|
//
|
|
|
|
|
|
-const selectedReferenceLabel = computed(() => {
|
|
|
- if (!selectedMonth.value || !selectedYear.value) {
|
|
|
- return "Mês selecionado";
|
|
|
- }
|
|
|
-
|
|
|
- const monthLabel =
|
|
|
- monthLabels[selectedMonth.value - 1] ?? String(selectedMonth.value);
|
|
|
-
|
|
|
- return `${monthLabel} ${selectedYear.value}`;
|
|
|
-});
|
|
|
-
|
|
|
const allMetricCards = computed(() =>
|
|
|
buildDashboardMetricCards(summary.value, {
|
|
|
isAllPropertiesSelected: isAllPropertiesSelected.value,
|
|
|
@@ -345,8 +335,41 @@ const secondRowCards = computed(() =>
|
|
|
allMetricCards.value.slice(7, 14),
|
|
|
);
|
|
|
|
|
|
+const selectedReferenceLabel = computed(() => {
|
|
|
+ if (!selectedMonth.value || !selectedYear.value) {
|
|
|
+ return "Mês selecionado";
|
|
|
+ }
|
|
|
+
|
|
|
+ const monthLabel =
|
|
|
+ monthLabels[selectedMonth.value - 1] ?? String(selectedMonth.value);
|
|
|
+
|
|
|
+ return `${monthLabel} ${selectedYear.value}`;
|
|
|
+});
|
|
|
+
|
|
|
+//
|
|
|
+
|
|
|
+const availabilityTotalLabel = computed(() => {
|
|
|
+ if (isAllPropertiesSelected.value) {
|
|
|
+ return "Total: 100% do período consolidado";
|
|
|
+ }
|
|
|
+
|
|
|
+ const total = Number(summary.value.days_in_month ?? 0);
|
|
|
+
|
|
|
+ return `Total: ${formatInteger(total)} dias no período`;
|
|
|
+});
|
|
|
+
|
|
|
+const channelsTotalLabel = computed(() => {
|
|
|
+ const total = Number(summary.value.reservations_count ?? 0);
|
|
|
+
|
|
|
+ return `Total: ${formatInteger(total)} reservas`;
|
|
|
+});
|
|
|
+
|
|
|
+//
|
|
|
+
|
|
|
const availabilityItems = computed(() => {
|
|
|
- const total = totalCapacityDays.value;
|
|
|
+ const total =
|
|
|
+ Number(summary.value.days_in_month ?? 0) *
|
|
|
+ Number(summary.value.properties_count ?? 1);
|
|
|
|
|
|
const items = [
|
|
|
{
|
|
|
@@ -366,21 +389,84 @@ const availabilityItems = computed(() => {
|
|
|
},
|
|
|
];
|
|
|
|
|
|
- return items.map((item) => ({
|
|
|
- ...item,
|
|
|
- valueLabel: isAllPropertiesSelected.value
|
|
|
- ? formatPercent(total > 0 ? (item.value * 100) / total : 0, 1)
|
|
|
- : `${formatInteger(item.value)} dias`,
|
|
|
+ return items.map((item, index) => ({
|
|
|
+ key: `${item.label}-${index}`,
|
|
|
+
|
|
|
+ label: item.label,
|
|
|
+
|
|
|
+ valueLabel: `${formatInteger(item.value)} dias (${formatPercent(
|
|
|
+ total > 0 ? (item.value * 100) / total : 0,
|
|
|
+ 1,
|
|
|
+ )})`,
|
|
|
+
|
|
|
+ percentage:
|
|
|
+ total > 0
|
|
|
+ ? Number(((item.value * 100) / total).toFixed(2))
|
|
|
+ : 0,
|
|
|
|
|
|
- percentage: total > 0 ? Number(((item.value * 100) / total).toFixed(2)) : 0,
|
|
|
+ color: item.color,
|
|
|
}));
|
|
|
});
|
|
|
|
|
|
-//
|
|
|
+const channelColorsMap = {
|
|
|
+ "API booking.com": "#6b93cf",
|
|
|
+ "API airbnb": "#ff8f93",
|
|
|
+ "API Decolar": "#c4b5fd",
|
|
|
+ "Sem canal": "#d3d3d3",
|
|
|
+ "Raniery Kohler": "#7BB2AB",
|
|
|
+};
|
|
|
+
|
|
|
+const ranieryFallbackPalette = [
|
|
|
+ "#7bb2ab",
|
|
|
+ "#8bbdb7",
|
|
|
+ "#9bc8c3",
|
|
|
+ "#abd3cf",
|
|
|
+ "#bbdedb",
|
|
|
+ "#cbe9e7",
|
|
|
+];
|
|
|
+
|
|
|
+const channelsBarItems = computed(() => {
|
|
|
+ const channels = dashboard.value?.channels ?? [];
|
|
|
+
|
|
|
+ let fallbackIndex = 0;
|
|
|
+
|
|
|
+ return channels.map((item, index) => {
|
|
|
+ const fallbackColor =
|
|
|
+ ranieryFallbackPalette[
|
|
|
+ fallbackIndex++ % ranieryFallbackPalette.length
|
|
|
+ ];
|
|
|
+
|
|
|
+ return {
|
|
|
+ key: `${item.channel}-${index}`,
|
|
|
+
|
|
|
+ label: item.channel,
|
|
|
+
|
|
|
+ valueLabel: `${formatInteger(
|
|
|
+ item.reservations_count,
|
|
|
+ )} reservas (${formatPercent(item.share_percentage, 1)})`,
|
|
|
+
|
|
|
+ percentage: Number(item.share_percentage ?? 0),
|
|
|
+
|
|
|
+ color: channelColorsMap[item.channel] ?? fallbackColor,
|
|
|
+ };
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+const shortMonthLabel = (month, year) => {
|
|
|
+ if (!month || !year) {
|
|
|
+ return "-";
|
|
|
+ }
|
|
|
+
|
|
|
+ return new Intl.DateTimeFormat("pt-BR", {
|
|
|
+ month: "short",
|
|
|
+ })
|
|
|
+ .format(new Date(year, month - 1, 1))
|
|
|
+ .replace(".", "");
|
|
|
+};
|
|
|
|
|
|
const revenueChartData = computed(() => {
|
|
|
- const series = revenueSeries.value.length
|
|
|
- ? revenueSeries.value
|
|
|
+ const seriesRaw = revenueSeries.value.length
|
|
|
+ ? [...revenueSeries.value]
|
|
|
: [
|
|
|
{
|
|
|
label: shortMonthLabel(selectedMonth.value, selectedYear.value),
|
|
|
@@ -388,20 +474,20 @@ const revenueChartData = computed(() => {
|
|
|
},
|
|
|
];
|
|
|
|
|
|
+ seriesRaw.pop();
|
|
|
+
|
|
|
return {
|
|
|
- labels: series.map((item) => item.label),
|
|
|
+ labels: seriesRaw.map((item) => item.label),
|
|
|
datasets: [
|
|
|
{
|
|
|
label: "Faturamento",
|
|
|
- data: series.map((item) => item.value),
|
|
|
+ data: seriesRaw.map((item) => item.value),
|
|
|
borderColor: "#399FE7",
|
|
|
borderWidth: 2,
|
|
|
fill: false,
|
|
|
tension: 0.35,
|
|
|
-
|
|
|
pointBackgroundColor: "#399FE7",
|
|
|
pointBorderColor: "#399FE7",
|
|
|
-
|
|
|
pointRadius: 3,
|
|
|
pointHoverRadius: 4,
|
|
|
},
|
|
|
@@ -452,20 +538,6 @@ const revenueChartOptions = computed(() => ({
|
|
|
|
|
|
//
|
|
|
|
|
|
-const shortMonthLabel = (month, year) => {
|
|
|
- if (!month || !year) {
|
|
|
- return "-";
|
|
|
- }
|
|
|
-
|
|
|
- return new Intl.DateTimeFormat("pt-BR", {
|
|
|
- month: "short",
|
|
|
- })
|
|
|
- .format(new Date(year, month - 1, 1))
|
|
|
- .replace(".", "");
|
|
|
-};
|
|
|
-
|
|
|
-//
|
|
|
-
|
|
|
const buildDashboardParams = ({ year, month, propertyId, format } = {}) => {
|
|
|
const params = {};
|
|
|
|
|
|
@@ -501,8 +573,6 @@ const buildFallbackReportFilename = (format) => {
|
|
|
return `owner_dashboard_report_${year}${month}_${propertySegment}.${format}`;
|
|
|
};
|
|
|
|
|
|
-//
|
|
|
-
|
|
|
const downloadBlob = (blob, filename) => {
|
|
|
const url = window.URL.createObjectURL(blob);
|
|
|
const link = document.createElement("a");
|
|
|
@@ -519,6 +589,52 @@ const downloadBlob = (blob, filename) => {
|
|
|
window.URL.revokeObjectURL(url);
|
|
|
};
|
|
|
|
|
|
+const exportDashboardReport = async (format) => {
|
|
|
+ if (!canExportReport.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ exporting.value = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await downloadOwnerDashboardReport(
|
|
|
+ buildDashboardParams({
|
|
|
+ year: selectedYear.value,
|
|
|
+ month: selectedMonth.value,
|
|
|
+ propertyId: selectedPropertyId.value,
|
|
|
+ format,
|
|
|
+ }),
|
|
|
+ );
|
|
|
+
|
|
|
+ downloadBlob(response.data, resolveReportFilename(response, format));
|
|
|
+ } catch (error) {
|
|
|
+ const blobMessage = await extractBlobErrorMessage(error?.response?.data);
|
|
|
+
|
|
|
+ $q.notify({
|
|
|
+ type: "negative",
|
|
|
+ message:
|
|
|
+ blobMessage ?? "Não foi possível exportar o relatório do dashboard.",
|
|
|
+ });
|
|
|
+ } finally {
|
|
|
+ exporting.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const extractBlobErrorMessage = async (blob) => {
|
|
|
+ if (!(blob instanceof Blob)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const text = await blob.text();
|
|
|
+ const parsed = JSON.parse(text);
|
|
|
+
|
|
|
+ return parsed?.message ?? null;
|
|
|
+ } catch {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
const findPropertyOptionById = (propertyId) => {
|
|
|
return (
|
|
|
propertyOptions.value.find((option) => option.id === propertyId) ??
|
|
|
@@ -553,54 +669,47 @@ const resolveReportFilename = (response, format) => {
|
|
|
|
|
|
//
|
|
|
|
|
|
-const extractBlobErrorMessage = async (blob) => {
|
|
|
- if (!(blob instanceof Blob)) {
|
|
|
- return null;
|
|
|
- }
|
|
|
+const fetchDashboard = async ({ year, month, propertyId } = {}) => {
|
|
|
+ const requestId = ++dashboardRequestId.value;
|
|
|
|
|
|
- try {
|
|
|
- const text = await blob.text();
|
|
|
- const parsed = JSON.parse(text);
|
|
|
+ loading.value = true;
|
|
|
|
|
|
- return parsed?.message ?? null;
|
|
|
- } catch {
|
|
|
- return null;
|
|
|
- }
|
|
|
-};
|
|
|
+ try {
|
|
|
+ const payload = await getOwnerDashboard(
|
|
|
+ buildDashboardParams({ year, month, propertyId }),
|
|
|
+ );
|
|
|
|
|
|
-const exportDashboardReport = async (format) => {
|
|
|
- if (!canExportReport.value) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ if (requestId !== dashboardRequestId.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- exporting.value = true;
|
|
|
+ dashboard.value = payload;
|
|
|
|
|
|
- try {
|
|
|
- const response = await downloadOwnerDashboardReport(
|
|
|
- buildDashboardParams({
|
|
|
- year: selectedYear.value,
|
|
|
- month: selectedMonth.value,
|
|
|
- propertyId: selectedPropertyId.value,
|
|
|
- format,
|
|
|
- }),
|
|
|
+ selectedPropertyOption.value = findPropertyOptionById(
|
|
|
+ payload.filters.selected?.property_id ?? null,
|
|
|
);
|
|
|
+ selectedMonth.value = payload.filters.selected?.reference_month ?? null;
|
|
|
+ selectedYear.value = payload.filters.selected?.reference_year ?? null;
|
|
|
|
|
|
- downloadBlob(response.data, resolveReportFilename(response, format));
|
|
|
+ await fetchRevenueHistory(payload);
|
|
|
} catch (error) {
|
|
|
- const blobMessage = await extractBlobErrorMessage(error?.response?.data);
|
|
|
+ if (requestId !== dashboardRequestId.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
$q.notify({
|
|
|
type: "negative",
|
|
|
message:
|
|
|
- blobMessage ?? "Não foi possível exportar o relatório do dashboard.",
|
|
|
+ error?.response?.data?.message ??
|
|
|
+ "Não foi possível carregar o dashboard do proprietário.",
|
|
|
});
|
|
|
} finally {
|
|
|
- exporting.value = false;
|
|
|
+ if (requestId === dashboardRequestId.value) {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-//
|
|
|
-
|
|
|
const fetchRevenueHistory = async (payload) => {
|
|
|
const requestId = ++historyRequestId.value;
|
|
|
|
|
|
@@ -646,65 +755,6 @@ const fetchRevenueHistory = async (payload) => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-const fetchDashboard = async ({ year, month, propertyId } = {}) => {
|
|
|
- const requestId = ++dashboardRequestId.value;
|
|
|
-
|
|
|
- loading.value = true;
|
|
|
-
|
|
|
- try {
|
|
|
- const payload = await getOwnerDashboard(
|
|
|
- buildDashboardParams({ year, month, propertyId }),
|
|
|
- );
|
|
|
-
|
|
|
- if (requestId !== dashboardRequestId.value) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- dashboard.value = payload;
|
|
|
-
|
|
|
- selectedPropertyOption.value = findPropertyOptionById(
|
|
|
- payload.filters.selected?.property_id ?? null,
|
|
|
- );
|
|
|
- selectedMonth.value = payload.filters.selected?.reference_month ?? null;
|
|
|
- selectedYear.value = payload.filters.selected?.reference_year ?? null;
|
|
|
-
|
|
|
- await fetchRevenueHistory(payload);
|
|
|
- } catch (error) {
|
|
|
- if (requestId !== dashboardRequestId.value) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- $q.notify({
|
|
|
- type: "negative",
|
|
|
- message:
|
|
|
- error?.response?.data?.message ??
|
|
|
- "Não foi possível carregar o dashboard do proprietário.",
|
|
|
- });
|
|
|
- } finally {
|
|
|
- if (requestId === dashboardRequestId.value) {
|
|
|
- loading.value = false;
|
|
|
- }
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-//
|
|
|
-
|
|
|
-const handlePropertyChange = async (propertyOption) => {
|
|
|
- const nextPropertyId = propertyOption?.id ?? null;
|
|
|
-
|
|
|
- const currentPropertyId = selectedFilters.value.property_id ?? null;
|
|
|
-
|
|
|
- if (nextPropertyId === currentPropertyId) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- await fetchDashboard({
|
|
|
- year: selectedYear.value,
|
|
|
- month: selectedMonth.value,
|
|
|
- propertyId: nextPropertyId,
|
|
|
- });
|
|
|
-};
|
|
|
-
|
|
|
//
|
|
|
|
|
|
const handleMonthChange = async (month) => {
|
|
|
@@ -740,6 +790,22 @@ const handleYearChange = async (year) => {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
+const handlePropertyChange = async (propertyOption) => {
|
|
|
+ const nextPropertyId = propertyOption?.id ?? null;
|
|
|
+
|
|
|
+ const currentPropertyId = selectedFilters.value.property_id ?? null;
|
|
|
+
|
|
|
+ if (nextPropertyId === currentPropertyId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ await fetchDashboard({
|
|
|
+ year: selectedYear.value,
|
|
|
+ month: selectedMonth.value,
|
|
|
+ propertyId: nextPropertyId,
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
//
|
|
|
|
|
|
onMounted(async () => {
|