Files
smoothschedule/frontend/src/components/dashboard/CustomerBreakdownWidget.tsx
poduck c7f241b30a feat(i18n): Comprehensive internationalization of frontend components and pages
Translate all hardcoded English strings to use i18n translation keys:

Components:
- TransactionDetailModal: payment details, refunds, technical info
- ConnectOnboarding/ConnectOnboardingEmbed: Stripe Connect setup
- StripeApiKeysForm: API key management
- DomainPurchase: domain registration flow
- Sidebar: navigation labels
- Schedule/Sidebar, PendingSidebar: scheduler UI
- MasqueradeBanner: masquerade status
- Dashboard widgets: metrics, capacity, customers, tickets
- Marketing: PricingTable, PluginShowcase, BenefitsSection
- ConfirmationModal, ServiceList: common UI

Pages:
- Staff: invitation flow, role management
- Customers: form placeholders
- Payments: transactions, payouts, billing
- BookingSettings: URL and redirect configuration
- TrialExpired: upgrade prompts and features
- PlatformSettings, PlatformBusinesses: admin UI
- HelpApiDocs: API documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 21:40:54 -05:00

137 lines
5.0 KiB
TypeScript

import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { GripVertical, X, Users, UserPlus, UserCheck } from 'lucide-react';
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
import { Customer } from '../../types';
interface CustomerBreakdownWidgetProps {
customers: Customer[];
isEditing?: boolean;
onRemove?: () => void;
}
const CustomerBreakdownWidget: React.FC<CustomerBreakdownWidgetProps> = ({
customers,
isEditing,
onRemove,
}) => {
const { t } = useTranslation();
const breakdownData = useMemo(() => {
// Customers with lastVisit are returning, without are new
const returning = customers.filter((c) => c.lastVisit !== null).length;
const newCustomers = customers.filter((c) => c.lastVisit === null).length;
const total = customers.length;
return {
new: newCustomers,
returning,
total,
newPercentage: total > 0 ? Math.round((newCustomers / total) * 100) : 0,
returningPercentage: total > 0 ? Math.round((returning / total) * 100) : 0,
chartData: [
{ name: 'New', value: newCustomers, color: '#8b5cf6' },
{ name: 'Returning', value: returning, color: '#10b981' },
],
};
}, [customers]);
return (
<div className="h-full p-3 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm relative group flex flex-col">
{isEditing && (
<>
<div className="absolute top-2 left-2 cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 drag-handle">
<GripVertical size={16} />
</div>
<button
onClick={onRemove}
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
>
<X size={14} />
</button>
</>
)}
<h3 className={`text-base font-semibold text-gray-900 dark:text-white mb-2 ${isEditing ? 'pl-5' : ''}`}>
Customers This Month
</h3>
<div className="flex-1 flex items-center gap-3 min-h-0">
{/* Pie Chart */}
<div className="w-20 h-20 flex-shrink-0">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={breakdownData.chartData}
cx="50%"
cy="50%"
innerRadius={20}
outerRadius={35}
paddingAngle={2}
dataKey="value"
>
{breakdownData.chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{
borderRadius: '8px',
border: 'none',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
backgroundColor: '#1F2937',
color: '#F3F4F6',
}}
/>
</PieChart>
</ResponsiveContainer>
</div>
{/* Stats */}
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<div className="p-1 rounded-lg bg-purple-100 dark:bg-purple-900/30">
<UserPlus size={12} className="text-purple-600 dark:text-purple-400" />
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">New</p>
<p className="text-base font-semibold text-gray-900 dark:text-white">
{breakdownData.new}{' '}
<span className="text-xs font-normal text-gray-400">
({breakdownData.newPercentage}%)
</span>
</p>
</div>
</div>
<div className="flex items-center gap-2">
<div className="p-1 rounded-lg bg-green-100 dark:bg-green-900/30">
<UserCheck size={12} className="text-green-600 dark:text-green-400" />
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Returning</p>
<p className="text-base font-semibold text-gray-900 dark:text-white">
{breakdownData.returning}{' '}
<span className="text-xs font-normal text-gray-400">
({breakdownData.returningPercentage}%)
</span>
</p>
</div>
</div>
</div>
</div>
<div className="mt-2 pt-2 border-t border-gray-100 dark:border-gray-700 flex-shrink-0">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1 text-gray-500 dark:text-gray-400">
<Users size={12} />
<span>{t('dashboard.totalCustomers')}</span>
</div>
<span className="font-semibold text-gray-900 dark:text-white">{breakdownData.total}</span>
</div>
</div>
</div>
);
};
export default CustomerBreakdownWidget;