Fixed notifications button
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS notifications (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
link TEXT,
|
||||||
|
is_read BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id, created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications(user_id, created_at DESC) WHERE is_read = FALSE;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS notification_preferences (
|
||||||
|
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
lead_assigned BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
lead_status BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
note_added BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
daily_digest BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
weekly_report BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
@@ -10,6 +10,24 @@
|
|||||||
\echo '=== Running 002_seed.sql (RBAC + Test Accounts + Reference Data) ==='
|
\echo '=== Running 002_seed.sql (RBAC + Test Accounts + Reference Data) ==='
|
||||||
\i 002_seed.sql
|
\i 002_seed.sql
|
||||||
|
|
||||||
|
\echo '=== Running 003_chat.sql (Chat Tables) ==='
|
||||||
|
\i 003_chat.sql
|
||||||
|
|
||||||
|
\echo '=== Running 004_avatar_url.sql (Avatar URL Column) ==='
|
||||||
|
\i 004_avatar_url.sql
|
||||||
|
|
||||||
|
\echo '=== Running 005_last_read_at.sql (Last Read At Column) ==='
|
||||||
|
\i 005_last_read_at.sql
|
||||||
|
|
||||||
|
\echo '=== Running 006_test_leads.sql (Test Lead Data) ==='
|
||||||
|
\i 006_test_leads.sql
|
||||||
|
|
||||||
|
\echo '=== Running 007_ai_features.sql (AI Features) ==='
|
||||||
|
\i 007_ai_features.sql
|
||||||
|
|
||||||
|
\echo '=== Running 008_notifications.sql (Notifications + Preferences) ==='
|
||||||
|
\i 008_notifications.sql
|
||||||
|
|
||||||
\echo '=== Migration Complete ==='
|
\echo '=== Migration Complete ==='
|
||||||
\echo ''
|
\echo ''
|
||||||
\echo 'Test accounts created:'
|
\echo 'Test accounts created:'
|
||||||
|
|||||||
Generated
-1
@@ -3297,7 +3297,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz",
|
||||||
"integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==",
|
"integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "5.6.2",
|
"chalk": "5.6.2",
|
||||||
"rxjs": "7.8.2",
|
"rxjs": "7.8.2",
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
import { getSessionUser } from "@/lib/auth"
|
||||||
|
import { query } from "@/lib/db"
|
||||||
|
|
||||||
|
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
try {
|
||||||
|
const user = await getSessionUser()
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
|
||||||
|
const { id } = await params
|
||||||
|
|
||||||
|
const result = await query(
|
||||||
|
`UPDATE notifications SET is_read = TRUE WHERE id = $1 AND user_id = $2 RETURNING id`,
|
||||||
|
[id, user.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
return NextResponse.json({ error: "Notification not found" }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Notification PATCH error:", error)
|
||||||
|
return NextResponse.json({ error: "Failed to mark notification as read" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
try {
|
||||||
|
const user = await getSessionUser()
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
|
||||||
|
const { id } = await params
|
||||||
|
|
||||||
|
const result = await query(
|
||||||
|
`DELETE FROM notifications WHERE id = $1 AND user_id = $2 RETURNING id`,
|
||||||
|
[id, user.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
return NextResponse.json({ error: "Notification not found" }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Notification DELETE error:", error)
|
||||||
|
return NextResponse.json({ error: "Failed to dismiss notification" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
import { getSessionUser } from "@/lib/auth"
|
||||||
|
import { query } from "@/lib/db"
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const user = await getSessionUser()
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
|
||||||
|
const result = await query(
|
||||||
|
`SELECT lead_assigned, lead_status, note_added, daily_digest, weekly_report
|
||||||
|
FROM notification_preferences
|
||||||
|
WHERE user_id = $1`,
|
||||||
|
[user.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
return NextResponse.json({
|
||||||
|
leadAssigned: true,
|
||||||
|
leadStatus: true,
|
||||||
|
noteAdded: false,
|
||||||
|
dailyDigest: false,
|
||||||
|
weeklyReport: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = result.rows[0]
|
||||||
|
return NextResponse.json({
|
||||||
|
leadAssigned: r.lead_assigned,
|
||||||
|
leadStatus: r.lead_status,
|
||||||
|
noteAdded: r.note_added,
|
||||||
|
dailyDigest: r.daily_digest,
|
||||||
|
weeklyReport: r.weekly_report,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Preferences GET error:", error)
|
||||||
|
return NextResponse.json({ error: "Failed to load preferences" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PATCH(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const user = await getSessionUser()
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
|
||||||
|
await query(
|
||||||
|
`INSERT INTO notification_preferences (user_id, lead_assigned, lead_status, note_added, daily_digest, weekly_report, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, NOW())
|
||||||
|
ON CONFLICT (user_id)
|
||||||
|
DO UPDATE SET lead_assigned = $2, lead_status = $3, note_added = $4,
|
||||||
|
daily_digest = $5, weekly_report = $6, updated_at = NOW()`,
|
||||||
|
[
|
||||||
|
user.id,
|
||||||
|
body.leadAssigned ?? true,
|
||||||
|
body.leadStatus ?? true,
|
||||||
|
body.noteAdded ?? false,
|
||||||
|
body.dailyDigest ?? false,
|
||||||
|
body.weeklyReport ?? true,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Preferences PATCH error:", error)
|
||||||
|
return NextResponse.json({ error: "Failed to save preferences" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
import { getSessionUser } from "@/lib/auth"
|
||||||
|
import { query } from "@/lib/db"
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const user = await getSessionUser()
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
|
||||||
|
const result = await query(
|
||||||
|
`SELECT id, type, title, description, link, is_read, created_at
|
||||||
|
FROM notifications
|
||||||
|
WHERE user_id = $1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 50`,
|
||||||
|
[user.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
const notifications = result.rows.map((r: any) => ({
|
||||||
|
id: r.id,
|
||||||
|
type: r.type,
|
||||||
|
title: r.title,
|
||||||
|
description: r.description,
|
||||||
|
link: r.link,
|
||||||
|
read: r.is_read,
|
||||||
|
timestamp: r.created_at,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const unreadResult = await query(
|
||||||
|
`SELECT COUNT(*) AS count FROM notifications WHERE user_id = $1 AND is_read = FALSE`,
|
||||||
|
[user.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
notifications,
|
||||||
|
unreadCount: parseInt(unreadResult.rows[0].count, 10),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Notifications GET error:", error)
|
||||||
|
return NextResponse.json({ error: "Failed to load notifications" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const user = await getSessionUser()
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
|
||||||
|
const { type, title, description, link, userId } = await request.json()
|
||||||
|
const targetUserId = userId || user.id
|
||||||
|
|
||||||
|
const result = await query(
|
||||||
|
`INSERT INTO notifications (user_id, type, title, description, link)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
RETURNING id, type, title, description, link, is_read, created_at`,
|
||||||
|
[targetUserId, type, title, description || null, link || null],
|
||||||
|
)
|
||||||
|
|
||||||
|
const notif = result.rows[0]
|
||||||
|
return NextResponse.json({
|
||||||
|
id: notif.id,
|
||||||
|
type: notif.type,
|
||||||
|
title: notif.title,
|
||||||
|
description: notif.description,
|
||||||
|
link: notif.link,
|
||||||
|
read: notif.is_read,
|
||||||
|
timestamp: notif.created_at,
|
||||||
|
}, { status: 201 })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Notifications POST error:", error)
|
||||||
|
return NextResponse.json({ error: "Failed to create notification" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PATCH() {
|
||||||
|
try {
|
||||||
|
const user = await getSessionUser()
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
|
||||||
|
await query(
|
||||||
|
`UPDATE notifications SET is_read = TRUE WHERE user_id = $1 AND is_read = FALSE`,
|
||||||
|
[user.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Notifications PATCH error:", error)
|
||||||
|
return NextResponse.json({ error: "Failed to mark all as read" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -134,7 +134,7 @@ export function Topbar({ onMenuClick }: TopbarProps) {
|
|||||||
>
|
>
|
||||||
<Bell className="h-5 w-5" />
|
<Bell className="h-5 w-5" />
|
||||||
<span className="absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[10px] font-medium text-primary-foreground">
|
<span className="absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[10px] font-medium text-primary-foreground">
|
||||||
3
|
{unreadCount}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|||||||
@@ -1,45 +1,82 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
|
interface Preferences {
|
||||||
|
leadAssigned: boolean
|
||||||
|
leadStatus: boolean
|
||||||
|
noteAdded: boolean
|
||||||
|
dailyDigest: boolean
|
||||||
|
weeklyReport: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPreferences: Preferences = {
|
||||||
|
leadAssigned: true,
|
||||||
|
leadStatus: true,
|
||||||
|
noteAdded: false,
|
||||||
|
dailyDigest: false,
|
||||||
|
weeklyReport: true,
|
||||||
|
}
|
||||||
|
|
||||||
const notifications = [
|
const notifications = [
|
||||||
{
|
{ id: "leadAssigned", title: "Lead Assigned", description: "When a lead is assigned to you" },
|
||||||
id: "lead-assigned",
|
{ id: "leadStatus", title: "Status Changes", description: "When a lead's status is updated" },
|
||||||
title: "Lead Assigned",
|
{ id: "noteAdded", title: "Note Added", description: "When a note is added to your lead" },
|
||||||
description: "When a lead is assigned to you",
|
{ id: "dailyDigest", title: "Daily Digest", description: "Receive a daily summary of lead activity" },
|
||||||
defaultChecked: true,
|
{ id: "weeklyReport", title: "Weekly Report", description: "Receive a weekly performance report" },
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "lead-status",
|
|
||||||
title: "Status Changes",
|
|
||||||
description: "When a lead's status is updated",
|
|
||||||
defaultChecked: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "note-added",
|
|
||||||
title: "Note Added",
|
|
||||||
description: "When a note is added to your lead",
|
|
||||||
defaultChecked: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "daily-digest",
|
|
||||||
title: "Daily Digest",
|
|
||||||
description: "Receive a daily summary of lead activity",
|
|
||||||
defaultChecked: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "weekly-report",
|
|
||||||
title: "Weekly Report",
|
|
||||||
description: "Receive a weekly performance report",
|
|
||||||
defaultChecked: true,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export function NotificationSettings() {
|
export function NotificationSettings() {
|
||||||
|
const [prefs, setPrefs] = useState<Preferences>(defaultPreferences)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/notifications/preferences")
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
setPrefs(data)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/notifications/preferences", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(prefs),
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success("Notification preferences saved")
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to save preferences")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("Failed to save preferences")
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(key: keyof Preferences) {
|
||||||
|
setPrefs((prev) => ({ ...prev, [key]: !prev[key] }))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -60,11 +97,18 @@ export function NotificationSettings() {
|
|||||||
</Label>
|
</Label>
|
||||||
<p className="text-sm text-muted-foreground">{n.description}</p>
|
<p className="text-sm text-muted-foreground">{n.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch id={n.id} defaultChecked={n.defaultChecked} />
|
<Switch
|
||||||
|
id={n.id}
|
||||||
|
disabled={loading}
|
||||||
|
checked={prefs[n.id as keyof Preferences]}
|
||||||
|
onCheckedChange={() => toggle(n.id as keyof Preferences)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="flex justify-end pt-4">
|
<div className="flex justify-end pt-4">
|
||||||
<Button onClick={() => toast.success("Notification preferences saved")}>Save Preferences</Button>
|
<Button onClick={handleSave} disabled={loading || saving}>
|
||||||
|
{saving ? "Saving..." : "Save Preferences"}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { createContext, useContext, useState, useCallback, useMemo, useEffect, ReactNode } from "react"
|
import { createContext, useContext, useState, useCallback, useMemo, useEffect, ReactNode } from "react"
|
||||||
import { Notification, NotificationType } from "@/types"
|
import { Notification, NotificationType } from "@/types"
|
||||||
import { leads } from "@/data/leads"
|
|
||||||
|
|
||||||
interface NotificationContextValue {
|
interface NotificationContextValue {
|
||||||
notifications: Notification[]
|
notifications: Notification[]
|
||||||
@@ -16,62 +15,29 @@ interface NotificationContextValue {
|
|||||||
|
|
||||||
const NotificationContext = createContext<NotificationContextValue | null>(null)
|
const NotificationContext = createContext<NotificationContextValue | null>(null)
|
||||||
|
|
||||||
function generateId(): string {
|
|
||||||
return `notif-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function seedInitialNotifications(): Notification[] {
|
|
||||||
const result: Notification[] = []
|
|
||||||
|
|
||||||
const recentLeads = leads.slice(0, 5)
|
|
||||||
recentLeads.forEach((lead) => {
|
|
||||||
result.push({
|
|
||||||
id: generateId(),
|
|
||||||
type: "lead_created",
|
|
||||||
title: "New Lead Created",
|
|
||||||
description: `${lead.companyName} — ${lead.contactName}`,
|
|
||||||
timestamp: lead.createdAt,
|
|
||||||
read: false,
|
|
||||||
link: `/leads/${lead.id}`,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const statusChanged = leads.filter((l) => l.status !== "open").slice(0, 3)
|
|
||||||
statusChanged.forEach((lead) => {
|
|
||||||
result.push({
|
|
||||||
id: generateId(),
|
|
||||||
type: "lead_status_changed",
|
|
||||||
title: "Lead Status Updated",
|
|
||||||
description: `${lead.companyName} moved to ${lead.status}`,
|
|
||||||
timestamp: lead.updatedAt,
|
|
||||||
read: false,
|
|
||||||
link: `/leads/${lead.id}`,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const assignedLeads = leads.filter((l) => l.assignedUser).slice(0, 3)
|
|
||||||
assignedLeads.forEach((lead) => {
|
|
||||||
result.push({
|
|
||||||
id: generateId(),
|
|
||||||
type: "lead_assigned",
|
|
||||||
title: "Lead Assigned",
|
|
||||||
description: `${lead.companyName} assigned to ${lead.assignedUser!.name}`,
|
|
||||||
timestamp: lead.updatedAt,
|
|
||||||
read: false,
|
|
||||||
link: `/leads/${lead.id}`,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
result.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NotificationProvider({ children }: { children: ReactNode }) {
|
export function NotificationProvider({ children }: { children: ReactNode }) {
|
||||||
const [notifications, setNotifications] = useState<Notification[]>(seedInitialNotifications)
|
const [notifications, setNotifications] = useState<Notification[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
const [unreadChatCount, setUnreadChatCount] = useState(0)
|
const [unreadChatCount, setUnreadChatCount] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchNotifications() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/notifications")
|
||||||
|
if (!res.ok) return
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.notifications) {
|
||||||
|
setNotifications(data.notifications)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchNotifications()
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
async function fetchUnread() {
|
async function fetchUnread() {
|
||||||
@@ -99,9 +65,9 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const addNotification = useCallback(
|
const addNotification = useCallback(
|
||||||
(type: NotificationType, title: string, description: string, link?: string) => {
|
async (type: NotificationType, title: string, description: string, link?: string) => {
|
||||||
const notif: Notification = {
|
const notif: Notification = {
|
||||||
id: generateId(),
|
id: `temp-${Date.now()}`,
|
||||||
type,
|
type,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
@@ -110,22 +76,48 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
|||||||
link,
|
link,
|
||||||
}
|
}
|
||||||
setNotifications((prev) => [notif, ...prev])
|
setNotifications((prev) => [notif, ...prev])
|
||||||
|
try {
|
||||||
|
await fetch("/api/notifications", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ type, title, description, link }),
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
const markAsRead = useCallback((id: string) => {
|
const markAsRead = useCallback(async (id: string) => {
|
||||||
|
if (id.startsWith("temp-")) return
|
||||||
setNotifications((prev) =>
|
setNotifications((prev) =>
|
||||||
prev.map((n) => (n.id === id ? { ...n, read: true } : n))
|
prev.map((n) => (n.id === id ? { ...n, read: true } : n))
|
||||||
)
|
)
|
||||||
|
try {
|
||||||
|
await fetch(`/api/notifications/${id}`, { method: "PATCH" })
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const markAllAsRead = useCallback(() => {
|
const markAllAsRead = useCallback(async () => {
|
||||||
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })))
|
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })))
|
||||||
|
try {
|
||||||
|
await fetch("/api/notifications", { method: "PATCH" })
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const dismiss = useCallback((id: string) => {
|
const dismiss = useCallback(async (id: string) => {
|
||||||
setNotifications((prev) => prev.filter((n) => n.id !== id))
|
setNotifications((prev) => prev.filter((n) => n.id !== id))
|
||||||
|
if (id.startsWith("temp-")) return
|
||||||
|
try {
|
||||||
|
await fetch(`/api/notifications/${id}`, { method: "DELETE" })
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user