Álvaro Valenzuela Valdes commited on
Commit
5f2460b
Β·
1 Parent(s): 0d71eae

feat: implement Purchase Order (OC) monitoring and real-time market monitor UI

Browse files
backend/app/main.py CHANGED
@@ -4,11 +4,12 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
4
 
5
  from fastapi import FastAPI
6
  from fastapi.middleware.cors import CORSMiddleware
7
- from app.routers import analysis, company, health, tenders, documents
8
  from app.database import engine, Base, SessionLocal
9
  from app.models.tender import TenderModel
10
  from app.models.analysis import AnalysisHistoryModel
11
  from app.models.company import CompanyProfileModel
 
12
  from app.config import settings
13
  from datetime import datetime, timedelta
14
  import json
@@ -32,6 +33,7 @@ app.include_router(tenders.router, prefix="/api", tags=["Tenders"])
32
  app.include_router(analysis.router, prefix="/api", tags=["Analysis"])
33
  app.include_router(company.router, prefix="/api", tags=["Company"])
34
  app.include_router(documents.router, prefix="/api", tags=["Documents"])
 
35
 
36
  @app.on_event("startup")
37
  async def startup_event():
 
4
 
5
  from fastapi import FastAPI
6
  from fastapi.middleware.cors import CORSMiddleware
7
+ from app.routers import analysis, company, health, tenders, documents, oc
8
  from app.database import engine, Base, SessionLocal
9
  from app.models.tender import TenderModel
10
  from app.models.analysis import AnalysisHistoryModel
11
  from app.models.company import CompanyProfileModel
12
+ from app.models.oc import OCModel
13
  from app.config import settings
14
  from datetime import datetime, timedelta
15
  import json
 
33
  app.include_router(analysis.router, prefix="/api", tags=["Analysis"])
34
  app.include_router(company.router, prefix="/api", tags=["Company"])
35
  app.include_router(documents.router, prefix="/api", tags=["Documents"])
36
+ app.include_router(oc.router, prefix="/api", tags=["Purchase Orders"])
37
 
38
  @app.on_event("startup")
39
  async def startup_event():
