← back to Omega Watches 2
src/app/(dashboard)/alerts/page.tsx
279 lines
"use client";
import { useEffect, useState } from "react";
import { formatDateTime, formatCurrency } from "@/lib/utils";
interface AlertRule {
id: string;
reference_number: string;
collection: string;
model_name: string;
alert_type: "price_below" | "price_above";
threshold: string;
current_avg_price: string | null;
is_triggered: boolean;
is_active: boolean;
trigger_count: number;
created_by: string;
notes: string | null;
created_at: string;
last_triggered_at: string | null;
}
export default function AlertsPage() {
const [alerts, setAlerts] = useState<AlertRule[]>([]);
const [summary, setSummary] = useState({ total: 0, active: 0, triggered: 0 });
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [creating, setCreating] = useState(false);
const [referenceNumber, setReferenceNumber] = useState("");
const [alertType, setAlertType] = useState<"price_below" | "price_above">("price_below");
const [threshold, setThreshold] = useState("");
const [notes, setNotes] = useState("");
function getHeaders() {
const token = typeof window !== "undefined" ? localStorage.getItem("omega_token") : null;
const h: Record<string, string> = { "Content-Type": "application/json" };
if (token) h["Authorization"] = `Bearer ${token}`;
return h;
}
async function loadAlerts() {
try {
const res = await fetch("/api/alerts", { headers: getHeaders() });
const json = await res.json();
if (json.success) {
setAlerts(json.data.rules);
setSummary(json.data.summary);
} else {
setError(json.error?.message || "Failed to load alerts");
}
} catch (e: any) {
setError(e.message);
} finally {
setLoading(false);
}
}
useEffect(() => { loadAlerts(); }, []);
async function toggleActive(id: string, isActive: boolean) {
try {
const res = await fetch("/api/alerts", {
method: "PUT",
headers: getHeaders(),
body: JSON.stringify({ id, is_active: isActive }),
});
const json = await res.json();
if (json.success) {
loadAlerts();
}
} catch (e: any) {
setError(e.message);
}
}
async function createAlert() {
if (!referenceNumber || !threshold) return;
setCreating(true);
try {
const res = await fetch("/api/alerts", {
method: "POST",
headers: getHeaders(),
body: JSON.stringify({
reference_number: referenceNumber,
alert_type: alertType,
threshold: parseFloat(threshold),
notes: notes || null,
}),
});
const json = await res.json();
if (json.success) {
setReferenceNumber("");
setThreshold("");
setNotes("");
loadAlerts();
} else {
setError(json.error?.message || "Failed to create alert");
}
} catch (e: any) {
setError(e.message);
} finally {
setCreating(false);
}
}
if (loading) {
return <div className="flex h-full items-center justify-center"><div className="text-gray-400">Loading alerts...</div></div>;
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold text-gray-100">Price Alert Rules</h1>
<button onClick={loadAlerts} className="btn-secondary text-xs">Refresh</button>
</div>
{error && (
<div className="rounded bg-red-900/30 p-3 text-red-300">
{error}
<button onClick={() => setError("")} className="ml-2 text-xs underline">Dismiss</button>
</div>
)}
{/* Summary */}
<div className="grid gap-4 md:grid-cols-3">
<div className="card border-navy-700 text-center">
<div className="text-3xl font-bold text-gray-200">{summary.total}</div>
<div className="text-xs text-gray-500">Total Rules</div>
</div>
<div className="card border-navy-700 text-center">
<div className="text-3xl font-bold text-green-400">{summary.active}</div>
<div className="text-xs text-gray-500">Active</div>
</div>
<div className="card border-navy-700 text-center">
<div className={`text-3xl font-bold ${summary.triggered > 0 ? "text-orange-400" : "text-green-400"}`}>
{summary.triggered}
</div>
<div className="text-xs text-gray-500">Triggered</div>
</div>
</div>
{/* Create Form */}
<div className="card border-navy-700">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400">Create New Alert</h2>
<div className="grid gap-4 md:grid-cols-2">
<div>
<label className="mb-1 block text-xs text-gray-400">Reference Number</label>
<input type="text" className="input" placeholder="e.g., 310.30.42.50.01.001"
value={referenceNumber} onChange={(e) => setReferenceNumber(e.target.value)} />
</div>
<div>
<label className="mb-1 block text-xs text-gray-400">Alert Type</label>
<select className="input" value={alertType}
onChange={(e) => setAlertType(e.target.value as "price_below" | "price_above")}>
<option value="price_below">Price Falls Below</option>
<option value="price_above">Price Rises Above</option>
</select>
</div>
<div>
<label className="mb-1 block text-xs text-gray-400">Threshold (USD)</label>
<input type="number" className="input" placeholder="e.g., 5000"
value={threshold} onChange={(e) => setThreshold(e.target.value)} />
</div>
<div>
<label className="mb-1 block text-xs text-gray-400">Notes (Optional)</label>
<input type="text" className="input" placeholder="e.g., Target buy price"
value={notes} onChange={(e) => setNotes(e.target.value)} />
</div>
</div>
<div className="mt-4 flex justify-end">
<button onClick={createAlert} disabled={creating || !referenceNumber || !threshold}
className="btn-primary disabled:opacity-50">
{creating ? "Creating..." : "Create Alert"}
</button>
</div>
</div>
{/* Alert Rules */}
<div className="card border-navy-700">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400">
Alert Rules ({alerts.length})
</h2>
{alerts.length === 0 ? (
<div className="p-8 text-center">
<div className="text-4xl text-gray-600">🔔</div>
<div className="mt-2 text-lg font-medium text-gray-400">No Alerts Yet</div>
<div className="text-sm text-gray-500">Create your first price alert rule above.</div>
</div>
) : (
<div className="space-y-3">
{alerts.map((alert) => {
const triggered = alert.is_triggered;
return (
<div key={alert.id}
className={triggered
? "rounded-lg border border-orange-700 bg-orange-900/20 p-4 shadow-lg shadow-orange-900/30"
: "rounded-lg border border-navy-700 bg-navy-900/50 p-4"
}>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-mono text-sm font-medium text-omega-400">
{alert.reference_number}
</span>
<span className="text-xs text-gray-500">{alert.collection}</span>
{triggered && (
<span className="rounded bg-orange-900/50 px-2 py-0.5 text-xs font-bold text-orange-300 animate-pulse">
TRIGGERED
</span>
)}
<span className={`rounded px-2 py-0.5 text-xs font-bold ${
alert.is_active ? "bg-green-900/50 text-green-300" : "bg-gray-800 text-gray-500"
}`}>
{alert.is_active ? "Active" : "Inactive"}
</span>
</div>
{alert.model_name && (
<div className="mt-1 text-xs text-gray-400">{alert.model_name}</div>
)}
<div className="mt-2 flex flex-wrap items-center gap-4 text-xs">
<div>
<span className="text-gray-500">Type: </span>
<span className="text-gray-300">
{alert.alert_type === "price_below" ? "Price Falls Below" : "Price Rises Above"}
</span>
</div>
<div>
<span className="text-gray-500">Threshold: </span>
<span className="font-medium text-gray-200">
{formatCurrency(parseFloat(alert.threshold))}
</span>
</div>
{alert.current_avg_price && (
<div>
<span className="text-gray-500">Current Avg: </span>
<span className={`font-medium ${triggered ? "text-orange-400" : "text-gray-200"}`}>
{formatCurrency(parseFloat(alert.current_avg_price))}
</span>
</div>
)}
<div>
<span className="text-gray-500">Triggers: </span>
<span className="text-gray-300">{alert.trigger_count}</span>
</div>
</div>
{alert.notes && (
<div className="mt-2 text-xs text-gray-500">
<span className="font-medium">Note:</span> {alert.notes}
</div>
)}
<div className="mt-2 flex items-center gap-4 text-xs text-gray-600">
<span>By {alert.created_by}</span>
<span>{formatDateTime(alert.created_at)}</span>
{alert.last_triggered_at && (
<span className="text-orange-500">Last triggered {formatDateTime(alert.last_triggered_at)}</span>
)}
</div>
</div>
<button onClick={() => toggleActive(alert.id, !alert.is_active)}
className={`ml-4 rounded-md px-3 py-1 text-xs font-medium transition-colors ${
alert.is_active
? "bg-red-900/50 text-red-300 hover:bg-red-900/70"
: "bg-green-900/50 text-green-300 hover:bg-green-900/70"
}`}>
{alert.is_active ? "Disable" : "Enable"}
</button>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
);
}