AddEditClientPaymentMethodDialog.vue 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. <template>
  2. <q-dialog ref="dialogRef" @hide="onDialogHide">
  3. <q-card class="q-dialog-plugin" style="width: 700px; max-width: 90vw">
  4. <DefaultDialogHeader :title="title" @close="onDialogCancel" />
  5. <q-form ref="formRef" @submit="onOKClick">
  6. <q-card-section class="row q-col-gutter-sm">
  7. <q-input
  8. v-model="form.card_number"
  9. :label="$t('client_payment_methods.card_number')"
  10. :rules="[inputRules.required, validateCardNumber]"
  11. :error="!!serverErrors?.card_number"
  12. :error-message="serverErrors?.card_number"
  13. :mask="cardNumberMask"
  14. unmasked-value
  15. class="col-12"
  16. @focus="onCardNumberFocus"
  17. @update:model-value="onCardNumberChange"
  18. >
  19. <template #append>
  20. <q-icon name="mdi-credit-card-outline" />
  21. </template>
  22. </q-input>
  23. <q-input
  24. v-model="form.holder_name"
  25. :label="$t('client_payment_methods.holder_name')"
  26. :rules="[inputRules.required]"
  27. :error="!!serverErrors?.holder_name"
  28. :error-message="serverErrors?.holder_name"
  29. class="col-12"
  30. @update:model-value="serverErrors.holder_name = null"
  31. />
  32. <q-input
  33. v-model="form.expiration"
  34. :label="$t('client_payment_methods.expiration')"
  35. :rules="[inputRules.required, validateExpiration]"
  36. :error="!!serverErrors?.expiration"
  37. :error-message="serverErrors?.expiration"
  38. mask="##/####"
  39. placeholder="MM/YYYY"
  40. class="col-md-6 col-12"
  41. @update:model-value="serverErrors.expiration = null"
  42. >
  43. <template #append>
  44. <q-icon name="mdi-calendar-outline" />
  45. </template>
  46. </q-input>
  47. <q-input
  48. v-model="form.cvv"
  49. :label="$t('client_payment_methods.cvv')"
  50. :rules="[inputRules.required]"
  51. :error="!!serverErrors?.cvv"
  52. :error-message="serverErrors?.cvv"
  53. mask="####"
  54. unmasked-value
  55. type="password"
  56. class="col-md-6 col-12"
  57. @update:model-value="serverErrors.cvv = null"
  58. >
  59. <template #append>
  60. <q-icon name="mdi-lock-outline" />
  61. </template>
  62. </q-input>
  63. <q-input
  64. v-model="form.card_name"
  65. :label="$t('client_payment_methods.card_name')"
  66. :error="!!serverErrors?.card_name"
  67. :error-message="serverErrors?.card_name"
  68. class="col-md-6 col-12"
  69. @update:model-value="serverErrors.card_name = null"
  70. />
  71. <q-input
  72. v-model="form.brand"
  73. :label="$t('client_payment_methods.brand')"
  74. :error="!!serverErrors?.brand"
  75. :error-message="serverErrors?.brand"
  76. readonly
  77. class="col-md-6 col-12"
  78. >
  79. <template #append>
  80. <q-icon name="mdi-credit-card-check-outline" />
  81. </template>
  82. </q-input>
  83. <q-checkbox
  84. v-model="form.is_active"
  85. :label="$t('client_payment_methods.is_active')"
  86. class="col-12"
  87. />
  88. </q-card-section>
  89. <q-card-actions align="right">
  90. <q-btn
  91. flat
  92. :label="$t('common.actions.cancel')"
  93. color="negative"
  94. @click="onDialogCancel"
  95. />
  96. <q-btn
  97. type="submit"
  98. :label="$t('common.actions.save')"
  99. :loading="loading"
  100. :disable="!hasUpdatedFields"
  101. color="primary"
  102. />
  103. </q-card-actions>
  104. </q-form>
  105. </q-card>
  106. </q-dialog>
  107. </template>
  108. <script setup>
  109. import { ref, computed, onMounted } from 'vue'
  110. import { useDialogPluginComponent } from 'quasar'
  111. import { useI18n } from 'vue-i18n'
  112. import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker'
  113. import { useSubmitHandler } from 'src/composables/useSubmitHandler'
  114. import {
  115. createClientPaymentMethod,
  116. updateClientPaymentMethod
  117. } from 'src/api/clientPaymentMethod'
  118. import DefaultDialogHeader from 'src/components/defaults/DefaultDialogHeader.vue'
  119. import { useInputRules } from 'src/composables/useInputRules'
  120. import {
  121. validateCardNumberLuhn,
  122. detectCardBrand,
  123. validateCardExpiration
  124. } from 'src/helpers/utils'
  125. const props = defineProps({
  126. paymentMethod: {
  127. type: Object,
  128. default: null
  129. },
  130. clientId: {
  131. type: Number,
  132. required: true
  133. },
  134. title: {
  135. type: Function,
  136. default: () => ''
  137. }
  138. })
  139. defineEmits([...useDialogPluginComponent.emits])
  140. const { t } = useI18n()
  141. const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
  142. const { inputRules } = useInputRules()
  143. const formRef = ref(null)
  144. const { form, getUpdatedFields, hasUpdatedFields } = useFormUpdateTracker({
  145. client_id: props.paymentMethod ? props.paymentMethod.client_id : props.clientId,
  146. card_number: props.paymentMethod ? props.paymentMethod.card_number : null,
  147. holder_name: props.paymentMethod ? props.paymentMethod.holder_name : null,
  148. expiration: props.paymentMethod ? props.paymentMethod.expiration : null,
  149. cvv: props.paymentMethod ? props.paymentMethod.cvv : null,
  150. card_name: props.paymentMethod ? props.paymentMethod.card_name : null,
  151. brand: props.paymentMethod ? props.paymentMethod.brand : null,
  152. last_four_digits: props.paymentMethod ? props.paymentMethod.last_four_digits : null,
  153. is_active: props.paymentMethod ? props.paymentMethod.is_active : true
  154. })
  155. const {
  156. loading,
  157. serverErrors,
  158. execute: submitForm,
  159. } = useSubmitHandler({
  160. onSuccess: () => onDialogOK(true),
  161. formRef: formRef,
  162. })
  163. const validateCardNumber = (val) => {
  164. if (!val) return true
  165. if (!validateCardNumberLuhn(val)) {
  166. return t('client_payment_methods.invalid_card_number')
  167. }
  168. return true
  169. }
  170. const validateExpiration = (val) => {
  171. if (!val) return true
  172. if (!validateCardExpiration(val)) {
  173. return t('client_payment_methods.expired_card')
  174. }
  175. return true
  176. }
  177. const cardNumberMask = computed(() => {
  178. const cardNumber = String(form.card_number || '')
  179. if (cardNumber.includes('*')) {
  180. return '**** **** **** ####'
  181. }
  182. const digits = cardNumber.replace(/\D/g, '')
  183. return digits.length > 16 ? '#### #### #### #### ###' : '#### #### #### ####'
  184. })
  185. const onCardNumberChange = () => {
  186. serverErrors.card_number = null
  187. if (form.card_number && form.card_number.length >= 6 && !form.card_number.includes('*')) {
  188. const brand = detectCardBrand(form.card_number)
  189. if (brand) {
  190. form.brand = brand
  191. }
  192. const digits = form.card_number.replace(/\D/g, '')
  193. if (digits.length >= 4) {
  194. form.last_four_digits = digits.slice(-4)
  195. }
  196. }
  197. }
  198. const onCardNumberFocus = () => {
  199. if (String(form.card_number || '').includes('*')) {
  200. form.card_number = ''
  201. }
  202. }
  203. const onOKClick = async () => {
  204. if (props.paymentMethod) {
  205. await submitForm(() => updateClientPaymentMethod(props.paymentMethod.id, getUpdatedFields.value))
  206. } else {
  207. await submitForm(() => createClientPaymentMethod({ ...form }))
  208. }
  209. }
  210. onMounted(() => {
  211. if (props.clientId) {
  212. form.client_id = props.clientId
  213. }
  214. })
  215. </script>