feat: Add contract summary calculations and update dashboard to display data by contract

This commit is contained in:
2026-01-22 20:01:22 +01:00
parent 3a382a28c0
commit 3d2065d8fb
3 changed files with 139 additions and 0 deletions

View File

@@ -35,6 +35,9 @@ export async function GET(request) {
};
});
// Calculate values by contract
const contractSummary = {};
projects.forEach(project => {
const value = parseFloat(project.wartosc_zlecenia) || 0;
const type = project.project_type;
@@ -46,6 +49,26 @@ export async function GET(request) {
} else if (project.wartosc_zlecenia && project.project_status !== 'cancelled') {
typeSummary[type].unrealisedValue += value;
}
// Group by contract
if (project.contract_number && project.wartosc_zlecenia && project.project_status !== 'cancelled') {
const contractKey = project.contract_number;
if (!contractSummary[contractKey]) {
contractSummary[contractKey] = {
contract_name: project.contract_name || project.contract_number,
realisedValue: 0,
unrealisedValue: 0,
totalValue: 0
};
}
if (project.project_status === 'fulfilled' && project.completion_date) {
contractSummary[contractKey].realisedValue += value;
} else {
contractSummary[contractKey].unrealisedValue += value;
}
contractSummary[contractKey].totalValue += value;
}
});
// Calculate overall totals
@@ -132,6 +155,26 @@ export async function GET(request) {
realisedValue: 158000,
unrealisedValue: 242000
}
},
byContract: {
'UMK/001/2024': {
contract_name: 'Modernizacja budynku głównego',
realisedValue: 320000,
unrealisedValue: 180000,
totalValue: 500000
},
'UMK/002/2024': {
contract_name: 'Budowa parkingu wielopoziomowego',
realisedValue: 480000,
unrealisedValue: 320000,
totalValue: 800000
},
'UMK/003/2024': {
contract_name: 'Remont elewacji',
realisedValue: 158000,
unrealisedValue: 242000,
totalValue: 400000
}
}
};
} else {
@@ -251,6 +294,17 @@ export async function GET(request) {
unrealisedValue: Math.round(data.unrealisedValue)
}
])
),
byContract: Object.fromEntries(
Object.entries(contractSummary).map(([contractNumber, data]) => [
contractNumber,
{
contract_name: data.contract_name,
realisedValue: Math.round(data.realisedValue),
unrealisedValue: Math.round(data.unrealisedValue),
totalValue: Math.round(data.totalValue)
}
])
)
}
});

View File

@@ -284,6 +284,87 @@ export default function TeamLeadsDashboard() {
</div>
</div>
</div>
{/* By Contract Section */}
{summaryData?.byContract && Object.keys(summaryData.byContract).length > 0 && (
<div className="mt-8">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-6">
{t('teamDashboard.byContract')}
</h2>
<div className="h-96">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={Object.entries(summaryData.byContract).map(([contractNumber, data]) => ({
name: contractNumber,
fullName: data.contract_name,
realised: data.realisedValue,
unrealised: data.unrealisedValue,
total: data.totalValue
}))}
margin={{
top: 20,
right: 30,
left: 20,
bottom: 100,
}}
>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis
dataKey="name"
angle={-45}
textAnchor="end"
height={100}
className="text-gray-600 dark:text-gray-400"
fontSize={11}
/>
<YAxis
className="text-gray-600 dark:text-gray-400"
fontSize={12}
tickFormatter={(value) => `${(value / 1000).toFixed(0)}k`}
/>
<Tooltip
content={({ active, payload }) => {
if (active && payload && payload.length) {
return (
<div className="bg-white dark:bg-gray-800 p-3 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg">
<p className="font-medium text-gray-900 dark:text-white mb-2">{payload[0].payload.fullName}</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">{payload[0].payload.name}</p>
<p className="text-green-600 dark:text-green-400 text-sm">
{`${t('teamDashboard.realised')}: ${formatCurrency(payload[0].payload.realised)}`}
</p>
<p className="text-purple-600 dark:text-purple-400 text-sm">
{`${t('teamDashboard.unrealised')}: ${formatCurrency(payload[0].payload.unrealised)}`}
</p>
<p className="text-blue-600 dark:text-blue-400 text-sm font-semibold mt-1">
{`${t('teamDashboard.total')}: ${formatCurrency(payload[0].payload.total)}`}
</p>
</div>
);
}
return null;
}}
/>
<Legend />
<Bar
dataKey="realised"
stackId="a"
fill="#10b981"
name={t('teamDashboard.realised')}
radius={[0, 0, 0, 0]}
/>
<Bar
dataKey="unrealised"
stackId="a"
fill="#8b5cf6"
name={t('teamDashboard.unrealised')}
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
)}
</div>
</PageContainer>
);

View File

@@ -136,6 +136,8 @@ const translations = {
realisedValue: "Wartość zrealizowana",
unrealisedValue: "Wartość niezrealizowana",
byProjectType: "Według typu projektu",
byContract: "Według umowy",
total: "Razem",
monthLabel: "Miesiąc:",
monthlyValue: "Wartość miesięczna:",
cumulative: "Skumulowana:",
@@ -769,6 +771,8 @@ const translations = {
realisedValue: "Realised Value",
unrealisedValue: "Unrealised Value",
byProjectType: "By Project Type",
byContract: "By Contract",
total: "Total",
monthLabel: "Month:",
monthlyValue: "Monthly Value:",
cumulative: "Cumulative:",