import React, { useMemo, useState } from "react";
// ---- Pricing data ----
const MIN_CHARGE = 475; // minimum subtotal before tax
const DEFAULT_MARKUP = 0.35; // 35%
const FORCED_300_MARKUP_SERVICES = ["glass_replacement", "mirror", "repair"];
const CATALOG = {
sqft: {
glass_replacement: {
"Clear 3/16\"": 2.50 * 1.3,
"Clear 1/4\"": 2.50 * 1.3,
"Clear 3/8\"": 6.46 * 1.3,
"Clear 1/2\"": 9.64 * 1.3,
"Clear 3/4\"": 26.59 * 1.3,
"Gray 1/4\"": 7.58 * 1.3,
"Bronze/Gray 1/4\"": 5.39 * 1.3,
"Bronze/Gray 3/8\"": 14.46 * 1.3,
"Starphire 1/4\"": 6.25 * 1.3,
"Starphire 3/8\"": 10.51 * 1.3,
"Starphire 1/2\"": 20.42 * 1.3,
"Starphire 3/4\"": 29.09 * 1.3,
"Acid Reg 1/4\"": 10.46 * 1.3,
"Acid Low-Iron 1/4\"": 15.26 * 1.3
},
mirror: {
"Mirror Clear 1/4\"": 4.00 * 1.3,
"Mirror Bronze/Gray 1/4\"": 6.50 * 1.3,
"One Way Mirror 1/4\"": 14.16 * 1.3,
"Low-E 1/4\"": 5.88 * 1.3
},
insulated_glass_unit: {
"IGU 7/8\" Clear Annealed": 18.5,
"IGU 7/8\" Clear Tempered": 20.0,
"IGU 7/8\" Low-E Annealed": 19.75,
"IGU 7/8\" Low-E Tempered": 21.25,
"IGU 1\" Clear Annealed": 19.0,
"IGU 1\" Clear Tempered": 20.5,
"IGU 1\" Low-E Annealed": 20.25,
"IGU 1\" Low-E Tempered": 21.75
},
screen_fabrication: {
"Fiberglass Screen": 7.07
},
repair: {
"General Repair": 0
}
},
edge: {
polish_in: 0.07 * 1.3,
polish_in_thick: 0.50 * 1.3,
miter_in: 0.13 * 1.3,
hole_each: 11.13 * 1.3,
notch_each: 16.69 * 1.3,
patch_each: 60.26 * 1.3,
},
parts: [
{ sku: "IGU-SPACER-ALUM", name: "Aluminum Spacer (ea)", cost: 5.00 },
{ sku: "IGU-GRIDS", name: "Grids (ea)", cost: 12.00 },
{ sku: "BAL-RED", name: "Red Tip Balance (ea)", cost: 5.75 },
{ sku: "BAL-BLACK-31-39", name: "Black Tip Balance 31–39\" (ea)", cost: 7.25 },
{ sku: "BAL-BLACK-10-30", name: "Black Tip Balance 10–30\" (ea)", cost: 7.00 },
{ sku: "A2.5-SHOE", name: "A2.5 Shoe (ea)", cost: 1.35 },
{ sku: "PIVOT-TILT-BAR", name: "Pivot Tilt Bar (ea)", cost: 0.65 },
{ sku: "TOP-MOUNT-LATCH", name: "Top Mount Latch (ea)", cost: 1.25 },
{ sku: "SCREEN-FB-ROLL-42x100", name: "Fiberglass Mesh Roll 42\"×100'", cost: 63.00 },
],
};
const RATES = { glazier: 95, helper: 55 };
const fmt = (n)=> n.toLocaleString(undefined,{style:"currency",currency:"USD"});
const toSqft = (w,h)=> Math.max(0,(w*h)/144);
const perimeter = (w,h)=> Math.max(0,2*(w+h));
export default function App(){
// Basic inputs
const [service, setService] = useState("glass_replacement");
const variantList = Object.keys(CATALOG.sqft[service] || {});
const [variant, setVariant] = useState(variantList[0] || "");
const [qty, setQty] = useState(1);
const [w, setW] = useState(36);
const [h, setH] = useState(60);
const [tempered, setTempered] = useState(false);
const [laminated, setLaminated] = useState(false);
const [lowe, setLowe] = useState(true);
const [tinted, setTinted] = useState(false);
const [soundproof, setSoundproof] = useState(false);
// Edgework
const [polish, setPolish] = useState(false);
const [polishThick, setPolishThick] = useState(false);
const [miter, setMiter] = useState(false);
const [holes, setHoles] = useState(0);
const [notches, setNotches] = useState(0);
const [patches, setPatches] = useState(0);
// Parts
const [parts, setParts] = useState([]); // {sku, name, qty, cost}
// Labor/fees/markup
const [hoursG, setHoursG] = useState(3);
const [hoursH, setHoursH] = useState(2);
const [travel, setTravel] = useState(95);
const [disposal, setDisposal] = useState(35);
const forced300 = FORCED_300_MARKUP_SERVICES.includes(service);
const [markup, setMarkup] = useState( forced300 ? 3.0 : DEFAULT_MARKUP );
const [taxRate, setTaxRate] = useState(0.08875);
// Sync variant when service changes
React.useEffect(()=>{
setMarkup(forced300 ? 3.0 : DEFAULT_MARKUP);
const first = Object.keys(CATALOG.sqft[service] || {})[0] || "";
setVariant(first);
},[service]);
// Material cost
const sqft = useMemo(()=> toSqft(w,h),[w,h]);
const rate = CATALOG.sqft[service]?.[variant] ?? 0;
let material = (sqft * 1.05) * rate * qty; // 5% waste
if(["glass_replacement","mirror"].includes(service)){
if(tempered) material *= 1.15;
if(laminated) material *= 1.25;
if(lowe) material *= 1.08;
if(tinted) material *= 1.05;
if(soundproof) material *= 1.12;
}
// Edgework
const perim = useMemo(()=> perimeter(w,h),[w,h]);
const edge = useMemo(()=>{
let c=0;
if(polish){ c += perim * (polishThick ? CATALOG.edge.polish_in_thick : CATALOG.edge.polish_in) * qty; }
if(miter){ c += perim * CATALOG.edge.miter_in * qty; }
c += holes * CATALOG.edge.hole_each;
c += notches * CATALOG.edge.notch_each;
c += patches * CATALOG.edge.patch_each;
return c;
},[polish,polishThick,miter,holes,notches,patches,perim,qty]);
// Parts
const partsCost = useMemo(()=> parts.reduce((s,p)=> s + (Number(p.cost)||0)*(Number(p.qty)||0),0),[parts]);
// Labor
const labor = (hoursG*RATES.glazier + hoursH*RATES.helper);
// Subtotal with min charge
const subtotalRaw = material + edge + partsCost + labor + travel + disposal;
const subtotal = Math.max(subtotalRaw, MIN_CHARGE);
// Pricing
const beforeTax = subtotal * (1 + markup);
const tax = beforeTax * taxRate;
const total = beforeTax + tax;
const margin = beforeTax ? (beforeTax - subtotal)/beforeTax : 0;
const addPart = (sku)=>{
const p = CATALOG.parts.find(x=>x.sku===sku);
if(!p) return;
setParts(prev=>[...prev,{...p, qty:1}]);
};
return (
);
}
{/* LEFT: inputs */}
{/* Service & variant */}
{/* Size */}
{/* Glass/Mirror options */}
{["glass_replacement","mirror"].includes(service) && (
{/* Parts */}
{/* Labor, fees, markup */}
{/* RIGHT: totals */}
Mr. Glazier — Pricing Calculator
Focus: Glass • Mirrors • IGU • Screens • Repair
setQty(Number(e.target.value)||0)} />
setW(Number(e.target.value)||0)} />
setH(Number(e.target.value)||0)} />
Area: {sqft.toFixed(2)} sqft • Perimeter: {perim.toFixed(1)} in
{[{k:"tempered",v:tempered,s:setTempered},{k:"laminated",v:laminated,s:setLaminated},{k:"lowe",v:lowe,s:setLowe},{k:"tinted",v:tinted,s:setTinted},{k:"soundproof",v:soundproof,s:setSoundproof}].map(o=> (
))}
)}
{/* Edgework */}
Edgework & Fabrication
setHoles(Number(e.target.value)||0)} />
setNotches(Number(e.target.value)||0)} />
setPatches(Number(e.target.value)||0)} />
Parts & Hardware
Edit qty or unit cost as needed:
{parts.length===0 && No parts yet.
}
{parts.map((p,i)=> (
setParts(prev=> prev.map((r,idx)=> idx===i?{...r,sku:e.target.value}:r))} />
setParts(prev=> prev.map((r,idx)=> idx===i?{...r,name:e.target.value}:r))} />
setParts(prev=> prev.map((r,idx)=> idx===i?{...r,qty:Number(e.target.value)||0}:r))} />
setParts(prev=> prev.map((r,idx)=> idx===i?{...r,cost:Number(e.target.value)||0}:r))} />
))}
setHoursG(Number(e.target.value)||0)} />
{fmt(RATES.glazier)}/hr
setHoursH(Number(e.target.value)||0)} />
{fmt(RATES.helper)}/hr
setTravel(Number(e.target.value)||0)} />
setDisposal(Number(e.target.value)||0)} />
setMarkup((Number(e.target.value)||0)/100)} />
Totals
Material
{fmt(material)}
Edgework
{fmt(edge)}
Parts
{fmt(partsCost)}
Labor
{fmt(labor)}
Travel
{fmt(travel)}
Disposal
{fmt(disposal)}
Subtotal (min {fmt(MIN_CHARGE)})
{fmt(subtotal)}
Markup
{((markup)*100).toFixed(1)}%
Before Tax
{fmt(beforeTax)}
Sales Tax
setTaxRate((Number(e.target.value)||0)/100)} /> %
Total
{fmt(total)}
Margin: {(margin*100).toFixed(1)}%