backend/app/models/oc.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, String, Float, DateTime, Text, JSON
2
+ from app.database import Base
3
+ from datetime import datetime
4
+
5
+ class OCModel(Base):
6
+ __tablename__ = "purchase_orders"
7
+
8
+ code = Column(String(50), primary_key=True, index=True)
9
+ name = Column(String(255), index=True)
10
+ status = Column(String(100))
11
+ status_code = Column(String(10), nullable=True)
12
+ buyer = Column(String(255), index=True)
13
+ buyer_rut = Column(String(20), nullable=True)
14
+ provider = Column(String(255), index=True)
15
+ provider_rut = Column(String(20), nullable=True)
16
+ date_creation = Column(DateTime, nullable=True)
17
+ total_amount = Column(Float, nullable=True)
18
+ currency = Column(String(10), nullable=True)
19
+ type = Column(String(50), nullable=True)
20
+
21
+ items = Column(JSON, nullable=True)
22
+ raw_data = Column(JSON, nullable=True)
23
+
24
+ last_updated = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
backend/app/routers/oc.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+ from fastapi import APIRouter, Query, Depends
3
+ from sqlalchemy.orm import Session
4
+ from app.schemas.oc import PurchaseOrder
5
+ from app.database import get_db
6
+ from app.models.oc import OCModel
7
+ from app.services.mercado_publico_oc import get_ocs_by_date, get_oc_by_code
8
+
9
+ router = APIRouter()
10
+
11
+ @router.get("/purchase-orders", response_model=List[PurchaseOrder])
12
+ async def list_purchase_orders(
13
+ date: Optional[str] = None,
14
+ status: str = "todos",
15
+ db: Session = Depends(get_db)
16
+ ):
17
+ """
18
+ List purchase orders for a specific date (ddmmaaaa).
19
+ """
20
+ if not date:
21
+ from datetime import datetime
22
+ date = datetime.now().strftime("%d%m%Y")
23
+
24
+ # Check if we have them in DB first?
25
+ # For now, let's just fetch from API as a real-time monitor
26
+ return await get_ocs_by_date(date, status)
27
+
28
+ @router.get("/purchase-orders/{code}", response_model=Optional[PurchaseOrder])
29
+ async def get_purchase_order(code: str):
30
+ return await get_oc_by_code(code)
backend/app/schemas/oc.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, ConfigDict
2
+ from typing import List, Optional, Union
3
+ from datetime import datetime
4
+
5
+ class OCItem(BaseModel):
6
+ correlative: Optional[int] = None
7
+ product_code: Optional[str] = None
8
+ name: str
9
+ description: Optional[str] = None
10
+ quantity: float
11
+ unit: str
12
+ price: Optional[float] = None
13
+ total: Optional[float] = None
14
+
15
+ class PurchaseOrder(BaseModel):
16
+ model_config = ConfigDict(from_attributes=True)
17
+
18
+ code: str
19
+ name: str
20
+ status: str
21
+ status_code: Optional[str] = None
22
+ buyer: str
23
+ buyer_rut: Optional[str] = None
24
+ provider: str
25
+ provider_rut: Optional[str] = None
26
+ date_creation: Union[str, datetime, None] = None
27
+ total_amount: Optional[float] = None
28
+ currency: Optional[str] = None
29
+ type: Optional[str] = None
30
+ items: List[OCItem] = []
31
+ raw_data: Optional[dict] = None
backend/app/services/mercado_publico_oc.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ from typing import List, Optional, Dict, Any
3
+ from app.config import settings
4
+ from app.schemas.oc import PurchaseOrder, OCItem
5
+ from datetime import datetime
6
+
7
+ API_BASE_OC = "https://api.mercadopublico.cl/servicios/v1/publico/ordenesdecompra.json"
8
+
9
+ OC_STATUS_CODES = {
10
+ "4": "Enviada a Proveedor",
11
+ "5": "En proceso",
12
+ "6": "Aceptada",
13
+ "9": "Cancelada",
14
+ "12": "RecepciΓ³n Conforme",
15
+ "13": "Pendiente de Recepcionar",
16
+ "14": "Recepcionada Parcialmente",
17
+ "15": "Recepcion Conforme Incompleta"
18
+ }
19
+
20
+ OC_TYPES = {
21
+ "1": "OC AutomΓ‘tica",
22
+ "2": "D1 - Proveedor Único",
23
+ "3": "C1 - Emergencia/Urgencia",
24
+ "4": "F3 - Confidencialidad",
25
+ "5": "G1 - Naturaleza de negociaciΓ³n",
26
+ "6": "R1 - Menor a 3UTM",
27
+ "7": "CA - Sin resoluciΓ³n",
28
+ "8": "SE - Sin emisiΓ³n automΓ‘tica",
29
+ "9": "CM - Convenio Marco",
30
+ "10": "FG - Trato Directo (Art. 8 f y g)",
31
+ "12": "MC - Microcompra",
32
+ "13": "AG - Compra Ágil",
33
+ "14": "CC - Compra Coordinada"
34
+ }
35
+
36
+ def map_raw_to_oc(item: Dict[str, Any]) -> PurchaseOrder:
37
+ # Handle items
38
+ items_list = []
39
+ raw_items = item.get("Items", {})
40
+ if isinstance(raw_items, dict) and "Listado" in raw_items:
41
+ for i in raw_items["Listado"]:
42
+ items_list.append(OCItem(
43
+ correlative=i.get("Correlativo"),
44
+ product_code=str(i.get("CodigoProducto", "")),
45
+ name=i.get("Nombre", ""),
46
+ description=i.get("EspecificacionComprador"),
47
+ quantity=float(i.get("Cantidad", 0)),
48
+ unit=i.get("Unidad"),
49
+ price=float(i.get("PrecioNeto", 0)),
50
+ total=float(i.get("TotalNeto", 0))
51
+ ))
52
+
53
+ def parse_dt(dt_str):
54
+ if not dt_str: return None
55
+ try:
56
+ return datetime.fromisoformat(dt_str.replace("Z", "").split(".")[0])
57
+ except:
58
+ return None
59
+
60
+ return PurchaseOrder(
61
+ code=item.get("Codigo", ""),
62
+ name=item.get("Nombre", ""),
63
+ status=item.get("Estado", "Desconocido"),
64
+ status_code=str(item.get("CodigoEstado", "")),
65
+ buyer=item.get("Comprador", {}).get("NombreOrganismo", "Unknown"),
66
+ buyer_rut=item.get("Comprador", {}).get("RutUnidad"),
67
+ provider=item.get("Proveedor", {}).get("Nombre", "Unknown"),
68
+ provider_rut=item.get("Proveedor", {}).get("Rut", ""),
69
+ date_creation=parse_dt(item.get("Fechas", {}).get("FechaCreacion")),
70
+ total_amount=float(item.get("Total", 0)),
71
+ currency=item.get("Moneda"),
72
+ type=item.get("Tipo"),
73
+ items=items_list,
74
+ raw_data=item
75
+ )
76
+
77
+ async def _fetch_oc(params: Dict[str, str]) -> List[PurchaseOrder]:
78
+ if not settings.mercado_publico_ticket:
79
+ return []
80
+
81
+ params["ticket"] = settings.mercado_publico_ticket
82
+
83
+ try:
84
+ async with httpx.AsyncClient(timeout=45.0) as client:
85
+ response = await client.get(API_BASE_OC, params=params)
86
+ response.raise_for_status()
87
+ data = response.json()
88
+
89
+ raw_list = data.get("Listado", [])
90
+ if not raw_list:
91
+ return []
92
+
93
+ return [map_raw_to_oc(item) for item in raw_list]
94
+ except Exception as e:
95
+ print(f"❌ OC API Error: {e}")
96
+ return []
97
+
98
+ async def get_oc_by_code(code: str) -> Optional[PurchaseOrder]:
99
+ results = await _fetch_oc({"codigo": code})
100
+ return results[0] if results else None
101
+
102
+ async def get_ocs_by_date(date: str, status: str = "todos") -> List[PurchaseOrder]:
103
+ return await _fetch_oc({"fecha": date, "estado": status})
104
+
105
+ async def get_ocs_by_provider(provider_code: str, date: str) -> List[PurchaseOrder]:
106
+ return await _fetch_oc({"CodigoProveedor": provider_code, "fecha": date})
backend/scratch_test_oc.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import json
4
+ import os
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ async def test_oc_api():
10
+ ticket = os.getenv("MERCADO_PUBLICO_TICKET")
11
+ if not ticket:
12
+ print("No ticket found in .env")
13
+ return
14
+
15
+ # 1. Fetch today's OCs
16
+ url_list = f"https://api.mercadopublico.cl/servicios/v1/publico/ordenesdecompra.json?ticket={ticket}"
17
+ print(f"Fetching OCs: {url_list}")
18
+
19
+ async with httpx.AsyncClient(timeout=30) as client:
20
+ try:
21
+ resp = await client.get(url_list)
22
+ data = resp.json()
23
+ items = data.get("Listado", [])
24
+ print(f"Found {len(items)} OCs today.")
25
+
26
+ if items:
27
+ code = items[0].get("Codigo")
28
+ print(f"Fetching details for OC code: {code}")
29
+
30
+ url_detail = f"https://api.mercadopublico.cl/servicios/v1/publico/ordenesdecompra.json?codigo={code}&ticket={ticket}"
31
+ resp_detail = await client.get(url_detail)
32
+ detail_data = resp_detail.json()
33
+
34
+ print("OC Detail sample:")
35
+ # print(json.dumps(detail_data, indent=2))
36
+
37
+ with open("oc_sample_detail.json", "w") as f:
38
+ json.dump(detail_data, f, indent=2)
39
+ print("Saved to oc_sample_detail.json")
40
+
41
+ except Exception as e:
42
+ print(f"Error: {e}")
43
+
44
+ if __name__ == "__main__":
45
+ asyncio.run(test_oc_api())
frontend/app/page.tsx CHANGED
@@ -10,6 +10,7 @@ import Reports from "../components/Reports";
10
  import Sidebar from "../components/Sidebar";
