| | 1 | == ОAuth автентикација |
| | 2 | Корисникот кликнува на "Sign in with Google" или "Sign in with Facebook" копчето на Login страната, што активира OnPost() метод со provider параметар (Google/Facebook). |
| | 3 | |
| | 4 | _signInManager.ConfigureExternalAuthenticationProperties() креира AuthenticationProperties објект со redirectUrl до "./ExternalLogin" Callback handler-от и враќа ChallengeResult(provider, properties). |
| | 5 | |
| | 6 | OnGetCallbackAsync() го повикува _signInManager.GetExternalLoginInfoAsync() за да ги добие ClaimTypes.Email и ClaimTypes.Name од OAuth provider-от преку FindFirstValue(). |
| | 7 | - Ако корисникот веќе има поврзан external login, _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey) го најавува директно и прави LocalRedirect(returnUrl). |
| | 8 | - Ако корисникот постои со истиот email но без поврзан login, _userManager.AddLoginAsync(existingUser, info) го поврзува OAuth налогот со постоечкиот User запис. |
| | 9 | - Ако корисникот не постои, CreateUser() креира нов User instance, email.Split('@')[0] го генерира почетниот username, а while loop со _userManager.FindByNameAsync() проверува уникатност зголемувајќи counter додека не најде слободен username. |
| | 10 | |
| | 11 | |
| | 12 | == Нотификации |
| | 13 | Системот креира нотификација преку CreateNotificationAsync(userId, type, message, recipeId). |
| | 14 | |
| | 15 | _userManager.FindByIdAsync(userId) го наоѓа User објектот, а потоа проверува со type switch { "RecipeRated" => user.NotifyRecipeRated, "RecipeAccepted" => user.NotifyRecipeAccepted... } дали корисникот има овозможено нотификации од тој тип. |
| | 16 | |
| | 17 | Ако shouldNotify е false, методот враќа false без да креира нотификација. Ако е true, се креира нов Notification објект со UserId, Type, Message, RecipeId, IsRead = false и CreatedAt = DateTime.UtcNow. |
| | 18 | |
| | 19 | _context.Notifications.Add(notification) го додава записот, а await _context.SaveChangesAsync() го зачувува во базата. |
| | 20 | |
| | 21 | /Notifications/Stream endpoint користи Response.Headers.Add("Content-Type", "text/event-stream") за Server-Sent Events (SSE) за real-time updates. |
| | 22 | while (!HttpContext.RequestAborted.IsCancellationRequested) непрекинато врти, _context.Notifications.Count(n => n.UserId == userId && !n.IsRead) го пресметува unreadCount секои 3000ms преку await Task.Delay(3000). |
| | 23 | |
| | 24 | Кога unreadCount != lastUnreadCount, .OrderByDescending(n => n.CreatedAt).FirstOrDefault() ја зема најновата нотификација, System.Text.Json.JsonSerializer.Serialize(payload) ја конвертира во JSON. |
| | 25 | await Response.WriteAsync($"data: {json}\n\n") го испраќа SSE форматираниот одговор, а await Response.Body.FlushAsync() осигурува дека податоците се веднаш испратени до клиентот. |
| | 26 | |
| | 27 | GetNotifications() ги враќа последните нотификации со .Take() и unreadCount преку Json(new { notifications, unreadCount }) response. |
| | 28 | |
| | 29 | MarkAsRead(notificationId) го сетира notification.IsRead = true и зачувува со _context.SaveChanges(), додека MarkAllAsRead() користи .Where(n => n.UserId == userId && !n.IsRead).ToList() и foreach loop за bulk update. |
| | 30 | |
| | 31 | Delete(notificationId) користи _context.Notifications.Remove(notification) за бришење, а DeleteAll() користи .RemoveRange(userNotifications) за да ги избриши сите нотификации на корисникот. |
| | 32 | |
| | 33 | NotificationPanel() враќа PartialView("_NotificationPanel", notifications) кој динамички генерира HTML со @foreach (var notification in Model) loop, каде GetTimeAgo(notification.CreatedAt) ја пресметува релативната временска разлика со timeSpan.TotalMinutes, timeSpan.TotalHours и timeSpan.TotalDays. |
| | 34 | JavaScript код повикува fetch('/Notifications/GetNotifications') за да ги вчита нотификациите, додека EventSource('/Notifications/Stream') креира SSE connection за real-time updates со onmessage event handler. |
| | 35 | |
| | 36 | |
| | 37 | == CRUD за Restaurant Meal |
| | 38 | Администраторот кликнува на "Manage Restaurant Meals" копчето што го отвора modal-от со id="restaurantMealsModal", каде loadRestaurantMeals() прави GET барање до /Restaurants/Index. |
| | 39 | |
| | 40 | restaurantSelect.addEventListener("change") го детектира избраниот ресторан, а loadRestaurantMeals(restaurantId) прави fetch(/Restaurants/GetRestaurantMeals/${restaurantId}) за да ги добие сите оброци. |
| | 41 | |
| | 42 | showAddMealForm() го прикажува addMealFormSection div-от со style.display = "block", каде администраторот пополнува input полиња за mealItemName, mealItemDescription, calories, protein, carbs, fat . |
| | 43 | addRestaurantMeal() валидира дека document.querySelectorAll('input[name="mealType"]:checked').length > 0, креира RestaurantMeal објект со selectedTypes = Array.from(checkedBoxes).map(cb => cb.value). |
| | 44 | fetch('/Restaurants/AddRestaurantMeal', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(meal) }) го испраќа POST барањето. |
| | 45 | |
| | 46 | Контролерот AddRestaurantMeal([FromBody] RestaurantMeal meal) валидира IsNullOrWhiteSpace(meal.ItemName), проверува meal.RestaurantId != null && meal.RestaurantId != 0, и користи await _context.Restaurants.FindAsync(meal.RestaurantId) за да го најде ресторанот. |
| | 47 | meal.RestaurantName = restaurant.Name го сетира името, _context.RestaurantMeals.Add(meal) го додава записот, а await _context.SaveChangesAsync() го зачувува. |
| | 48 | |
| | 49 | CreateMealNotifications(meal, restaurant) креира нотификации за сите followers на ресторанот преку _context.RestaurantFollowings.Include(f => f.User).Where(f => f.RestaurantId == meal.RestaurantId && f.User.NotifyRestaurantNewMeal). |
| | 50 | |
| | 51 | editRestaurantMeal() прикажува populated form со постоечки вредности, а EditRestaurantMeal([FromBody] RestaurantMeal meal) ги ажурира existing.ItemName, existing.Type, existing.Calories преку _context.RestaurantMeals.FindAsync(meal.Id). |
| | 52 | |
| | 53 | deleteRestaurantMeal(mealId) прикажува confirmation dialog, а DeleteRestaurantMeal(int id) користи await _mealPlanService.HandleDeletedRestaurantMealAsync(id) за да ги отстрани reference-ите од meal plans пред да направи _context.RestaurantMeals.Remove(meal). |
| | 54 | |
| | 55 | |
| | 56 | == CRUD за Restaurant |
| | 57 | Администраторот кликнува "Add New Restaurant" што го активира showAddRestaurantForm() кој сетира addRestaurantFormSection.style.display = "block" и restaurantFormTitle.textContent = "Add New Restaurant". |
| | 58 | |
| | 59 | previewImage() користи FileReader() API со reader.readAsDataURL(file) за да креира preview на избраната слика, а reader.onload = (e) => { previewImg.src = e.target.result } ја прикажува сликата во imagePreview div-от. |
| | 60 | |
| | 61 | addRestaurant() валидира дека restaurantName.value.trim() !== "" и restaurantImage.files.length > 0, креира FormData објект со formData.append('name', name), formData.append('description', description), formData.append('image', imageFile). |
| | 62 | fetch('/Restaurants/AddRestaurant', { method: 'POST', body: formData }) го испраќа multipart/form-data барањето. |
| | 63 | |
| | 64 | AddRestaurant([FromForm] string name, [FromForm] string description, [FromForm] IFormFile image) ги bind-ува параметрите, Path.GetExtension(image.FileName).ToLowerInvariant() го проверува extension-от. |
| | 65 | await _context.Users.Where(u => u.NotifyNewRestaurant).ToListAsync() ги добива сите корисници со овозможени нотификации за нови ресторани. |
| | 66 | foreach (var user in users) loop креира Notification со Type = "NewRestaurant", Message = "New restaurant added: " + restaurant.Name, RecipeId = restaurant.Id. |
| | 67 | |
| | 68 | EditRestaurant([FromForm] int id, [FromForm] IFormFile image) проверува дали image != null && image.Length > 0, и ако е така, System.IO.File.Exists(oldImagePath) проверува за стара слика, System.IO.File.Delete(oldImagePath) ја брише старата слика пред да ја зачува новата. |
| | 69 | |
| | 70 | showEditRestaurantForm() ја популира формата со restaurant.Name, restaurant.Description и currentImage.src = restaurant.ImageUrl, а editRestaurantId.value = restaurant.Id го сетира hidden input-от за идентификација. |
| | 71 | |
| | 72 | DeleteRestaurant(int id) користи .Include(r => r.RestaurantMeals).Include(r => r.Followers) за eager loading, _context.RestaurantFollowings.RemoveRange(restaurant.Followers) ги брише followers. |
| | 73 | |
| | 74 | foreach (var meal in restaurant.RestaurantMeals) await _mealPlanService.HandleDeletedRestaurantMealAsync(meal.Id) ги отстранува meal references од сите meal plans. |
| | 75 | |
| | 76 | _context.RestaurantMeals.RemoveRange(restaurant.RestaurantMeals) ги брише сите оброци, System.IO.File.Delete(imagePath) ја брише сликата од wwwroot/images, а _context.Restaurants.Remove(restaurant) го брише ресторанот. |
| | 77 | |
| | 78 | |
| | 79 | |
| | 80 | |
| | 81 | == CRUD за Meal Keyword |
| | 82 | Администраторот го отвора "Manage Meal Tags" modal-от со id="mealTagsModal", каде loadMealKeywords() прави fetch('/Admin/GetMealKeywords') за да ги добие сите постоечки keywords. |
| | 83 | |
| | 84 | response.json() го парсира одговорот, а keywords.forEach(keyword => { ... }) динамички креира div.keyword-card елементи со innerHTML што содржи keyword.Name, keyword.Tag badge и delete копче. |
| | 85 | |
| | 86 | newKeywordName input field и newKeywordTag select dropdown овозможуваат внесување на нов keyword со тип (breakfast, main, snack). |
| | 87 | |
| | 88 | addMealKeyword() креира објект { Name: name, Tag: tag }, и прави fetch('/Admin/AddMealKeyword', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(keyword) }). |
| | 89 | |
| | 90 | Контролерот AddMealKeyword([FromBody] MealKeyword keyword) проверува дали keyword.Tag е валиден тип (breakfast, main, snack). |
| | 91 | _context.MealKeywords.Add(keyword) го додава записот, await _context.SaveChangesAsync() го зачувува, а return Json(new { success = true, keyword }) го враќа креираниот keyword со неговиот ID. |
| | 92 | |
| | 93 | JavaScript then(response => response.json()) го добива одговорот, createKeywordCard(data.keyword) динамички креира нов card element, mealKeywordsList.insertBefore(card, mealKeywordsList.firstChild) го додава на врвот на листата. |
| | 94 | newKeywordName.value = "" и newKeywordTag.selectedIndex = 0 ги ресетираат input полињата. |
| | 95 | |
| | 96 | deleteMealKeyword(keywordId) прикажува confirmation dialog со confirm('Are you sure you want to delete this keyword?'), а fetch(/Admin/DeleteMealKeyword/${keywordId}, { method: 'DELETE' }) го испраќа DELETE барањето. |
| | 97 | |
| | 98 | DeleteMealKeyword(int id) користи await _context.MealKeywords.FindAsync(id) за да го најде keyword-от, _context.MealKeywords.Remove(keyword) го брише записот. |
| | 99 | |
| | 100 | JavaScript then() handler користи document.querySelector([data-keyword-id="${keywordId}"]) за да го најде card-от, card.remove() го отстранува од DOM. |
| | 101 | |
| | 102 | loadMealKeywords() се повикува при отварање на modal-от преку $('#mealTagsModal').on('show.bs.modal', loadMealKeywords) jQuery event listener. |
| | 103 | |
| | 104 | |