mirror of
https://git.coastit.co.za/caitlin/CRM_ENVR.git
synced 2026-07-10 03:05:43 +02:00
Fixing calander
This commit is contained in:
+197
-46
@@ -21,17 +21,14 @@ import { useNotifications } from "@/providers/notification-provider"
|
|||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import {
|
import {
|
||||||
ChevronLeft, ChevronRight, Plus, Calendar, Clock, Phone,
|
ChevronLeft, ChevronRight, Plus, Calendar, Clock, Phone,
|
||||||
Video, Users, CheckCircle2, XCircle, RotateCcw, Loader2,
|
Users, CheckCircle2, XCircle, RotateCcw, Loader2,
|
||||||
ArrowUpRight, Zap, StickyNote,
|
ArrowUpRight, Zap, StickyNote, Globe,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
const EVENT_ICONS: Record<EventType, React.ElementType> = {
|
const EVENT_ICONS: Record<EventType, React.ElementType> = {
|
||||||
meeting: Users,
|
|
||||||
call: Phone,
|
call: Phone,
|
||||||
chat: Video,
|
|
||||||
follow_up: Clock,
|
follow_up: Clock,
|
||||||
demo: Calendar,
|
website_creation: Globe,
|
||||||
other: Calendar,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventVars(type: EventType) {
|
function eventVars(type: EventType) {
|
||||||
@@ -85,14 +82,20 @@ export default function CalendarPage() {
|
|||||||
const [editingEvent, setEditingEvent] = useState<CalendarEvent | null>(null)
|
const [editingEvent, setEditingEvent] = useState<CalendarEvent | null>(null)
|
||||||
const [detailEvent, setDetailEvent] = useState<CalendarEvent | null>(null)
|
const [detailEvent, setDetailEvent] = useState<CalendarEvent | null>(null)
|
||||||
|
|
||||||
const [users, setUsers] = useState<{ id: string; name: string; email: string }[]>([])
|
const [users, setUsers] = useState<{ id: string; name: string; email: string; role: string }[]>([])
|
||||||
|
const [leads, setLeads] = useState<{ id: string; contactName: string; companyName: string }[]>([])
|
||||||
const [formTitle, setFormTitle] = useState("")
|
const [formTitle, setFormTitle] = useState("")
|
||||||
const [formDescription, setFormDescription] = useState("")
|
const [formDescription, setFormDescription] = useState("")
|
||||||
const [formType, setFormType] = useState<EventType>("meeting")
|
const [formType, setFormType] = useState<EventType>("website_creation")
|
||||||
const [formDate, setFormDate] = useState("")
|
const [formDate, setFormDate] = useState("")
|
||||||
const [formTime, setFormTime] = useState("")
|
const [formTime, setFormTime] = useState("")
|
||||||
const [formDuration, setFormDuration] = useState("30")
|
const [formDuration, setFormDuration] = useState("30")
|
||||||
const [formParticipantId, setFormParticipantId] = useState("")
|
const [formParticipantId, setFormParticipantId] = useState("")
|
||||||
|
const [formDeveloperId, setFormDeveloperId] = useState("")
|
||||||
|
const [formLeadId, setFormLeadId] = useState("")
|
||||||
|
const [formClientName, setFormClientName] = useState("")
|
||||||
|
const [formClientEmail, setFormClientEmail] = useState("")
|
||||||
|
const [formClientPhone, setFormClientPhone] = useState("")
|
||||||
const [formSaving, setFormSaving] = useState(false)
|
const [formSaving, setFormSaving] = useState(false)
|
||||||
const [editNotes, setEditNotes] = useState(false)
|
const [editNotes, setEditNotes] = useState(false)
|
||||||
const [notesText, setNotesText] = useState("")
|
const [notesText, setNotesText] = useState("")
|
||||||
@@ -124,6 +127,10 @@ export default function CalendarPage() {
|
|||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((data) => setUsers(data.users || []))
|
.then((data) => setUsers(data.users || []))
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
|
fetch("/api/leads?limit=200")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data) => setLeads((Array.isArray(data) ? data : data?.leads || []).map((l: any) => ({ id: l.id, contactName: l.contactName, companyName: l.companyName }))))
|
||||||
|
.catch(() => {})
|
||||||
}, [user, router])
|
}, [user, router])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -158,7 +165,7 @@ export default function CalendarPage() {
|
|||||||
|
|
||||||
const getEventsForDay = useCallback((day: number) => {
|
const getEventsForDay = useCallback((day: number) => {
|
||||||
const dateStr = `${currentYear}-${String(currentMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`
|
const dateStr = `${currentYear}-${String(currentMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`
|
||||||
return events.filter((e) => e.startTime.startsWith(dateStr))
|
return events.filter((e) => e.startTime && e.startTime.startsWith(dateStr))
|
||||||
}, [events, currentYear, currentMonth])
|
}, [events, currentYear, currentMonth])
|
||||||
|
|
||||||
const isToday = (day: number) => {
|
const isToday = (day: number) => {
|
||||||
@@ -169,9 +176,14 @@ export default function CalendarPage() {
|
|||||||
setEditingEvent(null)
|
setEditingEvent(null)
|
||||||
setFormTitle("")
|
setFormTitle("")
|
||||||
setFormDescription("")
|
setFormDescription("")
|
||||||
setFormType("meeting")
|
setFormType("website_creation")
|
||||||
setFormDuration("30")
|
setFormDuration("30")
|
||||||
setFormParticipantId("")
|
setFormParticipantId("")
|
||||||
|
setFormDeveloperId("")
|
||||||
|
setFormLeadId("")
|
||||||
|
setFormClientName("")
|
||||||
|
setFormClientEmail("")
|
||||||
|
setFormClientPhone("")
|
||||||
const d = day ? new Date(currentYear, currentMonth, day) : new Date()
|
const d = day ? new Date(currentYear, currentMonth, day) : new Date()
|
||||||
setFormDate(d.toISOString().split("T")[0])
|
setFormDate(d.toISOString().split("T")[0])
|
||||||
setFormTime("10:00")
|
setFormTime("10:00")
|
||||||
@@ -184,9 +196,14 @@ export default function CalendarPage() {
|
|||||||
setFormDescription(event.description || "")
|
setFormDescription(event.description || "")
|
||||||
setFormType(event.eventType)
|
setFormType(event.eventType)
|
||||||
setFormDuration(String(event.durationMinutes || 30))
|
setFormDuration(String(event.durationMinutes || 30))
|
||||||
setFormDate(new Date(event.startTime).toISOString().split("T")[0])
|
setFormDate(event.startTime ? new Date(event.startTime).toISOString().split("T")[0] : "")
|
||||||
setFormTime(new Date(event.startTime).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }))
|
setFormTime(event.startTime ? new Date(event.startTime).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }) : "10:00")
|
||||||
setFormParticipantId(event.participantId || "")
|
setFormParticipantId(event.participantId || "")
|
||||||
|
setFormDeveloperId(event.developerId || "")
|
||||||
|
setFormLeadId(event.leadId || "")
|
||||||
|
setFormClientName(event.clientName || "")
|
||||||
|
setFormClientEmail(event.clientEmail || "")
|
||||||
|
setFormClientPhone(event.clientPhone || "")
|
||||||
setDialogOpen(true)
|
setDialogOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,23 +215,35 @@ export default function CalendarPage() {
|
|||||||
|
|
||||||
setFormSaving(true)
|
setFormSaving(true)
|
||||||
try {
|
try {
|
||||||
const startTime = new Date(`${formDate}T${formTime}:00`).toISOString()
|
const body: Record<string, unknown> = {
|
||||||
|
title: formTitle.trim(),
|
||||||
|
description: formDescription.trim() || null,
|
||||||
|
eventType: formType,
|
||||||
|
}
|
||||||
|
|
||||||
|
let startTime: string | undefined
|
||||||
|
if (formType !== "website_creation") {
|
||||||
|
startTime = new Date(`${formDate}T${formTime}:00`).toISOString()
|
||||||
let endTime = null
|
let endTime = null
|
||||||
if (formDuration) {
|
if (formDuration) {
|
||||||
const end = new Date(startTime)
|
const end = new Date(startTime)
|
||||||
end.setMinutes(end.getMinutes() + parseInt(formDuration))
|
end.setMinutes(end.getMinutes() + parseInt(formDuration))
|
||||||
endTime = end.toISOString()
|
endTime = end.toISOString()
|
||||||
}
|
}
|
||||||
|
body.startTime = startTime
|
||||||
const body: Record<string, unknown> = {
|
body.endTime = endTime
|
||||||
title: formTitle.trim(),
|
body.durationMinutes = formDuration ? parseInt(formDuration) : null
|
||||||
description: formDescription.trim() || null,
|
} else if (formDate) {
|
||||||
eventType: formType,
|
startTime = new Date(`${formDate}T00:00:00`).toISOString()
|
||||||
startTime,
|
body.startTime = startTime
|
||||||
endTime,
|
|
||||||
durationMinutes: formDuration ? parseInt(formDuration) : null,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (formClientName) body.clientName = formClientName
|
||||||
|
if (formClientEmail) body.clientEmail = formClientEmail
|
||||||
|
if (formClientPhone) body.clientPhone = formClientPhone
|
||||||
if (formParticipantId) body.participantId = formParticipantId
|
if (formParticipantId) body.participantId = formParticipantId
|
||||||
|
if (formDeveloperId) body.developerId = formDeveloperId
|
||||||
|
if (formLeadId) body.leadId = formLeadId
|
||||||
|
|
||||||
let res
|
let res
|
||||||
if (editingEvent) {
|
if (editingEvent) {
|
||||||
@@ -235,11 +264,13 @@ export default function CalendarPage() {
|
|||||||
|
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (editingEvent) {
|
if (editingEvent) {
|
||||||
setEvents((prev) => prev.map((e) => e.id === editingEvent.id ? { ...data.event, participant: e.participant, lead: e.lead } : e))
|
setEvents((prev) => prev.map((e) => e.id === editingEvent.id ? { ...data.event, participant: e.participant, developer: e.developer, lead: e.lead } : e))
|
||||||
toast.success("Event updated")
|
toast.success("Event updated")
|
||||||
} else {
|
} else {
|
||||||
setEvents((prev) => [...prev, data.event])
|
setEvents((prev) => [...prev, data.event])
|
||||||
|
if (startTime) {
|
||||||
addNotification("event_scheduled", `Event created: ${formTitle}`, `Scheduled for ${formatDate(startTime)} at ${formatTime(startTime)}`, "/calendar")
|
addNotification("event_scheduled", `Event created: ${formTitle}`, `Scheduled for ${formatDate(startTime)} at ${formatTime(startTime)}`, "/calendar")
|
||||||
|
}
|
||||||
toast.success("Event created")
|
toast.success("Event created")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +291,7 @@ export default function CalendarPage() {
|
|||||||
})
|
})
|
||||||
if (!res.ok) throw new Error("Failed to update")
|
if (!res.ok) throw new Error("Failed to update")
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setEvents((prev) => prev.map((e) => e.id === event.id ? { ...data.event, participant: e.participant, lead: e.lead } : e))
|
setEvents((prev) => prev.map((e) => e.id === event.id ? { ...data.event, participant: e.participant, developer: e.developer, lead: e.lead } : e))
|
||||||
toast.success(`Event ${newStatus}`)
|
toast.success(`Event ${newStatus}`)
|
||||||
setDetailEvent(null)
|
setDetailEvent(null)
|
||||||
} catch {
|
} catch {
|
||||||
@@ -446,6 +477,11 @@ export default function CalendarPage() {
|
|||||||
>
|
>
|
||||||
<Icon className="h-2 w-2 shrink-0 opacity-50" />
|
<Icon className="h-2 w-2 shrink-0 opacity-50" />
|
||||||
<span className="truncate font-medium">{event.title}</span>
|
<span className="truncate font-medium">{event.title}</span>
|
||||||
|
{event.lead && (
|
||||||
|
<span className="shrink-0 text-[9px] font-semibold text-muted-foreground/50 ml-auto">
|
||||||
|
{event.lead.contactName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
@@ -464,6 +500,15 @@ export default function CalendarPage() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Unscheduled projects bar */}
|
||||||
|
{events.filter(e => !e.startTime && e.eventType === "website_creation").length > 0 && (
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 rounded-lg border bg-gradient-to-r from-violet-500/5 to-transparent shrink-0">
|
||||||
|
<Globe className="h-3.5 w-3.5 text-violet-500/70" />
|
||||||
|
<span className="text-xs font-semibold text-violet-600 dark:text-violet-400">
|
||||||
|
{events.filter(e => !e.startTime && e.eventType === "website_creation").length} unscheduled project(s)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -534,12 +579,19 @@ export default function CalendarPage() {
|
|||||||
{event.status}
|
{event.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{event.startTime ? (
|
||||||
<div className="flex items-center gap-1.5 mt-1 text-[10px] text-muted-foreground/60 font-medium">
|
<div className="flex items-center gap-1.5 mt-1 text-[10px] text-muted-foreground/60 font-medium">
|
||||||
<Clock className="h-3 w-3" />
|
<Clock className="h-3 w-3" />
|
||||||
{formatDate(event.startTime)}
|
{formatDate(event.startTime)}
|
||||||
<span className="text-muted-foreground/20">·</span>
|
<span className="text-muted-foreground/20">·</span>
|
||||||
{formatTime(event.startTime)}
|
{formatTime(event.startTime)}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-1.5 mt-1 text-[10px] text-muted-foreground/40 font-medium">
|
||||||
|
<Globe className="h-3 w-3" />
|
||||||
|
Website project — no schedule
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{event.durationMinutes && (
|
{event.durationMinutes && (
|
||||||
<div className="flex items-center gap-1 mt-0.5 text-[10px] text-muted-foreground/40 font-medium">
|
<div className="flex items-center gap-1 mt-0.5 text-[10px] text-muted-foreground/40 font-medium">
|
||||||
<Zap className="h-2.5 w-2.5" />
|
<Zap className="h-2.5 w-2.5" />
|
||||||
@@ -547,21 +599,20 @@ export default function CalendarPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-1 mt-1">
|
<div className="flex items-center gap-1 mt-1">
|
||||||
<span className={cn(
|
{event.developerId === user?.id ? (
|
||||||
"h-3.5 w-3.5 rounded-full text-[7px] flex items-center justify-center font-bold ring-1",
|
<>
|
||||||
event.userId === user?.id
|
<span className="h-3.5 w-3.5 rounded-full text-[7px] flex items-center justify-center font-bold ring-1 bg-amber-500/10 text-amber-600 dark:text-amber-400 ring-amber-500/20">
|
||||||
? "bg-primary/10 text-primary ring-primary/20"
|
{event.creator?.name.charAt(0) || "?"}
|
||||||
: "bg-muted text-muted-foreground/60 ring-border",
|
|
||||||
)}>
|
|
||||||
{event.userId === user?.id
|
|
||||||
? (event.participant?.name.charAt(0) || "?")
|
|
||||||
: (event.creator?.name.charAt(0) || "?")}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-muted-foreground/60 font-medium">
|
<span className="text-[10px] text-muted-foreground/60 font-medium">
|
||||||
{event.userId === user?.id
|
Assigned by {event.creator?.name || "Unknown"}
|
||||||
? (event.participant?.name || "No participant")
|
|
||||||
: (event.creator?.name || "Unknown")}
|
|
||||||
</span>
|
</span>
|
||||||
|
</>
|
||||||
|
) : event.lead ? (
|
||||||
|
<span className="text-[10px] text-muted-foreground/60 font-medium">
|
||||||
|
{event.lead.contactName}{event.lead.companyName ? ` — ${event.lead.companyName}` : ""}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -579,7 +630,7 @@ export default function CalendarPage() {
|
|||||||
|
|
||||||
{/* Create/Edit Dialog */}
|
{/* Create/Edit Dialog */}
|
||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<DialogContent className="sm:max-w-[520px]">
|
<DialogContent className="sm:max-w-[520px] max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
@@ -608,11 +659,28 @@ export default function CalendarPage() {
|
|||||||
<Label htmlFor="date" className="text-xs font-semibold">Date</Label>
|
<Label htmlFor="date" className="text-xs font-semibold">Date</Label>
|
||||||
<Input id="date" type="date" value={formDate} onChange={(e) => setFormDate(e.target.value)} className="h-10" />
|
<Input id="date" type="date" value={formDate} onChange={(e) => setFormDate(e.target.value)} className="h-10" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className={cn("space-y-2", formType === "website_creation" && "opacity-40 pointer-events-none")}>
|
||||||
<Label htmlFor="time" className="text-xs font-semibold">Time</Label>
|
<Label htmlFor="time" className="text-xs font-semibold">Time</Label>
|
||||||
<Input id="time" type="time" value={formTime} onChange={(e) => setFormTime(e.target.value)} className="h-10" />
|
<Input id="time" type="time" value={formTime} onChange={(e) => setFormTime(e.target.value)} className="h-10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{formType === "website_creation" && (
|
||||||
|
<div className="space-y-4 rounded-xl border bg-muted/20 p-4">
|
||||||
|
<p className="text-xs font-semibold text-muted-foreground/70 uppercase tracking-wider">Client Details</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="clientName" className="text-xs font-semibold">Name</Label>
|
||||||
|
<Input id="clientName" value={formClientName} onChange={(e) => setFormClientName(e.target.value)} placeholder="Client full name" className="h-10" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="clientEmail" className="text-xs font-semibold">Email</Label>
|
||||||
|
<Input id="clientEmail" type="email" value={formClientEmail} onChange={(e) => setFormClientEmail(e.target.value)} placeholder="client@example.com" className="h-10" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="clientPhone" className="text-xs font-semibold">Phone</Label>
|
||||||
|
<Input id="clientPhone" type="tel" value={formClientPhone} onChange={(e) => setFormClientPhone(e.target.value)} placeholder="+1 555-123-4567" className="h-10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="type" className="text-xs font-semibold">Type</Label>
|
<Label htmlFor="type" className="text-xs font-semibold">Type</Label>
|
||||||
@@ -621,19 +689,16 @@ export default function CalendarPage() {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="meeting">Meeting</SelectItem>
|
<SelectItem value="website_creation">Website Creation</SelectItem>
|
||||||
<SelectItem value="call">Call</SelectItem>
|
<SelectItem value="call">Call</SelectItem>
|
||||||
<SelectItem value="chat">Video Chat</SelectItem>
|
|
||||||
<SelectItem value="follow_up">Follow Up</SelectItem>
|
<SelectItem value="follow_up">Follow Up</SelectItem>
|
||||||
<SelectItem value="demo">Demo</SelectItem>
|
|
||||||
<SelectItem value="other">Other</SelectItem>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="duration" className="text-xs font-semibold">Duration</Label>
|
<Label htmlFor="duration" className="text-xs font-semibold">Duration</Label>
|
||||||
<Select value={formDuration} onValueChange={setFormDuration}>
|
<Select value={formDuration} onValueChange={setFormDuration}>
|
||||||
<SelectTrigger id="duration" className="h-10">
|
<SelectTrigger id="duration" className={cn("h-10", formType === "website_creation" && "opacity-40 pointer-events-none")}>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -664,8 +729,40 @@ export default function CalendarPage() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="description" className="text-xs font-semibold">Description</Label>
|
<Label htmlFor="developer" className="text-xs font-semibold">Assign Developer</Label>
|
||||||
<Textarea id="description" value={formDescription} onChange={(e) => setFormDescription(e.target.value)} placeholder="Add any notes or agenda items..." rows={3} className="resize-none" />
|
<Select value={formDeveloperId || "__none__"} onValueChange={(v) => setFormDeveloperId(v === "__none__" ? "" : v)}>
|
||||||
|
<SelectTrigger id="developer" className="h-10">
|
||||||
|
<SelectValue placeholder="Select a developer…" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">None</SelectItem>
|
||||||
|
{users.map((u) => (
|
||||||
|
<SelectItem key={u.id} value={u.id}>
|
||||||
|
{u.name} {u.role ? `(${u.role})` : ""}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="lead" className="text-xs font-semibold">Client</Label>
|
||||||
|
<Select value={formLeadId || "__none__"} onValueChange={(v) => setFormLeadId(v === "__none__" ? "" : v)}>
|
||||||
|
<SelectTrigger id="lead" className="h-10">
|
||||||
|
<SelectValue placeholder="Select a client lead…" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">None</SelectItem>
|
||||||
|
{leads.map((l) => (
|
||||||
|
<SelectItem key={l.id} value={l.id}>
|
||||||
|
{l.contactName}{l.companyName ? ` — ${l.companyName}` : ""}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="description" className="text-xs font-semibold">Project Details</Label>
|
||||||
|
<Textarea id="description" value={formDescription} onChange={(e) => setFormDescription(e.target.value)} placeholder="Describe the project, requirements, and any relevant details..." rows={3} className="resize-none" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="flex items-center justify-between border-t pt-4">
|
<DialogFooter className="flex items-center justify-between border-t pt-4">
|
||||||
@@ -690,7 +787,7 @@ export default function CalendarPage() {
|
|||||||
{/* Event Detail Dialog */}
|
{/* Event Detail Dialog */}
|
||||||
<Dialog open={!!detailEvent} onOpenChange={(open) => { if (!open) setDetailEvent(null) }}>
|
<Dialog open={!!detailEvent} onOpenChange={(open) => { if (!open) setDetailEvent(null) }}>
|
||||||
{detailEvent && (
|
{detailEvent && (
|
||||||
<DialogContent className="sm:max-w-[460px]">
|
<DialogContent className="sm:max-w-[460px] max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-3 rounded-xl border shadow-sm" style={eventBgStyle(detailEvent.eventType)}>
|
<div className="p-3 rounded-xl border shadow-sm" style={eventBgStyle(detailEvent.eventType)}>
|
||||||
@@ -711,6 +808,7 @@ export default function CalendarPage() {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Date / Time */}
|
{/* Date / Time */}
|
||||||
|
{detailEvent.startTime ? (
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
||||||
<div className="flex items-center gap-2 text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-wider mb-1.5">
|
<div className="flex items-center gap-2 text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-wider mb-1.5">
|
||||||
@@ -727,6 +825,12 @@ export default function CalendarPage() {
|
|||||||
<p className="text-sm font-bold">{formatTime(detailEvent.startTime)}</p>
|
<p className="text-sm font-bold">{formatTime(detailEvent.startTime)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
||||||
|
<p className="text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-wider mb-1.5">Schedule</p>
|
||||||
|
<p className="text-sm text-muted-foreground/50 italic">No scheduled time — website creation project</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Duration + Type */}
|
{/* Duration + Type */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
@@ -751,11 +855,26 @@ export default function CalendarPage() {
|
|||||||
{/* Description */}
|
{/* Description */}
|
||||||
{detailEvent.description && (
|
{detailEvent.description && (
|
||||||
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
||||||
<p className="text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-wider mb-1.5">Description</p>
|
<p className="text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-wider mb-1.5">Project Details</p>
|
||||||
<p className="text-sm whitespace-pre-wrap leading-relaxed">{detailEvent.description}</p>
|
<p className="text-sm whitespace-pre-wrap leading-relaxed">{detailEvent.description}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Client details */}
|
||||||
|
{(detailEvent.clientName || detailEvent.clientEmail || detailEvent.clientPhone) && (
|
||||||
|
<div className="p-3.5 rounded-xl bg-gradient-to-br from-violet-500/5 to-violet-500/[0.02] border shadow-sm">
|
||||||
|
<p className="text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-wider mb-1.5 flex items-center gap-1.5">
|
||||||
|
<Users className="h-3 w-3" />
|
||||||
|
Client Details
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{detailEvent.clientName && <p className="text-sm font-bold">{detailEvent.clientName}</p>}
|
||||||
|
{detailEvent.clientEmail && <p className="text-sm text-muted-foreground/70">{detailEvent.clientEmail}</p>}
|
||||||
|
{detailEvent.clientPhone && <p className="text-sm text-muted-foreground/70">{detailEvent.clientPhone}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Linked lead */}
|
{/* Linked lead */}
|
||||||
{detailEvent.lead && (
|
{detailEvent.lead && (
|
||||||
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
||||||
@@ -802,6 +921,32 @@ export default function CalendarPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Developer */}
|
||||||
|
<div className="p-3.5 rounded-xl bg-gradient-to-br from-amber-500/5 to-amber-500/[0.02] border shadow-sm">
|
||||||
|
<p className="text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-wider mb-1.5 flex items-center gap-1.5">
|
||||||
|
<Users className="h-3 w-3" />
|
||||||
|
{detailEvent.developerId === user?.id ? "You (Developer)" : "Developer"}
|
||||||
|
</p>
|
||||||
|
{detailEvent.developer ? (
|
||||||
|
<p className="text-sm font-bold flex items-center gap-2">
|
||||||
|
<span className="h-6 w-6 rounded-full bg-amber-500/10 text-amber-600 dark:text-amber-400 text-[10px] flex items-center justify-center font-bold ring-1 ring-amber-500/20">
|
||||||
|
{detailEvent.developer.name.charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
<span className="truncate">{detailEvent.developer.name}</span>
|
||||||
|
{detailEvent.developer.role && (
|
||||||
|
<span className="text-[10px] font-medium text-muted-foreground/50 capitalize shrink-0">({detailEvent.developer.role})</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground/50 italic">No developer assigned</p>
|
||||||
|
)}
|
||||||
|
{detailEvent.developerId === user?.id && detailEvent.userId !== user?.id && (
|
||||||
|
<p className="text-[11px] text-muted-foreground/60 mt-1.5 font-medium">
|
||||||
|
Assigned by {detailEvent.creator?.name || "Unknown"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Participant Notes (editable by both parties) */}
|
{/* Participant Notes (editable by both parties) */}
|
||||||
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
@@ -836,9 +981,15 @@ export default function CalendarPage() {
|
|||||||
<DialogFooter className="flex items-center gap-2 border-t pt-4">
|
<DialogFooter className="flex items-center gap-2 border-t pt-4">
|
||||||
{detailEvent.status === "scheduled" && (
|
{detailEvent.status === "scheduled" && (
|
||||||
<>
|
<>
|
||||||
|
{detailEvent.developerId === user?.id ? (
|
||||||
|
<Button variant="default" size="default" onClick={() => updateEventStatus(detailEvent, "completed")} className="shadow-sm bg-emerald-600 hover:bg-emerald-700">
|
||||||
|
<CheckCircle2 className="h-4 w-4 mr-1.5" /> Mark Done
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
<Button variant="default" size="sm" onClick={() => updateEventStatus(detailEvent, "completed")} className="shadow-sm">
|
<Button variant="default" size="sm" onClick={() => updateEventStatus(detailEvent, "completed")} className="shadow-sm">
|
||||||
<CheckCircle2 className="h-4 w-4 mr-1.5" /> Complete
|
<CheckCircle2 className="h-4 w-4 mr-1.5" /> Complete
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
<Button variant="outline" size="sm" onClick={() => updateEventStatus(detailEvent, "cancelled")}>
|
<Button variant="outline" size="sm" onClick={() => updateEventStatus(detailEvent, "cancelled")}>
|
||||||
<XCircle className="h-4 w-4 mr-1.5" /> Cancel
|
<XCircle className="h-4 w-4 mr-1.5" /> Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
export type EventType = "meeting" | "call" | "chat" | "follow_up" | "demo" | "other"
|
export type EventType = "call" | "follow_up" | "website_creation"
|
||||||
export type EventStatus = "scheduled" | "completed" | "cancelled" | "rescheduled"
|
export type EventStatus = "scheduled" | "completed" | "cancelled" | "rescheduled"
|
||||||
|
|
||||||
export interface CalendarEvent {
|
export interface CalendarEvent {
|
||||||
id: string
|
id: string
|
||||||
userId: string
|
userId: string
|
||||||
participantId: string | null
|
participantId: string | null
|
||||||
|
developerId: string | null
|
||||||
leadId: string | null
|
leadId: string | null
|
||||||
conversationId: string | null
|
conversationId: string | null
|
||||||
title: string
|
title: string
|
||||||
@@ -17,6 +18,10 @@ export interface CalendarEvent {
|
|||||||
status: EventStatus
|
status: EventStatus
|
||||||
creator: { id: string; name: string; email?: string; role?: string; avatar?: string } | null
|
creator: { id: string; name: string; email?: string; role?: string; avatar?: string } | null
|
||||||
participant: { id: string; name: string; email?: string; role?: string; avatar?: string } | null
|
participant: { id: string; name: string; email?: string; role?: string; avatar?: string } | null
|
||||||
|
developer: { id: string; name: string; email?: string; role?: string; avatar?: string } | null
|
||||||
lead: { id: string; companyName: string; contactName: string } | null
|
lead: { id: string; companyName: string; contactName: string } | null
|
||||||
|
clientName: string | null
|
||||||
|
clientEmail: string | null
|
||||||
|
clientPhone: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE scheduled_events
|
||||||
|
ADD COLUMN developer_id UUID REFERENCES users(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE INDEX idx_scheduled_events_developer ON scheduled_events(developer_id);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TABLE scheduled_events
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_event_type,
|
||||||
|
ADD CONSTRAINT chk_event_type CHECK (event_type IN ('call', 'follow_up', 'website_creation'));
|
||||||
|
|
||||||
|
ALTER TABLE scheduled_events
|
||||||
|
ADD COLUMN IF NOT EXISTS client_name VARCHAR(255),
|
||||||
|
ADD COLUMN IF NOT EXISTS client_email VARCHAR(255),
|
||||||
|
ADD COLUMN IF NOT EXISTS client_phone VARCHAR(50);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE scheduled_events
|
||||||
|
ALTER COLUMN start_time DROP NOT NULL;
|
||||||
@@ -66,5 +66,14 @@ BEGIN;
|
|||||||
|
|
||||||
\echo '=== Running 016_participant_notes.sql (Participant Notes Column) ==='
|
\echo '=== Running 016_participant_notes.sql (Participant Notes Column) ==='
|
||||||
\i 016_participant_notes.sql
|
\i 016_participant_notes.sql
|
||||||
|
|
||||||
|
\echo '=== Running 017_calendar_developer.sql (Calendar Developer Assignment) ==='
|
||||||
|
\i 017_calendar_developer.sql
|
||||||
|
|
||||||
|
\echo '=== Running 018_website_creation_type.sql (Website Creation Event Type) ==='
|
||||||
|
\i 018_website_creation_type.sql
|
||||||
|
|
||||||
|
\echo '=== Running 019_allow_null_start_time.sql (Allow Null Start Time) ==='
|
||||||
|
\i 019_allow_null_start_time.sql
|
||||||
\echo '=== Migration Complete ==='
|
\echo '=== Migration Complete ==='
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|||||||
+33
-3
@@ -1,12 +1,42 @@
|
|||||||
|
// ── Dependency Check ─────────────────────────────────────────────────
|
||||||
|
// Verifies all packages listed in package.json are installed.
|
||||||
|
// Auto-runs npm install if any are missing — zero manual setup steps.
|
||||||
|
|
||||||
|
import { readFileSync } from "node:fs"
|
||||||
|
import { resolve, dirname } from "node:path"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
import { createRequire } from "node:module"
|
||||||
|
import { execSync } from "node:child_process"
|
||||||
|
import { platform } from "node:os"
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const root = resolve(__dirname, "..")
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"))
|
||||||
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
|
||||||
|
const missing = Object.keys(allDeps).filter(name => {
|
||||||
|
try { require.resolve(name, { paths: [root] }); return false } catch { return true }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
console.log(`\n${missing.length} package(s) missing: ${missing.join(", ")}`)
|
||||||
|
console.log("Installing...\n")
|
||||||
|
execSync("npm install --no-audit --no-fund", { cwd: root, stdio: "inherit", timeout: 120000 })
|
||||||
|
console.log("Dependencies installed\n")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If package.json read or resolution fails, skip silently
|
||||||
|
}
|
||||||
|
|
||||||
// ── Port Precheck ──────────────────────────────────────────────────
|
// ── Port Precheck ──────────────────────────────────────────────────
|
||||||
// Kills any existing processes on ports 3001, 3006, 3007, 3008.
|
// Kills any existing processes on ports 3001, 3006, 3007, 3008.
|
||||||
// These are the AI server, Next.js frontend, Signaling server, and
|
// These are the AI server, Next.js frontend, Signaling server, and
|
||||||
// Python scraper respectively.
|
// Python scraper respectively.
|
||||||
// Runs before anything else starts to avoid EADDRINUSE errors.
|
// Runs before anything else starts to avoid EADDRINUSE errors.
|
||||||
|
|
||||||
import { execSync } from "node:child_process"
|
|
||||||
import { platform } from "node:os"
|
|
||||||
|
|
||||||
const PORTS = [3001, 3006, 3007, 3008]
|
const PORTS = [3001, 3006, 3007, 3008]
|
||||||
|
|
||||||
if (platform() === "win32") {
|
if (platform() === "win32") {
|
||||||
|
|||||||
@@ -21,17 +21,14 @@ import { useNotifications } from "@/providers/notification-provider"
|
|||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import {
|
import {
|
||||||
ChevronLeft, ChevronRight, Plus, Calendar, Clock, Phone,
|
ChevronLeft, ChevronRight, Plus, Calendar, Clock, Phone,
|
||||||
Video, Users, CheckCircle2, XCircle, RotateCcw, Loader2,
|
Users, CheckCircle2, XCircle, RotateCcw, Loader2,
|
||||||
ArrowUpRight, Zap, StickyNote,
|
ArrowUpRight, Zap, StickyNote, Globe,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
const EVENT_ICONS: Record<EventType, React.ElementType> = {
|
const EVENT_ICONS: Record<EventType, React.ElementType> = {
|
||||||
meeting: Users,
|
|
||||||
call: Phone,
|
call: Phone,
|
||||||
chat: Video,
|
|
||||||
follow_up: Clock,
|
follow_up: Clock,
|
||||||
demo: Calendar,
|
website_creation: Globe,
|
||||||
other: Calendar,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventVars(type: EventType) {
|
function eventVars(type: EventType) {
|
||||||
@@ -85,14 +82,20 @@ export default function CalendarPage() {
|
|||||||
const [editingEvent, setEditingEvent] = useState<CalendarEvent | null>(null)
|
const [editingEvent, setEditingEvent] = useState<CalendarEvent | null>(null)
|
||||||
const [detailEvent, setDetailEvent] = useState<CalendarEvent | null>(null)
|
const [detailEvent, setDetailEvent] = useState<CalendarEvent | null>(null)
|
||||||
|
|
||||||
const [users, setUsers] = useState<{ id: string; name: string; email: string }[]>([])
|
const [users, setUsers] = useState<{ id: string; name: string; email: string; role: string }[]>([])
|
||||||
|
const [leads, setLeads] = useState<{ id: string; contactName: string; companyName: string }[]>([])
|
||||||
const [formTitle, setFormTitle] = useState("")
|
const [formTitle, setFormTitle] = useState("")
|
||||||
const [formDescription, setFormDescription] = useState("")
|
const [formDescription, setFormDescription] = useState("")
|
||||||
const [formType, setFormType] = useState<EventType>("meeting")
|
const [formType, setFormType] = useState<EventType>("website_creation")
|
||||||
const [formDate, setFormDate] = useState("")
|
const [formDate, setFormDate] = useState("")
|
||||||
const [formTime, setFormTime] = useState("")
|
const [formTime, setFormTime] = useState("")
|
||||||
const [formDuration, setFormDuration] = useState("30")
|
const [formDuration, setFormDuration] = useState("30")
|
||||||
const [formParticipantId, setFormParticipantId] = useState("")
|
const [formParticipantId, setFormParticipantId] = useState("")
|
||||||
|
const [formDeveloperId, setFormDeveloperId] = useState("")
|
||||||
|
const [formLeadId, setFormLeadId] = useState("")
|
||||||
|
const [formClientName, setFormClientName] = useState("")
|
||||||
|
const [formClientEmail, setFormClientEmail] = useState("")
|
||||||
|
const [formClientPhone, setFormClientPhone] = useState("")
|
||||||
const [formSaving, setFormSaving] = useState(false)
|
const [formSaving, setFormSaving] = useState(false)
|
||||||
const [editNotes, setEditNotes] = useState(false)
|
const [editNotes, setEditNotes] = useState(false)
|
||||||
const [notesText, setNotesText] = useState("")
|
const [notesText, setNotesText] = useState("")
|
||||||
@@ -124,6 +127,10 @@ export default function CalendarPage() {
|
|||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((data) => setUsers(data.users || []))
|
.then((data) => setUsers(data.users || []))
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
|
fetch("/api/leads?limit=200")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data) => setLeads((Array.isArray(data) ? data : data?.leads || []).map((l: any) => ({ id: l.id, contactName: l.contactName, companyName: l.companyName }))))
|
||||||
|
.catch(() => {})
|
||||||
}, [user, router])
|
}, [user, router])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -158,7 +165,7 @@ export default function CalendarPage() {
|
|||||||
|
|
||||||
const getEventsForDay = useCallback((day: number) => {
|
const getEventsForDay = useCallback((day: number) => {
|
||||||
const dateStr = `${currentYear}-${String(currentMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`
|
const dateStr = `${currentYear}-${String(currentMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`
|
||||||
return events.filter((e) => e.startTime.startsWith(dateStr))
|
return events.filter((e) => e.startTime && e.startTime.startsWith(dateStr))
|
||||||
}, [events, currentYear, currentMonth])
|
}, [events, currentYear, currentMonth])
|
||||||
|
|
||||||
const isToday = (day: number) => {
|
const isToday = (day: number) => {
|
||||||
@@ -169,9 +176,14 @@ export default function CalendarPage() {
|
|||||||
setEditingEvent(null)
|
setEditingEvent(null)
|
||||||
setFormTitle("")
|
setFormTitle("")
|
||||||
setFormDescription("")
|
setFormDescription("")
|
||||||
setFormType("meeting")
|
setFormType("website_creation")
|
||||||
setFormDuration("30")
|
setFormDuration("30")
|
||||||
setFormParticipantId("")
|
setFormParticipantId("")
|
||||||
|
setFormDeveloperId("")
|
||||||
|
setFormLeadId("")
|
||||||
|
setFormClientName("")
|
||||||
|
setFormClientEmail("")
|
||||||
|
setFormClientPhone("")
|
||||||
const d = day ? new Date(currentYear, currentMonth, day) : new Date()
|
const d = day ? new Date(currentYear, currentMonth, day) : new Date()
|
||||||
setFormDate(d.toISOString().split("T")[0])
|
setFormDate(d.toISOString().split("T")[0])
|
||||||
setFormTime("10:00")
|
setFormTime("10:00")
|
||||||
@@ -184,37 +196,58 @@ export default function CalendarPage() {
|
|||||||
setFormDescription(event.description || "")
|
setFormDescription(event.description || "")
|
||||||
setFormType(event.eventType)
|
setFormType(event.eventType)
|
||||||
setFormDuration(String(event.durationMinutes || 30))
|
setFormDuration(String(event.durationMinutes || 30))
|
||||||
setFormDate(new Date(event.startTime).toISOString().split("T")[0])
|
setFormDate(event.startTime ? new Date(event.startTime).toISOString().split("T")[0] : "")
|
||||||
setFormTime(new Date(event.startTime).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }))
|
setFormTime(event.startTime ? new Date(event.startTime).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }) : "10:00")
|
||||||
setFormParticipantId(event.participantId || "")
|
setFormParticipantId(event.participantId || "")
|
||||||
|
setFormDeveloperId(event.developerId || "")
|
||||||
|
setFormLeadId(event.leadId || "")
|
||||||
|
setFormClientName(event.clientName || "")
|
||||||
|
setFormClientEmail(event.clientEmail || "")
|
||||||
|
setFormClientPhone(event.clientPhone || "")
|
||||||
setDialogOpen(true)
|
setDialogOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!formTitle.trim() || !formDate || !formTime) {
|
if (!formTitle.trim()) {
|
||||||
toast.error("Title, date, and time are required")
|
toast.error("Title is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (formType !== "website_creation" && (!formDate || !formTime)) {
|
||||||
|
toast.error("Date and time are required for this event type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setFormSaving(true)
|
setFormSaving(true)
|
||||||
try {
|
try {
|
||||||
const startTime = new Date(`${formDate}T${formTime}:00`).toISOString()
|
const body: Record<string, unknown> = {
|
||||||
|
title: formTitle.trim(),
|
||||||
|
description: formDescription.trim() || null,
|
||||||
|
eventType: formType,
|
||||||
|
}
|
||||||
|
|
||||||
|
let startTime: string | undefined
|
||||||
|
if (formType !== "website_creation") {
|
||||||
|
startTime = new Date(`${formDate}T${formTime}:00`).toISOString()
|
||||||
let endTime = null
|
let endTime = null
|
||||||
if (formDuration) {
|
if (formDuration) {
|
||||||
const end = new Date(startTime)
|
const end = new Date(startTime)
|
||||||
end.setMinutes(end.getMinutes() + parseInt(formDuration))
|
end.setMinutes(end.getMinutes() + parseInt(formDuration))
|
||||||
endTime = end.toISOString()
|
endTime = end.toISOString()
|
||||||
}
|
}
|
||||||
|
body.startTime = startTime
|
||||||
const body: Record<string, unknown> = {
|
body.endTime = endTime
|
||||||
title: formTitle.trim(),
|
body.durationMinutes = formDuration ? parseInt(formDuration) : null
|
||||||
description: formDescription.trim() || null,
|
} else if (formDate) {
|
||||||
eventType: formType,
|
startTime = new Date(`${formDate}T00:00:00`).toISOString()
|
||||||
startTime,
|
body.startTime = startTime
|
||||||
endTime,
|
|
||||||
durationMinutes: formDuration ? parseInt(formDuration) : null,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (formClientName) body.clientName = formClientName
|
||||||
|
if (formClientEmail) body.clientEmail = formClientEmail
|
||||||
|
if (formClientPhone) body.clientPhone = formClientPhone
|
||||||
if (formParticipantId) body.participantId = formParticipantId
|
if (formParticipantId) body.participantId = formParticipantId
|
||||||
|
if (formDeveloperId) body.developerId = formDeveloperId
|
||||||
|
if (formLeadId) body.leadId = formLeadId
|
||||||
|
|
||||||
let res
|
let res
|
||||||
if (editingEvent) {
|
if (editingEvent) {
|
||||||
@@ -235,11 +268,13 @@ export default function CalendarPage() {
|
|||||||
|
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (editingEvent) {
|
if (editingEvent) {
|
||||||
setEvents((prev) => prev.map((e) => e.id === editingEvent.id ? { ...data.event, participant: e.participant, lead: e.lead } : e))
|
setEvents((prev) => prev.map((e) => e.id === editingEvent.id ? { ...data.event, participant: e.participant, developer: e.developer, lead: e.lead } : e))
|
||||||
toast.success("Event updated")
|
toast.success("Event updated")
|
||||||
} else {
|
} else {
|
||||||
setEvents((prev) => [...prev, data.event])
|
setEvents((prev) => [...prev, data.event])
|
||||||
|
if (startTime) {
|
||||||
addNotification("event_scheduled", `Event created: ${formTitle}`, `Scheduled for ${formatDate(startTime)} at ${formatTime(startTime)}`, "/calendar")
|
addNotification("event_scheduled", `Event created: ${formTitle}`, `Scheduled for ${formatDate(startTime)} at ${formatTime(startTime)}`, "/calendar")
|
||||||
|
}
|
||||||
toast.success("Event created")
|
toast.success("Event created")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +295,7 @@ export default function CalendarPage() {
|
|||||||
})
|
})
|
||||||
if (!res.ok) throw new Error("Failed to update")
|
if (!res.ok) throw new Error("Failed to update")
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setEvents((prev) => prev.map((e) => e.id === event.id ? { ...data.event, participant: e.participant, lead: e.lead } : e))
|
setEvents((prev) => prev.map((e) => e.id === event.id ? { ...data.event, participant: e.participant, developer: e.developer, lead: e.lead } : e))
|
||||||
toast.success(`Event ${newStatus}`)
|
toast.success(`Event ${newStatus}`)
|
||||||
setDetailEvent(null)
|
setDetailEvent(null)
|
||||||
} catch {
|
} catch {
|
||||||
@@ -279,7 +314,7 @@ export default function CalendarPage() {
|
|||||||
})
|
})
|
||||||
if (!res.ok) throw new Error("Failed to save")
|
if (!res.ok) throw new Error("Failed to save")
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setEvents((prev) => prev.map((e) => e.id === event.id ? { ...data.event, participant: e.participant, lead: e.lead } : e))
|
setEvents((prev) => prev.map((e) => e.id === event.id ? { ...data.event, participant: e.participant, developer: e.developer, lead: e.lead } : e))
|
||||||
if (detailEvent && detailEvent.id === event.id) {
|
if (detailEvent && detailEvent.id === event.id) {
|
||||||
setDetailEvent({ ...detailEvent, participantNotes: notesText || null })
|
setDetailEvent({ ...detailEvent, participantNotes: notesText || null })
|
||||||
}
|
}
|
||||||
@@ -579,7 +614,7 @@ export default function CalendarPage() {
|
|||||||
|
|
||||||
{/* Create/Edit Dialog */}
|
{/* Create/Edit Dialog */}
|
||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<DialogContent className="sm:max-w-[520px]">
|
<DialogContent className="sm:max-w-[520px] max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
@@ -608,11 +643,28 @@ export default function CalendarPage() {
|
|||||||
<Label htmlFor="date" className="text-xs font-semibold">Date</Label>
|
<Label htmlFor="date" className="text-xs font-semibold">Date</Label>
|
||||||
<Input id="date" type="date" value={formDate} onChange={(e) => setFormDate(e.target.value)} className="h-10" />
|
<Input id="date" type="date" value={formDate} onChange={(e) => setFormDate(e.target.value)} className="h-10" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className={cn("space-y-2", formType === "website_creation" && "opacity-40 pointer-events-none")}>
|
||||||
<Label htmlFor="time" className="text-xs font-semibold">Time</Label>
|
<Label htmlFor="time" className="text-xs font-semibold">Time</Label>
|
||||||
<Input id="time" type="time" value={formTime} onChange={(e) => setFormTime(e.target.value)} className="h-10" />
|
<Input id="time" type="time" value={formTime} onChange={(e) => setFormTime(e.target.value)} className="h-10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{formType === "website_creation" && (
|
||||||
|
<div className="space-y-4 rounded-xl border bg-muted/20 p-4">
|
||||||
|
<p className="text-xs font-semibold text-muted-foreground/70 uppercase tracking-wider">Client Details</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="clientName" className="text-xs font-semibold">Name</Label>
|
||||||
|
<Input id="clientName" value={formClientName} onChange={(e) => setFormClientName(e.target.value)} placeholder="Client full name" className="h-10" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="clientEmail" className="text-xs font-semibold">Email</Label>
|
||||||
|
<Input id="clientEmail" type="email" value={formClientEmail} onChange={(e) => setFormClientEmail(e.target.value)} placeholder="client@example.com" className="h-10" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="clientPhone" className="text-xs font-semibold">Phone</Label>
|
||||||
|
<Input id="clientPhone" type="tel" value={formClientPhone} onChange={(e) => setFormClientPhone(e.target.value)} placeholder="+1 555-123-4567" className="h-10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="type" className="text-xs font-semibold">Type</Label>
|
<Label htmlFor="type" className="text-xs font-semibold">Type</Label>
|
||||||
@@ -621,19 +673,16 @@ export default function CalendarPage() {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="meeting">Meeting</SelectItem>
|
<SelectItem value="website_creation">Website Creation</SelectItem>
|
||||||
<SelectItem value="call">Call</SelectItem>
|
<SelectItem value="call">Call</SelectItem>
|
||||||
<SelectItem value="chat">Video Chat</SelectItem>
|
|
||||||
<SelectItem value="follow_up">Follow Up</SelectItem>
|
<SelectItem value="follow_up">Follow Up</SelectItem>
|
||||||
<SelectItem value="demo">Demo</SelectItem>
|
|
||||||
<SelectItem value="other">Other</SelectItem>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="duration" className="text-xs font-semibold">Duration</Label>
|
<Label htmlFor="duration" className="text-xs font-semibold">Duration</Label>
|
||||||
<Select value={formDuration} onValueChange={setFormDuration}>
|
<Select value={formDuration} onValueChange={setFormDuration}>
|
||||||
<SelectTrigger id="duration" className="h-10">
|
<SelectTrigger id="duration" className={cn("h-10", formType === "website_creation" && "opacity-40 pointer-events-none")}>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -663,6 +712,38 @@ export default function CalendarPage() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="developer" className="text-xs font-semibold">Assign Developer</Label>
|
||||||
|
<Select value={formDeveloperId || "__none__"} onValueChange={(v) => setFormDeveloperId(v === "__none__" ? "" : v)}>
|
||||||
|
<SelectTrigger id="developer" className="h-10">
|
||||||
|
<SelectValue placeholder="Select a developer…" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">None</SelectItem>
|
||||||
|
{users.map((u) => (
|
||||||
|
<SelectItem key={u.id} value={u.id}>
|
||||||
|
{u.name} {u.role ? `(${u.role})` : ""}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="lead" className="text-xs font-semibold">Client (Lead)</Label>
|
||||||
|
<Select value={formLeadId || "__none__"} onValueChange={(v) => setFormLeadId(v === "__none__" ? "" : v)}>
|
||||||
|
<SelectTrigger id="lead" className="h-10">
|
||||||
|
<SelectValue placeholder="Select a client lead…" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">None</SelectItem>
|
||||||
|
{leads.map((l) => (
|
||||||
|
<SelectItem key={l.id} value={l.id}>
|
||||||
|
{l.contactName}{l.companyName ? ` — ${l.companyName}` : ""}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="description" className="text-xs font-semibold">Description</Label>
|
<Label htmlFor="description" className="text-xs font-semibold">Description</Label>
|
||||||
<Textarea id="description" value={formDescription} onChange={(e) => setFormDescription(e.target.value)} placeholder="Add any notes or agenda items..." rows={3} className="resize-none" />
|
<Textarea id="description" value={formDescription} onChange={(e) => setFormDescription(e.target.value)} placeholder="Add any notes or agenda items..." rows={3} className="resize-none" />
|
||||||
@@ -690,7 +771,7 @@ export default function CalendarPage() {
|
|||||||
{/* Event Detail Dialog */}
|
{/* Event Detail Dialog */}
|
||||||
<Dialog open={!!detailEvent} onOpenChange={(open) => { if (!open) setDetailEvent(null) }}>
|
<Dialog open={!!detailEvent} onOpenChange={(open) => { if (!open) setDetailEvent(null) }}>
|
||||||
{detailEvent && (
|
{detailEvent && (
|
||||||
<DialogContent className="sm:max-w-[460px]">
|
<DialogContent className="sm:max-w-[460px] max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-3 rounded-xl border shadow-sm" style={eventBgStyle(detailEvent.eventType)}>
|
<div className="p-3 rounded-xl border shadow-sm" style={eventBgStyle(detailEvent.eventType)}>
|
||||||
@@ -711,6 +792,7 @@ export default function CalendarPage() {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Date / Time */}
|
{/* Date / Time */}
|
||||||
|
{detailEvent.startTime ? (
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
||||||
<div className="flex items-center gap-2 text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-wider mb-1.5">
|
<div className="flex items-center gap-2 text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-wider mb-1.5">
|
||||||
@@ -727,6 +809,12 @@ export default function CalendarPage() {
|
|||||||
<p className="text-sm font-bold">{formatTime(detailEvent.startTime)}</p>
|
<p className="text-sm font-bold">{formatTime(detailEvent.startTime)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
||||||
|
<p className="text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-wider mb-1.5">Schedule</p>
|
||||||
|
<p className="text-sm text-muted-foreground/50 italic">No scheduled time — website creation project</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Duration + Type */}
|
{/* Duration + Type */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
@@ -751,11 +839,26 @@ export default function CalendarPage() {
|
|||||||
{/* Description */}
|
{/* Description */}
|
||||||
{detailEvent.description && (
|
{detailEvent.description && (
|
||||||
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
||||||
<p className="text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-wider mb-1.5">Description</p>
|
<p className="text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-wider mb-1.5">Project Details</p>
|
||||||
<p className="text-sm whitespace-pre-wrap leading-relaxed">{detailEvent.description}</p>
|
<p className="text-sm whitespace-pre-wrap leading-relaxed">{detailEvent.description}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Client details */}
|
||||||
|
{(detailEvent.clientName || detailEvent.clientEmail || detailEvent.clientPhone) && (
|
||||||
|
<div className="p-3.5 rounded-xl bg-gradient-to-br from-violet-500/5 to-violet-500/[0.02] border shadow-sm">
|
||||||
|
<p className="text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-wider mb-1.5 flex items-center gap-1.5">
|
||||||
|
<Users className="h-3 w-3" />
|
||||||
|
Client Details
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{detailEvent.clientName && <p className="text-sm font-bold">{detailEvent.clientName}</p>}
|
||||||
|
{detailEvent.clientEmail && <p className="text-sm text-muted-foreground/70">{detailEvent.clientEmail}</p>}
|
||||||
|
{detailEvent.clientPhone && <p className="text-sm text-muted-foreground/70">{detailEvent.clientPhone}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Linked lead */}
|
{/* Linked lead */}
|
||||||
{detailEvent.lead && (
|
{detailEvent.lead && (
|
||||||
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
||||||
@@ -802,6 +905,32 @@ export default function CalendarPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Developer */}
|
||||||
|
<div className="p-3.5 rounded-xl bg-gradient-to-br from-amber-500/5 to-amber-500/[0.02] border shadow-sm">
|
||||||
|
<p className="text-[10px] font-semibold text-muted-foreground/50 uppercase tracking-wider mb-1.5 flex items-center gap-1.5">
|
||||||
|
<Users className="h-3 w-3" />
|
||||||
|
{detailEvent.developerId === user?.id ? "You (Developer)" : "Developer"}
|
||||||
|
</p>
|
||||||
|
{detailEvent.developer ? (
|
||||||
|
<p className="text-sm font-bold flex items-center gap-2">
|
||||||
|
<span className="h-6 w-6 rounded-full bg-amber-500/10 text-amber-600 dark:text-amber-400 text-[10px] flex items-center justify-center font-bold ring-1 ring-amber-500/20">
|
||||||
|
{detailEvent.developer.name.charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
<span className="truncate">{detailEvent.developer.name}</span>
|
||||||
|
{detailEvent.developer.role && (
|
||||||
|
<span className="text-[10px] font-medium text-muted-foreground/50 capitalize shrink-0">({detailEvent.developer.role})</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground/50 italic">No developer assigned</p>
|
||||||
|
)}
|
||||||
|
{detailEvent.developerId === user?.id && detailEvent.userId !== user?.id && (
|
||||||
|
<p className="text-[11px] text-muted-foreground/60 mt-1.5 font-medium">
|
||||||
|
Assigned by {detailEvent.creator?.name || "Unknown"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Participant Notes (editable by both parties) */}
|
{/* Participant Notes (editable by both parties) */}
|
||||||
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
<div className="p-3.5 rounded-xl bg-gradient-to-br from-muted/50 to-muted/20 border shadow-sm">
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
@@ -836,9 +965,15 @@ export default function CalendarPage() {
|
|||||||
<DialogFooter className="flex items-center gap-2 border-t pt-4">
|
<DialogFooter className="flex items-center gap-2 border-t pt-4">
|
||||||
{detailEvent.status === "scheduled" && (
|
{detailEvent.status === "scheduled" && (
|
||||||
<>
|
<>
|
||||||
|
{detailEvent.developerId === user?.id ? (
|
||||||
|
<Button variant="default" size="default" onClick={() => updateEventStatus(detailEvent, "completed")} className="shadow-sm bg-emerald-600 hover:bg-emerald-700">
|
||||||
|
<CheckCircle2 className="h-4 w-4 mr-1.5" /> Mark Done
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
<Button variant="default" size="sm" onClick={() => updateEventStatus(detailEvent, "completed")} className="shadow-sm">
|
<Button variant="default" size="sm" onClick={() => updateEventStatus(detailEvent, "completed")} className="shadow-sm">
|
||||||
<CheckCircle2 className="h-4 w-4 mr-1.5" /> Complete
|
<CheckCircle2 className="h-4 w-4 mr-1.5" /> Complete
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
<Button variant="outline" size="sm" onClick={() => updateEventStatus(detailEvent, "cancelled")}>
|
<Button variant="outline" size="sm" onClick={() => updateEventStatus(detailEvent, "cancelled")}>
|
||||||
<XCircle className="h-4 w-4 mr-1.5" /> Cancel
|
<XCircle className="h-4 w-4 mr-1.5" /> Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ export async function GET() {
|
|||||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
|
||||||
const result = await query(
|
const result = await query(
|
||||||
`SELECT u.id, u.first_name, u.last_name, u.email
|
`SELECT u.id, u.first_name, u.last_name, u.email,
|
||||||
|
ur.role_id, r.name AS role_name, r.display_name AS role_display
|
||||||
FROM users u
|
FROM users u
|
||||||
|
LEFT JOIN user_roles ur ON ur.user_id = u.id
|
||||||
|
LEFT JOIN roles r ON r.id = ur.role_id
|
||||||
WHERE u.deleted_at IS NULL AND u.id != $1
|
WHERE u.deleted_at IS NULL AND u.id != $1
|
||||||
ORDER BY u.first_name ASC`,
|
ORDER BY u.first_name ASC`,
|
||||||
[user.id],
|
[user.id],
|
||||||
@@ -19,6 +22,7 @@ export async function GET() {
|
|||||||
id: r.id,
|
id: r.id,
|
||||||
name: `${r.first_name} ${r.last_name}`,
|
name: `${r.first_name} ${r.last_name}`,
|
||||||
email: r.email,
|
email: r.email,
|
||||||
|
role: r.role_display || r.role_name || "",
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return NextResponse.json({ users })
|
return NextResponse.json({ users })
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
const { title, description, eventType, startTime, endTime, durationMinutes, status, participantId, participantNotes } = await request.json()
|
const { title, description, eventType, startTime, endTime, durationMinutes, status, participantId, developerId, participantNotes, clientName, clientEmail, clientPhone } = await request.json()
|
||||||
|
|
||||||
const existing = await query(
|
const existing = await query(
|
||||||
`SELECT e.user_id, e.participant_id, e.title, e.description, e.participant_notes, e.event_type,
|
`SELECT e.user_id, e.participant_id, e.developer_id, e.lead_id, e.title, e.description, e.participant_notes, e.event_type,
|
||||||
e.start_time, e.end_time, e.duration_minutes, e.status,
|
e.start_time, e.end_time, e.duration_minutes, e.status, e.client_name, e.client_email, e.client_phone,
|
||||||
u.email AS u_email, u.first_name AS u_first, u.last_name AS u_last,
|
u.email AS u_email, u.first_name AS u_first, u.last_name AS u_last,
|
||||||
p.email AS p_email, p.first_name AS p_first, p.last_name AS p_last
|
p.email AS p_email, p.first_name AS p_first, p.last_name AS p_last
|
||||||
FROM scheduled_events e
|
FROM scheduled_events e
|
||||||
@@ -31,8 +31,9 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
const old = existing.rows[0]
|
const old = existing.rows[0]
|
||||||
const isCreator = old.user_id === user.id
|
const isCreator = old.user_id === user.id
|
||||||
const isParticipant = old.participant_id === user.id
|
const isParticipant = old.participant_id === user.id
|
||||||
|
const isDeveloper = old.developer_id === user.id
|
||||||
|
|
||||||
if (!isCreator && !isParticipant) {
|
if (!isCreator && !isParticipant && !isDeveloper) {
|
||||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +43,8 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
(startTime !== undefined && startTime !== old.start_time) ||
|
(startTime !== undefined && startTime !== old.start_time) ||
|
||||||
(endTime !== undefined && endTime !== old.end_time) ||
|
(endTime !== undefined && endTime !== old.end_time) ||
|
||||||
(durationMinutes !== undefined && durationMinutes !== old.duration_minutes) ||
|
(durationMinutes !== undefined && durationMinutes !== old.duration_minutes) ||
|
||||||
(participantId !== undefined && participantId !== old.participant_id)
|
(participantId !== undefined && participantId !== old.participant_id) ||
|
||||||
|
(developerId !== undefined && developerId !== old.developer_id)
|
||||||
)) {
|
)) {
|
||||||
return NextResponse.json({ error: "Only the creator can edit event details" }, { status: 403 })
|
return NextResponse.json({ error: "Only the creator can edit event details" }, { status: 403 })
|
||||||
}
|
}
|
||||||
@@ -53,14 +55,18 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
description = COALESCE($2, description),
|
description = COALESCE($2, description),
|
||||||
participant_notes = COALESCE($3, participant_notes),
|
participant_notes = COALESCE($3, participant_notes),
|
||||||
event_type = COALESCE($4, event_type),
|
event_type = COALESCE($4, event_type),
|
||||||
start_time = COALESCE($5, start_time),
|
start_time = CASE WHEN $4::varchar = 'website_creation' THEN NULL ELSE COALESCE($5, start_time) END,
|
||||||
end_time = COALESCE($6, end_time),
|
end_time = CASE WHEN $4::varchar = 'website_creation' THEN NULL ELSE COALESCE($6, end_time) END,
|
||||||
duration_minutes = COALESCE($7, duration_minutes),
|
duration_minutes = CASE WHEN $4::varchar = 'website_creation' THEN NULL ELSE COALESCE($7, duration_minutes) END,
|
||||||
status = COALESCE($8, status),
|
status = COALESCE($8, status),
|
||||||
participant_id = COALESCE($9, participant_id),
|
participant_id = COALESCE($9, participant_id),
|
||||||
|
developer_id = COALESCE($10, developer_id),
|
||||||
|
client_name = COALESCE($11, client_name),
|
||||||
|
client_email = COALESCE($12, client_email),
|
||||||
|
client_phone = COALESCE($13, client_phone),
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $10
|
WHERE id = $14
|
||||||
RETURNING id, user_id, participant_id, lead_id, conversation_id, title, description, participant_notes, event_type, start_time, end_time, duration_minutes, status, created_at`,
|
RETURNING id, user_id, participant_id, developer_id, lead_id, conversation_id, title, description, participant_notes, event_type, start_time, end_time, duration_minutes, client_name, client_email, client_phone, status, created_at`,
|
||||||
[
|
[
|
||||||
title || null,
|
title || null,
|
||||||
description ?? null,
|
description ?? null,
|
||||||
@@ -71,6 +77,10 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
durationMinutes ?? null,
|
durationMinutes ?? null,
|
||||||
status || null,
|
status || null,
|
||||||
participantId ?? null,
|
participantId ?? null,
|
||||||
|
developerId ?? null,
|
||||||
|
clientName ?? null,
|
||||||
|
clientEmail ?? null,
|
||||||
|
clientPhone ?? null,
|
||||||
id,
|
id,
|
||||||
],
|
],
|
||||||
user.id,
|
user.id,
|
||||||
@@ -78,6 +88,17 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
|
|
||||||
const r = result.rows[0]
|
const r = result.rows[0]
|
||||||
|
|
||||||
|
// Auto-close lead when developer marks event as completed
|
||||||
|
if (status === "completed" && r.lead_id) {
|
||||||
|
const stageResult = await query("SELECT id FROM lead_stages WHERE name = 'Closed Won'")
|
||||||
|
if (stageResult.rows.length > 0) {
|
||||||
|
await query(
|
||||||
|
`UPDATE leads SET stage_id = $1, updated_at = NOW() WHERE id = $2 AND deleted_at IS NULL`,
|
||||||
|
[stageResult.rows[0].id, r.lead_id],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isChanged = r.start_time !== old.start_time || r.end_time !== old.end_time ||
|
const isChanged = r.start_time !== old.start_time || r.end_time !== old.end_time ||
|
||||||
r.title !== old.title || r.event_type !== old.event_type ||
|
r.title !== old.title || r.event_type !== old.event_type ||
|
||||||
r.status !== old.status
|
r.status !== old.status
|
||||||
@@ -108,12 +129,14 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
)
|
)
|
||||||
const creatorInfo = creatorResult.rows[0]
|
const creatorInfo = creatorResult.rows[0]
|
||||||
const participantInfo = old.participant_id ? { id: old.participant_id, name: `${old.p_first} ${old.p_last}` || old.participant_id, email: old.p_email || null } : null
|
const participantInfo = old.participant_id ? { id: old.participant_id, name: `${old.p_first} ${old.p_last}` || old.participant_id, email: old.p_email || null } : null
|
||||||
|
const developerInfo = old.developer_id ? { id: old.developer_id, name: "", email: "" } : null
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
event: {
|
event: {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
userId: r.user_id,
|
userId: r.user_id,
|
||||||
participantId: r.participant_id,
|
participantId: r.participant_id,
|
||||||
|
developerId: r.developer_id,
|
||||||
leadId: r.lead_id,
|
leadId: r.lead_id,
|
||||||
conversationId: r.conversation_id,
|
conversationId: r.conversation_id,
|
||||||
title: r.title,
|
title: r.title,
|
||||||
@@ -126,7 +149,11 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
|||||||
status: r.status,
|
status: r.status,
|
||||||
creator: creatorInfo ? { id: creatorInfo.id, name: `${creatorInfo.first_name} ${creatorInfo.last_name}`, email: creatorInfo.email, role: creatorInfo.role_display || creatorInfo.role_name, avatar: creatorInfo.avatar_url } : { id: old.user_id, name: "Unknown" },
|
creator: creatorInfo ? { id: creatorInfo.id, name: `${creatorInfo.first_name} ${creatorInfo.last_name}`, email: creatorInfo.email, role: creatorInfo.role_display || creatorInfo.role_name, avatar: creatorInfo.avatar_url } : { id: old.user_id, name: "Unknown" },
|
||||||
participant: participantInfo,
|
participant: participantInfo,
|
||||||
|
developer: developerInfo,
|
||||||
lead: null,
|
lead: null,
|
||||||
|
clientName: r.client_name || null,
|
||||||
|
clientEmail: r.client_email || null,
|
||||||
|
clientPhone: r.client_phone || null,
|
||||||
createdAt: r.created_at,
|
createdAt: r.created_at,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
+73
-15
@@ -13,27 +13,33 @@ export async function GET(request: NextRequest) {
|
|||||||
const end = searchParams.get("end")
|
const end = searchParams.get("end")
|
||||||
const status = searchParams.get("status")
|
const status = searchParams.get("status")
|
||||||
|
|
||||||
let sql = `SELECT e.id, e.user_id, e.participant_id, e.lead_id, e.conversation_id,
|
let sql = `SELECT e.id, e.user_id, e.participant_id, e.developer_id, e.lead_id, e.conversation_id,
|
||||||
e.title, e.description, e.participant_notes, e.event_type,
|
e.title, e.description, e.participant_notes, e.event_type,
|
||||||
e.start_time, e.end_time, e.duration_minutes, e.status, e.created_at,
|
e.start_time, e.end_time, e.duration_minutes, e.status, e.created_at,
|
||||||
|
e.client_name, e.client_email, e.client_phone,
|
||||||
u.id AS u_id, u.first_name AS u_first, u.last_name AS u_last, u.email AS u_email, u.avatar_url AS u_avatar,
|
u.id AS u_id, u.first_name AS u_first, u.last_name AS u_last, u.email AS u_email, u.avatar_url AS u_avatar,
|
||||||
ur.role_id AS u_role_id, r.name AS u_role_name, r.display_name AS u_role_display,
|
ur.role_id AS u_role_id, r.name AS u_role_name, r.display_name AS u_role_display,
|
||||||
p.id AS p_id, p.first_name AS p_first, p.last_name AS p_last, p.email AS p_email, p.avatar_url AS p_avatar,
|
p.id AS p_id, p.first_name AS p_first, p.last_name AS p_last, p.email AS p_email, p.avatar_url AS p_avatar,
|
||||||
pr.role_id AS p_role_id, pr2.name AS p_role_name, pr2.display_name AS p_role_display,
|
pr.role_id AS p_role_id, pr2.name AS p_role_name, pr2.display_name AS p_role_display,
|
||||||
|
d.id AS d_id, d.first_name AS d_first, d.last_name AS d_last, d.email AS d_email, d.avatar_url AS d_avatar,
|
||||||
|
dr.role_id AS d_role_id, dr2.name AS d_role_name, dr2.display_name AS d_role_display,
|
||||||
l.id AS l_id, l.company_name, l.contact_name
|
l.id AS l_id, l.company_name, l.contact_name
|
||||||
FROM scheduled_events e
|
FROM scheduled_events e
|
||||||
JOIN users u ON u.id = e.user_id
|
JOIN users u ON u.id = e.user_id
|
||||||
LEFT JOIN users p ON p.id = e.participant_id
|
LEFT JOIN users p ON p.id = e.participant_id
|
||||||
|
LEFT JOIN users d ON d.id = e.developer_id
|
||||||
LEFT JOIN leads l ON l.id = e.lead_id
|
LEFT JOIN leads l ON l.id = e.lead_id
|
||||||
LEFT JOIN user_roles ur ON ur.user_id = e.user_id
|
LEFT JOIN user_roles ur ON ur.user_id = e.user_id
|
||||||
LEFT JOIN roles r ON r.id = ur.role_id
|
LEFT JOIN roles r ON r.id = ur.role_id
|
||||||
LEFT JOIN user_roles pr ON pr.user_id = e.participant_id
|
LEFT JOIN user_roles pr ON pr.user_id = e.participant_id
|
||||||
LEFT JOIN roles pr2 ON pr2.id = pr.role_id`
|
LEFT JOIN roles pr2 ON pr2.id = pr.role_id
|
||||||
|
LEFT JOIN user_roles dr ON dr.user_id = e.developer_id
|
||||||
|
LEFT JOIN roles dr2 ON dr2.id = dr.role_id`
|
||||||
const params: unknown[] = []
|
const params: unknown[] = []
|
||||||
let idx = 1
|
let idx = 1
|
||||||
|
|
||||||
if (user.role !== "super_admin") {
|
if (user.role !== "super_admin") {
|
||||||
sql += ` WHERE e.user_id = $${idx} OR e.participant_id = $${idx}`
|
sql += ` WHERE e.user_id = $${idx} OR e.participant_id = $${idx} OR e.developer_id = $${idx}`
|
||||||
params.push(user.id)
|
params.push(user.id)
|
||||||
idx++
|
idx++
|
||||||
} else {
|
} else {
|
||||||
@@ -41,13 +47,13 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (start) {
|
if (start) {
|
||||||
sql += ` AND e.start_time >= $${idx}`
|
sql += ` AND (e.start_time IS NULL OR e.start_time >= $${idx})`
|
||||||
params.push(start)
|
params.push(start)
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (end) {
|
if (end) {
|
||||||
sql += ` AND e.start_time <= $${idx}`
|
sql += ` AND (e.start_time IS NULL OR e.start_time <= $${idx})`
|
||||||
params.push(end)
|
params.push(end)
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
@@ -66,6 +72,7 @@ export async function GET(request: NextRequest) {
|
|||||||
id: r.id,
|
id: r.id,
|
||||||
userId: r.user_id,
|
userId: r.user_id,
|
||||||
participantId: r.participant_id,
|
participantId: r.participant_id,
|
||||||
|
developerId: r.developer_id,
|
||||||
leadId: r.lead_id,
|
leadId: r.lead_id,
|
||||||
conversationId: r.conversation_id,
|
conversationId: r.conversation_id,
|
||||||
title: r.title,
|
title: r.title,
|
||||||
@@ -78,7 +85,11 @@ export async function GET(request: NextRequest) {
|
|||||||
status: r.status,
|
status: r.status,
|
||||||
creator: { id: r.u_id, name: `${r.u_first} ${r.u_last}`, email: r.u_email, role: r.u_role_display || r.u_role_name, avatar: r.u_avatar },
|
creator: { id: r.u_id, name: `${r.u_first} ${r.u_last}`, email: r.u_email, role: r.u_role_display || r.u_role_name, avatar: r.u_avatar },
|
||||||
participant: r.p_id ? { id: r.p_id, name: `${r.p_first} ${r.p_last}`, email: r.p_email, role: r.p_role_display || r.p_role_name, avatar: r.p_avatar } : null,
|
participant: r.p_id ? { id: r.p_id, name: `${r.p_first} ${r.p_last}`, email: r.p_email, role: r.p_role_display || r.p_role_name, avatar: r.p_avatar } : null,
|
||||||
|
developer: r.d_id ? { id: r.d_id, name: `${r.d_first} ${r.d_last}`, email: r.d_email, role: r.d_role_display || r.d_role_name, avatar: r.d_avatar } : null,
|
||||||
lead: r.l_id ? { id: r.l_id, companyName: r.company_name, contactName: r.contact_name } : null,
|
lead: r.l_id ? { id: r.l_id, companyName: r.company_name, contactName: r.contact_name } : null,
|
||||||
|
clientName: r.client_name || null,
|
||||||
|
clientEmail: r.client_email || null,
|
||||||
|
clientPhone: r.client_phone || null,
|
||||||
createdAt: r.created_at,
|
createdAt: r.created_at,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -95,36 +106,55 @@ export async function POST(request: NextRequest) {
|
|||||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
|
||||||
const {
|
const {
|
||||||
participantId, leadId, conversationId,
|
participantId, developerId, leadId, conversationId,
|
||||||
title, description, eventType,
|
title, description, eventType,
|
||||||
startTime, endTime, durationMinutes,
|
startTime, endTime, durationMinutes,
|
||||||
|
clientName, clientEmail, clientPhone,
|
||||||
} = await request.json()
|
} = await request.json()
|
||||||
|
|
||||||
if (!title || !startTime) {
|
if (!title) {
|
||||||
return NextResponse.json({ error: "Title and start time are required" }, { status: 400 })
|
return NextResponse.json({ error: "Title is required" }, { status: 400 })
|
||||||
|
}
|
||||||
|
if (eventType !== "website_creation" && !startTime) {
|
||||||
|
return NextResponse.json({ error: "Start time is required for this event type" }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await query(
|
const result = await query(
|
||||||
`INSERT INTO scheduled_events (user_id, participant_id, lead_id, conversation_id, title, description, event_type, start_time, end_time, duration_minutes)
|
`INSERT INTO scheduled_events (user_id, participant_id, developer_id, lead_id, conversation_id, title, description, event_type, start_time, end_time, duration_minutes, client_name, client_email, client_phone)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||||
RETURNING id, user_id, participant_id, lead_id, conversation_id, title, description, participant_notes, event_type, start_time, end_time, duration_minutes, status, created_at`,
|
RETURNING id, user_id, participant_id, developer_id, lead_id, conversation_id, title, description, participant_notes, event_type, start_time, end_time, duration_minutes, client_name, client_email, client_phone, status, created_at`,
|
||||||
[
|
[
|
||||||
user.id,
|
user.id,
|
||||||
participantId || null,
|
participantId || null,
|
||||||
|
developerId || null,
|
||||||
leadId || null,
|
leadId || null,
|
||||||
conversationId || null,
|
conversationId || null,
|
||||||
title,
|
title,
|
||||||
description || null,
|
description || null,
|
||||||
eventType || "meeting",
|
eventType || "website_creation",
|
||||||
startTime,
|
startTime || null,
|
||||||
endTime || null,
|
eventType === "website_creation" ? null : (endTime || null),
|
||||||
durationMinutes || null,
|
eventType === "website_creation" ? null : (durationMinutes || null),
|
||||||
|
clientName || null,
|
||||||
|
clientEmail || null,
|
||||||
|
clientPhone || null,
|
||||||
],
|
],
|
||||||
user.id,
|
user.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
const r = result.rows[0]
|
const r = result.rows[0]
|
||||||
|
|
||||||
|
// Auto-update lead to "pending" when a website creation event is created
|
||||||
|
if (r.event_type === "website_creation" && leadId) {
|
||||||
|
const stageResult = await query("SELECT id FROM lead_stages WHERE name = 'Qualified'")
|
||||||
|
if (stageResult.rows.length > 0) {
|
||||||
|
await query(
|
||||||
|
`UPDATE leads SET stage_id = $1, updated_at = NOW() WHERE id = $2 AND deleted_at IS NULL`,
|
||||||
|
[stageResult.rows[0].id, leadId],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (participantId && participantId !== user.id) {
|
if (participantId && participantId !== user.id) {
|
||||||
await query(
|
await query(
|
||||||
`INSERT INTO notifications (user_id, type, title, description, link, context_id, context_type)
|
`INSERT INTO notifications (user_id, type, title, description, link, context_id, context_type)
|
||||||
@@ -141,12 +171,35 @@ export async function POST(request: NextRequest) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify developer
|
||||||
|
if (developerId && developerId !== user.id) {
|
||||||
|
await query(
|
||||||
|
`INSERT INTO notifications (user_id, type, title, description, link, context_id, context_type)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||||
|
[
|
||||||
|
developerId,
|
||||||
|
"event_scheduled",
|
||||||
|
`Project assigned: ${title}`,
|
||||||
|
`${user.firstName} ${user.lastName} assigned a project to you`,
|
||||||
|
`/calendar`,
|
||||||
|
r.id,
|
||||||
|
"scheduled_event",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const participantResult = participantId ? await query(
|
const participantResult = participantId ? await query(
|
||||||
`SELECT email, first_name, last_name FROM users WHERE id = $1`,
|
`SELECT email, first_name, last_name FROM users WHERE id = $1`,
|
||||||
[participantId],
|
[participantId],
|
||||||
) : null
|
) : null
|
||||||
const participant = participantResult?.rows[0] || null
|
const participant = participantResult?.rows[0] || null
|
||||||
|
|
||||||
|
const developerResult = developerId ? await query(
|
||||||
|
`SELECT id, first_name, last_name, email FROM users WHERE id = $1`,
|
||||||
|
[developerId],
|
||||||
|
) : null
|
||||||
|
const dev = developerResult?.rows[0] || null
|
||||||
|
|
||||||
sendEventConfirmation({
|
sendEventConfirmation({
|
||||||
creatorName: `${user.firstName} ${user.lastName}`,
|
creatorName: `${user.firstName} ${user.lastName}`,
|
||||||
creatorEmail: user.email,
|
creatorEmail: user.email,
|
||||||
@@ -165,6 +218,7 @@ export async function POST(request: NextRequest) {
|
|||||||
id: r.id,
|
id: r.id,
|
||||||
userId: r.user_id,
|
userId: r.user_id,
|
||||||
participantId: r.participant_id,
|
participantId: r.participant_id,
|
||||||
|
developerId: r.developer_id,
|
||||||
leadId: r.lead_id,
|
leadId: r.lead_id,
|
||||||
conversationId: r.conversation_id,
|
conversationId: r.conversation_id,
|
||||||
title: r.title,
|
title: r.title,
|
||||||
@@ -177,7 +231,11 @@ export async function POST(request: NextRequest) {
|
|||||||
status: r.status,
|
status: r.status,
|
||||||
creator: { id: user.id, name: `${user.firstName} ${user.lastName}`, email: user.email, role: user.role },
|
creator: { id: user.id, name: `${user.firstName} ${user.lastName}`, email: user.email, role: user.role },
|
||||||
participant: participantId ? { id: participantId, name: participant ? `${participant.first_name} ${participant.last_name}` : participantId, email: participant?.email || null } : null,
|
participant: participantId ? { id: participantId, name: participant ? `${participant.first_name} ${participant.last_name}` : participantId, email: participant?.email || null } : null,
|
||||||
|
developer: dev ? { id: dev.id, name: `${dev.first_name} ${dev.last_name}`, email: dev.email } : null,
|
||||||
lead: leadId ? { id: leadId, companyName: "", contactName: "" } : null,
|
lead: leadId ? { id: leadId, companyName: "", contactName: "" } : null,
|
||||||
|
clientName: r.client_name || null,
|
||||||
|
clientEmail: r.client_email || null,
|
||||||
|
clientPhone: r.client_phone || null,
|
||||||
createdAt: r.created_at,
|
createdAt: r.created_at,
|
||||||
},
|
},
|
||||||
}, { status: 201 })
|
}, { status: 201 })
|
||||||
|
|||||||
+1
-4
@@ -105,12 +105,9 @@
|
|||||||
--sidebar-ring: 0 76.3% 48%;
|
--sidebar-ring: 0 76.3% 48%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--theme-primary: hsl(var(--primary));
|
--theme-primary: hsl(var(--primary));
|
||||||
--event-meeting: 217 91% 55%;
|
|
||||||
--event-call: 160 84% 39%;
|
--event-call: 160 84% 39%;
|
||||||
--event-chat: 271 81% 53%;
|
|
||||||
--event-follow_up: 38 92% 48%;
|
--event-follow_up: 38 92% 48%;
|
||||||
--event-demo: 346 77% 48%;
|
--event-website_creation: 263 70% 50%;
|
||||||
--event-other: 215 20% 55%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
|||||||
+6
-1
@@ -110,13 +110,14 @@ export interface Conversation {
|
|||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventType = "meeting" | "call" | "chat" | "follow_up" | "demo" | "other";
|
export type EventType = "call" | "follow_up" | "website_creation";
|
||||||
export type EventStatus = "scheduled" | "completed" | "cancelled" | "rescheduled";
|
export type EventStatus = "scheduled" | "completed" | "cancelled" | "rescheduled";
|
||||||
|
|
||||||
export interface CalendarEvent {
|
export interface CalendarEvent {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
participantId: string | null;
|
participantId: string | null;
|
||||||
|
developerId: string | null;
|
||||||
leadId: string | null;
|
leadId: string | null;
|
||||||
conversationId: string | null;
|
conversationId: string | null;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -129,6 +130,10 @@ export interface CalendarEvent {
|
|||||||
status: EventStatus;
|
status: EventStatus;
|
||||||
creator: { id: string; name: string; email?: string; role?: string; avatar?: string } | null;
|
creator: { id: string; name: string; email?: string; role?: string; avatar?: string } | null;
|
||||||
participant: { id: string; name: string; email?: string; role?: string; avatar?: string } | null;
|
participant: { id: string; name: string; email?: string; role?: string; avatar?: string } | null;
|
||||||
|
developer: { id: string; name: string; email?: string; role?: string; avatar?: string } | null;
|
||||||
lead: { id: string; companyName: string; contactName: string } | null;
|
lead: { id: string; companyName: string; contactName: string } | null;
|
||||||
|
clientName: string | null;
|
||||||
|
clientEmail: string | null;
|
||||||
|
clientPhone: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user