Forráskód Böngészése

refactor: ajustes dashboard

Gustavo Mantovani 4 napja
szülő
commit
c46020f81d

+ 11 - 8
src/components/layout/LeftMenuLayout.vue

@@ -13,20 +13,18 @@
     class="detached-container bg-surface"
   >
     <div class="column full-height q-pa-sm no-wrap">
-      <div
-        v-if="!$q.screen.lt.md"
-        class="toggle-button-wrapper absolute"
-        style="top: 50%; right: -32px; z-index: 1"
-      >
+      <div v-if="!$q.screen.lt.md" class="toggle-button-wrapper">
         <q-btn
+          class="toggle-button"
           flat
           round
-          size="sm"
-          padding="8px 8px"
+          size="md"
+          padding="10px"
           @click="miniState = !miniState"
         >
           <q-icon
             :name="miniState ? 'mdi-chevron-right' : 'mdi-chevron-left'"
+            size="32px"
           />
           <q-tooltip
             anchor="center right"
@@ -280,11 +278,16 @@ watch(miniState, () => {
 }
 
 .toggle-button-wrapper {
+  position: absolute;
+  top: 50dvh;
+  right: -28px;
+  z-index: 2;
+  transform: translateY(-50%);
   transition: transform 0.3s ease;
 }
 
 .toggle-button-wrapper:hover {
-  transform: scale(1.1);
+  transform: translateY(-50%) scale(1.1);
 }
 
 .menu-item--spaced {

+ 20 - 17
src/helpers/buildMetricCards.js

@@ -14,18 +14,20 @@ const capitalize = (text) => {
 export function buildMetricCards(summary, options = {}) {
   const { isAllPropertiesSelected = false, referenceLabel = "" } = options;
 
-  const displayedExpenses =
-    Number(summary.expenses_amount ?? 0) + Number(summary.ota_amount ?? 0);
-
-  const maintenanceExpenseAmount = Number(
-    summary.expenses_breakdown?.items
-      ?.filter((expense) => expense.source === "manual")
-      ?.filter((expense) => expense.category === "maintenance")
-      ?.reduce(
-        (total, expense) => total + Number(expense.final_amount ?? 0),
-        0,
-      ) ?? 0,
-  );
+  const manualExpenses =
+    summary.expenses_breakdown?.items?.filter(
+      (expense) => expense.source === "manual",
+    ) ?? [];
+
+  const resolveExpenseType = (expense) => expense.type ?? expense.category;
+
+  const expenseAmount = manualExpenses
+    .filter((expense) => resolveExpenseType(expense) === "expense")
+    .reduce((total, expense) => total + Number(expense.final_amount ?? 0), 0);
+
+  const maintenanceExpenseAmount = manualExpenses
+    .filter((expense) => resolveExpenseType(expense) === "maintenance")
+    .reduce((total, expense) => total + Number(expense.final_amount ?? 0), 0);
 
   const occupancyCaption = isAllPropertiesSelected
     ? "percentual do período"
@@ -67,7 +69,7 @@ export function buildMetricCards(summary, options = {}) {
     {
       label: "manutenção",
       value: formatCurrency(maintenanceExpenseAmount),
-      caption: "despesas manuais",
+      caption: "no período",
     },
 
     {
@@ -87,7 +89,7 @@ export function buildMetricCards(summary, options = {}) {
     },
 
     {
-      label: "ota",
+      label: "taxa de plataformas",
       value: formatCurrency(summary.ota_amount),
       caption: "taxas plataformas",
     },
@@ -95,7 +97,8 @@ export function buildMetricCards(summary, options = {}) {
     {
       label: "faturamento líquido",
       value: formatCurrency(summary.net_revenue_amount),
-      caption: "Bruto - OTA",
+      caption: "Bruto - taxas plataformas",
+      nowrapCaption: true,
     },
 
     {
@@ -112,8 +115,8 @@ export function buildMetricCards(summary, options = {}) {
 
     {
       label: "despesas",
-      value: formatCurrency(displayedExpenses),
-      caption: "inclui OTA",
+      value: formatCurrency(expenseAmount),
+      caption: "no período",
     },
 
     {

+ 87 - 21
src/pages/dashboard/DashboardExpensesPage.vue

@@ -41,6 +41,7 @@
             :key="card.label"
             :caption="card.caption"
             :label="card.label"
+            :nowrap-caption="card.nowrapCaption"
             :value="card.value"
             variant="primary"
           />
@@ -56,23 +57,39 @@
             :key="card.label"
             :caption="card.caption"
             :label="card.label"
+            :nowrap-caption="card.nowrapCaption"
             :value="card.value"
             variant="secondary"
           />
         </div>
 
-        <q-card flat class="panel-card panel-card--soft expenses-panel">
-          <div class="panel-title">Despesas do Período</div>
-
-          <DashboardPayoutTable
-            :columns="expenseTableColumns"
-            empty-label="Nenhuma despesa encontrada para o período."
-            row-key="key"
-            :rows="expenseTableRows"
-            total-label="Total de despesas"
-            :total-row="expenseTableTotalRow"
-          />
-        </q-card>
+        <div class="expenses-grid">
+          <q-card flat class="panel-card panel-card--soft expenses-panel">
+            <div class="panel-title">Despesas do Período</div>
+
+            <DashboardPayoutTable
+              :columns="expenseTableColumns"
+              empty-label="Nenhuma despesa encontrada para o período."
+              row-key="key"
+              :rows="expenseTableRows"
+              total-label="Total de despesas"
+              :total-row="expenseTableTotalRow"
+            />
+          </q-card>
+
+          <q-card flat class="panel-card panel-card--soft expenses-panel">
+            <div class="panel-title">Manutenções do Período</div>
+
+            <DashboardPayoutTable
+              :columns="expenseTableColumns"
+              empty-label="Nenhuma manutenção encontrada para o período."
+              row-key="key"
+              :rows="maintenanceTableRows"
+              total-label="Total de manutenções"
+              :total-row="maintenanceTableTotalRow"
+            />
+          </q-card>
+        </div>
       </template>
 
       <q-inner-loading :showing="loading">
@@ -137,11 +154,15 @@ const defaultSummary = Object.freeze({
   cleaning_total_amount: 0,
   management_fee_amount: 0,
   expenses_amount: 0,
+  manual_expenses_amount: 0,
   payout_amount: 0,
   expenses_breakdown: {
     items: [],
+    system_expenses: [],
     totals: {
       expenses_amount: 0,
+      manual_expenses_amount: 0,
+      system_expenses_amount: 0,
     },
   },
 });
@@ -260,14 +281,34 @@ const firstRowCards = computed(() => allMetricCards.value.slice(0, 7));
 
 const secondRowCards = computed(() => allMetricCards.value.slice(7, 14));
 
-const expenseRows = computed(
-  () => summary.value?.expenses_breakdown?.items ?? [],
+const manualExpenseRows = computed(
+  () =>
+    summary.value?.expenses_breakdown?.items?.filter(
+      (expense) => expense.source === "manual",
+    ) ?? [],
+);
+
+const resolveExpenseType = (expense) => expense.type ?? expense.category;
+
+const expenseRows = computed(() =>
+  manualExpenseRows.value.filter(
+    (expense) => resolveExpenseType(expense) === "expense",
+  ),
 );
 
-const expenseTotal = computed(
-  () => summary.value?.expenses_breakdown?.totals?.expenses_amount ?? 0,
+const maintenanceRows = computed(() =>
+  manualExpenseRows.value.filter(
+    (expense) => resolveExpenseType(expense) === "maintenance",
+  ),
 );
 
+const sumExpenseRows = (rows) =>
+  rows.reduce((total, expense) => total + Number(expense.final_amount ?? 0), 0);
+
+const expenseTotal = computed(() => sumExpenseRows(expenseRows.value));
+
+const maintenanceTotal = computed(() => sumExpenseRows(maintenanceRows.value));
+
 const expenseTableColumns = [
   {
     key: "label",
@@ -297,11 +338,24 @@ const expenseTableRows = computed(() =>
   })),
 );
 
+const maintenanceTableRows = computed(() =>
+  maintenanceRows.value.map((expense, index) => ({
+    ...expense,
+    key: expense.key || `${expense.label}-${index}`,
+    source_label: expense.source === "manual" ? "Interna" : "Stays/Sistema",
+  })),
+);
+
 const expenseTableTotalRow = computed(() => ({
   source_label: "",
   final_amount: expenseTotal.value,
 }));
 
+const maintenanceTableTotalRow = computed(() => ({
+  source_label: "",
+  final_amount: maintenanceTotal.value,
+}));
+
 const buildDashboardParams = ({ year, month, propertyId } = {}) => {
   const params = {};
 
@@ -510,9 +564,21 @@ onMounted(async () => {
   background: #f0f3f5;
 }
 
-.expenses-panel {
-  width: 50%;
-  align-self: flex-start;
+.expenses-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+  min-width: 0;
+}
+
+.expenses-panel :deep(.payout-table th),
+.expenses-panel :deep(.payout-table td),
+.expenses-panel :deep(.payout-total-row td) {
+  text-align: left;
+}
+
+.expenses-panel :deep(.header-wrapper) {
+  align-items: flex-start;
 }
 
 .panel-title {
@@ -557,8 +623,8 @@ onMounted(async () => {
     gap: 10px;
   }
 
-  .expenses-panel {
-    width: 100%;
+  .expenses-grid {
+    grid-template-columns: 1fr;
   }
 }
 </style>

+ 8 - 7
src/pages/dashboard/DashboardPage.vue

@@ -48,6 +48,7 @@
             :key="card.label"
             :caption="card.caption"
             :label="card.label"
+            :nowrap-caption="card.nowrapCaption"
             :value="card.value"
             variant="primary"
           />
@@ -69,6 +70,7 @@
             :key="card.label"
             :caption="card.caption"
             :label="card.label"
+            :nowrap-caption="card.nowrapCaption"
             :value="card.value"
             variant="secondary"
           />
@@ -207,11 +209,15 @@ const defaultSummary = Object.freeze({
   cleaning_total_amount: 0,
   management_fee_amount: 0,
   expenses_amount: 0,
+  manual_expenses_amount: 0,
   payout_amount: 0,
   expenses_breakdown: {
     items: [],
+    system_expenses: [],
     totals: {
       expenses_amount: 0,
+      manual_expenses_amount: 0,
+      system_expenses_amount: 0,
     },
   },
 });
@@ -310,14 +316,9 @@ const monthOptions = computed(() => {
   const currentMonth = now.getMonth() + 1;
 
   const lastMonth =
-    selectedYear.value < currentYear
-      ? 12
-      : Math.min(currentMonth, 12);
+    selectedYear.value < currentYear ? 12 : Math.min(currentMonth, 12);
 
-  const months = Array.from(
-    { length: lastMonth },
-    (_, index) => index + 1,
-  );
+  const months = Array.from({ length: lastMonth }, (_, index) => index + 1);
 
   return months.map((month) => ({
     label: monthLabels[month - 1] ?? String(month),

+ 23 - 5
src/pages/dashboard/components/DashboardMetricCard.vue

@@ -8,7 +8,9 @@
       {{ value }}
     </strong>
 
-    <span class="metric-caption">
+    <span
+      :class="['metric-caption', { 'metric-caption--nowrap': nowrapCaption }]"
+    >
       {{ caption }}
     </span>
   </q-card>
@@ -18,6 +20,7 @@
 defineProps({
   caption: { type: String, default: "" },
   label: { type: String, required: true },
+  nowrapCaption: { type: Boolean, default: false },
   value: { type: String, required: true },
   variant: { type: String, default: "primary" },
 });
@@ -25,13 +28,13 @@ defineProps({
 
 <style scoped lang="scss">
 .metric-card {
-  display: flex;
-  flex-direction: column;
-  justify-content: space-between;
+  display: grid;
+  grid-template-rows: 56px 34px 22px;
+  align-items: start;
   gap: 6px;
   min-height: 106px;
   min-width: 0;
-  padding: 16px;
+  padding: 12px 16px;
   border: 1px solid #d9e3e7;
   border-radius: 12px;
   background: #f0f3f5;
@@ -47,6 +50,9 @@ defineProps({
 }
 
 .metric-label {
+  display: flex;
+  align-self: stretch;
+  align-items: center;
   font-size: 19px;
   font-weight: 400;
   color: #4d4e4e;
@@ -54,6 +60,7 @@ defineProps({
 }
 
 .metric-value {
+  align-self: start;
   font-size: 24px;
   line-height: 1.15;
   color: #111212;
@@ -61,13 +68,20 @@ defineProps({
 }
 
 .metric-caption {
+  align-self: start;
   font-size: 14px;
   color: #4d4e4e;
   overflow-wrap: anywhere;
 }
 
+.metric-caption--nowrap {
+  font-size: 13px;
+  white-space: nowrap;
+}
+
 @media (max-width: 640px) {
   .metric-card {
+    grid-template-rows: auto;
     min-height: 0;
     padding: 14px;
   }
@@ -83,5 +97,9 @@ defineProps({
   .metric-caption {
     font-size: 13px;
   }
+
+  .metric-caption--nowrap {
+    font-size: 12px;
+  }
 }
 </style>

+ 45 - 9
src/pages/dashboard/components/DashboardPayoutTable.vue

@@ -3,9 +3,17 @@
     <table class="payout-table">
       <thead>
         <tr>
-          <th v-for="column in resolvedColumns" :key="column.key">
+          <th
+            v-for="column in resolvedColumns"
+            :key="column.key"
+            :class="`payout-cell--${column.align ?? 'center'}`"
+          >
             <div class="header-wrapper">
               <div class="header-content">
+                <span class="header-label">
+                  {{ column.label }}
+                </span>
+
                 <q-icon
                   class="cursor-pointer sort-icon q-my-md"
                   size="15px"
@@ -13,10 +21,6 @@
                   :name="column.direction === 'desc' ? 'south' : 'north'"
                   @click="toggleSort(column)"
                 />
-
-                <span class="header-label">
-                  {{ column.label }}
-                </span>
               </div>
             </div>
           </th>
@@ -25,7 +29,11 @@
 
       <tbody>
         <tr v-for="item in sortedRows" :key="resolveRowKey(item)">
-          <td v-for="column in resolvedColumns" :key="column.key">
+          <td
+            v-for="column in resolvedColumns"
+            :key="column.key"
+            :class="`payout-cell--${column.align ?? 'center'}`"
+          >
             {{ formatCell(resolveColumnValue(item, column), column) }}
           </td>
         </tr>
@@ -37,9 +45,15 @@
         </tr>
 
         <tr class="payout-total-row">
-          <td>{{ totalLabel }}</td>
+          <td :class="`payout-cell--${resolvedColumns[0]?.align ?? 'center'}`">
+            {{ totalLabel }}
+          </td>
 
-          <td v-for="column in totalColumns" :key="column.key">
+          <td
+            v-for="column in totalColumns"
+            :key="column.key"
+            :class="`payout-cell--${column.align ?? 'center'}`"
+          >
             {{ formatCell(resolvedTotalRow[column.key], column) }}
           </td>
         </tr>
@@ -98,13 +112,15 @@ const defaultColumns = [
     key: "property_label",
     label: "Unidade",
     type: "text",
+    align: "left",
     direction: null,
   },
 
   {
     key: "reservations_count",
     label: "Reservas",
-    type: "integer",
+    type: "reservations",
+    align: "left",
     direction: null,
   },
 
@@ -112,6 +128,7 @@ const defaultColumns = [
     key: "final_payout_amount",
     label: "Valor",
     type: "currency",
+    align: "left",
     direction: null,
   },
 ];
@@ -203,6 +220,10 @@ const formatCell = (value, column) => {
     return formatCurrency(value);
   }
 
+  if (column.type === "reservations") {
+    return formatReservations(value);
+  }
+
   if (column.type === "integer") {
     return formatInteger(value);
   }
@@ -222,6 +243,13 @@ const formatInteger = (value) => {
   return Number(value ?? 0).toLocaleString("pt-BR");
 };
 
+const formatReservations = (value) => {
+  const reservations = Number(value ?? 0);
+  const label = reservations === 1 ? "reserva" : "reservas";
+
+  return `${formatInteger(reservations)} ${label}`;
+};
+
 watch(
   () => props.columns,
   (value) => {
@@ -256,6 +284,14 @@ watch(
   color: #232323;
 }
 
+.payout-table .payout-cell--left {
+  text-align: left;
+}
+
+.payout-cell--left .header-wrapper {
+  align-items: flex-start;
+}
+
 .payout-total-row td {
   font-size: 17px !important;
   font-weight: 700;