ScheduleAcceptedDialog.vue 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. <template>
  2. <q-dialog
  3. ref="dialogRef"
  4. @hide="onDialogHide"
  5. >
  6. <q-card
  7. class="accepted-dialog-card bg-surface"
  8. :flat="false"
  9. >
  10. <q-card-section class="column items-center q-pt-lg q-pb-sm">
  11. <q-avatar
  12. class="text-h5 q-mb-sm"
  13. size="80px"
  14. :style="avatarStyle"
  15. >
  16. <img
  17. v-if="getProfileMediaUrl(item)"
  18. :src="getProfileMediaUrl(item)"
  19. style="object-fit: cover"
  20. />
  21. <span v-else>
  22. {{ displayProviderName.slice(0, 2).toUpperCase() || "??" }}
  23. </span>
  24. </q-avatar>
  25. <div class="font16 fontbold provider-name">
  26. {{ getFirstName(displayProviderName) || "—" }}
  27. </div>
  28. <div class="font14 fontmedium text-text">
  29. {{ displayDistrict }}
  30. </div>
  31. </q-card-section>
  32. <q-card-section class="text-center q-pt-xs q-pb-md">
  33. <div class="accepted-title font16 fontbold">
  34. {{ $t("dashboard_client.pending_schedules.accepted_title") }}
  35. </div>
  36. </q-card-section>
  37. <q-separator />
  38. <q-card-section class="q-py-md q-px-lg">
  39. <div
  40. v-if="activeSchedules.length > 1"
  41. class="font13 fontbold text-primary q-mb-xs"
  42. >
  43. {{ $t("dashboard_client.pending_schedules.detail_schedules_title") }}
  44. </div>
  45. <div
  46. v-for="schedule in activeSchedules"
  47. :key="schedule.id"
  48. class="detail-row schedule-row"
  49. >
  50. <span class="detail-value font13">
  51. {{ formatScheduleDate(schedule) }}
  52. </span>
  53. <span class="detail-valued text-text font13">
  54. {{ schedule.start_time?.slice(0, 5) }}
  55. {{ $t("dashboard_client.next_schedules.to") }}
  56. {{ schedule.end_time?.slice(0, 5) }}
  57. </span>
  58. </div>
  59. <q-separator class="q-my-sm" />
  60. <div class="detail-row">
  61. <span class="text-primary font14 fontmedium">
  62. {{ $t("dashboard_client.pending_schedules.detail_value") }}
  63. </span>
  64. <span class="detail-value font14">
  65. {{ formatCurrency(baseAmount) }}
  66. </span>
  67. </div>
  68. <div class="detail-row">
  69. <span class="text-primary font13 fontmedium">
  70. {{ $t("dashboard_client.pending_schedules.detail_service_fee_pix") }}
  71. </span>
  72. <span class="detail-value font13">
  73. {{ formatCurrency(platformFee("pix")) }}
  74. </span>
  75. </div>
  76. <div class="detail-row">
  77. <span class="text-primary font14 fontmedium">
  78. {{ hasServicePackageDiscount
  79. ? $t("dashboard_client.pending_schedules.detail_package_total")
  80. : $t("dashboard_client.pending_schedules.detail_pix_total") }}
  81. </span>
  82. <span class="total-value font14 fontbold">
  83. {{ formatCurrency(pixTotal) }}
  84. </span>
  85. </div>
  86. <div
  87. v-if="pixDiscount > 0"
  88. class="detail-row discount-row"
  89. >
  90. <span class="font13 fontmedium">
  91. {{ $t("dashboard_client.pending_schedules.detail_pix_discount") }}
  92. </span>
  93. <span class="font13 fontbold">
  94. {{ formatCurrency(pixDiscount) }}
  95. </span>
  96. </div>
  97. <div class="detail-row">
  98. <span class="text-primary font13 fontmedium">
  99. {{
  100. $t(
  101. "dashboard_client.pending_schedules.detail_service_fee_credit_card",
  102. )
  103. }}
  104. </span>
  105. <span class="detail-value font13">
  106. {{ formatCurrency(platformFee("credit_card")) }}
  107. </span>
  108. </div>
  109. <div class="detail-row">
  110. <span class="text-primary font14 fontmedium">
  111. {{
  112. $t("dashboard_client.pending_schedules.detail_credit_card_total")
  113. }}
  114. </span>
  115. <span class="total-value font14 fontbold">
  116. {{ formatCurrency(creditCardTotal) }}
  117. </span>
  118. </div>
  119. </q-card-section>
  120. <q-card-section class="q-pt-none q-pb-lg q-px-lg column q-gutter-y-sm">
  121. <q-btn
  122. class="payment-btn full-width"
  123. color="primary"
  124. padding="4px 12px"
  125. rounded
  126. :label="$t('dashboard_client.pending_schedules.btn_payment')"
  127. @click="onGoToPayment"
  128. />
  129. <q-btn
  130. class="full-width"
  131. color="grey-6"
  132. flat
  133. no-caps
  134. padding="4px 12px"
  135. :label="$t('dashboard_client.pending_schedules.btn_cancel')"
  136. @click="onDialogHide"
  137. />
  138. </q-card-section>
  139. </q-card>
  140. </q-dialog>
  141. </template>
  142. <script setup>
  143. import { avatarColors } from "src/helpers/avatarColors";
  144. import { computed, onMounted } from "vue";
  145. import { formatCurrency, getFirstName } from "src/helpers/utils";
  146. import { getProfileMediaUrl } from "src/helpers/profileMedia";
  147. import {
  148. getSchedulePlatformFeeRate,
  149. scheduleUsesServicePackageDiscount,
  150. } from "src/helpers/paymentPlatformFees";
  151. import { useDialogPluginComponent, useQuasar } from "quasar";
  152. import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
  153. import { usePaymentStore } from "src/stores/payment";
  154. import SchedulePaymentDialog from "./SchedulePaymentDialog.vue";
  155. import SchedulePaymentPixDialog from "./SchedulePaymentPixDialog.vue";
  156. const props = defineProps({
  157. servicePackage: { type: Object, required: true },
  158. });
  159. defineEmits([...useDialogPluginComponent.emits]);
  160. const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
  161. const $q = useQuasar();
  162. const paymentStore = usePaymentStore();
  163. const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
  164. const item = computed(() => props.servicePackage);
  165. const displayProviderName = computed(() => {
  166. return item.value.provider?.user?.name ?? item.value.provider_name ?? '';
  167. });
  168. const activeSchedules = computed(() =>
  169. (item.value.schedules ?? []).filter(
  170. (schedule) => !['cancelled', 'rejected'].includes(schedule.status),
  171. ),
  172. );
  173. const displayDistrict = computed(() => {
  174. return activeSchedules.value[0]?.address?.district ?? item.value.address?.district ?? '';
  175. });
  176. const formatScheduleDate = (schedule) => {
  177. if (!schedule) return '';
  178. if (schedule.formatted_date) return schedule.formatted_date;
  179. const raw = String(schedule.date || '');
  180. const m = raw.match(/^(\d{4})-(\d{2})-(\d{2})/);
  181. if (!m) return raw;
  182. const d = new Date(+m[1], +m[2] - 1, +m[3]);
  183. return d.toLocaleDateString('pt-BR', {
  184. day: '2-digit',
  185. month: 'long',
  186. year: 'numeric',
  187. });
  188. };
  189. const avatarStyle = computed(
  190. () => avatarColors[item.value.id % avatarColors.length],
  191. );
  192. const baseAmount = computed(() => {
  193. const total = Number(item.value.total_amount);
  194. if (Number.isFinite(total) && total > 0) return total;
  195. return activeSchedules.value.reduce(
  196. (sum, schedule) => sum + (Number(schedule.total_amount) || 0),
  197. 0,
  198. );
  199. });
  200. const creditCardTotal = computed(() =>
  201. parseFloat((baseAmount.value + platformFee("credit_card")).toFixed(2)),
  202. );
  203. const hasServicePackageDiscount = computed(() =>
  204. scheduleUsesServicePackageDiscount(item.value),
  205. );
  206. const pixDiscount = computed(() =>
  207. Math.max(0, parseFloat((creditCardTotal.value - pixTotal.value).toFixed(2))),
  208. );
  209. const pixTotal = computed(() =>
  210. parseFloat((baseAmount.value + platformFee("pix")).toFixed(2)),
  211. );
  212. const platformFee = (paymentType) => {
  213. const feeRate = getSchedulePlatformFeeRate(
  214. item.value,
  215. paymentType,
  216. platformFees.value,
  217. );
  218. return parseFloat((baseAmount.value * (feeRate ?? 0)).toFixed(2));
  219. };
  220. const onGoToPayment = () => {
  221. const validPixPayment = paymentStore.getValidPixPaymentForServicePackage(item.value.id);
  222. const hasValidPixPayment = !!validPixPayment;
  223. $q.dialog({
  224. component: hasValidPixPayment
  225. ? SchedulePaymentPixDialog
  226. : SchedulePaymentDialog,
  227. componentProps: {
  228. servicePackage: item.value,
  229. ...(hasValidPixPayment ? { total: pixTotal.value } : {}),
  230. },
  231. }).onOk(() => {
  232. onDialogOK();
  233. });
  234. };
  235. onMounted(() => {
  236. loadPlatformFees().catch(() => {});
  237. });
  238. </script>
  239. <style scoped lang="scss">
  240. .accepted-dialog-card {
  241. width: 320px;
  242. max-width: 92vw;
  243. border-radius: 20px !important;
  244. overflow: hidden;
  245. }
  246. .accepted-title {
  247. line-height: 1.3;
  248. color: #8b5cf6;
  249. }
  250. .provider-name {
  251. color: #8b5cf6;
  252. }
  253. .detail-row {
  254. display: flex;
  255. justify-content: space-between;
  256. align-items: center;
  257. padding: 4px 0;
  258. }
  259. .detail-label {
  260. color: #8a8a9a;
  261. }
  262. .detail-value {
  263. color: #3a3a4a;
  264. }
  265. .total-value {
  266. color: #3a3a4a;
  267. }
  268. .discount-row {
  269. color: #16845d;
  270. background: rgba(22, 132, 93, 0.08);
  271. border-radius: 8px;
  272. margin: 2px 0;
  273. padding: 5px 8px;
  274. }
  275. .payment-btn {
  276. height: 48px;
  277. }
  278. </style>