|
@@ -40,21 +40,22 @@
|
|
|
<q-badge
|
|
<q-badge
|
|
|
color="white"
|
|
color="white"
|
|
|
:text-color="column.badgeTextColor"
|
|
:text-color="column.badgeTextColor"
|
|
|
- :label="columnCards(column.phase).length"
|
|
|
|
|
|
|
+ :label="columnMap[column.phase].length"
|
|
|
style="font-size: 11px"
|
|
style="font-size: 11px"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Draggable card list -->
|
|
<!-- Draggable card list -->
|
|
|
<draggable
|
|
<draggable
|
|
|
- :list="columnCards(column.phase)"
|
|
|
|
|
|
|
+ :list="columnMap[column.phase]"
|
|
|
|
|
+ :data-phase="column.phase"
|
|
|
group="kanban"
|
|
group="kanban"
|
|
|
item-key="id"
|
|
item-key="id"
|
|
|
:animation="180"
|
|
:animation="180"
|
|
|
ghost-class="drag-ghost"
|
|
ghost-class="drag-ghost"
|
|
|
drag-class="drag-active"
|
|
drag-class="drag-active"
|
|
|
- style="display: flex; flex-direction: column; gap: 8px; min-height: 8px; flex: 1"
|
|
|
|
|
- @end="onDragEnd($event, column.phase)"
|
|
|
|
|
|
|
+ style="display: flex; flex-direction: column; gap: 8px; min-height: 48px; flex: 1"
|
|
|
|
|
+ @end="onDragEnd"
|
|
|
>
|
|
>
|
|
|
<template #item="{ element }">
|
|
<template #item="{ element }">
|
|
|
<KanbanCard
|
|
<KanbanCard
|
|
@@ -80,7 +81,7 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { ref, defineAsyncComponent, onMounted } from "vue";
|
|
|
|
|
|
|
+import { ref, reactive, defineAsyncComponent, onMounted } from "vue";
|
|
|
import { useQuasar } from "quasar";
|
|
import { useQuasar } from "quasar";
|
|
|
import draggable from "vuedraggable";
|
|
import draggable from "vuedraggable";
|
|
|
|
|
|
|
@@ -93,9 +94,7 @@ const AddEditKanbanDialog = defineAsyncComponent(
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
const $q = useQuasar();
|
|
const $q = useQuasar();
|
|
|
-
|
|
|
|
|
const loading = ref(false);
|
|
const loading = ref(false);
|
|
|
-const cards = ref([]);
|
|
|
|
|
|
|
|
|
|
const columns = [
|
|
const columns = [
|
|
|
{ phase: "a_fazer", label: "A Fazer", color: "#757575", badgeTextColor: "grey-9" },
|
|
{ phase: "a_fazer", label: "A Fazer", color: "#757575", badgeTextColor: "grey-9" },
|
|
@@ -105,50 +104,50 @@ const columns = [
|
|
|
{ phase: "demandas_especiais", label: "Demandas Especiais", color: "#F9A825", badgeTextColor: "yellow-9" },
|
|
{ phase: "demandas_especiais", label: "Demandas Especiais", color: "#F9A825", badgeTextColor: "yellow-9" },
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
-/** Returns a reactive slice of cards for the given phase, in order. */
|
|
|
|
|
-const columnCards = (phase) =>
|
|
|
|
|
- cards.value.filter((c) => c.phase === phase);
|
|
|
|
|
|
|
+// Each phase has its own reactive array — required for vuedraggable cross-list DnD
|
|
|
|
|
+const columnMap = reactive({
|
|
|
|
|
+ a_fazer: [],
|
|
|
|
|
+ em_progresso: [],
|
|
|
|
|
+ em_revisao: [],
|
|
|
|
|
+ concluido: [],
|
|
|
|
|
+ demandas_especiais: [],
|
|
|
|
|
+});
|
|
|
|
|
|
|
|
const loadCards = async () => {
|
|
const loadCards = async () => {
|
|
|
loading.value = true;
|
|
loading.value = true;
|
|
|
try {
|
|
try {
|
|
|
- cards.value = await getKanbans();
|
|
|
|
|
|
|
+ const data = await getKanbans();
|
|
|
|
|
+
|
|
|
|
|
+ // Reset all columns before filling
|
|
|
|
|
+ Object.keys(columnMap).forEach((k) => (columnMap[k] = []));
|
|
|
|
|
+
|
|
|
|
|
+ data.forEach((card) => {
|
|
|
|
|
+ if (columnMap[card.phase]) {
|
|
|
|
|
+ columnMap[card.phase].push(card);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
} finally {
|
|
} finally {
|
|
|
loading.value = false;
|
|
loading.value = false;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Called when any drag ends.
|
|
|
|
|
- * Rebuilds orders for every column that was affected and persists to the API.
|
|
|
|
|
|
|
+ * Called once when drag ends.
|
|
|
|
|
+ * By this point vuedraggable has already moved the item between the two arrays.
|
|
|
|
|
+ * We read source/destination phases from the DOM data-phase attribute and persist.
|
|
|
*/
|
|
*/
|
|
|
-const onDragEnd = async (evt, targetPhase) => {
|
|
|
|
|
- // Identify the card that was moved
|
|
|
|
|
- const movedCard = evt.item?.__draggable_context?.element;
|
|
|
|
|
- if (!movedCard) return;
|
|
|
|
|
-
|
|
|
|
|
- // Update the in-memory phase for the moved card
|
|
|
|
|
- movedCard.phase = targetPhase;
|
|
|
|
|
-
|
|
|
|
|
- // Collect all cards from source and destination columns (may be the same)
|
|
|
|
|
- const affectedPhases = new Set([targetPhase]);
|
|
|
|
|
- if (evt.from !== evt.to) {
|
|
|
|
|
- // The original phase is stored on the element before the move;
|
|
|
|
|
- // we resolve it from the source list id (set as data-phase attr) or
|
|
|
|
|
- // simply by iterating all columns — safest approach.
|
|
|
|
|
- columns.forEach((col) => {
|
|
|
|
|
- const colCards = columnCards(col.phase);
|
|
|
|
|
- if (colCards.some((c) => c.id === movedCard.id)) {
|
|
|
|
|
- affectedPhases.add(col.phase);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const onDragEnd = async (evt) => {
|
|
|
|
|
+ const sourcePhase = evt.from.dataset.phase;
|
|
|
|
|
+ const targetPhase = evt.to.dataset.phase;
|
|
|
|
|
+
|
|
|
|
|
+ // Collect phases to update (may be the same if reordering in-column)
|
|
|
|
|
+ const phasesToUpdate = new Set([sourcePhase, targetPhase].filter(Boolean));
|
|
|
|
|
|
|
|
- // Build the reorder payload for each affected phase
|
|
|
|
|
const items = [];
|
|
const items = [];
|
|
|
- affectedPhases.forEach((phase) => {
|
|
|
|
|
- columnCards(phase).forEach((card, idx) => {
|
|
|
|
|
- card.phase = phase; // ensure phase is in sync
|
|
|
|
|
|
|
+ phasesToUpdate.forEach((phase) => {
|
|
|
|
|
+ columnMap[phase].forEach((card, idx) => {
|
|
|
|
|
+ // Sync the card's phase in memory too
|
|
|
|
|
+ card.phase = phase;
|
|
|
items.push({ id: card.id, phase, order: idx });
|
|
items.push({ id: card.id, phase, order: idx });
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
@@ -156,7 +155,7 @@ const onDragEnd = async (evt, targetPhase) => {
|
|
|
try {
|
|
try {
|
|
|
await reorderKanbans(items);
|
|
await reorderKanbans(items);
|
|
|
} catch {
|
|
} catch {
|
|
|
- // On failure reload from server to restore consistent state
|
|
|
|
|
|
|
+ // On failure restore from server
|
|
|
await loadCards();
|
|
await loadCards();
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
@@ -181,6 +180,7 @@ onMounted(loadCards);
|
|
|
:deep(.drag-ghost) {
|
|
:deep(.drag-ghost) {
|
|
|
opacity: 0.4;
|
|
opacity: 0.4;
|
|
|
border: 2px dashed #aaa;
|
|
border: 2px dashed #aaa;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
:deep(.drag-active) {
|
|
:deep(.drag-active) {
|