new version frontend-webpage

This commit is contained in:
José Salazar
2025-11-21 09:13:05 -05:00
parent 23dfba35cc
commit de1cc96ba0
56 changed files with 7736 additions and 3367 deletions

View File

@@ -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>