Ver código fonte

implementacao didit - validacao de documentos

Gustavo Zanatta 3 dias atrás
pai
commit
ef729334a9

+ 1 - 1
src/api/provider.js

@@ -11,7 +11,7 @@ export const getProviders = async () => {
 };
 
 export const getPendingProviders = async ({ page = 1, perPage = 5 } = {}) => {
-  console.log(perPage);
+
   const response = await api.get("/provider/pending", { params: { page, per_page: perPage } });
   return { data: { result: response.data.payload } };
 };

+ 28 - 0
src/api/verification.js

@@ -0,0 +1,28 @@
+import api from "src/api";
+
+export const getPendingVerifications = async ({ page = 1, perPage = 5 } = {}) => {
+  const response = await api.get("/verification/pending", {
+    params: { page, per_page: perPage },
+  });
+
+  return { data: { result: response.data.payload } };
+};
+
+/**
+ * Busca o laudo completo. As imagens vêm do Didit no momento da chamada, porque
+ * as URLs deles são presignadas e de vida curta — por isso não são persistidas.
+ */
+export const getVerification = async (id) => {
+  const { data } = await api.get(`/verification/${id}`);
+  return data.payload;
+};
+
+export const approveVerification = async (id, comment = null) => {
+  const { data } = await api.patch(`/verification/${id}/approve`, { comment });
+  return data.payload;
+};
+
+export const rejectVerification = async (id, comment = null) => {
+  const { data } = await api.patch(`/verification/${id}/reject`, { comment });
+  return data.payload;
+};

+ 57 - 0
src/i18n/locales/en.json

@@ -895,5 +895,62 @@
       "warn_push_disabled": "{count} selected recipient(s) disabled notifications and will not receive it.",
       "sent_content": "Sent content"
     }
+  },
+  "verification": {
+    "pending_table_title": "Pending identity verifications",
+    "dialog_title": "Identity verification report",
+    "attempt_number": "Attempt {number}",
+    "decision_stale": "Cached report (Didit unavailable)",
+    "section_checks": "Checks",
+    "section_warnings": "Issues",
+    "section_images": "Captured images",
+    "no_warnings": "No actionable issues.",
+    "informational_warnings": "Informational warnings ({count})",
+    "no_images": "No images available.",
+    "review_comment": "Review note (optional)",
+    "review_failed": "Could not record the review",
+    "approve": "Approve",
+    "reject": "Reject",
+    "fields": {
+      "type": "Profile",
+      "status": "Status",
+      "scores": "Liveness / Face",
+      "completed_at": "Completed at"
+    },
+    "types": {
+      "provider": "Provider",
+      "client": "Client"
+    },
+    "didit_status": {
+      "Not Started": "Not started",
+      "In Progress": "In progress",
+      "Approved": "Approved",
+      "Declined": "Declined",
+      "In Review": "In review",
+      "Resubmitted": "Resubmitted",
+      "Abandoned": "Abandoned",
+      "Expired": "Expired",
+      "Kyc Expired": "KYC expired"
+    },
+    "checks": {
+      "ocr": "Document (OCR)",
+      "liveness": "Liveness",
+      "face_match": "Face match"
+    },
+    "images": {
+      "front": "Document (front)",
+      "back": "Document (back)",
+      "portrait": "Document portrait",
+      "selfie": "Selfie"
+    },
+    "open_report": "View full report",
+    "status": {
+      "not_started": "Not started",
+      "pending": "In progress",
+      "approved": "Verified",
+      "in_review": "In review",
+      "declined": "Declined",
+      "expired": "Expired"
+    }
   }
 }

+ 57 - 0
src/i18n/locales/es.json

@@ -895,5 +895,62 @@
       "warn_push_disabled": "{count} seleccionado(s) desactivaron las notificaciones y no la recibirán.",
       "sent_content": "Contenido enviado"
     }
