The product was added to your favourites list The product was removed from your favourites list
Otto schachner logo
Error executing template "Designs/OttoSchachner/eCom/ProductCatalog/ProductView.cshtml"
System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')
   at System.Collections.Generic.List`1.get_Item(Int32 index)
   at CompiledRazorTemplates.Dynamic.RazorEngine_eca0db00dd794a8e97a58a7f82e065c6.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.Ecommerce.ProductCatalog.ProductViewModel> 2 @using System.Diagnostics.SymbolStore 3 @using Dynamicweb.Content 4 @using Dynamicweb.Content.Data 5 @using Dynamicweb.Core 6 @using Dynamicweb.Ecommerce 7 @using Dynamicweb.Ecommerce.CustomerExperienceCenter.Favorites 8 @using Dynamicweb.Ecommerce.Prices 9 @using Dynamicweb.Frontend 10 @using Dynamicweb.Frontend.Devices 11 @using Dynamicweb.Frontend.Navigation 12 @using Dynamicweb.Ecommerce.ProductCatalog 13 @using Dynamicweb.Ecommerce.Products 14 @using Dynamicweb.Ecommerce.Shops 15 @using Dynamicweb.Ecommerce.Variants 16 @using Dynamicweb.Rendering 17 @using OttoSchachner.CustomCode.Helpers 18 @using Dynamicweb.Ecommerce.DynamicwebLiveIntegration.Products 19 20 @{ 21 var isCsv = string.Equals(Dynamicweb.Context.Current.Request.QueryString["download"], "csv", StringComparison.OrdinalIgnoreCase); 22 if (isCsv) 23 { 24 var fileVirtual = OttoSchachner.CustomCode.Helpers.ProductCsvExport.CreateTempCsvFile( 25 Model.Id, 26 Model.VariantId, 27 Model.LanguageId, 28 Model, 29 Pageview.User 30 ); 31 32 Dynamicweb.Context.Current.Response.Redirect(fileVirtual); 33 return; 34 } 35 36 37 var isZip = string.Equals(Dynamicweb.Context.Current.Request.QueryString["download"], "imageszip", StringComparison.OrdinalIgnoreCase); 38 if (isZip) 39 { 40 var zipVirtual = OttoSchachner.CustomCode.Helpers.ProductImagesZipExport.CreateTempImagesZipFile( 41 Model.Id, 42 Model.VariantId, 43 Model.LanguageId, 44 Model 45 ); 46 47 Dynamicweb.Context.Current.Response.Redirect(zipVirtual); 48 return; 49 } 50 51 ProductService productService = new ProductService(); 52 ProductQuantityPrices matrixProvider = new ProductQuantityPrices(); 53 var productPriceMatrix = matrixProvider.GetProductQuantityPrices(productService.GetProductById(Model.Id, Model.VariantId, Model.LanguageId)); 54 var uniquePricesMatrix = productPriceMatrix 55 .GroupBy(p => new { p.Quantity, Unit = Model.ProductFields["VjmSalesUnit"].Value, Amount = Dynamicweb.Core.Converter.ToDouble(p.Amount) }) 56 .Select(g => g.First()) 57 .ToList(); 58 } 59 60 @{ 61 string RenderPriceMatrix(List<ProductPrice> uniquePrices, bool isAssortmentGroup, bool alwaysNetPrice) 62 { 63 <div class="pview__matrix mb-4 @(isAssortmentGroup || alwaysNetPrice ? "d-block" : "")"> 64 @if (uniquePrices.Count != 0) 65 { 66 <div class="d-flex flex-row justify-content-between mb-2"> 67 <div class="fw-bold">@Translate("price-matrix-amount", "Antal")</div> 68 <div class="fw-bold">@Translate("price-matrix-price", "Pris")</div> 69 </div> 70 71 72 bool notLast = true; 73 int index = 0; 74 75 76 foreach (var price in uniquePrices) 77 { 78 <div class="d-flex flex-row justify-content-between"> 79 <div>@price.Quantity @price.UnitId</div> 80 <div>@Dynamicweb.Core.Converter.ToDouble(price.Amount).ToString("F2")</div> 81 </div> 82 83 if (index < uniquePrices.Count - 1) 84 { 85 <hr></hr> 86 } 87 88 index++; 89 } 90 } 91 else 92 { 93 <div class="fw-bold"> 94 @Translate("price-matrix-no-prices", "Ingen mængdepriser fundet") 95 </div> 96 } 97 </div> 98 99 return ""; 100 } 101 102 103 string RenderSections() 104 { 105 <div class="mb-4"> 106 107 @{ 108 var download = Dynamicweb.Core.Converter.ToString(Model.ProductFields.Where(x => x.Key == "VjmDocumentLibraryHtml").FirstOrDefault().Value.Value); 109 bool hasDocDownloads = !string.IsNullOrEmpty(download); 110 111 var current = Dynamicweb.Context.Current.Request.RawUrl ?? ""; 112 var csvUrl = current + (current.Contains("?") ? "&" : "?") + "download=csv"; 113 var zipUrl = (Dynamicweb.Context.Current.Request.RawUrl ?? "/") + 114 ((Dynamicweb.Context.Current.Request.RawUrl ?? "/").Contains("?") ? "&" : "?") + 115 "download=imageszip"; 116 } 117 118 <div class="pview__section"> 119 <a href="javascript:void(0)" data-id="Downloads" 120 class="pview__section__name-container d-flex align-items-center justify-content-between"> 121 <div class="d-flex flex-row"> 122 <i class="pview__section__name-container__icon fa-solid fa-file-pdf"></i> 123 <div class="pview__section__name-container__name"> 124 @(Translate("product-downloads", "Downloads")) 125 </div> 126 </div> 127 <i class="bi-chevron-down"></i> 128 <i class="bi-chevron-up d-none"></i> 129 </a> 130 131 <div data-id="Downloads" class="pview__section__content"> 132 <div class="d-flex flex-column"> 133 <a class="os-button os-button--red mb-2" href="@csvUrl"> 134 @Translate("download-csv", "Download CSV") 135 </a> 136 137 <a class="os-button os-button--red mb-2" href="@zipUrl"> 138 @Translate("download-images-zip", "Download billeder ZIP") 139 </a> 140 141 @if (hasDocDownloads) 142 { 143 @download 144 } 145 </div> 146 </div> 147 </div> 148 </div> 149 150 return ""; 151 } 152 153 154 bool signedIn = PageView.Current().User != null; 155 bool purchasable = false; 156 bool alwaysNetPrice = false; 157 158 if (signedIn) 159 { 160 var alwaysNetPriceField = Pageview.User.CustomFieldValues.FirstOrDefault(x => x.CustomField.SystemName == "AccessUser_AlwaysNettopPrices"); 161 if (alwaysNetPriceField != null && alwaysNetPriceField.Value != null) 162 { 163 alwaysNetPrice = Dynamicweb.Core.Converter.ToBoolean(alwaysNetPriceField.Value); 164 } 165 166 if (Model.VariantCombinations().Count > 1 && string.IsNullOrEmpty(Model.VariantId)) 167 { 168 purchasable = false; 169 } 170 else 171 { 172 purchasable = true; 173 } 174 } 175 176 List<BreadcrumbItem> items = new List<BreadcrumbItem>(); 177 bool isMobile = PageView.Current().Device == DeviceType.Mobile; 178 179 var primaryGroup = Model.PrimaryOrDefaultGroup; 180 var categoryName = primaryGroup?.Name ?? ""; 181 var categoryId = primaryGroup?.Id ?? ""; 182 183 184 var groupService = new GroupService(); 185 var shop = new ShopService().GetShop("SHOP1"); 186 var groups = groupService.FindPath(shop, groupService.GetGroup(primaryGroup.Id)); 187 var shopPageId = GetPageIdByNavigationTag("Shop"); 188 var product = productService.GetProductById(Model.Id, Model.VariantId, Model.LanguageId); 189 var productImageService = new ProductImageService(); 190 var images = productImageService.GetImagesFromPatterns(product, shop).ToList(); 191 bool variantSelected = !string.IsNullOrEmpty(Model.VariantId); 192 var pageService = new PageService(); 193 string mp4Path = product.ProductFieldValues.GetProductFieldValue("MP4Video")?.Value?.ToString(); 194 bool showVideo = !string.IsNullOrEmpty(mp4Path) && mp4Path.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase); 195 196 197 if (variantSelected) 198 { 199 images.Clear(); 200 images.Add(Model.DefaultImage.Value); 201 } 202 203 foreach (var group in groups) 204 { 205 items.Add(new BreadcrumbItem 206 { 207 Title = group.Name, 208 Url = "/Default.aspx?ID=" + shopPageId + "&groupid=" + group.Id 209 }); 210 } 211 212 var parameters = new Dictionary<string, object>(); 213 parameters.Add("items", items); 214 215 var _navigationSettings = new Dynamicweb.Frontend.Navigation.NavigationSettings() 216 { 217 Parameters = parameters 218 }; 219 220 var _navigationTemplate = "../Partials/Breadcrumb.cshtml"; 221 222 var alternativeImageObject = Model.ProductFields.Where(x => x.Key == "VjmpAlternativeManufacturerBrandLogo").FirstOrDefault(); 223 var alternativeImage = Dynamicweb.Core.Converter.ToString(alternativeImageObject.Value.Value); 224 225 bool isAssortmentGroup = Model.GroupPaths?.Any(groupList => 226 groupList.Any(group => group.Name == "Logovare")) == true; 227 } 228 229 <div class="pview os-container "> 230 231 <div class="pview__top position-relative d-flex flex-row justify-content-between align-items-center"> 232 233 <div class="pview__top__breadcrumb"> 234 @Navigation.RenderNavigation(_navigationTemplate, _navigationSettings) 235 </div> 236 237 <img class="pview__top__brand" src="@alternativeImage"/> 238 </div> 239 240 <div class="d-flex flew-row flex-wrap"> 241 242 243 <div class="pview__row row z-0"> 244 245 <div class="col-12 col-lg-6"> 246 <div class="product-view-gallery-wrapper position-relative"> 247 <div class="swiper product-view-gallery"> 248 <div class="swiper-wrapper"> 249 250 @foreach (var image in images) 251 { 252 var imageSrc = ""; 253 if (!string.IsNullOrEmpty(image)) 254 { 255 Dynamicweb.VestjyskMarketing.Models.ResizeImageSettings imageSettings = new Dynamicweb.VestjyskMarketing.Models.ResizeImageSettings 256 { 257 Width = 1500, 258 Height = 1500, 259 Crop = "5", 260 Quality = 90, 261 Image = image 262 }; 263 264 imageSrc = Dynamicweb.VestjyskMarketing.Helpers.ImageHelper.ResizeImage(imageSettings); 265 } 266 else 267 { 268 imageSrc = "/Files/Images/missing_image.jpg"; 269 } 270 271 string altText = OttoSchachner.Api.AISeo.GetProductImageAlt(product.Id, Model.VariantId, Model.LanguageId, image).FirstOrDefault()?.Alt ?? string.Empty; 272 if (string.IsNullOrEmpty(altText)) 273 { 274 altText = Translate("product-image-alt", "Produkt billede ") + " " + images.IndexOf(image); 275 } 276 277 <div class="swiper-slide"> 278 <img alt="@altText" class="pview__image-container__image" src="@imageSrc"> 279 </div> 280 } 281 282 @if (showVideo) 283 { 284 <div class="swiper-slide"> 285 <div class="pview__image-container__video-wrapper"> 286 <video class="pview__image-container__video" controls> 287 <source src="@mp4Path" type="video/mp4"> 288 Din browser understøtter ikke video-tagget. 289 </video> 290 </div> 291 </div> 292 } 293 294 @if (images.Count() == 0 && !showVideo) 295 { 296 <div class="swiper-slide"> 297 <img alt="Produkt billede 0" class="pview__image-container__image" 298 src="/Files/Images/missing_image.jpg"> 299 </div> 300 } 301 302 303 </div> 304 </div> 305 306 @{ 307 if (signedIn && purchasable) 308 { 309 string favoriteLink = "/Default.aspx?ID=" + pageService.GetPageByNavigationTag(PageView.Current().AreaID, "FavoriteService").ID + "&ProductID=" + Model.Id + "&ProductVariantId=" + Model.VariantId + "&UserID=" + PageView.Current().User.ID + "&ReloadPage=false"; 310 bool isInFavoriteList = false; 311 var favoriteLists = Pageview.User.GetFavoriteLists(); 312 int favoriteListContainingProductId = 0; 313 string command = "add"; 314 new FavoriteListService().ClearCache(); 315 316 foreach (var favoriteList in favoriteLists) 317 { 318 isInFavoriteList = Pageview.User.IsProductInFavoriteList(favoriteList.ListId, product.Id, Model.VariantId); 319 320 if (isInFavoriteList) 321 { 322 favoriteListContainingProductId = favoriteList.ListId; 323 command = "remove"; 324 break; 325 } 326 } 327 328 <a data-command="@command" data-in-this-list="@favoriteListContainingProductId" 329 data-url="@favoriteLink" href="javascript:void(0)" 330 title="@Translate("add-or-remove-favorites")" class="product-list__favorite z-2"> 331 332 @if (isInFavoriteList) 333 { 334 <i class="fa-sharp fa-solid fa-heart"></i> 335 } 336 else 337 { 338 <i class="fa-regular fa-heart"></i> 339 } 340 341 </a> 342 } 343 } 344 <div 345 class="product-view-gallery__navigation position-absolute top-50 translate-middle-y z-1 justify-content-between d-flex "> 346 <i class="os-chevron os-chevron--prev bi-chevron-left product-view-prev"></i> 347 <i class="os-chevron bi-chevron-right product-view-next"></i> 348 </div> 349 </div> 350 351 <div class="pview__thumbnails product-view-thumbs row g-3 mt-2 "> 352 @if (images.Count + (!showVideo ? 0 : 1) > 1) 353 { 354 var imagesArray = images.ToArray(); 355 356 for (int i = 0; 357 i < images.Count(); 358 i++) 359 { 360 Dynamicweb.VestjyskMarketing.Models.ResizeImageSettings imageSettings = new Dynamicweb.VestjyskMarketing.Models.ResizeImageSettings 361 { 362 Width = 750, 363 Height = 750, 364 Crop = "5", 365 Quality = 90, 366 Image = imagesArray[i] 367 }; 368 369 var imageSrc = Dynamicweb.VestjyskMarketing.Helpers.ImageHelper.ResizeImage(imageSettings); 370 371 int number = i + 1; 372 373 string altText = OttoSchachner.Api.AISeo.GetProductImageAlt(product.Id, Model.VariantId, Model.LanguageId, imagesArray[i]).FirstOrDefault()?.Alt ?? string.Empty; 374 if (string.IsNullOrEmpty(altText)) 375 { 376 altText = Translate("product-image-alt", "Produkt billede ") + " " + i; 377 } 378 379 <a title="@Translate("go-to-image", "Gå til billede ") @number" data-slide="@i" 380 class="pview__thumbnails__item col-2 "> 381 <img alt="@altText" loading="lazy" src="@imageSrc"/> 382 </a> 383 } 384 385 386 @if (showVideo) 387 { 388 <a title="@Translate("go-to-video", "Gå til video")" style="" data-slide="@images.Count()" 389 class="pview__thumbnails__item col-2 position-relative"> 390 <div class="w-100 h-100" style="background-color: white; border-radius: 10px"> 391 </div> 392 <i class="fa-solid fa-play position-absolute top-50 start-50 translate-middle fs-1 text-black"></i> 393 </a> 394 } 395 } 396 397 </div> 398 399 @if (signedIn && isMobile == false) 400 { 401 @RenderSections() 402 } 403 404 405 </div> 406 <div class="col-12 col-lg-6"> 407 <h1 class="pview__name"> 408 @Model.Name 409 </h1> 410 <div class="pview__productid "> 411 @Translate("product-number", "Varenr: ") @Model.Number 412 </div> 413 414 415 @if (purchasable && !isAssortmentGroup) 416 { 417 <div class="pview__price d-flex flex-row align-items-center"> 418 <div class="pview__price__text me-3"> 419 <span class="pview__price__text"> 420 @Translate("recommended-price", "Vejl. pris") 421 </span> 422 </div> 423 <span class="pview__price__amount">@product.DefaultPrice.ToString("F2") KR. <span 424 class="pview__price__amount__netto @(alwaysNetPrice ? "" : "d-none")" 425 data-lazy-netprice 426 data-product-id="@Model.Id" 427 data-variant-id="@Model.VariantId" 428 data-language-id="@Model.LanguageId">(@Translate("net-price", "netto") <span 429 class="lazy-netprice-value"><span 430 class="spinner-border spinner-border-sm align-baseline" role="status" 431 aria-hidden="true"></span></span> KR. )</span></span> 432 433 </div> 434 435 <div class="pview__price-toggler d-flex flex-row mb-3"> 436 437 438 <div 439 class="pview__price-toggler__container @(alwaysNetPrice ? "d-none" : "d-flex") flex-row align-items-center"> 440 <input class="me-3" type="checkbox" id="pview__netprice" name="horns" 441 @(alwaysNetPrice ? "checked" : "")/> 442 <label for="pview__netprice"> 443 @Translate("show-net-price", "Vis nettopris") 444 </label> 445 </div> 446 447 448 </div> 449 } 450 else if (signedIn == false) 451 { 452 <div class="pview__signin-message "> 453 @Translate("sign-in-to-see-prices", "LOG IND FOR AT SE PRISER OG KØBE PRODUKTET") 454 </div> 455 } 456 457 @if (purchasable) 458 { 459 @RenderPriceMatrix(uniquePricesMatrix, isAssortmentGroup, alwaysNetPrice) 460 } 461 462 <div class="pview__description"> 463 @Model.ShortDescription 464 @Model.LongDescription 465 </div> 466 467 @{ 468 string pictogramHtml = Model.ProductFields["VjmPictogramHtml"]?.Value.ToString(); 469 if (!string.IsNullOrEmpty(pictogramHtml)) 470 { 471 <div class="pictogram__section d-flex"> 472 <div class="d-flex "> 473 @pictogramHtml 474 </div> 475 </div> 476 } 477 } 478 479 @if (Model.VariantCombinations().Count > 1 && string.IsNullOrEmpty(Model.VariantId) && signedIn) 480 { 481 <div class="pview__variant-selector__message mb-3 fw-bold"> 482 @Translate("select-variant-message", "Du skal vælge variant før du kan tilføje til kurv") 483 </div> 484 } 485 @{ 486 string variantGroupName = ""; 487 bool colorGroup = Model.VariantInfo.VariantInfoGroupName == "Farver"; 488 489 @if (Model.VariantGroups().Count > 0) 490 { 491 if (Model.VariantGroups().Count == 1) 492 { 493 variantGroupName = Model.VariantGroups().First().Name.Replace(" ", ""); 494 variantGroupName = Translate("VariantGroup.Name." + variantGroupName, variantGroupName); 495 496 <div class="pview__cart__variant mb-3"> 497 <span class="fw-bold">@variantGroupName@(colorGroup ? ": " : "")</span> 498 @if (colorGroup) 499 { 500 <span>@Model.VariantName</span> 501 } 502 </div> 503 } 504 } 505 } 506 507 @if (signedIn && isMobile) 508 { 509 @RenderSections() 510 } 511 512 @{ 513 var step = Model.PurchaseQuantityStep; 514 if (step == null || step == 0) 515 { 516 step = 1; 517 } 518 519 if (isAssortmentGroup && uniquePricesMatrix.Count > 0) 520 { 521 step = uniquePricesMatrix?.FirstOrDefault()?.Quantity; 522 } 523 524 string stockLevel = Convert.ToString(product != null ? product.ProductFieldValues["VjmStockLevel"]?.Value : Model.ProductFields["VjmStockLevel"]?.Value); 525 string stockLevelText = ""; 526 527 if (!string.IsNullOrEmpty(stockLevel)) 528 { 529 stockLevel = stockLevel.ToLower(); 530 stockLevelText = StockLevelText(stockLevel); 531 } 532 533 534 <div class="pview__cart"> 535 <div class="pview__variant-selector row g-2 mb-2 mb-lg-4 d-flex flex-row flex-wrap"> 536 537 @if (Model.VariantGroups().Count == 1) 538 { 539 @foreach (var variantGroup in Model.VariantGroups()) 540 { 541 foreach (var option in variantGroup.Options) 542 { 543 string variantUrl = "/Default.aspx?ID=" + Pageview.ID + "&groupid=" + primaryGroup.Id + "&productid=" + Model.Id + "&variantid=" + option.Id; 544 var variantProduct = productService.GetProductById(Model.Id, option.Id, Model.LanguageId); 545 546 @if (Model.VariantInfo.VariantInfoGroupName == "Farver") 547 { 548 var variantImagePath = productImageService.GetImagePath(variantProduct); 549 550 Dynamicweb.VestjyskMarketing.Models.ResizeImageSettings imageSettings = new Dynamicweb.VestjyskMarketing.Models.ResizeImageSettings 551 { 552 Width = 1500, 553 Height = 1500, 554 Crop = "5", 555 Quality = 90, 556 Image = variantImagePath 557 }; 558 559 string imageSrc = Dynamicweb.VestjyskMarketing.Helpers.ImageHelper.ResizeImage(imageSettings); 560 561 <div class="w-auto"> 562 <div class="pview__variant-selector__item"> 563 <a href="@variantUrl" 564 title="@Translate("go-to", "Gå til") @variantGroupName.ToLower() @option.Name.ToLower()"> 565 <img class="w-100" alt="Variant @option.Name" src="@imageSrc"/> 566 </a> 567 </div> 568 </div> 569 } 570 else 571 { 572 <div class="w-auto"> 573 <div 574 class="pview__variant-selector__item number-selector @(Model.VariantId == option.Id ? "active" : "")"> 575 <a href="@variantUrl" 576 title="@Translate("go-to", "Gå til") @variantGroupName.ToLower() @option.Name.ToLower()" 577 class="d-flex h-100 w-100 align-items-center justify-content-center text-decoration-none"> 578 @option.Name 579 </a> 580 </div> 581 </div> 582 } 583 } 584 } 585 } 586 //multi variant håndtering 587 else 588 { 589 <div hidden="" id="variant-combinations"> 590 @string.Join(",", Model.VariantCombinations()) 591 </div> 592 @foreach (var variantGroup in Model.VariantGroups()) 593 { 594 string variantGroupNameMulti = Translate("VariantGroup.Name." + variantGroup.Name, variantGroup.Name); 595 <div class="pview__cart__variant fw-bold"> 596 @variantGroupNameMulti 597 </div> 598 <div class="d-flex mb-1 flex-wrap"> 599 @{ 600 foreach (var option in variantGroup.Options) 601 { 602 string variantUrl = "/Default.aspx?ID=" + Pageview.ID + "&groupid=" + primaryGroup.Id + "&productid=" + Model.Id + "&variantid=" + option.Id; 603 bool active = Model.VariantId.Contains(option.Id); 604 605 <div class="me-2 mb-2"> 606 <a data-group="@variantGroup.Name" data-id="@option.Id" 607 data-url="@variantUrl" href="javascript:void(0)" 608 title="@Translate("select", "vælg") @option.Name" 609 class="os-button os-button--no-hover @(active ? "" : "active os-button--transparent") d-flex h-100 w-100 align-items-center justify-content-center text-decoration-none"> 610 @option.Name 611 </a> 612 </div> 613 } 614 } 615 </div> 616 } 617 } 618 </div> 619 620 @if (purchasable) 621 { 622 <form method="post" class="h-100" id="pview-add-to-cart-form"> 623 <div class="pview__cart__container d-flex align-items-stretch mb-3"> 624 <div class="col-4"> 625 <input name="Quantity" type="number" min="@step" step="@step" value="@step" 626 class="pview__cart__container__quantity col-4"/> 627 </div> 628 <div class="col-8"> 629 630 <input type="hidden" name="ProductId" value='@Model.Id'/> 631 <input type="hidden" name="VariantId" value="@Model.VariantId"/> 632 <button class="pview__cart__container__button" type="submit" name="CartCmd" 633 value="add">Tilføj til kurv 634 </button> 635 636 </div> 637 </div> 638 </form> 639 <div class="d-flex flex-row align-items-center"> 640 <div class="col-4 d-flex flex-row justify-content-between align-items-center"> 641 <div class="pview__cart__min ">Min. antal: @step</div> 642 <div class="pview__cart__status-color pview__cart__status-color--@stockLevel"> 643 </div> 644 </div> 645 <div class="pview__cart__message d-flex align-items-center"> 646 @stockLevelText 647 </div> 648 </div> 649 } 650 </div>} 651 652 @if (signedIn == false) 653 { 654 @RenderSections() 655 } 656 </div> 657 </div> 658 </div> 659 </div> 660 661 662 663 @functions{ 664 665 string StockLevelText(string stockLevel) 666 { 667 string result = ""; 668 669 if (!string.IsNullOrEmpty(stockLevel)) 670 { 671 stockLevel = stockLevel.ToLower(); 672 673 switch (stockLevel) 674 { 675 case "green": 676 result = Translate("in-stock", "På lager"); 677 break; 678 case "yellow": 679 result = Translate("low-stock", "På vej hjem"); 680 break; 681 case "red": 682 result = Translate("out-of-stock", "Ikke på lager"); 683 break; 684 default: 685 result = Translate("out-of-stock", "Ikke på lager"); 686 break; 687 } 688 } 689 690 return result; 691 } 692 693 } 694 695 @if (Model.VariantCombinations().Count > 0 && !isAssortmentGroup) 696 { 697 var quickOrderVariants = product.GetVariantCombinations().Where(x => x.GetProduct(Model.LanguageId) != null && x.GetProduct(Model.LanguageId).Active).ToList(); 698 quickOrderVariants = quickOrderVariants.OrderBy(x => x.GetProduct(Model.LanguageId).Number).ToList(); 699 int count = 0; 700 <div class="pview__qorder"> 701 <div class="os-container"> 702 <div class="d-flex flex-column flex-lg-row justify-content-between flex-wrap mb-3 mb-lg-5"> 703 <div class="pview__qorder__header col-12 col-lg-auto mb-3 mb-lg-0"> 704 @Translate("quick-order", "Hurtig bestilling") 705 </div> 706 707 <div class="pview__qorder__input-container d-flex flex-row align-items-center col-12 col-lg-5"> 708 <i class="bi-search me-2"></i> 709 <input class="pview__qorder__input-container__input col-5" 710 placeholder='@Translate("quick-order-search", "Søg efter varenr., DB nr., materiale, størrelse m.m.")'/> 711 </div> 712 </div> 713 <form id="pview-qorder-form" method="post" action=""> 714 715 716 <input type="hidden" name="cartcmd" value="addmulti"/> 717 <input type="hidden" name="redirect" value="false"/> 718 719 <div class="product-table"> 720 <div 721 class="row header @(signedIn ? "signed-in" : "") @(Model.VariantGroups().Count > 1 ? "multi" : "")"> 722 <div>@Translate("order-variant-number", "Varenr.")</div> 723 724 @{ 725 List<string> variantGroupNames = new List<string>(); 726 } 727 728 @foreach (var variantGroup in Model.VariantGroups()) 729 { 730 string _variantGroupName = Translate("VariantGroup.Name." + variantGroup.Name, variantGroup.Name); 731 variantGroupNames.Add(_variantGroupName); 732 <div>@_variantGroupName</div> 733 } 734 735 736 <div>@Translate("db-number", "DB-nummer")</div> 737 <div>@Translate("ean-upc", "EAN/UPC")</div> 738 <div>@Translate("special-order-item", "Skaffevare")</div> 739 <div>@Translate("order-quantity", "Ordrekvantum")</div> 740 <div>@Translate("packaging", "Forpakning")</div> 741 @if (signedIn) 742 { 743 <div>@Translate("stock-status", "Lagerstatus")</div> 744 } 745 </div> 746 747 @{ 748 var matrixItems = new List<(Dynamicweb.Ecommerce.Products.Product Product, VariantCombination VariantComb, bool IsVariant)>(); 749 750 //hvis ingen varianter - hvis hovedproduktet i tabellen 751 if (quickOrderVariants.Count == 0) 752 { 753 matrixItems.Add((product, null, false)); 754 } 755 756 // Tilføj alle variants bagefter 757 foreach (var vc in quickOrderVariants) 758 { 759 matrixItems.Add((vc.GetProduct(Model.LanguageId), vc, true)); 760 } 761 } 762 763 <div class="row-wrapper"> 764 @foreach (var item in matrixItems) 765 { 766 count++; 767 768 769 var variant = item.Product; 770 var variantComb = item.VariantComb; 771 bool isVariant = item.IsVariant; 772 // DB-nummer (VjmDBNumber) 773 string dbNumber = 774 variant?.ProductFieldValues? 775 .GetProductFieldValue("VjmDBNumber")?.Value?.ToString() 776 ?? Translate("not-available", "N/A"); 777 778 // EAN 779 string EAN = !string.IsNullOrEmpty(variant?.EAN) 780 ? variant.EAN 781 : Translate("not-available", "N/A"); 782 783 string spStatus = variant?.ProductFieldValues.GetProductFieldValue("SpStatus")?.Value?.ToString().ToLower(); 784 785 if (spStatus == "skaffevare" || spStatus == "pris på forespørgsel") 786 { 787 spStatus = Translate("Yes", "Ja"); 788 } 789 else 790 { 791 spStatus = Translate("No", "Nej"); 792 } 793 794 string variantStockLevel = Convert.ToString(variant != null ? variant.ProductFieldValues["VjmStockLevel"]?.Value : "red"); 795 string variantStockLevelText = ""; 796 797 798 if (!string.IsNullOrEmpty(variantStockLevel)) 799 { 800 variantStockLevel = variantStockLevel.ToLower(); 801 variantStockLevelText = StockLevelText(variantStockLevel); 802 } 803 804 // Packaging (VjmUnitsConcat) 805 string packaging = 806 variant?.ProductFieldValues? 807 .GetProductFieldValue("VjmUnitsConcat")?.Value?.ToString() 808 ?? ""; 809 810 // Unit (VjmSalesUnit) 811 string unit = 812 variant?.ProductFieldValues? 813 .GetProductFieldValue("VjmSalesUnit")?.Value?.ToString() 814 ?? ""; 815 816 // Minimum order quantity 817 string minQty = variant?.PurchaseQuantityStep.ToString() ?? ""; 818 819 if (minQty == "0") 820 { 821 minQty = "1"; 822 unit = ""; 823 } 824 825 // Price 826 string bruttoPrice = $"{variant.DefaultPrice:0.00} KR"; 827 828 829 <div 830 class="row data @(count % 2 == 0 ? "visible-index-even" : "") @(signedIn ? "signed-in" : "") @(Model.VariantGroups().Count > 1 ? "multi" : "")" 831 data-id="@variant.Id" 832 data-name="@variant.Name" 833 data-variant="@(isVariant ? variantComb.VariantId : "")"> 834 <input type="hidden" name="ProductLoopCounter@(count)" id="ProductLoopCounter@(count)" 835 value="@count"/> 836 <input type="hidden" name="ProductId@(count)" id="ProductId@(count)" 837 value="@variant.Id"/> 838 @if (isVariant) 839 { 840 <input type="hidden" name="VariantId@(count)" value="@variantComb.VariantId"/> 841 } 842 843 <!-- Varenr. --> 844 <div class="d-flex justify-content-between d-lg-block"> 845 <span class="d-block d-lg-none">@Translate("order-variant-number", "Varenr.")</span> 846 <span class="searchable">@variant.Number</span> 847 </div> 848 <hr/> 849 <!-- Variantnavne --> 850 @if (isVariant) 851 { 852 int index = 0; 853 854 @foreach (var optionId in variantComb.GetVariantOptionIds()) 855 { 856 // Find gruppen og optionen mere sikkert 857 var group = Model.VariantGroups().FirstOrDefault(g => g.Options.Any(o => o.Id == optionId)); 858 var option = group?.Options.FirstOrDefault(o => o.Id == optionId); 859 860 <div class="d-flex justify-content-between d-lg-block"> 861 <span class="d-block d-lg-none">@variantGroupNames[index]</span> 862 <span class="searchable"> 863 @(option?.Name ?? "-") 864 </span> 865 </div> 866 <hr/> 867 index++; 868 } 869 } 870 871 872 <!-- DB nummer --> 873 <div class="d-flex justify-content-between d-lg-block"> 874 <span class="d-block d-lg-none">@Translate("db-number", "DB-nummer")</span> 875 <span class="searchable">@dbNumber</span> 876 </div> 877 878 <hr/> 879 <!-- EAN --> 880 <div class="d-flex justify-content-between d-lg-block"> 881 <span class="d-block d-lg-none">@Translate("ean-upc", "EAN/UPC")</span> 882 <span class="searchable">@EAN</span> 883 </div> 884 <hr/> 885 886 <!-- Skaffevare --> 887 <div class="d-flex justify-content-between d-lg-block"> 888 <span 889 class="d-block d-lg-none">@Translate("special-order-item", "Skaffevare")</span> 890 <span class="searchable">@spStatus</span> 891 </div> 892 893 <hr/> 894 <!-- Ordrekvantum --> 895 <div class="d-flex justify-content-between d-lg-block"> 896 <span class="d-block d-lg-none">@Translate("order-quantity", "Ordrekvantum")</span> 897 <span>@minQty @unit</span> 898 </div> 899 <hr/> 900 901 <!-- Forpakning --> 902 <div class="d-flex justify-content-between d-lg-block"> 903 <span class="d-block d-lg-none">@Translate("packaging", "Forpakning")</span> 904 <span>@packaging</span> 905 </div> 906 907 @if (signedIn) 908 { 909 <hr/> 910 <!-- Lagerstatus --> 911 <div class="d-flex justify-content-between d-lg-block align-items-center"> 912 <span class="d-block d-lg-none">@Translate("stock-status", "Lagerstatus")</span> 913 <div class="d-flex flex-row align-items-center p-0"> 914 <div 915 class="pview__cart__status-color pview__cart__status-color--@variantStockLevel"></div> 916 @variantStockLevelText 917 </div> 918 </div> 919 920 <hr/> 921 922 <!-- Pris --> 923 <div 924 class="d-flex justify-content-between d-lg-block flex-row flex-lg-column align-items-center"> 925 <span class="d-block d-lg-none">@Translate("price", "Pris")</span> 926 <div class="d-flex flex-column"> 927 <span class="fw-bold">@bruttoPrice</span> 928 <span 929 class="quick-order-net @(alwaysNetPrice ? "" : "d-none")" 930 data-lazy-netprice 931 data-product-id="@variant.Id" 932 data-variant-id="@(isVariant ? variantComb.VariantId : "")" 933 data-language-id="@Model.LanguageId">(<span class="lazy-netprice-value"><span 934 class="spinner-border spinner-border-sm align-baseline" 935 role="status" aria-hidden="true"></span></span> KR)</span> 936 <small>@Translate("quick-order-min", "Bestilles i"): @minQty @unit</small> 937 </div> 938 939 </div> 940 <hr/> 941 942 <!-- Antal vælger --> 943 <div class="d-flex justify-content-between d-lg-block align-items-center"> 944 <span class="d-block d-lg-none">@Translate("quantity", "Antal")</span> 945 <div class="pview__qorder__actions d-flex flex-row"> 946 <a class="pview__qorder__actions__decrement" href="javascript:void(0)">-</a> 947 <input class="pview__qorder__actions__input" name="Quantity@(count)" 948 type="number" step="@minQty" min="0" value="0"> 949 <a class="pview__qorder__actions__incremenet" 950 href="javascript:void(0)">+</a> 951 </div> 952 </div> 953 } 954 </div> 955 } 956 957 </div> 958 </div> 959 960 961 <div class="pview__qorder__no-results justify-content-center mt-3" style="display: none"> 962 @Translate("quick-order-no-results", "Ingen resultater fundet") 963 </div> 964 965 @if (signedIn) 966 { 967 <div class="d-flex justify-content-end"> 968 <button type="submit" class="pview__qorder__add-button os-button os-button--red mt-4"> 969 @Translate("add-all-to-cart", "Læg alle i kurv") 970 </button> 971 </div> 972 } 973 </form> 974 975 </div> 976 </div> 977 } 978 979 @{ 980 var productListParameters = new Dictionary<string, object>(); 981 productListParameters.Add("itemsToShow", 4); 982 productListParameters.Add("maxItems", 99); 983 984 List<dynamic> productsRelated = new List<dynamic>(); 985 foreach (var relatedGroup in Model.RelatedGroups) 986 { 987 if (relatedGroup.Id == "TILBEHØR") 988 { 989 foreach (var relatedProduct in relatedGroup.Products) 990 { 991 productsRelated.Add(ProductInfoViewModelExtensions.GetProduct(relatedProduct)); 992 } 993 } 994 } 995 996 bool slider = productsRelated.Count > 4; 997 productListParameters.Add("slider", slider); 998 productListParameters.Add("products", productsRelated); 999 } 1000 1001 @if (productsRelated.Count > 0) 1002 { 1003 <div class="os-container pview__related"> 1004 <div class="pview__related__header mb-4"> 1005 @Translate("product-add-ons", "Tilbehør") 1006 </div> 1007 <div class="@(slider ? "" : "row g-3") "> 1008 @RenderPartial("/Designs/OttoSchachner/Partials/ProductList.cshtml", new ParagraphViewModel(), productListParameters) 1009 </div> 1010 </div> 1011 } 1012 1013 @if (signedIn == false) 1014 { 1015 <div class="os-container pview__signup"> 1016 @{ 1017 var signupParagraphPageId = GetPageIdByNavigationTag("NotSignedInProductViewParagraph"); 1018 @(new Content(Pageview).RenderExternalGrid(signupParagraphPageId, "")) 1019 } 1020 </div> 1021 } 1022 1023 @if (purchasable && signedIn) 1024 { 1025 <script> 1026 window.dataLayer = window.dataLayer || []; 1027 (function () { 1028 function pushAddToCart(items, currency) { 1029 if (items.length > 0) { 1030 dataLayer.push({ecommerce: null}); 1031 dataLayer.push({ 1032 event: "add_to_cart", 1033 ecommerce: { 1034 currency: currency, 1035 items: items 1036 } 1037 }); 1038 } 1039 } 1040 1041 const quickOrderForm = document.getElementById('pview-qorder-form'); 1042 if (quickOrderForm) { 1043 function buildAndPushQuickOrderItems() { 1044 const items = []; 1045 const rows = quickOrderForm.querySelectorAll('.row.data'); 1046 rows.forEach(function (row) { 1047 const quantityInput = row.querySelector('.pview__qorder__actions__input'); 1048 const quantity = quantityInput ? parseInt(quantityInput.value || '0', 10) : 0; 1049 1050 if (quantity > 0) { 1051 items.push({ 1052 item_id: row.dataset.id, 1053 item_name: row.dataset.name, 1054 affiliation: "Online Store", 1055 variant: row.dataset.variant, 1056 price: parseFloat(row.dataset.price) || 0, 1057 quantity: quantity 1058 }); 1059 } 1060 }); 1061 1062 pushAddToCart(items, "@Model.Price.CurrencyCode"); 1063 } 1064 1065 quickOrderForm.addEventListener('submit', function (e) { 1066 var pending = (typeof window.lazyNetPriceFlush === 'function') 1067 ? window.lazyNetPriceFlush() 1068 : null; 1069 1070 if (pending && typeof pending.then === 'function') { 1071 e.preventDefault(); 1072 var submitBtn = quickOrderForm.querySelector('.pview__qorder__add-button'); 1073 if (submitBtn) submitBtn.disabled = true; 1074 1075 pending.then(buildAndPushQuickOrderItems, buildAndPushQuickOrderItems) 1076 .then(function () { 1077 if (submitBtn) submitBtn.disabled = false; 1078 quickOrderForm.submit(); 1079 }); 1080 } else { 1081 buildAndPushQuickOrderItems(); 1082 } 1083 }); 1084 } 1085 1086 const form = document.getElementById('pview-add-to-cart-form'); 1087 if (form) { 1088 form.addEventListener('submit', function () { 1089 const quantityInput = form.querySelector('input[name="Quantity"]'); 1090 const quantity = quantityInput ? parseInt(quantityInput.value || '1', 10) : 1; 1091 1092 const items = [ 1093 { 1094 item_id: "@Model.Id", 1095 item_name: "@Model.Name", 1096 affiliation: "Online Store", 1097 variant: "@Model.VariantId", 1098 price: @Model.Price?.PriceWithoutVat.ToString("F2", System.Globalization.CultureInfo.InvariantCulture), 1099 quantity: quantity 1100 } 1101 ]; 1102 1103 pushAddToCart(items, "@Model.Price.CurrencyCode"); 1104 }); 1105 } 1106 })(); 1107 1108 dataLayer.push({ecommerce: null}); 1109 dataLayer.push({ 1110 event: 'view_item', 1111 ecommerce: { 1112 currency: "@Model.Price?.CurrencyCode", 1113 value: @Model.Price?.PriceWithoutVat.ToString("F2", System.Globalization.CultureInfo.InvariantCulture), 1114 items: [ 1115 { 1116 item_name: "@Model.Name", 1117 item_id: "@Model.Id", 1118 price: @Model.Price?.PriceWithoutVat.ToString("F2", System.Globalization.CultureInfo.InvariantCulture), 1119 item_brand: "@Model.Manufacturer?.Name", 1120 item_category: "@categoryName", 1121 item_list_name: "@categoryName", 1122 item_list_id: "@categoryId", 1123 quantity: 1 1124 } 1125 ] 1126 } 1127 }); 1128 </script> 1129 } 1130