246 lines
12 KiB
JavaScript
246 lines
12 KiB
JavaScript
|
|
import React from "react";
|
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { MapPin, TrendingUp, AlertTriangle, CheckCircle2, Clock, Users } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { createPageUrl } from "@/utils";
|
|
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, AreaChart, Area } from 'recharts';
|
|
|
|
import PageHeader from "../components/common/PageHeader";
|
|
|
|
const coverageData = [
|
|
{ hub: 'San Jose', coverage: 97, incidents: 0, satisfaction: 4.9 },
|
|
{ hub: 'San Francisco', coverage: 92, incidents: 1, satisfaction: 4.7 },
|
|
{ hub: 'Oakland', coverage: 89, incidents: 2, satisfaction: 4.5 },
|
|
{ hub: 'Sacramento', coverage: 95, incidents: 1, satisfaction: 4.8 },
|
|
];
|
|
|
|
const demandForecast = [
|
|
{ week: 'Week 1', predicted: 120, actual: 118 },
|
|
{ week: 'Week 2', predicted: 135, actual: 140 },
|
|
{ week: 'Week 3', predicted: 145, actual: 142 },
|
|
{ week: 'Week 4', predicted: 160, actual: null },
|
|
{ week: 'Week 5', predicted: 155, actual: null },
|
|
];
|
|
|
|
const incidents = [
|
|
{ id: 1, type: 'Late Arrival', hub: 'San Francisco', staff: 'John Doe', severity: 'low', time: '2 hours ago' },
|
|
{ id: 2, type: 'No Show', hub: 'Oakland', staff: 'Jane Smith', severity: 'high', time: '5 hours ago' },
|
|
];
|
|
|
|
const feedbackData = [
|
|
{ client: 'Tech Corp', rating: 5, comment: 'Excellent service, very professional staff', date: '2 days ago' },
|
|
{ client: 'Event Solutions', rating: 4, comment: 'Good overall, minor scheduling issues', date: '3 days ago' },
|
|
{ client: 'Premier Events', rating: 5, comment: 'Outstanding performance, will book again', date: '1 week ago' },
|
|
];
|
|
|
|
export default function OperatorDashboard() {
|
|
const navigate = useNavigate();
|
|
|
|
return (
|
|
<div className="p-4 md:p-8 bg-slate-50 min-h-screen">
|
|
<div className="max-w-7xl mx-auto">
|
|
<PageHeader
|
|
title="Operator & Sector Dashboard"
|
|
subtitle="Live coverage, demand forecasting, and incident tracking"
|
|
showUnpublished={true}
|
|
backTo={createPageUrl("Dashboard")}
|
|
backButtonLabel="Back to Dashboard"
|
|
/>
|
|
|
|
{/* Key Metrics */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
|
<Card className="border-slate-200 shadow-lg">
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<Users className="w-8 h-8 text-[#0A39DF]" />
|
|
<Badge className="bg-green-100 text-green-700">+5%</Badge>
|
|
</div>
|
|
<p className="text-sm text-slate-500">Coverage Rate</p>
|
|
<p className="text-3xl font-bold text-[#1C323E]">94%</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-slate-200 shadow-lg">
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<AlertTriangle className="w-8 h-8 text-yellow-600" />
|
|
<Badge className="bg-green-100 text-green-700">Low</Badge>
|
|
</div>
|
|
<p className="text-sm text-slate-500">Active Incidents</p>
|
|
<p className="text-3xl font-bold text-[#1C323E]">2</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-slate-200 shadow-lg">
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<CheckCircle2 className="w-8 h-8 text-green-600" />
|
|
<Badge className="bg-blue-100 text-blue-700">4.8/5.0</Badge>
|
|
</div>
|
|
<p className="text-sm text-slate-500">Client Satisfaction</p>
|
|
<p className="text-3xl font-bold text-[#1C323E]">4.8</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-slate-200 shadow-lg">
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<TrendingUp className="w-8 h-8 text-emerald-600" />
|
|
<Badge className="bg-emerald-100 text-emerald-700">91%</Badge>
|
|
</div>
|
|
<p className="text-sm text-slate-500">Forecast Accuracy</p>
|
|
<p className="text-3xl font-bold text-[#1C323E]">91%</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Shift Coverage Heat Map */}
|
|
<Card className="mb-8 border-slate-200 shadow-lg">
|
|
<CardHeader className="bg-gradient-to-br from-slate-50 to-white border-b border-slate-100">
|
|
<CardTitle className="text-[#1C323E]">Live Shift Coverage Map</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{coverageData.map((hub, index) => (
|
|
<div key={index} className="p-6 rounded-xl border-2 border-slate-200 hover:border-[#0A39DF] hover:shadow-lg transition-all">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${
|
|
hub.coverage >= 95 ? 'bg-green-100' :
|
|
hub.coverage >= 90 ? 'bg-blue-100' :
|
|
'bg-yellow-100'
|
|
}`}>
|
|
<MapPin className={`w-6 h-6 ${
|
|
hub.coverage >= 95 ? 'text-green-600' :
|
|
hub.coverage >= 90 ? 'text-blue-600' :
|
|
'text-yellow-600'
|
|
}`} />
|
|
</div>
|
|
<div>
|
|
<h4 className="font-bold text-[#1C323E]">{hub.hub}</h4>
|
|
<p className="text-sm text-slate-500">Coverage: {hub.coverage}%</p>
|
|
</div>
|
|
</div>
|
|
<Badge className={`${
|
|
hub.coverage >= 95 ? 'bg-green-100 text-green-700' :
|
|
hub.coverage >= 90 ? 'bg-blue-100 text-blue-700' :
|
|
'bg-yellow-100 text-yellow-700'
|
|
} text-lg px-3 py-1`}>
|
|
{hub.coverage}%
|
|
</Badge>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<p className="text-slate-500">Incidents</p>
|
|
<p className="font-bold text-red-600">{hub.incidents}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-slate-500">Satisfaction</p>
|
|
<p className="font-bold text-emerald-600">{hub.satisfaction}/5.0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
|
{/* Predictive Demand Forecast */}
|
|
<Card className="border-slate-200 shadow-lg">
|
|
<CardHeader className="bg-gradient-to-br from-slate-50 to-white border-b border-slate-100">
|
|
<CardTitle className="text-[#1C323E]">Predictive Demand Forecast</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-6">
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<AreaChart data={demandForecast}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="week" />
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Legend />
|
|
<Area type="monotone" dataKey="predicted" stackId="1" stroke="#0A39DF" fill="#0A39DF" fillOpacity={0.3} name="Predicted" />
|
|
<Area type="monotone" dataKey="actual" stackId="2" stroke="#10b981" fill="#10b981" fillOpacity={0.6} name="Actual" />
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
<p className="text-sm text-slate-500 mt-4">
|
|
Forecast accuracy: <span className="font-bold text-[#0A39DF]">91%</span> based on historical data
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Incident Feed */}
|
|
<Card className="border-slate-200 shadow-lg">
|
|
<CardHeader className="bg-gradient-to-br from-slate-50 to-white border-b border-slate-100">
|
|
<CardTitle className="text-[#1C323E]">Incident Feed</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-6">
|
|
<div className="space-y-4">
|
|
{incidents.map((incident) => (
|
|
<div key={incident.id} className="p-4 rounded-lg border-2 border-slate-200 hover:border-red-300 transition-all">
|
|
<div className="flex items-start justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<AlertTriangle className={`w-5 h-5 ${
|
|
incident.severity === 'high' ? 'text-red-600' :
|
|
incident.severity === 'medium' ? 'text-yellow-600' :
|
|
'text-blue-600'
|
|
}`} />
|
|
<h4 className="font-semibold text-[#1C323E]">{incident.type}</h4>
|
|
</div>
|
|
<Badge className={`${
|
|
incident.severity === 'high' ? 'bg-red-100 text-red-700' :
|
|
incident.severity === 'medium' ? 'bg-yellow-100 text-yellow-700' :
|
|
'bg-blue-100 text-blue-700'
|
|
}`}>
|
|
{incident.severity.toUpperCase()}
|
|
</Badge>
|
|
</div>
|
|
<div className="space-y-1 text-sm">
|
|
<p><span className="text-slate-500">Hub:</span> <span className="font-medium">{incident.hub}</span></p>
|
|
<p><span className="text-slate-500">Staff:</span> <span className="font-medium">{incident.staff}</span></p>
|
|
<p className="text-slate-400 flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
{incident.time}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Client Feedback */}
|
|
<Card className="border-slate-200 shadow-lg">
|
|
<CardHeader className="bg-gradient-to-br from-slate-50 to-white border-b border-slate-100">
|
|
<CardTitle className="text-[#1C323E]">Recent Client Feedback</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-6">
|
|
<div className="space-y-4">
|
|
{feedbackData.map((feedback, index) => (
|
|
<div key={index} className="p-4 rounded-lg border border-slate-200 hover:border-[#0A39DF] hover:shadow-md transition-all">
|
|
<div className="flex items-start justify-between mb-2">
|
|
<div>
|
|
<h4 className="font-semibold text-[#1C323E]">{feedback.client}</h4>
|
|
<div className="flex items-center gap-1 mt-1">
|
|
{[...Array(5)].map((_, i) => (
|
|
<span key={i} className={i < feedback.rating ? "text-yellow-400" : "text-slate-300"}>★</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<span className="text-xs text-slate-400">{feedback.date}</span>
|
|
</div>
|
|
<p className="text-sm text-slate-600">{feedback.comment}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|