Spaces:
Running
Running
| /* MTM Project Studio – browser-native sales application. | |
| Works without API keys or a backend. Leads are prepared as email drafts and | |
| project drafts/favorites are stored only in the visitor's browser. */ | |
| const MTM_EMAIL = "info@mtmehitus.ee"; | |
| const MTM_PHONE = "3725071042"; | |
| const storageKeys = { | |
| favorites: "mtm-project-favorites-v1", | |
| draft: "mtm-project-draft-v1", | |
| leads: "mtm-project-leads-v1" | |
| }; | |
| const projects = [ | |
| { | |
| id: "nordic-140", | |
| name: "Nordic 140", | |
| type: "home", | |
| typeLabel: "Eramu", | |
| area: 140, | |
| rooms: 5, | |
| floors: 1, | |
| style: "Põhjamaine", | |
| material: "Puit ja krohv", | |
| designPrice: 1850, | |
| status: "Kliendi valitud suund", | |
| package: "3D-disain + kohandamine", | |
| icon: "fa-house-chimney-window", | |
| gradient: "linear-gradient(135deg,#78909c,#263238)", | |
| description: "Valgusküllane ühekorruseline lahendus, kus köök ja elutuba avanevad terrassile. Selge ruumiloogika sobib nii linnalähedasele kui maapiirkonna krundile.", | |
| customize: "Pindala 115–165 m², 3–4 magamistuba, fassaadimaterjalid, katusekalle, terrassi asukoht ja tehnosüsteemid." | |
| }, | |
| { | |
| id: "compact-92", | |
| name: "Compact 92", | |
| type: "home", | |
| typeLabel: "Eramu", | |
| area: 92, | |
| rooms: 4, | |
| floors: 1, | |
| style: "Ajatu", | |
| material: "Puitkarkass", | |
| designPrice: 1450, | |
| status: "Projektina valitud", | |
| package: "3D-disain + ehitusprojekt", | |
| icon: "fa-house", | |
| gradient: "linear-gradient(135deg,#90a4ae,#455a64)", | |
| description: "Kompaktne viilkatusega kodu, mis annab väikese ehitusaluse pinna juures hea ruumikasutuse ja kontrollitava ehituseelarve.", | |
| customize: "Pindala 78–110 m², 2–3 magamistuba, varjualune, terrass, katusematerjal ja siseplaneering." | |
| }, | |
| { | |
| id: "courtyard-168", | |
| name: "Courtyard 168", | |
| type: "home", | |
| typeLabel: "Eramu", | |
| area: 168, | |
| rooms: 6, | |
| floors: 1, | |
| style: "Modernne", | |
| material: "Kergplokk ja puit", | |
| designPrice: 2450, | |
| status: "Täislahenduse lähteprojekt", | |
| package: "Disainist teostuseni", | |
| icon: "fa-building", | |
| gradient: "linear-gradient(135deg,#8d6e63,#3e2723)", | |
| description: "L-kujuline sisehooviga lahendus eraldab privaatse magamistubade tiiva ja avatud eluala. Hoov loob tuulevarjulise väliruumi.", | |
| customize: "Pindala 145–195 m², garaaž või varjualune, sisehoovi suurus, fassaadikombinatsioon ja ruumide arv." | |
| }, | |
| { | |
| id: "forest-76", | |
| name: "Forest 76", | |
| type: "summer", | |
| typeLabel: "Puhkemaja", | |
| area: 76, | |
| rooms: 3, | |
| floors: 1, | |
| style: "Looduslähedane", | |
| material: "Termopuit", | |
| designPrice: 1350, | |
| status: "Kohandatav näidislahendus", | |
| package: "3D-kontseptsioon", | |
| icon: "fa-tree-city", | |
| gradient: "linear-gradient(135deg,#7cb342,#1b5e20)", | |
| description: "Kompaktne puhkemaja suure klaasfassaadi, saunaploki ja kaetud terrassiga. Sobib metsa, mere või järve lähedale.", | |
| customize: "Pindala 55–95 m², saun, magamiskohtade arv, terrass, vai- või plaatvundament ja aastaringne kasutus." | |
| }, | |
| { | |
| id: "studio-48", | |
| name: "Studio 48", | |
| type: "outbuilding", | |
| typeLabel: "Stuudio / abihoone", | |
| area: 48, | |
| rooms: 2, | |
| floors: 1, | |
| style: "Minimalistlik", | |
| material: "Puit ja plekk", | |
| designPrice: 980, | |
| status: "Kohandatav näidislahendus", | |
| package: "3D-disain", | |
| icon: "fa-warehouse", | |
| gradient: "linear-gradient(135deg,#607d8b,#102027)", | |
| description: "Paindlik väikehoone kodukontoriks, stuudioks, külalistemajaks või rendipinnaks. Tehnosüsteemid saab lahendada põhiehitisega koos või eraldi.", | |
| customize: "Pindala 30–60 m², pesuruum, kööginurk, eraldi sissepääsud, fassaad ja tehniline valmidus." | |
| }, | |
| { | |
| id: "workhub-210", | |
| name: "WorkHub 210", | |
| type: "office", | |
| typeLabel: "Ärihoone", | |
| area: 210, | |
| rooms: 8, | |
| floors: 2, | |
| style: "Kaasaegne", | |
| material: "Sandwich-paneel ja klaas", | |
| designPrice: 2900, | |
| status: "Ärilahenduse kontseptsioon", | |
| package: "3D-disain + põhiprojekt", | |
| icon: "fa-city", | |
| gradient: "linear-gradient(135deg,#5c6bc0,#1a237e)", | |
| description: "Kahekorruseline paindlik töö- ja teenindushoone, mille ruume saab jagada ühe või mitme kasutaja vahel.", | |
| customize: "Pindala 160–320 m², töökohtade arv, lao- või teenindusala, parkimine, reklaampinnad ja energialahendus." | |
| } | |
| ]; | |
| const packageMeta = { | |
| concept: { | |
| name: "3D-kontseptsioon", | |
| duration: "3–6 nädalat", | |
| stages: ["Lähteülesanne", "Eskiissuund", "3D-mudel", "Visualiseeringud", "Üleandmine"], | |
| descriptions: [ | |
| "Vajadused, krundiinfo, ruumiprogramm ja eesmärgid.", | |
| "Kaks ruumilist suunda ja eelistatud variandi valik.", | |
| "Mahud, fassaadid, avad ja põhikompositsioon.", | |
| "Materjalid, valgus ja esitlusvaated.", | |
| "Failipakett ning soovitused järgmise etapi jaoks." | |
| ], | |
| shares: [0.15, 0.2, 0.3, 0.25, 0.1], | |
| deliverables: ["Lähteülesanne ja ruumiprogramm", "2 eskiissuunda", "3D-mudel ja 4 vaadet", "Materjali- ja värvikaart"] | |
| }, | |
| project: { | |
| name: "Ehitusprojekt", | |
| duration: "8–16 nädalat", | |
| stages: ["Lähteülesanne", "3D-kontseptsioon", "Põhijoonised", "Tehniline kooskõla", "Üleandmine"], | |
| descriptions: [ | |
| "Krundiinfo, ruumiprogramm ja tehnilised eesmärgid.", | |
| "Mahud, plaaniloogika, välisilme ja materjalid.", | |
| "Plaanid, vaated, lõiked ja põhikonstruktsioonid.", | |
| "Konstruktsioonide ja tehnosüsteemide lähteinfo.", | |
| "Koondfailid, konsultatsioon ja järgmiste sammude plaan." | |
| ], | |
| shares: [0.12, 0.22, 0.3, 0.24, 0.12], | |
| deliverables: ["Lähteülesanne ja ruumiprogramm", "3D-kontseptsioon ja materjalid", "Plaanid, vaated ja lõiked", "Tehniliste osade lähteinfo"] | |
| }, | |
| turnkey: { | |
| name: "Täislahendus", | |
| duration: "9–16 kuud", | |
| stages: ["Projekteerimine", "Vundament", "Konstruktsioonid", "Tehnosüsteemid", "Viimistlus ja üleandmine"], | |
| descriptions: [ | |
| "Disain, projekt, kooskõlastused ja detailne eelarve.", | |
| "Pinnasetööd, vundament, soojustus ja aluspõrand.", | |
| "Kandekonstruktsioonid, katus, avatäited ja fassaad.", | |
| "Elekter, vesi, kanalisatsioon, küte ja ventilatsioon.", | |
| "Siseviimistlus, seadistus, kontroll ja dokumentatsioon." | |
| ], | |
| shares: [0.08, 0.14, 0.3, 0.21, 0.27], | |
| deliverables: ["Disain ja ehitusprojekt", "Detailne eelarve ja ajakava", "Ehitusjuhtimine ja kvaliteedikontroll", "Valmis objekt ja üleandmisdokumendid"] | |
| } | |
| }; | |
| let appState = { | |
| package: "project", | |
| selectedProjectId: null, | |
| favoritesOnly: false, | |
| estimate: null | |
| }; | |
| let configState = { | |
| area: 120, | |
| floors: 1, | |
| foundation: "plate", | |
| wall: "block", | |
| roof: "gable-tin", | |
| heating: "geo", | |
| finish: "turnkey", | |
| region: "harju", | |
| use: "home" | |
| }; | |
| function safeJsonRead(key, fallback) { | |
| try { | |
| const parsed = JSON.parse(localStorage.getItem(key)); | |
| return parsed ?? fallback; | |
| } catch { | |
| return fallback; | |
| } | |
| } | |
| function saveJson(key, value) { | |
| try { | |
| localStorage.setItem(key, JSON.stringify(value)); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| function formatCurrency(value) { | |
| return Math.round(value).toLocaleString("et-EE") + " €"; | |
| } | |
| function packageValueToForm(value) { | |
| return packageMeta[value]?.name || "Ehitusprojekt"; | |
| } | |
| function showToast(message) { | |
| const toast = document.getElementById("toast"); | |
| toast.textContent = message; | |
| toast.classList.add("show"); | |
| clearTimeout(showToast.timer); | |
| showToast.timer = setTimeout(() => toast.classList.remove("show"), 3200); | |
| } | |
| window.selectOption = function(type, value, element) { | |
| configState[type] = value; | |
| element.parentElement.querySelectorAll(".option-card").forEach(card => card.classList.remove("selected")); | |
| element.classList.add("selected"); | |
| updateCalculator(); | |
| }; | |
| window.switchTab = function(tabName) { | |
| document.querySelectorAll(".tab-btn").forEach(btn => btn.classList.remove("active")); | |
| document.querySelectorAll(".tab-content").forEach(content => content.classList.remove("active")); | |
| const custom = tabName === "customizer"; | |
| document.querySelectorAll(".tab-btn")[custom ? 0 : 1].classList.add("active"); | |
| document.getElementById(custom ? "tab-customizer" : "tab-ai-prompt").classList.add("active"); | |
| }; | |
| window.selectPackage = function(packageId, element) { | |
| appState.package = packageId; | |
| document.querySelectorAll(".package-option").forEach(card => card.classList.remove("selected")); | |
| const target = element || document.querySelector(`[data-package="${packageId}"]`); | |
| if (target) target.classList.add("selected"); | |
| document.getElementById("form-package").value = packageValueToForm(packageId); | |
| updateCalculator(); | |
| }; | |
| window.choosePackageAndScroll = function(packageId) { | |
| selectPackage(packageId); | |
| document.querySelector("#kalkulaator").scrollIntoView({behavior: "smooth"}); | |
| }; | |
| window.updateCalculator = function() { | |
| const area = Number(document.getElementById("house-area").value); | |
| configState.area = area; | |
| configState.foundation = document.getElementById("foundation-type").value; | |
| configState.wall = document.getElementById("wall-type").value; | |
| configState.roof = document.getElementById("roof-type").value; | |
| configState.heating = document.getElementById("heating-type").value; | |
| configState.region = document.getElementById("project-region").value; | |
| configState.use = document.getElementById("building-use").value; | |
| document.getElementById("area-val").textContent = area; | |
| let baseRate = 1200; | |
| if (configState.floors === 1.5) baseRate += 100; | |
| if (configState.floors === 2) baseRate += 180; | |
| if (configState.foundation === "plate") baseRate += 60; | |
| if (configState.foundation === "pile") baseRate += 90; | |
| if (configState.wall === "clt") baseRate += 220; | |
| if (configState.wall === "wood") baseRate += 50; | |
| if (configState.heating === "geo") baseRate += 80; | |
| if (configState.heating === "air-water") baseRate += 50; | |
| if (configState.finish === "shell") baseRate -= 350; | |
| if (configState.finish === "turnkey") baseRate += 250; | |
| if (configState.region === "harju") baseRate *= 1.04; | |
| let midpoint; | |
| let lowFactor; | |
| let highFactor; | |
| if (appState.package === "concept") { | |
| midpoint = 850 + area * 8.5 + (configState.floors > 1 ? 350 : 0); | |
| lowFactor = 0.88; | |
| highFactor = 1.18; | |
| } else if (appState.package === "project") { | |
| midpoint = 2600 + area * 31 + (configState.floors > 1 ? 800 : 0) + (configState.wall === "clt" ? 500 : 0); | |
| lowFactor = 0.86; | |
| highFactor = 1.22; | |
| } else { | |
| midpoint = area * baseRate + 6200; | |
| lowFactor = 0.9; | |
| highFactor = 1.13; | |
| } | |
| const low = Math.round(midpoint * lowFactor / 50) * 50; | |
| const high = Math.round(midpoint * highFactor / 50) * 50; | |
| const meta = packageMeta[appState.package]; | |
| const selectedProject = projects.find(p => p.id === appState.selectedProjectId); | |
| const readiness = Math.min(95, 45 + (selectedProject ? 15 : 0) + (document.getElementById("ai-description").value.trim() ? 15 : 0) + (configState.region ? 10 : 0)); | |
| appState.estimate = { midpoint, low, high, baseRate, readiness }; | |
| document.getElementById("total-price").textContent = `${formatCurrency(low)} – ${formatCurrency(high)}`; | |
| document.getElementById("price-per-m2").textContent = appState.package === "turnkey" | |
| ? `hinnanguliselt ${Math.round(baseRate)} € / m²` | |
| : `${meta.name.toLowerCase()} valitud mahu põhjal`; | |
| meta.shares.forEach((share, index) => { | |
| document.getElementById(`stage-label-${index + 1}`).textContent = `${index + 1}. ${meta.stages[index]}`; | |
| document.getElementById(`stage-desc-${index + 1}`).textContent = meta.descriptions[index]; | |
| document.getElementById(["cost-foundation","cost-walls","cost-roof","cost-utility","cost-finish"][index]).textContent = formatCurrency(midpoint * share); | |
| }); | |
| meta.deliverables.forEach((item, index) => { | |
| document.getElementById(`deliverable-${index + 1}`).textContent = item; | |
| }); | |
| const useLabels = {home:"eramu", summer:"puhkemaja", office:"ärihoone", outbuilding:"abihoone"}; | |
| document.getElementById("brief-title").textContent = `${selectedProject ? selectedProject.name + " põhjal" : "Uus"} ${area} m² ${useLabels[configState.use]} – ${meta.name.toLowerCase()}`; | |
| document.getElementById("brief-package").textContent = meta.name; | |
| document.getElementById("brief-range").textContent = `${formatCurrency(low)}–${formatCurrency(high)}`; | |
| document.getElementById("brief-duration").textContent = meta.duration; | |
| document.getElementById("brief-readiness").textContent = `${readiness}%`; | |
| }; | |
| function getBriefText() { | |
| const selected = projects.find(p => p.id === appState.selectedProjectId); | |
| const meta = packageMeta[appState.package]; | |
| const regionLabels = {harju:"Harjumaa / Tallinn", tartu:"Tartumaa", parnu:"Pärnumaa", rapla:"Raplamaa", other:"Muu Eesti piirkond"}; | |
| const useLabels = {home:"Eramu", summer:"Suvila / puhkemaja", office:"Äri- või büroohoone", outbuilding:"Abihoone / stuudio"}; | |
| const estimate = appState.estimate || {}; | |
| return `MTM EHITUS – PROJEKTI LÄHTEÜLESANNE | |
| Koostatud: ${new Date().toLocaleDateString("et-EE")} | |
| VALITUD LAHENDUS | |
| Pakett: ${meta.name} | |
| Lähteprojekt: ${selected ? selected.name : "Personaalne uus lahendus"} | |
| Hoone: ${useLabels[configState.use]} | |
| Piirkond: ${regionLabels[configState.region]} | |
| Pindala: ${configState.area} m² | |
| Korruseid: ${configState.floors} | |
| TEHNILISED EELISTUSED | |
| Vundament: ${configState.foundation} | |
| Konstruktsioon: ${configState.wall} | |
| Katus: ${configState.roof} | |
| Küte: ${configState.heating} | |
| Viimistlus: ${configState.finish} | |
| IDEE JA ERISOOVID | |
| ${document.getElementById("ai-description").value.trim() || "Täpsustatakse konsultatsioonil."} | |
| ESIALGNE PLANEERING | |
| Hinnavahemik: ${formatCurrency(estimate.low || 0)} – ${formatCurrency(estimate.high || 0)} | |
| Ajakava: ${meta.duration} | |
| Lähteandmete valmidus: ${estimate.readiness || 0}% | |
| Märkus: hinnavahemik on planeerimise lähtehinnang, mitte siduv pakkumine.`; | |
| } | |
| window.downloadBrief = function() { | |
| const blob = new Blob([getBriefText()], {type: "text/plain;charset=utf-8"}); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement("a"); | |
| link.href = url; | |
| link.download = `MTM-projekti-lahteulesanne-${new Date().toISOString().slice(0,10)}.txt`; | |
| link.click(); | |
| URL.revokeObjectURL(url); | |
| showToast("Lähteülesanne laaditi alla."); | |
| }; | |
| window.copyBrief = async function() { | |
| try { | |
| await navigator.clipboard.writeText(getBriefText()); | |
| showToast("Projektikokkuvõte kopeeriti."); | |
| } catch { | |
| showToast("Kopeerimine ei õnnestunud. Laadi fail alla."); | |
| } | |
| }; | |
| window.shareByEmail = function() { | |
| const subject = encodeURIComponent(`MTM projektipäring – ${packageMeta[appState.package].name}`); | |
| const body = encodeURIComponent(getBriefText()); | |
| window.location.href = `mailto:${MTM_EMAIL}?subject=${subject}&body=${body}`; | |
| }; | |
| window.shareByWhatsApp = function() { | |
| const text = encodeURIComponent(getBriefText()); | |
| window.open(`https://wa.me/${MTM_PHONE}?text=${text}`, "_blank", "noopener"); | |
| }; | |
| window.saveDraft = function() { | |
| const draft = { | |
| config: {...configState}, | |
| app: {package: appState.package, selectedProjectId: appState.selectedProjectId}, | |
| description: document.getElementById("ai-description").value, | |
| savedAt: new Date().toISOString() | |
| }; | |
| if (saveJson(storageKeys.draft, draft)) showToast("Projekt salvestati selles brauseris."); | |
| else showToast("Brauser ei lubanud mustandit salvestada."); | |
| }; | |
| function restoreDraft() { | |
| const draft = safeJsonRead(storageKeys.draft, null); | |
| if (!draft?.config) return; | |
| Object.assign(configState, draft.config); | |
| appState.package = draft.app?.package || "project"; | |
| appState.selectedProjectId = draft.app?.selectedProjectId || null; | |
| document.getElementById("house-area").value = configState.area || 120; | |
| ["foundation","wall","roof","heating"].forEach(key => { | |
| const ids = {foundation:"foundation-type",wall:"wall-type",roof:"roof-type",heating:"heating-type"}; | |
| if (configState[key]) document.getElementById(ids[key]).value = configState[key]; | |
| }); | |
| if (configState.region) document.getElementById("project-region").value = configState.region; | |
| if (configState.use) document.getElementById("building-use").value = configState.use; | |
| document.getElementById("ai-description").value = draft.description || ""; | |
| selectPackage(appState.package); | |
| if (appState.selectedProjectId) updateStickyProject(); | |
| showToast("Varasem projektimustand taastati."); | |
| } | |
| window.analyzeProjectIdea = function() { | |
| const desc = document.getElementById("ai-description").value.trim(); | |
| const file = document.getElementById("ai-image-upload").files[0]; | |
| if (!desc && !file) { | |
| showToast("Lisa kirjeldus või inspiratsioonipilt."); | |
| return; | |
| } | |
| const lower = desc.toLowerCase(); | |
| const recommendations = []; | |
| if (/modern|minimal|lamekatus/.test(lower)) recommendations.push("Kontseptsioonis tasub kontrollida suure klaaspinna, varjestuse ja lamekatuse tehnilist sobivust."); | |
| if (/puit|clt|loodus/.test(lower)) recommendations.push("Puitlahenduse puhul lisa lähteülesandesse fassaadi hooldusklass, tulepüsivus ja niiskusrežiim."); | |
| if (/saun|spaa|bassein/.test(lower)) recommendations.push("Märgruumid vajavad varases etapis ventilatsiooni, äravoolude ja niiskustehnilise lahenduse koordineerimist."); | |
| if (/äri|büroo|klient|teenindus/.test(lower)) recommendations.push("Ärihoone puhul tuleb kohe määrata kasutajate arv, ligipääsetavus, evakuatsioon ja parkimisvajadus."); | |
| if (/odav|eelarve|soodne/.test(lower)) recommendations.push("Eelarve hoidmiseks eelista kompaktset mahtu, korduvaid avatäiteid ja lihtsat katusegeomeetriat."); | |
| if (!recommendations.length) recommendations.push("Järgmine mõistlik samm on ruumiprogrammi, krundi piirangute ja soovitud eelarve ühine kinnitamine."); | |
| if (file) recommendations.push(`Inspiratsioonipilt „${file.name}” lisatakse konsultatsiooni lähteinfo hulka.`); | |
| const selected = projects.find(p => p.id === appState.selectedProjectId); | |
| const intro = selected ? `Valitud ${selected.name} annab hea lähtekoha.` : "Kirjelduse põhjal soovitame alustada ruumilise kontseptsiooniga."; | |
| document.getElementById("ai-text-output").textContent = `${intro} ${recommendations.join(" ")}`; | |
| document.getElementById("ai-analysis-result").style.display = "block"; | |
| updateCalculator(); | |
| }; | |
| function getFavorites() { | |
| return safeJsonRead(storageKeys.favorites, []); | |
| } | |
| window.toggleFavorite = function(id, event) { | |
| event?.stopPropagation(); | |
| const favorites = getFavorites(); | |
| const next = favorites.includes(id) ? favorites.filter(item => item !== id) : [...favorites, id]; | |
| saveJson(storageKeys.favorites, next); | |
| renderProjects(); | |
| showToast(next.includes(id) ? "Projekt lisati lemmikutesse." : "Projekt eemaldati lemmikutest."); | |
| }; | |
| window.toggleFavoritesOnly = function() { | |
| appState.favoritesOnly = !appState.favoritesOnly; | |
| renderProjects(); | |
| }; | |
| window.renderProjects = function() { | |
| const query = document.getElementById("catalog-search").value.trim().toLowerCase(); | |
| const type = document.getElementById("catalog-type").value; | |
| const area = document.getElementById("catalog-area").value; | |
| const sort = document.getElementById("catalog-sort").value; | |
| const favorites = getFavorites(); | |
| let filtered = projects.filter(project => { | |
| const searchable = `${project.name} ${project.typeLabel} ${project.style} ${project.material} ${project.description}`.toLowerCase(); | |
| const areaMatch = area === "all" || (area === "small" && project.area <= 100) || (area === "medium" && project.area > 100 && project.area <= 160) || (area === "large" && project.area > 160); | |
| return (!query || searchable.includes(query)) && (type === "all" || project.type === type) && areaMatch && (!appState.favoritesOnly || favorites.includes(project.id)); | |
| }); | |
| if (sort === "area-asc") filtered.sort((a,b) => a.area - b.area); | |
| if (sort === "area-desc") filtered.sort((a,b) => b.area - a.area); | |
| if (sort === "price-asc") filtered.sort((a,b) => a.designPrice - b.designPrice); | |
| document.getElementById("catalog-count").textContent = `${filtered.length} lahendust`; | |
| document.getElementById("favorite-count").textContent = favorites.length; | |
| const grid = document.getElementById("project-grid"); | |
| if (!filtered.length) { | |
| grid.innerHTML = `<div class="empty-state"><i class="fas fa-magnifying-glass" style="font-size:2.4rem;margin-bottom:12px;"></i><h3>Sobivaid lahendusi ei leitud</h3><p>Muuda filtreid või koosta planeerijas personaalne projekt.</p></div>`; | |
| return; | |
| } | |
| grid.innerHTML = filtered.map(project => ` | |
| <article class="project-card"> | |
| <div class="project-img" style="background:${project.gradient}"> | |
| <button class="favorite-btn ${favorites.includes(project.id) ? "active" : ""}" onclick="toggleFavorite('${project.id}', event)" aria-label="Lisa lemmikuks"><i class="${favorites.includes(project.id) ? "fas" : "far"} fa-heart"></i></button> | |
| <i class="fas ${project.icon}"></i> | |
| </div> | |
| <div class="project-info"> | |
| <span class="tag">${project.package}</span> | |
| <h3 style="margin-top:12px;">${project.name}</h3> | |
| <p class="location"><i class="fas fa-circle-check"></i> ${project.status}</p> | |
| <div class="project-specs"> | |
| <span>${project.area} m²</span><span>${project.rooms} ruumi</span><span>${project.floors} korrus${project.floors > 1 ? "t" : ""}</span><span>${project.style}</span> | |
| </div> | |
| <p style="font-size:.88rem;color:var(--gray-text);">${project.description}</p> | |
| <p class="package-price">3D-disain alates ${formatCurrency(project.designPrice)}</p> | |
| <div class="project-actions"> | |
| <button class="btn btn-secondary btn-small" onclick="openProjectModal('${project.id}')">Vaata detaile</button> | |
| <button class="btn btn-primary btn-small" onclick="useProject('${project.id}')">Kasuta lähtepunktina</button> | |
| </div> | |
| </div> | |
| </article>`).join(""); | |
| }; | |
| window.openProjectModal = function(id) { | |
| const project = projects.find(item => item.id === id); | |
| if (!project) return; | |
| document.getElementById("modal-tag").textContent = project.package; | |
| document.getElementById("modal-title").textContent = project.name; | |
| document.getElementById("modal-description").textContent = project.description; | |
| document.getElementById("modal-customize").textContent = project.customize; | |
| document.getElementById("modal-hero").style.background = project.gradient; | |
| document.getElementById("modal-hero").innerHTML = `<i class="fas ${project.icon}"></i>`; | |
| document.getElementById("modal-specs").innerHTML = `<span>${project.area} m²</span><span>${project.rooms} ruumi</span><span>${project.floors} korrus${project.floors > 1 ? "t" : ""}</span><span>${project.material}</span>`; | |
| document.getElementById("modal-use-btn").onclick = () => useProject(id); | |
| document.getElementById("modal-quote-btn").onclick = () => chooseDesign(project.name); | |
| document.getElementById("project-modal").classList.add("open"); | |
| document.body.style.overflow = "hidden"; | |
| }; | |
| window.closeProjectModal = function() { | |
| document.getElementById("project-modal").classList.remove("open"); | |
| document.body.style.overflow = ""; | |
| }; | |
| window.closeModalFromBackdrop = function(event) { | |
| if (event.target.id === "project-modal") closeProjectModal(); | |
| }; | |
| window.useProject = function(id) { | |
| const project = projects.find(item => item.id === id); | |
| if (!project) return; | |
| appState.selectedProjectId = id; | |
| document.getElementById("house-area").value = project.area; | |
| document.getElementById("building-use").value = project.type; | |
| configState.floors = project.floors; | |
| updateStickyProject(); | |
| closeProjectModal(); | |
| updateCalculator(); | |
| document.querySelector("#kalkulaator").scrollIntoView({behavior:"smooth"}); | |
| showToast(`${project.name} valiti projekti lähtepunktiks.`); | |
| }; | |
| function updateStickyProject() { | |
| const project = projects.find(item => item.id === appState.selectedProjectId); | |
| const bar = document.getElementById("sticky-project-bar"); | |
| if (!project) { | |
| bar.classList.remove("show"); | |
| return; | |
| } | |
| document.getElementById("sticky-project-name").textContent = project.name; | |
| bar.classList.add("show"); | |
| } | |
| window.chooseDesign = function(designName) { | |
| const project = projects.find(item => item.name === designName); | |
| if (project) appState.selectedProjectId = project.id; | |
| document.getElementById("form-message").value = `${getBriefText()}\n\nSoovin selle lahenduse kohta personaalset pakkumist.`; | |
| document.getElementById("form-package").value = packageValueToForm(appState.package); | |
| closeProjectModal(); | |
| document.querySelector("#kontakt").scrollIntoView({behavior:"smooth"}); | |
| }; | |
| window.sendProjectToForm = function() { | |
| document.getElementById("form-message").value = `${getBriefText()}\n\nPalun võtke minuga ühendust lahenduse täpsustamiseks.`; | |
| document.getElementById("form-package").value = packageValueToForm(appState.package); | |
| document.querySelector("#kontakt").scrollIntoView({behavior:"smooth"}); | |
| }; | |
| window.handleFormSubmit = function(event) { | |
| event.preventDefault(); | |
| if (document.getElementById("form-company").value) return; | |
| const form = event.currentTarget; | |
| if (!form.checkValidity()) { | |
| form.reportValidity(); | |
| return; | |
| } | |
| const reference = `MTM-${new Date().toISOString().slice(0,10).replaceAll("-","")}-${String(Date.now()).slice(-4)}`; | |
| const lead = { | |
| reference, | |
| name: document.getElementById("form-name").value.trim(), | |
| email: document.getElementById("form-email").value.trim(), | |
| phone: document.getElementById("form-phone").value.trim(), | |
| package: document.getElementById("form-package").value, | |
| message: document.getElementById("form-message").value.trim(), | |
| createdAt: new Date().toISOString() | |
| }; | |
| const leads = safeJsonRead(storageKeys.leads, []); | |
| saveJson(storageKeys.leads, [...leads.slice(-9), lead]); | |
| const status = document.getElementById("form-status"); | |
| status.className = "form-status success"; | |
| status.textContent = `Päring ${reference} koostati ja salvestati selles brauseris. Avame nüüd sinu e-posti rakenduse, et saaksid selle MTM Ehitusele saata.`; | |
| const subject = encodeURIComponent(`Projektipäring ${reference} – ${lead.package}`); | |
| const body = encodeURIComponent(`Nimi: ${lead.name}\nE-post: ${lead.email}\nTelefon: ${lead.phone || "-"}\n\n${lead.message}`); | |
| setTimeout(() => { window.location.href = `mailto:${MTM_EMAIL}?subject=${subject}&body=${body}`; }, 350); | |
| }; | |
| document.addEventListener("keydown", event => { | |
| if (event.key === "Escape") closeProjectModal(); | |
| }); | |
| document.getElementById("menuToggle").addEventListener("click", () => { | |
| document.getElementById("navMenu").classList.toggle("open"); | |
| }); | |
| document.getElementById("ai-image-upload").addEventListener("change", () => updateCalculator()); | |
| document.getElementById("ai-description").addEventListener("input", () => updateCalculator()); | |
| renderProjects(); | |
| updateCalculator(); | |
| restoreDraft(); | |