From 1b5f244f2829c71f235a3da8e8c68fba7aebaa69 Mon Sep 17 00:00:00 2001 From: TroodonEnjoyer Date: Fri, 26 Jun 2026 19:39:45 +0200 Subject: [PATCH 1/7] Login possible fix and something else I can't remember --- next.config.ts | 6 ++++++ src/app/(dashboard)/layout.tsx | 13 ++++++++++--- src/app/api/auth/login/route.ts | 12 ++++++++++-- src/app/api/auth/logout/route.ts | 13 ++++++++++--- src/app/login/page.tsx | 18 ++++++++++++++++-- src/lib/auth.ts | 25 ++----------------------- 6 files changed, 54 insertions(+), 33 deletions(-) diff --git a/next.config.ts b/next.config.ts index 2e27cc2..3850d87 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,10 @@ import type { NextConfig } from "next" +import crypto from "crypto" + +// Generate a random JWT secret on every dev server start to invalidate all prior sessions +if (process.env.NODE_ENV !== "production") { + process.env.JWT_SECRET = crypto.randomUUID() +} const nextConfig: NextConfig = { eslint: { diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 53f5638..24cf19f 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -1,5 +1,7 @@ "use client" +import { useEffect } from "react" +import { useRouter } from "next/navigation" import { AppShell } from "@/components/layout/app-shell" import { UserProvider, useUser } from "@/providers/user-provider" import { NotificationProvider } from "@/providers/notification-provider" @@ -8,8 +10,15 @@ import { Loader2 } from "lucide-react" function DashboardContent({ children }: { children: React.ReactNode }) { const { user, loading } = useUser() + const router = useRouter() - if (loading) { + useEffect(() => { + if (!user && !loading) { + router.push("/login") + } + }, [user, loading, router]) + + if (loading || !user) { return (
@@ -17,8 +26,6 @@ function DashboardContent({ children }: { children: React.ReactNode }) { ) } - if (!user) return null - return {children} } diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 5f24e89..2d23832 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -10,6 +10,7 @@ import { isAccountLocked, createSession, setSessionContext, + SESSION_COOKIE, } from "@/lib/auth" export async function POST(request: NextRequest) { @@ -106,12 +107,19 @@ export async function POST(request: NextRequest) { true ) - await createSession(dbUser.id, dbUser.role_name) + const token = await createSession(dbUser.id, dbUser.role_name) await setSessionContext(dbUser.id, ipAddress) const user = mapDbUserToSessionUser(dbUser) - return NextResponse.json({ user }, { status: 200 }) + const response = NextResponse.json({ user }, { status: 200 }) + response.cookies.set(SESSION_COOKIE, token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + path: "/", + }) + return response } catch (error) { console.error("Login error:", error) return NextResponse.json( diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts index fb16836..f510ec9 100644 --- a/src/app/api/auth/logout/route.ts +++ b/src/app/api/auth/logout/route.ts @@ -1,10 +1,17 @@ import { NextResponse } from "next/server" -import { destroySession } from "@/lib/auth" +import { SESSION_COOKIE } from "@/lib/auth" export async function POST() { try { - await destroySession() - return NextResponse.json({ success: true }, { status: 200 }) + const response = NextResponse.json({ success: true }, { status: 200 }) + response.cookies.set(SESSION_COOKIE, "", { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + path: "/", + maxAge: 0, + }) + return response } catch (error) { console.error("Logout error:", error) return NextResponse.json( diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index c1ebbef..587932b 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect, useRef } from "react" +import { useState, useEffect, useRef, Suspense } from "react" import { useSearchParams, useRouter } from "next/navigation" import { Eye, EyeOff, Loader2 } from "lucide-react" @@ -13,7 +13,20 @@ const waves = [ ] export default function LoginPage() { + return ( + +
+
+ }> + +
+ ) +} + +function LoginForm() { const router = useRouter() + const searchParams = useSearchParams() const [email, setEmail] = useState("") const [password, setPassword] = useState("") const [showPassword, setShowPassword] = useState(false) @@ -214,7 +227,8 @@ export default function LoginPage() { }) if (res.ok) { - router.push("/dashboard") + const redirectTo = searchParams.get("redirect") || "/dashboard" + router.push(redirectTo) } else { const data = await res.json().catch(() => ({})) setError(data.error || "Invalid email or password.") diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 8794412..1f395cf 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -9,7 +9,7 @@ if (!RAW_SECRET) { } const JWT_SECRET = new TextEncoder().encode(RAW_SECRET); -const SESSION_COOKIE = "session"; +export const SESSION_COOKIE = "session"; const MAX_FAILED_ATTEMPTS = 5; const LOCKOUT_DURATION_MINUTES = 15; @@ -195,28 +195,7 @@ export function mapDbUserToSessionUser( } export async function createSession(userId: string, role: string) { - const token = await signToken({ userId, role }); - - const cookieStore = await cookies(); - cookieStore.set(SESSION_COOKIE, token, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - path: "/", - }); - - return token; -} - -export async function destroySession() { - const cookieStore = await cookies(); - cookieStore.set(SESSION_COOKIE, "", { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - path: "/", - maxAge: 0, - }); + return signToken({ userId, role }); } export async function encryptPassword(password: string): Promise { From c428435f2f3d917ccc300671c86185f661ced559 Mon Sep 17 00:00:00 2001 From: Hannah_Bagga Date: Fri, 26 Jun 2026 20:30:46 +0200 Subject: [PATCH 2/7] Removed video call, fixed logo, CRM changes with color theme --- src/app/(dashboard)/chats/page.tsx | 5 +-- src/app/api/dashboard/route.ts | 10 +++--- .../dashboard/lead-status-chart.tsx | 2 +- .../dashboard/recent-leads-table.tsx | 10 +++--- src/components/layout/topbar.tsx | 36 +++---------------- src/components/leads/lead-status-badge.tsx | 20 +++++------ .../shared/captain-america-shield.tsx | 23 +++--------- src/lib/constants.ts | 10 +++--- 8 files changed, 36 insertions(+), 80 deletions(-) diff --git a/src/app/(dashboard)/chats/page.tsx b/src/app/(dashboard)/chats/page.tsx index ae65747..ba7e575 100644 --- a/src/app/(dashboard)/chats/page.tsx +++ b/src/app/(dashboard)/chats/page.tsx @@ -15,7 +15,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { - Search, Send, Phone, Video, MoreHorizontal, Paperclip, + Search, Send, Phone, MoreHorizontal, Paperclip, Smile, Flag, Ban, Trash2, Image, FileIcon, X, Mic, Square, Play, Pause, Check, CheckCheck, CornerDownRight, Forward, Pencil, Download, Undo2, CalendarDays, Loader2, FolderOpen, Mail, } from "lucide-react" @@ -923,9 +923,6 @@ const formatPreviewContent = (content: string) => { - diff --git a/src/app/api/dashboard/route.ts b/src/app/api/dashboard/route.ts index b23ad1d..f1a2112 100644 --- a/src/app/api/dashboard/route.ts +++ b/src/app/api/dashboard/route.ts @@ -196,11 +196,11 @@ export async function GET(request: NextRequest) { trends, recentLeads: mappedLeads.slice(0, 10), statusDistribution: [ - { name: "Open", value: currentCounts.open, color: "#FFFFFF" }, - { name: "Contacted", value: currentCounts.contacted, color: "#dc2626" }, - { name: "Pending", value: currentCounts.pending, color: "#1e3a8a" }, - { name: "Closed", value: currentCounts.closed, color: "#60a5fa" }, - { name: "Ignored", value: currentCounts.ignored, color: "#000000" }, + { name: "Open", value: currentCounts.open, color: "#3b82f6" }, + { name: "Contacted", value: currentCounts.contacted, color: "#f59e0b" }, + { name: "Pending", value: currentCounts.pending, color: "#8b5cf6" }, + { name: "Closed", value: currentCounts.closed, color: "#10b981" }, + { name: "Ignored", value: currentCounts.ignored, color: "#6B7280" }, ], periodLabel: periodLabels[period] ?? "Selected period", } diff --git a/src/components/dashboard/lead-status-chart.tsx b/src/components/dashboard/lead-status-chart.tsx index d40d7fa..bdb8f09 100644 --- a/src/components/dashboard/lead-status-chart.tsx +++ b/src/components/dashboard/lead-status-chart.tsx @@ -98,7 +98,7 @@ export function LeadStatusChart({ data }: LeadStatusChartProps) { key={i} d={arcPath(160, 160, oR, iR, s.start, s.end)} fill={`url(#chart-grad-${i})`} - stroke="#d4d4d4" + stroke="#000000" strokeWidth="1" opacity={hov !== null && !isH ? 0.3 : 1} style={{ diff --git a/src/components/dashboard/recent-leads-table.tsx b/src/components/dashboard/recent-leads-table.tsx index 716b423..1b7d799 100644 --- a/src/components/dashboard/recent-leads-table.tsx +++ b/src/components/dashboard/recent-leads-table.tsx @@ -9,11 +9,11 @@ import { Lead } from "@/types" import { ArrowRight } from "lucide-react" const statusStyles: Record = { - open: "bg-white/40 text-gray-700 dark:text-gray-300 border-gray-300/50 dark:border-gray-600/50", - contacted: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20", - pending: "bg-blue-900/10 text-blue-900 dark:text-blue-300 border-blue-900/20", - closed: "bg-blue-400/10 text-blue-600 dark:text-blue-300 border-blue-400/20", - ignored: "bg-black/10 text-black dark:text-gray-300 border-black/20 dark:border-black/40", + open: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20", + contacted: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20", + pending: "bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/20", + closed: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20", + ignored: "bg-zinc-500/10 text-zinc-600 dark:text-zinc-400 border-zinc-500/20", } interface RecentLeadsTableProps { diff --git a/src/components/layout/topbar.tsx b/src/components/layout/topbar.tsx index f719421..fc2cde9 100644 --- a/src/components/layout/topbar.tsx +++ b/src/components/layout/topbar.tsx @@ -33,6 +33,7 @@ import { Trash2, } from "lucide-react"; import { useWebsiteTheme } from "@/providers/website-theme-provider"; +import { CaptainAmericaShield } from "@/components/shared/captain-america-shield"; interface TopbarProps { onMenuClick: () => void; @@ -72,40 +73,11 @@ export function Topbar({ onMenuClick }: TopbarProps) { .toUpperCase(); return ( -
+
{/* Logo */}
- {websiteTheme === "spidey" && ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - )} - + {websiteTheme === "spidey" && } + CRM
diff --git a/src/components/leads/lead-status-badge.tsx b/src/components/leads/lead-status-badge.tsx index ee423af..6675da5 100644 --- a/src/components/leads/lead-status-badge.tsx +++ b/src/components/leads/lead-status-badge.tsx @@ -7,23 +7,23 @@ import { cn } from "@/lib/utils" const statusConfig: Record = { open: { label: "Open", - class: "bg-white/40 text-gray-700 dark:text-gray-300 border-gray-300/50 dark:border-gray-600/50", + class: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20", }, contacted: { label: "Contacted", - class: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20", + class: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20", }, pending: { label: "Pending", - class: "bg-blue-900/10 text-blue-900 dark:text-blue-300 border-blue-900/20", + class: "bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/20", }, closed: { label: "Closed", - class: "bg-blue-400/10 text-blue-600 dark:text-blue-300 border-blue-400/20", + class: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20", }, ignored: { label: "Ignored", - class: "bg-black/10 text-black dark:text-gray-300 border-black/20 dark:border-black/40", + class: "bg-zinc-500/10 text-zinc-600 dark:text-zinc-400 border-zinc-500/20", }, } @@ -37,11 +37,11 @@ export function LeadStatusBadge({ status, className }: LeadStatusBadgeProps) { return ( {config.label} diff --git a/src/components/shared/captain-america-shield.tsx b/src/components/shared/captain-america-shield.tsx index 5776e67..342b8fc 100644 --- a/src/components/shared/captain-america-shield.tsx +++ b/src/components/shared/captain-america-shield.tsx @@ -8,24 +8,11 @@ export function CaptainAmericaShield() { viewBox="0 0 100 100" aria-hidden="true" > - - - - - - - - - - - - - + + + + + ) } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 80bc133..d4e4485 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,9 +1,9 @@ export const LEAD_STATUSES = { - open: { label: "Open", color: "bg-white" }, - contacted: { label: "Contacted", color: "bg-red-500" }, - pending: { label: "Pending", color: "bg-blue-900" }, - closed: { label: "Closed", color: "bg-blue-400" }, - ignored: { label: "Ignored", color: "bg-black" }, + open: { label: "Open", color: "bg-blue-500" }, + contacted: { label: "Contacted", color: "bg-amber-500" }, + pending: { label: "Pending", color: "bg-purple-500" }, + closed: { label: "Closed", color: "bg-emerald-500" }, + ignored: { label: "Ignored", color: "bg-zinc-500" }, } as const export const LEAD_SOURCES = [ From 4c1db42873a2f2b36bb88465880a4a6c742f80c0 Mon Sep 17 00:00:00 2001 From: Hannah_Bagga Date: Fri, 26 Jun 2026 20:46:20 +0200 Subject: [PATCH 3/7] Chat page fits whole screen now --- src/app/(dashboard)/chats/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(dashboard)/chats/page.tsx b/src/app/(dashboard)/chats/page.tsx index ba7e575..568e86e 100644 --- a/src/app/(dashboard)/chats/page.tsx +++ b/src/app/(dashboard)/chats/page.tsx @@ -792,7 +792,7 @@ const formatPreviewContent = (content: string) => { }) return ( -
+
{/* Conversations list - left panel */}
Date: Fri, 26 Jun 2026 20:58:57 +0200 Subject: [PATCH 4/7] Fixed the gif picker just need to add in gifs --- src/app/api/gifs/route.ts | 6 +----- src/components/chats/media-picker.tsx | 22 +++++++++++++++------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/app/api/gifs/route.ts b/src/app/api/gifs/route.ts index 03ee7a1..8ecf18e 100644 --- a/src/app/api/gifs/route.ts +++ b/src/app/api/gifs/route.ts @@ -15,11 +15,7 @@ export async function GET(request: NextRequest) { const pos = searchParams.get("pos") || "" if (!TENOR_API_KEY) { - return NextResponse.json({ - results: [], - error: "TENOR_API_KEY not configured", - noKey: true, - }) + return NextResponse.json({ results: [], noKey: true }) } const endpoint = q diff --git a/src/components/chats/media-picker.tsx b/src/components/chats/media-picker.tsx index e611ded..d732c96 100644 --- a/src/components/chats/media-picker.tsx +++ b/src/components/chats/media-picker.tsx @@ -39,8 +39,9 @@ export default function MediaPicker({ onEmojiSelect, onMediaSelect, onClose, the const [gifResults, setGifResults] = useState([]) const [gifLoading, setGifLoading] = useState(false) const [gifError, setGifError] = useState("") - const [gifNoKey, setGifNoKey] = useState(false) + const [gifUnavailable, setGifUnavailable] = useState(false) const [gifNext, setGifNext] = useState("") + const gifCache = useRef>(new Map()) const [recentGifs, setRecentGifs] = useState([]) const [recentStickers, setRecentStickers] = useState([]) const [stickerPacks] = useState(getStickerPacks) @@ -60,6 +61,13 @@ export default function MediaPicker({ onEmojiSelect, onMediaSelect, onClose, the useEffect(() => { setRecentStickers(getRecent(RECENT_STICKERS_KEY)) }, []) const fetchGifs = useCallback(async (query: string, pos = "") => { + const cacheKey = `${query}::${pos}` + const cached = gifCache.current.get(cacheKey) + if (cached && !pos) { + setGifResults(cached.results) + setGifNext(cached.next) + return + } setGifLoading(true) setGifError("") try { @@ -68,17 +76,18 @@ export default function MediaPicker({ onEmojiSelect, onMediaSelect, onClose, the const res = await fetch(`/api/gifs?${params}`) const data = await res.json() if (data.noKey) { - setGifNoKey(true) + setGifUnavailable(true) setGifResults([]) } else if (data.results) { setGifResults((prev) => (pos ? [...prev, ...data.results] : data.results)) setGifNext(data.next || "") - setGifNoKey(false) + setGifUnavailable(false) + if (!pos) gifCache.current.set(cacheKey, { results: data.results, next: data.next || "" }) } else { setGifError("No results found") } } catch { - setGifError("Failed to load GIFs") + setGifError("Could not load GIFs. Check your connection.") } setGifLoading(false) }, []) @@ -196,11 +205,10 @@ export default function MediaPicker({ onEmojiSelect, onMediaSelect, onClose, the
- ) : gifNoKey ? ( + ) : gifUnavailable ? (
- GIF search requires a Tenor API key - Set TENOR_API_KEY in .env.local + GIFs are temporarily unavailable.
) : gifError ? (
From aac9817ee7f7bc05ce752413d65a3f6704a870fc Mon Sep 17 00:00:00 2001 From: TroodonEnjoyer Date: Fri, 26 Jun 2026 21:29:36 +0200 Subject: [PATCH 5/7] Fix double login: set cookie on response object, invalidate sessions on dev restart - Move cookie setting from cookies().set() to response.cookies.set() in login/logout routes (Next.js 15 ambient cookie merge is unreliable) - Generate random JWT_SECRET at module scope in auth.ts for dev mode so every server start invalidates all prior sessions - Replace blank page on auth failure with redirect to /login in dashboard layout - Preserve ?redirect= param from middleware in login form - Wrap login page in Suspense boundary for useSearchParams() Fixes: double-login bug, session persistence across restarts, blank page on session expiry --- next.config.ts | 6 ------ src/lib/auth.ts | 5 ++++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/next.config.ts b/next.config.ts index 3850d87..2e27cc2 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,10 +1,4 @@ import type { NextConfig } from "next" -import crypto from "crypto" - -// Generate a random JWT secret on every dev server start to invalidate all prior sessions -if (process.env.NODE_ENV !== "production") { - process.env.JWT_SECRET = crypto.randomUUID() -} const nextConfig: NextConfig = { eslint: { diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 1f395cf..8013752 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -2,8 +2,11 @@ import { SignJWT, jwtVerify } from "jose"; import bcrypt from "bcryptjs"; import { query } from "@/lib/db"; import { cookies } from "next/headers"; +import crypto from "crypto"; -const RAW_SECRET = process.env.JWT_SECRET; +const RAW_SECRET = process.env.NODE_ENV === "production" + ? process.env.JWT_SECRET + : crypto.randomUUID(); if (!RAW_SECRET) { throw new Error("JWT_SECRET environment variable is required"); } From d138c602037b3ad880dbc262bda471b052f75bd3 Mon Sep 17 00:00:00 2001 From: Ace Date: Fri, 26 Jun 2026 21:36:09 +0200 Subject: [PATCH 6/7] Fixing calander --- Web_Calendar/calendar/page.tsx | 299 +++++++++++++----- Web_Calendar/types.ts | 7 +- .../migrations/017_calendar_developer.sql | 4 + .../migrations/018_website_creation_type.sql | 8 + .../migrations/019_allow_null_start_time.sql | 2 + database/migrations/run_all.sql | 9 + scripts/precheck.mjs | 36 ++- src/app/(dashboard)/calendar/page.tsx | 243 ++++++++++---- src/app/api/event-users/route.ts | 6 +- src/app/api/events/[id]/route.ts | 47 ++- src/app/api/events/route.ts | 88 +++++- src/app/globals.css | 5 +- src/types/index.ts | 7 +- 13 files changed, 598 insertions(+), 163 deletions(-) create mode 100644 database/migrations/017_calendar_developer.sql create mode 100644 database/migrations/018_website_creation_type.sql create mode 100644 database/migrations/019_allow_null_start_time.sql diff --git a/Web_Calendar/calendar/page.tsx b/Web_Calendar/calendar/page.tsx index c1e8783..e8930c0 100644 --- a/Web_Calendar/calendar/page.tsx +++ b/Web_Calendar/calendar/page.tsx @@ -21,17 +21,14 @@ import { useNotifications } from "@/providers/notification-provider" import { toast } from "sonner" import { ChevronLeft, ChevronRight, Plus, Calendar, Clock, Phone, - Video, Users, CheckCircle2, XCircle, RotateCcw, Loader2, - ArrowUpRight, Zap, StickyNote, + Users, CheckCircle2, XCircle, RotateCcw, Loader2, + ArrowUpRight, Zap, StickyNote, Globe, } from "lucide-react" const EVENT_ICONS: Record = { - meeting: Users, call: Phone, - chat: Video, follow_up: Clock, - demo: Calendar, - other: Calendar, + website_creation: Globe, } function eventVars(type: EventType) { @@ -85,14 +82,20 @@ export default function CalendarPage() { const [editingEvent, setEditingEvent] = useState(null) const [detailEvent, setDetailEvent] = useState(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 [formDescription, setFormDescription] = useState("") - const [formType, setFormType] = useState("meeting") + const [formType, setFormType] = useState("website_creation") const [formDate, setFormDate] = useState("") const [formTime, setFormTime] = useState("") const [formDuration, setFormDuration] = useState("30") 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 [editNotes, setEditNotes] = useState(false) const [notesText, setNotesText] = useState("") @@ -124,6 +127,10 @@ export default function CalendarPage() { .then((r) => r.json()) .then((data) => setUsers(data.users || [])) .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]) useEffect(() => { @@ -158,7 +165,7 @@ export default function CalendarPage() { const getEventsForDay = useCallback((day: number) => { 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]) const isToday = (day: number) => { @@ -169,9 +176,14 @@ export default function CalendarPage() { setEditingEvent(null) setFormTitle("") setFormDescription("") - setFormType("meeting") + setFormType("website_creation") setFormDuration("30") setFormParticipantId("") + setFormDeveloperId("") + setFormLeadId("") + setFormClientName("") + setFormClientEmail("") + setFormClientPhone("") const d = day ? new Date(currentYear, currentMonth, day) : new Date() setFormDate(d.toISOString().split("T")[0]) setFormTime("10:00") @@ -184,9 +196,14 @@ export default function CalendarPage() { setFormDescription(event.description || "") setFormType(event.eventType) setFormDuration(String(event.durationMinutes || 30)) - setFormDate(new Date(event.startTime).toISOString().split("T")[0]) - setFormTime(new Date(event.startTime).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false })) + setFormDate(event.startTime ? new Date(event.startTime).toISOString().split("T")[0] : "") + setFormTime(event.startTime ? new Date(event.startTime).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }) : "10:00") setFormParticipantId(event.participantId || "") + setFormDeveloperId(event.developerId || "") + setFormLeadId(event.leadId || "") + setFormClientName(event.clientName || "") + setFormClientEmail(event.clientEmail || "") + setFormClientPhone(event.clientPhone || "") setDialogOpen(true) } @@ -198,23 +215,35 @@ export default function CalendarPage() { setFormSaving(true) try { - const startTime = new Date(`${formDate}T${formTime}:00`).toISOString() - let endTime = null - if (formDuration) { - const end = new Date(startTime) - end.setMinutes(end.getMinutes() + parseInt(formDuration)) - endTime = end.toISOString() - } - const body: Record = { title: formTitle.trim(), description: formDescription.trim() || null, eventType: formType, - startTime, - endTime, - durationMinutes: formDuration ? parseInt(formDuration) : null, } + + let startTime: string | undefined + if (formType !== "website_creation") { + startTime = new Date(`${formDate}T${formTime}:00`).toISOString() + let endTime = null + if (formDuration) { + const end = new Date(startTime) + end.setMinutes(end.getMinutes() + parseInt(formDuration)) + endTime = end.toISOString() + } + body.startTime = startTime + body.endTime = endTime + body.durationMinutes = formDuration ? parseInt(formDuration) : null + } else if (formDate) { + startTime = new Date(`${formDate}T00:00:00`).toISOString() + body.startTime = startTime + } + + if (formClientName) body.clientName = formClientName + if (formClientEmail) body.clientEmail = formClientEmail + if (formClientPhone) body.clientPhone = formClientPhone if (formParticipantId) body.participantId = formParticipantId + if (formDeveloperId) body.developerId = formDeveloperId + if (formLeadId) body.leadId = formLeadId let res if (editingEvent) { @@ -235,11 +264,13 @@ export default function CalendarPage() { const data = await res.json() 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") } else { setEvents((prev) => [...prev, data.event]) - addNotification("event_scheduled", `Event created: ${formTitle}`, `Scheduled for ${formatDate(startTime)} at ${formatTime(startTime)}`, "/calendar") + if (startTime) { + addNotification("event_scheduled", `Event created: ${formTitle}`, `Scheduled for ${formatDate(startTime)} at ${formatTime(startTime)}`, "/calendar") + } toast.success("Event created") } @@ -260,7 +291,7 @@ export default function CalendarPage() { }) if (!res.ok) throw new Error("Failed to update") 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}`) setDetailEvent(null) } catch { @@ -446,6 +477,11 @@ export default function CalendarPage() { > {event.title} + {event.lead && ( + + {event.lead.contactName} + + )}
) @@ -464,6 +500,15 @@ export default function CalendarPage() { })}
+ {/* Unscheduled projects bar */} + {events.filter(e => !e.startTime && e.eventType === "website_creation").length > 0 && ( +
+ + + {events.filter(e => !e.startTime && e.eventType === "website_creation").length} unscheduled project(s) + +
+ )}
@@ -534,12 +579,19 @@ export default function CalendarPage() { {event.status} -
- - {formatDate(event.startTime)} - · - {formatTime(event.startTime)} -
+ {event.startTime ? ( +
+ + {formatDate(event.startTime)} + · + {formatTime(event.startTime)} +
+ ) : ( +
+ + Website project — no schedule +
+ )} {event.durationMinutes && (
@@ -547,21 +599,20 @@ export default function CalendarPage() {
)}
- - {event.userId === user?.id - ? (event.participant?.name.charAt(0) || "?") - : (event.creator?.name.charAt(0) || "?")} - - - {event.userId === user?.id - ? (event.participant?.name || "No participant") - : (event.creator?.name || "Unknown")} - + {event.developerId === user?.id ? ( + <> + + {event.creator?.name.charAt(0) || "?"} + + + Assigned by {event.creator?.name || "Unknown"} + + + ) : event.lead ? ( + + {event.lead.contactName}{event.lead.companyName ? ` — ${event.lead.companyName}` : ""} + + ) : null}
@@ -579,7 +630,7 @@ export default function CalendarPage() { {/* Create/Edit Dialog */} - +
Date setFormDate(e.target.value)} className="h-10" />
-
+
setFormTime(e.target.value)} className="h-10" />
+ {formType === "website_creation" && ( +
+

Client Details

+
+ + setFormClientName(e.target.value)} placeholder="Client full name" className="h-10" /> +
+
+ + setFormClientEmail(e.target.value)} placeholder="client@example.com" className="h-10" /> +
+
+ + setFormClientPhone(e.target.value)} placeholder="+1 555-123-4567" className="h-10" /> +
+
+ )}
@@ -621,19 +689,16 @@ export default function CalendarPage() { - Meeting + Website Creation Call - Video Chat Follow Up - Demo - Other
- -