|
|
@@ -1,3 +1,4 @@
|
|
|
+<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
|
|
|
<template>
|
|
|
<div class="q-mb-md q-mx-md">
|
|
|
<div class="items-center justify-between q-mb-sm row">
|
|
|
@@ -15,106 +16,133 @@
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
- <div class="scroll-wrapper">
|
|
|
- <div class="no-wrap q-gutter-x-md row scroll-track">
|
|
|
+ <div
|
|
|
+ class="scroll-wrapper"
|
|
|
+ :class="{ 'scroll-wrapper--single': groupedSolicitations.length === 1 }"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="no-wrap row scroll-track"
|
|
|
+ :class="{
|
|
|
+ 'q-gutter-x-md': groupedSolicitations.length > 1,
|
|
|
+ 'scroll-track--single': groupedSolicitations.length === 1,
|
|
|
+ }"
|
|
|
+ >
|
|
|
<q-card
|
|
|
- v-for="item in data"
|
|
|
+ v-for="item in groupedSolicitations"
|
|
|
:key="item.id"
|
|
|
+ :class="{
|
|
|
+ 'solicitation-card--single': groupedSolicitations.length === 1,
|
|
|
+ 'solicitation-card--multiple': groupedSolicitations.length > 1,
|
|
|
+ }"
|
|
|
:flat="false"
|
|
|
class="bg-surface card-border q-ma-md shadow-card solicitation-card"
|
|
|
>
|
|
|
- <q-card-section class="q-pa-sm">
|
|
|
- <div class="items-center no-wrap q-gutter-x-sm row text-text">
|
|
|
- <q-avatar size="48px">
|
|
|
- <img
|
|
|
- v-if="item.customer_photo"
|
|
|
- :src="item.customer_photo"
|
|
|
- style="object-fit: cover"
|
|
|
- />
|
|
|
+ <q-card-section class="q-pa-sm solicitation-card-section">
|
|
|
+ <div class="items-start no-wrap q-gutter-x-sm row text-text">
|
|
|
+ <div class="solicitation-avatar-column">
|
|
|
+ <!--
|
|
|
+ <div class="font9 fontmedium text-text solicitation-time">
|
|
|
+ {{ formatTimeSinceRequest(item.time_since_request) }}
|
|
|
+ </div>
|
|
|
+ !-->
|
|
|
+
|
|
|
+ <q-avatar size="48px">
|
|
|
+ <img
|
|
|
+ v-if="item.customer_photo"
|
|
|
+ :src="item.customer_photo"
|
|
|
+ style="object-fit: cover"
|
|
|
+ />
|
|
|
+
|
|
|
+ <span
|
|
|
+ v-else
|
|
|
+ class="flex flex-center full-height full-width"
|
|
|
+ style="border-radius: 50%"
|
|
|
+ :style="avatarColors[item.id % avatarColors.length]"
|
|
|
+ >
|
|
|
+ {{
|
|
|
+ getFirstName(item.client_name)
|
|
|
+ ?.slice(0, 2)
|
|
|
+ .toUpperCase() ?? "??"
|
|
|
+ }}
|
|
|
+ </span>
|
|
|
+ </q-avatar>
|
|
|
|
|
|
- <span
|
|
|
- v-else
|
|
|
- class="flex flex-center full-height full-width"
|
|
|
- style="border-radius: 50%"
|
|
|
- :style="avatarColors[item.id % avatarColors.length]"
|
|
|
- >
|
|
|
- {{
|
|
|
- getFirstName(item.client_name)
|
|
|
- ?.slice(0, 2)
|
|
|
- .toUpperCase() ?? "??"
|
|
|
- }}
|
|
|
- </span>
|
|
|
- </q-avatar>
|
|
|
+ </div>
|
|
|
|
|
|
- <div class="column flex-1">
|
|
|
- <div class="items-center q-gutter-x-xs row">
|
|
|
- <span class="ellipsis font9 fontmedium">
|
|
|
- {{ getFirstName(item.client_name) || "—" }}
|
|
|
- </span>
|
|
|
+ <div class="column flex-1 solicitation-info">
|
|
|
+ <div class="info-line solicitation-top-line no-wrap row">
|
|
|
+ <div class="items-center no-wrap row info-line__main">
|
|
|
+ <span class="ellipsis font12 fontbold">
|
|
|
+ {{ getFirstName(item.client_name) || "—" }}
|
|
|
+ </span>
|
|
|
|
|
|
- <div class="items-center row">
|
|
|
<q-icon
|
|
|
+ class="q-ml-xs"
|
|
|
color="warning"
|
|
|
name="mdi-star"
|
|
|
size="12px"
|
|
|
/>
|
|
|
|
|
|
- <span class="font9 text-text">
|
|
|
+ <span class="font9 text-text no-wrap">
|
|
|
{{ item.average_rating }}
|
|
|
</span>
|
|
|
</div>
|
|
|
+
|
|
|
+ <div class="column items-end no-wrap q-ml-xs solicitation-price">
|
|
|
+ <span class="font12 fontbold no-wrap">
|
|
|
+ {{ formatCurrency(packageTotal(item)) }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
- <div class="font9 items-center no-wrap row">
|
|
|
- <span class="fontbold">
|
|
|
- {{ formatWeekday(item.date) }}
|
|
|
- </span>
|
|
|
+ <div class="schedule-list font9">
|
|
|
+ <div
|
|
|
+ v-if="item.package_items.length > 1"
|
|
|
+ class="package-summary font8"
|
|
|
+ >
|
|
|
+ {{ $t("provider.dashboard.solicitations.package_count", { count: item.package_items.length }) }}
|
|
|
+ </div>
|
|
|
|
|
|
- <span>{{ `, ${formatDayMonth(item.date)}` }}</span>
|
|
|
- </div>
|
|
|
+ <div
|
|
|
+ v-for="scheduleItem in item.package_items"
|
|
|
+ :key="scheduleItem.id"
|
|
|
+ class="solicitation-date-time"
|
|
|
+ >
|
|
|
+ <span class="schedule-weekday">
|
|
|
+ {{ formatWeekday(scheduleItem.date) }}
|
|
|
+ </span>
|
|
|
|
|
|
- <div class="font9">
|
|
|
- {{ $t("common.from") }}
|
|
|
+ <span class="fontbold schedule-day-month">
|
|
|
+ {{ formatDayMonth(scheduleItem.date) }}
|
|
|
+ </span>
|
|
|
|
|
|
- <span class="fontbold">
|
|
|
- {{ item.start_time?.slice(0, 5) }}
|
|
|
- {{ $t("common.to") }}
|
|
|
- {{ item.end_time?.slice(0, 5) }}
|
|
|
- </span>
|
|
|
+ <span class="fontbold schedule-time">
|
|
|
+ {{ $t("common.from") }}
|
|
|
+ {{ scheduleItem.start_time?.slice(0, 5) }}
|
|
|
+ {{ $t("common.to") }}
|
|
|
+ {{ scheduleItem.end_time?.slice(0, 5) }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ </div>
|
|
|
|
|
|
- <div class="column items-end text-text">
|
|
|
- <div class="font12 fontbold">
|
|
|
- {{ formatCurrency(item.total_amount) }}
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="font9">
|
|
|
- {{ t(labelsPeriodTypes.find((label) => label.value == item.period_type)?.label) }}
|
|
|
- </div>
|
|
|
+ <div class="font9 info-line solicitation-address-line row text-text">
|
|
|
+ {{ formatTimeSinceRequest(item.time_since_request) }} -
|
|
|
|
|
|
- <div class="font9 fontbold">
|
|
|
- {{ item.address?.district || "N/A" }}
|
|
|
- </div>
|
|
|
+ <span class="info-line__main solicitation-address">
|
|
|
+ {{ item.address?.district || "N/A" }}
|
|
|
|
|
|
- <div class="font9">
|
|
|
- <span class="q-pr-xs">
|
|
|
- {{ item.distance_km ?? 0 }}
|
|
|
- </span>
|
|
|
-
|
|
|
- {{ $t("common.km") }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <span class="fontbold no-wrap solicitation-distance">
|
|
|
+ {{ item.distance_km ?? 0 }} {{ $t("common.km") }}
|
|
|
+ </span>
|
|
|
+ </span>
|
|
|
</div>
|
|
|
|
|
|
- <div class="items-center q-gutter-x-sm q-mt-sm row">
|
|
|
- <div>
|
|
|
- <div class="font9 fontmedium text-text">
|
|
|
- {{ item.time_since_request }}
|
|
|
- </div>
|
|
|
-
|
|
|
+ <div class="items-center q-gutter-x-sm q-mt-sm row solicitation-actions">
|
|
|
+ <div class="solicitation-details">
|
|
|
<q-btn
|
|
|
- class="col-auto q-px-none"
|
|
|
+ class="col-auto q-px-none solicitation-details-btn"
|
|
|
color="primary"
|
|
|
flat
|
|
|
no-caps
|
|
|
@@ -155,11 +183,11 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
+import { computed } from "vue";
|
|
|
import { formatCurrency, getFirstName } from "src/helpers/utils";
|
|
|
-import { labelsPeriodTypes } from "src/helpers/labelsPeriodTypes.js";
|
|
|
import { useI18n } from "vue-i18n";
|
|
|
|
|
|
-defineProps({
|
|
|
+const props = defineProps({
|
|
|
data: {
|
|
|
default: () => [],
|
|
|
type: Array,
|
|
|
@@ -168,6 +196,31 @@ defineProps({
|
|
|
|
|
|
const emit = defineEmits(["accept", "reject", "view-details"]);
|
|
|
|
|
|
+const groupedSolicitations = computed(() => {
|
|
|
+ const groups = new Map();
|
|
|
+
|
|
|
+ props.data.forEach((item) => {
|
|
|
+ const key = item.service_package_id
|
|
|
+ ? `package:${item.service_package_id}`
|
|
|
+ : `schedule:${item.id}`;
|
|
|
+ const group = groups.get(key);
|
|
|
+
|
|
|
+ if (group) {
|
|
|
+ group.package_items.push(item);
|
|
|
+ } else {
|
|
|
+ groups.set(key, { ...item, package_items: [item] });
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return [...groups.values()];
|
|
|
+});
|
|
|
+
|
|
|
+const packageTotal = (item) =>
|
|
|
+ item.package_items.reduce(
|
|
|
+ (total, packageItem) => total + (Number(packageItem.total_amount) || 0),
|
|
|
+ 0,
|
|
|
+ );
|
|
|
+
|
|
|
const { t } = useI18n();
|
|
|
|
|
|
const avatarColors = [
|
|
|
@@ -207,20 +260,72 @@ const formatWeekday = (iso) => {
|
|
|
|
|
|
return weekday.charAt(0).toUpperCase() + weekday.slice(1);
|
|
|
};
|
|
|
+
|
|
|
+const formatTimeSinceRequest = (value) => {
|
|
|
+ if (!value) return "";
|
|
|
+
|
|
|
+ const normalizedValue = String(value).trim().replace(/\s+/g, " ");
|
|
|
+
|
|
|
+ if (/^just now$/i.test(normalizedValue)) {
|
|
|
+ return t("provider.dashboard.solicitations.time_ago.just_now");
|
|
|
+ }
|
|
|
+
|
|
|
+ const match = normalizedValue.match(
|
|
|
+ /^(-?\d+|an?|one)\s+(second|minute|hour|day|week|month|year)s?(?:\s+ago)?$/i,
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!match) return normalizedValue;
|
|
|
+
|
|
|
+ const count = /^-?\d+$/.test(match[1]) ? Math.abs(Number(match[1])) : 1;
|
|
|
+
|
|
|
+ return t(
|
|
|
+ `provider.dashboard.solicitations.time_ago.${match[2].toLowerCase()}`,
|
|
|
+ count,
|
|
|
+ );
|
|
|
+};
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
.scroll-wrapper {
|
|
|
overflow-x: auto;
|
|
|
- padding-bottom: 8px;
|
|
|
+ padding: 0 0 8px 8px;
|
|
|
|
|
|
&::-webkit-scrollbar {
|
|
|
display: none;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+.scroll-wrapper--single {
|
|
|
+ overflow-x: visible;
|
|
|
+ padding: 0 0 8px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.scroll-track {
|
|
|
+ align-items: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
.solicitation-card {
|
|
|
- min-width: 85vw;
|
|
|
+ box-shadow: -4px 2px 12px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.06) !important;
|
|
|
+ overflow: visible;
|
|
|
+}
|
|
|
+
|
|
|
+.solicitation-card--single {
|
|
|
+ flex: 0 0 100%;
|
|
|
+ margin: 0 !important;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.solicitation-card--multiple {
|
|
|
+ flex: 0 0 82vw;
|
|
|
+ margin-left: 8px !important;
|
|
|
+ margin-right: 8px !important;
|
|
|
+ max-width: 82vw;
|
|
|
+ min-width: 82vw;
|
|
|
+ width: 82vw;
|
|
|
+}
|
|
|
+
|
|
|
+.scroll-track--single {
|
|
|
+ width: 100%;
|
|
|
}
|
|
|
|
|
|
.btn-action {
|
|
|
@@ -231,4 +336,103 @@ const formatWeekday = (iso) => {
|
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
+
|
|
|
+.solicitation-avatar-column {
|
|
|
+ flex: 0 0 48px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.solicitation-time {
|
|
|
+ margin-bottom: 6px;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.solicitation-info {
|
|
|
+ align-self: stretch;
|
|
|
+ justify-content: space-around;
|
|
|
+ gap: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.info-line {
|
|
|
+ align-items: baseline;
|
|
|
+ justify-content: space-between;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.solicitation-top-line {
|
|
|
+ align-items: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+.info-line__main {
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.solicitation-address {
|
|
|
+ max-width: 100%;
|
|
|
+ overflow-wrap: anywhere;
|
|
|
+ white-space: normal;
|
|
|
+}
|
|
|
+
|
|
|
+.solicitation-distance::before {
|
|
|
+ content: " - ";
|
|
|
+}
|
|
|
+
|
|
|
+.schedule-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.solicitation-date-time {
|
|
|
+ align-items: center;
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.4fr);
|
|
|
+ border-bottom: 1px dotted #d5d5df;
|
|
|
+ padding: 1px 0;
|
|
|
+ width: 100%;
|
|
|
+ line-height: 1.35;
|
|
|
+}
|
|
|
+
|
|
|
+.schedule-weekday {
|
|
|
+ text-align: left;
|
|
|
+}
|
|
|
+
|
|
|
+.schedule-day-month {
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.schedule-time {
|
|
|
+ min-width: 0;
|
|
|
+ text-align: right;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.solicitation-address-line {
|
|
|
+ border-bottom: 1px solid #dcdce5;
|
|
|
+ display: block;
|
|
|
+ margin: 8px -8px 0;
|
|
|
+ padding: 8px 8px 6px;
|
|
|
+ text-align: center;
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ color: var(--q-primary);
|
|
|
+ content: "●";
|
|
|
+ margin-right: 4px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.solicitation-details {
|
|
|
+ align-items: center;
|
|
|
+ display: flex;
|
|
|
+}
|
|
|
+
|
|
|
+.solicitation-details-btn {
|
|
|
+ margin-top: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.package-summary {
|
|
|
+ color: #666;
|
|
|
+ line-height: 1.35;
|
|
|
+}
|
|
|
</style>
|