Files
LynkedUpPro_CRM/src/components/StatCard.jsx
T
Satyam bc4e25f132 feat: comprehensive mobile optimization, role-based AI chatbot, and multi-role dashboard enhancements
- Rewrote AI chatbot context system with deep role-aware data injection for all 7 roles (Owner, Admin, Field Agent, Contractor, Subcontractor, Vendor, Customer) plus guest fallback
- Added mobile-responsive bottom-sheet modals across all roles (ChangeOrderDrawer, InvoiceDetailModal, FinancialSummaryModal, VendorFinancialSummaryModal, FinancialDetailsModal)
- Converted Owner Project List and Vendor Orders tables to mobile card views with touch-friendly layouts
- Optimized Vendor Management and People Directory with mobile list/detail toggle and back navigation
- Fixed Document Control mobile view: horizontally scrollable filter tabs, proper overflow chain for document visibility
- Added responsive padding, text scaling, and flex-wrap across StatCard, TaskDetailsModal, OwnerSnapshot, OwnerProjectDetail, VendorDashboard, and DocumentManagement
- Expanded mock data store with richer vendor invoices, orders, documents, and compliance records
- Enhanced Owner Snapshot with financial KPI cards, urgent items panel, and Action Center modal
- Built Contractor Dashboard with task management, financial summary, and performance metrics
- Built Subcontractor Dashboard with clock-in tracking, task assignments, and invoice management
- Enhanced Vendor Dashboard with earnings summary, active orders, compliance status, and performance rating
- Added icon-only tab navigation on mobile for OwnerProjectDetail
- Extended attribution signatures across all platform pages
2026-02-18 12:34:55 +05:30

102 lines
3.6 KiB
React

import React, { useRef, useEffect } from 'react';
import { SpotlightCard } from './SpotlightCard';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
export const StatCard = ({ label, value, suffix = '', icon: Icon, trend, trendLabel, color = 'blue', delay = 0 }) => {
const counterRef = useRef(null);
useEffect(() => {
if (!counterRef.current) return;
// Clean value string if it contains non-numeric chars (e.g., "$5,000") to get raw number
// But for GSAP to animate "5,000" to "10,000" gracefully, best to animate a proxy object
// and format on update.
let targetValue = 0;
let prefix = '';
if (typeof value === 'string') {
// Check for prefix like $
if (value.startsWith('$')) prefix = '$';
// Parse number
targetValue = parseFloat(value.replace(/[^0-9.-]+/g, ""));
} else {
targetValue = value;
}
const counter = { val: 0 };
const el = counterRef.current;
// Initial state
el.innerText = prefix + '0' + suffix;
gsap.to(counter, {
val: targetValue,
duration: 2,
ease: "power2.out",
delay: delay,
scrollTrigger: {
trigger: el,
start: "top 90%",
toggleActions: "play none none none"
},
onUpdate: () => {
// Determine if we need integers or decimals based on target
const isFloat = targetValue % 1 !== 0;
const current = isFloat ? counter.val.toFixed(2) : Math.round(counter.val);
el.innerText = prefix + current.toLocaleString('en-US') + suffix;
}
});
}, [value, suffix, delay]);
// Color maps
const colors = {
blue: 'text-blue-500 bg-blue-500/10',
emerald: 'text-emerald-500 bg-emerald-500/10',
amber: 'text-amber-500 bg-amber-500/10',
red: 'text-red-500 bg-red-500/10',
purple: 'text-purple-500 bg-purple-500/10',
};
// Safely get color class or default
const colorClass = colors[color] || colors.blue;
const textColor = colorClass.split(' ')[0];
const bgColor = colorClass.split(' ')[1];
return (
<SpotlightCard className="h-full flex flex-col justify-between p-4 sm:p-6">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-sm font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">{label}</h3>
<div className="mt-2 flex items-baseline gap-1">
<span
ref={counterRef}
className={`text-2xl sm:text-3xl font-mono font-bold ${textColor} tracking-tight`}
>
0
</span>
</div>
</div>
{Icon && (
<div className={`p-3 rounded-xl ${bgColor} ${textColor}`}>
<Icon size={24} />
</div>
)}
</div>
{trend && (
<div className="flex items-center text-xs font-medium">
<span className={`${trend > 0 ? 'text-emerald-500' : 'text-red-500'} flex items-center`}>
{trend > 0 ? '+' : ''}{trend}%
</span>
<span className="text-zinc-400 ml-2">{trendLabel || 'vs last month'}</span>
</div>
)}
</SpotlightCard>
);
};