From 74aad3d9811398e4f9e202ec195ad078fa996e18 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal Date: Tue, 24 Feb 2026 09:32:23 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20implement=206=20pending=20features=20?= =?UTF-8?q?=E2=80=94=20vision=20engine,=20global=20codes,=20change=20order?= =?UTF-8?q?s,=20IFC=20export,=20product=20matching,=20reconstruction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Vision Engine: Dockerfile + docker-compose service (port 8010), remove floor-plan amber warning - Global Building Codes: US IRC 2021, EU Eurocode, UK Building Regs JSON data + multi-jurisdiction compliance router/UI - Change Orders: full dashboard page with summary cards, status filters, create/approve/reject, impact analysis - IFC/BIM Export: ifcopenshell-based IFC4 writer, drawing pipeline integration, IFC download button - Visual Product Matching: CLIP ViT-B/32 embedder, pgvector indexer/searcher, VLM reranker, orchestration pipeline - Photo-to-3D Reconstruction: tRPC router, vision-engine backend with VLM depth estimation + glTF mesh, full UI page Co-Authored-By: Claude Opus 4.6 --- .../project/[id]/change-orders/page.tsx | 491 ++++++++++++++++++ .../project/[id]/compliance/page.tsx | 34 +- .../project/[id]/drawings/page.tsx | 16 +- .../project/[id]/floor-plan/page.tsx | 7 - .../project/[id]/reconstruction/page.tsx | 412 +++++++++++++++ apps/web/src/components/sidebar.tsx | 4 + .../web/src/server/trpc/routers/compliance.ts | 50 +- apps/web/src/server/trpc/routers/index.ts | 4 + .../src/server/trpc/routers/reconstruction.ts | 105 ++++ apps/web/src/server/trpc/routers/schedule.ts | 37 ++ data/building-codes/eu-eurocode.json | 197 +++++++ data/building-codes/uk-building-regs.json | 197 +++++++ data/building-codes/us-irc.json | 262 ++++++++++ docker-compose.yml | 31 ++ .../openlintel_product_matching/embedder.py | 218 ++++++++ .../openlintel_product_matching/indexer.py | 137 +++++ .../openlintel_product_matching/pipeline.py | 240 +++++++++ .../openlintel_product_matching/reranker.py | 208 ++++++++ .../openlintel_product_matching/schemas.py | 68 +++ .../openlintel_product_matching/searcher.py | 228 ++++++++ packages/db/src/schema/app.ts | 1 + .../src/openlintel_shared/job_worker.py | 8 +- .../src/services/vector_search.py | 44 ++ services/drawing-generator/pyproject.toml | 1 + .../drawing-generator/src/routers/drawings.py | 16 + .../src/services/ifc_writer.py | 292 +++++++++++ services/vision-engine/Dockerfile | 33 ++ services/vision-engine/main.py | 2 + .../src/routers/reconstruction.py | 408 +++++++++++++++ 29 files changed, 3722 insertions(+), 29 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/project/[id]/change-orders/page.tsx create mode 100644 apps/web/src/app/(dashboard)/project/[id]/reconstruction/page.tsx create mode 100644 apps/web/src/server/trpc/routers/reconstruction.ts create mode 100644 data/building-codes/eu-eurocode.json create mode 100644 data/building-codes/uk-building-regs.json create mode 100644 data/building-codes/us-irc.json create mode 100644 ml/product-matching/src/openlintel_product_matching/embedder.py create mode 100644 ml/product-matching/src/openlintel_product_matching/indexer.py create mode 100644 ml/product-matching/src/openlintel_product_matching/pipeline.py create mode 100644 ml/product-matching/src/openlintel_product_matching/reranker.py create mode 100644 ml/product-matching/src/openlintel_product_matching/schemas.py create mode 100644 ml/product-matching/src/openlintel_product_matching/searcher.py create mode 100644 services/drawing-generator/src/services/ifc_writer.py create mode 100644 services/vision-engine/Dockerfile create mode 100644 services/vision-engine/src/routers/reconstruction.py diff --git a/apps/web/src/app/(dashboard)/project/[id]/change-orders/page.tsx b/apps/web/src/app/(dashboard)/project/[id]/change-orders/page.tsx new file mode 100644 index 0000000..a1d20a1 --- /dev/null +++ b/apps/web/src/app/(dashboard)/project/[id]/change-orders/page.tsx @@ -0,0 +1,491 @@ +'use client'; + +import { use, useState } from 'react'; +import { trpc } from '@/lib/trpc/client'; +import { + Button, + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Badge, + Skeleton, + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, + Input, + Label, + Textarea, + toast, +} from '@openlintel/ui'; +import { + FileEdit, + Plus, + Loader2, + CheckCircle2, + XCircle, + Clock, + DollarSign, + CalendarDays, + AlertCircle, + TrendingUp, +} from 'lucide-react'; + +/* ─── Constants ─────────────────────────────────────────────── */ + +const STATUS_COLORS: Record = { + pending: 'bg-yellow-100 text-yellow-800', + approved: 'bg-green-100 text-green-800', + rejected: 'bg-red-100 text-red-800', +}; + +type StatusFilter = 'all' | 'pending' | 'approved' | 'rejected'; + +function formatDate(date: string | Date): string { + return new Date(date).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); +} + +function formatCurrency(amount: number): string { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(amount); +} + +/* ─── Page Component ────────────────────────────────────────── */ + +export default function ChangeOrdersPage({ params }: { params: Promise<{ id: string }> }) { + const { id: projectId } = use(params); + const utils = trpc.useUtils(); + + const [statusFilter, setStatusFilter] = useState('all'); + const [dialogOpen, setDialogOpen] = useState(false); + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [costImpact, setCostImpact] = useState(''); + const [timeImpactDays, setTimeImpactDays] = useState(''); + + /* ── Queries ──────────────────────────────────────────────── */ + const { data: changeOrders = [], isLoading } = trpc.schedule.listChangeOrders.useQuery({ projectId }); + + /* ── Mutations ────────────────────────────────────────────── */ + const createChangeOrder = trpc.schedule.createChangeOrder.useMutation({ + onSuccess: () => { + utils.schedule.listChangeOrders.invalidate(); + setDialogOpen(false); + resetForm(); + toast({ title: 'Change order created', description: 'The change order has been submitted.' }); + }, + onError: (err) => { + toast({ title: 'Failed to create change order', description: err.message, variant: 'destructive' }); + }, + }); + + const updateChangeOrder = trpc.schedule.updateChangeOrder.useMutation({ + onSuccess: () => { + utils.schedule.listChangeOrders.invalidate(); + toast({ title: 'Change order updated' }); + }, + onError: (err) => { + toast({ title: 'Failed to update change order', description: err.message, variant: 'destructive' }); + }, + }); + + const analyzeImpact = trpc.schedule.analyzeChangeOrderImpact.useMutation({ + onSuccess: (data) => { + toast({ title: 'Impact analysis complete', description: data.summary || 'Analysis returned.' }); + }, + onError: (err) => { + toast({ title: 'Impact analysis failed', description: err.message, variant: 'destructive' }); + }, + }); + + /* ── Form helpers ─────────────────────────────────────────── */ + function resetForm() { + setTitle(''); + setDescription(''); + setCostImpact(''); + setTimeImpactDays(''); + } + + function handleCreate() { + if (!title) return; + createChangeOrder.mutate({ + projectId, + title, + description: description || undefined, + costImpact: costImpact ? parseFloat(costImpact) : undefined, + timeImpactDays: timeImpactDays ? parseInt(timeImpactDays, 10) : undefined, + }); + } + + /* ── Derived data ─────────────────────────────────────────── */ + const filtered = statusFilter === 'all' + ? changeOrders + : changeOrders.filter((co: any) => co.status === statusFilter); + + const totalCount = changeOrders.length; + const pendingCount = changeOrders.filter((co: any) => co.status === 'pending').length; + const approvedOrders = changeOrders.filter((co: any) => co.status === 'approved'); + const approvedCostImpact = approvedOrders.reduce((sum: number, co: any) => sum + (co.costImpact || 0), 0); + const approvedTimeImpact = approvedOrders.reduce((sum: number, co: any) => sum + (co.timeImpactDays || 0), 0); + + /* ── Loading state ────────────────────────────────────────── */ + if (isLoading) { + return ( +
+ + +
+ {[1, 2, 3, 4].map((i) => ( + + ))} +
+
+ {[1, 2, 3].map((i) => ( + + ))} +
+
+ ); + } + + return ( +
+ {/* ── Header ──────────────────────────────────────────── */} +
+
+ +
+

Change Orders

+

+ Track scope changes, cost impacts, and schedule adjustments. +

+
+
+
+ + {/* ── Summary Cards ───────────────────────────────────── */} +
+ + +
+
+

Total Orders

+

{totalCount}

+
+ +
+
+
+ + + +
+
+

Pending

+

{pendingCount}

+
+ +
+
+
+ + + +
+
+

Approved Cost Impact

+

+ {formatCurrency(approvedCostImpact)} +

+
+ +
+
+
+ + + +
+
+

Approved Time Impact

+

+ {approvedTimeImpact} days +

+
+ +
+
+
+
+ + {/* ── Status Filter Tabs ──────────────────────────────── */} +
+
+ {(['all', 'pending', 'approved', 'rejected'] as const).map((status) => ( + + ))} +
+ + + + + + + + New Change Order + + Submit a change order to track scope, cost, and timeline changes. + + +
+
+ + setTitle(e.target.value)} + /> +
+
+ +