| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- <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>
|