init repo
This commit is contained in:
320
app/tasks/page.tsx
Normal file
320
app/tasks/page.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Topbar } from "@/components/topbar"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import {
|
||||
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Plus, Search, Filter, MoreHorizontal, Eye, Edit, Trash2, Calendar, User } from "lucide-react"
|
||||
import {
|
||||
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { useTranslation } from "@/lib/i18n"
|
||||
|
||||
type Task = {
|
||||
id: string; title: string; description: string
|
||||
status: "Todo" | "In Progress" | "Completed" | "Overdue"
|
||||
priority: "Low" | "Medium" | "High" | "Urgent"
|
||||
assignee: string; assigneeAvatar: string; dueDate: string; createdAt: string
|
||||
relatedTo: string; relatedType: "Deal" | "Contact" | "General"
|
||||
}
|
||||
|
||||
const initialTasks: Task[] = [
|
||||
{ id: "TASK-001", title: "Follow up with TechCorp Inc.", description: "Schedule demo call for enterprise software license", status: "Todo", priority: "High", assignee: "Jane Doe", assigneeAvatar: "https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face", dueDate: "2024-01-20", createdAt: "2024-01-15", relatedTo: "Enterprise Software License", relatedType: "Deal" },
|
||||
{ id: "TASK-002", title: "Prepare proposal for StartupXYZ", description: "Create detailed proposal for marketing automation setup", status: "In Progress", priority: "Medium", assignee: "Mike Roberts", assigneeAvatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face", dueDate: "2024-01-18", createdAt: "2024-01-14", relatedTo: "Marketing Automation Setup", relatedType: "Deal" },
|
||||
{ id: "TASK-003", title: "Send contract to Global Solutions", description: "Finalize and send signed contract for cloud migration project", status: "Completed", priority: "High", assignee: "Sarah Johnson", assigneeAvatar: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face", dueDate: "2024-01-16", createdAt: "2024-01-12", relatedTo: "Cloud Migration Project", relatedType: "Deal" },
|
||||
{ id: "TASK-004", title: "Update CRM data", description: "Clean up and update contact information in CRM system", status: "Overdue", priority: "Low", assignee: "Alex Lee", assigneeAvatar: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face", dueDate: "2024-01-10", createdAt: "2024-01-08", relatedTo: "General Maintenance", relatedType: "General" },
|
||||
]
|
||||
|
||||
const getStatusColor = (status: Task["status"]) => {
|
||||
const colors = { Todo: "bg-gray-100 text-gray-800", "In Progress": "bg-blue-100 text-blue-800", Completed: "bg-green-100 text-green-800", Overdue: "bg-red-100 text-red-800" }
|
||||
return colors[status]
|
||||
}
|
||||
|
||||
const getPriorityColor = (priority: Task["priority"]) => {
|
||||
const colors = { Low: "bg-green-100 text-green-800", Medium: "bg-yellow-100 text-yellow-800", High: "bg-orange-100 text-orange-800", Urgent: "bg-red-100 text-red-800" }
|
||||
return colors[priority]
|
||||
}
|
||||
|
||||
const statusMap: Record<string, string> = { Todo: "tasks.todo", "In Progress": "tasks.inProgress", Completed: "tasks.completed", Overdue: "tasks.overdue" }
|
||||
const priorityMap: Record<string, string> = { Low: "tasks.low", Medium: "tasks.medium", High: "tasks.high", Urgent: "tasks.urgent" }
|
||||
|
||||
export default function TasksPage() {
|
||||
const { t } = useTranslation()
|
||||
const [tasks, setTasks] = useState<Task[]>(initialTasks)
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all")
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
const [editingTask, setEditingTask] = useState<Task | null>(null)
|
||||
|
||||
const filteredTasks = tasks.filter((task) => {
|
||||
const matchesSearch = task.title.toLowerCase().includes(searchTerm.toLowerCase()) || task.description.toLowerCase().includes(searchTerm.toLowerCase()) || task.relatedTo.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const matchesStatus = statusFilter === "all" || task.status === statusFilter
|
||||
return matchesSearch && matchesStatus
|
||||
})
|
||||
|
||||
const handleAddTask = (taskData: Partial<Task>) => {
|
||||
const newTask: Task = {
|
||||
id: `TASK-${String(tasks.length + 1).padStart(3, "0")}`, title: taskData.title || "", description: taskData.description || "",
|
||||
status: taskData.status || "Todo", priority: taskData.priority || "Medium", assignee: taskData.assignee || "Current User",
|
||||
assigneeAvatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face",
|
||||
dueDate: taskData.dueDate || "", createdAt: new Date().toISOString().split("T")[0], relatedTo: taskData.relatedTo || "", relatedType: taskData.relatedType || "General",
|
||||
}
|
||||
setTasks([...tasks, newTask]); setIsDialogOpen(false)
|
||||
}
|
||||
|
||||
const handleEditTask = (taskData: Partial<Task>) => {
|
||||
if (editingTask) { setTasks(tasks.map((task) => (task.id === editingTask.id ? { ...task, ...taskData } : task))); setEditingTask(null); setIsDialogOpen(false) }
|
||||
}
|
||||
|
||||
const handleDeleteTask = (taskId: string) => { setTasks(tasks.filter((task) => task.id !== taskId)) }
|
||||
|
||||
const handleToggleComplete = (taskId: string) => {
|
||||
setTasks(tasks.map((task) => task.id === taskId ? { ...task, status: task.status === "Completed" ? "Todo" : "Completed" } : task))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Topbar />
|
||||
<div className="flex-1 p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{t("tasks.title")}</h1>
|
||||
<p className="text-muted-foreground">{t("tasks.description")}</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={() => setEditingTask(null)}>
|
||||
<Plus className="mr-2 h-4 w-4" />{t("tasks.addNew")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<TaskDialog task={editingTask} onSave={editingTask ? handleEditTask : handleAddTask} onCancel={() => { setIsDialogOpen(false); setEditingTask(null) }} />
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||
<Input placeholder={t("tasks.search")} className="pl-10" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />
|
||||
</div>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-48">
|
||||
<Filter className="mr-2 h-4 w-4" />
|
||||
<SelectValue placeholder={t("tasks.filterByStatus")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t("tasks.allStatus")}</SelectItem>
|
||||
<SelectItem value="Todo">{t("tasks.todo")}</SelectItem>
|
||||
<SelectItem value="In Progress">{t("tasks.inProgress")}</SelectItem>
|
||||
<SelectItem value="Completed">{t("tasks.completed")}</SelectItem>
|
||||
<SelectItem value="Overdue">{t("tasks.overdue")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="list" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="list">{t("tasks.listView")}</TabsTrigger>
|
||||
<TabsTrigger value="board">{t("tasks.boardView")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="list" className="space-y-4">
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="divide-y">
|
||||
{filteredTasks.map((task) => (
|
||||
<div key={task.id} className="p-4 hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center gap-4">
|
||||
<Checkbox checked={task.status === "Completed"} onCheckedChange={() => handleToggleComplete(task.id)} />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className={`font-medium ${task.status === "Completed" ? "line-through text-muted-foreground" : ""}`}>{task.title}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className={getPriorityColor(task.priority)}>{t(priorityMap[task.priority])}</Badge>
|
||||
<Badge className={getStatusColor(task.status)}>{t(statusMap[task.status])}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{task.description}</p>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>{t("tasks.due")} {new Date(task.dueDate).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<User className="h-4 w-4" />
|
||||
<Avatar className="h-4 w-4">
|
||||
<AvatarImage src={task.assigneeAvatar || "/placeholder.svg"} className="object-cover" />
|
||||
<AvatarFallback className="text-xs">{task.assignee.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span>{task.assignee}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{t("tasks.relatedTo")} {task.relatedTo}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8"><MoreHorizontal className="h-4 w-4" /></Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{t("tasks.actions")}</DropdownMenuLabel>
|
||||
<DropdownMenuItem><Eye className="mr-2 h-4 w-4" />{t("tasks.viewDetails")}</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => { setEditingTask(task); setIsDialogOpen(true) }}>
|
||||
<Edit className="mr-2 h-4 w-4" />{t("tasks.editTask")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-red-600" onClick={() => handleDeleteTask(task.id)}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />{t("tasks.deleteTask")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="board" className="space-y-4">
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
{(["Todo", "In Progress", "Completed", "Overdue"] as const).map((status) => (
|
||||
<Card key={status}>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium flex items-center justify-between">
|
||||
{t(statusMap[status])}
|
||||
<Badge variant="secondary">{filteredTasks.filter((task) => task.status === status).length}</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{filteredTasks.filter((task) => task.status === status).map((task) => (
|
||||
<Card key={task.id} className="p-3 hover:shadow-sm transition-shadow cursor-pointer">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-medium text-sm">{task.title}</h4>
|
||||
<Badge className={getPriorityColor(task.priority)} variant="outline">{t(priorityMap[task.priority])}</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">{task.description}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">{new Date(task.dueDate).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<Avatar className="h-5 w-5">
|
||||
<AvatarImage src={task.assigneeAvatar || "/placeholder.svg"} className="object-cover" />
|
||||
<AvatarFallback className="text-xs">{task.assignee.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TaskDialog({ task, onSave, onCancel }: { task: Task | null; onSave: (data: Partial<Task>) => void; onCancel: () => void }) {
|
||||
const { t } = useTranslation()
|
||||
const [formData, setFormData] = useState({
|
||||
title: task?.title || "", description: task?.description || "", status: task?.status || "Todo",
|
||||
priority: task?.priority || "Medium", assignee: task?.assignee || "", dueDate: task?.dueDate || "",
|
||||
relatedTo: task?.relatedTo || "", relatedType: task?.relatedType || "General",
|
||||
})
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSave(formData) }
|
||||
|
||||
return (
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{task ? t("tasks.editTitle") : t("tasks.addTitle")}</DialogTitle>
|
||||
<DialogDescription>{task ? t("tasks.editDesc") : t("tasks.addDesc")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">{t("tasks.titleLabel")}</Label>
|
||||
<Input id="title" value={formData.title} onChange={(e) => setFormData({ ...formData, title: e.target.value })} required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">{t("tasks.descriptionLabel")}</Label>
|
||||
<Textarea id="description" value={formData.description} onChange={(e) => setFormData({ ...formData, description: e.target.value })} rows={3} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status">{t("tasks.status")}</Label>
|
||||
<Select value={formData.status} onValueChange={(value) => setFormData({ ...formData, status: value as Task["status"] })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Todo">{t("tasks.todo")}</SelectItem>
|
||||
<SelectItem value="In Progress">{t("tasks.inProgress")}</SelectItem>
|
||||
<SelectItem value="Completed">{t("tasks.completed")}</SelectItem>
|
||||
<SelectItem value="Overdue">{t("tasks.overdue")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="priority">{t("tasks.priority")}</Label>
|
||||
<Select value={formData.priority} onValueChange={(value) => setFormData({ ...formData, priority: value as Task["priority"] })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Low">{t("tasks.low")}</SelectItem>
|
||||
<SelectItem value="Medium">{t("tasks.medium")}</SelectItem>
|
||||
<SelectItem value="High">{t("tasks.high")}</SelectItem>
|
||||
<SelectItem value="Urgent">{t("tasks.urgent")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="assignee">{t("tasks.assignee")}</Label>
|
||||
<Input id="assignee" value={formData.assignee} onChange={(e) => setFormData({ ...formData, assignee: e.target.value })} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dueDate">{t("tasks.dueDate")}</Label>
|
||||
<Input id="dueDate" type="date" value={formData.dueDate} onChange={(e) => setFormData({ ...formData, dueDate: e.target.value })} required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="relatedTo">{t("tasks.relatedToLabel")}</Label>
|
||||
<Input id="relatedTo" value={formData.relatedTo} onChange={(e) => setFormData({ ...formData, relatedTo: e.target.value })} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="relatedType">{t("tasks.type")}</Label>
|
||||
<Select value={formData.relatedType} onValueChange={(value) => setFormData({ ...formData, relatedType: value as Task["relatedType"] })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Deal">{t("tasks.deal")}</SelectItem>
|
||||
<SelectItem value="Contact">{t("tasks.contact")}</SelectItem>
|
||||
<SelectItem value="General">{t("tasks.general")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onCancel}>{t("tasks.cancel")}</Button>
|
||||
<Button type="submit">{task ? t("tasks.updateTask") : t("tasks.createTask")}</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user