ProviderApprovalDialog.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. <template>
  2. <q-dialog
  3. ref="dialogRef"
  4. @hide="onDialogHide"
  5. >
  6. <q-card class="q-dialog-plugin column approval-dialog">
  7. <DefaultDialogHeader
  8. :title="() => $t('provider.approval.dialog_title')"
  9. @close="onDialogCancel"
  10. />
  11. <div class="col scroll q-pa-md">
  12. <div class="row items-center q-mb-md">
  13. <q-badge
  14. :color="statusColor"
  15. :label="$t(`provider.approval_status.${provider.approval_status}`)"
  16. class="text-body2 q-pa-sm"
  17. />
  18. </div>
  19. <div class="row q-col-gutter-md">
  20. <!-- Dados pessoais -->
  21. <div class="col-12 col-lg-4">
  22. <div class="text-subtitle1 text-weight-bold q-mb-sm">
  23. {{ $t("provider.approval.section_personal") }}
  24. </div>
  25. <q-list
  26. bordered
  27. separator
  28. class="rounded-borders"
  29. >
  30. <q-item>
  31. <q-item-section>
  32. <q-item-label caption>
  33. {{ $t("common.terms.name") }}
  34. </q-item-label>
  35. <q-item-label>
  36. {{ provider.user?.name || "—" }}
  37. </q-item-label>
  38. </q-item-section>
  39. </q-item>
  40. <q-item>
  41. <q-item-section>
  42. <q-item-label caption>
  43. {{ $t("common.terms.email") }}
  44. </q-item-label>
  45. <q-item-label>
  46. {{ provider.user?.email || "—" }}
  47. </q-item-label>
  48. </q-item-section>
  49. </q-item>
  50. <q-item>
  51. <q-item-section>
  52. <q-item-label caption>
  53. {{ $t("provider.fields.document") }}
  54. </q-item-label>
  55. <q-item-label>
  56. {{ provider.document || "—" }}
  57. </q-item-label>
  58. </q-item-section>
  59. </q-item>
  60. <q-item>
  61. <q-item-section>
  62. <q-item-label caption>
  63. {{ $t("provider.fields.rg") }}
  64. </q-item-label>
  65. <q-item-label>
  66. {{ provider.rg || "—" }}
  67. </q-item-label>
  68. </q-item-section>
  69. </q-item>
  70. <q-item>
  71. <q-item-section>
  72. <q-item-label caption>
  73. {{ $t("provider.fields.birth_date") }}
  74. </q-item-label>
  75. <q-item-label>
  76. {{ provider.birth_date || "—" }}
  77. </q-item-label>
  78. </q-item-section>
  79. </q-item>
  80. </q-list>
  81. </div>
  82. <!-- Preços -->
  83. <div class="col-12 col-lg-4">
  84. <div class="text-subtitle1 text-weight-bold q-mb-sm">
  85. {{ $t("provider.approval.section_pricing") }}
  86. </div>
  87. <q-list
  88. bordered
  89. separator
  90. class="rounded-borders"
  91. >
  92. <q-item>
  93. <q-item-section>
  94. <q-item-label caption>
  95. {{ $t("provider.fields.daily_price_8h") }}
  96. </q-item-label>
  97. <q-item-label>
  98. {{ formatCurrency(provider.daily_price_8h) }}
  99. </q-item-label>
  100. </q-item-section>
  101. </q-item>
  102. <q-item>
  103. <q-item-section>
  104. <q-item-label caption>
  105. {{ $t("provider.fields.daily_price_6h") }}
  106. </q-item-label>
  107. <q-item-label>
  108. {{ formatCurrency(provider.daily_price_6h) }}
  109. </q-item-label>
  110. </q-item-section>
  111. </q-item>
  112. <q-item>
  113. <q-item-section>
  114. <q-item-label caption>
  115. {{ $t("provider.fields.daily_price_4h") }}
  116. </q-item-label>
  117. <q-item-label>
  118. {{ formatCurrency(provider.daily_price_4h) }}
  119. </q-item-label>
  120. </q-item-section>
  121. </q-item>
  122. <q-item>
  123. <q-item-section>
  124. <q-item-label caption>
  125. {{ $t("provider.fields.daily_price_2h") }}
  126. </q-item-label>
  127. <q-item-label>
  128. {{ formatCurrency(provider.daily_price_2h) }}
  129. </q-item-label>
  130. </q-item-section>
  131. </q-item>
  132. </q-list>
  133. </div>
  134. <!-- Documentos -->
  135. <div class="col-12 col-lg-4">
  136. <div class="text-subtitle1 text-weight-bold q-mb-sm">
  137. {{ $t("provider.approval.section_documents") }}
  138. </div>
  139. <!-- Selfie -->
  140. <div class="q-mb-md">
  141. <div class="text-caption text-grey-6 q-mb-xs">
  142. {{ $t("provider.approval.selfie") }}
  143. </div>
  144. <div
  145. class="doc-frame"
  146. :class="{ 'doc-frame--clickable': avatarPreview }"
  147. @click="avatarPreview && (docLightbox = avatarPreview)"
  148. >
  149. <q-img
  150. v-if="avatarPreview"
  151. :src="avatarPreview"
  152. fit="cover"
  153. style="width: 100%; height: 100%;"
  154. />
  155. <div
  156. v-else
  157. class="column flex-center text-grey-5"
  158. >
  159. <q-icon
  160. name="mdi-account"
  161. size="40px"
  162. />
  163. </div>
  164. </div>
  165. </div>
  166. <!-- Documento Frente -->
  167. <div class="q-mb-md">
  168. <div class="text-caption text-grey-6 q-mb-xs">
  169. {{ $t("provider.fields.document_front") }}
  170. </div>
  171. <div
  172. class="doc-frame"
  173. :class="{ 'doc-frame--clickable': documentFrontUrl }"
  174. @click="documentFrontUrl && (docLightbox = documentFrontUrl)"
  175. >
  176. <q-img
  177. v-if="documentFrontUrl"
  178. :src="documentFrontUrl"
  179. fit="contain"
  180. style="width:100%;height:100%;"
  181. />
  182. <div
  183. v-else
  184. class="column flex-center text-grey-5"
  185. >
  186. <q-icon
  187. name="mdi-image-off-outline"
  188. size="36px"
  189. />
  190. </div>
  191. </div>
  192. </div>
  193. <!-- Documento Verso -->
  194. <div>
  195. <div class="text-caption text-grey-6 q-mb-xs">
  196. {{ $t("provider.fields.document_back") }}
  197. </div>
  198. <div
  199. class="doc-frame"
  200. :class="{ 'doc-frame--clickable': documentBackUrl }"
  201. @click="documentBackUrl && (docLightbox = documentBackUrl)"
  202. >
  203. <q-img
  204. v-if="documentBackUrl"
  205. :src="documentBackUrl"
  206. fit="contain"
  207. style="width:100%;height:100%;"
  208. />
  209. <div
  210. v-else
  211. class="column flex-center text-grey-5"
  212. >
  213. <q-icon
  214. name="mdi-image-off-outline"
  215. size="36px"
  216. />
  217. </div>
  218. </div>
  219. </div>
  220. </div>
  221. </div>
  222. <div class="text-caption text-grey-6 q-mt-md">
  223. {{ provider.created_at || "—" }}
  224. </div>
  225. </div>
  226. <q-card-actions class="q-pa-md row q-col-gutter-sm">
  227. <div class="col-12 col-sm-auto">
  228. <q-btn
  229. class="full-width"
  230. flat
  231. :label="$t('common.actions.cancel')"
  232. color="grey-7"
  233. @click="onDialogCancel"
  234. />
  235. </div>
  236. <div
  237. v-if="provider.approval_status !== 'rejected'"
  238. class="col-12 col-sm-auto"
  239. >
  240. <q-btn
  241. class="full-width"
  242. unelevated
  243. color="negative"
  244. :label="$t('provider.approval.btn_reject')"
  245. :loading="loadingReject"
  246. @click="onReject"
  247. />
  248. </div>
  249. <div
  250. v-if="provider.approval_status !== 'accepted'"
  251. class="col-12 col-sm-auto"
  252. >
  253. <q-btn
  254. class="full-width"
  255. unelevated
  256. color="positive"
  257. :label="$t('provider.approval.btn_approve')"
  258. :loading="loadingApprove"
  259. @click="onApprove"
  260. />
  261. </div>
  262. </q-card-actions>
  263. </q-card>
  264. </q-dialog>
  265. <q-dialog
  266. v-model="docLightboxOpen"
  267. maximized
  268. >
  269. <div
  270. class="flex flex-center bg-black"
  271. style="height:100%;"
  272. @click="docLightbox = null"
  273. >
  274. <q-img
  275. :src="docLightbox"
  276. fit="contain"
  277. style="max-width:100vw;max-height:100vh;"
  278. />
  279. <q-btn
  280. flat
  281. round
  282. color="white"
  283. icon="mdi-close"
  284. class="absolute-top-right q-ma-sm"
  285. @click.stop="docLightbox = null"
  286. />
  287. </div>
  288. </q-dialog>
  289. </template>
  290. <script setup>
  291. import { computed, ref, onMounted } from "vue";
  292. import { useDialogPluginComponent, useQuasar } from "quasar";
  293. import { useI18n } from "vue-i18n";
  294. import {
  295. approveProvider,
  296. rejectProvider,
  297. getProvider,
  298. } from "src/api/provider";
  299. import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
  300. const props = defineProps({
  301. provider: {
  302. type: Object,
  303. required: true,
  304. },
  305. });
  306. defineEmits([...useDialogPluginComponent.emits]);
  307. const {
  308. dialogRef,
  309. onDialogHide,
  310. onDialogOK,
  311. onDialogCancel,
  312. } = useDialogPluginComponent();
  313. const $q = useQuasar();
  314. const { t } = useI18n();
  315. const loadingApprove = ref(false);
  316. const loadingReject = ref(false);
  317. // Imagens
  318. const avatarPreview = ref(null);
  319. const documentFrontUrl = ref(null);
  320. const documentBackUrl = ref(null);
  321. // Lightbox
  322. const docLightbox = ref(null);
  323. const docLightboxOpen = computed({
  324. get: () => !!docLightbox.value,
  325. set: (value) => {
  326. if (!value) {
  327. docLightbox.value = null;
  328. }
  329. },
  330. });
  331. const statusColor = computed(() => {
  332. const map = {
  333. pending: "warning",
  334. accepted: "positive",
  335. rejected: "negative",
  336. };
  337. return map[props.provider.approval_status] ?? "grey";
  338. });
  339. const formatCurrency = (value) => {
  340. if (!value) return "—";
  341. return new Intl.NumberFormat("pt-BR", {
  342. style: "currency",
  343. currency: "BRL",
  344. }).format(value);
  345. };
  346. onMounted(async () => {
  347. try {
  348. const full = await getProvider(props.provider.id);
  349. console.log("FULL", full);
  350. console.log("PROFILE", full.profile_media);
  351. console.log("URL", full.profile_media?.url);
  352. avatarPreview.value = full.profile_media?.url ?? null;
  353. console.log("AVATAR", avatarPreview.value);
  354. documentFrontUrl.value = full.document_front_media?.url ?? null;
  355. documentBackUrl.value = full.document_back_media?.url ?? null;
  356. } catch (error) {
  357. console.error(error);
  358. }
  359. });
  360. const onApprove = async () => {
  361. loadingApprove.value = true;
  362. try {
  363. await approveProvider(props.provider.id);
  364. onDialogOK({ action: "approved" });
  365. } catch {
  366. $q.notify({
  367. type: "negative",
  368. message: t("http.errors.failed"),
  369. position: "top",
  370. });
  371. } finally {
  372. loadingApprove.value = false;
  373. }
  374. };
  375. const onReject = async () => {
  376. loadingReject.value = true;
  377. try {
  378. await rejectProvider(props.provider.id);
  379. onDialogOK({ action: "rejected" });
  380. } catch {
  381. $q.notify({
  382. type: "negative",
  383. message: t("http.errors.failed"),
  384. position: "top",
  385. });
  386. } finally {
  387. loadingReject.value = false;
  388. }
  389. };
  390. </script>
  391. <style scoped>
  392. .approval-dialog {
  393. width: 700px;
  394. max-width: 95vw;
  395. max-height: 90vh;
  396. }
  397. @media (max-width: 768px) {
  398. .approval-dialog {
  399. width: 100vw;
  400. max-width: 100vw;
  401. height: 100vh;
  402. max-height: 100vh;
  403. border-radius: 0;
  404. }
  405. }
  406. </style>