SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
5.3 KB · 92 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Lou-Ka — Agrégateur de logements à louer (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/TrueCostCard.tsx : « Coût réel mensuel » — loyer + frais non inclus,5//   chaque poste étiqueté (inclus / observé / estimé / inconnu), total et6//   annuel, prix au pi² avec percentile réellement calculé, estimation7//   Hydro-Québec (cache ; bouton pour lancer le calcul à la demande).8// -----------------------------------------------------------------------------9import { useState } from "react";10import { CoutReel, HydroEstimate, fetchHydro, fmtPrice } from "../api";11import { IcoBolt2, IcoWallet } from "../components/Icons";12import { Accordion, ErrorState, SectionCard, SkeletonLines, NBSP } from "./ui";13import { Res } from "./useFicheData";1415const ST: Record<string, string> = { included: "inclus", observed: "observé", estimated: "estimé", unknown: "inconnu", calculated: "calculé", inferred: "estimé" };1617export default function TrueCostCard({ cout, hydro, adresse, uid, lat, lng, onRetry }: {18  cout: Res<CoutReel>; hydro: Res<HydroEstimate>; adresse: string; uid: string;19  lat: number | null; lng: number | null; onRetry: () => void;20}) {21  const [h, setH] = useState<HydroEstimate | null>(null);22  const [busy, setBusy] = useState(false);23  const d = cout.status === "ok" ? cout.data : null;24  const hy = h ?? (hydro.status === "ok" ? hydro.data : null);25  const hydroVisible = hy && (hy.disponible || hy.en_attente || !/captcha|configuré|incomplète/.test(hy.raison || ""));26  if (cout.status === "na" || (d && d.loyer == null)) return null;2728  const lancer = () => {29    setBusy(true);30    fetchHydro(adresse, { uid, lat, lng }, true).then(setH).catch(() => {}).finally(() => setBusy(false));31  };3233  return (34    <SectionCard id="cout" title="Coût réel mensuel" icon={<IcoWallet size={18} />}35                 sub="Loyer + frais non inclus, chaque poste avec son statut">36      {cout.status === "loading" && <SkeletonLines n={4} />}37      {cout.status === "error" && <ErrorState onRetry={onRetry}>Calcul du coût réel temporairement indisponible.</ErrorState>}38      {d && (39        <>40          <ul className="lk-cost">41            {d.lignes.map((li) => (42              <li key={li.poste}>43                <span className="n">{li.poste}<span className={`lk-pill ${li.statut in ST ? (li.statut === "inferred" || li.statut === "calculated" ? "estimated" : li.statut) : "unknown"}`}>{ST[li.statut] ?? li.statut}</span></span>44                <span className={`v ${li.montant == null ? "na" : ""}`}>45                  {li.montant != null && li.montant > 0 && fmtPrice(li.montant)}46                  {li.montant === 0 && li.statut === "included" && `0${NBSP}$`}47                  {li.montant == null && "—"}48                </span>49              </li>50            ))}51            {d.total_estime != null && (52              <li className="total"><span className="n">Total estimé</span><span className="v">≈{NBSP}{fmtPrice(d.total_estime)}{NBSP}/mois</span></li>53            )}54            {d.annuel_estime != null && (55              <li className="annuel"><span className="n">soit sur 12 mois</span><span className="v">≈{NBSP}{fmtPrice(d.annuel_estime)}</span></li>56            )}57          </ul>58          {d.postes_inconnus.length > 0 && (59            <p className="lk-cost-note">Non chiffrables avec les données publiées : {d.postes_inconnus.join(", ").toLowerCase()} — le total réel peut être plus élevé.</p>60          )}61          {d.pi2 && (62            <p className="lk-cost-note">63              <b>{d.pi2.valeur.toFixed(2).replace(".", ",")}{NBSP}$/pi²</b>64              {d.pi2.percentile_secteur != null && <> — moins cher que <b>{100 - d.pi2.percentile_secteur}{NBSP}%</b> des {d.pi2.n_secteur} logements comparables du secteur (~2{NBSP}km)</>}65              {d.pi2.percentile_secteur == null && d.pi2.percentile_ville != null && <> — moins cher que <b>{100 - d.pi2.percentile_ville}{NBSP}%</b> des {d.pi2.n_ville} comparables ({d.pi2.portee_ville})</>}66              {d.pi2.percentile_note && <> {d.pi2.percentile_note}</>}67            </p>68          )}69          {hydroVisible && hy && (70            <div className="lk-note info" style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>71              <IcoBolt2 size={16} />72              {hy.disponible ? (73                <span><b>Électricité : {fmtPrice(hy.cout_mensuel!)}{NBSP}/mois</b> (≈{NBSP}{fmtPrice(hy.cout_annuel!)}{NBSP}/an{hy.kwh_annuel ? `, ${hy.kwh_annuel.toLocaleString("fr-CA")} kWh` : ""}) — estimation Hydro-Québec fondée sur la consommation réelle du logement.</span>74              ) : hy.en_attente ? (75                <>76                  <span style={{ flex: 1 }}>Estimation Hydro-Québec du coût d'électricité disponible à la demande.</span>77                  <button type="button" className="lk-btn lk-btn-ghost" style={{ minHeight: 38 }} onClick={lancer} disabled={busy}>78                    {busy ? "Estimation en cours…" : "Estimer"}79                  </button>80                </>81              ) : (82                <span>Hydro-Québec n'a pas d'estimation pour cette adresse.</span>83              )}84            </div>85          )}86          <Accordion title="Méthodologie" small><p>{d.methode}</p></Accordion>87        </>88      )}89    </SectionCard>90  );91}92