bc4e25f132
- 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
102 lines
3.6 KiB
React
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>
|
|
);
|
|
};
|