-
{board.name}
{board.groupId && (
-
-
+
{board.groupId}
@@ -177,17 +201,22 @@ function EnhancedBoardCard({ board }: { board: EnhancedBoard }) {
Progress
- {board.stats.completionRate}%
+
+ {board.stats.completionRate}%
+
-
-
{board.stats.completionRate > 80 && (
-
+
)}
@@ -196,19 +225,19 @@ function EnhancedBoardCard({ board }: { board: EnhancedBoard }) {
{/* Enhanced Stats Grid */}
-
+
- {board.stats.totalTasks}
- tasks
+ {board.stats.totalTasks}
+ tasks
-
+
- {board.stats.totalLists}
- lists
+ {board.stats.totalLists}
+ lists
@@ -216,22 +245,30 @@ function EnhancedBoardCard({ board }: { board: EnhancedBoard }) {
{/* Status Indicators */}
{board.stats.activeTasks > 0 && (
-
+
- Active tasks
+
+ Active tasks
+
-
{board.stats.activeTasks}
+
+ {board.stats.activeTasks}
+
)}
{board.stats.overdueTasks > 0 && (
-
+
-
{board.stats.overdueTasks}
+
+ {board.stats.overdueTasks}
+
)}
@@ -239,19 +276,28 @@ function EnhancedBoardCard({ board }: { board: EnhancedBoard }) {
{/* Enhanced Priority Badges */}
{board.stats.priorityDistribution.urgent > 0 && (
-
+
{board.stats.priorityDistribution.urgent} urgent
)}
{board.stats.priorityDistribution.high > 0 && (
-
+
{board.stats.priorityDistribution.high} high
)}
{board.stats.completionRate > 75 && (
-
+
High progress
@@ -259,19 +305,21 @@ function EnhancedBoardCard({ board }: { board: EnhancedBoard }) {
{/* Last Activity */}
-
+
- Updated {new Date(board.stats.lastActivity).toLocaleDateString()}
+
+ Updated {new Date(board.stats.lastActivity).toLocaleDateString()}
+
{/* Enhanced Action Button */}
-
@@ -279,31 +327,43 @@ function EnhancedBoardCard({ board }: { board: EnhancedBoard }) {
);
}
-function GroupsView({
- boards,
- settings,
- onSettingsChange
-}: {
- boards: EnhancedBoard[];
+function GroupsView({
+ boards,
+ settings,
+ onSettingsChange,
+}: {
+ boards: EnhancedBoard[];
settings: ViewSettings;
onSettingsChange: (settings: ViewSettings) => void;
}) {
const [groups, setGroups] = useState
([
- { id: 'gaming', name: 'Gaming', boards: [], color: GROUP_COLORS.Gaming, order: 1 },
- { id: 'robotics', name: 'Robotics', boards: [], color: GROUP_COLORS.Robotics, order: 2 },
+ {
+ id: 'gaming',
+ name: 'Gaming',
+ boards: [],
+ color: GROUP_COLORS.Gaming,
+ order: 1,
+ },
+ {
+ id: 'robotics',
+ name: 'Robotics',
+ boards: [],
+ color: GROUP_COLORS.Robotics,
+ order: 2,
+ },
]);
const [newGroupName, setNewGroupName] = useState('');
const [isCreatingGroup, setIsCreatingGroup] = useState(false);
// Organize boards into groups
- const organizedGroups = groups.map(group => ({
+ const organizedGroups = groups.map((group) => ({
...group,
- boards: boards.filter(board => board.groupId === group.id)
+ boards: boards.filter((board) => board.groupId === group.id),
}));
// Unassigned boards
- const unassignedBoards = boards.filter(board =>
- !groups.find(group => group.id === board.groupId)
+ const unassignedBoards = boards.filter(
+ (board) => !groups.find((group) => group.id === board.groupId)
);
const handleCreateGroup = () => {
@@ -313,7 +373,7 @@ function GroupsView({
name: newGroupName,
boards: [],
color: GROUP_COLORS.Default,
- order: groups.length + 1
+ order: groups.length + 1,
};
setGroups([...groups, newGroup]);
setNewGroupName('');
@@ -322,28 +382,28 @@ function GroupsView({
};
const handleDeleteGroup = (groupId: string) => {
- setGroups(groups.filter(g => g.id !== groupId));
+ setGroups(groups.filter((g) => g.id !== groupId));
};
const toggleGroup = (groupId: string) => {
const collapsed = settings.groupView?.collapsed?.includes(groupId)
- ? settings.groupView.collapsed.filter(id => id !== groupId)
+ ? settings.groupView.collapsed.filter((id) => id !== groupId)
: [...(settings.groupView?.collapsed || []), groupId];
-
+
onSettingsChange({
...settings,
- groupView: {
- ...settings.groupView,
- collapsed
- }
+ groupView: {
+ ...settings.groupView,
+ collapsed,
+ },
});
};
return (
-
+
{/* Groups Management Header */}
-
+
Board Groups
{isCreatingGroup ? (
@@ -361,13 +421,17 @@ function GroupsView({
-
) : (
setIsCreatingGroup(true)}>
-
+
Add Group
)}
@@ -376,27 +440,29 @@ function GroupsView({
{/* Scrollable Groups Container */}
-
+
{/* Existing Groups */}
{organizedGroups.map((group) => (
-
toggleGroup(group.id)}
onDelete={() => handleDeleteGroup(group.id)}
/>
))}
-
+
{/* Unassigned Boards Column */}
{unassignedBoards.length > 0 && (
- {}}
@@ -411,26 +477,26 @@ function GroupsView({
);
}
-function GroupColumn({
- group,
- isCollapsed,
- onToggle,
+function GroupColumn({
+ group,
+ isCollapsed,
+ onToggle,
onDelete,
- isUnassigned = false
-}: {
- group: BoardGroup;
- isCollapsed: boolean;
- onToggle: () => void;
+ isUnassigned = false,
+}: {
+ group: BoardGroup;
+ isCollapsed: boolean;
+ onToggle: () => void;
onDelete: () => void;
isUnassigned?: boolean;
}) {
const [dragOver, setDragOver] = useState(false);
return (
- {
e.preventDefault();
@@ -443,15 +509,15 @@ function GroupColumn({
// Handle board drop logic here
}}
>
-
+
{/* Group Header */}
-
-
+
-
{group.name}
@@ -468,11 +534,11 @@ function GroupColumn({
)}
-
+
{!isUnassigned && (
-
@@ -483,13 +549,17 @@ function GroupColumn({
{/* Boards List */}
{!isCollapsed && (
-
+
{group.boards.map((board) => (
-
+
))}
-
+
{group.boards.length === 0 && (
-
+
Drop boards here
)}
@@ -500,25 +570,33 @@ function GroupColumn({
);
}
-function DraggableBoardCard({ board, groupColor }: { board: EnhancedBoard; groupColor: string }) {
+function DraggableBoardCard({
+ board,
+ groupColor,
+}: {
+ board: EnhancedBoard;
+ groupColor: string;
+}) {
const [isDragging, setIsDragging] = useState(false);
return (
-
setIsDragging(true)}
onDragEnd={() => setIsDragging(false)}
>
-
+
-
{board.name}
@@ -549,10 +627,17 @@ function DraggableBoardCard({ board, groupColor }: { board: EnhancedBoard; group
{/* Status Badges */}
{board.stats.hasUrgentTasks && (
- Urgent
+
+ Urgent
+
)}
{board.stats.hasMultipleOverdue && (
- Overdue
+
+ Overdue
+
)}
@@ -579,25 +664,38 @@ function BoardActionsMenu({ board }: { board: EnhancedBoard }) {
return (
-
+
-
+
Edit Board
-
+
Archive Board
-
+
Delete Board
);
-}
\ No newline at end of file
+}
diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/components/view-settings.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/components/view-settings.tsx
index b3dd668f9..069fb5b3c 100644
--- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/components/view-settings.tsx
+++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/components/view-settings.tsx
@@ -1,20 +1,20 @@
'use client';
-import { useState } from 'react';
+import { SmartFilters, ViewSettings } from '../types';
+import { Badge } from '@tuturuuu/ui/badge';
import { Button } from '@tuturuuu/ui/button';
import {
DropdownMenu,
+ DropdownMenuCheckboxItem,
DropdownMenuContent,
- DropdownMenuTrigger,
DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
} from '@tuturuuu/ui/dropdown-menu';
-import { Badge } from '@tuturuuu/ui/badge';
import { Columns3, RefreshCw, SortAsc, SortDesc } from '@tuturuuu/ui/icons';
-import { ViewSettings, SmartFilters } from '../types';
+import { useState } from 'react';
interface ViewSettingsPanelProps {
settings: ViewSettings;
@@ -62,17 +62,19 @@ export function ViewSettingsPanel({
-
+
{isRefreshing ? 'Refreshing...' : 'Refresh Data'}
-
+
-
+
{/* Sorting Section */}
@@ -84,18 +86,28 @@ export function ViewSettingsPanel({
onSettingsChange({ ...settings, sortBy: value as any })
}
>
- Sort by Name
- Sort by ID
- Sort by Created Date
- Sort by Progress
- Sort by Task Count
+
+ Sort by Name
+
+
+ Sort by ID
+
+
+ Sort by Created Date
+
+
+ Sort by Progress
+
+
+ Sort by Task Count
+
-
+
onSettingsChange({
...settings,
@@ -126,23 +138,45 @@ export function ViewSettingsPanel({
{
- const columns = settings.visibleColumns || ['board', 'progress', 'tasks', 'status', 'last_updated', 'actions'];
+ const columns = settings.visibleColumns || [
+ 'board',
+ 'progress',
+ 'tasks',
+ 'status',
+ 'last_updated',
+ 'actions',
+ ];
const newColumns = checked
- ? [...columns.filter(c => c !== 'board'), 'board']
- : columns.filter(c => c !== 'board');
- onSettingsChange({ ...settings, visibleColumns: newColumns });
+ ? [...columns.filter((c) => c !== 'board'), 'board']
+ : columns.filter((c) => c !== 'board');
+ onSettingsChange({
+ ...settings,
+ visibleColumns: newColumns,
+ });
}}
>
Board Name
{
- const columns = settings.visibleColumns || ['board', 'progress', 'tasks', 'status', 'last_updated', 'actions'];
+ const columns = settings.visibleColumns || [
+ 'board',
+ 'progress',
+ 'tasks',
+ 'status',
+ 'last_updated',
+ 'actions',
+ ];
const newColumns = checked
- ? [...columns.filter(c => c !== 'progress'), 'progress']
- : columns.filter(c => c !== 'progress');
- onSettingsChange({ ...settings, visibleColumns: newColumns });
+ ? [...columns.filter((c) => c !== 'progress'), 'progress']
+ : columns.filter((c) => c !== 'progress');
+ onSettingsChange({
+ ...settings,
+ visibleColumns: newColumns,
+ });
}}
>
Progress
@@ -150,11 +184,21 @@ export function ViewSettingsPanel({
{
- const columns = settings.visibleColumns || ['board', 'progress', 'tasks', 'status', 'last_updated', 'actions'];
+ const columns = settings.visibleColumns || [
+ 'board',
+ 'progress',
+ 'tasks',
+ 'status',
+ 'last_updated',
+ 'actions',
+ ];
const newColumns = checked
- ? [...columns.filter(c => c !== 'tasks'), 'tasks']
- : columns.filter(c => c !== 'tasks');
- onSettingsChange({ ...settings, visibleColumns: newColumns });
+ ? [...columns.filter((c) => c !== 'tasks'), 'tasks']
+ : columns.filter((c) => c !== 'tasks');
+ onSettingsChange({
+ ...settings,
+ visibleColumns: newColumns,
+ });
}}
>
Tasks
@@ -162,23 +206,48 @@ export function ViewSettingsPanel({
{
- const columns = settings.visibleColumns || ['board', 'progress', 'tasks', 'status', 'last_updated', 'actions'];
+ const columns = settings.visibleColumns || [
+ 'board',
+ 'progress',
+ 'tasks',
+ 'status',
+ 'last_updated',
+ 'actions',
+ ];
const newColumns = checked
- ? [...columns.filter(c => c !== 'status'), 'status']
- : columns.filter(c => c !== 'status');
- onSettingsChange({ ...settings, visibleColumns: newColumns });
+ ? [...columns.filter((c) => c !== 'status'), 'status']
+ : columns.filter((c) => c !== 'status');
+ onSettingsChange({
+ ...settings,
+ visibleColumns: newColumns,
+ });
}}
>
Status
{
- const columns = settings.visibleColumns || ['board', 'progress', 'tasks', 'status', 'last_updated', 'actions'];
+ const columns = settings.visibleColumns || [
+ 'board',
+ 'progress',
+ 'tasks',
+ 'status',
+ 'last_updated',
+ 'actions',
+ ];
const newColumns = checked
- ? [...columns.filter(c => c !== 'last_updated'), 'last_updated']
- : columns.filter(c => c !== 'last_updated');
- onSettingsChange({ ...settings, visibleColumns: newColumns });
+ ? [
+ ...columns.filter((c) => c !== 'last_updated'),
+ 'last_updated',
+ ]
+ : columns.filter((c) => c !== 'last_updated');
+ onSettingsChange({
+ ...settings,
+ visibleColumns: newColumns,
+ });
}}
>
Last Updated
@@ -186,11 +255,21 @@ export function ViewSettingsPanel({
{
- const columns = settings.visibleColumns || ['board', 'progress', 'tasks', 'status', 'last_updated', 'actions'];
+ const columns = settings.visibleColumns || [
+ 'board',
+ 'progress',
+ 'tasks',
+ 'status',
+ 'last_updated',
+ 'actions',
+ ];
const newColumns = checked
- ? [...columns.filter(c => c !== 'actions'), 'actions']
- : columns.filter(c => c !== 'actions');
- onSettingsChange({ ...settings, visibleColumns: newColumns });
+ ? [...columns.filter((c) => c !== 'actions'), 'actions']
+ : columns.filter((c) => c !== 'actions');
+ onSettingsChange({
+ ...settings,
+ visibleColumns: newColumns,
+ });
}}
>
Actions
@@ -208,7 +287,7 @@ export function ViewSettingsPanel({
smartFilters.hasUrgentTasks && 'Urgent Tasks',
smartFilters.hasMultipleOverdue && 'Multiple Overdue',
smartFilters.hasWorkloadImbalance && 'Workload Imbalance',
- settings.forceShowAll && 'Show All Override'
+ settings.forceShowAll && 'Show All Override',
].filter(Boolean);
return (
@@ -268,13 +347,23 @@ export function ViewSettingsPanel({
onSettingsChange({ ...settings, sortBy: value as any })
}
>
- Sort by Name
+
+ Sort by Name
+
Sort by ID
- Sort by Created Date
- Sort by Progress
- Sort by Task Count
+
+ Sort by Created Date
+
+
+ Sort by Progress
+
+
+ Sort by Task Count
+
{settings.viewMode === 'groups' && (
- Sort by Group
+
+ Sort by Group
+
)}
@@ -282,7 +371,7 @@ export function ViewSettingsPanel({
onSettingsChange({
...settings,
@@ -330,4 +419,4 @@ export function ViewSettingsPanel({
);
-}
\ No newline at end of file
+}
diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/enhanced-content.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/enhanced-content.tsx
index 976e4e125..c211cd136 100644
--- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/enhanced-content.tsx
+++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/enhanced-content.tsx
@@ -1,25 +1,31 @@
'use client';
-import { EnhancedBoard, ViewSettings, DEFAULT_VIEW_SETTINGS, STORAGE_KEYS, SmartFilters } from './types';
+import { projectColumns } from './columns';
import { BoardList } from './components/board-list';
import { ViewSettingsPanel } from './components/view-settings';
-import { useState, useEffect } from 'react';
-import dynamic from 'next/dynamic';
-import { Button } from '@tuturuuu/ui/button';
+import {
+ DEFAULT_VIEW_SETTINGS,
+ EnhancedBoard,
+ STORAGE_KEYS,
+ SmartFilters,
+ ViewSettings,
+} from './types';
+import { CustomDataTable } from '@/components/custom-data-table';
import { Badge } from '@tuturuuu/ui/badge';
-import {
- Grid3X3,
- LayoutGrid,
- List,
- TrendingUp,
- Clock,
+import { Button } from '@tuturuuu/ui/button';
+import {
AlertCircle,
+ Clock,
+ Columns,
Flag,
+ Grid3X3,
+ LayoutGrid,
+ List,
RefreshCw,
- Columns
+ TrendingUp,
} from '@tuturuuu/ui/icons';
-import { CustomDataTable } from '@/components/custom-data-table';
-import { projectColumns } from './columns';
+import dynamic from 'next/dynamic';
+import { useEffect, useState } from 'react';
interface EnhancedTaskBoardsContentProps {
boards: EnhancedBoard[];
@@ -28,11 +34,11 @@ interface EnhancedTaskBoardsContentProps {
isOwner?: boolean;
}
-function EnhancedTaskBoardsContentInner({
- boards,
+function EnhancedTaskBoardsContentInner({
+ boards,
count,
wsId: _wsId,
- isOwner = false
+ isOwner = false,
}: EnhancedTaskBoardsContentProps) {
const [settings, setSettings] = useState(DEFAULT_VIEW_SETTINGS);
@@ -52,14 +58,17 @@ function EnhancedTaskBoardsContentInner({
// Save settings to localStorage
const handleSettingsChange = (newSettings: ViewSettings) => {
setSettings(newSettings);
- localStorage.setItem(STORAGE_KEYS.VIEW_SETTINGS, JSON.stringify(newSettings));
+ localStorage.setItem(
+ STORAGE_KEYS.VIEW_SETTINGS,
+ JSON.stringify(newSettings)
+ );
};
// Create smart filters data
const smartFilters: SmartFilters = {
- hasUrgentTasks: boards.some(b => b.stats.hasUrgentTasks),
- hasMultipleOverdue: boards.some(b => b.stats.hasMultipleOverdue),
- hasWorkloadImbalance: boards.some(b => b.stats.hasWorkloadImbalance),
+ hasUrgentTasks: boards.some((b) => b.stats.hasUrgentTasks),
+ hasMultipleOverdue: boards.some((b) => b.stats.hasMultipleOverdue),
+ hasWorkloadImbalance: boards.some((b) => b.stats.hasWorkloadImbalance),
};
// Refresh function
@@ -76,50 +85,67 @@ function EnhancedTaskBoardsContentInner({
];
// Summary statistics
- const totalActiveBoards = boards.filter(b => b.stats.activeTasks > 0).length;
- const totalUrgentTasks = boards.reduce((sum, b) => sum + b.stats.priorityDistribution.urgent, 0);
- const totalOverdueBoards = boards.filter(b => b.stats.overdueTasks > 0).length;
- const avgProgress = boards.length > 0
- ? Math.round(boards.reduce((sum, b) => sum + b.stats.completionRate, 0) / boards.length)
- : 0;
+ const totalActiveBoards = boards.filter(
+ (b) => b.stats.activeTasks > 0
+ ).length;
+ const totalUrgentTasks = boards.reduce(
+ (sum, b) => sum + b.stats.priorityDistribution.urgent,
+ 0
+ );
+ const totalOverdueBoards = boards.filter(
+ (b) => b.stats.overdueTasks > 0
+ ).length;
+ const avgProgress =
+ boards.length > 0
+ ? Math.round(
+ boards.reduce((sum, b) => sum + b.stats.completionRate, 0) /
+ boards.length
+ )
+ : 0;
return (
{/* Enhanced Header with Quick Stats */}
-
-
+
+
{avgProgress}%
- avg progress
+ avg progress
-
+
{totalActiveBoards}
- active boards
+ active boards
-
+
{totalUrgentTasks > 0 && (
- {totalUrgentTasks}
- urgent tasks
+
+ {totalUrgentTasks}
+
+ urgent tasks
)}
-
+
{totalOverdueBoards > 0 && (
- {totalOverdueBoards}
- overdue boards
+
+ {totalOverdueBoards}
+
+
+ overdue boards
+
)}
@@ -127,21 +153,22 @@ function EnhancedTaskBoardsContentInner({
{/* Enhanced Controls */}
-
+
{count} board{count !== 1 ? 's' : ''}
- {(smartFilters.hasUrgentTasks ||
- smartFilters.hasMultipleOverdue ||
- smartFilters.hasWorkloadImbalance) && !settings.forceShowAll && (
-
- Smart filters active
-
- )}
+ {(smartFilters.hasUrgentTasks ||
+ smartFilters.hasMultipleOverdue ||
+ smartFilters.hasWorkloadImbalance) &&
+ !settings.forceShowAll && (
+
+ Smart filters active
+
+ )}
-
+
{/* View Mode Buttons */}
{viewModeButtons.map(({ mode, icon: Icon, label }) => (
@@ -149,7 +176,9 @@ function EnhancedTaskBoardsContentInner({
key={mode}
variant={settings.viewMode === mode ? 'default' : 'ghost'}
size="sm"
- onClick={() => handleSettingsChange({ ...settings, viewMode: mode })}
+ onClick={() =>
+ handleSettingsChange({ ...settings, viewMode: mode })
+ }
className="flex items-center gap-2 text-xs"
>
@@ -170,7 +199,7 @@ function EnhancedTaskBoardsContentInner({
Refresh
-
+
Promise.resolve(EnhancedTaskBoardsContentInner),
- {
+ {
ssr: false,
loading: () => (
- )
+ ),
}
-);
\ No newline at end of file
+);
diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/page.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/page.tsx
index 42c09bc8f..7918e1b79 100644
--- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/page.tsx
+++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/page.tsx
@@ -1,5 +1,6 @@
import { EnhancedTaskBoardsContent } from './enhanced-content';
import { TaskBoardForm } from './form';
+import { EnhancedBoard } from './types';
import { createClient } from '@tuturuuu/supabase/next/server';
import { TaskBoard } from '@tuturuuu/types/primitives/TaskBoard';
import FeatureSummary from '@tuturuuu/ui/custom/feature-summary';
@@ -7,7 +8,6 @@ import { Separator } from '@tuturuuu/ui/separator';
import { getPermissions } from '@tuturuuu/utils/workspace-helper';
import { getTranslations } from 'next-intl/server';
import { redirect } from 'next/navigation';
-import { EnhancedBoard } from './types';
interface Props {
params: Promise<{
@@ -33,14 +33,18 @@ export default async function WorkspaceProjectsPage({
// Check if the current user is a workspace owner/creator
const supabase = await createClient();
- const { data: { user } } = await supabase.auth.getUser();
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
const { data: workspace } = await supabase
.from('workspaces')
.select('creator_id')
.eq('id', wsId)
.single();
-
- const isOwner = Boolean(user && workspace && workspace.creator_id === user.id);
+
+ const isOwner = Boolean(
+ user && workspace && workspace.creator_id === user.id
+ );
const { data: rawData, count } = await getData(wsId, await searchParams);
const t = await getTranslations();
@@ -49,11 +53,17 @@ export default async function WorkspaceProjectsPage({
const enhancedBoards: EnhancedBoard[] = await Promise.all(
rawData.map(async (board, index) => {
const boardStats = await getBoardStats(board.id);
-
+
// Sample group assignment based on board name or index
- const groups = ['gaming', 'robotics', 'marketing', 'development', 'design'];
+ const groups = [
+ 'gaming',
+ 'robotics',
+ 'marketing',
+ 'development',
+ 'design',
+ ];
const groupId = groups[index % groups.length];
-
+
return {
...board,
href: `/${wsId}/tasks/boards/${board.id}`,
@@ -75,10 +85,10 @@ export default async function WorkspaceProjectsPage({
requireExpansion
/>
-
>
@@ -122,17 +132,19 @@ async function getData(
async function getBoardStats(boardId: string) {
const supabase = await createClient();
-
+
// Get all tasks for this board with assignee information
const { data: tasks } = await supabase
.from('tasks')
- .select(`
+ .select(
+ `
id,
list_id,
priority,
end_date,
task_assignees!inner(user_id, workspace_users!inner(display_name))
- `)
+ `
+ )
.eq('board_id', boardId);
// Get all lists for this board with their status
@@ -142,81 +154,107 @@ async function getBoardStats(boardId: string) {
.eq('board_id', boardId);
const totalTasks = tasks?.length || 0;
-
+
// Create a map of list_id to status for quick lookup
- const listStatusMap = new Map(lists?.map(list => [list.id, list.status]) || []);
-
+ const listStatusMap = new Map(
+ lists?.map((list) => [list.id, list.status]) || []
+ );
+
// Calculate task statuses based on which list they're in
- const completedTasks = tasks?.filter(task => {
- const listStatus = listStatusMap.get(task.list_id);
- return listStatus === 'done' || listStatus === 'closed';
- }).length || 0;
-
- const activeTasks = tasks?.filter(task => {
- const listStatus = listStatusMap.get(task.list_id);
- return listStatus === 'active' || listStatus === 'not_started';
- }).length || 0;
-
+ const completedTasks =
+ tasks?.filter((task) => {
+ const listStatus = listStatusMap.get(task.list_id);
+ return listStatus === 'done' || listStatus === 'closed';
+ }).length || 0;
+
+ const activeTasks =
+ tasks?.filter((task) => {
+ const listStatus = listStatusMap.get(task.list_id);
+ return listStatus === 'active' || listStatus === 'not_started';
+ }).length || 0;
+
const now = new Date();
- const overdueTasks = tasks?.filter(task => {
- const listStatus = listStatusMap.get(task.list_id);
- return task.end_date &&
- new Date(task.end_date) < now &&
- (listStatus !== 'done' && listStatus !== 'closed');
- }).length || 0;
+ const overdueTasks =
+ tasks?.filter((task) => {
+ const listStatus = listStatusMap.get(task.list_id);
+ return (
+ task.end_date &&
+ new Date(task.end_date) < now &&
+ listStatus !== 'done' &&
+ listStatus !== 'closed'
+ );
+ }).length || 0;
// Priority distribution - simplified since we don't know the exact priority values
const priorityDistribution = {
- low: tasks?.filter(t => t.priority === 1).length || 0,
- medium: tasks?.filter(t => t.priority === 2).length || 0,
- high: tasks?.filter(t => t.priority === 3).length || 0,
- urgent: tasks?.filter(t => t.priority === 4).length || 0,
+ low: tasks?.filter((t) => t.priority === 1).length || 0,
+ medium: tasks?.filter((t) => t.priority === 2).length || 0,
+ high: tasks?.filter((t) => t.priority === 3).length || 0,
+ urgent: tasks?.filter((t) => t.priority === 4).length || 0,
};
// Status distribution based on list statuses
const statusDistribution = {
- not_started: tasks?.filter(task => listStatusMap.get(task.list_id) === 'not_started').length || 0,
- active: tasks?.filter(task => listStatusMap.get(task.list_id) === 'active').length || 0,
- done: tasks?.filter(task => listStatusMap.get(task.list_id) === 'done').length || 0,
- closed: tasks?.filter(task => listStatusMap.get(task.list_id) === 'closed').length || 0,
+ not_started:
+ tasks?.filter((task) => listStatusMap.get(task.list_id) === 'not_started')
+ .length || 0,
+ active:
+ tasks?.filter((task) => listStatusMap.get(task.list_id) === 'active')
+ .length || 0,
+ done:
+ tasks?.filter((task) => listStatusMap.get(task.list_id) === 'done')
+ .length || 0,
+ closed:
+ tasks?.filter((task) => listStatusMap.get(task.list_id) === 'closed')
+ .length || 0,
};
// Workload analysis
- const assigneeWorkload = tasks?.reduce((acc, task) => {
- task.task_assignees?.forEach((assignee: any) => {
- const userId = assignee.user_id;
- const userName = assignee.workspace_users?.display_name || 'Unknown';
-
- if (!acc[userId]) {
- acc[userId] = { userId, name: userName, taskCount: 0 };
- }
- acc[userId].taskCount++;
- });
- return acc;
- }, {} as Record) || {};
+ const assigneeWorkload =
+ tasks?.reduce(
+ (acc, task) => {
+ task.task_assignees?.forEach((assignee: any) => {
+ const userId = assignee.user_id;
+ const userName = assignee.workspace_users?.display_name || 'Unknown';
+
+ if (!acc[userId]) {
+ acc[userId] = { userId, name: userName, taskCount: 0 };
+ }
+ acc[userId].taskCount++;
+ });
+ return acc;
+ },
+ {} as Record
+ ) || {};
const workloadStats = Object.values(assigneeWorkload);
- const avgTasksPerPerson = workloadStats.length > 0
- ? workloadStats.reduce((sum, stat) => sum + stat.taskCount, 0) / workloadStats.length
- : 0;
-
+ const avgTasksPerPerson =
+ workloadStats.length > 0
+ ? workloadStats.reduce((sum, stat) => sum + stat.taskCount, 0) /
+ workloadStats.length
+ : 0;
+
// Mark users as overloaded if they have >150% of average tasks
- const assigneeWorkloadArray = workloadStats.map(stat => ({
+ const assigneeWorkloadArray = workloadStats.map((stat) => ({
...stat,
- isOverloaded: stat.taskCount > avgTasksPerPerson * 1.5 && avgTasksPerPerson > 0
+ isOverloaded:
+ stat.taskCount > avgTasksPerPerson * 1.5 && avgTasksPerPerson > 0,
}));
// Smart detection flags
const hasUrgentTasks = priorityDistribution.urgent > 0;
const hasMultipleOverdue = overdueTasks >= 2;
- const hasWorkloadImbalance = assigneeWorkloadArray.some(assignee => assignee.isOverloaded);
+ const hasWorkloadImbalance = assigneeWorkloadArray.some(
+ (assignee) => assignee.isOverloaded
+ );
return {
totalTasks,
completedTasks,
activeTasks,
overdueTasks,
- completionRate: totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0,
+ completionRate:
+ totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0,
priorityDistribution,
statusDistribution,
totalLists: lists?.length || 0,
diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/types.ts b/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/types.ts
index dd1093a67..07a325ca7 100644
--- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/types.ts
+++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/tasks/boards/types.ts
@@ -48,7 +48,13 @@ export interface BoardGroup {
}
export type ViewMode = 'table' | 'cards' | 'groups';
-export type SortField = 'name' | 'id' | 'created_at' | 'progress' | 'tasks' | 'group';
+export type SortField =
+ | 'name'
+ | 'id'
+ | 'created_at'
+ | 'progress'
+ | 'tasks'
+ | 'group';
export type SortDirection = 'asc' | 'desc';
export interface SmartFilters {
@@ -77,33 +83,40 @@ export const DEFAULT_VIEW_SETTINGS: ViewSettings = {
sortBy: 'name',
sortOrder: 'asc',
forceShowAll: false,
- visibleColumns: ['board', 'progress', 'tasks', 'status', 'last_updated', 'actions'],
+ visibleColumns: [
+ 'board',
+ 'progress',
+ 'tasks',
+ 'status',
+ 'last_updated',
+ 'actions',
+ ],
groupView: {
- collapsed: []
+ collapsed: [],
},
pagination: {
page: 1,
- pageSize: 5
- }
+ pageSize: 5,
+ },
};
export const STORAGE_KEYS = {
- VIEW_SETTINGS: 'task_boards_view_settings'
+ VIEW_SETTINGS: 'task_boards_view_settings',
} as const;
// Predefined group colors
export const GROUP_COLORS = {
- 'Gaming': '#10b981', // emerald
- 'Robotics': '#3b82f6', // blue
- 'Marketing': '#f59e0b', // amber
- 'Development': '#8b5cf6', // violet
- 'Design': '#ec4899', // pink
- 'Research': '#06b6d4', // cyan
- 'Sales': '#84cc16', // lime
- 'Support': '#f97316', // orange
- 'Finance': '#6366f1', // indigo
- 'HR': '#14b8a6', // teal
- 'Operations': '#ef4444', // red
- 'Strategy': '#64748b', // slate
- 'Default': '#6b7280' // gray
-} as const;
\ No newline at end of file
+ Gaming: '#10b981', // emerald
+ Robotics: '#3b82f6', // blue
+ Marketing: '#f59e0b', // amber
+ Development: '#8b5cf6', // violet
+ Design: '#ec4899', // pink
+ Research: '#06b6d4', // cyan
+ Sales: '#84cc16', // lime
+ Support: '#f97316', // orange
+ Finance: '#6366f1', // indigo
+ HR: '#14b8a6', // teal
+ Operations: '#ef4444', // red
+ Strategy: '#64748b', // slate
+ Default: '#6b7280', // gray
+} as const;