|
|
@@ -0,0 +1,302 @@
|
|
|
+<template>
|
|
|
+ <q-dialog ref="dialogRef" @hide="onDialogHide">
|
|
|
+ <div style="width: 100%; max-width: 1100px">
|
|
|
+ <q-card style="height: 500px; display: flex; flex-direction: column; overflow: hidden">
|
|
|
+ <DefaultDialogHeader
|
|
|
+ :title="ticket ? 'Editar Ticket' : 'Novo Ticket'"
|
|
|
+ @close="onDialogCancel"
|
|
|
+ />
|
|
|
+
|
|
|
+ <q-form
|
|
|
+ ref="formRef"
|
|
|
+ style="
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+ "
|
|
|
+ @submit="onOKClick"
|
|
|
+ >
|
|
|
+ <q-card-section class="q-pt-sm" style="flex: 1; overflow-y: auto; min-height: 0">
|
|
|
+ <CustomTabComponent
|
|
|
+ v-if="ticket?.id"
|
|
|
+ v-model:active-tab="currentTab"
|
|
|
+ :tabs="tabs"
|
|
|
+ class="q-mb-md"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- Tab: Ticket -->
|
|
|
+ <div v-show="currentTab === 'ticket'">
|
|
|
+ <div class="row q-col-gutter-sm">
|
|
|
+ <DefaultInput
|
|
|
+ v-model="form.title"
|
|
|
+ label="Título da Tarefa"
|
|
|
+ class="col-12"
|
|
|
+ />
|
|
|
+
|
|
|
+ <DefaultSelect
|
|
|
+ v-model="form.severity"
|
|
|
+ label="Prioridade"
|
|
|
+ :options="priorityOptions"
|
|
|
+ emit-value
|
|
|
+ map-options
|
|
|
+ class="col-6"
|
|
|
+ />
|
|
|
+
|
|
|
+ <DefaultInput
|
|
|
+ :model-value="user?.name"
|
|
|
+ label="Responsável"
|
|
|
+ disable
|
|
|
+ class="col-6"
|
|
|
+ />
|
|
|
+
|
|
|
+ <DefaultSelect
|
|
|
+ v-model="form.scope"
|
|
|
+ label="Destino"
|
|
|
+ :options="unitTargetOptions"
|
|
|
+ emit-value
|
|
|
+ map-options
|
|
|
+ class="col-6"
|
|
|
+ />
|
|
|
+
|
|
|
+ <DefaultInput
|
|
|
+ v-model="form.sector"
|
|
|
+ label="Setor"
|
|
|
+ class="col-6"
|
|
|
+ />
|
|
|
+
|
|
|
+ <UnitSelect
|
|
|
+ v-if="form.scope === 'specific'"
|
|
|
+ v-model="selectedUnit"
|
|
|
+ class="col-6"
|
|
|
+ />
|
|
|
+
|
|
|
+ <DefaultInput
|
|
|
+ v-model="form.description"
|
|
|
+ label="Descrição"
|
|
|
+ type="textarea"
|
|
|
+ class="col-12"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Tab: Comentários -->
|
|
|
+ <div v-show="currentTab === 'comentarios'">
|
|
|
+ <div
|
|
|
+ v-if="ticket?.status === 'in_progress'"
|
|
|
+ class="flex justify-end q-mb-sm"
|
|
|
+ >
|
|
|
+ <q-btn
|
|
|
+ color="primary"
|
|
|
+ icon="mdi-plus"
|
|
|
+ unelevated
|
|
|
+ style="width: 40px; height: 40px"
|
|
|
+ @click="onAddComment"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ style="
|
|
|
+ height: 100%;
|
|
|
+ max-height: 340px;
|
|
|
+ overflow-y: auto;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ "
|
|
|
+ >
|
|
|
+ <template v-if="replies.length">
|
|
|
+ <TicketCommentCard
|
|
|
+ v-for="reply in replies"
|
|
|
+ :key="reply.id"
|
|
|
+ :reply="reply.reply"
|
|
|
+ :created-at="reply.created_at"
|
|
|
+ :user-name="reply.user_name"
|
|
|
+ @edit="onEditComment(reply)"
|
|
|
+ @delete="onDeleteComment(reply)"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ <div
|
|
|
+ v-else
|
|
|
+ class="flex flex-center full-height text-grey-5 text-body2"
|
|
|
+ >
|
|
|
+ Nenhum comentário registrado.
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </q-card-section>
|
|
|
+
|
|
|
+ <q-card-actions align="right" class="q-px-md q-pb-md" style="flex-shrink: 0">
|
|
|
+ <q-btn
|
|
|
+ outline
|
|
|
+ color="primary"
|
|
|
+ label="Cancelar"
|
|
|
+ @click="onDialogCancel"
|
|
|
+ />
|
|
|
+ <q-btn
|
|
|
+ v-if="ticket?.id && ticket?.status === 'in_progress'"
|
|
|
+ outline
|
|
|
+ color="negative"
|
|
|
+ label="Encerrar"
|
|
|
+ @click="onCloseTicket"
|
|
|
+ />
|
|
|
+ <q-btn
|
|
|
+ v-if="!ticket?.id || ticket?.status === 'in_progress'"
|
|
|
+ color="primary"
|
|
|
+ label="Salvar"
|
|
|
+ type="submit"
|
|
|
+ :loading="loading"
|
|
|
+ />
|
|
|
+ </q-card-actions>
|
|
|
+ </q-form>
|
|
|
+ </q-card>
|
|
|
+ </div>
|
|
|
+ </q-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed, onMounted } from "vue";
|
|
|
+import { useDialogPluginComponent } from "quasar";
|
|
|
+
|
|
|
+import CustomTabComponent from "src/components/shared/CustomTabComponent.vue";
|
|
|
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
|
|
|
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
|
|
|
+import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
|
|
|
+import UnitSelect from "src/components/selects/UnitSelect.vue";
|
|
|
+import TicketCommentCard from "./TicketCommentCard.vue";
|
|
|
+import { userStore } from "src/stores/user";
|
|
|
+import { useQuasar } from "quasar";
|
|
|
+import {
|
|
|
+ createSupportTicket,
|
|
|
+ updateSupportTicket,
|
|
|
+} from "src/api/support_ticket";
|
|
|
+import CloseTicketDialog from "./CloseTicketDialog.vue";
|
|
|
+import AddEditReplyDialog from "./AddEditReplyDialog.vue";
|
|
|
+import { getSupportReplies, deleteSupportReply } from "src/api/support_reply";
|
|
|
+
|
|
|
+defineEmits([...useDialogPluginComponent.emits]);
|
|
|
+
|
|
|
+const { ticket } = defineProps({
|
|
|
+ ticket: {
|
|
|
+ type: Object,
|
|
|
+ default: null,
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
|
|
|
+ useDialogPluginComponent();
|
|
|
+
|
|
|
+const $q = useQuasar();
|
|
|
+const { user } = userStore();
|
|
|
+
|
|
|
+const formRef = ref(null);
|
|
|
+const loading = ref(false);
|
|
|
+const currentTab = ref("ticket");
|
|
|
+const selectedUnit = ref(ticket?.target_unit_id ? { value: ticket.target_unit_id } : null);
|
|
|
+
|
|
|
+const tabs = computed(() => {
|
|
|
+ const base = [{ name: "ticket", label: "Ticket" }];
|
|
|
+ if (ticket?.id) base.push({ name: "comentarios", label: "Comentários" });
|
|
|
+ return base;
|
|
|
+});
|
|
|
+
|
|
|
+const replies = ref([]);
|
|
|
+
|
|
|
+const loadReplies = async () => {
|
|
|
+ if (!ticket?.id) return;
|
|
|
+ replies.value = await getSupportReplies(ticket.id);
|
|
|
+};
|
|
|
+
|
|
|
+const priorityOptions = [
|
|
|
+ { label: "Alta", value: "alta" },
|
|
|
+ { label: "Normal", value: "normal" },
|
|
|
+ { label: "Baixa", value: "baixa" },
|
|
|
+];
|
|
|
+
|
|
|
+const unitTargetOptions = [
|
|
|
+ { label: "Todas as Unidades", value: "all" },
|
|
|
+ { label: "Suporte Interno", value: "internal" },
|
|
|
+ { label: "Unidade Específica", value: "specific" },
|
|
|
+];
|
|
|
+
|
|
|
+const form = ref({
|
|
|
+ title: ticket?.title ?? null,
|
|
|
+ severity: ticket?.severity ?? null,
|
|
|
+ scope: ticket?.scope ?? null,
|
|
|
+ sector: ticket?.sector ?? null,
|
|
|
+ description: ticket?.description ?? null,
|
|
|
+});
|
|
|
+
|
|
|
+const buildPayload = () => {
|
|
|
+ const payload = {
|
|
|
+ title: form.value.title,
|
|
|
+ severity: form.value.severity,
|
|
|
+ scope: form.value.scope,
|
|
|
+ sector: form.value.sector || null,
|
|
|
+ description: form.value.description || null,
|
|
|
+ };
|
|
|
+
|
|
|
+ if (form.value.scope === "specific") {
|
|
|
+ payload.target_unit_id = selectedUnit.value?.value ?? null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return payload;
|
|
|
+};
|
|
|
+
|
|
|
+const onAddComment = () => {
|
|
|
+ $q.dialog({
|
|
|
+ component: AddEditReplyDialog,
|
|
|
+ componentProps: { ticketId: ticket.id },
|
|
|
+ }).onOk(() => {
|
|
|
+ loadReplies();
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const onEditComment = (reply) => {
|
|
|
+ $q.dialog({
|
|
|
+ component: AddEditReplyDialog,
|
|
|
+ componentProps: { ticketId: ticket.id, replyItem: reply },
|
|
|
+ }).onOk(() => {
|
|
|
+ loadReplies();
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const onDeleteComment = (reply) => {
|
|
|
+ $q.dialog({
|
|
|
+ title: "Excluir Comentário",
|
|
|
+ message: "Tem certeza que deseja excluir este comentário?",
|
|
|
+ cancel: { outline: true, color: "primary", label: "Cancelar" },
|
|
|
+ ok: { color: "negative", label: "Excluir" },
|
|
|
+ }).onOk(async () => {
|
|
|
+ await deleteSupportReply(ticket.id, reply.id);
|
|
|
+ loadReplies();
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ loadReplies();
|
|
|
+});
|
|
|
+
|
|
|
+const onCloseTicket = () => {
|
|
|
+ $q.dialog({
|
|
|
+ component: CloseTicketDialog,
|
|
|
+ componentProps: { ticket },
|
|
|
+ }).onOk(() => {
|
|
|
+ onDialogOK(true);
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const onOKClick = async () => {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const payload = buildPayload();
|
|
|
+ if (ticket?.id) {
|
|
|
+ await updateSupportTicket(ticket.id, payload);
|
|
|
+ } else {
|
|
|
+ await createSupportTicket(payload);
|
|
|
+ }
|
|
|
+ onDialogOK(true);
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|