11
  import AnalysisHistory from "../components/AnalysisHistory";
12
  import GlobalSync from "../components/GlobalSync";
 
13
  import SystemInfo from "../components/SystemInfo";
14
  import { analyzeTender, fetchAnalysisHistory, fetchCompanyProfile, healthCheck, saveCompanyProfile, searchTenders } from "../lib/api";
15
  import type { AnalysisHistoryItem, AnalysisResult, CompanyProfile as CompanyProfileType, Tender } from "../lib/types";
@@ -19,6 +20,7 @@ const tabs = [
19
  "Dashboard",
20
  "Tender Search",
21
  "My Portfolio",
 
22
  "Company Profile",
23
  "Agent Analysis",
24
  "Proposal Draft",
@@ -256,6 +258,7 @@ export default function HomePage() {
256
  lang={lang}
257
  />
258
  )}
 
259
  {activeTab === "Company Profile" && (
260
  <CompanyProfile profile={companyProfile} onSave={handleProfileSave} />
261
  )}
 
10
  import Sidebar from "../components/Sidebar";
11
  import AnalysisHistory from "../components/AnalysisHistory";
12
  import GlobalSync from "../components/GlobalSync";
13
+ import MarketMonitor from "../components/MarketMonitor";
14
  import SystemInfo from "../components/SystemInfo";
