All checks were successful
Build and Push Docker Images / docker (push) Successful in 49s
Backend: - Add getWrenPortfolios() to wrenClient.js with detailed logging including certification status - Modify checkout endpoint to fetch and include portfolio data in order response - Add stripeSessionId to order response for Payment ID display Frontend: - Create new StaticPortfolioPieChart component for printable receipt - Add portfolio distribution visualization to CheckoutSuccess page - Update receipt to show Payment ID (Stripe) and Offsetting Order ID (Wren) - Implement responsive grid layout for order IDs - Add print-friendly styling with Tailwind print classes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
195 lines
6.2 KiB
TypeScript
195 lines
6.2 KiB
TypeScript
import React from 'react';
|
|
import type { OffsetProject } from '../types';
|
|
import { getProjectColor } from '../utils/portfolioColors';
|
|
|
|
interface StaticPortfolioPieChartProps {
|
|
projects: OffsetProject[];
|
|
totalTons: number;
|
|
size?: number;
|
|
}
|
|
|
|
export function StaticPortfolioPieChart({
|
|
projects,
|
|
totalTons,
|
|
size = 280,
|
|
}: StaticPortfolioPieChartProps) {
|
|
const centerX = size / 2;
|
|
const centerY = size / 2;
|
|
const radius = size * 0.35;
|
|
const innerRadius = size * 0.20; // Slightly smaller for better labeling
|
|
const labelRadius = size * 0.50; // Distance for label lines
|
|
|
|
// Helper function to convert polar coordinates to cartesian
|
|
const polarToCartesian = (centerX: number, centerY: number, radius: number, angleInDegrees: number) => {
|
|
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
|
|
return {
|
|
x: centerX + radius * Math.cos(angleInRadians),
|
|
y: centerY + radius * Math.sin(angleInRadians),
|
|
};
|
|
};
|
|
|
|
// Generate SVG path for a donut segment
|
|
const createSegmentPath = (
|
|
startAngle: number,
|
|
endAngle: number,
|
|
outerR: number,
|
|
innerR: number
|
|
): string => {
|
|
const start = polarToCartesian(centerX, centerY, outerR, endAngle);
|
|
const end = polarToCartesian(centerX, centerY, outerR, startAngle);
|
|
const innerStart = polarToCartesian(centerX, centerY, innerR, endAngle);
|
|
const innerEnd = polarToCartesian(centerX, centerY, innerR, startAngle);
|
|
const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1';
|
|
|
|
return [
|
|
'M', start.x, start.y,
|
|
'A', outerR, outerR, 0, largeArcFlag, 0, end.x, end.y,
|
|
'L', innerEnd.x, innerEnd.y,
|
|
'A', innerR, innerR, 0, largeArcFlag, 1, innerStart.x, innerStart.y,
|
|
'Z',
|
|
].join(' ');
|
|
};
|
|
|
|
// Calculate segments with label positions
|
|
const segments: Array<{
|
|
path: string;
|
|
percentage: number;
|
|
project: OffsetProject;
|
|
color: string;
|
|
midAngle: number;
|
|
tons: number;
|
|
}> = [];
|
|
|
|
let currentAngle = 0;
|
|
|
|
projects.forEach((project, index) => {
|
|
if (!project.percentage) return;
|
|
|
|
const percentage = project.percentage * 100;
|
|
const segmentAngle = (percentage / 100) * 360;
|
|
const midAngle = currentAngle + segmentAngle / 2;
|
|
const tons = parseFloat((totalTons * project.percentage).toFixed(2));
|
|
const color = getProjectColor(index, projects.length);
|
|
|
|
segments.push({
|
|
path: createSegmentPath(currentAngle, currentAngle + segmentAngle, radius, innerRadius),
|
|
percentage,
|
|
project,
|
|
color,
|
|
midAngle,
|
|
tons,
|
|
});
|
|
|
|
currentAngle += segmentAngle;
|
|
});
|
|
|
|
return (
|
|
<div className="flex flex-col items-center w-full">
|
|
{/* SVG Chart */}
|
|
<svg
|
|
width={size * 2.2}
|
|
height={size * 1.4}
|
|
viewBox={`0 0 ${size * 2.2} ${size * 1.4}`}
|
|
className="overflow-visible print:overflow-visible"
|
|
>
|
|
{/* Donut segments */}
|
|
<g transform={`translate(${size * 1.1 - centerX}, ${size * 0.7 - centerY})`}>
|
|
{segments.map((segment, index) => {
|
|
const labelPoint = polarToCartesian(centerX, centerY, labelRadius, segment.midAngle);
|
|
const textAnchor = labelPoint.x > centerX ? 'start' : 'end';
|
|
const lineEndX = labelPoint.x > centerX ? labelPoint.x + 40 : labelPoint.x - 40;
|
|
|
|
return (
|
|
<g key={index}>
|
|
{/* Segment path */}
|
|
<path
|
|
d={segment.path}
|
|
fill={segment.color}
|
|
stroke="white"
|
|
strokeWidth="2"
|
|
className="print:stroke-gray-300"
|
|
/>
|
|
|
|
{/* Label line */}
|
|
<line
|
|
x1={labelPoint.x}
|
|
y1={labelPoint.y}
|
|
x2={lineEndX}
|
|
y2={labelPoint.y}
|
|
stroke={segment.color}
|
|
strokeWidth="2"
|
|
className="print:stroke-gray-400"
|
|
/>
|
|
|
|
{/* Label text */}
|
|
<text
|
|
x={lineEndX}
|
|
y={labelPoint.y - 20}
|
|
textAnchor={textAnchor}
|
|
className="text-sm font-semibold fill-slate-700 print:text-xs"
|
|
>
|
|
{segment.project.name}
|
|
</text>
|
|
<text
|
|
x={lineEndX}
|
|
y={labelPoint.y - 4}
|
|
textAnchor={textAnchor}
|
|
className="text-xs fill-slate-600 print:text-[10px]"
|
|
>
|
|
{segment.tons} tons
|
|
</text>
|
|
<text
|
|
x={lineEndX}
|
|
y={labelPoint.y + 12}
|
|
textAnchor={textAnchor}
|
|
className="text-xs font-bold fill-slate-800 print:text-[10px]"
|
|
>
|
|
{segment.percentage.toFixed(1)}%
|
|
</text>
|
|
</g>
|
|
);
|
|
})}
|
|
|
|
{/* Center text showing total */}
|
|
<text
|
|
x={centerX}
|
|
y={centerY - 10}
|
|
textAnchor="middle"
|
|
className="text-2xl font-bold fill-emerald-600 print:text-xl"
|
|
>
|
|
{totalTons}
|
|
</text>
|
|
<text
|
|
x={centerX}
|
|
y={centerY + 8}
|
|
textAnchor="middle"
|
|
className="text-sm fill-slate-600 print:text-xs"
|
|
>
|
|
tons CO₂
|
|
</text>
|
|
</g>
|
|
</svg>
|
|
|
|
{/* Legend below chart (for print clarity) */}
|
|
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-3 w-full max-w-2xl print:mt-4 print:gap-2">
|
|
{segments.map((segment, index) => (
|
|
<div key={index} className="flex items-center gap-3 print:gap-2">
|
|
<div
|
|
className="w-4 h-4 rounded-sm flex-shrink-0 print:w-3 print:h-3"
|
|
style={{ backgroundColor: segment.color }}
|
|
/>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-slate-800 truncate print:text-xs">
|
|
{segment.project.name}
|
|
</p>
|
|
<p className="text-xs text-slate-600 print:text-[10px]">
|
|
{segment.tons} tons ({segment.percentage.toFixed(1)}%)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|