{
  "id": "OvYK6agG8RmwF6Gq",
  "name": "Photo Cost Estimate Pro v2.0",
  "tags": [],
  "nodes": [
    {
      "id": "0f4cbe14-7ba4-46c5-81d2-6b1d4482efcc",
      "name": "Photo Upload Form",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        29104,
        400
      ],
      "parameters": {
        "options": {},
        "formTitle": "\ud83d\udcf8 Photo Cost Estimate Pro v2",
        "formFields": {
          "values": [
            {
              "fieldType": "file",
              "fieldLabel": "\ud83d\udcf7 Upload Photo",
              "multipleFiles": false,
              "requiredField": true,
              "acceptFileTypes": ".jpg,.jpeg,.png,.webp"
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "\ud83c\udf0d Region & Language",
              "fieldOptions": {
                "values": [
                  {
                    "option": "\ud83c\udde9\ud83c\uddea German - Berlin (EUR \u20ac)"
                  },
                  {
                    "option": "\ud83c\uddec\ud83c\udde7 English - Toronto (CAD $)"
                  },
                  {
                    "option": "\ud83c\uddf7\ud83c\uddfa Russian - St. Petersburg (RUB \u20bd)"
                  },
                  {
                    "option": "\ud83c\uddea\ud83c\uddf8 Spanish - Barcelona (EUR \u20ac)"
                  },
                  {
                    "option": "\ud83c\uddeb\ud83c\uddf7 French - Paris (EUR \u20ac)"
                  },
                  {
                    "option": "\ud83c\udde7\ud83c\uddf7 Portuguese - S\u00e3o Paulo (BRL R$)"
                  },
                  {
                    "option": "\ud83c\udde8\ud83c\uddf3 Chinese - Shanghai (CNY \u00a5)"
                  },
                  {
                    "option": "\ud83c\udde6\ud83c\uddea Arabic - Dubai (AED \u062f.\u0625)"
                  },
                  {
                    "option": "\ud83c\uddee\ud83c\uddf3 Hindi - Mumbai (INR \u20b9)"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "\ud83c\udfd7\ufe0f Work Type",
              "fieldOptions": {
                "values": [
                  {
                    "option": "\ud83d\udd28 New Construction"
                  },
                  {
                    "option": "\ud83d\udd04 Renovation / Remodel"
                  },
                  {
                    "option": "\ud83d\udd27 Repair"
                  },
                  {
                    "option": "\u2753 Auto-detect"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldType": "textarea",
              "fieldLabel": "\ud83d\udcdd Description (optional)",
              "placeholder": "Describe what's in the photo, specify dimensions, or add context..."
            }
          ]
        },
        "responseMode": "lastNode",
        "formDescription": "Upload a construction photo for automatic cost estimation.\nIMPROVED: Multi-stage AI decomposition for accurate work identification.\nPrices based on DDC CWICR regional databases (9 languages)."
      },
      "typeVersion": 2.2
    },
    {
      "id": "fae54f1a-9225-48c1-a755-83dca9f15033",
      "name": "Extract Input",
      "type": "n8n-nodes-base.code",
      "position": [
        29328,
        400
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// EXTRACT INPUT FROM FORM - 9 LANGUAGES + WORK TYPE\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst input = $input.first();\nconst formData = input.json || {};\n\nconst regionField = formData['\ud83c\udf0d Region & Language'] || formData['Region & Language'] || '';\nconst workTypeField = formData['\ud83c\udfd7\ufe0f Work Type'] || formData['Work Type'] || 'Auto-detect';\n\n// 9 languages mapping\nconst langMap = {\n  'German': 'DE',\n  'English': 'EN',\n  'Russian': 'RU',\n  'Spanish': 'ES',\n  'French': 'FR',\n  'Portuguese': 'PT',\n  'Chinese': 'ZH',\n  'Arabic': 'AR',\n  'Hindi': 'HI'\n};\n\nlet languageCode = 'EN';\nfor (const [lang, code] of Object.entries(langMap)) {\n  if (regionField.includes(lang)) {\n    languageCode = code;\n    break;\n  }\n}\n\n// Work type detection\nlet workType = 'auto';\nif (workTypeField.includes('New')) workType = 'new_construction';\nelse if (workTypeField.includes('Renovation')) workType = 'renovation';\nelse if (workTypeField.includes('Repair')) workType = 'repair';\n\nconst userDescription = formData['\ud83d\udcdd Description (optional)'] || formData['Description (optional)'] || '';\n\nlet photoBase64 = '';\nlet photoMimeType = 'image/jpeg';\n\nif (input.binary) {\n  for (const key of Object.keys(input.binary)) {\n    const bin = input.binary[key];\n    if (bin.mimeType && bin.mimeType.startsWith('image/')) {\n      photoBase64 = bin.data;\n      photoMimeType = bin.mimeType;\n      break;\n    }\n  }\n}\n\nreturn {\n  json: {\n    language_code: languageCode,\n    photo_base64: photoBase64,\n    photo_mime_type: photoMimeType,\n    has_photo: photoBase64.length > 100,\n    user_description: userDescription,\n    selected_region: regionField,\n    work_type: workType\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "d5b99499-be6d-4da7-a381-94cdf5e9f7d4",
      "name": "Configure Language",
      "type": "n8n-nodes-base.code",
      "position": [
        29552,
        400
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// LANGUAGE & VECTOR DB CONFIGURATION - 9 LANGUAGES\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst input = $input.first().json;\nconst languageCode = (input.language_code || 'EN').toUpperCase();\n\nconst languageConfig = {\n  'DE': {\n    city: 'Berlin',\n    vectorDb: 'DE_BERLIN_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'German',\n    languageNative: 'Deutsch',\n    currency: 'EUR',\n    currencySymbol: '\u20ac',\n    locale: 'de-DE',\n    systemPromptLang: 'Antworte auf Deutsch.',\n    searchLang: 'German'\n  },\n  'EN': {\n    city: 'Toronto',\n    vectorDb: 'ENG_TORONTO_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'English',\n    languageNative: 'English',\n    currency: 'CAD',\n    currencySymbol: '$',\n    locale: 'en-CA',\n    systemPromptLang: 'Respond in English.',\n    searchLang: 'English'\n  },\n  'RU': {\n    city: 'St. Petersburg',\n    vectorDb: 'RU_STPETERSBURG_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'Russian',\n    languageNative: '\u0420\u0443\u0441\u0441\u043a\u0438\u0439',\n    currency: 'RUB',\n    currencySymbol: '\u20bd',\n    locale: 'ru-RU',\n    systemPromptLang: '\u041e\u0442\u0432\u0435\u0447\u0430\u0439 \u043d\u0430 \u0440\u0443\u0441\u0441\u043a\u043e\u043c \u044f\u0437\u044b\u043a\u0435.',\n    searchLang: 'Russian'\n  },\n  'FR': {\n    city: 'Paris',\n    vectorDb: 'FR_PARIS_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'French',\n    languageNative: 'Fran\u00e7ais',\n    currency: 'EUR',\n    currencySymbol: '\u20ac',\n    locale: 'fr-FR',\n    systemPromptLang: 'R\u00e9pondez en fran\u00e7ais.',\n    searchLang: 'French'\n  },\n  'ES': {\n    city: 'Barcelona',\n    vectorDb: 'ES_BARCELONA_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'Spanish',\n    languageNative: 'Espa\u00f1ol',\n    currency: 'EUR',\n    currencySymbol: '\u20ac',\n    locale: 'es-ES',\n    systemPromptLang: 'Responde en espa\u00f1ol.',\n    searchLang: 'Spanish'\n  },\n  'PT': {\n    city: 'S\u00e3o Paulo',\n    vectorDb: 'PT_SAOPAULO_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'Portuguese',\n    languageNative: 'Portugu\u00eas',\n    currency: 'BRL',\n    currencySymbol: 'R$',\n    locale: 'pt-BR',\n    systemPromptLang: 'Responda em portugu\u00eas.',\n    searchLang: 'Portuguese'\n  },\n  'ZH': {\n    city: 'Shanghai',\n    vectorDb: 'ZH_SHANGHAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'Chinese',\n    languageNative: '\u4e2d\u6587',\n    currency: 'CNY',\n    currencySymbol: '\u00a5',\n    locale: 'zh-CN',\n    systemPromptLang: '\u8bf7\u7528\u4e2d\u6587\u56de\u7b54\u3002',\n    searchLang: 'Chinese'\n  },\n  'AR': {\n    city: 'Dubai',\n    vectorDb: 'AR_DUBAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'Arabic',\n    languageNative: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629',\n    currency: 'AED',\n    currencySymbol: '\u062f.\u0625',\n    locale: 'ar-AE',\n    systemPromptLang: '\u0623\u062c\u0628 \u0628\u0627\u0644\u0644\u063a\u0629 \u0627\u0644\u0639\u0631\u0628\u064a\u0629.',\n    searchLang: 'Arabic'\n  },\n  'HI': {\n    city: 'Mumbai',\n    vectorDb: 'HI_MUMBAI_workitems_costs_resources_EMBEDDINGS_3072_DDC_CWICR',\n    language: 'Hindi',\n    languageNative: '\u0939\u093f\u0928\u094d\u0926\u0940',\n    currency: 'INR',\n    currencySymbol: '\u20b9',\n    locale: 'hi-IN',\n    systemPromptLang: '\u0939\u093f\u0902\u0926\u0940 \u092e\u0947\u0902 \u091c\u0935\u093e\u092c \u0926\u0947\u0902\u0964',\n    searchLang: 'Hindi'\n  }\n};\n\nconst config = languageConfig[languageCode] || languageConfig['EN'];\n\nreturn {\n  json: {\n    ...input,\n    language_code: languageCode,\n    language_config: config,\n    qdrant_collection: config.vectorDb,\n    city: config.city,\n    language: config.language,\n    language_native: config.languageNative,\n    currency: config.currency,\n    currency_symbol: config.currencySymbol,\n    locale: config.locale,\n    system_prompt_lang: config.systemPromptLang,\n    search_lang: config.searchLang,\n    pricing_level: config.city\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "2ad831a9-2893-4a0c-992e-337eb4a4bc36",
      "name": "Has Photo?",
      "type": "n8n-nodes-base.if",
      "position": [
        29776,
        400
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.has_photo }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "97d8463c-6e3b-4b16-b44f-6147d7b17f91",
      "name": "Error No Photo",
      "type": "n8n-nodes-base.code",
      "position": [
        29984,
        560
      ],
      "parameters": {
        "jsCode": "return {\n  json: {\n    success: false,\n    error: true,\n    message: '\u274c No photo provided',\n    html_content: '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"></head><body style=\"font-family:system-ui;padding:40px;text-align:center;\"><h1>\u274c Error</h1><p>No photo was uploaded. Please go back and upload a construction photo.</p></body></html>'\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "b9545c52-79e0-447f-bb2c-fe81fc38a6bf",
      "name": "STAGE 1 Vision Prompt",
      "type": "n8n-nodes-base.set",
      "position": [
        29984,
        384
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "prompt",
              "name": "chatInput",
              "type": "string",
              "value": "=You are an expert construction surveyor. {{ $json.system_prompt_lang }}\n\n## STAGE 1: IDENTIFY ELEMENTS FROM PHOTO\n\nAnalyze this construction photo and identify ALL visible elements.\n\n### IDENTIFICATION RULES:\n1. **Identify ROOM TYPE** first (bathroom, kitchen, bedroom, living room, hallway, exterior, facade, roof, basement, utility)\n2. **List ALL visible construction elements** with their materials\n3. **Estimate dimensions** using reference objects:\n   - Standard door: 2.0m height, 0.9m width\n   - Standard window: 1.2m \u00d7 1.4m\n   - Ceiling height: typically 2.5-2.7m\n   - Brick: 25cm \u00d7 12cm \u00d7 6.5cm\n   - Tile: 30cm \u00d7 30cm or 60cm \u00d7 60cm\n   - Socket/switch: 8cm \u00d7 8cm\n\n{{ $json.user_description ? 'User note: ' + $json.user_description : '' }}\n\nReturn ONLY valid JSON (no markdown, no code blocks):\n{\n  \"room_type\": \"bathroom|kitchen|bedroom|living_room|hallway|exterior|facade|roof|basement|utility|other\",\n  \"room_description\": \"Brief description of what you see\",\n  \"estimated_dimensions\": {\n    \"floor_area_m2\": 0,\n    \"wall_area_m2\": 0,\n    \"ceiling_height_m\": 2.5,\n    \"perimeter_m\": 0\n  },\n  \"elements\": [\n    {\n      \"element_type\": \"wall|floor|ceiling|window|door|fixture|furniture|mep\",\n      \"element_name\": \"Descriptive name\",\n      \"material\": \"concrete|brick|drywall|tile|wood|glass|metal|plastic\",\n      \"surface_finish\": \"painted|tiled|plastered|wallpaper|raw|laminate\",\n      \"quantity\": 1,\n      \"unit\": \"m\u00b2|m|pcs\",\n      \"estimated_size\": \"e.g. 3.5 m\u00b2 or 2.1 m\",\n      \"condition\": \"new|good|worn|damaged\",\n      \"notes\": \"Any additional observations\"\n    }\n  ],\n  \"fixtures\": [\n    {\n      \"fixture_type\": \"toilet|sink|bathtub|shower|faucet|radiator|socket|switch|light|vent\",\n      \"quantity\": 1,\n      \"notes\": \"Description\"\n    }\n  ],\n  \"work_type_detected\": \"new_construction|renovation|repair\",\n  \"confidence\": \"high|medium|low\"\n}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "0fb1af6a-fa06-44e6-9181-2f0c3073f1f6",
      "name": "STAGE 1 Analyze Photo",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        30208,
        384
      ],
      "parameters": {
        "messages": {
          "messageValues": [
            {
              "message": "={{ $json.chatInput }}"
            }
          ]
        }
      },
      "typeVersion": 1.4
    },
    {
      "id": "7a62ea97-a203-4ce0-b693-90d385aa542c",
      "name": "GPT-4 Vision",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        30208,
        592
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "chatgpt-4o-latest",
          "cachedResultName": "chatgpt-4o-latest"
        },
        "options": {
          "maxTokens": 4000,
          "temperature": 0.2
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "f4168e15-aa61-4878-a583-79df493e8104",
      "name": "Parse STAGE 1",
      "type": "n8n-nodes-base.code",
      "position": [
        30480,
        384
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// PARSE STAGE 1 VISION RESPONSE\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst aiResponse = $input.first().json;\nconst configData = $('Configure Language').first().json;\n\nlet parsed = {\n  room_type: 'unknown',\n  room_description: 'Analysis failed',\n  elements: [],\n  fixtures: [],\n  estimated_dimensions: { floor_area_m2: 10, wall_area_m2: 30, ceiling_height_m: 2.5, perimeter_m: 12 }\n};\n\ntry {\n  const content = aiResponse.text || aiResponse.content || aiResponse.response || '';\n  let jsonStr = content;\n  \n  const jsonMatch = content.match(/```json\\s*([\\s\\S]*?)\\s*```/);\n  if (jsonMatch) {\n    jsonStr = jsonMatch[1];\n  } else {\n    const codeMatch = content.match(/```\\s*([\\s\\S]*?)\\s*```/);\n    if (codeMatch) {\n      jsonStr = codeMatch[1];\n    } else {\n      const objMatch = content.match(/\\{[\\s\\S]*\\}/);\n      if (objMatch) {\n        jsonStr = objMatch[0];\n      }\n    }\n  }\n  \n  parsed = JSON.parse(jsonStr);\n} catch (error) {\n  console.error('Parse error:', error.message);\n}\n\nreturn {\n  json: {\n    ...configData,\n    stage1_result: parsed,\n    room_type: parsed.room_type || 'unknown',\n    room_description: parsed.room_description || 'Photo analysis',\n    elements: parsed.elements || [],\n    fixtures: parsed.fixtures || [],\n    dimensions: parsed.estimated_dimensions || {},\n    work_type_detected: parsed.work_type_detected || configData.work_type || 'renovation',\n    confidence: parsed.confidence || 'medium'\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "c733ceb2-3357-42b1-b585-0cf2f8d3f2d1",
      "name": "STAGE 4 Decompose Prompt",
      "type": "n8n-nodes-base.set",
      "position": [
        30656,
        384
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "prompt2",
              "name": "chatInput",
              "type": "string",
              "value": "={{ $json.system_prompt_lang }}\n\n## STAGE 4: DECOMPOSE ELEMENTS TO CONSTRUCTION WORKS\n\nYou are a construction cost estimator. Based on the photo analysis, decompose each element into specific construction work items.\n\n### PHOTO ANALYSIS RESULTS:\n- Room Type: {{ $json.room_type }}\n- Description: {{ $json.room_description }}\n- Work Type: {{ $json.work_type_detected }}\n- Dimensions: Floor {{ $json.dimensions.floor_area_m2 || 10 }} m\u00b2, Walls {{ $json.dimensions.wall_area_m2 || 30 }} m\u00b2\n\n### ELEMENTS DETECTED:\n{{ JSON.stringify($json.elements, null, 2) }}\n\n### FIXTURES DETECTED:\n{{ JSON.stringify($json.fixtures, null, 2) }}\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n## CATEGORY \u2192 WORK ITEMS MAPPING (USE THIS!)\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n### BATHROOM:\n1. Demolition of old finishes (m\u00b2) - if renovation\n2. Waterproofing floor (m\u00b2)\n3. Waterproofing walls wet zone (m\u00b2)\n4. Floor screed (m\u00b2)\n5. Wall tiling (m\u00b2)\n6. Floor tiling (m\u00b2)\n7. Ceiling finishing (m\u00b2)\n8. Toilet installation (pcs)\n9. Sink/washbasin installation (pcs)\n10. Bathtub installation (pcs) - if present\n11. Shower cabin installation (pcs) - if present\n12. Faucet/mixer installation (pcs)\n13. Towel rail installation (pcs)\n14. Mirror installation (pcs)\n15. Ventilation installation (pcs)\n16. Electrical: sockets, switches, lighting (pcs)\n17. Plumbing pipes (m)\n18. Door installation (pcs)\n\n### KITCHEN:\n1. Demolition of old finishes (m\u00b2) - if renovation\n2. Wall preparation/plastering (m\u00b2)\n3. Wall tiling (backsplash area) (m\u00b2)\n4. Floor finishing (m\u00b2)\n5. Ceiling finishing (m\u00b2)\n6. Kitchen cabinets installation (m)\n7. Countertop installation (m)\n8. Sink installation (pcs)\n9. Faucet installation (pcs)\n10. Appliance connections (pcs)\n11. Electrical: sockets, switches (pcs)\n12. Lighting installation (pcs)\n13. Ventilation hood installation (pcs)\n14. Plumbing pipes (m)\n\n### LIVING ROOM / BEDROOM:\n1. Demolition of old finishes (m\u00b2) - if renovation\n2. Wall preparation (m\u00b2)\n3. Wall painting OR wallpaper (m\u00b2)\n4. Floor preparation/screed (m\u00b2)\n5. Floor covering - laminate/parquet/carpet (m\u00b2)\n6. Baseboard/skirting installation (m)\n7. Ceiling finishing - paint/stretch/drywall (m\u00b2)\n8. Electrical: sockets, switches (pcs)\n9. Lighting installation (pcs)\n10. Window sill finishing (m)\n11. Door installation (pcs)\n12. Radiator installation (pcs) - if visible\n\n### FLOOR WORKS:\n1. Old floor demolition (m\u00b2) - if renovation\n2. Floor leveling/screed (m\u00b2)\n3. Insulation (m\u00b2) - if ground floor\n4. Underfloor heating (m\u00b2) - if applicable\n5. Primer/preparation (m\u00b2)\n6. Floor covering (m\u00b2)\n7. Baseboard installation (m)\n\n### WALL WORKS:\n- Drywall: Metal framing (m\u00b2) \u2192 Insulation (m\u00b2) \u2192 Boarding (m\u00b2) \u2192 Jointing (m\u00b2) \u2192 Painting (m\u00b2)\n- Masonry: Brickwork (m\u00b2) \u2192 Plastering (m\u00b2) \u2192 Painting (m\u00b2)\n- Tiling: Wall preparation (m\u00b2) \u2192 Tiling (m\u00b2) \u2192 Grouting (m\u00b2)\n\n### WINDOW WORKS:\n1. Old window demolition (pcs) - if renovation\n2. Window frame installation (pcs)\n3. Glazing (m\u00b2)\n4. Internal sill (m)\n5. External sill (m)\n6. Sealing/foam (m)\n7. Trim/architrave (m)\n8. Painting/finishing (m)\n\n### DOOR WORKS:\n1. Old door demolition (pcs) - if renovation\n2. Door frame installation (pcs)\n3. Door leaf hanging (pcs)\n4. Hardware installation (pcs)\n5. Trim/architrave (m)\n6. Painting/finishing (m\u00b2)\n\n### ELECTRICAL WORKS:\n1. Cable routing (m)\n2. Socket installation (pcs)\n3. Switch installation (pcs)\n4. Junction box (pcs)\n5. Lighting point (pcs)\n6. Panel/breaker installation (pcs)\n\n### PLUMBING WORKS:\n1. Pipe installation - supply (m)\n2. Pipe installation - drain (m)\n3. Valve installation (pcs)\n4. Connection to fixture (pcs)\n5. Pressure testing (pcs)\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n## CRITICAL RULES:\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n1. **NEVER return empty work_items** - minimum 3 works per element\n2. **Include PREPARATION works** - demolition, priming, leveling\n3. **Include FINISHING works** - painting, sealing, cleaning\n4. **Match units correctly**: areas\u2192m\u00b2, lengths\u2192m, items\u2192pcs\n5. **For RENOVATION**: always include demolition/removal works first\n6. **Scale quantities** from dimensions provided\n7. **search_query must be in {{ $json.search_lang }}** for vector database\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nReturn ONLY valid JSON:\n{\n  \"work_items\": [\n    {\n      \"work_sequence\": 1,\n      \"work_category\": \"PREPARATION|MAIN|FINISHING|MEP\",\n      \"work_name\": \"Work name in {{ $json.language }}\",\n      \"search_query\": \"Search terms in {{ $json.search_lang }} for DDC CWICR database - be specific!\",\n      \"quantity\": 12.5,\n      \"unit\": \"m\u00b2|m|pcs\",\n      \"calculation_basis\": \"floor_area \u00d7 1.0 = 12.5 m\u00b2\",\n      \"source_element\": \"Which element this work is for\",\n      \"is_demolition\": false\n    }\n  ],\n  \"total_works_count\": 15,\n  \"phases\": [\"PREPARATION\", \"MAIN\", \"FINISHING\", \"MEP\"]\n}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "b7550960-96b9-400a-82cd-57fcae2813ec",
      "name": "STAGE 4 Decompose LLM",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        30880,
        384
      ],
      "parameters": {
        "messages": {
          "messageValues": [
            {
              "message": "={{ $json.chatInput }}"
            }
          ]
        }
      },
      "typeVersion": 1.4
    },
    {
      "id": "b973f4c7-7800-48ac-bd3f-f6a7f1e4d278",
      "name": "GPT-4 Decompose",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        30880,
        592
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "chatgpt-4o-latest",
          "cachedResultName": "chatgpt-4o-latest"
        },
        "options": {
          "maxTokens": 8000,
          "temperature": 0.3
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "ae0ff40d-08ce-4c80-8cdc-69d7a343b1a7",
      "name": "Parse STAGE 4",
      "type": "n8n-nodes-base.code",
      "position": [
        31168,
        384
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// PARSE STAGE 4 DECOMPOSITION RESPONSE\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst aiResponse = $input.first().json;\nconst configData = $('Parse STAGE 1').first().json;\n\nlet parsed = { work_items: [] };\n\ntry {\n  const content = aiResponse.text || aiResponse.content || aiResponse.response || '';\n  let jsonStr = content;\n  \n  const jsonMatch = content.match(/```json\\s*([\\s\\S]*?)\\s*```/);\n  if (jsonMatch) {\n    jsonStr = jsonMatch[1];\n  } else {\n    const objMatch = content.match(/\\{[\\s\\S]*\\}/);\n    if (objMatch) {\n      jsonStr = objMatch[0];\n    }\n  }\n  \n  parsed = JSON.parse(jsonStr);\n} catch (error) {\n  console.error('STAGE 4 Parse error:', error.message);\n}\n\n// Validate and enrich work items\nconst workItems = (parsed.work_items || []).map((work, idx) => {\n  // Ensure search_query exists and is meaningful\n  let searchQuery = work.search_query || work.work_name || '';\n  \n  // Add language-specific terms if needed\n  if (searchQuery.length < 5) {\n    searchQuery = work.work_name + ' ' + (work.source_element || '');\n  }\n  \n  return {\n    work_id: `W${String(idx + 1).padStart(3, '0')}`,\n    work_sequence: work.work_sequence || idx + 1,\n    work_category: work.work_category || 'MAIN',\n    work_name: work.work_name || 'Unnamed work',\n    search_query: searchQuery.trim(),\n    project_quantity: parseFloat(work.quantity) || 1,\n    unit: work.unit || 'm\u00b2',\n    calculation_basis: work.calculation_basis || '',\n    source_element: work.source_element || '',\n    is_demolition: work.is_demolition || false\n  };\n});\n\n// Sort by sequence\nworkItems.sort((a, b) => a.work_sequence - b.work_sequence);\n\nreturn {\n  json: {\n    ...configData,\n    work_items: workItems,\n    works_count: workItems.length,\n    phases: parsed.phases || ['PREPARATION', 'MAIN', 'FINISHING', 'MEP'],\n    stage4_success: workItems.length >= 3\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "70e62b94-292e-4f2e-b3bc-ca607618d772",
      "name": "Prepare Works",
      "type": "n8n-nodes-base.code",
      "position": [
        31344,
        384
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// PREPARE WORKS FOR LOOP\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst data = $input.first().json;\nconst works = data.work_items || [];\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData.photo_config = {\n  language_code: data.language_code,\n  language: data.language,\n  currency: data.currency,\n  currency_symbol: data.currency_symbol,\n  locale: data.locale,\n  city: data.city,\n  qdrant_collection: data.qdrant_collection,\n  room_type: data.room_type,\n  room_description: data.room_description,\n  dimensions: data.dimensions,\n  work_type_detected: data.work_type_detected,\n  phases: data.phases,\n  elements: data.elements,\n  fixtures: data.fixtures\n};\nstaticData.work_results = [];\n\nif (works.length === 0) {\n  return [{ json: { _no_works: true, message: 'No work items generated' } }];\n}\n\nreturn works.map((work) => ({\n  json: {\n    ...work,\n    expected_unit: work.unit,\n    type_name: work.work_name,\n    category: work.work_category,\n    assigned_phase: work.work_category,\n    qdrant_collection: data.qdrant_collection,\n    language: data.language,\n    currency: data.currency,\n    currency_symbol: data.currency_symbol,\n    locale: data.locale\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "10b9b98b-f782-499f-9c12-e254af13bd0a",
      "name": "Loop Works",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        31536,
        384
      ],
      "parameters": {
        "options": {
          "reset": false
        }
      },
      "typeVersion": 3
    },
    {
      "id": "79f735d7-6de7-4a05-88e7-12d08b2fd6df",
      "name": "Store Work Data",
      "type": "n8n-nodes-base.code",
      "position": [
        32016,
        592
      ],
      "parameters": {
        "jsCode": "// Store current work item for Vector Search\nconst work = $input.first().json;\nconst staticData = $getWorkflowStaticData('global');\nstaticData.currentWork = work;\nreturn { json: work };"
      },
      "typeVersion": 2
    },
    {
      "id": "60873726-0ece-4e69-91dc-73c77ad796b2",
      "name": "Wait",
      "type": "n8n-nodes-base.wait",
      "position": [
        32160,
        592
      ],
      "parameters": {
        "amount": 0.3
      },
      "typeVersion": 1.1
    },
    {
      "id": "c5e35231-575a-4d53-95c4-ef2875363906",
      "name": "Restore Work Data",
      "type": "n8n-nodes-base.code",
      "position": [
        32304,
        592
      ],
      "parameters": {
        "jsCode": "// Restore work data from staticData after Wait\nconst staticData = $getWorkflowStaticData('global');\nconst work = staticData.currentWork || {};\nreturn { json: work };"
      },
      "typeVersion": 2
    },
    {
      "id": "595e380e-4b57-4063-a594-fb89e010f81f",
      "name": "Vector Search",
      "type": "@n8n/n8n-nodes-langchain.vectorStoreQdrant",
      "position": [
        32464,
        592
      ],
      "parameters": {
        "mode": "load",
        "topK": 5,
        "prompt": "={{ $json.search_query || $json.work_name }}",
        "options": {
          "contentPayloadKey": "content"
        },
        "qdrantCollection": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.qdrant_collection }}"
        }
      },
      "credentials": {
        "qdrantApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "782d04ee-61c6-40a0-85bf-c5554533f3b5",
      "name": "Embeddings",
      "type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
      "position": [
        32448,
        752
      ],
      "parameters": {
        "model": "text-embedding-3-large",
        "options": {
          "dimensions": 3072
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "e527cbd6-8841-4425-902a-ee4a28706c2f",
      "name": "STAGE 5 Parse & Score",
      "type": "n8n-nodes-base.code",
      "position": [
        32720,
        592
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// STAGE 5: PARSE & VALIDATE VECTOR SEARCH RESULTS\n// FIXED: Correct document/pageContent extraction\n// Quality scoring v2.0 + Resource extraction\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst searchResults = $input.all();\nconst staticData = $getWorkflowStaticData('global');\nconst workData = staticData.currentWork || {};\n\n// MACHINE/LABOR PATTERNS (9 languages)\nconst MACHINE_PATTERNS = ['masch', 'maschinenstunde', 'ger\u00e4t', 'machine', 'mach-h', 'equipment', 'heure machine', 'engin', 'm\u00e1quina', 'equipo', 'equipamento', '\u043c\u0430\u0448\u0438\u043d', '\u043c\u0430\u0448-\u0447', '\u043c\u0430\u0448.\u0447', '\u043c\u0430\u0448.-\u0447', '\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c', '\u673a\u5668', '\u673a\u68b0', '\u8bbe\u5907', '\u53f0\u73ed', '\u0622\u0644\u0629', '\u0645\u0639\u062f\u0629', '\u092e\u0936\u0940\u0928', '\u092f\u0902\u0924\u094d\u0930'];\nconst LABOR_PATTERNS = ['std', 'stunde', 'arbeiter', 'hour', 'man-hour', 'labor', 'worker', 'heure', 'ouvrier', 'hora', 'obrero', 'oper\u00e1rio', '\u0447-\u0447', '\u0447\u0435\u043b-\u0447', '\u0447\u0435\u043b\u043e\u0432\u0435\u043a\u043e-\u0447\u0430\u0441', '\u0442\u0440\u0443\u0434', '\u0440\u0430\u0431\u043e\u0447\u0438\u0439', '\u0440\u0430\u0437\u0440\u044f\u0434', ' \u0447', '\u5de5\u65f6', '\u4eba\u5de5', '\u5de5\u4eba', '\u0633\u0627\u0639\u0629', '\u0639\u0627\u0645\u0644', '\u0918\u0902\u091f\u093e', '\u0936\u094d\u0930\u092e', '\u092e\u091c\u0926\u0942\u0930'];\n\nfunction detectResourceType(unit, name, code) {\n  const combined = ((unit || '') + ' ' + (name || '') + ' ' + (code || '')).toLowerCase();\n  if (MACHINE_PATTERNS.some(p => combined.includes(p.toLowerCase()))) return 'machine';\n  if (LABOR_PATTERNS.some(p => combined.includes(p.toLowerCase()))) return 'labor';\n  return 'material';\n}\n\nfunction normalizeUnit(unit) {\n  if (!unit) return '';\n  const u = unit.toLowerCase().trim();\n  const unitGroups = {\n    'm2': ['m\u00b2', 'm2', 'qm', '\u043a\u0432\u043c', '\u043a\u0432.\u043c', '\u043a\u0432. \u043c', 'sq.m', 'sqm', '\u5e73\u65b9\u7c73'],\n    'm3': ['m\u00b3', 'm3', 'cbm', '\u043a\u0443\u0431.\u043c', '\u043a\u0443\u0431. \u043c', 'cu.m', '\u7acb\u65b9\u7c73'],\n    'm': ['m', '\u043c', 'lm', 'lfm', '\u043f.\u043c', '\u043f. \u043c', 'lin.m', '\u7c73'],\n    'stk': ['stk', 'st\u00fcck', 'st', '\u0448\u0442', '\u0448\u0442.', 'pcs', 'ea', 'each', 'unit', '\u4e2a', '\u4ef6'],\n    '100m2': ['100 m\u00b2', '100m\u00b2', '100 m2', '100m2', '100 qm', '100 \u043a\u0432.\u043c']\n  };\n  for (const [normalized, variants] of Object.entries(unitGroups)) {\n    if (variants.some(v => u === v || u.includes(v))) return normalized;\n  }\n  return u;\n}\n\n// MATERIAL KEYWORDS for matching\nconst MATERIAL_KEYWORDS = [\n  'aluminum', 'aluminium', 'alu', '\u0430\u043b\u044e\u043c\u0438\u043d',\n  'wood', 'holz', '\u0434\u0435\u0440\u0435\u0432', '\u0434\u0440\u0435\u0432\u0435\u0441', '\u6728',\n  'plastic', 'pvc', 'kunststoff', '\u043f\u043b\u0430\u0441\u0442\u0438\u043a', '\u043f\u0432\u0445',\n  'steel', 'stahl', '\u0441\u0442\u0430\u043b', '\u043c\u0435\u0442\u0430\u043b\u043b', '\u94a2',\n  'concrete', 'beton', '\u0431\u0435\u0442\u043e\u043d', '\u6df7\u51dd\u571f',\n  'glass', 'glas', '\u0441\u0442\u0435\u043a\u043b', '\u73bb\u7483',\n  'brick', 'ziegel', '\u043a\u0438\u0440\u043f\u0438\u0447', '\u7816',\n  'tile', 'fliese', '\u043f\u043b\u0438\u0442\u043a', '\u043a\u0430\u0444\u0435\u043b\u044c', '\u74f7\u7816',\n  'ceramic', 'keramik', '\u043a\u0435\u0440\u0430\u043c\u0438\u043a', '\u9676\u74f7',\n  'gypsum', 'gips', '\u0433\u0438\u043f\u0441', '\u77f3\u818f',\n  'drywall', '\u0433\u0438\u043f\u0441\u043e\u043a\u0430\u0440\u0442\u043e\u043d', '\u0433\u043a\u043b',\n  'paint', 'farbe', '\u043a\u0440\u0430\u0441\u043a', '\u6cb9\u6f06',\n  'wallpaper', 'tapete', '\u043e\u0431\u043e', '\u58c1\u7eb8',\n  'laminate', 'laminat', '\u043b\u0430\u043c\u0438\u043d\u0430\u0442',\n  'parquet', 'parkett', '\u043f\u0430\u0440\u043a\u0435\u0442'\n];\n\n// WORK TYPE KEYWORDS for matching\nconst WORK_TYPE_KEYWORDS = [\n  'installation', 'montage', '\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a', '\u043c\u043e\u043d\u0442\u0430\u0436', '\u5b89\u88c5',\n  'demolition', 'abbruch', '\u0434\u0435\u043c\u043e\u043d\u0442\u0430\u0436', '\u0441\u043d\u043e\u0441', '\u0440\u0430\u0437\u0431\u043e\u0440', '\u62c6\u9664',\n  'preparation', 'vorbereitung', '\u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432', '\u51c6\u5907',\n  'sealing', 'abdichtung', '\u0433\u0435\u0440\u043c\u0435\u0442\u0438\u0437', '\u0433\u0438\u0434\u0440\u043e\u0438\u0437\u043e\u043b', '\u5bc6\u5c01',\n  'insulation', 'd\u00e4mmung', '\u0438\u0437\u043e\u043b\u044f\u0446', '\u0443\u0442\u0435\u043f\u043b', '\u4fdd\u6e29',\n  'finishing', '\u043e\u0442\u0434\u0435\u043b\u043a', 'finish', '\u88c5\u4fee',\n  'excavation', 'aushub', '\u0432\u044b\u0435\u043c\u043a', '\u043a\u043e\u043f\u0430\u043d', '\u5f00\u6316',\n  'concrete', 'beton', '\u0431\u0435\u0442\u043e\u043d\u0438\u0440', '\u6df7\u51dd\u571f',\n  'reinforcement', 'bewehrung', '\u0430\u0440\u043c\u0438\u0440', '\u94a2\u7b4b',\n  'plastering', 'putz', '\u0448\u0442\u0443\u043a\u0430\u0442\u0443\u0440', '\u62b9\u7070',\n  'painting', 'malen', 'anstrich', '\u043e\u043a\u0440\u0430\u0441\u043a', '\u043f\u043e\u043a\u0440\u0430\u0441\u043a', '\u6cb9\u6f06',\n  'tiling', 'fliesen', '\u043e\u0431\u043b\u0438\u0446\u043e\u0432', '\u0443\u043a\u043b\u0430\u0434\u043a \u043f\u043b\u0438\u0442\u043a', '\u8d34\u7816',\n  'flooring', 'boden', '\u043d\u0430\u043f\u043e\u043b\u044c\u043d', '\u043f\u043e\u043b', '\u5730\u677f',\n  'roofing', 'dach', '\u043a\u0440\u043e\u0432\u043b', '\u5c4b\u9762',\n  'plumbing', 'sanit\u00e4r', '\u0441\u0430\u043d\u0442\u0435\u0445\u043d', '\u7ba1\u9053',\n  'electrical', 'elektro', '\u044d\u043b\u0435\u043a\u0442\u0440', '\u7535\u6c14'\n];\n\n// NOT FOUND FALLBACK\nif (!searchResults || searchResults.length === 0) {\n  return [{\n    json: {\n      ...workData,\n      rate_code: 'NOT_FOUND',\n      rate_name: '[Not Found] ' + (workData.work_name || 'Unknown'),\n      rate_unit: workData.expected_unit || 'm\u00b2',\n      unit_cost: 0,\n      total_cost: 0,\n      estimated_labor_hours: 0,\n      quality_level: 'not_found',\n      quality_score: 0,\n      quality_reason: 'No search results',\n      resources_all: [],\n      cost_breakdown: { workers: 0, machines: 0, materials: 0 }\n    }\n  }];\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// FIX: CORRECT EXTRACTION OF document.pageContent\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst parsedResults = searchResults.map((item) => {\n  const data = item.json || {};\n  \n  // FIX: Handle nested document structure from Vector Search\n  const doc = data.document || {};\n  const content = String(doc.pageContent || doc.content || data.pageContent || data.content || '');\n  const metadata = doc.metadata || data.metadata || {};\n  const score = data.score || 0;\n  \n  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n  // EXTRACT TOTAL COST from \"Total cost: 3127.16 EUR\"\n  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n  let totalCost = 0;\n  const costPatterns = [\n    /Total\\s*cost:\\s*([\\d.,]+)\\s*(?:EUR|USD|RUB|CAD|CNY|AED|INR|BRL)/i,\n    /Resources?\\s*cost:\\s*([\\d.,]+)/i,\n    /(?:Gesamt|\u0418\u0422\u041e\u0413\u041e|\u0412\u0441\u0435\u0433\u043e|\u0421\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c)[^\\d]*([\\d.,]+)/i,\n    /(?:EUR|USD|RUB|CAD|\\$|\u20ac|\u20bd)\\s*([\\d.,]+)/i\n  ];\n  for (const pattern of costPatterns) {\n    const match = content.match(pattern);\n    if (match) {\n      totalCost = parseFloat(match[1].replace(/,/g, '.').replace(/\\s/g, '')) || 0;\n      if (totalCost > 0) break;\n    }\n  }\n  \n  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n  // EXTRACT CODE, NAME, UNIT from content if not in metadata\n  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n  let rateCode = metadata.rsts || metadata.code || '';\n  let rateName = metadata.names || metadata.name || '';\n  let rateUnit = metadata.unit || '';\n  \n  // Fallback: extract from content\n  if (!rateCode) {\n    const codeMatch = content.match(/CODE:\\s*([A-Z\u0410-\u042fa-z\u0430-\u044f0-9_-]+)/i);\n    if (codeMatch) rateCode = codeMatch[1];\n  }\n  if (!rateName) {\n    const nameMatch = content.match(/NAME:\\s*(.+?)(?:\\n|UNIT:|CATEGORY:)/i);\n    if (nameMatch) rateName = nameMatch[1].trim();\n  }\n  if (!rateUnit) {\n    const unitMatch = content.match(/UNIT:\\s*(.+?)(?:\\n|CATEGORY:)/i);\n    if (unitMatch) rateUnit = unitMatch[1].trim();\n  }\n  \n  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n  // EXTRACT LABOR HOURS\n  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n  let laborHours = 0;\n  const laborMatch = content.match(/(?:\u0440\u0430\u0437\u0440\u044f\u0434|worker|arbeiter|labor)[^\u2014\\n]*\u2014\\s*([\\d.,]+)\\s*(?:\u0447|\u0447\u0430\u0441|std|hour|h)/i);\n  if (laborMatch) {\n    laborHours = parseFloat(laborMatch[1].replace(',', '.')) || 0;\n  }\n  \n  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n  // EXTRACT RESOURCES from RESOURCES: section\n  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n  const resources = [];\n  const resourcesSection = content.match(/RESOURCES:\\s*([\\s\\S]*?)(?:$|MACHINES:|SCOPE|CLASSIFICATION:)/i);\n  if (resourcesSection) {\n    const resourcesText = resourcesSection[1];\n    const resourceLines = resourcesText.split('\\n').filter(line => line.trim().startsWith('-'));\n    \n    for (const line of resourceLines) {\n      const resMatch = line.match(/^-\\s*([A-Z\u0410-\u042fa-z\u0430-\u044f0-9_-]+)\\s*[\u2014\u2013-]\\s*(.+?)\\s*[\u2014\u2013-]\\s*([\\d.,]+)\\s*(.+?)$/i);\n      if (resMatch) {\n        const resCode = resMatch[1].trim();\n        const resName = resMatch[2].trim();\n        const resQty = parseFloat(resMatch[3].replace(',', '.')) || 0;\n        const resUnit = resMatch[4].trim();\n        const resType = detectResourceType(resUnit, resName, resCode);\n        \n        resources.push({\n          resource_code: resCode,\n          resource_name: resName,\n          resource_quantity: resQty,\n          resource_unit: resUnit,\n          resource_cost: 0,\n          resource_type: resType\n        });\n      }\n    }\n  }\n  \n  return {\n    rate_code: rateCode,\n    rate_name: rateName,\n    rate_unit: rateUnit,\n    total_cost_position: totalCost,\n    worker_labor_hours: laborHours,\n    hierarchy: metadata.hierarchy || '',\n    resources: resources,\n    resources_count: resources.length,\n    vector_score: score,\n    content_preview: content.substring(0, 300)\n  };\n});\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// QUALITY SCORING v2.0\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst expectedUnit = normalizeUnit(workData.expected_unit || workData.unit || '');\nconst searchQuery = (workData.search_query || workData.work_name || '').toLowerCase();\nconst workName = (workData.work_name || '').toLowerCase();\n\nconst scoredResults = parsedResults.map(result => {\n  let score = 0;\n  const factors = [];\n  const resultUnit = normalizeUnit(result.rate_unit);\n  const rateName = (result.rate_name || '').toLowerCase();\n  const rateContent = (result.content_preview || '').toLowerCase();\n  const combined = rateName + ' ' + rateContent;\n  \n  if (result.total_cost_position > 0) { \n    score += 30; \n    factors.push('has_price:' + result.total_cost_position.toFixed(0)); \n  }\n  \n  if (result.resources_count > 0) { \n    score += Math.min(25, result.resources_count * 5); \n    factors.push('resources:' + result.resources_count); \n  }\n  \n  if (resultUnit && expectedUnit) {\n    if (resultUnit === expectedUnit) { \n      score += 20; \n      factors.push('unit_exact'); \n    } else if (resultUnit.includes(expectedUnit) || expectedUnit.includes(resultUnit)) { \n      score += 10; \n      factors.push('unit_partial'); \n    }\n  }\n  \n  const materialMatches = MATERIAL_KEYWORDS.filter(kw => \n    (workName.includes(kw) || searchQuery.includes(kw)) && combined.includes(kw)\n  );\n  if (materialMatches.length > 0) {\n    score += 15;\n    factors.push('material:' + materialMatches[0]);\n  }\n  \n  const workTypeMatches = WORK_TYPE_KEYWORDS.filter(kw => \n    (workName.includes(kw) || searchQuery.includes(kw)) && combined.includes(kw)\n  );\n  if (workTypeMatches.length > 0) {\n    score += 10;\n    factors.push('work_type:' + workTypeMatches[0]);\n  }\n  \n  const queryWords = searchQuery.split(/\\s+/).filter(w => w.length > 3);\n  const matchedWords = queryWords.filter(w => rateName.includes(w) || rateContent.includes(w));\n  if (matchedWords.length > 0) { \n    score += Math.min(15, matchedWords.length * 5); \n    factors.push('words:' + matchedWords.length); \n  }\n  \n  if (result.vector_score > 0.5) {\n    score += 10;\n    factors.push('vscore:' + result.vector_score.toFixed(2));\n  } else if (result.vector_score > 0.4) {\n    score += 5;\n    factors.push('vscore:' + result.vector_score.toFixed(2));\n  }\n  \n  const hasLabor = result.resources.some(r => r.resource_type === 'labor');\n  const hasMaterial = result.resources.some(r => r.resource_type === 'material');\n  if (hasLabor && hasMaterial) {\n    score += 5;\n    factors.push('complete_rate');\n  }\n  \n  return { ...result, quality_score: score, quality_factors: factors };\n});\n\nconst sorted = scoredResults.sort((a, b) => b.quality_score - a.quality_score);\nconst best = sorted[0] || { quality_score: 0, quality_factors: [], resources: [] };\n\nlet qualityLevel = 'not_found';\nconst s = best.quality_score;\nif (s >= 60) { qualityLevel = 'high'; }\nelse if (s >= 40) { qualityLevel = 'medium'; }\nelse if (s >= 20) { qualityLevel = 'low'; }\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// CALCULATE COSTS\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst projectQuantity = workData.project_quantity || 0;\nconst unitCost = best.total_cost_position || 0;\n\nlet unitDivisor = 1;\nconst rateUnit = (best.rate_unit || '').toLowerCase();\nif (rateUnit.includes('100')) unitDivisor = 100;\nelse if (rateUnit.includes('10 ')) unitDivisor = 10;\n\nconst quantityInRateUnits = projectQuantity / unitDivisor;\nconst totalCost = quantityInRateUnits * unitCost;\nconst estimatedLaborHours = (best.worker_labor_hours || 0) * quantityInRateUnits;\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// SCALE & CATEGORIZE RESOURCES\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nlet laborCost = 0, machineCost = 0, materialCost = 0;\nconst scaledResources = (best.resources || []).map(r => {\n  const scaledQty = (r.resource_quantity || 0) * quantityInRateUnits;\n  const scaledCost = (r.resource_cost || 0) * quantityInRateUnits;\n  \n  if (r.resource_type === 'labor') laborCost += scaledCost;\n  else if (r.resource_type === 'machine') machineCost += scaledCost;\n  else materialCost += scaledCost;\n  \n  return { ...r, scaled_quantity: scaledQty, scaled_cost: scaledCost };\n});\n\nif (laborCost === 0 && materialCost === 0 && totalCost > 0) {\n  laborCost = totalCost * 0.35;\n  materialCost = totalCost * 0.55;\n  machineCost = totalCost * 0.10;\n}\n\nreturn [{\n  json: {\n    work_id: workData.work_id,\n    work_sequence: workData.work_sequence,\n    work_category: workData.work_category,\n    work_name: workData.work_name,\n    source_element: workData.source_element,\n    calculation_basis: workData.calculation_basis,\n    is_demolition: workData.is_demolition,\n    rate_code: best.rate_code || 'NOT_FOUND',\n    rate_name: best.rate_name || workData.work_name || 'Not found',\n    rate_unit: best.rate_unit || workData.expected_unit,\n    project_quantity: projectQuantity,\n    project_unit: workData.unit || workData.expected_unit,\n    quantity_in_rate_units: quantityInRateUnits,\n    calculated_quantity: quantityInRateUnits,\n    unit_divisor: unitDivisor,\n    unit_cost: unitCost,\n    total_cost: totalCost,\n    estimated_labor_hours: estimatedLaborHours,\n    quality_level: qualityLevel,\n    quality_score: s,\n    quality_reason: 'Score ' + s + '/100: ' + best.quality_factors.join(', '),\n    currency: workData.currency,\n    currency_symbol: workData.currency_symbol,\n    locale: workData.locale,\n    type_name: workData.type_name,\n    category: workData.category,\n    assigned_phase: workData.assigned_phase,\n    resources_all: scaledResources,\n    cost_breakdown: {\n      workers: laborCost,\n      machines: machineCost,\n      materials: materialCost\n    },\n    calculation_details: {\n      method: 'photo_analysis',\n      calculation_basis: workData.calculation_basis,\n      raw_value: projectQuantity,\n      unit_divisor: unitDivisor,\n      formula_display: (workData.unit || 'Qty') + ' = ' + projectQuantity\n    },\n    search_debug: {\n      query_used: workData.search_query,\n      results_count: searchResults.length,\n      best_match_score: s,\n      best_vector_score: best.vector_score || 0,\n      best_rate_found: best.rate_name || 'none'\n    }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "577e8350-ff19-4b73-a53f-80605484a493",
      "name": "Accumulate",
      "type": "n8n-nodes-base.code",
      "position": [
        32864,
        592
      ],
      "parameters": {
        "jsCode": "// ACCUMULATE RESULTS\nconst staticData = $getWorkflowStaticData('global');\nconst work = $input.first().json;\nif (!staticData.work_results) staticData.work_results = [];\nstaticData.work_results.push(work);\nreturn { json: work };"
      },
      "typeVersion": 2
    },
    {
      "id": "9b8dd6f9-e36b-4fc0-bac8-5a0857890e32",
      "name": "STAGE 7.5 Aggregate & Validate",
      "type": "n8n-nodes-base.code",
      "position": [
        31824,
        352
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// STAGE 7.5: AGGREGATE & VALIDATE RESULTS\n// Build by_phase structure with validation\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst staticData = $getWorkflowStaticData('global');\nconst config = staticData.photo_config || {};\nconst works = staticData.work_results || [];\n\n// Calculate totals\nlet grandTotal = 0, grandHours = 0;\nlet grandLaborCost = 0, grandMaterialCost = 0, grandMachineCost = 0;\nlet qHigh = 0, qMedium = 0, qLow = 0, qNotFound = 0;\n\nworks.forEach(w => {\n  grandTotal += w.total_cost || 0;\n  grandHours += w.estimated_labor_hours || 0;\n  \n  const cb = w.cost_breakdown || {};\n  grandLaborCost += cb.workers || 0;\n  grandMaterialCost += cb.materials || 0;\n  grandMachineCost += cb.machines || 0;\n  \n  if (w.quality_level === 'high') qHigh++;\n  else if (w.quality_level === 'medium') qMedium++;\n  else if (w.quality_level === 'low') qLow++;\n  else qNotFound++;\n});\n\n// Sort by sequence\nworks.sort((a, b) => (a.work_sequence || 0) - (b.work_sequence || 0));\n\n// Group by category (PREPARATION, MAIN, FINISHING, MEP)\nconst categories = {};\nworks.forEach(w => {\n  const cat = w.work_category || 'MAIN';\n  if (!categories[cat]) {\n    categories[cat] = {\n      works: [],\n      total_cost: 0,\n      labor_hours: 0\n    };\n  }\n  categories[cat].works.push(w);\n  categories[cat].total_cost += w.total_cost || 0;\n  categories[cat].labor_hours += w.estimated_labor_hours || 0;\n});\n\n// Build by_phase structure\nconst phaseOrder = ['PREPARATION', 'MAIN', 'FINISHING', 'MEP'];\nconst byPhase = phaseOrder\n  .filter(phase => categories[phase])\n  .map((phase, idx) => {\n    const cat = categories[phase];\n    const phaseName = {\n      'PREPARATION': config.language_code === 'RU' ? '\u041f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0440\u0430\u0431\u043e\u0442\u044b' : \n                     config.language_code === 'DE' ? 'Vorbereitungsarbeiten' : 'Preparation Works',\n      'MAIN': config.language_code === 'RU' ? '\u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u0440\u0430\u0431\u043e\u0442\u044b' : \n              config.language_code === 'DE' ? 'Hauptarbeiten' : 'Main Works',\n      'FINISHING': config.language_code === 'RU' ? '\u041e\u0442\u0434\u0435\u043b\u043e\u0447\u043d\u044b\u0435 \u0440\u0430\u0431\u043e\u0442\u044b' : \n                   config.language_code === 'DE' ? 'Ausbauarbeiten' : 'Finishing Works',\n      'MEP': config.language_code === 'RU' ? '\u0418\u043d\u0436\u0435\u043d\u0435\u0440\u043d\u044b\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u044b' : \n             config.language_code === 'DE' ? 'Haustechnik' : 'MEP Systems'\n    }[phase] || phase;\n    \n    return {\n      phase_name: phaseName,\n      phase_code: phase,\n      phase_total_cost: cat.total_cost,\n      phase_labor_hours: cat.labor_hours,\n      types: [{\n        type_name: config.room_description || 'Photo Analysis',\n        category: config.room_type || 'General',\n        element_count: cat.works.length,\n        type_total_cost: cat.total_cost,\n        type_total_labor_hours: cat.labor_hours,\n        works: cat.works\n      }]\n    };\n  });\n\n// VALIDATION CHECKS\nconst validationIssues = [];\n\nif (works.length < 3) {\n  validationIssues.push('\u26a0\ufe0f Less than 3 work items generated');\n}\n\nconst foundPercent = works.length > 0 ? Math.round((qHigh + qMedium + qLow) / works.length * 100) : 0;\nif (foundPercent < 50) {\n  validationIssues.push('\u26a0\ufe0f Less than 50% rates found - manual review needed');\n}\n\nconst zeroCostCount = works.filter(w => w.total_cost === 0).length;\nif (zeroCostCount > works.length * 0.3) {\n  validationIssues.push('\u26a0\ufe0f Many items with zero cost - database coverage issue');\n}\n\nif (config.work_type_detected === 'renovation') {\n  const hasDemolition = works.some(w => w.is_demolition || \n    (w.work_name || '').toLowerCase().match(/\u0434\u0435\u043c\u043e\u043d\u0442\u0430\u0436|\u0441\u043d\u043e\u0441|\u0440\u0430\u0437\u0431\u043e\u0440|demolition|abbruch|removal/));\n  if (!hasDemolition) {\n    validationIssues.push('\ud83d\udca1 Renovation detected but no demolition works - consider adding');\n  }\n}\n\nconst photoConfig = staticData.photo_config;\nstaticData.work_results = [];\nstaticData.photo_config = null;\n\nreturn {\n  json: {\n    by_phase: byPhase,\n    cost_summary: {\n      grand_total_cost: grandTotal,\n      grand_labor_hours: grandHours,\n      grand_resource_cost: grandLaborCost,\n      grand_material_cost: grandMaterialCost,\n      grand_machine_cost: grandMachineCost\n    },\n    photo_analysis: {\n      description: photoConfig?.room_description || 'Photo Analysis',\n      room_type: photoConfig?.room_type || 'unknown',\n      location_type: photoConfig?.room_type || 'interior',\n      room_size: (photoConfig?.dimensions?.floor_area_m2 || 'N/A') + ' m\u00b2',\n      work_phase: photoConfig?.work_type_detected || 'general',\n      materials: photoConfig?.elements?.map(e => e.material).filter(Boolean) || [],\n      elements_count: photoConfig?.elements?.length || 0,\n      fixtures_count: photoConfig?.fixtures?.length || 0\n    },\n    language_code: photoConfig?.language_code || 'EN',\n    currency: photoConfig?.currency || 'EUR',\n    currency_symbol: photoConfig?.currency_symbol || '\u20ac',\n    locale: photoConfig?.locale || 'en-US',\n    city: photoConfig?.city || 'Toronto',\n    language: photoConfig?.language || 'English',\n    pricing_level: photoConfig?.city || 'Toronto',\n    quality_stats: {\n      total: works.length,\n      high: qHigh,\n      medium: qMedium,\n      low: qLow,\n      not_found: qNotFound,\n      found_percent: foundPercent\n    },\n    validation: {\n      issues: validationIssues,\n      issues_count: validationIssues.length,\n      passed: validationIssues.length === 0\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "d3fe33db-d2a6-4217-a976-10aeab400af9",
      "name": "STAGE 9 HTML Report",
      "type": "n8n-nodes-base.code",
      "position": [
        32240,
        352
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// STAGE 9: PROFESSIONAL HTML REPORT - 9 LANGUAGES\n// Photo Cost Estimate Pro v2 - with validation section\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst input = $input.first().json;\n\nconst langCode = (input.language_code || 'EN').toUpperCase();\nconst currency = input.currency || 'EUR';\nconst currencySymbol = input.currency_symbol || '\u20ac';\nconst locale = input.locale || 'en-US';\nconst pricingLevel = input.pricing_level || input.city || 'Unknown';\nconst photoAnalysis = input.photo_analysis || {};\nconst validation = input.validation || {};\nconst projectName = photoAnalysis.description || 'Photo Cost Estimate';\n\nconst RATES_LINK = 'https://openconstructionestimate.com/all-estimates/?utm=OCE';\n\n// TRANSLATIONS - 9 LANGUAGES\nconst translations = {\n  'DE': { doc_title: 'KOSTENVORANSCHLAG', project: 'Projekt', pricing_level: 'Preisniveau', date: 'Datum', col_pos: 'Pos.', col_code: 'Kennziffer', col_description: 'Bezeichnung', col_calc: 'Berechnung', col_unit: 'Einheit', col_qty: 'Menge', col_price: 'EP', col_total: 'GP', col_labor: 'Std.', col_quality: 'Q', subtotal: 'Zwischensumme', grand_total: 'GESAMTSUMME', labor_cost: 'Lohnkosten', material_cost: 'Materialkosten', labor_days: 'Arbeitstage', found_rates: 'Gefunden', manual_check: 'pr\u00fcfen', kpi_total: 'Gesamtkosten', kpi_hours: 'Arbeitsstunden', kpi_days: 'Arbeitstage', chart_cost_structure: 'Kostenstruktur', chart_by_phase: 'Nach Phase', chart_labor: 'Lohn', chart_material: 'Material', chart_machines: 'Maschinen', chart_timeline: 'Zeitplan', chart_hierarchy: 'Kostenhierarchie', collapse_all: 'Alles einklappen', expand_all: 'Alles ausklappen', res_labor: 'Lohn', res_material: 'Mat.', res_machine: 'Masch.', photo_analysis: 'Fotoanalyse', location: 'Raumtyp', room_size: 'Raumgr\u00f6\u00dfe', work_phase: 'Arbeitstyp', materials: 'Materialien', validation: 'Validierung', validation_passed: 'Alle Pr\u00fcfungen bestanden', validation_issues: 'Hinweise' },\n  'EN': { doc_title: 'COST ESTIMATE', project: 'Project', pricing_level: 'Pricing Level', date: 'Date', col_pos: 'Pos.', col_code: 'Code', col_description: 'Description', col_calc: 'Calculation', col_unit: 'Unit', col_qty: 'Qty', col_price: 'UP', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q', subtotal: 'Subtotal', grand_total: 'GRAND TOTAL', labor_cost: 'Labor Cost', material_cost: 'Material Cost', labor_days: 'Work Days', found_rates: 'Found', manual_check: 'check', kpi_total: 'Total Cost', kpi_hours: 'Work Hours', kpi_days: 'Work Days', chart_cost_structure: 'Cost Structure', chart_by_phase: 'By Phase', chart_labor: 'Labor', chart_material: 'Material', chart_machines: 'Machines', chart_timeline: 'Timeline', chart_hierarchy: 'Cost Hierarchy', collapse_all: 'Collapse All', expand_all: 'Expand All', res_labor: 'Labor', res_material: 'Mat.', res_machine: 'Mach.', photo_analysis: 'Photo Analysis', location: 'Room Type', room_size: 'Room Size', work_phase: 'Work Type', materials: 'Materials', validation: 'Validation', validation_passed: 'All checks passed', validation_issues: 'Issues' },\n  'RU': { doc_title: '\u0421\u041c\u0415\u0422\u0410', project: '\u041f\u0440\u043e\u0435\u043a\u0442', pricing_level: '\u0423\u0440\u043e\u0432\u0435\u043d\u044c \u0446\u0435\u043d', date: '\u0414\u0430\u0442\u0430', col_pos: 'N', col_code: '\u0428\u0438\u0444\u0440', col_description: '\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435', col_calc: '\u0420\u0430\u0441\u0447\u0451\u0442', col_unit: '\u0415\u0434.', col_qty: '\u041a\u043e\u043b-\u0432\u043e', col_price: '\u0426\u0435\u043d\u0430', col_total: '\u0421\u0443\u043c\u043c\u0430', col_labor: '\u0427/\u0447', col_quality: '\u041a', subtotal: '\u0418\u0442\u043e\u0433\u043e', grand_total: '\u0412\u0421\u0415\u0413\u041e', labor_cost: '\u0422\u0440\u0443\u0434', material_cost: '\u041c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u044b', labor_days: '\u0414\u043d\u0435\u0439', found_rates: '\u041d\u0430\u0439\u0434\u0435\u043d\u043e', manual_check: '\u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c', kpi_total: '\u041e\u0431\u0449\u0430\u044f \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c', kpi_hours: '\u0427\u0430\u0441\u044b', kpi_days: '\u0414\u043d\u0438', chart_cost_structure: '\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0437\u0430\u0442\u0440\u0430\u0442', chart_by_phase: '\u041f\u043e \u044d\u0442\u0430\u043f\u0430\u043c', chart_labor: '\u0422\u0440\u0443\u0434', chart_material: '\u041c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u044b', chart_machines: '\u041c\u0430\u0448\u0438\u043d\u044b', chart_timeline: '\u0413\u0440\u0430\u0444\u0438\u043a', chart_hierarchy: '\u0418\u0435\u0440\u0430\u0440\u0445\u0438\u044f \u0437\u0430\u0442\u0440\u0430\u0442', collapse_all: '\u0421\u0432\u0435\u0440\u043d\u0443\u0442\u044c \u0432\u0441\u0451', expand_all: '\u0420\u0430\u0437\u0432\u0435\u0440\u043d\u0443\u0442\u044c \u0432\u0441\u0451', res_labor: '\u0422\u0440\u0443\u0434', res_material: '\u041c\u0430\u0442.', res_machine: '\u041c\u0430\u0448.', photo_analysis: '\u0410\u043d\u0430\u043b\u0438\u0437 \u0444\u043e\u0442\u043e', location: '\u0422\u0438\u043f \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u044f', room_size: '\u041f\u043b\u043e\u0449\u0430\u0434\u044c', work_phase: '\u0422\u0438\u043f \u0440\u0430\u0431\u043e\u0442', materials: '\u041c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u044b', validation: '\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430', validation_passed: '\u0412\u0441\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u044b', validation_issues: '\u0417\u0430\u043c\u0435\u0447\u0430\u043d\u0438\u044f' },\n  'FR': { doc_title: 'DEVIS ESTIMATIF', project: 'Projet', pricing_level: 'Niveau de prix', date: 'Date', col_pos: 'Pos.', col_code: 'Code', col_description: 'D\u00e9signation', col_calc: 'Calcul', col_unit: 'Unit\u00e9', col_qty: 'Qt\u00e9', col_price: 'PU', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q', subtotal: 'Sous-total', grand_total: 'TOTAL G\u00c9N\u00c9RAL', labor_cost: 'Main-d-oeuvre', material_cost: 'Mat\u00e9riaux', labor_days: 'Jours', found_rates: 'Trouv\u00e9s', manual_check: 'v\u00e9rifier', kpi_total: 'Co\u00fbt Total', kpi_hours: 'Heures', kpi_days: 'Jours', chart_cost_structure: 'Structure des co\u00fbts', chart_by_phase: 'Par phase', chart_labor: 'Main-d-oeuvre', chart_material: 'Mat\u00e9riaux', chart_machines: 'Machines', chart_timeline: 'Planning', chart_hierarchy: 'Hi\u00e9rarchie', collapse_all: 'Tout r\u00e9duire', expand_all: 'Tout d\u00e9velopper', res_labor: 'M.O.', res_material: 'Mat.', res_machine: 'Mach.', photo_analysis: 'Analyse photo', location: 'Type de pi\u00e8ce', room_size: 'Surface', work_phase: 'Type de travaux', materials: 'Mat\u00e9riaux', validation: 'Validation', validation_passed: 'Toutes les v\u00e9rifications pass\u00e9es', validation_issues: 'Remarques' },\n  'ES': { doc_title: 'PRESUPUESTO', project: 'Proyecto', pricing_level: 'Nivel de precios', date: 'Fecha', col_pos: 'Pos.', col_code: 'C\u00f3digo', col_description: 'Descripci\u00f3n', col_calc: 'C\u00e1lculo', col_unit: 'Unidad', col_qty: 'Cant.', col_price: 'P.U.', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q', subtotal: 'Subtotal', grand_total: 'TOTAL GENERAL', labor_cost: 'Mano obra', material_cost: 'Materiales', labor_days: 'D\u00edas', found_rates: 'Encontrados', manual_check: 'verificar', kpi_total: 'Coste Total', kpi_hours: 'Horas', kpi_days: 'D\u00edas', chart_cost_structure: 'Estructura de costes', chart_by_phase: 'Por fase', chart_labor: 'Mano obra', chart_material: 'Material', chart_machines: 'M\u00e1quinas', chart_timeline: 'Cronograma', chart_hierarchy: 'Jerarqu\u00eda', collapse_all: 'Contraer todo', expand_all: 'Expandir todo', res_labor: 'M.O.', res_material: 'Mat.', res_machine: 'M\u00e1q.', photo_analysis: 'An\u00e1lisis foto', location: 'Tipo de espacio', room_size: 'Superficie', work_phase: 'Tipo de obra', materials: 'Materiales', validation: 'Validaci\u00f3n', validation_passed: 'Todas las verificaciones pasaron', validation_issues: 'Observaciones' },\n  'PT': { doc_title: 'OR\u00c7AMENTO', project: 'Projeto', pricing_level: 'N\u00edvel de pre\u00e7os', date: 'Data', col_pos: 'Pos.', col_code: 'C\u00f3digo', col_description: 'Descri\u00e7\u00e3o', col_calc: 'C\u00e1lculo', col_unit: 'Unidade', col_qty: 'Qtd.', col_price: 'P.U.', col_total: 'Total', col_labor: 'Hrs', col_quality: 'Q', subtotal: 'Subtotal', grand_total: 'TOTAL GERAL', labor_cost: 'M\u00e3o obra', material_cost: 'Materiais', labor_days: 'Dias', found_rates: 'Encontrados', manual_check: 'verificar', kpi_total: 'Custo Total', kpi_hours: 'Horas', kpi_days: 'Dias', chart_cost_structure: 'Estrutura de custos', chart_by_phase: 'Por fase', chart_labor: 'M\u00e3o obra', chart_material: 'Material', chart_machines: 'M\u00e1quinas', chart_timeline: 'Cronograma', chart_hierarchy: 'Hierarquia', collapse_all: 'Recolher tudo', expand_all: 'Expandir tudo', res_labor: 'M.O.', res_material: 'Mat.', res_machine: 'M\u00e1q.', photo_analysis: 'An\u00e1lise foto', location: 'Tipo de espa\u00e7o', room_size: '\u00c1rea', work_phase: 'Tipo de obra', materials: 'Materiais', validation: 'Valida\u00e7\u00e3o', validation_passed: 'Todas as verifica\u00e7\u00f5es passaram', validation_issues: 'Observa\u00e7\u00f5es' },\n  'ZH': { doc_title: '\u5de5\u7a0b\u9884\u7b97', project: '\u9879\u76ee', pricing_level: '\u4ef7\u683c\u6c34\u5e73', date: '\u65e5\u671f', col_pos: '\u5e8f\u53f7', col_code: '\u7f16\u7801', col_description: '\u540d\u79f0', col_calc: '\u8ba1\u7b97', col_unit: '\u5355\u4f4d', col_qty: '\u6570\u91cf', col_price: '\u5355\u4ef7', col_total: '\u5408\u8ba1', col_labor: '\u5de5\u65f6', col_quality: '\u8d28', subtotal: '\u5c0f\u8ba1', grand_total: '\u603b\u8ba1', labor_cost: '\u4eba\u5de5\u8d39', material_cost: '\u6750\u6599\u8d39', labor_days: '\u5de5\u4f5c\u65e5', found_rates: '\u5df2\u627e\u5230', manual_check: '\u6838\u67e5', kpi_total: '\u603b\u6210\u672c', kpi_hours: '\u5de5\u65f6', kpi_days: '\u5de5\u4f5c\u65e5', chart_cost_structure: '\u6210\u672c\u7ed3\u6784', chart_by_phase: '\u6309\u9636\u6bb5', chart_labor: '\u4eba\u5de5', chart_material: '\u6750\u6599', chart_machines: '\u673a\u68b0', chart_timeline: '\u8fdb\u5ea6', chart_hierarchy: '\u5c42\u6b21', collapse_all: '\u5168\u90e8\u6298\u53e0', expand_all: '\u5168\u90e8\u5c55\u5f00', res_labor: '\u4eba\u5de5', res_material: '\u6750\u6599', res_machine: '\u673a\u68b0', photo_analysis: '\u7167\u7247\u5206\u6790', location: '\u623f\u95f4\u7c7b\u578b', room_size: '\u9762\u79ef', work_phase: '\u65bd\u5de5\u7c7b\u578b', materials: '\u6750\u6599', validation: '\u9a8c\u8bc1', validation_passed: '\u6240\u6709\u68c0\u67e5\u901a\u8fc7', validation_issues: '\u95ee\u9898' },\n  'AR': { doc_title: '\u062a\u0642\u062f\u064a\u0631 \u0627\u0644\u062a\u0643\u0644\u0641\u0629', project: '\u0627\u0644\u0645\u0634\u0631\u0648\u0639', pricing_level: '\u0645\u0633\u062a\u0648\u0649 \u0627\u0644\u0623\u0633\u0639\u0627\u0631', date: '\u0627\u0644\u062a\u0627\u0631\u064a\u062e', col_pos: '\u0631\u0642\u0645', col_code: '\u0627\u0644\u0631\u0645\u0632', col_description: '\u0627\u0644\u0648\u0635\u0641', col_calc: '\u0627\u0644\u062d\u0633\u0627\u0628', col_unit: '\u0627\u0644\u0648\u062d\u062f\u0629', col_qty: '\u0627\u0644\u0643\u0645\u064a\u0629', col_price: '\u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629', col_total: '\u0627\u0644\u0645\u062c\u0645\u0648\u0639', col_labor: '\u0633\u0627\u0639\u0627\u062a', col_quality: '\u062c', subtotal: '\u0627\u0644\u0645\u062c\u0645\u0648\u0639 \u0627\u0644\u0641\u0631\u0639\u064a', grand_total: '\u0627\u0644\u0645\u062c\u0645\u0648\u0639 \u0627\u0644\u0643\u0644\u064a', labor_cost: '\u062a\u0643\u0644\u0641\u0629 \u0627\u0644\u0639\u0645\u0627\u0644\u0629', material_cost: '\u062a\u0643\u0644\u0641\u0629 \u0627\u0644\u0645\u0648\u0627\u062f', labor_days: '\u0623\u064a\u0627\u0645 \u0627\u0644\u0639\u0645\u0644', found_rates: '\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631', manual_check: '\u062a\u062d\u0642\u0642', kpi_total: '\u0627\u0644\u062a\u0643\u0644\u0641\u0629 \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a\u0629', kpi_hours: '\u0633\u0627\u0639\u0627\u062a \u0627\u0644\u0639\u0645\u0644', kpi_days: '\u0623\u064a\u0627\u0645 \u0627\u0644\u0639\u0645\u0644', chart_cost_structure: '\u0647\u064a\u0643\u0644 \u0627\u0644\u062a\u0643\u0644\u0641\u0629', chart_by_phase: '\u062d\u0633\u0628 \u0627\u0644\u0645\u0631\u062d\u0644\u0629', chart_labor: '\u0627\u0644\u0639\u0645\u0627\u0644\u0629', chart_material: '\u0627\u0644\u0645\u0648\u0627\u062f', chart_machines: '\u0627\u0644\u0645\u0639\u062f\u0627\u062a', chart_timeline: '\u0627\u0644\u062c\u062f\u0648\u0644 \u0627\u0644\u0632\u0645\u0646\u064a', chart_hierarchy: '\u062a\u0633\u0644\u0633\u0644 \u0627\u0644\u062a\u0643\u0627\u0644\u064a\u0641', collapse_all: '\u0637\u064a \u0627\u0644\u0643\u0644', expand_all: '\u062a\u0648\u0633\u064a\u0639 \u0627\u0644\u0643\u0644', res_labor: '\u0639\u0645\u0627\u0644\u0629', res_material: '\u0645\u0648\u0627\u062f', res_machine: '\u0645\u0639\u062f\u0627\u062a', photo_analysis: '\u062a\u062d\u0644\u064a\u0644 \u0627\u0644\u0635\u0648\u0631\u0629', location: '\u0646\u0648\u0639 \u0627\u0644\u063a\u0631\u0641\u0629', room_size: '\u0627\u0644\u0645\u0633\u0627\u062d\u0629', work_phase: '\u0646\u0648\u0639 \u0627\u0644\u0639\u0645\u0644', materials: '\u0627\u0644\u0645\u0648\u0627\u062f', validation: '\u0627\u0644\u062a\u062d\u0642\u0642', validation_passed: '\u062c\u0645\u064a\u0639 \u0627\u0644\u0641\u062d\u0648\u0635\u0627\u062a \u0646\u0627\u062c\u062d\u0629', validation_issues: '\u0645\u0644\u0627\u062d\u0638\u0627\u062a' },\n  'HI': { doc_title: '\u0932\u093e\u0917\u0924 \u0905\u0928\u0941\u092e\u093e\u0928', project: '\u092a\u0930\u093f\u092f\u094b\u091c\u0928\u093e', pricing_level: '\u092e\u0942\u0932\u094d\u092f \u0938\u094d\u0924\u0930', date: '\u0924\u093e\u0930\u0940\u0916', col_pos: '\u0915\u094d\u0930\u092e', col_code: '\u0915\u094b\u0921', col_description: '\u0935\u093f\u0935\u0930\u0923', col_calc: '\u0917\u0923\u0928\u093e', col_unit: '\u0907\u0915\u093e\u0908', col_qty: '\u092e\u093e\u0924\u094d\u0930\u093e', col_price: '\u0926\u0930', col_total: '\u0915\u0941\u0932', col_labor: '\u0918\u0902\u091f\u0947', col_quality: '\u0917\u0941', subtotal: '\u0909\u092a-\u092f\u094b\u0917', grand_total: '\u0915\u0941\u0932 \u092f\u094b\u0917', labor_cost: '\u0936\u094d\u0930\u092e \u0932\u093e\u0917\u0924', material_cost: '\u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u0932\u093e\u0917\u0924', labor_days: '\u0915\u093e\u0930\u094d\u092f \u0926\u093f\u0935\u0938', found_rates: '\u092e\u093f\u0932\u093e', manual_check: '\u091c\u093e\u0901\u091a\u0947\u0902', kpi_total: '\u0915\u0941\u0932 \u0932\u093e\u0917\u0924', kpi_hours: '\u0915\u093e\u0930\u094d\u092f \u0918\u0902\u091f\u0947', kpi_days: '\u0915\u093e\u0930\u094d\u092f \u0926\u093f\u0935\u0938', chart_cost_structure: '\u0932\u093e\u0917\u0924 \u0938\u0902\u0930\u091a\u0928\u093e', chart_by_phase: '\u091a\u0930\u0923 \u0905\u0928\u0941\u0938\u093e\u0930', chart_labor: '\u0936\u094d\u0930\u092e', chart_material: '\u0938\u093e\u092e\u0917\u094d\u0930\u0940', chart_machines: '\u092e\u0936\u0940\u0928\u0947\u0902', chart_timeline: '\u0938\u092e\u092f\u0930\u0947\u0916\u093e', chart_hierarchy: '\u092a\u0926\u093e\u0928\u0941\u0915\u094d\u0930\u092e', collapse_all: '\u0938\u092d\u0940 \u0938\u0902\u0915\u094d\u0937\u093f\u092a\u094d\u0924 \u0915\u0930\u0947\u0902', expand_all: '\u0938\u092d\u0940 \u0935\u093f\u0938\u094d\u0924\u0943\u0924 \u0915\u0930\u0947\u0902', res_labor: '\u0936\u094d\u0930\u092e', res_material: '\u0938\u093e\u092e\u0917\u094d\u0930\u0940', res_machine: '\u092e\u0936\u0940\u0928', photo_analysis: '\u092b\u094b\u091f\u094b \u0935\u093f\u0936\u094d\u0932\u0947\u0937\u0923', location: '\u0915\u092e\u0930\u0947 \u0915\u093e \u092a\u094d\u0930\u0915\u093e\u0930', room_size: '\u0915\u094d\u0937\u0947\u0924\u094d\u0930\u092b\u0932', work_phase: '\u0915\u093e\u0930\u094d\u092f \u092a\u094d\u0930\u0915\u093e\u0930', materials: '\u0938\u093e\u092e\u0917\u094d\u0930\u0940', validation: '\u0938\u0924\u094d\u092f\u093e\u092a\u0928', validation_passed: '\u0938\u092d\u0940 \u091c\u093e\u0901\u091a \u092a\u093e\u0938', validation_issues: '\u092e\u0941\u0926\u094d\u0926\u0947' }\n};\nconst t = translations[langCode] || translations['EN'];\n\n// HELPERS\nfunction formatCurrency(value) {\n  if (value === null || value === undefined || isNaN(value)) return '\u2014';\n  try { return new Intl.NumberFormat(locale, { style: 'currency', currency: currency, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value); }\n  catch (e) { return currencySymbol + value.toFixed(2); }\n}\nfunction formatNumber(value, decimals) {\n  if (decimals === undefined) decimals = 3;\n  if (value === null || value === undefined || isNaN(value)) return '\u2014';\n  try { return new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(value); }\n  catch (e) { return value.toFixed(decimals); }\n}\nfunction formatDateTime() {\n  try { return new Intl.DateTimeFormat(locale, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).format(new Date()); }\n  catch (e) { return new Date().toISOString().split('T')[0]; }\n}\nfunction escapeHtml(text) {\n  if (!text) return '';\n  return String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n}\n\n// DATA\nconst phases = input.by_phase || [];\nconst costSummary = input.cost_summary || {};\nconst qualityStats = input.quality_stats || {};\nconst grandTotal = costSummary.grand_total_cost || 0;\nconst grandLaborHours = costSummary.grand_labor_hours || 0;\nconst grandResourceCost = costSummary.grand_resource_cost || 0;\nconst grandMaterialCost = costSummary.grand_material_cost || 0;\nconst grandMachineCost = costSummary.grand_machine_cost || 0;\n\nconst totalWorks = qualityStats.total || 0;\nconst qualityHigh = qualityStats.high || 0;\nconst qualityMedium = qualityStats.medium || 0;\nconst qualityLow = qualityStats.low || 0;\nconst qualityNotFound = qualityStats.not_found || 0;\nconst foundRates = qualityHigh + qualityMedium + qualityLow;\nconst foundPercent = qualityStats.found_percent || 0;\n\nconst laborDays = Math.ceil(grandLaborHours / 8);\nconst laborPercent = grandTotal > 0 ? Math.round(grandResourceCost / grandTotal * 100) : 35;\nconst materialPercent = grandTotal > 0 ? Math.round(grandMaterialCost / grandTotal * 100) : 55;\nconst machinesPercent = grandTotal > 0 ? Math.round(grandMachineCost / grandTotal * 100) : 10;\n\nconst dateTimeStr = formatDateTime();\n\nconst phaseChartData = phases.map(function(phase) { return { name: phase.phase_name || 'Phase', cost: phase.phase_total_cost || 0, hours: phase.phase_labor_hours || 0 }; });\n\n// HTML GENERATION\nvar html = `<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>${t.doc_title} - ${escapeHtml(projectName)}</title>\n<style>\n:root { --primary: #007AFF; --primary-light: #5AC8FA; --primary-dark: #0051D5; --text-primary: #1D1D1F; --text-secondary: #86868B; --text-muted: #AEAEB2; --bg-white: #FFFFFF; --bg-light: #F5F5F7; --bg-medium: #E8E8ED; --border: #D2D2D7; --success: #34C759; --warning: #FF9500; --error: #FF3B30; }\nbody { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif; margin: 0; padding: 20px; background: #F5F5F7; color: var(--text-primary); line-height: 1.5; font-size: 11px; }\n.container { background: var(--bg-white); max-width: 1350px; margin: 0 auto; box-shadow: 0 4px 20px rgba(0,0,0,0.08); border-radius: 20px; overflow: hidden; }\ntable { border-collapse: collapse; width: 100%; }\ntd, th { padding: 8px 10px; text-align: left; vertical-align: middle; }\n.header-new { background: var(--bg-white); color: var(--text-primary); display: flex; justify-content: space-between; align-items: center; padding: 14px 20px; border-bottom: 1px solid var(--border); }\n.header-left { display: flex; flex-direction: column; }\n.header-title-new { font-size: 18px; font-weight: 600; letter-spacing: -0.3px; color: var(--text-primary); }\n.header-project-new { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-top: 2px; }\n.header-info-new { font-size: 11px; font-weight: 400; color: var(--text-muted); margin-top: 2px; }\n.header-right { display: flex; align-items: center; gap: 12px; }\n.header-logo { height: 32px; opacity: 0.9; }\n.header-logo:hover { opacity: 1; }\n.header-btn { display: inline-flex; align-items: center; gap: 5px; padding: 7px 12px; background: var(--bg-light); border: 1px solid var(--border); border-radius: 16px; color: var(--text-secondary); text-decoration: none; font-size: 11px; font-weight: 500; transition: all 0.2s; }\n.header-btn:hover { background: var(--bg-medium); color: var(--text-primary); }\n.header-btn svg { width: 14px; height: 14px; }\n.charts-section { background: var(--bg-light); padding: 16px 20px; }\n.chart-card { background: var(--bg-white); border-radius: 10px; padding: 14px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); border: 1px solid var(--border); }\n.chart-title { font-size: 10px; font-weight: 600; color: var(--text-muted); margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.4px; }\n.row-1 { display: grid; grid-template-columns: auto 1fr; gap: 12px; margin-bottom: 12px; }\n.kpi-row { display: flex; gap: 6px; }\n.kpi-card { background: var(--bg-white); border-radius: 8px; padding: 10px 14px; display: flex; align-items: center; gap: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); border: 1px solid var(--border); white-space: nowrap; }\n.kpi-icon { font-size: 16px; opacity: 0.5; }\n.kpi-value { font-size: 16px; font-weight: 600; letter-spacing: -0.3px; color: var(--text-primary); line-height: 1.1; }\n.kpi-label { font-size: 8px; color: var(--text-muted); font-weight: 500; text-transform: uppercase; letter-spacing: 0.2px; }\n.row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }\n.row-3 { display: grid; grid-template-columns: 1fr; gap: 12px; }\n.h-bar { margin-bottom: 8px; }\n.h-bar-label { display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 3px; color: var(--text-primary); }\n.h-bar-label span:last-child { font-weight: 600; }\n.h-bar-track { background: var(--bg-medium); border-radius: 2px; height: 5px; overflow: hidden; display: flex; }\n.h-bar-fill { height: 100%; }\n.treemap { display: flex; gap: 6px; height: 60px; }\n.treemap-item { border-radius: 8px; padding: 8px 10px; background: var(--bg-light); color: var(--text-primary); display: flex; flex-direction: column; justify-content: space-between; border: 1px solid var(--border); min-width: 0; cursor: pointer; transition: all 0.15s; }\n.treemap-item:hover { background: var(--bg-medium); border-color: var(--text-secondary); transform: translateY(-1px); }\n.treemap-name { font-size: 9px; color: var(--text-muted); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.treemap-value { font-size: 12px; font-weight: 600; color: var(--text-primary); }\n.treemap-percent { font-size: 9px; color: var(--text-secondary); }\n.timeline-row { display: flex; align-items: center; margin-bottom: 8px; }\n.timeline-label { width: 120px; font-size: 11px; font-weight: 500; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.timeline-track { flex: 1; height: 18px; background: var(--bg-medium); border-radius: 4px; position: relative; overflow: hidden; }\n.timeline-bar { position: absolute; height: 100%; border-radius: 4px; font-size: 10px; color: white; display: flex; align-items: center; justify-content: center; font-weight: 500; background: #1D1D1F; min-width: 4px; }\n.quality-bar { background: var(--bg-medium); padding: 8px 16px; font-size: 10px; color: var(--text-secondary); display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }\n.quality-item { display: flex; align-items: center; gap: 4px; }\n.quality-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }\n.dot-high { background: #1D1D1F; }\n.dot-medium { background: #86868B; }\n.dot-low { background: #AEAEB2; }\n.dot-notfound { background: #D2D2D7; }\n.quality-percent { font-weight: 600; color: var(--text-primary); }\n.table-controls { display: flex; gap: 8px; padding: 10px 16px; background: var(--bg-light); border-bottom: 1px solid var(--border); }\n.control-btn { padding: 6px 12px; background: var(--bg-white); border: 1px solid var(--border); border-radius: 6px; font-size: 11px; font-weight: 500; color: var(--text-secondary); cursor: pointer; transition: all 0.2s; }\n.control-btn:hover { background: var(--bg-medium); color: var(--text-primary); }\n.col-header { background: var(--bg-medium); color: var(--text-secondary); font-weight: 600; font-size: 9px; text-transform: uppercase; letter-spacing: 0.5px; text-align: center; padding: 8px 6px; border-bottom: 2px solid var(--border); }\n.phase { background: var(--primary); color: #FFFFFF; font-weight: 600; font-size: 11px; cursor: pointer; }\n.phase:hover { opacity: 0.95; }\n.type { background: #EFF6FF; color: var(--text-primary); font-weight: 600; font-size: 10px; border-left: 3px solid var(--primary); cursor: pointer; }\n.type:hover { opacity: 0.95; }\n.work { background: var(--bg-white); color: var(--text-primary); font-size: 10px; border-bottom: 1px solid var(--bg-medium); cursor: pointer; }\n.work:hover { background: #FAFAFA; }\n.work-demolition { background: #FEF3C7; }\n.work-demolition:hover { background: #FDE68A; }\n.resource { background: var(--bg-light); color: var(--text-muted); font-size: 9px; border-bottom: 1px solid var(--bg-medium); }\n.subtotal { background: var(--bg-light); color: var(--text-primary); font-weight: 600; font-size: 12px; border-top: 1px solid var(--border); }\n.grand-total { background: var(--text-primary); color: #FFFFFF; font-weight: 600; font-size: 14px; }\n.grand-total-info { background: var(--primary-dark); color: rgba(255,255,255,0.9); font-size: 10px; }\n.highlight { color: #FCD34D; font-weight: 600; }\n.right { text-align: right; }\n.center { text-align: center; }\n.num { font-variant-numeric: tabular-nums; }\n.link { color: var(--primary); text-decoration: none; }\n.link:hover { text-decoration: underline; }\n.toggle-icon { display: inline-block; width: 16px; transition: transform 0.2s; cursor: pointer; }\ntr.hidden { display: none; }\n.calc-cell { font-size: 9px; color: var(--text-muted); line-height: 1.4; max-width: 200px; }\n.calc-type { color: var(--primary); font-weight: 600; font-size: 9px; display: block; margin-bottom: 2px; }\n.calc-formula { font-family: monospace; font-size: 8px; color: var(--text-secondary); background: #F0F9FF; border: 1px solid #BAE6FD; padding: 3px 6px; border-radius: 4px; display: block; }\n.res-tag { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 8px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; margin-left: 4px; }\n.res-tag-labor { background: #1D1D1F; color: white; }\n.res-tag-material { background: #86868B; color: white; }\n.res-tag-machine { background: #AEAEB2; color: white; }\n.photo-info { padding: 12px 20px; background: #EFF6FF; border-bottom: 1px solid var(--border); font-size: 11px; }\n.photo-info-title { font-weight: 600; color: var(--primary-dark); margin-bottom: 6px; }\n.photo-info-item { display: inline-block; margin-right: 16px; color: var(--text-secondary); }\n.photo-info-item strong { color: var(--text-primary); }\n.validation-bar { padding: 10px 20px; font-size: 11px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }\n.validation-passed { background: #D1FAE5; color: #065F46; }\n.validation-issues { background: #FEF3C7; color: #92400E; }\n.validation-icon { font-size: 14px; }\n.validation-text { font-weight: 500; }\n.validation-issue { background: rgba(0,0,0,0.05); padding: 4px 8px; border-radius: 4px; font-size: 10px; }\n.footer { background: var(--bg-medium); padding: 10px 16px; border-top: 1px solid var(--border); }\n.footer-main { display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; }\n.footer-brand { font-weight: 700; font-size: 10px; color: var(--primary); white-space: nowrap; }\n.footer-info { font-size: 9px; color: var(--text-muted); flex: 1; min-width: 200px; }\n.footer-links { display: flex; gap: 10px; font-size: 9px; }\n.footer-links a { color: var(--primary); text-decoration: none; white-space: nowrap; }\n.footer-db { font-size: 8px; color: var(--text-muted); margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border); }\n@media print { body { padding: 0; background: white; } .container { box-shadow: none; } }\n</style>\n</head>\n<body>\n<div class=\"container\">\n`;\n\n// HEADER\nhtml += '<div class=\"header-new\"><div class=\"header-left\"><div class=\"header-title-new\">' + t.doc_title + ' v2</div><div class=\"header-project-new\">' + escapeHtml(projectName) + '</div><div class=\"header-info-new\">' + t.pricing_level + ': ' + pricingLevel + ' | ' + dateTimeStr + '</div></div><div class=\"header-right\"><a href=\"https://github.com/datadrivenconstruction\" target=\"_blank\" class=\"header-btn\"><svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.+123456789066 1.983-.399 3.+123456789038 3.+12345678907-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z\"/></svg>GitHub</a><a href=\"https://datadrivenconstruction.io/\" target=\"_blank\"><img src=\"https://datadrivenconstruction.io/wp-content/uploads/2023/07/DataDrivenConstruction-1-1.png\" alt=\"DDC\" class=\"header-logo\"></a></div></div>\\n';\n\n// PHOTO INFO\nhtml += '<div class=\"photo-info\"><div class=\"photo-info-title\">\ud83d\udcf7 ' + t.photo_analysis + '</div><span class=\"photo-info-item\"><strong>' + t.location + ':</strong> ' + escapeHtml(photoAnalysis.room_type || 'N/A') + '</span><span class=\"photo-info-item\"><strong>' + t.room_size + ':</strong> ' + escapeHtml(photoAnalysis.room_size || 'N/A') + '</span><span class=\"photo-info-item\"><strong>' + t.work_phase + ':</strong> ' + escapeHtml(photoAnalysis.work_phase || 'N/A') + '</span><span class=\"photo-info-item\"><strong>Elements:</strong> ' + (photoAnalysis.elements_count || 0) + '</span><span class=\"photo-info-item\"><strong>Fixtures:</strong> ' + (photoAnalysis.fixtures_count || 0) + '</span>';\nif (photoAnalysis.materials && photoAnalysis.materials.length > 0) {\n  html += '<span class=\"photo-info-item\"><strong>' + t.materials + ':</strong> ' + escapeHtml(photoAnalysis.materials.slice(0, 5).join(', ')) + '</span>';\n}\nhtml += '</div>\\n';\n\n// VALIDATION BAR\nif (validation.passed) {\n  html += '<div class=\"validation-bar validation-passed\"><span class=\"validation-icon\">\u2705</span><span class=\"validation-text\">' + t.validation_passed + '</span></div>\\n';\n} else if (validation.issues && validation.issues.length > 0) {\n  html += '<div class=\"validation-bar validation-issues\"><span class=\"validation-icon\">\u26a0\ufe0f</span><span class=\"validation-text\">' + t.validation_issues + ':</span>';\n  validation.issues.forEach(function(issue) {\n    html += '<span class=\"validation-issue\">' + escapeHtml(issue) + '</span>';\n  });\n  html += '</div>\\n';\n}\n\n// CHARTS SECTION\nhtml += '<div class=\"charts-section\"><div class=\"row-1\"><div class=\"kpi-row\"><div class=\"kpi-card\"><div class=\"kpi-icon\">\ud83d\udcb0</div><div class=\"kpi-content\"><div class=\"kpi-value\">' + formatCurrency(grandTotal) + '</div><div class=\"kpi-label\">' + t.kpi_total + '</div></div></div><div class=\"kpi-card\"><div class=\"kpi-icon\">\u23f1\ufe0f</div><div class=\"kpi-content\"><div class=\"kpi-value\">' + formatNumber(grandLaborHours, 0) + ' h</div><div class=\"kpi-label\">' + t.kpi_hours + '</div></div></div><div class=\"kpi-card\"><div class=\"kpi-icon\">\ud83d\udcc5</div><div class=\"kpi-content\"><div class=\"kpi-value\">' + laborDays + '</div><div class=\"kpi-label\">' + t.kpi_days + '</div></div></div></div><div class=\"chart-card\"><div class=\"chart-title\">' + t.chart_cost_structure + '</div><div class=\"h-bar\" style=\"margin-bottom:10px;\"><div class=\"h-bar-track\" style=\"height:8px;\"><div class=\"h-bar-fill\" style=\"width:' + laborPercent + '%;background:#1D1D1F;\"></div><div class=\"h-bar-fill\" style=\"width:' + materialPercent + '%;background:#86868B;\"></div><div class=\"h-bar-fill\" style=\"width:' + machinesPercent + '%;background:#AEAEB2;\"></div></div></div><div style=\"display:flex;flex-direction:column;gap:5px;font-size:10px;\"><div style=\"display:flex;justify-content:space-between;\"><span><span style=\"display:inline-block;width:8px;height:8px;background:#1D1D1F;border-radius:2px;margin-right:6px;\"></span>' + t.chart_labor + '</span><span style=\"font-weight:600;\">' + formatCurrency(grandResourceCost) + ' (' + laborPercent + '%)</span></div><div style=\"display:flex;justify-content:space-between;\"><span><span style=\"display:inline-block;width:8px;height:8px;background:#86868B;border-radius:2px;margin-right:6px;\"></span>' + t.chart_material + '</span><span style=\"font-weight:600;\">' + formatCurrency(grandMaterialCost) + ' (' + materialPercent + '%)</span></div><div style=\"display:flex;justify-content:space-between;\"><span><span style=\"display:inline-block;width:8px;height:8px;background:#AEAEB2;border-radius:2px;margin-right:6px;\"></span>' + t.chart_machines + '</span><span style=\"font-weight:600;\">' + formatCurrency(grandMachineCost) + ' (' + machinesPercent + '%)</span></div></div></div></div>';\n\n// Phase chart\nvar phaseHtml = '';\nfor (var i = 0; i < phaseChartData.length; i++) {\n  var p = phaseChartData[i];\n  var pct = grandTotal > 0 ? Math.round(p.cost / grandTotal * 100) : 0;\n  phaseHtml += '<div class=\"h-bar\"><div class=\"h-bar-label\"><span>' + escapeHtml(p.name) + '</span><span>' + formatCurrency(p.cost) + '</span></div><div class=\"h-bar-track\"><div class=\"h-bar-fill\" style=\"width:' + pct + '%;background:#1D1D1F;\"></div></div></div>';\n}\n\n// Timeline chart\nvar timelineHtml = '';\nfor (var i = 0; i < phaseChartData.length; i++) {\n  var p = phaseChartData[i];\n  var phaseDays = Math.ceil(p.hours / 8);\n  var widthPct = laborDays > 0 ? Math.min(phaseDays / laborDays * 100, 100) : 100;\n  timelineHtml += '<div class=\"timeline-row\"><div class=\"timeline-label\">' + escapeHtml(p.name) + '</div><div class=\"timeline-track\"><div class=\"timeline-bar\" style=\"left:0;width:' + widthPct + '%;\">' + phaseDays + 'd</div></div></div>';\n}\n\nhtml += '<div class=\"row-2\"><div class=\"chart-card\"><div class=\"chart-title\">' + t.chart_by_phase + '</div>' + phaseHtml + '</div><div class=\"chart-card\"><div class=\"chart-title\">' + t.chart_timeline + '</div>' + timelineHtml + '</div></div>';\n\n// Treemap\nvar treemapHtml = '';\nfor (var i = 0; i < phaseChartData.length; i++) {\n  var p = phaseChartData[i];\n  var pct = grandTotal > 0 ? Math.round(p.cost / grandTotal * 100) : 0;\n  treemapHtml += '<div class=\"treemap-item\" data-phase=\"' + (i+1) + '\" style=\"flex:' + (pct || 1) + ';\"><div class=\"treemap-name\">' + escapeHtml(p.name) + '</div><div class=\"treemap-value\">' + formatCurrency(p.cost) + '</div><div class=\"treemap-percent\">' + pct + '%</div></div>';\n}\nhtml += '<div class=\"row-3\"><div class=\"chart-card\"><div class=\"chart-title\">' + t.chart_hierarchy + '</div><div class=\"treemap\">' + treemapHtml + '</div></div></div></div>\\n';\n\n// TABLE CONTROLS\nhtml += '<div class=\"table-controls\"><button class=\"control-btn\" onclick=\"collapseAll()\">' + t.collapse_all + '</button><button class=\"control-btn\" onclick=\"expandAll()\">' + t.expand_all + '</button></div>\\n';\n\n// QUALITY BAR\nhtml += '<div class=\"quality-bar\"><div class=\"quality-item\"><span>' + t.found_rates + ':</span><span class=\"quality-percent\">' + foundPercent + '%</span><span>(' + foundRates + '/' + totalWorks + ')</span></div><div class=\"quality-item\"><span class=\"quality-dot dot-high\"></span><span>' + qualityHigh + '</span></div><div class=\"quality-item\"><span class=\"quality-dot dot-medium\"></span><span>' + qualityMedium + '</span></div><div class=\"quality-item\"><span class=\"quality-dot dot-low\"></span><span>' + qualityLow + '</span></div><div class=\"quality-item\"><span class=\"quality-dot dot-notfound\"></span><span>' + qualityNotFound + '</span></div>';\nif (qualityNotFound > 0) {\n  html += '<div class=\"quality-item\" style=\"color:var(--warning)\">\u26a0 ' + qualityNotFound + ' ' + t.manual_check + '</div>';\n}\nhtml += '</div>\\n';\n\n// TABLE\nhtml += '<table><tr class=\"col-header\"><td style=\"width:4%\">' + t.col_pos + '</td><td style=\"width:9%\">' + t.col_code + '</td><td style=\"width:24%\">' + t.col_description + '</td><td style=\"width:16%\">' + t.col_calc + '</td><td style=\"width:6%\">' + t.col_unit + '</td><td style=\"width:7%\" class=\"right\">' + t.col_qty + '</td><td style=\"width:9%\" class=\"right\">' + t.col_price + '</td><td style=\"width:9%\" class=\"right\">' + t.col_total + '</td><td style=\"width:5%\" class=\"right\">' + t.col_labor + '</td><td style=\"width:4%\" class=\"center\">' + t.col_quality + '</td></tr>';\n\n// DATA ROWS\nvar globalWorkIndex = 0;\nfor (var phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {\n  var phase = phases[phaseIdx];\n  var phaseNum = phaseIdx + 1;\n  var phasePercent = grandTotal > 0 ? ((phase.phase_total_cost || 0) / grandTotal * 100).toFixed(0) : '0';\n  \n  html += '<tr class=\"phase\" id=\"phase-' + phaseNum + '\" data-phase=\"' + phaseNum + '\"><td>' + phaseNum + '</td><td></td><td colspan=\"2\"><span class=\"toggle-icon\">\u25bc</span> ' + escapeHtml(phase.phase_name || 'Phase') + '</td><td></td><td></td><td></td><td class=\"right num\">' + formatCurrency(phase.phase_total_cost || 0) + '</td><td class=\"right num\">' + formatNumber(phase.phase_labor_hours || 0, 1) + '</td><td class=\"center\">' + phasePercent + '%</td></tr>';\n\n  var types = phase.types || [];\n  for (var typeIdx = 0; typeIdx < types.length; typeIdx++) {\n    var type = types[typeIdx];\n    var typeNum = typeIdx + 1;\n    var typeName = type.type_name || 'Unknown Type';\n    var elementCount = type.element_count || 1;\n    var category = type.category || '';\n    \n    html += '<tr class=\"type\" data-phase=\"' + phaseNum + '\" data-type=\"' + typeNum + '\"><td>' + phaseNum + '.' + typeNum + '</td><td></td><td><span class=\"toggle-icon\">\u25bd</span> ' + escapeHtml(typeName) + ' <span style=\"color:var(--text-muted)\">(\u00d7' + elementCount + ')</span></td><td class=\"calc-cell\"><span class=\"calc-type\">' + escapeHtml(category) + '</span></td><td></td><td></td><td></td><td class=\"right num\">' + formatCurrency(type.type_total_cost || 0) + '</td><td class=\"right num\">' + formatNumber(type.type_total_labor_hours || 0, 1) + '</td><td></td></tr>';\n\n    var works = type.works || [];\n    for (var workIdx = 0; workIdx < works.length; workIdx++) {\n      var work = works[workIdx];\n      var workNum = workIdx + 1;\n      globalWorkIndex++;\n      var level = work.quality_level || 'not_found';\n      var dotClass = 'dot-notfound';\n      if (level === 'high') dotClass = 'dot-high';\n      else if (level === 'medium') dotClass = 'dot-medium';\n      else if (level === 'low') dotClass = 'dot-low';\n      \n      var rateCode = work.rate_code || '';\n      var codeLink = (rateCode && rateCode !== 'NOT_FOUND') ? '<a href=\"' + RATES_LINK + '\" class=\"link\" target=\"_blank\">' + escapeHtml(rateCode) + '</a>' : '\u2014';\n      \n      var calcDetails = work.calculation_details || {};\n      var calcDisplay = '<span class=\"calc-type\">' + escapeHtml(work.calculation_basis || work.source_element || '') + '</span>';\n      if (calcDetails.formula_display) {\n        calcDisplay += '<span class=\"calc-formula\">' + escapeHtml(calcDetails.formula_display) + '</span>';\n      }\n      \n      var workRowClass = 'work';\n      if (work.is_demolition) workRowClass += ' work-demolition';\n      \n      html += '<tr class=\"' + workRowClass + '\" data-phase=\"' + phaseNum + '\" data-type=\"' + typeNum + '\" data-work=\"' + globalWorkIndex + '\"><td style=\"padding-left:16px\">' + phaseNum + '.' + typeNum + '.' + workNum + '</td><td>' + codeLink + '</td><td style=\"padding-left:24px\"><span class=\"toggle-icon\">\u25bf</span> ' + (work.is_demolition ? '\ud83d\udd28 ' : '') + escapeHtml(work.rate_name || work.work_name || 'Work') + '</td><td class=\"calc-cell\">' + calcDisplay + '</td><td>' + escapeHtml(work.rate_unit || work.project_unit || '\u2014') + '</td><td class=\"right num\">' + formatNumber(work.calculated_quantity || work.quantity_in_rate_units || work.project_quantity || 0, 4) + '</td><td class=\"right num\">' + formatCurrency(work.unit_cost || 0) + '</td><td class=\"right num\">' + formatCurrency(work.total_cost || 0) + '</td><td class=\"right num\">' + formatNumber(work.estimated_labor_hours || 0, 1) + '</td><td class=\"center\"><span class=\"quality-dot ' + dotClass + '\"></span></td></tr>';\n\n      var resources = work.resources_all || [];\n      for (var resIdx = 0; resIdx < resources.length; resIdx++) {\n        var res = resources[resIdx];\n        var isLast = resIdx === resources.length - 1;\n        var prefix = isLast ? '\u2514' : '\u251c';\n        var resType = res.resource_type || 'material';\n        var tagLabels = { labor: t.res_labor, material: t.res_material, machine: t.res_machine };\n        var tagLabel = tagLabels[resType] || t.res_material;\n        var tagClass = 'res-tag res-tag-' + resType;\n        \n        html += '<tr class=\"resource\" data-phase=\"' + phaseNum + '\" data-type=\"' + typeNum + '\" data-work=\"' + globalWorkIndex + '\"><td></td><td style=\"font-size:9px;\"><span style=\"opacity:0.5;\">' + escapeHtml(res.resource_code || '') + '</span><span class=\"' + tagClass + '\">' + tagLabel + '</span></td><td style=\"padding-left:36px\">' + prefix + ' ' + escapeHtml(res.resource_name || 'Resource') + '</td><td></td><td>' + escapeHtml(res.resource_unit || '') + '</td><td class=\"right num\">' + formatNumber(res.scaled_quantity || res.resource_quantity || 0, 4) + '</td><td class=\"right num\">' + formatCurrency(res.price_per_unit || 0) + '</td><td class=\"right num\">' + formatCurrency(res.scaled_cost || res.resource_cost || 0) + '</td><td></td><td></td></tr>';\n      }\n    }\n  }\n  \n  html += '<tr class=\"subtotal\" data-phase=\"' + phaseNum + '\"><td></td><td></td><td colspan=\"2\">' + t.subtotal + ' ' + phaseNum + '</td><td></td><td></td><td></td><td class=\"right num\">' + formatCurrency(phase.phase_total_cost || 0) + '</td><td class=\"right num\">' + formatNumber(phase.phase_labor_hours || 0, 1) + '</td><td class=\"center\">' + phasePercent + '%</td></tr>';\n}\n\n// GRAND TOTAL\nhtml += '<tr class=\"grand-total\"><td colspan=\"4\" style=\"padding-left:16px\">' + t.grand_total + '</td><td></td><td></td><td></td><td class=\"right num\">' + formatCurrency(grandTotal) + '</td><td class=\"right num\">' + formatNumber(grandLaborHours, 1) + '</td><td class=\"center\">100%</td></tr>';\nhtml += '<tr class=\"grand-total-info\"><td colspan=\"4\" style=\"padding-left:16px\">' + t.labor_cost + ': <span class=\"highlight\">' + formatCurrency(grandResourceCost) + '</span> (' + laborPercent + '%)</td><td colspan=\"3\">' + t.material_cost + ': <span class=\"highlight\">' + formatCurrency(grandMaterialCost) + '</span></td><td colspan=\"3\" class=\"right\" style=\"padding-right:16px\"><span class=\"highlight\">' + laborDays + '</span> ' + t.labor_days + '</td></tr>';\nhtml += '</table>\\n';\n\n// FOOTER\nhtml += '<div class=\"footer\"><div class=\"footer-main\"><div class=\"footer-brand\">OPEN CONSTRUCTION ESTIMATE v2</div><div class=\"footer-info\">Professional cost estimation powered by DDC CWICR database | Multi-stage AI decomposition</div><div class=\"footer-links\"><a href=\"' + RATES_LINK + '\" target=\"_blank\">openconstructionestimate.com</a><a href=\"https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR\" target=\"_blank\">GitHub</a><a href=\"https://datadrivenconstruction.io\" target=\"_blank\">datadrivenconstruction.io</a></div></div><div class=\"footer-db\">DDC CWICR \u2014 Construction Work Items, Costs & Resources Database | Generated: ' + dateTimeStr + '</div></div>\\n';\n\nhtml += '</div>\\n';\n\n// SCRIPT\nhtml += `<script>\nfunction collapseAll() {\n  document.querySelectorAll(\"tr.phase\").forEach(function(row) {\n    row.classList.add(\"collapsed\");\n    var icon = row.querySelector(\".toggle-icon\");\n    if (icon) icon.textContent = \"\u25b6\";\n    var phaseId = row.dataset.phase;\n    document.querySelectorAll('tr[data-phase=\"' + phaseId + '\"]:not(.phase)').forEach(function(child) {\n      child.classList.add(\"hidden\");\n    });\n  });\n}\n\nfunction expandAll() {\n  document.querySelectorAll(\"tr.phase\").forEach(function(row) {\n    row.classList.remove(\"collapsed\");\n    var icon = row.querySelector(\".toggle-icon\");\n    if (icon) icon.textContent = \"\u25bc\";\n    var phaseId = row.dataset.phase;\n    document.querySelectorAll('tr[data-phase=\"' + phaseId + '\"]:not(.phase)').forEach(function(child) {\n      child.classList.remove(\"hidden\");\n    });\n  });\n  document.querySelectorAll(\"tr.type\").forEach(function(row) {\n    row.classList.remove(\"collapsed\");\n    var icon = row.querySelector(\".toggle-icon\");\n    if (icon) icon.textContent = \"\u25bd\";\n  });\n  document.querySelectorAll(\"tr.work\").forEach(function(row) {\n    row.classList.remove(\"collapsed\");\n    var icon = row.querySelector(\".toggle-icon\");\n    if (icon) icon.textContent = \"\u25bf\";\n  });\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", function() {\n  document.querySelectorAll(\"tr.phase\").forEach(function(row) {\n    row.addEventListener(\"click\", function(e) {\n      if (e.target.tagName === \"A\") return;\n      var phaseId = this.dataset.phase;\n      this.classList.toggle(\"collapsed\");\n      var icon = this.querySelector(\".toggle-icon\");\n      var isCollapsed = this.classList.contains(\"collapsed\");\n      if (icon) icon.textContent = isCollapsed ? \"\u25b6\" : \"\u25bc\";\n      document.querySelectorAll('tr[data-phase=\"' + phaseId + '\"]:not(.phase)').forEach(function(child) {\n        if (isCollapsed) {\n          child.classList.add(\"hidden\");\n        } else {\n          if (child.classList.contains(\"resource\")) {\n            var workId = child.dataset.work;\n            var parentWork = document.querySelector('tr.work[data-work=\"' + workId + '\"]');\n            if (parentWork && !parentWork.classList.contains(\"collapsed\")) {\n              child.classList.remove(\"hidden\");\n            }\n          } else if (child.classList.contains(\"work\")) {\n            var typeId = child.dataset.type;\n            var parentType = document.querySelector('tr.type[data-phase=\"' + phaseId + '\"][data-type=\"' + typeId + '\"]');\n            if (parentType && !parentType.classList.contains(\"collapsed\")) {\n              child.classList.remove(\"hidden\");\n            }\n          } else {\n            child.classList.remove(\"hidden\");\n          }\n        }\n      });\n    });\n  });\n  \n  document.querySelectorAll(\"tr.type\").forEach(function(row) {\n    row.addEventListener(\"click\", function(e) {\n      e.stopPropagation();\n      if (e.target.tagName === \"A\") return;\n      var phaseId = this.dataset.phase;\n      var typeId = this.dataset.type;\n      this.classList.toggle(\"collapsed\");\n      var icon = this.querySelector(\".toggle-icon\");\n      var isCollapsed = this.classList.contains(\"collapsed\");\n      if (icon) icon.textContent = isCollapsed ? \"\u25b7\" : \"\u25bd\";\n      \n      document.querySelectorAll('tr.work[data-phase=\"' + phaseId + '\"][data-type=\"' + typeId + '\"]').forEach(function(work) {\n        if (isCollapsed) {\n          work.classList.add(\"hidden\");\n        } else {\n          work.classList.remove(\"hidden\");\n        }\n      });\n      \n      document.querySelectorAll('tr.resource[data-phase=\"' + phaseId + '\"][data-type=\"' + typeId + '\"]').forEach(function(res) {\n        if (isCollapsed) {\n          res.classList.add(\"hidden\");\n        } else {\n          var workId = res.dataset.work;\n          var parentWork = document.querySelector('tr.work[data-work=\"' + workId + '\"]');\n          if (parentWork && !parentWork.classList.contains(\"collapsed\")) {\n            res.classList.remove(\"hidden\");\n          }\n        }\n      });\n    });\n  });\n  \n  document.querySelectorAll(\"tr.work\").forEach(function(row) {\n    row.addEventListener(\"click\", function(e) {\n      e.stopPropagation();\n      if (e.target.tagName === \"A\") return;\n      var workId = this.dataset.work;\n      this.classList.toggle(\"collapsed\");\n      var icon = this.querySelector(\".toggle-icon\");\n      var isCollapsed = this.classList.contains(\"collapsed\");\n      if (icon) icon.textContent = isCollapsed ? \"\u25b9\" : \"\u25bf\";\n      \n      document.querySelectorAll('tr.resource[data-work=\"' + workId + '\"]').forEach(function(res) {\n        if (isCollapsed) {\n          res.classList.add(\"hidden\");\n        } else {\n          res.classList.remove(\"hidden\");\n        }\n      });\n    });\n  });\n  \n  document.querySelectorAll(\".treemap-item\").forEach(function(item) {\n    item.addEventListener(\"click\", function() {\n      var phaseId = this.dataset.phase;\n      var targetRow = document.getElementById(\"phase-\" + phaseId);\n      if (targetRow) {\n        if (targetRow.classList.contains(\"collapsed\")) targetRow.click();\n        targetRow.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n        targetRow.style.background = \"#FFFBEB\";\n        setTimeout(function() { targetRow.style.background = \"\"; }, 1500);\n      }\n    });\n  });\n  \n  // Default: hide resources\n  document.querySelectorAll(\"tr.resource\").forEach(function(row) {\n    row.classList.add(\"hidden\");\n  });\n  document.querySelectorAll(\"tr.work\").forEach(function(row) {\n    row.classList.add(\"collapsed\");\n    var icon = row.querySelector(\".toggle-icon\");\n    if (icon) icon.textContent = \"\u25b9\";\n  });\n});\n</script>\n`;\n\nhtml += '</body>\\n</html>';\n\nreturn {\n  json: {\n    success: true,\n    html_content: html,\n    summary: {\n      total_cost: grandTotal,\n      total_cost_formatted: formatCurrency(grandTotal),\n      labor_hours: grandLaborHours,\n      labor_days: laborDays,\n      works_count: totalWorks,\n      found_percent: foundPercent,\n      pricing_city: pricingLevel,\n      currency: currency\n    },\n    photo_analysis: photoAnalysis,\n    quality_stats: qualityStats,\n    validation: validation,\n    message: '\u2705 ' + t.doc_title + ' v2! ' + totalWorks + ' works \u2192 ' + formatCurrency(grandTotal) + ' (' + pricingLevel + ')'\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "286a9e9b-d24d-49b1-aaf1-eaeea6fb079a",
      "name": "Final HTML Output",
      "type": "n8n-nodes-base.code",
      "position": [
        32448,
        352
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// FINAL HTML OUTPUT - Returns HTML directly to Form\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst input = $input.first().json;\nconst htmlContent = input.html_content || '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"></head><body><h1>Error</h1><p>No content generated</p></body></html>';\n\n// Form Trigger with responseMode: lastNode will return this\nreturn {\n  json: {\n    success: true,\n    message: input.message || 'Estimate complete!',\n    summary: input.summary || {},\n    validation: input.validation || {}\n  },\n  binary: {\n    data: {\n      data: Buffer.from(htmlContent, 'utf-8').toString('base64'),\n      mimeType: 'text/html',\n      fileName: 'PhotoEstimate_v2.html'\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "b32c35fe-7284-4c29-818b-c8fed8b42ee5",
      "name": "Validation Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        31696,
        48
      ],
      "parameters": {
        "color": 5,
        "width": 260,
        "height": 464,
        "content": "## \u2705 STAGE 7.5: Validation\n\n**Checks performed:**\n1. Minimum 3 work items\n2. Found rate % > 50%\n3. Zero cost items < 30%\n4. Renovation has demolition\n\nGroups works by category:\n- PREPARATION\n- MAIN\n- FINISHING\n- MEP"
      },
      "typeVersion": 1
    },
    {
      "id": "fec19cc3-f61f-48bd-86a2-d919d0b36b3f",
      "name": "Pipeline Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        28688,
        640
      ],
      "parameters": {
        "color": 7,
        "width": 328,
        "height": 240,
        "content": "## \ud83d\udcca Pipeline Overview\n\n| Stage | Description |\n|-------|-------------|\n| 1 | Vision analyzes photo |\n| 4 | Decompose to works |\n| 5 | Vector search pricing |\n| 5.2 | Parse & Score results |\n| 7 | Calculate costs |\n| 7.5 | Validate works |\n| 9 | Generate HTML report |"
      },
      "typeVersion": 1
    },
    {
      "id": "3623a3d0-55cd-4741-bf4e-58b49c7e2bc2",
      "name": "Block 1 Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        29040,
        48
      ],
      "parameters": {
        "color": 5,
        "width": 1080,
        "height": 824,
        "content": "## Block 1: Photo Upload & Config\n\nThis block:\n- Receives photo from web form\n- Extracts language/region settings\n- Configures Vector DB collection\n- Validates photo presence\n\n**Supported formats:** JPG, PNG, WebP"
      },
      "typeVersion": 1
    },
    {
      "id": "a8f67a11-ec59-401c-bf06-429aec72f28a",
      "name": "Block 2 Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        30144,
        48
      ],
      "parameters": {
        "color": 6,
        "width": 464,
        "height": 820,
        "content": "## Block 2: STAGE 1 - Vision Analysis\n\n### \ud83e\udd16 AI Photo Analysis\n\nGPT-4 Vision analyzes photo:\n- Detects room type (bathroom, kitchen, etc.)\n- Identifies construction elements\n- Estimates dimensions from references\n- Lists fixtures and materials\n\n**Output:** Structured JSON with elements"
      },
      "typeVersion": 1
    },
    {
      "id": "fb2c4ced-db96-4bd2-9032-22ccd960fec4",
      "name": "Block 3 Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        30624,
        48
      ],
      "parameters": {
        "color": 6,
        "width": 656,
        "height": 816,
        "content": "## Block 3: STAGE 4 - Work Decomposition\n\n### \ud83e\udd16 AI Decomposition\n\nDecomposes elements into construction works:\n- BATHROOM \u2192 waterproofing, tiling, plumbing\n- KITCHEN \u2192 cabinets, countertops, backsplash\n- FLOOR \u2192 screed, covering, baseboard\n- WALL \u2192 prep, finish, paint\n- MEP \u2192 electrical, plumbing\n\n**Rules:**\n- Minimum 3 works per element\n- Include PREP + MAIN + FINISHING\n- For renovation: add demolition first"
      },
      "typeVersion": 1
    },
    {
      "id": "9db962cb-16a4-4368-961d-f00fb59edca9",
      "name": "Block 4 Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        31296,
        48
      ],
      "parameters": {
        "color": 5,
        "width": 380,
        "height": 816,
        "content": "## Block 4: STAGE 5 - Pricing Loop\n\nProcesses each work item:\n\n1. **Vector Search** - Find rates in Qdrant\n2. **Parse Results** - Extract costs & resources\n3. **Quality Scoring** - Rate match quality\n4. **Calculate** - Qty \u00d7 Unit Price\n\n**Database:** DDC CWICR\n700,000+ construction rates"
      },
      "typeVersion": 1
    },
    {
      "id": "d34124bf-ae65-49e3-b801-4d80f553964d",
      "name": "Vector Search Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        32768,
        48
      ],
      "parameters": {
        "width": 276,
        "height": 176,
        "content": "### \ud83d\udd0d Vector Search\n\nSearches DDC CWICR database:\n- 3072-dim embeddings\n- Top 5 matches per query\n- Multilingual support"
      },
      "typeVersion": 1
    },
    {
      "id": "581a9ed1-67e0-4677-be4a-921a38abc68e",
      "name": "Parse Score Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        31696,
        528
      ],
      "parameters": {
        "color": 4,
        "width": 1356,
        "height": 336,
        "content": "### \u26a1 STAGE 5.2 Parse & Score\n\n**FIXED:** Correct document extraction\n\nExtracts from Qdrant results:\n- Total cost from content\n- Rate code, name, unit\n- Resources (labor/material/machine)\n"
      },
      "typeVersion": 1
    },
    {
      "id": "77b90b95-27a5-4703-90d7-7cc6364d559f",
      "name": "Block 6 Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        31968,
        48
      ],
      "parameters": {
        "color": 5,
        "width": 776,
        "height": 460,
        "content": "## Block 6: Report Generation\n\n### \ud83d\udcca STAGE 9 HTML Report\n**Features:**\n- Professional design\n- Quality indicators (\u25cf \u25cb)\n- Calculation formulas\n- Clickable rate links\n- Cost structure charts\n- Phase timeline\n- Treemap visualization\n- 9 language support\n\n**Output:** HTML + XLS files"
      },
      "typeVersion": 1
    },
    {
      "id": "d260439b-2562-4319-b022-cc7954dacb71",
      "name": "AI Models Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        28688,
        48
      ],
      "parameters": {
        "width": 328,
        "height": 232,
        "content": "## \ud83e\udde0 AI Models Used\n\n**GPT-4 Vision** \u2014 Photo analysis\n**GPT-4** \u2014 Work decomposition\n\nCan be replaced with:\n- Anthropic Claude 3.5\n- Google Gemini Pro\n- OpenRouter models"
      },
      "typeVersion": 1
    },
    {
      "id": "6ddf3397-f636-42cd-aac2-ade61036c5e4",
      "name": "Qdrant Setup Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        32768,
        256
      ],
      "parameters": {
        "width": 272,
        "height": 248,
        "content": "### \ud83d\udce5 Vector Database Setup\n\nTo enable search:\n1. Install Qdrant (local or VPS)\n2. Add Qdrant credentials\n3. Upload DDC CWICR dataset\n4. Select collection for your language\n\nCollections available on [GitHub](https://github.com/datadrivenconstruction)"
      },
      "typeVersion": 1
    },
    {
      "id": "60030e9d-0323-425b-a99f-973e6ec0e077",
      "name": "Info1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        29040,
        -112
      ],
      "parameters": {
        "width": 560,
        "height": 144,
        "content": "# \ud83d\udcf8 Photo Cost Estimate Pro v2\n\n## Multi-stage AI decomposition pipeline\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "809715a7-2395-4fe1-a987-7e8df7c0cdb8",
      "name": "AI Models Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        28688,
        304
      ],
      "parameters": {
        "width": 328,
        "height": 312,
        "content": "### Pipeline:\n1. \ud83d\udcf7 GPT-4 Vision \u2192 Elements\n2. \ud83d\udd04 STAGE 4: Decompose \u2192 Works  \n3. \ud83d\udd0d Vector Search DDC CWICR\n4. \u2705 Validation Stage\n5. \ud83d\udcca HTML Report\n\n### Regions (9):\n\ud83c\udde9\ud83c\uddea Berlin | \ud83c\uddec\ud83c\udde7 Toronto | \ud83c\uddf7\ud83c\uddfa St. Petersburg\n\ud83c\uddea\ud83c\uddf8 Barcelona | \ud83c\uddeb\ud83c\uddf7 Paris | \ud83c\udde7\ud83c\uddf7 S\u00e3o Paulo\n\ud83c\udde8\ud83c\uddf3 Shanghai | \ud83c\udde6\ud83c\uddea Dubai | \ud83c\uddee\ud83c\uddf3 Mumbai\n\n\u2b50 **Star our repository** on [GitHub](https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR)"
      },
      "typeVersion": 1
    },
    {
      "id": "b4e01f61-6023-4851-b7ae-7d555c5059de",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        32368,
        -112
      ],
      "parameters": {
        "width": 356,
        "height": 132,
        "content": "\u2b50 **If you find our tools helpful**, please consider **starring our repository** on [GitHub](https://github.com/datadrivenconstruction/OpenConstructionEstimate-DDC-CWICR). \n\nYour support helps us improve and continue developing open solutions for the community!\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "56ea10a3-8e5f-4545-95b5-d107d41cad75",
  "connections": {
    "Wait": {
      "main": [
        [
          {
            "node": "Restore Work Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Accumulate": {
      "main": [
        [
          {
            "node": "Loop Works",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Embeddings": {
      "ai_embedding": [
        [
          {
            "node": "Vector Search",
            "type": "ai_embedding",
            "index": 0
          }
        ]
      ]
    },
    "Has Photo?": {
      "main": [
        [
          {
            "node": "STAGE 1 Vision Prompt",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error No Photo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Works": {
      "main": [
        [
          {
            "node": "STAGE 7.5 Aggregate & Validate",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Store Work Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GPT-4 Vision": {
      "ai_languageModel": [
        [
          {
            "node": "STAGE 1 Analyze Photo",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Extract Input": {
      "main": [
        [
          {
            "node": "Configure Language",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse STAGE 1": {
      "main": [
        [
          {
            "node": "STAGE 4 Decompose Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse STAGE 4": {
      "main": [
        [
          {
            "node": "Prepare Works",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Works": {
      "main": [
        [
          {
            "node": "Loop Works",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vector Search": {
      "main": [
        [
          {
            "node": "STAGE 5 Parse & Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GPT-4 Decompose": {
      "ai_languageModel": [
        [
          {
            "node": "STAGE 4 Decompose LLM",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Store Work Data": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Photo Upload Form": {
      "main": [
        [
          {
            "node": "Extract Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore Work Data": {
      "main": [
        [
          {
            "node": "Vector Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure Language": {
      "main": [
        [
          {
            "node": "Has Photo?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "STAGE 9 HTML Report": {
      "main": [
        [
          {
            "node": "Final HTML Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "STAGE 1 Analyze Photo": {
      "main": [
        [
          {
            "node": "Parse STAGE 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "STAGE 1 Vision Prompt": {
      "main": [
        [
          {
            "node": "STAGE 1 Analyze Photo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "STAGE 4 Decompose LLM": {
      "main": [
        [
          {
            "node": "Parse STAGE 4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "STAGE 5 Parse & Score": {
      "main": [
        [
          {
            "node": "Accumulate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "STAGE 4 Decompose Prompt": {
      "main": [
        [
          {
            "node": "STAGE 4 Decompose LLM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "STAGE 7.5 Aggregate & Validate": {
      "main": [
        [
          {
            "node": "STAGE 9 HTML Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}