init repo
This commit is contained in:
3
app/contacts/loading.tsx
Normal file
3
app/contacts/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
442
app/contacts/page.tsx
Normal file
442
app/contacts/page.tsx
Normal file
@@ -0,0 +1,442 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Topbar } from "@/components/topbar"
|
||||
import { Card, CardContent, CardDescription, 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 { Plus, Search, Filter, MoreHorizontal, Eye, Edit, Trash2, Mail, Phone, Building } from "lucide-react"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { useTranslation } from "@/lib/i18n"
|
||||
|
||||
type Contact = {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
phone: string
|
||||
company: string
|
||||
position: string
|
||||
status: "Active" | "Inactive" | "Lead" | "Customer"
|
||||
avatar: string
|
||||
lastContact: string
|
||||
createdAt: string
|
||||
notes: string
|
||||
}
|
||||
|
||||
const initialContacts: Contact[] = [
|
||||
{
|
||||
id: "CONTACT-001",
|
||||
name: "Sarah Wilson",
|
||||
email: "sarah.wilson@techcorp.com",
|
||||
phone: "+1 (555) 123-4567",
|
||||
company: "TechCorp Inc.",
|
||||
position: "CTO",
|
||||
status: "Customer",
|
||||
avatar: "https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face",
|
||||
lastContact: "2024-01-15",
|
||||
createdAt: "2023-12-01",
|
||||
notes: "Key decision maker for enterprise software purchases",
|
||||
},
|
||||
{
|
||||
id: "CONTACT-002",
|
||||
name: "Michael Chen",
|
||||
email: "m.chen@startupxyz.com",
|
||||
phone: "+1 (555) 234-5678",
|
||||
company: "StartupXYZ",
|
||||
position: "CEO",
|
||||
status: "Lead",
|
||||
avatar: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face",
|
||||
lastContact: "2024-01-14",
|
||||
createdAt: "2023-12-15",
|
||||
notes: "Interested in marketing automation solutions",
|
||||
},
|
||||
{
|
||||
id: "CONTACT-003",
|
||||
name: "Emily Rodriguez",
|
||||
email: "emily.r@globalsolutions.com",
|
||||
phone: "+1 (555) 345-6789",
|
||||
company: "Global Solutions",
|
||||
position: "IT Director",
|
||||
status: "Active",
|
||||
avatar: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face",
|
||||
lastContact: "2024-01-12",
|
||||
createdAt: "2023-11-20",
|
||||
notes: "Evaluating cloud migration options",
|
||||
},
|
||||
{
|
||||
id: "CONTACT-004",
|
||||
name: "David Park",
|
||||
email: "david.park@retailchain.com",
|
||||
phone: "+1 (555) 456-7890",
|
||||
company: "Retail Chain Co.",
|
||||
position: "Operations Manager",
|
||||
status: "Customer",
|
||||
avatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face",
|
||||
lastContact: "2024-01-08",
|
||||
createdAt: "2023-10-15",
|
||||
notes: "Recently implemented CRM system",
|
||||
},
|
||||
]
|
||||
|
||||
const getStatusColor = (status: Contact["status"]) => {
|
||||
const colors = {
|
||||
Active: "bg-blue-100 text-blue-800",
|
||||
Inactive: "bg-gray-100 text-gray-800",
|
||||
Lead: "bg-yellow-100 text-yellow-800",
|
||||
Customer: "bg-green-100 text-green-800",
|
||||
}
|
||||
return colors[status]
|
||||
}
|
||||
|
||||
export default function ContactsPage() {
|
||||
const { t } = useTranslation()
|
||||
const [contacts, setContacts] = useState<Contact[]>(initialContacts)
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all")
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
const [editingContact, setEditingContact] = useState<Contact | null>(null)
|
||||
|
||||
const filteredContacts = contacts.filter((contact) => {
|
||||
const matchesSearch =
|
||||
contact.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
contact.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
contact.company.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const matchesStatus = statusFilter === "all" || contact.status === statusFilter
|
||||
return matchesSearch && matchesStatus
|
||||
})
|
||||
|
||||
const handleAddContact = (contactData: Partial<Contact>) => {
|
||||
const newContact: Contact = {
|
||||
id: `CONTACT-${String(contacts.length + 1).padStart(3, "0")}`,
|
||||
name: contactData.name || "",
|
||||
email: contactData.email || "",
|
||||
phone: contactData.phone || "",
|
||||
company: contactData.company || "",
|
||||
position: contactData.position || "",
|
||||
status: contactData.status || "Lead",
|
||||
avatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face",
|
||||
lastContact: new Date().toISOString().split("T")[0],
|
||||
createdAt: new Date().toISOString().split("T")[0],
|
||||
notes: contactData.notes || "",
|
||||
}
|
||||
setContacts([...contacts, newContact])
|
||||
setIsDialogOpen(false)
|
||||
}
|
||||
|
||||
const handleEditContact = (contactData: Partial<Contact>) => {
|
||||
if (editingContact) {
|
||||
setContacts(
|
||||
contacts.map((contact) => (contact.id === editingContact.id ? { ...contact, ...contactData } : contact)),
|
||||
)
|
||||
setEditingContact(null)
|
||||
setIsDialogOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteContact = (contactId: string) => {
|
||||
setContacts(contacts.filter((contact) => contact.id !== contactId))
|
||||
}
|
||||
|
||||
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("contacts.title")}</h1>
|
||||
<p className="text-muted-foreground">{t("contacts.description")}</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={() => setEditingContact(null)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("contacts.addNew")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<ContactDialog
|
||||
contact={editingContact}
|
||||
onSave={editingContact ? handleEditContact : handleAddContact}
|
||||
onCancel={() => {
|
||||
setIsDialogOpen(false)
|
||||
setEditingContact(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("contacts.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("contacts.filterByStatus")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t("contacts.allStatus")}</SelectItem>
|
||||
<SelectItem value="Active">{t("contacts.active")}</SelectItem>
|
||||
<SelectItem value="Inactive">{t("contacts.inactive")}</SelectItem>
|
||||
<SelectItem value="Lead">{t("contacts.lead")}</SelectItem>
|
||||
<SelectItem value="Customer">{t("contacts.customer")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="grid" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="grid">{t("contacts.gridView")}</TabsTrigger>
|
||||
<TabsTrigger value="list">{t("contacts.listView")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="grid" className="space-y-4">
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredContacts.map((contact) => (
|
||||
<Card key={contact.id} className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarImage src={contact.avatar || "/placeholder.svg"} className="object-cover" />
|
||||
<AvatarFallback>
|
||||
{contact.name.split(" ").map((n) => n[0]).join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-lg">{contact.name}</CardTitle>
|
||||
<CardDescription>{contact.position}</CardDescription>
|
||||
</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("contacts.actions")}</DropdownMenuLabel>
|
||||
<DropdownMenuItem>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
{t("contacts.viewDetails")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setEditingContact(contact)
|
||||
setIsDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
{t("contacts.editContact")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-red-600" onClick={() => handleDeleteContact(contact.id)}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{t("contacts.deleteContact")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge className={getStatusColor(contact.status)}>{contact.status}</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{new Date(contact.lastContact).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Building className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{contact.company}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Mail className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="truncate">{contact.email}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Phone className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{contact.phone}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="list" className="space-y-4">
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="divide-y">
|
||||
{filteredContacts.map((contact) => (
|
||||
<div key={contact.id} className="p-4 hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage src={contact.avatar || "/placeholder.svg"} className="object-cover" />
|
||||
<AvatarFallback>
|
||||
{contact.name.split(" ").map((n) => n[0]).join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-medium">{contact.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{contact.position} at {contact.company}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span>{contact.email}</span>
|
||||
<span>{contact.phone}</span>
|
||||
</div>
|
||||
<Badge className={getStatusColor(contact.status)}>{contact.status}</Badge>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{new Date(contact.lastContact).toLocaleDateString()}
|
||||
</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("contacts.actions")}</DropdownMenuLabel>
|
||||
<DropdownMenuItem>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
{t("contacts.viewDetails")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setEditingContact(contact)
|
||||
setIsDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
{t("contacts.editContact")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-red-600" onClick={() => handleDeleteContact(contact.id)}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{t("contacts.deleteContact")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ContactDialog({
|
||||
contact,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: {
|
||||
contact: Contact | null
|
||||
onSave: (data: Partial<Contact>) => void
|
||||
onCancel: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [formData, setFormData] = useState({
|
||||
name: contact?.name || "",
|
||||
email: contact?.email || "",
|
||||
phone: contact?.phone || "",
|
||||
company: contact?.company || "",
|
||||
position: contact?.position || "",
|
||||
status: contact?.status || "Lead",
|
||||
notes: contact?.notes || "",
|
||||
})
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
onSave(formData)
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{contact ? t("contacts.editTitle") : t("contacts.addTitle")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{contact ? t("contacts.editDesc") : t("contacts.addDesc")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">{t("contacts.name")}</Label>
|
||||
<Input id="name" value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">{t("contacts.email")}</Label>
|
||||
<Input id="email" type="email" value={formData.email} onChange={(e) => setFormData({ ...formData, email: e.target.value })} required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">{t("contacts.phone")}</Label>
|
||||
<Input id="phone" value={formData.phone} onChange={(e) => setFormData({ ...formData, phone: e.target.value })} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="company">{t("contacts.company")}</Label>
|
||||
<Input id="company" value={formData.company} onChange={(e) => setFormData({ ...formData, company: e.target.value })} required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="position">{t("contacts.position")}</Label>
|
||||
<Input id="position" value={formData.position} onChange={(e) => setFormData({ ...formData, position: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status">{t("contacts.status")}</Label>
|
||||
<Select value={formData.status} onValueChange={(value) => setFormData({ ...formData, status: value as Contact["status"] })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Lead">{t("contacts.lead")}</SelectItem>
|
||||
<SelectItem value="Active">{t("contacts.active")}</SelectItem>
|
||||
<SelectItem value="Customer">{t("contacts.customer")}</SelectItem>
|
||||
<SelectItem value="Inactive">{t("contacts.inactive")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onCancel}>{t("contacts.cancel")}</Button>
|
||||
<Button type="submit">{contact ? t("contacts.updateContact") : t("contacts.createContact")}</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
3
app/deals/loading.tsx
Normal file
3
app/deals/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
292
app/deals/page.tsx
Normal file
292
app/deals/page.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Topbar } from "@/components/topbar"
|
||||
import { Card, CardContent, CardDescription, 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 { Plus, Search, Filter, MoreHorizontal, Eye, Edit, Trash2 } from "lucide-react"
|
||||
import {
|
||||
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { useTranslation } from "@/lib/i18n"
|
||||
|
||||
type Deal = {
|
||||
id: string; dealName: string; client: string
|
||||
stage: "Lead" | "Qualified" | "Proposal" | "Negotiation" | "Closed Won" | "Closed Lost"
|
||||
value: number; probability: number; owner: string; ownerAvatar: string
|
||||
expectedClose: string; description: string; createdAt: string
|
||||
}
|
||||
|
||||
const initialDeals: Deal[] = [
|
||||
{ id: "DEAL-001", dealName: "Enterprise Software License", client: "TechCorp Inc.", stage: "Negotiation", value: 45000, probability: 75, owner: "Jane Doe", ownerAvatar: "https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face", expectedClose: "2024-02-15", description: "Large enterprise software licensing deal for 500+ users", createdAt: "2023-12-01" },
|
||||
{ id: "DEAL-002", dealName: "Marketing Automation Setup", client: "StartupXYZ", stage: "Proposal", value: 12500, probability: 60, owner: "Mike Roberts", ownerAvatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face", expectedClose: "2024-01-30", description: "Complete marketing automation platform implementation", createdAt: "2023-12-15" },
|
||||
{ id: "DEAL-003", dealName: "Cloud Migration Project", client: "Global Solutions", stage: "Qualified", value: 78000, probability: 40, owner: "Sarah Johnson", ownerAvatar: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face", expectedClose: "2024-03-01", description: "Full cloud infrastructure migration and optimization", createdAt: "2023-11-20" },
|
||||
{ id: "DEAL-004", dealName: "CRM Implementation", client: "Retail Chain Co.", stage: "Closed Won", value: 25000, probability: 100, owner: "Alex Lee", ownerAvatar: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face", expectedClose: "2024-01-15", description: "Custom CRM implementation for retail operations", createdAt: "2023-10-15" },
|
||||
]
|
||||
|
||||
const getStageColor = (stage: Deal["stage"]) => {
|
||||
const colors = { Lead: "bg-gray-100 text-gray-800", Qualified: "bg-blue-100 text-blue-800", Proposal: "bg-yellow-100 text-yellow-800", Negotiation: "bg-orange-100 text-orange-800", "Closed Won": "bg-green-100 text-green-800", "Closed Lost": "bg-red-100 text-red-800" }
|
||||
return colors[stage]
|
||||
}
|
||||
|
||||
export default function DealsPage() {
|
||||
const { t } = useTranslation()
|
||||
const [deals, setDeals] = useState<Deal[]>(initialDeals)
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [stageFilter, setStageFilter] = useState<string>("all")
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
const [editingDeal, setEditingDeal] = useState<Deal | null>(null)
|
||||
|
||||
const filteredDeals = deals.filter((deal) => {
|
||||
const matchesSearch = deal.dealName.toLowerCase().includes(searchTerm.toLowerCase()) || deal.client.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const matchesStage = stageFilter === "all" || deal.stage === stageFilter
|
||||
return matchesSearch && matchesStage
|
||||
})
|
||||
|
||||
const handleAddDeal = (dealData: Partial<Deal>) => {
|
||||
const newDeal: Deal = {
|
||||
id: `DEAL-${String(deals.length + 1).padStart(3, "0")}`, dealName: dealData.dealName || "", client: dealData.client || "",
|
||||
stage: dealData.stage || "Lead", value: dealData.value || 0, probability: dealData.probability || 0,
|
||||
owner: dealData.owner || "Current User", ownerAvatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face",
|
||||
expectedClose: dealData.expectedClose || "", description: dealData.description || "", createdAt: new Date().toISOString().split("T")[0],
|
||||
}
|
||||
setDeals([...deals, newDeal]); setIsDialogOpen(false)
|
||||
}
|
||||
|
||||
const handleEditDeal = (dealData: Partial<Deal>) => {
|
||||
if (editingDeal) {
|
||||
setDeals(deals.map((deal) => (deal.id === editingDeal.id ? { ...deal, ...dealData } : deal)))
|
||||
setEditingDeal(null); setIsDialogOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteDeal = (dealId: string) => { setDeals(deals.filter((deal) => deal.id !== dealId)) }
|
||||
|
||||
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("deals.title")}</h1>
|
||||
<p className="text-muted-foreground">{t("deals.description")}</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={() => setEditingDeal(null)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("deals.addNew")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DealDialog deal={editingDeal} onSave={editingDeal ? handleEditDeal : handleAddDeal} onCancel={() => { setIsDialogOpen(false); setEditingDeal(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("deals.search")} className="pl-10" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />
|
||||
</div>
|
||||
<Select value={stageFilter} onValueChange={setStageFilter}>
|
||||
<SelectTrigger className="w-48">
|
||||
<Filter className="mr-2 h-4 w-4" />
|
||||
<SelectValue placeholder={t("deals.filterByStage")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t("deals.allStages")}</SelectItem>
|
||||
<SelectItem value="Lead">{t("deals.lead")}</SelectItem>
|
||||
<SelectItem value="Qualified">{t("deals.qualified")}</SelectItem>
|
||||
<SelectItem value="Proposal">{t("deals.proposal")}</SelectItem>
|
||||
<SelectItem value="Negotiation">{t("deals.negotiation")}</SelectItem>
|
||||
<SelectItem value="Closed Won">{t("deals.closedWon")}</SelectItem>
|
||||
<SelectItem value="Closed Lost">{t("deals.closedLost")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="grid" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="grid">{t("deals.gridView")}</TabsTrigger>
|
||||
<TabsTrigger value="list">{t("deals.listView")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="grid" className="space-y-4">
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredDeals.map((deal) => (
|
||||
<Card key={deal.id} className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-lg">{deal.dealName}</CardTitle>
|
||||
<CardDescription>{deal.client}</CardDescription>
|
||||
</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("deals.actions")}</DropdownMenuLabel>
|
||||
<DropdownMenuItem><Eye className="mr-2 h-4 w-4" />{t("deals.viewDetails")}</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => { setEditingDeal(deal); setIsDialogOpen(true) }}>
|
||||
<Edit className="mr-2 h-4 w-4" />{t("deals.editDeal")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-red-600" onClick={() => handleDeleteDeal(deal.id)}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />{t("deals.deleteDeal")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge className={getStageColor(deal.stage)}>{deal.stage}</Badge>
|
||||
<span className="text-sm text-muted-foreground">{deal.probability}%</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">${deal.value.toLocaleString()}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={deal.ownerAvatar || "/placeholder.svg"} className="object-cover" />
|
||||
<AvatarFallback className="text-xs">{deal.owner.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-sm text-muted-foreground">{deal.owner}</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("deals.expectedClose")} {new Date(deal.expectedClose).toLocaleDateString()}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="list" className="space-y-4">
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="divide-y">
|
||||
{filteredDeals.map((deal) => (
|
||||
<div key={deal.id} className="p-4 hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-medium">{deal.dealName}</h3>
|
||||
<p className="text-sm text-muted-foreground">{deal.client}</p>
|
||||
</div>
|
||||
<Badge className={getStageColor(deal.stage)}>{deal.stage}</Badge>
|
||||
<div className="text-lg font-semibold">${deal.value.toLocaleString()}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={deal.ownerAvatar || "/placeholder.svg"} className="object-cover" />
|
||||
<AvatarFallback className="text-xs">{deal.owner.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-sm">{deal.owner}</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{new Date(deal.expectedClose).toLocaleDateString()}</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("deals.actions")}</DropdownMenuLabel>
|
||||
<DropdownMenuItem><Eye className="mr-2 h-4 w-4" />{t("deals.viewDetails")}</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => { setEditingDeal(deal); setIsDialogOpen(true) }}>
|
||||
<Edit className="mr-2 h-4 w-4" />{t("deals.editDeal")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-red-600" onClick={() => handleDeleteDeal(deal.id)}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />{t("deals.deleteDeal")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DealDialog({ deal, onSave, onCancel }: { deal: Deal | null; onSave: (data: Partial<Deal>) => void; onCancel: () => void }) {
|
||||
const { t } = useTranslation()
|
||||
const [formData, setFormData] = useState({
|
||||
dealName: deal?.dealName || "", client: deal?.client || "", stage: deal?.stage || "Lead",
|
||||
value: deal?.value || 0, probability: deal?.probability || 0, expectedClose: deal?.expectedClose || "", description: deal?.description || "",
|
||||
})
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSave(formData) }
|
||||
|
||||
return (
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{deal ? t("deals.editTitle") : t("deals.addTitle")}</DialogTitle>
|
||||
<DialogDescription>{deal ? t("deals.editDesc") : t("deals.addDesc")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dealName">{t("deals.dealName")}</Label>
|
||||
<Input id="dealName" value={formData.dealName} onChange={(e) => setFormData({ ...formData, dealName: e.target.value })} required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="client">{t("deals.client")}</Label>
|
||||
<Input id="client" value={formData.client} onChange={(e) => setFormData({ ...formData, client: e.target.value })} required />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="stage">{t("deals.stage")}</Label>
|
||||
<Select value={formData.stage} onValueChange={(value) => setFormData({ ...formData, stage: value as Deal["stage"] })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Lead">{t("deals.lead")}</SelectItem>
|
||||
<SelectItem value="Qualified">{t("deals.qualified")}</SelectItem>
|
||||
<SelectItem value="Proposal">{t("deals.proposal")}</SelectItem>
|
||||
<SelectItem value="Negotiation">{t("deals.negotiation")}</SelectItem>
|
||||
<SelectItem value="Closed Won">{t("deals.closedWon")}</SelectItem>
|
||||
<SelectItem value="Closed Lost">{t("deals.closedLost")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="probability">{t("deals.probability")}</Label>
|
||||
<Input id="probability" type="number" min="0" max="100" value={formData.probability} onChange={(e) => setFormData({ ...formData, probability: Number(e.target.value) })} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="value">{t("deals.value")}</Label>
|
||||
<Input id="value" type="number" min="0" value={formData.value} onChange={(e) => setFormData({ ...formData, value: Number(e.target.value) })} required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="expectedClose">{t("deals.expectedCloseDate")}</Label>
|
||||
<Input id="expectedClose" type="date" value={formData.expectedClose} onChange={(e) => setFormData({ ...formData, expectedClose: e.target.value })} required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">{t("deals.descriptionLabel")}</Label>
|
||||
<Textarea id="description" value={formData.description} onChange={(e) => setFormData({ ...formData, description: e.target.value })} rows={3} />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onCancel}>{t("deals.cancel")}</Button>
|
||||
<Button type="submit">{deal ? t("deals.updateDeal") : t("deals.createDeal")}</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
69
app/globals.css
Normal file
69
app/globals.css
Normal file
@@ -0,0 +1,69 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96%;
|
||||
--secondary-foreground: 222.2 84% 4.9%;
|
||||
--muted: 210 40% 96%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96%;
|
||||
--accent-foreground: 222.2 84% 4.9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 0.75rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 84% 4.9%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 224.3 76.3% 94.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
3
app/integrations/loading.tsx
Normal file
3
app/integrations/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
135
app/integrations/page.tsx
Normal file
135
app/integrations/page.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
"use client"
|
||||
|
||||
import { Topbar } from "@/components/topbar"
|
||||
import { Card, CardContent, CardDescription, 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 { Search, Slack, Calendar, Mail, Zap, Github, Chrome, Database, MessageSquare } from "lucide-react"
|
||||
import { useTranslation } from "@/lib/i18n"
|
||||
|
||||
const integrationDescKeys: Record<string, string> = {
|
||||
Slack: "integrations.slackDesc",
|
||||
"Google Calendar": "integrations.calendarDesc",
|
||||
HubSpot: "integrations.hubspotDesc",
|
||||
Zapier: "integrations.zapierDesc",
|
||||
Gmail: "integrations.gmailDesc",
|
||||
GitHub: "integrations.githubDesc",
|
||||
"Chrome Extension": "integrations.chromeDesc",
|
||||
"Microsoft Teams": "integrations.teamsDesc",
|
||||
}
|
||||
|
||||
const categoryKeys: Record<string, string> = {
|
||||
All: "integrations.all",
|
||||
Communication: "integrations.communication",
|
||||
Productivity: "integrations.productivity",
|
||||
CRM: "integrations.crm",
|
||||
Automation: "integrations.automation",
|
||||
Email: "integrations.email",
|
||||
Development: "integrations.development",
|
||||
Browser: "integrations.browser",
|
||||
}
|
||||
|
||||
const integrations = [
|
||||
{ id: 1, name: "Slack", icon: Slack, connected: true, category: "Communication" },
|
||||
{ id: 2, name: "Google Calendar", icon: Calendar, connected: true, category: "Productivity" },
|
||||
{ id: 3, name: "HubSpot", icon: Database, connected: false, category: "CRM" },
|
||||
{ id: 4, name: "Zapier", icon: Zap, connected: false, category: "Automation" },
|
||||
{ id: 5, name: "Gmail", icon: Mail, connected: true, category: "Email" },
|
||||
{ id: 6, name: "GitHub", icon: Github, connected: false, category: "Development" },
|
||||
{ id: 7, name: "Chrome Extension", icon: Chrome, connected: false, category: "Browser" },
|
||||
{ id: 8, name: "Microsoft Teams", icon: MessageSquare, connected: false, category: "Communication" },
|
||||
]
|
||||
|
||||
const categories = ["All", "Communication", "Productivity", "CRM", "Automation", "Email", "Development", "Browser"]
|
||||
|
||||
export default function Integrations() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Topbar />
|
||||
<div className="flex-1 p-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{t("integrations.title")}</h1>
|
||||
<p className="text-muted-foreground">{t("integrations.description")}</p>
|
||||
</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("integrations.search")} className="pl-10" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{categories.map((category) => (
|
||||
<Button key={category} variant={category === "All" ? "default" : "outline"} size="sm">
|
||||
{t(categoryKeys[category])}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{integrations.map((integration) => (
|
||||
<Card key={integration.id} className="relative">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted">
|
||||
<integration.icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg">{integration.name}</CardTitle>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{t(categoryKeys[integration.category])}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{integration.connected && <Badge className="bg-green-100 text-green-800">{t("integrations.connected")}</Badge>}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="mb-4">{t(integrationDescKeys[integration.name])}</CardDescription>
|
||||
<Button className="w-full" variant={integration.connected ? "outline" : "default"}>
|
||||
{integration.connected ? t("integrations.manage") : t("integrations.connect")}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("integrations.popularTitle")}</CardTitle>
|
||||
<CardDescription>{t("integrations.popularDesc")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="flex items-center gap-3 p-3 border rounded-lg">
|
||||
<Slack className="h-8 w-8 text-purple-600" />
|
||||
<div>
|
||||
<p className="font-medium">Slack</p>
|
||||
<p className="text-sm text-muted-foreground">{t("integrations.slackPopular")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 border rounded-lg">
|
||||
<Calendar className="h-8 w-8 text-blue-600" />
|
||||
<div>
|
||||
<p className="font-medium">Google Calendar</p>
|
||||
<p className="text-sm text-muted-foreground">{t("integrations.calendarPopular")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 border rounded-lg">
|
||||
<Mail className="h-8 w-8 text-red-600" />
|
||||
<div>
|
||||
<p className="font-medium">Gmail</p>
|
||||
<p className="text-sm text-muted-foreground">{t("integrations.gmailPopular")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
38
app/layout.tsx
Normal file
38
app/layout.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import type React from "react"
|
||||
import type { Metadata } from "next"
|
||||
import { Inter } from "next/font/google"
|
||||
import "./globals.css"
|
||||
import { SidebarProvider } from "@/components/ui/sidebar"
|
||||
import { AppSidebar } from "@/components/app-sidebar"
|
||||
import { Toaster } from "@/components/ui/toaster"
|
||||
import { I18nProvider } from "@/lib/i18n"
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Sales CRM - Dashboard",
|
||||
description: "Modern Sales CRM Interface",
|
||||
generator: 'v0.app'
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<I18nProvider>
|
||||
<SidebarProvider>
|
||||
<div className="flex min-h-screen w-full">
|
||||
<AppSidebar />
|
||||
<main className="flex-1 overflow-auto">{children}</main>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
<Toaster />
|
||||
</I18nProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
157
app/page.tsx
Normal file
157
app/page.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client"
|
||||
|
||||
import { Topbar } from "@/components/topbar"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { CalendarDateRangePicker } from "@/components/date-range-picker"
|
||||
import { Overview } from "@/components/overview"
|
||||
import { RecentSales } from "@/components/recent-sales"
|
||||
import { DataTable } from "@/components/data-table"
|
||||
import { DollarSign, TrendingUp, Users, Target, Plus } from "lucide-react"
|
||||
import { useTranslation } from "@/lib/i18n"
|
||||
|
||||
export default function Dashboard() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const dashboardStats = [
|
||||
{
|
||||
title: t("dashboard.stats.monthlyRevenue"),
|
||||
value: "$48,500",
|
||||
description: t("dashboard.stats.monthlyRevenueChange"),
|
||||
icon: DollarSign,
|
||||
},
|
||||
{
|
||||
title: t("dashboard.stats.newDealsClosed"),
|
||||
value: "12",
|
||||
description: t("dashboard.stats.newDealsChange"),
|
||||
icon: Target,
|
||||
},
|
||||
{
|
||||
title: t("dashboard.stats.pipelineValue"),
|
||||
value: "$320,000",
|
||||
description: t("dashboard.stats.pipelineChange"),
|
||||
icon: TrendingUp,
|
||||
},
|
||||
{
|
||||
title: t("dashboard.stats.conversionRate"),
|
||||
value: "18.4%",
|
||||
description: t("dashboard.stats.conversionChange"),
|
||||
icon: Users,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Topbar />
|
||||
|
||||
<div className="flex-1 space-y-4 p-8 pt-6">
|
||||
<div className="flex items-center justify-between space-y-2">
|
||||
<h2 className="text-3xl font-bold tracking-tight">{t("dashboard.title")}</h2>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CalendarDateRangePicker />
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("dashboard.addNewDeal")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs defaultValue="overview" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">{t("dashboard.tabs.overview")}</TabsTrigger>
|
||||
<TabsTrigger value="analytics">{t("dashboard.tabs.analytics")}</TabsTrigger>
|
||||
<TabsTrigger value="deals">{t("dashboard.tabs.allDeals")}</TabsTrigger>
|
||||
<TabsTrigger value="reports">{t("dashboard.tabs.reports")}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview" className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{dashboardStats.map((stat, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{stat.title}</CardTitle>
|
||||
<stat.icon className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stat.value}</div>
|
||||
<p className="text-xs text-muted-foreground">{stat.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
||||
<Card className="col-span-4">
|
||||
<CardHeader>
|
||||
<CardTitle>{t("dashboard.revenueOverview")}</CardTitle>
|
||||
<CardDescription>{t("dashboard.revenueOverviewDesc")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pl-2">
|
||||
<Overview />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="col-span-3">
|
||||
<CardHeader>
|
||||
<CardTitle>{t("dashboard.recentSales")}</CardTitle>
|
||||
<CardDescription>{t("dashboard.recentSalesDesc")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RecentSales />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="deals" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("dashboard.allDealsTitle")}</CardTitle>
|
||||
<CardDescription>{t("dashboard.allDealsDesc")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="analytics" className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{dashboardStats.map((stat, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{stat.title}</CardTitle>
|
||||
<stat.icon className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stat.value}</div>
|
||||
<p className="text-xs text-muted-foreground">{stat.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("dashboard.analyticsTitle")}</CardTitle>
|
||||
<CardDescription>{t("dashboard.analyticsDesc")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center h-[200px] text-muted-foreground">
|
||||
{t("dashboard.analyticsPlaceholder")}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="reports" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("dashboard.reportsTitle")}</CardTitle>
|
||||
<CardDescription>{t("dashboard.reportsDesc")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center h-[200px] text-muted-foreground">
|
||||
{t("dashboard.reportsPlaceholder")}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
270
app/settings/page.tsx
Normal file
270
app/settings/page.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Topbar } from "@/components/topbar"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { User, Bell, Shield, CreditCard, Users, Plus, Trash2 } from "lucide-react"
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import { useTranslation } from "@/lib/i18n"
|
||||
|
||||
const teamMembers = [
|
||||
{ id: 1, name: "John Doe", email: "john@company.com", role: "Admin", status: "Active", avatar: "/placeholder.svg?height=32&width=32" },
|
||||
{ id: 2, name: "Sarah Johnson", email: "sarah@company.com", role: "Sales Manager", status: "Active", avatar: "/placeholder.svg?height=32&width=32" },
|
||||
{ id: 3, name: "Mike Chen", email: "mike@company.com", role: "Sales Rep", status: "Active", avatar: "/placeholder.svg?height=32&width=32" },
|
||||
{ id: 4, name: "Emily Davis", email: "emily@company.com", role: "Sales Rep", status: "Pending", avatar: "/placeholder.svg?height=32&width=32" },
|
||||
]
|
||||
|
||||
export default function Settings() {
|
||||
const { t } = useTranslation()
|
||||
const { toast } = useToast()
|
||||
const [notifications, setNotifications] = useState({
|
||||
email: true, sms: false, inApp: true, dealUpdates: true, taskReminders: true, weeklyReports: false,
|
||||
})
|
||||
|
||||
const handleSave = () => {
|
||||
toast({ title: t("settings.toast.saved"), description: t("settings.toast.savedDesc") })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Topbar />
|
||||
<div className="flex-1 p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold tracking-tight">{t("settings.title")}</h1>
|
||||
<p className="text-muted-foreground">{t("settings.description")}</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="profile" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-5">
|
||||
<TabsTrigger value="profile" className="flex items-center gap-2"><User className="h-4 w-4" />{t("settings.tabs.profile")}</TabsTrigger>
|
||||
<TabsTrigger value="team" className="flex items-center gap-2"><Users className="h-4 w-4" />{t("settings.tabs.team")}</TabsTrigger>
|
||||
<TabsTrigger value="billing" className="flex items-center gap-2"><CreditCard className="h-4 w-4" />{t("settings.tabs.billing")}</TabsTrigger>
|
||||
<TabsTrigger value="notifications" className="flex items-center gap-2"><Bell className="h-4 w-4" />{t("settings.tabs.notifications")}</TabsTrigger>
|
||||
<TabsTrigger value="security" className="flex items-center gap-2"><Shield className="h-4 w-4" />{t("settings.tabs.security")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="profile" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("settings.profile.title")}</CardTitle>
|
||||
<CardDescription>{t("settings.profile.desc")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center gap-6">
|
||||
<Avatar className="h-20 w-20"><AvatarImage src="/placeholder.svg?height=80&width=80" /><AvatarFallback>JD</AvatarFallback></Avatar>
|
||||
<div>
|
||||
<Button variant="outline">{t("settings.profile.changePhoto")}</Button>
|
||||
<p className="text-sm text-muted-foreground mt-2">{t("settings.profile.photoHint")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2"><Label htmlFor="firstName">{t("settings.profile.firstName")}</Label><Input id="firstName" defaultValue="John" /></div>
|
||||
<div className="space-y-2"><Label htmlFor="lastName">{t("settings.profile.lastName")}</Label><Input id="lastName" defaultValue="Doe" /></div>
|
||||
</div>
|
||||
<div className="space-y-2"><Label htmlFor="email">{t("settings.profile.email")}</Label><Input id="email" type="email" defaultValue="john@company.com" /></div>
|
||||
<div className="space-y-2"><Label htmlFor="title">{t("settings.profile.jobTitle")}</Label><Input id="title" defaultValue="Sales Manager" /></div>
|
||||
<Button onClick={handleSave}>{t("settings.profile.saveChanges")}</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="team" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div><CardTitle>{t("settings.team.title")}</CardTitle><CardDescription>{t("settings.team.desc")}</CardDescription></div>
|
||||
<Button><Plus className="mr-2 h-4 w-4" />{t("settings.team.inviteUser")}</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("settings.team.user")}</TableHead>
|
||||
<TableHead>{t("settings.team.role")}</TableHead>
|
||||
<TableHead>{t("settings.team.status")}</TableHead>
|
||||
<TableHead>{t("settings.team.actions")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{teamMembers.map((member) => (
|
||||
<TableRow key={member.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="h-8 w-8"><AvatarImage src={member.avatar || "/placeholder.svg"} /><AvatarFallback>{member.name.split(" ").map((n) => n[0]).join("")}</AvatarFallback></Avatar>
|
||||
<div><p className="font-medium">{member.name}</p><p className="text-sm text-muted-foreground">{member.email}</p></div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Select defaultValue={member.role.toLowerCase().replace(" ", "_")}>
|
||||
<SelectTrigger className="w-32"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="admin">{t("settings.team.admin")}</SelectItem>
|
||||
<SelectItem value="sales_manager">{t("settings.team.salesManager")}</SelectItem>
|
||||
<SelectItem value="sales_rep">{t("settings.team.salesRep")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={member.status === "Active" ? "bg-green-100 text-green-800" : "bg-yellow-100 text-yellow-800"}>{member.status}</Badge>
|
||||
</TableCell>
|
||||
<TableCell><Button variant="ghost" size="icon"><Trash2 className="h-4 w-4" /></Button></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="billing" className="space-y-6">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader><CardTitle>{t("settings.billing.currentPlan")}</CardTitle><CardDescription>{t("settings.billing.currentPlanDesc")}</CardDescription></CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between"><span>{t("settings.billing.plan")}</span><Badge>{t("settings.billing.professional")}</Badge></div>
|
||||
<div className="flex items-center justify-between"><span>{t("settings.billing.price")}</span><span className="font-medium">{t("settings.billing.priceValue")}</span></div>
|
||||
<div className="flex items-center justify-between"><span>{t("settings.billing.users")}</span><span>{t("settings.billing.usersValue")}</span></div>
|
||||
<Button className="w-full">{t("settings.billing.upgradePlan")}</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>{t("settings.billing.paymentMethod")}</CardTitle><CardDescription>{t("settings.billing.paymentDesc")}</CardDescription></CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 p-3 border rounded-lg">
|
||||
<CreditCard className="h-6 w-6" />
|
||||
<div className="flex-1"><p className="font-medium">•••• •••• •••• 4242</p><p className="text-sm text-muted-foreground">{t("settings.billing.expires")}</p></div>
|
||||
</div>
|
||||
<Button variant="outline" className="w-full">{t("settings.billing.updatePayment")}</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>{t("settings.billing.billingHistory")}</CardTitle><CardDescription>{t("settings.billing.billingHistoryDesc")}</CardDescription></CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("settings.billing.date")}</TableHead>
|
||||
<TableHead>{t("settings.billing.amount")}</TableHead>
|
||||
<TableHead>{t("settings.billing.status")}</TableHead>
|
||||
<TableHead>{t("settings.billing.invoice")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Dec 1, 2024</TableCell><TableCell>$49.00</TableCell>
|
||||
<TableCell><Badge className="bg-green-100 text-green-800">{t("settings.billing.paid")}</Badge></TableCell>
|
||||
<TableCell><Button variant="ghost" size="sm">{t("settings.billing.download")}</Button></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Nov 1, 2024</TableCell><TableCell>$49.00</TableCell>
|
||||
<TableCell><Badge className="bg-green-100 text-green-800">{t("settings.billing.paid")}</Badge></TableCell>
|
||||
<TableCell><Button variant="ghost" size="sm">{t("settings.billing.download")}</Button></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="notifications" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader><CardTitle>{t("settings.notifications.title")}</CardTitle><CardDescription>{t("settings.notifications.desc")}</CardDescription></CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-medium">{t("settings.notifications.communication")}</h4>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div><Label htmlFor="email-notifications">{t("settings.notifications.emailNotifications")}</Label><p className="text-sm text-muted-foreground">{t("settings.notifications.emailNotificationsDesc")}</p></div>
|
||||
<Switch id="email-notifications" checked={notifications.email} onCheckedChange={(checked) => setNotifications((prev) => ({ ...prev, email: checked }))} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div><Label htmlFor="sms-notifications">{t("settings.notifications.smsNotifications")}</Label><p className="text-sm text-muted-foreground">{t("settings.notifications.smsNotificationsDesc")}</p></div>
|
||||
<Switch id="sms-notifications" checked={notifications.sms} onCheckedChange={(checked) => setNotifications((prev) => ({ ...prev, sms: checked }))} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div><Label htmlFor="in-app-notifications">{t("settings.notifications.inAppNotifications")}</Label><p className="text-sm text-muted-foreground">{t("settings.notifications.inAppNotificationsDesc")}</p></div>
|
||||
<Switch id="in-app-notifications" checked={notifications.inApp} onCheckedChange={(checked) => setNotifications((prev) => ({ ...prev, inApp: checked }))} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-medium">{t("settings.notifications.salesUpdates")}</h4>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div><Label htmlFor="deal-updates">{t("settings.notifications.dealUpdates")}</Label><p className="text-sm text-muted-foreground">{t("settings.notifications.dealUpdatesDesc")}</p></div>
|
||||
<Switch id="deal-updates" checked={notifications.dealUpdates} onCheckedChange={(checked) => setNotifications((prev) => ({ ...prev, dealUpdates: checked }))} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div><Label htmlFor="task-reminders">{t("settings.notifications.taskReminders")}</Label><p className="text-sm text-muted-foreground">{t("settings.notifications.taskRemindersDesc")}</p></div>
|
||||
<Switch id="task-reminders" checked={notifications.taskReminders} onCheckedChange={(checked) => setNotifications((prev) => ({ ...prev, taskReminders: checked }))} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div><Label htmlFor="weekly-reports">{t("settings.notifications.weeklyReports")}</Label><p className="text-sm text-muted-foreground">{t("settings.notifications.weeklyReportsDesc")}</p></div>
|
||||
<Switch id="weekly-reports" checked={notifications.weeklyReports} onCheckedChange={(checked) => setNotifications((prev) => ({ ...prev, weeklyReports: checked }))} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleSave}>{t("settings.notifications.savePreferences")}</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="security" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader><CardTitle>{t("settings.security.password")}</CardTitle><CardDescription>{t("settings.security.passwordDesc")}</CardDescription></CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2"><Label htmlFor="current-password">{t("settings.security.currentPassword")}</Label><Input id="current-password" type="password" /></div>
|
||||
<div className="space-y-2"><Label htmlFor="new-password">{t("settings.security.newPassword")}</Label><Input id="new-password" type="password" /></div>
|
||||
<div className="space-y-2"><Label htmlFor="confirm-password">{t("settings.security.confirmPassword")}</Label><Input id="confirm-password" type="password" /></div>
|
||||
<Button>{t("settings.security.updatePassword")}</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>{t("settings.security.twoFactor")}</CardTitle><CardDescription>{t("settings.security.twoFactorDesc")}</CardDescription></CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div><p className="font-medium">{t("settings.security.authenticatorApp")}</p><p className="text-sm text-muted-foreground">{t("settings.security.authenticatorDesc")}</p></div>
|
||||
<Button variant="outline">{t("settings.security.setup")}</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div><p className="font-medium">{t("settings.security.smsVerification")}</p><p className="text-sm text-muted-foreground">{t("settings.security.smsVerificationDesc")}</p></div>
|
||||
<Button variant="outline">{t("settings.security.setup")}</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>{t("settings.security.activeSessions")}</CardTitle><CardDescription>{t("settings.security.activeSessionsDesc")}</CardDescription></CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div><p className="font-medium">{t("settings.security.currentSession")}</p><p className="text-sm text-muted-foreground">{t("settings.security.currentSessionDetail")}</p></div>
|
||||
<Badge className="bg-green-100 text-green-800">{t("settings.security.activeLabel")}</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div><p className="font-medium">{t("settings.security.mobileApp")}</p><p className="text-sm text-muted-foreground">{t("settings.security.mobileAppDetail")}</p></div>
|
||||
<Button variant="outline" size="sm">{t("settings.security.revoke")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/tasks/loading.tsx
Normal file
3
app/tasks/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
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