SPB Git forge

spb/immo-ka

Public

Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)

112commits 1branches 0releases
125.4 MBsize
maindefault branch
13 days agolast push
Python 47.5% HTML 27.9% TypeScript 15.5% CSS 7.2% JavaScript 2%
6.4 KB · 99 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/PropertyDossier.tsx : « Dossier de l'annonce » — ce qu'Immo-Ka observe5//   réellement, en accordéons compacts : suivi Immo-Ka (première observation,6//   jours en ligne, variations de prix), autres publications de la même7//   propriété (doublons rattachés par la déduplication), courtier et agence,8//   identifiants de la source. Chaque volet a un état vide propre.9// -----------------------------------------------------------------------------10import { Listing, fmtDate, fmtPrice, sourceName } from "../api";11import { Ico } from "../components/Icons";12import { Accordion, SectionCard, StatTile, StatusBadge, NBSP } from "./ui";1314function Vide({ children }: { children: React.ReactNode }) { return <p style={{ color: "var(--ik-muted)", margin: 0 }}>{children}</p>; }1516export default function PropertyDossier({ l }: { l: Listing }) {17  const hist = (l.price_history ?? []).filter((h) => h.price != null);18  const jours = l.days_on_market ?? (l.first_seen ? Math.max(0, Math.round((Date.now() / 1000 - l.first_seen) / 86400)) : null);19  const variation = hist.length >= 2 ? (hist[0].price! - hist[hist.length - 1].price!) / hist[hist.length - 1].price! : null;20  const dups = l.duplicates ?? [];21  const d = l.details ?? {};22  const listedAt = typeof d.listed_at === "string" ? d.listed_at : null;23  const origin = typeof d.listing_origin_url === "string" ? d.listing_origin_url : null;2425  return (26    <SectionCard id="dossier" title="Dossier de l'annonce" icon={<Ico name="folder" size={18} />}27                 sub="Ce qu'Immo-Ka observe réellement : suivi, publications, courtier, source">28      <Accordion title={<><Ico name="history" size={15} className="ik-acc-ico" />Suivi Immo-Ka</>}29                 meta={jours != null ? (jours === 0 ? "publiée aujourd'hui" : `${jours}${NBSP}j en ligne`) : undefined}>30        <div className="ik-kpis cols-3" style={{ marginBottom: 10 }}>31          <StatTile value={l.first_seen ? fmtDate(l.first_seen) : "—"} label="Première observation" anim={false} />32          <StatTile value={jours != null ? (jours === 0 ? "Aujourd'hui" : `${jours}${NBSP}j`) : "—"} label="Sur le marché (observé)" anim={false} />33          <StatTile value={Math.max(0, hist.length - 1)} label="Changement(s) de prix" anim={false} />34        </div>35        {variation != null && variation !== 0 && (36          <p style={{ margin: "0 0 8px" }}>37            Prix initial observé {fmtPrice(hist[hist.length - 1].price!)} → <b>{fmtPrice(hist[0].price!)}</b>{" "}38            <StatusBadge tone={variation < 0 ? "good" : "warn"}>{variation > 0 ? "+" : "−"}{Math.abs(Math.round(variation * 100))}{NBSP}%</StatusBadge>39          </p>40        )}41        {listedAt && <p style={{ margin: "0 0 8px" }}>Date d'inscription indiquée par la source : {listedAt}.</p>}42        <p className="ik-fine">Première observation, jours en ligne et variations sont mesurés par les synchronisations Immo-Ka43          (plusieurs fois par jour) — ils ne remplacent pas la date d'inscription officielle Centris.</p>44      </Accordion>4546      <Accordion title={<><Ico name="layers" size={15} className="ik-acc-ico" />Aussi publiée sur</>}47                 meta={dups.length ? `${dups.length} plateforme${dups.length > 1 ? "s" : ""}` : "aucune"}>48        {dups.length === 0 ? (49          <Vide>Aucune autre publication de cette propriété repérée parmi les sources Immo-Ka.</Vide>50        ) : (51          <>52            <p style={{ margin: "0 0 8px" }}>53              Cette propriété a été repérée sur {dups.length} autre{dups.length > 1 ? "s" : ""} plateforme{dups.length > 1 ? "s" : ""} —54              Immo-Ka affiche la version la plus complète.55            </p>56            <ul className="ik-list">57              {dups.map((x) => (58                <li className="ik-item" key={x.uid}>59                  <span className="ik-item-ico" aria-hidden="true"><Ico name="building" size={17} /></span>60                  <div className="ik-item-main">61                    <div className="ik-item-t">{sourceName(x.source)}</div>62                    <div className="ik-item-s">{x.broker_name || x.agency || x.price_label || ""}</div>63                  </div>64                  <div className="ik-item-r">65                    <a className="ik-link" href={x.url} target="_blank" rel="noopener noreferrer">Voir ↗</a>66                  </div>67                </li>68              ))}69            </ul>70          </>71        )}72      </Accordion>7374      <Accordion title={<><Ico name="people" size={15} className="ik-acc-ico" />Courtier et agence</>}75                 meta={l.broker_name || sourceName(l.source)}>76        <p style={{ margin: "0 0 6px" }}>77          <b style={{ fontSize: 15 }}>{l.broker_name || "Courtier non précisé"}</b>78          {l.agency ? <> — {l.agency}</> : null}79        </p>80        <p style={{ margin: "0 0 6px" }}>Annonce publiée par <b>{sourceName(l.source)}</b>81          {l.broker_phone && <> · <a href={`tel:${l.broker_phone.replace(/\s/g, "")}`}>{l.broker_phone}</a></>}.82        </p>83        <p className="ik-fine">Immo-Ka est un agrégateur indépendant : la relation contractuelle se fait avec le courtier ou l'agence84          de l'annonce originale. Consultez le registre de l'OACIQ pour vérifier un permis de courtier.</p>85      </Accordion>8687      <Accordion title={<><Ico name="tag" size={15} className="ik-acc-ico" />Identifiants et source</>} meta={l.mls || l.external_id}>88        <div className="ik-facts-rows" style={{ marginTop: 0 }}>89          {l.mls && <div className="ik-kv"><span className="k">Nº Centris / MLS</span><span className="v">{l.mls}</span></div>}90          <div className="ik-kv"><span className="k">Identifiant source</span><span className="v">{l.external_id}</span></div>91          <div className="ik-kv"><span className="k">Identifiant Immo-Ka</span><span className="v">{l.uid}</span></div>92          <div className="ik-kv"><span className="k">Annonce originale</span><span className="v"><a href={l.url} target="_blank" rel="noopener noreferrer">{sourceName(l.source)} ↗</a></span></div>93          {origin && origin !== l.url && <div className="ik-kv"><span className="k">Fiche d'origine</span><span className="v"><a href={origin} target="_blank" rel="noopener noreferrer">Ouvrir ↗</a></span></div>}94        </div>95      </Accordion>96    </SectionCard>97  );98}99