diff --git a/modules/planka/api.php b/modules/planka/api.php
index 06dce12..637ed1f 100644
--- a/modules/planka/api.php
+++ b/modules/planka/api.php
@@ -99,25 +99,28 @@ if ($action === 'set_active_board') {
if ($action === 'create_list') {
$board_id = $_GET['board_id'] ?? null; if (!$board_id) tErr('board_id manquant');
$name = trim($_GET['name'] ?? ''); if (!$name) tErr('name manquant');
- $position = (int) ($_GET['position'] ?? 65535);
+ $position = (float) ($_GET['position'] ?? 65535);
$resp = planka_api('POST', '/api/boards/' . $board_id . '/lists', [
'name' => $name,
'position' => $position,
+ 'type' => 'active',
], $token);
- if (empty($resp['item'])) tErr($resp['message'] ?? 'Planka n\'a pas créé la liste', 502);
+ if (empty($resp['item'])) tErr($resp['message'] ?? ($resp['problems'][0] ?? 'Erreur Planka'), 502);
tOk($resp['item']);
}
if ($action === 'create_card') {
- $list_id = $_GET['list_id'] ?? null; if (!$list_id) tErr('list_id manquant');
- $board_id = $_GET['board_id'] ?? null; if (!$board_id) tErr('board_id manquant');
- $name = trim($_GET['name'] ?? ''); if (!$name) tErr('name manquant');
- $resp = planka_api('POST', '/api/boards/' . $board_id . '/cards', [
- 'listId' => $list_id,
+ $list_id = $_GET['list_id'] ?? null; if (!$list_id) tErr('list_id manquant');
+ $name = trim($_GET['name'] ?? ''); if (!$name) tErr('name manquant');
+ $pos = (float) ($_GET['position'] ?? 65535);
+ // Planka v2+: POST /api/lists/{listId}/cards with type required
+ $resp = planka_api('POST', '/api/lists/' . $list_id . '/cards', [
'name' => $name,
- 'position' => 65535,
+ 'position' => $pos,
+ 'type' => 'story',
], $token);
- tOk($resp['item'] ?? []);
+ if (empty($resp['item'])) tErr($resp['message'] ?? ($resp['problems'][0] ?? 'Erreur Planka'), 502);
+ tOk($resp['item']);
}
if ($action === 'update_card') {
diff --git a/modules/planka/assets/planka.css b/modules/planka/assets/planka.css
index eb8ab33..24d4cc2 100644
--- a/modules/planka/assets/planka.css
+++ b/modules/planka/assets/planka.css
@@ -488,3 +488,17 @@
background: var(--bg-panel, #f3f4f6);
color: var(--primary, #4361ee);
}
+
+.pk-drop-before {
+ height: 4px;
+ border-radius: 4px;
+ transition: height .12s, background .12s;
+ margin: -2px 0;
+}
+.pk-drop-before.drop-before-active {
+ height: 28px;
+ background: rgba(59,130,246,.15);
+ border: 2px dashed var(--primary, #4361ee);
+ border-radius: 6px;
+ margin: 2px 0;
+}
diff --git a/planka.php b/planka.php
index 4af8c94..59ed373 100644
--- a/planka.php
+++ b/planka.php
@@ -150,24 +150,50 @@ function renderBoard() {
`;
+ // Drag source — cards
area.querySelectorAll('.pk-card').forEach(el => {
el.addEventListener('click', () => openCardModal(el.dataset.id));
el.addEventListener('dragstart', e => {
- e.dataTransfer.setData('text/plain', el.dataset.id);
+ e.dataTransfer.setData('card-id', el.dataset.id);
e.dataTransfer.effectAllowed = 'move';
- el.classList.add('is-dragging');
+ setTimeout(() => el.classList.add('is-dragging'), 0);
+ });
+ el.addEventListener('dragend', () => {
+ el.classList.remove('is-dragging');
+ area.querySelectorAll('.drag-over,.drop-before-active').forEach(x => x.classList.remove('drag-over','drop-before-active'));
});
- el.addEventListener('dragend', e => { el.classList.remove('is-dragging'); });
});
- // Drop target = toute la colonne .pk-cards
+
+ // Drop zone — before a specific card
+ area.querySelectorAll('.pk-drop-before').forEach(el => {
+ el.addEventListener('dragover', e => { e.preventDefault(); e.stopPropagation(); el.classList.add('drop-before-active'); });
+ el.addEventListener('dragleave', () => el.classList.remove('drop-before-active'));
+ el.addEventListener('drop', e => {
+ e.preventDefault(); e.stopPropagation();
+ el.classList.remove('drop-before-active');
+ const cardId = e.dataTransfer.getData('card-id');
+ if (!cardId) return;
+ const listId = el.dataset.listId;
+ const prevPos = parseFloat(el.dataset.prevPos || 0);
+ const curPos = parseFloat(el.dataset.curPos || 0);
+ const newPos = prevPos === 0 ? curPos / 2 : (prevPos + curPos) / 2;
+ moveCard(cardId, listId, newPos);
+ });
+ });
+
+ // Drop zone — end of column (whole .pk-cards)
area.querySelectorAll('.pk-cards').forEach(el => {
el.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; el.classList.add('drag-over'); });
el.addEventListener('dragleave', e => { if (!el.contains(e.relatedTarget)) el.classList.remove('drag-over'); });
el.addEventListener('drop', e => {
- e.preventDefault();
+ e.preventDefault(); e.stopPropagation();
el.classList.remove('drag-over');
- const cardId = e.dataTransfer.getData('text/plain');
- if (cardId) dropCard(el.dataset.listId, cardId);
+ const cardId = e.dataTransfer.getData('card-id');
+ if (!cardId) return;
+ const listId = el.dataset.listId;
+ const listCards = state.cards.filter(c => c.listId === listId && c.id !== cardId);
+ const maxPos = listCards.reduce((m, c) => Math.max(m, c.position || 0), 0);
+ moveCard(cardId, listId, maxPos + 65535);
});
});
}
@@ -180,15 +206,15 @@ function renderList(list) {
${esc(list.name)}
${cards.length}
-
+
${cards.length === 0 ? '
Déposer ici
' : ''}
- ${cards.map(c => renderCard(c)).join('')}
+ ${cards.map((c, i) => renderCard(c, cards[i-1] ?? null)).join('')}
`;
}
-function renderCard(card) {
+function renderCard(card, prevCard) {
const labels = (state.cardLabels || []).filter(cl => cl.cardId === card.id).map(cl => {
const lbl = (state.labels || []).find(l => l.id === cl.labelId);
if (!lbl) return '';
@@ -203,24 +229,25 @@ function renderCard(card) {
return `📅 ${formatDate(card.dueDate)}`;
})() : '';
const taskHtml = cardTasks.length ? `☑ ${doneCount}/${cardTasks.length}` : '';
+ const prevPos = prevCard ? (prevCard.position ?? 0) : 0;
return `
-
+
+
${labels ? `
${labels}
` : ''}
${esc(card.name)}
${dueHtml || taskHtml ? `
${dueHtml}${taskHtml}
` : ''}
`;
}
-async function dropCard(listId, cardId) {
+async function moveCard(cardId, listId, position) {
const card = state.cards.find(c => c.id === cardId);
- if (!card || card.listId === listId) return;
- // Planka requires position when changing listId
- const targetCards = state.cards.filter(c => c.listId === listId);
- const maxPos = targetCards.reduce((m, c) => Math.max(m, c.position || 0), 0);
- const position = maxPos + 65535;
- const res = await apiFetch('update_card', { id: cardId }, 'PATCH', { listId, position });
+ if (!card) return;
+ // Skip no-op (same list, same position range)
+ if (card.listId === listId && Math.abs((card.position || 0) - position) < 1) return;
+ const body = { listId, position };
+ const res = await apiFetch('update_card', { id: cardId }, 'PATCH', body);
if (!res.ok) { showToast('Erreur : ' + (res.error || 'déplacement impossible'), 'error'); return; }
- card.listId = listId;
+ card.listId = listId;
card.position = position;
renderBoard();
showToast('Carte déplacée ✓');
@@ -229,7 +256,9 @@ async function dropCard(listId, cardId) {
async function quickAddCard(listId) {
const name = prompt('Nom de la carte :');
if (!name || !name.trim()) return;
- const res = await apiFetch('create_card', { list_id: listId, board_id: state.activeBoardId, name: name.trim() });
+ const listCards = state.cards.filter(c => c.listId === listId);
+ const maxPos = listCards.reduce((m, c) => Math.max(m, c.position || 0), 0);
+ const res = await apiFetch('create_card', { list_id: listId, name: name.trim(), position: maxPos + 65535 });
if (!res.ok) { showToast('Erreur: ' + res.error, 'error'); return; }
state.cards.push(res.data);
renderBoard();