15
  import { analyzeTender, fetchAnalysisHistory, fetchCompanyProfile, healthCheck, saveCompanyProfile, searchTenders } from "../lib/api";
16
  import type { AnalysisHistoryItem, AnalysisResult, CompanyProfile as CompanyProfileType, Tender } from "../lib/types";
 
20
  "Dashboard",
21
  "Tender Search",
22
  "My Portfolio",
23
+ "Market Monitor",
24
  "Company Profile",
25
  "Agent Analysis",
26
  "Proposal Draft",
 
258
  lang={lang}
259
  />
260
  )}
261
+ {activeTab === "Market Monitor" && <MarketMonitor />}
262
  {activeTab === "Company Profile" && (
263
  <CompanyProfile profile={companyProfile} onSave={handleProfileSave} />
264
  )}
frontend/components/MarketMonitor.tsx ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { fetchPurchaseOrders } from "../lib/api";
5
+ import { PurchaseOrder } from "../lib/types";
6
+ import BrandLoader from "./BrandLoader";
7
+
8
+ export default function MarketMonitor() {
9
+ const [ocs, setOcs] = useState<PurchaseOrder[]>([]);
10
+ const [isLoading, setIsLoading] = useState(true);
11
+ const [filter, setFilter] = useState("todos");
12
+
13
+ useEffect(() => {
14
+ loadOcs();
15
+ }, [filter]);
16
+
17
+ async function loadOcs() {
18
+ setIsLoading(true);
19
+ try {
20
+ const data = await fetchPurchaseOrders(undefined, filter);
21
+ setOcs(data);
22
+ } catch (e) {
23
+ console.error(e);
24
+ } finally {
25
+ setIsLoading(false);
26
+ }
27
+ }
28
+
29
+ const formatCurrency = (amount: number | null, currency: string | null) => {
30
+ if (amount === null) return "---";
31
+ return new Intl.NumberFormat("es-CL", {
32
+ style: "currency",
33
+ currency: currency || "CLP",
34
+ maximumFractionDigits: 0
35
+ }).format(amount);
36
+ };
37
+
38
+ return (
39
+ <div className="space-y-8 animate-in fade-in duration-700">
40
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
41
+ <div>
42
+ <p className="text-[10px] uppercase tracking-[0.4em] text-cyan/60 font-black mb-2">Real-Time Intelligence</p>
43
+ <h2 className="text-4xl font-black text-white tracking-tight">Market Monitor</h2>
44
+ <p className="text-slate-400 text-sm mt-2 max-w-xl">
45
+ Monitor real-time Purchase Orders (OCs) issued by public organizations.
46
+ Identify high-velocity buying patterns and competitive wins.
47
+ </p>
48
+ </div>
49
+
50
+ <div className="flex bg-slate-900/50 p-1 rounded-2xl border border-white/5 backdrop-blur-xl">
51
+ {["todos", "aceptada", "enviadaproveedor"].map((f) => (
52
+ <button
53
+ key={f}
54
+ onClick={() => setFilter(f)}
55
+ className={`px-6 py-2.5 rounded-xl text-[10px] uppercase font-black tracking-widest transition-all ${
56
+ filter === f ? "bg-cyan text-slate-950 shadow-lg shadow-cyan/20" : "text-slate-500 hover:text-white"
57
+ }`}
58
+ >
59
+ {f === "todos" ? "Live Stream" : f}
60
+ </button>
61
+ ))}
62
+ </div>
63
+ </div>
64
+
65
+ <div className="grid gap-6">
66
+ {isLoading ? (
67
+ <div className="py-20">
68
+ <BrandLoader />
69
+ </div>
70
+ ) : ocs.length > 0 ? (
71
+ <div className="glass-card rounded-[2rem] overflow-hidden border border-white/5">
72
+ <div className="overflow-x-auto custom-scrollbar">
73
+ <table className="w-full text-left text-xs border-collapse">
74
+ <thead>
75
+ <tr className="bg-white/5 text-slate-500 uppercase font-black tracking-tighter border-b border-white/5">
76
+ <th className="px-6 py-5">Issue Date</th>
77
+ <th className="px-6 py-5">Order ID / Description</th>
78
+ <th className="px-6 py-5">Buyer Organism</th>
79
+ <th className="px-6 py-5">Vendor (Winner)</th>
80
+ <th className="px-6 py-5 text-right">Total Amount</th>
81
+ </tr>
82
+ </thead>
83
+ <tbody className="divide-y divide-white/5">
84
+ {ocs.map((oc) => (
85
+ <tr key={oc.code} className="hover:bg-white/[0.03] transition-colors group">
86
+ <td className="px-6 py-5 whitespace-nowrap">
87
+ <div className="text-slate-300 font-mono">
88
+ {oc.date_creation ? new Date(oc.date_creation).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '---'}
89
+ </div>
90
+ <div className="text-[10px] text-slate-600">
91
+ {oc.date_creation ? new Date(oc.date_creation).toLocaleDateString() : 'Today'}
92
+ </div>
93
+ </td>
94
+ <td className="px-6 py-5 max-w-md">
95
+ <div className="flex items-center gap-2 mb-1">
96
+ <span className="text-cyan font-bold font-mono text-[10px] bg-cyan/5 px-2 py-0.5 rounded border border-cyan/10">
97
+ {oc.code}
98
+ </span>
99
+ {oc.type && (
100
+ <span className="text-[8px] bg-white/5 text-slate-500 px-1.5 py-0.5 rounded uppercase font-black">
101
+ {oc.type}
102
+ </span>
103
+ )}
104
+ </div>
105
+ <div className="text-white font-bold truncate group-hover:text-cyan transition-colors">
106
+ {oc.name || "Orden de Compra"}
107
+ </div>
108
+ </td>
109
+ <td className="px-6 py-5">
110
+ <div className="text-slate-300 font-medium truncate max-w-[200px]">{oc.buyer}</div>
111
+ <div className="text-[10px] text-slate-600">{oc.buyer_rut}</div>
112
+ </td>
113
+ <td className="px-6 py-5">
114
+ <div className="text-sky-400 font-bold truncate max-w-[200px]">{oc.provider}</div>
115
+ <div className="text-[10px] text-slate-600">{oc.provider_rut}</div>
116
+ </td>
117
+ <td className="px-6 py-5 text-right">
118
+ <div className="text-white font-black text-sm">
119
+ {formatCurrency(oc.total_amount, oc.currency)}
120
+ </div>
121
+ <div className="text-[10px] text-slate-500 uppercase font-bold">{oc.currency}</div>
122
+ </td>
123
+ </tr>
124
+ ))}
125
+ </tbody>
126
+ </table>
127
+ </div>
128
+ </div>
129
+ ) : (
130
+ <div className="py-40 text-center glass-card rounded-[2rem] border border-white/5">
131
+ <div className="text-4xl mb-4">πŸ›’</div>
132
+ <p className="text-slate-500 font-medium italic">No purchase orders detected in the last hour.</p>
133
+ </div>
134
+ )}
135
+ </div>
136
+ </div>
137
+ );
138
+ }
frontend/components/Sidebar.tsx CHANGED
@@ -7,6 +7,7 @@ type SidebarTab =
7
  | "Dashboard"
