271 lines
11 KiB
JavaScript
271 lines
11 KiB
JavaScript
import React, { useState, useEffect, useCallback } 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 [data, setData] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [expandedRange, setExpandedRange] = useState(null);
|
|
|
|
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 className="card border-0 h-100" style={{ background: '#0f172a' }}>
|
|
<div className="card-header border-0 d-flex justify-content-between align-items-center py-3"
|
|
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')}
|
|
</h6>
|
|
<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 className="card-body py-2" style={{ maxHeight: '400px', overflowY: 'auto' }}>
|
|
{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-4">
|
|
<i className="bi bi-check-circle fs-1 mb-2 d-block text-success"></i>
|
|
<p className="small mb-0">{t('dashboard.noOverdueTransactions')}</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{data.by_range.map((range) => (
|
|
<div key={range.key} className="mb-2">
|
|
{/* Header da faixa (clicável) */}
|
|
<div
|
|
className="d-flex align-items-center justify-content-between p-2 rounded cursor-pointer"
|
|
style={{
|
|
background: `${getRangeColor(range.key)}15`,
|
|
borderLeft: `3px solid ${getRangeColor(range.key)}`,
|
|
cursor: 'pointer',
|
|
}}
|
|
onClick={() => toggleRange(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>
|
|
<span
|
|
className="badge rounded-pill"
|
|
style={{
|
|
background: getRangeColor(range.key),
|
|
fontSize: '10px',
|
|
padding: '2px 6px',
|
|
}}
|
|
>
|
|
{range.count}
|
|
</span>
|
|
</div>
|
|
<div className="d-flex align-items-center gap-2">
|
|
<span className="text-danger fw-bold small">
|
|
{currency(range.total, 'BRL')}
|
|
</span>
|
|
<i className={`bi bi-chevron-${expandedRange === range.key ? 'up' : 'down'} text-slate-500`}></i>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Items da faixa (expandível) */}
|
|
{expandedRange === range.key && (
|
|
<div className="mt-1">
|
|
{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 ms-2"
|
|
style={{
|
|
background: 'rgba(30, 41, 59, 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: '28px',
|
|
height: '28px',
|
|
background: `${getTypeColor(item)}20`,
|
|
color: getTypeColor(item),
|
|
}}
|
|
>
|
|
<i className={`bi ${getTypeIcon(item)}`} style={{ fontSize: '12px' }}></i>
|
|
</div>
|
|
|
|
{/* Info */}
|
|
<div className="flex-grow-1 min-width-0">
|
|
<div className="text-white small text-truncate" title={item.description}>
|
|
{item.description}
|
|
</div>
|
|
<div className="d-flex align-items-center gap-2">
|
|
<small className="text-slate-500" style={{ fontSize: '10px' }}>
|
|
{item.planned_date_formatted}
|
|
</small>
|
|
<span
|
|
className="badge bg-danger"
|
|
style={{ fontSize: '9px', padding: '1px 4px' }}
|
|
>
|
|
{item.days_overdue} {t('dashboard.daysLate')}
|
|
</span>
|
|
{item.type === 'recurring' && (
|
|
<span
|
|
className="badge bg-warning text-dark"
|
|
style={{ fontSize: '8px', padding: '1px 4px' }}
|
|
>
|
|
#{item.occurrence_number}
|
|
</span>
|
|
)}
|
|
{item.type === 'liability' && (
|
|
<span
|
|
className="badge text-white"
|
|
style={{ fontSize: '8px', padding: '1px 4px', background: '#8b5cf6' }}
|
|
>
|
|
<i className="bi bi-credit-card-2-back me-1" style={{ fontSize: '8px' }}></i>
|
|
#{item.installment_number}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Valor */}
|
|
<div className={`fw-bold small text-end ${
|
|
item.transaction_type === 'credit' ? 'text-success' : 'text-danger'
|
|
}`}>
|
|
{item.transaction_type === 'credit' ? '+' : '-'}
|
|
{currency(item.amount, item.account?.currency || 'BRL')}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer com resumo */}
|
|
{data?.summary && !loading && data.items?.length > 0 && (
|
|
<div className="card-footer border-0 py-2" style={{ background: 'rgba(239, 68, 68, 0.1)' }}>
|
|
<div className="row g-2 text-center">
|
|
<div className="col-4">
|
|
<small className="text-slate-500 d-block" style={{ fontSize: '10px' }}>
|
|
{t('dashboard.totalOverdue')}
|
|
</small>
|
|
<span className="text-danger fw-bold small">
|
|
{data.summary.total_items}
|
|
</span>
|
|
</div>
|
|
<div className="col-4">
|
|
<small className="text-slate-500 d-block" style={{ fontSize: '10px' }}>
|
|
{t('dashboard.totalAmount')}
|
|
</small>
|
|
<span className="text-danger fw-bold small">
|
|
{currency(data.summary.total_amount || 0, 'BRL')}
|
|
</span>
|
|
</div>
|
|
<div className="col-4">
|
|
<small className="text-slate-500 d-block" style={{ fontSize: '10px' }}>
|
|
{t('dashboard.maxDelay')}
|
|
</small>
|
|
<span className="text-danger fw-bold small">
|
|
{data.summary.max_days_overdue} {t('common.days')}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default OverdueWidget;
|