KanbanPage.vue 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. <template>
  2. <div>
  3. <DefaultHeaderPage title="Atividades" :show-filter-icon="false" />
  4. <div v-if="loading" class="flex flex-center q-pa-xl">
  5. <q-spinner color="primary" size="48px" />
  6. </div>
  7. <div
  8. v-else
  9. class="kanban-board q-px-md q-pb-md"
  10. style="
  11. display: flex;
  12. gap: 16px;
  13. overflow-x: auto;
  14. align-items: flex-start;
  15. min-height: calc(100vh - 120px);
  16. "
  17. >
  18. <div
  19. v-for="column in columns"
  20. :key="column.phase"
  21. style="
  22. min-width: 280px;
  23. max-width: 320px;
  24. flex: 1;
  25. display: flex;
  26. flex-direction: column;
  27. gap: 8px;
  28. "
  29. >
  30. <!-- Column header -->
  31. <div
  32. class="row items-center justify-between q-px-md q-py-sm"
  33. :style="{ backgroundColor: column.color, borderRadius: '8px' }"
  34. >
  35. <span class="text-weight-bold text-white" style="font-size: 14px">
  36. {{ column.label }}
  37. </span>
  38. <q-badge
  39. color="white"
  40. :text-color="column.badgeTextColor"
  41. :label="columnMap[column.phase].length"
  42. style="font-size: 11px"
  43. />
  44. </div>
  45. <!-- Draggable card list -->
  46. <draggable
  47. :list="columnMap[column.phase]"
  48. :data-phase="column.phase"
  49. group="kanban"
  50. item-key="id"
  51. :animation="180"
  52. ghost-class="drag-ghost"
  53. drag-class="drag-active"
  54. style="display: flex; flex-direction: column; gap: 8px; min-height: 48px; flex: 1"
  55. @end="onDragEnd"
  56. >
  57. <template #item="{ element }">
  58. <KanbanCard
  59. :card="element"
  60. @edit="openDialog(element, column.phase)"
  61. @delete="removeCard($event, column.phase)"
  62. />
  63. </template>
  64. </draggable>
  65. <!-- Add button -->
  66. <q-btn
  67. flat
  68. color="grey-6"
  69. icon="mdi-plus"
  70. label="Adicionar"
  71. class="q-mt-xs full-width"
  72. style="border-radius: 8px; border: 1px dashed #ccc"
  73. @click="openDialog(null, column.phase)"
  74. />
  75. </div>
  76. </div>
  77. </div>
  78. </template>
  79. <script setup>
  80. import { ref, reactive, defineAsyncComponent, onMounted, watch } from "vue";
  81. import { useQuasar } from "quasar";
  82. import draggable from "vuedraggable";
  83. import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
  84. import KanbanCard from "./components/KanbanCard.vue";
  85. import { getKanbans, reorderKanbans } from "src/api/kanban";
  86. import { userStore } from "src/stores/user";
  87. const AddEditKanbanDialog = defineAsyncComponent(
  88. () => import("./components/AddEditKanbanDialog.vue"),
  89. );
  90. const $q = useQuasar();
  91. const { selectedUnit } = userStore();
  92. const loading = ref(false);
  93. const columns = [
  94. { phase: "a_fazer", label: "A Fazer", color: "#757575", badgeTextColor: "grey-9" },
  95. { phase: "em_progresso", label: "Em Progresso", color: "#1976D2", badgeTextColor: "blue-9" },
  96. { phase: "em_revisao", label: "Em Revisão", color: "#F57C00", badgeTextColor: "orange-9" },
  97. { phase: "concluido", label: "Concluído", color: "#388E3C", badgeTextColor: "green-9" },
  98. { phase: "demandas_especiais", label: "Demandas Especiais", color: "#F9A825", badgeTextColor: "yellow-9" },
  99. ];
  100. const columnMap = reactive({
  101. a_fazer: [],
  102. em_progresso: [],
  103. em_revisao: [],
  104. concluido: [],
  105. demandas_especiais: [],
  106. });
  107. const loadCards = async () => {
  108. loading.value = true;
  109. try {
  110. const data = await getKanbans();
  111. Object.keys(columnMap).forEach((k) => (columnMap[k] = []));
  112. data.forEach((card) => {
  113. if (columnMap[card.phase]) {
  114. columnMap[card.phase].push(card);
  115. }
  116. });
  117. } finally {
  118. loading.value = false;
  119. }
  120. };
  121. const onDragEnd = async (evt) => {
  122. const sourcePhase = evt.from.dataset.phase;
  123. const targetPhase = evt.to.dataset.phase;
  124. const phasesToUpdate = new Set([sourcePhase, targetPhase].filter(Boolean));
  125. const items = [];
  126. phasesToUpdate.forEach((phase) => {
  127. columnMap[phase].forEach((card, idx) => {
  128. card.phase = phase;
  129. items.push({ id: card.id, phase, order: idx });
  130. });
  131. });
  132. try {
  133. await reorderKanbans(items);
  134. } catch {
  135. await loadCards();
  136. }
  137. };
  138. const removeCard = (cardId, phase) => {
  139. const idx = columnMap[phase].findIndex((c) => c.id === cardId);
  140. if (idx !== -1) columnMap[phase].splice(idx, 1);
  141. };
  142. const openDialog = (card = null, phase = "a_fazer") => {
  143. $q.dialog({
  144. component: AddEditKanbanDialog,
  145. componentProps: { card, initialPhase: phase },
  146. }).onOk(() => {
  147. loadCards();
  148. });
  149. };
  150. // Reload whenever the user switches units via the top selector
  151. watch(() => selectedUnit?.id, (newId, oldId) => {
  152. if (newId !== oldId) loadCards();
  153. });
  154. onMounted(loadCards);
  155. </script>
  156. <style scoped>
  157. .kanban-board {
  158. padding-top: 12px;
  159. }
  160. :deep(.drag-ghost) {
  161. opacity: 0.4;
  162. border: 2px dashed #aaa;
  163. border-radius: 10px;
  164. }
  165. :deep(.drag-active) {
  166. opacity: 0.95;
  167. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
  168. transform: rotate(1.5deg);
  169. }
  170. </style>