Error executing template "/Designs/Swift/Paragraph/Custom_Offer.cshtml" System.NullReferenceException: Object reference not set to an instance of an object. at CompiledRazorTemplates.Dynamic.RazorEngine_eeee9130fbcb4d16bcc2e114fea0b626.ExecuteAsync() at RazorEngine.Templating.TemplateBase.Run(ExecuteContext context, TextWriter reader) at RazorEngine.Templating.RazorEngineCore.RunTemplate(ICompiledTemplate template, TextWriter writer, Object model, DynamicViewBag viewBag) at RazorEngine.Templating.RazorEngineService.Run(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag) at RazorEngine.Templating.DynamicWrapperService.Run(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag) at RazorEngine.Templating.RazorEngineServiceExtensions.Run(IRazorEngineService service, String name, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag) at RazorEngine.Templating.RazorEngineServiceExtensions.<>c__DisplayClass23_0.<Run>b__0(TextWriter writer) at RazorEngine.Templating.RazorEngineServiceExtensions.WithWriter(Action`1 withWriter) at RazorEngine.Templating.RazorEngineServiceExtensions.Run(IRazorEngineService service, String name, Type modelType, Object model, DynamicViewBag viewBag) at Dynamicweb.Rendering.RazorTemplateRenderingProvider.Render(Template template) at Dynamicweb.Rendering.TemplateRenderingService.Render(Template template) at Dynamicweb.Rendering.Template.RenderRazorTemplate()
1 @inherits Dynamicweb.Rendering.ViewModelTemplate<Dynamicweb.Frontend.ParagraphViewModel> 2 @using Dynamicweb.Security.UserManagement; 3 @using System 4 @using System.Web 5 @using System.Globalization 6 @using Dynamicweb.Ecommerce.ProductCatalog 7 @using Dynamicweb.Rendering 8 @using Dynamicweb.Ecommerce.Products 9 10 @{ 11 var productsParam = Dynamicweb.Context.Current.Request.QueryString["products"]; 12 string[] products = !string.IsNullOrEmpty(productsParam) ? productsParam.Split(',') : new string[0]; 13 14 string title = Model.Item.GetString("Title"); 15 string description = Model.Item.GetString("Description"); 16 17 var currentUser = Dynamicweb.Security.UserManagement.User.GetCurrentExtranetUser(); 18 var secondaryUser = Dynamicweb.Security.UserManagement.User.GetCurrentSecondaryUser(); 19 20 var salesPersonName = secondaryUser != null ? secondaryUser.Name : currentUser?.Name; 21 var salesPersonMail = secondaryUser != null ? secondaryUser.Email : currentUser?.Email; 22 23 var customerAddresses = currentUser.GetAddresses(); 24 } 25 26 <style> 27 .qty-wrapper { 28 display: flex; 29 align-items: center; 30 gap: 5px; 31 } 32 .qty-btn { 33 padding: 6px 10px; 34 font-size: 1.2rem; 35 border: 1px solid #ccc; 36 background: #f9f9f9; 37 cursor: pointer; 38 border-radius: 4px; 39 } 40 .qty-btn:active { 41 background: #eee; 42 } 43 .qty-wrapper input { 44 width: 60px; 45 text-align: center; 46 } 47 </style> 48 49 <div> 50 <h1 class="my-3">@title</h1> 51 <div class="w-50">@description</div> 52 </div> 53 54 <div class="mt-4 grid"> 55 <div class="g-col-3"> 56 <h2>@Translate("Salesperson")</h2> 57 58 <div class="mt-3"> 59 <div id="contactPerson">Kontaktperson: <span>@salesPersonName</span></div> 60 <div id="contactMail">E-post: <span>@salesPersonMail</span></div> 61 </div> 62 </div> 63 64 @if (secondaryUser != null) 65 { 66 <div class="g-col-3"> 67 <h2>@Translate("Customer")</h2> 68 69 <div class="mt-3"> 70 <div>Attn: <span id="customCompany">@currentUser.Company</span></div> 71 <div>Kundnummer: <span>@currentUser.CustomerNumber</span></div> 72 <div>@currentUser.Name @currentUser.ID</div> 73 <div>@currentUser.Address, @currentUser.Zip @currentUser.City</div> 74 75 <div class="d-none" id="customerID">@currentUser.ID</div> 76 </div> 77 </div> 78 } 79 </div> 80 81 <div class="mt-4"> 82 <h2>@Translate("Settings")</h2> 83 84 <div class="grid mt-3"> 85 <div class="form-floating g-col-3"> 86 <input id="expires" class="form-control" type="date" name="expires" placeholder="" value=""> 87 <label for="expires" class="form-label">Giltig t.o.m</label> 88 </div> 89 90 <div class="mb-3 g-col-12"> 91 <label for="terms" class="form-label fw-bold fs-5">Terms</label> 92 <br /> 93 <small>If not filled out a standard terms and conditions text will been shown in the offer.</small> 94 <textarea class="form-control" id="terms" rows="5"></textarea> 95 </div> 96 </div> 97 </div> 98 99 <div class="mt-4"> 100 <h2>@Translate("Products")</h2> 101 102 <div> 103 <table class="w-100"> 104 <thead> 105 <tr style="border-bottom: 2px solid #ccc; font-weight: bold;"> 106 <th style="padding: 10px 20px;"></th> <!-- bilde --> 107 <th style="padding: 10px 20px; text-align:left;">Product name</th> 108 <th style="padding: 10px 20px; text-align:left;">Product number</th> 109 <th style="padding: 10px 20px; text-align:center;">Quantity</th> 110 <th style="padding: 10px 20px; text-align:left;">Default price</th> 111 <th style="padding: 10px 20px; text-align:left;">Discount %</th> 112 <th style="padding: 10px 20px; text-align:left;">Offer price</th> 113 </tr> 114 </thead> 115 <tbody> 116 @foreach (var item in products) 117 { 118 var product = Dynamicweb.Ecommerce.Services.Products.GetProductByNumber(item, Dynamicweb.Ecommerce.Services.Languages.GetDefaultLanguageId()); 119 120 ProductImageService productImageService = new ProductImageService(); 121 var image = productImageService.GetImagePath(product); 122 123 var vmSettings = new ProductViewModelSettings 124 { 125 LanguageId = Dynamicweb.Ecommerce.Services.Languages.GetDefaultLanguageId(), 126 CurrencyCode = "SEK", 127 CountryCode = "SE", 128 ShopId = Dynamicweb.Ecommerce.Services.Shops.GetShop(Pageview.Area.EcomShopId).ToString(), 129 UserId = 43750 130 }; 131 132 var productVm = ViewModelFactory.CreateView(vmSettings, product.Number); 133 134 var priceSteps = productVm.Prices? 135 .Select(p => $"{p.Quantity}:{p.Price.Price.ToString(CultureInfo.InvariantCulture)}"); 136 137 var priceBreaks = priceSteps != null ? string.Join(";", priceSteps) : ""; 138 139 @*<pre>Product:<code class="language-json">@System.Text.Json.JsonSerializer.Serialize(productVm)</code></pre>*@ 140 141 <tr style="border-bottom: 1px solid grey; background-color: white;" class="product-item"> 142 <td style="padding: 10px 20px;"> 143 <img src="@image" width="100" /> 144 </td> 145 <td style="padding: 10px 20px;">@product.Name</td> 146 <td style="padding: 10px 20px;" class="product-number">@product.Number</td> 147 <td style="padding: 10px 20px;"> 148 <div class="qty-wrapper"> 149 <button type="button" class="qty-btn minus">−</button> 150 <input type="number" name="quantity" min="1" value="1" /> 151 <button type="button" class="qty-btn plus">+</button> 152 </div> 153 </td> 154 <td style="padding: 10px 20px;" class="product-price" 155 data-raw-price="@productVm.Price?.Price" 156 data-price-breaks="@priceBreaks"> 157 158 <div class="line-total">@productVm.Price?.PriceFormattedNoSymbol</div> 159 <small class="unit-price text-muted"></small> 160 </td> 161 <td style="padding: 10px 20px;"> 162 <input type="text" name="discount" placeholder="Eks: 20" title="Skriv 20 for prosent" /> 163 </td> 164 <td style="padding: 10px 20px;" class="offer-price"></td> 165 </tr> 166 } 167 </tbody> 168 </table> 169 </div> 170 171 <div class="text-end"> 172 <a href="#" id="resetOffer" class="btn btn-secondary mt-3 me-3">Reset offer</a> 173 <a href="#" id="generatePDF" class="btn btn-primary mt-3">Generate offer</a> 174 </div> 175 </div> 176 177 <script> 178 document.addEventListener("DOMContentLoaded", () => { 179 const STORAGE_PREFIX = "offer-"; 180 const EXPIRES_KEY = STORAGE_PREFIX + "expires"; 181 const TERMS_KEY = STORAGE_PREFIX + "terms"; 182 183 function clearOfferStorage() { 184 // Fjern alle keys som starter med offer- 185 Object.keys(localStorage).forEach((key) => { 186 if (key.startsWith(STORAGE_PREFIX)) { 187 localStorage.removeItem(key); 188 } 189 }); 190 } 191 192 /* SET DEFAULT EXPIRED DATE */ 193 const expiresInput = document.getElementById("expires"); 194 if (expiresInput) { 195 const savedExpires = localStorage.getItem(EXPIRES_KEY); 196 197 if (savedExpires) { 198 // Bruk lagret verdi 199 expiresInput.value = savedExpires; 200 } else { 201 // Sett standard +30 dager, og lagre den 202 const d = new Date(); 203 d.setDate(d.getDate() + 30); 204 const yyyy = d.getFullYear(); 205 const mm = String(d.getMonth() + 1).padStart(2, '0'); 206 const dd = String(d.getDate()).padStart(2, '0'); 207 const defaultExpires = `${yyyy}-${mm}-${dd}`; 208 expiresInput.value = defaultExpires; 209 localStorage.setItem(EXPIRES_KEY, defaultExpires); 210 } 211 212 // Lagre endringer fortløpende 213 expiresInput.addEventListener("change", () => { 214 localStorage.setItem(EXPIRES_KEY, expiresInput.value || ""); 215 }); 216 } 217 218 const termsInput = document.getElementById("terms"); 219 if (termsInput) { 220 const savedTerms = localStorage.getItem(TERMS_KEY); 221 if (savedTerms !== null) { 222 termsInput.value = savedTerms; 223 } 224 225 termsInput.addEventListener("input", () => { 226 localStorage.setItem(TERMS_KEY, termsInput.value || ""); 227 }); 228 } 229 230 function getUnitPriceForQuantity(priceEl, qty) { 231 const base = parseFloat((priceEl.dataset.rawPrice || "0").replace(",", ".")) || 0; 232 const breaksStr = priceEl.dataset.priceBreaks; 233 234 if (!breaksStr) { 235 return base; 236 } 237 238 let bestPrice = base; 239 let bestQty = 0; 240 241 // "1:2200;12:2100;24:2000" 242 breaksStr.split(";").forEach(pair => { 243 if (!pair) return; 244 const parts = pair.split(":"); 245 if (parts.length !== 2) return; 246 247 const q = parseInt(parts[0], 10); 248 const p = parseFloat((parts[1] || "").replace(",", ".")); 249 250 if (!isNaN(q) && !isNaN(p) && qty >= q && q >= bestQty) { 251 bestQty = q; 252 bestPrice = p; 253 } 254 }); 255 256 return bestPrice; 257 } 258 259 /* FUNKSJON: Beregn priser med prosent-rabatt */ 260 function updateOfferPrice(priceEl, discountInput, offerPriceEl, quantityInput) { 261 if (!priceEl || !offerPriceEl || !quantityInput || !discountInput) return; 262 263 const qty = Math.max(1, parseInt(quantityInput.value || "1", 10)); 264 265 // 👇 Hent riktig enhetspris 266 const unitPrice = getUnitPriceForQuantity(priceEl, qty); 267 268 const totalStandard = unitPrice * qty; 269 270 const discountRaw = (discountInput.value || "").replace(",", "."); 271 const discount = parseFloat(discountRaw); 272 let totalOffer = totalStandard; 273 274 if (!isNaN(discount) && discount > 0 && discount <= 100) { 275 totalOffer = totalStandard * (1 - discount / 100); 276 } 277 278 const fmt = (n) => new Intl.NumberFormat('sv-SE', { 279 style: 'currency', 280 currency: 'SEK' 281 }).format(n); 282 283 // oppdater standardpris (linjepris) 284 const lineTotalEl = priceEl.querySelector(".line-total"); 285 if (lineTotalEl) lineTotalEl.textContent = fmt(totalStandard); 286 287 // 👇 oppdater enhetspris 288 const unitPriceEl = priceEl.querySelector(".unit-price"); 289 if (unitPriceEl) unitPriceEl.textContent = `(Unit: ${fmt(unitPrice)})`; 290 291 // oppdater tilbudspris 292 offerPriceEl.innerHTML = fmt(totalOffer); 293 } 294 295 296 /* CHANGE QUANTITY BUTTONS */ 297 document.querySelectorAll(".qty-wrapper").forEach(wrapper => { 298 const input = wrapper.querySelector("input[name='quantity']"); 299 const minus = wrapper.querySelector(".minus"); 300 const plus = wrapper.querySelector(".plus"); 301 302 minus.addEventListener("click", () => { 303 let val = Math.max(1, parseInt(input.value || "1", 10) - 1); 304 input.value = val; 305 input.dispatchEvent(new Event("input")); // trigge din logikk 306 }); 307 308 plus.addEventListener("click", () => { 309 let val = Math.max(1, parseInt(input.value || "1", 10) + 1); 310 input.value = val; 311 input.dispatchEvent(new Event("input")); 312 }); 313 }); 314 315 /* KNYTT RABATT + ANTALL TIL BEREGNING PER RAD */ 316 document.querySelectorAll(".product-item").forEach(item => { 317 const priceEl = item.querySelector(".product-price"); 318 const offerPriceEl = item.querySelector(".offer-price"); 319 const quantityInput = item.querySelector("input[name='quantity']"); 320 const discountInput = item.querySelector("input[name='discount']"); 321 const numberEl = item.querySelector(".product-number"); 322 323 if (!priceEl || !offerPriceEl || !quantityInput || !discountInput || !numberEl) return; 324 325 const productNumber = (numberEl.textContent || "").trim(); 326 const qtyKey = `${STORAGE_PREFIX}qty-${productNumber}`; 327 const discountKey = `${STORAGE_PREFIX}discount-${productNumber}`; 328 329 // Hent lagret quantity 330 const savedQty = localStorage.getItem(qtyKey); 331 if (savedQty !== null && !isNaN(parseInt(savedQty, 10))) { 332 quantityInput.value = String(Math.max(1, parseInt(savedQty, 10))); 333 } 334 335 // Hent lagret discount 336 const savedDiscount = localStorage.getItem(discountKey); 337 if (savedDiscount !== null) { 338 discountInput.value = savedDiscount; 339 } 340 341 // Første beregning 342 updateOfferPrice(priceEl, discountInput, offerPriceEl, quantityInput); 343 344 // Lytt på endring i rabatt (prosent) 345 discountInput.addEventListener("input", () => { 346 const val = parseFloat(discountInput.value.replace(",", ".")); 347 if (!isNaN(val) && val >= 0 && val <= 100) { 348 discountInput.style.color = ""; 349 discountInput.title = "Skriv 0–100 for rabatt i prosent"; 350 } else if (discountInput.value.trim() !== "") { 351 discountInput.style.color = "red"; 352 discountInput.title = "Ugyldig rabatt – må være mellom 0 og 100"; 353 } else { 354 discountInput.style.color = ""; 355 discountInput.title = "Skriv 0–100 for rabatt i prosent"; 356 } 357 358 // 💾 lagre rabatt 359 localStorage.setItem(discountKey, discountInput.value || ""); 360 361 updateOfferPrice(priceEl, discountInput, offerPriceEl, quantityInput); 362 }); 363 364 // Lytt på endring i antall 365 quantityInput.addEventListener("input", () => { 366 const q = Math.max(1, parseInt(quantityInput.value || "1", 10)); 367 quantityInput.value = String(q); // normaliser 368 369 // 💾 lagre antall 370 localStorage.setItem(qtyKey, String(q)); 371 372 updateOfferPrice(priceEl, discountInput, offerPriceEl, quantityInput); 373 }); 374 }); 375 376 function formatDateToDDMMYYYY(dateStr) { 377 if (!dateStr || !dateStr.includes("-")) return ""; 378 379 const parts = dateStr.split("-"); 380 if (parts.length !== 3) return ""; 381 382 const yyyy = parts[0]; 383 const mm = parts[1]; 384 const dd = parts[2]; 385 386 return `${dd}.${mm}.${yyyy}`; 387 } 388 389 // Bygg PDF-URL: products, contactPerson, contactMail, expires, terms, customerId 390 function buildPdfUrl() { 391 const productLines = []; 392 393 document.querySelectorAll(".product-item").forEach(item => { 394 const numberEl = item.querySelector(".product-number"); 395 const quantityInput = item.querySelector("input[name='quantity']"); 396 const discountInput = item.querySelector("input[name='discount']"); 397 const priceEl = item.querySelector(".product-price"); 398 399 if (!numberEl || !quantityInput || !discountInput || !priceEl) { 400 return; 401 } 402 403 const productNumber = (numberEl.textContent || "").trim(); 404 405 // quantity 406 const qty = Math.max(1, parseInt(quantityInput.value || "1", 10)); 407 408 // discount (prosent) 409 const discountRaw = (discountInput.value || "").replace(",", "."); 410 const discount = parseFloat(discountRaw); 411 412 // 👇 alltid minst "0" 413 let discountForUrl = "0"; 414 if (!isNaN(discount) && discount >= 0 && discount <= 100) { 415 discountForUrl = discount.toString().replace(".", ","); 416 } 417 418 // offerPrice 419 const unitPrice = parseFloat((priceEl.dataset.rawPrice || "0").replace(",", ".")) || 0; 420 const totalStandard = unitPrice * qty; 421 let totalOffer = totalStandard; 422 423 if (!isNaN(discount) && discount > 0 && discount <= 100) { 424 totalOffer = totalStandard * (1 - discount / 100); 425 } 426 if (totalOffer < 0) totalOffer = 0; 427 428 const offerPriceForUrl = totalOffer.toFixed(2); // f.eks. 216.00 429 430 // Format: productNumber_quantity_discount_offerPrice 431 const line = `${productNumber}_${qty}_${discountForUrl}_${offerPriceForUrl}`; 432 productLines.push(line); 433 }); 434 435 // Kontaktinfo fra HTML 436 const contactPersonSpan = document.querySelector("#contactPerson span"); 437 const contactMailSpan = document.querySelector("#contactMail span"); 438 const customerIdEl = document.getElementById("customerID"); 439 440 const contactPerson = contactPersonSpan ? contactPersonSpan.textContent.trim() : ""; 441 const contactMail = contactMailSpan ? contactMailSpan.textContent.trim() : ""; 442 const customerId = customerIdEl ? customerIdEl.textContent.trim() : ""; 443 444 // expires i format yyyy-MM-dd (direkte fra input) 445 const expiresInput = document.getElementById("expires"); 446 const expiresRaw = expiresInput ? (expiresInput.value || "") : ""; 447 const expiresFormatted = formatDateToDDMMYYYY(expiresRaw); 448 449 // terms fra textarea 450 const termsInput = document.getElementById("terms"); 451 const termsRaw = termsInput ? (termsInput.value || "") : ""; 452 453 // Bygg querystring – products blir semikolon-separert 454 const baseUrl = "https://sternhammar.dw10staging.dynamicweb-cms.com/pdf"; // bytt til faktisk endpoint når klart 455 456 const url = 457 baseUrl + 458 "?products=" + encodeURIComponent(productLines.join(";")) + 459 "&contactPerson=" + encodeURIComponent(contactPerson) + 460 "&contactMail=" + encodeURIComponent(contactMail) + 461 "&expires=" + encodeURIComponent(expiresFormatted) + 462 "&terms=" + encodeURIComponent(termsRaw) + 463 "&customerId=" + encodeURIComponent(customerId); 464 465 return url; 466 } 467 468 function getCompanyNameForFilename() { 469 const el = document.getElementById("customCompany"); 470 const name = (el?.textContent || "").trim(); 471 return name && name.length > 0 ? name : "Offert"; 472 } 473 474 const generateBtn = document.getElementById("generatePDF"); 475 476 if (generateBtn) { 477 generateBtn.addEventListener("click", async (e) => { 478 e.preventDefault(); 479 480 const pdfUrl = buildPdfUrl(); 481 482 if (!pdfUrl) { 483 alert("Kunne ikke generere URL for tilbudet."); 484 return; 485 } 486 487 const fileName = getCompanyNameForFilename(); 488 489 // Ny webhook 490 const webhookUrl = "https://menntor.mennt.it/webhook/9d841d83-8f2b-4c52-a9ab-d28c5b48cdc0" + 491 "?url=" + encodeURIComponent(pdfUrl) + 492 "&filename=" + encodeURIComponent(fileName); 493 494 try { 495 const response = await fetch(webhookUrl); 496 if (!response.ok) { 497 throw new Error("Feil ved PDF-generering"); 498 } 499 500 const result = await response.json(); 501 const fileUrl = result?.Files?.[0]?.Url; 502 503 if (fileUrl) { 504 // Trigger filnedlasting 505 const a = document.createElement("a"); 506 a.href = fileUrl; 507 a.download = result?.Files?.[0]?.FileName || "offer.pdf"; 508 document.body.appendChild(a); 509 a.click(); 510 a.remove(); 511 512 clearOfferStorage(); 513 } else { 514 alert("Kunne ikke hente PDF-link fra respons."); 515 } 516 } catch (err) { 517 console.error(err); 518 alert("En feil oppstod ved generering av PDF."); 519 } 520 }); 521 } 522 523 const resetBtn = document.getElementById("resetOffer"); 524 if (resetBtn) { 525 resetBtn.addEventListener("click", (e) => { 526 e.preventDefault(); 527 528 // Slett lagret data 529 clearOfferStorage(); 530 531 // Reset dato 532 if (expiresInput) { 533 const d = new Date(); 534 d.setDate(d.getDate() + 30); 535 const yyyy = d.getFullYear(); 536 const mm = String(d.getMonth() + 1).padStart(2, '0'); 537 const dd = String(d.getDate()).padStart(2, '0'); 538 const defaultExpires = `${yyyy}-${mm}-${dd}`; 539 expiresInput.value = defaultExpires; 540 localStorage.setItem(EXPIRES_KEY, defaultExpires); 541 } 542 543 // Reset terms 544 if (termsInput) { 545 termsInput.value = ""; 546 } 547 548 // Reset alle produkter 549 document.querySelectorAll(".product-item").forEach(item => { 550 const priceEl = item.querySelector(".product-price"); 551 const offerPriceEl = item.querySelector(".offer-price"); 552 const quantityInput = item.querySelector("input[name='quantity']"); 553 const discountInput = item.querySelector("input[name='discount']"); 554 555 if (!priceEl || !offerPriceEl || !quantityInput || !discountInput) return; 556 557 quantityInput.value = "1"; 558 discountInput.value = ""; 559 560 updateOfferPrice(priceEl, discountInput, offerPriceEl, quantityInput); 561 }); 562 }); 563 } 564 }); 565 </script>