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

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

Added major improvements.

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