Ver Fonte

feat: :sparkles: feat (suporte) adicionada opcao de registrar suporte

adicionada funcao do usuario preencher formulario e enviar para suporte com duvidas/ajudas que precisa. Visualizacao e interacao via backoffice

fase:staging | origin:escopo
Gustavo Zanatta há 3 semanas atrás
pai
commit
a6d26f13bf

+ 6 - 0
src/api/supportRequest.js

@@ -0,0 +1,6 @@
+import api from 'src/api';
+
+export const createSupportRequest = async (payload) => {
+  const { data } = await api.post('/support-request', payload);
+  return data.payload;
+};

+ 35 - 9
src/components/profile/ProfileHelpDialog.vue

@@ -7,6 +7,17 @@
         <q-space />
         <span class="text-primary font16 fontbold gradient-diarista">{{ $t('profile.help.title') }}</span>
         <q-space />
+        <q-btn
+          flat
+          round
+          dense
+          icon="mdi-face-agent"
+          color="primary"
+          size="sm"
+          @click="openHumanSupport"
+        >
+          <q-tooltip>{{ $t('profile.help.suggestion_human') }}</q-tooltip>
+        </q-btn>
         <q-btn
           v-if="store.messages.length > 1"
           flat
@@ -17,7 +28,6 @@
           size="sm"
           @click="clearChat"
         />
-        <div v-else style="width: 32px"></div>
       </div>
 
       <div class="col overflow-auto">
@@ -79,11 +89,11 @@
             <div class="col-12 text-grey-6 q-mb-sm">{{ $t('profile.help.quick_suggestions') }}</div>
             <div
               v-for="suggestion in suggestions"
-              :key="suggestion"
+              :key="suggestion.value"
               class="row col-12 q-py-xs"
-              @click="sendMessage(suggestion)"
+              @click="onSuggestionClick(suggestion)"
             >
-              <span class="text-text bg-surface suggestion-btn font12 fontregular q-py-sm q-px-md cursor-pointer">{{ suggestion }}</span>
+              <span class="text-text bg-surface suggestion-btn font12 fontregular q-py-sm q-px-md cursor-pointer">{{ suggestion.label }}</span>
             </div>
           </div>
 
@@ -124,15 +134,17 @@
 
 <script setup>
 import { ref, computed, nextTick, onMounted } from 'vue';
-import { useDialogPluginComponent } from 'quasar';
+import { useDialogPluginComponent, useQuasar } from 'quasar';
 import { useI18n } from 'vue-i18n';
 import { chatbotStore } from 'src/stores/chatbot';
 import { sendChatMessage } from 'src/api/chatbot';
+import SupportRequestDialog from 'src/components/profile/SupportRequestDialog.vue';
 import diarinho_suporte from 'src/assets/diarinho_suporte.svg';
 
 defineEmits([...useDialogPluginComponent.emits]);
 
 const { dialogRef } = useDialogPluginComponent();
+const $q = useQuasar();
 const { t } = useI18n();
 const store = chatbotStore();
 
@@ -141,12 +153,26 @@ const loading = ref(false);
 const messagesContainer = ref(null);
 
 const suggestions = computed(() => [
-  t('profile.help.suggestion_cancel'),
-  t('profile.help.suggestion_data'),
-  t('profile.help.suggestion_payment'),
-  t('profile.help.suggestion_human'),
+  { label: t('profile.help.suggestion_cancel'), value: 'cancel' },
+  { label: t('profile.help.suggestion_data'), value: 'data' },
+  { label: t('profile.help.suggestion_payment'), value: 'payment' },
+  { label: t('profile.help.suggestion_human'), value: 'human' },
 ]);
 
