SchedulePaymentDialog.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. <template>
  2. <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down" @hide="onDialogHide">
  3. <div class="bg-page full-height column">
  4. <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
  5. <q-btn icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
  6. <q-space />
  7. <span class="font16 fontbold gradient-diarista">
  8. {{ $t('payment.title') }}
  9. </span>
  10. <q-space />
  11. <div style="width: 32px" />
  12. </div>
  13. <div class="col overflow-auto q-px-md q-pt-lg q-pb-xl">
  14. <div class="q-mb-sm text-text font14 fontbold">{{ $t('payment.schedule_address') }}</div>
  15. <div class="address-box row items-center no-wrap q-mb-lg">
  16. <div class="col">
  17. <div class="address-type-label fontbold">{{ addressTypeLabel }}</div>
  18. <div class="address-full-text text-grey-7">{{ addressFullText }}</div>
  19. </div>
  20. <q-icon name="mdi-chevron-down" color="grey-5" size="22px" />
  21. </div>
  22. <div class="text-text q-mb-sm font14 fontbold">{{ $t('payment.pay_with') }}</div>
  23. <div class="row q-gutter-sm q-mb-sm">
  24. <div
  25. class="payment-option-card col column items-center justify-center q-pa-md cursor-pointer"
  26. :class="{ 'payment-option-selected': selectedMethod === 'pix' }"
  27. @click="selectedMethod = 'pix'"
  28. >
  29. <span class="payment-option-title">{{ $t('payment.pix') }}</span>
  30. <q-icon name="mdi-cash-fast" size="32px" color="teal" class="q-mt-xs" />
  31. </div>
  32. <div
  33. class="payment-option-card col column items-center justify-center q-pa-md cursor-pointer"
  34. :class="{ 'payment-option-selected': selectedMethod === 'new_card' }"
  35. @click="openAddCard"
  36. >
  37. <span class="payment-option-title">{{ $t('payment.add_card') }}</span>
  38. <q-icon name="mdi-plus-circle-outline" size="22px" color="grey-5" class="q-mt-xs" />
  39. <span class="payment-option-sub">{{ $t('payment.credit_debit') }}</span>
  40. </div>
  41. </div>
  42. <div v-if="loadingCards" class="flex flex-center q-py-md">
  43. <q-spinner color="primary" size="2em" />
  44. </div>
  45. <div v-else-if="paymentMethods.length > 0" class="column q-gutter-y-sm q-mb-lg">
  46. <div
  47. v-for="card in paymentMethods"
  48. :key="card.id"
  49. class="saved-card-box row items-center no-wrap q-pa-md cursor-pointer"
  50. :class="{ 'payment-option-selected': selectedMethod === `card_${card.id}` }"
  51. @click="selectedMethod = `card_${card.id}`"
  52. >
  53. <div class="col column">
  54. <span class="card-titular-label">{{ $t('payment.card_holder') }}</span>
  55. <span class="card-holder-name text-text">{{ card.holder_name }}</span>
  56. </div>
  57. <div class="column items-end">
  58. <span class="card-brand-text">{{ brandDisplay(card.brand) }}</span>
  59. <span class="card-last-four">{{ '**** **** **** ' + card.last_four_digits }}</span>
  60. <span class="card-expiry-text">{{ card.expiration }}</span>
  61. </div>
  62. </div>
  63. </div>
  64. <q-separator class="q-my-lg" />
  65. <div class="payment-summary q-mb-lg">
  66. <div class="row items-center justify-between q-mb-xs">
  67. <span class="summary-label">{{ $t('dashboard_client.pending_schedules.detail_value') }}</span>
  68. <span class="summary-value">{{ formatCurrency(baseAmount) }}</span>
  69. </div>
  70. <div class="row items-center justify-between q-mb-xs">
  71. <span class="summary-label">{{ $t('dashboard_client.pending_schedules.detail_service_fee') }}</span>
  72. <span class="summary-value">{{ formatCurrency(selectedPlatformFee) }}</span>
  73. </div>
  74. <q-separator class="q-my-sm" />
  75. <div class="row items-center justify-between">
  76. <span class="summary-total-label">{{ $t('dashboard_client.pending_schedules.detail_total') }}</span>
  77. <span class="summary-total-value">{{ formatCurrency(selectedTotal) }}</span>
  78. </div>
  79. </div>
  80. <div class="row items-center q-mb-lg">
  81. <q-checkbox v-model="agreedToTerms" color="primary" keep-color />
  82. <span class="terms-text">
  83. {{ $t('payment.agree_prefix') }}
  84. <span class="text-primary cursor-pointer text-underline">{{ $t('payment.terms_link') }}</span>
  85. </span>
  86. </div>
  87. <q-btn
  88. unelevated
  89. rounded
  90. no-caps
  91. padding="8px 12px"
  92. color="primary"
  93. class="full-width"
  94. :label="$t('payment.confirm_btn')"
  95. :disable="!canConfirm"
  96. @click="onConfirm"
  97. />
  98. </div>
  99. </div>
  100. </q-dialog>
  101. </template>
  102. <script setup>
  103. import { ref, computed, onMounted } from 'vue'
  104. import { useDialogPluginComponent, useQuasar } from 'quasar'
  105. import { useI18n } from 'vue-i18n'
  106. import { userStore } from 'src/stores/user'
  107. import { usePaymentStore } from 'src/stores/payment'
  108. import { getClientPaymentMethods } from 'src/api/clientPaymentMethod'
  109. import { formatCurrency } from 'src/helpers/utils'
  110. import { getSchedulePlatformFeeRate } from 'src/helpers/paymentPlatformFees'
  111. import { usePaymentPlatformFees } from 'src/composables/usePaymentPlatformFees'
  112. import ProfilePaymentAddDialog from 'src/components/profile/ProfilePaymentAddDialog.vue'
  113. import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
  114. import SchedulePaymentProcessingDialog from './SchedulePaymentProcessingDialog.vue'
  115. const props = defineProps({
  116. schedule: {
  117. type: Object,
  118. required: true,
  119. },
  120. })
  121. defineEmits([...useDialogPluginComponent.emits])
  122. const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
  123. const $q = useQuasar()
  124. const { t } = useI18n()
  125. const store = userStore()
  126. const paymentStore = usePaymentStore()
  127. const { platformFees, loadPlatformFees } = usePaymentPlatformFees()
  128. const selectedMethod = ref(null)
  129. const agreedToTerms = ref(false)
  130. const paymentMethods = ref([])
  131. const loadingCards = ref(false)
  132. const baseAmount = computed(() => Number(props.schedule.total_amount) || 0)
  133. const selectedPaymentType = computed(() => selectedMethod.value === 'pix' ? 'pix' : 'credit_card')
  134. const selectedPlatformFeeRate = computed(() => getSchedulePlatformFeeRate(props.schedule, selectedPaymentType.value, platformFees.value) ?? platformFees.value.pix)
  135. const selectedPlatformFee = computed(() => parseFloat((baseAmount.value * selectedPlatformFeeRate.value).toFixed(2)))
  136. const selectedTotal = computed(() => parseFloat((baseAmount.value + selectedPlatformFee.value).toFixed(2)))
  137. const addressTypeLabel = computed(() => {
  138. const type = props.schedule.address?.address_type
  139. if (!type) return ''
  140. return t(`profile.address.type.${type}`, type)
  141. })
  142. const addressFullText = computed(() => {
  143. const a = props.schedule.address
  144. if (!a) return ''
  145. const parts = [a.address, a.number, a.district].filter(Boolean)
  146. return parts.join(', ')
  147. })
  148. const canConfirm = computed(() => selectedMethod.value !== null && agreedToTerms.value)
  149. const brandDisplay = (brand) => {
  150. if (!brand) return ''
  151. const map = { visa: 'VISA', mastercard: 'Mastercard', elo: 'Elo', hipercard: 'Hipercard', diners: 'Diners', discover: 'Discover' }
  152. return map[brand] ?? brand.toUpperCase()
  153. }
  154. const loadCards = async () => {
  155. loadingCards.value = true
  156. try {
  157. paymentMethods.value = await getClientPaymentMethods(store.user?.client_id)
  158. const selectedCardId = String(selectedMethod.value || '').replace('card_', '')
  159. const hasSelectedCard = paymentMethods.value.some((card) => String(card.id) === selectedCardId)
  160. if (selectedMethod.value !== 'pix' && !hasSelectedCard) {
  161. selectedMethod.value = paymentMethods.value.length > 0 ? `card_${paymentMethods.value[0].id}` : 'pix'
  162. }
  163. } catch (e) {
  164. console.error(e)
  165. } finally {
  166. loadingCards.value = false
  167. }
  168. }
  169. const openAddCard = () => {
  170. $q.dialog({
  171. component: ProfilePaymentAddDialog,
  172. componentProps: { clientId: store.user?.client_id },
  173. }).onOk(() => {
  174. loadCards()
  175. })
  176. }
  177. const openPixPayment = () => {
  178. $q.dialog({
  179. component: SchedulePaymentPixDialog,
  180. componentProps: { schedule: props.schedule, total: selectedTotal.value },
  181. }).onOk(() => {
  182. onDialogOK()
  183. })
  184. }
  185. const onConfirm = () => {
  186. if (selectedMethod.value === 'pix') {
  187. openPixPayment()
  188. return
  189. }
  190. const clientPaymentMethodId = Number(String(selectedMethod.value).replace('card_', ''))
  191. $q.dialog({
  192. component: SchedulePaymentProcessingDialog,
  193. componentProps: { schedule: props.schedule, clientPaymentMethodId, total: selectedTotal.value },
  194. }).onOk(() => {
  195. onDialogOK()
  196. })
  197. }
  198. onMounted(() => {
  199. loadPlatformFees().catch(() => {})
  200. if (paymentStore.getValidPixPayment(props.schedule.id)) {
  201. openPixPayment()
  202. return
  203. }
  204. loadCards()
  205. })
  206. </script>
  207. <style scoped lang="scss">
  208. .shadow-header {
  209. box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.1);
  210. }
  211. .address-box {
  212. border: 1px solid #e0e0e0;
  213. border-radius: 10px;
  214. padding: 12px 16px;
  215. background: #fff;
  216. }
  217. .address-type-label {
  218. color: #3a3a4a;
  219. }
  220. .address-full-text {
  221. line-height: 1.4;
  222. margin-top: 2px;
  223. }
  224. .payment-option-card {
  225. border: 1.5px solid #e0e0e0;
  226. border-radius: 12px;
  227. background: #fff;
  228. min-height: 90px;
  229. text-align: center;
  230. transition: border-color 0.2s;
  231. }
  232. .payment-option-selected {
  233. border-color: #22c55e !important;
  234. box-shadow: 0 0 0 1px #22c55e;
  235. }
  236. .payment-option-title {
  237. color: #3a3a4a;
  238. }
  239. .payment-option-sub {
  240. color: #9a9aaa;
  241. margin-top: 2px;
  242. }
  243. .payment-summary {
  244. border: 1px solid #e0e0e0;
  245. border-radius: 10px;
  246. background: #fff;
  247. padding: 12px 16px;
  248. }
  249. .summary-label,
  250. .summary-value {
  251. color: #5a5a6a;
  252. }
  253. .summary-total-label,
  254. .summary-total-value {
  255. color: #3a3a4a;
  256. font-weight: 700;
  257. }
  258. .saved-card-box {
  259. border: 1.5px solid #e0e0e0;
  260. border-radius: 12px;
  261. background: #fff;
  262. transition: border-color 0.2s;
  263. }
  264. .card-titular-label {
  265. color: #9a9aaa;
  266. }
  267. .card-holder-name {
  268. color: #3a3a4a;
  269. }
  270. .card-brand-text {
  271. color: #3a3a4a;
  272. text-align: right;
  273. }
  274. .card-last-four {
  275. color: #6a6a7a;
  276. letter-spacing: 1px;
  277. }
  278. .card-expiry-text {
  279. color: #9a9aaa;
  280. }
  281. .terms-text {
  282. color: #5a5a6a;
  283. line-height: 1.4;
  284. }
  285. .text-underline {
  286. text-decoration: underline;
  287. }
  288. </style>