|
|
@@ -20,7 +20,6 @@
|
|
|
<div
|
|
|
v-for="column in columns"
|
|
|
:key="column.phase"
|
|
|
- class="kanban-column"
|
|
|
style="
|
|
|
min-width: 280px;
|
|
|
max-width: 320px;
|
|
|
@@ -32,7 +31,7 @@
|
|
|
>
|
|
|
<!-- Column header -->
|
|
|
<div
|
|
|
- class="kanban-column-header row items-center justify-between q-px-md q-py-sm"
|
|
|
+ class="row items-center justify-between q-px-md q-py-sm"
|
|
|
:style="{ backgroundColor: column.color, borderRadius: '8px' }"
|
|
|
>
|
|
|
<span class="text-weight-bold text-white" style="font-size: 14px">
|
|
|
@@ -40,28 +39,30 @@
|
|
|
</span>
|
|
|
<q-badge
|
|
|
color="white"
|
|
|
- :text-color="column.textColor"
|
|
|
- :label="cardsForPhase(column.phase).length"
|
|
|
+ :text-color="column.badgeTextColor"
|
|
|
+ :label="columnCards(column.phase).length"
|
|
|
style="font-size: 11px"
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
- <!-- Cards -->
|
|
|
- <div
|
|
|
- style="
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 8px;
|
|
|
- flex: 1;
|
|
|
- "
|
|
|
+ <!-- Draggable card list -->
|
|
|
+ <draggable
|
|
|
+ :list="columnCards(column.phase)"
|
|
|
+ group="kanban"
|
|
|
+ item-key="id"
|
|
|
+ :animation="180"
|
|
|
+ ghost-class="drag-ghost"
|
|
|
+ drag-class="drag-active"
|
|
|
+ style="display: flex; flex-direction: column; gap: 8px; min-height: 8px; flex: 1"
|
|
|
+ @end="onDragEnd($event, column.phase)"
|
|
|
>
|
|
|
- <KanbanCard
|
|
|
- v-for="card in cardsForPhase(column.phase)"
|
|
|
- :key="card.id"
|
|
|
- :card="card"
|
|
|
- @edit="openDialog(card, column.phase)"
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <template #item="{ element }">
|
|
|
+ <KanbanCard
|
|
|
+ :card="element"
|
|
|
+ @edit="openDialog(element, column.phase)"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </draggable>
|
|
|
|
|
|
<!-- Add button -->
|
|
|
<q-btn
|
|
|
@@ -81,10 +82,11 @@
|
|
|
<script setup>
|
|
|
import { ref, defineAsyncComponent, onMounted } from "vue";
|
|
|
import { useQuasar } from "quasar";
|
|
|
+import draggable from "vuedraggable";
|
|
|
|
|
|
import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
|
import KanbanCard from "./components/KanbanCard.vue";
|
|
|
-import { getKanbans } from "src/api/kanban";
|
|
|
+import { getKanbans, reorderKanbans } from "src/api/kanban";
|
|
|
|
|
|
const AddEditKanbanDialog = defineAsyncComponent(
|
|
|
() => import("./components/AddEditKanbanDialog.vue"),
|
|
|
@@ -96,14 +98,15 @@ const loading = ref(false);
|
|
|
const cards = ref([]);
|
|
|
|
|
|
const columns = [
|
|
|
- { phase: "a_fazer", label: "A Fazer", color: "#757575", textColor: "grey-9" },
|
|
|
- { phase: "em_progresso", label: "Em Progresso", color: "#1976D2", textColor: "blue-9" },
|
|
|
- { phase: "em_revisao", label: "Em Revisão", color: "#F57C00", textColor: "orange-9"},
|
|
|
- { phase: "concluido", label: "Concluído", color: "#388E3C", textColor: "green-9" },
|
|
|
- { phase: "demandas_especiais",label: "Demandas Especiais",color: "#F9A825", textColor: "yellow-9"},
|
|
|
+ { phase: "a_fazer", label: "A Fazer", color: "#757575", badgeTextColor: "grey-9" },
|
|
|
+ { phase: "em_progresso", label: "Em Progresso", color: "#1976D2", badgeTextColor: "blue-9" },
|
|
|
+ { phase: "em_revisao", label: "Em Revisão", color: "#F57C00", badgeTextColor: "orange-9" },
|
|
|
+ { phase: "concluido", label: "Concluído", color: "#388E3C", badgeTextColor: "green-9" },
|
|
|
+ { phase: "demandas_especiais", label: "Demandas Especiais", color: "#F9A825", badgeTextColor: "yellow-9" },
|
|
|
];
|
|
|
|
|
|
-const cardsForPhase = (phase) =>
|
|
|
+/** Returns a reactive slice of cards for the given phase, in order. */
|
|
|
+const columnCards = (phase) =>
|
|
|
cards.value.filter((c) => c.phase === phase);
|
|
|
|
|
|
const loadCards = async () => {
|
|
|
@@ -115,6 +118,49 @@ const loadCards = async () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+/**
|
|
|
+ * Called when any drag ends.
|
|
|
+ * Rebuilds orders for every column that was affected and persists to the API.
|
|
|
+ */
|
|
|
+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);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Build the reorder payload for each affected phase
|
|
|
+ const items = [];
|
|
|
+ affectedPhases.forEach((phase) => {
|
|
|
+ columnCards(phase).forEach((card, idx) => {
|
|
|
+ card.phase = phase; // ensure phase is in sync
|
|
|
+ items.push({ id: card.id, phase, order: idx });
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ await reorderKanbans(items);
|
|
|
+ } catch {
|
|
|
+ // On failure reload from server to restore consistent state
|
|
|
+ await loadCards();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
const openDialog = (card = null, phase = "a_fazer") => {
|
|
|
$q.dialog({
|
|
|
component: AddEditKanbanDialog,
|
|
|
@@ -131,4 +177,15 @@ onMounted(loadCards);
|
|
|
.kanban-board {
|
|
|
padding-top: 12px;
|
|
|
}
|
|
|
+
|
|
|
+:deep(.drag-ghost) {
|
|
|
+ opacity: 0.4;
|
|
|
+ border: 2px dashed #aaa;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.drag-active) {
|
|
|
+ opacity: 0.95;
|
|
|
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
|
|
|
+ transform: rotate(1.5deg);
|
|
|
+}
|
|
|
</style>
|