v1.43.9 - Drill-down de 3 níveis em Centros de Custo (Centro → Categoria → Subcategoria)
This commit is contained in:
parent
db87da95c1
commit
bb06ca8fae
33
CHANGELOG.md
33
CHANGELOG.md
@ -5,6 +5,39 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/).
|
||||
Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/).
|
||||
|
||||
|
||||
## [1.43.9] - 2025-12-16
|
||||
|
||||
### Added
|
||||
- **Relatório de Centros de Custo - Drill-down de 3 Níveis**
|
||||
- Nível 1: Mostra todos os centros de custo
|
||||
- Nível 2: Clicar em um centro de custo mostra categorias (agrupadas por categoria pai)
|
||||
- Nível 3: Clicar em uma categoria mostra suas subcategorias
|
||||
- Navegação com breadcrumbs: "Todos os Centros" > Nome do Centro > Nome da Categoria
|
||||
- Botões "Voltar" em cada nível
|
||||
- Otimização mobile: cards responsivos em todos os 3 níveis
|
||||
- Hover visual em tabelas e cards clicáveis
|
||||
|
||||
- **Backend - API /reports/by-cost-center**
|
||||
- Parâmetros: `cost_center_id` (nível 2) e `category_id` (nível 3)
|
||||
- Sem parâmetros: lista centros de custo
|
||||
- Com `cost_center_id`: lista categorias pai do centro de custo
|
||||
- Com `cost_center_id` + `category_id`: lista subcategorias da categoria
|
||||
- Query usa COALESCE para agregar subcategorias na categoria pai
|
||||
- Suporte para transações sem centro de custo (id=0, "Sem classificar")
|
||||
|
||||
- **Tradução i18n**
|
||||
- pt-BR: "Todos os Centros", "Voltar para Todos os Centros", "Voltar para {nome}"
|
||||
- es: "Todos los Centros"
|
||||
- en: "All Centers"
|
||||
|
||||
### Changed
|
||||
- **Frontend - Reports.jsx**
|
||||
- Adicionados estados: `selectedCostCenter` e `selectedCostCenterCategory`
|
||||
- Função `renderCostCenter` reescrita com lógica de 3 níveis
|
||||
- useEffect reage a mudanças de `selectedCostCenter` e `selectedCostCenterCategory`
|
||||
- Estilo condicional: cursor pointer e hover apenas para itens clicáveis
|
||||
- Breadcrumbs dinâmicos baseados no nível de navegação
|
||||
|
||||
## [1.43.8] - 2025-12-16
|
||||
|
||||
### Added
|
||||
|
||||
@ -259,66 +259,160 @@ public function byCategory(Request $request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reporte por centro de costos
|
||||
* Reporte por centro de costos (3 niveles: centro -> categoría -> subcategoría)
|
||||
*/
|
||||
public function byCostCenter(Request $request)
|
||||
{
|
||||
$this->init();
|
||||
$startDate = $request->get('start_date', now()->startOfYear()->format('Y-m-d'));
|
||||
$endDate = $request->get('end_date', now()->format('Y-m-d'));
|
||||
$costCenterId = $request->get('cost_center_id');
|
||||
$categoryId = $request->get('category_id');
|
||||
|
||||
$data = DB::select("
|
||||
SELECT
|
||||
COALESCE(cc.id, 0) as cost_center_id,
|
||||
COALESCE(cc.name, 'Sem classificar') as cost_center_name,
|
||||
COALESCE(cc.color, '#6b7280') as color,
|
||||
COALESCE(a.currency, 'EUR') as currency,
|
||||
SUM(CASE WHEN t.type = 'credit' THEN t.amount ELSE 0 END) as income,
|
||||
SUM(CASE WHEN t.type = 'debit' THEN ABS(t.amount) ELSE 0 END) as expense
|
||||
FROM transactions t
|
||||
LEFT JOIN cost_centers cc ON t.cost_center_id = cc.id
|
||||
LEFT JOIN accounts a ON t.account_id = a.id
|
||||
WHERE t.user_id = ?
|
||||
AND t.effective_date BETWEEN ? AND ?
|
||||
AND t.deleted_at IS NULL
|
||||
AND {$this->excludeTransfers()}
|
||||
GROUP BY COALESCE(cc.id, 0), COALESCE(cc.name, 'Sem classificar'), COALESCE(cc.color, '#6b7280'), COALESCE(a.currency, 'EUR')
|
||||
ORDER BY expense DESC
|
||||
", [$this->userId, $startDate, $endDate]);
|
||||
|
||||
// Agrupar por centro de costo
|
||||
$byCostCenter = [];
|
||||
foreach ($data as $row) {
|
||||
$ccId = $row->cost_center_id;
|
||||
if (!isset($byCostCenter[$ccId])) {
|
||||
$byCostCenter[$ccId] = [
|
||||
'id' => $ccId == 0 ? null : $ccId,
|
||||
'name' => $row->cost_center_name,
|
||||
'color' => $row->color,
|
||||
'income_converted' => 0,
|
||||
'expense_converted' => 0,
|
||||
'by_currency' => [],
|
||||
];
|
||||
}
|
||||
$byCostCenter[$ccId]['by_currency'][$row->currency] = [
|
||||
'income' => round($row->income, 2),
|
||||
'expense' => round($row->expense, 2),
|
||||
];
|
||||
$byCostCenter[$ccId]['income_converted'] += $this->convertToPrimaryCurrency($row->income, $row->currency);
|
||||
$byCostCenter[$ccId]['expense_converted'] += $this->convertToPrimaryCurrency($row->expense, $row->currency);
|
||||
// Nível 3: Subcategorias de uma categoria específica de um centro de custo
|
||||
if ($costCenterId !== null && $categoryId !== null) {
|
||||
$data = DB::select("
|
||||
SELECT
|
||||
c.id as category_id,
|
||||
c.name as category_name,
|
||||
c.icon as category_icon,
|
||||
c.color as category_color,
|
||||
COALESCE(a.currency, 'EUR') as currency,
|
||||
SUM(CASE WHEN t.type = 'credit' THEN t.amount ELSE 0 END) as income,
|
||||
SUM(CASE WHEN t.type = 'debit' THEN ABS(t.amount) ELSE 0 END) as expense
|
||||
FROM transactions t
|
||||
LEFT JOIN categories c ON t.category_id = c.id
|
||||
LEFT JOIN accounts a ON t.account_id = a.id
|
||||
WHERE t.user_id = ?
|
||||
AND t.effective_date BETWEEN ? AND ?
|
||||
AND " . ($costCenterId == 0 ? "t.cost_center_id IS NULL" : "t.cost_center_id = ?") . "
|
||||
AND c.parent_id = ?
|
||||
AND t.deleted_at IS NULL
|
||||
AND {$this->excludeTransfers()}
|
||||
GROUP BY c.id, c.name, c.icon, c.color, COALESCE(a.currency, 'EUR')
|
||||
ORDER BY expense DESC
|
||||
", $costCenterId == 0
|
||||
? [$this->userId, $startDate, $endDate, $categoryId]
|
||||
: [$this->userId, $startDate, $endDate, $costCenterId, $categoryId]
|
||||
);
|
||||
}
|
||||
// Nível 2: Categorias pai de um centro de custo específico
|
||||
else if ($costCenterId !== null) {
|
||||
$data = DB::select("
|
||||
SELECT
|
||||
COALESCE(c.parent_id, c.id) as category_id,
|
||||
COALESCE(cp.name, c.name) as category_name,
|
||||
COALESCE(cp.icon, c.icon) as category_icon,
|
||||
COALESCE(cp.color, c.color) as category_color,
|
||||
COALESCE(a.currency, 'EUR') as currency,
|
||||
SUM(CASE WHEN t.type = 'credit' THEN t.amount ELSE 0 END) as income,
|
||||
SUM(CASE WHEN t.type = 'debit' THEN ABS(t.amount) ELSE 0 END) as expense
|
||||
FROM transactions t
|
||||
LEFT JOIN categories c ON t.category_id = c.id
|
||||
LEFT JOIN categories cp ON c.parent_id = cp.id
|
||||
LEFT JOIN accounts a ON t.account_id = a.id
|
||||
WHERE t.user_id = ?
|
||||
AND t.effective_date BETWEEN ? AND ?
|
||||
AND " . ($costCenterId == 0 ? "t.cost_center_id IS NULL" : "t.cost_center_id = ?") . "
|
||||
AND t.deleted_at IS NULL
|
||||
AND {$this->excludeTransfers()}
|
||||
GROUP BY COALESCE(c.parent_id, c.id), COALESCE(cp.name, c.name), COALESCE(cp.icon, c.icon), COALESCE(cp.color, c.color), COALESCE(a.currency, 'EUR')
|
||||
ORDER BY expense DESC
|
||||
", $costCenterId == 0
|
||||
? [$this->userId, $startDate, $endDate]
|
||||
: [$this->userId, $startDate, $endDate, $costCenterId]
|
||||
);
|
||||
}
|
||||
// Nível 1: Centros de custo
|
||||
else {
|
||||
$data = DB::select("
|
||||
SELECT
|
||||
COALESCE(cc.id, 0) as cost_center_id,
|
||||
COALESCE(cc.name, 'Sem classificar') as cost_center_name,
|
||||
COALESCE(cc.color, '#6b7280') as color,
|
||||
COALESCE(a.currency, 'EUR') as currency,
|
||||
SUM(CASE WHEN t.type = 'credit' THEN t.amount ELSE 0 END) as income,
|
||||
SUM(CASE WHEN t.type = 'debit' THEN ABS(t.amount) ELSE 0 END) as expense
|
||||
FROM transactions t
|
||||
LEFT JOIN cost_centers cc ON t.cost_center_id = cc.id
|
||||
LEFT JOIN accounts a ON t.account_id = a.id
|
||||
WHERE t.user_id = ?
|
||||
AND t.effective_date BETWEEN ? AND ?
|
||||
AND t.deleted_at IS NULL
|
||||
AND {$this->excludeTransfers()}
|
||||
GROUP BY COALESCE(cc.id, 0), COALESCE(cc.name, 'Sem classificar'), COALESCE(cc.color, '#6b7280'), COALESCE(a.currency, 'EUR')
|
||||
ORDER BY expense DESC
|
||||
", [$this->userId, $startDate, $endDate]);
|
||||
}
|
||||
|
||||
$result = array_map(function($cc) {
|
||||
// Agrupar y procesar resultados
|
||||
$results = [];
|
||||
|
||||
// Nível 3: Subcategorias
|
||||
if ($costCenterId !== null && $categoryId !== null) {
|
||||
foreach ($data as $row) {
|
||||
$id = $row->category_id ?? 0;
|
||||
if (!isset($results[$id])) {
|
||||
$results[$id] = [
|
||||
'id' => $id == 0 ? null : $id,
|
||||
'name' => $row->category_name ?? 'Sin categoría',
|
||||
'icon' => $row->category_icon ?? 'bi-tag',
|
||||
'color' => $row->category_color ?? '#6b7280',
|
||||
'income_converted' => 0,
|
||||
'expense_converted' => 0,
|
||||
];
|
||||
}
|
||||
$results[$id]['income_converted'] += $this->convertToPrimaryCurrency($row->income, $row->currency);
|
||||
$results[$id]['expense_converted'] += $this->convertToPrimaryCurrency($row->expense, $row->currency);
|
||||
}
|
||||
}
|
||||
// Nível 2: Categorias
|
||||
else if ($costCenterId !== null) {
|
||||
foreach ($data as $row) {
|
||||
$id = $row->category_id ?? 0;
|
||||
if (!isset($results[$id])) {
|
||||
$results[$id] = [
|
||||
'id' => $id == 0 ? null : $id,
|
||||
'name' => $row->category_name ?? 'Sin categoría',
|
||||
'icon' => $row->category_icon ?? 'bi-tag',
|
||||
'color' => $row->category_color ?? '#6b7280',
|
||||
'income_converted' => 0,
|
||||
'expense_converted' => 0,
|
||||
];
|
||||
}
|
||||
$results[$id]['income_converted'] += $this->convertToPrimaryCurrency($row->income, $row->currency);
|
||||
$results[$id]['expense_converted'] += $this->convertToPrimaryCurrency($row->expense, $row->currency);
|
||||
}
|
||||
}
|
||||
// Nível 1: Centros de custo
|
||||
else {
|
||||
foreach ($data as $row) {
|
||||
$id = $row->cost_center_id;
|
||||
if (!isset($results[$id])) {
|
||||
$results[$id] = [
|
||||
'id' => $id == 0 ? null : $id,
|
||||
'name' => $row->cost_center_name,
|
||||
'color' => $row->color,
|
||||
'income_converted' => 0,
|
||||
'expense_converted' => 0,
|
||||
];
|
||||
}
|
||||
$results[$id]['income_converted'] += $this->convertToPrimaryCurrency($row->income, $row->currency);
|
||||
$results[$id]['expense_converted'] += $this->convertToPrimaryCurrency($row->expense, $row->currency);
|
||||
}
|
||||
}
|
||||
|
||||
$result = array_map(function($item) {
|
||||
return [
|
||||
'id' => $cc['id'],
|
||||
'name' => $cc['name'],
|
||||
'color' => $cc['color'],
|
||||
'income' => round($cc['income_converted'], 2),
|
||||
'expense' => round($cc['expense_converted'], 2),
|
||||
'balance' => round($cc['income_converted'] - $cc['expense_converted'], 2),
|
||||
'by_currency' => $cc['by_currency'],
|
||||
'id' => $item['id'],
|
||||
'name' => $item['name'],
|
||||
'icon' => $item['icon'] ?? null,
|
||||
'color' => $item['color'],
|
||||
'income' => round($item['income_converted'], 2),
|
||||
'expense' => round($item['expense_converted'], 2),
|
||||
'balance' => round($item['income_converted'] - $item['expense_converted'], 2),
|
||||
];
|
||||
}, $byCostCenter);
|
||||
}, $results);
|
||||
|
||||
usort($result, fn($a, $b) => $b['expense'] <=> $a['expense']);
|
||||
|
||||
|
||||
@ -1803,6 +1803,7 @@
|
||||
"balance": "Balance",
|
||||
"byCategory": "By Category",
|
||||
"byCostCenter": "By Cost Center",
|
||||
"allCostCenters": "All Centers",
|
||||
"comparison": "Comparison",
|
||||
"periodComparison": "Period Comparison",
|
||||
"vsPreviousPeriod": "vs previous period",
|
||||
|
||||
@ -1788,6 +1788,7 @@
|
||||
"summary": "Resumen",
|
||||
"byCategory": "Por Categoría",
|
||||
"byCostCenter": "Por Centro de Costo",
|
||||
"allCostCenters": "Todos los Centros",
|
||||
"monthlyEvolution": "Evolución Mensual",
|
||||
"comparison": "Comparativa",
|
||||
"periodComparison": "Comparación de Períodos",
|
||||
|
||||
@ -1809,6 +1809,7 @@
|
||||
"balance": "Saldo",
|
||||
"byCategory": "Por Categoria",
|
||||
"byCostCenter": "Por Centro de Custo",
|
||||
"allCostCenters": "Todos os Centros",
|
||||
"comparison": "Comparação",
|
||||
"periodComparison": "Comparação de Períodos",
|
||||
"vsPreviousPeriod": "vs período anterior",
|
||||
|
||||
@ -59,6 +59,8 @@ const Reports = () => {
|
||||
const [projection, setProjection] = useState(null);
|
||||
const [comparison, setComparison] = useState(null);
|
||||
const [costCenterData, setCostCenterData] = useState(null);
|
||||
const [selectedCostCenter, setSelectedCostCenter] = useState(null);
|
||||
const [selectedCostCenterCategory, setSelectedCostCenterCategory] = useState(null);
|
||||
const [recurringData, setRecurringData] = useState(null);
|
||||
const [liabilitiesData, setLiabilitiesData] = useState(null);
|
||||
const [futureData, setFutureData] = useState(null);
|
||||
@ -102,7 +104,14 @@ const Reports = () => {
|
||||
setComparison(compRes);
|
||||
break;
|
||||
case 'costCenter':
|
||||
const ccRes = await reportService.getByCostCenter();
|
||||
const ccParams = {};
|
||||
if (selectedCostCenter) {
|
||||
ccParams.cost_center_id = selectedCostCenter.id ?? 0;
|
||||
}
|
||||
if (selectedCostCenterCategory) {
|
||||
ccParams.category_id = selectedCostCenterCategory.id;
|
||||
}
|
||||
const ccRes = await reportService.getByCostCenter(ccParams);
|
||||
setCostCenterData(ccRes);
|
||||
break;
|
||||
case 'recurring':
|
||||
@ -127,7 +136,7 @@ const Reports = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [activeTab, year, months, selectedCategory]);
|
||||
}, [activeTab, year, months, selectedCategory, selectedCostCenter, selectedCostCenterCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
@ -140,6 +149,13 @@ const Reports = () => {
|
||||
}
|
||||
}, [selectedCategory]);
|
||||
|
||||
// Recarrega dados quando centro de custo ou categoria selecionada muda
|
||||
useEffect(() => {
|
||||
if (activeTab === 'costCenter') {
|
||||
loadData();
|
||||
}
|
||||
}, [selectedCostCenter, selectedCostCenterCategory]);
|
||||
|
||||
const tabs = [
|
||||
{ id: 'summary', label: t('reports.summary'), icon: 'bi-clipboard-data' },
|
||||
{ id: 'category', label: t('reports.byCategory'), icon: 'bi-pie-chart' },
|
||||
@ -1488,72 +1504,223 @@ const Reports = () => {
|
||||
if (!costCenterData) return null;
|
||||
const data = costCenterData.data || [];
|
||||
|
||||
const handleCostCenterClick = (cc) => {
|
||||
setSelectedCostCenter(cc);
|
||||
};
|
||||
|
||||
const handleCategoryClick = (cat) => {
|
||||
setSelectedCostCenterCategory(cat);
|
||||
};
|
||||
|
||||
const handleBackToCostCenters = () => {
|
||||
setSelectedCostCenter(null);
|
||||
setSelectedCostCenterCategory(null);
|
||||
};
|
||||
|
||||
const handleBackToCategories = () => {
|
||||
setSelectedCostCenterCategory(null);
|
||||
};
|
||||
|
||||
// Determinar título baseado no nível
|
||||
let title = t('reports.byCostCenter');
|
||||
if (selectedCostCenter && selectedCostCenterCategory) {
|
||||
title = t('reports.subcategoryDistribution');
|
||||
} else if (selectedCostCenter) {
|
||||
title = t('reports.expenseDistribution');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row g-4">
|
||||
<div className="col-md-4">
|
||||
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
|
||||
<div className="card-body">
|
||||
<small className="text-slate-400">{t('reports.totalIncome')}</small>
|
||||
<h5 className="text-success mb-0">{currency(costCenterData.total_income || 0, costCenterData.currency)}</h5>
|
||||
{/* Breadcrumb/Navegação */}
|
||||
{(selectedCostCenter || selectedCostCenterCategory) && (
|
||||
<div className="col-12">
|
||||
<div className="d-flex gap-2 flex-wrap">
|
||||
{selectedCostCenter && (
|
||||
<button
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
onClick={handleBackToCostCenters}
|
||||
>
|
||||
<i className="bi bi-house me-2"></i>
|
||||
{t('reports.allCostCenters')}
|
||||
</button>
|
||||
)}
|
||||
{selectedCostCenterCategory && (
|
||||
<button
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
onClick={handleBackToCategories}
|
||||
>
|
||||
<i className="bi bi-arrow-left me-2"></i>
|
||||
{selectedCostCenter.name}
|
||||
</button>
|
||||
)}
|
||||
{selectedCostCenterCategory && (
|
||||
<span className="btn btn-sm btn-secondary" disabled>
|
||||
{selectedCostCenterCategory.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cards de Totais */}
|
||||
<div className={isMobile ? 'col-12' : 'col-md-4'}>
|
||||
<div className="card border-0 text-center" style={{ background: '#1e293b', padding: isMobile ? '0.75rem' : '1rem' }}>
|
||||
<div className="card-body" style={{ padding: isMobile ? '0.5rem' : '1rem' }}>
|
||||
<small className="text-slate-400" style={{ fontSize: isMobile ? '0.7rem' : '0.875rem' }}>
|
||||
{t('reports.totalIncome')}
|
||||
</small>
|
||||
<h5 className="text-success mb-0" style={{ fontSize: isMobile ? '0.95rem' : '1.25rem' }}>
|
||||
{currency(costCenterData.total_income || 0, costCenterData.currency)}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
|
||||
<div className="card-body">
|
||||
<small className="text-slate-400">{t('reports.totalExpense')}</small>
|
||||
<h5 className="text-danger mb-0">{currency(costCenterData.total_expense || 0, costCenterData.currency)}</h5>
|
||||
<div className={isMobile ? 'col-12' : 'col-md-4'}>
|
||||
<div className="card border-0 text-center" style={{ background: '#1e293b', padding: isMobile ? '0.75rem' : '1rem' }}>
|
||||
<div className="card-body" style={{ padding: isMobile ? '0.5rem' : '1rem' }}>
|
||||
<small className="text-slate-400" style={{ fontSize: isMobile ? '0.7rem' : '0.875rem' }}>
|
||||
{t('reports.totalExpense')}
|
||||
</small>
|
||||
<h5 className="text-danger mb-0" style={{ fontSize: isMobile ? '0.95rem' : '1.25rem' }}>
|
||||
{currency(costCenterData.total_expense || 0, costCenterData.currency)}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="card border-0 text-center" style={{ background: '#1e293b' }}>
|
||||
<div className="card-body">
|
||||
<small className="text-slate-400">{t('reports.balance')}</small>
|
||||
<h5 className={`mb-0 ${(costCenterData.total_income - costCenterData.total_expense) >= 0 ? 'text-success' : 'text-danger'}`}>
|
||||
<div className={isMobile ? 'col-12' : 'col-md-4'}>
|
||||
<div className="card border-0 text-center" style={{ background: '#1e293b', padding: isMobile ? '0.75rem' : '1rem' }}>
|
||||
<div className="card-body" style={{ padding: isMobile ? '0.5rem' : '1rem' }}>
|
||||
<small className="text-slate-400" style={{ fontSize: isMobile ? '0.7rem' : '0.875rem' }}>
|
||||
{t('reports.balance')}
|
||||
</small>
|
||||
<h5
|
||||
className={`mb-0 ${(costCenterData.total_income - costCenterData.total_expense) >= 0 ? 'text-success' : 'text-danger'}`}
|
||||
style={{ fontSize: isMobile ? '0.95rem' : '1.25rem' }}
|
||||
>
|
||||
{currency((costCenterData.total_income || 0) - (costCenterData.total_expense || 0), costCenterData.currency)}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabela/Lista */}
|
||||
<div className="col-12">
|
||||
<div className="card border-0" style={{ background: '#0f172a' }}>
|
||||
<div className="card-header border-0 bg-transparent">
|
||||
<h6 className="text-white mb-0">
|
||||
<div className={`card-header border-0 bg-transparent ${isMobile ? 'p-2' : ''}`}>
|
||||
<h6 className="text-white mb-0" style={{ fontSize: isMobile ? '0.85rem' : '1rem' }}>
|
||||
<i className="bi bi-diagram-3 me-2"></i>
|
||||
{t('reports.byCostCenter')}
|
||||
{title}
|
||||
</h6>
|
||||
</div>
|
||||
<div className="card-body p-0">
|
||||
<div className="table-responsive">
|
||||
<table className="table table-dark table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('costCenters.name')}</th>
|
||||
<th className="text-end">{t('reports.income')}</th>
|
||||
<th className="text-end">{t('reports.expenses')}</th>
|
||||
<th className="text-end">{t('reports.balance')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map(cc => (
|
||||
<tr key={cc.id}>
|
||||
<td>
|
||||
<span className="me-2" style={{ color: cc.color || '#6b7280' }}>●</span>
|
||||
{cc.name}
|
||||
</td>
|
||||
<td className="text-end text-success">{currency(cc.income, costCenterData.currency)}</td>
|
||||
<td className="text-end text-danger">{currency(cc.expense, costCenterData.currency)}</td>
|
||||
<td className={`text-end ${cc.balance >= 0 ? 'text-success' : 'text-danger'}`}>
|
||||
{currency(cc.balance, costCenterData.currency)}
|
||||
</td>
|
||||
|
||||
{/* Desktop: Tabela */}
|
||||
{!isMobile && (
|
||||
<div className="card-body p-0">
|
||||
<div className="table-responsive">
|
||||
<table className="table table-dark table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{selectedCostCenter ? t('categories.category') : t('costCenters.name')}</th>
|
||||
<th className="text-end">{t('reports.income')}</th>
|
||||
<th className="text-end">{t('reports.expenses')}</th>
|
||||
<th className="text-end">{t('reports.balance')}</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item, idx) => (
|
||||
<tr
|
||||
key={item.id || idx}
|
||||
onClick={() => {
|
||||
if (!selectedCostCenter) {
|
||||
handleCostCenterClick(item);
|
||||
} else if (!selectedCostCenterCategory && item.id !== null) {
|
||||
handleCategoryClick(item);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
cursor: (!selectedCostCenter || (!selectedCostCenterCategory && item.id !== null)) ? 'pointer' : 'default'
|
||||
}}
|
||||
className={(!selectedCostCenter || (!selectedCostCenterCategory && item.id !== null)) ? 'table-hover-row' : ''}
|
||||
>
|
||||
<td>
|
||||
{item.icon && <i className={`bi ${item.icon} me-2`} style={{ color: item.color }}></i>}
|
||||
{!item.icon && <span className="me-2" style={{ color: item.color || '#6b7280' }}>●</span>}
|
||||
{item.name}
|
||||
</td>
|
||||
<td className="text-end text-success">{currency(item.income, costCenterData.currency)}</td>
|
||||
<td className="text-end text-danger">{currency(item.expense, costCenterData.currency)}</td>
|
||||
<td className={`text-end ${item.balance >= 0 ? 'text-success' : 'text-danger'}`}>
|
||||
{currency(item.balance, costCenterData.currency)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile: Cards */}
|
||||
{isMobile && (
|
||||
<div className="p-2" style={{ maxHeight: '500px', overflowY: 'auto' }}>
|
||||
{data.map((item, idx) => (
|
||||
<div
|
||||
key={item.id || idx}
|
||||
className="card mb-2 border-0"
|
||||
style={{
|
||||
background: '#1e293b',
|
||||
cursor: (!selectedCostCenter || (!selectedCostCenterCategory && item.id !== null)) ? 'pointer' : 'default'
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!selectedCostCenter) {
|
||||
handleCostCenterClick(item);
|
||||
} else if (!selectedCostCenterCategory && item.id !== null) {
|
||||
handleCategoryClick(item);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="card-body p-2">
|
||||
<div className="d-flex justify-content-between align-items-start mb-2">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
{item.icon && <i className={`bi ${item.icon}`} style={{ color: item.color, fontSize: '1.2rem' }}></i>}
|
||||
{!item.icon && <span style={{ color: item.color || '#6b7280', fontSize: '1.2rem' }}>●</span>}
|
||||
<span className="text-white fw-bold" style={{ fontSize: '0.85rem' }}>{item.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row g-2">
|
||||
<div className="col-4">
|
||||
<small className="text-slate-400 d-block" style={{ fontSize: '0.7rem' }}>
|
||||
{t('reports.income')}
|
||||
</small>
|
||||
<span className="text-success fw-bold" style={{ fontSize: '0.8rem' }}>
|
||||
{currency(item.income, costCenterData.currency)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
<small className="text-slate-400 d-block" style={{ fontSize: '0.7rem' }}>
|
||||
{t('reports.expenses')}
|
||||
</small>
|
||||
<span className="text-danger fw-bold" style={{ fontSize: '0.8rem' }}>
|
||||
{currency(item.expense, costCenterData.currency)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
<small className="text-slate-400 d-block" style={{ fontSize: '0.7rem' }}>
|
||||
{t('reports.balance')}
|
||||
</small>
|
||||
<span
|
||||
className={`fw-bold ${item.balance >= 0 ? 'text-success' : 'text-danger'}`}
|
||||
style={{ fontSize: '0.8rem' }}
|
||||
>
|
||||
{currency(item.balance, costCenterData.currency)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user