8
  | "Tender Search"
9
  | "My Portfolio"
 
10
  | "Company Profile"
11
  | "Agent Analysis"
12
  | "Proposal Draft"
@@ -33,6 +34,7 @@ export default function Sidebar({ tabs, activeTab, onTabSelect, status, lang, fo
33
  case "Dashboard": return { label: t.dashboard, icon: "πŸ“Š" };
34
  case "Tender Search": return { label: t.tenderSearch, icon: "πŸ“‘" };
35
  case "My Portfolio": return { label: t.myPortfolio, icon: "β˜…" };
 
36
  case "Company Profile": return { label: t.companyProfile, icon: "🏒" };
37
  case "Agent Analysis": return { label: t.agentAnalysis, icon: "πŸ€–" };
38
  case "Proposal Draft": return { label: t.proposalDraft, icon: "✍️" };
 
7
  | "Dashboard"
8
  | "Tender Search"
9
  | "My Portfolio"
10
+ | "Market Monitor"
11
  | "Company Profile"
12
  | "Agent Analysis"
13
  | "Proposal Draft"
 
34
  case "Dashboard": return { label: t.dashboard, icon: "πŸ“Š" };
35
  case "Tender Search": return { label: t.tenderSearch, icon: "πŸ“‘" };
36
  case "My Portfolio": return { label: t.myPortfolio, icon: "β˜…" };
37
+ case "Market Monitor": return { label: "Market Monitor", icon: "πŸ›’" };
38
  case "Company Profile": return { label: t.companyProfile, icon: "🏒" };
39
  case "Agent Analysis": return { label: t.agentAnalysis, icon: "πŸ€–" };
40
  case "Proposal Draft": return { label: t.proposalDraft, icon: "✍️" };
frontend/lib/api.ts CHANGED
@@ -122,3 +122,15 @@ export async function scrapeTenders(keyword: string): Promise<Tender[]> {
122
  }
123
  return res.json();
124
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  }
123
  return res.json();
124
  }
