Initial commit: MOPC platform with Docker deployment setup

Full Next.js 15 platform with tRPC, Prisma, PostgreSQL, NextAuth.
Includes production Dockerfile (multi-stage, port 7600), docker-compose
with registry-based image pull, Gitea Actions CI workflow, nginx config
for portal.monaco-opc.com, deployment scripts, and DEPLOYMENT.md guide.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 13:41:32 +01:00
commit a606292aaa
290 changed files with 70691 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
'use client'
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Cell,
ReferenceLine,
} from 'recharts'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
interface ProjectRankingData {
id: string
title: string
teamName: string | null
status: string
averageScore: number | null
evaluationCount: number
}
interface ProjectRankingsProps {
data: ProjectRankingData[]
limit?: number
}
// Generate color based on score (red to green gradient)
const getScoreColor = (score: number): string => {
if (score >= 8) return '#0bd90f' // Excellent - green
if (score >= 6) return '#82ca9d' // Good - light green
if (score >= 4) return '#ffc658' // Average - yellow
if (score >= 2) return '#ff7300' // Poor - orange
return '#de0f1e' // Very poor - red
}
export function ProjectRankingsChart({
data,
limit = 20,
}: ProjectRankingsProps) {
const displayData = data.slice(0, limit).map((d, index) => ({
...d,
rank: index + 1,
displayTitle:
d.title.length > 25 ? d.title.substring(0, 25) + '...' : d.title,
score: d.averageScore || 0,
}))
const averageScore =
data.length > 0
? data.reduce((sum, d) => sum + (d.averageScore || 0), 0) / data.length
: 0
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Project Rankings</span>
<span className="text-sm font-normal text-muted-foreground">
Top {displayData.length} of {data.length} projects
</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[500px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={displayData}
layout="vertical"
margin={{ top: 20, right: 30, bottom: 20, left: 150 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis type="number" domain={[0, 10]} />
<YAxis
dataKey="displayTitle"
type="category"
width={140}
tick={{ fontSize: 11 }}
/>
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '6px',
}}
formatter={(value: number | undefined) => [(value ?? 0).toFixed(2), 'Average Score']}
labelFormatter={(_, payload) => {
if (payload && payload[0]) {
const item = payload[0].payload as ProjectRankingData & {
rank: number
}
return `#${item.rank} - ${item.title}${item.teamName ? ` (${item.teamName})` : ''}`
}
return ''
}}
/>
<ReferenceLine
x={averageScore}
stroke="#666"
strokeDasharray="5 5"
label={{
value: `Avg: ${averageScore.toFixed(1)}`,
position: 'top',
fill: '#666',
fontSize: 11,
}}
/>
<Bar dataKey="score" radius={[0, 4, 4, 0]}>
{displayData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={getScoreColor(entry.score)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
)
}