+const onSuggestionClick = (suggestion) => {
+  if (suggestion.value === 'human') {
+    openHumanSupport();
+    return;
+  }
+  sendMessage(suggestion.label);
+};
+
+const openHumanSupport = () => {
+  $q.dialog({
+    component: SupportRequestDialog,
+  });
+};
+
 const scrollToBottom = async () => {
   await nextTick();
   if (messagesContainer.value) {

+ 241 - 0
src/components/profile/SupportRequestDialog.vue

@@ -0,0 +1,241 @@
+<template>
+  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
+    <div class="bg-page full-height column no-shadow support-dialog">
+
+      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
+        <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
+        <q-space />
+        <span class="text-subtitle1 text-primary font16 fontbold gradient-diarista">{{ $t('support_request.title') }}</span>
+        <q-space />
+        <div style="width: 32px"></div>
+      </div>
+
+      <div class="col column no-wrap q-px-md q-pt-lg overflow-auto">
+        <span class="text-grey-7 q-mb-lg font12 fontregular" style="line-height: 1.4;">
+          {{ $t('support_request.subtitle') }}
+        </span>
+
+        <q-form ref="formRef" class="column no-wrap q-gutter-y-md support-form" @submit="onSubmit">
+          <div>
+            <div class="text-text font12 fontbold q-mb-xs">{{ $t('support_request.fields.category') }}</div>
+            <q-select
+              v-model="form.category"
+              :options="categoryOptions"
+              :rules="[inputRules.required]"
+              emit-value
+              map-options
+              outlined
+              dense
+              input-class="text-text"
+              popup-content-class="text-text"
+            />
+          </div>
+
+          <div>
+            <div class="text-text font12 fontbold q-mb-xs">{{ $t('support_request.fields.title') }}</div>
+            <q-input
+              v-model="form.title"
+              :rules="[inputRules.required, inputRules.max(150)]"
+              outlined
+              dense
+              counter
+              maxlength="150"
+              input-class="text-text"
+            />
+          </div>
+
+          <div>
+            <div class="text-text font12 fontbold q-mb-xs">{{ $t('support_request.fields.message') }}</div>
+            <q-input
+              v-model="form.message"
+              type="textarea"
+              :rules="[inputRules.required]"
+              outlined
+              autogrow
+              input-class="text-text"
+              :input-style="{ minHeight: '90px' }"
+            />
+          </div>
+
+          <div class="text-grey-6 font10 fontregular row items-center q-gutter-x-xs">
+            <q-icon name="mdi-information-outline" size="14px" />
+            <span>{{ $t('support_request.chat_attached_note') }}</span>
+          </div>
+
+          <q-separator class="q-my-sm" />
+
+          <span class="text-dark font12 fontbold">{{ $t('support_request.contact_title') }}</span>
+          <span class="text-grey-6 font10 fontregular" style="line-height: 1.4;">
+            {{ $t('support_request.contact_subtitle') }}
+          </span>
+
+          <div>
+            <div class="text-text font12 fontbold q-mb-xs">{{ $t('support_request.fields.name') }}</div>
+            <q-input
+              v-model="form.name"
+              outlined
+              dense
+              input-class="text-text"
+            />
+          </div>
+          <div>
+            <div class="text-text font12 fontbold q-mb-xs">{{ $t('support_request.fields.email') }}</div>
+            <q-input
+              v-model="form.email"
+              type="email"
+              :rules="form.email ? [inputRules.email] : []"
+              outlined
+              dense
+              input-class="text-text"
+            />
+          </div>
+          <div>
+            <div class="text-text font12 fontbold q-mb-xs">{{ $t('support_request.fields.phone') }}</div>
+            <q-input
+              v-model="form.phone"
+              outlined
+              dense
+              input-class="text-text"
+            />
+          </div>
+
+          <q-btn
+            type="submit"
+            unelevated
+            rounded
+            no-caps
+            color="primary"
+            class="full-width save-btn q-mt-md"
+            padding="10px 12px"
+            :label="$t('support_request.actions.send')"
+            :loading="loading"
+          />
+        </q-form>
+      </div>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { useDialogPluginComponent, useQuasar } from 'quasar';
+import { useI18n } from 'vue-i18n';
+import { useInputRules } from 'src/composables/useInputRules';
+import { userStore } from 'src/stores/user';
+import { chatbotStore } from 'src/stores/chatbot';
+import { createSupportRequest } from 'src/api/supportRequest';
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogOK } = useDialogPluginComponent();
+const $q = useQuasar();
+const { t } = useI18n();
+const { inputRules } = useInputRules();
+const user = userStore();
+const chatbot = chatbotStore();
+
+const formRef = ref(null);
+const loading = ref(false);
+
+const form = reactive({
+  category: null,
+  title: '',
+  message: '',
+  name: user.user?.name || '',
+  email: user.user?.email || '',
+  phone: user.user?.phone || '',
+});
+
+const categoryOptions = [
+  { label: t('support_request.categories.payment'), value: 'payment' },
+  { label: t('support_request.categories.schedule'), value: 'schedule' },
+  { label: t('support_request.categories.account'), value: 'account' },
+  { label: t('support_request.categories.other'), value: 'other' },
+];
+
+const buildTranscript = () => {
+  const lines = chatbot.messages.map((msg) => {
+    const who = msg.role === 'user' ? t('support_request.transcript.you') : t('support_request.transcript.bot');
+    return `${who}: ${msg.text}`;
+  });
+  return lines.join('\n');
+};
+
+const buildMessage = () => {
+  const transcript = buildTranscript();
+  const header = t('support_request.transcript.chat_header');
+  const userHeader = t('support_request.transcript.user_message_header');
+  const parts = [];
+  if (transcript.trim()) {
+    parts.push(`${header}\n${transcript}`);
+  }
+  parts.push(`${userHeader}\n${form.message.trim()}`);
+  return parts.join('\n\n');
+};
+
+const onSubmit = async () => {
+  loading.value = true;
+  try {
+    await createSupportRequest({
+      category: form.category,
+      title: form.title.trim(),
+      message: buildMessage(),
+      name: form.name?.trim() || null,
+      email: form.email?.trim() || null,
+      phone: form.phone?.trim() || null,
+    });
+    $q.notify({ type: 'positive', message: t('support_request.success') });
+    onDialogOK(true);
+  } catch (error) {
+    console.error(error);
+    $q.notify({ type: 'negative', message: t('support_request.error') });
+  } finally {
+    loading.value = false;
+  }
+};
+</script>
+
+<style scoped lang="scss">
+// Fonte consistente entre os dois apps e alinhada às telas (o dialog é teleportado
+// para fora do .q-page, então não herda a fonte definida lá).
+.support-dialog {
+  font-family: "Inter", sans-serif;
+
+  // Inputs, textareas e o texto interno dos campos NÃO herdam font-family do navegador
+  // por padrão. Sem isso, os valores dos campos caem na fonte do body (Roboto) e ficam
+  // diferentes dos labels — e diferentes entre os apps. Aqui travamos tudo na mesma fonte.
+  :deep(.q-field),
+  :deep(.q-field__native),
+  :deep(.q-field__input),
+  :deep(.q-field__label),
+  :deep(.q-field__prefix),
+  :deep(.q-field__suffix),
+  :deep(input),
+  :deep(textarea),
+  :deep(.q-item__label),
+  :deep(.q-btn),
+  :deep(.q-btn__content) {
+    font-family: "Inter", sans-serif;
+  }
+}
+
+.support-form {
+  // A seta do select herda cor clara/branca no tema dos apps e fica invisível;
+  // forçamos uma cor visível para deixar explícito que é um campo de seleção.
+  :deep(.q-select__dropdown-icon) {
+    color: #555555 !important;
+  }
+
+  :deep(.q-field__native),
+  :deep(.q-field__native span),
+  :deep(.q-field__input),
+  :deep(textarea.q-field__native) {
+    color: #555555 !important;
+  }
+
+  :deep(.q-field__native::placeholder),
+  :deep(.q-field__input::placeholder) {
+    color: #9e9e9e !important;
+  }
+}
+</style>

+ 33 - 1
src/i18n/locales/en.json

@@ -797,5 +797,37 @@
         }
       }
     }