125
+
126
+ export async function fetchPurchaseOrders(date?: string, status: string = "todos"): Promise<PurchaseOrder[]> {
127
+ const query = new URLSearchParams();
128
+ if (date) query.append("date", date);
129
+ query.append("status", status);
130
+
131
+ const res = await fetch(`${API_BASE}/api/purchase-orders?${query.toString()}`);
132
+ if (!res.ok) {
133
+ throw new Error("Error fetching purchase orders");
134
+ }
135
+ return res.json();
136
+ }
frontend/lib/types.ts CHANGED
@@ -70,6 +70,33 @@ export type AnalysisResult = {
70
  audit_log: string[];
71
  };
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  export type AnalysisHistoryItem = {
74
  tender_code: string;
75
  tender_name: string;
 
70
  audit_log: string[];
71
  };
72
 
73
+ export type OCItem = {
74
+ correlative?: number;
75
+ product_code?: string;
76
+ name: string;
77
+ description?: string;
78
+ quantity: number;
79
+ unit: string;
80
+ price?: number;
81
+ total?: number;
82
+ };
83
+
84
+ export type PurchaseOrder = {
85
+ code: string;
86
+ name: string;
87
+ status: string;
88
+ status_code?: string;
89
+ buyer: string;
90
+ buyer_rut?: string;
91
+ provider: string;
92
+ provider_rut?: string;
93
+ date_creation: string | null;
94
+ total_amount: number | null;
95
+ currency: string | null;
96
+ type?: string;
97
+ items?: OCItem[];
98
+ };
99
+
100
  export type AnalysisHistoryItem = {
101
  tender_code: string;
102
  tender_name: string;