Browse Source

feat(kanban): dialog/card redesign for franchisor

Dialog:
- Destino é o primeiro campo de configuração (antes de Prioridade/Fase)
- Remove Responsável — Matriz nunca atribui responsável (null no DB)
- UnitSelect aparece apenas quando scope=specific

Card:
- Botão lixeira com @click.stop e confirmação antes de excluir
- emit('delete', id) → KanbanPage remove do columnMap sem reload
ebagabee 2 weeks ago
parent
commit
1d9332261f

+ 6 - 0
src/pages/kanban/KanbanPage.vue

@@ -61,6 +61,7 @@
             <KanbanCard
               :card="element"
               @edit="openDialog(element, column.phase)"
+              @delete="removeCard($event, column.phase)"
             />
           </template>
         </draggable>
@@ -160,6 +161,11 @@ const onDragEnd = async (evt) => {
   }
 };
 
+const removeCard = (cardId, phase) => {
+  const idx = columnMap[phase].findIndex((c) => c.id === cardId);
+  if (idx !== -1) columnMap[phase].splice(idx, 1);
+};
+
 const openDialog = (card = null, phase = "a_fazer") => {
   $q.dialog({
     component: AddEditKanbanDialog,

+ 69 - 121
src/pages/kanban/components/AddEditKanbanDialog.vue

@@ -13,7 +13,6 @@
           @submit="onOKClick"
         >
           <q-card-section class="q-pt-sm" style="flex: 1; overflow-y: auto; min-height: 0">
-            <!-- Tabs: only shown when editing -->
             <CustomTabComponent
               v-if="card?.id"
               v-model:active-tab="currentTab"
@@ -24,6 +23,7 @@
             <!-- 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"
@@ -31,6 +31,18 @@
                   :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"
@@ -38,6 +50,14 @@
                   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"
@@ -48,15 +68,6 @@
                   :rules="[val => !!val || 'Campo obrigatório']"
                 />
 
-                <DefaultSelect
-                  v-model="form.responsible_user_id"
-                  label="Responsável"
-                  :options="userOptions"
-                  emit-value
-                  map-options
-                  class="col-12"
-                />
-
                 <DefaultSelect
                   v-model="form.phase"
                   label="Fase"
@@ -67,28 +78,14 @@
                   :rules="[val => !!val || 'Campo obrigatório']"
                 />
 
+                <!-- 4. Setor -->
                 <DefaultInput
                   v-model="form.sector"
                   label="Setor"
                   class="col-6"
                 />
 
-                <DefaultSelect
-                  v-model="form.scope"
-                  label="Destino"
-                  :options="scopeOptions"
-                  emit-value
-                  map-options
-                  class="col-6"
-                  :rules="[val => !!val || 'Campo obrigatório']"
-                />
-
-                <UnitSelect
-                  v-if="form.scope === 'specific'"
-                  v-model="selectedUnit"
-                  class="col-6"
-                />
-
+                <!-- 5. Descrição -->
                 <DefaultInput
                   v-model="form.description"
                   label="Descrição"
@@ -130,10 +127,7 @@
                     @delete="onDeleteComment(reply)"
                   />
                 </template>
-                <div
-                  v-else
-                  class="flex flex-center full-height text-grey-5 text-body2"
-                >
+                <div v-else class="flex flex-center full-height text-grey-5 text-body2">
                   Nenhum comentário registrado.
                 </div>
               </div>
@@ -141,18 +135,8 @@
           </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-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>
@@ -179,20 +163,12 @@ import {
   updateKanbanReply,
   deleteKanbanReply,
 } from "src/api/kanban_reply";
-import { getUsers } from "src/api/user";
 
 defineEmits([...useDialogPluginComponent.emits]);
 
 const { card, initialPhase } = defineProps({
-  card: {
-    type: Object,
-    default: null,
-  },
-  /** Phase pre-selected when opening the dialog from a column's "add" button */
-  initialPhase: {
-    type: String,
-    default: "a_fazer",
-  },
+  card:         { type: Object, default: null },
+  initialPhase: { type: String, default: "a_fazer" },
 });
 
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
@@ -200,39 +176,37 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
 
 const $q = useQuasar();
 
-const formRef = ref(null);
-const loading = ref(false);
+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 userOptions = ref([]);
-const replies = ref([]);
 
 const tabs = computed(() => [
-  { name: "atividade", label: "Atividade" },
-  { name: "comentarios", label: "Comentários" },
+  { name: "atividade",    label: "Atividade"    },
+  { name: "comentarios",  label: "Comentários"  },
 ]);
 
 const priorityOptions = [
-  { label: "Alta", value: "alta" },
+  { label: "Alta",   value: "alta"   },
   { label: "Normal", value: "normal" },
-  { label: "Baixa", value: "baixa" },
+  { 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: "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: "Interno",            value: "internal" },
+  { label: "Todas as Unidades",  value: "all"      },
   { label: "Unidade Específica", value: "specific" },
 ];
 
-// Format date from YYYY-MM-DD to DD/MM/YYYY for display
 const formatDisplayDate = (rawDate) => {
   if (!rawDate) return null;
   const [y, m, d] = rawDate.split("-");
@@ -240,44 +214,29 @@ const formatDisplayDate = (rawDate) => {
 };
 
 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,
+  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),
-  responsible_user_id: card?.responsible_user_id ?? null,
 });
 
-const buildPayload = () => {
-  const payload = {
-    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: form.value.responsible_user_id || null,
-  };
-
-  if (form.value.scope === "specific") {
-    payload.target_unit_id = selectedUnit.value?.value ?? null;
-  } else {
-    payload.target_unit_id = null;
-  }
-
-  return payload;
-};
-
-const loadUsers = async () => {
-  try {
-    const users = await getUsers();
-    userOptions.value = users.map((u) => ({ label: u.name, value: u.id }));
-  } catch {}
-};
+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;
@@ -287,12 +246,8 @@ const loadReplies = async () => {
 const onAddComment = () => {
   $q.dialog({
     title: "Adicionar Comentário",
-    prompt: {
-      model: "",
-      type: "textarea",
-      label: "Comentário",
-    },
-    ok: { label: "Salvar", color: "primary" },
+    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;
@@ -304,12 +259,8 @@ const onAddComment = () => {
 const onEditComment = (reply) => {
   $q.dialog({
     title: "Editar Comentário",
-    prompt: {
-      model: reply.reply,
-      type: "textarea",
-      label: "Comentário",
-    },
-    ok: { label: "Salvar", color: "primary" },
+    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;
@@ -320,10 +271,10 @@ const onEditComment = (reply) => {
 
 const onDeleteComment = (reply) => {
   $q.dialog({
-    title: "Excluir Comentário",
+    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" },
+    cancel:  { outline: true, color: "primary",  label: "Cancelar" },
+    ok:      {               color: "negative",  label: "Excluir"  },
   }).onOk(async () => {
     await deleteKanbanReply(card.id, reply.id);
     loadReplies();
@@ -345,8 +296,5 @@ const onOKClick = async () => {
   }
 };
 
-onMounted(() => {
-  loadUsers();
-  loadReplies();
-});
+onMounted(loadReplies);
 </script>

+ 41 - 16
src/pages/kanban/components/KanbanCard.vue

@@ -4,16 +4,28 @@
     :style="{ borderLeft: `4px solid ${phaseColor}` }"
     @click="$emit('edit', card)"
   >
-    <!-- Header: Creator name + Priority badge -->
+    <!-- Header: Creator name + Priority badge + Delete button -->
     <div class="row items-center justify-between q-px-md q-pt-md">
-      <span class="text-body2 text-weight-medium text-grey-8 ellipsis" style="max-width: 60%">
+      <span class="text-body2 text-weight-medium text-grey-8 ellipsis" style="max-width: 55%">
         {{ card.created_by_user_name ?? '—' }}
       </span>
-      <q-badge
-        :color="priorityColor"
-        :label="priorityLabel"
-        style="padding: 4px 8px; font-size: 11px"
-      />
+
+      <div class="row items-center" style="gap: 6px">
+        <q-badge
+          :color="priorityColor"
+          :label="priorityLabel"
+          style="padding: 4px 8px; font-size: 11px"
+        />
+        <q-btn
+          flat
+          dense
+          round
+          icon="mdi-trash-can-outline"
+          size="xs"
+          color="grey-5"
+          @click.stop="onDelete"
+        />
+      </div>
     </div>
 
     <!-- De / Para -->
@@ -56,16 +68,17 @@
 
 <script setup>
 import { computed } from "vue";
+import { useQuasar } from "quasar";
+import { deleteKanban } from "src/api/kanban";
 
-defineEmits(["edit"]);
+const emit = defineEmits(["edit", "delete"]);
 
 const { card } = defineProps({
-  card: {
-    type: Object,
-    required: true,
-  },
+  card: { type: Object, required: true },
 });
 
+const $q = useQuasar();
+
 const truncated = computed(() => {
   const desc = card.description ?? "";
   return desc.length > 50 ? desc.slice(0, 50) + "…" : desc;
@@ -88,14 +101,26 @@ const priorityLabel = computed(() => {
 
 const phaseColor = computed(() => {
   const map = {
-    a_fazer: "#9E9E9E",
-    em_progresso: "#1976D2",
-    em_revisao: "#FF9800",
-    concluido: "#4CAF50",
+    a_fazer:            "#9E9E9E",
+    em_progresso:       "#1976D2",
+    em_revisao:         "#FF9800",
+    concluido:          "#4CAF50",
     demandas_especiais: "#FFC107",
   };
   return map[card.phase] ?? "#9E9E9E";
 });
+
+const onDelete = () => {
+  $q.dialog({
+    title:   "Excluir Atividade",
+    message: `Tem certeza que deseja excluir "${card.title}"?`,
+    cancel:  { outline: true, color: "primary",  label: "Cancelar" },
+    ok:      {               color: "negative",  label: "Excluir"  },
+  }).onOk(async () => {
+    await deleteKanban(card.id);
+    emit("delete", card.id);
+  });
+};
 </script>
 
 <style scoped>