source: KernelRecordsMVC.Web/Controllers/AdminController.cs@ d2eccb3

main
Last change on this file since d2eccb3 was d2eccb3, checked in by mmilevski <markomilevski3@…>, 4 weeks ago

Minor fixes.

  • Property mode set to 100644
File size: 27.3 KB
Line 
1using KernelRecordsMVC.Application.ViewModels;
2using KernelRecordsMVC.Domain.Enums;
3using KernelRecordsMVC.Domain.Models;
4using KernelRecordsMVC.Infrastructure.Data;
5using KernelRecordsMVC.Models;
6using Microsoft.AspNetCore.Mvc;
7using Microsoft.EntityFrameworkCore;
8
9namespace KernelRecordsMVC.Web.Controllers;
10
11public class AdminController : Controller
12{
13 private readonly KernelRecordsContext _context;
14
15 public AdminController(KernelRecordsContext context)
16 {
17 _context = context;
18 }
19
20
21 // =========================================================
22 // ADMIN DASHBOARD
23 // =========================================================
24
25 [HttpGet]
26 public IActionResult Index()
27 {
28 if (!IsAdmin())
29 return Forbid();
30
31
32 ViewBag.ProductCount =
33 _context.Products.Count();
34
35 ViewBag.ReleaseCount =
36 _context.Releases.Count();
37
38 ViewBag.OutOfStockCount =
39 _context.Products.Count(x =>
40 x.Stock <= 0);
41
42 ViewBag.LowStockCount =
43 _context.Products.Count(x =>
44 x.Stock > 0 &&
45 x.Stock <= 5);
46
47
48 return View();
49 }
50
51
52 // =========================================================
53 // PRODUCTS
54 // =========================================================
55
56 [HttpGet]
57 public IActionResult Products()
58 {
59 if (!IsProductManager())
60 return Forbid();
61
62
63 var products = _context.Products
64 .Include(p => p.Release)
65 .OrderBy(p => p.Release.Title)
66 .ThenBy(p => p.Format)
67 .ToList();
68
69
70 return View(products);
71 }
72
73
74 // =========================================================
75 // CREATE PRODUCT - GET
76 // =========================================================
77
78 [HttpGet]
79 public IActionResult CreateProduct()
80 {
81 if (!IsProductManager())
82 return Forbid();
83
84
85 LoadReleases();
86
87
88 return View(
89 new CreateProductViewModel());
90 }
91
92
93 // =========================================================
94 // CREATE PRODUCT - POST
95 // =========================================================
96
97 [HttpPost]
98 [ValidateAntiForgeryToken]
99 public IActionResult CreateProduct(
100 CreateProductViewModel model)
101 {
102 if (!IsProductManager())
103 return Forbid();
104
105
106 if (!ModelState.IsValid)
107 {
108 LoadReleases();
109
110 return View(model);
111 }
112
113
114 var release = _context.Releases
115 .FirstOrDefault(x =>
116 x.ReleaseId == model.ReleaseId);
117
118
119 if (release == null)
120 {
121 ModelState.AddModelError(
122 nameof(model.ReleaseId),
123 "The selected release does not exist.");
124
125
126 LoadReleases();
127
128
129 return View(model);
130 }
131
132
133 var alreadyExists =
134 _context.Products.Any(p =>
135 p.ReleaseId == model.ReleaseId &&
136 p.Format == model.Format);
137
138
139 if (alreadyExists)
140 {
141 ModelState.AddModelError(
142 nameof(model.Format),
143 "This release already has a product in this format.");
144
145
146 LoadReleases();
147
148
149 return View(model);
150 }
151
152
153 using var transaction =
154 _context.Database.BeginTransaction();
155
156
157 try
158 {
159 var product = new Product
160 {
161 ProductId =
162 GetNextProductId(),
163
164 ReleaseId =
165 model.ReleaseId,
166
167 Format =
168 model.Format,
169
170 Price =
171 model.Price,
172
173 ProductDescription =
174 model.ProductDescription,
175
176 Stock =
177 model.Stock
178 };
179
180
181 _context.Products.Add(product);
182
183 _context.SaveChanges();
184
185
186 CreateModification(
187 ModificationType.CREATE,
188 product.ProductId);
189
190
191 transaction.Commit();
192
193
194 TempData["Success"] =
195 $"{release.Title} ({product.Format}) was created successfully.";
196
197
198 return RedirectToAction(
199 nameof(Products));
200 }
201 catch
202 {
203 transaction.Rollback();
204
205 throw;
206 }
207 }
208
209
210 // =========================================================
211 // EDIT PRODUCT - GET
212 // =========================================================
213
214 [HttpGet]
215 public IActionResult EditProduct(long id)
216 {
217 if (!IsProductManager())
218 return Forbid();
219
220
221 var product = _context.Products
222 .Include(p => p.Release)
223 .FirstOrDefault(p =>
224 p.ProductId == id);
225
226
227 if (product == null)
228 return NotFound();
229
230
231 var model =
232 new EditProductViewModel
233 {
234 ProductId =
235 product.ProductId,
236
237 ReleaseId =
238 product.ReleaseId,
239
240 ReleaseTitle =
241 product.Release.Title,
242
243 Format =
244 product.Format,
245
246 Price =
247 product.Price,
248
249 ProductDescription =
250 product.ProductDescription,
251
252 Stock =
253 product.Stock,
254
255 ModificationType =
256 ModificationType.UPDATE
257 };
258
259
260 return View(model);
261 }
262
263
264 // =========================================================
265 // EDIT PRODUCT - POST
266 // =========================================================
267
268 [HttpPost]
269 [ValidateAntiForgeryToken]
270 public IActionResult EditProduct(
271 EditProductViewModel model)
272 {
273 if (!IsProductManager())
274 return Forbid();
275
276
277 var product = _context.Products
278 .Include(p => p.Release)
279 .FirstOrDefault(p =>
280 p.ProductId == model.ProductId);
281
282
283 if (product == null)
284 return NotFound();
285
286
287 // These values come from the database.
288 model.ReleaseId =
289 product.ReleaseId;
290
291 model.ReleaseTitle =
292 product.Release.Title;
293
294 model.Format =
295 product.Format;
296
297
298 // Display-only fields should not block
299 // validation of the submitted form.
300 ModelState.Remove(
301 nameof(model.ReleaseId));
302
303 ModelState.Remove(
304 nameof(model.ReleaseTitle));
305
306 ModelState.Remove(
307 nameof(model.Format));
308
309
310 // =====================================================
311 // DISCOUNT VALIDATION
312 // =====================================================
313
314 if (model.ModificationType ==
315 ModificationType.DISCOUNT)
316 {
317 if (!model.Discount.HasValue)
318 {
319 ModelState.AddModelError(
320 nameof(model.Discount),
321 "Please enter a discount percentage.");
322 }
323 else if (
324 model.Discount.Value <= 0 ||
325 model.Discount.Value >= 100)
326 {
327 ModelState.AddModelError(
328 nameof(model.Discount),
329 "Discount must be greater than 0 and less than 100.");
330 }
331 }
332
333
334 if (!ModelState.IsValid)
335 return View(model);
336
337
338 using var transaction =
339 _context.Database.BeginTransaction();
340
341
342 try
343 {
344 // =================================================
345 // APPLY DISCOUNT
346 // =================================================
347
348 if (model.ModificationType ==
349 ModificationType.DISCOUNT)
350 {
351 var discountPercentage =
352 model.Discount!.Value;
353
354
355 var oldPrice =
356 product.Price;
357
358
359 var discountAmount =
360 oldPrice *
361 (discountPercentage / 100m);
362
363
364 var newPrice =
365 oldPrice - discountAmount;
366
367
368 newPrice =
369 Math.Round(
370 newPrice,
371 2,
372 MidpointRounding.AwayFromZero);
373
374
375 // Product.Price becomes the
376 // actual current sale price.
377 product.Price =
378 newPrice;
379
380
381 _context.Products.Update(
382 product);
383
384 _context.SaveChanges();
385
386
387 CreateModification(
388 ModificationType.DISCOUNT,
389 product.ProductId,
390 discountPercentage);
391
392
393 transaction.Commit();
394
395
396 TempData["Success"] =
397 $"{discountPercentage:0.##}% discount applied. " +
398 $"Price changed from {oldPrice:C} to {newPrice:C}.";
399
400
401 return RedirectToAction(
402 nameof(Products));
403 }
404
405
406 // =================================================
407 // NORMAL UPDATE
408 // =================================================
409
410 product.Price =
411 model.Price;
412
413 product.ProductDescription =
414 model.ProductDescription;
415
416 product.Stock =
417 model.Stock;
418
419
420 _context.Products.Update(
421 product);
422
423 _context.SaveChanges();
424
425
426 CreateModification(
427 ModificationType.UPDATE,
428 product.ProductId);
429
430
431 transaction.Commit();
432
433
434 TempData["Success"] =
435 $"{product.Release.Title} was updated successfully.";
436
437
438 return RedirectToAction(
439 nameof(Products));
440 }
441 catch
442 {
443 transaction.Rollback();
444
445 throw;
446 }
447 }
448
449
450 // =========================================================
451 // CREATE RELEASE - GET
452 // =========================================================
453
454 [HttpGet]
455 public IActionResult CreateRelease()
456 {
457 if (!IsAdmin())
458 return Forbid();
459
460
461 LoadArtists();
462
463
464 var model =
465 new CreateReleaseViewModel
466 {
467 ReleaseDate =
468 DateTime.Today,
469
470 ReleaseType =
471 "ALBUM",
472
473 // Start an album with one empty track.
474 Tracks =
475 new List<CreateTrackViewModel>
476 {
477 new CreateTrackViewModel()
478 }
479 };
480
481
482 return View(model);
483 }
484
485
486 // =========================================================
487 // CREATE RELEASE - POST
488 // =========================================================
489
490 [HttpPost]
491 [ValidateAntiForgeryToken]
492 public IActionResult CreateRelease(
493 CreateReleaseViewModel model)
494 {
495 if (!IsAdmin())
496 return Forbid();
497
498
499 // =====================================================
500 // RELEASE TYPE VALIDATION
501 // =====================================================
502
503 if (model.ReleaseType != "ALBUM" &&
504 model.ReleaseType != "SINGLE")
505 {
506 ModelState.AddModelError(
507 nameof(model.ReleaseType),
508 "Please select Album or Single.");
509 }
510
511
512 // =====================================================
513 // MAIN ARTIST VALIDATION
514 // =====================================================
515
516 var mainArtistExists =
517 _context.Artists.Any(a =>
518 a.ArtistId ==
519 model.MainArtistId);
520
521
522 if (!mainArtistExists)
523 {
524 ModelState.AddModelError(
525 nameof(model.MainArtistId),
526 "Please select a valid main artist.");
527 }
528
529
530 // =====================================================
531 // FEATURED ARTISTS
532 // =====================================================
533
534 model.FeaturedArtistIds ??=
535 new List<long>();
536
537
538 model.FeaturedArtistIds =
539 model.FeaturedArtistIds
540 .Distinct()
541 .ToList();
542
543
544 // Do not allow the main artist
545 // to also be a featured artist.
546 model.FeaturedArtistIds.Remove(
547 model.MainArtistId);
548
549
550 // Validate that featured artists exist.
551 if (model.FeaturedArtistIds.Count > 0)
552 {
553 var validFeaturedArtistCount =
554 _context.Artists.Count(a =>
555 model.FeaturedArtistIds
556 .Contains(a.ArtistId));
557
558
559 if (validFeaturedArtistCount !=
560 model.FeaturedArtistIds.Count)
561 {
562 ModelState.AddModelError(
563 nameof(model.FeaturedArtistIds),
564 "One or more featured artists are invalid.");
565 }
566 }
567
568
569 // =====================================================
570 // SINGLE VALIDATION
571 // =====================================================
572
573 if (model.ReleaseType == "SINGLE")
574 {
575 if (string.IsNullOrWhiteSpace(
576 model.SingleDuration))
577 {
578 ModelState.AddModelError(
579 nameof(model.SingleDuration),
580 "Duration is required for a single.");
581 }
582 }
583
584
585 // =====================================================
586 // ALBUM VALIDATION
587 // =====================================================
588
589 if (model.ReleaseType == "ALBUM")
590 {
591 model.Tracks ??=
592 new List<CreateTrackViewModel>();
593
594
595 // Remove completely empty rows.
596 model.Tracks =
597 model.Tracks
598 .Where(t =>
599 !string.IsNullOrWhiteSpace(
600 t.SongName) ||
601 !string.IsNullOrWhiteSpace(
602 t.SongDuration))
603 .ToList();
604
605
606 if (model.Tracks.Count == 0)
607 {
608 ModelState.AddModelError(
609 nameof(model.Tracks),
610 "An album must contain at least one track.");
611 }
612
613
614 for (var i = 0;
615 i < model.Tracks.Count;
616 i++)
617 {
618 var track =
619 model.Tracks[i];
620
621
622 if (string.IsNullOrWhiteSpace(
623 track.SongName))
624 {
625 ModelState.AddModelError(
626 $"Tracks[{i}].SongName",
627 "Track name is required.");
628 }
629
630
631 if (string.IsNullOrWhiteSpace(
632 track.SongDuration))
633 {
634 ModelState.AddModelError(
635 $"Tracks[{i}].SongDuration",
636 "Track duration is required.");
637 }
638
639
640 track.ArtistIds ??=
641 new List<long>();
642
643
644 track.ArtistIds =
645 track.ArtistIds
646 .Distinct()
647 .ToList();
648
649
650 // Validate selected track artists.
651 if (track.ArtistIds.Count > 0)
652 {
653 var validTrackArtistCount =
654 _context.Artists.Count(a =>
655 track.ArtistIds.Contains(
656 a.ArtistId));
657
658
659 if (validTrackArtistCount !=
660 track.ArtistIds.Count)
661 {
662 ModelState.AddModelError(
663 $"Tracks[{i}].ArtistIds",
664 "One or more selected track artists are invalid.");
665 }
666 }
667 }
668 }
669
670
671 if (!ModelState.IsValid)
672 {
673 LoadArtists();
674
675 return View(model);
676 }
677
678
679 using var transaction =
680 _context.Database.BeginTransaction();
681
682
683 try
684 {
685 // =================================================
686 // CREATE RELEASE
687 // =================================================
688
689 var release =
690 new Release
691 {
692 ReleaseId =
693 GetNextReleaseId(),
694
695 Title =
696 model.Title.Trim(),
697
698 RecordLabel =
699 string.IsNullOrWhiteSpace(
700 model.RecordLabel)
701 ? null
702 : model.RecordLabel.Trim(),
703
704 Genre =
705 model.Genre.Trim(),
706
707 ReleaseDate =
708 model.ReleaseDate,
709
710 CoverPhoto =
711 model.CoverPhoto.Trim()
712 };
713
714
715 _context.Releases.Add(
716 release);
717
718 _context.SaveChanges();
719
720
721 // =================================================
722 // MAIN RELEASE ARTIST
723 // =================================================
724
725 var mainReleaseArtist =
726 new ReleaseArtist
727 {
728 ReleaseId =
729 release.ReleaseId,
730
731 ArtistId =
732 model.MainArtistId,
733
734 ReleaseOrdinal = 1,
735
736 Type =
737 ArtistReleaseType.MAIN
738 };
739
740
741 _context.ReleaseArtists.Add(
742 mainReleaseArtist);
743
744
745 // =================================================
746 // FEATURED RELEASE ARTISTS
747 // =================================================
748
749 long releaseOrdinal = 2;
750
751
752 foreach (var artistId
753 in model.FeaturedArtistIds)
754 {
755 var featuredArtist =
756 new ReleaseArtist
757 {
758 ReleaseId =
759 release.ReleaseId,
760
761 ArtistId =
762 artistId,
763
764 ReleaseOrdinal =
765 releaseOrdinal++,
766
767 Type =
768 ArtistReleaseType.FEATURE
769 };
770
771
772 _context.ReleaseArtists.Add(
773 featuredArtist);
774 }
775
776
777 _context.SaveChanges();
778
779
780 // =================================================
781 // ALBUM
782 // =================================================
783
784 if (model.ReleaseType == "ALBUM")
785 {
786 var album =
787 new Album
788 {
789 ReleaseId =
790 release.ReleaseId
791 };
792
793
794 _context.Albums.Add(
795 album);
796
797 _context.SaveChanges();
798
799
800 // =============================================
801 // TRACKS
802 // =============================================
803
804 foreach (var trackModel
805 in model.Tracks)
806 {
807 var song =
808 new Song
809 {
810 SongId =
811 GetNextSongId(),
812
813 SongName =
814 trackModel
815 .SongName
816 .Trim(),
817
818 SongDuration =
819 trackModel
820 .SongDuration
821 .Trim()
822 };
823
824
825 _context.Songs.Add(
826 song);
827
828 _context.SaveChanges();
829
830
831 // =========================================
832 // LINK SONG TO ALBUM
833 // =========================================
834
835 var albumSong =
836 new AlbumSong
837 {
838 AlbumId =
839 release.ReleaseId,
840
841 SongId =
842 song.SongId
843 };
844
845
846 _context.AlbumSongs.Add(
847 albumSong);
848
849
850 // =========================================
851 // SONG ARTISTS
852 // =========================================
853
854 var trackArtistIds =
855 trackModel.ArtistIds
856 .Distinct()
857 .ToList();
858
859
860 // If no track artists were selected,
861 // automatically use the release's
862 // main artist.
863 if (trackArtistIds.Count == 0)
864 {
865 trackArtistIds.Add(
866 model.MainArtistId);
867 }
868
869
870 long songOrdinal = 1;
871
872
873 foreach (var artistId
874 in trackArtistIds)
875 {
876 var songArtist =
877 new SongArtist
878 {
879 SongId =
880 song.SongId,
881
882 ArtistId =
883 artistId,
884
885 SongOrdinal =
886 songOrdinal++
887 };
888
889
890 _context.SongArtists.Add(
891 songArtist);
892 }
893
894
895 _context.SaveChanges();
896 }
897 }
898
899
900 // =================================================
901 // SINGLE
902 // =================================================
903
904 else
905 {
906 var single =
907 new SingleRelease
908 {
909 ReleaseId =
910 release.ReleaseId,
911
912 Duration =
913 model.SingleDuration!
914 .Trim()
915 };
916
917
918 _context.SingleReleases.Add(
919 single);
920
921 _context.SaveChanges();
922 }
923
924
925 transaction.Commit();
926
927
928 TempData["Success"] =
929 $"{release.Title} was created successfully.";
930
931
932 return RedirectToAction(
933 nameof(Index));
934 }
935 catch
936 {
937 transaction.Rollback();
938
939 throw;
940 }
941 }
942
943
944 // =========================================================
945 // AUTHORIZATION HELPERS
946 // =========================================================
947
948 private bool IsAdmin()
949 {
950 return HttpContext.Session
951 .GetString("Role") ==
952 "Admin";
953 }
954
955
956 private bool IsProductManager()
957 {
958 var role =
959 HttpContext.Session
960 .GetString("Role");
961
962 var adminType =
963 HttpContext.Session
964 .GetString("AdminType");
965
966
967 return role == "Admin" &&
968 (
969 adminType == "PRODUCT_MANAGER" ||
970 adminType == "SUPER_ADMIN"
971 );
972 }
973
974
975 // =========================================================
976 // CURRENT ADMIN ID
977 // =========================================================
978
979 private long? GetCurrentUserId()
980 {
981 var userId =
982 HttpContext.Session
983 .GetInt32("UserId");
984
985
986 if (!userId.HasValue)
987 return null;
988
989
990 return userId.Value;
991 }
992
993
994 // =========================================================
995 // LOAD RELEASES
996 // =========================================================
997
998 private void LoadReleases()
999 {
1000 ViewBag.Releases =
1001 _context.Releases
1002 .OrderBy(x =>
1003 x.Title)
1004 .ToList();
1005 }
1006
1007
1008 // =========================================================
1009 // LOAD ARTISTS
1010 // =========================================================
1011
1012 private void LoadArtists()
1013 {
1014 ViewBag.Artists =
1015 _context.Artists
1016 .OrderBy(a =>
1017 a.ArtistName)
1018 .ToList();
1019 }
1020
1021
1022 // =========================================================
1023 // NEXT PRODUCT ID
1024 // =========================================================
1025
1026 private long GetNextProductId()
1027 {
1028 var maxId =
1029 _context.Products
1030 .Select(x =>
1031 (long?)x.ProductId)
1032 .Max();
1033
1034
1035 return (maxId ?? 0) + 1;
1036 }
1037
1038
1039 // =========================================================
1040 // NEXT RELEASE ID
1041 // =========================================================
1042
1043 private long GetNextReleaseId()
1044 {
1045 var maxId =
1046 _context.Releases
1047 .Select(x =>
1048 (long?)x.ReleaseId)
1049 .Max();
1050
1051
1052 return (maxId ?? 0) + 1;
1053 }
1054
1055
1056 // =========================================================
1057 // NEXT SONG ID
1058 // =========================================================
1059
1060 private long GetNextSongId()
1061 {
1062 var maxId =
1063 _context.Songs
1064 .Select(x =>
1065 (long?)x.SongId)
1066 .Max();
1067
1068
1069 return (maxId ?? 0) + 1;
1070 }
1071
1072
1073 // =========================================================
1074 // NEXT MODIFICATION ID
1075 // =========================================================
1076
1077 private long GetNextModificationId()
1078 {
1079 var maxId =
1080 _context.Modifications
1081 .Select(x =>
1082 (long?)x.ModificationId)
1083 .Max();
1084
1085
1086 return (maxId ?? 0) + 1;
1087 }
1088
1089
1090 // =========================================================
1091 // CREATE MODIFICATION RECORD
1092 // =========================================================
1093
1094 private void CreateModification(
1095 ModificationType type,
1096 long productId,
1097 decimal? discount = null)
1098 {
1099 var adminId =
1100 GetCurrentUserId();
1101
1102
1103 if (adminId == null)
1104 {
1105 throw new InvalidOperationException(
1106 "The current admin could not be identified.");
1107 }
1108
1109
1110 var modification =
1111 new Modification
1112 {
1113 ModificationId =
1114 GetNextModificationId(),
1115
1116 AdminId =
1117 adminId.Value,
1118
1119 DateModified =
1120 DateTime.Today,
1121
1122 TypeOfModification =
1123 type,
1124
1125 Discount =
1126 discount
1127 };
1128
1129
1130 _context.Modifications.Add(
1131 modification);
1132
1133 _context.SaveChanges();
1134
1135
1136 var modificationProduct =
1137 new ModificationProduct
1138 {
1139 ModificationId =
1140 modification.ModificationId,
1141
1142 ProductId =
1143 productId
1144 };
1145
1146
1147 _context.ModificationProducts.Add(
1148 modificationProduct);
1149
1150 _context.SaveChanges();
1151 }
1152}
Note: See TracBrowser for help on using the repository browser.