+  },
+  "support_request": {
+    "title": "Talk to an agent",
+    "subtitle": "Fill in the fields below and a human agent will get in touch with you.",
+    "contact_title": "How can we reach you?",
+    "contact_subtitle": "We prefilled it with your account details. Change it if you prefer to be contacted through another channel.",
+    "chat_attached_note": "Your recent conversation with the assistant will be attached to give the agent context.",
+    "success": "Request sent! An agent will contact you soon.",
+    "error": "We could not send your request. Please try again.",
+    "fields": {
+      "category": "Category",
+      "title": "Subject",
+      "message": "Your message",
+      "name": "Name",
+      "email": "Email",
+      "phone": "Phone"
+    },
+    "categories": {
+      "payment": "Payment",
+      "schedule": "Schedule",
+      "account": "Account",
+      "other": "Other"
+    },
+    "actions": {
+      "send": "Send request"
+    },
+    "transcript": {
+      "you": "You",
+      "bot": "Assistant",
+      "chat_header": "--- Conversation with the assistant ---",
+      "user_message_header": "--- User message ---"
+    }
   }
-}
+}

+ 33 - 1
src/i18n/locales/es.json

@@ -797,5 +797,37 @@
         }
       }
     }
+  },
+  "support_request": {
+    "title": "Hablar con un agente",
+    "subtitle": "Completa los campos a continuación y un agente humano se pondrá en contacto contigo.",
+    "contact_title": "¿Cómo podemos contactarte?",
+    "contact_subtitle": "Lo completamos con tus datos de cuenta. Cámbialo si prefieres que te contacten por otro canal.",
+    "chat_attached_note": "Tu conversación reciente con el asistente se adjuntará para dar contexto al agente.",
+    "success": "¡Solicitud enviada! Un agente se pondrá en contacto pronto.",
+    "error": "No pudimos enviar tu solicitud. Inténtalo de nuevo.",
+    "fields": {
+      "category": "Categoría",
+      "title": "Asunto",
+      "message": "Tu mensaje",
+      "name": "Nombre",
+      "email": "Correo",
+      "phone": "Teléfono"
+    },
+    "categories": {
+      "payment": "Pago",
+      "schedule": "Agenda",
+      "account": "Cuenta",
+      "other": "Otro"
+    },
+    "actions": {
+      "send": "Enviar solicitud"
+    },
+    "transcript": {
+      "you": "Tú",
+      "bot": "Asistente",
+      "chat_header": "--- Conversación con el asistente ---",
+      "user_message_header": "--- Mensaje del usuario ---"
+    }
   }
