231 lines
6.5 KiB
Go
231 lines
6.5 KiB
Go
|
|
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
|
||
|
|
}
|