KanbanPage.vue 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  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. <div class="row items-center gap-xs">
  39. <q-badge
  40. color="white"
  41. :text-color="column.badgeTextColor"
  42. :label="columnMap[column.phase].length"
  43. style="font-size: 11px"
  44. />
  45. <q-btn
  46. v-if="canAdd"
  47. flat
  48. round
  49. dense
  50. icon="mdi-plus"
  51. color="white"
  52. size="sm"
  53. @click="openDialog(null, column.phase)"
  54. />
  55. </div>
  56. </div>
  57. <!-- Draggable card list -->
  58. <draggable
  59. :list="columnMap[column.phase]"
  60. :data-phase="column.phase"
  61. :group="canEdit ? 'kanban' : { name: 'kanban', pull: false, put: false }"
  62. :disabled="!canEdit"
  63. item-key="id"
  64. :animation="180"
  65. ghost-class="drag-ghost"
  66. drag-class="drag-active"
  67. style="display: flex; flex-direction: column; gap: 8px; min-height: 48px; flex: 1"
  68. @end="onDragEnd"
  69. >
  70. <template #item="{ element }">
  71. <KanbanCard
  72. :card="element"
  73. :can-edit="canEdit"
  74. :can-delete="canDelete"
  75. @edit="canEdit && openDialog(element, column.phase)"
  76. @delete="removeCard($event, column.phase)"
  77. />
  78. </template>
  79. </draggable>
  80. </div>
  81. </div>
  82. </div>
  83. </template>
  84. <script setup>
  85. import { ref, reactive, defineAsyncComponent, onMounted } from "vue";
  86. import { useQuasar } from "quasar";
  87. import draggable from "vuedraggable";
  88. import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
  89. import KanbanCard from "./components/KanbanCard.vue";
  90. import { getKanbans, reorderKanbans } from "src/api/kanban";
  91. import { permissionStore } from "src/stores/permission";
  92. const AddEditKanbanDialog = defineAsyncComponent(
  93. () => import("./components/AddEditKanbanDialog.vue"),
  94. );
  95. const $q = useQuasar();
  96. const permissions = permissionStore();
  97. const canAdd = permissions.getAccess("franchisor_activities", "add");
  98. const canEdit = permissions.getAccess("franchisor_activities", "edit");
  99. const canDelete = permissions.getAccess("franchisor_activities", "delete");
  100. const loading = ref(false);
  101. const columns = [
  102. { phase: "a_fazer", label: "A Fazer", color: "#757575", badgeTextColor: "grey-9" },
  103. { phase: "em_progresso", label: "Em Progresso", color: "#1976D2", badgeTextColor: "blue-9" },
  104. { phase: "em_revisao", label: "Em Revisão", color: "#F57C00", badgeTextColor: "orange-9" },
  105. { phase: "concluido", label: "Concluído", color: "#388E3C", badgeTextColor: "green-9" },
  106. { phase: "demandas_especiais", label: "Demandas Especiais", color: "#F9A825", badgeTextColor: "yellow-9" },
  107. ];
  108. // Each phase has its own reactive array — required for vuedraggable cross-list DnD
  109. const columnMap = reactive({
  110. a_fazer: [],
  111. em_progresso: [],
  112. em_revisao: [],
  113. concluido: [],
  114. demandas_especiais: [],
  115. });
  116. const loadCards = async () => {
  117. loading.value = true;
  118. try {
  119. const data = await getKanbans();
  120. // Reset all columns before filling
  121. Object.keys(columnMap).forEach((k) => (columnMap[k] = []));
  122. data.forEach((card) => {
  123. if (columnMap[card.phase]) {
  124. columnMap[card.phase].push(card);
  125. }
  126. });
  127. } finally {
  128. loading.value = false;
  129. }
  130. };
  131. /**
  132. * Called once when drag ends.
  133. * By this point vuedraggable has already moved the item between the two arrays.
  134. * We read source/destination phases from the DOM data-phase attribute and persist.
  135. */
  136. const onDragEnd = async (evt) => {
  137. const sourcePhase = evt.from.dataset.phase;
  138. const targetPhase = evt.to.dataset.phase;
  139. // Collect phases to update (may be the same if reordering in-column)
  140. const phasesToUpdate = new Set([sourcePhase, targetPhase].filter(Boolean));
  141. const items = [];
  142. phasesToUpdate.forEach((phase) => {
  143. columnMap[phase].forEach((card, idx) => {
  144. // Sync the card's phase in memory too
  145. card.phase = phase;
  146. items.push({ id: card.id, phase, order: idx });
  147. });
  148. });
  149. try {
  150. await reorderKanbans(items);
  151. } catch {
  152. // On failure restore from server
  153. await loadCards();
  154. }
  155. };
  156. const removeCard = (cardId, phase) => {
  157. const idx = columnMap[phase].findIndex((c) => c.id === cardId);
  158. if (idx !== -1) columnMap[phase].splice(idx, 1);
  159. };
  160. const openDialog = (card = null, phase = "a_fazer") => {
  161. $q.dialog({
  162. component: AddEditKanbanDialog,
  163. componentProps: { card, initialPhase: phase },
  164. }).onOk(() => {
  165. loadCards();
  166. });
  167. };
  168. onMounted(loadCards);
  169. </script>
  170. <style scoped>
  171. .kanban-board {
  172. padding-top: 12px;
  173. }
  174. :deep(.drag-ghost) {
  175. opacity: 0.4;
  176. border: 2px dashed #aaa;
  177. border-radius: 10px;
  178. }
  179. :deep(.drag-active) {
  180. opacity: 0.95;
  181. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
  182. transform: rotate(1.5deg);
  183. }
  184. </style>