← back to Nationalrealestate

src/server/listings.ts

111 lines

/**
 * M-B3 listing routes — the firm↔listing tie made queryable + browsable.
 * Kept in its OWN module (mountListings(app)) so index.ts only gains an import + mount line;
 * the existing /api/firm/:id in index.ts is NOT touched — the per-firm listings live at the
 * new sub-route GET /api/firm/:id/listings.
 *
 * Every listing row is FACTS ONLY + the broker's own source url (we link out, never present
 * a listing as ours). Each row also carries the holding brokerage FIRM name — the key relationship.
 */
import type { Express } from 'express';
import { query } from '../../db/pool.ts';

export function mountListings(app: Express): void {
  // GET /api/listings?firm=<id>&county=<region_id>&source=&limit=
  app.get('/api/listings', async (req, res) => {
    try {
      const limit = Math.min(Math.max(Number(req.query.limit) || 500, 1), 2000);
      const conds: string[] = [];
      const params: unknown[] = [];
      if (req.query.firm) { params.push(Number(req.query.firm)); conds.push(`l.firm_id = $${params.length}`); }
      if (req.query.county) { params.push(Number(req.query.county)); conds.push(`l.region_id = $${params.length}`); }
      if (req.query.source) { params.push(String(req.query.source)); conds.push(`l.source = $${params.length}`); }
      const where = conds.length ? 'WHERE ' + conds.join(' AND ') : '';
      params.push(limit);
      const r = await query(
        `SELECT l.id, l.source, l.source_id, l.address, l.price, l.beds, l.baths, l.sqft,
                l.lat, l.lng, l.url, l.status, l.first_seen, l.last_seen,
                l.firm_id, f.name AS firm_name,
                l.region_id, rg.name AS county_name, rg.state_code
           FROM listing l
           LEFT JOIN firm f ON f.id = l.firm_id
           LEFT JOIN region rg ON rg.id = l.region_id
          ${where}
          ORDER BY l.last_seen DESC NULLS LAST, l.id DESC
          LIMIT $${params.length}`,
        params,
      );
      res.json({ count: r.rows.length, rows: r.rows });
    } catch (e: any) {
      res.status(500).json({ error: String(e.message || e) });
    }
  });

  // GET /api/listings/stats — count by firm + by source (proves the firm↔listing tie)
  app.get('/api/listings/stats', async (_req, res) => {
    try {
      const [bySource, byFirm, totals] = await Promise.all([
        query<{ source: string; n: string; firms: string }>(
          `SELECT source, COUNT(*)::text AS n, COUNT(DISTINCT firm_id)::text AS firms
             FROM listing GROUP BY source ORDER BY source`),
        query<{ firm_id: number; firm_name: string; source: string; n: string }>(
          `SELECT l.firm_id, f.name AS firm_name, l.source, COUNT(*)::text AS n
             FROM listing l JOIN firm f ON f.id = l.firm_id
            GROUP BY l.firm_id, f.name, l.source
            ORDER BY COUNT(*) DESC`),
        query<{ total: string; with_firm: string; with_region: string }>(
          `SELECT COUNT(*)::text AS total,
                  COUNT(firm_id)::text AS with_firm,
                  COUNT(region_id)::text AS with_region
             FROM listing`),
      ]);
      res.json({
        by_source: bySource.rows.map(r => ({ source: r.source, listings: Number(r.n), firms: Number(r.firms) })),
        by_firm: byFirm.rows.map(r => ({ firm_id: r.firm_id, firm_name: r.firm_name, source: r.source, listings: Number(r.n) })),
        totals: {
          total: Number(totals.rows[0].total),
          with_firm: Number(totals.rows[0].with_firm),
          with_region: Number(totals.rows[0].with_region),
        },
      });
    } catch (e: any) {
      res.status(500).json({ error: String(e.message || e) });
    }
  });

  // GET /api/firm/:id/listings — "N listings from this brokerage" for a firm view.
  // NEW sub-route; the existing /api/firm/:id in index.ts is left untouched.
  app.get('/api/firm/:id/listings', async (req, res) => {
    try {
      const id = Number(req.params.id);
      if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ error: 'bad firm id' });
      const r = await query(
        `SELECT l.id, l.source, l.source_id, l.address, l.price, l.beds, l.baths, l.sqft,
                l.lat, l.lng, l.url, l.status, l.first_seen, l.last_seen,
                rg.name AS county_name, rg.state_code
           FROM listing l LEFT JOIN region rg ON rg.id = l.region_id
          WHERE l.firm_id = $1
          ORDER BY l.last_seen DESC NULLS LAST, l.id DESC
          LIMIT 200`,
        [id],
      );
      res.json({ firm_id: id, count: r.rows.length, listings: r.rows });
    } catch (e: any) {
      res.status(500).json({ error: String(e.message || e) });
    }
  });

  // Firms that hold at least one piloted listing — powers the listings.html firm filter.
  app.get('/api/listings/firms', async (_req, res) => {
    try {
      const r = await query<{ firm_id: number; firm_name: string; n: string }>(
        `SELECT l.firm_id, f.name AS firm_name, COUNT(*)::text AS n
           FROM listing l JOIN firm f ON f.id = l.firm_id
          GROUP BY l.firm_id, f.name ORDER BY f.name`);
      res.json({ firms: r.rows.map(x => ({ firm_id: x.firm_id, firm_name: x.firm_name, listings: Number(x.n) })) });
    } catch (e: any) {
      res.status(500).json({ error: String(e.message || e) });
    }
  });
}