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:
@@ -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]);
|
||||
};
|
||||
Reference in New Issue
Block a user