AddEditTicketDialog.vue 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. <template>
  2. <q-dialog ref="dialogRef" @hide="onDialogHide">
  3. <div style="width: 100%; max-width: 1100px">
  4. <q-card style="height: 500px; display: flex; flex-direction: column; overflow: hidden">
  5. <DefaultDialogHeader
  6. :title="ticket ? 'Editar Ticket' : 'Novo Ticket'"
  7. @close="onDialogCancel"
  8. />
  9. <q-form
  10. ref="formRef"
  11. style="
  12. flex: 1;
  13. display: flex;
  14. flex-direction: column;
  15. overflow: hidden;
  16. "
  17. @submit="onOKClick"
  18. >
  19. <q-card-section class="q-pt-sm" style="flex: 1; overflow-y: auto; min-height: 0">
  20. <CustomTabComponent
  21. v-if="ticket?.id"
  22. v-model:active-tab="currentTab"
  23. :tabs="tabs"
  24. class="q-mb-md"
  25. />
  26. <!-- Tab: Ticket -->
  27. <div v-show="currentTab === 'ticket'">
  28. <div class="row q-col-gutter-sm">
  29. <DefaultInput
  30. v-model="form.title"
  31. label="Título da Tarefa"
  32. class="col-12"
  33. />
  34. <DefaultSelect
  35. v-model="form.severity"
  36. label="Prioridade"
  37. :options="priorityOptions"
  38. emit-value
  39. map-options
  40. class="col-6"
  41. />
  42. <DefaultInput
  43. :model-value="user?.name"
  44. label="Responsável"
  45. disable
  46. class="col-6"
  47. />
  48. <DefaultSelect
  49. v-model="form.scope"
  50. label="Destino"
  51. :options="unitTargetOptions"
  52. emit-value
  53. map-options
  54. class="col-6"
  55. />
  56. <DefaultInput
  57. v-model="form.sector"
  58. label="Setor"
  59. class="col-6"
  60. />
  61. <DefaultInput
  62. v-model="form.description"
  63. label="Descrição"
  64. type="textarea"
  65. class="col-12"
  66. />
  67. </div>
  68. </div>
  69. <!-- Tab: Comentários -->
  70. <div v-show="currentTab === 'comentarios'">
  71. <div
  72. v-if="canAdd && ticket?.status === 'in_progress'"
  73. class="flex justify-end q-mb-sm"
  74. >
  75. <q-btn
  76. color="primary"
  77. icon="mdi-plus"
  78. unelevated
  79. style="width: 40px; height: 40px"
  80. @click="onAddComment"
  81. />
  82. </div>
  83. <div
  84. style="
  85. height: 100%;
  86. max-height: 340px;
  87. overflow-y: auto;
  88. display: flex;
  89. flex-direction: column;
  90. gap: 8px;
  91. "
  92. >
  93. <template v-if="replies.length">
  94. <TicketCommentCard
  95. v-for="reply in replies"
  96. :key="reply.id"
  97. :reply="reply.reply"
  98. :created-at="reply.created_at"
  99. :user-name="reply.user_name"
  100. @edit="onEditComment(reply)"
  101. @delete="onDeleteComment(reply)"
  102. />
  103. </template>
  104. <div
  105. v-else
  106. class="flex flex-center full-height text-grey-5 text-body2"
  107. >
  108. Nenhum comentário registrado.
  109. </div>
  110. </div>
  111. </div>
  112. </q-card-section>
  113. <q-card-actions align="right" class="q-px-md q-pb-md" style="flex-shrink: 0">
  114. <q-btn
  115. outline
  116. color="primary"
  117. label="Cancelar"
  118. @click="onDialogCancel"
  119. />
  120. <q-btn
  121. v-if="canEdit && canManage && ticket?.id && ticket?.status === 'in_progress'"
  122. outline
  123. color="negative"
  124. label="Encerrar"
  125. @click="onCloseTicket"
  126. />
  127. <q-btn
  128. v-if="canSave && canManage && (!ticket?.id || ticket?.status === 'in_progress')"
  129. color="primary"
  130. label="Salvar"
  131. type="submit"
  132. :loading="loading"
  133. />
  134. </q-card-actions>
  135. </q-form>
  136. </q-card>
  137. </div>
  138. </q-dialog>
  139. </template>
  140. <script setup>
  141. import { ref, computed, onMounted } from "vue";
  142. import { useDialogPluginComponent } from "quasar";
  143. import CustomTabComponent from "src/components/shared/CustomTabComponent.vue";
  144. import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
  145. import DefaultInput from "src/components/defaults/DefaultInput.vue";
  146. import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
  147. import TicketCommentCard from "./TicketCommentCard.vue";
  148. import { userStore } from "src/stores/user";
  149. import { useQuasar } from "quasar";
  150. import {
  151. createSupportTicket,
  152. updateSupportTicket,
  153. } from "src/api/support_ticket";
  154. import CloseTicketDialog from "./CloseTicketDialog.vue";
  155. import AddEditReplyDialog from "./AddEditReplyDialog.vue";
  156. import { getSupportReplies, deleteSupportReply } from "src/api/support_reply";
  157. import { permissionStore } from "src/stores/permission";
  158. defineEmits([...useDialogPluginComponent.emits]);
  159. const { ticket } = defineProps({
  160. ticket: {
  161. type: Object,
  162. default: null,
  163. },
  164. });
  165. const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
  166. useDialogPluginComponent();
  167. const $q = useQuasar();
  168. const { user } = userStore();
  169. const permissions = permissionStore();
  170. const canAdd = computed(() => permissions.getAccess("franchisee_support", "add"));
  171. const canEdit = computed(() => permissions.getAccess("franchisee_support", "edit"));
  172. const canSave = computed(() => ticket?.id ? canEdit.value : canAdd.value);
  173. const formRef = ref(null);
  174. const loading = ref(false);
  175. const currentTab = ref("ticket");
  176. // Novo ticket (sem id) → pode salvar. Ticket existente → só se for interno próprio.
  177. const canManage = computed(() =>
  178. !ticket?.id || (ticket?.origin === "unit" && ticket?.scope === "internal")
  179. );
  180. const tabs = computed(() => {
  181. const base = [{ name: "ticket", label: "Ticket" }];
  182. if (ticket?.id) base.push({ name: "comentarios", label: "Comentários" });
  183. return base;
  184. });
  185. const replies = ref([]);
  186. const loadReplies = async () => {
  187. if (!ticket?.id) return;
  188. replies.value = await getSupportReplies(ticket.id);
  189. };
  190. const priorityOptions = [
  191. { label: "Alta", value: "alta" },
  192. { label: "Normal", value: "normal" },
  193. { label: "Baixa", value: "baixa" },
  194. ];
  195. const unitTargetOptions = [
  196. { label: "Suporte à Matriz", value: "specific" },
  197. { label: "Suporte Interno", value: "internal" },
  198. ];
  199. const form = ref({
  200. title: ticket?.title ?? null,
  201. severity: ticket?.severity ?? null,
  202. scope: ticket?.scope ?? null,
  203. sector: ticket?.sector ?? null,
  204. description: ticket?.description ?? null,
  205. });
  206. const buildPayload = () => ({
  207. title: form.value.title,
  208. severity: form.value.severity,
  209. scope: form.value.scope,
  210. sector: form.value.sector || null,
  211. description: form.value.description || null,
  212. target_unit_id: null, // Backend resolve baseado em scope + user.unit_id
  213. });
  214. const onAddComment = () => {
  215. $q.dialog({
  216. component: AddEditReplyDialog,
  217. componentProps: { ticketId: ticket.id },
  218. }).onOk(() => {
  219. loadReplies();
  220. });
  221. };
  222. const onEditComment = (reply) => {
  223. $q.dialog({
  224. component: AddEditReplyDialog,
  225. componentProps: { ticketId: ticket.id, replyItem: reply },
  226. }).onOk(() => {
  227. loadReplies();
  228. });
  229. };
  230. const onDeleteComment = (reply) => {
  231. $q.dialog({
  232. title: "Excluir Comentário",
  233. message: "Tem certeza que deseja excluir este comentário?",
  234. cancel: { outline: true, color: "primary", label: "Cancelar" },
  235. ok: { color: "negative", label: "Excluir" },
  236. }).onOk(async () => {
  237. await deleteSupportReply(ticket.id, reply.id);
  238. loadReplies();
  239. });
  240. };
  241. onMounted(() => {
  242. loadReplies();
  243. });
  244. const onCloseTicket = () => {
  245. $q.dialog({
  246. component: CloseTicketDialog,
  247. componentProps: { ticket },
  248. }).onOk(() => {
  249. onDialogOK(true);
  250. });
  251. };
  252. const onOKClick = async () => {
  253. loading.value = true;
  254. try {
  255. const payload = buildPayload();
  256. if (ticket?.id) {
  257. await updateSupportTicket(ticket.id, payload);
  258. } else {
  259. await createSupportTicket(payload);
  260. }
  261. onDialogOK(true);
  262. } finally {
  263. loading.value = false;
  264. }
  265. };
  266. </script>