1(function() { 2 'use strict'; 3 4 class CoffeeBuybackWidget { 5 constructor(options) { 6 this.containerId = options.containerId; 7 this.supabaseUrl = options.supabaseUrl || 'https://your-project.supabase.co'; 8 this.supabaseKey = options.supabaseKey || 'your-anon-key'; 9 this.debugMode = options.debugMode || false; 10 this.supabase = null; 11 this.step = 1; 12 this.selectedBrand = null; 13 this.selectedType = null; 14 this.selectedDefect = null; 15 this.selectedCondition = null; 16 this.finalPrice = null; 17 this.sellerData = { 18 firstName: '', 19 lastName: '', 20 email: '', 21 phone: '', 22 street: '', 23 city: '', 24 postalCode: '', 25 country: '', 26 iban: '', 27 pickupDate: '', 28 gdprConsent: false 29 }; 30 this.contractData = null; 31 this.contractId = null; 32 this.isLoading = false; 33 this.isDownloading = false; 34 this.isSendingEmail = false; 35 this.downloadSuccess = false; 36 this.emailSuccess = false; 37 this.errorMessage = null; 38 this.formErrors = {}; 39 this.loadingStatus = ''; 40 this.data = { 41 brands: [], 42 types: [], 43 defects: [], 44 conditions: [], 45 priceRules: [], 46 translations: [] 47 }; 48 this.language = 'sk'; 49 this.currency = 'EUR'; 50 this.basePrice = null; 51 this.init(); 52 } 53 54 log(...args) { 55 if (this.debugMode) { 56 console.log('[CoffeeBuybackWidget]', ...args); 57 } 58 } 59 60 error(...args) { 61 if (this.debugMode) { 62 console.error('[CoffeeBuybackWidget ERROR]', ...args); 63 } 64 } 65 66 async init() { 67 this.detectLanguage(); 68 await this.loadData(); 69 this.render(); 70 this.injectStyles(); 71 } 72 73 detectLanguage() { 74 const htmlLang = document.documentElement.lang?.toLowerCase(); 75 const supportedLanguages = ['sk', 'cs', 'en', 'de']; 76 if (htmlLang && supportedLanguages.includes(htmlLang.split('-')[0])) { 77 this.language = htmlLang.split('-')[0]; 78 } 79 } 80 81 async loadData() { 82 this.loadingStatus = this.getUIText('loadingConnecting'); 83 this.log('Starting data load...'); 84 85 try { 86 const isSupabaseConfigured = this.supabaseUrl && this.supabaseKey && 87 this.supabaseUrl !== 'https://your-project.supabase.co' && 88 this.supabaseKey !== 'your-anon-key'; 89 90 if (!isSupabaseConfigured) { 91 this.log('Supabase not configured, using fallback data'); 92 this.data = this.getFallbackData(); 93 return; 94 } 95 96 try { 97 this.log('Loading Supabase client...'); 98 const { createClient } = await import('https://esm.sh/@supabase/supabase-js@2'); 99 this.supabase = createClient(this.supabaseUrl, this.supabaseKey); 100 this.log('Supabase client initialized'); 101 102 this.loadingStatus = this.getUIText('loadingData'); 103 this.log('Fetching data from database...'); 104 105 const [brandsRes, typesRes, defectsRes, conditionsRes, pricesRes, translationsRes] = await Promise.all([ 106 this.supabase.from('brands').select('*').order('name'), 107 this.supabase.from('machine_types').select('*').order('name'), 108 this.supabase.from('defects').select('*').order('price_multiplier', { ascending: false }), 109 this.supabase.from('conditions').select('*').order('price_multiplier', { ascending: false }), 110 this.supabase.from('price_rules').select('*'), 111 this.supabase.from('translations').select('*') 112 ]); 113 114 if (brandsRes.error) { 115 this.error('Brands fetch error:', brandsRes.error); 116 throw new Error(`Chyba pri naÄÃtanà znaÄiek: ${brandsRes.error.message}`); 117 } 118 if (typesRes.error) { 119 this.error('Types fetch error:', typesRes.error); 120 throw new Error(`Chyba pri naÄÃtanà typov: ${typesRes.error.message}`); 121 } 122 if (defectsRes.error) { 123 this.error('Defects fetch error:', defectsRes.error); 124 throw new Error(`Chyba pri naÄÃtanà závad: ${defectsRes.error.message}`); 125 } 126 if (conditionsRes.error) { 127 this.error('Conditions fetch error:', conditionsRes.error); 128 throw new Error(`Chyba pri naÄÃtanà stavov: ${conditionsRes.error.message}`); 129 } 130 if (pricesRes.error) {
131 this.error('Prices fetch error:', pricesRes.error); 132 throw new Error(`Chyba pri naÄÃtanà cien: ${pricesRes.error.message}`); 133 } 134 135 const directData = { 136 brands: brandsRes.data || [], 137 types: typesRes.data || [], 138 defects: defectsRes.data || [], 139 conditions: conditionsRes.data || [], 140 priceRules: pricesRes.data || [], 141 translations: translationsRes.data || [] 142 }; 143 144 this.log('Data loaded successfully:', { 145 brands: directData.brands.length, 146 types: directData.types.length, 147 defects: directData.defects.length, 148 conditions: directData.conditions.length, 149 priceRules: directData.priceRules.length, 150 translations: directData.translations.length 151 }); 152 153 if (directData.brands.length === 0) { 154 this.log('No brands found in database, using fallback data'); 155 this.data = this.getFallbackData(); 156 } else { 157 this.data = directData; 158 this.log('Using live database data'); 159 } 160 } catch (supabaseError) { 161 this.error('Supabase connection failed:', supabaseError); 162 this.log('Falling back to demo data'); 163 this.data = this.getFallbackData(); 164 } 165 } catch (error) { 166 this.error('Error loading data:', error); 167 this.log('Using fallback data due to error'); 168 this.data = this.getFallbackData(); 169 } 170 171 this.loadingStatus = ''; 172 } 173 174 getFallbackData() { 175 return { 176 brands: [ 177 { id: 'fallback-1', name: 'DeLonghi (Demo)' }, 178 { id: 'fallback-2', name: 'Krups (Demo)' }, 179 { id: 'fallback-3', name: 'Philips (Demo)' }, 180 { id: 'fallback-4', name: 'Sage (Demo)' } 181 ], 182 types: [ 183 { id: 'fallback-1', name: 'Automaticky espresso (Demo)', brand_id: 'fallback-1' }, 184 { id: 'fallback-2', name: 'Pakovy espresso (Demo)', brand_id: 'fallback-1' }, 185 { id: 'fallback-3', name: 'Kapslovy kavovar (Demo)', brand_id: 'fallback-2' }, 186 { id: 'fallback-4', name: 'Filtrovy kavovar (Demo)', brand_id: 'fallback-3' } 187 ], 188 defects: [ 189 { id: 'fallback-1', name: 'Ziadne - plne funkcny (Demo)', price_multiplier: 1.0 }, 190 { id: 'fallback-2', name: 'Mensie opotrebovanie (Demo)', price_multiplier: 0.8 }, 191 { id: 'fallback-3', name: 'Potrebuje opravu (Demo)', price_multiplier: 0.5 }, 192 { id: 'fallback-4', name: 'Nefunkcny (Demo)', price_multiplier: 0.2 } 193 ], 194 conditions: [ 195 { id: 'fallback-1', name: 'Vyborny (Demo)', description: 'Bez viditelneho poskodenia', price_multiplier: 1.0 }, 196 { id: 'fallback-2', name: 'Dobry (Demo)', description: 'Mierne znaky pouzitia', price_multiplier: 0.85 }, 197 { id: 'fallback-3', name: 'Prijatelny (Demo)', description: 'Viditelne znaky pouzitia', price_multiplier: 0.7 }, 198 { id: 'fallback-4', name: 'Zly (Demo)', description: 'Vyrazne poskodenie', price_multiplier: 0.5 } 199 ], 200 priceRules: [ 201 { id: 'fallback-1', brand_id: 'fallback-1', machine_type_id: 'fallback-1', base_price: 150 }, 202 { id: 'fallback-2', brand_id: 'fallback-1', machine_type_id: 'fallback-2', base_price: 120 }, 203 { id: 'fallback-3', brand_id: 'fallback-2', machine_type_id: 'fallback-3', base_price: 80 }, 204 { id: 'fallback-4', brand_id: 'fallback-3', machine_type_id: 'fallback-4', base_price: 60 } 205 ], 206 translations: [] 207 }; 208 } 209 210 getTranslation(entityType, entityId, fallbackName, fallbackDescription) { 211 if (this.language === 'sk') { 212 return { name: fallbackName, description: fallbackDescription }; 213 } 214 const translation = this.data.translations.find( 215 t => t.entity_type === entityType && t.entity_id === entityId && t.language === this.language 216 ); 217 return { 218 name: translation?.name || fallbackName, 219 description: translation?.description || fallbackDescription 220 }; 221 } 222 223 getUIText(key) { 224 const texts = { 225 sk: { 226 widgetTitle: 'Výkup Kávovarov', 227 widgetSubtitle: 'Zistite cenu vášho kávovaru', 228 selectBrand: 'Vyberte znaÄku', 229 selectType: 'Vyberte typ', 230 selectDefect: 'Vyberte závadu', 231 selectCondition: 'Vyberte stav', 232 continue: 'PokraÄovaÅ¥', 233 back: 'Späť', 234 estimatedPrice: 'Odhadovaná cena',
235 finalPrice: 'Výkupná cena', 236 yourMachine: 'Váš kávovar', 237 brand: 'ZnaÄka', 238 type: 'Typ', 239 defect: 'Závada', 240 condition: 'Stav', 241 continueToForm: 'PredaÅ¥ kávovar za túto cenu', 242 newCalculation: 'Nový výpoÄet', 243 sellerInfo: 'Ãdaje predávajúceho', 244 sellerInfoSubtitle: 'VyplÅte prosÃm vaÅ¡e kontaktné údaje', 245 firstName: 'Meno', 246 lastName: 'Priezvisko', 247 email: 'Email', 248 phone: 'Telefón', 249 street: 'Ulica a ÄÃslo', 250 city: 'Mesto', 251 postalCode: 'PSÄ', 252 country: 'Krajina', 253 iban: 'IBAN', 254 pickupDate: 'Dátum vyzdvihnutia', 255 gdprConsent: 'SúhlasÃm so spracovanÃm osobných údajov', 256 generateContract: 'VygenerovaÅ¥ zmluvu', 257 contractPreview: 'Náhľad zmluvy', 258 contractPreviewSubtitle: 'Skontrolujte si údaje pred stiahnutÃm', 259 downloadContract: 'StiahnuÅ¥ zmluvu (PDF)', 260 sendEmail: 'OdoslaÅ¥ na email', 261 contractSuccess: 'Zmluva bola úspeÅ¡ne stiahnutá!', 262 emailSuccess: 'Zmluva bola úspeÅ¡ne odoslaná na email!', 263 loading: 'NaÄÃtavam...', 264 loadingConnecting: 'Pripájam sa k databáze...', 265 loadingData: 'NaÄÃtavam dáta...', 266 generating: 'Generujem zmluvu...', 267 downloading: 'SÅ¥ahujem...', 268 sending: 'Odosielam...', 269 errorNoData: 'Chyba: Žiadne údaje o zmluve', 270 errorRequired: 'je povinné', 271 errorInvalidEmail: 'Neplatný formát emailu', 272 errorInvalidPhone: 'Neplatný formát telefónu', 273 errorInvalidPostalCode: 'Neplatné PSÄ (formát: 123 45)', 274 errorInvalidIban: 'Neplatný IBAN', 275 errorGdprRequired: 'MusÃte súhlasiÅ¥ so spracovanÃm osobných údajov', 276 errorMinLength2: 'musà maÅ¥ aspoÅ 2 znaky', 277 errorWeekdayOnly: 'Vyberte pracovný deÅ (Pondelok - Piatok)', 278 errorDatabaseUnavailable: 'Databáza nie je dostupná. Skúste obnoviÅ¥ stránku.', 279 errorSavingContract: 'Nepodarilo sa uložiÅ¥ zmluvu do databázy', 280 errorGeneratingContract: 'Nepodarilo sa vytvoriÅ¥ zmluvu. Skúste to prosÃm znova.', 281 errorDownloadingContract: 'Chyba pri sÅ¥ahovanà zmluvy. Skúste to prosÃm znova.', 282 errorSendingEmail: 'Nepodarilo sa odoslaÅ¥ email. Skúste to prosÃm znova.', 283 loadingCreatingContract: 'Vytváram zmluvu...', 284 loadingSavingContract: 'Ukladám do databázy...', 285 nextStepsTitle: 'ÄalÅ¡ie kroky:', 286 nextStep1: 'VytlaÄte zmluvu', 287 nextStep2: 'PodpÃÅ¡te zmluvu na vyznaÄenom mieste', 288 nextStep3: 'Zabalte kávovar, kuriér ho vyzdvihne vo vami zvolený pracovný deÅ' 289 }, 290 cs: { 291 widgetTitle: 'Výkup Kávovarů', 292 widgetSubtitle: 'ZjistÄte cenu vaÅ¡eho kávovaru', 293 selectBrand: 'Vyberte znaÄku', 294 selectType: 'Vyberte typ', 295 selectDefect: 'Vyberte závadu', 296 selectCondition: 'Vyberte stav', 297 continue: 'PokraÄovat', 298 back: 'ZpÄt', 299 estimatedPrice: 'Odhadovaná cena', 300 finalPrice: 'Výkupnà cena', 301 yourMachine: 'Váš kávovar', 302 brand: 'ZnaÄka', 303 type: 'Typ', 304 defect: 'Závada', 305 condition: 'Stav', 306 continueToForm: 'Prodat kávovar za tuto cenu', 307 newCalculation: 'Nový výpoÄet', 308 sellerInfo: 'Ãdaje prodávajÃcÃho', 309 sellerInfoSubtitle: 'VyplÅte prosÃm vaÅ¡e kontaktnà údaje', 310 firstName: 'Jméno', 311 lastName: 'PÅÃjmenÃ', 312 email: 'Email', 313 phone: 'Telefon', 314 street: 'Ulice a ÄÃslo', 315 city: 'MÄsto', 316 postalCode: 'PSÄ', 317 country: 'ZemÄ', 318 iban: 'IBAN', 319 pickupDate: 'Datum vyzvednutÃ', 320 gdprConsent: 'SouhlasÃm se zpracovánÃm osobnÃch údajů', 321 generateContract: 'Vygenerovat smlouvu', 322 contractPreview: 'Náhled smlouvy', 323 contractPreviewSubtitle: 'Zkontrolujte si údaje pÅed staženÃm', 324 downloadContract: 'Stáhnout smlouvu (PDF)', 325 sendEmail: 'Odeslat na email', 326 contractSuccess: 'Smlouva byla úspÄÅ¡nÄ stažena!', 327 emailSuccess: 'Smlouva byla úspÄÅ¡nÄ odeslána na email!', 328 loading: 'NaÄÃtám...', 329 loadingConnecting: 'PÅipojuji se k databázi...', 330 loadingData: 'NaÄÃtám data...', 331 generating: 'Generuji smlouvu...', 332 downloading: 'Stahuji...', 333 sending: 'OdesÃlám...', 334 errorNoData: 'Chyba: Žádné údaje o smlouvÄ', 335 errorRequired: 'je povinné', 336 errorInvalidEmail: 'Neplatný formát emailu', 337 errorInvalidPhone: 'Neplatný formát telefonu', 338 errorInvalidPostalCode: 'Neplatné PSÄ (formát: 123 45)', 339 errorInvalidIban: 'Neplatný IBAN', 340 errorGdprRequired: 'MusÃte souhlasit se zpracovánÃm osobnÃch údajů', 341 errorMinLength2: 'musà mÃt alespoÅ 2 znaky', 342 errorWeekdayOnly: 'Vyberte pracovnà den (PondÄlà - Pátek)', 343 errorDatabaseUnavailable: 'Databáze nenà dostupná. Zkuste obnovit stránku.', 344 errorSavingContract: 'NepodaÅilo se uložit smlouvu do databáze', 345 errorGeneratingContract: 'NepodaÅilo se vytvoÅit smlouvu. Zkuste to prosÃm znovu.', 346 errorDownloadingContract: 'Chyba pÅi stahovánà smlouvy. Zkuste to prosÃm znovu.', 347 errorSendingEmail: 'NepodaÅilo se odeslat email. Zkuste to prosÃm znovu.', 348 loadingCreatingContract: 'VytváÅÃm smlouvu...', 349 loadingSavingContract: 'Ukládám do databáze...', 350 nextStepsTitle: 'Dalšà kroky:', 351 nextStep1: 'VytisknÄte smlouvu', 352 nextStep2: 'PodepiÅ¡te smlouvu na vyznaÄeném mÃstÄ', 353 nextStep3: 'Zabalte kávovar, kurýr ho vyzvedne ve vámi zvolený pracovnà den' 354 }, 355 en: { 356 widgetTitle: 'Coffee Machine Buyback', 357 widgetSubtitle: 'Find out the price of your coffee machine', 358 selectBrand: 'Select brand', 359 selectType: 'Select type', 360 selectDefect: 'Select defect', 361 selectCondition: 'Select condition', 362 continue: 'Continue', 363 back: 'Back', 364 estimatedPrice: 'Estimated price',
365 finalPrice: 'Buyback price', 366 yourMachine: 'Your coffee machine', 367 brand: 'Brand', 368 type: 'Type', 369 defect: 'Defect', 370 condition: 'Condition', 371 continueToForm: 'Sell coffee machine for this price', 372 newCalculation: 'New calculation', 373 sellerInfo: 'Seller information', 374 sellerInfoSubtitle: 'Please fill in your contact details', 375 firstName: 'First name', 376 lastName: 'Last name', 377 email: 'Email', 378 phone: 'Phone', 379 street: 'Street and number', 380 city: 'City', 381 postalCode: 'Postal code', 382 country: 'Country', 383 iban: 'IBAN', 384 pickupDate: 'Pickup date', 385 gdprConsent: 'I agree to the processing of personal data', 386 generateContract: 'Generate contract', 387 contractPreview: 'Contract preview', 388 contractPreviewSubtitle: 'Check the details before downloading', 389 downloadContract: 'Download contract (PDF)', 390 sendEmail: 'Send by email', 391 contractSuccess: 'Contract downloaded successfully!', 392 emailSuccess: 'Contract sent by email successfully!', 393 loading: 'Loading...', 394 loadingConnecting: 'Connecting to database...', 395 loadingData: 'Loading data...', 396 generating: 'Generating contract...', 397 downloading: 'Downloading...', 398 sending: 'Sending...', 399 errorNoData: 'Error: No contract data', 400 errorRequired: 'is required', 401 errorInvalidEmail: 'Invalid email format', 402 errorInvalidPhone: 'Invalid phone format', 403 errorInvalidPostalCode: 'Invalid postal code (format: 123 45)', 404 errorInvalidIban: 'Invalid IBAN', 405 errorGdprRequired: 'You must agree to the processing of personal data', 406 errorMinLength2: 'must be at least 2 characters', 407 errorWeekdayOnly: 'Select a weekday (Monday - Friday)', 408 errorDatabaseUnavailable: 'Database is not available. Please refresh the page.', 409 errorSavingContract: 'Failed to save contract to database', 410 errorGeneratingContract: 'Failed to create contract. Please try again.', 411 errorDownloadingContract: 'Error downloading contract. Please try again.', 412 errorSendingEmail: 'Failed to send email. Please try again.', 413 loadingCreatingContract: 'Creating contract...', 414 loadingSavingContract: 'Saving to database...', 415 nextStepsTitle: 'Next steps:', 416 nextStep1: 'Print the contract', 417 nextStep2: 'Sign the contract in the designated place', 418 nextStep3: 'Pack the coffee machine, courier will pick it up on your chosen working day' 419 }, 420 de: { 421 widgetTitle: 'Kaffeemaschinen Ankauf', 422 widgetSubtitle: 'Finden Sie den Preis Ihrer Kaffeemaschine heraus', 423 selectBrand: 'Marke wählen', 424 selectType: 'Typ wählen', 425 selectDefect: 'Defekt wählen', 426 selectCondition: 'Zustand wählen', 427 continue: 'Weiter', 428 back: 'Zurück', 429 estimatedPrice: 'Geschätzter Preis', 430 finalPrice: 'Ankaufspreis', 431 yourMachine: 'Ihre Kaffeemaschine', 432 brand: 'Marke', 433 type: 'Typ', 434 defect: 'Defekt', 435 condition: 'Zustand', 436 continueToForm: 'Kaffeemaschine für diesen Preis verkaufen', 437 newCalculation: 'Neue Berechnung', 438 sellerInfo: 'Verkäuferinformationen', 439 sellerInfoSubtitle: 'Bitte füllen Sie Ihre Kontaktdaten aus', 440 firstName: 'Vorname', 441 lastName: 'Nachname', 442 email: 'E-Mail', 443 phone: 'Telefon', 444 street: 'StraÃe und Nummer', 445 city: 'Stadt', 446 postalCode: 'Postleitzahl', 447 country: 'Land', 448 iban: 'IBAN', 449 pickupDate: 'Abholtermin', 450 gdprConsent: 'Ich stimme der Verarbeitung personenbezogener Daten zu', 451 generateContract: 'Vertrag erstellen', 452 contractPreview: 'Vertragsvorschau', 453 contractPreviewSubtitle: 'Ãberprüfen Sie die Details vor dem Download', 454 downloadContract: 'Vertrag herunterladen (PDF)', 455 sendEmail: 'Per E-Mail senden', 456 contractSuccess: 'Vertrag erfolgreich heruntergeladen!', 457 emailSuccess: 'Vertrag erfolgreich per E-Mail gesendet!', 458 loading: 'Laden...', 459 loadingConnecting: 'Verbindung zur Datenbank...', 460 loadingData: 'Daten werden geladen...', 461 generating: 'Vertrag wird erstellt...', 462 downloading: 'Herunterladen...', 463 sending: 'Senden...', 464 errorNoData: 'Fehler: Keine Vertragsdaten', 465 errorRequired: 'ist erforderlich', 466 errorInvalidEmail: 'Ungültiges E-Mail-Format', 467 errorInvalidPhone: 'Ungültiges Telefonformat', 468 errorInvalidPostalCode: 'Ungültige Postleitzahl (Format: 123 45)', 469 errorInvalidIban: 'Ungültige IBAN', 470 errorGdprRequired: 'Sie müssen der Verarbeitung personenbezogener Daten zustimmen', 471 errorMinLength2: 'muss mindestens 2 Zeichen lang sein', 472 errorWeekdayOnly: 'Wählen Sie einen Wochentag (Montag - Freitag)', 473 errorDatabaseUnavailable: 'Datenbank ist nicht verfügbar. Bitte aktualisieren Sie die Seite.', 474 errorSavingContract: 'Fehler beim Speichern des Vertrags in der Datenbank', 475 errorGeneratingContract: 'Vertrag konnte nicht erstellt werden. Bitte versuchen Sie es erneut.', 476 errorDownloadingContract: 'Fehler beim Herunterladen des Vertrags. Bitte versuchen Sie es erneut.', 477 errorSendingEmail: 'E-Mail konnte nicht gesendet werden. Bitte versuchen Sie es erneut.', 478 loadingCreatingContract: 'Vertrag wird erstellt...', 479 loadingSavingContract: 'Wird in Datenbank gespeichert...', 480 nextStepsTitle: 'Nächste Schritte:', 481 nextStep1: 'Drucken Sie den Vertrag aus', 482 nextStep2: 'Unterschreiben Sie den Vertrag an der markierten Stelle', 483 nextStep3: 'Verpacken Sie die Kaffeemaschine, Kurier holt sie an Ihrem gewählten Werktag ab' 484 } 485 }; 486 487 return texts[this.language]?.[key] || texts['sk'][key] || key; 488 } 489 490 injectStyles() { 491 if (document.getElementById('coffee-widget-styles')) return; 492 493 const styles = ` 494 .coffee-widget { 495 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 496 background: white; 497 border-radius: 8px; 498 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 499 border: 1px solid #e5e7eb; 500 max-width: 448px; 501 margin: 0 auto; 502 } 503 .coffee-widget-header { 504 background: #0D2518; 505 color: white; 506 padding: 16px; 507 border-radius: 8px 8px 0 0; 508 } 509 .coffee-widget-title { 510 font-size: 18px; 511 font-weight: 600; 512 margin: 0; 513 } 514 .coffee-widget-subtitle { 515 font-size: 14px; 516 color: rgba(255, 255, 255, 0.7); 517 margin: 4px 0 0 0; 518 } 519 .coffee-widget-body { 520 padding: 24px; 521 } 522 .coffee-widget-progress { 523 display: flex; 524 align-items: center; 525 justify-content: space-between; 526 margin-bottom: 24px; 527 } 528 .coffee-widget-step { 529 width: 32px; 530 height: 32px; 531 border-radius: 50%; 532 display: flex; 533 align-items: center; 534 justify-content: center; 535 font-size: 14px; 536 font-weight: 500; 537 } 538 .coffee-widget-step.active { 539 background: #88C540; 540 color: white; 541 } 542 .coffee-widget-step.inactive { 543 background: #e5e7eb; 544 color: #6b7280; 545 } 546 .coffee-widget-arrow { 547 color: #d1d5db; 548 font-size: 16px; 549 } 550 .coffee-widget-arrow.passed { 551 color: #88C540; 552 } 553 .coffee-widget-section-title { 554 font-size: 18px; 555 font-weight: 500; 556 margin-bottom: 16px; 557 color: #0D2518; 558 } 559 .coffee-widget-option { 560 width: 100%; 561 padding: 12px; 562 margin-bottom: 8px; 563 text-align: left; 564 border: 1px solid #d1d5db; 565 border-radius: 8px; 566 background: white; 567 cursor: pointer; 568 transition: all 0.2s; 569 font-size: 14px; 570 } 571 .coffee-widget-option:hover { 572 background: rgba(136, 197, 64, 0.05); 573 border-color: #88C540; 574 } 575 .coffee-widget-option-title { 576 font-weight: 500; 577 margin-bottom: 4px; 578 } 579 .coffee-widget-option-desc { 580 font-size: 12px; 581 color: #6b7280; 582 } 583 .coffee-widget-back { 584 margin-top: 16px; 585 color: #88C540; 586 background: none; 587 border: none; 588 cursor: pointer; 589 font-size: 14px; 590 padding: 0; 591 } 592 .coffee-widget-back:hover { 593 color: #0D2518; 594 } 595 .coffee-widget-price-box { 596 background: rgba(136, 197, 64, 0.1); 597 border: 1px solid rgba(136, 197, 64, 0.3); 598 border-radius: 8px; 599 padding: 24px; 600 text-align: center; 601 margin-bottom: 16px; 602 } 603 .coffee-widget-price-title { 604 font-size: 20px; 605 font-weight: bold; 606 color: #0D2518; 607 margin-bottom: 8px; 608 } 609 .coffee-widget-price { 610 font-size: 36px; 611 font-weight: bold; 612 color: #88C540; 613 margin: 8px 0; 614 } 615 .coffee-widget-price-details { 616 color: rgba(13, 37, 24, 0.8); 617 font-size: 14px; 618 line-height: 1.5; 619 } 620 .coffee-widget-btn-primary { 621 width: 100%; 622 background: #88C540; 623 color: white; 624 border: none; 625 padding: 16px; 626 border-radius: 8px; 627 font-weight: 600; 628 font-size: 16px; 629 cursor: pointer; 630 margin-bottom: 12px; 631 transition: all 0.2s; 632 display: flex; 633 align-items: center; 634 justify-content: center; 635 gap: 8px; 636 } 637 .coffee-widget-btn-primary:hover { 638 background: #76b032; 639 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 640 } 641 .coffee-widget-btn-primary:disabled { 642 background: #d1d5db;
643 cursor: not-allowed; 644 opacity: 0.5; 645 } 646 .coffee-widget-btn-secondary { 647 width: 100%; 648 background: white; 649 color: #0D2518; 650 border: 2px solid #88C540; 651 padding: 16px; 652 border-radius: 8px; 653 font-weight: 600; 654 font-size: 16px; 655 cursor: pointer; 656 transition: all 0.2s; 657 display: flex; 658 align-items: center; 659 justify-content: center; 660 gap: 8px; 661 margin-bottom: 12px; 662 } 663 .coffee-widget-btn-secondary:hover { 664 background: #f9fafb; 665 } 666 .coffee-widget-btn-secondary:disabled { 667 opacity: 0.5; 668 cursor: not-allowed; 669 } 670 .coffee-widget-btn-tertiary { 671 width: 100%; 672 background: #f3f4f6; 673 color: #374151; 674 border: none; 675 padding: 16px; 676 border-radius: 8px; 677 font-weight: 600; 678 font-size: 16px; 679 cursor: pointer; 680 transition: all 0.2s; 681 display: flex; 682 align-items: center; 683 justify-content: center; 684 gap: 8px; 685 } 686 .coffee-widget-btn-tertiary:hover { 687 background: #e5e7eb; 688 } 689 .coffee-widget-form-row { 690 display: grid; 691 grid-template-columns: 1fr 1fr; 692 gap: 16px; 693 } 694 .coffee-widget-form-group { 695 margin-bottom: 20px; 696 } 697 .coffee-widget-label { 698 display: block; 699 font-size: 14px; 700 font-weight: 500; 701 margin-bottom: 8px; 702 color: #374151; 703 } 704 .coffee-widget-input { 705 width: 100%; 706 padding: 12px 16px; 707 border: 1px solid #d1d5db; 708 border-radius: 8px; 709 font-size: 14px; 710 box-sizing: border-box; 711 transition: all 0.2s; 712 } 713 .coffee-widget-input:focus { 714 outline: none; 715 border-color: #88C540; 716 box-shadow: 0 0 0 3px rgba(136, 197, 64, 0.1); 717 } 718 .coffee-widget-input.error { 719 border-color: #ef4444; 720 } 721 .coffee-widget-input-hint { 722 font-size: 12px; 723 color: #6b7280; 724 margin-top: 4px; 725 } 726 .coffee-widget-error { 727 color: #ef4444; 728 font-size: 12px; 729 margin-top: 4px; 730 } 731 .coffee-widget-checkbox-group { 732 background: #f9fafb; 733 padding: 16px; 734 border-radius: 8px; 735 margin-bottom: 20px; 736 } 737 .coffee-widget-checkbox-label { 738 display: flex; 739 align-items: flex-start; 740 gap: 12px; 741 cursor: pointer; 742 } 743 .coffee-widget-checkbox { 744 width: 20px; 745 height: 20px; 746 margin-top: 2px; 747 cursor: pointer; 748 accent-color: #88C540; 749 } 750 .coffee-widget-checkbox-text { 751 font-size: 14px; 752 color: #374151; 753 line-height: 1.5; 754 } 755 .coffee-widget-loading { 756 text-align: center; 757 padding: 40px 20px; 758 } 759 .coffee-widget-spinner { 760 border: 3px solid #f3f3f3; 761 border-top: 3px solid #88C540; 762 border-radius: 50%; 763 width: 32px; 764 height: 32px; 765 animation: coffee-widget-spin 1s linear infinite; 766 margin: 0 auto 16px; 767 } 768 .coffee-widget-contract-scroll { 769 background: white; 770 border: 1px solid #e5e7eb; 771 border-radius: 8px; 772 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 773 max-height: 500px; 774 overflow-y: auto; 775 margin-bottom: 16px; 776 } 777 .coffee-widget-contract-inner { 778 padding: 32px; 779 } 780 .coffee-widget-contract-logo { 781 width: 64px; 782 height: 64px; 783 border-radius: 8px; 784 display: block; 785 margin: 0 auto 16px; 786 } 787 .coffee-widget-contract-header { 788 text-align: center; 789 border-bottom: 1px solid #e5e7eb; 790 padding-bottom: 24px; 791 margin-bottom: 24px; 792 } 793 .coffee-widget-contract-title { 794 font-size: 24px; 795 font-weight: bold; 796 color: #0D2518; 797 margin: 0 0 8px 0; 798 } 799 .coffee-widget-contract-subtitle { 800 color: #6b7280; 801 font-size: 14px; 802 margin: 0; 803 } 804 .coffee-widget-contract-section { 805 margin-bottom: 24px; 806 } 807 .coffee-widget-contract-section-title { 808 font-size: 18px; 809 font-weight: bold; 810 color: #0D2518; 811 text-align: center; 812 margin: 0 0 4px 0; 813 } 814 .coffee-widget-contract-section-subtitle { 815 font-size: 18px; 816 font-weight: bold; 817 color: #0D2518; 818 text-align: center; 819 margin: 0 0 12px 0; 820 } 821 .coffee-widget-contract-party-title { 822 font-weight: 600; 823 color: #111827; 824 margin-bottom: 8px; 825 } 826 .coffee-widget-contract-text { 827 font-size: 14px; 828 color: #374151; 829 line-height: 1.6; 830 margin: 0 0 8px 0; 831 } 832 .coffee-widget-contract-text-center { 833 text-align: center; 834 font-weight: 600; 835 } 836 .coffee-widget-contract-text-bold { 837 font-weight: bold; 838 } 839 .coffee-widget-contract-text-indent { 840 margin-left: 16px; 841 } 842 .coffee-widget-contract-text-price { 843 font-weight: bold; 844 color: #88C540; 845 } 846 .coffee-widget-contract-signatures { 847 display: grid; 848 grid-template-columns: 1fr 1fr; 849 gap: 32px; 850 padding-top: 32px; 851 border-top: 1px solid #e5e7eb; 852 margin-top: 32px; 853 } 854 .coffee-widget-signature-block { 855 856 } 857 .coffee-widget-signature-date { 858 font-size: 14px; 859 color: #374151; 860 margin-bottom: 48px; 861 } 862 .coffee-widget-signature-line { 863 border-top: 1px solid #d1d5db; 864 padding-top: 8px; 865 } 866 .coffee-widget-signature-label { 867 font-size: 14px; 868 font-weight: 600; 869 color: #374151; 870 } 871 .coffee-widget-signature-company { 872 font-size: 14px; 873 color: #6b7280; 874 } 875 .coffee-widget-alert-error { 876 background: #fef2f2; 877 border: 1px solid #fecaca; 878 color: #991b1b; 879 padding: 12px 16px; 880 border-radius: 8px; 881 margin-bottom: 16px; 882 font-size: 14px; 883 } 884 .coffee-widget-alert-success { 885 background: #f0fdf4; 886 border: 1px solid #bbf7d0; 887 color: #166534; 888 padding: 12px 16px; 889 border-radius: 8px; 890 margin-bottom: 16px; 891 font-size: 14px; 892 } 893 .coffee-widget-info-box { 894 background: linear-gradient(135deg, #88C540 0%, #76b032 100%); 895 border: 2px solid #76b032; 896 border-radius: 12px; 897 padding: 20px; 898 margin-top: 16px; 899 box-shadow: 0 4px 12px rgba(136, 197, 64, 0.3); 900 } 901 .coffee-widget-info-title { 902 font-size: 16px; 903 font-weight: 700; 904 color: white; 905 margin: 0 0 12px 0; 906 display: flex; 907 align-items: center; 908 gap: 8px; 909 } 910 .coffee-widget-info-title:before { 911 content: "â ï¸"; 912 font-size: 20px; 913 } 914 .coffee-widget-info-list { 915 font-size: 14px; 916 color: white; 917 margin: 0; 918 padding-left: 24px; 919 line-height: 1.8; 920 } 921 .coffee-widget-info-list li { 922 margin-bottom: 8px; 923 font-weight: 500; 924 } 925 .coffee-widget-info-list li:last-child { 926 margin-bottom: 0; 927 } 928 .coffee-widget-form-note { 929 font-size: 12px; 930 color: #6b7280; 931 text-align: center; 932 margin-top: 16px; 933 } 934 .coffee-widget-header-row { 935 display: flex; 936 align-items: center; 937 gap: 12px; 938 margin-bottom: 24px; 939 } 940 .coffee-widget-header-back { 941 padding: 8px; 942 background: none; 943 border: none; 944 cursor: pointer; 945 border-radius: 8px; 946 display: flex; 947 align-items: center; 948 justify-content: center; 949 } 950 .coffee-widget-header-back:hover { 951 background: #f3f4f6; 952 } 953 .coffee-widget-header-content h2 { 954 font-size: 24px; 955 font-weight: bold; 956 color: #0D2518; 957 margin: 0; 958 } 959 .coffee-widget-header-content p { 960 font-size: 14px; 961 color: #6b7280; 962 margin: 4px 0 0 0; 963 } 964 @keyframes coffee-widget-spin { 965 0% { transform: rotate(0deg); } 966 100% { transform: rotate(360deg); } 967 } 968 @media (max-width: 480px) { 969 .coffee-widget-form-row { 970 grid-template-columns: 1fr; 971 } 972 .coffee-widget-contract-signatures { 973 grid-template-columns: 1fr; 974 } 975 } 976 `; 977 978 const styleSheet = document.createElement('style'); 979 styleSheet.id = 'coffee-widget-styles'; 980 styleSheet.textContent = styles; 981 document.head.appendChild(styleSheet); 982 } 983 984 render() { 985 const container = document.getElementById(this.containerId); 986 if (!container) return; 987 988 container.innerHTML = ` 989 <div class="coffee-widget"> 990 <div class="coffee-widget-header"> 991 <div style="display: flex; align-items: center; gap: 8px;"> 992 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 8h1a4 4 0 1 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/><line x1="6" x2="6" y1="2" y2="4"/><line x1="10" x2="10" y1="2" y2="4"/><line x1="14" x2="14" y1="2" y2="4"/></svg> 993 <h2 class="coffee-widget-title">${this.getUIText('widgetTitle')}</h2> 994 </div> 995 <p class="coffee-widget-subtitle">${this.getUIText('widgetSubtitle')}</p> 996 </div> 997 <div class="coffee-widget-body"> 998 ${this.renderProgress()} 999 ${this.renderCurrentStep()} 1000 </div> 1001 </div> 1002 `; 1003 1004 this.attachEventListeners(); 1005 } 1006 1007 renderProgress() { 1008 if (this.step >= 5) return ''; 1009 1010 return ` 1011 <div class="coffee-widget-progress"> 1012 <div class="coffee-widget-step ${this.step >= 1 ? 'active' : 'inactive'}">1</div>
1013 <span class="coffee-widget-arrow ${this.step > 1 ? 'passed' : ''}"> 1014 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg> 1015 </span> 1016 <div class="coffee-widget-step ${this.step >= 2 ? 'active' : 'inactive'}">2</div> 1017 <span class="coffee-widget-arrow ${this.step > 2 ? 'passed' : ''}"> 1018 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg> 1019 </span> 1020 <div class="coffee-widget-step ${this.step >= 3 ? 'active' : 'inactive'}">3</div> 1021 <span class="coffee-widget-arrow ${this.step > 3 ? 'passed' : ''}"> 1022 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg> 1023 </span> 1024 <div class="coffee-widget-step ${this.step >= 4 ? 'active' : 'inactive'}">
10244</div> 1025 </div> 1026 `; 1027 } 1028 1029 renderCurrentStep() { 1030 if (this.isLoading) { 1031 return this.renderLoading(); 1032 } 1033 1034 switch (this.step) { 1035 case 1: 1036 return this.renderBrandSelection(); 1037 case 2: 1038 return this.renderTypeSelection(); 1039 case 3: 1040 return this.renderDefectSelection(); 1041 case 4: 1042 return this.renderConditionSelection(); 1043 case 5: 1044 return this.renderFinalPrice(); 1045 case 6: 1046 return this.renderSellerForm(); 1047 case 7: 1048 return this.renderContractPreview(); 1049 default: 1050 return ''; 1051 } 1052 } 1053 1054 renderLoading() { 1055 return ` 1056 <div class="coffee-widget-loading"> 1057 <div class="coffee-widget-spinner"></div> 1058 <p style="color: #6b7280;">${this.loadingStatus || this.getUIText('loading')}</p> 1059 </div> 1060 `; 1061 } 1062 1063 renderBrandSelection() { 1064 return ` 1065 <div> 1066 <h3 class="coffee-widget-section-title">${this.getUIText('selectBrand')}</h3> 1067 <div> 1068 ${this.data.brands.map(brand => { 1069 const translated = this.getTranslation('brand', brand.id, brand.name); 1070 return `<button class="coffee-widget-option" data-brand-id="${brand.id}">${translated.name}</button>`; 1071 }).join('')} 1072 </div> 1073 </div> 1074 `; 1075 } 1076 1077 renderTypeSelection() { 1078 const availableTypes = this.data.types.filter(type => type.brand_id === this.selectedBrand.id); 1079 const translatedBrand = this.getTranslation('brand', this.selectedBrand.id, this.selectedBrand.name); 1080 return ` 1081 <div> 1082 <h3 class="coffee-widget-section-title">${this.getUIText('selectType')}</h3> 1083 <p style="color: #6b7280; margin-bottom: 16px;">${this.getUIText('brand')}: <strong>${translatedBrand.name}</strong></p> 1084 <div> 1085 ${availableTypes.map(type => { 1086 const translated = this.getTranslation('machine_type', type.id, type.name); 1087 return `<button class="coffee-widget-option" data-type-id="${type.id}">${translated.name}</button>`; 1088 }).join('')} 1089 </div> 1090 <button class="coffee-widget-back" onclick="coffeeWidget.goBack()"> 1091 <span style="margin-right: 4px;">←</span> ${this.getUIText('back')} 1092 </button> 1093 </div> 1094 `; 1095 } 1096 1097 renderDefectSelection() { 1098 const translatedBrand = this.getTranslation('brand', this.selectedBrand.id, this.selectedBrand.name); 1099 const translatedType = this.getTranslation('machine_type', this.selectedType.id, this.selectedType.name); 1100 return ` 1101 <div> 1102 <h3 class="coffee-widget-section-title">${this.getUIText('selectDefect')}</h3> 1103 <p style="color: #6b7280; margin-bottom: 16px;"> 1104 ${translatedBrand.name} - ${translatedType.name} 1105 </p> 1106 <div> 1107 ${this.data.defects.map(defect => { 1108 const translated = this.getTranslation('defect', defect.id, defect.name); 1109 return `<button class="coffee-widget-option" data-defect-id="${defect.id}"> 1110 <span>${translated.name}</span> 1111 </button>`; 1112 }).join('')} 1113 </div> 1114 <button class="coffee-widget-back" onclick="coffeeWidget.goBack()"> 1115 <span style="margin-right: 4px;">←</span> ${this.getUIText('back')} 1116 </button> 1117 </div> 1118 `; 1119 } 1120 1121 renderConditionSelection() { 1122 const translatedBrand = this.getTranslation('brand', this.selectedBrand.id, this.selectedBrand.name); 1123 const translatedType = this.getTranslation('machine_type', this.selectedType.id, this.selectedType.name); 1124 return ` 1125 <div> 1126 <h3 class="coffee-widget-section-title">${this.getUIText('selectCondition')}</h3> 1127 <p style="color: #6b7280; margin-bottom: 16px;"> 1128 ${translatedBrand.name} - ${translatedType.name} 1129 </p> 1130 <div> 1131 ${this.data.conditions.map(condition => { 1132 const translated = this.getTranslation('condition', condition.id, condition.name, condition.description); 1133 return `<button class="coffee-widget-option" data-condition-id="${c
1133ondition.id}"> 1134 <div class="coffee-widget-option-title">${translated.name}</div> 1135 ${translated.description ? `<div class="coffee-widget-option-desc">${translated.description}</div>` : ''} 1136 </button>`; 1137 }).join('')} 1138 </div> 1139 <button class="coffee-widget-back" onclick="coffeeWidget.goBack()"> 1140 <span style="margin-right: 4px;">←</span> ${this.getUIText('back')} 1141 </button> 1142 </div> 1143 `; 1144 } 1145 1146 renderFinalPrice() { 1147 const translatedBrand = this.getTranslation('brand', this.selectedBrand.id, this.selectedBrand.name); 1148 const translatedType = this.getTranslation('machine_type', this.selectedType.id, this.selectedType.name); 1149 const translatedDefect = this.getTranslation('defect', this.selectedDefect.id, this.selectedDefect.name); 1150 const translatedCondition = this.getTranslation('condition', this.selectedCondition.id, this.selectedCondition.name); 1151 1152 return ` 1153 <div style="text-align: center;"> 1154 <div class="coffee-widget-price-box"> 1155 <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#88C540" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin: 0 auto 8px;"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg> 1156 <div class="coffee-widget-price-title">${this.getUIText('finalPrice')}</div> 1157 <div class="coffee-widget-price">${this.formatPrice(this.finalPrice)}</div> 1158 <div class="coffee-widget-price-details"> 1159 ${translatedBrand.name} - ${translatedType.name}<br> 1160 ${this.getUIText('defect')}: ${translatedDefect.name}<br> 1161 ${this.getUIText('condition')}: ${translatedCondition.name} 1162 </div> 1163 </div> 1164 <button class="coffee-widget-btn-primary" onclick="coffeeWidget.goToSellerForm()"> 1165 ${this.getUIText('continueToForm')} 1166 </button> 1167 <button class="coffee-widget-btn-secondary" onclick="coffeeWidget.reset()" style="border-color: #d1d5db; color: #374151;"> 1168 ${this.getUIText('newCalculation')} 1169 </button> 1170 </div> 1171 `; 1172 } 1173 1174 getMinSelectableDate() { 1175 const tomorrow = new Date(); 1176 tomorrow.setDate(tomorrow.getDate() + 1); 1177 while (tomorrow.getDay() === 0 || tomorrow.getDay() === 6) { 1178 tomorrow.setDate(tomorrow.getDate() + 1); 1179 } 1180 const year = tomorrow.getFullYear();
1181 const month = String(tomorrow.getMonth() + 1).padStart(2, '0'); 1182 const day = String(tomorrow.getDate()).padStart(2, '0'); 1183 return `${year}-${month}-${day}`; 1184 } 1185 1186 renderSellerForm() { 1187 const minDate = this.getMinSelectableDate(); 1188 const pickupDateValue = this.sellerData.pickupDate || minDate; 1189 1190 const renderError = (field) => { 1191 return this.formErrors[field] ? `<div class="coffee-widget-error">${this.formErrors[field]}</div>` : ''; 1192 }; 1193 1194 const inputClass = (field) => { 1195 return `coffee-widget-input${this.formErrors[field] ? ' error' : ''}`; 1196 }; 1197 1198 return ` 1199 <div> 1200 <div class="coffee-widget-header-row"> 1201 <button class="coffee-widget-header-back" onclick="coffeeWidget.goBack()"> 1202 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg> 1203 </button> 1204 <div class="coffee-widget-header-content"> 1205 <h2>${this.getUIText('sellerInfo')}</h2> 1206 <p>${this.getUIText('sellerInfoSubtitle')}</p> 1207 </div> 1208 </div> 1209 1210 ${this.errorMessage ? `<div class="coffee-widget-alert-error">${this.errorMessage}</div>` : ''} 1211 1212 <form id="seller-form"> 1213 <div class="coffee-widget-form-row"> 1214 <div class="coffee-widget-form-group"> 1215 <label class="coffee-widget-label">${this.getUIText('firstName')} *</label> 1216 <input type="text" class="${inputClass('firstName')}" id="firstName" value="${this.sellerData.firstName}" placeholder="${this.language === 'cs' ? 'Jan' : 'Ján'}"> 1217 ${renderError('firstName')} 1218 </div> 1219 <div class="coffee-widget-form-group"> 1220 <label class="coffee-widget-label">${this.getUIText('lastName')} *</label> 1221 <input type="text" class="${inputClass('lastName')}" id="lastName" value="${this.sellerData.lastName}" placeholder="${this.language === 'cs' ? 'Novák' : 'Novák'}"> 1222 ${renderError('lastName')} 1223 </div> 1224 </div> 1225 1226 <div class="coffee-widget-form-group"> 1227 <label class="coffee-widget-label">${this.getUIText('street')} *</label> 1228 <input type="text" class="${inputClass('street')}" id="street" value="${this.sellerData.street}" placeholder="${this.language === 'cs' ? 'Hlavnà 123' : 'Hlavná 123'}"> 1229 ${renderError('street')} 1230 </div> 1231 1232 <div class="coffee-widget-form-row"> 1233 <div class="coffee-widget-form-group"> 1234 <label class="coffee-widget-label">${this.getUIText('city')} *</label> 1235 <input type="text" class="${inputClass('city')}" id="city" value="${this.sellerData.city}" placeholder="${this.language === 'cs' ? 'Praha' : 'Bratislava'}"> 1236 ${renderError('city')} 1237 </div> 1238 <div class="coffee-widget-form-group"> 1239 <label class="coffee-widget-label">${this.getUIText('postalCode')} *</label> 1240 <input type="text" class="${inputClass('postalCode')}" id="postalCode" value="${this.sellerData.postalCode}" placeholder="${this.language === 'cs' ? '110 00' : '811 01'}"> 1241 ${renderError('postalCode')} 1242 </div> 1243 </div> 1244 1245 <div class="coffee-widget-form-group"> 1246 <label class="coffee-widget-label">${this.getUIText('country')} *</label> 1247 <input type="text" class="${inputClass('country')}" id="country" value="${this.sellerData.country}" placeholder="${this.language === 'cs' ? 'Äesko' : 'Slovensko'}"> 1248 ${renderError('country')} 1249 </div> 1250 1251 <div class="coffee-widget-form-group"> 1252 <label class="coffee-widget-label">${this.getUIText('phone')} *</label> 1253 <input type="tel" class="${inputClass('phone')}" id="phone" value="${this.sellerData.phone}" placeholder="${this.language === 'cs' ? '+420 601 123 456' : '+421 910 123 456'}"> 1254 ${renderError('phone')} 1255 </div> 1256 1257 <div class="coffee-widget-form-group"> 1258 <label class="coffee-widget-label">${this.getUIText('email')} *</label> 1259 <input type="email" class="${inputClass('email')}" id="email" value="${this.sellerData.email}" placeholder="[email protected]"> 1260 ${renderError('email')} 1261 </div> 1262 1263 <div class="coffee-widget-form-group"> 1264 <label class="coffee-widget-label">${this.getUIText('iban')} *</label> 1265 <input type="text" class="${inputClass('iban')}" id="iban" value="${this.sellerData.iban}" placeholder="${this.language === 'cs' ? 'CZ65 0800 0000 1920 0014 5399' : 'SK31 1200 0000 1987 4263 7541'}"> 1266 ${renderError('iban')} 1267 </div> 1268 1269 <div class="coffee-widget-form-group"> 1270 <label class="coffee-widget-label">${this.getUIText('pickupDate')} *</label> 1271 <input type="date" class="${inputClass('pickupDate')}
1271" id="pickupDate" value="${pickupDateValue}" min="${minDate}"> 1272 ${renderError('pickupDate')} 1273 </div> 1274 1275 <div class="coffee-widget-checkbox-group"> 1276 <label class="coffee-widget-checkbox-label"> 1277 <input type="checkbox" class="coffee-widget-checkbox" id="gdprConsent" ${this.sellerData.gdprConsent ? 'checked' : ''}> 1278 <span class="coffee-widget-checkbox-text"> 1279 ${this.getUIText('gdprConsent')} * 1280 </span> 1281 </label> 1282 ${renderError('gdprConsent')} 1283 </div> 1284 1285 <button type="submit" class="coffee-widget-btn-primary"> 1286 ${this.getUIText('generateContract')} 1287 </button> 1288 </form> 1289 </div> 1290 `; 1291 } 1292 1293 formatPrice(price) { 1294 const locale = this.language === 'cs' ? 'cs-CZ' : 'sk-SK'; 1295 const currency = this.currency; 1296 1297 return new Intl.NumberFormat(locale, { 1298 style: 'currency', 1299 currency: currency, 1300 minimumFractionDigits: 0, 1301 maximumFractionDigits: 0, 1302 }).format(price); 1303 } 1304 1305 formatDate(date = new Date()) { 1306 const day = String(date.getDate()).padStart(2, '0');
1307 const month = String(date.getMonth() + 1).padStart(2, '0'); 1308 const year = date.getFullYear(); 1309 return `${day}.${month}.${year}`; 1310 } 1311 1312 renderContractPreview() { 1313 if (!this.contractData) { 1314 return `<div class="coffee-widget-alert-error">${this.getUIText('errorNoData')}</div>`; 1315 } 1316 1317 const { seller, machine, contractNumber, contractDate } = this.contractData; 1318 1319 return ` 1320 <div> 1321 <div class="coffee-widget-header-row"> 1322 <button class="coffee-widget-header-back" onclick="coffeeWidget.goBack()"> 1323 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg> 1324 </button> 1325 <div class="coffee-widget-header-content"> 1326 <h2>${this.getUIText('contractPreview')}</h2> 1327 <p>${this.getUIText('contractPreviewSubtitle')}</p> 1328 </div> 1329 </div> 1330 1331 ${this.errorMessage ? `<div class="coffee-widget-alert-error">${this.errorMessage}</div>` : ''} 1332 ${this.downloadSuccess ? `<div class="coffee-widget-alert-success">${this.getUIText('contractSuccess')}</div>` : ''} 1333 ${this.emailSuccess ? `<div class="coffee-widget-alert-success">${this.getUIText('emailSuccess')}</div>` : ''} 1334 1335 <div class="coffee-widget-contract-scroll"> 1336 <div class="coffee-widget-contract-inner"> 1337 <img src="/brewive_square.png" alt="Brewive" class="coffee-widget-contract-logo" onerror="this.style.display='none'"> 1338 1339 <div class="coffee-widget-contract-header"> 1340 <h1 class="coffee-widget-contract-title">KUPNà SMLOUVA</h1> 1341 <p class="coffee-widget-contract-subtitle">na prodej movité vÄci</p> 1342 </div> 1343 1344 <p class="coffee-widget-contract-text coffee-widget-contract-text-center" style="margin-bottom: 16px;"> 1345 podle ustanovenà § 2079 a následujÃcÃch obÄanského zákonÃku 1346 </p> 1347 1348 <p class="coffee-widget-contract-text coffee-widget-contract-text-center" style="margin-bottom: 24px;"> 1349 mezi 1350 </p> 1351 1352 <div class="coffee-widget-contract-section"> 1353 <div class="coffee-widget-contract-party-title">1. ProdávajÃcÃ:</div> 1354 <p class="coffee-widget-contract-text">Jméno a pÅÃjmenÃ: ${seller.firstName} ${seller.lastName}</p> 1355 <p class="coffee-widget-contract-text">BydliÅ¡tÄ: ${seller.street}, ${seller.postalCode} ${seller.city}, ${seller.country}</p> 1356 <p class="coffee-widget-contract-text">Telefon: ${seller.phone}</p> 1357 <p class="coffee-widget-contract-text">E-mail: ${seller.email}</p> 1358 <p class="coffee-widget-contract-text">IBAN: ${seller.iban}</p> 1359 <p class="coffee-widget-contract-text" style="font-style: italic;">(dále také jen "prodávajÃcÃ")</p> 1360 </div> 1361 1362 <p class="coffee-widget-contract-text coffee-widget-contract-text-center" style="margin: 16px 0;">a</p> 1363 1364 <div class="coffee-widget-contract-section"> 1365 <div class="coffee-widget-contract-party-title">2. KupujÃcÃ:</div> 1366 <p class="coffee-widget-contract-text">Brewive s.r.o.</p> 1367 <p class="coffee-widget-contract-text">se sÃdlem: JiÄÃnská 226/17, 130 00 Praha â Žižkov</p> 1368 <p class="coffee-widget-contract-text">IÄO: 23032189</p> 1369 <p class="coffee-widget-contract-text">DIÄ: CZ23032189</p> 1370 <p class="coffee-widget-contract-text">zastoupená: Lukášem Bojkovským, jednatelem</p> 1371 <p class="coffee-widget-contract-text">kontakt: [email protected]</p> 1372 <p class="coffee-widget-contract-text" style="font-style: italic;">(dále také jen "kupujÃcÃ")</p> 1373 </div> 1374 1375 <p class="coffee-widget-contract-text coffee-widget-contract-text-center" style="margin: 16px 0;"> 1376 spoleÄnÄ také "smluvnà strany" 1377 </p> 1378 1379 <div class="coffee-widget-contract-section"> 1380 <h3 class="coffee-widget-contract-section-title">I.</h3> 1381 <h3 class="coffee-widget-contract-section-subtitle">Ãvodnà ustanovenÃ</h3> 1382 <p class="coffee-widget-contract-text">1. ProdávajÃcà prohlaÅ¡uje, že je vlastnÃkem:</p> 1383 <p class="coffee-widget-contract-text coffee-widget-contract-text-indent coffee-widget-contract-text-bold">${machine.brand} ${machine.type}</p> 1384 <p class="coffee-widget-contract-text">(dále jen "pÅedmÄt koupÄ").</p> 1385 <p class="coffee-widget-contract-text">2. KupujÃcà prohlaÅ¡uje, že si pÅedmÄt koupÄ ÅádnÄ prohlédl a vyzkouÅ¡el, a že byl seznámen s jeho stavem.</p> 1386 </div> 1387 1388 <div class="coffee-widget-contract-section"> 1389 <h3 class="coffee-widget-contract-section-title">II.</h3> 1390 <h3 class="coffee-widget-contract-section-subtitle">PÅedmÄt smlouvy</h3> 1391 <p class="coffee-widget-contract-text">1. ProdávajÃcà se touto smlouvou zavazuje pÅedmÄt koupÄ kupujÃcÃmu odevzdat.</p> 1392 <p class="coffee-widget-contract-text">2. KupujÃcà se touto smlouvou zavazuje pÅedmÄt koupÄ pÅevzÃt a zaplatit za nÄj kupnà cenu sjednanou v Älánku IV. této smlouvy.</p> 1393 </div> 1394 1395 <div class="coffee-widget-contract-section"> 1396 <h3 class="coffee-widget-contract-section-title">III.</h3> 1397 <h3 class="coffee-widget-contract-section-subtitle">TermÃn a mÃsto odevzdánà a pÅevzetà pÅedmÄtu koupÄ, pÅechod vlastnického práva</h3> 1398 <p class="coffee-widget-contract-text">1. ProdávajÃcà odevzdá kupujÃcÃmu pÅedmÄt koupÄ bezprostÅednÄ po podpisu této smlouvy, pÅÃpadnÄ doruÄenÃm na adresu sÃdla kupujÃcÃho.</p> 1399 <p class="coffee-widget-contract-text">2. Vlastnické právo k pÅedmÄtu koupÄ kupujÃcà nabývá již samotnou úÄinnostà smlouvy.</p> 1400 </div> 1401 1402 <div class="coffee-widget-contract-section"> 1403 <h3 class="coffee-widget-contract-section-title">IV.</h3> 1404 <h3 class="coffee-widget-contract-section-subtitle">Cena a způsob úhrady</h3> 1405 <p class="coffee-widget-contract-text">1. Kupnà cena byla dle dohody prodávajÃcÃho a kupujÃcÃho stanovena ve výši:</p> 1406 <p class="coffee-widget-contract-text coffee-widget-contract-text-indent coffee-widget-contract-text-price">${this.formatPrice(machine.price)}</p> 1407 <p class="coffee-widget-contract-text">2. KupujÃcà se zavazuje uhradit kupnà cenu bezhotovostnÃm pÅevodem na bankovnà úÄet prodávajÃcÃho (IBAN: ${seller.iban}), a to do 7 pracovnÃch dnů od doruÄenà pÅedmÄtu koupÄ na adresu sÃdla kupujÃcÃho.</p> 1408 </div> 1409 1410 <div class="coffee-widget-contract-section"> 1411 <h3 class="coffee-widget-contract-section-title">V.</h3> 1412 <h3 class="coffee-widget-contract-section-subtitle">OdpovÄdnost za vady</h3> 1413 <p class="coffee-widget-contract-text">1. ProdávajÃcà výslovnÄ upozorÅuje na následujÃcà vady pÅedmÄtu koupÄ:</p> 1414 <p class="coffee-widget-contract-text coffee-widget-contract-text-indent">Stav: ${machine.condition}</p> 1415 <p class="coffee-widget-contract-text coffee-widget-contract-text-indent">Závada: ${machine.defect}</p> 1416 <p class="coffee-widget-contract-text">2. ProdávajÃcà prohlaÅ¡uje, že pÅedmÄt koupÄ nemá žádné právnà vady a nenà zatÞen právy tÅetÃch osob.</p> 1417 <p class="coffee-widget-contract-text">3. V pÅÃpadÄ nepodstatného poruÅ¡enà smlouvy má kupujÃcà právo na pÅimÄÅenou slevu ze sjednané ceny odpovÃdajÃcà rozsahu a povaze vady. Jde-li o podstatné poruÅ¡enà smlouvy, má kupujÃcà právo od smlouvy odstoupit.</p> 1418 <p class="coffee-widget-contract-text">4. KupujÃcà prohlaÅ¡uje, že se ÅádnÄ seznámil se stavem pÅedmÄtu koupÄ, jakož i vadami uvedenými v odstavci 1 tohoto Älánku smlouvy.</p> 1419 </div> 1420 1421 <div class="coffee-widget-contract-section"> 1422 <h3 class="coffee-widget-contract-section-title">VI.</h3> 1423 <h3 class="coffee-widget-contract-section-subtitle">ZávÄreÄná ustanovenÃ</h3> 1424 <p class="coffee-widget-contract-text">1. Práva a povinnosti neupravené touto smlouvou se ÅÃdà ustanovenÃmi obÄanského zákonÃku a právnÃmi pÅedpisy souvisejÃcÃmi.</p> 1425 <p class="coffee-widget-contract-text">
14252. ZmÄny a doplÅky této smlouvy je možné Äinit pouze po dohodÄ smluvnÃch stran formou pÃsemných vzestupnÄ ÄÃslovaných dodatků.</p> 1426 <p class="coffee-widget-contract-text">3. Smlouva byla vypracována ve dvou vyhotovenÃch, z nichž každý z úÄastnÃků obdržà jedno.</p> 1427 <p class="coffee-widget-contract-text">4. Smluvnà strany prohlaÅ¡ujÃ, že si tuto smlouvu pÅed jejÃm podpisem pÅeÄetly, že byla uzavÅena po vzájemném projednánà podle jejich pravé a svobodné vůle, urÄitÄ, vážnÄ a srozumitelnÄ, nikoli v tÃsni za nápadnÄ nevýhodných podmÃnek.</p> 1428 </div> 1429 1430 <div class="coffee-widget-contract-signatures"> 1431 <div class="coffee-widget-signature-block"> 1432 <p class="coffee-widget-signature-date">V ${seller.city} dne ${contractDate}</p> 1433 <div class="coffee-widget-signature-line"> 1434 <p class="coffee-widget-signature-label">ProdávajÃcÃ</p> 1435 <p class="coffee-widget-signature-company">${seller.firstName} ${seller.lastName}</p> 1436 </div> 1437 </div> 1438 <div class="coffee-widget-signature-block"> 1439 <p class="coffee-widget-signature-date">V Praze dne ${contractDate}</p> 1440 <div class="coffee-widget-signature-line"> 1441 <p class="coffee-widget-signature-label">KupujÃcÃ</p> 1442 <p class="coffee-widget-signature-company">Brewive s.r.o.</p> 1443 <p class="coffee-widget-signature-company">Lukáš Bojkovský, jednatel</p> 1444 </div> 1445 </div> 1446 </div> 1447 </div> 1448 </div> 1449 1450 <div style="display: flex; flex-direction: column; gap: 12px;"> 1451 <button class="coffee-widget-btn-primary" onclick="coffeeWidget.downloadContract()" ${this.isDownloading ? 'disabled' : ''}> 1452 ${this.isDownloading ? ` 1453 <svg class="coffee-widget-spinner" style="width: 20px; height: 20px; margin: 0;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg> 1454 ${this.getUIText('downloading')} 1455 ` : ` 1456 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg> 1457 ${this.getUIText('downloadContract')} 1458 `} 1459 </button> 1460 1461 <button class="coffee-widget-btn-tertiary" onclick="coffeeWidget.reset()"> 1462 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg> 1463 ${this.getUIText('newCalculation')} 1464 </button> 1465 </div> 1466 1467 <div class="coffee-widget-info-box"> 1468 <p class="coffee-widget-info-title">${this.getUIText('nextStepsTitle')}</p> 1469 <ol class="coffee-widget-info-list"> 1470 <li>${this.getUIText('nextStep1')}</li> 1471 <li>${this.getUIText('nextStep2')}</li> 1472 <li>${this.getUIText('nextStep3')}</li> 1473 </ol> 1474 </div> 1475 </div> 1476 `; 1477 } 1478 1479 attachEventListeners() { 1480 document.querySelectorAll('[data-brand-id]').forEach(btn => { 1481 btn.addEventListener('click', (e) => { 1482 const brandId = e.currentTarget.dataset.brandId; 1483 this.selectedBrand = this.data.brands.find(b => b.id === brandId); 1484 this.step = 2; 1485 this.render(); 1486 }); 1487 }); 1488 1489 document.querySelectorAll('[data-type-id]').forEach(btn => { 1490 btn.addEventListener('click', (e) => { 1491 const typeId = e.currentTarget.dataset.typeId; 1492 this.selectedType = this.data.types.find(t => t.id === typeId); 1493 this.step = 3; 1494 this.render(); 1495 }); 1496 }); 1497 1498 document.querySelectorAll('[data-defect-id]').forEach(btn => { 1499 btn.addEventListener('click', (e) => { 1500 const defectId = e.currentTarget.dataset.defectId; 1501 this.selectedDefect = this.data.defects.find(d => d.id === defectId); 1502 this.step = 4; 1503 this.render(); 1504 }); 1505 }); 1506 1507 document.querySelectorAll('[data-condition-id]').forEach(btn => { 1508 btn.addEventListener('click', (e) => { 1509 const conditionId = e.currentTarget.dataset.conditionId; 1510 this.selectedCondition = this.data.conditions.find(c => c.id === conditionId); 1511 this.calculatePrice(); 1512 }); 1513 }); 1514 1515 const sellerForm = document.getElementById('seller-form'); 1516 if (sellerForm) { 1517 sellerForm.addEventListener('submit', (e) => { 1518 e.preventDefault(); 1519 this.submitSellerForm(); 1520 }); 1521 } 1522 } 1523 1524 calculatePrice() { 1525 const priceRule = this.data.priceRules.find(rule => 1526 rule.brand_id === this.selectedBrand.id && rule.machine_type_id === this.selectedType.id 1527 ); 1528 1529 if (priceRule) { 1530 const baseEurPrice = Math.round(priceRule.base_price * this.selectedDefect.price_multiplier * this.selectedCondition.price_multiplier); 1531 this.basePrice = baseEurPrice; 1532 1533 // Convert price based on language/currency 1534 switch (this.language) { 1535 case 'cs': 1536 // Czech Republic uses CZK (1 EUR â 25 CZK) 1537 this.finalPrice = baseEurPrice * 25; 1538 this.currency = 'CZK'; 1539 break; 1540 case 'sk': 1541 case 'de': 1542 case 'en': 1543 default: 1544 // Slovakia, Germany, and English markets use EUR 1545 this.finalPrice = baseEurPrice; 1546 this.currency = 'EUR'; 1547 break; 1548 } 1549 1550 this.step = 5; 1551 this.render(); 1552 } 1553 } 1554 1555 goToSellerForm() { 1556 this.step = 6; 1557 this.errorMessage = null; 1558 this.formErrors = {}; 1559 this.render(); 1560 } 1561 1562 async submitSellerForm() { 1563 this.sellerData = { 1564 firstName: document.getElementById('firstName').value.trim(), 1565 lastName: document.getElementById('lastName').value.trim(), 1566 email: document.getElementById('email').value.trim(), 1567 phone: document.getElementById('phone').value.trim(), 1568 street: document.getElementById('street').value.trim(), 1569 city: document.getElementById('city').value.trim(), 1570 postalCode: document.getElementById('postalCode').value.trim(), 1571 country: document.getElementById('country').value.trim(), 1572 iban: document.getElementById('iban').value.trim().toUpperCase(), 1573 pickupDate: document.getElementById('pickupDate').value,
1574 gdprConsent: document.getElementById('gdprConsent').checked 1575 }; 1576 1577 if (!this.validateSellerData()) { 1578 return; 1579 } 1580 1581 await this.generateContract(); 1582 } 1583 1584 validateSellerData() { 1585 this.formErrors = {}; 1586 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 1587 const phoneRegex = /^[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{3,6}$/im; 1588 const pscRegex = /^\d{3}\s?\d{2}$/; 1589 const ibanRegex = /^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}$/; 1590 1591 if (!this.sellerData.firstName) { 1592 this.formErrors.firstName = `${this.getUIText('firstName')} ${this.getUIText('errorRequired')}`; 1593 } else if (this.sellerData.firstName.length < 2) { 1594 this.formErrors.firstName = `${this.getUIText('firstName')} ${this.getUIText('errorMinLength2')}`; 1595 } 1596 1597 if (!this.sellerData.lastName) { 1598 this.formErrors.lastName = `${this.getUIText('lastName')} ${this.getUIText('errorRequired')}`; 1599 } else if (this.sellerData.lastName.length < 2) { 1600 this.formErrors.lastName = `${this.getUIText('lastName')} ${this.getUIText('errorMinLength2')}`; 1601 } 1602 1603 if (!this.sellerData.street) { 1604 this.formErrors.street = `${this.getUIText('street')} ${this.getUIText('errorRequired')}`; 1605 } 1606 1607 if (!this.sellerData.city) { 1608 this.formErrors.city = `${this.getUIText('city')} ${this.getUIText('errorRequired')}`; 1609 } 1610 1611 if (!this.sellerData.postalCode) { 1612 this.formErrors.postalCode = `${this.getUIText('postalCode')} ${this.getUIText('errorRequired')}`; 1613 } else if (!pscRegex.test(this.sellerData.postalCode)) { 1614 this.formErrors.postalCode = this.getUIText('errorInvalidPostalCode'); 1615 } 1616 1617 if (!this.sellerData.country) { 1618 this.formErrors.country = `${this.getUIText('country')} ${this.getUIText('errorRequired')}`; 1619 } 1620 1621 if (!this.sellerData.phone) { 1622 this.formErrors.phone = `${this.getUIText('phone')} ${this.getUIText('errorRequired')}`; 1623 } else if (!phoneRegex.test(this.sellerData.phone)) { 1624 this.formErrors.phone = this.getUIText('errorInvalidPhone'); 1625 } 1626 1627 if (!this.sellerData.email) { 1628 this.formErrors.email = `${this.getUIText('email')} ${this.getUIText('errorRequired')}`; 1629 } else if (!emailRegex.test(this.sellerData.email)) { 1630 this.formErrors.email = this.getUIText('errorInvalidEmail'); 1631 } 1632 1633 const ibanClean = this.sellerData.iban.replace(/\s/g, ''); 1634 if (!this.sellerData.iban) { 1635 this.formErrors.iban = `${this.getUIText('iban')} ${this.getUIText('errorRequired')}`; 1636 } else if (!ibanRegex.test(ibanClean)) { 1637 this.formErrors.iban = this.getUIText('errorInvalidIban'); 1638 } 1639 1640 if (!this.sellerData.pickupDate) { 1641 this.formErrors.pickupDate = `${this.getUIText('pickupDate')} ${this.getUIText('errorRequired')}`; 1642 } else { 1643 const selectedDate = new Date(this.sellerData.pickupDate); 1644 const dayOfWeek = selectedDate.getDay(); 1645 if (dayOfWeek === 0 || dayOfWeek === 6) { 1646 this.formErrors.pickupDate = this.getUIText('errorWeekdayOnly'); 1647 } 1648 } 1649 1650 if (!this.sellerData.gdprConsent) { 1651 this.formErrors.gdprConsent = this.getUIText('errorGdprRequired'); 1652 } 1653 1654 if (Object.keys(this.formErrors).length > 0) { 1655 this.render(); 1656 return false; 1657 } 1658 1659 return true; 1660 } 1661 1662 generateContractNumber() { 1663 const date = new Date(); 1664 const year = date.getFullYear();
1665 const month = String(date.getMonth() + 1).padStart(2, '0'); 1666 const day = String(date.getDate()).padStart(2, '0'); 1667 const random = String(Math.floor(Math.random() * 10000)).padStart(4, '0'); 1668 return `KZ-${year}${month}${day}-${random}`; 1669 } 1670 1671 async generateContract() { 1672 this.isLoading = true; 1673 this.loadingStatus = this.getUIText('loadingCreatingContract'); 1674 this.log('Starting contract generation...'); 1675 this.render(); 1676 1677 try { 1678 const contractNumber = this.generateContractNumber(); 1679 const contractDate = this.formatDate(); 1680 this.log('Contract number generated:', contractNumber); 1681 1682 if (!this.supabase) { 1683 throw new Error(this.getUIText('errorDatabaseUnavailable')); 1684 } 1685 1686 this.loadingStatus = this.getUIText('loadingSavingContract'); 1687 this.log('Saving contract to database...'); 1688 1689 const { data, error } = await this.supabase 1690 .from('contracts') 1691 .insert({ 1692 contract_number: contractNumber, 1693 seller_first_name: this.sellerData.firstName, 1694 seller_last_name: this.sellerData.lastName, 1695 seller_street: this.sellerData.street, 1696 seller_city: this.sellerData.city, 1697 seller_postal_code: this.sellerData.postalCode, 1698 seller_country: this.sellerData.country, 1699 seller_phone: this.sellerData.phone, 1700 seller_email: this.sellerData.email, 1701 seller_iban: this.sellerData.iban, 1702 pickup_date: this.sellerData.pickupDate, 1703 brand_name: this.selectedBrand.name, 1704 machine_type_name: this.selectedType.name, 1705 defect_name: this.selectedDefect.name, 1706 condition_name: this.selectedCondition.name, 1707 final_price: this.basePrice || this.finalPrice, 1708 language: this.language, 1709 status: 'draft', 1710 }) 1711 .select() 1712 .single(); 1713 1714 if (error) { 1715 this.error('Error creating contract in DB:', error); 1716 throw new Error(`${this.getUIText('errorSavingContract')}: ${error.message}`); 1717 } 1718 1719 this.contractId = data.id; 1720 this.log('Contract saved to database successfully, ID:', data.id); 1721 1722 this.contractData = { 1723 seller: this.sellerData, 1724 machine: { 1725 brand: this.selectedBrand.name, 1726 type: this.selectedType.name, 1727 defect: this.selectedDefect.name, 1728 condition: this.selectedCondition.name, 1729 price: this.finalPrice, 1730 }, 1731 contractNumber, 1732 contractDate, 1733 }; 1734 this.log('Contract data prepared, moving to preview'); 1735 this.step = 7; 1736 } catch (error) { 1737 this.error('Error generating contract:', error); 1738 this.errorMessage = error.message || this.getUIText('errorGeneratingContract'); 1739 this.step = 6; 1740 } finally { 1741 this.isLoading = false; 1742 this.loadingStatus = ''; 1743 this.render(); 1744 } 1745 } 1746 1747 async downloadContract() { 1748 this.isDownloading = true; 1749 this.errorMessage = null; 1750 this.log('Starting contract download...'); 1751 this.render(); 1752 1753 try { 1754 if (!window.jspdf) { 1755 this.log('Loading jsPDF library...'); 1756 await this.loadJsPDF(); 1757 this.log('jsPDF library loaded'); 1758 } 1759 1760 this.log('Generating PDF document...'); 1761 const { jsPDF } = window.jspdf; 1762 const doc = new jsPDF(); 1763 const { seller, machine, contractNumber, contractDate } = this.contractData; 1764 1765 doc.setFont('helvetica'); 1766 1767 doc.setFontSize(18); 1768 doc.setFont('helvetica', 'bold'); 1769 doc.text('KUPNI SMLOUVA', 105, 25, { align: 'center' }); 1770 1771 doc.setFontSize(11); 1772 doc.setFont('helvetica', 'normal'); 1773 doc.text('na prodej movite veci', 105, 32, { align: 'center' }); 1774 doc.text('podle ustanoveni § 2079 a nasledujicich obcanskeho zakoniku', 105, 40, { align: 'center' }); 1775 doc.text('mezi', 105, 50, { align: 'center' }); 1776 1777 let y = 60; 1778 doc.setFont('helvetica', 'bold'); 1779 doc.text('1. Prodavajici:', 20, y); 1780 doc.setFont('helvetica', 'normal'); 1781 y += 7; 1782 doc.text(`Jmeno a prijmeni: ${seller.firstName} ${seller.lastName}`, 20, y); 1783 y += 6; 1784 doc.text(`Bydliste: ${seller.street}, ${seller.postalCode} ${seller.city}, ${seller.country}`, 20, y); 1785 y += 6; 1786 doc.text(`Telefon: ${seller.phone}`, 20, y); 1787 y += 6; 1788 doc.text(`E-mail: ${seller.email}`, 20, y); 1789 y += 6; 1790 doc.text(`IBAN: ${seller.iban}`, 20, y); 1791 y += 6; 1792 doc.text('(dale take jen "prodavajici")', 20, y); 1793 1794 y += 12; 1795 doc.text('a', 105, y, { align: 'center' }); 1796 1797 y += 10; 1798 doc.setFont('helvetica', 'bold'); 1799 doc.text('2. Kupujici:', 20, y); 1800 doc.setFont('helvetica', 'normal'); 1801 y += 7; 1802 doc.text('Brewive s.r.o.', 20, y); 1803 y += 6; 1804 doc.text('se sidlem: Jicinska 226/17, 130 00 Praha - Zizkov', 20, y); 1805 y += 6; 1806 doc.text('ICO: 23032189, DIC: CZ23032189', 20, y); 1807 y += 6; 1808 doc.text('zastoupena: Lukasem Bojkovskym, jednatelem', 20, y); 1809 y += 6; 1810 doc.text('(dale take jen "kupujici")', 20, y); 1811 1812 y += 12; 1813 doc.text('spolecne take "smluvni strany"', 105, y, { align: 'center' }); 1814 1815 y += 15; 1816 doc.setFont('helvetica', 'bold'); 1817 doc.text('I. Uvodni ustanoveni', 105, y, { align: 'center' }); 1818 doc.setFont('helvetica', 'normal'); 1819 y += 8; 1820 doc.text('1. Prodavajici prohlasuje, ze je vlastnikem:', 20, y); 1821 y += 6; 1822 doc.setFont('helvetica', 'bold'); 1823 doc.text(` ${machine.brand} ${machine.type}`, 20, y); 1824 doc.setFont('helvetica', 'normal'); 1825 y += 6; 1826 doc.text('(dale jen "predmet koupe").', 20, y); 1827 1828 y += 12; 1829 doc.setFont('helvetica', 'bold'); 1830 doc.text('IV. Cena a zpusob uhrady', 105, y, { align: 'center' }); 1831 doc.setFont('helvetica', 'normal'); 1832 y += 8; 1833 doc.text('1. Kupni cena byla stanovena ve vysi:', 20, y); 1834 y += 6; 1835 doc.setFont('helvetica', 'bold'); 1836 doc.text(` ${this.formatPrice(machine.price)}`, 20, y); 1837 doc.setFont('helvetica', 'normal'); 1838 1839 y += 12; 1840 doc.setFont('helvetica', 'bold'); 1841 doc.text('V. Odpovednost za vady', 105, y, { align: 'center' }); 1842 doc.setFont('helvetica', 'normal'); 1843 y += 8; 1844 doc.text('1. Prodavajici upozornuje na nasledujici vady:', 20, y); 1845 y += 6; 1846 doc.text(` Stav: ${machine.condition}`, 20, y); 1847 y += 6; 1848 doc.text(` Zavada: ${machine.defect}`, 20, y); 1849 1850 y += 20; 1851 doc.line(20, y, 85, y); 1852 doc.line(115, y, 190, y); 1853 y += 5; 1854 doc.text('Prodavajici', 52, y, { align: 'center' }); 1855 doc.text('Kupujici', 152, y, { align: 'center' }); 1856 y += 5; 1857 doc.setFontSize(9); 1858 doc.text(`${seller.firstName} ${seller.lastName}`, 52, y, { align: 'center' }); 1859 doc.text(`V ${seller.city} dne ${contractDate}`, 52, y + 4, { align: 'center' }); 1860 doc.text('Brewive s.r.o.', 152, y, { align: 'center' }); 1861 doc.text(`V Praze dne ${contractDate}
1861`, 152, y + 4, { align: 'center' }); 1862 1863 const fileName = `Zmluva_${contractNumber}.pdf`; 1864 this.log('Saving PDF as:', fileName); 1865 doc.save(fileName); 1866 1867 if (this.supabase && this.contractId) { 1868 this.log('Updating contract status to downloaded...'); 1869 const { error } = await this.supabase 1870 .from('contracts') 1871 .update({ status: 'downloaded' }) 1872 .eq('id', this.contractId); 1873 1874 if (error) { 1875 this.error('Error updating contract status:', error); 1876 } else { 1877 this.log('Contract status updated successfully'); 1878 } 1879 } 1880 1881 this.log('Contract downloaded successfully'); 1882 this.downloadSuccess = true; 1883 setTimeout(() => { 1884 this.downloadSuccess = false; 1885 this.render(); 1886 }, 3000); 1887 } catch (error) { 1888 this.error('Error downloading contract:', error); 1889 this.errorMessage = this.getUIText('errorDownloadingContract'); 1890 } finally { 1891 this.isDownloading = false; 1892 this.render(); 1893 } 1894 } 1895 1896 async loadJsPDF() { 1897 return new Promise((resolve, reject) => { 1898 const script = document.createElement('script'); 1899 script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js'; 1900 script.onload = resolve; 1901 script.onerror = reject; 1902 document.head.appendChild(script); 1903 }); 1904 } 1905 1906 async sendContractEmail() { 1907 if (!this.contractData) { 1908 this.errorMessage = this.getUIText('errorNoData'); 1909 this.render(); 1910 return; 1911 } 1912 1913 this.isSendingEmail = true; 1914 this.errorMessage = null; 1915 this.render(); 1916 1917 try { 1918 const response = await fetch(`${this.supabaseUrl}/functions/v1/send-contract-email`, { 1919 method: 'POST', 1920 headers: { 1921 'Content-Type': 'application/json', 1922 'Authorization': `Bearer ${this.supabaseKey}`, 1923 }, 1924 body: JSON.stringify({ 1925 contractId: this.contractId, 1926 contractData: this.contractData 1927 }) 1928 }); 1929 1930 if (!response.ok) { 1931 throw new Error(this.getUIText('errorSendingEmail')); 1932 } 1933 1934 if (this.supabase && this.contractId) { 1935 await this.supabase 1936 .from('contracts') 1937 .update({ status: 'emailed', email_sent: true, email_sent_at: new Date().toISOString() }) 1938 .eq('id', this.contractId); 1939 } 1940 1941 this.emailSuccess = true; 1942 setTimeout(() => { 1943 this.emailSuccess = false; 1944 this.render(); 1945 }, 3000); 1946 } catch (error) { 1947 console.error('Error sending email:', error); 1948 this.errorMessage = this.getUIText('errorSendingEmail'); 1949 } finally { 1950 this.isSendingEmail = false; 1951 this.render(); 1952 } 1953 } 1954 1955 goBack() { 1956 if (this.step > 1) { 1957 if (this.step === 7) { 1958 this.step = 6; 1959 } else if (this.step === 6) { 1960 this.step = 5; 1961 } else { 1962 this.step--; 1963 } 1964 this.errorMessage = null; 1965 this.formErrors = {}; 1966 this.render(); 1967 } 1968 } 1969 1970 reset() { 1971 this.step = 1; 1972 this.selectedBrand = null; 1973 this.selectedType = null; 1974 this.selectedDefect = null; 1975 this.selectedCondition = null; 1976 this.finalPrice = null; 1977 this.sellerData = { 1978 firstName: '', 1979 lastName: '', 1980 email: '', 1981 phone: '', 1982 street: '', 1983 city: '', 1984 postalCode: '', 1985 country: '', 1986 iban: '', 1987 pickupDate: '', 1988 gdprConsent: false 1989 }; 1990 this.contractData = null; 1991 this.contractId = null; 1992 this.errorMessage = null; 1993 this.formErrors = {}; 1994 this.downloadSuccess = false; 1995 this.emailSuccess = false; 1996 this.render(); 1997 } 1998 } 1999 2000 window.CoffeeBuybackWidget = { 2001 init: function(options) { 2002 if (!options.supabaseUrl) { 2003 options.supabaseUrl = 'https://your-project.supabase.co'; 2004 } 2005 if (!options.supabaseKey) { 2006 options.supabaseKey = 'your-anon-key'; 2007 } 2008 window.coffeeWidget = new CoffeeBuybackWidget(options); 2009 } 2010 }; 2011})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.