'use client'
Browse filesimport { useState, useEffect } from 'react'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { ArrowUpDown, TrendingUp, DollarSign, Calendar, Zap } from 'lucide-react'
interface KPIMetrics {
roi: number
paybackYears: number
cet: number
npv: number
irr: number
lcoe: number
}
interface FinancingDetails {
monthlyPayment: number
totalFinancingCost: number
breakEvenYears: number
}
interface BestFinancingOption {
institution: string
product: string
interestRate: number
maxTerm: number
ltvMax: number
}
interface LeaderboardEntry {
projectCategory: string
projectTier: string
systemSizeKw: number
annualEnergyGeneration: number
electricityTariff: number
bestFinancingOption: BestFinancingOption
kpis: KPIMetrics
financingDetails: FinancingDetails
}
interface KPITableProps {
data: LeaderboardEntry[]
isLoading?: boolean
onInteraction?: (action: string, data?: any) => void
}
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(value)
}
const formatPercent = (value: number) => {
return `${value.toFixed(2)}%`
}
const formatNumber = (value: number, decimals = 2) => {
return value.toFixed(decimals)
}
const getCategoryColor = (category: string) => {
const colors = {
XPP: 'bg-blue-100 text-blue-800',
PP: 'bg-green-100 text-green-800',
P: 'bg-yellow-100 text-yellow-800',
M: 'bg-orange-100 text-orange-800',
G: 'bg-red-100 text-red-800',
GG: 'bg-purple-100 text-purple-800'
}
return colors[category as keyof typeof colors] || 'bg-gray-100 text-gray-800'
}
const getTierColor = (tier: string) => {
const colors = {
PADRAO: 'bg-gray-100 text-gray-800',
CONSCIENTE: 'bg-emerald-100 text-emerald-800',
MODERADO: 'bg-amber-100 text-amber-800',
ACELERADO: 'bg-rose-100 text-rose-800'
}
return colors[tier as keyof typeof colors] || 'bg-gray-100 text-gray-800'
}
export function KPITable({
data,
isLoading,
onInteraction,
}: Readonly<KPITableProps>) {
const [sortBy, setSortBy] = useState<string>("roi")
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc")
const [filteredData, setFilteredData] = useState<LeaderboardEntry[]>(data)
useEffect(() => {
const sorted = [...data].sort((a, b) => {
const aValue = a.kpis[sortBy as keyof KPIMetrics] as number
const bValue = b.kpis[sortBy as keyof KPIMetrics] as number
return sortOrder === "desc" ? bValue - aValue : aValue - bValue
})
setFilteredData(sorted)
}, [data, sortBy, sortOrder])
const handleSort = (column: string) => {
if (sortBy === column) {
setSortOrder(sortOrder === "asc" ? "desc" : "asc")
} else {
setSortBy(column)
setSortOrder("desc")
}
onInteraction?.("sort_change", {
column,
sortOrder: sortOrder === "asc" ? "desc" : "asc",
})
}
if (isLoading) {
return (
<Card className="border-gradient">
<CardHeader>
<CardTitle>Carregando Leaderboard KPI...</CardTitle>
</CardHeader>
<CardContent>
<div className="animate-pulse space-y-4">
{[1, 2, 3, 4, 5].map((num) => (
<div
key={`loading-${num}`}
className="h-16 bg-gray-200 rounded"
></div>
))}
</div>
</CardContent>
</Card>
)
}
return (
<Card className="border-gradient">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
Leaderboard KPI - Projetos Solares vs Financiamento
</CardTitle>
<CardDescription>
Comparação de indicadores financeiros por categoria e tier de projeto
</CardDescription>
<div className="flex gap-4">
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-48">
<SelectValue placeholder="Ordenar por" />
</SelectTrigger>
<SelectContent>
<SelectItem value="roi">ROI (%)</SelectItem>
<SelectItem value="paybackYears">Payback (anos)</SelectItem>
<SelectItem value="cet">CET (%)</SelectItem>
<SelectItem value="npv">NPV (R$)</SelectItem>
<SelectItem value="irr">IRR (%)</SelectItem>
<SelectItem value="lcoe">LCOE (R$/kWh)</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
onClick={() => setSortOrder(sortOrder === "asc" ? "desc" : "asc")}
>
<ArrowUpDown className="h-4 w-4 mr-2" />
{sortOrder === "desc" ? "Decrescente" : "Crescente"}
</Button>
</div>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Projeto</TableHead>
<TableHead>Tamanho</TableHead>
<TableHead>Geração Anual</TableHead>
<TableHead>Tarifa</TableHead>
<TableHead>Instituição</TableHead>
<TableHead>Produto</TableHead>
<TableHead>Taxa (%)</TableHead>
<TableHead>Prazo (meses)</TableHead>
<TableHead>Parcela Mensal</TableHead>
<TableHead
className="cursor-pointer"
onClick={() => handleSort("roi")}
>
ROI{" "}
{sortBy === "roi" && (
<ArrowUpDown className="inline h-4 w-4 ml-1" />
)}
</TableHead>
<TableHead
className="cursor-pointer"
onClick={() => handleSort("paybackYears")}
>
Payback{" "}
{sortBy === "paybackYears" && (
<ArrowUpDown className="inline h-4 w-4 ml-1" />
)}
</TableHead>
<TableHead
className="cursor-pointer"
onClick={() => handleSort("cet")}
>
CET{" "}
{sortBy === "cet" && (
<ArrowUpDown className="inline h-4 w-4 ml-1" />
)}
</TableHead>
<TableHead
className="cursor-pointer"
onClick={() => handleSort("npv")}
>
NPV{" "}
{sortBy === "npv" && (
<ArrowUpDown className="inline h-4 w-4 ml-1" />
)}
</TableHead>
<TableHead
className="cursor-pointer"
onClick={() => handleSort("lcoe")}
>
LCOE{" "}
{sortBy === "lcoe" && (
<ArrowUpDown className="inline h-4 w-4 ml-1" />
)}
</TableHead>
<TableHead>Break-even</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredData.map((entry, index) => (
<TableRow
key={`${entry.projectCategory}-${entry.projectTier}-${index}`}
className="hover:bg-[var(--glass-light)] hover:backdrop-blur-sm transition-all duration-200 cursor-pointer"
onClick={() => onInteraction?.("row_click", { entry, index })}
>
<TableCell>
<div className="flex gap-2">
<Badge
className={getCategoryColor(entry.projectCategory)}
>
{entry.projectCategory}
</Badge>
<Badge className={getTierColor(entry.projectTier)}>
{entry.projectTier}
</Badge>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Zap className="h-4 w-4 text-yellow-500" />
{formatNumber(entry.systemSizeKw)} kWp
</div>
</TableCell>
<TableCell>
{formatNumber(entry.annualEnergyGeneration)} kWh/ano
</TableCell>
<TableCell>
{formatCurrency(entry.electricityTariff)}
</TableCell>
<TableCell className="font-medium">
{entry.bestFinancingOption.institution}
</TableCell>
<TableCell>{entry.bestFinancingOption.product}</TableCell>
<TableCell>
{formatPercent(entry.bestFinancingOption.interestRate)}
</TableCell>
<TableCell>
{entry.bestFinancingOption.maxTerm} meses
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<DollarSign className="h-4 w-4 text-green-500" />
{formatCurrency(entry.financingDetails.monthlyPayment)}
</div>
</TableCell>
<TableCell className="font-semibold text-green-600">
{formatPercent(entry.kpis.roi)}
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4 text-blue-500" />
{formatNumber(entry.kpis.paybackYears)} anos
</div>
</TableCell>
<TableCell>{formatPercent(entry.kpis.cet)}</Ta
|
@@ -26,11 +26,93 @@ function setupThemeToggle() {
|
|
| 26 |
|
| 27 |
document.addEventListener('DOMContentLoaded', () => {
|
| 28 |
setupThemeToggle();
|
| 29 |
-
|
| 30 |
-
// Mock data for demonstration
|
| 31 |
const mockData = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
panels: {
|
| 33 |
-
|
| 34 |
{ rank: 1, brand: "SunPower", model: "X22-370", value: 370, unit: "Wp", series: "X-Series", price: 899, efficiency: 22.8, warranty: 25 },
|
| 35 |
{ rank: 2, brand: "LG", model: "LG370Q1C-A5", value: 370, unit: "Wp", series: "NeON R", price: 799, efficiency: 21.7, warranty: 25 },
|
| 36 |
{ rank: 3, brand: "Panasonic", model: "VBHN330SA16", value: 330, unit: "Wp", series: "HIT", price: 699, efficiency: 19.7, warranty: 25 },
|
|
@@ -64,12 +146,11 @@ const mockData = {
|
|
| 64 |
]
|
| 65 |
}
|
| 66 |
};
|
| 67 |
-
|
| 68 |
-
// Render the equipment ranking table
|
| 69 |
const app = document.getElementById('app');
|
| 70 |
app.innerHTML = `
|
| 71 |
-
<div class="space-y-
|
| 72 |
-
|
| 73 |
<div class="tabs-list grid w-full grid-cols-2 gap-2 mb-6">
|
| 74 |
<button class="tab-trigger active flex items-center gap-2 py-3 px-4 rounded-lg bg-primary-500 text-white" data-tab="panels">
|
| 75 |
<i data-feather="zap"></i>
|
|
@@ -93,10 +174,149 @@ const mockData = {
|
|
| 93 |
${renderRankingTable("Top Eficiência - Inversores", mockData.inverters.topEfficiency, "trending-up", "Eficiência (%)")}
|
| 94 |
</div>
|
| 95 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
</div>
|
| 97 |
`;
|
| 98 |
|
| 99 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
document.querySelectorAll('.tab-trigger').forEach(trigger => {
|
| 101 |
trigger.addEventListener('click', () => {
|
| 102 |
const tabId = trigger.dataset.tab;
|
|
|
|
| 26 |
|
| 27 |
document.addEventListener('DOMContentLoaded', () => {
|
| 28 |
setupThemeToggle();
|
| 29 |
+
// Mock data for solar projects and financing
|
|
|
|
| 30 |
const mockData = {
|
| 31 |
+
projects: [
|
| 32 |
+
{
|
| 33 |
+
projectCategory: "XPP",
|
| 34 |
+
projectTier: "ACELERADO",
|
| 35 |
+
systemSizeKw: 10.5,
|
| 36 |
+
annualEnergyGeneration: 15000,
|
| 37 |
+
electricityTariff: 0.85,
|
| 38 |
+
bestFinancingOption: {
|
| 39 |
+
institution: "Banco Solar",
|
| 40 |
+
product: "Financiamento Verde",
|
| 41 |
+
interestRate: 8.5,
|
| 42 |
+
maxTerm: 120,
|
| 43 |
+
ltvMax: 80
|
| 44 |
+
},
|
| 45 |
+
kpis: {
|
| 46 |
+
roi: 25.8,
|
| 47 |
+
paybackYears: 4.2,
|
| 48 |
+
cet: 12.3,
|
| 49 |
+
npv: 45000,
|
| 50 |
+
irr: 18.7,
|
| 51 |
+
lcoe: 0.32
|
| 52 |
+
},
|
| 53 |
+
financingDetails: {
|
| 54 |
+
monthlyPayment: 850,
|
| 55 |
+
totalFinancingCost: 102000,
|
| 56 |
+
breakEvenYears: 5.1
|
| 57 |
+
}
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
projectCategory: "PP",
|
| 61 |
+
projectTier: "MODERADO",
|
| 62 |
+
systemSizeKw: 7.2,
|
| 63 |
+
annualEnergyGeneration: 10500,
|
| 64 |
+
electricityTariff: 0.75,
|
| 65 |
+
bestFinancingOption: {
|
| 66 |
+
institution: "Banco Verde",
|
| 67 |
+
product: "Crédito Sustentável",
|
| 68 |
+
interestRate: 9.2,
|
| 69 |
+
maxTerm: 96,
|
| 70 |
+
ltvMax: 75
|
| 71 |
+
},
|
| 72 |
+
kpis: {
|
| 73 |
+
roi: 21.3,
|
| 74 |
+
paybackYears: 5.1,
|
| 75 |
+
cet: 13.5,
|
| 76 |
+
npv: 32000,
|
| 77 |
+
irr: 15.2,
|
| 78 |
+
lcoe: 0.38
|
| 79 |
+
},
|
| 80 |
+
financingDetails: {
|
| 81 |
+
monthlyPayment: 620,
|
| 82 |
+
totalFinancingCost: 59520,
|
| 83 |
+
breakEvenYears: 6.3
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
projectCategory: "P",
|
| 88 |
+
projectTier: "CONSCIENTE",
|
| 89 |
+
systemSizeKw: 5.0,
|
| 90 |
+
annualEnergyGeneration: 7200,
|
| 91 |
+
electricityTariff: 0.68,
|
| 92 |
+
bestFinancingOption: {
|
| 93 |
+
institution: "Banco Eco",
|
| 94 |
+
product: "Linha Solar",
|
| 95 |
+
interestRate: 10.5,
|
| 96 |
+
maxTerm: 84,
|
| 97 |
+
ltvMax: 70
|
| 98 |
+
},
|
| 99 |
+
kpis: {
|
| 100 |
+
roi: 18.7,
|
| 101 |
+
paybackYears: 6.8,
|
| 102 |
+
cet: 14.8,
|
| 103 |
+
npv: 21000,
|
| 104 |
+
irr: 12.5,
|
| 105 |
+
lcoe: 0.42
|
| 106 |
+
},
|
| 107 |
+
financingDetails: {
|
| 108 |
+
monthlyPayment: 480,
|
| 109 |
+
totalFinancingCost: 40320,
|
| 110 |
+
breakEvenYears: 7.5
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
],
|
| 114 |
panels: {
|
| 115 |
+
topPower: [
|
| 116 |
{ rank: 1, brand: "SunPower", model: "X22-370", value: 370, unit: "Wp", series: "X-Series", price: 899, efficiency: 22.8, warranty: 25 },
|
| 117 |
{ rank: 2, brand: "LG", model: "LG370Q1C-A5", value: 370, unit: "Wp", series: "NeON R", price: 799, efficiency: 21.7, warranty: 25 },
|
| 118 |
{ rank: 3, brand: "Panasonic", model: "VBHN330SA16", value: 330, unit: "Wp", series: "HIT", price: 699, efficiency: 19.7, warranty: 25 },
|
|
|
|
| 146 |
]
|
| 147 |
}
|
| 148 |
};
|
| 149 |
+
// Render the equipment ranking and KPI tables
|
|
|
|
| 150 |
const app = document.getElementById('app');
|
| 151 |
app.innerHTML = `
|
| 152 |
+
<div class="space-y-12">
|
| 153 |
+
<div class="tabs">
|
| 154 |
<div class="tabs-list grid w-full grid-cols-2 gap-2 mb-6">
|
| 155 |
<button class="tab-trigger active flex items-center gap-2 py-3 px-4 rounded-lg bg-primary-500 text-white" data-tab="panels">
|
| 156 |
<i data-feather="zap"></i>
|
|
|
|
| 174 |
${renderRankingTable("Top Eficiência - Inversores", mockData.inverters.topEfficiency, "trending-up", "Eficiência (%)")}
|
| 175 |
</div>
|
| 176 |
</div>
|
| 177 |
+
<div class="space-y-6">
|
| 178 |
+
<div class="tabs">
|
| 179 |
+
<div class="tabs-list grid w-full grid-cols-2 gap-2 mb-6">
|
| 180 |
+
<button class="tab-trigger active flex items-center gap-2 py-3 px-4 rounded-lg bg-primary-500 text-white" data-tab="panels">
|
| 181 |
+
<i data-feather="zap"></i>
|
| 182 |
+
Painéis Solares
|
| 183 |
+
</button>
|
| 184 |
+
<button class="tab-trigger flex items-center gap-2 py-3 px-4 rounded-lg bg-gray-200 text-gray-700" data-tab="inverters">
|
| 185 |
+
<i data-feather="trending-up"></i>
|
| 186 |
+
Inversores
|
| 187 |
+
</button>
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
<div class="tab-content active" data-tab="panels">
|
| 191 |
+
${renderRankingTable("Maior Potência - Painéis Solares", mockData.panels.topPower, "zap", "Potência (Wp)")}
|
| 192 |
+
${renderRankingTable("Melhor Preço por Watt - Painéis Solares", mockData.panels.bestPricePerWatt, "dollar-sign", "Preço/W (R$/W)")}
|
| 193 |
+
${renderRankingTable("Top Eficiência - Painéis Solares", mockData.panels.topEfficiency, "trending-up", "Eficiência (%)")}
|
| 194 |
+
</div>
|
| 195 |
+
|
| 196 |
+
<div class="tab-content hidden" data-tab="inverters">
|
| 197 |
+
${renderRankingTable("Maior Garantia - Inversores", mockData.inverters.longestWarranty, "shield", "Garantia (anos)")}
|
| 198 |
+
${renderRankingTable("Melhor Preço por kW - Inversores", mockData.inverters.bestPricePerKw, "dollar-sign", "Preço/kW (R$/kW)")}
|
| 199 |
+
${renderRankingTable("Top Eficiência - Inversores", mockData.inverters.topEfficiency, "trending-up", "Eficiência (%)")}
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
|
| 204 |
+
<div class="border-gradient">
|
| 205 |
+
<div class="card-header p-6">
|
| 206 |
+
<h3 class="text-xl font-bold flex items-center gap-2">
|
| 207 |
+
<i data-feather="trending-up" class="text-primary-500"></i>
|
| 208 |
+
Leaderboard KPI - Projetos Solares vs Financiamento
|
| 209 |
+
</h3>
|
| 210 |
+
<p class="text-gray-600 mt-1">Comparação de indicadores financeiros por categoria e tier de projeto</p>
|
| 211 |
+
</div>
|
| 212 |
+
<div class="card-content px-6 pb-6">
|
| 213 |
+
<div class="flex gap-4 mb-4">
|
| 214 |
+
<select class="w-48 p-2 rounded border border-gray-300">
|
| 215 |
+
<option value="roi">ROI (%)</option>
|
| 216 |
+
<option value="paybackYears">Payback (anos)</option>
|
| 217 |
+
<option value="cet">CET (%)</option>
|
| 218 |
+
<option value="npv">NPV (R$)</option>
|
| 219 |
+
<option value="irr">IRR (%)</option>
|
| 220 |
+
<option value="lcoe">LCOE (R$/kWh)</option>
|
| 221 |
+
</select>
|
| 222 |
+
<button class="flex items-center gap-2 px-4 py-2 bg-gray-100 rounded hover:bg-gray-200">
|
| 223 |
+
<i data-feather="arrow-up-down" class="h-4 w-4"></i>
|
| 224 |
+
Ordenar
|
| 225 |
+
</button>
|
| 226 |
+
</div>
|
| 227 |
+
<div class="overflow-x-auto">
|
| 228 |
+
<table class="w-full">
|
| 229 |
+
<thead>
|
| 230 |
+
<tr class="text-left border-b">
|
| 231 |
+
<th class="pb-3">Projeto</th>
|
| 232 |
+
<th class="pb-3">Tamanho</th>
|
| 233 |
+
<th class="pb-3">Geração Anual</th>
|
| 234 |
+
<th class="pb-3">Tarifa</th>
|
| 235 |
+
<th class="pb-3">Instituição</th>
|
| 236 |
+
<th class="pb-3">Produto</th>
|
| 237 |
+
<th class="pb-3">Taxa (%)</th>
|
| 238 |
+
<th class="pb-3">Prazo (meses)</th>
|
| 239 |
+
<th class="pb-3">Parcela Mensal</th>
|
| 240 |
+
<th class="pb-3 cursor-pointer">ROI</th>
|
| 241 |
+
<th class="pb-3 cursor-pointer">Payback</th>
|
| 242 |
+
<th class="pb-3 cursor-pointer">CET</th>
|
| 243 |
+
<th class="pb-3 cursor-pointer">NPV</th>
|
| 244 |
+
<th class="pb-3 cursor-pointer">LCOE</th>
|
| 245 |
+
<th class="pb-3">Break-even</th>
|
| 246 |
+
</tr>
|
| 247 |
+
</thead>
|
| 248 |
+
<tbody>
|
| 249 |
+
${mockData.projects.map(project => `
|
| 250 |
+
<tr class="border-b hover:bg-primary-50 transition-colors cursor-pointer">
|
| 251 |
+
<td class="py-4">
|
| 252 |
+
<div class="flex gap-2">
|
| 253 |
+
<span class="px-2 py-1 rounded text-xs ${getCategoryColor(project.projectCategory)}">
|
| 254 |
+
${project.projectCategory}
|
| 255 |
+
</span>
|
| 256 |
+
<span class="px-2 py-1 rounded text-xs ${getTierColor(project.projectTier)}">
|
| 257 |
+
${project.projectTier}
|
| 258 |
+
</span>
|
| 259 |
+
</div>
|
| 260 |
+
</td>
|
| 261 |
+
<td>
|
| 262 |
+
<div class="flex items-center gap-1">
|
| 263 |
+
<i data-feather="zap" class="h-4 w-4 text-yellow-500"></i>
|
| 264 |
+
${formatNumber(project.systemSizeKw)} kWp
|
| 265 |
+
</div>
|
| 266 |
+
</td>
|
| 267 |
+
<td>${formatNumber(project.annualEnergyGeneration)} kWh/ano</td>
|
| 268 |
+
<td>${formatCurrency(project.electricityTariff)}</td>
|
| 269 |
+
<td class="font-medium">${project.bestFinancingOption.institution}</td>
|
| 270 |
+
<td>${project.bestFinancingOption.product}</td>
|
| 271 |
+
<td>${formatPercent(project.bestFinancingOption.interestRate)}</td>
|
| 272 |
+
<td>${project.bestFinancingOption.maxTerm} meses</td>
|
| 273 |
+
<td>
|
| 274 |
+
<div class="flex items-center gap-1">
|
| 275 |
+
<i data-feather="dollar-sign" class="h-4 w-4 text-green-500"></i>
|
| 276 |
+
${formatCurrency(project.financingDetails.monthlyPayment)}
|
| 277 |
+
</div>
|
| 278 |
+
</td>
|
| 279 |
+
<td class="font-semibold text-green-600">${formatPercent(project.kpis.roi)}</td>
|
| 280 |
+
<td>
|
| 281 |
+
<div class="flex items-center gap-1">
|
| 282 |
+
<i data-feather="calendar" class="h-4 w-4 text-blue-500"></i>
|
| 283 |
+
${formatNumber(project.kpis.paybackYears)} anos
|
| 284 |
+
</div>
|
| 285 |
+
</td>
|
| 286 |
+
<td>${formatPercent(project.kpis.cet)}</td>
|
| 287 |
+
<td>${formatCurrency(project.kpis.npv)}</td>
|
| 288 |
+
<td>${formatCurrency(project.kpis.lcoe)}</td>
|
| 289 |
+
<td>${formatNumber(project.financingDetails.breakEvenYears)} anos</td>
|
| 290 |
+
</tr>
|
| 291 |
+
`).join('')}
|
| 292 |
+
</tbody>
|
| 293 |
+
</table>
|
| 294 |
+
</div>
|
| 295 |
+
</div>
|
| 296 |
+
</div>
|
| 297 |
</div>
|
| 298 |
`;
|
| 299 |
|
| 300 |
+
// Add event listeners for sorting
|
| 301 |
+
document.querySelectorAll('th.cursor-pointer').forEach(th => {
|
| 302 |
+
th.addEventListener('click', () => {
|
| 303 |
+
const column = th.textContent.trim().toLowerCase();
|
| 304 |
+
console.log(`Sorting by ${column}`);
|
| 305 |
+
// Implement sorting logic here
|
| 306 |
+
});
|
| 307 |
+
});
|
| 308 |
+
|
| 309 |
+
document.querySelector('select').addEventListener('change', (e) => {
|
| 310 |
+
const sortBy = e.target.value;
|
| 311 |
+
console.log(`Sorting by ${sortBy}`);
|
| 312 |
+
// Implement sorting logic here
|
| 313 |
+
});
|
| 314 |
+
|
| 315 |
+
document.querySelector('button').addEventListener('click', () => {
|
| 316 |
+
console.log('Toggling sort order');
|
| 317 |
+
// Implement sort order toggle here
|
| 318 |
+
});
|
| 319 |
+
// Tab functionality
|
| 320 |
document.querySelectorAll('.tab-trigger').forEach(trigger => {
|
| 321 |
trigger.addEventListener('click', () => {
|
| 322 |
const tabId = trigger.dataset.tab;
|
|
@@ -33,10 +33,105 @@
|
|
| 33 |
.dark-theme table td {
|
| 34 |
border-color: #334155;
|
| 35 |
}
|
| 36 |
-
|
| 37 |
.dark-theme .hover-scale:hover {
|
| 38 |
background-color: #1e293b;
|
| 39 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
.glass-effect {
|
| 41 |
background: rgba(255, 255, 255, 0.7);
|
| 42 |
backdrop-filter: blur(10px);
|
|
|
|
| 33 |
.dark-theme table td {
|
| 34 |
border-color: #334155;
|
| 35 |
}
|
|
|
|
| 36 |
.dark-theme .hover-scale:hover {
|
| 37 |
background-color: #1e293b;
|
| 38 |
}
|
| 39 |
+
|
| 40 |
+
/* KPI Table Styles */
|
| 41 |
+
.kpi-table {
|
| 42 |
+
width: 100%;
|
| 43 |
+
border-collapse: collapse;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.kpi-table th {
|
| 47 |
+
text-align: left;
|
| 48 |
+
padding: 0.75rem;
|
| 49 |
+
border-bottom: 1px solid #e5e7eb;
|
| 50 |
+
font-weight: 600;
|
| 51 |
+
color: #4b5563;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.kpi-table td {
|
| 55 |
+
padding: 0.75rem;
|
| 56 |
+
border-bottom: 1px solid #e5e7eb;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.kpi-table tr:hover {
|
| 60 |
+
background-color: #f9fafb;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.dark-theme .kpi-table th {
|
| 64 |
+
color: #9ca3af;
|
| 65 |
+
border-color: #374151;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.dark-theme .kpi-table td {
|
| 69 |
+
border-color: #374151;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.dark-theme .kpi-table tr:hover {
|
| 73 |
+
background-color: #1f2937;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* Badge Styles */
|
| 77 |
+
.badge {
|
| 78 |
+
display: inline-block;
|
| 79 |
+
padding: 0.25rem 0.5rem;
|
| 80 |
+
border-radius: 0.25rem;
|
| 81 |
+
font-size: 0.75rem;
|
| 82 |
+
font-weight: 600;
|
| 83 |
+
text-transform: uppercase;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.badge-blue {
|
| 87 |
+
background-color: #dbeafe;
|
| 88 |
+
color: #1e40af;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.badge-green {
|
| 92 |
+
background-color: #d1fae5;
|
| 93 |
+
color: #065f46;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.badge-yellow {
|
| 97 |
+
background-color: #fef3c7;
|
| 98 |
+
color: #92400e;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.badge-orange {
|
| 102 |
+
background-color: #ffedd5;
|
| 103 |
+
color: #9a3412;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.badge-red {
|
| 107 |
+
background-color: #fee2e2;
|
| 108 |
+
color: #991b1b;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.badge-purple {
|
| 112 |
+
background-color: #f3e8ff;
|
| 113 |
+
color: #6b21a8;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.badge-gray {
|
| 117 |
+
background-color: #f3f4f6;
|
| 118 |
+
color: #4b5563;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.badge-emerald {
|
| 122 |
+
background-color: #d1fae5;
|
| 123 |
+
color: #047857;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.badge-amber {
|
| 127 |
+
background-color: #fef3c7;
|
| 128 |
+
color: #b45309;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.badge-rose {
|
| 132 |
+
background-color: #ffe4e6;
|
| 133 |
+
color: #be123c;
|
| 134 |
+
}
|
| 135 |
.glass-effect {
|
| 136 |
background: rgba(255, 255, 255, 0.7);
|
| 137 |
backdrop-filter: blur(10px);
|