|
|
@@ -0,0 +1,300 @@
|
|
|
+<template>
|
|
|
+ <q-dialog ref="dialogRef" @hide="onDialogHide">
|
|
|
+ <div style="width: 100%; max-width: 1100px">
|
|
|
+ <q-card style="height: 560px; display: flex; flex-direction: column; overflow: hidden">
|
|
|
+ <DefaultDialogHeader
|
|
|
+ :title="card ? 'Editar Atividade' : 'Adicionar Atividade'"
|
|
|
+ @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="card?.id"
|
|
|
+ v-model:active-tab="currentTab"
|
|
|
+ :tabs="tabs"
|
|
|
+ class="q-mb-md"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- Tab: Atividade -->
|
|
|
+ <div v-show="currentTab === 'atividade'">
|
|
|
+ <div class="row q-col-gutter-sm">
|
|
|
+ <!-- 1. Título -->
|
|
|
+ <DefaultInput
|
|
|
+ v-model="form.title"
|
|
|
+ label="Título da Tarefa"
|
|
|
+ class="col-12"
|
|
|
+ :rules="[val => !!val || 'Campo obrigatório']"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 2. Destino (primeiro campo de configuração) -->
|
|
|
+ <DefaultSelect
|
|
|
+ v-model="form.scope"
|
|
|
+ label="Destino"
|
|
|
+ :options="scopeOptions"
|
|
|
+ emit-value
|
|
|
+ map-options
|
|
|
+ class="col-6"
|
|
|
+ :rules="[val => !!val || 'Campo obrigatório']"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- Prazo ao lado do Destino -->
|
|
|
+ <DefaultInputDatePicker
|
|
|
+ v-model="form.due_date_display"
|
|
|
+ v-model:untreated-date="form.due_date"
|
|
|
+ label="Prazo de Entrega"
|
|
|
+ class="col-6"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- Unidade específica — só aparece quando scope = specific -->
|
|
|
+ <UnitSelect
|
|
|
+ v-if="form.scope === 'specific'"
|
|
|
+ v-model="selectedUnit"
|
|
|
+ class="col-6"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 3. Prioridade + Fase -->
|
|
|
+ <DefaultSelect
|
|
|
+ v-model="form.priority"
|
|
|
+ label="Prioridade"
|
|
|
+ :options="priorityOptions"
|
|
|
+ emit-value
|
|
|
+ map-options
|
|
|
+ class="col-6"
|
|
|
+ :rules="[val => !!val || 'Campo obrigatório']"
|
|
|
+ />
|
|
|
+
|
|
|
+ <DefaultSelect
|
|
|
+ v-model="form.phase"
|
|
|
+ label="Fase"
|
|
|
+ :options="phaseOptions"
|
|
|
+ emit-value
|
|
|
+ map-options
|
|
|
+ class="col-6"
|
|
|
+ :rules="[val => !!val || 'Campo obrigatório']"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 4. Setor -->
|
|
|
+ <DefaultInput
|
|
|
+ v-model="form.sector"
|
|
|
+ label="Setor"
|
|
|
+ class="col-6"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 5. Descrição -->
|
|
|
+ <DefaultInput
|
|
|
+ v-model="form.description"
|
|
|
+ label="Descrição"
|
|
|
+ type="textarea"
|
|
|
+ class="col-12"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Tab: Comentários -->
|
|
|
+ <div v-show="currentTab === 'comentarios'">
|
|
|
+ <div 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: 380px;
|
|
|
+ overflow-y: auto;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ "
|
|
|
+ >
|
|
|
+ <template v-if="replies.length">
|
|
|
+ <KanbanCommentCard
|
|
|
+ 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 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, useQuasar } 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 DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
|
|
|
+import UnitSelect from "src/components/selects/UnitSelect.vue";
|
|
|
+import KanbanCommentCard from "./KanbanCommentCard.vue";
|
|
|
+
|
|
|
+import { createKanban, updateKanban } from "src/api/kanban";
|
|
|
+import {
|
|
|
+ getKanbanReplies,
|
|
|
+ createKanbanReply,
|
|
|
+ updateKanbanReply,
|
|
|
+ deleteKanbanReply,
|
|
|
+} from "src/api/kanban_reply";
|
|
|
+
|
|
|
+defineEmits([...useDialogPluginComponent.emits]);
|
|
|
+
|
|
|
+const { card, initialPhase } = defineProps({
|
|
|
+ card: { type: Object, default: null },
|
|
|
+ initialPhase: { type: String, default: "a_fazer" },
|
|
|
+});
|
|
|
+
|
|
|
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
|
|
|
+ useDialogPluginComponent();
|
|
|
+
|
|
|
+const $q = useQuasar();
|
|
|
+
|
|
|
+const formRef = ref(null);
|
|
|
+const loading = ref(false);
|
|
|
+const currentTab = ref("atividade");
|
|
|
+const replies = ref([]);
|
|
|
+const selectedUnit = ref(card?.target_unit_id ? { value: card.target_unit_id } : null);
|
|
|
+
|
|
|
+const tabs = computed(() => [
|
|
|
+ { name: "atividade", label: "Atividade" },
|
|
|
+ { name: "comentarios", label: "Comentários" },
|
|
|
+]);
|
|
|
+
|
|
|
+const priorityOptions = [
|
|
|
+ { label: "Alta", value: "alta" },
|
|
|
+ { label: "Normal", value: "normal" },
|
|
|
+ { label: "Baixa", value: "baixa" },
|
|
|
+];
|
|
|
+
|
|
|
+const phaseOptions = [
|
|
|
+ { label: "A Fazer", value: "a_fazer" },
|
|
|
+ { label: "Em Progresso", value: "em_progresso" },
|
|
|
+ { label: "Em Revisão", value: "em_revisao" },
|
|
|
+ { label: "Concluído", value: "concluido" },
|
|
|
+ { label: "Demandas Especiais", value: "demandas_especiais" },
|
|
|
+];
|
|
|
+
|
|
|
+const scopeOptions = [
|
|
|
+ { label: "Interno", value: "internal" },
|
|
|
+ { label: "Todas as Unidades", value: "all" },
|
|
|
+ { label: "Unidade Específica", value: "specific" },
|
|
|
+];
|
|
|
+
|
|
|
+const formatDisplayDate = (rawDate) => {
|
|
|
+ if (!rawDate) return null;
|
|
|
+ const [y, m, d] = rawDate.split("-");
|
|
|
+ return `${d}/${m}/${y}`;
|
|
|
+};
|
|
|
+
|
|
|
+const form = ref({
|
|
|
+ title: card?.title ?? null,
|
|
|
+ priority: card?.priority ?? "normal",
|
|
|
+ phase: card?.phase ?? initialPhase,
|
|
|
+ scope: card?.scope ?? "internal",
|
|
|
+ sector: card?.sector ?? null,
|
|
|
+ description: card?.description ?? null,
|
|
|
+ due_date: card?.due_date ?? null,
|
|
|
+ due_date_display: formatDisplayDate(card?.due_date),
|
|
|
+});
|
|
|
+
|
|
|
+const buildPayload = () => ({
|
|
|
+ title: form.value.title,
|
|
|
+ priority: form.value.priority,
|
|
|
+ phase: form.value.phase,
|
|
|
+ scope: form.value.scope,
|
|
|
+ sector: form.value.sector || null,
|
|
|
+ description: form.value.description || null,
|
|
|
+ due_date: form.value.due_date || null,
|
|
|
+ responsible_user_id: null, // Matriz never assigns a responsible
|
|
|
+ target_unit_id: form.value.scope === "specific"
|
|
|
+ ? (selectedUnit.value?.value ?? null)
|
|
|
+ : null,
|
|
|
+});
|
|
|
+
|
|
|
+const loadReplies = async () => {
|
|
|
+ if (!card?.id) return;
|
|
|
+ replies.value = await getKanbanReplies(card.id);
|
|
|
+};
|
|
|
+
|
|
|
+const onAddComment = () => {
|
|
|
+ $q.dialog({
|
|
|
+ title: "Adicionar Comentário",
|
|
|
+ prompt: { model: "", type: "textarea", label: "Comentário" },
|
|
|
+ ok: { label: "Salvar", color: "primary" },
|
|
|
+ cancel: { label: "Cancelar", color: "primary", outline: true },
|
|
|
+ }).onOk(async (text) => {
|
|
|
+ if (!text?.trim()) return;
|
|
|
+ await createKanbanReply(card.id, { reply: text.trim() });
|
|
|
+ loadReplies();
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const onEditComment = (reply) => {
|
|
|
+ $q.dialog({
|
|
|
+ title: "Editar Comentário",
|
|
|
+ prompt: { model: reply.reply, type: "textarea", label: "Comentário" },
|
|
|
+ ok: { label: "Salvar", color: "primary" },
|
|
|
+ cancel: { label: "Cancelar", color: "primary", outline: true },
|
|
|
+ }).onOk(async (text) => {
|
|
|
+ if (!text?.trim()) return;
|
|
|
+ await updateKanbanReply(card.id, reply.id, { reply: text.trim() });
|
|
|
+ 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 deleteKanbanReply(card.id, reply.id);
|
|
|
+ loadReplies();
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const onOKClick = async () => {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const payload = buildPayload();
|
|
|
+ if (card?.id) {
|
|
|
+ await updateKanban(card.id, payload);
|
|
|
+ } else {
|
|
|
+ await createKanban(payload);
|
|
|
+ }
|
|
|
+ onDialogOK(true);
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(loadReplies);
|
|
|
+</script>
|