Kaynağa Gözat

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 3 hafta önce
ebeveyn
işleme
fbec85b1e6

+ 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-subtitle1 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 q-py-sm q-px-md cursor-pointer">{{ suggestion }}</span>
+              <span class="text-text bg-surface suggestion-btn 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

@@ -900,5 +900,37 @@
         "btn_keep": "Cancel the service"
       }
     }
+  },
+  "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

@@ -896,5 +896,37 @@
         "btn_keep": "Cancelar el servicio"
       }
     }
+  },
+  "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 ---"
+    }
   }
-}
+}

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

@@ -913,5 +913,37 @@
     "title": "Notificações",
     "unread": "Não lidas",
     "mark_all_read": " Marcar todas como lidas"
+  },
+  "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 ---"
+    }
   }
-}
+}