package handler import ( "encoding/json" "math" "net/http" ) // ── Calculator models ─────────────────────────────────────── type CalcRoomInput struct { Name string `json:"name"` Length float64 `json:"length"` Width float64 `json:"width"` Deductions []CalcDeduction `json:"deductions"` } type CalcDeduction struct { Name string `json:"name"` Shape string `json:"shape"` // rectangle, circle, custom Length float64 `json:"length"` Width float64 `json:"width"` Radius float64 `json:"radius"` Area float64 `json:"area"` // custom area override } type CalcInput struct { FloorType string `json:"floor_type"` // wood, tile Rooms []CalcRoomInput `json:"rooms"` PatternIndex int `json:"pattern_index"` PieceLength float64 `json:"piece_length"` PieceWidth float64 `json:"piece_width"` PiecesPerBox int `json:"pieces_per_box"` PricePerSqm float64 `json:"price_per_sqm"` JointWidth float64 `json:"joint_width"` } type CalcRoomResult struct { Name string `json:"name"` Length float64 `json:"length"` Width float64 `json:"width"` Area float64 `json:"area"` Deductions []CalcDeduction `json:"deductions"` DeductionTotal float64 `json:"deduction_total"` NetArea float64 `json:"net_area"` } type CalcResult struct { FloorType string `json:"floor_type"` Rooms []CalcRoomResult `json:"rooms"` TotalRoomArea float64 `json:"total_room_area"` TotalDeduct float64 `json:"total_deduct"` TotalNetArea float64 `json:"total_net_area"` WasteRate float64 `json:"waste_rate"` WasteArea float64 `json:"waste_area"` TotalArea float64 `json:"total_area"` PiecesNeeded int `json:"pieces_needed"` BoxesNeeded *float64 `json:"boxes_needed,omitempty"` JointWidth float64 `json:"joint_width"` GroutKg *float64 `json:"grout_kg,omitempty"` GroutBags *int `json:"grout_bags,omitempty"` PricePerSqm float64 `json:"price_per_sqm"` TotalPrice *float64 `json:"total_price,omitempty"` PatternName string `json:"pattern_name"` PieceLength float64 `json:"piece_length"` PieceWidth float64 `json:"piece_width"` } var woodPatternsCalc = []struct { Name string Waste float64 }{ {"Straight Lay", 0.05}, {"Offset Lay", 0.08}, {"Herringbone", 0.12}, {"Diagonal 45deg", 0.15}, } var tilePatternsCalc = []struct { Name string Waste float64 }{ {"Straight Grid", 0.05}, {"Running Bond", 0.07}, {"1/3 Offset", 0.08}, {"Hexagonal", 0.10}, {"Diagonal 45deg", 0.15}, {"Versailles", 0.20}, } // ── Handler ───────────────────────────────────────────────── func handleCalc(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeErr(w, 405, "Method not allowed") return } var input CalcInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSON(w, 400, map[string]string{"error": "invalid JSON: " + err.Error()}) return } if len(input.Rooms) == 0 { writeJSON(w, 400, map[string]string{"error": "at least one room required"}) return } if input.PieceLength <= 0 || input.PieceWidth <= 0 { writeJSON(w, 400, map[string]string{"error": "piece dimensions required"}) return } result := calculate(input) writeJSON(w, 200, result) } // ── Calculation ───────────────────────────────────────────── func calculate(input CalcInput) CalcResult { patterns := woodPatternsCalc if input.FloorType == "tile" { patterns = tilePatternsCalc } if input.PatternIndex < 0 || input.PatternIndex >= len(patterns) { input.PatternIndex = 0 } wasteRate := patterns[input.PatternIndex].Waste var roomResults []CalcRoomResult totalRoomArea := 0.0 totalDeduct := 0.0 for _, room := range input.Rooms { area := room.Length * room.Width deductTotal := 0.0 var dedResults []CalcDeduction for _, d := range room.Deductions { dedArea := deductionArea(d) deductTotal += dedArea d.Area = round2(dedArea) dedResults = append(dedResults, d) } netArea := area - deductTotal if netArea < 0 { netArea = 0 } roomResults = append(roomResults, CalcRoomResult{ Name: room.Name, Length: room.Length, Width: room.Width, Area: round2(area), Deductions: dedResults, DeductionTotal: round2(deductTotal), NetArea: round2(netArea), }) totalRoomArea += area totalDeduct += deductTotal } totalNetArea := totalRoomArea - totalDeduct if totalNetArea < 0 { totalNetArea = 0 } // Adjust piece for joint effLength := input.PieceLength effWidth := input.PieceWidth if input.FloorType == "tile" && input.JointWidth > 0 { effLength += input.JointWidth effWidth += input.JointWidth } pieceArea := effLength * effWidth if pieceArea <= 0 { pieceArea = 1 } totalPieces := totalNetArea / pieceArea wasteArea := totalNetArea * wasteRate totalArea := totalNetArea + wasteArea piecesNeeded := int(math.Ceil(totalArea / pieceArea)) result := CalcResult{ FloorType: input.FloorType, Rooms: roomResults, TotalRoomArea: round2(totalRoomArea), TotalDeduct: round2(totalDeduct), TotalNetArea: round2(totalNetArea), WasteRate: round2(wasteRate * 100), WasteArea: round2(wasteArea), TotalArea: round2(totalArea), PiecesNeeded: piecesNeeded, JointWidth: input.JointWidth, PricePerSqm: input.PricePerSqm, PatternName: patterns[input.PatternIndex].Name, PieceLength: input.PieceLength, PieceWidth: input.PieceWidth, } // Boxes if input.PiecesPerBox > 0 { b := math.Ceil(float64(piecesNeeded) / float64(input.PiecesPerBox)) result.BoxesNeeded = &b } // Grout (tile only) if input.FloorType == "tile" && totalPieces > 0 { kg := totalNetArea * 0.1 result.GroutKg = &kg bags := int(math.Ceil(kg / 2.0)) result.GroutBags = &bags } // Price if input.PricePerSqm > 0 { p := totalArea * input.PricePerSqm result.TotalPrice = &p } return result } func deductionArea(d CalcDeduction) float64 { switch d.Shape { case "circle": return math.Pi * d.Radius * d.Radius case "custom": return d.Area default: // rectangle return d.Length * d.Width } } func round2(v float64) float64 { return math.Round(v*100) / 100 }