DashboardTodayServices.vue 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. <template>
  2. <div v-if="props.data.length > 0" class="q-mx-md q-mb-md">
  3. <q-card
  4. v-for="item in props.data"
  5. :key="item.id"
  6. class="today-card card-border shadow-card bg-surface q-mb-sm"
  7. :flat="false"
  8. >
  9. <q-card-section class="q-pa-sm">
  10. <div class="row no-wrap items-center q-mb-sm">
  11. <q-avatar size="40px" class="q-mr-sm">
  12. <img :src="item.customer_photo || 'https://cdn.quasar.dev/img/avatar.png'" />
  13. </q-avatar>
  14. <div class="col column">
  15. <span class="text-body2 text-text">
  16. {{ $t('provider.dashboard.today_services.start_label') }}
  17. <span class="text-weight-bold">{{ item.client_name }}</span>
  18. </span>
  19. <div class="row items-center q-gutter-x-xs q-mt-xs">
  20. <q-icon name="mdi-clock-outline" color="grey-5" size="14px" />
  21. <span class="text-caption text-grey-6">
  22. {{ $t('common.from') }}
  23. <strong class="text-text">{{ item.start_time?.slice(0, 5) }}</strong>
  24. {{ $t('common.to') }}
  25. <strong class="text-text">{{ item.end_time?.slice(0, 5) }}</strong>
  26. </span>
  27. </div>
  28. </div>
  29. <div class="col-auto text-caption text-grey-5 text-right q-pl-xs hint-text">
  30. {{ $t('provider.dashboard.today_services.code_hint') }}
  31. </div>
  32. </div>
  33. <div
  34. class="code-container row justify-center q-gutter-x-sm q-mb-sm"
  35. :class="{ 'code-disabled': item.code_verified || !canEnterCode(item) }"
  36. @click="focusInput(item.id)"
  37. >
  38. <div
  39. v-for="i in 4"
  40. :key="i"
  41. class="code-box"
  42. :class="{
  43. 'code-box--filled': (codes[item.id] || '').length >= i,
  44. 'code-box--verified': item.code_verified
  45. }"
  46. >
  47. <template v-if="item.code_verified">
  48. <q-icon v-if="i === 2" name="mdi-check-circle" color="positive" size="18px" />
  49. <span v-else></span>
  50. </template>
  51. <span v-else>{{ (codes[item.id] || '')[i - 1] || '' }}</span>
  52. </div>
  53. <input
  54. :id="`code-input-${item.id}`"
  55. v-model="codes[item.id]"
  56. type="tel"
  57. inputmode="numeric"
  58. maxlength="4"
  59. class="code-real-input"
  60. :disabled="item.code_verified || !canEnterCode(item)"
  61. @input="onCodeInput(item)"
  62. />
  63. </div>
  64. <q-linear-progress
  65. :value="progressValue(item.status)"
  66. color="secondary"
  67. track-color="grey-3"
  68. rounded
  69. size="5px"
  70. class="q-mb-sm"
  71. />
  72. <div class="row items-center">
  73. <q-btn
  74. flat
  75. no-caps
  76. color="primary"
  77. size="sm"
  78. class="q-px-none btn-help"
  79. :label="$t('provider.dashboard.today_services.help')"
  80. @click="openHelp"
  81. />
  82. <q-space />
  83. <div class="row items-center no-wrap q-gutter-x-xs">
  84. <q-icon name="mdi-map-marker-outline" color="grey-5" size="14px" />
  85. <span class="text-caption text-grey-7 ellipsis address-text">
  86. {{ formatAddressShort(item.address) }}
  87. </span>
  88. <q-btn
  89. flat
  90. round
  91. dense
  92. icon="mdi-content-copy"
  93. color="primary"
  94. size="xs"
  95. @click.stop="copyAddress(item.address)"
  96. />
  97. </div>
  98. </div>
  99. </q-card-section>
  100. </q-card>
  101. </div>
  102. </template>
  103. <script setup>
  104. import { ref, nextTick } from 'vue'
  105. import { useI18n } from 'vue-i18n'
  106. import { useQuasar } from 'quasar'
  107. import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
  108. import { verifyScheduleCode } from 'src/api/schedule'
  109. const props = defineProps({
  110. data: {
  111. type: Array,
  112. default: () => []
  113. }
  114. })
  115. const emit = defineEmits(['refresh'])
  116. const { t } = useI18n()
  117. const $q = useQuasar()
  118. const codes = ref({})
  119. const loadingCode = ref({})
  120. const progressValue = (status) => {
  121. const map = { accepted: 0.4, paid: 0.6, started: 0.8, finished: 1.0 }
  122. return map[status] ?? 0.4
  123. }
  124. const canEnterCode = (item) => ['paid', 'started'].includes(item.status)
  125. const focusInput = (id) => {
  126. nextTick(() => document.getElementById(`code-input-${id}`)?.focus())
  127. }
  128. const onCodeInput = async (item) => {
  129. const val = codes.value[item.id] || ''
  130. if (val.length < 4 || item.code_verified || !canEnterCode(item)) return
  131. loadingCode.value[item.id] = true
  132. try {
  133. const response = await verifyScheduleCode(item.id, val)
  134. if (response?.data?.success || response?.success) {
  135. $q.notify({ type: 'positive', message: t('provider.dashboard.today_services.code_success'), position: 'top' })
  136. emit('refresh')
  137. } else {
  138. $q.notify({ type: 'negative', message: t('provider.dashboard.today_services.code_error'), position: 'top' })
  139. codes.value[item.id] = ''
  140. }
  141. } catch {
  142. $q.notify({ type: 'negative', message: t('provider.dashboard.today_services.code_error'), position: 'top' })
  143. codes.value[item.id] = ''
  144. } finally {
  145. loadingCode.value[item.id] = false
  146. }
  147. }
  148. const formatAddressShort = (address) => {
  149. if (!address) return ''
  150. return [address.address, address.number, address.district].filter(Boolean).join(', ')
  151. }
  152. const copyAddress = (address) => {
  153. const text = formatAddressShort(address)
  154. if (text) navigator.clipboard.writeText(text)
  155. $q.notify({ message: t('provider.dashboard.next_schedules.address_copied'), color: 'positive', position: 'top' })
  156. }
  157. const openHelp = () => {
  158. $q.dialog({ component: ProfileHelpDialog })
  159. }
  160. </script>
  161. <style scoped lang="scss">
  162. .today-card {
  163. border-radius: 12px;
  164. }
  165. .hint-text {
  166. max-width: 100px;
  167. line-height: 1.3;
  168. font-size: 11px;
  169. }
  170. /* OTP input */
  171. .code-container {
  172. position: relative;
  173. cursor: text;
  174. user-select: none;
  175. }
  176. .code-real-input {
  177. position: absolute;
  178. width: 1px;
  179. height: 1px;
  180. opacity: 0;
  181. pointer-events: none;
  182. top: 0;
  183. left: 0;
  184. }
  185. .code-box {
  186. width: 52px;
  187. height: 44px;
  188. background: #efefef;
  189. border-radius: 10px;
  190. display: flex;
  191. align-items: center;
  192. justify-content: center;
  193. font-size: 22px;
  194. font-weight: 700;
  195. color: #3a3a4a;
  196. transition: background 0.15s;
  197. }
  198. .code-box--filled {
  199. background: #e0d8f8;
  200. color: var(--q-secondary);
  201. }
  202. .code-box--verified {
  203. background: #e8f5e9;
  204. }
  205. .code-disabled .code-box {
  206. opacity: 0.5;
  207. cursor: not-allowed;
  208. }
  209. .address-text {
  210. max-width: 150px;
  211. }
  212. .btn-help {
  213. font-weight: 700;
  214. font-size: 13px;
  215. }
  216. </style>