-}
+}

+ 83 - 51
src/i18n/locales/pt.json

@@ -179,60 +179,60 @@
           "rg": "RG"
         },
         "step_4": {
-            "document_front": "Lado 1 (Frente)",
-            "document_front_desc": "Foto da frente do seu documento (RG ou CNH).",
-            "document_back": "Lado 2 (Verso)",
-            "document_back_desc": "Foto do verso do seu documento (RG ou CNH).",
-            "selfie": "Selfie com documento",
-            "selfie_desc": "Tire uma selfie segurando o seu documento próximo ao rosto.",
-            "photo_captured": "Foto capturada!",
-            "action": "enviar documentos",
-            "btn_selfie": "Tirar selfie",
-            "selfie_hint": "Escolha um ambiente iluminado",
-            "selfie_sent": "Selfie enviada!",
-            "btn_document": "Fotografar documento",
-            "document_hint": "RG com CPF, frente e verso",
-            "document_sent": "Documento enviado!",
-            "btn_retake": "Tirar outra foto",
-            "btn_confirm": "Usar esta selfie",
-            "btn_use_photo": "Usar esta foto",
-            "error_message": "Não foi possível processar a imagem. Tente novamente.",
-            "selfie_success": "Selfie enviada com sucesso!",
-            "document_success": "Documento enviado com sucesso!",
-            "btn_retry": "Tentar novamente",
-            "btn_continue": "Continuar",
-            "document_hint_result": "Continue para enviar o verso do documento",
-            "btn_capture_selfie": "Tirar selfie",
-            "btn_capture_front": "Fotografar frente",
-            "btn_capture_back": "Fotografar verso",
-            "upload_all_photos": "Anexe todas as fotos necessárias!"
+          "document_front": "Lado 1 (Frente)",
+          "document_front_desc": "Foto da frente do seu documento (RG ou CNH).",
+          "document_back": "Lado 2 (Verso)",
+          "document_back_desc": "Foto do verso do seu documento (RG ou CNH).",
+          "selfie": "Selfie com documento",
+          "selfie_desc": "Tire uma selfie segurando o seu documento próximo ao rosto.",
+          "photo_captured": "Foto capturada!",
+          "action": "enviar documentos",
+          "btn_selfie": "Tirar selfie",
+          "selfie_hint": "Escolha um ambiente iluminado",
+          "selfie_sent": "Selfie enviada!",
+          "btn_document": "Fotografar documento",
+          "document_hint": "RG com CPF, frente e verso",
+          "document_sent": "Documento enviado!",
+          "btn_retake": "Tirar outra foto",
+          "btn_confirm": "Usar esta selfie",
+          "btn_use_photo": "Usar esta foto",
+          "error_message": "Não foi possível processar a imagem. Tente novamente.",
+          "selfie_success": "Selfie enviada com sucesso!",
+          "document_success": "Documento enviado com sucesso!",
+          "btn_retry": "Tentar novamente",
+          "btn_continue": "Continuar",
+          "document_hint_result": "Continue para enviar o verso do documento",
+          "btn_capture_selfie": "Tirar selfie",
+          "btn_capture_front": "Fotografar frente",
+          "btn_capture_back": "Fotografar verso",
+          "upload_all_photos": "Anexe todas as fotos necessárias!"
         },
         "step_5": {
-            "daily_price_title": "Qual valor da sua diária de até 8 horas?",
-            "daily_price_min_max": "Valor mínimo R$ 3,00. Valor máximo R$ 500,00.",
-            "dont_worry": "Não se preocupe!",
-            "change_anytime": "Você pode alterar o valor da sua diária quando quiser ;)",
-            "shorter_services": "Baseado no valor acima, os valores de serviços mais curtos serão:",
-            "up_to_6h": "até 6 horas",
-            "up_to_4h": "até 4 horas",
-            "up_to_2h": "até 2 horas",
-            "other_services": "Além da limpeza básica, quais serviços você também realiza?",
-            "change_in_profile": "(poderá ser alterado no perfil)",
-            "no_services": "Nenhum tipo de serviço disponível no momento.",
-            "search_visibility": "Essas informações aparecerão para quem buscar o seu perfil na busca.",
-            "action": "continuar"
+          "daily_price_title": "Qual valor da sua diária de até 8 horas?",
+          "daily_price_min_max": "Valor mínimo R$ 3,00. Valor máximo R$ 500,00.",
+          "dont_worry": "Não se preocupe!",
+          "change_anytime": "Você pode alterar o valor da sua diária quando quiser ;)",
+          "shorter_services": "Baseado no valor acima, os valores de serviços mais curtos serão:",
+          "up_to_6h": "até 6 horas",
+          "up_to_4h": "até 4 horas",
+          "up_to_2h": "até 2 horas",
+          "other_services": "Além da limpeza básica, quais serviços você também realiza?",
+          "change_in_profile": "(poderá ser alterado no perfil)",
+          "no_services": "Nenhum tipo de serviço disponível no momento.",
+          "search_visibility": "Essas informações aparecerão para quem buscar o seu perfil na busca.",
+          "action": "continuar"
         },
         "step_6": {
-            "title": "Agora vamos escolher quais melhor horários para você receber solicitações de serviços!",
-            "lock_hint": "Toque e bloqueie os dias ou períodos",
-            "lock_description": "se você não deseja receber solicitações de serviços neste período.",
-            "morning": "manhã",
-            "afternoon": "tarde",
-            "instruction": "Para bloquear dias individuais, clique no dia, para bloquear apenas períodos, clique em manhã ou tarde.",
-            "dont_worry": "Não se preocupe!",
-            "change_anytime": "Depois você poderá alterar a disponibilidade de sua preferência a qualquer momento no app ;)",
-            "select_at_least_one": "Selecione ao menos um período de trabalho.",
-            "action": "concluir cadastro"
+          "title": "Agora vamos escolher quais melhor horários para você receber solicitações de serviços!",
+          "lock_hint": "Toque e bloqueie os dias ou períodos",
+          "lock_description": "se você não deseja receber solicitações de serviços neste período.",
+          "morning": "manhã",
+          "afternoon": "tarde",
+          "instruction": "Para bloquear dias individuais, clique no dia, para bloquear apenas períodos, clique em manhã ou tarde.",
+          "dont_worry": "Não se preocupe!",
+          "change_anytime": "Depois você poderá alterar a disponibilidade de sua preferência a qualquer momento no app ;)",
+          "select_at_least_one": "Selecione ao menos um período de trabalho.",
+          "action": "concluir cadastro"
         }
       },
       "pending_approval": {
@@ -800,5 +800,37 @@
         }
       }
     }
