diff --git a/src/App.jsx b/src/App.jsx index 173c825..b86c217 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -89,6 +89,14 @@ function App() { } /> + + + + } + /> {/* Owner Routes */} @@ -165,6 +173,14 @@ function App() { } /> + + + + } + /> {/* Contractor Routes */} { + 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 ( + + {value.toLocaleString()} + + ); +}; diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx index afd94bd..f91bf5f 100644 --- a/src/components/Layout.jsx +++ b/src/components/Layout.jsx @@ -154,6 +154,11 @@ const Layout = () => { { to: "/admin/schedule", icon: Calendar, label: "Schedule" }, { to: "/admin/leaderboard", icon: Trophy, label: "Leaderboard" }, { to: "/admin/maps", icon: Map, label: "Territory Map" }, + { + to: "/admin/pro-canvas", + icon: LayoutDashboard, + label: ProCanvas + }, ...commonItems, ]; case 'CONTRACTOR': @@ -173,6 +178,11 @@ const Layout = () => { return [ { to: "/emp/fa/dashboard", icon: LayoutDashboard, label: "Dashboard" }, { to: "/emp/fa/maps", icon: Map, label: "My Map" }, + { + to: "/emp/fa/pro-canvas", + icon: LayoutDashboard, + label: ProCanvas + }, ...commonItems, ]; default: // Customer or Fallback diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index ae73a01..73f684f 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -653,11 +653,11 @@ const MOCK_USERS = [ { id: 'c3', type: 'customer', username: 'charlie', email: 'charlie@example.com', password: 'password', name: 'Charlie Client' }, // Field Agents (5 Agents) - { id: 'e1', type: 'employee', empId: 'FA001', email: 'agent1@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Frank Agent' }, - { id: 'e2', type: 'employee', empId: 'FA002', email: 'agent2@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Fiona Field' }, - { id: 'e3', type: 'employee', empId: 'FA003', email: 'agent3@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Fred Flyer' }, - { id: 'e4', type: 'employee', empId: 'FA004', email: 'agent4@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Felicity Fast' }, - { id: 'e5', type: 'employee', empId: 'FA005', email: 'agent5@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Felix Fixer' }, + { 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', 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', 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', 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', xp: 5400, doorsKnocked: 156, leadsGained: 20, appointmentsSet: 8, streakDays: 1, achievements: ['First Knock'] }, // Admins (3 Admins) { id: 'a1', type: 'employee', empId: 'ADM01', email: 'admin@plano.com', password: 'password', role: 'ADMIN', name: 'Adam Admin' }, diff --git a/src/hooks/useGamification.js b/src/hooks/useGamification.js new file mode 100644 index 0000000..9b12b64 --- /dev/null +++ b/src/hooks/useGamification.js @@ -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]); +}; diff --git a/src/pages/ProCanvas.jsx b/src/pages/ProCanvas.jsx index 71c6bed..a3bc296 100644 --- a/src/pages/ProCanvas.jsx +++ b/src/pages/ProCanvas.jsx @@ -1,26 +1,520 @@ 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 { AnimatedCounter } from '../components/AnimatedCounter'; -const ProCanvas = () => { +// --- NEOMORPHIC STYLING CONSTANTS --- +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"; +const NEON_GREEN = "text-[#39ff14] drop-shadow-[0_0_8px_rgba(57,255,20,0.4)]"; +const NEON_GOLD = "text-[#fda913] drop-shadow-[0_0_8px_rgba(253,169,19,0.4)]"; +const NEON_ORANGE = "text-[#ff4500] drop-shadow-[0_0_8px_rgba(255,69,0,0.4)]"; +const NEON_BLUE = "text-[#00f0ff] drop-shadow-[0_0_8px_rgba(0,240,255,0.4)]"; + +// ----------------------------------------------------------------- +// SHARED UI COMPONENTS +// ----------------------------------------------------------------- + +const NeoCard = ({ children, className = "", innerClassName = "", spotlightColor = "rgba(255, 255, 255, 0.05)" }) => ( + +
+ {children} +
+
+); + +const ProgressBar = ({ progress, goal, colorClass = "bg-[#39ff14]", shadowClass = "shadow-[0_0_10px_#39ff14]" }) => { + const pct = Math.min((progress / goal) * 100, 100); return ( -
- -
- -
-

- ProCanvas -

-

- This module is currently under construction. -

-
-

STATUS: DEVELOPMENT_IN_PROGRESS

-
-
+
+
); }; -export default ProCanvas; +// ----------------------------------------------------------------- +// ROLE-AGNOSTIC WIDGETS +// ----------------------------------------------------------------- + +const OperatorProfile = ({ gamestate, user, className = "" }) => { + return ( + +
+
+ + + + +
+ + {user.name.split(' ').map(n => n[0]).join('')} + +
+
+ +
+
+
+

Op ID: {user.empId || 'UNK-99'}

+

{user.name}