+  },
+  "verification": {
+    "pending_table_title": "Verificaciones de identidad pendientes",
+    "dialog_title": "Informe de verificación de identidad",
+    "attempt_number": "Intento {number}",
+    "decision_stale": "Informe en caché (Didit no disponible)",
+    "section_checks": "Comprobaciones",
+    "section_warnings": "Incidencias",
+    "section_images": "Imágenes capturadas",
+    "no_warnings": "Sin incidencias accionables.",
+    "informational_warnings": "Avisos informativos ({count})",
+    "no_images": "Sin imágenes disponibles.",
+    "review_comment": "Observación del análisis (opcional)",
+    "review_failed": "No se pudo registrar el análisis",
+    "approve": "Aprobar",
+    "reject": "Rechazar",
+    "fields": {
+      "type": "Perfil",
+      "status": "Estado",
+      "scores": "Vivacidad / Rostro",
+      "completed_at": "Completada el"
+    },
+    "types": {
+      "provider": "Prestador",
+      "client": "Cliente"
+    },
+    "didit_status": {
+      "Not Started": "No iniciada",
+      "In Progress": "En curso",
+      "Approved": "Aprobada",
+      "Declined": "Rechazada",
+      "In Review": "En análisis",
+      "Resubmitted": "Reenviada",
+      "Abandoned": "Abandonada",
+      "Expired": "Expirada",
+      "Kyc Expired": "Verificación vencida"
+    },
+    "checks": {
+      "ocr": "Documento (OCR)",
+      "liveness": "Prueba de vida",
+      "face_match": "Coincidencia facial"
+    },
+    "images": {
+      "front": "Documento (frente)",
+      "back": "Documento (dorso)",
+      "portrait": "Retrato del documento",
+      "selfie": "Selfie"
+    },
+    "open_report": "Ver informe completo",
+    "status": {
+      "not_started": "No iniciada",
+      "pending": "En curso",
+      "approved": "Verificada",
+      "in_review": "En análisis",
+      "declined": "Rechazada",
+      "expired": "Vencida"
+    }
   }
 }

+ 57 - 0
src/i18n/locales/pt.json

@@ -897,5 +897,62 @@
       "warn_push_disabled": "{count} selecionado(s) desativaram as notificações e não vão receber.",
       "sent_content": "Conteúdo enviado"
     }
+  },
+  "verification": {
+    "pending_table_title": "Verificações de identidade pendentes",
+    "dialog_title": "Laudo de verificação de identidade",
+    "attempt_number": "Tentativa {number}",
+    "decision_stale": "Laudo em cache (Didit indisponível)",
+    "section_checks": "Checagens",
+    "section_warnings": "Pendências",
+    "section_images": "Imagens capturadas",
+    "no_warnings": "Nenhuma pendência acionável.",
+    "informational_warnings": "Avisos informativos ({count})",
+    "no_images": "Nenhuma imagem disponível.",
+    "review_comment": "Observação da análise (opcional)",
+    "review_failed": "Não foi possível registrar a análise",
+    "approve": "Aprovar",
+    "reject": "Reprovar",
+    "fields": {
+      "type": "Perfil",
+      "status": "Status",
+      "scores": "Vivacidade / Face",
+      "completed_at": "Concluída em"
+    },
+    "types": {
+      "provider": "Prestador",
+      "client": "Cliente"
+    },
+    "didit_status": {
+      "Not Started": "Não iniciada",
+      "In Progress": "Em andamento",
+      "Approved": "Aprovada",
+      "Declined": "Reprovada",
+      "In Review": "Em análise",
+      "Resubmitted": "Reenviada",
+      "Abandoned": "Abandonada",
+      "Expired": "Expirada",
+      "Kyc Expired": "Verificação vencida"
+    },
+    "checks": {
+      "ocr": "Documento (OCR)",
+      "liveness": "Prova de vida",
+      "face_match": "Semelhança facial"
+    },
+    "images": {
+      "front": "Documento (frente)",
+      "back": "Documento (verso)",
+      "portrait": "Retrato do documento",
+      "selfie": "Selfie"
+    },
+    "open_report": "Ver laudo completo",
+    "status": {
+      "not_started": "Não iniciada",
+      "pending": "Em andamento",
+      "approved": "Verificada",
+      "in_review": "Em análise",
+      "declined": "Reprovada",
+      "expired": "Vencida"
+    }
   }
 }

