webmoney/frontend/src/components/dashboard/OverdueWidget.jsx

258 lines
9.5 KiB
JavaScript

import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { dashboardService } from '../../services/api';
import useFormatters from '../../hooks/useFormatters';
const OverdueWidget = () => {
const { t } = useTranslation();
const { currency } = useFormatters();
const navigate = useNavigate();
const cardRef = useRef(null);
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [expandedRange, setExpandedRange] = useState(null);
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const [isExpanded, setIsExpanded] = useState(false);
// Detectar mudanças de tamanho da tela
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Auto-expandir quando houver dados em mobile
useEffect(() => {
if (isMobile && data?.items?.length > 0 && !isExpanded) {
setIsExpanded(true);
}
}, [isMobile, data, isExpanded]);
const loadData = useCallback(async () => {
setLoading(true);
try {
const result = await dashboardService.getOverdue(50);
setData(result);
// Expandir automaticamente a primeira faixa com items
if (result?.by_range?.length > 0) {
setExpandedRange(result.by_range[0].key);
}
} catch (error) {
console.error('Error loading overdue transactions:', error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadData();
}, [loadData]);
const getTypeIcon = (item) => {
if (item.type === 'recurring') {
return 'bi-arrow-repeat';
}
if (item.type === 'liability') {
return 'bi-credit-card-2-back';
}
return item.transaction_type === 'credit' ? 'bi-arrow-down-circle' : 'bi-arrow-up-circle';
};
const getTypeColor = (item) => {
if (item.type === 'recurring') return '#f59e0b';
if (item.type === 'liability') return '#8b5cf6'; // purple for liability
return item.transaction_type === 'credit' ? '#10b981' : '#ef4444';
};
const getRangeColor = (key) => {
switch (key) {
case 'critical': return '#dc2626'; // red-600
case 'high': return '#ea580c'; // orange-600
case 'medium': return '#d97706'; // amber-600
case 'low': return '#ca8a04'; // yellow-600
default: return '#6b7280';
}
};
const getRangeIcon = (key) => {
switch (key) {
case 'critical': return 'bi-exclamation-octagon-fill';
case 'high': return 'bi-exclamation-triangle-fill';
case 'medium': return 'bi-exclamation-circle-fill';
case 'low': return 'bi-clock-fill';
default: return 'bi-circle-fill';
}
};
const handleTransactionClick = (item) => {
if (item.type === 'transaction') {
navigate(`/transactions?highlight=${item.id}`);
} else if (item.type === 'recurring') {
navigate(`/recurring?highlight=${item.template_id}`);
} else if (item.type === 'liability') {
navigate(`/liabilities?highlight=${item.liability_account_id}`);
}
};
const toggleRange = (key) => {
setExpandedRange(expandedRange === key ? null : key);
};
return (
<div
ref={cardRef}
className="card border-0"
style={{
background: '#0f172a',
height: isMobile ? 'auto' : undefined
}}
>
<div className="card-header border-0 d-flex justify-content-between align-items-center py-2"
style={{ background: 'transparent', borderBottom: '1px solid rgba(239, 68, 68, 0.3)' }}>
<h6 className="text-white mb-0 d-flex align-items-center">
<i className="bi bi-exclamation-triangle me-2 text-danger"></i>
{t('dashboard.overdueTransactions')}
{data?.summary?.total_items > 0 && (
<span className="badge bg-danger ms-2">{data.summary.total_items}</span>
)}
</h6>
<div className="d-flex gap-2">
{isMobile && (
<button
className="btn btn-link text-primary p-0"
onClick={() => setIsExpanded(!isExpanded)}
>
<i className={`bi bi-chevron-${isExpanded ? 'up' : 'down'}`}></i>
</button>
)}
<button
className="btn btn-sm btn-outline-secondary border-0"
onClick={loadData}
disabled={loading}
>
<i className={`bi bi-arrow-clockwise ${loading ? 'spin' : ''}`}></i>
</button>
</div>
</div>
<div className="card-body py-3" style={{
display: isMobile && !isExpanded ? 'none' : 'block'
}}>
{loading ? (
<div className="text-center py-4">
<div className="spinner-border spinner-border-sm text-danger" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
) : !data?.items?.length ? (
<div className="text-center text-slate-400 py-3">
<i className="bi bi-check-circle fs-2 mb-2 d-block text-success"></i>
<p className="small mb-0">{t('dashboard.noOverdueTransactions')}</p>
</div>
) : (
<div className="row g-3">
{data.by_range.map((range) => (
<div key={range.key} className="col-md-6 col-lg-3">
{/* Header da faixa */}
<div
className="d-flex align-items-center justify-content-between p-2 rounded-top"
style={{
background: `${getRangeColor(range.key)}20`,
borderBottom: `2px solid ${getRangeColor(range.key)}`,
}}
>
<div className="d-flex align-items-center gap-2">
<i
className={`bi ${getRangeIcon(range.key)}`}
style={{ color: getRangeColor(range.key), fontSize: '14px' }}
></i>
<span className="text-white small fw-semibold">
{t(`dashboard.overdueRange.${range.key}`)}
</span>
</div>
<span
className="badge rounded-pill"
style={{
background: getRangeColor(range.key),
fontSize: '10px',
padding: '2px 8px',
}}
>
{range.count}
</span>
</div>
{/* Items da faixa */}
<div
className="rounded-bottom p-2"
style={{
background: 'rgba(30, 41, 59, 0.5)',
maxHeight: '200px',
overflowY: 'auto',
}}
>
{range.items.length === 0 ? (
<div className="text-center text-slate-500 py-2">
<small>-</small>
</div>
) : (
range.items.map((item) => (
<div
key={`${item.type}-${item.id}`}
className="d-flex align-items-center gap-2 py-2 px-2 rounded mb-1"
style={{
background: 'rgba(15, 23, 42, 0.5)',
borderLeft: `3px solid ${getTypeColor(item)}`,
cursor: 'pointer',
}}
onClick={() => handleTransactionClick(item)}
>
{/* Ícone */}
<div
className="rounded-circle d-flex align-items-center justify-content-center flex-shrink-0"
style={{
width: '24px',
height: '24px',
background: `${getTypeColor(item)}20`,
color: getTypeColor(item),
}}
>
<i className={`bi ${getTypeIcon(item)}`} style={{ fontSize: '10px' }}></i>
</div>
{/* Info */}
<div className="flex-grow-1 min-width-0">
<div className="text-white small text-truncate" style={{ fontSize: '11px' }} title={item.description}>
{item.description}
</div>
<div className="d-flex align-items-center gap-1 flex-wrap">
<span
className="badge bg-danger"
style={{ fontSize: '8px', padding: '1px 3px' }}
>
{item.days_overdue}d
</span>
<span className={`fw-bold ${item.transaction_type === 'credit' ? 'text-success' : 'text-danger'}`} style={{ fontSize: '10px' }}>
{currency(item.amount, item.currency || 'EUR')}
</span>
</div>
</div>
</div>
))
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};
export default OverdueWidget;