new version frontend-webpage
This commit is contained in:
@@ -11,7 +11,15 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { DragDropContext, Draggable } from "@hello-pangea/dnd";
|
||||
import { Link2, Plus, Users } from "lucide-react";
|
||||
import { Link2, Plus, Users, Search, UserCircle, Filter, ArrowUpDown, EyeOff, Grid3x3, MoreVertical, Pin, Ruler, Palette } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuLabel,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import TaskCard from "@/components/tasks/TaskCard";
|
||||
import TaskColumn from "@/components/tasks/TaskColumn";
|
||||
import TaskDetailModal from "@/components/tasks/TaskDetailModal";
|
||||
@@ -32,6 +40,15 @@ export default function TaskBoard() {
|
||||
assigned_members: []
|
||||
});
|
||||
const [selectedMembers, setSelectedMembers] = useState([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [filterPerson, setFilterPerson] = useState("all");
|
||||
const [filterPriority, setFilterPriority] = useState("all");
|
||||
const [sortBy, setSortBy] = useState("due_date");
|
||||
const [showCompleted, setShowCompleted] = useState(true);
|
||||
const [groupBy, setGroupBy] = useState("status");
|
||||
const [pinnedColumns, setPinnedColumns] = useState([]);
|
||||
const [itemHeight, setItemHeight] = useState("normal");
|
||||
const [conditionalColoring, setConditionalColoring] = useState(true);
|
||||
|
||||
const { data: user } = useQuery({
|
||||
queryKey: ['current-user-taskboard'],
|
||||
@@ -57,7 +74,30 @@ export default function TaskBoard() {
|
||||
});
|
||||
|
||||
const userTeam = teams.find(t => t.owner_id === user?.id) || teams[0];
|
||||
const teamTasks = tasks.filter(t => t.team_id === userTeam?.id);
|
||||
let teamTasks = tasks.filter(t => t.team_id === userTeam?.id);
|
||||
|
||||
// Apply filters
|
||||
if (searchQuery) {
|
||||
teamTasks = teamTasks.filter(t =>
|
||||
t.task_name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
t.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (filterPerson !== "all") {
|
||||
teamTasks = teamTasks.filter(t =>
|
||||
t.assigned_members?.some(m => m.member_id === filterPerson)
|
||||
);
|
||||
}
|
||||
|
||||
if (filterPriority !== "all") {
|
||||
teamTasks = teamTasks.filter(t => t.priority === filterPriority);
|
||||
}
|
||||
|
||||
if (!showCompleted) {
|
||||
teamTasks = teamTasks.filter(t => t.status !== "completed");
|
||||
}
|
||||
|
||||
const currentTeamMembers = teamMembers.filter(m => m.team_id === userTeam?.id);
|
||||
|
||||
const leadMembers = currentTeamMembers.filter(m => m.role === 'admin' || m.role === 'manager');
|
||||
@@ -66,12 +106,30 @@ export default function TaskBoard() {
|
||||
// Get unique departments from team members
|
||||
const departments = [...new Set(currentTeamMembers.map(m => m.department).filter(Boolean))];
|
||||
|
||||
const sortTasks = (tasks) => {
|
||||
return [...tasks].sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case "due_date":
|
||||
return new Date(a.due_date || '9999-12-31') - new Date(b.due_date || '9999-12-31');
|
||||
case "priority":
|
||||
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
||||
return (priorityOrder[a.priority] || 1) - (priorityOrder[b.priority] || 1);
|
||||
case "created_date":
|
||||
return new Date(b.created_date || 0) - new Date(a.created_date || 0);
|
||||
case "task_name":
|
||||
return (a.task_name || '').localeCompare(b.task_name || '');
|
||||
default:
|
||||
return (a.order_index || 0) - (b.order_index || 0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const tasksByStatus = useMemo(() => ({
|
||||
pending: teamTasks.filter(t => t.status === 'pending').sort((a, b) => (a.order_index || 0) - (b.order_index || 0)),
|
||||
in_progress: teamTasks.filter(t => t.status === 'in_progress').sort((a, b) => (a.order_index || 0) - (b.order_index || 0)),
|
||||
on_hold: teamTasks.filter(t => t.status === 'on_hold').sort((a, b) => (a.order_index || 0) - (b.order_index || 0)),
|
||||
completed: teamTasks.filter(t => t.status === 'completed').sort((a, b) => (a.order_index || 0) - (b.order_index || 0)),
|
||||
}), [teamTasks]);
|
||||
pending: sortTasks(teamTasks.filter(t => t.status === 'pending')),
|
||||
in_progress: sortTasks(teamTasks.filter(t => t.status === 'in_progress')),
|
||||
on_hold: sortTasks(teamTasks.filter(t => t.status === 'on_hold')),
|
||||
completed: sortTasks(teamTasks.filter(t => t.status === 'completed')),
|
||||
}), [teamTasks, sortBy]);
|
||||
|
||||
const overallProgress = useMemo(() => {
|
||||
if (teamTasks.length === 0) return 0;
|
||||
@@ -158,6 +216,130 @@ export default function TaskBoard() {
|
||||
<div className="max-w-[1800px] mx-auto">
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-xl p-6 mb-6 shadow-sm border border-slate-200">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center gap-3 mb-6 pb-4 border-b border-slate-200">
|
||||
<div className="relative flex-1 max-w-xs">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<Input
|
||||
placeholder="Search"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 h-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<UserCircle className="w-4 h-4" />
|
||||
Person
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56">
|
||||
<DropdownMenuItem onClick={() => setFilterPerson("all")}>
|
||||
All People
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{currentTeamMembers.map((member) => (
|
||||
<DropdownMenuItem
|
||||
key={member.id}
|
||||
onClick={() => setFilterPerson(member.id)}
|
||||
>
|
||||
{member.member_name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<Filter className="w-4 h-4" />
|
||||
Filter
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuLabel>Priority</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => setFilterPriority("all")}>All</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setFilterPriority("high")}>High</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setFilterPriority("normal")}>Normal</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setFilterPriority("low")}>Low</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<ArrowUpDown className="w-4 h-4" />
|
||||
Sort
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => setSortBy("due_date")}>Due Date</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setSortBy("priority")}>Priority</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setSortBy("created_date")}>Created Date</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setSortBy("task_name")}>Name</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
onClick={() => setShowCompleted(!showCompleted)}
|
||||
>
|
||||
<EyeOff className="w-4 h-4" />
|
||||
Hide
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<Grid3x3 className="w-4 h-4" />
|
||||
Group by
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => setGroupBy("status")}>Status</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setGroupBy("priority")}>Priority</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setGroupBy("assigned")}>Assigned To</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuItem onClick={() => setPinnedColumns(pinnedColumns.length > 0 ? [] : ['pending'])}>
|
||||
<Pin className="w-4 h-4 mr-2" />
|
||||
Pin columns
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Item height</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => setItemHeight("compact")}>
|
||||
<Ruler className="w-4 h-4 mr-2" />
|
||||
Compact
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setItemHeight("normal")}>
|
||||
<Ruler className="w-4 h-4 mr-2" />
|
||||
Normal
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setItemHeight("comfortable")}>
|
||||
<Ruler className="w-4 h-4 mr-2" />
|
||||
Comfortable
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setConditionalColoring(!conditionalColoring)}>
|
||||
<Palette className="w-4 h-4 mr-2" />
|
||||
Conditional coloring
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 mb-2">Task Board</h1>
|
||||
@@ -205,8 +387,8 @@ export default function TaskBoard() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" className="border-slate-300">
|
||||
<Link2 className="w-4 h-4 mr-2" />
|
||||
<Button variant="outline" className="gap-2 bg-white hover:bg-slate-50 border border-slate-300 text-slate-700 font-medium">
|
||||
<Link2 className="w-4 h-4" />
|
||||
Share
|
||||
</Button>
|
||||
<Button
|
||||
@@ -214,10 +396,10 @@ export default function TaskBoard() {
|
||||
setSelectedStatus("pending");
|
||||
setCreateDialog(true);
|
||||
}}
|
||||
className="bg-[#0A39DF] hover:bg-blue-700"
|
||||
className="gap-2 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-semibold shadow-md"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create List
|
||||
<Plus className="w-5 h-5" />
|
||||
Create Task
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -256,6 +438,8 @@ export default function TaskBoard() {
|
||||
task={task}
|
||||
provided={provided}
|
||||
onClick={() => setSelectedTask(task)}
|
||||
itemHeight={itemHeight}
|
||||
conditionalColoring={conditionalColoring}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
|
||||
Reference in New Issue
Block a user