+  },
+  "support_request": {
+    "title": "Falar com atendente",
+    "subtitle": "Preencha os campos abaixo e um atendente humano entrará em contato com você.",
+    "contact_title": "Como podemos te contatar?",
+    "contact_subtitle": "Já preenchemos com seus dados de cadastro. Altere se preferir ser contatado por outro canal.",
+    "chat_attached_note": "Sua conversa recente com o assistente será anexada para dar contexto ao atendente.",
+    "success": "Solicitação enviada! Em breve um atendente entrará em contato.",
+    "error": "Não foi possível enviar sua solicitação. Tente novamente.",
+    "fields": {
+      "category": "Categoria",
+      "title": "Assunto",
+      "message": "Sua mensagem",
+      "name": "Nome",
+      "email": "E-mail",
+      "phone": "Telefone"
+    },
+    "categories": {
+      "payment": "Pagamento",
+      "schedule": "Agendamento",
+      "account": "Conta",
+      "other": "Outro"
+    },
+    "actions": {
+      "send": "Enviar solicitação"
+    },
+    "transcript": {
+      "you": "Você",
+      "bot": "Assistente",
+      "chat_header": "--- Conversa com o assistente ---",
+      "user_message_header": "--- Mensagem do usuário ---"
+    }
   }
-}
+}