Mega push vol 4
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
import type { Database, KanbanBoard, KanbanColumn, KanbanCard } from '$lib/supabase/types';
|
||||
import { createLogger } from '$lib/utils/logger';
|
||||
|
||||
const log = createLogger('api.kanban');
|
||||
|
||||
export interface ColumnWithCards extends KanbanColumn {
|
||||
cards: KanbanCard[];
|
||||
@@ -19,7 +22,11 @@ export async function fetchBoards(
|
||||
.eq('org_id', orgId)
|
||||
.order('created_at');
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('fetchBoards failed', { error, data: { orgId } });
|
||||
throw error;
|
||||
}
|
||||
log.debug('fetchBoards ok', { data: { count: data?.length ?? 0 } });
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
@@ -33,7 +40,10 @@ export async function fetchBoardWithColumns(
|
||||
.eq('id', boardId)
|
||||
.single();
|
||||
|
||||
if (boardError) throw boardError;
|
||||
if (boardError) {
|
||||
log.error('fetchBoardWithColumns failed (board)', { error: boardError, data: { boardId } });
|
||||
throw boardError;
|
||||
}
|
||||
if (!board) return null;
|
||||
|
||||
const { data: columns, error: colError } = await supabase
|
||||
@@ -42,22 +52,55 @@ export async function fetchBoardWithColumns(
|
||||
.eq('board_id', boardId)
|
||||
.order('position');
|
||||
|
||||
if (colError) throw colError;
|
||||
if (colError) {
|
||||
log.error('fetchBoardWithColumns failed (columns)', { error: colError, data: { boardId } });
|
||||
throw colError;
|
||||
}
|
||||
|
||||
const columnIds = (columns ?? []).map((c) => c.id);
|
||||
|
||||
const { data: cards, error: cardError } = await supabase
|
||||
.from('kanban_cards')
|
||||
.select('*')
|
||||
.in('column_id', (columns ?? []).map((c) => c.id))
|
||||
.in('column_id', columnIds)
|
||||
.order('position');
|
||||
|
||||
if (cardError) throw cardError;
|
||||
if (cardError) {
|
||||
log.error('fetchBoardWithColumns failed (cards)', { error: cardError, data: { boardId } });
|
||||
throw cardError;
|
||||
}
|
||||
|
||||
const cardsByColumn = new Map<string, KanbanCard[]>();
|
||||
// Fetch tags for all cards in one query
|
||||
const cardIds = (cards ?? []).map((c) => c.id);
|
||||
let cardTagsMap = new Map<string, { id: string; name: string; color: string | null }[]>();
|
||||
|
||||
if (cardIds.length > 0) {
|
||||
const { data: cardTags } = await supabase
|
||||
.from('card_tags')
|
||||
.select('card_id, tags:tag_id (id, name, color)')
|
||||
.in('card_id', cardIds);
|
||||
|
||||
(cardTags ?? []).forEach((ct: any) => {
|
||||
const tag = Array.isArray(ct.tags) ? ct.tags[0] : ct.tags;
|
||||
if (!tag) return;
|
||||
if (!cardTagsMap.has(ct.card_id)) {
|
||||
cardTagsMap.set(ct.card_id, []);
|
||||
}
|
||||
cardTagsMap.get(ct.card_id)!.push(tag);
|
||||
});
|
||||
}
|
||||
|
||||
const cardsByColumn = new Map<string, (KanbanCard & { tags?: { id: string; name: string; color: string | null }[] })[]>();
|
||||
(cards ?? []).forEach((card) => {
|
||||
if (!cardsByColumn.has(card.column_id)) {
|
||||
cardsByColumn.set(card.column_id, []);
|
||||
const colId = card.column_id;
|
||||
if (!colId) return;
|
||||
if (!cardsByColumn.has(colId)) {
|
||||
cardsByColumn.set(colId, []);
|
||||
}
|
||||
cardsByColumn.get(card.column_id)!.push(card);
|
||||
cardsByColumn.get(colId)!.push({
|
||||
...card,
|
||||
tags: cardTagsMap.get(card.id) ?? []
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -74,13 +117,17 @@ export async function createBoard(
|
||||
orgId: string,
|
||||
name: string
|
||||
): Promise<KanbanBoard> {
|
||||
log.info('createBoard', { data: { orgId, name } });
|
||||
const { data, error } = await supabase
|
||||
.from('kanban_boards')
|
||||
.insert({ org_id: orgId, name })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('createBoard failed', { error, data: { orgId, name } });
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Create default columns
|
||||
const defaultColumns = ['To Do', 'In Progress', 'Done'];
|
||||
@@ -101,7 +148,10 @@ export async function updateBoard(
|
||||
name: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_boards').update({ name }).eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('updateBoard failed', { error, data: { id, name } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteBoard(
|
||||
@@ -109,7 +159,10 @@ export async function deleteBoard(
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_boards').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('deleteBoard failed', { error, data: { id } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createColumn(
|
||||
@@ -124,7 +177,10 @@ export async function createColumn(
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('createColumn failed', { error, data: { boardId, name, position } });
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -134,7 +190,10 @@ export async function updateColumn(
|
||||
updates: Partial<Pick<KanbanColumn, 'name' | 'position' | 'color'>>
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_columns').update(updates).eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('updateColumn failed', { error, data: { id, updates } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteColumn(
|
||||
@@ -142,7 +201,10 @@ export async function deleteColumn(
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_columns').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('deleteColumn failed', { error, data: { id } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCard(
|
||||
@@ -163,7 +225,10 @@ export async function createCard(
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('createCard failed', { error, data: { columnId, title, position } });
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -173,7 +238,10 @@ export async function updateCard(
|
||||
updates: Partial<Pick<KanbanCard, 'title' | 'description' | 'column_id' | 'position' | 'due_date' | 'color'>>
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_cards').update(updates).eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('updateCard failed', { error, data: { id, updates } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCard(
|
||||
@@ -181,7 +249,10 @@ export async function deleteCard(
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_cards').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('deleteCard failed', { error, data: { id } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function moveCard(
|
||||
@@ -190,12 +261,48 @@ export async function moveCard(
|
||||
newColumnId: string,
|
||||
newPosition: number
|
||||
): Promise<void> {
|
||||
const { error } = await supabase
|
||||
// Fetch all cards in the target column (ordered by position)
|
||||
const { data: targetCards, error: fetchErr } = await supabase
|
||||
.from('kanban_cards')
|
||||
.update({ column_id: newColumnId, position: newPosition })
|
||||
.eq('id', cardId);
|
||||
.select('id, position')
|
||||
.eq('column_id', newColumnId)
|
||||
.order('position');
|
||||
|
||||
if (error) throw error;
|
||||
if (fetchErr) {
|
||||
log.error('moveCard: failed to fetch target column cards', { error: fetchErr });
|
||||
throw fetchErr;
|
||||
}
|
||||
|
||||
// Remove the moved card from the list if it's already in this column
|
||||
const otherCards = (targetCards ?? []).filter((c) => c.id !== cardId);
|
||||
|
||||
// Insert at the new position and reassign sequential positions
|
||||
const reordered = [
|
||||
...otherCards.slice(0, newPosition),
|
||||
{ id: cardId },
|
||||
...otherCards.slice(newPosition),
|
||||
];
|
||||
|
||||
// Batch update: move card to column + set position, then update siblings
|
||||
const updates = reordered.map((c, i) => {
|
||||
if (c.id === cardId) {
|
||||
return supabase
|
||||
.from('kanban_cards')
|
||||
.update({ column_id: newColumnId, position: i })
|
||||
.eq('id', c.id);
|
||||
}
|
||||
return supabase
|
||||
.from('kanban_cards')
|
||||
.update({ position: i })
|
||||
.eq('id', c.id);
|
||||
});
|
||||
|
||||
const results = await Promise.all(updates);
|
||||
const failed = results.find((r) => r.error);
|
||||
if (failed?.error) {
|
||||
log.error('moveCard failed', { error: failed.error, data: { cardId, newColumnId, newPosition } });
|
||||
throw failed.error;
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeToBoard(
|
||||
|
||||
Reference in New Issue
Block a user