SPB Git

spb/vquant Public MIT

VibeQuant — AI-powered institutional-grade financial intelligence platform.

TypeScript 84.3% Python 11.7% JavaScript 1.6% CSS 1.5% HTML 0.7%
11.4 KB · 290 lines tsx
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      client/src/components/explore/financial-statements-view.tsx6 *7 *  Author:    Simon-Pierre Boucher8 *  Contact:   contact@spboucher.ai9 *  Website:   https://www.spboucher.ai10 *  Demo:      https://www.vquant.ai11 *  License:   MIT (see LICENSE)12 *13 *  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";18import { Badge } from "@/components/ui/badge";19import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";20import { TrendingUp, TrendingDown, FileText, DollarSign, Wallet, Activity } from "lucide-react";2122interface FinancialStatement {23  date: string;24  symbol: string;25  reportedCurrency: string;26  cik: string;27  fillingDate: string;28  acceptedDate: string;29  calendarYear: string;30  period: string;31  link: string;32  finalLink: string;33  [key: string]: any;34}3536interface FinancialStatementsViewProps {37  incomeStatement?: FinancialStatement[];38  balanceSheet?: FinancialStatement[];39  cashFlow?: FinancialStatement[];40  period: "annual" | "quarter";41}4243export function FinancialStatementsView({44  incomeStatement,45  balanceSheet,46  cashFlow,47  period48}: FinancialStatementsViewProps) {49  const formatCurrency = (value: number) => {50    if (!value) return "N/A";51    const absValue = Math.abs(value);52    if (absValue >= 1e12) return `$${(value / 1e12).toFixed(2)}T`;53    if (absValue >= 1e9) return `$${(value / 1e9).toFixed(2)}B`;54    if (absValue >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;55    if (absValue >= 1e3) return `$${(value / 1e3).toFixed(2)}K`;56    return `$${value.toFixed(2)}`;57  };5859  const formatPercent = (value: number) => {60    if (!value) return "N/A";61    return `${(value * 100).toFixed(2)}%`;62  };6364  const formatDate = (dateString: string) => {65    return new Date(dateString).toLocaleDateString('fr-FR', {66      year: 'numeric',67      month: 'short',68      day: 'numeric'69    });70  };7172  const getChangeIndicator = (current: number, previous: number) => {73    if (!current || !previous) return null;74    const change = ((current - previous) / Math.abs(previous)) * 100;75    const isPositive = change >= 0;7677    return (78      <span className={`text-xs flex items-center gap-1 ${isPositive ? 'text-green-500' : 'text-red-500'}`}>79        {isPositive ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}80        {isPositive ? '+' : ''}{change.toFixed(1)}%81      </span>82    );83  };8485  // Income Statement86  const incomeStatementRows = [87    { label: "Revenu Total", key: "revenue", highlight: true },88    { label: "Coût des Revenus", key: "costOfRevenue" },89    { label: "Marge Brute", key: "grossProfit", highlight: true },90    { label: "Dépenses R&D", key: "researchAndDevelopmentExpenses" },91    { label: "Dépenses Générales", key: "generalAndAdministrativeExpenses" },92    { label: "Dépenses Marketing", key: "sellingAndMarketingExpenses" },93    { label: "Dépenses d'Exploitation", key: "operatingExpenses" },94    { label: "Revenu d'Exploitation", key: "operatingIncome", highlight: true },95    { label: "Revenu d'Intérêts", key: "interestIncome" },96    { label: "Dépenses d'Intérêts", key: "interestExpense" },97    { label: "Autres Revenus/Dépenses", key: "otherExpenses" },98    { label: "Revenu avant Impôts", key: "incomeBeforeTax", highlight: true },99    { label: "Charge d'Impôt", key: "incomeTaxExpense" },100    { label: "Revenu Net", key: "netIncome", highlight: true, important: true },101    { label: "BPA", key: "eps", isEPS: true },102    { label: "BPA Dilué", key: "epsdiluted", isEPS: true },103  ];104105  // Balance Sheet106  const balanceSheetRows = [107    { label: "Actifs", section: true },108    { label: "Trésorerie", key: "cashAndCashEquivalents" },109    { label: "Placements Court Terme", key: "shortTermInvestments" },110    { label: "Comptes Clients", key: "netReceivables" },111    { label: "Inventaire", key: "inventory" },112    { label: "Actifs Courants", key: "totalCurrentAssets", highlight: true },113    { label: "Immobilisations", key: "propertyPlantEquipmentNet" },114    { label: "Goodwill", key: "goodwill" },115    { label: "Actifs Intangibles", key: "intangibleAssets" },116    { label: "Placements Long Terme", key: "longTermInvestments" },117    { label: "Total Actifs", key: "totalAssets", highlight: true, important: true },118    { label: "Passifs", section: true },119    { label: "Comptes Fournisseurs", key: "accountPayables" },120    { label: "Dette Court Terme", key: "shortTermDebt" },121    { label: "Passifs Courants", key: "totalCurrentLiabilities", highlight: true },122    { label: "Dette Long Terme", key: "longTermDebt" },123    { label: "Total Passifs", key: "totalLiabilities", highlight: true, important: true },124    { label: "Capitaux Propres", section: true },125    { label: "Actions Ordinaires", key: "commonStock" },126    { label: "Bénéfices Non Répartis", key: "retainedEarnings" },127    { label: "Total Capitaux Propres", key: "totalStockholdersEquity", highlight: true, important: true },128  ];129130  // Cash Flow131  const cashFlowRows = [132    { label: "Activités d'Exploitation", section: true },133    { label: "Revenu Net", key: "netIncome", highlight: true },134    { label: "Dépréciation & Amortissement", key: "depreciationAndAmortization" },135    { label: "Variation Fonds de Roulement", key: "changeInWorkingCapital" },136    { label: "Flux de Trésorerie d'Exploitation", key: "operatingCashFlow", highlight: true, important: true },137    { label: "Activités d'Investissement", section: true },138    { label: "Investissements en Immobilisations", key: "capitalExpenditure" },139    { label: "Acquisitions", key: "acquisitionsNet" },140    { label: "Achats d'Investissements", key: "purchasesOfInvestments" },141    { label: "Ventes d'Investissements", key: "salesMaturitiesOfInvestments" },142    { label: "Flux de Trésorerie d'Investissement", key: "netCashUsedForInvestingActivites", highlight: true },143    { label: "Activités de Financement", section: true },144    { label: "Dette Émise", key: "debtRepayment" },145    { label: "Actions Rachetées", key: "commonStockRepurchased" },146    { label: "Dividendes Payés", key: "dividendsPaid" },147    { label: "Flux de Trésorerie de Financement", key: "netCashUsedProvidedByFinancingActivities", highlight: true },148    { label: "Variation Nette de Trésorerie", key: "netChangeInCash", highlight: true, important: true },149    { label: "Flux de Trésorerie Libre", key: "freeCashFlow", highlight: true, important: true },150  ];151152  const renderTable = (153    data: FinancialStatement[] | undefined,154    rows: Array<{ label: string; key?: string; highlight?: boolean; important?: boolean; section?: boolean; isEPS?: boolean }>,155    title: string,156    description: string,157    icon: React.ReactNode158  ) => {159    if (!data || data.length === 0) {160      return (161        <Card>162          <CardHeader>163            <CardTitle className="flex items-center gap-2">164              {icon}165              {title}166            </CardTitle>167            <CardDescription>{description}</CardDescription>168          </CardHeader>169          <CardContent>170            <p className="text-muted-foreground">Aucune donnée disponible</p>171          </CardContent>172        </Card>173      );174    }175176    return (177      <Card>178        <CardHeader>179          <CardTitle className="flex items-center gap-2">180            {icon}181            {title}182          </CardTitle>183          <CardDescription>{description}</CardDescription>184        </CardHeader>185        <CardContent>186          <div className="overflow-x-auto">187            <Table>188              <TableHeader>189                <TableRow>190                  <TableHead className="w-[250px] sticky left-0 bg-card z-10">Élément</TableHead>191                  {data.slice(0, 5).map((item, idx) => (192                    <TableHead key={idx} className="text-right min-w-[150px]">193                      <div>194                        <div className="font-semibold">{formatDate(item.date)}</div>195                        <Badge variant="outline" className="text-xs mt-1">196                          {item.period}197                        </Badge>198                      </div>199                    </TableHead>200                  ))}201                </TableRow>202              </TableHeader>203              <TableBody>204                {rows.map((row, rowIdx) => {205                  if (row.section) {206                    return (207                      <TableRow key={rowIdx} className="bg-muted/50">208                        <TableCell colSpan={6} className="font-bold sticky left-0 bg-muted/50 z-10">209                          {row.label}210                        </TableCell>211                      </TableRow>212                    );213                  }214215                  return (216                    <TableRow217                      key={rowIdx}218                      className={`${row.highlight ? 'bg-primary/5' : ''} ${row.important ? 'border-l-4 border-l-primary' : ''}`}219                    >220                      <TableCell className={`sticky left-0 bg-card z-10 ${row.highlight ? 'font-semibold' : ''} ${row.important ? 'font-bold text-primary' : ''}`}>221                        {row.label}222                      </TableCell>223                      {data.slice(0, 5).map((item, idx) => {224                        const value = row.key ? item[row.key] : null;225                        const prevValue = idx < data.length - 1 && row.key ? data[idx + 1][row.key] : null;226227                        return (228                          <TableCell key={idx} className={`text-right ${row.highlight ? 'font-semibold' : ''} ${row.important ? 'font-bold' : ''}`}>229                            <div>230                              <div>231                                {row.isEPS ? (value ? `$${value.toFixed(2)}` : 'N/A') : formatCurrency(value)}232                              </div>233                              {value && prevValue && getChangeIndicator(value, prevValue)}234                            </div>235                          </TableCell>236                        );237                      })}238                    </TableRow>239                  );240                })}241              </TableBody>242            </Table>243          </div>244245          {data[0]?.link && (246            <div className="mt-4 pt-4 border-t">247              <a248                href={data[0].link}249                target="_blank"250                rel="noopener noreferrer"251                className="text-sm text-primary hover:underline"252              >253                Voir le document SEC complet →254              </a>255            </div>256          )}257        </CardContent>258      </Card>259    );260  };261262  return (263    <div className="space-y-6">264      {renderTable(265        incomeStatement,266        incomeStatementRows,267        "Compte de Résultat",268        "Performance financière et rentabilité",269        <FileText className="w-5 h-5" />270      )}271272      {renderTable(273        balanceSheet,274        balanceSheetRows,275        "Bilan",276        "Actifs, passifs et capitaux propres",277        <Wallet className="w-5 h-5" />278      )}279280      {renderTable(281        cashFlow,282        cashFlowRows,283        "Tableau de Flux de Trésorerie",284        "Flux de trésorerie par activité",285        <Activity className="w-5 h-5" />286      )}287    </div>288  );289}290