|
@@ -0,0 +1,681 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <q-page class="service-package-page bg-page q-pb-xl">
|
|
|
|
|
+ <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-service-package">
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ color="primary"
|
|
|
|
|
+ dense
|
|
|
|
|
+ flat
|
|
|
|
|
+ icon="mdi-chevron-left"
|
|
|
|
|
+ round
|
|
|
|
|
+ @click="router.back()"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="col text-center font16 fontbold gradient-diarista text-primary"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ $t("service_package.title") }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div style="width: 36px"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="servicePackage.items.length === 0"
|
|
|
|
|
+ class="q-px-md q-pt-md"
|
|
|
|
|
+ >
|
|
|
|
|
+ <q-card
|
|
|
|
|
+ :flat="false"
|
|
|
|
|
+ class="empty-card bg-surface shadow-card card-border"
|
|
|
|
|
+ >
|
|
|
|
|
+ <q-card-section class="column items-center text-center q-pa-lg">
|
|
|
|
|
+ <q-icon
|
|
|
|
|
+ color="grey-5"
|
|
|
|
|
+ name="mdi-shopping-outline"
|
|
|
|
|
+ size="42px"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font14 fontbold text-text q-mt-sm">
|
|
|
|
|
+ {{ $t("service_package.empty_title") }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font12 text-grey-7 q-mt-xs">
|
|
|
|
|
+ {{ $t("service_package.empty_message") }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ class="q-mt-md"
|
|
|
|
|
+ color="primary"
|
|
|
|
|
+ no-caps
|
|
|
|
|
+ padding="8px 18px"
|
|
|
|
|
+ rounded
|
|
|
|
|
+ unelevated
|
|
|
|
|
+ :label="$t('service_package.view_favorites')"
|
|
|
|
|
+ @click="router.push({ name: 'ProfilePage' })"
|
|
|
|
|
+ />
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+ </q-card>
|
|
|
|
|
+
|
|
|
|
|
+ <q-card
|
|
|
|
|
+ v-if="servicePackage.lastCreatedGroup"
|
|
|
|
|
+ :flat="false"
|
|
|
|
|
+ class="created-card bg-surface shadow-card card-border q-mt-md"
|
|
|
|
|
+ >
|
|
|
|
|
+ <q-card-section class="q-pa-md">
|
|
|
|
|
+ <div class="row items-center no-wrap">
|
|
|
|
|
+ <q-icon
|
|
|
|
|
+ color="positive"
|
|
|
|
|
+ name="mdi-check-circle-outline"
|
|
|
|
|
+ size="24px"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <div class="column q-ml-sm">
|
|
|
|
|
+ <span class="font14 fontbold text-text">
|
|
|
|
|
+ {{ $t("service_package.last_request_title") }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font12 text-grey-7">
|
|
|
|
|
+ {{
|
|
|
|
|
+ $t("service_package.created_count", {
|
|
|
|
|
+ count: servicePackage.lastCreatedGroup.schedule_ids.length,
|
|
|
|
|
+ })
|
|
|
|
|
+ }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+ </q-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <div class="q-px-md q-pt-md column q-gutter-y-sm">
|
|
|
|
|
+ <div class="service-package-discount-banner row items-center no-wrap q-pa-sm">
|
|
|
|
|
+ <q-icon
|
|
|
|
|
+ color="positive"
|
|
|
|
|
+ name="mdi-sale-outline"
|
|
|
|
|
+ size="20px"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font12 fontbold text-positive q-ml-sm">
|
|
|
|
|
+ {{ servicePackageDiscountMessage }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <q-card
|
|
|
|
|
+ v-for="(item, index) in servicePackage.items"
|
|
|
|
|
+ :key="item.local_id"
|
|
|
|
|
+ :flat="false"
|
|
|
|
|
+ class="service-package-item-card bg-surface shadow-card card-border"
|
|
|
|
|
+ >
|
|
|
|
|
+ <q-card-section class="row no-wrap q-pa-md">
|
|
|
|
|
+ <q-avatar
|
|
|
|
|
+ color="indigo-1"
|
|
|
|
|
+ size="44px"
|
|
|
|
|
+ text-color="indigo-4"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img
|
|
|
|
|
+ v-if="getProfileMediaUrl(item)"
|
|
|
|
|
+ :src="getProfileMediaUrl(item)"
|
|
|
|
|
+ style="object-fit: cover"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <span v-else>
|
|
|
|
|
+ {{ getProviderInitial(item) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </q-avatar>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="column col q-ml-sm">
|
|
|
|
|
+ <span class="font14 fontbold text-text">
|
|
|
|
|
+ {{ item.provider_name }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font12 text-grey-7">
|
|
|
|
|
+ {{ formatDate(item.date) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font12 text-grey-7">
|
|
|
|
|
+ {{
|
|
|
|
|
+ $t("service_package.time_range", {
|
|
|
|
|
+ start: item.start_time,
|
|
|
|
|
+ end: item.end_time,
|
|
|
|
|
+ })
|
|
|
|
|
+ }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="row items-center q-gutter-x-sm q-mt-xs">
|
|
|
|
|
+ <q-chip
|
|
|
|
|
+ class="font10"
|
|
|
|
|
+ color="purple-1"
|
|
|
|
|
+ dense
|
|
|
|
|
+ square
|
|
|
|
|
+ text-color="primary"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{
|
|
|
|
|
+ $t("service_package.hours_short", { hours: item.period_type })
|
|
|
|
|
+ }}
|
|
|
|
|
+ </q-chip>
|
|
|
|
|
+
|
|
|
|
|
+ <q-chip
|
|
|
|
|
+ class="font10"
|
|
|
|
|
+ color="grey-2"
|
|
|
|
|
+ dense
|
|
|
|
|
+ square
|
|
|
|
|
+ text-color="grey-8"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{
|
|
|
|
|
+ item.offers_meal
|
|
|
|
|
+ ? $t("service_package.with_meal")
|
|
|
|
|
+ : $t("service_package.without_meal")
|
|
|
|
|
+ }}
|
|
|
|
|
+ </q-chip>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="item-prices q-mt-sm">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="!hasServicePackageDiscount"
|
|
|
|
|
+ class="row items-center no-wrap"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="font11 fontbold text-primary">{{
|
|
|
|
|
+ $t("service_package.pix_total")
|
|
|
|
|
+ }}</span>
|
|
|
|
|
+
|
|
|
|
|
+ <q-space />
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font12 fontbold text-primary">
|
|
|
|
|
+ {{ formatCurrencyValue(getStandardPixItemTotal(item)) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="!hasServicePackageDiscount"
|
|
|
|
|
+ class="row items-center no-wrap q-mt-xs"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="font11 text-grey-7">
|
|
|
|
|
+ {{ $t("service_package.credit_card_total") }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+ <q-space />
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font12 fontbold text-text">
|
|
|
|
|
+ {{ formatCurrencyValue(getCreditCardItemTotal(item)) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="hasServicePackageDiscount"
|
|
|
|
|
+ class="row items-center no-wrap q-mt-xs"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="font11 fontbold text-positive">
|
|
|
|
|
+ {{ $t("service_package.discount_total") }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+ <q-space />
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font12 fontbold text-positive">
|
|
|
|
|
+ {{ formatCurrencyValue(getServicePackageDiscountItemTotal(item)) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ color="negative"
|
|
|
|
|
+ dense
|
|
|
|
|
+ flat
|
|
|
|
|
+ icon="mdi-delete-outline"
|
|
|
|
|
+ round
|
|
|
|
|
+ @click="servicePackage.removeItem(index)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+ </q-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="q-px-md q-pt-md">
|
|
|
|
|
+ <q-card
|
|
|
|
|
+ :flat="false"
|
|
|
|
|
+ class="checkout-card bg-surface shadow-card card-border"
|
|
|
|
|
+ >
|
|
|
|
|
+ <q-card-section class="q-pa-md">
|
|
|
|
|
+ <div class="row items-center no-wrap q-mb-sm">
|
|
|
|
|
+ <span class="font14 fontbold text-primary">
|
|
|
|
|
+ {{ $t("service_package.summary") }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+ <q-space />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="summary-row row items-center no-wrap q-py-xs">
|
|
|
|
|
+ <span class="font12 text-grey-7">
|
|
|
|
|
+ {{ $t("service_package.provider") }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+ <q-space />
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font12 fontbold text-text text-right">
|
|
|
|
|
+ {{ providerName }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="summary-row row items-center no-wrap q-py-xs">
|
|
|
|
|
+ <span class="font12 text-grey-7">
|
|
|
|
|
+ {{ $t("service_package.address") }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+ <q-space />
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font12 fontbold text-text text-right">
|
|
|
|
|
+ {{ primaryAddressLabel }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="summary-row row items-center no-wrap q-py-xs">
|
|
|
|
|
+ <span class="font12 text-grey-7">
|
|
|
|
|
+ {{ $t("service_package.quantity") }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+ <q-space />
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font12 fontbold text-text">
|
|
|
|
|
+ {{
|
|
|
|
|
+ $t("service_package.schedule_count", {
|
|
|
|
|
+ count: servicePackage.items.length,
|
|
|
|
|
+ })
|
|
|
|
|
+ }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="!hasServicePackageDiscount"
|
|
|
|
|
+ class="summary-row row items-center no-wrap q-py-xs"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="font12 fontbold text-primary">
|
|
|
|
|
+ {{ $t("service_package.pix_total") }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+ <q-space />
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font14 fontbold text-primary">
|
|
|
|
|
+ {{ formatCurrencyValue(pixTotalAmount) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="hasServicePackageDiscount"
|
|
|
|
|
+ class="summary-row row items-center no-wrap q-py-xs"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="font12 fontbold text-positive">
|
|
|
|
|
+ {{ $t("service_package.discount_total") }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+ <q-space />
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font14 fontbold text-positive">
|
|
|
|
|
+ {{ formatCurrencyValue(servicePackageDiscountTotalAmount) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="pixDiscountAmount > 0"
|
|
|
|
|
+ class="summary-discount row items-center no-wrap q-py-xs q-px-sm q-mt-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ <q-icon
|
|
|
|
|
+ color="positive"
|
|
|
|
|
+ name="mdi-tag-outline"
|
|
|
|
|
+ size="18px"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <span class="font12 fontbold text-positive q-ml-xs">
|
|
|
|
|
+ {{
|
|
|
|
|
+ $t("service_package.discount_saved", {
|
|
|
|
|
+ value: formatPrice(pixDiscountAmount),
|
|
|
|
|
+ })
|
|
|
|
|
+ }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+
|
|
|
|
|
+ <q-card-actions class="q-px-md q-pb-md">
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ class="full-width"
|
|
|
|
|
+ color="primary"
|
|
|
|
|
+ no-caps
|
|
|
|
|
+ padding="10px 16px"
|
|
|
|
|
+ rounded
|
|
|
|
|
+ unelevated
|
|
|
|
|
+ :disable="addressLoading || !platformFeesReady || !hasSingleProvider"
|
|
|
|
|
+ :label="$t('service_package.close')"
|
|
|
|
|
+ :loading="submitting"
|
|
|
|
|
+ @click="submitServicePackage"
|
|
|
|
|
+ />
|
|
|
|
|
+ </q-card-actions>
|
|
|
|
|
+ </q-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </q-page>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { computed, onMounted, ref } from "vue";
|
|
|
|
|
+import { createScheduleServicePackage } from "src/api/servicePackage";
|
|
|
|
|
+import { getAddresses } from "src/api/address";
|
|
|
|
|
+import { getProfileMediaUrl } from "src/helpers/profileMedia";
|
|
|
|
|
+import { useI18n } from "vue-i18n";
|
|
|
|
|
+import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
|
|
|
|
|
+import { useQuasar } from "quasar";
|
|
|
|
|
+import { useRouter } from "vue-router";
|
|
|
|
|
+import { userStore } from "src/stores/user";
|
|
|
|
|
+import { useServicePackageStore } from "src/stores/servicePackage";
|
|
|
|
|
+
|
|
|
|
|
+const { t } = useI18n();
|
|
|
|
|
+const router = useRouter();
|
|
|
|
|
+const $q = useQuasar();
|
|
|
|
|
+const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
|
|
|
|
|
+
|
|
|
|
|
+const servicePackage = useServicePackageStore();
|
|
|
|
|
+const store = userStore();
|
|
|
|
|
+
|
|
|
|
|
+const addressLoading = ref(false);
|
|
|
|
|
+const primaryAddress = ref(null);
|
|
|
|
|
+const submitting = ref(false);
|
|
|
|
|
+
|
|
|
|
|
+const isValidFee = (fee) =>
|
|
|
|
|
+ fee !== null && fee !== undefined && Number.isFinite(Number(fee));
|
|
|
|
|
+
|
|
|
|
|
+const roundMoney = (value) => Number(Number(value ?? 0).toFixed(2));
|
|
|
|
|
+
|
|
|
|
|
+const roundNullableMoney = (value) => {
|
|
|
|
|
+ if (value === null || value === undefined || !Number.isFinite(Number(value)))
|
|
|
|
|
+ return null;
|
|
|
|
|
+
|
|
|
|
|
+ return roundMoney(value);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const sumItems = (callback) => {
|
|
|
|
|
+ let total = 0;
|
|
|
|
|
+
|
|
|
|
|
+ for (const item of servicePackage.items) {
|
|
|
|
|
+ const itemTotal = callback(item);
|
|
|
|
|
+
|
|
|
|
|
+ if (itemTotal === null) return null;
|
|
|
|
|
+
|
|
|
|
|
+ total += itemTotal;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return total;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const getTotalWithFee = (item, fee) => {
|
|
|
|
|
+ if (!isValidFee(fee)) return null;
|
|
|
|
|
+
|
|
|
|
|
+ return roundMoney(Number(item.base_amount ?? 0) * (1 + Number(fee)));
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const getServicePackageDiscountItemTotal = (item) =>
|
|
|
|
|
+ getTotalWithFee(item, platformFees.value.service_package_min_3_schedules);
|
|
|
|
|
+
|
|
|
|
|
+const getStandardPixItemTotal = (item) =>
|
|
|
|
|
+ getTotalWithFee(item, platformFees.value.pix);
|
|
|
|
|
+
|
|
|
|
|
+const servicePackageDiscountTotalAmount = computed(() =>
|
|
|
|
|
+ roundNullableMoney(sumItems(getServicePackageDiscountItemTotal)),
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+const hasServicePackageDiscount = computed(() => servicePackage.items.length >= 3);
|
|
|
|
|
+
|
|
|
|
|
+const missingSchedulesForDiscount = computed(() =>
|
|
|
|
|
+ Math.max(3 - servicePackage.items.length, 0),
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+const servicePackageDiscountMessage = computed(() => {
|
|
|
|
|
+ if (hasServicePackageDiscount.value) {
|
|
|
|
|
+ return t("service_package.discount_applied");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return t("service_package.discount_hint", {
|
|
|
|
|
+ count: missingSchedulesForDiscount.value,
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const platformFeesReady = computed(
|
|
|
|
|
+ () =>
|
|
|
|
|
+ isValidFee(platformFees.value.pix) &&
|
|
|
|
|
+ isValidFee(platformFees.value.credit_card) &&
|
|
|
|
|
+ (!hasServicePackageDiscount.value ||
|
|
|
|
|
+ isValidFee(platformFees.value.service_package_min_3_schedules)),
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+const hasSingleProvider = computed(() => {
|
|
|
|
|
+ const providers = new Set(
|
|
|
|
|
+ servicePackage.items.map((item) => item.provider_id).filter(Boolean),
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ return providers.size <= 1;
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const pixDiscountAmount = computed(() => {
|
|
|
|
|
+ if (!hasServicePackageDiscount.value) return 0;
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ roundNullableMoney(
|
|
|
|
|
+ sumItems((item) => {
|
|
|
|
|
+ const standardPixItemTotal = getStandardPixItemTotal(item);
|
|
|
|
|
+ const discountedPixItemTotal = getServicePackageDiscountItemTotal(item);
|
|
|
|
|
+
|
|
|
|
|
+ if (standardPixItemTotal === null || discountedPixItemTotal === null)
|
|
|
|
|
+ return null;
|
|
|
|
|
+
|
|
|
|
|
+ return standardPixItemTotal - discountedPixItemTotal;
|
|
|
|
|
+ }),
|
|
|
|
|
+ ) ?? 0
|
|
|
|
|
+ );
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const pixTotalAmount = computed(() =>
|
|
|
|
|
+ roundNullableMoney(sumItems(getStandardPixItemTotal)),
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+const primaryAddressLabel = computed(() => {
|
|
|
|
|
+ if (addressLoading.value) return t("service_package.loading");
|
|
|
|
|
+ if (!primaryAddress.value) return t("service_package.no_primary_address");
|
|
|
|
|
+
|
|
|
|
|
+ return [
|
|
|
|
|
+ primaryAddress.value.address,
|
|
|
|
|
+ primaryAddress.value.number,
|
|
|
|
|
+ primaryAddress.value.district,
|
|
|
|
|
+ ]
|
|
|
|
|
+ .filter(Boolean)
|
|
|
|
|
+ .join(", ");
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const providerName = computed(() => {
|
|
|
|
|
+ const providerNames = [
|
|
|
|
|
+ ...new Set(servicePackage.items.map((item) => item.provider_name).filter(Boolean)),
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ if (providerNames.length === 0) return t("service_package.unknown");
|
|
|
|
|
+ if (providerNames.length === 1) return providerNames[0];
|
|
|
|
|
+
|
|
|
|
|
+ return t("service_package.multiple_providers", { count: providerNames.length });
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const normalizeDate = (value) =>
|
|
|
|
|
+ String(value ?? "")
|
|
|
|
|
+ .replace(/\//g, "-")
|
|
|
|
|
+ .slice(0, 10);
|
|
|
|
|
+
|
|
|
|
|
+const formatDate = (value) => {
|
|
|
|
|
+ const parsed = new Date(`${normalizeDate(value)}T12:00:00`);
|
|
|
|
|
+
|
|
|
|
|
+ return new Intl.DateTimeFormat("pt-BR", {
|
|
|
|
|
+ day: "2-digit",
|
|
|
|
|
+ month: "2-digit",
|
|
|
|
|
+ year: "numeric",
|
|
|
|
|
+ }).format(parsed);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const formatPrice = (value) => {
|
|
|
|
|
+ if (
|
|
|
|
|
+ value === null ||
|
|
|
|
|
+ value === undefined ||
|
|
|
|
|
+ !Number.isFinite(Number(value))
|
|
|
|
|
+ ) {
|
|
|
|
|
+ return t("service_package.loading");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return Number(value ?? 0).toLocaleString("pt-BR", {
|
|
|
|
|
+ minimumFractionDigits: 2,
|
|
|
|
|
+ maximumFractionDigits: 2,
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const formatCurrencyValue = (value) => {
|
|
|
|
|
+ if (
|
|
|
|
|
+ value === null ||
|
|
|
|
|
+ value === undefined ||
|
|
|
|
|
+ !Number.isFinite(Number(value))
|
|
|
|
|
+ ) {
|
|
|
|
|
+ return t("service_package.loading");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return t("service_package.currency_value", { value: formatPrice(value) });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const getCreditCardItemTotal = (item) =>
|
|
|
|
|
+ getTotalWithFee(item, platformFees.value.credit_card);
|
|
|
|
|
+
|
|
|
|
|
+const getProviderInitial = (item) =>
|
|
|
|
|
+ item.provider_name?.slice(0, 1).toUpperCase() ??
|
|
|
|
|
+ t("service_package.provider_initial");
|
|
|
|
|
+
|
|
|
|
|
+const loadPrimaryAddress = async () => {
|
|
|
|
|
+ const clientId = store.user?.client_id ?? store.user?.client?.id;
|
|
|
|
|
+
|
|
|
|
|
+ if (!clientId) return;
|
|
|
|
|
+
|
|
|
|
|
+ addressLoading.value = true;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const addresses = await getAddresses("client", clientId);
|
|
|
|
|
+
|
|
|
|
|
+ primaryAddress.value =
|
|
|
|
|
+ (addresses ?? []).find((address) => address.is_primary) ?? null;
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ primaryAddress.value = null;
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ addressLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const submitServicePackage = async () => {
|
|
|
|
|
+ if (!servicePackage.items.length || submitting.value) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (!hasSingleProvider.value) {
|
|
|
|
|
+ $q.notify({
|
|
|
|
|
+ type: "warning",
|
|
|
|
|
+ message: t("service_package.same_provider_required"),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!primaryAddress.value) {
|
|
|
|
|
+ $q.notify({
|
|
|
|
|
+ type: "warning",
|
|
|
|
|
+ message: t("service_package.no_primary_address_notify"),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ client_id: store.user.client_id ?? store.user?.client?.id,
|
|
|
|
|
+ address_id: primaryAddress.value.id,
|
|
|
|
|
+ schedule_type: "default",
|
|
|
|
|
+
|
|
|
|
|
+ schedules: servicePackage.items.map((item) => ({
|
|
|
|
|
+ provider_id: item.provider_id,
|
|
|
|
|
+ date: item.date,
|
|
|
|
|
+ period_type: item.period_type,
|
|
|
|
|
+ start_time: item.start_time,
|
|
|
|
|
+ end_time: item.end_time,
|
|
|
|
|
+ total_amount: item.base_amount,
|
|
|
|
|
+ offers_meal: item.offers_meal,
|
|
|
|
|
+ })),
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ submitting.value = true;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const createdServicePackage = await createScheduleServicePackage(payload);
|
|
|
|
|
+
|
|
|
|
|
+ const createdSchedules = createdServicePackage?.schedules ?? [];
|
|
|
|
|
+
|
|
|
|
|
+ const scheduleIds =
|
|
|
|
|
+ createdServicePackage?.schedule_ids ??
|
|
|
|
|
+ createdSchedules.map((schedule) => schedule.id);
|
|
|
|
|
+
|
|
|
|
|
+ servicePackage.setLastCreatedGroup({
|
|
|
|
|
+ local_group_id: createdServicePackage?.id ?? `sp-${Date.now()}`,
|
|
|
|
|
+ service_package_id: createdServicePackage?.id ?? null,
|
|
|
|
|
+ schedule_ids: scheduleIds,
|
|
|
|
|
+ schedules: createdSchedules,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ servicePackage.clear();
|
|
|
|
|
+
|
|
|
|
|
+ $q.notify({
|
|
|
|
|
+ type: "positive",
|
|
|
|
|
+ message: t("service_package.closed_success"),
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const message =
|
|
|
|
|
+ error?.response?.data?.message ??
|
|
|
|
|
+ error?.message ??
|
|
|
|
|
+ t("service_package.submit_error");
|
|
|
|
|
+
|
|
|
|
|
+ $q.notify({ type: "negative", message });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ submitting.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ loadPrimaryAddress();
|
|
|
|
|
+ loadPlatformFees().catch(() => {});
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.service-package-page {
|
|
|
|
|
+ min-height: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.shadow-service-package {
|
|
|
|
|
+ box-shadow: 0 1px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.service-package-item-card,
|
|
|
|
|
+.checkout-card,
|
|
|
|
|
+.empty-card,
|
|
|
|
|
+.created-card {
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.summary-row {
|
|
|
|
|
+ border-top: 1px solid rgba(0, 0, 0, 0.05);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.item-prices {
|
|
|
|
|
+ max-width: 220px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.service-package-discount-banner {
|
|
|
|
|
+ background: rgba(34, 197, 94, 0.1);
|
|
|
|
|
+ border: 1px solid rgba(34, 197, 94, 0.22);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.summary-discount {
|
|
|
|
|
+ background: rgba(34, 197, 94, 0.12);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|