+ 2 - 0
src/pages/dashboard/DashboardPage.vue

@@ -3,6 +3,7 @@
     <DefaultHeaderPage />
 
     <div v-if="!isLoading" class="q-pa-md">
+      <PendingVerificationsTable />
       <PendingProvidersTable />
       <PendingProfileMediaTable />
 
@@ -492,6 +493,7 @@ import { onMounted, ref, computed, watch } from 'vue'
 import { useQuasar } from 'quasar'
 import { useI18n } from 'vue-i18n'
 import DefaultHeaderPage from 'src/components/layout/DefaultHeaderPage.vue'
+import PendingVerificationsTable from 'src/pages/dashboard/components/PendingVerificationsTable.vue'
 import PendingProvidersTable from 'src/pages/dashboard/components/PendingProvidersTable.vue'
 import PendingProfileMediaTable from 'src/pages/dashboard/components/PendingProfileMediaTable.vue'
 import ViewScheduleDialog from 'src/pages/schedule/components/ViewScheduleDialog.vue'

+ 323 - 0
src/pages/dashboard/components/IdentityVerificationDialog.vue

@@ -0,0 +1,323 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin verification-dialog">
+      <DefaultDialogHeader
+        :title="() => $t('verification.dialog_title')"
+        @close="onDialogCancel"
+      />
+
+      <div class="col scroll q-pa-md">
+        <div class="row items-center q-gutter-sm q-mb-md">
+          <q-badge
+            class="text-body2 q-pa-sm"
+            :color="statusColor"
+            :label="$t(`verification.didit_status.${verification.didit_status}`)"
+          />
+
+          <q-badge
+            class="text-body2 q-pa-sm"
+            color="grey-7"
+            :label="$t('verification.attempt_number', { number: verification.attempt })"
+          />
+
+          <q-space />
+
+          <q-badge
+            v-if="!loading && !decisionFresh"
+            class="text-body2 q-pa-sm"
+            color="warning"
+            :label="$t('verification.decision_stale')"
+          />
+        </div>
+
+        <div class="row q-col-gutter-md">
+          <!-- Resultado por checagem -->
+          <div class="col-12 col-lg-5">
+            <div class="text-subtitle1 text-weight-bold q-mb-sm">
+              {{ $t("verification.section_checks") }}
+            </div>
+
+            <q-list bordered separator class="rounded-borders">
+              <q-item v-for="check in checks" :key="check.key">
+                <q-item-section>
+                  <q-item-label caption>{{ check.label }}</q-item-label>
+                  <q-item-label>{{ check.detail }}</q-item-label>
+                </q-item-section>
+
+                <q-item-section side>
+                  <q-icon :color="check.color" :name="check.icon" size="20px" />
+                </q-item-section>
+              </q-item>
+            </q-list>
+          </div>
+
+          <!-- Pendências apontadas pelo Didit -->
+          <div class="col-12 col-lg-7">
+            <div class="text-subtitle1 text-weight-bold q-mb-sm">
+              {{ $t("verification.section_warnings") }}
+            </div>
+
+            <q-banner v-if="!actionableWarnings.length" class="bg-grey-2 rounded-borders">
+              {{ $t("verification.no_warnings") }}
+            </q-banner>
+
+            <q-list v-else bordered separator class="rounded-borders">
+              <q-item v-for="(warning, index) in actionableWarnings" :key="index">
+                <q-item-section avatar>
+                  <q-icon color="negative" name="mdi-alert-circle-outline" />
+                </q-item-section>
+
+                <q-item-section>
+                  <q-item-label class="text-weight-medium">
+                    {{ warning.short_description || warning.risk }}
+                  </q-item-label>
+
+                  <q-item-label caption>{{ warning.long_description }}</q-item-label>
+
+                  <q-item-label
+                    v-if="warning.additional_data"
+                    caption
+                    class="text-grey-8 q-mt-xs"
+                  >
+                    {{ formatAdditionalData(warning.additional_data) }}
+                  </q-item-label>
+                </q-item-section>
+              </q-item>
+            </q-list>
+
+            <!-- Informativos: registrados, mas não bloqueiam -->
+            <q-expansion-item
+              v-if="informationalWarnings.length"
+              class="q-mt-sm"
+              dense
+              :label="$t('verification.informational_warnings', { count: informationalWarnings.length })"
+            >
+              <q-list bordered separator class="rounded-borders">
+                <q-item v-for="(warning, index) in informationalWarnings" :key="index">
+                  <q-item-section>
+                    <q-item-label caption>{{ warning.risk }}</q-item-label>
+                    <q-item-label class="text-caption">
+                      {{ warning.short_description }}
+                    </q-item-label>
+                  </q-item-section>
+                </q-item>
+              </q-list>
+            </q-expansion-item>
+
+            <!-- Imagens: buscadas na hora, as URLs do Didit expiram rápido -->
+            <div class="text-subtitle1 text-weight-bold q-mb-sm q-mt-md">
+              {{ $t("verification.section_images") }}
+            </div>
+
+            <div v-if="loading" class="row justify-center q-pa-md">
+              <q-spinner color="primary" size="32px" />
+            </div>
+
+            <div v-else-if="images.length" class="row q-col-gutter-sm">
+              <div v-for="image in images" :key="image.key" class="col-6 col-sm-4">
+                <q-img
+                  class="cursor-pointer rounded-borders"
+                  fit="cover"
+                  ratio="1"
+                  :src="image.url"
+                  @click="lightbox = image.url"
+                />
+
+                <div class="text-caption text-center q-mt-xs">{{ image.label }}</div>
+              </div>
+            </div>
+
+            <q-banner v-else class="bg-grey-2 rounded-borders">
+              {{ $t("verification.no_images") }}
+            </q-banner>
+          </div>
+        </div>
+
+        <q-input
+          v-model="comment"
+          class="q-mt-md"
+          dense
+          outlined
+          type="textarea"
+          :label="$t('verification.review_comment')"
+        />
+      </div>
+
+      <q-card-actions align="right" class="q-pa-md">
+        <q-btn
+          flat
+          no-caps
+          :label="$t('common.actions.cancel')"
+          @click="onDialogCancel"
+        />
+
+        <q-btn
+          color="negative"
+          no-caps
+          unelevated
+          :label="$t('verification.reject')"
+          :loading="loadingReject"
+          @click="onReject"
+        />
+
+        <q-btn
+          color="positive"
+          no-caps
+          unelevated
+          :label="$t('verification.approve')"
+          :loading="loadingApprove"
+          @click="onApprove"
+        />
+      </q-card-actions>
+
+      <q-dialog v-model="lightboxOpen">
+        <q-img fit="contain" :src="lightbox" style="max-width: 90vw" />
+      </q-dialog>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { computed, onMounted, ref } from "vue";
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
+
+import {
+  approveVerification,
+  getVerification,
+  rejectVerification,
+} from "src/api/verification";
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+
+const props = defineProps({
+  verification: {
+    type: Object,
+    required: true,
+  },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
+
+const $q = useQuasar();
+const { t } = useI18n();
+
+const loading = ref(true);
+const loadingApprove = ref(false);
+const loadingReject = ref(false);
+const comment = ref("");
+const lightbox = ref(null);
+const decision = ref(null);
+const decisionFresh = ref(false);
+
+const lightboxOpen = computed({
+  get: () => !!lightbox.value,
+  set: (value) => {
+    if (!value) lightbox.value = null;
+  },
+});
+
+onMounted(async () => {
+  try {
+    const full = await getVerification(props.verification.id);
+
+    decision.value = full.decision;
+    decisionFresh.value = full.decision_fresh;
+  } catch {
+    decision.value = null;
+  } finally {
+    loading.value = false;
+  }
+});
+
+const statusColor = computed(
+  () => (props.verification.didit_status === "Declined" ? "negative" : "warning"),
+);
+
+const scoreLabel = (value) =>
+  value === null || value === undefined ? "—" : `${Number(value).toFixed(1)} / 100`;
+
+const statusIcon = (status) => {
+  if (status === "Approved") return { icon: "mdi-check-circle", color: "positive" };
+  if (status === "Declined") return { icon: "mdi-close-circle", color: "negative" };
+  if (!status) return { icon: "mdi-minus-circle-outline", color: "grey" };
+
+  return { icon: "mdi-alert-circle", color: "warning" };
+};
+
+const checks = computed(() => {
+  const v = props.verification;
+
+  return [
+    { key: "ocr", label: t("verification.checks.ocr"), status: v.id_verification_status, detail: v.id_verification_status || "—" },
+    { key: "liveness", label: t("verification.checks.liveness"), status: v.liveness_status, detail: scoreLabel(v.liveness_score) },
+    { key: "face_match", label: t("verification.checks.face_match"), status: v.face_match_status, detail: scoreLabel(v.face_match_score) },
+  ].map((check) => ({ ...check, ...statusIcon(check.status) }));
+});
+
+const allWarnings = computed(() => props.verification.warnings ?? []);
+
+const actionableWarnings = computed(() =>
+  allWarnings.value.filter((warning) => warning.log_type === "warning"),
+);
+
+const informationalWarnings = computed(() =>
+  allWarnings.value.filter((warning) => warning.log_type !== "warning"),
+);
+
+const formatAdditionalData = (data) =>
+  Object.entries(data)
+    .filter(([, value]) => value !== null && value !== undefined)
+    .map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : value}`)
+    .join(" · ");
+
+const first = (key) => {
+  const items = decision.value?.[key];
+
+  return Array.isArray(items) && items.length ? items[0] : null;
+};
+
+const images = computed(() => {
+  const id = first("id_verifications");
+  const liveness = first("liveness_checks");
+
+  return [
+    { key: "front", label: t("verification.images.front"), url: id?.front_image },
+    { key: "back", label: t("verification.images.back"), url: id?.back_image },
+    { key: "portrait", label: t("verification.images.portrait"), url: id?.portrait_image },
+    { key: "selfie", label: t("verification.images.selfie"), url: liveness?.reference_image },
+  ].filter((image) => !!image.url);
+});
+
+const review = async (action, loadingRef) => {
+  loadingRef.value = true;
+
+  try {
+    await action(props.verification.id, comment.value || null);
+
+    onDialogOK();
+  } catch (error) {
+    $q.notify({
+      message: error?.response?.data?.message ?? t("verification.review_failed"),
+      type: "negative",
+    });
+  } finally {
+    loadingRef.value = false;
+  }
+};
+
+const onApprove = () => review(approveVerification, loadingApprove);
+const onReject = () => review(rejectVerification, loadingReject);
+</script>
+
+<style lang="scss" scoped>
+.verification-dialog {
+  display: flex;
+  flex-direction: column;
+  max-width: 1100px;
+  width: 90vw;
+  max-height: 90vh;
+}
+</style>

+ 113 - 0
src/pages/dashboard/components/PendingVerificationsTable.vue

@@ -0,0 +1,113 @@
+<template>
+  <div class="q-mb-lg">
+    <div class="text-h6 text-weight-bold q-mb-sm">
+      {{ $t('verification.pending_table_title') }}
+    </div>
+
+    <DefaultTableServerSide
+      ref="tableRef"
+      class="bg-surface-light"
+      :columns="columns"
+      :api-call="getPendingVerifications"
+      :add-item="false"
+      :show-search-field="false"
+      :open-item="true"
+      @on-row-click="onRowClick"
+    >
+      <template #body-cell-name="slotProps">
+        <q-td :props="slotProps">
+          {{ slotProps.row.user?.name || '—' }}
+        </q-td>
+      </template>
+
+      <template #body-cell-type="slotProps">
+        <q-td :props="slotProps">
+          <q-badge
+            :color="slotProps.row.user?.type === 'PROVIDER' ? 'primary' : 'secondary'"
+            :label="$t(`verification.types.${String(slotProps.row.user?.type || '').toLowerCase()}`)"
+          />
+        </q-td>
+      </template>
+
+      <template #body-cell-didit_status="slotProps">
+        <q-td :props="slotProps">
+          <q-badge
+            :color="slotProps.row.didit_status === 'Declined' ? 'negative' : 'warning'"
+            :label="$t(`verification.didit_status.${slotProps.row.didit_status}`)"
+          />
+        </q-td>
+      </template>
+
+      <template #body-cell-scores="slotProps">
+        <q-td :props="slotProps">
+          <span class="text-caption">
+            {{ `${formatScore(slotProps.row.liveness_score)} / ${formatScore(slotProps.row.face_match_score)}` }}
+          </span>
+        </q-td>
+      </template>
+
+      <template #body-cell-completed_at="slotProps">
+        <q-td :props="slotProps">
+          {{ formatDate(slotProps.row.completed_at) }}
+        </q-td>
+      </template>
+
+      <template #body-cell-actions="slotProps">
+        <q-td :props="slotProps" class="text-right">
+          <q-btn
+            flat
+            dense
+            round
+            icon="mdi-eye-outline"
+            color="primary"
+            @click.stop="onRowClick({ row: slotProps.row })"
+          >
+            <q-tooltip>{{ $t('common.actions.view') }}</q-tooltip>
+          </q-btn>
+        </q-td>
+      </template>
+    </DefaultTableServerSide>
+  </div>
+</template>
+
+<script setup>
+import { defineAsyncComponent, ref } from 'vue';
+import { useQuasar } from 'quasar';
+import { useI18n } from 'vue-i18n';
+import { format, parseISO } from 'date-fns';
+
+import { getPendingVerifications } from 'src/api/verification';
+import DefaultTableServerSide from 'src/components/defaults/DefaultTableServerSide.vue';
+
+const IdentityVerificationDialog = defineAsyncComponent(
+  () => import('src/pages/dashboard/components/IdentityVerificationDialog.vue'),
+);
+
+const $q = useQuasar();
+const { t } = useI18n();
+const tableRef = ref(null);
+
+const columns = [
+  { name: 'name', label: t('common.terms.name'), field: (row) => row.user?.name || '—', align: 'left', sortable: false },
+  { name: 'type', label: t('verification.fields.type'), field: (row) => row.user?.type, align: 'left', sortable: false },
+  { name: 'didit_status', label: t('verification.fields.status'), field: 'didit_status', align: 'left', sortable: false },
+  { name: 'scores', label: t('verification.fields.scores'), field: 'liveness_score', align: 'left', sortable: false },
+  { name: 'completed_at', label: t('verification.fields.completed_at'), field: 'completed_at', align: 'left', sortable: false },
+  { name: 'actions', label: '', field: 'actions', align: 'right', sortable: false },
+];
+
+const formatScore = (value) =>
+  value === null || value === undefined ? '—' : Number(value).toFixed(0);
+
+const formatDate = (value) => {
+  if (!value) return '—';
+  try { return format(parseISO(value), 'dd/MM/yyyy HH:mm'); } catch { return value; }
+};
+
+const onRowClick = ({ row }) => {
+  $q.dialog({
+    component: IdentityVerificationDialog,
+    componentProps: { verification: row },
+  }).onOk(() => tableRef.value?.refresh());
+};
+</script>

+ 87 - 10
src/pages/dashboard/components/ProviderApprovalDialog.vue

@@ -173,7 +173,54 @@
             </div>
 
             <!-- Documento Frente -->
-            <div class="q-mb-md">
+            <!-- Verificação de identidade (cadastros a partir do Didit) -->
+            <div v-if="identityStatus" class="q-mb-md">
+              <div class="text-caption text-grey-6 q-mb-xs">
+                {{ $t("verification.dialog_title") }}
+              </div>
+
+              <q-list bordered separator class="rounded-borders">
+                <q-item>
+                  <q-item-section>
+                    <q-item-label caption>
+                      {{ $t("verification.fields.status") }}
+                    </q-item-label>
+                  </q-item-section>
+
+                  <q-item-section side>
+                    <q-badge
+                      :color="identityStatusColor"
+                      :label="$t(`verification.status.${identityStatus}`)"
+                    />
+                  </q-item-section>
+                </q-item>
+
+                <q-item v-if="lastVerification">
+                  <q-item-section>
+                    <q-item-label caption>
+                      {{ $t("verification.fields.scores") }}
+                    </q-item-label>
+
+                    <q-item-label>
+                      {{ identityScores }}
+                    </q-item-label>
+                  </q-item-section>
+                </q-item>
+              </q-list>
+
+              <q-btn
+                v-if="lastVerification"
+                class="full-width q-mt-sm"
+                color="primary"
+                icon="mdi-file-search-outline"
+                no-caps
+                outline
+                :label="$t('verification.open_report')"
+                @click="openVerificationReport"
+              />
+            </div>
+
+            <div v-if="documentFrontUrl" class="q-mb-md">
               <div class="text-caption text-grey-6 q-mb-xs">
                 {{ $t("provider.fields.document_front") }}
               </div>
@@ -196,8 +243,8 @@
               </div>
             </div>
 
-            <!-- Documento Verso -->
-            <div>
+            <!-- Documento Verso (apenas cadastros anteriores ao Didit) -->
+            <div v-if="documentBackUrl">
               <div class="text-caption text-grey-6 q-mb-xs">
                 {{ $t("provider.fields.document_back") }}
               </div>
@@ -296,12 +343,16 @@
 </template>
 
 <script setup>
-import { computed, ref, onMounted } from "vue";
+import { computed, defineAsyncComponent, ref, onMounted } from "vue";
 import { useDialogPluginComponent, useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import { approveProvider, rejectProvider, getProvider } from "src/api/provider";
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
 
+const IdentityVerificationDialog = defineAsyncComponent(
+  () => import("src/pages/dashboard/components/IdentityVerificationDialog.vue"),
+);
+
 const props = defineProps({
   provider: {
     type: Object,
@@ -322,6 +373,8 @@ const loadingReject = ref(false);
 
 // Imagens
 const avatarPreview = ref(null);
+const identityStatus = ref(null);
+const lastVerification = ref(null);
 const documentFrontUrl = ref(null);
 const documentBackUrl = ref(null);
 
@@ -360,21 +413,45 @@ onMounted(async () => {
   try {
     const full = await getProvider(props.provider.id);
 
-    console.log("FULL", full);
-    console.log("PROFILE", full.profile_media);
-    console.log("URL", full.profile_media?.url);
-
     avatarPreview.value = full.profile_media?.url ?? null;
 
-    console.log("AVATAR", avatarPreview.value);
-
+    // Cadastros anteriores ao Didit ainda têm as fotos no S3.
     documentFrontUrl.value = full.document_front_media?.url ?? null;
     documentBackUrl.value = full.document_back_media?.url ?? null;
+
+    identityStatus.value = full.identity_verification_status ?? null;
+    lastVerification.value = full.identity_verification ?? null;
   } catch (error) {
     console.error(error);
   }
 });
 
+const identityStatusColor = computed(() => {
+  const map = {
+    approved: "positive",
+    declined: "negative",
+    in_review: "warning",
+  };
+
+  return map[identityStatus.value] ?? "grey-7";
+});
+
+const identityScores = computed(() => {
+  const liveness = lastVerification.value?.liveness_score;
+  const faceMatch = lastVerification.value?.face_match_score;
+  const format = (value) =>
+    value === null || value === undefined ? "—" : Number(value).toFixed(0);
+
+  return `${format(liveness)} / ${format(faceMatch)}`;
+});
+
+const openVerificationReport = () => {
+  $q.dialog({
+    component: IdentityVerificationDialog,
+    componentProps: { verification: lastVerification.value },
+  });
+};
+
 const onApprove = async () => {
   loadingApprove.value = true;