+
+
+ LVL + {gamestate.title} +
+
+ +
+
+ XP + NEXT {gamestate.nextLevelXp.toLocaleString()} +
+ +
+
+
+
+ ); +}; + +const LiveMissionTracker = ({ stage, className = "" }) => { + const navigate = useNavigate(); + + return ( + +
+
+ +

Live Mission Tracker

+
+ +
+

ACTIVE DIRECTIVE

+

{stage.title}

+

+ Target: {stage.goal} {stage.requirement.type.replace(/([A-Z])/g, ' $1').trim()} +

+
+ +
+
+ PROGRESS + / {stage.goal} +
+ +
+ + +
+
+ ); +}; + +const CheckpointsSystem = ({ stages, className = "" }) => { + return ( + +
+ +

Rewards & Checkpoints

+
+ +
+ {/* Connecting Line for Desktop */} +
+ + {stages.map((stage, idx) => { + const isLocked = stage.status === 'locked'; + const isCompleted = stage.status === 'completed'; + const isInProgress = stage.status === 'in-progress'; + + return ( +
+ {/* Icon Node */} +
+ {isCompleted ? : isLocked ? : } +
+ +

{stage.title}

+

{stage.progress} / {stage.goal} {stage.requirement.type.replace(/([A-Z])/g, ' $1').trim()}

+ + {/* Reward Box */} +
+ + {stage.reward.name} {isLocked ? '(LOCKED)' : ''} +
+
+ ); + })} +
+ + ); +}; + +const DetailedBadges = ({ badges, className = "" }) => { + return ( + +
+ +

Commendations & Badges

+
+
+ {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 ( +
+
+ +
+
+
+

{badge.title}

+ {!isUnlocked && } +
+

{badge.desc}

+

CRITERIA: {badge.criteria}

+
+
+ ); + })} +
+
+ ); +}; + +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 ( + +
+

+ {title} +

+
+ +
+ {leaders.map((agent, i) => ( +
+
+ + 0{agent.rank} + +
+ {agent.name} + {agent.role} +
+
+
+ XP + {agent.delta} Today +
+
+ ))} +
+
+ ); +}; + +// ----------------------------------------------------------------- +// ADMIN / OWNER SPECIFIC WIDGETS +// ----------------------------------------------------------------- + +const AdminCommandCenter = ({ className = "" }) => { + return ( + +
+ +
+

Global Control

+

Administrative Overseer Panel

+
+
+ +
+
+

ACTIVE SQUADS

+
+ + All Online +
+
+ +
+

DOORS KNOCKED TODAY

+
+ + +14% vs Yday +
+
+ +
+

APPROVAL QUEUE

+
+ + +
+
+
+
+ ); +}; + +const OwnerROIDashboard = ({ className = "" }) => { + return ( + +
+
+ +
+

Executive ROI

+

Gamification Investment Returns

+
+
+
+ Program Cost YTD: + $ +
+
+ +
+
+
+
+ TEAM PARTICIPATION + % +
+ +
+
+
+ PRIZE FUND DISPERSED + % +
+ +
+
+ +
+
+

GAMIFIED CONVERSION

+

+%

+

vs Non-participants

+
+
+

GENERATED REVENUE

+

$1.2M

+

Directly attributed

+
+
+
+
+ ); +}; + +// ----------------------------------------------------------------- +// SHARED ACTION WIDGETS +// ----------------------------------------------------------------- + +const QuickLogAction = ({ className = "", gridClassName = "grid grid-cols-2 gap-3 flex-1" }) => { + return ( + +
+ +

Quick Operations Log

+
+
+ + + + +
+
+ ); +}; + +const ScanUpload = ({ className = "" }) => { + return ( + +
+ +
+

SCAN UPLOAD

+

DRAG RECON INTEL HERE (+500XP)

+
+ ); +}; + +// ----------------------------------------------------------------- +// 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
INITIALIZING HUD...
; + + return ( +
+ {/* Hex Grid Background & Scanlines */} +
+
+ + + + + + + + +
+ {/* CSS Scanline Overlay */} +
+
+ +
+ +
+
+

+ PROCANVAS +

+

+ {role === 'FIELD_AGENT' ? 'Tactical HUD Controller' : role === 'ADMIN' ? 'Command Center Dashboard' : 'Executive ROI Overview'} +

+
+
+ SYS_STATUS: ONLINE + NET_SECURE: TRUE + AUTH_LVL: {role} +
+
+ +
+ + {/* Unified Top Row */} + + + + {/* Checkpoints & Stages */} + + + {/* Field Agent Only Features */} + {role === 'FIELD_AGENT' && ( + + )} + + {/* Owner / Admin Tops */} + {role === 'OWNER' && ( + + )} + {(role === 'ADMIN' || role === 'OWNER') && ( + + )} + + {/* Shared Bottom Row */} + + + {/* Side Column spanning same height as Leaderboard */} +
+ + {role === 'FIELD_AGENT' && ( + + )} +
+ +
+
+
+ ); +} diff --git a/src/pages/UnderConstruction.jsx b/src/pages/UnderConstruction.jsx new file mode 100644 index 0000000..22706f0 --- /dev/null +++ b/src/pages/UnderConstruction.jsx @@ -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 ( +
+ +
+ +
+

+ {title} +

+

+ This {moduleName} is currently under construction. +

+
+

STATUS: DEVELOPMENT_IN_PROGRESS

+
+
+
+ ); +}; + +export default UnderConstruction; diff --git a/src/pages/owner/OwnerSnapshot.jsx b/src/pages/owner/OwnerSnapshot.jsx index 34136d9..2135d0d 100644 --- a/src/pages/owner/OwnerSnapshot.jsx +++ b/src/pages/owner/OwnerSnapshot.jsx @@ -198,7 +198,7 @@ const OwnerSnapshot = () => {

Budget vs Actual Spend

Per project (in $k)

- + @@ -215,7 +215,7 @@ const OwnerSnapshot = () => {

Project Status

Distribution

- + {

Monthly Spend Trend

Last 12 months (in $k)

- + @@ -275,8 +275,8 @@ const OwnerSnapshot = () => { {activityFeed.length > 0 ? activityFeed.map((item, i) => (