|
|
@@ -65,7 +65,7 @@
|
|
|
<DashboardPayoutTable
|
|
|
:rows="payoutRows"
|
|
|
:total-reservations="Number(summary.reservations_count ?? 0)"
|
|
|
- :total-value="Number(summary.owner_payout_amount ?? 0)"
|
|
|
+ :total-value="Number(summary.final_payout_amount ?? 0)"
|
|
|
/>
|
|
|
</q-card>
|
|
|
|
|
|
@@ -140,34 +140,34 @@ ChartJS.register(
|
|
|
|
|
|
const $q = useQuasar();
|
|
|
|
|
|
-const dashboard = ref(null);
|
|
|
-const loading = ref(false);
|
|
|
-const exporting = ref(false);
|
|
|
+const dashboard = ref(null);
|
|
|
+const loading = ref(false);
|
|
|
+const exporting = ref(false);
|
|
|
const selectedPropertyOption = ref(null);
|
|
|
-const selectedMonth = ref(null);
|
|
|
-const selectedYear = ref(null);
|
|
|
-const revenueSeries = ref([]);
|
|
|
-const dashboardRequestId = ref(0);
|
|
|
-const historyRequestId = ref(0);
|
|
|
+const selectedMonth = ref(null);
|
|
|
+const selectedYear = ref(null);
|
|
|
+const revenueSeries = ref([]);
|
|
|
+const dashboardRequestId = ref(0);
|
|
|
+const historyRequestId = ref(0);
|
|
|
|
|
|
const defaultSummary = Object.freeze({
|
|
|
- reserve_total: 0,
|
|
|
- total_forward_fee_all: 0,
|
|
|
- owner_payout_amount: 0,
|
|
|
- final_payout_amount: 0,
|
|
|
- total_expenses_amount: 0,
|
|
|
- occupied_nights_in_month: 0,
|
|
|
- occupancy_rate: 0,
|
|
|
- average_price_per_night: null,
|
|
|
- average_reservation_ticket: null,
|
|
|
+ reserve_total: 0,
|
|
|
+ total_forward_fee_all: 0,
|
|
|
+ owner_payout_amount: 0,
|
|
|
+ final_payout_amount: 0,
|
|
|
+ total_expenses_amount: 0,
|
|
|
+ occupied_nights_in_month: 0,
|
|
|
+ occupancy_rate: 0,
|
|
|
+ average_price_per_night: null,
|
|
|
+ average_reservation_ticket: null,
|
|
|
average_nights_per_reservation: 0,
|
|
|
- maintenance_days: 0,
|
|
|
- cleanings_count: 0,
|
|
|
- expenses: 0,
|
|
|
- reservations_count: 0,
|
|
|
- available_days: 0,
|
|
|
- days_in_month: 30,
|
|
|
- properties_count: 1,
|
|
|
+ maintenance_days: 0,
|
|
|
+ cleanings_count: 0,
|
|
|
+ expenses: 0,
|
|
|
+ reservations_count: 0,
|
|
|
+ available_days: 0,
|
|
|
+ days_in_month: 30,
|
|
|
+ properties_count: 1,
|
|
|
});
|
|
|
|
|
|
const monthLabels = [
|
|
|
@@ -203,7 +203,7 @@ const propertyOptions = computed(() => {
|
|
|
return [
|
|
|
{ id: null, label: "Todos os imóveis" },
|
|
|
...properties.map((property) => ({
|
|
|
- id: property.id,
|
|
|
+ id: property.id,
|
|
|
label: property.label,
|
|
|
})),
|
|
|
];
|
|
|
@@ -218,27 +218,38 @@ const selectedPropertyId = computed(() => {
|
|
|
});
|
|
|
|
|
|
const canExportReport = computed(
|
|
|
- () => hasProperties.value && dashboard.value !== null && selectedPropertyId.value !== null,
|
|
|
+ () =>
|
|
|
+ hasProperties.value &&
|
|
|
+ dashboard.value !== null &&
|
|
|
+ selectedPropertyId.value !== null,
|
|
|
);
|
|
|
|
|
|
-const isAllPropertiesSelected = computed(() => selectedPropertyId.value === null);
|
|
|
+const isAllPropertiesSelected = computed(
|
|
|
+ () => selectedPropertyId.value === null,
|
|
|
+);
|
|
|
|
|
|
//
|
|
|
|
|
|
-const payoutRows = computed(
|
|
|
- () => dashboard.value?.properties_breakdown ?? [],
|
|
|
-);
|
|
|
+const payoutRows = computed(() => dashboard.value?.properties_breakdown ?? []);
|
|
|
|
|
|
const summary = computed(() => dashboard.value?.summary ?? defaultSummary);
|
|
|
|
|
|
-const totalCapacityDays = computed(
|
|
|
- () => Math.max(1, Number(summary.value.days_in_month ?? 0) * Number(summary.value.properties_count ?? 1)),
|
|
|
+const totalCapacityDays = computed(() =>
|
|
|
+ Math.max(
|
|
|
+ 1,
|
|
|
+ Number(summary.value.days_in_month ?? 0) *
|
|
|
+ Number(summary.value.properties_count ?? 1),
|
|
|
+ ),
|
|
|
);
|
|
|
|
|
|
//
|
|
|
|
|
|
const yearOptions = computed(() => {
|
|
|
- const years = [...new Set(availableReferences.value.map((reference) => reference.reference_year))];
|
|
|
+ const years = [
|
|
|
+ ...new Set(
|
|
|
+ availableReferences.value.map((reference) => reference.reference_year),
|
|
|
+ ),
|
|
|
+ ];
|
|
|
|
|
|
return years
|
|
|
.sort((a, b) => b - a)
|
|
|
@@ -253,11 +264,13 @@ const monthOptions = computed(() => {
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
- const months = [...new Set(
|
|
|
- availableReferences.value
|
|
|
- .filter((reference) => reference.reference_year === selectedYear.value)
|
|
|
- .map((reference) => reference.reference_month),
|
|
|
- )];
|
|
|
+ const months = [
|
|
|
+ ...new Set(
|
|
|
+ availableReferences.value
|
|
|
+ .filter((reference) => reference.reference_year === selectedYear.value)
|
|
|
+ .map((reference) => reference.reference_month),
|
|
|
+ ),
|
|
|
+ ];
|
|
|
|
|
|
return months
|
|
|
.sort((a, b) => a - b)
|
|
|
@@ -274,17 +287,20 @@ const selectedReferenceLabel = computed(() => {
|
|
|
return "Mês selecionado";
|
|
|
}
|
|
|
|
|
|
- const monthLabel = monthLabels[selectedMonth.value - 1] ?? String(selectedMonth.value);
|
|
|
+ const monthLabel =
|
|
|
+ monthLabels[selectedMonth.value - 1] ?? String(selectedMonth.value);
|
|
|
|
|
|
return `${monthLabel} ${selectedYear.value}`;
|
|
|
});
|
|
|
|
|
|
-const allMetricCards = computed(() => buildDashboardMetricCards(summary.value, {
|
|
|
- isAllPropertiesSelected: isAllPropertiesSelected.value,
|
|
|
- referenceLabel: selectedReferenceLabel.value,
|
|
|
- totalCapacityDays: totalCapacityDays.value,
|
|
|
-}));
|
|
|
-const firstRowCards = computed(() => allMetricCards.value.slice(0, 5));
|
|
|
+const allMetricCards = computed(() =>
|
|
|
+ buildDashboardMetricCards(summary.value, {
|
|
|
+ isAllPropertiesSelected: isAllPropertiesSelected.value,
|
|
|
+ referenceLabel: selectedReferenceLabel.value,
|
|
|
+ totalCapacityDays: totalCapacityDays.value,
|
|
|
+ }),
|
|
|
+);
|
|
|
+const firstRowCards = computed(() => allMetricCards.value.slice(0, 5));
|
|
|
const secondRowCards = computed(() => allMetricCards.value.slice(5, 10));
|
|
|
|
|
|
const availabilityItems = computed(() => {
|
|
|
@@ -324,27 +340,27 @@ const revenueChartData = computed(() => {
|
|
|
const series = revenueSeries.value.length
|
|
|
? revenueSeries.value
|
|
|
: [
|
|
|
- {
|
|
|
- label: shortMonthLabel(selectedMonth.value, selectedYear.value),
|
|
|
- value: Number(summary.value.reserve_total ?? 0),
|
|
|
- },
|
|
|
- ];
|
|
|
+ {
|
|
|
+ label: shortMonthLabel(selectedMonth.value, selectedYear.value),
|
|
|
+ value: Number(summary.value.reserve_total ?? 0),
|
|
|
+ },
|
|
|
+ ];
|
|
|
|
|
|
return {
|
|
|
labels: series.map((item) => item.label),
|
|
|
datasets: [
|
|
|
{
|
|
|
- label: "Faturamento",
|
|
|
- data: series.map((item) => item.value),
|
|
|
- borderColor: "#399FE7",
|
|
|
- borderWidth: 2,
|
|
|
- fill: false,
|
|
|
- tension: 0.35,
|
|
|
+ label: "Faturamento",
|
|
|
+ data: series.map((item) => item.value),
|
|
|
+ borderColor: "#399FE7",
|
|
|
+ borderWidth: 2,
|
|
|
+ fill: false,
|
|
|
+ tension: 0.35,
|
|
|
|
|
|
pointBackgroundColor: "#399FE7",
|
|
|
- pointBorderColor: "#399FE7",
|
|
|
+ pointBorderColor: "#399FE7",
|
|
|
|
|
|
- pointRadius: 3,
|
|
|
+ pointRadius: 3,
|
|
|
pointHoverRadius: 4,
|
|
|
},
|
|
|
],
|
|
|
@@ -352,7 +368,7 @@ const revenueChartData = computed(() => {
|
|
|
});
|
|
|
|
|
|
const revenueChartOptions = computed(() => ({
|
|
|
- responsive: true,
|
|
|
+ responsive: true,
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
|
plugins: {
|
|
|
@@ -385,7 +401,7 @@ const revenueChartOptions = computed(() => ({
|
|
|
},
|
|
|
|
|
|
ticks: {
|
|
|
- color: "#7B878C",
|
|
|
+ color: "#7B878C",
|
|
|
callback: (value) => formatCurrencyCompact(value),
|
|
|
},
|
|
|
},
|
|
|
@@ -404,7 +420,7 @@ const shortMonthLabel = (month, year) => {
|
|
|
})
|
|
|
.format(new Date(year, month - 1, 1))
|
|
|
.replace(".", "");
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
//
|
|
|
|
|
|
@@ -428,27 +444,28 @@ const buildDashboardParams = ({ year, month, propertyId, format } = {}) => {
|
|
|
}
|
|
|
|
|
|
return params;
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
const buildFallbackReportFilename = (format) => {
|
|
|
const year = String(selectedYear.value ?? "0000");
|
|
|
|
|
|
const month = String(selectedMonth.value ?? "00").padStart(2, "0");
|
|
|
|
|
|
- const propertySegment = selectedPropertyId.value === null
|
|
|
- ? "all-properties"
|
|
|
- : `property_${selectedPropertyId.value}`;
|
|
|
+ const propertySegment =
|
|
|
+ selectedPropertyId.value === null
|
|
|
+ ? "all-properties"
|
|
|
+ : `property_${selectedPropertyId.value}`;
|
|
|
|
|
|
return `owner_dashboard_report_${year}${month}_${propertySegment}.${format}`;
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
//
|
|
|
|
|
|
const downloadBlob = (blob, filename) => {
|
|
|
- const url = window.URL.createObjectURL(blob);
|
|
|
+ const url = window.URL.createObjectURL(blob);
|
|
|
const link = document.createElement("a");
|
|
|
|
|
|
- link.href = url;
|
|
|
+ link.href = url;
|
|
|
link.download = filename;
|
|
|
|
|
|
document.body.appendChild(link);
|
|
|
@@ -458,11 +475,15 @@ const downloadBlob = (blob, filename) => {
|
|
|
document.body.removeChild(link);
|
|
|
|
|
|
window.URL.revokeObjectURL(url);
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
const findPropertyOptionById = (propertyId) => {
|
|
|
- return propertyOptions.value.find((option) => option.id === propertyId) ?? propertyOptions.value[0] ?? null;
|
|
|
-}
|
|
|
+ return (
|
|
|
+ propertyOptions.value.find((option) => option.id === propertyId) ??
|
|
|
+ propertyOptions.value[0] ??
|
|
|
+ null
|
|
|
+ );
|
|
|
+};
|
|
|
|
|
|
const resolveReportFilename = (response, format) => {
|
|
|
const explicitHeader = response.headers["x-report-filename"];
|
|
|
@@ -486,7 +507,7 @@ const resolveReportFilename = (response, format) => {
|
|
|
}
|
|
|
|
|
|
return buildFallbackReportFilename(format);
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
//
|
|
|
|
|
|
@@ -496,14 +517,14 @@ const extractBlobErrorMessage = async (blob) => {
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- const text = await blob.text();
|
|
|
+ const text = await blob.text();
|
|
|
const parsed = JSON.parse(text);
|
|
|
|
|
|
return parsed?.message ?? null;
|
|
|
} catch {
|
|
|
return null;
|
|
|
}
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
const exportDashboardReport = async (format) => {
|
|
|
if (!canExportReport.value) {
|
|
|
@@ -513,24 +534,28 @@ const exportDashboardReport = async (format) => {
|
|
|
exporting.value = true;
|
|
|
|
|
|
try {
|
|
|
- const response = await downloadOwnerDashboardReport(buildDashboardParams({
|
|
|
- year: selectedYear.value,
|
|
|
- month: selectedMonth.value,
|
|
|
- propertyId: selectedPropertyId.value,
|
|
|
- format,
|
|
|
- }));
|
|
|
+ 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.",
|
|
|
+ type: "negative",
|
|
|
+ message:
|
|
|
+ blobMessage ?? "Não foi possível exportar o relatório do dashboard.",
|
|
|
});
|
|
|
} finally {
|
|
|
exporting.value = false;
|
|
|
}
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
//
|
|
|
|
|
|
@@ -553,14 +578,17 @@ const fetchRevenueHistory = async (payload) => {
|
|
|
references.map(async (reference) => {
|
|
|
const historyPayload = await getOwnerDashboard(
|
|
|
buildDashboardParams({
|
|
|
- year: reference.reference_year,
|
|
|
+ year: reference.reference_year,
|
|
|
month: reference.reference_month,
|
|
|
propertyId,
|
|
|
}),
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
- label: shortMonthLabel(reference.reference_month, reference.reference_year),
|
|
|
+ label: shortMonthLabel(
|
|
|
+ reference.reference_month,
|
|
|
+ reference.reference_year,
|
|
|
+ ),
|
|
|
value: Number(historyPayload?.summary?.reserve_total ?? 0),
|
|
|
};
|
|
|
}),
|
|
|
@@ -574,9 +602,9 @@ const fetchRevenueHistory = async (payload) => {
|
|
|
revenueSeries.value = [];
|
|
|
}
|
|
|
}
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
-const fetchDashboard = async ({ year, month, propertyId } = {}) => {
|
|
|
+const fetchDashboard = async ({ year, month, propertyId } = {}) => {
|
|
|
const requestId = ++dashboardRequestId.value;
|
|
|
|
|
|
loading.value = true;
|
|
|
@@ -592,9 +620,11 @@ const fetchDashboard = async ({ year, month, propertyId } = {}) => {
|
|
|
|
|
|
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;
|
|
|
+ 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) {
|
|
|
@@ -603,14 +633,17 @@ const fetchDashboard = async ({ year, month, propertyId } = {}) => {
|
|
|
}
|
|
|
|
|
|
$q.notify({
|
|
|
- type: "negative", message: error?.response?.data?.message ?? "Não foi possível carregar o dashboard do proprietário.",
|
|
|
+ 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;
|
|
|
}
|
|
|
}
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
//
|
|
|
|
|
|
@@ -624,11 +657,11 @@ const handlePropertyChange = async (propertyOption) => {
|
|
|
}
|
|
|
|
|
|
await fetchDashboard({
|
|
|
- year: selectedYear.value,
|
|
|
- month: selectedMonth.value,
|
|
|
+ year: selectedYear.value,
|
|
|
+ month: selectedMonth.value,
|
|
|
propertyId: nextPropertyId,
|
|
|
});
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
//
|
|
|
|
|
|
@@ -638,26 +671,32 @@ const handleMonthChange = async (month) => {
|
|
|
}
|
|
|
|
|
|
await fetchDashboard({
|
|
|
- year: selectedYear.value, month, propertyId: selectedPropertyId.value,
|
|
|
+ year: selectedYear.value,
|
|
|
+ month,
|
|
|
+ propertyId: selectedPropertyId.value,
|
|
|
});
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
const handleYearChange = async (year) => {
|
|
|
- const availableMonth = availableReferences.value
|
|
|
- .filter((reference) => reference.reference_year === year)
|
|
|
- .sort((a, b) => b.reference_month - a.reference_month)[0]?.reference_month ?? selectedMonth.value;
|
|
|
+ const availableMonth =
|
|
|
+ availableReferences.value
|
|
|
+ .filter((reference) => reference.reference_year === year)
|
|
|
+ .sort((a, b) => b.reference_month - a.reference_month)[0]
|
|
|
+ ?.reference_month ?? selectedMonth.value;
|
|
|
|
|
|
if (
|
|
|
- year === selectedFilters.value.reference_year
|
|
|
- && availableMonth === selectedFilters.value.reference_month
|
|
|
+ year === selectedFilters.value.reference_year &&
|
|
|
+ availableMonth === selectedFilters.value.reference_month
|
|
|
) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
await fetchDashboard({
|
|
|
- year, month: availableMonth, propertyId: selectedPropertyId.value,
|
|
|
+ year,
|
|
|
+ month: availableMonth,
|
|
|
+ propertyId: selectedPropertyId.value,
|
|
|
});
|
|
|
-}
|
|
|
+};
|
|
|
|
|
|
//
|
|
|
|
|
|
@@ -672,10 +711,11 @@ onMounted(async () => {
|
|
|
}
|
|
|
|
|
|
.dashboard-shell {
|
|
|
- position: relative;
|
|
|
- display: flex;
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
flex-direction: column;
|
|
|
- gap: 16px;
|
|
|
+ gap: 16px;
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
|
|
|
.dashboard-separator {
|
|
|
@@ -684,38 +724,37 @@ onMounted(async () => {
|
|
|
}
|
|
|
|
|
|
.dashboard-section-caption {
|
|
|
- margin-top: -2px;
|
|
|
- margin-bottom: -4px;
|
|
|
- color: #08514c;
|
|
|
- font-size: 19px;
|
|
|
- font-weight: 400;
|
|
|
- line-height: 1.1;
|
|
|
- vertical-align: middle;
|
|
|
+ margin-top: -2px;
|
|
|
+ margin-bottom: -4px;
|
|
|
+ color: #08514c;
|
|
|
+ font-size: 19px;
|
|
|
+ font-weight: 400;
|
|
|
+ line-height: 1.1;
|
|
|
+ vertical-align: middle;
|
|
|
}
|
|
|
|
|
|
.metrics-grid {
|
|
|
display: grid;
|
|
|
-
|
|
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
|
-
|
|
|
gap: 12px;
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
|
|
|
.dashboard-panels {
|
|
|
display: grid;
|
|
|
-
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
-
|
|
|
- gap: 16px;
|
|
|
+ gap: 16px;
|
|
|
align-items: start;
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
|
|
|
.panel-card {
|
|
|
- padding: 18px;
|
|
|
- border-radius: 14px;
|
|
|
- background: #ffffff;
|
|
|
- border: 1px solid #d9e3e7;
|
|
|
- min-height: 300px;
|
|
|
+ padding: 18px;
|
|
|
+ border-radius: 14px;
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1px solid #d9e3e7;
|
|
|
+ min-height: 300px;
|
|
|
+ min-width: 0;
|
|
|
vertical-align: middle;
|
|
|
}
|
|
|
|
|
|
@@ -730,9 +769,9 @@ onMounted(async () => {
|
|
|
|
|
|
.panel-title {
|
|
|
margin-bottom: 16px;
|
|
|
- font-size: 19px;
|
|
|
- font-weight: 400;
|
|
|
- color: #08514c;
|
|
|
+ font-size: 19px;
|
|
|
+ font-weight: 400;
|
|
|
+ color: #08514c;
|
|
|
}
|
|
|
|
|
|
.owner-dashboard-page :deep(.text-h6.text-bold) {
|
|
|
@@ -758,8 +797,34 @@ onMounted(async () => {
|
|
|
}
|
|
|
|
|
|
@media (max-width: 640px) {
|
|
|
+ .dashboard-shell {
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .dashboard-section-caption {
|
|
|
+ font-size: 16px;
|
|
|
+ line-height: 1.3;
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+
|
|
|
.metrics-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .dashboard-panels {
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .panel-card {
|
|
|
+ padding: 14px;
|
|
|
+ min-height: 0;
|
|
|
+ border-radius: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .panel-title {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ font-size: 17px;
|
|
|
}
|
|
|
}
|
|
|
</style>
|