feat(ui): complete ProCanvas role-based overhaul and fix Recharts crash
- Implement unified Neomorphic tactical layout for ProCanvas across Agent, Admin, and Owner roles. - Add Quick Operations Log widget for immediate action logging. - Integrate discrete Checkpoint/Stages progression system to replace static prize tracks. - Build custom AnimatedCounter for GSAP-style numerical state changes. - Fix Recharts ResponsiveContainer loading crash in OwnerSnapshot.jsx.
This commit is contained in:
+16
@@ -89,6 +89,14 @@ function App() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/emp/fa/pro-canvas"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute allowedRoles={['FIELD_AGENT', 'ADMIN', 'OWNER']}>
|
||||||
|
<ProCanvas />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
{/* Owner Routes */}
|
{/* Owner Routes */}
|
||||||
<Route path="/owner/snapshot" element={
|
<Route path="/owner/snapshot" element={
|
||||||
<ProtectedRoute allowedRoles={['OWNER']}>
|
<ProtectedRoute allowedRoles={['OWNER']}>
|
||||||
@@ -165,6 +173,14 @@ function App() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/admin/pro-canvas"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute allowedRoles={['ADMIN', 'OWNER']}>
|
||||||
|
<ProCanvas />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Contractor Routes */}
|
{/* Contractor Routes */}
|
||||||
<Route path="/contractor/dashboard" element={
|
<Route path="/contractor/dashboard" element={
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { useInView } from 'framer-motion';
|
||||||
|
|
||||||
|
export const AnimatedCounter = ({ from = 0, to, duration = 1.5, className = "" }) => {
|
||||||
|
const [value, setValue] = useState(from);
|
||||||
|
const ref = useRef(null);
|
||||||
|
const isInView = useInView(ref, { once: true, margin: "-20px" });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isInView) return;
|
||||||
|
|
||||||
|
let startTime;
|
||||||
|
let animationFrame;
|
||||||
|
|
||||||
|
const animate = (timestamp) => {
|
||||||
|
if (!startTime) startTime = timestamp;
|
||||||
|
const progress = (timestamp - startTime) / (duration * 1000);
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
// Easing function: easeOutQuart
|
||||||
|
const easeOut = 1 - Math.pow(1 - progress, 4);
|
||||||
|
setValue(Math.floor(from + (to - from) * easeOut));
|
||||||
|
animationFrame = requestAnimationFrame(animate);
|
||||||
|
} else {
|
||||||
|
setValue(to);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
animationFrame = requestAnimationFrame(animate);
|
||||||
|
|
||||||
|
return () => cancelAnimationFrame(animationFrame);
|
||||||
|
}, [from, to, duration, isInView]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span ref={ref} className={className}>
|
||||||
|
{value.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -154,6 +154,11 @@ const Layout = () => {
|
|||||||
{ to: "/admin/schedule", icon: Calendar, label: "Schedule" },
|
{ to: "/admin/schedule", icon: Calendar, label: "Schedule" },
|
||||||
{ to: "/admin/leaderboard", icon: Trophy, label: "Leaderboard" },
|
{ to: "/admin/leaderboard", icon: Trophy, label: "Leaderboard" },
|
||||||
{ to: "/admin/maps", icon: Map, label: "Territory Map" },
|
{ to: "/admin/maps", icon: Map, label: "Territory Map" },
|
||||||
|
{
|
||||||
|
to: "/admin/pro-canvas",
|
||||||
|
icon: LayoutDashboard,
|
||||||
|
label: <span><span className="text-[#fda913]">Pro</span>Canvas</span>
|
||||||
|
},
|
||||||
...commonItems,
|
...commonItems,
|
||||||
];
|
];
|
||||||
case 'CONTRACTOR':
|
case 'CONTRACTOR':
|
||||||
@@ -173,6 +178,11 @@ const Layout = () => {
|
|||||||
return [
|
return [
|
||||||
{ to: "/emp/fa/dashboard", icon: LayoutDashboard, label: "Dashboard" },
|
{ to: "/emp/fa/dashboard", icon: LayoutDashboard, label: "Dashboard" },
|
||||||
{ to: "/emp/fa/maps", icon: Map, label: "My Map" },
|
{ to: "/emp/fa/maps", icon: Map, label: "My Map" },
|
||||||
|
{
|
||||||
|
to: "/emp/fa/pro-canvas",
|
||||||
|
icon: LayoutDashboard,
|
||||||
|
label: <span><span className="text-[#fda913]">Pro</span>Canvas</span>
|
||||||
|
},
|
||||||
...commonItems,
|
...commonItems,
|
||||||
];
|
];
|
||||||
default: // Customer or Fallback
|
default: // Customer or Fallback
|
||||||
|
|||||||
@@ -653,11 +653,11 @@ const MOCK_USERS = [
|
|||||||
{ id: 'c3', type: 'customer', username: 'charlie', email: 'charlie@example.com', password: 'password', name: 'Charlie Client' },
|
{ id: 'c3', type: 'customer', username: 'charlie', email: 'charlie@example.com', password: 'password', name: 'Charlie Client' },
|
||||||
|
|
||||||
// Field Agents (5 Agents)
|
// Field Agents (5 Agents)
|
||||||
{ id: 'e1', type: 'employee', empId: 'FA001', email: 'agent1@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Frank Agent' },
|
{ id: 'e1', type: 'employee', empId: 'FA001', email: 'agent1@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Frank Agent', xp: 12450, doorsKnocked: 342, leadsGained: 45, appointmentsSet: 18, streakDays: 5, achievements: ['Hot Spot Hunter', 'Storm Chaser'] },
|
||||||
{ id: 'e2', type: 'employee', empId: 'FA002', email: 'agent2@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Fiona Field' },
|
{ id: 'e2', type: 'employee', empId: 'FA002', email: 'agent2@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Fiona Field', xp: 8200, doorsKnocked: 215, leadsGained: 30, appointmentsSet: 12, streakDays: 2, achievements: ['Star Closer'] },
|
||||||
{ id: 'e3', type: 'employee', empId: 'FA003', email: 'agent3@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Fred Flyer' },
|
{ id: 'e3', type: 'employee', empId: 'FA003', email: 'agent3@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Fred Flyer', xp: 2100, doorsKnocked: 85, leadsGained: 5, appointmentsSet: 2, streakDays: 0, achievements: [] },
|
||||||
{ id: 'e4', type: 'employee', empId: 'FA004', email: 'agent4@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Felicity Fast' },
|
{ id: 'e4', type: 'employee', empId: 'FA004', email: 'agent4@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Felicity Fast', xp: 15600, doorsKnocked: 410, leadsGained: 65, appointmentsSet: 24, streakDays: 8, achievements: ['Map Master', 'Hot Spot Hunter', 'Storm Chaser'] },
|
||||||
{ id: 'e5', type: 'employee', empId: 'FA005', email: 'agent5@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Felix Fixer' },
|
{ id: 'e5', type: 'employee', empId: 'FA005', email: 'agent5@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Felix Fixer', xp: 5400, doorsKnocked: 156, leadsGained: 20, appointmentsSet: 8, streakDays: 1, achievements: ['First Knock'] },
|
||||||
|
|
||||||
// Admins (3 Admins)
|
// Admins (3 Admins)
|
||||||
{ id: 'a1', type: 'employee', empId: 'ADM01', email: 'admin@plano.com', password: 'password', role: 'ADMIN', name: 'Adam Admin' },
|
{ id: 'a1', type: 'employee', empId: 'ADM01', email: 'admin@plano.com', password: 'password', role: 'ADMIN', name: 'Adam Admin' },
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
const LEVEL_THRESHOLDS = [
|
||||||
|
{ level: 1, xp: 0, title: 'Rookie' },
|
||||||
|
{ level: 2, xp: 1000, title: 'Scout' },
|
||||||
|
{ level: 3, xp: 2500, title: 'Trekker' },
|
||||||
|
{ level: 4, xp: 5000, title: 'Pathfinder' },
|
||||||
|
{ level: 5, xp: 10000, title: 'Roof Warrior' },
|
||||||
|
{ level: 6, xp: 15000, title: 'Storm Chaser' },
|
||||||
|
{ level: 7, xp: 25000, title: 'Elite Operator' },
|
||||||
|
{ level: 8, xp: 40000, title: 'Neighborhood Boss' },
|
||||||
|
{ level: 9, xp: 60000, title: 'Commanding Officer' },
|
||||||
|
{ level: 10, xp: 100000, title: 'Legend' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const useGamification = (user) => {
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
const xp = user.xp || 0;
|
||||||
|
|
||||||
|
// Calculate Level
|
||||||
|
let currentLevelObj = LEVEL_THRESHOLDS[0];
|
||||||
|
let nextLevelObj = LEVEL_THRESHOLDS[1];
|
||||||
|
|
||||||
|
for (let i = 0; i < LEVEL_THRESHOLDS.length; i++) {
|
||||||
|
if (xp >= LEVEL_THRESHOLDS[i].xp) {
|
||||||
|
currentLevelObj = LEVEL_THRESHOLDS[i];
|
||||||
|
nextLevelObj = LEVEL_THRESHOLDS[i + 1] || LEVEL_THRESHOLDS[i]; // Max level fallback
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { level, title } = currentLevelObj;
|
||||||
|
|
||||||
|
// XP Progress
|
||||||
|
let xpProgress = 100;
|
||||||
|
let xpToNext = 0;
|
||||||
|
let nextLevelXp = nextLevelObj.xp;
|
||||||
|
let currentLevelBaseXp = currentLevelObj.xp;
|
||||||
|
|
||||||
|
if (level < LEVEL_THRESHOLDS[LEVEL_THRESHOLDS.length - 1].level) {
|
||||||
|
const range = nextLevelXp - currentLevelBaseXp;
|
||||||
|
const progress = xp - currentLevelBaseXp;
|
||||||
|
xpProgress = Math.min(100, Math.max(0, (progress / range) * 100));
|
||||||
|
xpToNext = nextLevelXp - xp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PHASE 3: CHECKPOINTS & STAGES ---
|
||||||
|
const STAGES = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Stage 1: Groundwork",
|
||||||
|
requirement: { type: "doorsKnocked", amount: 20 },
|
||||||
|
reward: { type: "coupon", name: "$25 Uber Eats Card", icon: "uber" },
|
||||||
|
progress: Math.min((user.doorsKnocked || 0), 20),
|
||||||
|
goal: 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Stage 2: Connections",
|
||||||
|
requirement: { type: "appointmentsSet", amount: 10 },
|
||||||
|
reward: { type: "giftcard", name: "$50 Amazon Card", icon: "amazon" },
|
||||||
|
progress: Math.min((user.appointmentsSet || 0), 10),
|
||||||
|
goal: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Stage 3: Rainmaker",
|
||||||
|
requirement: { type: "leadsGained", amount: 25 },
|
||||||
|
reward: { type: "cash", name: "$150 Performance Bonus", icon: "cash" },
|
||||||
|
progress: Math.min((user.leadsGained || 0), 25),
|
||||||
|
goal: 25
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Determine lock states
|
||||||
|
const stages = STAGES.map((s, idx) => {
|
||||||
|
let status = "locked";
|
||||||
|
if (s.progress >= s.goal) {
|
||||||
|
status = "completed";
|
||||||
|
} else if (idx === 0 || STAGES[idx - 1].progress >= STAGES[idx - 1].goal) {
|
||||||
|
status = "in-progress";
|
||||||
|
}
|
||||||
|
return { ...s, status };
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeStage = stages.find(s => s.status === "in-progress") || stages[stages.length - 1];
|
||||||
|
|
||||||
|
// --- PHASE 3: DETAILED BADGE ARCHITECTURE ---
|
||||||
|
// Mapping known string arrays from mockStore into detailed objects
|
||||||
|
const BADGE_DEFINITIONS = [
|
||||||
|
{ id: 'Hot Spot Hunter', title: "Hot Spot Hunter", desc: "Identify 10 high-damage zones.", criteria: "10 Hot Leads identified." },
|
||||||
|
{ id: 'Storm Chaser', title: "Storm Chaser", desc: "Canvass a territory within 48h of a storm.", criteria: "1 Storm deployed operation." },
|
||||||
|
{ id: 'Star Closer', title: "Star Closer", desc: "Maintain a 50% conversion rate.", criteria: "Convert 5 deals continuously." },
|
||||||
|
{ id: 'Map Master', title: "Map Master", desc: "Knock 500 total doors.", criteria: "500 Lifetime Doors." },
|
||||||
|
{ id: 'First Knock', title: "First Knock", desc: "Complete your first day in the field.", criteria: "Log your first shift." }
|
||||||
|
];
|
||||||
|
|
||||||
|
const badges = BADGE_DEFINITIONS.map(def => {
|
||||||
|
const isUnlocked = (user.achievements || []).includes(def.id);
|
||||||
|
return { ...def, status: isUnlocked ? 'unlocked' : 'locked' };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Derived Stats
|
||||||
|
const stats = {
|
||||||
|
doorsKnocked: user.doorsKnocked || 0,
|
||||||
|
leadsGained: user.leadsGained || 0,
|
||||||
|
appointmentsSet: user.appointmentsSet || 0,
|
||||||
|
streakDays: user.streakDays || 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
level,
|
||||||
|
title,
|
||||||
|
currentXp: xp,
|
||||||
|
nextLevelXp,
|
||||||
|
xpProgress,
|
||||||
|
xpToNext,
|
||||||
|
stats,
|
||||||
|
badges,
|
||||||
|
stages,
|
||||||
|
activeStage,
|
||||||
|
achievements: user.achievements || [] // keeping for legacy
|
||||||
|
};
|
||||||
|
}, [user]);
|
||||||
|
};
|
||||||
+511
-17
@@ -1,26 +1,520 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Construction } from 'lucide-react';
|
import { motion } from 'framer-motion';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Flame, Target, Crosshair, Trophy, UploadCloud,
|
||||||
|
Shield, Zap, Star, Medal, Map, Activity,
|
||||||
|
Users, TrendingUp, CheckCircle, Lock, FastForward, Award, Calendar
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import { useMockStore } from '../data/mockStore';
|
||||||
|
import { useGamification } from '../hooks/useGamification';
|
||||||
import { SpotlightCard } from '../components/SpotlightCard';
|
import { SpotlightCard } from '../components/SpotlightCard';
|
||||||
|
import { AnimatedCounter } from '../components/AnimatedCounter';
|
||||||
|
|
||||||
const ProCanvas = () => {
|
// --- NEOMORPHIC STYLING CONSTANTS ---
|
||||||
return (
|
const NEO_PANEL_CLASS = "bg-zinc-900/40 backdrop-blur-3xl border border-white/5 shadow-[8px_8px_16px_rgba(0,0,0,0.4),-8px_-8px_16px_rgba(255,255,255,0.02)] rounded-2xl relative overflow-hidden transition-all duration-300";
|
||||||
<div className="min-h-full bg-zinc-50 dark:bg-[#09090b] text-zinc-900 dark:text-white p-8 flex items-center justify-center">
|
const NEON_GREEN = "text-[#39ff14] drop-shadow-[0_0_8px_rgba(57,255,20,0.4)]";
|
||||||
<SpotlightCard className="max-w-2xl w-full p-12 text-center">
|
const NEON_GOLD = "text-[#fda913] drop-shadow-[0_0_8px_rgba(253,169,19,0.4)]";
|
||||||
<div className="flex justify-center mb-6">
|
const NEON_ORANGE = "text-[#ff4500] drop-shadow-[0_0_8px_rgba(255,69,0,0.4)]";
|
||||||
<Construction size={64} className="text-[#fda913] animate-pulse" />
|
const NEON_BLUE = "text-[#00f0ff] drop-shadow-[0_0_8px_rgba(0,240,255,0.4)]";
|
||||||
</div>
|
|
||||||
<h1 className="text-4xl font-extrabold mb-4">
|
// -----------------------------------------------------------------
|
||||||
Pro<span className="text-[#fda913]">Canvas</span>
|
// SHARED UI COMPONENTS
|
||||||
</h1>
|
// -----------------------------------------------------------------
|
||||||
<p className="text-xl text-zinc-500 dark:text-zinc-400 mb-8 font-light">
|
|
||||||
This module is currently under construction.
|
const NeoCard = ({ children, className = "", innerClassName = "", spotlightColor = "rgba(255, 255, 255, 0.05)" }) => (
|
||||||
</p>
|
<SpotlightCard className={`${NEO_PANEL_CLASS} h-full w-full ${className}`} spotlightColor={spotlightColor}>
|
||||||
<div className="p-4 bg-zinc-100 dark:bg-white/5 rounded-xl border border-dashed border-zinc-300 dark:border-white/10">
|
<div className={`relative z-10 w-full h-full p-4 md:p-6 flex flex-col ${innerClassName}`}>
|
||||||
<p className="text-sm font-mono text-[#fda913]">STATUS: DEVELOPMENT_IN_PROGRESS</p>
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</SpotlightCard>
|
</SpotlightCard>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ProgressBar = ({ progress, goal, colorClass = "bg-[#39ff14]", shadowClass = "shadow-[0_0_10px_#39ff14]" }) => {
|
||||||
|
const pct = Math.min((progress / goal) * 100, 100);
|
||||||
|
return (
|
||||||
|
<div className="h-2 w-full bg-black/80 rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.6)] border border-white/5 relative overflow-hidden flex-shrink-0">
|
||||||
|
<motion.div
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${pct}%` }}
|
||||||
|
transition={{ duration: 1.5, ease: "easeOut" }}
|
||||||
|
className={`absolute top-0 left-0 h-full rounded-full ${colorClass} ${shadowClass}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProCanvas;
|
// -----------------------------------------------------------------
|
||||||
|
// ROLE-AGNOSTIC WIDGETS
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
const OperatorProfile = ({ gamestate, user, className = "" }) => {
|
||||||
|
return (
|
||||||
|
<NeoCard spotlightColor="rgba(253, 169, 19, 0.1)" className={className} innerClassName="justify-center">
|
||||||
|
<div className="flex flex-col md:flex-row items-center gap-6 md:gap-8">
|
||||||
|
<div className="relative w-28 h-28 md:w-32 md:h-32 flex-shrink-0">
|
||||||
|
<motion.svg
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 30, repeat: Infinity, ease: "linear" }}
|
||||||
|
className="absolute inset-0 w-full h-full text-[#fda913]/30"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
>
|
||||||
|
<circle cx="50" cy="50" r="48" fill="none" stroke="currentColor" strokeWidth="1" strokeDasharray="4 4" />
|
||||||
|
<circle cx="50" cy="50" r="42" fill="none" stroke="currentColor" strokeWidth="2" strokeDasharray="20 10" />
|
||||||
|
</motion.svg>
|
||||||
|
<div className="absolute inset-3 rounded-full bg-zinc-900 border border-white/10 shadow-[inset_4px_4px_10px_rgba(0,0,0,0.8),inset_-4px_-4px_10px_rgba(255,255,255,0.05)] flex items-center justify-center overflow-hidden">
|
||||||
|
<span className="text-3xl md:text-4xl font-black text-transparent bg-clip-text bg-gradient-to-br from-white to-zinc-500 md:translate-y-0.5">
|
||||||
|
{user.name.split(' ').map(n => n[0]).join('')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 text-center md:text-left z-10 w-full flex flex-col justify-center">
|
||||||
|
<div className="flex justify-between items-start mb-4 gap-4 md:gap-8 w-full">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-[10px] font-mono text-zinc-500 tracking-[0.2em] mb-1 uppercase truncate">Op ID: {user.empId || 'UNK-99'}</p>
|
||||||
|
<h2 className="text-2xl md:text-3xl font-black text-white tracking-wide truncate">{user.name}</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end flex-shrink-0">
|
||||||
|
<span className={`text-xl font-black ${NEON_GOLD}`}>LVL <AnimatedCounter to={gamestate.level} duration={1} /></span>
|
||||||
|
<span className="text-xs font-mono text-zinc-400 tracking-widest uppercase whitespace-nowrap">{gamestate.title}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full mt-2">
|
||||||
|
<div className="flex justify-between text-xs font-mono mb-2">
|
||||||
|
<span className="text-zinc-400">XP <span className="text-white font-bold"><AnimatedCounter to={gamestate.currentXp} /></span></span>
|
||||||
|
<span className="text-zinc-500">NEXT <span className="text-zinc-300">{gamestate.nextLevelXp.toLocaleString()}</span></span>
|
||||||
|
</div>
|
||||||
|
<ProgressBar progress={gamestate.currentXp} goal={gamestate.nextLevelXp} colorClass="bg-gradient-to-r from-amber-600 to-[#fda913]" shadowClass="shadow-[0_0_15px_#fda913]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NeoCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LiveMissionTracker = ({ stage, className = "" }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NeoCard spotlightColor="rgba(57, 255, 20, 0.1)" className={className} innerClassName="justify-between h-full">
|
||||||
|
<div className="flex flex-col h-full w-full">
|
||||||
|
<div className="flex items-center gap-3 mb-6 flex-shrink-0">
|
||||||
|
<Crosshair className={NEON_GREEN} size={24} />
|
||||||
|
<h3 className="text-sm md:text-base font-mono font-bold text-white tracking-widest uppercase">Live Mission Tracker</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4 flex-shrink-0">
|
||||||
|
<p className="text-[10px] text-zinc-500 font-mono tracking-widest mb-1 uppercase">ACTIVE DIRECTIVE</p>
|
||||||
|
<p className="text-lg md:text-xl text-white font-bold mb-1 truncate">{stage.title}</p>
|
||||||
|
<p className="text-xs md:text-sm text-zinc-400 font-mono truncate">
|
||||||
|
Target: {stage.goal} {stage.requirement.type.replace(/([A-Z])/g, ' $1').trim()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 p-4 bg-black/30 rounded-xl shadow-[inset_0_2px_10px_rgba(0,0,0,0.4)] border border-white/5 mb-6 flex-shrink-0">
|
||||||
|
<div className="flex justify-between font-mono text-xs">
|
||||||
|
<span className={NEON_GREEN}>PROGRESS</span>
|
||||||
|
<span className="text-white font-bold"><AnimatedCounter to={stage.progress} /> / {stage.goal}</span>
|
||||||
|
</div>
|
||||||
|
<ProgressBar progress={stage.progress} goal={stage.goal} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/emp/fa/maps')}
|
||||||
|
className="w-full group mt-auto relative overflow-hidden rounded-xl bg-zinc-900 border border-[#39ff14]/30 p-4 shadow-[4px_4px_15px_rgba(0,0,0,0.5),-4px_-4px_15px_rgba(255,255,255,0.02)] transition-all hover:-translate-y-1 hover:shadow-[#39ff14]/20 flex-shrink-0"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-[#39ff14]/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
|
<span className={`relative z-10 font-mono font-bold tracking-[0.2em] text-sm md:text-base uppercase flex justify-center w-full ${NEON_GREEN}`}>
|
||||||
|
[ GOCANVAS ]
|
||||||
|
</span>
|
||||||
|
<span className="absolute inset-0 border border-[#39ff14] rounded-xl animate-ping opacity-10" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</NeoCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CheckpointsSystem = ({ stages, className = "" }) => {
|
||||||
|
return (
|
||||||
|
<NeoCard spotlightColor="rgba(0, 240, 255, 0.05)" className={className}>
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<FastForward className={NEON_BLUE} size={24} />
|
||||||
|
<h3 className="text-lg font-mono font-bold text-white tracking-widest uppercase">Rewards & Checkpoints</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 relative w-full flex-1">
|
||||||
|
{/* Connecting Line for Desktop */}
|
||||||
|
<div className="hidden md:block absolute top-[28px] left-[15%] right-[15%] h-0.5 bg-zinc-800 rounded z-0" />
|
||||||
|
|
||||||
|
{stages.map((stage, idx) => {
|
||||||
|
const isLocked = stage.status === 'locked';
|
||||||
|
const isCompleted = stage.status === 'completed';
|
||||||
|
const isInProgress = stage.status === 'in-progress';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={stage.id} className="relative z-10 flex flex-col items-center md:items-start text-center md:text-left bg-black/40 md:bg-transparent p-4 md:p-0 rounded-xl md:rounded-none border border-white/5 md:border-none shadow-[inset_0_2px_10px_rgba(0,0,0,0.5)] md:shadow-none h-full">
|
||||||
|
{/* Icon Node */}
|
||||||
|
<div className={`w-14 h-14 flex-shrink-0 rounded-xl flex items-center justify-center mb-4 transition-all duration-500 mx-auto md:mx-0
|
||||||
|
${isCompleted ? 'bg-[#39ff14]/20 text-[#39ff14] border-2 border-[#39ff14] shadow-[0_0_15px_#39ff14]' :
|
||||||
|
isInProgress ? 'bg-amber-500/20 text-amber-500 border-2 border-amber-500 shadow-[0_0_15px_#fda913] animate-pulse' :
|
||||||
|
'bg-zinc-900 text-zinc-600 border border-zinc-700'}
|
||||||
|
`}>
|
||||||
|
{isCompleted ? <CheckCircle size={24} /> : isLocked ? <Lock size={24} /> : <Target size={24} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 className="font-bold text-white mb-1">{stage.title}</h4>
|
||||||
|
<p className="text-xs font-mono text-zinc-400 mb-3">{stage.progress} / {stage.goal} {stage.requirement.type.replace(/([A-Z])/g, ' $1').trim()}</p>
|
||||||
|
|
||||||
|
{/* Reward Box */}
|
||||||
|
<div className={`px-3 py-2 mt-auto rounded flex items-center gap-2 text-[10px] font-mono w-full justify-center md:justify-start
|
||||||
|
${isCompleted ? 'bg-[#39ff14]/10 border border-[#39ff14]/30 text-[#39ff14]' : 'bg-black/50 border border-white/5 text-zinc-500'}
|
||||||
|
`}>
|
||||||
|
<Award size={14} className="flex-shrink-0" />
|
||||||
|
<span className="truncate">{stage.reward.name} {isLocked ? '(LOCKED)' : ''}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</NeoCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DetailedBadges = ({ badges, className = "" }) => {
|
||||||
|
return (
|
||||||
|
<NeoCard className={className}>
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<Medal className={NEON_GOLD} size={24} />
|
||||||
|
<h3 className="text-lg font-mono font-bold text-white tracking-widest uppercase">Commendations & Badges</h3>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
|
{badges.map((badge, idx) => {
|
||||||
|
const isUnlocked = badge.status === 'unlocked';
|
||||||
|
|
||||||
|
let Icon = Shield;
|
||||||
|
if (badge.id.includes("Hunter")) Icon = Crosshair;
|
||||||
|
if (badge.id.includes("Storm")) Icon = Zap;
|
||||||
|
if (badge.id.includes("Star")) Icon = Star;
|
||||||
|
if (badge.id.includes("Master")) Icon = Map;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={idx} className={`p-4 rounded-xl flex gap-4 transition-all duration-300 h-full
|
||||||
|
${isUnlocked ? 'bg-zinc-800/50 border border-amber-500/30 shadow-[inset_0_2px_10px_rgba(0,0,0,0.5)]' : 'bg-black/40 border border-white/5 opacity-60 grayscale'}`
|
||||||
|
}>
|
||||||
|
<div className={`w-12 h-12 rounded-lg flex-shrink-0 flex items-center justify-center
|
||||||
|
${isUnlocked ? 'bg-black border border-amber-500/50 shadow-[0_0_10px_#fda913]' : 'bg-zinc-900 border border-zinc-700'}`
|
||||||
|
}>
|
||||||
|
<Icon size={20} className={isUnlocked ? NEON_GOLD : 'text-zinc-600'} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-center min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h4 className="font-bold text-sm text-white truncate">{badge.title}</h4>
|
||||||
|
{!isUnlocked && <Lock size={12} className="text-zinc-600 flex-shrink-0" />}
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] md:text-xs text-zinc-400 font-mono mb-2 leading-tight line-clamp-2">{badge.desc}</p>
|
||||||
|
<p className="text-[9px] text-[#39ff14] font-mono tracking-widest uppercase opacity-80 truncate">CRITERIA: {badge.criteria}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</NeoCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Leaderboard = ({ title = "Live Intel Feed", icon: Icon = Target, className = "" }) => {
|
||||||
|
const leaders = [
|
||||||
|
{ rank: 1, name: "Felicity Fast", xp: 15600, delta: "+450", role: "Agent" },
|
||||||
|
{ rank: 2, name: "Frank Agent", xp: 12450, delta: "+310", role: "Agent" },
|
||||||
|
{ rank: 3, name: "Fiona Field", xp: 8200, delta: "+120", role: "Agent" },
|
||||||
|
{ rank: 4, name: "Felix Fixer", xp: 5400, delta: "+50", role: "Agent" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NeoCard className={className} innerClassName="flex flex-col h-full">
|
||||||
|
<div className="flex flex-shrink-0 items-center justify-between mb-6">
|
||||||
|
<h3 className="text-sm md:text-base font-mono text-zinc-400 font-bold tracking-widest uppercase flex items-center gap-2">
|
||||||
|
<Icon size={18} className={NEON_BLUE} /> {title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 flex-1 flex flex-col justify-center">
|
||||||
|
{leaders.map((agent, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`group relative flex items-center justify-between p-3 md:p-4 rounded-xl transition-all duration-300
|
||||||
|
${agent.rank <= 3 ? 'bg-black/40 shadow-[inset_0_2px_10px_rgba(0,0,0,0.5)] border border-white/5' : 'bg-transparent hover:bg-white/5'}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 md:gap-4 flex-1 min-w-0">
|
||||||
|
<span className={`font-mono font-bold w-6 text-lg md:text-xl flex-shrink-0 ${agent.rank === 1 ? NEON_GOLD : agent.rank === 2 ? 'text-zinc-300' : agent.rank === 3 ? 'text-amber-700' : 'text-zinc-600'}`}>
|
||||||
|
0{agent.rank}
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<span className="font-bold text-white text-sm md:text-base block truncate">{agent.name}</span>
|
||||||
|
<span className="text-[9px] md:text-[10px] text-zinc-500 uppercase font-mono tracking-widest block truncate">{agent.role}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end gap-1 font-mono text-xs md:text-sm flex-shrink-0">
|
||||||
|
<span className="text-white font-bold"><AnimatedCounter to={agent.xp} /> XP</span>
|
||||||
|
<span className="text-[#39ff14] text-[9px] md:text-[10px] tracking-wider">{agent.delta} Today</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</NeoCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// ADMIN / OWNER SPECIFIC WIDGETS
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
const AdminCommandCenter = ({ className = "" }) => {
|
||||||
|
return (
|
||||||
|
<NeoCard spotlightColor="rgba(0, 240, 255, 0.1)" className={className} innerClassName="flex flex-col justify-between h-full">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<Activity className={NEON_BLUE} size={28} />
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl md:text-2xl font-black text-white tracking-widest uppercase">Global Control</h2>
|
||||||
|
<p className="font-mono text-[10px] md:text-xs text-zinc-500 tracking-[0.2em]">Administrative Overseer Panel</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 flex-1">
|
||||||
|
<div className="p-4 md:p-5 rounded-2xl bg-black/40 border border-white/5 shadow-[inset_0_2px_15px_rgba(0,0,0,0.5)] flex flex-col justify-center">
|
||||||
|
<p className="text-[10px] md:text-xs font-mono text-zinc-400 tracking-widest mb-2">ACTIVE SQUADS</p>
|
||||||
|
<div className="flex items-end gap-3">
|
||||||
|
<span className={`text-3xl md:text-4xl font-black ${NEON_BLUE}`}><AnimatedCounter to={12} /></span>
|
||||||
|
<span className="text-[10px] md:text-sm font-mono text-[#39ff14] pb-1">All Online</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 md:p-5 rounded-2xl bg-black/40 border border-white/5 shadow-[inset_0_2px_15px_rgba(0,0,0,0.5)] flex flex-col justify-center">
|
||||||
|
<p className="text-[10px] md:text-xs font-mono text-zinc-400 tracking-widest mb-2">DOORS KNOCKED TODAY</p>
|
||||||
|
<div className="flex items-end gap-3">
|
||||||
|
<span className={`text-3xl md:text-4xl font-black ${NEON_GOLD}`}><AnimatedCounter to={1432} /></span>
|
||||||
|
<span className="text-[10px] md:text-sm font-mono text-[#39ff14] pb-1">+14% vs Yday</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 md:p-5 rounded-2xl bg-black/40 border border-white/5 shadow-[inset_0_2px_15px_rgba(0,0,0,0.5)] flex flex-col justify-between h-full">
|
||||||
|
<p className="text-[10px] md:text-xs font-mono text-zinc-400 tracking-widest mb-2">APPROVAL QUEUE</p>
|
||||||
|
<div className="flex items-center justify-between mt-auto">
|
||||||
|
<span className={`text-3xl md:text-4xl font-black ${NEON_ORANGE}`}><AnimatedCounter to={8} /></span>
|
||||||
|
<button className="px-3 md:px-4 py-2 bg-zinc-800 border border-white/10 rounded-lg text-[10px] md:text-xs font-mono text-white hover:bg-zinc-700 transition-colors">
|
||||||
|
REVIEW
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NeoCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OwnerROIDashboard = ({ className = "" }) => {
|
||||||
|
return (
|
||||||
|
<NeoCard spotlightColor="rgba(253, 169, 19, 0.1)" className={className} innerClassName="flex flex-col justify-between h-full">
|
||||||
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between mb-8 gap-4 flex-shrink-0">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<TrendingUp className={NEON_GOLD} size={28} />
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl md:text-2xl font-black text-white tracking-widest uppercase">Executive ROI</h2>
|
||||||
|
<p className="font-mono text-[10px] md:text-xs text-zinc-500 tracking-[0.2em]">Gamification Investment Returns</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-3 bg-black/50 border border-amber-500/20 rounded-lg flex items-center gap-4 shadow-[inset_0_2px_10px_rgba(0,0,0,0.5)] w-full md:w-auto overflow-hidden">
|
||||||
|
<span className="text-[10px] md:text-xs font-mono text-zinc-400 whitespace-nowrap">Program Cost YTD:</span>
|
||||||
|
<span className={`text-base md:text-lg font-bold ${NEON_ORANGE}`}>$<AnimatedCounter to={12450} /></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 flex-1">
|
||||||
|
<div className="space-y-6 flex flex-col justify-center h-full">
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex justify-between text-[10px] md:text-xs font-mono mb-2">
|
||||||
|
<span className="text-zinc-400">TEAM PARTICIPATION</span>
|
||||||
|
<span className="text-[#39ff14]"><AnimatedCounter to={92} />%</span>
|
||||||
|
</div>
|
||||||
|
<ProgressBar progress={92} goal={100} />
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex justify-between text-[10px] md:text-xs font-mono mb-2">
|
||||||
|
<span className="text-zinc-400">PRIZE FUND DISPERSED</span>
|
||||||
|
<span className="text-[#fda913]"><AnimatedCounter to={68} />%</span>
|
||||||
|
</div>
|
||||||
|
<ProgressBar progress={68} goal={100} colorClass="bg-[#fda913]" shadowClass="shadow-[0_0_10px_#fda913]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3 md:gap-4 h-full">
|
||||||
|
<div className="p-4 rounded-xl bg-zinc-900/50 border border-white/5 flex flex-col justify-center items-center text-center shadow-[inset_0_2px_10px_rgba(0,0,0,0.3)] h-full">
|
||||||
|
<p className="text-[9px] md:text-[10px] font-mono text-zinc-500 mb-2">GAMIFIED CONVERSION</p>
|
||||||
|
<p className={`text-2xl md:text-3xl font-black ${NEON_GREEN}`}>+<AnimatedCounter to={24} />%</p>
|
||||||
|
<p className="text-[9px] md:text-[10px] text-zinc-400 mt-1">vs Non-participants</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 rounded-xl bg-zinc-900/50 border border-white/5 flex flex-col justify-center items-center text-center shadow-[inset_0_2px_10px_rgba(0,0,0,0.3)] h-full">
|
||||||
|
<p className="text-[9px] md:text-[10px] font-mono text-zinc-500 mb-2">GENERATED REVENUE</p>
|
||||||
|
<p className={`text-2xl md:text-3xl font-black ${NEON_GOLD}`}>$1.2M</p>
|
||||||
|
<p className="text-[9px] md:text-[10px] text-zinc-400 mt-1">Directly attributed</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NeoCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// SHARED ACTION WIDGETS
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
const QuickLogAction = ({ className = "", gridClassName = "grid grid-cols-2 gap-3 flex-1" }) => {
|
||||||
|
return (
|
||||||
|
<NeoCard spotlightColor="rgba(57, 255, 20, 0.1)" className={className} innerClassName="flex flex-col h-full">
|
||||||
|
<div className="flex items-center gap-3 mb-4 flex-shrink-0">
|
||||||
|
<Zap className={NEON_GREEN} size={20} />
|
||||||
|
<h3 className="text-sm md:text-base font-mono font-bold text-white tracking-widest uppercase">Quick Operations Log</h3>
|
||||||
|
</div>
|
||||||
|
<div className={`w-full ${gridClassName}`}>
|
||||||
|
<button className="h-full p-1.5 md:p-2 rounded-xl bg-black/40 border border-white/5 hover:bg-zinc-800 hover:border-[#39ff14]/50 hover:shadow-[0_0_15px_rgba(57,255,20,0.2)] transition-all flex flex-col items-center justify-center gap-1 md:gap-2 group cursor-crosshair min-h-0">
|
||||||
|
<Map size={18} className="text-zinc-500 group-hover:text-[#39ff14] transition-colors flex-shrink-0" />
|
||||||
|
<span className="text-[9px] md:text-[10px] text-center font-mono text-zinc-400 group-hover:text-white transition-colors truncate w-full px-1">Door Knock</span>
|
||||||
|
</button>
|
||||||
|
<button className="h-full p-1.5 md:p-2 rounded-xl bg-black/40 border border-white/5 hover:bg-zinc-800 hover:border-[#fda913]/50 hover:shadow-[0_0_15px_rgba(253,169,19,0.2)] transition-all flex flex-col items-center justify-center gap-1 md:gap-2 group cursor-crosshair min-h-0">
|
||||||
|
<Users size={18} className="text-zinc-500 group-hover:text-[#fda913] transition-colors flex-shrink-0" />
|
||||||
|
<span className="text-[9px] md:text-[10px] text-center font-mono text-zinc-400 group-hover:text-white transition-colors truncate w-full px-1">Lead Gained</span>
|
||||||
|
</button>
|
||||||
|
<button className="h-full p-1.5 md:p-2 rounded-xl bg-black/40 border border-white/5 hover:bg-zinc-800 hover:border-[#00f0ff]/50 hover:shadow-[0_0_15px_rgba(0,240,255,0.2)] transition-all flex flex-col items-center justify-center gap-1 md:gap-2 group cursor-crosshair min-h-0">
|
||||||
|
<Calendar size={18} className="text-zinc-500 group-hover:text-[#00f0ff] transition-colors flex-shrink-0" />
|
||||||
|
<span className="text-[9px] md:text-[10px] text-center font-mono text-zinc-400 group-hover:text-white transition-colors truncate w-full px-1">Appt Set</span>
|
||||||
|
</button>
|
||||||
|
<button className="h-full p-1.5 md:p-2 rounded-xl bg-black/40 border border-white/5 hover:bg-zinc-800 hover:border-pink-500/50 hover:shadow-[0_0_15px_rgba(236,72,153,0.2)] transition-all flex flex-col items-center justify-center gap-1 md:gap-2 group cursor-crosshair min-h-0">
|
||||||
|
<CheckCircle size={18} className="text-zinc-500 group-hover:text-pink-500 transition-colors flex-shrink-0" />
|
||||||
|
<span className="text-[9px] md:text-[10px] text-center font-mono text-zinc-400 group-hover:text-white transition-colors truncate w-full px-1">Deal Closed</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</NeoCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ScanUpload = ({ className = "" }) => {
|
||||||
|
return (
|
||||||
|
<NeoCard className={className} innerClassName="flex flex-col items-center justify-center text-center h-full group cursor-pointer w-full">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-zinc-900 shadow-[inset_4px_4px_8px_rgba(0,0,0,0.5),inset_-4px_-4px_8px_rgba(255,255,255,0.05)] flex items-center justify-center mb-4 group-hover:shadow-[0_0_15px_#39ff14] transition-all duration-300 transform group-hover:-translate-y-2">
|
||||||
|
<UploadCloud size={28} className="text-zinc-500 group-hover:text-[#39ff14] transition-colors" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-mono text-sm tracking-widest text-white mb-2">SCAN UPLOAD</h4>
|
||||||
|
<p className="text-[10px] text-zinc-500 font-mono mb-0">DRAG RECON INTEL HERE (+500XP)</p>
|
||||||
|
</NeoCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// MAIN COMPONENT EXPORT
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
export default function ProCanvas() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { users } = useMockStore();
|
||||||
|
|
||||||
|
const role = user?.role || 'FIELD_AGENT';
|
||||||
|
|
||||||
|
const mockDbUser = users.find(u => u.email === user?.email) || users.find(u => u.roles?.includes('FIELD_AGENT')) || users.find(u => u.type === 'employee') ||
|
||||||
|
{
|
||||||
|
name: user?.name || 'Agent Unknown',
|
||||||
|
empId: 'OP-00',
|
||||||
|
xp: 12450,
|
||||||
|
doorsKnocked: 342,
|
||||||
|
leadsGained: 45,
|
||||||
|
appointmentsSet: 18,
|
||||||
|
streakDays: 5,
|
||||||
|
achievements: ['Hot Spot Hunter', 'Storm Chaser']
|
||||||
|
};
|
||||||
|
|
||||||
|
const gamestate = useGamification(mockDbUser);
|
||||||
|
|
||||||
|
if (!gamestate) return <div className="p-8 text-white font-mono flex items-center justify-center h-screen bg-[#050505]">INITIALIZING HUD...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-full bg-[#09090b] p-4 md:p-8 overflow-y-auto selection:bg-[#fda913] selection:text-black">
|
||||||
|
{/* Hex Grid Background & Scanlines */}
|
||||||
|
<div className="fixed inset-0 pointer-events-none z-0">
|
||||||
|
<div className="absolute inset-0 opacity-20">
|
||||||
|
<svg className="w-full h-full" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<pattern id="hexagons" width="50" height="43.4" patternUnits="userSpaceOnUse" patternTransform="scale(2)">
|
||||||
|
<path d="M25 0 L50 14.4 L50 43.3 L25 57.7 L0 43.3 L0 14.4 Z" fill="none" stroke="rgba(255,255,255,0.05)" strokeWidth="1" />
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" fill="url(#hexagons)" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{/* CSS Scanline Overlay */}
|
||||||
|
<div className="absolute inset-0 opacity-[0.03] bg-[linear-gradient(to_bottom,transparent_50%,rgba(0,0,0,0.5)_50%)] bg-[length:100%_4px]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6 md:space-y-8 relative z-10 w-full">
|
||||||
|
|
||||||
|
<header className="mb-6 md:mb-10 flex flex-col md:flex-row justify-between items-start md:items-end gap-4 border-b border-white/10 pb-6 w-full">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl md:text-4xl font-black text-transparent bg-clip-text bg-gradient-to-r from-white via-zinc-300 to-zinc-600 tracking-tighter drop-shadow-xl">
|
||||||
|
PRO<span className="text-[#fda913]">CANVAS</span>
|
||||||
|
</h1>
|
||||||
|
<p className="font-mono text-zinc-500 text-xs md:text-sm tracking-widest uppercase mt-2">
|
||||||
|
{role === 'FIELD_AGENT' ? 'Tactical HUD Controller' : role === 'ADMIN' ? 'Command Center Dashboard' : 'Executive ROI Overview'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="font-mono text-[9px] md:text-[10px] text-[#39ff14] flex flex-col md:items-end opacity-70 bg-black/40 p-2 md:p-3 rounded border border-[#39ff14]/20 shadow-[0_0_10px_rgba(57,255,20,0.1)] w-full md:w-auto">
|
||||||
|
<span className="flex justify-between md:justify-end gap-8 w-full">SYS_STATUS: <span>ONLINE</span></span>
|
||||||
|
<span className="flex justify-between md:justify-end gap-8 w-full">NET_SECURE: <span>TRUE</span></span>
|
||||||
|
<span className="flex justify-between md:justify-end gap-8 w-full text-zinc-500">AUTH_LVL: <span>{role}</span></span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 md:gap-8 w-full items-stretch">
|
||||||
|
|
||||||
|
{/* Unified Top Row */}
|
||||||
|
<OperatorProfile gamestate={gamestate} user={mockDbUser} className="lg:col-span-2" />
|
||||||
|
<LiveMissionTracker stage={gamestate.activeStage} className="lg:col-span-1" />
|
||||||
|
|
||||||
|
{/* Checkpoints & Stages */}
|
||||||
|
<CheckpointsSystem stages={gamestate.stages} className="lg:col-span-3" />
|
||||||
|
|
||||||
|
{/* Field Agent Only Features */}
|
||||||
|
{role === 'FIELD_AGENT' && (
|
||||||
|
<DetailedBadges badges={gamestate.badges} className="lg:col-span-3" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Owner / Admin Tops */}
|
||||||
|
{role === 'OWNER' && (
|
||||||
|
<OwnerROIDashboard className="lg:col-span-3" />
|
||||||
|
)}
|
||||||
|
{(role === 'ADMIN' || role === 'OWNER') && (
|
||||||
|
<AdminCommandCenter className="lg:col-span-3" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Shared Bottom Row */}
|
||||||
|
<Leaderboard
|
||||||
|
title={role === 'FIELD_AGENT' ? "Squad Rankings" : "Global Leaderboard"}
|
||||||
|
icon={role === 'OWNER' ? Star : Trophy}
|
||||||
|
className="lg:col-span-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Side Column spanning same height as Leaderboard */}
|
||||||
|
<div className="lg:col-span-1 flex flex-col gap-6 md:gap-8 h-full w-full min-h-0">
|
||||||
|
<QuickLogAction
|
||||||
|
className={role === 'FIELD_AGENT' ? "flex-1 min-h-0" : "h-full min-h-0"}
|
||||||
|
gridClassName="grid grid-cols-2 gap-2 lg:gap-3 flex-1 min-h-0"
|
||||||
|
/>
|
||||||
|
{role === 'FIELD_AGENT' && (
|
||||||
|
<ScanUpload className="flex-1 min-h-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Construction } from 'lucide-react';
|
||||||
|
import { SpotlightCard } from '../components/SpotlightCard';
|
||||||
|
|
||||||
|
const UnderConstruction = ({ title = "Under Construction", moduleName = "Module" }) => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-full bg-zinc-50 dark:bg-[#09090b] text-zinc-900 dark:text-white p-8 flex items-center justify-center">
|
||||||
|
<SpotlightCard className="max-w-2xl w-full p-12 text-center">
|
||||||
|
<div className="flex justify-center mb-6">
|
||||||
|
<Construction size={64} className="text-[#fda913] animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-extrabold mb-4">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-zinc-500 dark:text-zinc-400 mb-8 font-light">
|
||||||
|
This {moduleName} is currently under construction.
|
||||||
|
</p>
|
||||||
|
<div className="p-4 bg-zinc-100 dark:bg-white/5 rounded-xl border border-dashed border-zinc-300 dark:border-white/10">
|
||||||
|
<p className="text-sm font-mono text-[#fda913]">STATUS: DEVELOPMENT_IN_PROGRESS</p>
|
||||||
|
</div>
|
||||||
|
</SpotlightCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnderConstruction;
|
||||||
@@ -198,7 +198,7 @@ const OwnerSnapshot = () => {
|
|||||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white mb-1">Budget vs Actual Spend</h3>
|
<h3 className="text-lg font-bold text-zinc-900 dark:text-white mb-1">Budget vs Actual Spend</h3>
|
||||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mb-4">Per project (in $k)</p>
|
<p className="text-xs text-zinc-500 dark:text-zinc-400 mb-4">Per project (in $k)</p>
|
||||||
<div className="h-64">
|
<div className="h-64">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height={256}>
|
||||||
<BarChart data={budgetChartData} barGap={4}>
|
<BarChart data={budgetChartData} barGap={4}>
|
||||||
<XAxis dataKey="name" tick={{ fontSize: 11, fill: '#a1a1aa' }} axisLine={false} tickLine={false} />
|
<XAxis dataKey="name" tick={{ fontSize: 11, fill: '#a1a1aa' }} axisLine={false} tickLine={false} />
|
||||||
<YAxis tick={{ fontSize: 11, fill: '#a1a1aa' }} axisLine={false} tickLine={false} />
|
<YAxis tick={{ fontSize: 11, fill: '#a1a1aa' }} axisLine={false} tickLine={false} />
|
||||||
@@ -215,7 +215,7 @@ const OwnerSnapshot = () => {
|
|||||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white mb-1">Project Status</h3>
|
<h3 className="text-lg font-bold text-zinc-900 dark:text-white mb-1">Project Status</h3>
|
||||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mb-4">Distribution</p>
|
<p className="text-xs text-zinc-500 dark:text-zinc-400 mb-4">Distribution</p>
|
||||||
<div className="h-64 flex items-center justify-center">
|
<div className="h-64 flex items-center justify-center">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height={256}>
|
||||||
<PieChart>
|
<PieChart>
|
||||||
<Pie
|
<Pie
|
||||||
data={statusChartData}
|
data={statusChartData}
|
||||||
@@ -244,7 +244,7 @@ const OwnerSnapshot = () => {
|
|||||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white mb-1">Monthly Spend Trend</h3>
|
<h3 className="text-lg font-bold text-zinc-900 dark:text-white mb-1">Monthly Spend Trend</h3>
|
||||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mb-4">Last 12 months (in $k)</p>
|
<p className="text-xs text-zinc-500 dark:text-zinc-400 mb-4">Last 12 months (in $k)</p>
|
||||||
<div className="h-56">
|
<div className="h-56">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height={224}>
|
||||||
<BarChart data={monthlySpendData}>
|
<BarChart data={monthlySpendData}>
|
||||||
<XAxis dataKey="month" tick={{ fontSize: 11, fill: '#a1a1aa' }} axisLine={false} tickLine={false} />
|
<XAxis dataKey="month" tick={{ fontSize: 11, fill: '#a1a1aa' }} axisLine={false} tickLine={false} />
|
||||||
<YAxis tick={{ fontSize: 11, fill: '#a1a1aa' }} axisLine={false} tickLine={false} />
|
<YAxis tick={{ fontSize: 11, fill: '#a1a1aa' }} axisLine={false} tickLine={false} />
|
||||||
|
|||||||
Reference in New Issue
Block a user