First commit
This commit is contained in:
118
src/lib/api/calendar.ts
Normal file
118
src/lib/api/calendar.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
import type { Database, CalendarEvent } from '$lib/supabase/types';
|
||||
|
||||
export async function fetchEvents(
|
||||
supabase: SupabaseClient<Database>,
|
||||
orgId: string,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): Promise<CalendarEvent[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('calendar_events')
|
||||
.select('*')
|
||||
.eq('org_id', orgId)
|
||||
.gte('start_time', startDate.toISOString())
|
||||
.lte('end_time', endDate.toISOString())
|
||||
.order('start_time');
|
||||
|
||||
if (error) throw error;
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
export async function createEvent(
|
||||
supabase: SupabaseClient<Database>,
|
||||
orgId: string,
|
||||
event: {
|
||||
title: string;
|
||||
description?: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
all_day?: boolean;
|
||||
color?: string;
|
||||
},
|
||||
userId: string
|
||||
): Promise<CalendarEvent> {
|
||||
const { data, error } = await supabase
|
||||
.from('calendar_events')
|
||||
.insert({
|
||||
org_id: orgId,
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
start_time: event.start_time,
|
||||
end_time: event.end_time,
|
||||
all_day: event.all_day ?? false,
|
||||
color: event.color,
|
||||
created_by: userId
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateEvent(
|
||||
supabase: SupabaseClient<Database>,
|
||||
id: string,
|
||||
updates: Partial<Pick<CalendarEvent, 'title' | 'description' | 'start_time' | 'end_time' | 'all_day' | 'color'>>
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('calendar_events').update(updates).eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export async function deleteEvent(
|
||||
supabase: SupabaseClient<Database>,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('calendar_events').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export function subscribeToEvents(
|
||||
supabase: SupabaseClient<Database>,
|
||||
orgId: string,
|
||||
onChange: () => void
|
||||
) {
|
||||
return supabase
|
||||
.channel(`calendar:${orgId}`)
|
||||
.on('postgres_changes', { event: '*', schema: 'public', table: 'calendar_events', filter: `org_id=eq.${orgId}` }, onChange)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
// Calendar utility functions
|
||||
export function getMonthDays(year: number, month: number): Date[] {
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const days: Date[] = [];
|
||||
|
||||
// Add days from previous month to fill first week
|
||||
const startDayOfWeek = firstDay.getDay();
|
||||
for (let i = startDayOfWeek - 1; i >= 0; i--) {
|
||||
days.push(new Date(year, month, -i));
|
||||
}
|
||||
|
||||
// Add days of current month
|
||||
for (let i = 1; i <= lastDay.getDate(); i++) {
|
||||
days.push(new Date(year, month, i));
|
||||
}
|
||||
|
||||
// Add days from next month to fill last week
|
||||
const remainingDays = 42 - days.length; // 6 weeks * 7 days
|
||||
for (let i = 1; i <= remainingDays; i++) {
|
||||
days.push(new Date(year, month + 1, i));
|
||||
}
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
export function isSameDay(a: Date, b: Date): boolean {
|
||||
return (
|
||||
a.getFullYear() === b.getFullYear() &&
|
||||
a.getMonth() === b.getMonth() &&
|
||||
a.getDate() === b.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
export function formatTime(date: Date): string {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
132
src/lib/api/documents.ts
Normal file
132
src/lib/api/documents.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
import type { Database, Document } from '$lib/supabase/types';
|
||||
|
||||
export interface DocumentWithChildren extends Document {
|
||||
children?: DocumentWithChildren[];
|
||||
}
|
||||
|
||||
export async function fetchDocuments(
|
||||
supabase: SupabaseClient<Database>,
|
||||
orgId: string
|
||||
): Promise<Document[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('documents')
|
||||
.select('*')
|
||||
.eq('org_id', orgId)
|
||||
.order('type', { ascending: false }) // folders first
|
||||
.order('name');
|
||||
|
||||
if (error) throw error;
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
export async function createDocument(
|
||||
supabase: SupabaseClient<Database>,
|
||||
orgId: string,
|
||||
name: string,
|
||||
type: 'folder' | 'document',
|
||||
parentId: string | null = null,
|
||||
userId: string
|
||||
): Promise<Document> {
|
||||
const { data, error } = await supabase
|
||||
.from('documents')
|
||||
.insert({
|
||||
org_id: orgId,
|
||||
name,
|
||||
type,
|
||||
parent_id: parentId,
|
||||
created_by: userId,
|
||||
content: type === 'document' ? { type: 'doc', content: [] } : null
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateDocument(
|
||||
supabase: SupabaseClient<Database>,
|
||||
id: string,
|
||||
updates: Partial<Pick<Document, 'name' | 'content' | 'parent_id'>>
|
||||
): Promise<Document> {
|
||||
const { data, error } = await supabase
|
||||
.from('documents')
|
||||
.update({ ...updates, updated_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteDocument(
|
||||
supabase: SupabaseClient<Database>,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('documents').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export async function moveDocument(
|
||||
supabase: SupabaseClient<Database>,
|
||||
id: string,
|
||||
newParentId: string | null
|
||||
): Promise<void> {
|
||||
const { error } = await supabase
|
||||
.from('documents')
|
||||
.update({ parent_id: newParentId, updated_at: new Date().toISOString() })
|
||||
.eq('id', id);
|
||||
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export function buildDocumentTree(documents: Document[]): DocumentWithChildren[] {
|
||||
const map = new Map<string, DocumentWithChildren>();
|
||||
const roots: DocumentWithChildren[] = [];
|
||||
|
||||
// First pass: create map
|
||||
documents.forEach((doc) => {
|
||||
map.set(doc.id, { ...doc, children: [] });
|
||||
});
|
||||
|
||||
// Second pass: build tree
|
||||
documents.forEach((doc) => {
|
||||
const node = map.get(doc.id)!;
|
||||
if (doc.parent_id && map.has(doc.parent_id)) {
|
||||
map.get(doc.parent_id)!.children!.push(node);
|
||||
} else {
|
||||
roots.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
export function subscribeToDocuments(
|
||||
supabase: SupabaseClient<Database>,
|
||||
orgId: string,
|
||||
onInsert: (doc: Document) => void,
|
||||
onUpdate: (doc: Document) => void,
|
||||
onDelete: (id: string) => void
|
||||
) {
|
||||
return supabase
|
||||
.channel(`documents:${orgId}`)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: 'INSERT', schema: 'public', table: 'documents', filter: `org_id=eq.${orgId}` },
|
||||
(payload) => onInsert(payload.new as Document)
|
||||
)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: 'UPDATE', schema: 'public', table: 'documents', filter: `org_id=eq.${orgId}` },
|
||||
(payload) => onUpdate(payload.new as Document)
|
||||
)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: 'DELETE', schema: 'public', table: 'documents', filter: `org_id=eq.${orgId}` },
|
||||
(payload) => onDelete((payload.old as { id: string }).id)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
264
src/lib/api/google-calendar.ts
Normal file
264
src/lib/api/google-calendar.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
import type { Database } from '$lib/supabase/types';
|
||||
|
||||
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID;
|
||||
const GOOGLE_CLIENT_SECRET = import.meta.env.VITE_GOOGLE_CLIENT_SECRET;
|
||||
|
||||
interface GoogleTokens {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
expires_in: number;
|
||||
}
|
||||
|
||||
interface GoogleCalendarEvent {
|
||||
id: string;
|
||||
summary: string;
|
||||
description?: string;
|
||||
start: { dateTime?: string; date?: string };
|
||||
end: { dateTime?: string; date?: string };
|
||||
colorId?: string;
|
||||
}
|
||||
|
||||
export function getGoogleAuthUrl(redirectUri: string, state: string): string {
|
||||
const params = new URLSearchParams({
|
||||
client_id: GOOGLE_CLIENT_ID,
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
scope: 'https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events',
|
||||
access_type: 'offline',
|
||||
prompt: 'consent',
|
||||
state
|
||||
});
|
||||
return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
|
||||
}
|
||||
|
||||
export async function exchangeCodeForTokens(code: string, redirectUri: string): Promise<GoogleTokens> {
|
||||
const response = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
code,
|
||||
client_id: GOOGLE_CLIENT_ID,
|
||||
client_secret: GOOGLE_CLIENT_SECRET,
|
||||
redirect_uri: redirectUri,
|
||||
grant_type: 'authorization_code'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to exchange code for tokens');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function refreshAccessToken(refreshToken: string): Promise<GoogleTokens> {
|
||||
const response = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
refresh_token: refreshToken,
|
||||
client_id: GOOGLE_CLIENT_ID,
|
||||
client_secret: GOOGLE_CLIENT_SECRET,
|
||||
grant_type: 'refresh_token'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to refresh access token');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getValidAccessToken(
|
||||
supabase: SupabaseClient<Database>,
|
||||
userId: string
|
||||
): Promise<string | null> {
|
||||
const { data: connection } = await supabase
|
||||
.from('google_calendar_connections')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (!connection) return null;
|
||||
|
||||
const expiresAt = new Date(connection.token_expires_at);
|
||||
const now = new Date();
|
||||
|
||||
// Refresh if expires within 5 minutes
|
||||
if (expiresAt.getTime() - now.getTime() < 5 * 60 * 1000) {
|
||||
try {
|
||||
const tokens = await refreshAccessToken(connection.refresh_token);
|
||||
const newExpiresAt = new Date(Date.now() + tokens.expires_in * 1000);
|
||||
|
||||
await supabase
|
||||
.from('google_calendar_connections')
|
||||
.update({
|
||||
access_token: tokens.access_token,
|
||||
token_expires_at: newExpiresAt.toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('user_id', userId);
|
||||
|
||||
return tokens.access_token;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return connection.access_token;
|
||||
}
|
||||
|
||||
export async function fetchGoogleCalendarEvents(
|
||||
accessToken: string,
|
||||
calendarId: string = 'primary',
|
||||
timeMin: Date,
|
||||
timeMax: Date
|
||||
): Promise<GoogleCalendarEvent[]> {
|
||||
const params = new URLSearchParams({
|
||||
timeMin: timeMin.toISOString(),
|
||||
timeMax: timeMax.toISOString(),
|
||||
singleEvents: 'true',
|
||||
orderBy: 'startTime'
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events?${params}`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${accessToken}` }
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch Google Calendar events');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.items ?? [];
|
||||
}
|
||||
|
||||
export async function createGoogleCalendarEvent(
|
||||
accessToken: string,
|
||||
calendarId: string = 'primary',
|
||||
event: {
|
||||
title: string;
|
||||
description?: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
allDay?: boolean;
|
||||
}
|
||||
): Promise<GoogleCalendarEvent> {
|
||||
const body: Record<string, unknown> = {
|
||||
summary: event.title,
|
||||
description: event.description
|
||||
};
|
||||
|
||||
if (event.allDay) {
|
||||
const startDate = event.startTime.split('T')[0];
|
||||
const endDate = event.endTime.split('T')[0];
|
||||
body.start = { date: startDate };
|
||||
body.end = { date: endDate };
|
||||
} else {
|
||||
body.start = { dateTime: event.startTime, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone };
|
||||
body.end = { dateTime: event.endTime, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone };
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create Google Calendar event');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function updateGoogleCalendarEvent(
|
||||
accessToken: string,
|
||||
calendarId: string = 'primary',
|
||||
eventId: string,
|
||||
event: {
|
||||
title: string;
|
||||
description?: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
allDay?: boolean;
|
||||
}
|
||||
): Promise<GoogleCalendarEvent> {
|
||||
const body: Record<string, unknown> = {
|
||||
summary: event.title,
|
||||
description: event.description
|
||||
};
|
||||
|
||||
if (event.allDay) {
|
||||
const startDate = event.startTime.split('T')[0];
|
||||
const endDate = event.endTime.split('T')[0];
|
||||
body.start = { date: startDate };
|
||||
body.end = { date: endDate };
|
||||
} else {
|
||||
body.start = { dateTime: event.startTime, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone };
|
||||
body.end = { dateTime: event.endTime, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone };
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events/${eventId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update Google Calendar event');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function deleteGoogleCalendarEvent(
|
||||
accessToken: string,
|
||||
calendarId: string = 'primary',
|
||||
eventId: string
|
||||
): Promise<void> {
|
||||
const response = await fetch(
|
||||
`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events/${eventId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${accessToken}` }
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok && response.status !== 404) {
|
||||
throw new Error('Failed to delete Google Calendar event');
|
||||
}
|
||||
}
|
||||
|
||||
export async function listGoogleCalendars(accessToken: string): Promise<{ id: string; summary: string }[]> {
|
||||
const response = await fetch('https://www.googleapis.com/calendar/v3/users/me/calendarList', {
|
||||
headers: { Authorization: `Bearer ${accessToken}` }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to list calendars');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return (data.items ?? []).map((cal: { id: string; summary: string }) => ({
|
||||
id: cal.id,
|
||||
summary: cal.summary
|
||||
}));
|
||||
}
|
||||
215
src/lib/api/kanban.ts
Normal file
215
src/lib/api/kanban.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
import type { Database, KanbanBoard, KanbanColumn, KanbanCard } from '$lib/supabase/types';
|
||||
|
||||
export interface ColumnWithCards extends KanbanColumn {
|
||||
cards: KanbanCard[];
|
||||
}
|
||||
|
||||
export interface BoardWithColumns extends KanbanBoard {
|
||||
columns: ColumnWithCards[];
|
||||
}
|
||||
|
||||
export async function fetchBoards(
|
||||
supabase: SupabaseClient<Database>,
|
||||
orgId: string
|
||||
): Promise<KanbanBoard[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('kanban_boards')
|
||||
.select('*')
|
||||
.eq('org_id', orgId)
|
||||
.order('created_at');
|
||||
|
||||
if (error) throw error;
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
export async function fetchBoardWithColumns(
|
||||
supabase: SupabaseClient<Database>,
|
||||
boardId: string
|
||||
): Promise<BoardWithColumns | null> {
|
||||
const { data: board, error: boardError } = await supabase
|
||||
.from('kanban_boards')
|
||||
.select('*')
|
||||
.eq('id', boardId)
|
||||
.single();
|
||||
|
||||
if (boardError) throw boardError;
|
||||
if (!board) return null;
|
||||
|
||||
const { data: columns, error: colError } = await supabase
|
||||
.from('kanban_columns')
|
||||
.select('*')
|
||||
.eq('board_id', boardId)
|
||||
.order('position');
|
||||
|
||||
if (colError) throw colError;
|
||||
|
||||
const { data: cards, error: cardError } = await supabase
|
||||
.from('kanban_cards')
|
||||
.select('*')
|
||||
.in('column_id', (columns ?? []).map((c) => c.id))
|
||||
.order('position');
|
||||
|
||||
if (cardError) throw cardError;
|
||||
|
||||
const cardsByColumn = new Map<string, KanbanCard[]>();
|
||||
(cards ?? []).forEach((card) => {
|
||||
if (!cardsByColumn.has(card.column_id)) {
|
||||
cardsByColumn.set(card.column_id, []);
|
||||
}
|
||||
cardsByColumn.get(card.column_id)!.push(card);
|
||||
});
|
||||
|
||||
return {
|
||||
...board,
|
||||
columns: (columns ?? []).map((col) => ({
|
||||
...col,
|
||||
cards: cardsByColumn.get(col.id) ?? []
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
export async function createBoard(
|
||||
supabase: SupabaseClient<Database>,
|
||||
orgId: string,
|
||||
name: string
|
||||
): Promise<KanbanBoard> {
|
||||
const { data, error } = await supabase
|
||||
.from('kanban_boards')
|
||||
.insert({ org_id: orgId, name })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Create default columns
|
||||
const defaultColumns = ['To Do', 'In Progress', 'Done'];
|
||||
await supabase.from('kanban_columns').insert(
|
||||
defaultColumns.map((name, index) => ({
|
||||
board_id: data.id,
|
||||
name,
|
||||
position: index
|
||||
}))
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateBoard(
|
||||
supabase: SupabaseClient<Database>,
|
||||
id: string,
|
||||
name: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_boards').update({ name }).eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export async function deleteBoard(
|
||||
supabase: SupabaseClient<Database>,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_boards').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export async function createColumn(
|
||||
supabase: SupabaseClient<Database>,
|
||||
boardId: string,
|
||||
name: string,
|
||||
position: number
|
||||
): Promise<KanbanColumn> {
|
||||
const { data, error } = await supabase
|
||||
.from('kanban_columns')
|
||||
.insert({ board_id: boardId, name, position })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateColumn(
|
||||
supabase: SupabaseClient<Database>,
|
||||
id: string,
|
||||
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;
|
||||
}
|
||||
|
||||
export async function deleteColumn(
|
||||
supabase: SupabaseClient<Database>,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_columns').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export async function createCard(
|
||||
supabase: SupabaseClient<Database>,
|
||||
columnId: string,
|
||||
title: string,
|
||||
position: number,
|
||||
userId: string
|
||||
): Promise<KanbanCard> {
|
||||
const { data, error } = await supabase
|
||||
.from('kanban_cards')
|
||||
.insert({
|
||||
column_id: columnId,
|
||||
title,
|
||||
position,
|
||||
created_by: userId
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateCard(
|
||||
supabase: SupabaseClient<Database>,
|
||||
id: string,
|
||||
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;
|
||||
}
|
||||
|
||||
export async function deleteCard(
|
||||
supabase: SupabaseClient<Database>,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_cards').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export async function moveCard(
|
||||
supabase: SupabaseClient<Database>,
|
||||
cardId: string,
|
||||
newColumnId: string,
|
||||
newPosition: number
|
||||
): Promise<void> {
|
||||
const { error } = await supabase
|
||||
.from('kanban_cards')
|
||||
.update({ column_id: newColumnId, position: newPosition })
|
||||
.eq('id', cardId);
|
||||
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export function subscribeToBoard(
|
||||
supabase: SupabaseClient<Database>,
|
||||
boardId: string,
|
||||
onColumnChange: () => void,
|
||||
onCardChange: () => void
|
||||
) {
|
||||
const channel = supabase.channel(`kanban:${boardId}`);
|
||||
|
||||
channel
|
||||
.on('postgres_changes', { event: '*', schema: 'public', table: 'kanban_columns', filter: `board_id=eq.${boardId}` }, onColumnChange)
|
||||
.on('postgres_changes', { event: '*', schema: 'public', table: 'kanban_cards' }, onCardChange)
|
||||
.subscribe();
|
||||
|
||||
return channel;
|
||||
}
|
||||
164
src/lib/api/organizations.ts
Normal file
164
src/lib/api/organizations.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
import type { Database, Organization, MemberRole } from '$lib/supabase/types';
|
||||
import type { OrgWithRole } from '$lib/stores/organizations.svelte';
|
||||
|
||||
export async function fetchUserOrganizations(
|
||||
supabase: SupabaseClient<Database>
|
||||
): Promise<OrgWithRole[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('org_members')
|
||||
.select(`
|
||||
role,
|
||||
organizations (
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
avatar_url,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
`)
|
||||
.not('joined_at', 'is', null);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return (data ?? [])
|
||||
.filter((item) => item.organizations)
|
||||
.map((item) => ({
|
||||
...(item.organizations as Organization),
|
||||
role: item.role as MemberRole
|
||||
}));
|
||||
}
|
||||
|
||||
export async function createOrganization(
|
||||
supabase: SupabaseClient<Database>,
|
||||
name: string,
|
||||
slug: string
|
||||
): Promise<Organization> {
|
||||
const { data, error } = await supabase
|
||||
.from('organizations')
|
||||
.insert({ name, slug })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateOrganization(
|
||||
supabase: SupabaseClient<Database>,
|
||||
id: string,
|
||||
updates: Partial<Pick<Organization, 'name' | 'slug' | 'avatar_url'>>
|
||||
): Promise<Organization> {
|
||||
const { data, error } = await supabase
|
||||
.from('organizations')
|
||||
.update({ ...updates, updated_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteOrganization(
|
||||
supabase: SupabaseClient<Database>,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('organizations').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export async function fetchOrgMembers(
|
||||
supabase: SupabaseClient<Database>,
|
||||
orgId: string
|
||||
) {
|
||||
const { data, error } = await supabase
|
||||
.from('org_members')
|
||||
.select(`
|
||||
id,
|
||||
org_id,
|
||||
user_id,
|
||||
role,
|
||||
invited_at,
|
||||
joined_at,
|
||||
profiles (
|
||||
email,
|
||||
full_name,
|
||||
avatar_url
|
||||
)
|
||||
`)
|
||||
.eq('org_id', orgId);
|
||||
|
||||
if (error) throw error;
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
export async function inviteMember(
|
||||
supabase: SupabaseClient<Database>,
|
||||
orgId: string,
|
||||
email: string,
|
||||
role: MemberRole = 'viewer'
|
||||
): Promise<void> {
|
||||
// First, find user by email
|
||||
const { data: profile, error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.select('id')
|
||||
.eq('email', email)
|
||||
.single();
|
||||
|
||||
if (profileError || !profile) {
|
||||
throw new Error('User not found. They need to sign up first.');
|
||||
}
|
||||
|
||||
// Check if already a member
|
||||
const { data: existing } = await supabase
|
||||
.from('org_members')
|
||||
.select('id')
|
||||
.eq('org_id', orgId)
|
||||
.eq('user_id', profile.id)
|
||||
.single();
|
||||
|
||||
if (existing) {
|
||||
throw new Error('User is already a member of this organization.');
|
||||
}
|
||||
|
||||
// Create invitation
|
||||
const { error } = await supabase.from('org_members').insert({
|
||||
org_id: orgId,
|
||||
user_id: profile.id,
|
||||
role,
|
||||
joined_at: new Date().toISOString() // Auto-join for now
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export async function updateMemberRole(
|
||||
supabase: SupabaseClient<Database>,
|
||||
memberId: string,
|
||||
role: MemberRole
|
||||
): Promise<void> {
|
||||
const { error } = await supabase
|
||||
.from('org_members')
|
||||
.update({ role })
|
||||
.eq('id', memberId);
|
||||
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export async function removeMember(
|
||||
supabase: SupabaseClient<Database>,
|
||||
memberId: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('org_members').delete().eq('id', memberId);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export function generateSlug(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
.slice(0, 50);
|
||||
}
|
||||
Reference in New Issue
Block a user