SchedulePaymentDialog.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  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="text-subtitle1 text-weight-bold text-primary">
  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="section-label q-mb-sm">{{ $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">{{ 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="section-label q-mb-sm">{{ $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">{{ 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="row items-center q-mb-lg">
  66. <q-checkbox v-model="agreedToTerms" color="primary" keep-color />
  67. <span class="terms-text">
  68. {{ $t('payment.agree_prefix') }}
  69. <span class="text-primary text-weight-bold cursor-pointer text-underline">{{ $t('payment.terms_link') }}</span>
  70. </span>
  71. </div>
  72. <q-btn
  73. unelevated
  74. rounded
  75. no-caps
  76. color="primary"
  77. class="full-width confirm-btn"
  78. :label="$t('payment.confirm_btn')"
  79. :disable="!canConfirm"
  80. @click="onConfirm"
  81. />
  82. </div>
  83. </div>
  84. </q-dialog>
  85. </template>
  86. <script setup>
  87. import { ref, computed, onMounted } from 'vue'
  88. import { useDialogPluginComponent, useQuasar } from 'quasar'
  89. import { useI18n } from 'vue-i18n'
  90. import { userStore } from 'src/stores/user'
  91. import { getClientPaymentMethods } from 'src/api/clientPaymentMethod'
  92. import ProfilePaymentAddDialog from 'src/components/profile/ProfilePaymentAddDialog.vue'
  93. import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
  94. import SchedulePaymentProcessingDialog from './SchedulePaymentProcessingDialog.vue'
  95. const props = defineProps({
  96. schedule: {
  97. type: Object,
  98. required: true,
  99. },
  100. total: {
  101. type: Number,
  102. required: true,
  103. },
  104. })
  105. defineEmits([...useDialogPluginComponent.emits])
  106. const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
  107. const $q = useQuasar()
  108. const { t } = useI18n()
  109. const store = userStore()
  110. const selectedMethod = ref(null)
  111. const agreedToTerms = ref(false)
  112. const paymentMethods = ref([])
  113. const loadingCards = ref(false)
  114. const addressTypeLabel = computed(() => {
  115. const type = props.schedule.address?.address_type
  116. if (!type) return ''
  117. return t(`profile.address.type.${type}`, type)
  118. })
  119. const addressFullText = computed(() => {
  120. const a = props.schedule.address
  121. if (!a) return ''
  122. const parts = [a.address, a.number, a.district].filter(Boolean)
  123. return parts.join(', ')
  124. })
  125. const canConfirm = computed(() => selectedMethod.value !== null && agreedToTerms.value)
  126. const brandDisplay = (brand) => {
  127. if (!brand) return ''
  128. const map = { visa: 'VISA', mastercard: 'Mastercard', elo: 'Elo', hipercard: 'Hipercard', diners: 'Diners', discover: 'Discover' }
  129. return map[brand] ?? brand.toUpperCase()
  130. }
  131. const loadCards = async () => {
  132. loadingCards.value = true
  133. try {
  134. paymentMethods.value = await getClientPaymentMethods(store.user?.client_id)
  135. } catch (e) {
  136. console.error(e)
  137. } finally {
  138. loadingCards.value = false
  139. }
  140. }
  141. const openAddCard = () => {
  142. $q.dialog({
  143. component: ProfilePaymentAddDialog,
  144. componentProps: { clientId: store.user?.client_id },
  145. }).onOk(() => {
  146. loadCards()
  147. })
  148. }
  149. const onConfirm = () => {
  150. if (selectedMethod.value === 'pix') {
  151. $q.dialog({
  152. component: SchedulePaymentPixDialog,
  153. componentProps: { schedule: props.schedule, total: props.total },
  154. }).onOk(() => {
  155. onDialogOK()
  156. })
  157. } else {
  158. $q.dialog({
  159. component: SchedulePaymentProcessingDialog,
  160. componentProps: { schedule: props.schedule },
  161. }).onOk(() => {
  162. onDialogOK()
  163. })
  164. }
  165. }
  166. onMounted(() => {
  167. loadCards()
  168. })
  169. </script>
  170. <style scoped lang="scss">
  171. .shadow-header {
  172. box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.1);
  173. }
  174. .section-label {
  175. font-size: 15px;
  176. font-weight: 700;
  177. color: #3a3a4a;
  178. }
  179. .address-box {
  180. border: 1px solid #e0e0e0;
  181. border-radius: 10px;
  182. padding: 12px 16px;
  183. background: #fff;
  184. }
  185. .address-type-label {
  186. font-size: 14px;
  187. font-weight: 700;
  188. color: #3a3a4a;
  189. }
  190. .address-full-text {
  191. font-size: 12px;
  192. line-height: 1.4;
  193. margin-top: 2px;
  194. }
  195. .payment-option-card {
  196. border: 1.5px solid #e0e0e0;
  197. border-radius: 12px;
  198. background: #fff;
  199. min-height: 90px;
  200. text-align: center;
  201. transition: border-color 0.2s;
  202. }
  203. .payment-option-selected {
  204. border-color: #22c55e !important;
  205. box-shadow: 0 0 0 1px #22c55e;
  206. }
  207. .payment-option-title {
  208. font-size: 15px;
  209. font-weight: 700;
  210. color: #3a3a4a;
  211. }
  212. .payment-option-sub {
  213. font-size: 11px;
  214. color: #9a9aaa;
  215. margin-top: 2px;
  216. }
  217. .saved-card-box {
  218. border: 1.5px solid #e0e0e0;
  219. border-radius: 12px;
  220. background: #fff;
  221. transition: border-color 0.2s;
  222. }
  223. .card-titular-label {
  224. font-size: 11px;
  225. color: #9a9aaa;
  226. }
  227. .card-holder-name {
  228. font-size: 14px;
  229. font-weight: 600;
  230. color: #3a3a4a;
  231. }
  232. .card-brand-text {
  233. font-size: 14px;
  234. font-weight: 700;
  235. color: #3a3a4a;
  236. text-align: right;
  237. }
  238. .card-last-four {
  239. font-size: 13px;
  240. color: #6a6a7a;
  241. letter-spacing: 1px;
  242. }
  243. .card-expiry-text {
  244. font-size: 12px;
  245. color: #9a9aaa;
  246. }
  247. .terms-text {
  248. font-size: 13px;
  249. color: #5a5a6a;
  250. line-height: 1.4;
  251. }
  252. .text-underline {
  253. text-decoration: underline;
  254. }
  255. .confirm-btn {
  256. font-size: 16px;
  257. font-weight: 700;
  258. height: 52px;
  259. }
  260. </style>