Просмотр исходного кода

refactor: alterando valores monetarios para fazer de acordo com o layout

Gustavo Mantovani 1 месяц назад
Родитель
Сommit
ed4fbdce39

+ 5 - 1
src/helpers/buildMetricCards.js

@@ -95,7 +95,11 @@ export function buildMetricCards(summary, options = {}) {
 
     {
       label: "Total de Reservas",
-      value: formatInteger(summary.reservations_count),
+      value: formatInteger(
+        summary.distinct_reservations_count ??
+        summary.checkout_reservations_count ??
+        summary.reservations_count
+      ),
       caption: "no período",
     },
 

+ 10 - 2
src/pages/dashboard/DashboardPage.vue

@@ -101,7 +101,7 @@
 
             <DashboardPayoutTable
               :rows="payoutRows"
-              :total-reservations="Number(summary.reservations_count ?? 0)"
+              :total-reservations="Number(summary.checkout_reservations_count ?? 0)"
               :total-value="Number(summary.final_payout_amount ?? 0)"
             />
           </q-card>
@@ -190,9 +190,12 @@ const defaultSummary = Object.freeze({
   cleanings_count: 0,
   expenses: 0,
   reservations_count: 0,
+  distinct_reservations_count: 0,
   checkout_reservations_count: 0,
   crossed_reservations_count: 0,
+  repeated_reservations_count: 0,
   crossed_cleanings_count: 0,
+  repeated_cleanings_count: 0,
   available_days: 0,
   days_in_month: 30,
   properties_count: 1,
@@ -352,7 +355,12 @@ const availabilityTotalLabel = computed(() => {
 });
 
 const channelsTotalLabel = computed(() => {
-  const total = Number(summary.value.reservations_count ?? 0);
+  const total = Number(
+    summary.value.distinct_reservations_count ??
+    summary.value.checkout_reservations_count ??
+    summary.value.reservations_count ??
+    0
+  );
 
   return `Total: ${formatInteger(total)} reservas`;
 });

+ 0 - 261
src/pages/dashboard/components/DashboardAvailabilityPanel.vue

@@ -1,261 +0,0 @@
-<template>
-  <q-card class="panel-card panel-card--soft" flat>
-    <div class="panel-title">Disponibilidade do Período</div>
-
-    <div class="availability-list">
-      <div
-        v-for="item in decoratedItems"
-        :key="item.label"
-        class="availability-item"
-      >
-        <div class="availability-track">
-          <div
-            class="availability-bar"
-            :style="{
-              backgroundColor: item.color,
-            }"
-          >
-            <div class="availability-track-meta">
-              <div class="availability-bar-label-wrap">
-                <span
-                  class="availability-bar-value-dot"
-                  :style="{
-                    color: item.textColor,
-                    backgroundColor: item.textColor,
-                  }"
-                />
-
-                <span class="availability-bar-label">{{ item.label }}</span>
-              </div>
-
-              <span class="availability-bar-value-wrap">
-                <span
-                  class="availability-bar-value"
-                  :style="{ color: item.textColor }"
-                  >{{ item.valueLabel }}</span
-                >
-              </span>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <div class="availability-total-track">
-      <div
-        v-for="item in items"
-        :key="`${item.label}-total`"
-        :style="{
-          width: `${item.percentage}%`,
-          backgroundColor: item.color,
-        }"
-        class="availability-total-segment"
-      />
-    </div>
-
-    <div class="availability-total">
-      {{ totalLabel }}
-    </div>
-  </q-card>
-</template>
-
-<script setup>
-import { computed } from "vue";
-import { formatInteger } from "src/helpers/utils";
-
-const props = defineProps({
-  isAllPropertiesSelected: {
-    type: Boolean,
-    default: false,
-  },
-  items: {
-    type: Array,
-    default: () => [],
-  },
-  totalCapacityDays: {
-    type: Number,
-    default: 0,
-  },
-});
-
-const totalLabel = computed(() =>
-  props.isAllPropertiesSelected
-    ? "Total: 100% do período consolidado"
-    : `Total: ${formatInteger(props.totalCapacityDays)} dias no período`,
-);
-
-const clampChannel = (value) => Math.max(0, Math.min(255, value));
-
-const darkenColor = (hexColor, amount = 0.42) => {
-  const hex = String(hexColor ?? "").replace("#", "");
-
-  if (hex.length !== 6) {
-    return "#173235";
-  }
-
-  const channels = [0, 2, 4].map((start) =>
-    Number.parseInt(hex.slice(start, start + 2), 16),
-  );
-  const darkened = channels.map((channel) =>
-    clampChannel(Math.round(channel * (1 - amount))),
-  );
-
-  return `#${darkened.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
-};
-
-const decoratedItems = computed(() =>
-  props.items.map((item) => ({
-    ...item,
-    textColor: darkenColor(item.color),
-  })),
-);
-</script>
-
-<style scoped lang="scss">
-.panel-card {
-  padding: 18px;
-  border-radius: 14px;
-  background: #ffffff;
-  border: 1px solid #d9e3e7;
-  min-height: 300px;
-  vertical-align: middle;
-}
-
-.panel-card--soft {
-  background: #f0f3f5;
-}
-
-.panel-title {
-  margin-bottom: 16px;
-  font-size: 19px;
-  font-weight: 400;
-  color: #08514c;
-}
-
-.availability-list {
-  display: flex;
-  flex-direction: column;
-  gap: 16px;
-}
-
-.availability-item {
-  display: flex;
-  flex-direction: column;
-  gap: 10px;
-}
-
-.availability-track {
-  position: relative;
-  width: 100%;
-  min-height: 42px;
-  overflow: hidden;
-  background: #dde5e8;
-  vertical-align: middle;
-}
-
-.availability-bar {
-  width: 100%;
-  min-height: 42px;
-  vertical-align: middle;
-}
-
-.availability-track-meta {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  gap: 12px;
-  width: 100%;
-  min-width: 0;
-  min-height: 42px;
-  padding: 0 14px;
-  position: relative;
-  z-index: 1;
-  color: #173235;
-  font-size: 13px;
-  font-weight: 700;
-  white-space: nowrap;
-  vertical-align: middle;
-}
-
-.availability-bar-label {
-  text-align: left;
-  vertical-align: middle;
-}
-
-.availability-bar-label-wrap,
-.availability-bar-value-wrap {
-  display: inline-flex;
-  align-items: center;
-  justify-content: flex-start;
-  gap: 8px;
-  min-width: 0;
-  vertical-align: middle;
-}
-
-.availability-bar-value {
-  text-align: right;
-  overflow-wrap: anywhere;
-  vertical-align: middle;
-}
-
-.availability-bar-value-dot {
-  display: inline-flex;
-  width: 8px;
-  height: 8px;
-  border-radius: 50%;
-  vertical-align: middle;
-}
-
-.availability-total-track {
-  display: flex;
-  width: 100%;
-  height: 18px;
-  margin-top: 18px;
-  overflow: hidden;
-  background: #dde5e8;
-  vertical-align: middle;
-}
-
-.availability-total-segment {
-  height: 100%;
-  min-width: 0;
-  vertical-align: middle;
-}
-
-.availability-total {
-  margin-top: 18px;
-  color: #657177;
-  font-size: 14px;
-  text-align: center;
-}
-
-@media (max-width: 640px) {
-  .availability-list {
-    gap: 12px;
-  }
-
-  .availability-track,
-  .availability-bar {
-    min-height: 48px;
-  }
-
-  .availability-track-meta {
-    flex-direction: column;
-    align-items: flex-start;
-    justify-content: center;
-    gap: 8px;
-    min-height: 48px;
-    padding: 0 12px;
-    font-size: 12px;
-    white-space: normal;
-  }
-
-  .availability-bar-value-wrap {
-    width: 100%;
-  }
-
-  .availability-bar-value {
-    text-align: left;
-  }
-}
-</style>

+ 0 - 252
src/pages/dashboard/components/DashboardChannelsPanel.vue

@@ -1,252 +0,0 @@
-<template>
-  <q-card class="panel-card panel-card--soft panel-card--channels" flat>
-    <div class="panel-title">Canais de Aquisição</div>
-
-    <div class="channel-chart-wrap">
-      <div class="channel-chart-box">
-        <Pie :data="chartData" :options="chartOptions" />
-      </div>
-    </div>
-
-    <div class="channel-legend">
-      <div
-        v-for="channel in legendItems"
-        :key="channel.key"
-        class="channel-legend-item"
-      >
-        <span class="channel-dot" :style="{ backgroundColor: channel.color }" />
-
-        <span class="channel-name">{{ channel.label }}</span>
-
-        <span class="channel-total">
-          {{ formatInteger(channel.reservations_count) }} reservas
-        </span>
-      </div>
-    </div>
-  </q-card>
-</template>
-
-<script setup>
-import { computed } from "vue";
-import { Pie } from "vue-chartjs";
-import { formatInteger } from "src/helpers/utils";
-
-const props = defineProps({
-  channels: {
-    type: Array,
-    default: () => [],
-  },
-});
-
-const fixedChannelColors = Object.freeze({
-  airbnb: "#FF385C",
-  booking: "#60A5FA",
-  decolar: "#22C55E",
-  direto: "#F59E0B",
-  vrbo: "#0EA5E9",
-  sem_canal: "#B0BEC5",
-  raniery_kohler: "#A855F7",
-});
-
-const fallbackChannelPalette = Object.freeze([
-  "#A855F7",
-  "#8B5CF6",
-  "#C084FC",
-  "#9333EA",
-  "#D946EF",
-  "#7C3AED",
-  "#E879F9",
-  "#6D28D9",
-]);
-
-const normalize = (channel) => {
-  const value = String(channel ?? "").toLowerCase();
-
-  if (value.includes("airbnb")) {
-    return "airbnb";
-  }
-
-  if (value.includes("booking")) {
-    return "booking";
-  }
-
-  if (value.includes("decolar")) {
-    return "decolar";
-  }
-
-  if (
-    value.includes("sem canal") ||
-    value.includes("sem_canal") ||
-    value.includes("no channel")
-  ) {
-    return "sem_canal";
-  }
-
-  if (value.includes("raniery kohler")) {
-    return "raniery_kohler";
-  }
-
-  if (
-    value.includes("direto") ||
-    value.includes("direct") ||
-    value.includes("site") ||
-    value.includes("whatsapp") ||
-    value.includes("instagram")
-  ) {
-    return "direto";
-  }
-
-  if (value.includes("vrbo")) {
-    return "vrbo";
-  }
-
-  return "outros";
-};
-
-const buildFallbackColor = (channel) => {
-  const seed = String(channel ?? "").trim().toLowerCase();
-
-  let hash = 0;
-
-  for (let index = 0; index < seed.length; index += 1) {
-    hash = (hash * 31 + seed.charCodeAt(index)) % fallbackChannelPalette.length;
-  }
-
-  return fallbackChannelPalette[hash];
-};
-
-const resolveChannelColor = (channel) => {
-  const normalizedChannel = normalize(channel);
-
-  return fixedChannelColors[normalizedChannel] ?? buildFallbackColor(channel);
-};
-
-const legendItems = computed(() =>
-  props.channels.map((c) => ({
-    key: String(c.channel ?? ""),
-    label: c.channel,
-    reservations_count: Number(c.reservations_count ?? 0),
-    color: resolveChannelColor(c.channel),
-  })),
-);
-
-const hasChannels = computed(() => legendItems.value.length > 0);
-
-const chartData = computed(() => ({
-  labels: hasChannels.value
-    ? legendItems.value.map((item) => item.label)
-    : ["Sem dados"],
-  datasets: [
-    {
-      data: hasChannels.value
-        ? legendItems.value.map((item) => item.reservations_count)
-        : [1],
-      backgroundColor: hasChannels.value
-        ? legendItems.value.map((item) => item.color)
-        : ["#D9E3E7"],
-      // borderColor: "#FFFFFF",
-      borderWidth: 0,
-    },
-  ],
-}));
-
-const chartOptions = computed(() => ({
-  responsive: true,
-  maintainAspectRatio: false,
-  plugins: {
-    legend: { display: false },
-    tooltip: { enabled: false },
-  },
-}));
-</script>
-
-<style scoped>
-.panel-title {
-  margin-bottom: 16px;
-  font-size: 19px;
-  font-weight: 400;
-  color: #08514c;
-}
-
-.channel-chart-wrap {
-  display: flex;
-  justify-content: center;
-  min-width: 0;
-  padding: 8px 0 16px;
-}
-
-.channel-chart-box {
-  width: min(120px, 100%);
-  height: 120px;
-  margin-bottom: 74px;
-}
-
-.channel-legend {
-  display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
-  gap: 14px;
-  justify-items: center;
-  text-align: center;
-  min-width: 0;
-}
-
-.channel-legend-item {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  gap: 4px;
-  min-width: 0;
-  vertical-align: middle;
-}
-
-.channel-dot {
-  display: inline-flex;
-  width: 10px;
-  height: 10px;
-  border-radius: 50%;
-}
-
-.channel-name {
-  display: inline-flex;
-  align-items: center;
-  gap: 8px;
-  font-size: 15px;
-  font-weight: 600;
-  color: #273136;
-  min-width: 0;
-  overflow-wrap: anywhere;
-  text-align: center;
-}
-
-.channel-total {
-  font-size: 14px;
-  font-weight: 400;
-  color: #5f6d73;
-  overflow-wrap: anywhere;
-}
-
-@media (max-width: 640px) {
-  .channel-chart-wrap {
-    padding: 0 0 12px;
-  }
-
-  .channel-chart-box {
-    width: min(160px, 100%);
-    height: 160px;
-    margin-bottom: 24px;
-  }
-
-  .channel-legend {
-    grid-template-columns: minmax(0, 1fr);
-    gap: 12px;
-  }
-
-  .channel-name {
-    font-size: 14px;
-  }
-
-  .channel-total {
-    font-size: 13px;
-  }
-}
-</style>

+ 51 - 37
src/pages/dashboard/components/DashboardPayoutTable.vue

@@ -8,29 +8,28 @@
             :key="column.key"
           >
             <div class="header-wrapper">
-              <q-icon
-                class="cursor-pointer sort-icon q-my-md"
-                :name="
-                  column.direction === 'desc'
-                    ? 'south'
-                    : 'north'
-                "
-                size="15px"
-                :color="
-                  column.direction
-                    ? 'primary'
-                    : '#b0b0b0'
-                "
-                @click="toggleSort(column)"
-              />
-
               <div class="header-content">
                 <q-icon
-                  v-if="index > 0"
+                  class="cursor-pointer sort-icon q-my-md"
+                  :name="
+                    column.direction === 'desc'
+                      ? 'south'
+                      : 'north'
+                  "
+                  size="15px"
+                  :color="
+                    column.direction
+                      ? 'primary'
+                      : '#b0b0b0'
+                  "
+                  @click="toggleSort(column)"
+                />
+
+                <q-icon
                   class="cursor-pointer move-icon"
+                  color="#888"
                   name="chevron_left"
                   size="18px"
-                  color="#888"
                   @click="moveLeft(index)"
                 />
 
@@ -39,14 +38,10 @@
                 </span>
 
                 <q-icon
-                  v-if="
-                    index <
-                    columns.length - 1
-                  "
                   class="cursor-pointer move-icon"
+                  color="#888"
                   name="chevron_right"
                   size="18px"
-                  color="#888"
                   @click="moveRight(index)"
                 />
               </div>
@@ -85,7 +80,7 @@
             >
               {{
                 formatInteger(
-                  item[column.key]
+                  resolveReservationsCount(item)
                 )
               }}
             </template>
@@ -171,31 +166,38 @@ const toggleSort = (
 };
 
 const moveLeft = (index) => {
-  if (index === 0) return;
+  const lastIndex =
+    columns.value.length - 1;
+
+  const targetIndex =
+    index === 0
+      ? lastIndex
+      : index - 1;
 
   [
-    columns.value[index - 1],
+    columns.value[targetIndex],
     columns.value[index],
   ] = [
     columns.value[index],
-    columns.value[index - 1],
+    columns.value[targetIndex],
   ];
 };
 
 const moveRight = (index) => {
-  if (
-    index ===
-    columns.value.length - 1
-  ) {
-    return;
-  }
+  const lastIndex =
+    columns.value.length - 1;
+
+  const targetIndex =
+    index === lastIndex
+      ? 0
+      : index + 1;
 
   [
-    columns.value[index + 1],
+    columns.value[targetIndex],
     columns.value[index],
   ] = [
     columns.value[index],
-    columns.value[index + 1],
+    columns.value[targetIndex],
   ];
 };
 
@@ -218,10 +220,14 @@ const sortedRows = computed(() => {
             : -1;
 
         const valueA =
-          a[sort.key];
+          sort.key === "reservations_count"
+            ? resolveReservationsCount(a)
+            : a[sort.key];
 
         const valueB =
-          b[sort.key];
+          sort.key === "reservations_count"
+            ? resolveReservationsCount(b)
+            : b[sort.key];
 
         if (valueA > valueB) {
           return direction;
@@ -237,6 +243,14 @@ const sortedRows = computed(() => {
   );
 });
 
+const resolveReservationsCount = (item) =>
+  Number(
+    item.checkout_reservations_count ??
+      item.distinct_reservations_count ??
+      item.reservations_count ??
+      0
+  );
+
 const formatCurrency = (
   value
 ) => {