feat(kanban): badges done/locked + drag locked en mismo column + migrations + UX stickers

- badges: locked → tiempo bloqueado; done → fecha completion + total lead time; otherwise → tiempo en columna
- locked cards: drag permitido dentro de mismo column (cross-column rejected con notification)
- card field: locked_at desde JOIN card_lock_history (open period)
- migrations: refactor a embed.FS, archivos 002-005 extraidos de ensureColumns; ensureColumns queda como backstop
- stickers UX: opacidad 1, debajo del texto, picker estable (useRef), boton entra directo a modo con 😀, popover cierra outside, cards done filter brightness
- format: formatDateTimeShort

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 00:44:43 +02:00
parent 9931890d9b
commit 5ba0254e57
10 changed files with 175 additions and 28 deletions
+13 -1
View File
@@ -423,6 +423,12 @@ export function App() {
// Card drag
const destCol = resolveColumnId(overId);
if (!destCol) return;
const activeCard = board.cards.find((c) => c.id === activeId);
if (activeCard?.locked && activeCard.column_id !== destCol) {
notifications.show({ color: "yellow", message: "Card bloqueada: no se puede mover entre columnas" });
reload();
return;
}
const destCards = board.cards
.filter((c) => c.column_id === destCol)
.sort((a, b) => a.position - b.position);
@@ -1108,7 +1114,13 @@ export function App() {
variant={activeSticker ? "filled" : "default"}
color={activeSticker ? "yellow" : undefined}
leftSection={<IconMoodSmile size={14} />}
onClick={() => setStickerPickerOpen((v) => !v)}
onClick={() => {
if (!activeSticker) {
setActiveSticker("😀");
} else {
setStickerPickerOpen((v) => !v);
}
}}
>
{activeSticker ? `Modo sticker: ${activeSticker}` : "Stickers"}
</Button>