diff --git a/.github/workflows/buildAndTest.yml b/.github/workflows/buildAndTest.yml index d09d2fa5..8cdbb17e 100644 --- a/.github/workflows/buildAndTest.yml +++ b/.github/workflows/buildAndTest.yml @@ -34,8 +34,28 @@ jobs: cache-dependency-path: package-lock.json - run: npm ci - run: npm run build --if-present - - run: npm run test:dev - - run: npm run test:unit + + - name: Test with Angular + run: npm run test:dev + + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '17' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + working-directory: backend + + - name: Test with Gradle + run: ./gradlew test + working-directory: backend + env: + JWT_SECRET: ${{ secrets.JWT_SECRET }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + DB_USERNAME: ${{ secrets.DB_USERNAME }} + DB_URI: ${{ secrets.DB_URI }} - name: Download artifacts uses: actions/download-artifact@v2 diff --git a/.gitignore b/.gitignore index e1caddf6..c96a4da0 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,5 @@ out/ ### VS Code ### .vscode/ angular.json +package-lock.json +package.json \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 623309d5..e69de29b 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "ionic.ionic" - ] -} diff --git a/backend/build.gradle b/backend/build.gradle index d63e24fb..a4b9dc37 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -14,7 +14,10 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-neo4j' - // implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'io.github.cdimascio:java-dotenv:5.2.2' diff --git a/backend/gradlew b/backend/gradlew old mode 100644 new mode 100755 index a69d9cb6..ab2855b4 --- a/backend/gradlew +++ b/backend/gradlew @@ -237,4 +237,4 @@ eval "set -- $( tr '\n' ' ' )" '"$@"' -exec "$JAVACMD" "$@" +exec "$JAVACMD" "$@" \ No newline at end of file diff --git a/backend/gradlew.bat b/backend/gradlew.bat old mode 100644 new mode 100755 diff --git a/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java b/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java new file mode 100644 index 00000000..987aaaa5 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java @@ -0,0 +1,49 @@ +package fellowship.mealmaestro.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +import fellowship.mealmaestro.services.UserService; + +@Configuration +public class ApplicationConfig { + + private final UserService userService; + + public ApplicationConfig(UserService userService){ + this.userService = userService; + } + + @Bean + public UserDetailsService userDetailsService(){ + return username -> userService.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("User '" + username + "' not found")); + } + + @Bean + public AuthenticationProvider authenticationProvider(){ + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService()); + provider.setPasswordEncoder(passwordEncoder()); + return provider; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + //TODO + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/config/CORSConfig.java b/backend/src/main/java/fellowship/mealmaestro/config/CORSConfig.java deleted file mode 100644 index beb42f2f..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/config/CORSConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package fellowship.mealmaestro.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class CORSConfig { - - @Bean - public WebMvcConfigurer corsConfigurer(){ - return new WebMvcConfigurer(){ - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedMethods("*") - .allowedOrigins("http://localhost:4200", "http://localhost:8100"); - } - }; - } -} diff --git a/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java b/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java new file mode 100644 index 00000000..c9951139 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java @@ -0,0 +1,16 @@ +package fellowship.mealmaestro.config; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleUserNotFoundException(RuntimeException e){ + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/config/JwtAuthenticationFilter.java b/backend/src/main/java/fellowship/mealmaestro/config/JwtAuthenticationFilter.java new file mode 100644 index 00000000..7869908c --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/config/JwtAuthenticationFilter.java @@ -0,0 +1,68 @@ +package fellowship.mealmaestro.config; + +import java.io.IOException; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import fellowship.mealmaestro.models.UserModel; +import fellowship.mealmaestro.services.auth.JwtService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + + private final UserDetailsService userDetailsService; + + public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService){ + this.jwtService = jwtService; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + final String authHeader = request.getHeader("Authorization"); + final String jwtToken; + final String userEmail; + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + jwtToken = authHeader.substring(7); + userEmail = jwtService.extractUserEmail(jwtToken); + + if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserModel userDetails = (UserModel) this.userDetailsService.loadUserByUsername(userEmail); + if (jwtService.isTokenValid(jwtToken, userDetails)){ + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + + authToken.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request) + ); + + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + + filterChain.doFilter(request, response); + } + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/config/Neo4jConfig.java b/backend/src/main/java/fellowship/mealmaestro/config/Neo4jConfig.java index 82f8f3e1..f5ac5198 100644 --- a/backend/src/main/java/fellowship/mealmaestro/config/Neo4jConfig.java +++ b/backend/src/main/java/fellowship/mealmaestro/config/Neo4jConfig.java @@ -12,10 +12,32 @@ public class Neo4jConfig { @Bean public Driver neo4jDriver() { - Dotenv dotenv = Dotenv.load(); - String uri = dotenv.get("DB_URI"); - String username = dotenv.get("DB_USERNAME"); - String password = dotenv.get("DB_PASSWORD"); + String uri; + String username; + String password; + Dotenv dotenv; + + if (System.getenv("DB_URI") != null) { + uri = System.getenv("DB_URI"); + username = System.getenv("DB_USERNAME"); + password = System.getenv("DB_PASSWORD"); + + return GraphDatabase.driver(uri, AuthTokens.basic(username, password)); + } + + try { + dotenv = Dotenv.load(); + uri = dotenv.get("DB_URI"); + username = dotenv.get("DB_USERNAME"); + password = dotenv.get("DB_PASSWORD"); + } catch (Exception e){ + dotenv = Dotenv.configure() + .ignoreIfMissing() + .load(); + uri = "No DB URI Found"; + username = "No DB Username Found"; + password = "No DB Password Found"; + } return GraphDatabase.driver(uri, AuthTokens.basic(username, password)); } diff --git a/backend/src/main/java/fellowship/mealmaestro/config/SecurityConfig.java b/backend/src/main/java/fellowship/mealmaestro/config/SecurityConfig.java new file mode 100644 index 00000000..e2c1f80a --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/config/SecurityConfig.java @@ -0,0 +1,61 @@ +package fellowship.mealmaestro.config; + +import java.util.Arrays; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthFilter; + + private final AuthenticationProvider authenticationProvider; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter, AuthenticationProvider authenticationProvider){ + this.jwtAuthFilter = jwtAuthFilter; + this.authenticationProvider = authenticationProvider; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ + http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(authReq -> authReq + .requestMatchers("/register", "/authenticate") + .permitAll() + .anyRequest() + .authenticated() + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + + + http.authenticationProvider(authenticationProvider); + return http.build(); + } + + @Bean + CorsConfigurationSource corsConfigurationSource(){ + CorsConfiguration corsConfig = new CorsConfiguration(); + corsConfig.setAllowedOrigins(Arrays.asList("http://localhost:4200", "http://localhost:8100")); + corsConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + corsConfig.setAllowedHeaders(Arrays.asList("*")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfig); + return source; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/BrowseController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/BrowseController.java new file mode 100644 index 00000000..df202956 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/BrowseController.java @@ -0,0 +1,43 @@ +package fellowship.mealmaestro.controllers; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import fellowship.mealmaestro.models.MealModel; +import fellowship.mealmaestro.services.BrowseService; +//import fellowship.mealmaestro.services.PantryService; +import jakarta.validation.Valid; + +@RestController +public class BrowseController { + + @Autowired + private BrowseService browseService; + + @GetMapping("/getPopularMeals") + public ResponseEntity> getPopularMeals(@RequestHeader("Authorization") String token){ + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + String authToken = token.substring(7); + return ResponseEntity.ok(browseService.getPopularMeals(authToken)); + } + + @GetMapping("/getSearchedMeals") + public ResponseEntity> getSearcedhMeals(@RequestParam("query") String mealName, @RequestHeader("Authorization") String token){ + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + String authToken = token.substring(7); + return ResponseEntity.ok(browseService.getSearchedMeals(mealName,authToken)); + } + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java index 25943a13..9dfea63f 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java @@ -1,27 +1,181 @@ package fellowship.mealmaestro.controllers; +import java.time.DayOfWeek; +import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; + +import org.springframework.web.bind.annotation.RequestParam; + import org.springframework.web.bind.annotation.RestController; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import fellowship.mealmaestro.models.DaysMealsModel; +import fellowship.mealmaestro.models.MealModel; +import fellowship.mealmaestro.services.MealDatabseService; import fellowship.mealmaestro.services.MealManagementService; +import jakarta.validation.Valid; @RestController public class MealManagementController { @Autowired - private MealManagementService mealManagementService = new MealManagementService(); + private MealManagementService mealManagementService; + @Autowired + private MealDatabseService mealDatabseService; + + public static class DateModel { + private DayOfWeek dayOfWeek; + + public void setDayOfWeek(DayOfWeek dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } + + public DayOfWeek getDayOfWeek() { + return this.dayOfWeek; + } + + public DateModel() { + }; + } + + @PostMapping("/getDaysMeals") + public String dailyMeals(@Valid @RequestBody DateModel request, @RequestHeader("Authorization") String token) + throws JsonMappingException, JsonProcessingException { + // admin + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build().toString(); + } + + DayOfWeek dayOfWeek = request.getDayOfWeek(); + ObjectMapper objectMapper = new ObjectMapper(); + // retrieve + Optional mealsForWeek = mealDatabseService.findUsersDaysMeals(dayOfWeek, token); + if (mealsForWeek.isPresent()) { + System.out.println("loaded from database"); + ObjectNode daysMealsModel = objectMapper.valueToTree(mealsForWeek.get()); + + return daysMealsModel.toString(); + } else { + // generate + + System.out.println("generated"); + + JsonNode mealsModels = mealManagementService.generateDaysMealsJson(); + // += + // objectMapper.treeToValue(mealManagementService.generateDaysMealsJson(),DaysMealsModel.class); + // save + mealDatabseService.saveDaysMeals(mealsModels, dayOfWeek, token); + // return + return mealsModels.toString(); + } - @GetMapping("/getDaysMeals") - public String dailyMeals() throws JsonMappingException, JsonProcessingException{ - return mealManagementService.generateDaysMeals(); } @GetMapping("/getMeal") - public String meal() throws JsonMappingException, JsonProcessingException{ + public String meal() throws JsonMappingException, JsonProcessingException { return mealManagementService.generateMeal(); } -} + + public static JsonNode findMealSegment(JsonNode jsonNode, String mealType) { + if (jsonNode.isObject()) { + JsonNode startNode = jsonNode.get("start"); + if (startNode != null) { + JsonNode startProperties = startNode.get("properties"); + if (startProperties != null) { + JsonNode mealDateNode = startProperties.get("mealDate"); + if (mealDateNode != null && mealType.equalsIgnoreCase(mealDateNode.asText())) { + return jsonNode; + } + } + } + + JsonNode segmentsNode = jsonNode.get("segments"); + if (segmentsNode != null) { + for (JsonNode segment : segmentsNode) { + JsonNode foundNode = findMealSegment(segment, mealType); + if (foundNode != null) { + return foundNode; + } + } + } + } + + return null; + } + + @PostMapping("/regenerate") + public String regenerate(@Valid @RequestBody DaysMealsModel request, @RequestHeader("Authorization") String token) + throws JsonMappingException, JsonProcessingException { + + ObjectMapper objectMapper = new ObjectMapper(); + MealModel mealModel = new MealModel(); + DayOfWeek dayOfWeek = request.getMealDate(); + Optional databaseModel = mealDatabseService.findUsersDaysMeals(dayOfWeek, token); + + if (databaseModel.isPresent()) { + DaysMealsModel newModel = databaseModel.get(); + System.out.println("present"); + + String meal = request.getMeal(); + if (meal.equals("breakfast")) { + + mealModel = newModel.getBreakfast(); + mealModel = objectMapper.readValue(mealManagementService.generateMeal(request.getMeal()), + MealModel.class); + + newModel.setBreakfast(mealModel); + } else if (meal.equals("lunch")) { + + mealModel = newModel.getLunch(); + mealModel = objectMapper.readValue(mealManagementService.generateMeal(request.getMeal()), + MealModel.class); + + newModel.setLunch(mealModel); + } else if (meal.equals("dinner")) { + + mealModel = newModel.getDinner(); + mealModel = objectMapper.readValue(mealManagementService.generateMeal(request.getMeal()), + MealModel.class); + + newModel.setDinner(mealModel); + } + + System.out.println(objectMapper.valueToTree(mealModel).toString()); + + this.mealDatabseService.saveRegeneratedMeal(newModel); + + ObjectNode daysMealsModel = objectMapper.valueToTree(newModel); + + return daysMealsModel.toString(); + + } + ObjectNode daysMealsModel = objectMapper.valueToTree(request); + return daysMealsModel.toString(); + } + + + // @GetMapping("/getPopularMeals") + // public String popularMeals() throws JsonMappingException, JsonProcessingException{ + // return mealManagementService.generatePopularMeals(); + // } + + // @GetMapping("/getSearchedMeals") + // public String searchedMeals(@RequestParam String query) throws JsonMappingException, JsonProcessingException { + // // Call the mealManagementService to search meals based on the query + // return mealManagementService.generateSearchedMeals(query); + // } + } + + + diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/PantryController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/PantryController.java index 73fcf934..fa274ba3 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/PantryController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/PantryController.java @@ -3,13 +3,13 @@ import java.util.List; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import fellowship.mealmaestro.models.FoodModel; -import fellowship.mealmaestro.models.UserModel; -import fellowship.mealmaestro.models.PantryRequestModel; import fellowship.mealmaestro.services.PantryService; import jakarta.validation.Valid; @@ -20,23 +20,40 @@ public class PantryController { private PantryService pantryService; @PostMapping("/addToPantry") - public FoodModel addToPantry(@Valid @RequestBody PantryRequestModel pantryRequest){ - return pantryService.addToPantry(pantryRequest); + public ResponseEntity addToPantry(@Valid @RequestBody FoodModel request, @RequestHeader("Authorization") String token){ + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + String authToken = token.substring(7); + return ResponseEntity.ok(pantryService.addToPantry(request, authToken)); } @PostMapping("/removeFromPantry") - public void removeFromPantry(@Valid @RequestBody PantryRequestModel pantryRequest){ - pantryService.removeFromPantry(pantryRequest); + public ResponseEntity removeFromPantry(@Valid @RequestBody FoodModel request, @RequestHeader("Authorization") String token){ + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + String authToken = token.substring(7); + pantryService.removeFromPantry(request, authToken); + return ResponseEntity.ok().build(); } @PostMapping("/updatePantry") - public void updatePantry(@Valid @RequestBody PantryRequestModel pantryRequest){ - pantryService.updatePantry(pantryRequest); + public ResponseEntity updatePantry(@Valid @RequestBody FoodModel request, @RequestHeader("Authorization") String token){ + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + String authToken = token.substring(7); + pantryService.updatePantry(request, authToken); + return ResponseEntity.ok().build(); } @PostMapping("/getPantry") - public List getPantry(@RequestBody UserModel user){ - return pantryService.getPantry(user); + public ResponseEntity> getPantry(@RequestHeader("Authorization") String token){ + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + String authToken = token.substring(7); + return ResponseEntity.ok(pantryService.getPantry(authToken)); } - } diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/RecipeBookController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/RecipeBookController.java new file mode 100644 index 00000000..326f9236 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/RecipeBookController.java @@ -0,0 +1,52 @@ +package fellowship.mealmaestro.controllers; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import fellowship.mealmaestro.models.MealModel; +import fellowship.mealmaestro.services.RecipeBookService; +import jakarta.validation.Valid; + +import java.util.List; + +@RestController +public class RecipeBookController { + + private final RecipeBookService recipeBookService; + + public RecipeBookController(RecipeBookService recipeBookService) { + this.recipeBookService = recipeBookService; + } + + @PostMapping("/addRecipe") + public ResponseEntity addRecipe(@Valid @RequestBody MealModel request, @RequestHeader("Authorization") String token) { + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + + String authToken = token.substring(7); + return ResponseEntity.ok(recipeBookService.addRecipe(request, authToken)); + } + + @PostMapping("/removeRecipe") + public ResponseEntity removeRecipe(@Valid @RequestBody MealModel request, @RequestHeader("Authorization") String token) { + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + + String authToken = token.substring(7); + recipeBookService.removeRecipe(request, authToken); + + return ResponseEntity.ok().build(); + } + + @PostMapping("/getAllRecipes") + public ResponseEntity> getAllRecipes(@RequestHeader("Authorization") String token) { + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + + String authToken = token.substring(7); + return ResponseEntity.ok(recipeBookService.getAllRecipes(authToken)); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/SettingsController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/SettingsController.java new file mode 100644 index 00000000..021c92fb --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/SettingsController.java @@ -0,0 +1,41 @@ +package fellowship.mealmaestro.controllers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import fellowship.mealmaestro.models.SettingsModel; +import fellowship.mealmaestro.services.SettingsService; +import jakarta.validation.Valid; + +@RestController +public class SettingsController { + + @Autowired + private SettingsService settingsService; + + + @PostMapping("/getSettings") + public ResponseEntity getSettings(@RequestHeader("Authorization") String token){ + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + String authToken = token.substring(7); + return ResponseEntity.ok(settingsService.getSettings(authToken)); + } + + @PostMapping("/updateSettings") + public ResponseEntity updateSettings(@Valid @RequestBody SettingsModel request, @RequestHeader("Authorization") String token){ + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + String authToken = token.substring(7); + request.setUserBMI(request.getUserHeight(), request.getUserWeight()); + settingsService.updateSettings(request, authToken); + return ResponseEntity.ok().build(); + } + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/ShoppingListController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/ShoppingListController.java index a5d81a66..a5370fca 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/ShoppingListController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/ShoppingListController.java @@ -3,13 +3,13 @@ import java.util.List; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import fellowship.mealmaestro.models.FoodModel; -import fellowship.mealmaestro.models.ShoppingListRequestModel; -import fellowship.mealmaestro.models.UserModel; import fellowship.mealmaestro.services.ShoppingListService; import jakarta.validation.Valid; @@ -20,22 +20,51 @@ public class ShoppingListController { private ShoppingListService shoppingListService; @PostMapping("/addToShoppingList") - public FoodModel addToShoppingList(@Valid @RequestBody ShoppingListRequestModel request){ - return shoppingListService.addToShoppingList(request); + public ResponseEntity addToShoppingList(@Valid @RequestBody FoodModel request, @RequestHeader("Authorization") String token){ + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + String authToken = token.substring(7); + return ResponseEntity.ok(shoppingListService.addToShoppingList(request, authToken)); } @PostMapping("/removeFromShoppingList") - public void removeFromShoppingList(@Valid @RequestBody ShoppingListRequestModel request){ - shoppingListService.removeFromShoppingList(request); + public ResponseEntity removeFromShoppingList(@Valid @RequestBody FoodModel request, @RequestHeader("Authorization") String token){ + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + String authToken = token.substring(7); + shoppingListService.removeFromShoppingList(request, authToken); + return ResponseEntity.ok().build(); } @PostMapping("/updateShoppingList") - public void updateShoppingList(@Valid @RequestBody ShoppingListRequestModel request){ - shoppingListService.updateShoppingList(request); + public ResponseEntity updateShoppingList(@Valid @RequestBody FoodModel request, @RequestHeader("Authorization") String token){ + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + String authToken = token.substring(7); + shoppingListService.updateShoppingList(request, authToken); + return ResponseEntity.ok().build(); } @PostMapping("/getShoppingList") - public List getShoppingList(@RequestBody UserModel user){ - return shoppingListService.getShoppingList(user); + public ResponseEntity> getShoppingList(@RequestHeader("Authorization") String token){ + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + String authToken = token.substring(7); + return ResponseEntity.ok(shoppingListService.getShoppingList(authToken)); + } + + @PostMapping("/buyItem") + public ResponseEntity> buyItem(@Valid @RequestBody FoodModel request, @RequestHeader("Authorization") String token){ + //Will move item from shopping list to pantry and return updated pantry + + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + String authToken = token.substring(7); + return ResponseEntity.ok(shoppingListService.buyItem(request, authToken)); } } diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java index e53bbc56..6cf64447 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java @@ -1,36 +1,70 @@ package fellowship.mealmaestro.controllers; +import java.util.Optional; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import fellowship.mealmaestro.models.UserModel; +import fellowship.mealmaestro.models.auth.AuthenticationRequestModel; +import fellowship.mealmaestro.models.auth.AuthenticationResponseModel; +import fellowship.mealmaestro.models.auth.RegisterRequestModel; import fellowship.mealmaestro.services.UserService; +import fellowship.mealmaestro.services.auth.AuthenticationService; @RestController public class UserController { @Autowired private UserService userService; - - @PostMapping("/createUser") - public void createUser(@RequestBody UserModel user){ - userService.createUser(user); + + private final AuthenticationService authenticationService; + + public UserController(AuthenticationService authenticationService){ + this.authenticationService = authenticationService; + } + + @PostMapping("/findByEmail") + public UserModel findByEmail(@RequestBody UserModel user){ + return userService.findByEmail(user.getEmail()).orElseThrow(() -> new RuntimeException("User not found")); + } + + @PostMapping("/register") + public ResponseEntity register( + @RequestBody RegisterRequestModel request + ){ + Optional response = authenticationService.register(request); + if(response.isEmpty()){ + return ResponseEntity.badRequest().build(); + } + return ResponseEntity.ok(response.get()); } - @PostMapping("/checkUser") - public boolean checkUser(@RequestBody UserModel user){ - return userService.checkUser(user); + @PostMapping("/authenticate") + public ResponseEntity authenticate( + @RequestBody AuthenticationRequestModel request + ){ + return ResponseEntity.ok(authenticationService.authenticate(request)); } - @PostMapping("/login") - public boolean login(@RequestBody UserModel user){ - return userService.login(user); + @PutMapping("/updateUser") + public ResponseEntity updateUser( + @RequestBody UserModel user, + @RequestHeader("Authorization") String token + ){ + return ResponseEntity.ok(userService.updateUser(user, token)); } - @PostMapping("/getUser") - public UserModel getUser(@RequestBody UserModel user){ - return userService.getUser(user); + @GetMapping("/getUser") + public ResponseEntity getUser( + @RequestHeader("Authorization") String token + ){ + return ResponseEntity.ok(userService.getUser(token)); } } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/DaysMealsModel.java b/backend/src/main/java/fellowship/mealmaestro/models/DaysMealsModel.java new file mode 100644 index 00000000..1cba70ab --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/DaysMealsModel.java @@ -0,0 +1,88 @@ +package fellowship.mealmaestro.models; + +import java.time.DayOfWeek; +import org.springframework.data.annotation.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + +import com.fasterxml.jackson.annotation.JsonFormat; + +@Node("DaysMeals") +public class DaysMealsModel { + + @Relationship(type = "breakfast") + private MealModel breakfast; + + @Relationship(type = "lunch") + private MealModel lunch; + + @Relationship(type = "dinner") + private MealModel dinner; + + @Id + private String userDateIdentifier; + + @JsonFormat(pattern = "yyyy-MM-dd") + private DayOfWeek mealDate; + + @Relationship(type = "HAS_DAY", direction = Relationship.Direction.INCOMING) + private UserModel user; + + private String meal; + + public DaysMealsModel() { + }; + + public DaysMealsModel(MealModel breakfast, MealModel lunch, MealModel dinner, DayOfWeek mealDate, UserModel user) { + this.breakfast = breakfast; + this.lunch = lunch; + this.dinner = dinner; + this.mealDate = mealDate; + this.user = user; + this.userDateIdentifier = (user.getEmail() + mealDate.toString()); + } + + public MealModel getBreakfast() { + return this.breakfast; + } + + public void setBreakfast(MealModel breakfast) { + this.breakfast = breakfast; + } + + public MealModel getLunch() { + return this.lunch; + } + + public void setLunch(MealModel lunch) { + this.lunch = lunch; + } + + public MealModel getDinner() { + return this.dinner; + } + + public void setDinner(MealModel dinner) { + this.dinner = dinner; + } + + public void setMealDate(DayOfWeek mealDate) { + this.mealDate = mealDate; + } + + public DayOfWeek getMealDate() { + return this.mealDate; + } + + public void setMeal(String meal) { + this.meal = meal; + } + + public String getMeal() { + return this.meal; + } + + public void setUserDateIdentifier(String asText) { + this.userDateIdentifier = asText; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/FoodModel.java b/backend/src/main/java/fellowship/mealmaestro/models/FoodModel.java index 32c9fd6a..dddc225b 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/FoodModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/FoodModel.java @@ -7,29 +7,28 @@ public class FoodModel { @NotBlank(message = "A Food Name is required") private String name; - // private String category; - @PositiveOrZero(message = "Quantity must be a positive number") private int quantity; @PositiveOrZero(message = "Weight must be a positive number") private int weight; + public FoodModel(){ + this.name = ""; + this.quantity = 0; + this.weight = 0; + } + public FoodModel(String name, int quantity, int weight){ this.name = name; this.quantity = quantity; this.weight = weight; } - public String getName(){ return this.name; } - // public String getCategory(){ - // return this.category; - // } - public int getQuantity(){ return this.quantity; } @@ -42,10 +41,6 @@ public void setName(String name){ this.name = name; } - // public void setCategory(String category){ - // this.category = category; - // } - public void setQuantity(int quantity){ this.quantity = quantity; } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/MealModel.java b/backend/src/main/java/fellowship/mealmaestro/models/MealModel.java index b71a7cb6..cc8580b5 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/MealModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/MealModel.java @@ -1,32 +1,96 @@ package fellowship.mealmaestro.models; +import org.springframework.data.annotation.Id; +import org.springframework.data.neo4j.core.schema.Node; + import jakarta.validation.constraints.NotBlank; +@Node("Meal") public class MealModel { + + @Id @NotBlank(message = "A Meal Name is required") private String name; - @NotBlank(message = "A Recipe is required") - private String recipe; + @NotBlank(message = "An image is required") + private String image; + + @NotBlank(message = "A Description is required") + private String description; + + @NotBlank(message = "Ingredients are required") + private String ingredients; - public MealModel(String name, String recipe){ + @NotBlank(message = "Instructions are required") + private String instructions; + + @NotBlank(message = "Cooking time is required") + private String cookingTime; + public MealModel(){}; + + public MealModel(String name, String instructions,String description, String image, String ingredients, String cookingTime){ this.name = name; - this.recipe = recipe; + this.instructions = instructions; + this.description = description; + this.image = image; + this.ingredients = ingredients; + this.cookingTime = cookingTime; } public String getName(){ return this.name; } - public String getRecipe(){ - return this.recipe; - } - public void setName(String name){ this.name = name; } - public void setRecipe(String recipe){ - this.recipe = recipe; + public String getinstructions(){ + return this.instructions; + } + + public void setinstructions(String instructions){ + this.instructions = instructions; + } + + public String getdescription(){ + return this.description; + } + + public void setdescription(String description){ + this.description = description; + } + + public String getimage(){ + return this.image; + } + + public void setimage(String image){ + this.image = image; + } + + public String getingredients(){ + return this.ingredients; + } + + public void setingredients(String ingredients){ + this.ingredients = ingredients; + } + + public String getcookingTime(){ + return this.cookingTime; + } + + public void setcookingTime(String cookingTime){ + this.cookingTime = cookingTime; + } + + public void copyFromOtherModel(MealModel mealModel){ + this.name = mealModel.getName(); + this.cookingTime = mealModel.getcookingTime(); + this.ingredients = mealModel.getingredients(); + this.instructions = mealModel.getinstructions(); + this.description = mealModel.getdescription(); + this.image = mealModel.getimage(); } } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/PantryRequestModel.java b/backend/src/main/java/fellowship/mealmaestro/models/PantryRequestModel.java deleted file mode 100644 index 751d4c53..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/models/PantryRequestModel.java +++ /dev/null @@ -1,32 +0,0 @@ -package fellowship.mealmaestro.models; - -import jakarta.validation.constraints.NotNull; - -public class PantryRequestModel { - @NotNull(message = "A User is required") - private UserModel user; - - @NotNull(message = "A Food is required") - private FoodModel food; - - public PantryRequestModel(UserModel user, FoodModel food){ - this.user = user; - this.food = food; - } - - public UserModel getUser(){ - return this.user; - } - - public FoodModel getFood(){ - return this.food; - } - - public void setUser(UserModel user){ - this.user = user; - } - - public void setFood(FoodModel food){ - this.food = food; - } -} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/PopularMealsModel.java b/backend/src/main/java/fellowship/mealmaestro/models/PopularMealsModel.java new file mode 100644 index 00000000..24b1fce4 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/PopularMealsModel.java @@ -0,0 +1,29 @@ +package fellowship.mealmaestro.models; + +import org.springframework.data.annotation.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + +import java.util.List; + +@Node("PopularMeals") +public class PopularMealsModel { + @Relationship(type = "HAS_MEAL") + private List popularMealList; + + @Id + private String idString = "PopularMeals"; + + public PopularMealsModel(){}; + + public PopularMealsModel(List mealModels){ + this.popularMealList = mealModels; + }; + + public void setPopularMeals(List mealModels){ + this.popularMealList = mealModels; + } + public List getPopularMeals(){ + return this.popularMealList; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/RecipeModel.java b/backend/src/main/java/fellowship/mealmaestro/models/RecipeModel.java new file mode 100644 index 00000000..28fefd1c --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/RecipeModel.java @@ -0,0 +1,32 @@ +package fellowship.mealmaestro.models; + +import jakarta.validation.constraints.NotBlank; + +public class RecipeModel { + @NotBlank(message = "A title is required") + private String title; + + @NotBlank(message = "An image is required") + private String image; + + public RecipeModel(String title, String image) { + this.title = title; + this.image = image; + } + + public String getTitle() { + return this.title; + } + + public String getImage() { + return this.image; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setImage(String image) { + this.image = image; + } +} \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/models/SavedMealsModel.java b/backend/src/main/java/fellowship/mealmaestro/models/SavedMealsModel.java new file mode 100644 index 00000000..c67ae11b --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/SavedMealsModel.java @@ -0,0 +1,49 @@ +package fellowship.mealmaestro.models; + +import org.springframework.data.annotation.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + +import java.util.List; + +@Node("SavedMeals") +public class SavedMealsModel { + @Relationship(type = "SAVED_MEAL") + private List savedMealList; + + @Relationship(type = "HAS_DAY", direction = Relationship.Direction.INCOMING) + private UserModel user; + + @Id + private String userSavedIdentifier; + + public SavedMealsModel(){}; + + public SavedMealsModel(List savedMealList, UserModel user, String userSavedIdentifier){ + this.savedMealList = savedMealList; + this.user = user; + this.userSavedIdentifier = userSavedIdentifier; + }; + + public List getMealModels(){ + return this.savedMealList; + } + public void setMealModels(List mealModels){ + this.savedMealList = mealModels; + } + + public UserModel getUserModel(){ + return this.user; + } + public void setUser(UserModel user){ + this.user = user; + } + + public String getUserSavedIdentifier(){ + return this.userSavedIdentifier; + } + public void setUserDateIdentifier(String id){ + this.userSavedIdentifier = id; + } + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/SettingsModel.java b/backend/src/main/java/fellowship/mealmaestro/models/SettingsModel.java new file mode 100644 index 00000000..84e60d0f --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/SettingsModel.java @@ -0,0 +1,225 @@ +package fellowship.mealmaestro.models; + +import java.util.List; + + +import java.util.Map; + +public class SettingsModel{ + + private String goal; + private String shoppingInterval; + private List foodPreferences; + private int calorieAmount; + private String budgetRange; + private Map macroRatio; + + private List allergies; + private String cookingTime; + private int userHeight; // consider moving to account + private int userWeight; // consider moving to account + private int userBMI; + + private boolean BMISet = false; + private boolean cookingTimeSet = false; + private boolean allergiesSet = false; + private boolean macroSet = false; + private boolean budgetSet = false; + private boolean calorieSet = false; + private boolean foodPreferenceSet = false; + private boolean shoppingIntervalSet = false; + + public SettingsModel() { + // Empty constructor with all booleans set to false by default + } + + public SettingsModel(String goal, String shoppingInterval, List foodPreferences, int calorieAmount, + String budgetRange, Map macroRatio, List allergies, String cookingTime, + int userHeight, int userWeight, int userBMI, boolean BMISet, boolean cookingTimeSet, + boolean allergiesSet, boolean macroSet, boolean budgetSet, boolean calorieSet, + boolean foodPreferenceSet, boolean shoppingIntervalSet) { + this.goal = goal; + this.shoppingInterval = shoppingInterval; + this.foodPreferences = foodPreferences; + this.calorieAmount = calorieAmount; + this.budgetRange = budgetRange; + this.macroRatio = macroRatio; + + this.allergies = allergies; + this.cookingTime = cookingTime; + this.userHeight = userHeight; + this.userWeight = userWeight; + this.userBMI = userBMI; + this.BMISet = BMISet; + this.cookingTimeSet = cookingTimeSet; + this.allergiesSet = allergiesSet; + this.macroSet = macroSet; + this.budgetSet = budgetSet; + this.calorieSet = calorieSet; + this.foodPreferenceSet = foodPreferenceSet; + this.shoppingIntervalSet = shoppingIntervalSet; + } + + public String getGoal() { + return goal; + } + + public void setGoal(String goal) { + this.goal = goal; + } + + public String getShoppingInterval() { + return shoppingInterval; + } + + public void setShoppingInterval(String shoppingInterval) { + this.shoppingInterval = shoppingInterval; + } + + public List getFoodPreferences() { + return foodPreferences; + } + + public void setFoodPreferences(List foodPreferences) { + this.foodPreferences = foodPreferences; + } + + public int getCalorieAmount() { + + return calorieAmount; + } + + public void setCalorieAmount(int calorieAmount) { + this.calorieAmount = calorieAmount; + } + + public String getBudgetRange() { + return budgetRange; + } + + public void setBudgetRange(String budgetRange) { + this.budgetRange = budgetRange; + } + + public Map getMacroRatio() { + return macroRatio; + } + + public void setMacroRatio(Map macroRatio) { + this.macroRatio = macroRatio; + } + + public List getAllergies() { + return allergies; + } + + public void setAllergies(List allergies) { + this.allergies = allergies; + } + + public String getCookingTime() { + return cookingTime; + } + + public void setCookingTime(String cookingTime) { + this.cookingTime = cookingTime; + } + + public int getUserHeight() { + return userHeight; + } + + public void setUserHeight(int userHeight) { + this.userHeight = userHeight; + } + + public int getUserWeight() { + return userWeight; + } + + public void setUserWeight(int userWeight) { + this.userWeight = userWeight; + } + + public int getUserBMI() { + return userBMI; + } + + public void setUserBMI(int userHeight, int userWeight) { + if (userWeight == 0) { + this.userBMI = 0; + } else { + this.userBMI = userHeight/userWeight; + } + } + + public void setUserBMI(int userBMI) { + this.userBMI = userBMI; + } + + public boolean isBMISet() { + return BMISet; + } + + public void setBMISet(boolean BMISet) { + this.BMISet = BMISet; + } + + public boolean isCookingTimeSet() { + return cookingTimeSet; + } + + public void setCookingTimeSet(boolean cookingTimeSet) { + this.cookingTimeSet = cookingTimeSet; + } + + public boolean isAllergiesSet() { + return allergiesSet; + } + + public void setAllergiesSet(boolean allergiesSet) { + this.allergiesSet = allergiesSet; + } + + public boolean isMacroSet() { + return macroSet; + } + + public void setMacroSet(boolean macroSet) { + this.macroSet = macroSet; + } + + public boolean isBudgetSet() { + return budgetSet; + } + + public void setBudgetSet(boolean budgetSet) { + this.budgetSet = budgetSet; + } + + public boolean isCalorieSet() { + return calorieSet; + } + + public void setCalorieSet(boolean calorieSet) { + this.calorieSet = calorieSet; + } + + public boolean isFoodPreferenceSet() { + return foodPreferenceSet; + } + + public void setFoodPreferenceSet(boolean foodPreferenceSet) { + this.foodPreferenceSet = foodPreferenceSet; + } + + public boolean isShoppingIntervalSet() { + return shoppingIntervalSet; + } + + public void setShoppingIntervalSet(boolean shoppingIntervalSet) { + this.shoppingIntervalSet = shoppingIntervalSet; + } + + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/ShoppingListRequestModel.java b/backend/src/main/java/fellowship/mealmaestro/models/ShoppingListRequestModel.java deleted file mode 100644 index 7cb0fb21..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/models/ShoppingListRequestModel.java +++ /dev/null @@ -1,33 +0,0 @@ -package fellowship.mealmaestro.models; - -import jakarta.validation.constraints.NotNull; - -public class ShoppingListRequestModel { - - @NotNull(message = "User cannot be null") - private UserModel user; - - @NotNull(message = "Food cannot be null") - private FoodModel food; - - public ShoppingListRequestModel(UserModel user, FoodModel food){ - this.user = user; - this.food = food; - } - - public UserModel getUser(){ - return user; - } - - public void setUser(UserModel user){ - this.user = user; - } - - public FoodModel getFood(){ - return food; - } - - public void setFood(FoodModel food){ - this.food = food; - } -} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/UserModel.java b/backend/src/main/java/fellowship/mealmaestro/models/UserModel.java index f3459b72..af01adb1 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/UserModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/UserModel.java @@ -1,41 +1,55 @@ package fellowship.mealmaestro.models; +import java.util.Collection; +import java.util.List; + +import org.springframework.data.annotation.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import fellowship.mealmaestro.models.auth.AuthorityRoleModel; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; -public class UserModel { +@Node("User") +public class UserModel implements UserDetails{ @NotBlank(message = "A Username is required") - private String username; + private String name; @NotBlank @Size(min = 8, max = 20, message = "Password must be between 8 and 20 characters") private String password; - + @Id @NotBlank @Email(message = "Email must be valid") private String email; - public UserModel(String username, String password, String email){ - this.username = username; - this.password = password; - this.email = email; + private AuthorityRoleModel authorityRole; + + public UserModel(){ + this.authorityRole = AuthorityRoleModel.USER; } - public String getUsername(){ - return this.username; + public UserModel(String name, String password, String email, AuthorityRoleModel authorityRole){ + this.name = name; + this.password = password; + this.email = email; + this.authorityRole = AuthorityRoleModel.USER; } - public String getPassword(){ - return this.password; + public String getName(){ + return this.name; } public String getEmail(){ return this.email; } - public void setUsername(String username){ - this.username = username; + public void setName(String name){ + this.name = name; } public void setPassword(String password){ @@ -45,4 +59,47 @@ public void setPassword(String password){ public void setEmail(String email){ this.email = email; } + + public AuthorityRoleModel getAuthorityRole(){ + return this.authorityRole; + } + + public void setAuthorityRole(AuthorityRoleModel authorityRole){ + this.authorityRole = authorityRole; + } + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority(authorityRole.name())); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public String getUsername(){ + return email; + } + + @Override + public String getPassword(){ + return password; + } } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthenticationRequestModel.java b/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthenticationRequestModel.java new file mode 100644 index 00000000..afa03000 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthenticationRequestModel.java @@ -0,0 +1,30 @@ +package fellowship.mealmaestro.models.auth; + +public class AuthenticationRequestModel { + private String email; + private String password; + + public AuthenticationRequestModel(){ + } + + public AuthenticationRequestModel(String email, String password){ + this.email = email; + this.password = password; + } + + public String getEmail(){ + return email; + } + + public void setEmail(String email){ + this.email = email; + } + + public void setPassword(String password){ + this.password = password; + } + + public String getPassword(){ + return password; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthenticationResponseModel.java b/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthenticationResponseModel.java new file mode 100644 index 00000000..ab979473 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthenticationResponseModel.java @@ -0,0 +1,40 @@ +package fellowship.mealmaestro.models.auth; + + +public class AuthenticationResponseModel { + + private String token; + + public AuthenticationResponseModel(){ + } + + public AuthenticationResponseModel(String token){ + this.token = token; + } + + public String getToken(){ + return token; + } + + public void setToken(String token){ + this.token = token; + } + + @Override + public String toString(){ + return "AuthenticationResponseModel [token=" + token + "]"; + } + + @Override + public boolean equals(Object o){ + if (o == this) return true; + if (o == null || getClass() != o.getClass()) return false; + AuthenticationResponseModel other = (AuthenticationResponseModel) o; + return token != null ? token.equals(other.token) : other.token == null; + } + + @Override + public int hashCode(){ + return token != null ? token.hashCode() : 0; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthorityRoleModel.java b/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthorityRoleModel.java new file mode 100644 index 00000000..b4d7b33c --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/auth/AuthorityRoleModel.java @@ -0,0 +1,6 @@ +package fellowship.mealmaestro.models.auth; + +public enum AuthorityRoleModel { + ADMIN, + USER +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/auth/RegisterRequestModel.java b/backend/src/main/java/fellowship/mealmaestro/models/auth/RegisterRequestModel.java new file mode 100644 index 00000000..b6c80044 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/auth/RegisterRequestModel.java @@ -0,0 +1,41 @@ +package fellowship.mealmaestro.models.auth; + +public class RegisterRequestModel { + + private String username; + private String email; + private String password; + + public RegisterRequestModel(){ + } + + public RegisterRequestModel(String username, String email, String password){ + this.username = username; + this.email = email; + this.password = password; + } + + public String getUsername(){ + return username; + } + + public void setUserame(String username){ + this.username = username; + } + + public String getEmail(){ + return email; + } + + public void setEmail(String email){ + this.email = email; + } + + public void setPassword(String password){ + this.password = password; + } + + public String getPassword(){ + return password; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/BrowseRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/BrowseRepository.java new file mode 100644 index 00000000..6b3e3700 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/BrowseRepository.java @@ -0,0 +1,131 @@ +package fellowship.mealmaestro.repositories; +import java.util.ArrayList; +import java.util.List; + +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.Values; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.TransactionCallback; +import org.neo4j.driver.TransactionWork; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +//import fellowship.mealmaestro.models.FoodModel; +import fellowship.mealmaestro.models.MealModel; + + +@Repository +public class BrowseRepository { + + @Autowired + private final Driver driver; + + public BrowseRepository(Driver driver){ + this.driver = driver; + } + + + public List getPopularMeals(String email) { + try (Session session = driver.session()) { + //return session.readTransaction(tx -> getRandomMealsTransaction(tx, numberOfMeals)); + return session.executeRead(getPopularMealsTransaction(email)); + } + } + + public TransactionCallback> getPopularMealsTransaction(String email) { + return transaction -> { + + List randomMeals = new ArrayList<>(); + // org.neo4j.driver.Result result = transaction.run("MATCH (User{email: $email})-[:HAS_BROWSE]->(p:Browse)-[:IN_BROWSE]->(m:Meal)\n" + + org.neo4j.driver.Result result = transaction.run("MATCH (m:Meal)\n" + + "WITH m, rand() as random\n" + + "ORDER BY random\n" + + "RETURN m.name AS name, m.instructions AS instructions, m.description AS description, " + + "m.image AS image, m.ingredients AS ingredients, m.cookingTime AS cookingTime", + Values.parameters("email", email)); + + while (result.hasNext()) { + org.neo4j.driver.Record record = result.next(); + String name = record.get("name").asString(); + String instructions = record.get("instructions").asString(); + String description = record.get("description").asString(); + String image = record.get("image").asString(); + String ingredients = record.get("ingredients").asString(); + String cookingTime = record.get("cookingTime").asString(); + randomMeals.add(new MealModel(name, instructions, description, image, ingredients, cookingTime)); + } + + return randomMeals; + }; + } + + + + // public List getPopularMeals() { + // try (Session session = driver.session()) { + // return session.readTransaction(this::getPopularMealsTransaction); + // } + // } + + // public List getPopularMealsTransaction(Transaction tx) { + // List popularMeals = new ArrayList<>(); + + // org.neo4j.driver.Result result = tx.run("MATCH (m:Meal)<--(u:User)\n" + + // "WITH m, count(u) as popularity\n" + + // "ORDER BY popularity DESC\n" + + // "LIMIT 10\n" + + // "RETURN m.name AS name, m.recipe AS recipe"); + + // while (result.hasNext()) { + // org.neo4j.driver.Record record = result.next(); + // String name = record.get("name").asString(); + // String recipe = record.get("recipe").asString(); + // popularMeals.add(new MealModel(name, recipe)); + // } + + // return getRandomMeals(5); + // } + + + public List getSearchedMeals(String mealName, String email) { + try (Session session = driver.session()) { + return session.executeRead(getSearchedMealsTransaction(mealName, email)); + // return session.readTransaction(tx -> searchMealByNameTransaction(tx, mealName)); + } + } + + public TransactionCallback> getSearchedMealsTransaction(String mealName, String email) { + return transaction -> { + + List matchingPopularMeals = new ArrayList<>(); + // org.neo4j.driver.Result result = transaction.run("MATCH (m:Meal {name: $name})\n" + + // "RETURN m.name AS name, m.instructions AS instructions, m.description AS description, " + + // "m.image AS image, m.ingredients AS ingredients, m.cookingTime AS cookingTime", + // Values.parameters("email", email)); + org.neo4j.driver.Result result = transaction.run( + "MATCH (m:Meal)\n" + + "WHERE m.name =~ $namePattern OR m.ingredients =~ $namePattern OR m.description =~ $namePattern\n" + // Use regular expression matching + "RETURN m.name AS name, m.instructions AS instructions, m.description AS description, " + + "m.image AS image, m.ingredients AS ingredients, m.cookingTime AS cookingTime", + Values.parameters("namePattern", "(?i).*" + mealName + ".*") // (?i) for case-insensitive + ); + + if (result.hasNext()) { + org.neo4j.driver.Record record = result.next(); + String name = record.get("name").asString(); + String instructions = record.get("instructions").asString(); + String description = record.get("description").asString(); + String image = record.get("image").asString(); + String ingredients = record.get("ingredients").asString(); + String cookingTime = record.get("cookingTime").asString(); + // return new MealModel(name, instructions, description, image, ingredients, cookingTime); + matchingPopularMeals.add(new MealModel(name, instructions, description, image, ingredients, cookingTime)); + } + + return matchingPopularMeals; + }; + } + + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/DaysMealsRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/DaysMealsRepository.java new file mode 100644 index 00000000..f73c1503 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/DaysMealsRepository.java @@ -0,0 +1,32 @@ +package fellowship.mealmaestro.repositories; + +import java.time.DayOfWeek; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.query.Query; + +import com.fasterxml.jackson.databind.JsonNode; + +import fellowship.mealmaestro.models.DaysMealsModel; + +public interface DaysMealsRepository extends Neo4jRepository { + @Query("MATCH (d:DaysMeals) WHERE d.mealDate >= $startDate AND d.mealDate <= datetime($startDate) + duration('P4D') RETURN d") + List findMealsForNextWeek(DayOfWeek startDate); + + @Query("MATCH (d:DaysMeals {mealDate: $mealDate}) RETURN d LIMIT 1") + Optional findByMealDate(DayOfWeek mealDate); + + @Query("MATCH (d:DaysMeals {mealDate: $mealDate}) RETURN d") + Optional findMealsForDate(DayOfWeek mealDate); + + @Query("MATCH (user:User {email: $email})-[:HAS_DAY]->(daysMeals:DaysMeals {userDateIdentifier: $userDateIdentifier}), " + + + "(daysMeals)-[:breakfast]->(breakfast:Meal), " + + "(daysMeals)-[:lunch]->(lunch:Meal), " + + "(daysMeals)-[:dinner]->(dinner:Meal) " + + "RETURN daysMeals, breakfast, lunch, dinner") + + Optional findDaysMealsWithMealsForUserAndDate(String email, String userDateIdentifier); +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/MealManagementRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/MealManagementRepository.java new file mode 100644 index 00000000..eddd17f1 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/MealManagementRepository.java @@ -0,0 +1,5 @@ +package fellowship.mealmaestro.repositories; + +public class MealManagementRepository { + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/MealRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/MealRepository.java new file mode 100644 index 00000000..0c922b79 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/MealRepository.java @@ -0,0 +1,9 @@ +package fellowship.mealmaestro.repositories; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +import fellowship.mealmaestro.models.MealModel; + +public interface MealRepository extends Neo4jRepository{ + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/PantryRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/PantryRepository.java index 51f0dc0c..fa0b7989 100644 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/PantryRepository.java +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/PantryRepository.java @@ -11,8 +11,6 @@ import org.springframework.stereotype.Repository; import fellowship.mealmaestro.models.FoodModel; -import fellowship.mealmaestro.models.PantryRequestModel; -import fellowship.mealmaestro.models.UserModel; @Repository public class PantryRepository { @@ -25,20 +23,18 @@ public PantryRepository(Driver driver){ } //#region Create - public FoodModel addToPantry(PantryRequestModel pantryRequest){ - FoodModel food = pantryRequest.getFood(); - UserModel user = pantryRequest.getUser(); + public FoodModel addToPantry(FoodModel food, String email){ try (Session session = driver.session()){ - return session.executeWrite(addToPantryTransaction(food, user.getUsername(), user.getEmail())); + return session.executeWrite(addToPantryTransaction(food, email)); } } - public static TransactionCallback addToPantryTransaction(FoodModel food, String username, String email){ + public static TransactionCallback addToPantryTransaction(FoodModel food, String email){ return transaction -> { - transaction.run("MATCH (User{username: $username, email: $email})-[:HAS_PANTRY]->(p:Pantry) \r\n" + // + transaction.run("MATCH (User{email: $email})-[:HAS_PANTRY]->(p:Pantry) \r\n" + // "CREATE (p)-[:IN_PANTRY]->(:Food {name: $name, quantity: $quantity, weight: $weight})", - Values.parameters("username", username, "email", email, "name", food.getName(), + Values.parameters("email", email, "name", food.getName(), "quantity", food.getQuantity(), "weight", food.getWeight())); FoodModel addedFood = new FoodModel(food.getName(), food.getQuantity(), food.getWeight()); return addedFood; @@ -51,26 +47,22 @@ public static TransactionCallback addToPantryTransaction(FoodModel fo * "quantity": "17", * "weight": "42" * }, - * "user": { - * "username": "Frank", - * "email": "test@example.com" - * } * } */ //#endregion //#region Read - public List getPantry(UserModel user){ + public List getPantry(String email){ try (Session session = driver.session()){ - return session.executeRead(getPantryTransaction(user.getUsername(), user.getEmail())); + return session.executeRead(getPantryTransaction(email)); } } - public static TransactionCallback> getPantryTransaction(String username, String email){ + public static TransactionCallback> getPantryTransaction(String email){ return transaction -> { - var result = transaction.run("MATCH (User{username: $username, email: $email})-[:HAS_PANTRY]->(p:Pantry)-[:IN_PANTRY]->(f:Food) \r\n" + // + var result = transaction.run("MATCH (User{email: $email})-[:HAS_PANTRY]->(p:Pantry)-[:IN_PANTRY]->(f:Food) \r\n" + // "RETURN f.name AS name, f.quantity AS quantity, f.weight AS weight", - Values.parameters("username", username, "email", email)); + Values.parameters("email", email)); List foods = new ArrayList<>(); while (result.hasNext()){ @@ -82,26 +74,22 @@ var record = result.next(); } /* Example Post data: * { - * "username": "Frank", - * "email": "test@example.com" * } */ //#endregion //#region Update - public void updatePantry(PantryRequestModel pantryRequest){ - FoodModel food = pantryRequest.getFood(); - UserModel user = pantryRequest.getUser(); + public void updatePantry(FoodModel food, String email){ try (Session session = driver.session()){ - session.executeWrite(updatePantryTransaction(food, user.getUsername(), user.getEmail())); + session.executeWrite(updatePantryTransaction(food, email)); } } - public static TransactionCallback updatePantryTransaction(FoodModel food, String username, String email){ + public static TransactionCallback updatePantryTransaction(FoodModel food, String email){ return transaction -> { - transaction.run("MATCH (User{username: $username, email: $email})-[:HAS_PANTRY]->(p:Pantry)-[:IN_PANTRY]->(f:Food {name: $name}) \r\n" + // + transaction.run("MATCH (User{email: $email})-[:HAS_PANTRY]->(p:Pantry)-[:IN_PANTRY]->(f:Food {name: $name}) \r\n" + // "SET f.quantity = $quantity, f.weight = $weight", - Values.parameters("username", username, "email", email, "name", food.getName(), + Values.parameters("email", email, "name", food.getName(), "quantity", food.getQuantity(), "weight", food.getWeight())); return null; }; @@ -109,19 +97,17 @@ public static TransactionCallback updatePantryTransaction(FoodModel food, //#endregion //#region Delete - public void removeFromPantry(PantryRequestModel pantryRequest){ - FoodModel food = pantryRequest.getFood(); - UserModel user = pantryRequest.getUser(); + public void removeFromPantry(FoodModel food, String email){ try (Session session = driver.session()){ - session.executeWrite(removeFromPantryTransaction(food, user.getUsername(), user.getEmail())); + session.executeWrite(removeFromPantryTransaction(food, email)); } } - public static TransactionCallback removeFromPantryTransaction(FoodModel food, String username, String email){ + public static TransactionCallback removeFromPantryTransaction(FoodModel food, String email){ return transaction -> { - transaction.run("MATCH (User{username: $username, email: $email})-[:HAS_PANTRY]->(p:Pantry)-[r:IN_PANTRY]->(f:Food {name: $name}) \r\n" + // + transaction.run("MATCH (User{email: $email})-[:HAS_PANTRY]->(p:Pantry)-[r:IN_PANTRY]->(f:Food {name: $name}) \r\n" + // "DELETE r,f", - Values.parameters("username", username, "email", email, "name", food.getName())); + Values.parameters("email", email, "name", food.getName())); return null; }; } diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/PopularMealsRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/PopularMealsRepository.java new file mode 100644 index 00000000..e8d733ed --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/PopularMealsRepository.java @@ -0,0 +1,9 @@ +package fellowship.mealmaestro.repositories; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +import fellowship.mealmaestro.models.PopularMealsModel; + +public interface PopularMealsRepository extends Neo4jRepository{ + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/RecipeBookRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/RecipeBookRepository.java new file mode 100644 index 00000000..5988b27d --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/RecipeBookRepository.java @@ -0,0 +1,86 @@ +package fellowship.mealmaestro.repositories; + +import org.springframework.stereotype.Repository; + +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.TransactionCallback; +import org.springframework.beans.factory.annotation.Autowired; +import org.neo4j.driver.Values; + +import java.util.List; +import java.util.ArrayList; + +import fellowship.mealmaestro.models.MealModel; + +@Repository +public class RecipeBookRepository { + + @Autowired + private final Driver driver; + + public RecipeBookRepository(Driver driver){ + this.driver = driver; + } + + //#region Create + public MealModel addRecipe(MealModel recipe, String email){ + try (Session session = driver.session()){ + return session.executeWrite(addRecipeTransaction(recipe, email)); + } + } + + public static TransactionCallback addRecipeTransaction(MealModel recipe, String email) { + return transaction -> { + transaction.run("MATCH (user:User {email: $email}), (recipe:Meal {name: $name, description: $desc, image: $image, ingredients: $ing, " + + "instructions: $ins, cookingTime: $ck})" + + "MERGE (user)-[:HAS_RECIPE_BOOK]->(recipeBook:`Recipe Book`) " + + "MERGE (recipeBook)-[:CONTAINS]->(recipe)", + Values.parameters("email", email, "name", recipe.getName(), "desc", recipe.getdescription(), "image", recipe.getimage(), + "ing", recipe.getingredients(), "ins", recipe.getinstructions(), "ck", recipe.getcookingTime())); + return (new MealModel(recipe.getName(), recipe.getinstructions(), recipe.getdescription(), recipe.getimage(), recipe.getingredients(), recipe.getcookingTime())); + }; + } + //#endregion + + //#region Read + public List getAllRecipes(String user){ + try (Session session = driver.session()){ + return session.executeRead(getAllRecipesTransaction(user)); + } + } + + public static TransactionCallback> getAllRecipesTransaction(String user) { + return transaction -> { + var result = transaction.run("MATCH (user:User {email: $email})-[:HAS_RECIPE_BOOK]->(book:`Recipe Book`)-[:CONTAINS]->(recipe:Meal) " + + "RETURN recipe.name AS name, recipe.image AS image, recipe.description AS description, recipe.ingredients as ingredients, recipe.instructions as instructions, recipe.cookingTime as cookingTime", + Values.parameters("email", user)); + + List recipes = new ArrayList<>(); + while (result.hasNext()){ + var record = result.next(); + recipes.add(new MealModel(record.get("name").asString(), record.get("instructions").asString(), record.get("description").asString(), record.get("image").asString(), + record.get("ingredients").asString(), record.get("cookingTime").asString())); + } + return recipes; + }; + } + //#endregion + + //#region Delete + public void removeRecipe(MealModel recipe, String email){ + try (Session session = driver.session()){ + session.executeWrite(removeRecipeTransaction(recipe, email)); + } + } + + public static TransactionCallback removeRecipeTransaction(MealModel recipe, String email) { + return transaction -> { + transaction.run("MATCH (user:User {email: $email})-[:HAS_RECIPE_BOOK]->(book:`Recipe Book`)-[r:CONTAINS]->(recipe:Meal {name: $name}) " + + "DELETE r", + Values.parameters("email", email, "name", recipe.getName())); + return null; + }; + } + //#endregion +} \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/SettingsRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/SettingsRepository.java new file mode 100644 index 00000000..2a661cfa --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/SettingsRepository.java @@ -0,0 +1,212 @@ +package fellowship.mealmaestro.repositories; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.TransactionCallback; +import org.neo4j.driver.Values; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.neo4j.driver.Value; +import fellowship.mealmaestro.models.SettingsModel; +import org.neo4j.driver.Record; + +@Repository +public class SettingsRepository { + + @Autowired + private final Driver driver; + + public SettingsRepository(Driver driver) { + this.driver = driver; + } + + public SettingsModel getSettings(String email) { + try (Session session = driver.session()) { + System.out.println("getSettingscalled"); + return session.executeRead(getSettingsTransaction(email)); + } + } + + public static TransactionCallback getSettingsTransaction(String email) { + System.out.println("getSettingsTransaction"); + + return transaction -> { + var result = transaction.run("MATCH (u:User {email: $email})-[:HAS_PREFERENCES]->(p:Preferences) " + + "MATCH (p)-[:HAS_INTERVAL]->(i:Interval)" + + "MATCH (p)-[:HAS_GOAL]->(g:Goal) " + + "MATCH (p)-[:HAS_CALORIE_GOAL]->(c:`Calorie Goal`) " + + "MATCH (p)-[:HAS_EATING_STYLE]->(e:`Eating Style`) " + + "MATCH (p)-[:HAS_MACRO]->(m:Macro) " + + "MATCH (p)-[:HAS_BUDGET]->(b:Budget) " + + "MATCH (p)-[:HAS_COOKING_TIME]->(ct:`Cooking Time`) " + + "MATCH (p)-[:HAS_ALLERGIES]->(a:Allergies) " + + "MATCH (p)-[:HAS_BMI]->(bm:BMI) " + + "RETURN i.interval AS shoppingInterval, g.goal AS goal, c.caloriegoal AS calorieAmount, e.style AS foodPreferences, " + + "m.protein AS protein, m.carbs AS carbs, m.fat AS fat, " + + "b.budgetRange AS budgetRange, ct.value AS cookingTime, a.allergies AS allergies, " + + "bm.height AS userHeight, bm.weight AS userWeight, bm.BMI AS userBMI", + Values.parameters("email", email)); + if (result.hasNext()) { + var record = result.next(); + + List foodPreferences = null; + if (!record.get("foodPreferences").isNull()) { + foodPreferences = record.get("foodPreferences").asList(Value::asString); + } + + List allergies = null; + if (!record.get("allergies").isNull()) { + allergies = record.get("allergies").asList(Value::asString); + } + + Map macroRatio = new HashMap<>(); + Integer protein = record.get("protein").isNull() ? 0 : record.get("protein").asInt(); + Integer carbs = record.get("carbs").isNull() ? 0 : record.get("carbs").asInt(); + Integer fat = record.get("fat").isNull() ? 0 : record.get("fat").asInt(); + + macroRatio.put("protein", protein); + macroRatio.put("carbs", carbs); + macroRatio.put("fat", fat); + + + System.out.println("MacroRatio"); + System.out.println(macroRatio); + + + String goal = record.get("goal").isNull() ? null : record.get("goal").asString(); + String shoppingInterval = record.get("shoppingInterval").isNull() ? null : record.get("shoppingInterval").asString(); + Integer calorieAmount = record.get("calorieAmount").isNull() ? 0 : record.get("calorieAmount").asInt(); + String budgetRange = record.get("budgetRange").isNull() ? null : record.get("budgetRange").asString(); + String cookingTime = record.get("cookingTime").isNull() ? "normal" : record.get("cookingTime").asString(); + Integer userHeight = record.get("userHeight").isNull() ? 0 : record.get("userHeight").asInt(); + Integer userWeight = record.get("userWeight").isNull() ? 0 : record.get("userWeight").asInt(); + + Integer userBMI = record.get("userBMI").isNull() ? 0 : record.get("userBMI").asInt(); + + // Update the following lines to default to false if the value is null + Boolean BMISet = record.get("BMISet").isNull() ? false : record.get("BMISet").asBoolean(); + Boolean cookingTimeSet = record.get("cookingTimeSet").isNull() ? false : record.get("cookingTimeSet").asBoolean(); + Boolean allergiesSet = record.get("allergiesSet").isNull() ? false : record.get("allergiesSet").asBoolean(); + Boolean macroSet = record.get("macroSet").isNull() ? false : record.get("macroSet").asBoolean(); + Boolean budgetSet = record.get("budgetSet").isNull() ? false : record.get("budgetSet").asBoolean(); + Boolean calorieSet = record.get("calorieSet").isNull() ? false : record.get("calorieSet").asBoolean(); + Boolean foodPreferenceSet = record.get("foodPreferenceSet").isNull() ? false : record.get("foodPreferenceSet").asBoolean(); + Boolean shoppingIntervalSet = record.get("shoppingIntervalSet").isNull() ? false : record.get("shoppingIntervalSet").asBoolean(); + + SettingsModel settings = new SettingsModel( + goal, + shoppingInterval, + foodPreferences, + calorieAmount, + budgetRange, + macroRatio, + allergies, + cookingTime, + userHeight, + userWeight, + userBMI, + BMISet, + cookingTimeSet, + allergiesSet, + macroSet, + budgetSet, + calorieSet, + foodPreferenceSet, + shoppingIntervalSet + ); + + + return settings; + + } + return null; + }; + } + + private static Map getMacroRatioFromRecord(Record record) { + Map macroRatioValue = record.get("macroRatio").asMap(); + Map macroRatioMap = new HashMap<>(); + + for (Map.Entry entry : macroRatioValue.entrySet()) { + String key = entry.getKey(); + Integer value = ((Number) entry.getValue()).intValue(); + macroRatioMap.put(key, value); + } + System.out.println("MacroRatioMap"); + System.out.println(macroRatioMap); + + return macroRatioMap; + } + + + + + public void updateSettings(SettingsModel request, String email) { + System.out.println("UpdateSettings"); + try (Session session = driver.session()) { + session.executeWrite(updateSettingsTransaction(request, email)); + } + } + + public static TransactionCallback updateSettingsTransaction(SettingsModel request, String email) { + System.out.println("UpdateSettingsTransaction"); + return transaction -> { + Map parameters = new HashMap<>(); + parameters.put("email", email); + + // Settings to update + parameters.put("goal", request.getGoal()); + parameters.put("shoppingInterval", request.getShoppingInterval()); + parameters.put("foodPreferences", request.getFoodPreferences()); + parameters.put("calorieAmount", request.getCalorieAmount()); + parameters.put("budgetRange", request.getBudgetRange()); + + // Split the macroRatio into individual elements + Map macroRatio = request.getMacroRatio(); + parameters.put("protein", macroRatio.get("protein")); + parameters.put("carbs", macroRatio.get("carbs")); + parameters.put("fat", macroRatio.get("fat")); + + parameters.put("allergies", request.getAllergies()); + parameters.put("cookingTime", request.getCookingTime()); + parameters.put("userHeight", request.getUserHeight()); + parameters.put("userWeight", request.getUserWeight()); + parameters.put("userBMI", request.getUserBMI()); + System.out.println(parameters); + + String cypherQuery = "MATCH (u:User {email: $email})-[:HAS_PREFERENCES]->(p:Preferences) " + + "MATCH (p)-[:HAS_INTERVAL]->(i:Interval)" + + "MATCH (p)-[:HAS_GOAL]->(g:Goal) " + + "MATCH (p)-[:HAS_CALORIE_GOAL]->(c:`Calorie Goal`) " + + "MATCH (p)-[:HAS_EATING_STYLE]->(e:`Eating Style`) " + + "MATCH (p)-[:HAS_MACRO]->(m:Macro) " + + "MATCH (p)-[:HAS_BUDGET]->(b:Budget) " + + "MATCH (p)-[:HAS_BMI]->(bm:BMI) " + + "MATCH (p)-[:HAS_COOKING_TIME]->(ct:`Cooking Time`) " + + "MATCH (p)-[:HAS_ALLERGIES]->(a:Allergies) " + + "SET i.interval = $shoppingInterval, " + + "g.goal = $goal," + + "c.caloriegoal = $calorieAmount," + + "e.style = $foodPreferences," + + "m.protein = $protein," + + "m.carbs = $carbs," + + "m.fat = $fat," + + "b.budgetRange = $budgetRange," + + "bm.height = $userHeight," + + "bm.weight = $userWeight," + + "bm.BMI = $userBMI," + + "ct.value = $cookingTime," + + "a.allergies = $allergies"; + + transaction.run(cypherQuery, parameters); + return null; + }; + } + + +} \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/ShoppingListRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/ShoppingListRepository.java index 334e5a24..f71fe929 100644 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/ShoppingListRepository.java +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/ShoppingListRepository.java @@ -11,8 +11,6 @@ import org.springframework.stereotype.Repository; import fellowship.mealmaestro.models.FoodModel; -import fellowship.mealmaestro.models.ShoppingListRequestModel; -import fellowship.mealmaestro.models.UserModel; @Repository public class ShoppingListRepository { @@ -25,20 +23,18 @@ public ShoppingListRepository(Driver driver){ } //#region Create - public FoodModel addToShoppingList(ShoppingListRequestModel request){ - FoodModel food = request.getFood(); - UserModel user = request.getUser(); + public FoodModel addToShoppingList(FoodModel food, String email){ try (Session session = driver.session()){ - return session.executeWrite(addToShoppingListTransaction(food, user.getUsername(), user.getEmail())); + return session.executeWrite(addToShoppingListTransaction(food, email)); } } - public static TransactionCallback addToShoppingListTransaction(FoodModel food, String username, String email){ + public static TransactionCallback addToShoppingListTransaction(FoodModel food, String email){ return transaction -> { - transaction.run("MATCH (User{username: $username, email: $email})-[:HAS_LIST]->(s:`Shopping List`) \r\n" + // + transaction.run("MATCH (User{email: $email})-[:HAS_LIST]->(s:`Shopping List`) \r\n" + // "CREATE (s)-[:IN_LIST]->(:Food {name: $name, quantity: $quantity, weight: $weight})", - Values.parameters("username", username, "email", email, "name", food.getName(), + Values.parameters("email", email, "name", food.getName(), "quantity", food.getQuantity(), "weight", food.getWeight())); FoodModel addedFood = new FoodModel(food.getName(), food.getQuantity(), food.getWeight()); @@ -47,32 +43,26 @@ public static TransactionCallback addToShoppingListTransaction(FoodMo } /* Example Post data: * { - * "food": { * "name": "Carrot", * "quantity": "0", * "weight": "0" - * }, - * "user": { - * "username": "Frank", - * "email": "test@example.com" - * } * } */ //#endregion //#region Read - public List getShoppingList(UserModel user){ + public List getShoppingList(String email){ try (Session session = driver.session()){ - return session.executeRead(getShoppingListTransaction(user.getUsername(), user.getEmail())); + return session.executeRead(getShoppingListTransaction(email)); } } - public static TransactionCallback> getShoppingListTransaction(String username, String email){ + public static TransactionCallback> getShoppingListTransaction(String email){ return transaction -> { - var result = transaction.run("MATCH (User{username: $username, email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[:IN_LIST]->(f:Food) \r\n" + // + var result = transaction.run("MATCH (User{email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[:IN_LIST]->(f:Food) \r\n" + // "RETURN f.name AS name, f.quantity AS quantity, f.weight AS weight", - Values.parameters("username", username, "email", email)); + Values.parameters("email", email)); List foods = new ArrayList<>(); while (result.hasNext()){ @@ -84,27 +74,23 @@ var record = result.next(); } /* Example Post data: * { - * "username": "Frank", - * "email": "test@example.com" * } */ //#endregion //#region Update - public void updateShoppingList(ShoppingListRequestModel request){ - FoodModel food = request.getFood(); - UserModel user = request.getUser(); + public void updateShoppingList(FoodModel food, String email){ try (Session session = driver.session()){ - session.executeWrite(updateShoppingListTransaction(food, user.getUsername(), user.getEmail())); + session.executeWrite(updateShoppingListTransaction(food, email)); } } - public static TransactionCallback updateShoppingListTransaction(FoodModel food, String username, String email){ + public static TransactionCallback updateShoppingListTransaction(FoodModel food, String email){ return transaction -> { - transaction.run("MATCH (User{username: $username, email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[:IN_LIST]->(f:Food {name: $name}) \r\n" + // + transaction.run("MATCH (User{email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[:IN_LIST]->(f:Food {name: $name}) \r\n" + // "SET f.quantity = $quantity, f.weight = $weight", - Values.parameters("username", username, "email", email, "name", food.getName(), + Values.parameters("email", email, "name", food.getName(), "quantity", food.getQuantity(), "weight", food.getWeight())); return null; }; @@ -112,22 +98,72 @@ public static TransactionCallback updateShoppingListTransaction(FoodModel //#endregion //#region Delete - public void removeFromShoppingList(ShoppingListRequestModel request){ - FoodModel food = request.getFood(); - UserModel user = request.getUser(); + public void removeFromShoppingList(FoodModel food, String email){ try (Session session = driver.session()){ - session.executeWrite(removeFromShoppingListTransaction(food, user.getUsername(), user.getEmail())); + session.executeWrite(removeFromShoppingListTransaction(food, email)); } } - public static TransactionCallback removeFromShoppingListTransaction(FoodModel food, String username, String email){ + public static TransactionCallback removeFromShoppingListTransaction(FoodModel food, String email){ return transaction -> { - transaction.run("MATCH (User{username: $username, email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[r:IN_LIST]->(f:Food {name: $name}) \r\n" + // + transaction.run("MATCH (User{email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[r:IN_LIST]->(f:Food {name: $name}) \r\n" + // "DELETE r,f", - Values.parameters("username", username, "email", email, "name", food.getName())); + Values.parameters("email", email, "name", food.getName())); return null; }; } //#endregion + + //#region Move to Pantry + public List buyItem(FoodModel food, String email){ + try (Session session = driver.session()){ + return session.executeWrite(buyItemTransaction(food, email)); + } + } + + public static TransactionCallback> buyItemTransaction(FoodModel food, String email){ + return transaction -> { + var findFoodInBoth = transaction.run( + "MATCH (u:User{email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[:IN_LIST]->(:Food {name: $name}), \r\n" + // + "(u)-[:HAS_PANTRY]->(p:`Pantry`)-[:IN_PANTRY]->(:Food {name: $name}) \r\n" + // + "RETURN count(*)", + Values.parameters("email", email, "name", food.getName()) + ); + + boolean foodInBoth = findFoodInBoth.single().get(0).asInt() > 0; + + if (foodInBoth) { + transaction.run( + // If food exists in both, update the pantry food and delete the shopping list food + "MATCH (u:User{email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[r:IN_LIST]->(f:Food {name: $name}), \r\n" + // + "(u)-[:HAS_PANTRY]->(p:`Pantry`)-[:IN_PANTRY]->(fp:Food{name: $name}) \r\n" + // + "SET fp.weight = fp.weight + f.weight, fp.quantity = fp.quantity + f.quantity \r\n" + // + "DELETE r, f", + Values.parameters("email", email, "name", food.getName()) + ); + } else { + transaction.run( + // If food only exists in shopping list, create it in the pantry and delete it from the shopping list + "MATCH (u:User{email: $email})-[:HAS_LIST]->(s:`Shopping List`)-[r:IN_LIST]->(f:Food {name: $name}), \r\n" + // + "(u)-[:HAS_PANTRY]->(p:`Pantry`) \r\n" + // + "CREATE (p)-[:IN_PANTRY]->(:Food {name: $name, quantity: f.quantity, weight: f.weight}) \r\n" + // + "DELETE r, f", + Values.parameters("email", email, "name", food.getName()) + ); + } + + var result = transaction.run( + "MATCH (u:User{email: $email})-[:HAS_PANTRY]->(:`Pantry`)-[:IN_PANTRY]->(f:Food) RETURN f.name AS name, f.quantity AS quantity, f.weight AS weight \r\n", // + Values.parameters("email", email) + ); + + List foods = new ArrayList<>(); + while (result.hasNext()){ + var record = result.next(); + foods.add(new FoodModel(record.get("name").asString(), record.get("quantity").asInt(), record.get("weight").asInt())); + } + return foods; + }; + } } diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/UserRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/UserRepository.java index 083192a6..68a3e9e2 100644 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/UserRepository.java +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/UserRepository.java @@ -1,5 +1,7 @@ package fellowship.mealmaestro.repositories; +import java.util.Optional; + import org.neo4j.driver.Driver; import org.neo4j.driver.Session; import org.neo4j.driver.TransactionCallback; @@ -8,6 +10,7 @@ import org.springframework.stereotype.Repository; import fellowship.mealmaestro.models.UserModel; +import fellowship.mealmaestro.models.auth.AuthorityRoleModel; @Repository public class UserRepository { @@ -22,69 +25,67 @@ public UserRepository(Driver driver){ //#region Create public void createUser(UserModel user){ try (Session session = driver.session()){ - - session.executeWrite(createUserTransaction(user.getUsername(), user.getPassword(), user.getEmail())); + + session.executeWrite(createUserTransaction(user.getName(), user.getPassword(), user.getEmail())); } } public static TransactionCallback createUserTransaction(String username, String password, String email) { - //creates user with default pantry, shopping list, recipe book, and preferences + // Creates user with default pantry, shopping list, recipe book, and preferences return transaction -> { - transaction.run("CREATE (:Preferences)<-[:HAS_PREFERENCES]-(n0:User {username: $username, password: $password, email: $email})-[:HAS_PANTRY]->(:Pantry),\r\n" + // - "(:`Shopping List`)<-[:HAS_LIST]-(n0)-[:HAS_RECIPE_BOOK]->(:`Recipe Book`)", - Values.parameters("username", username, "password", password, "email", email)); + transaction.run("CREATE (:Pantry)<-[:HAS_PANTRY]-(n0:User {username: $username, password: $password, email: $email})-[:HAS_PREFERENCES]->(n1:Preferences)-[:HAS_ALLERGIES]->(:Allergies), " + + "(:`Shopping List`)<-[:HAS_LIST]-(n0)-[:HAS_RECIPE_BOOK]->(:`Recipe Book`), " + + "(:Interval)<-[:HAS_INTERVAL]-(n1)-[:HAS_GOAL]->(:Goal), " + + "(:`Calorie Goal`)<-[:HAS_CALORIE_GOAL]-(n1)-[:HAS_EATING_STYLE]->(:`Eating Style`), " + + "(:Macro {protein: 0,carbs: 0, fat: 0})<-[:HAS_MACRO]-(n1)-[:HAS_BUDGET]->(:Budget), " + + "(:BMI {height: 0, weight: 0, BMI: 0})<-[:HAS_BMI]-(n1)-[:HAS_COOKING_TIME]->(:`Cooking Time`)", + Values.parameters("username", username, "password", password, "email", email)); return null; }; } //#endregion - //#region Check User - public boolean checkUser(UserModel user){ + //#region Get User + public Optional findByEmail(String email){ try (Session session = driver.session()){ - return session.executeRead(checkUserTransaction(user.getUsername(), user.getEmail())); + UserModel user = session.executeRead(findByEmailTransaction(email)); + return Optional.ofNullable(user); } } - public static TransactionCallback checkUserTransaction(String username, String email) { + public static TransactionCallback findByEmailTransaction(String email) { return transaction -> { - var result = transaction.run("MATCH (n0:User {username: $username, email: $email}) RETURN n0", - Values.parameters("username", username, "email", email)); - return result.hasNext(); - }; - } - //#endregion + var result = transaction.run("MATCH (n0:User {email: $email}) RETURN n0", + Values.parameters("email", email)); - //#region Login - public boolean login(UserModel user){ - try (Session session = driver.session()){ - return session.executeRead(loginTransaction(user.getEmail(), user.getPassword())); - } - } + if (!result.hasNext()) { + return null; + } - public static TransactionCallback loginTransaction(String email, String password) { - return transaction -> { - var result = transaction.run("MATCH (n0:User {email: $email, password: $password}) RETURN n0", - Values.parameters("email", email, "password", password)); - return result.hasNext(); + var record = result.single(); + var node = record.get("n0"); + UserModel user = new UserModel( + node.get("username").asString(), + node.get("password").asString(), + node.get("email").asString(), + AuthorityRoleModel.USER + ); + return user; }; } - //#endregion - //#region Get User - public UserModel getUser(UserModel user){ + public UserModel updateUser(UserModel user, String email) { try (Session session = driver.session()){ - return session.executeRead(getUserTransaction(user.getEmail())); + session.executeWrite(updateUserTransaction(user.getName(), email)); + return user; } } - public static TransactionCallback getUserTransaction(String email) { + public static TransactionCallback updateUserTransaction(String username, String email) { return transaction -> { - var result = transaction.run("MATCH (n0:User {email: $email}) RETURN n0", - Values.parameters("email", email)); - var record = result.single(); - var node = record.get("n0"); - UserModel user = new UserModel(node.get("username").asString(), node.get("password").asString(), node.get("email").asString()); - return user; + transaction.run("MATCH (n0:User {email: $email}) SET n0.username = $username", + Values.parameters("email", email, "username", username)); + return null; }; } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/BrowseService.java b/backend/src/main/java/fellowship/mealmaestro/services/BrowseService.java new file mode 100644 index 00000000..05036c9f --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/BrowseService.java @@ -0,0 +1,32 @@ +package fellowship.mealmaestro.services; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import fellowship.mealmaestro.models.MealModel; +import fellowship.mealmaestro.repositories.BrowseRepository; +import fellowship.mealmaestro.services.auth.JwtService; + + +@Service +public class BrowseService { + + @Autowired + private JwtService jwtService; + + @Autowired + private BrowseRepository browseRepository; + + public List getPopularMeals(String token){ + String email = jwtService.extractUserEmail(token); + return browseRepository.getPopularMeals(email); + } + + public List getSearchedMeals(String mealName, String token){ + String email = jwtService.extractUserEmail(token); + return browseRepository.getSearchedMeals(mealName,email); + } + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealDatabseService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealDatabseService.java new file mode 100644 index 00000000..6f4e4d8e --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/MealDatabseService.java @@ -0,0 +1,112 @@ +package fellowship.mealmaestro.services; + +import java.time.DayOfWeek; +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import fellowship.mealmaestro.models.DaysMealsModel; +import fellowship.mealmaestro.models.MealModel; +import fellowship.mealmaestro.models.UserModel; +import fellowship.mealmaestro.repositories.DaysMealsRepository; +import fellowship.mealmaestro.repositories.MealRepository; + +@Service +public class MealDatabseService { + private final MealRepository mealRepository; + + public MealRepository getMealRepository() { + return mealRepository; + } + + private final DaysMealsRepository daysMealsRepository; + + public DaysMealsRepository getDaysMealsRepository() { + return daysMealsRepository; + } + + @Autowired + public MealDatabseService(MealRepository mealRepository, DaysMealsRepository daysMealsRepository) { + this.daysMealsRepository = daysMealsRepository; + this.mealRepository = mealRepository; + + } + + @Autowired + private UserService userService; + + public void saveDaysMeals(JsonNode daysMealsJson, DayOfWeek date, String token) + throws JsonProcessingException, IllegalArgumentException { + + UserModel userModel = userService.getUser(token); + + ObjectMapper objectMapper = new ObjectMapper(); + + MealModel breakfast = objectMapper.treeToValue(daysMealsJson.get("breakfast"), MealModel.class); + MealModel lunch = objectMapper.treeToValue(daysMealsJson.get("lunch"), MealModel.class); + MealModel dinner = objectMapper.treeToValue(daysMealsJson.get("dinner"), MealModel.class); + + DaysMealsModel daysMealsModel = new DaysMealsModel(breakfast, lunch, dinner, date, userModel); + daysMealsRepository.save(daysMealsModel); + } + + public List retrieveDaysMealsModel(DayOfWeek date) { + return daysMealsRepository.findMealsForNextWeek(date); + } + + public Optional retrieveDatesMealModel(DayOfWeek date) { + return daysMealsRepository.findMealsForDate(date); + } + + public Optional fetchDay(DayOfWeek mealDate) { + return daysMealsRepository.findByMealDate(mealDate); + } + + public void saveRegeneratedMeal(DaysMealsModel daysMealsModel) { + daysMealsRepository.save(daysMealsModel); + } + + public void changeMealForDate(DayOfWeek mealDate, MealModel mealModel, String time) { + + Optional optionalDaysMealsModel = daysMealsRepository.findByMealDate(mealDate); + if (optionalDaysMealsModel.isEmpty()) { + // Handle error, node not found for the given mealDate + return; + } + + DaysMealsModel daysMealsModel = optionalDaysMealsModel.get(); + + if (time == "breakfast") { + daysMealsModel.setBreakfast(mealModel); + MealModel updatedMeal = mealRepository.save(mealModel); + daysMealsModel.setBreakfast(updatedMeal); + } + if (time == "lunch") { + daysMealsModel.setLunch(mealModel); + MealModel updatedMeal = mealRepository.save(mealModel); + daysMealsModel.setLunch(updatedMeal); + } + if (time == "dinner") { + daysMealsModel.setDinner(mealModel); + MealModel updatedMeal = mealRepository.save(mealModel); + daysMealsModel.setDinner(updatedMeal); + } + + daysMealsRepository.save(daysMealsModel); + } + + public Optional findUsersDaysMeals(DayOfWeek day, String token) + throws JsonProcessingException, IllegalArgumentException { + UserModel userModel = userService.getUser(token); + + return daysMealsRepository.findById((userModel.getEmail() + day.toString())); + + } + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java index 65a4680e..31d69d39 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java @@ -1,5 +1,8 @@ package fellowship.mealmaestro.services; +import java.util.ArrayList; +import java.util.List; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -7,6 +10,7 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -14,54 +18,48 @@ public class MealManagementService { @Autowired - private OpenaiApiService openaiApiService = new OpenaiApiService(); + private OpenaiApiService openaiApiService; @Autowired - private ObjectMapper objectMapper = new ObjectMapper(); + private ObjectMapper objectMapper; public String generateDaysMeals() throws JsonMappingException, JsonProcessingException { int i = 0; JsonNode breakfastJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - if(breakfastJson.isMissingNode()) - { + if (breakfastJson.isMissingNode()) { int prevBestOfN = openaiApiService.getBestofN(); Boolean success = false; openaiApiService.setBestofN(prevBestOfN + 1); - while(!success && i < 5) - { + while (!success && i < 5) { breakfastJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - if(!breakfastJson.isMissingNode()) + if (!breakfastJson.isMissingNode()) success = true; - i++; + i++; } openaiApiService.setBestofN(prevBestOfN); } JsonNode lunchJson = objectMapper.readTree(openaiApiService.fetchMealResponse("lunch")); - if(lunchJson.isMissingNode()) - { + if (lunchJson.isMissingNode()) { int prevBestOfN = openaiApiService.getBestofN(); Boolean success = false; openaiApiService.setBestofN(prevBestOfN + 1); - while(!success&& i < 5) - { + while (!success && i < 5) { lunchJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - if(!lunchJson.isMissingNode()) + if (!lunchJson.isMissingNode()) success = true; - i++; + i++; } openaiApiService.setBestofN(prevBestOfN); } JsonNode dinnerJson = objectMapper.readTree(openaiApiService.fetchMealResponse("dinner")); - if(dinnerJson.isMissingNode()) - { + if (dinnerJson.isMissingNode()) { int prevBestOfN = openaiApiService.getBestofN(); Boolean success = false; openaiApiService.setBestofN(prevBestOfN + 1); - while(!success&& i < 5) - { + while (!success && i < 5) { dinnerJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - if(!dinnerJson.isMissingNode()) + if (!dinnerJson.isMissingNode()) success = true; - i++; + i++; } openaiApiService.setBestofN(prevBestOfN); } @@ -70,30 +68,204 @@ public String generateDaysMeals() throws JsonMappingException, JsonProcessingExc combinedNode.set("lunch", lunchJson); combinedNode.set("dinner", dinnerJson); // - // DaysMeals daysMeals = objectMapper.treeToValue(combinedNode, DaysMeals.class); - - String res = combinedNode.toString(); - res = res.replace("/r/n", "\\r\\n"); - return res; + // DaysMeals daysMeals = objectMapper.treeToValue(combinedNode, + // DaysMeals.class); + return combinedNode.toString(); + } + + public JsonNode generateDaysMealsJson() throws JsonMappingException, JsonProcessingException { + int i = 0; + JsonNode breakfastJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); + if (breakfastJson.isMissingNode()) { + int prevBestOfN = openaiApiService.getBestofN(); + Boolean success = false; + openaiApiService.setBestofN(prevBestOfN + 1); + while (!success && i < 5) { + breakfastJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); + if (!breakfastJson.isMissingNode()) + success = true; + i++; + } + openaiApiService.setBestofN(prevBestOfN); + } + JsonNode lunchJson = objectMapper.readTree(openaiApiService.fetchMealResponse("lunch")); + if (lunchJson.isMissingNode()) { + int prevBestOfN = openaiApiService.getBestofN(); + Boolean success = false; + openaiApiService.setBestofN(prevBestOfN + 1); + while (!success && i < 5) { + lunchJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); + if (!lunchJson.isMissingNode()) + success = true; + i++; + } + openaiApiService.setBestofN(prevBestOfN); + } + JsonNode dinnerJson = objectMapper.readTree(openaiApiService.fetchMealResponse("dinner")); + if (dinnerJson.isMissingNode()) { + int prevBestOfN = openaiApiService.getBestofN(); + Boolean success = false; + openaiApiService.setBestofN(prevBestOfN + 1); + while (!success && i < 5) { + dinnerJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); + if (!dinnerJson.isMissingNode()) + success = true; + i++; + } + openaiApiService.setBestofN(prevBestOfN); + } + ObjectNode combinedNode = JsonNodeFactory.instance.objectNode(); + combinedNode.set("breakfast", breakfastJson); + combinedNode.set("lunch", lunchJson); + combinedNode.set("dinner", dinnerJson); + // + // DaysMeals daysMeals = objectMapper.treeToValue(combinedNode, + // DaysMeals.class); + return combinedNode; } public String generateMeal() throws JsonMappingException, JsonProcessingException { int i = 0; JsonNode mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast lunch or dinner")); - if(mealJson.isMissingNode()) - { + if (mealJson.isMissingNode()) { int prevBestOfN = openaiApiService.getBestofN(); Boolean success = false; openaiApiService.setBestofN(prevBestOfN + 1); - while(!success&& i < 5) - { - mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); - if(!mealJson.isMissingNode()) + while (!success && i < 5) { + mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast lunch or dinner")); + if (!mealJson.isMissingNode()) success = true; - i++; + i++; } openaiApiService.setBestofN(prevBestOfN); } return mealJson.toString(); } + + public String generateMeal(String mealType) throws JsonMappingException, JsonProcessingException { + + JsonNode mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse(mealType)); + int i = 0; + if(mealJson.isMissingNode()) + { + int prevBestOfN = openaiApiService.getBestofN(); + Boolean success = false; + openaiApiService.setBestofN(prevBestOfN + 1); + while(!success&& i < 4) + { + mealJson = + objectMapper.readTree(openaiApiService.fetchMealResponse(mealType)); + if(!mealJson.isMissingNode()) + success = true; + i++; + } + openaiApiService.setBestofN(prevBestOfN); + } + return mealJson.toString(); + } + + // public String generatePopularMeals()throws JsonMappingException, JsonProcessingException { + + // JsonNode mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast lunch or dinner")); + // int i = 0; + // if(mealJson.isMissingNode()) + // { + // int prevBestOfN = openaiApiService.getBestofN(); + // Boolean success = false; + // openaiApiService.setBestofN(prevBestOfN + 1); + // while(!success&& i < 5) + // { + // mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); + // if(!mealJson.isMissingNode()) + // success = true; + // i++; + // } + // openaiApiService.setBestofN(prevBestOfN); + // } + // return mealJson.toString(); + + // } + + // public String generateSearchedMeals(String query) throws JsonProcessingException { + + // JsonNode mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast lunch or dinner")); + // int i = 0; + // if(mealJson.isMissingNode()) + // { + // int prevBestOfN = openaiApiService.getBestofN(); + // Boolean success = false; + // openaiApiService.setBestofN(prevBestOfN + 1); + // while(!success&& i < 5) + // { + // mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse("breakfast")); + // if(!mealJson.isMissingNode()) + // success = true; + // i++; + // } + // openaiApiService.setBestofN(prevBestOfN); + // } + + // // Convert the JSON node to a List to filter the entities + // List mealList = new ArrayList<>(); + // if (mealJson.isArray()) { + // for (JsonNode entity : mealJson) { + // mealList.add(entity); + // } + // } + + // // Split the query into individual words + // String[] searchWords = query.toLowerCase().split(" "); + + // // Filter the entities based on the query parameter + // List filteredEntities = new ArrayList<>(); + // for (JsonNode entity : mealList) { + // String name = entity.get("name").asText().toLowerCase(); + // // String description = entity.get("description").asText().toLowerCase(); + // String ingredients = entity.get("ingredients").asText().toLowerCase(); + // String description = entity.get("description").asText().toLowerCase(); + // // String instructions = entity.get("instruction").asText().toLowerCase(); + + // // Check if all search words are present in the name, ingredients, or description + // boolean allWordsFound = true; + // for (String word : searchWords) { + // if (!name.contains(word) && !ingredients.contains(word) && !description.contains(word)) { + // allWordsFound = false; + // break; + // } + // } + // if (allWordsFound) { + // filteredEntities.add(entity); + // } + + // } + // // if (name.contains(query.toLowerCase()) || ingredients.contains(query.toLowerCase()) || description.contains(query.toLowerCase()) ) { + // // filteredEntities.add(entity); + // // } + // // } + // // Create a new JSON array node to store the filtered entities + // ArrayNode filteredEntitiesArray = JsonNodeFactory.instance.arrayNode(); + // filteredEntities.forEach(filteredEntitiesArray::add); + + // return filteredEntitiesArray.toString(); + + // // int i = 0; + // // JsonNode searchedMeal = objectMapper.readTree(openaiApiService.fetchMealResponse(query)); + // // if (searchedMeal.isMissingNode()) { + // // int prevBestOfN = openaiApiService.getBestofN(); + // // boolean success = false; + // // openaiApiService.setBestofN(prevBestOfN + 1); + // // while (!success && i < 5) { + // // searchedMeal = objectMapper.readTree(openaiApiService.fetchMealResponse(query)); + // // if (!searchedMeal.isMissingNode()) + // // success = true; + // // i++; + // // } + // // openaiApiService.setBestofN(prevBestOfN); + // // } + // // return searchedMeal.toString(); + + + // } + } + diff --git a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java index 12a8254a..07687639 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java @@ -20,85 +20,108 @@ @Service public class OpenaiApiService { - Dotenv dotenv = Dotenv.load(); private static final String OPENAI_URL = "https://api.openai.com/v1/completions"; - private final String API_KEY = dotenv.get("OPENAI_API_KEY"); + private final static String API_KEY; private final RestTemplate restTemplate = new RestTemplate(); + static{ + String apiKey; + Dotenv dotenv; + if (System.getenv("OPENAI_API_KEY") != null) { + apiKey = System.getenv("OPENAI_API_KEY"); + } else { + try{ + dotenv = Dotenv.load(); + apiKey = dotenv.get("OPENAI_API_KEY"); + } catch (Exception e){ + dotenv = Dotenv.configure() + .ignoreIfMissing() + .load(); + apiKey = "No API Key Found"; + } + } + API_KEY = apiKey; + } + private String model = "text-davinci-003"; private String stop = ""; - private double temperature = 0.2; + private double temperature = 0.5; private double topP = 1.0; private double freqPenalty = 0.0; private double presencePenalty = 0.0; private int maximumTokenLength = 800; - + // potential vars - // will make a few prompts and return best, heavy on token use - private int bestOfN = 1; - // detect abuse - // private String user = ""; - // echo back prompt and its compeletion - // private boolean echo = false; - // stream prompt as it generates - // private boolean stream = false; - - @Autowired private ObjectMapper jsonMapper = new ObjectMapper(); - @Autowired private OpenaiPromptBuilder pBuilder = new OpenaiPromptBuilder(); - - public String fetchMealResponse(String Type) throws JsonMappingException, JsonProcessingException{ - String jsonResponse = getJSONResponse(Type); - JsonNode jsonNode = jsonMapper.readTree(jsonResponse); - - String text = jsonNode.get("choices").get(0).get("text").asText(); - text = text.replace("\\\"", "\""); - text = text.replace("\n", ""); - return text; + + // will make a few prompts and return best, heavy on token use + private int bestOfN = 1; + // detect abuse + // private String user = ""; + // echo back prompt and its compeletion + // private boolean echo = false; + // stream prompt as it generates + // private boolean stream = false; + + @Autowired + private ObjectMapper jsonMapper = new ObjectMapper(); + @Autowired + private OpenaiPromptBuilder pBuilder = new OpenaiPromptBuilder(); + + public String fetchMealResponse(String Type) throws JsonMappingException, JsonProcessingException { + // String jsonResponse = getJSONResponse(Type); + // JsonNode jsonNode = jsonMapper.readTree(jsonResponse); + + // String text = jsonNode.get("choices").get(0).get("text").asText(); + // text = text.replace("\\\"", "\""); + // text = text.replace("\n", ""); + // text = text.replace("/r/n", "\\r\\n"); + // return text; + + return "{\"instructions\":\"1. Preheat oven to 375 degrees/r/n2. Grease a baking dish with butter/r/n3. Beat together the eggs, milk, and a pinch of salt/r/n4. Place the bread slices in the baking dish and pour the egg mixture over them/r/n5. Bake in the preheated oven for 25 minutes/r/n6. Serve warm with your favorite toppings\",\"name\":\"Baked French Toast\",\"description\":\"a delicious breakfast dish of egg-soaked bread\",\"ingredients\":\"6 slices of bread/r/n3 eggs/r/n3/4 cup of milk/r/nSalt/r/nButter\",\"cookingTime\":\"30 minutes\"}"; } - public String fetchMealResponse(String Type,String extendedPrompt) throws JsonMappingException, JsonProcessingException{ - JsonNode jsonNode = jsonMapper.readTree(getJSONResponse(Type,extendedPrompt)); + public String fetchMealResponse(String Type, String extendedPrompt) + throws JsonMappingException, JsonProcessingException { + JsonNode jsonNode = jsonMapper.readTree(getJSONResponse(Type, extendedPrompt)); return jsonNode.get("text").asText(); } - - - public String getJSONResponse(String Type) throws JsonProcessingException{ - + + public String getJSONResponse(String Type) throws JsonProcessingException { + String prompt; String jsonRequest; - prompt = pBuilder.buildPrompt(Type); jsonRequest = buildJsonApiRequest(prompt); - + HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Authorization", "Bearer " + API_KEY); HttpEntity request = new HttpEntity(jsonRequest, headers); ResponseEntity response = restTemplate.postForEntity(OPENAI_URL, request, String.class); - + return response.getBody(); } - public String getJSONResponse(String Type, String extendedPrompt) throws JsonProcessingException{ - + + public String getJSONResponse(String Type, String extendedPrompt) throws JsonProcessingException { + String prompt; String jsonRequest; - - prompt = pBuilder.buildPrompt(Type,extendedPrompt); + prompt = pBuilder.buildPrompt(Type, extendedPrompt); jsonRequest = buildJsonApiRequest(prompt); - + HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Authorization", "Bearer " + API_KEY); HttpEntity request = new HttpEntity(jsonRequest, headers); ResponseEntity response = restTemplate.postForEntity(OPENAI_URL, request, String.class); - + return response.getBody().replace("\\\"", "\""); } @@ -110,7 +133,7 @@ public String buildJsonApiRequest(String prompt) throws JsonProcessingException params.put("temperature", temperature); params.put("max_tokens", maximumTokenLength); params.put("stop", "####"); - // params.put("top_p", topP); + // params.put("top_p", topP); params.put("frequency_penalty", freqPenalty); params.put("presence_penalty", presencePenalty); params.put("n", bestOfN); @@ -148,6 +171,7 @@ public void setPresencePenalty(double x) { public void setBestofN(int x) { this.bestOfN = x; } + public int getBestofN() { return this.bestOfN; } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java index 9dcc3137..fa02bd93 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java @@ -3,6 +3,7 @@ import java.util.HashMap; import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.JsonProcessingException; @@ -11,10 +12,12 @@ @Service public class OpenaiPromptBuilder { + @Autowired + private SettingsService settingsService; public String buildPrompt(String Type) throws JsonProcessingException { String prompt = ""; - - prompt += buildContext(Type); + String preferenceString =settingsService.ALL_SETTINGS; + prompt += buildContext(Type, preferenceString); prompt += buildGoal(); prompt += buildFormat(); prompt += buildSubtasks(); diff --git a/backend/src/main/java/fellowship/mealmaestro/services/PantryService.java b/backend/src/main/java/fellowship/mealmaestro/services/PantryService.java index dcbb3506..8bb818d5 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/PantryService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/PantryService.java @@ -6,29 +6,35 @@ import org.springframework.stereotype.Service; import fellowship.mealmaestro.models.FoodModel; -import fellowship.mealmaestro.models.PantryRequestModel; -import fellowship.mealmaestro.models.UserModel; import fellowship.mealmaestro.repositories.PantryRepository; +import fellowship.mealmaestro.services.auth.JwtService; @Service public class PantryService { + + @Autowired + private JwtService jwtService; @Autowired private PantryRepository pantryRepository; - public FoodModel addToPantry(PantryRequestModel pantryRequest){ - return pantryRepository.addToPantry(pantryRequest); + public FoodModel addToPantry(FoodModel request, String token){ + String email = jwtService.extractUserEmail(token); + return pantryRepository.addToPantry(request, email); } - public void removeFromPantry(PantryRequestModel pantryRequest){ - pantryRepository.removeFromPantry(pantryRequest); + public void removeFromPantry(FoodModel request, String token){ + String email = jwtService.extractUserEmail(token); + pantryRepository.removeFromPantry(request, email); } - public void updatePantry(PantryRequestModel pantryRequest){ - pantryRepository.updatePantry(pantryRequest); + public void updatePantry(FoodModel request, String token){ + String email = jwtService.extractUserEmail(token); + pantryRepository.updatePantry(request, email); } - public List getPantry(UserModel user){ - return pantryRepository.getPantry(user); + public List getPantry(String token){ + String email = jwtService.extractUserEmail(token); + return pantryRepository.getPantry(email); } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecipeBookService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecipeBookService.java new file mode 100644 index 00000000..6c72f20a --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecipeBookService.java @@ -0,0 +1,39 @@ +package fellowship.mealmaestro.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +import fellowship.mealmaestro.models.MealModel; +import fellowship.mealmaestro.repositories.RecipeBookRepository; +import fellowship.mealmaestro.services.auth.JwtService; + +@Service +public class RecipeBookService { + + @Autowired + private JwtService jwtService; + + private final RecipeBookRepository recipeBookRepository; + + public RecipeBookService(RecipeBookRepository recipeBookRepository) { + this.recipeBookRepository = recipeBookRepository; + } + + public MealModel addRecipe(MealModel recipe, String token) { + String email = jwtService.extractUserEmail(token); + return recipeBookRepository.addRecipe(recipe, email); + } + + public void removeRecipe(MealModel request, String token) { + String email = jwtService.extractUserEmail(token); + recipeBookRepository.removeRecipe(request, email); + } + + public List getAllRecipes(String token) { + String email = jwtService.extractUserEmail(token); + + return recipeBookRepository.getAllRecipes(email); + } +} \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/services/SettingsService.java b/backend/src/main/java/fellowship/mealmaestro/services/SettingsService.java new file mode 100644 index 00000000..51e2f426 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/SettingsService.java @@ -0,0 +1,66 @@ +package fellowship.mealmaestro.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + + +import fellowship.mealmaestro.models.SettingsModel; +import fellowship.mealmaestro.repositories.SettingsRepository; +import fellowship.mealmaestro.services.auth.JwtService; + + +@Service +public class SettingsService { + + + @Autowired + private JwtService jwtService; + + @Autowired + private SettingsRepository SettingsRepository; + + public String ALL_SETTINGS; + + public SettingsModel getSettings(String token){ + String email = jwtService.extractUserEmail(token); + SettingsModel settingsModel = SettingsRepository.getSettings(email); + ALL_SETTINGS = makeString(settingsModel); + return settingsModel; + } + + public void updateSettings(SettingsModel request, String token){ + this.makeString(request); + String email = jwtService.extractUserEmail(token); + SettingsRepository.updateSettings(request, email); + ALL_SETTINGS = makeString(request); + } + + public String makeString(SettingsModel request){ + + String s ="";// + "The goal: " + request.getGoal().toString() +// +". The budget range is: "+ request.getBudgetRange().toString() +// +". The average daily calorie goal is: "+ request.getCalorieAmount() +// +". The average cooking time per meal is : "+ request.getCookingTime().toString() +// + ". The grocery shopping interval is: "+request.getShoppingInterval().toString() +// +". The user's BMI is: "+ request.getUserBMI() + // + ". The user eats like "+ request.getFoodPreferences().toString() + // +". The user's allergens: "+ request.getAllergies().toString() +// +". The macro ratio for the user is "+ request.getMacroRatio().toString() + //; + if(request.isFoodPreferenceSet()) + { + s += ". The user prefers "+ request.getFoodPreferences().toString() ; + } + if(request.isAllergiesSet()) + {}else { + s += ". The user is allergic to "+ request.getAllergies().toString() ; + } + + System.out.println(s); + + return s; + + } + + +} \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/services/ShoppingListService.java b/backend/src/main/java/fellowship/mealmaestro/services/ShoppingListService.java index 254a6f0a..bc931d33 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/ShoppingListService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/ShoppingListService.java @@ -6,29 +6,40 @@ import org.springframework.stereotype.Service; import fellowship.mealmaestro.models.FoodModel; -import fellowship.mealmaestro.models.ShoppingListRequestModel; -import fellowship.mealmaestro.models.UserModel; import fellowship.mealmaestro.repositories.ShoppingListRepository; +import fellowship.mealmaestro.services.auth.JwtService; @Service public class ShoppingListService { + + @Autowired + private JwtService jwtService; @Autowired private ShoppingListRepository shoppingListRepository; - public FoodModel addToShoppingList(ShoppingListRequestModel request){ - return shoppingListRepository.addToShoppingList(request); + public FoodModel addToShoppingList(FoodModel request, String token){ + String email = jwtService.extractUserEmail(token); + return shoppingListRepository.addToShoppingList(request, email); + } + + public void removeFromShoppingList(FoodModel request, String token){ + String email = jwtService.extractUserEmail(token); + shoppingListRepository.removeFromShoppingList(request, email); } - public void removeFromShoppingList(ShoppingListRequestModel request){ - shoppingListRepository.removeFromShoppingList(request); + public void updateShoppingList(FoodModel request, String token){ + String email = jwtService.extractUserEmail(token); + shoppingListRepository.updateShoppingList(request, email); } - public void updateShoppingList(ShoppingListRequestModel request){ - shoppingListRepository.updateShoppingList(request); + public List getShoppingList(String token){ + String email = jwtService.extractUserEmail(token); + return shoppingListRepository.getShoppingList(email); } - public List getShoppingList(UserModel user){ - return shoppingListRepository.getShoppingList(user); + public List buyItem(FoodModel request, String token){ + String email = jwtService.extractUserEmail(token); + return shoppingListRepository.buyItem(request, email); } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/UserService.java b/backend/src/main/java/fellowship/mealmaestro/services/UserService.java index 90836596..4d4bcb46 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/UserService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/UserService.java @@ -1,10 +1,13 @@ package fellowship.mealmaestro.services; +import java.util.Optional; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import fellowship.mealmaestro.models.UserModel; import fellowship.mealmaestro.repositories.UserRepository; +import fellowship.mealmaestro.services.auth.JwtService; @Service public class UserService { @@ -12,20 +15,22 @@ public class UserService { @Autowired private UserRepository userRepository; - public void createUser(UserModel user){ - userRepository.createUser(user); - } + @Autowired + private JwtService jwtService; - public boolean checkUser(UserModel user){ - //TODO: hash password - return userRepository.checkUser(user); + public Optional findByEmail(String email){ + return userRepository.findByEmail(email); } - public boolean login(UserModel user){ - return userRepository.login(user); + public UserModel updateUser(UserModel user, String token) { + String authToken = token.substring(7); + String email = jwtService.extractUserEmail(authToken); + return userRepository.updateUser(user, email); } - public UserModel getUser(UserModel user){ - return userRepository.getUser(user); + public UserModel getUser(String token) { + String authToken = token.substring(7); + String email = jwtService.extractUserEmail(authToken); + return userRepository.findByEmail(email).orElseThrow(() -> new RuntimeException("User not found")); } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/auth/AuthenticationService.java b/backend/src/main/java/fellowship/mealmaestro/services/auth/AuthenticationService.java new file mode 100644 index 00000000..9cdfb52f --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/auth/AuthenticationService.java @@ -0,0 +1,69 @@ +package fellowship.mealmaestro.services.auth; + +import java.util.Optional; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import fellowship.mealmaestro.models.UserModel; +import fellowship.mealmaestro.models.auth.AuthenticationRequestModel; +import fellowship.mealmaestro.models.auth.AuthenticationResponseModel; +import fellowship.mealmaestro.models.auth.AuthorityRoleModel; +import fellowship.mealmaestro.models.auth.RegisterRequestModel; +import fellowship.mealmaestro.repositories.UserRepository; + +@Service +public class AuthenticationService { + + private final UserRepository userRepository; + + private final PasswordEncoder passwordEncoder; + + private final JwtService jwtService; + + private final AuthenticationManager authenticationManager; + + public AuthenticationService(PasswordEncoder passwordEncoder, UserRepository userRepository, JwtService jwtService, AuthenticationManager authenticationManager){ + this.passwordEncoder = passwordEncoder; + this.userRepository = userRepository; + this.jwtService = jwtService; + this.authenticationManager = authenticationManager; + } + + public Optional register(RegisterRequestModel request){ + var user = new UserModel( + request.getUsername(), + passwordEncoder.encode(request.getPassword()), + request.getEmail(), + AuthorityRoleModel.USER + ); + + boolean userExists = userRepository.findByEmail(request.getEmail()).isPresent(); + + if(userExists){ + return Optional.empty(); + } + + userRepository.createUser(user); + + var jwt = jwtService.generateToken(user); + return Optional.of(new AuthenticationResponseModel(jwt)); + } + + public AuthenticationResponseModel authenticate(AuthenticationRequestModel request){ + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + request.getEmail(), + request.getPassword() + ) + ); + + var user = userRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new RuntimeException("User not found")); + + var jwt = jwtService.generateToken(user); + return new AuthenticationResponseModel(jwt); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/services/auth/JwtService.java b/backend/src/main/java/fellowship/mealmaestro/services/auth/JwtService.java new file mode 100644 index 00000000..e9eb0cb8 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/auth/JwtService.java @@ -0,0 +1,93 @@ +package fellowship.mealmaestro.services.auth; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import io.github.cdimascio.dotenv.Dotenv; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; + +@Service +public class JwtService { + + private static final String SIGNING_KEY; + + static { + String jwtSecret; + Dotenv dotenv; + if (System.getenv("JWT_SECRET") != null) { + jwtSecret = System.getenv("JWT_SECRET"); + } else { + try { + dotenv = Dotenv.load(); + jwtSecret = dotenv.get("JWT_SECRET"); + } catch (Exception e){ + dotenv = Dotenv.configure() + .ignoreIfMissing() + .load(); + jwtSecret = "No JWT Secret Found"; + } + } + SIGNING_KEY = jwtSecret; + } + + public String extractUserEmail(String token){ + return extractClaim(token, Claims::getSubject); + } + + public T extractClaim(String token, Function claimsResolver){ + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token){ + return Jwts + .parserBuilder() + .setSigningKey(getSignInKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + private Key getSignInKey() { + byte[] signingKeyBytes = Decoders.BASE64.decode(SIGNING_KEY); + return Keys.hmacShaKeyFor(signingKeyBytes); + } + + public String generateToken(UserDetails userDetails){ + return generateToken(new HashMap<>(), userDetails); + } + + public String generateToken(Map extraClaims, UserDetails userDetails){ + return Jwts + .builder() + .setClaims(extraClaims) + .setSubject(userDetails.getUsername()) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) + .signWith(getSignInKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public boolean isTokenValid(String token, UserDetails userDetails){ + final String userEmail = extractUserEmail(token); + return (userEmail.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + private boolean isTokenExpired(String token){ + return extractExpiration(token).before(new Date()); + } + + private Date extractExpiration(String token){ + return extractClaim(token, Claims::getExpiration); + } +} diff --git a/backend/src/test/java/fellowship/mealmaestro/MealmaestroApplicationTests.java b/backend/src/test/java/fellowship/mealmaestro/MealmaestroApplicationTests.java index 57f3d367..074d6fe7 100644 --- a/backend/src/test/java/fellowship/mealmaestro/MealmaestroApplicationTests.java +++ b/backend/src/test/java/fellowship/mealmaestro/MealmaestroApplicationTests.java @@ -2,8 +2,16 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; @SpringBootTest +@TestPropertySource(properties = { + "JWT_SECRET=secret", + "DB_URI=bolt://localhost:7687", + "DB_USERNAME=neo4j", + "DB_PASSWORD=password", + "OPENAI_API_KEY=secret" +}) class MealmaestroApplicationTests { @Test diff --git a/backend/src/test/java/fellowship/mealmaestro/controllers/HelloWorldControllerTest.java b/backend/src/test/java/fellowship/mealmaestro/controllers/HelloWorldControllerTest.java index 0fbf22e1..ccdfebed 100644 --- a/backend/src/test/java/fellowship/mealmaestro/controllers/HelloWorldControllerTest.java +++ b/backend/src/test/java/fellowship/mealmaestro/controllers/HelloWorldControllerTest.java @@ -11,10 +11,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; @SpringBootTest @AutoConfigureMockMvc +@TestPropertySource(properties = { + "JWT_SECRET=secret", + "DB_URI=bolt://localhost:7687", + "DB_USERNAME=neo4j", + "DB_PASSWORD=password" +}) public class HelloWorldControllerTest { @Autowired diff --git a/backend/src/test/java/fellowship/mealmaestro/controllers/PantryControllerTest.java b/backend/src/test/java/fellowship/mealmaestro/controllers/PantryControllerTest.java new file mode 100644 index 00000000..33663149 --- /dev/null +++ b/backend/src/test/java/fellowship/mealmaestro/controllers/PantryControllerTest.java @@ -0,0 +1,168 @@ +package fellowship.mealmaestro.controllers; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; + +import java.util.ArrayList; +import java.util.List; + + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; + +import fellowship.mealmaestro.models.FoodModel; +import fellowship.mealmaestro.services.PantryService; +import fellowship.mealmaestro.services.auth.JwtService; + +@SpringBootTest +@AutoConfigureMockMvc +@TestPropertySource(properties = { + "JWT_SECRET=secret", + "DB_URI=bolt://localhost:7687", + "DB_USERNAME=neo4j", + "DB_PASSWORD=password" +}) +public class PantryControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private PantryService pantryService; + + @MockBean + private static JwtService jwtService; + + + @Test + public void addToPantrySuccessTest() throws Exception { + FoodModel foodModel = new FoodModel("testFood", 2, 2); + + // When addToPantry method is called with any FoodModel and any String, it returns foodModel + when(pantryService.addToPantry(any(FoodModel.class), any(String.class))).thenReturn(foodModel); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/addToPantry") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer testToken..") + .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) + .andExpect(status().isOk()); + } + + @Test + public void addToPantryBadRequestTest() throws Exception { + FoodModel foodModel = new FoodModel("testFood", 2, 2); + + // When addToPantry method is called with any FoodModel and any String, it returns foodModel + when(pantryService.addToPantry(any(FoodModel.class), any(String.class))).thenReturn(foodModel); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/addToPantry") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) + .andExpect(status().isBadRequest()); + } + + @Test + public void removeFromPantrySuccessTest() throws Exception { + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/removeFromPantry") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer testToken..") + .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) + .andExpect(status().isOk()); + } + + @Test + public void removeFromPantryBadRequestTest() throws Exception { + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/removeFromPantry") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) + .andExpect(status().isBadRequest()); + } + + @Test + public void updatePantrySuccessTest() throws Exception { + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/updatePantry") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer testToken..") + .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) + .andExpect(status().isOk()); + } + + @Test + public void updatePantryBadRequestTest() throws Exception { + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/updatePantry") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) + .andExpect(status().isBadRequest()); + } + + @Test + public void getPantrySuccessTest() throws Exception { + List foodModelList = new ArrayList<>(); + when(pantryService.getPantry(any(String.class))).thenReturn(foodModelList); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/getPantry") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer testToken..")) + .andExpect(status().isOk()); + } + + @Test + public void getPantryBadRequestTest() throws Exception { + List foodModelList = new ArrayList<>(); + when(pantryService.getPantry(any(String.class))).thenReturn(foodModelList); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/getPantry") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } +} diff --git a/backend/src/test/java/fellowship/mealmaestro/controllers/ShoppingListControllerTest.java b/backend/src/test/java/fellowship/mealmaestro/controllers/ShoppingListControllerTest.java new file mode 100644 index 00000000..063a1abf --- /dev/null +++ b/backend/src/test/java/fellowship/mealmaestro/controllers/ShoppingListControllerTest.java @@ -0,0 +1,202 @@ +package fellowship.mealmaestro.controllers; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; + +import fellowship.mealmaestro.models.FoodModel; +import fellowship.mealmaestro.services.ShoppingListService; +import fellowship.mealmaestro.services.auth.JwtService; + +@SpringBootTest +@AutoConfigureMockMvc +@TestPropertySource(properties = { + "JWT_SECRET=secret", + "DB_URI=bolt://localhost:7687", + "DB_USERNAME=neo4j", + "DB_PASSWORD=password" +}) +public class ShoppingListControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ShoppingListService shoppingListService; + + @MockBean + private static JwtService jwtService; + + + @Test + public void addToShoppingListSuccessTest() throws Exception { + FoodModel foodModel = new FoodModel("testFood", 2, 2); + + // When addToShoppingList method is called with any FoodModel and any String, it returns foodModel + when(shoppingListService.addToShoppingList(any(FoodModel.class), any(String.class))).thenReturn(foodModel); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/addToShoppingList") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer testToken..") + .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) + .andExpect(status().isOk()); + } + + @Test + public void addToShoppingListBadRequestTest() throws Exception { + FoodModel foodModel = new FoodModel("testFood", 2, 2); + + // When addToShoppingList method is called with any FoodModel and any String, it returns foodModel + when(shoppingListService.addToShoppingList(any(FoodModel.class), any(String.class))).thenReturn(foodModel); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/addToShoppingList") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) + .andExpect(status().isBadRequest()); + } + + @Test + public void removeFromShoppingListSuccessTest() throws Exception { + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/removeFromShoppingList") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer testToken..") + .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) + .andExpect(status().isOk()); + } + + @Test + public void removeFromShoppingListBadRequestTest() throws Exception { + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/removeFromShoppingList") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) + .andExpect(status().isBadRequest()); + } + + @Test + public void updateShoppingListSuccessTest() throws Exception { + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/updateShoppingList") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer testToken..") + .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) + .andExpect(status().isOk()); + } + + @Test + public void updateShoppingListBadRequestTest() throws Exception { + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/updateShoppingList") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) + .andExpect(status().isBadRequest()); + } + + @Test + public void getShoppingListSuccessTest() throws Exception { + List foodModelList = new ArrayList<>(); + when(shoppingListService.getShoppingList(any(String.class))).thenReturn(foodModelList); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/getShoppingList") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer testToken..")) + .andExpect(status().isOk()); + } + + @Test + public void getShoppingListBadRequestTest() throws Exception { + List foodModelList = new ArrayList<>(); + when(shoppingListService.getShoppingList(any(String.class))).thenReturn(foodModelList); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/getShoppingList") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void buyItemSuccessTest() throws Exception { + List foodModelList = new ArrayList<>(); + when(shoppingListService.buyItem(any(FoodModel.class), any(String.class))).thenReturn(foodModelList); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/buyItem") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer testToken..") + .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) + .andExpect(status().isOk()); + } + + @Test + public void buyItemBadRequestTest() throws Exception { + List foodModelList = new ArrayList<>(); + when(shoppingListService.buyItem(any(FoodModel.class), any(String.class))).thenReturn(foodModelList); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/buyItem") + .with(user("user")) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"testFood\",\"quantity\":2,\"weight\":2}")) + .andExpect(status().isBadRequest()); + } + +} diff --git a/backend/src/test/java/fellowship/mealmaestro/controllers/UserControllerTest.java b/backend/src/test/java/fellowship/mealmaestro/controllers/UserControllerTest.java new file mode 100644 index 00000000..0c00d4de --- /dev/null +++ b/backend/src/test/java/fellowship/mealmaestro/controllers/UserControllerTest.java @@ -0,0 +1,128 @@ +package fellowship.mealmaestro.controllers; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Optional; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; + +import fellowship.mealmaestro.models.UserModel; +import fellowship.mealmaestro.models.auth.AuthenticationRequestModel; +import fellowship.mealmaestro.models.auth.AuthenticationResponseModel; +import fellowship.mealmaestro.models.auth.RegisterRequestModel; +import fellowship.mealmaestro.services.UserService; +import fellowship.mealmaestro.services.auth.AuthenticationService; +import fellowship.mealmaestro.services.auth.JwtService; + +@SpringBootTest +@AutoConfigureMockMvc +@TestPropertySource(properties = { + "JWT_SECRET=secret", + "DB_URI=bolt://localhost:7687", + "DB_USERNAME=neo4j", + "DB_PASSWORD=password" +}) +public class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService userService; + + @MockBean + private AuthenticationService authenticationService; + + @MockBean + private static JwtService jwtService; + + + @Test + public void findByEmailSuccessTest() throws Exception { + UserModel userModel = new UserModel(); + + when(userService.findByEmail(any(String.class))).thenReturn(java.util.Optional.of(userModel)); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/findByEmail") + .with(user("user")) + .contentType("application/json") + .header("Authorization", "Bearer testToken..") + .content("{\"name\":\"username\",\"password\":\"password\",\"email\":\"email\"}")) + .andExpect(status().isOk()); + } + + @Test + public void findByEmailFailureTest() throws Exception { + + when(userService.findByEmail(any(String.class))).thenReturn(Optional.empty()); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/findByEmail") + .with(user("user")) + .contentType("application/json") + .header("Authorization", "Bearer testToken..") + .content("{\"name\":\"username\",\"password\":\"password\",\"email\":\"email\"}")) + .andExpect(status().isNotFound()); + } + + @Test + public void registerSuccessTest() throws Exception { + when(authenticationService.register(any(RegisterRequestModel.class))).thenReturn(Optional.of(new AuthenticationResponseModel("token"))); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/register") + .contentType("application/json") + .content("{\"username\":\"username\",\"email\":\"email\",\"password\":\"password\"}")) + .andExpect(status().isOk()); + } + + @Test + public void registerFailureTest() throws Exception { + when(authenticationService.register(any(RegisterRequestModel.class))).thenReturn(Optional.empty()); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/register") + .contentType("application/json") + .content("{\"username\":\"username\",\"email\":\"email\",\"password\":\"password\"}")) + .andExpect(status().isBadRequest()); + } + + @Test + public void authenticateSuccessTest() throws Exception { + when(authenticationService.authenticate(any(AuthenticationRequestModel.class))).thenReturn(new AuthenticationResponseModel("token")); + + when(jwtService.extractUserEmail(any(String.class))).thenReturn("test@test.com"); + when(jwtService.generateToken(any(UserDetails.class))).thenReturn("testToken.."); + when(jwtService.isTokenValid(any(String.class), any(UserDetails.class))).thenReturn(true); + + mockMvc.perform(post("/authenticate") + .contentType("application/json") + .content("{\"email\":\"email\",\"password\":\"password\"}")) + .andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index a0675c72..6e6773d2 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -13,4 +13,12 @@ export class AppComponent { public environmentInjector = inject(EnvironmentInjector); constructor() {} + + //const url = 'http://localhost:7867/removeFromPantry + // const body = { + // id : 'pantryItemId' + // } + + // this.http.post(url, body) + } diff --git a/frontend/src/app/components/browse-meals/browse-meals.component.html b/frontend/src/app/components/browse-meals/browse-meals.component.html index f3500822..847786b8 100644 --- a/frontend/src/app/components/browse-meals/browse-meals.component.html +++ b/frontend/src/app/components/browse-meals/browse-meals.component.html @@ -1,32 +1,121 @@ - + + + + + + + + + + {{mealsData.name}} + + + {{mealsData.description}} + + + + {{mealsData.image}} + + + + + + + + {{mealsData.name}} + + Close + + + + + + + {{mealsData.image}} + + + +

{{mealsData.ingredients}}

+

{{mealsData.instructions}}

+

{{mealsData.cookingTime}}

+
+
+
+
diff --git a/frontend/src/app/components/browse-meals/browse-meals.component.spec.ts b/frontend/src/app/components/browse-meals/browse-meals.component.spec.ts index 685ea7e2..864c8bcf 100644 --- a/frontend/src/app/components/browse-meals/browse-meals.component.spec.ts +++ b/frontend/src/app/components/browse-meals/browse-meals.component.spec.ts @@ -2,18 +2,31 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { IonicModule } from '@ionic/angular'; import { BrowseMealsComponent } from './browse-meals.component'; +import { MealI } from '../../models/interfaces'; describe('BrowseMealsComponent', () => { let component: BrowseMealsComponent; let fixture: ComponentFixture; + let mockMeal: MealI; beforeEach(waitForAsync(() => { + mockMeal = { + name: 'test', + description: 'test', + image: 'test', + ingredients: 'test', + instructions: 'test', + cookingTime: 'test', + }; + TestBed.configureTestingModule({ imports: [IonicModule.forRoot(), BrowseMealsComponent] }).compileComponents(); fixture = TestBed.createComponent(BrowseMealsComponent); component = fixture.componentInstance; + component.mealsData = mockMeal; + component.searchData = mockMeal; fixture.detectChanges(); })); diff --git a/frontend/src/app/components/browse-meals/browse-meals.component.ts b/frontend/src/app/components/browse-meals/browse-meals.component.ts index 5c5761cc..f6c9e4de 100644 --- a/frontend/src/app/components/browse-meals/browse-meals.component.ts +++ b/frontend/src/app/components/browse-meals/browse-meals.component.ts @@ -1,9 +1,8 @@ import { CommonModule } from '@angular/common'; import { Component, OnInit, Input } from '@angular/core'; import { IonicModule } from '@ionic/angular'; -import { MealBrowseI } from '../../models/mealBrowse.model'; -import { BrowsePage } from '../../pages/browse/browse.page'; -import { MealI } from '../../models/meal.model'; +import { MealI } from '../../models/interfaces'; + @Component({ selector: 'app-browse-meals', templateUrl: './browse-meals.component.html', @@ -11,22 +10,33 @@ import { MealI } from '../../models/meal.model'; standalone:true, imports: [CommonModule, IonicModule], }) + export class BrowseMealsComponent implements OnInit { + @Input() mealsData!: MealI; + @Input() searchData!: MealI; + @Input() Searched: boolean = false; - @Input() meal!: MealI; + item: MealI | undefined; + popularMeals: MealI[] = []; + thing: MealI | undefined; + searchedMeals: MealI[] = []; isModalOpen = false; - currentObject :any - setOpen(isOpen: boolean, o :any) { - if(o==null) - o = this.currentObject - this.isModalOpen = isOpen; - this.setCurrent(o) - } + currentObject: any; + constructor() { } - ngOnInit() {} + ngOnInit() { + // console.log(this.mealsData); + } setCurrent(o : any) { this.currentObject = o; } + + setOpen(isOpen: boolean, o :any) { + if(o==null) + o = this.currentObject + this.isModalOpen = isOpen; + this.setCurrent(o) + } } diff --git a/frontend/src/app/components/daily-meals/daily-meals.component.html b/frontend/src/app/components/daily-meals/daily-meals.component.html index b517a436..dfbdff7f 100644 --- a/frontend/src/app/components/daily-meals/daily-meals.component.html +++ b/frontend/src/app/components/daily-meals/daily-meals.component.html @@ -1,115 +1,200 @@ -Daily plan + +
+
+ + + {{ dayData?.mealDate }} + + + +
- - - - -
- - Breakfast - {{ dayData.breakfast.name }} - - - {{ dayData.breakfast.description }} - - - https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 - + + + + +
+ + Breakfast + {{ dayData?.breakfast?.name }} + + + + {{ dayData?.breakfast?.description }} + + - + + + https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 + +
+ + - {{ dayData.breakfast.name }} + {{ dayData?.breakfast?.name }} - Close + Close - https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 + https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 -

{{ dayData.breakfast.ingredients }}

-

{{ dayData.breakfast.instructions }}

-

{{ dayData.breakfast.cookingTime }}

+

Ingredients:

+

{{ dayData?.breakfast?.ingredients }}

+

Instructions:

+

{{ dayData?.breakfast?.instructions }}

+

Cooking Time:

+

{{ dayData?.breakfast?.cookingTime }}

-
-
- - - -
- - Lunch - {{ dayData.lunch.name }} - - {{ dayData.lunch.description }} - - https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 - + + + + + + + + + - + + + + + + +
+ + Lunch + {{ dayData?.lunch?.name }} + + + + {{ dayData?.lunch?.description }} + + + + + + https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 + +
+ + + + - {{ dayData.lunch.name }} + {{ dayData?.lunch?.name }} - Close + Close - https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 + https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 -

{{ dayData.lunch.ingredients }}

-

{{ dayData.lunch.instructions }}

-

{{ dayData.lunch.cookingTime }}

+

Ingredients:

+

{{ dayData?.lunch?.ingredients }}

+

Instructions:

+

{{ dayData?.lunch?.instructions }}

+

Cooking time:

+

{{ dayData?.lunch?.cookingTime }}

-
-
- - - -
- - Dinner - {{ dayData.dinner.name }} - + + + + + + + + + + + + + + + +
+ + Dinner + {{ dayData?.dinner?.name }} + + + + {{ dayData?.dinner?.description }} + + - - {{ dayData.dinner.description }} - - - https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 - + + + https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 + +
- + + + - {{ dayData.dinner.name }} + {{ dayData?.dinner?.name }} - Close + Close - https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 + https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 -

{{ dayData.dinner.ingredients }}

-

{{ dayData.dinner.instructions }}

-

{{ dayData.dinner.cookingTime }}

+

Ingredients:

+

{{ dayData?.dinner?.ingredients }}

+

Instructions:

+

{{ dayData?.dinner?.instructions }}

+

Cooking Time:

+

{{ dayData?.dinner?.cookingTime }}

-
-
- + + + + + + + + + + + diff --git a/frontend/src/app/components/daily-meals/daily-meals.component.scss b/frontend/src/app/components/daily-meals/daily-meals.component.scss index 9dc38234..0cf33f5f 100644 --- a/frontend/src/app/components/daily-meals/daily-meals.component.scss +++ b/frontend/src/app/components/daily-meals/daily-meals.component.scss @@ -1,5 +1,55 @@ ion-avatar { - width: auto; - height: 15vh; - --border-radius:5%; -} \ No newline at end of file + width: 100%; + height: 15vh; + --border-radius: 0%; + +} +ion-item-sliding { + + --ion-padding: 0px; + } + +ion-item { + width: 100%; + display: block; + --ion-padding: 0px; + + + + +} +ion-card { + padding: 0%; + --ion-padding: 0px; +} + +.div1 img{ + width: 100%; + height: 100%; + object-fit: cover; + padding-right: 0px !important; +} + +.no-style { + --padding-start:0; + --padding-end: 0; + padding-right: 0%; + +} +.side { + display: inline; +} + +.label-container { + display: flex; + align-items: center; +} + +.line { + flex-grow: 1; + + height: 1px; + background-color: var(--ion-color-primary); + margin-left: 8px; + margin-right: 8px; +} diff --git a/frontend/src/app/components/daily-meals/daily-meals.component.spec.ts b/frontend/src/app/components/daily-meals/daily-meals.component.spec.ts index bb69f51e..9cf4f4b6 100644 --- a/frontend/src/app/components/daily-meals/daily-meals.component.spec.ts +++ b/frontend/src/app/components/daily-meals/daily-meals.component.spec.ts @@ -2,14 +2,21 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { IonicModule } from '@ionic/angular'; import { DailyMealsComponent } from './daily-meals.component'; +import { MealGenerationService } from '../../services/meal-generation/meal-generation.service'; describe('DailyMealsComponent', () => { let component: DailyMealsComponent; let fixture: ComponentFixture; + let mockMealGenerationService: jasmine.SpyObj; beforeEach(waitForAsync(() => { + mockMealGenerationService = jasmine.createSpyObj('MealGenerationService', ['getDailyMeals']); + TestBed.configureTestingModule({ - imports: [IonicModule.forRoot(), DailyMealsComponent] + imports: [IonicModule.forRoot(), DailyMealsComponent], + providers: [ + { provide: MealGenerationService, useValue: mockMealGenerationService } + ] }).compileComponents(); fixture = TestBed.createComponent(DailyMealsComponent); diff --git a/frontend/src/app/components/daily-meals/daily-meals.component.ts b/frontend/src/app/components/daily-meals/daily-meals.component.ts index ddfa668c..1a50fd1e 100644 --- a/frontend/src/app/components/daily-meals/daily-meals.component.ts +++ b/frontend/src/app/components/daily-meals/daily-meals.component.ts @@ -1,11 +1,12 @@ import { CommonModule } from '@angular/common'; import { Component, OnInit, Input } from '@angular/core'; -import { IonicModule } from '@ionic/angular'; -import { MealI } from '../../models/meal.model'; +import { IonicModule, IonicSlides } from '@ionic/angular'; import { Router } from '@angular/router'; import { MealGenerationService } from '../../services/meal-generation/meal-generation.service'; import { DaysMealsI } from '../../models/daysMeals.model'; import { ErrorHandlerService } from '../../services/services'; +import { MealI, RecipeItemI } from '../../models/interfaces'; +import { AddRecipeService } from '../../services/recipe-book/add-recipe.service'; @Component({ selector: 'app-daily-meals', @@ -13,24 +14,45 @@ import { ErrorHandlerService } from '../../services/services'; styleUrls: ['./daily-meals.component.scss'], standalone : true, imports: [CommonModule, IonicModule], - }) -export class DailyMealsComponent { +export class DailyMealsComponent implements OnInit { + breakfast: string = "breakfast"; + lunch: string = "lunch"; + dinner: string = "dinner"; + mealDate: string | undefined; @Input() todayData!: MealI[]; @Input() dayData!: DaysMealsI; item: DaysMealsI | undefined; daysMeals: DaysMealsI[] = [] ; meals:MealI[] = []; + isBreakfastModalOpen = false; + isLunchModalOpen = false; + isDinnerModalOpen = false; isModalOpen = false; - currentObject :any - setOpen(isOpen: boolean, o :any) { - if(o==null) - o = this.currentObject - this.isModalOpen = isOpen; - this.setCurrent(o) + currentObject :DaysMealsI | undefined + setOpen(isOpen: boolean, mealType: string) { + if (mealType === 'breakfast') { + this.isBreakfastModalOpen = isOpen; + if (isOpen) { + this.setCurrent(this.dayData?.breakfast); + } + } else if (mealType === 'lunch') { + this.isLunchModalOpen = isOpen; + if (isOpen) { + this.setCurrent(this.dayData?.lunch); + } + } else if (mealType === 'dinner') { + this.isDinnerModalOpen = isOpen; + if (isOpen) { + this.setCurrent(this.dayData?.dinner); + } + } } - constructor() {} + constructor(public r : Router + , private mealGenerationservice:MealGenerationService + , private errorHandlerService:ErrorHandlerService, + private addService: AddRecipeService) {} ngOnInit() { // this.mealGenerationservice.getDailyMeals().subscribe({ @@ -48,6 +70,38 @@ export class DailyMealsComponent { } + handleArchive(meal:string) { + // Function to handle the "Archive" option action + var recipe: MealI | undefined; + + if (meal == "breakfast") + recipe = this.dayData.breakfast; + else if (meal == "lunch") + recipe = this.dayData.lunch; + else recipe = this.dayData.dinner; + + this.addService.setRecipeItem(recipe); + } + + async handleSync(meal:string) { + // Function to handle the "Sync" option action + console.log('Sync option clicked'); + // Add your custom logic here + this.mealGenerationservice.handleArchive(this.dayData, meal).subscribe({ + next: (data) => { + data.mealDate = this.dayData.mealDate; + this.dayData = data; + + console.log(this.meals); + }, + error: (err) => { + this.errorHandlerService.presentErrorToast( + 'Error regenerating meal items', err + ) + } + }) + } + setCurrent(o : any) { this.currentObject = o; } diff --git a/frontend/src/app/components/food-list-item/food-list-item.component.html b/frontend/src/app/components/food-list-item/food-list-item.component.html index f3efee72..37c9fa5f 100644 --- a/frontend/src/app/components/food-list-item/food-list-item.component.html +++ b/frontend/src/app/components/food-list-item/food-list-item.component.html @@ -1,22 +1,30 @@ - - - - + + + + + - + {{ item.name }} - + {{ item.quantity }} + + {{ item.weight }}g + - - - + + + + + + \ No newline at end of file diff --git a/frontend/src/app/components/food-list-item/food-list-item.component.scss b/frontend/src/app/components/food-list-item/food-list-item.component.scss index e69de29b..8e353719 100644 --- a/frontend/src/app/components/food-list-item/food-list-item.component.scss +++ b/frontend/src/app/components/food-list-item/food-list-item.component.scss @@ -0,0 +1,8 @@ +.low-quantity{ + --color: red; +} + +.low-weight{ + --color: red; +} + diff --git a/frontend/src/app/components/food-list-item/food-list-item.component.spec.ts b/frontend/src/app/components/food-list-item/food-list-item.component.spec.ts index cfcaf934..b7d9fe3c 100644 --- a/frontend/src/app/components/food-list-item/food-list-item.component.spec.ts +++ b/frontend/src/app/components/food-list-item/food-list-item.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; import { ActionSheetController, IonicModule, PickerController } from '@ionic/angular'; import { FoodListItemComponent } from './food-list-item.component'; @@ -6,6 +6,8 @@ import { PantryApiService } from '../../services/pantry-api/pantry-api.service'; import { ShoppingListApiService } from '../../services/shopping-list-api/shopping-list-api.service'; import { FoodItemI } from '../../models/interfaces'; import { of } from 'rxjs'; +import { HttpResponse } from '@angular/common/http'; +import { ElementRef } from '@angular/core'; describe('FoodListItemComponent', () => { let component: FoodListItemComponent; @@ -16,6 +18,8 @@ describe('FoodListItemComponent', () => { let mockPickerController: jasmine.SpyObj; let mockItem: FoodItemI; + const mockElementRef = new ElementRef({ nativeElement: document.createElement('div') }); + beforeEach(waitForAsync(() => { mockPantryService = jasmine.createSpyObj('PantryApiService', ['updatePantryItem']); mockShoppingListService = jasmine.createSpyObj('ShoppingListApiService', ['updateShoppingListItem']); @@ -27,14 +31,10 @@ describe('FoodListItemComponent', () => { weight: 1, }; - const emptyFoodItem: FoodItemI = { - name: '', - quantity: null, - weight: null, - }; + const emptyResponse = new HttpResponse({ body: null, status: 200 }); - mockPantryService.updatePantryItem.and.returnValue(of(emptyFoodItem)); - mockShoppingListService.updateShoppingListItem.and.returnValue(of(emptyFoodItem)); + mockPantryService.updatePantryItem.and.returnValue(of(emptyResponse)); + mockShoppingListService.updateShoppingListItem.and.returnValue(of(emptyResponse)); mockActionSheetController.create.calls.reset(); TestBed.configureTestingModule({ @@ -44,12 +44,15 @@ describe('FoodListItemComponent', () => { { provide: ShoppingListApiService, useValue: mockShoppingListService }, { provide: ActionSheetController, useValue: mockActionSheetController }, { provide: PickerController, useValue: mockPickerController }, + { provide: ElementRef, useValue: mockElementRef }, ], }).compileComponents(); fixture = TestBed.createComponent(FoodListItemComponent); component = fixture.componentInstance; component.item = mockItem; + component.segment = 'pantry'; + component.isVisible = true; fixture.detectChanges(); })); @@ -57,83 +60,90 @@ describe('FoodListItemComponent', () => { expect(component).toBeTruthy(); }); - // it('should inject ActionSheetController', () => { - // expect(mockActionSheetController).toBeDefined(); - // }); + it('should inject ActionSheetController', () => { + expect(mockActionSheetController).toBeDefined(); + }); // describe('openDeleteSheet', () => { - // it('should call actionSheetController.create', () => { - // component.openDeleteSheet(); - // expect(mockActionSheetController.create).toHaveBeenCalled(); - // }); - - // it('should call actionSheetController.create with correct arguments', () => { - // component.openDeleteSheet(); - // expect(mockActionSheetController.create).toHaveBeenCalledWith({ - // header: 'Are you sure?', - // buttons: [ - // { - // text: 'Delete', - // role: 'destructive', - // data: { - // name: mockItem.name, - // quantity: mockItem.quantity, - // weight: mockItem.weight, - // }, - // }, - // { - // text: 'Cancel', - // role: 'cancel', - // data: { - // action: 'cancel', - // }, - // }, - // ], - // }); - // }); - - // it('should present the action sheet', async () => { - // const mockActionSheet = { - // present: jasmine.createSpy('present'), - // onDidDismiss: () => Promise.resolve({ role: 'cancel', data: mockItem }), - // }; - // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet)); - // await component.openDeleteSheet(); - // expect(mockActionSheet.present).toHaveBeenCalled(); - // }); - - // it('should call emit itemDeleted when role is destructive', async () => { - // const mockActionSheet = { - // present: jasmine.createSpy('present'), - // onDidDismiss: () => Promise.resolve({ role: 'destructive', data: mockItem }), - // }; - // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet)); - // spyOn(component.itemDeleted, 'emit'); - // await component.openDeleteSheet(); - // expect(component.itemDeleted.emit).toHaveBeenCalledWith(mockItem); - // }); - - // it('should not call emit itemDeleted when role is cancel', async () => { - // const mockActionSheet = { - // present: jasmine.createSpy('present'), - // onDidDismiss: () => Promise.resolve({ role: 'cancel', data: mockItem }), - // }; - // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet)); - // spyOn(component.itemDeleted, 'emit'); - // await component.openDeleteSheet(); - // expect(component.itemDeleted.emit).not.toHaveBeenCalled(); - // }); - - // it('should call closeItem when role is cancel', async () => { - // const mockActionSheet = { - // present: jasmine.createSpy('present'), - // onDidDismiss: () => Promise.resolve({ role: 'cancel', data: mockItem }), - // }; - // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet)); - // spyOn(component, 'closeItem'); - // await component.openDeleteSheet(); - // expect(component.closeItem).toHaveBeenCalled(); - // }); + // it('should call actionSheetController.create', () => { + // component.openDeleteSheet(); + // expect(mockActionSheetController.create).toHaveBeenCalled(); + // }); + + // it('should call actionSheetController.create with correct arguments', () => { + // component.openDeleteSheet(); + // expect(mockActionSheetController.create).toHaveBeenCalledWith({ + // header: 'Are you sure?', + // buttons: [ + // { + // text: 'Delete', + // role: 'destructive', + // data: { + // name: mockItem.name, + // quantity: mockItem.quantity, + // weight: mockItem.weight, + // }, + // }, + // { + // text: 'Cancel', + // role: 'cancel', + // data: { + // action: 'cancel', + // }, + // }, + // ], + // }); + // }); + + // it('should present the action sheet', fakeAsync (() => { + // const mockActionSheet = { + // present: jasmine.createSpy('present').and.returnValue(Promise.resolve()), + // onDidDismiss: () => jasmine.createSpy('onDidDismiss').and.returnValue(Promise.resolve({ role: 'cancel', data: mockItem })), + // }; + // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet)); + + // component.openDeleteSheet(); + // tick(); + + // expect(mockActionSheet.present).toHaveBeenCalled(); + // })); + + // it('should call emit itemDeleted when role is destructive', fakeAsync (() => { + // const mockActionSheet = { + // present: jasmine.createSpy('present').and.returnValue(Promise.resolve()), + // onDidDismiss: () => jasmine.createSpy('onDidDismiss').and.returnValue(Promise.resolve({ role: 'destructive', data: mockItem })) + // }; + // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet)); + + // spyOn(component.itemDeleted, 'emit'); + // component.openDeleteSheet(); + // tick(); + // // expect(component.itemDeleted.emit); + // })); + + // it('should not call emit itemDeleted when role is cancel', fakeAsync (() => { + // const mockActionSheet = { + // present: jasmine.createSpy('present').and.returnValue(Promise.resolve()), + // onDidDismiss: () => jasmine.createSpy('onDidDismiss').and.returnValue(Promise.resolve({ role: 'cancel', data: mockItem })) + // }; + // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet)); + + // spyOn(component.itemDeleted, 'emit'); + // component.openDeleteSheet(); + // tick(); + // expect(component.itemDeleted.emit).not.toHaveBeenCalled(); + // })); + + // it('should call closeItem when role is cancel', async () => { + // const mockActionSheet = { + // present: jasmine.createSpy('present').and.returnValue(Promise.resolve()), + // onDidDismiss: () => Promise.resolve({ role: 'cancel', data: mockItem }), + // }; + // mockActionSheetController.create.and.returnValue(Promise.resolve(mockActionSheet)); + // spyOn(component, 'closeItem'); + // await component.openDeleteSheet(); + // expect(component.closeItem).toHaveBeenCalled(); + // }); // }); }); diff --git a/frontend/src/app/components/food-list-item/food-list-item.component.ts b/frontend/src/app/components/food-list-item/food-list-item.component.ts index 9595984e..7dcb5f2e 100644 --- a/frontend/src/app/components/food-list-item/food-list-item.component.ts +++ b/frontend/src/app/components/food-list-item/food-list-item.component.ts @@ -1,28 +1,72 @@ -import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; -import { ActionSheetController, IonItemSliding, IonicModule, PickerController } from '@ionic/angular'; +import { AfterViewInit, Component, ElementRef, EventEmitter, Input, NgZone, Output, ViewChild } from '@angular/core'; +import { ActionSheetController, Animation, AnimationController, IonItemSliding, IonicModule, PickerController } from '@ionic/angular'; import { FoodItemI } from '../../models/interfaces'; import { ErrorHandlerService, PantryApiService, ShoppingListApiService } from '../../services/services'; +import { CommonModule } from '@angular/common'; @Component({ selector: 'app-food-list-item', templateUrl: './food-list-item.component.html', styleUrls: ['./food-list-item.component.scss'], standalone: true, - imports: [IonicModule], + imports: [IonicModule, CommonModule], }) -export class FoodListItemComponent implements OnInit { +export class FoodListItemComponent implements AfterViewInit { @Input() item! : FoodItemI; @Input() segment! : 'pantry' | 'shopping'; + @Input() isVisible! : boolean; @Output() itemDeleted: EventEmitter = new EventEmitter(); + @Output() itemBought: EventEmitter = new EventEmitter(); @ViewChild(IonItemSliding, { static: false }) slidingItem!: IonItemSliding; + @ViewChild(IonItemSliding, { read: ElementRef }) slidingItemRef!: ElementRef; + + private buyAnimation!: Animation; + private deleteAnimation!: Animation; + private boughtItem?: FoodItemI; + private deletedItem?: FoodItemI; constructor(private pantryService : PantryApiService, private actionSheetController: ActionSheetController, private pickerController: PickerController, private shoppingListService: ShoppingListApiService, - private errorHandlerService: ErrorHandlerService) { } + private errorHandlerService: ErrorHandlerService, + private animationCtrl: AnimationController, + private ngZone: NgZone) { } + + ngAfterViewInit() { + this.buyAnimation = this.animationCtrl + .create() + .addElement(this.slidingItemRef.nativeElement) + .duration(200) + .iterations(1) + .keyframes([ + { offset: 0, transform: 'translateX(0px)' }, + { offset: 0.4, transform: 'translateX(10%)' }, + { offset: 1, transform: 'translateX(-100%)' }, + ]) + .onFinish(() => { + this.ngZone.run(() => { + this.itemBought.emit(this.boughtItem); + }); + }); - ngOnInit() {} + + this.deleteAnimation = this.animationCtrl + .create() + .addElement(this.slidingItemRef.nativeElement) + .duration(200) + .iterations(1) + .keyframes([ + { offset: 0, transform: 'translateX(0px)' }, + { offset: 0.4, transform: 'translateX(-10%)' }, + { offset: 1, transform: 'translateX(100%)' }, + ]) + .onFinish(() => { + this.ngZone.run(() => { + this.itemDeleted.emit(this.deletedItem); + }); + }); + } async openDeleteSheet() { const actionSheet = await this.actionSheetController.create({ @@ -46,24 +90,70 @@ export class FoodListItemComponent implements OnInit { }, ], }); - console.log("actionSheet: ", actionSheet); await actionSheet.present(); const { data, role } = await actionSheet.onDidDismiss(); if (role === 'destructive') { this.closeItem(); - this.itemDeleted.emit(data) + this.deletedItem = data; + this.deleteAnimation.play(); }else if(role === 'cancel'){ this.closeItem(); } } - async openEditPicker(){ + async openAddToPantrySheet(){ + if(this.segment === 'pantry'){ + return; + } + + const actionSheet = await this.actionSheetController.create({ + header: 'Add to Pantry?', + buttons: [ + { + text: 'Add', + role: 'destructive', + data: { + name: this.item.name, + quantity: this.item.quantity, + weight: this.item.weight, + }, + }, + { + text: 'Cancel', + role: 'cancel', + data: { + action: 'cancel', + }, + }, + ] + }); + await actionSheet.present(); + + const { data, role } = await actionSheet.onDidDismiss(); + if (role === 'destructive') { + this.closeItem(); + this.boughtItem = data; + this.buyAnimation.play(); + // this.itemBought.emit(data); + }else if(role === 'cancel'){ + this.closeItem(); + } + } + + async choosePicker(){ + if (this.item.quantity !== 0 && this.item.quantity !== null){ + this.openQuantityPicker(); + } + else if (this.item.weight !== 0 && this.item.weight !== null){ + this.openWeightPicker(); + } + } + + async openQuantityPicker(){ const quantityOptions = []; - const weightOptions = []; let quantitySelectedIndex = 0; - let weightSelectedIndex = 0; for(let i = 1; i <= 100; i++){ quantityOptions.push({ @@ -71,18 +161,9 @@ export class FoodListItemComponent implements OnInit { value: i }); - weightOptions.push({ - text: String(i), - value: i - }); - if(i === this.item.quantity) { quantitySelectedIndex = i - 1; } - - if(i === this.item.weight) { - weightSelectedIndex = i - 1; - } } const picker = await this.pickerController.create({ @@ -92,6 +173,83 @@ export class FoodListItemComponent implements OnInit { options: quantityOptions, selectedIndex: quantitySelectedIndex, }, + ], + buttons: [ + { + text: 'Cancel', + role: 'cancel', + handler: () => { + this.closeItem(); + } + }, + { + text: 'Confirm', + handler: (value) => { + const updatedItem: FoodItemI = { + name: this.item.name, + quantity: value.quantity.value, + weight: 0, + } + if(this.segment === 'pantry') { + this.pantryService.updatePantryItem(updatedItem).subscribe({ + next: (response) => { + if (response.status === 200) { + this.item.quantity = value.quantity.value; + this.item.weight = 0; + this.closeItem(); + } + }, + error: (err) => { + if (err.status === 403){ + this.errorHandlerService.presentErrorToast('Unauthorized access. Please login again.', err); + } else { + this.errorHandlerService.presentErrorToast('Error updating item', err); + } + } + }); + } else if (this.segment === 'shopping') { + this.shoppingListService.updateShoppingListItem(updatedItem).subscribe({ + next: (response) => { + if (response.status === 200) { + this.item.quantity = value.quantity.value; + this.item.weight = value.weight.value; + this.closeItem(); + } + }, + error: (err) => { + if (err.status === 403){ + this.errorHandlerService.presentErrorToast('Unauthorized access. Please login again.', err); + } else { + this.errorHandlerService.presentErrorToast('Error updating item', err); + } + } + }); + } + }, + }, + ], + backdropDismiss: true, + }); + await picker.present(); + } + + async openWeightPicker(){ + const weightOptions = []; + + let weightSelectedIndex = 0; + + for(let i = 1; i <= 200; i++){ + weightOptions.push({ + text: String(i*10)+'g', + value: i*10 + }); + + if(i*10 === this.item.weight) { + weightSelectedIndex = i - 1; + } + } + const picker = await this.pickerController.create({ + columns: [ { name: 'weight', options: weightOptions, @@ -111,29 +269,41 @@ export class FoodListItemComponent implements OnInit { handler: (value) => { const updatedItem: FoodItemI = { name: this.item.name, - quantity: value.quantity.value, + quantity: 0, weight: value.weight.value, } if(this.segment === 'pantry') { this.pantryService.updatePantryItem(updatedItem).subscribe({ - next: () => { - this.item.quantity = value.quantity.value; - this.item.weight = value.weight.value; - this.closeItem(); + next: (response) => { + if (response.status === 200) { + this.item.quantity = 0; + this.item.weight = value.weight.value; + this.closeItem(); + } }, error: (err) => { - this.errorHandlerService.presentErrorToast('Error updating item', err); + if (err.status === 403){ + this.errorHandlerService.presentErrorToast('Unauthorized access. Please login again.', err); + } else { + this.errorHandlerService.presentErrorToast('Error updating item', err); + } } }); } else if (this.segment === 'shopping') { this.shoppingListService.updateShoppingListItem(updatedItem).subscribe({ - next: () => { - this.item.quantity = value.quantity.value; - this.item.weight = value.weight.value; - this.closeItem(); + next: (response) => { + if (response.status === 200) { + this.item.quantity = value.quantity.value; + this.item.weight = value.weight.value; + this.closeItem(); + } }, error: (err) => { - this.errorHandlerService.presentErrorToast('Error updating item', err); + if (err.status === 403){ + this.errorHandlerService.presentErrorToast('Unauthorized access. Please login again.', err); + } else { + this.errorHandlerService.presentErrorToast('Error updating item', err); + } } }); } diff --git a/frontend/src/app/components/recipe-details/recipe-details.component.html b/frontend/src/app/components/recipe-details/recipe-details.component.html new file mode 100644 index 00000000..70a4ca40 --- /dev/null +++ b/frontend/src/app/components/recipe-details/recipe-details.component.html @@ -0,0 +1,27 @@ + + + {{ item.name }} + + Close + + + + + + + + +
+ + + Save to Recipe Book + +
+

{{ item.description }}

+

Preparation Time

+

{{ item.cookingTime }}

+

Ingredients

+

{{ item.ingredients }}

+

Instructions

+

{{ item.instructions }}

+
\ No newline at end of file diff --git a/frontend/src/app/components/recipe-details/recipe-details.component.scss b/frontend/src/app/components/recipe-details/recipe-details.component.scss new file mode 100644 index 00000000..3364de14 --- /dev/null +++ b/frontend/src/app/components/recipe-details/recipe-details.component.scss @@ -0,0 +1,21 @@ +.savebutton { + color: black; + position: fixed; + right: 5px; + padding-top: 5px; + text-transform: capitalize; +} + +ion-avatar { + height: 20vh; + width: auto; + --border-radius: 2%; +} + +p { + padding-left: 5vw; +} + +ion-content { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +} \ No newline at end of file diff --git a/frontend/src/app/components/recipe-details/recipe-details.component.spec.ts b/frontend/src/app/components/recipe-details/recipe-details.component.spec.ts new file mode 100644 index 00000000..1eca26fe --- /dev/null +++ b/frontend/src/app/components/recipe-details/recipe-details.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { IonicModule } from '@ionic/angular'; + +import { RecipeDetailsComponent } from './recipe-details.component'; +import { MealI } from '../../models/meal.model'; + +describe('RecipeDetailsComponent', () => { + let component: RecipeDetailsComponent; + let fixture: ComponentFixture; + let mockItem: MealI; + let mockItems: MealI[]; + + beforeEach(waitForAsync(() => { + mockItem = { + name: 'test', + description: 'test', + ingredients: 'test', + instructions: 'test', + image: 'test', + cookingTime: 'test', + }; + + mockItems = [mockItem]; + + TestBed.configureTestingModule({ + imports: [IonicModule.forRoot(), RecipeDetailsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RecipeDetailsComponent); + component = fixture.componentInstance; + component.item = mockItem; + component.items = mockItems; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/recipe-details/recipe-details.component.ts b/frontend/src/app/components/recipe-details/recipe-details.component.ts new file mode 100644 index 00000000..2d436eff --- /dev/null +++ b/frontend/src/app/components/recipe-details/recipe-details.component.ts @@ -0,0 +1,35 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { IonicModule, ModalController } from '@ionic/angular'; +import { RecipeItemI } from '../../models/recipeItem.model'; +import { RecipeBookPage } from '../../pages/recipe-book/recipe-book.page'; +import { CommonModule } from '@angular/common'; +import { AddRecipeService } from '../../services/recipe-book/add-recipe.service'; +import { MealI } from '../../models/interfaces'; + +@Component({ + selector: 'app-recipe-details', + templateUrl: './recipe-details.component.html', + styleUrls: ['./recipe-details.component.scss'], + standalone: true, + imports: [IonicModule, RecipeBookPage, CommonModule] +}) +export class RecipeDetailsComponent implements OnInit { + @Input() item!: MealI; + @Input() items!: MealI[]; + + constructor(private modalController: ModalController, private addService: AddRecipeService) { } + + ngOnInit() {} + + closeModal() { + this.modalController.dismiss(); + } + + notSaved(): boolean { + return !this.items.includes(this.item); + } + + addRecipe(item: MealI) { + this.addService.setRecipeItem(item); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/recipe-item/recipe-item.component.html b/frontend/src/app/components/recipe-item/recipe-item.component.html index 6acae8a3..8b137891 100644 --- a/frontend/src/app/components/recipe-item/recipe-item.component.html +++ b/frontend/src/app/components/recipe-item/recipe-item.component.html @@ -1,21 +1 @@ - - -
-
{{ meal.name }}
-
- - - - - {{ meal.name }} - - Close - - - - - https://images.unsplash.com/photo-1498837167922-ddd27525d352?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80 - - -
\ No newline at end of file diff --git a/frontend/src/app/components/recipe-item/recipe-item.component.ts b/frontend/src/app/components/recipe-item/recipe-item.component.ts index 4bc99fb3..bd5464b3 100644 --- a/frontend/src/app/components/recipe-item/recipe-item.component.ts +++ b/frontend/src/app/components/recipe-item/recipe-item.component.ts @@ -1,34 +1,38 @@ import { Component, Input } from '@angular/core'; import { IonicModule, ModalController } from '@ionic/angular'; +import { RecipeDetailsComponent } from '../recipe-details/recipe-details.component'; + import { MealI } from '../../models/meal.model'; import { CommonModule } from '@angular/common'; +import { RecipeItemI } from '../../models/recipeItem.model'; + @Component({ selector: 'app-recipe-item', templateUrl: './recipe-item.component.html', styleUrls: ['./recipe-item.component.scss'], standalone: true, + imports: [IonicModule, CommonModule] + }) export class RecipeItemComponent { - // @Input() image!: string; - // @Input() title!: string; - @Input() meal!:MealI; - constructor(private modalController: ModalController) { } + items: MealI[] = []; - closeModal() { - this.modalController.dismiss(); + async openModal(item: any) { + const modal = await this.modalController.create({ + component: RecipeDetailsComponent, + componentProps: { + item: item, + items: this.items + } + }); + await modal.present(); } - isModalOpen = false; - currentObject :any - setOpen(isOpen: boolean, o :any) { - if(o==null) - o = this.currentObject - this.isModalOpen = isOpen; - this.setCurrent(o) - } - setCurrent(o : any) { - this.currentObject = o; + + public passItems(items: MealI[]): void { + this.items = items; } -} + constructor(private modalController: ModalController) { } +} diff --git a/frontend/src/app/models/authResponse.model.ts b/frontend/src/app/models/authResponse.model.ts new file mode 100644 index 00000000..04a9de09 --- /dev/null +++ b/frontend/src/app/models/authResponse.model.ts @@ -0,0 +1,3 @@ +export interface AuthResponseI { + token: string; +} \ No newline at end of file diff --git a/frontend/src/app/models/daysMeals.model.ts b/frontend/src/app/models/daysMeals.model.ts index d7e9ed6f..855b02bc 100644 --- a/frontend/src/app/models/daysMeals.model.ts +++ b/frontend/src/app/models/daysMeals.model.ts @@ -1,7 +1,8 @@ import { MealI } from "./meal.model"; export interface DaysMealsI { - breakfast:MealI; - lunch:MealI; - dinner:MealI; + breakfast:MealI | undefined; + lunch:MealI | undefined ; + dinner:MealI | undefined; + mealDate:string | undefined; } \ No newline at end of file diff --git a/frontend/src/app/models/interfaces.ts b/frontend/src/app/models/interfaces.ts index bd590bdf..7a66691e 100644 --- a/frontend/src/app/models/interfaces.ts +++ b/frontend/src/app/models/interfaces.ts @@ -4,5 +4,4 @@ export { UserI } from './user.model'; export { MealBrowseI } from './mealBrowse.model'; export { MealI } from './meal.model'; export { DaysMealsI } from './daysMeals.model'; - - +export { RecipeItemI } from './recipeItem.model'; \ No newline at end of file diff --git a/frontend/src/app/models/meal.model.ts b/frontend/src/app/models/meal.model.ts index 0ba7fad0..1b3e9b59 100644 --- a/frontend/src/app/models/meal.model.ts +++ b/frontend/src/app/models/meal.model.ts @@ -1,7 +1,7 @@ export interface MealI { name: string; description: string; - url: string; + image: string; ingredients:string; instructions:string; cookingTime:string; diff --git a/frontend/src/app/models/mealBrowse.model.ts b/frontend/src/app/models/mealBrowse.model.ts index 94bd193a..baffe4df 100644 --- a/frontend/src/app/models/mealBrowse.model.ts +++ b/frontend/src/app/models/mealBrowse.model.ts @@ -1,5 +1,5 @@ export interface MealBrowseI { - title: string; + name: string; description: string; url: string; ingredients:string; diff --git a/frontend/src/app/models/recipeItem.model.ts b/frontend/src/app/models/recipeItem.model.ts new file mode 100644 index 00000000..3cbb430e --- /dev/null +++ b/frontend/src/app/models/recipeItem.model.ts @@ -0,0 +1,4 @@ +export interface RecipeItemI { + image: string; + title: string; +} \ No newline at end of file diff --git a/frontend/src/app/models/user.model.ts b/frontend/src/app/models/user.model.ts index b991138c..00fcfadb 100644 --- a/frontend/src/app/models/user.model.ts +++ b/frontend/src/app/models/user.model.ts @@ -2,4 +2,5 @@ export interface UserI { username: string; email: string; password: string; + name?: string; //can be ignored don't worry about it } \ No newline at end of file diff --git a/frontend/src/app/models/userpreference.model.ts b/frontend/src/app/models/userpreference.model.ts index 3a24035b..191e373b 100644 --- a/frontend/src/app/models/userpreference.model.ts +++ b/frontend/src/app/models/userpreference.model.ts @@ -1,24 +1,24 @@ export interface UserPreferencesI { goal: string; - shopping_interval: string; - food_preferences: string[]; - calorie_amount: number; - budget_range: string; - macro_ratio: {protein: number, carbs: number, fats: number}; + shoppingInterval: string; + foodPreferences: string[]; + calorieAmount: number; + budgetRange: string; + macroRatio: {protein: number, carbs: number, fat: number}; allergies: string[]; - cooking_time: number; - user_height: number; //consider moving to account - user_weight: number; //consider moving to account - user_BMI: number; + cookingTime: string; + userHeight: number; //consider moving to account + userWeight: number; //consider moving to account + userBMI: number; - BMI_set : boolean; - cookingtime_set : boolean; - allergies_set : boolean; - macro_set : boolean; - budget_set : boolean; - calorie_set : boolean; - foodpreferance_set : boolean; - shoppinginterfval_set : boolean; + bmiset : boolean; + cookingTimeSet : boolean; + allergiesSet : boolean; + macroSet : boolean; + budgetSet : boolean; + calorieSet : boolean; + foodPreferenceSet : boolean; + shoppingIntervalSet : boolean; } \ No newline at end of file diff --git a/frontend/src/app/pages/acc-profile/acc-profile.page.html b/frontend/src/app/pages/acc-profile/acc-profile.page.html index 7894e52d..144d9568 100644 --- a/frontend/src/app/pages/acc-profile/acc-profile.page.html +++ b/frontend/src/app/pages/acc-profile/acc-profile.page.html @@ -9,19 +9,20 @@ - Username - Franko - - - Email - example@gmail.com - - - Password - ....... - + Username + {{user.username}} + + + Email + {{user.email}} + + + Password + ******** + + Logout diff --git a/frontend/src/app/pages/acc-profile/acc-profile.page.spec.ts b/frontend/src/app/pages/acc-profile/acc-profile.page.spec.ts index 718241f2..2abd62d7 100644 --- a/frontend/src/app/pages/acc-profile/acc-profile.page.spec.ts +++ b/frontend/src/app/pages/acc-profile/acc-profile.page.spec.ts @@ -1,14 +1,34 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AccProfilePage } from './acc-profile.page'; import { IonicModule } from '@ionic/angular'; +import { AuthenticationService } from '../../services/services'; +import { HttpResponse } from '@angular/common/http'; +import { UserI } from '../../models/user.model'; +import { of } from 'rxjs'; describe('AccProfilePage', () => { let component: AccProfilePage; let fixture: ComponentFixture; + let mockAuthService: jasmine.SpyObj; + let mockUser: UserI; beforeEach(async() => { + mockAuthService = jasmine.createSpyObj('AuthenticationService', ['logout', 'getUser']); + + mockUser = { + username: "test", + email: "test@test.com", + password: "secret" + } + + const response = new HttpResponse({ body: mockUser, status: 200 }); + mockAuthService.getUser.and.returnValue(of(response)); + await TestBed.configureTestingModule({ imports: [AccProfilePage, IonicModule], + providers: [ + { provide: AuthenticationService, useValue: mockAuthService }, + ], }).compileComponents(); fixture = TestBed.createComponent(AccProfilePage); component = fixture.componentInstance; diff --git a/frontend/src/app/pages/acc-profile/acc-profile.page.ts b/frontend/src/app/pages/acc-profile/acc-profile.page.ts index f3eec022..4d7531bb 100644 --- a/frontend/src/app/pages/acc-profile/acc-profile.page.ts +++ b/frontend/src/app/pages/acc-profile/acc-profile.page.ts @@ -3,6 +3,8 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { IonicModule } from '@ionic/angular'; import { Router } from '@angular/router'; +import { AuthenticationService } from '../../services/services'; +import { UserI } from '../../models/user.model'; @Component({ selector: 'app-acc-profile', @@ -13,13 +15,40 @@ import { Router } from '@angular/router'; }) export class AccProfilePage implements OnInit { + user: UserI; + + + constructor(private router: Router, private auth: AuthenticationService) { + this.user = { + username: '', + email: '', + password: '' + }; + } + + ngOnInit() { + this.auth.getUser().subscribe({ + next: (response) => { + if (response.status == 200) { + if (response.body && response.body.name) { + this.user.username = response.body.name; + this.user.email = response.body.email; + this.user.password = response.body.password; + } + } + }, + error: (error) => { + console.log(error); + } + }) + } + goBack() { this.router.navigate(['app/tabs/profile']) } - constructor(private router: Router) { } - - ngOnInit() { + logout() { + this.auth.logout(); } } diff --git a/frontend/src/app/pages/browse/browse.page.html b/frontend/src/app/pages/browse/browse.page.html index 3beb5fb1..b4061620 100644 --- a/frontend/src/app/pages/browse/browse.page.html +++ b/frontend/src/app/pages/browse/browse.page.html @@ -1,11 +1,17 @@ + - + + + + + + @@ -13,7 +19,57 @@ - + + + + +
+ +
+ + +
+ No results were found. + +
+
+ +
+
+ + + + + + + + + + +
+ - + - + + + + + + +
- +
- - + \ No newline at end of file diff --git a/frontend/src/app/pages/browse/browse.page.spec.ts b/frontend/src/app/pages/browse/browse.page.spec.ts index bcd44c56..c5ddfd83 100644 --- a/frontend/src/app/pages/browse/browse.page.spec.ts +++ b/frontend/src/app/pages/browse/browse.page.spec.ts @@ -2,9 +2,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BrowsePage } from './browse.page'; //import { ExploreContainerComponent } from '../../components/explore-container/explore-container.component'; import { IonicModule } from '@ionic/angular'; + import { RecipeItemComponent } from '../../components/recipe-item/recipe-item.component'; import { MealGenerationService } from '../../services/meal-generation/meal-generation.service'; + describe('BrowsePage', () => { let component: BrowsePage; let fixture: ComponentFixture; @@ -12,10 +14,12 @@ describe('BrowsePage', () => { beforeEach(async () => { await TestBed.configureTestingModule({ + imports: [BrowsePage, IonicModule, RecipeItemComponent], providers: [ { provide: MealGenerationService, useValue: mockMealGenerationService }, ], + }).compileComponents(); fixture = TestBed.createComponent(BrowsePage); diff --git a/frontend/src/app/pages/browse/browse.page.ts b/frontend/src/app/pages/browse/browse.page.ts index 90cafd51..ee570671 100644 --- a/frontend/src/app/pages/browse/browse.page.ts +++ b/frontend/src/app/pages/browse/browse.page.ts @@ -1,61 +1,125 @@ -import { Component, OnInit } from '@angular/core'; +import { AfterViewInit, Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { IonicModule } from '@ionic/angular'; import { Router } from '@angular/router'; import { BrowseMealsComponent } from '../../components/browse-meals/browse-meals.component'; -import { MealI } from '../../models/meal.model'; import { MealGenerationService } from '../../services/meal-generation/meal-generation.service'; import { ErrorHandlerService } from '../../services/services'; +import { DaysMealsI } from '../../models/daysMeals.model'; +import { MealBrowseI } from '../../models/mealBrowse.model'; +import { MealI } from '../../models/interfaces'; @Component({ selector: 'app-browse', templateUrl: './browse.page.html', styleUrls: ['./browse.page.scss'], standalone: true, - imports: [IonicModule,CommonModule, BrowseMealsComponent ] + imports: [IonicModule, BrowseMealsComponent, CommonModule ] }) -export class BrowsePage { +export class BrowsePage implements OnInit{ + // meals: DaysMealsI[]; - // mealsArray: { title: string, description: string, url: string, ingredients: string, instructions:string, cookingTime:string }[] = [ - // { title: "Greek Salad with Chicken", description: "A light and refreshing salad featuring a flavorful and protein-packed option for a healthy meal prep.", url: "https://images.unsplash.com/photo-1580013759032-c96505e24c1f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1818&q=80",ingredients: "INGREDIENTS :", instructions:"INSTRUCTIONS :", cookingTime:"COOKING TIME :"}, - // { title: "Curry Tofu Stir-Fry", description: "It's a balanced and refreshing option packed with protein and nutrients.", url: "https://images.unsplash.com/photo-1564834724105-918b73d1b9e0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=388&q=80",ingredients: "INGREDIENTS :", instructions:"INSTRUCTIONS :", cookingTime:"COOKING TIME :" }, - // { title: "Fried Chicken Tenders", description: "Crispy and golden-brown on the outside, tender and juicy on the inside, the classic comfort food favorite", url: "https://images.unsplash.com/photo-1614398751058-eb2e0bf63e53?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1014&q=80", ingredients: "INGREDIENTS :", instructions:"INSTRUCTIONS :", cookingTime:"COOKING TIME :" }, - // { title: "Spaghetti & Prawns", description: "This Italian-inspired recipe is a crowd-pleaser and can be made in advance, making it perfect for meal prepping.", url: "https://images.unsplash.com/photo-1563379926898-05f4575a45d8?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80", ingredients: "INGREDIENTS :", instructions:"INSTRUCTIONS :", cookingTime:"COOKING TIME :" }, - // // Additional entries... - - // ]; - - meals : MealI[] = []; - - constructor(public r : Router - , private mealGenerationservice:MealGenerationService - , private errorHandlerService:ErrorHandlerService) { } + popularMeals: MealI[] = []; + searchedMeals : MealI[] = []; + noResultsFound: boolean = false; + Searched: boolean = false; + Loading : boolean = false; + searchQuery: string=''; + searchResults: any; + + + constructor(public r : Router, + private mealGenerationservice:MealGenerationService, + private errorHandlerService:ErrorHandlerService,) + { + this.searchQuery = ''; + } async ngOnInit() { - for (let index = 0; index < 8; index++) { - this.mealGenerationservice.getMeal().subscribe({ + this.mealGenerationservice.getPopularMeals().subscribe({ next: (data) => { - if(Array.isArray(data)){ - this.meals.push(...data); - } - else { - this.meals.push(data); - } - - console.log(this.meals); + this.Searched = false; + this.popularMeals = this.popularMeals.concat(data); + + console.log(this.popularMeals); }, error: (err) => { this.errorHandlerService.presentErrorToast( 'Error loading meal items', err ) } + }) - - } - + +} + + +// Function to handle the search bar input event +onSearch(event: Event) { + // Get the search query from the event object + const customEvent = event as CustomEvent; + const query: string = customEvent.detail.value; + + if(query == "") { + this.ngOnInit(); + return; + } + // const query: string = event.detail.value; + // this.searchQuery = event.detail.value; + // Call the getSearchedMeals function with the new search query + // this.mealGenerationservice.getSearchedMeals(query).subscribe; + this.mealGenerationservice.getSearchedMeals(query).subscribe({ + next: (data) => { + this.Searched = true; + + if (data.length === 0) { + this.noResultsFound = true; + // console.log(this.searchedMeals); + } + else { + //this.Searched = true; + this.noResultsFound = false; + this.searchedMeals = data; + console.log(this.searchedMeals); + } + + }, + error: (err) => { + this.errorHandlerService.presentErrorToast('Error loading meal items', err); + }, + }); + +} + + cancel() { + this.Searched = false + console.log(this.Searched) + } + + + + RefreshMeals(event:any) { + this.Loading = true; + setTimeout(() => { + this.Loading = false; + event.target.complete(); + },2000); } + +// generateSearchMeals(query: string) { +// // Call the service function to get the searched meals with the provided query +// this.mealGenerationservice.getSearchedMeals(query).subscribe({ +// next: (data) => { +// // Update the searchedMeals array with the data returned from the service +// this.searchedMeals = data; +// console.log(this.searchedMeals); +// }, +// }) +// } + } + diff --git a/frontend/src/app/pages/home/home.page.html b/frontend/src/app/pages/home/home.page.html index 2f36398b..1919c386 100644 --- a/frontend/src/app/pages/home/home.page.html +++ b/frontend/src/app/pages/home/home.page.html @@ -11,7 +11,7 @@ - + diff --git a/frontend/src/app/pages/home/home.page.scss b/frontend/src/app/pages/home/home.page.scss index 8118feca..d54ae959 100644 --- a/frontend/src/app/pages/home/home.page.scss +++ b/frontend/src/app/pages/home/home.page.scss @@ -1,3 +1,7 @@ +.list-container { + width: 100%; /* Adjust the width as needed */ +} + ion-item { --border-color: var(--ion-color-primary-shade); diff --git a/frontend/src/app/pages/home/home.page.spec.ts b/frontend/src/app/pages/home/home.page.spec.ts index 42adeb09..f69a28ac 100644 --- a/frontend/src/app/pages/home/home.page.spec.ts +++ b/frontend/src/app/pages/home/home.page.spec.ts @@ -10,6 +10,8 @@ describe('HomePage', () => { let mockMealGenerationService: jasmine.SpyObj; beforeEach(async () => { + // mockMealGenerationService = jasmine.createSpyObj('MealGenerationService', ['generateMeals']); + await TestBed.configureTestingModule({ imports: [HomePage, IonicModule, DailyMealsComponent], providers: [ diff --git a/frontend/src/app/pages/home/home.page.ts b/frontend/src/app/pages/home/home.page.ts index 446ddcf2..bcc58a17 100644 --- a/frontend/src/app/pages/home/home.page.ts +++ b/frontend/src/app/pages/home/home.page.ts @@ -9,8 +9,7 @@ import { Router } from '@angular/router'; templateUrl: 'home.page.html', styleUrls: ['home.page.scss'], standalone: true, - imports: [ CommonModule, IonicModule, DailyMealsComponent - ] + imports: [IonicModule, DailyMealsComponent, CommonModule], }) export class HomePage implements OnInit{ daysMeals: DaysMealsI[] = []; @@ -19,42 +18,72 @@ export class HomePage implements OnInit{ , private errorHandlerService:ErrorHandlerService) {}; async ngOnInit() { - this.mealGenerationservice.getDailyMeals().subscribe({ - next: (data) => { - if(Array.isArray(data)){ - this.daysMeals = data; - } - else { - this.daysMeals = [data]; - } - - console.log(this.daysMeals); + + // for (let index = 0; index < 4; index++) { + // this.mealGenerationservice.getDailyMeals(this.getDayOfWeek(index)).subscribe({ + // next: (data: DaysMealsI[] | DaysMealsI) => { + // if (Array.isArray(data)) { + // const mealsWithDate = data.map((item) => ({ + // ...item, + // mealDate: this.getDayOfWeek(index), + // })); + // this.daysMeals.push(...mealsWithDate); + // } else { + // data.mealDate = this.getDayOfWeek(index); + // this.daysMeals.push(data); + // } + + // }, + // error: (err) => { + // this.errorHandlerService.presentErrorToast( + // 'Error loading meal items', + // err + // ); + // }, + // }); + + const observables = []; + + for (let index = 0; index < 4; index++) { + const observable = this.mealGenerationservice.getDailyMeals(this.getDayOfWeek(index)); + observables.push(observable); + } + + forkJoin(observables).subscribe({ + next: (dataArray: (DaysMealsI[] | DaysMealsI)[]) => { + dataArray.forEach((data, index) => { + if (Array.isArray(data)) { + const mealsWithDate = data.map((item) => ({ + ...item, + mealDate: this.getDayOfWeek(index), + })); + this.daysMeals.push(...mealsWithDate); + } else { + data.mealDate = this.getDayOfWeek(index); + this.daysMeals.push(data); + } + }); }, error: (err) => { - this.errorHandlerService.presentErrorToast( - 'Error loading meal items', err - ) - } - }) - this.mealGenerationservice.getDailyMeals().subscribe({ - next: (data) => { - if(Array.isArray(data)){ - this.daysMeals.push(...data); - } - else { - this.daysMeals.push(data); - } - - console.log(this.daysMeals); + this.errorHandlerService.presentErrorToast('Error loading meal items', err); }, - error: (err) => { - this.errorHandlerService.presentErrorToast( - 'Error loading meal items', err - ) - } - }) + }); + } + + private getDayOfWeek(dayOffset: number): string { + const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + const today = new Date(); + const targetDate = new Date(today); + targetDate.setDate(today.getDate() + dayOffset); + const dayIndex = targetDate.getDay(); + return daysOfWeek[dayIndex]; + } + private addDays(date: Date, days: number): Date { + const result = new Date(date); + result.setDate(result.getDate() + days); + return result; } @@ -84,5 +113,7 @@ export class HomePage implements OnInit{ }import { MealGenerationService } from '../../services/meal-generation/meal-generation.service'; import { ErrorHandlerService } from '../../services/services'; +import { BrowserModule } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; +import { forkJoin } from 'rxjs'; diff --git a/frontend/src/app/pages/login/login.page.html b/frontend/src/app/pages/login/login.page.html index 1ed4f0d1..f7d6d83a 100644 --- a/frontend/src/app/pages/login/login.page.html +++ b/frontend/src/app/pages/login/login.page.html @@ -1,64 +1,55 @@ - - - - - - - - - - -
-
- - +
+ +
+ +
+
+ + + + +
+ Please enter a valid email. +
+ - -
- Please enter a valid email. -
- - -
- Please enter a password. -
+
+
+ Please enter a password. +
+
+ + + + Login - - - - Login - - - - + +
+ - - Forget Password? - - - - - -
-
- \ No newline at end of file + + Forget Password? + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/pages/login/login.page.integration.spec.ts b/frontend/src/app/pages/login/login.page.integration.spec.ts new file mode 100644 index 00000000..6f866cc5 --- /dev/null +++ b/frontend/src/app/pages/login/login.page.integration.spec.ts @@ -0,0 +1,110 @@ +import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; +import { AuthenticationService, ErrorHandlerService } from "../../services/services"; +import { LoginPage } from "./login.page"; +import { TestBed } from "@angular/core/testing"; +import { IonicModule } from "@ionic/angular"; +import { RouterTestingModule } from "@angular/router/testing"; +import { Router } from "@angular/router"; +import { UserI } from "../../models/user.model"; +import { Component } from "@angular/core"; + +describe('LoginPageIntegration', () => { + let httpMock: HttpTestingController; + let auth: AuthenticationService; + let errorHandler: ErrorHandlerService; + let component: LoginPage; + let routerSpy = {navigate: jasmine.createSpy('navigate')}; + let apiUrl = 'http://localhost:8080'; + let mockUser: UserI; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [IonicModule.forRoot(), HttpClientTestingModule, RouterTestingModule.withRoutes([ + {path: 'app/tabs/home', component: DummyComponent} + ])], + providers: [ + AuthenticationService, + ErrorHandlerService, + { provide: Router, useValue: routerSpy }, + LoginPage + ] + }).compileComponents(); + + httpMock = TestBed.inject(HttpTestingController); + auth = TestBed.inject(AuthenticationService); + errorHandler = TestBed.inject(ErrorHandlerService); + component = TestBed.inject(LoginPage); + + mockUser = { + username: 'test', + password: 'test', + email: 'test@test.com' + }; + }) + + afterEach(() => { + httpMock.verify(); + }); + + it('should login a user and navigate to home', async () => { + spyOn(auth, 'login').and.callThrough(); + spyOn(errorHandler, 'presentSuccessToast').and.callThrough(); + + let mockForm = { + email: 'test@test.com', + password: 'test' + }; + + await component.login(mockForm); + + const req = httpMock.expectOne(apiUrl + '/authenticate'); + expect(req.request.method).toBe('POST'); + req.flush({token: 'test'}, {status: 200, statusText: 'OK'}); + + expect(auth.login).toHaveBeenCalled(); + expect(errorHandler.presentSuccessToast).toHaveBeenCalled(); + expect(routerSpy.navigate).toHaveBeenCalledWith(['app/tabs/home']); + + }); + + it('should not login a user if 403 response and display an error', async () => { + spyOn(auth, 'login').and.callThrough(); + spyOn(errorHandler, 'presentErrorToast').and.callThrough(); + + let mockForm = { + email: 'test@test.com', + password: 'test' + }; + + await component.login(mockForm); + + const req = httpMock.expectOne(apiUrl + '/authenticate'); + expect(req.request.method).toBe('POST'); + req.flush(null, {status: 403, statusText: 'Forbidden'}); + + expect(auth.login).toHaveBeenCalled(); + expect(errorHandler.presentErrorToast).toHaveBeenCalled(); + }); + + it('should not login a user if 500 response and display an error', async () => { + spyOn(auth, 'login').and.callThrough(); + spyOn(errorHandler, 'presentErrorToast').and.callThrough(); + + let mockForm = { + email: 'test@test.com', + password: 'test' + }; + + await component.login(mockForm); + + const req = httpMock.expectOne(apiUrl + '/authenticate'); + expect(req.request.method).toBe('POST'); + req.flush(null, {status: 500, statusText: 'Internal Server Error'}); + + expect(auth.login).toHaveBeenCalled(); + expect(errorHandler.presentErrorToast).toHaveBeenCalled(); + }); +}); + +@Component({template: ''}) +class DummyComponent {} \ No newline at end of file diff --git a/frontend/src/app/pages/login/login.page.scss b/frontend/src/app/pages/login/login.page.scss index c68fc6cd..c4c42fe7 100644 --- a/frontend/src/app/pages/login/login.page.scss +++ b/frontend/src/app/pages/login/login.page.scss @@ -1,10 +1,14 @@ -.logo { +.logo-container { + display: flex; + justify-content: center; align-items: center; - margin-top: 7vh; - margin-left: 5vw; - // border-radius: 50%; - height: 17vh; - width: 37vw; + margin-top: 5vh; +} + +.logo{ + width: 200px; + height: 200px; + z-index: 1; } .firstinput { @@ -23,15 +27,16 @@ } ion-input{ - //--background: #8a8a8a; - --background : var(--ion-input-background); + --background: #d3d3d383; + // --background : var(--ion-input-background); --border-radius: 20px; --color: black; --padding-bottom: 20px; - --padding-top: 15px; + --padding-top: 20px; --padding-start: 20px; max-width: 85%; margin: 0 auto; + // backdrop-filter: blur(2px); } @@ -44,10 +49,34 @@ ion-button { --padding-start: 20px; width: 85vw; height: 7vh; - margin-top: 15vh; + margin-top: 14vh; font-size: 2.5vh; } +.background-image { + position: fixed; + animation: slide 2s linear infinite; + + background: radial-gradient( + circle, rgba(255,255,255,0) 0%, + rgba(255,255,255,0) 10%, + rgba(255,127,80,0.3) 13%, + rgba(255,127,80,0.3) 15%, + rgba(255,255,255,0) 19% + ); + background-size: 40px 40px; + height: 100vh; + width: 100vw; +} + +@keyframes slide{ + 0%{ + background-position: 40px 0; + } + 100%{ + background-position: 0 40px; + } +} a { // font-size: 20vh; @@ -57,10 +86,14 @@ a { .firstlink { margin-top: 1vh; + font-size: 2vh; + z-index: 10; } .signup { width: 100%; text-align: center; - margin-top: 10vh; + margin-top: 5vh; + font-size: 2vh; + z-index: 10; } \ No newline at end of file diff --git a/frontend/src/app/pages/login/login.page.spec.ts b/frontend/src/app/pages/login/login.page.spec.ts index 8f1ff1b7..3ecd338b 100644 --- a/frontend/src/app/pages/login/login.page.spec.ts +++ b/frontend/src/app/pages/login/login.page.spec.ts @@ -2,6 +2,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { LoginPage } from './login.page'; import { IonicModule } from '@ionic/angular'; import { AuthenticationService } from '../../services/services'; +import { UserI } from '../../models/user.model'; +import { HttpResponse } from '@angular/common/http'; +import { AuthResponseI } from '../../models/authResponse.model'; +import { of } from 'rxjs'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Component } from '@angular/core'; describe('LoginPage', () => { let component: LoginPage; @@ -9,10 +15,16 @@ describe('LoginPage', () => { let mockAuthenicationService: jasmine.SpyObj; beforeEach(async () => { - mockAuthenicationService = jasmine.createSpyObj('AuthenticationService', ['login']); + mockAuthenicationService = jasmine.createSpyObj('AuthenticationService', ['login', 'setToken']); + + const response = new HttpResponse({ body: { token: 'test' }, status: 200 }); + + mockAuthenicationService.login.and.returnValue(of(response)); await TestBed.configureTestingModule({ - imports: [LoginPage, IonicModule], + imports: [LoginPage, IonicModule, RouterTestingModule.withRoutes([ + {path: 'app/tabs/home', component: DummyComponent} + ])], providers: [ { provide: AuthenticationService, useValue: mockAuthenicationService }, ], @@ -28,4 +40,19 @@ describe('LoginPage', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should call login', () => { + let mockUser : UserI = { + username: 'test', + password: 'test', + email: 'test@test.com', + } + + component.login(mockUser); + expect(mockAuthenicationService.login).toHaveBeenCalled(); + + }); }); + +@Component({template: ''}) +class DummyComponent {} \ No newline at end of file diff --git a/frontend/src/app/pages/login/login.page.ts b/frontend/src/app/pages/login/login.page.ts index ad9b7df6..0c6f9ce0 100644 --- a/frontend/src/app/pages/login/login.page.ts +++ b/frontend/src/app/pages/login/login.page.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { IonicModule } from '@ionic/angular'; @@ -13,7 +13,7 @@ import { UserI } from '../../models/user.model'; standalone: true, imports: [IonicModule, CommonModule, FormsModule] }) -export class LoginPage { +export class LoginPage implements OnInit { user: UserI = { username: '', email: '', @@ -21,43 +21,46 @@ export class LoginPage { } - constructor( private router: Router, private errorHandlerService: ErrorHandlerService, private auth: AuthenticationService ) { } + constructor(private router: Router, + private errorHandlerService: ErrorHandlerService, + private auth: AuthenticationService + ) { } - login(form: any) { + ngOnInit() { + } + async login(form: any) { const loginUser: UserI = { username: '', email: form.email, password: form.password, } - console.log(loginUser); this.auth.login(loginUser).subscribe({ - next: (result) => { - if (result) { - this.auth.getUser(loginUser.email).subscribe({ - next: (user) => { - localStorage.setItem('user', user.username); - localStorage.setItem('email', user.email); - }, - error: error => { - this.errorHandlerService.presentErrorToast('Login failed', error); - } - }); - - this.errorHandlerService.presentSuccessToast('Login successful'); - this.router.navigate(['app/tabs/home']); + next: (response) => { + if (response.status == 200) { + if (response.body) { + this.auth.setToken(response.body.token); + this.errorHandlerService.presentSuccessToast('Login successful'); + this.router.navigate(['app/tabs/home']); + } } - else { + }, + error: (error) => { + if (error.status == 403){ this.errorHandlerService.presentErrorToast('Invalid credentials', 'Invalid credentials'); + localStorage.removeItem('token'); + }else if(error.status == 404){ + this.errorHandlerService.presentErrorToast('Email or password incorrect', 'Email or password incorrect'); + localStorage.removeItem('token'); + }else{ + this.errorHandlerService.presentErrorToast('Unexpected error. Please try again', error); } - }, - error: error => { - this.errorHandlerService.presentErrorToast('Login failed', error); } }); } goToSignup() { this.router.navigate(['../signup']); + localStorage.removeItem('token'); } } diff --git a/frontend/src/app/pages/pantry/pantry.page.html b/frontend/src/app/pages/pantry/pantry.page.html index 9c9ea831..7dcc2aae 100644 --- a/frontend/src/app/pages/pantry/pantry.page.html +++ b/frontend/src/app/pages/pantry/pantry.page.html @@ -1,52 +1,88 @@ - - - - - Pantry - - - Shopping - - - - + + + + + Pantry + + + Shopping + + + + + + + + + Name + + + + + + + + + + + + Amount + + + + + + + + + + + + + - - - Name - Quantity - - - - - + + + + + + +
Pantry is empty :(
- - +
- - +
+ [isVisible]="isVisible(item.name)" + (itemDeleted)="onItemDeleted($event)" + (itemBought)="onItemBought($event)"> >
Shopping list is empty :(
- - - +
+ @@ -66,17 +102,28 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -86,6 +133,14 @@ + @@ -105,17 +160,28 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/app/pages/pantry/pantry.page.integration.spec.ts b/frontend/src/app/pages/pantry/pantry.page.integration.spec.ts new file mode 100644 index 00000000..84e1f322 --- /dev/null +++ b/frontend/src/app/pages/pantry/pantry.page.integration.spec.ts @@ -0,0 +1,630 @@ +import { TestBed } from '@angular/core/testing'; +import { IonicModule } from '@ionic/angular'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { PantryPage } from './pantry.page'; +import { FoodItemI } from '../../models/interfaces'; +import { AuthenticationService, PantryApiService, ShoppingListApiService } from '../../services/services'; +import { OverlayEventDetail } from '@ionic/core/components'; + + +describe('PantryPageIntegration', () => { + let httpMock: HttpTestingController; + let pantryService: PantryApiService; + let shoppingListService: ShoppingListApiService; + let authService: AuthenticationService; + let component: PantryPage; + let pantryItems: FoodItemI[]; + let shoppingListItems: FoodItemI[]; + let apiUrl: string = 'http://localhost:8080'; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [IonicModule.forRoot(), HttpClientTestingModule], + providers: [PantryApiService, + ShoppingListApiService, + AuthenticationService, + PantryPage + ], + }).compileComponents(); + + httpMock = TestBed.inject(HttpTestingController); + pantryService = TestBed.inject(PantryApiService); + shoppingListService = TestBed.inject(ShoppingListApiService); + authService = TestBed.inject(AuthenticationService); + component = TestBed.inject(PantryPage); + + pantryItems = [ + { + name: 'test', + quantity: 1, + weight: 1, + }, + { + name: 'test2', + quantity: 2, + weight: 2, + }, + ]; + + shoppingListItems = [ + { + name: 'test3', + quantity: 3, + weight: 3, + }, + { + name: 'test4', + quantity: 4, + weight: 4, + }, + ]; + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should fetch pantry and shopping list', async () => { + spyOn(pantryService, 'getPantryItems').and.callThrough(); + spyOn(authService, 'logout').and.callThrough(); + spyOn(shoppingListService, 'getShoppingListItems').and.callThrough(); + + await component.fetchItems(); + + const req = httpMock.expectOne(apiUrl + '/getPantry'); + expect(req.request.method).toBe('POST'); + req.flush(pantryItems); + + const req2 = httpMock.expectOne(apiUrl + '/getShoppingList'); + expect(req2.request.method).toBe('POST'); + req2.flush(shoppingListItems); + + expect(component.pantryItems).toEqual(pantryItems); + expect(pantryService.getPantryItems).toHaveBeenCalled(); + + expect(component.shoppingItems).toEqual(shoppingListItems); + expect(shoppingListService.getShoppingListItems).toHaveBeenCalled(); + + }) + + it('should handle 403 error when getting items', async () => { + spyOn(pantryService, 'getPantryItems').and.callThrough(); + spyOn(shoppingListService, 'getShoppingListItems').and.callThrough(); + spyOn(authService, 'logout').and.callThrough(); + spyOn(component, 'fetchItems').and.callThrough(); + + await component.fetchItems(); + + const req = httpMock.expectOne(apiUrl + '/getPantry'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 403, statusText: 'Forbidden' }); + + const req2 = httpMock.expectOne(apiUrl + '/getShoppingList'); + expect(req2.request.method).toBe('POST'); + req2.flush(null, { status: 403, statusText: 'Forbidden' }); + + expect(pantryService.getPantryItems).toHaveBeenCalled(); + expect(authService.logout).toHaveBeenCalled(); + expect(shoppingListService.getShoppingListItems).toHaveBeenCalled(); + }); + + it('should handle other error when getting items', async () => { + spyOn(pantryService, 'getPantryItems').and.callThrough(); + spyOn(shoppingListService, 'getShoppingListItems').and.callThrough(); + spyOn(authService, 'logout').and.callThrough(); + // spyOn(component, 'fetchItems').and.callThrough(); + + await component.fetchItems(); + + const req = httpMock.expectOne(apiUrl + '/getPantry'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 500, statusText: 'Internal Server Error' }); + + const req2 = httpMock.expectOne(apiUrl + '/getShoppingList'); + expect(req2.request.method).toBe('POST'); + req2.flush(null, { status: 500, statusText: 'Internal Server Error' }); + + expect(pantryService.getPantryItems).toHaveBeenCalled(); + expect(shoppingListService.getShoppingListItems).toHaveBeenCalled(); + }); + + it('should add item to pantry', async () => { + let item: FoodItemI = { + name: 'test4', + quantity: 1, + weight: 1, + }; + let response = { + status: 200, + body: item + } + let ev: CustomEvent> = { + detail: { + data: item, + role: 'confirm' + }, + initCustomEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined, detail?: OverlayEventDetail | undefined): void { + throw new Error('Function not implemented.'); + }, + bubbles: false, + cancelBubble: false, + cancelable: false, + composed: false, + currentTarget: null, + defaultPrevented: false, + eventPhase: 0, + isTrusted: false, + returnValue: false, + srcElement: null, + target: null, + timeStamp: 0, + type: '', + composedPath: function (): EventTarget[] { + throw new Error('Function not implemented.'); + }, + initEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { + throw new Error('Function not implemented.'); + }, + preventDefault: function (): void { + throw new Error('Function not implemented.'); + }, + stopImmediatePropagation: function (): void { + throw new Error('Function not implemented.'); + }, + stopPropagation: function (): void { + throw new Error('Function not implemented.'); + }, + NONE: 0, + CAPTURING_PHASE: 1, + AT_TARGET: 2, + BUBBLING_PHASE: 3 + } + + spyOn(pantryService, 'addToPantry').and.callThrough(); + + await component.addItemToPantry(ev); + + const req = httpMock.expectOne(apiUrl + '/addToPantry'); + expect(req.request.method).toBe('POST'); + req.flush(response.body); + + expect(pantryService.addToPantry).toHaveBeenCalled(); + expect(component.pantryItems).toContain(item); + }); + + it('should handle 403 error when adding item to pantry', async () => { + let item: FoodItemI = { + name: 'test4', + quantity: 1, + weight: 1, + }; + let response = { + status: 403, + body: item + } + let ev: CustomEvent> = { + detail: { + data: item, + role: 'confirm' + }, + initCustomEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined, detail?: OverlayEventDetail | undefined): void { + throw new Error('Function not implemented.'); + }, + bubbles: false, + cancelBubble: false, + cancelable: false, + composed: false, + currentTarget: null, + defaultPrevented: false, + eventPhase: 0, + isTrusted: false, + returnValue: false, + srcElement: null, + target: null, + timeStamp: 0, + type: '', + composedPath: function (): EventTarget[] { + throw new Error('Function not implemented.'); + }, + initEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { + throw new Error('Function not implemented.'); + }, + preventDefault: function (): void { + throw new Error('Function not implemented.'); + }, + stopImmediatePropagation: function (): void { + throw new Error('Function not implemented.'); + }, + stopPropagation: function (): void { + throw new Error('Function not implemented.'); + }, + NONE: 0, + CAPTURING_PHASE: 1, + AT_TARGET: 2, + BUBBLING_PHASE: 3 + } + + spyOn(pantryService, 'addToPantry').and.callThrough(); + spyOn(authService, 'logout').and.callThrough(); + + await component.addItemToPantry(ev); + + const req = httpMock.expectOne(apiUrl + '/addToPantry'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 403, statusText: 'Forbidden' }); + + expect(pantryService.addToPantry).toHaveBeenCalled(); + expect(authService.logout).toHaveBeenCalled(); + }); + + it('should handle other errors when adding item to pantry', async () => { + let item: FoodItemI = { + name: 'test4', + quantity: 1, + weight: 1, + }; + let response = { + status: 500, + body: item + } + let ev: CustomEvent> = { + detail: { + data: item, + role: 'confirm' + }, + initCustomEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined, detail?: OverlayEventDetail | undefined): void { + throw new Error('Function not implemented.'); + }, + bubbles: false, + cancelBubble: false, + cancelable: false, + composed: false, + currentTarget: null, + defaultPrevented: false, + eventPhase: 0, + isTrusted: false, + returnValue: false, + srcElement: null, + target: null, + timeStamp: 0, + type: '', + composedPath: function (): EventTarget[] { + throw new Error('Function not implemented.'); + }, + initEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { + throw new Error('Function not implemented.'); + }, + preventDefault: function (): void { + throw new Error('Function not implemented.'); + }, + stopImmediatePropagation: function (): void { + throw new Error('Function not implemented.'); + }, + stopPropagation: function (): void { + throw new Error('Function not implemented.'); + }, + NONE: 0, + CAPTURING_PHASE: 1, + AT_TARGET: 2, + BUBBLING_PHASE: 3 + } + + spyOn(pantryService, 'addToPantry').and.callThrough(); + + await component.addItemToPantry(ev); + + const req = httpMock.expectOne(apiUrl + '/addToPantry'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 500, statusText: 'Internal Server Error' }); + + expect(pantryService.addToPantry).toHaveBeenCalled(); + }); + + it('should add item to shopping list', async () => { + let item: FoodItemI = { + name: 'test4', + quantity: 1, + weight: 1, + }; + let response = { + status: 200, + body: item + } + let ev: CustomEvent> = { + detail: { + data: item, + role: 'confirm' + }, + initCustomEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined, detail?: OverlayEventDetail | undefined): void { + throw new Error('Function not implemented.'); + }, + bubbles: false, + cancelBubble: false, + cancelable: false, + composed: false, + currentTarget: null, + defaultPrevented: false, + eventPhase: 0, + isTrusted: false, + returnValue: false, + srcElement: null, + target: null, + timeStamp: 0, + type: '', + composedPath: function (): EventTarget[] { + throw new Error('Function not implemented.'); + }, + initEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { + throw new Error('Function not implemented.'); + }, + preventDefault: function (): void { + throw new Error('Function not implemented.'); + }, + stopImmediatePropagation: function (): void { + throw new Error('Function not implemented.'); + }, + stopPropagation: function (): void { + throw new Error('Function not implemented.'); + }, + NONE: 0, + CAPTURING_PHASE: 1, + AT_TARGET: 2, + BUBBLING_PHASE: 3 + } + + spyOn(shoppingListService, 'addToShoppingList').and.callThrough(); + + await component.addItemToShoppingList(ev); + + const req = httpMock.expectOne(apiUrl + '/addToShoppingList'); + expect(req.request.method).toBe('POST'); + req.flush(response.body); + + expect(shoppingListService.addToShoppingList).toHaveBeenCalled(); + expect(component.shoppingItems).toContain(item); + }); + + it('should handle 403 error when adding item to shopping list', async () => { + let item: FoodItemI = { + name: 'test4', + quantity: 1, + weight: 1, + }; + let response = { + status: 403, + body: item + } + let ev: CustomEvent> = { + detail: { + data: item, + role: 'confirm' + }, + initCustomEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined, detail?: OverlayEventDetail | undefined): void { + throw new Error('Function not implemented.'); + }, + bubbles: false, + cancelBubble: false, + cancelable: false, + composed: false, + currentTarget: null, + defaultPrevented: false, + eventPhase: 0, + isTrusted: false, + returnValue: false, + srcElement: null, + target: null, + timeStamp: 0, + type: '', + composedPath: function (): EventTarget[] { + throw new Error('Function not implemented.'); + }, + initEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { + throw new Error('Function not implemented.'); + }, + preventDefault: function (): void { + throw new Error('Function not implemented.'); + }, + stopImmediatePropagation: function (): void { + throw new Error('Function not implemented.'); + }, + stopPropagation: function (): void { + throw new Error('Function not implemented.'); + }, + NONE: 0, + CAPTURING_PHASE: 1, + AT_TARGET: 2, + BUBBLING_PHASE: 3 + } + + spyOn(shoppingListService, 'addToShoppingList').and.callThrough(); + spyOn(authService, 'logout').and.callThrough(); + + await component.addItemToShoppingList(ev); + + const req = httpMock.expectOne(apiUrl + '/addToShoppingList'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 403, statusText: 'Forbidden' }); + + expect(shoppingListService.addToShoppingList).toHaveBeenCalled(); + expect(authService.logout).toHaveBeenCalled(); + }); + + it('should handle other errors when adding item to shopping list', async () => { + let item: FoodItemI = { + name: 'test4', + quantity: 1, + weight: 1, + }; + let response = { + status: 500, + body: item + } + let ev: CustomEvent> = { + detail: { + data: item, + role: 'confirm' + }, + initCustomEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined, detail?: OverlayEventDetail | undefined): void { + throw new Error('Function not implemented.'); + }, + bubbles: false, + cancelBubble: false, + cancelable: false, + composed: false, + currentTarget: null, + defaultPrevented: false, + eventPhase: 0, + isTrusted: false, + returnValue: false, + srcElement: null, + target: null, + timeStamp: 0, + type: '', + composedPath: function (): EventTarget[] { + throw new Error('Function not implemented.'); + }, + initEvent: function (type: string, bubbles?: boolean | undefined, cancelable?: boolean | undefined): void { + throw new Error('Function not implemented.'); + }, + preventDefault: function (): void { + throw new Error('Function not implemented.'); + }, + stopImmediatePropagation: function (): void { + throw new Error('Function not implemented.'); + }, + stopPropagation: function (): void { + throw new Error('Function not implemented.'); + }, + NONE: 0, + CAPTURING_PHASE: 1, + AT_TARGET: 2, + BUBBLING_PHASE: 3 + } + + spyOn(shoppingListService, 'addToShoppingList').and.callThrough(); + + await component.addItemToShoppingList(ev); + + const req = httpMock.expectOne(apiUrl + '/addToShoppingList'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 500, statusText: 'Internal Server Error' }); + + expect(shoppingListService.addToShoppingList).toHaveBeenCalled(); + }); + + it('should delete item from pantry if segment is pantry', async () => { + let item: FoodItemI = { + name: 'test', + quantity: 1, + weight: 1, + }; + component.segment = 'pantry'; + component.pantryItems = pantryItems; + component.shoppingItems = shoppingListItems; + + spyOn(pantryService, 'deletePantryItem').and.callThrough(); + + await component.onItemDeleted(item); + + const req = httpMock.expectOne(apiUrl + '/removeFromPantry'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 200, statusText: 'OK' }); + + expect(pantryService.deletePantryItem).toHaveBeenCalled(); + expect(component.pantryItems).not.toContain(item); + + expect(component.shoppingItems).toEqual(shoppingListItems); + }); + + it('should delete item from shopping list if segment is shopping', async () => { + let item: FoodItemI = { + name: 'test', + quantity: 1, + weight: 1, + }; + component.segment = 'shopping'; + component.pantryItems = pantryItems; + component.shoppingItems = shoppingListItems; + + spyOn(shoppingListService, 'deleteShoppingListItem').and.callThrough(); + + await component.onItemDeleted(item); + + const req = httpMock.expectOne(apiUrl + '/removeFromShoppingList'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 200, statusText: 'OK' }); + + expect(shoppingListService.deleteShoppingListItem).toHaveBeenCalled(); + expect(component.shoppingItems).not.toContain(item); + + expect(component.pantryItems).toEqual(pantryItems); + }); + + it('should move item from shopping list to pantry if item is bought', async () => { + let item: FoodItemI = { + name: 'test3', + quantity: 3, + weight: 3, + }; + let response = { + body: [ + { + name: 'test', + quantity: 1, + weight: 1, + }, + { + name: 'test2', + quantity: 2, + weight: 2, + }, + { + name: 'test3', + quantity: 3, + weight: 3, + } + ] + } + component.segment = 'shopping'; + component.pantryItems = pantryItems; + component.shoppingItems = shoppingListItems; + + spyOn(shoppingListService, 'buyItem').and.callThrough(); + + await component.onItemBought(item); + + const req = httpMock.expectOne(apiUrl + '/buyItem'); + expect(req.request.method).toBe('POST'); + req.flush(response.body, { status: 200, statusText: 'OK' }); + + expect(shoppingListService.buyItem).toHaveBeenCalled(); + expect(component.shoppingItems).not.toContain(item); + expect(component.pantryItems).toContain(item); + + expect(component.pantryItems).toEqual(response.body); + }); + + it('should handle 403 error when moving item from shopping list to pantry if item is bought', async () => { + let item: FoodItemI = { + name: 'test3', + quantity: 3, + weight: 3, + }; + component.segment = 'shopping'; + component.pantryItems = pantryItems; + component.shoppingItems = shoppingListItems; + + spyOn(shoppingListService, 'buyItem').and.callThrough(); + spyOn(authService, 'logout').and.callThrough(); + + await component.onItemBought(item); + + const req = httpMock.expectOne(apiUrl + '/buyItem'); + expect(req.request.method).toBe('POST'); + req.flush(null, { status: 403, statusText: 'Forbidden' }); + + expect(shoppingListService.buyItem).toHaveBeenCalled(); + expect(authService.logout).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/frontend/src/app/pages/pantry/pantry.page.scss b/frontend/src/app/pages/pantry/pantry.page.scss index cf86f51a..7a8c55a3 100644 --- a/frontend/src/app/pages/pantry/pantry.page.scss +++ b/frontend/src/app/pages/pantry/pantry.page.scss @@ -7,16 +7,68 @@ font-family: 'Roboto', sans-serif; } -.header{ - width: 100%; +.list{ + margin-bottom: 10px; } ion-toolbar{ --border-width: 0px; + --border-color: transparent; +} + +ion-toolbar.labels{ + --min-height: 12px; + --padding-start: 4vw; + --padding-end: 4vw; + --padding-bottom: 10px; + --padding-top: 5px; + font-size: 18px; + font-weight: bold; + font-family: 'Roboto', sans-serif; } ion-content{ --padding-bottom: 80px; - --padding-top: 50px; +} + +ion-toggle { + padding: 12px; + padding-top: 20px; + + --track-background: #ddd; + --track-background-checked: #ddd; + + --handle-background: var(--ion-color-secondary); + --handle-background-checked: var(--ion-color-primary); + + --handle-width: 25px; + --handle-height: 27px; + --handle-max-height: auto; + --handle-spacing: 6px; + + --handle-border-radius: 4px; + --handle-box-shadow: none; +} + +ion-toggle::part(track) { + height: 10px; + width: 65px; + + /* Required for iOS handle to overflow the height of the track */ + overflow: visible; +} + +ion-segment-button{ + --indicator-color: var(--ion-color-primary); +} + +ion-segment-button.ios { + --color: black; + --color-checked: white; +} +ion-segment-button.md { + --color: #000; + --color-checked: var(--ion-color-primary); + --indicator-height: 4px; } \ No newline at end of file diff --git a/frontend/src/app/pages/pantry/pantry.page.spec.ts b/frontend/src/app/pages/pantry/pantry.page.spec.ts index e2639ff9..4a4a5dfc 100644 --- a/frontend/src/app/pages/pantry/pantry.page.spec.ts +++ b/frontend/src/app/pages/pantry/pantry.page.spec.ts @@ -5,18 +5,21 @@ import { IonicModule } from '@ionic/angular'; import { PantryPage } from './pantry.page'; import { of } from 'rxjs'; import { FoodItemI } from '../../models/interfaces'; -import { PantryApiService, ShoppingListApiService } from '../../services/services'; +import { AuthenticationService, PantryApiService, ShoppingListApiService } from '../../services/services'; +import { HttpResponse } from '@angular/common/http'; describe('PantryPage', () => { let component: PantryPage; let fixture: ComponentFixture; let mockPantryService: jasmine.SpyObj; let mockShoppingListService: jasmine.SpyObj; + let mockAuthService: jasmine.SpyObj; let mockItems: FoodItemI[]; beforeEach(async () => { mockPantryService = jasmine.createSpyObj('PantryApiService', ['getPantryItems', 'addToPantry', 'deletePantryItem']); mockShoppingListService = jasmine.createSpyObj('ShoppingListApiService', ['getShoppingListItems', 'addToShoppingList', 'deleteShoppingListItem']); + mockAuthService = jasmine.createSpyObj('AuthenticationService', ['logout']); mockItems = [ { name: 'test', @@ -30,18 +33,16 @@ describe('PantryPage', () => { }, ]; - const emptyFoodItem: FoodItemI = { - name: '', - quantity: null, - weight: null, - }; + const emptyResponse = new HttpResponse({ body: null, status: 200 }); + const itemsResponse = new HttpResponse({ body: mockItems, status: 200 }); + const itemResponse = new HttpResponse({ body: mockItems[0], status: 200 }); - mockPantryService.getPantryItems.and.returnValue(of(mockItems)); - mockPantryService.addToPantry.and.returnValue(of(mockItems[0])); - mockPantryService.deletePantryItem.and.returnValue(of(emptyFoodItem)); - mockShoppingListService.getShoppingListItems.and.returnValue(of(mockItems)); - mockShoppingListService.addToShoppingList.and.returnValue(of(mockItems[0])); - mockShoppingListService.deleteShoppingListItem.and.returnValue(of(emptyFoodItem)); + mockPantryService.getPantryItems.and.returnValue(of(itemsResponse)); + mockPantryService.addToPantry.and.returnValue(of(itemResponse)); + mockPantryService.deletePantryItem.and.returnValue(of(emptyResponse)); + mockShoppingListService.getShoppingListItems.and.returnValue(of(itemsResponse)); + mockShoppingListService.addToShoppingList.and.returnValue(of(itemResponse)); + mockShoppingListService.deleteShoppingListItem.and.returnValue(of(emptyResponse)); await TestBed.configureTestingModule({ @@ -49,6 +50,7 @@ describe('PantryPage', () => { providers: [ { provide: PantryApiService, useValue: mockPantryService }, { provide: ShoppingListApiService, useValue: mockShoppingListService }, + { provide: AuthenticationService, useValue: mockAuthService }, ], }).compileComponents(); diff --git a/frontend/src/app/pages/pantry/pantry.page.ts b/frontend/src/app/pages/pantry/pantry.page.ts index 5de3e3c0..ae90570a 100644 --- a/frontend/src/app/pages/pantry/pantry.page.ts +++ b/frontend/src/app/pages/pantry/pantry.page.ts @@ -6,7 +6,7 @@ import { FormsModule } from '@angular/forms'; import { FoodListItemComponent } from '../../components/food-list-item/food-list-item.component'; import { FoodItemI } from '../../models/interfaces'; import { OverlayEventDetail } from '@ionic/core/components'; -import { ErrorHandlerService, PantryApiService, ShoppingListApiService } from '../../services/services'; +import { AuthenticationService, ErrorHandlerService, PantryApiService, ShoppingListApiService } from '../../services/services'; @Component({ @@ -21,8 +21,12 @@ export class PantryPage implements OnInit{ @ViewChild(IonModal) modal!: IonModal; segment: 'pantry'|'shopping'| null = 'pantry'; + isQuantity: boolean = false; + isLoading: boolean = false; pantryItems: FoodItemI[] = []; shoppingItems: FoodItemI[] = []; + searchTerm: string = ''; + currentSort: string = 'name-down'; newItem: FoodItemI = { name: '', quantity: null, @@ -32,29 +36,66 @@ export class PantryPage implements OnInit{ constructor(public r : Router, private pantryService: PantryApiService, private shoppingListService: ShoppingListApiService, - private errorHandlerService: ErrorHandlerService) {} + private errorHandlerService: ErrorHandlerService, + private auth: AuthenticationService) {} async ngOnInit() { + this.fetchItems(); + } + + async fetchItems(){ + this.isLoading = true; this.pantryService.getPantryItems().subscribe({ - next: (data) => { - this.pantryItems = data; + next: (response) => { + if (response.status === 200) { + if (response.body){ + this.pantryItems = response.body; + this.isLoading = false; + this.sortNameDescending(); + } + } }, error: (err) => { - this.errorHandlerService.presentErrorToast( - 'Error loading pantry items', - err - ) + if (err.status === 403){ + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again.', + err + ) + this.isLoading = false; + this.auth.logout(); + }else{ + this.errorHandlerService.presentErrorToast( + 'Error loading pantry items', + err + ) + this.isLoading = false; + } } }) + this.shoppingListService.getShoppingListItems().subscribe({ - next: (data) => { - this.shoppingItems = data; + next: (response) => { + if (response.status === 200) { + if (response.body){ + this.shoppingItems = response.body; + this.isLoading = false; + this.sortNameDescending(); + } + } }, error: (err) => { - this.errorHandlerService.presentErrorToast( - 'Error loading shopping list items', - err - ) + if (err.status === 403){ + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again.', + err + ) + this.auth.logout(); + }else{ + this.errorHandlerService.presentErrorToast( + 'Error loading shopping list items', + err + ) + } } }); } @@ -64,20 +105,32 @@ export class PantryPage implements OnInit{ if (ev.detail.role === 'confirm') { this.pantryService.addToPantry(ev.detail.data!).subscribe({ - next: (data) => { - console.log(data); - this.pantryItems.push(data); - this.newItem = { - name: '', - quantity: null, - weight: null, - }; + next: (response) => { + if (response.status === 200) { + if (response.body){ + this.pantryItems.unshift(response.body); + this.newItem = { + name: '', + quantity: null, + weight: null, + }; + this.isQuantity = false; + } + } }, error: (err) => { - this.errorHandlerService.presentErrorToast( - 'Error adding item to pantry', - err - ) + if (err.status === 403){ + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again.', + err + ) + this.auth.logout(); + }else{ + this.errorHandlerService.presentErrorToast( + 'Error adding item to pantry', + err + ) + } } }); } @@ -87,53 +140,114 @@ export class PantryPage implements OnInit{ var ev = event as CustomEvent>; if (ev.detail.role === 'confirm') { this.shoppingListService.addToShoppingList(ev.detail.data!).subscribe({ - next: (data) => { - console.log(data); - this.shoppingItems.push(data); - this.newItem = { - name: '', - quantity: null, - weight: null, - }; + next: (response) => { + if (response.status === 200) { + if (response.body){ + this.shoppingItems.unshift(response.body); + this.newItem = { + name: '', + quantity: null, + weight: null, + }; + this.isQuantity = false; + } + } + }, error: (err) => { - this.errorHandlerService.presentErrorToast( - 'Error adding item to shopping list', - err - ) + if (err.status === 403){ + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again.', + err + ) + this.auth.logout(); + }else{ + this.errorHandlerService.presentErrorToast( + 'Error adding item to shopping list', + err + ) + } } }); } } - onItemDeleted(item : FoodItemI){ + async onItemDeleted(item : FoodItemI){ if (this.segment === 'pantry'){ this.pantryService.deletePantryItem(item).subscribe({ - next: () => { - this.pantryItems = this.pantryItems.filter((i) => i.name !== item.name); + next: (response) => { + if (response.status === 200) { + this.pantryItems = this.pantryItems.filter((i) => i.name !== item.name); + } }, error: (err) => { - this.errorHandlerService.presentErrorToast( - 'Error deleting item from pantry', - err - ) + if (err.status === 403){ + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again.', + err + ) + this.auth.logout(); + }else{ + this.errorHandlerService.presentErrorToast( + 'Error deleting item from pantry', + err + ) + } } }); } else if (this.segment === 'shopping'){ this.shoppingListService.deleteShoppingListItem(item).subscribe({ - next: () => { - this.shoppingItems = this.shoppingItems.filter((i) => i.name !== item.name); + next: (response) => { + if (response.status === 200) { + this.shoppingItems = this.shoppingItems.filter((i) => i.name !== item.name); + } }, error: (err) => { - this.errorHandlerService.presentErrorToast( - 'Error deleting item from shopping list', - err - ) + if (err.status === 403){ + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again.', + err + ) + this.auth.logout(); + }else{ + this.errorHandlerService.presentErrorToast( + 'Error deleting item from shopping list', + err + ) + } } }); } } + async onItemBought(item : FoodItemI){ + this.shoppingListService.buyItem(item).subscribe({ + next: (response) => { + if (response.status === 200) { + if (response.body){ + this.pantryItems = response.body; + this.shoppingItems = this.shoppingItems.filter((i) => i.name !== item.name); + this.errorHandlerService.presentSuccessToast("Item Bought!"); + } + } + }, + error: (err) => { + if (err.status === 403){ + this.errorHandlerService.presentErrorToast( + 'Unauthorize access. Please login again.', + err + ) + this.auth.logout(); + } else { + this.errorHandlerService.presentErrorToast( + 'Error buying item.', + err + ) + } + } + }) + } + closeSlidingItems(){ this.foodListItem.forEach((item) => { item.closeItem(); @@ -151,9 +265,125 @@ export class PantryPage implements OnInit{ dismissModal(){ this.modal.dismiss(null, 'cancel'); + this.newItem = { + name: '', + quantity: null, + weight: null, + }; + this.isQuantity = false; } confirmModal(){ + if (this.newItem.name === ''){ + this.errorHandlerService.presentErrorToast('Please enter a name for the item', 'No name entered'); + return; + } + if ((this.newItem.quantity !== null && this.newItem.quantity < 0) || + (this.newItem.weight !== null && this.newItem.weight < 0)){ + this.errorHandlerService.presentErrorToast('Please enter a valid quantity or weight', 'Invalid quantity or weight'); + return; + } + if (this.newItem.quantity === null && this.newItem.weight === null){ + this.errorHandlerService.presentErrorToast('Please enter a quantity or weight', 'No quantity or weight entered'); + return; + } this.modal.dismiss(this.newItem, 'confirm'); } + + doRefresh(event : any){ + this.isLoading = true; + setTimeout(() => { + this.fetchItems(); + this.isLoading = false; + event.target.complete(); + }, 2000); + } + + search(event: any) { + this.searchTerm = event.detail.value; + } + + isVisible(itemName: String){ + // decides whether to show item based on search term + + if (!this.searchTerm) return true; + return itemName.toLowerCase().includes(this.searchTerm.toLowerCase()); + } + + changeSort(sort1: string, sort2: string){ + this.currentSort = this.currentSort === sort1 ? sort2 : sort1; + this.sortChanged(); + } + + sortChanged(): void { + switch (this.currentSort) { + case 'name-down': + // sort by name descending + this.sortNameDescending(); + break; + case 'name-up': + // sort by name ascending + this.sortNameAscending(); + break; + case 'amount-down': + // sort by amount descending + this.sortAmountDescending(); + break; + case 'amount-up': + // sort by amount ascending + this.sortAmountAscending(); + break; + default: + break; + } + } + + sortNameDescending(): void { + if (this.segment === 'pantry'){ + this.pantryItems.sort((a, b) => { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + }); + } else if (this.segment === 'shopping'){ + this.shoppingItems.sort((a, b) => { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + }); + } + } + + sortNameAscending(): void { + if (this.segment === 'pantry'){ + this.pantryItems.sort((a, b) => { + return a.name.toLowerCase() > b.name.toLowerCase() ? -1 : 1; + }); + } else if (this.segment === 'shopping'){ + this.shoppingItems.sort((a, b) => { + return a.name.toLowerCase() > b.name.toLowerCase() ? -1 : 1; + }); + } + } + + sortAmountDescending(): void { + if (this.segment === 'pantry'){ + this.pantryItems.sort((a, b) => { + return (a.quantity! + a.weight!) > (b.quantity! + b.weight!) ? -1 : 1; + }); + } else if (this.segment === 'shopping'){ + this.shoppingItems.sort((a, b) => { + return (a.quantity! + a.weight!) > (b.quantity! + b.weight!) ? -1 : 1; + }); + } + } + + sortAmountAscending(): void { + if (this.segment === 'pantry'){ + this.pantryItems.sort((a, b) => { + return (a.quantity! + a.weight!) < (b.quantity! + b.weight!) ? -1 : 1; + }); + } else if (this.segment === 'shopping'){ + this.shoppingItems.sort((a, b) => { + return (a.quantity! + a.weight!) < (b.quantity! + b.weight!) ? -1 : 1; + }); + } + } + } diff --git a/frontend/src/app/pages/profile/profile.page.html b/frontend/src/app/pages/profile/profile.page.html index 36bcf50f..4a6385e9 100644 --- a/frontend/src/app/pages/profile/profile.page.html +++ b/frontend/src/app/pages/profile/profile.page.html @@ -9,9 +9,9 @@ - + - Franko's profile + {{this.user.username}}'s Profile @@ -19,7 +19,7 @@ Goal - + @@ -31,7 +31,7 @@
Shopping Interval
-
{{ userpreferences.shopping_interval }}
+
{{ userpreferences.shoppingInterval }}
@@ -61,29 +61,29 @@
- + - Weekly + Weekly - Bi-Weekly + Bi-Weekly - Monthly + Monthly - Other... + Other... - + Every {{ shoppingIntervalOtherValue }} day(s) - + @@ -146,21 +146,21 @@
Set Preferences - +
- Vegetarian + Vegetarian - Vegan + Vegan - Gluten-intolerant + Gluten-intolerant - Lactose-intolerant + Lactose-intolerant @@ -189,7 +189,7 @@
Calorie Amount
-
{{ userpreferences.calorie_amount }}
+
{{ userpreferences.calorieAmount }}
@@ -219,8 +219,8 @@ - - + +
0
5000
@@ -250,7 +250,7 @@
Budget Range
-
{{ userpreferences.budget_range }}
+
{{ userpreferences.budgetRange }}
@@ -280,19 +280,19 @@ - Low Budget + Low Budget - Moderate Budget + Moderate Budget - High Budget + High Budget - Custom Amount + Custom Amount - + @@ -351,8 +351,8 @@ Macro Ratio: - - {{ userpreferences.macro_ratio.protein }} : {{ userpreferences.macro_ratio.carbs }} : {{ userpreferences.macro_ratio.fats }} + + {{ userpreferences.macroRatio.protein }} : {{ userpreferences.macroRatio.carbs }} : {{ userpreferences.macroRatio.fat }} @@ -362,7 +362,7 @@

- A macro ratio is the proportion of carbohydrates, proteins, and fats in a person's diet. It describes how much of each macronutrient contributes to the total calorie intake. Common ratios, like the "40-30-30" ratio, specify the percentage of calories from each macronutrient. It helps customize nutrition for goals like weight loss, muscle gain, or performance. Consulting a professional is advised for personalized macro ratios. + A macro ratio is the proportion of carbohydrates, proteins, and fat in a person's diet. It describes how much of each macronutrient contributes to the total calorie intake. Common ratios, like the "40-30-30" ratio, specify the percentage of calories from each macronutrient. It helps customize nutrition for goals like weight loss, muscle gain, or performance. Consulting a professional is advised for personalized macro ratios.

@@ -410,16 +410,16 @@ - Seafood + Seafood - Nuts + Nuts - Eggs + Eggs - Soy + Soy @@ -445,7 +445,7 @@
Cooking Time
-
{{ userpreferences.cooking_time }}
+
{{ userpreferences.cookingTime }}
@@ -474,15 +474,15 @@ - + - Quick + Quick - Medium + Medium - Long + Long @@ -512,7 +512,7 @@
BMI Calculator
-
{{ userpreferences.user_BMI }}
+
{{ userpreferences.userBMI }}
@@ -526,7 +526,7 @@ Cancel - Confirm + Confirm @@ -541,10 +541,10 @@ - Height (cm) + Height (cm) - Weight (kg) + Weight (kg) diff --git a/frontend/src/app/pages/profile/profile.page.spec.ts b/frontend/src/app/pages/profile/profile.page.spec.ts index f2d4b797..b4ebafa7 100644 --- a/frontend/src/app/pages/profile/profile.page.spec.ts +++ b/frontend/src/app/pages/profile/profile.page.spec.ts @@ -1,21 +1,33 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { IonicModule } from '@ionic/angular'; - import { ProfilePage } from './profile.page'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { AuthenticationService } from '../../services/services'; +import { UserI } from '../../models/interfaces'; + + describe('ProfilePage', () => { let component: ProfilePage; let fixture: ComponentFixture; + let mockAuthService: jasmine.SpyObj; + let mockUser: UserI; + + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + //removed ProfilePage from declarations + imports: [IonicModule.forRoot(), HttpClientTestingModule, RouterTestingModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA] // added this line - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ProfilePage, IonicModule], }).compileComponents(); fixture = TestBed.createComponent(ProfilePage); component = fixture.componentInstance; fixture.detectChanges(); - }); + })); it('should create', () => { expect(component).toBeTruthy(); diff --git a/frontend/src/app/pages/profile/profile.page.ts b/frontend/src/app/pages/profile/profile.page.ts index 9ce6b18c..0af001a3 100644 --- a/frontend/src/app/pages/profile/profile.page.ts +++ b/frontend/src/app/pages/profile/profile.page.ts @@ -1,11 +1,14 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { IonicModule, PickerController } from '@ionic/angular'; import { FormsModule } from '@angular/forms'; import { UserPreferencesI } from '../../models/userpreference.model'; import { CommonModule } from '@angular/common'; -import { RangeCustomEvent, RangeValue } from '@ionic/core'; import { Router } from '@angular/router'; +import { SettingsApiService } from '../../services/settings-api/settings-api.service'; + +import { UserI } from '../../models/user.model'; +import { AuthenticationService } from '../../services/services'; @Component({ @@ -15,55 +18,72 @@ import { Router } from '@angular/router'; standalone: true, imports: [IonicModule, FormsModule, CommonModule], }) -export class ProfilePage { - constructor( private router: Router , private pickerController: PickerController ) {this.selectedPriceRange = ''; -} - userpreferences : UserPreferencesI = { + +export class ProfilePage implements OnInit { + constructor( + private router: Router, + private pickerController: PickerController, + private settingsApiService: SettingsApiService, + private auth: AuthenticationService + ) { + this.selectedPriceRange = ''; + } + + // User data + user: UserI = { + username: '', + email: '', + password: '', + }; + + userpreferences: UserPreferencesI = { goal: '', - shopping_interval: '', - food_preferences: [], - calorie_amount: 0, - budget_range: '', - macro_ratio: {protein: 0, carbs: 0, fats: 0}, + shoppingInterval: '', + foodPreferences: [], + calorieAmount: 0, + budgetRange: '', + macroRatio: { protein: 0, carbs: 0, fat: 0 }, allergies: [], - cooking_time: 0, - user_height: 0, - user_weight: 0, - user_BMI: 0, - - BMI_set : false, - cookingtime_set : false, - allergies_set : false, - macro_set : false, - budget_set : false, - calorie_set : false, - foodpreferance_set : false, - shoppinginterfval_set : false, + cookingTime: '', + userHeight: 0, + userWeight: 0, + userBMI: 0, + + bmiset: false, + cookingTimeSet: false, + allergiesSet: false, + macroSet: false, + budgetSet: false, + calorieSet: false, + foodPreferenceSet: false, + shoppingIntervalSet: false, }; - //Variables for displaying + + // Variables for displaying displaying_Macroratio: string | undefined; shoppingIntervalOtherValue: number | undefined | any; - shopping_interval: string | any; - displayAllergies: string = ''; - displayPreferences: string = ''; + shoppingInterval: string | any; + displayAllergies: string[] | string = ''; + displayPreferences: string[] | string = '' ; selectedPreferences: string | any; selectedPriceRange: string; - - //check if possible to change + + // Check if possible to change preferences = { vegetarian: false, vegan: false, glutenIntolerant: false, - lactoseIntolerant: false + lactoseIntolerant: false, }; + allergens = { nuts: false, seafood: false, soy: false, - eggs: false + eggs: false, }; - //modal controllers + // Modal controllers isPreferencesModalOpen: boolean = false; isCalorieModalOpen: boolean = false; isBudgetModalOpen: boolean = false; @@ -82,13 +102,110 @@ export class ProfilePage { allergiesToggle: boolean = false; cookingToggle: boolean = false; BMIToggle: boolean = false; - - + ngOnInit() { + this.loadUserSettings(); + this.auth.getUser().subscribe({ + next: (response) => { + if (response.status == 200) { + if (response.body && response.body.name) { + this.user.username = response.body.name; + this.user.email = response.body.email; + this.user.password = response.body.password; + } + } + }, + error: (error) => { + console.log(error); + } + }) + } + private async loadUserSettings() { + this.settingsApiService.getSettings().subscribe({ + + + next: (response) => { + + if (response.status === 200) { + + if (response.body) { + + + this.userpreferences.goal = response.body.goal; + this.userpreferences.shoppingInterval = response.body.shoppingInterval; + this.userpreferences.foodPreferences = response.body.foodPreferences; + + if (response.body.calorieAmount == null) { + this.userpreferences.calorieAmount = 0; + } + else + this.userpreferences.calorieAmount = response.body.calorieAmount; - //function to navigate to account-profile page - navToProfile(){ + this.userpreferences.budgetRange = response.body.budgetRange; + this.userpreferences.allergies = response.body.allergies; + this.userpreferences.cookingTime = response.body.cookingTime; + this.userpreferences.userHeight = response.body.userHeight; + this.userpreferences.userWeight = response.body.userWeight; + this.userpreferences.userBMI = response.body.userBMI; + this.userpreferences.bmiset = response.body.bmiset; + this.userpreferences.cookingTimeSet = response.body.cookingTimeSet; + this.userpreferences.allergiesSet = response.body.allergiesSet; + this.userpreferences.macroSet = response.body.macroSet; + this.userpreferences.budgetSet = response.body.budgetSet; + this.userpreferences.calorieSet = response.body.calorieSet; + this.userpreferences.foodPreferenceSet = response.body.foodPreferenceSet; + this.userpreferences.shoppingIntervalSet = response.body.shoppingIntervalSet; + this.userpreferences.macroRatio.fat = response.body.macroRatio.fat; + this.userpreferences.macroRatio.carbs = response.body.macroRatio.carbs; + this.userpreferences.macroRatio.protein = response.body.macroRatio.protein; + + + + this.displayPreferences = this.userpreferences.foodPreferences; + this.displayAllergies = this.userpreferences.allergies; + this.displaying_Macroratio = this.getDisplayMacroratio(); + this.updateDisplayData(); + } + } + }, + error: (err) => { + if (err.status === 403) { + console.log('Unauthorized access. Please login again.', err); + this.router.navigate(['../']); + } else { + console.log('Error loading user settings', err); + } + + }, + }); + + + } + + setGoal() { + this.updateSettingsOnServer(); // Update the settings on the server when the goal is set + } + + + private updateSettingsOnServer() { + // console.log(this.userpreferences); + this.settingsApiService.updateSettings(this.userpreferences).subscribe( + (response) => { + if (response.status === 200) { + // Successfully updated settings on the server + console.log('Settings updated successfully'); + } + }, + (error) => { + // Handle error while updating settings + console.log('Error updating settings', error); + } + ); + } + + // Function to navigate to account-profile page + navToProfile() { this.router.navigate(['acc-profile']); } @@ -101,98 +218,118 @@ export class ProfilePage { setOpenPreferences(isOpen: boolean) { this.isPreferencesModalOpen = isOpen; } + setOpenPreferencesSave(isOpen: boolean) { - if (this.userpreferences.foodpreferance_set === true) { - if (this.preferences.vegetarian || this.preferences.vegan || this.preferences.glutenIntolerant || this.preferences.lactoseIntolerant) { - if (!isOpen) { - this.displayPreferences = this.getSelectedPreferences(); + if (this.userpreferences.foodPreferenceSet === true) { + if ( + this.preferences.vegetarian || + this.preferences.vegan || + this.preferences.glutenIntolerant || + this.preferences.lactoseIntolerant + ) { + if (!isOpen) { + this.updateDisplayData(); // Update the display data when the modal is closed + } + this.isPreferencesModalOpen = isOpen; } + } else if (this.userpreferences.foodPreferenceSet === false) { + this.userpreferences.foodPreferences = []; + this.displayPreferences = ''; this.isPreferencesModalOpen = isOpen; } + this.updateSettingsOnServer(); } - else if (this.userpreferences.foodpreferance_set === false) { - this.userpreferences.food_preferences = []; - this.displayPreferences = ''; - this.isPreferencesModalOpen = isOpen; - } + + preference_Toggle() { + this.userpreferences.foodPreferenceSet = !this.userpreferences.foodPreferenceSet; + this.updateSettingsOnServer(); } - pereference_Toggle() -{ - this.userpreferences.foodpreferance_set = !this.userpreferences.foodpreferance_set; -} - + getSelectedPreferences(): string { const selectedPreferences = []; - - if (this.preferences.vegetarian) { - selectedPreferences.push('Vegetarian'); - this.userpreferences.food_preferences.push('Vegetarian'); - } - if (this.preferences.vegan) { - selectedPreferences.push('Vegan'); - this.userpreferences.food_preferences.push('Vegan'); - } - if (this.preferences.glutenIntolerant) { - selectedPreferences.push('Gluten-intolerant'); - this.userpreferences.food_preferences.push('Gluten-intolerant'); - } - if (this.preferences.lactoseIntolerant) { - selectedPreferences.push('Lactose-intolerant'); - this.userpreferences.food_preferences.push('Lactose-intolerant'); + if (this.userpreferences.foodPreferences == null) { + this.userpreferences.foodPreferences = []; + return ''; } + else + { + this.userpreferences.foodPreferences = []; + if (this.preferences.vegetarian && !this.userpreferences.foodPreferences.includes('Vegetarian')) { + console.log("here") + selectedPreferences.push('Vegetarian'); + this.userpreferences.foodPreferences.push('Vegetarian'); + } + if (this.preferences.vegan && !this.userpreferences.foodPreferences.includes('Vegan')) { + selectedPreferences.push('Vegan'); + this.userpreferences.foodPreferences.push('Vegan'); + } + if (this.preferences.glutenIntolerant && !this.userpreferences.foodPreferences.includes('Gluten-intolerant')) { + selectedPreferences.push('Gluten-intolerant'); + this.userpreferences.foodPreferences.push('Gluten-intolerant'); + } + if (this.preferences.lactoseIntolerant && !this.userpreferences.foodPreferences.includes('Lactose-intolerant')) { + selectedPreferences.push('Lactose-intolerant'); + this.userpreferences.foodPreferences.push('Lactose-intolerant'); + } - if (selectedPreferences.length === 1) { - return selectedPreferences[0]; - } else if (selectedPreferences.length > 1) { - return 'Multiple'; - } else { - return ''; - + if (selectedPreferences.length === 1) { + return selectedPreferences[0]; + } else if (selectedPreferences.length > 1) { + return 'Multiple'; + } else { + return ''; + } + } } - - } + + setOpenCalorie(isOpen: boolean) { this.isCalorieModalOpen = isOpen; } + setOpenCalorieSave(isOpen: boolean) { - if (this.userpreferences.calorie_set === true) { - if (this.userpreferences.calorie_amount) { - if (!isOpen) { + if (this.userpreferences.calorieSet === true) { + if (this.userpreferences.calorieAmount) { + if (!isOpen) { + this.updateDisplayData(); // Update the display data when the modal is closed + } + this.isCalorieModalOpen = isOpen; + } + } else if (this.userpreferences.calorieSet === false) { + this.userpreferences.calorieAmount = 0; + this.isCalorieModalOpen = isOpen; } - this.isCalorieModalOpen = isOpen; + this.updateSettingsOnServer(); } -} -else if (this.userpreferences.calorie_set === false) { - this.userpreferences.calorie_amount = 0; - this.isCalorieModalOpen = isOpen; -} -} - calorieAmount_Toggle(){ - this.userpreferences.calorie_set = !this.userpreferences.calorie_set; + calorieAmount_Toggle() { + this.userpreferences.calorieSet = !this.userpreferences.calorieSet; + this.updateSettingsOnServer(); } showSelectedCalorieAmount(event: any) { - this.userpreferences.calorie_amount = event.target.value; + this.userpreferences.calorieAmount = event.target.value; } setOpenBudget(isOpen: boolean) { this.isBudgetModalOpen = isOpen; } + setOpenBudgetSave(isOpen: boolean) { - if (this.userpreferences.budget_set === true) { - this.userpreferences.budget_range = this.selectedPriceRange; + if (this.userpreferences.budgetSet === true) { + this.userpreferences.budgetRange = this.selectedPriceRange; this.isBudgetModalOpen = isOpen; - } - else if (this.userpreferences.budget_set === false) { - this.userpreferences.budget_range = ''; + } else if (this.userpreferences.budgetSet === false) { + this.userpreferences.budgetRange = ''; this.isBudgetModalOpen = isOpen; } + this.updateSettingsOnServer(); } - budgetRange_Toggle() - { - this.userpreferences.budget_set = !this.userpreferences.budget_set; + + budgetRange_Toggle() { + this.userpreferences.budgetSet = !this.userpreferences.budgetSet; + this.updateSettingsOnServer(); } async openPicker() { @@ -206,9 +343,8 @@ else if (this.userpreferences.calorie_set === false) { { text: '3', value: 3 }, { text: '4', value: 4 }, { text: '5', value: 5 }, - ], - selectedIndex: 0, // Set the default selected index + selectedIndex: this.userpreferences.macroRatio.protein, // Set the default selected index }, { name: 'carbs', @@ -218,21 +354,19 @@ else if (this.userpreferences.calorie_set === false) { { text: '3', value: 3 }, { text: '4', value: 4 }, { text: '5', value: 5 }, - ], - selectedIndex: 0, // Set the default selected index + selectedIndex: this.userpreferences.macroRatio.carbs, // Set the default selected index }, { - name: 'fats', + name: 'fat', options: [ { text: '1', value: 1 }, { text: '2', value: 2 }, { text: '3', value: 3 }, { text: '4', value: 4 }, { text: '5', value: 5 }, - ], - selectedIndex: 0, // Set the default selected index + selectedIndex: this.userpreferences.macroRatio.fat, // Set the default selected index }, ], buttons: [ @@ -244,9 +378,10 @@ else if (this.userpreferences.calorie_set === false) { text: 'Confirm', handler: (value) => { // Update the selected macro values based on the selected indexes - this.userpreferences.macro_ratio.protein = value['protein'].value; - this.userpreferences.macro_ratio.carbs = value['carbs'].value; - this.userpreferences.macro_ratio.fats = value['fats'].value; + this.userpreferences.macroRatio.protein = value['protein'].value; + this.userpreferences.macroRatio.carbs = value['carbs'].value; + this.userpreferences.macroRatio.fat = value['fat'].value; + this.updateSettingsOnServer(); }, }, ], @@ -254,152 +389,265 @@ else if (this.userpreferences.calorie_set === false) { await picker.present(); } + setOpenMacro(isOpen: boolean) { this.isMacroModalOpen = isOpen; } + setOpenMacroSave(isOpen: boolean) { - if (this.userpreferences.macro_set === true) { - if (!isOpen) { - this.displaying_Macroratio = this.userpreferences.macro_ratio.protein + ' : ' + this.userpreferences.macro_ratio.carbs + ' : ' + this.userpreferences.macro_ratio.fats; + if (this.userpreferences.macroSet === true) { + if (!isOpen) { + this.displaying_Macroratio = this.getDisplayMacroratio(); // Update the display data when the modal is closed + } + this.isMacroModalOpen = isOpen; + } else if (this.userpreferences.macroSet === false) { + this.userpreferences.macroRatio.protein = 0; + this.userpreferences.macroRatio.carbs = 0; + this.userpreferences.macroRatio.fat = 0; + this.displaying_Macroratio = ''; + this.isMacroModalOpen = isOpen; } - this.isMacroModalOpen = isOpen; - } - else if (this.userpreferences.macro_set === false) { - this.userpreferences.macro_ratio.protein = 0; - this.userpreferences.macro_ratio.carbs = 0; - this.userpreferences.macro_ratio.fats = 0; - this.displaying_Macroratio = ''; - this.isMacroModalOpen = isOpen; - } + this.updateSettingsOnServer(); } - macro_Toggle() - { - this.userpreferences.macro_set = !this.userpreferences.macro_set; + + macro_Toggle() { + this.userpreferences.macroSet = !this.userpreferences.macroSet; + this.updateSettingsOnServer(); } + setOpenAllergies(isOpen: boolean) { this.isAllergiesModalOpen = isOpen; } + setOpenAllergiesSave(isOpen: boolean) { - if (this.userpreferences.allergies_set === true) { - if (this.allergens.seafood || this.allergens.nuts || this.allergens.eggs || this.allergens.soy) { - + if (this.userpreferences.allergiesSet === true) { + if ( + this.allergens.seafood || + this.allergens.nuts || + this.allergens.eggs || + this.allergens.soy + ) { if (!isOpen) { - this.displayAllergies = this.getSelectedAllergens(); + this.displayAllergies = this.getSelectedAllergens(); // Update the display data when the modal is closed } - this.isAllergiesModalOpen = isOpen; + this.isAllergiesModalOpen = isOpen; } - } - else if (this.userpreferences.allergies_set === false) { + } else if (this.userpreferences.allergiesSet === false) { this.userpreferences.allergies = []; this.displayAllergies = ''; this.isAllergiesModalOpen = isOpen; } + this.updateSettingsOnServer(); + } + + allergies_Toggle() { + this.userpreferences.allergiesSet = !this.userpreferences.allergiesSet; + this.updateSettingsOnServer(); + } + + getSelectedAllergens(): string { + const selectedAllergens = []; + if (this.userpreferences.allergies == null) { + this.userpreferences.allergies = []; + return ''; } - allergies_Toggle() + else { - this.userpreferences.allergies_set = !this.userpreferences.allergies_set; + this.userpreferences.allergies = []; + + if (this.allergens.seafood && !this.userpreferences.allergies.includes('Seafood')) { + selectedAllergens.push('Seafood'); + this.userpreferences.allergies.push('Seafood'); + } + if (this.allergens.nuts && !this.userpreferences.allergies.includes('Nuts')) { + selectedAllergens.push('Nuts'); + this.userpreferences.allergies.push('Nuts'); + } + if (this.allergens.eggs && !this.userpreferences.allergies.includes('Eggs')) { + selectedAllergens.push('Eggs'); + this.userpreferences.allergies.push('Eggs'); + } + if (this.allergens.soy && !this.userpreferences.allergies.includes('Soy')) { + selectedAllergens.push('Soy'); + this.userpreferences.allergies.push('Soy'); } - getSelectedAllergens(): string { - const selectedAllergens = []; - - if (this.allergens.seafood) { - selectedAllergens.push('Seafood'); - this.userpreferences.allergies.push('Seafood') - } - if (this.allergens.nuts) { - selectedAllergens.push('Nuts'); - this.userpreferences.allergies.push('Nuts') - } - if (this.allergens.eggs) { - selectedAllergens.push('Eggs'); - this.userpreferences.allergies.push('Eggs') - } - if (this.allergens.soy) { - selectedAllergens.push('Soy'); - this.userpreferences.allergies.push('Soy') - } - - if (selectedAllergens.length === 1) { - console.log(this.displayAllergies); - return selectedAllergens[0]; - } else if (selectedAllergens.length > 1) { - return 'Multiple'; - } else { - console.log(this.displayAllergies); - return ''; - - } - - } + if (selectedAllergens.length === 1) { + return selectedAllergens[0]; + } else if (selectedAllergens.length > 1) { + return 'Multiple'; + } else { + return ''; + } + } + } + + setOpenCooking(isOpen: boolean) { this.isCookingModalOpen = isOpen; } - setOpenCookingSave(isOpen: boolean) { - if (this.userpreferences.cookingtime_set === true) { - this.isCookingModalOpen = isOpen; - } - else if (this.userpreferences.cookingtime_set === false){ - this.userpreferences.cooking_time = 0; + setOpenCookingSave(isOpen: boolean) { + if (this.userpreferences.cookingTimeSet === true) { + this.isCookingModalOpen = isOpen; + } else if (this.userpreferences.cookingTimeSet === false) { + this.userpreferences.cookingTime = ''; this.isCookingModalOpen = isOpen; } + this.updateSettingsOnServer(); } - cookingtime_Toggle() - { - this.userpreferences.cookingtime_set = !this.userpreferences.cookingtime_set; + + cookingtime_Toggle() { + this.userpreferences.cookingTimeSet = !this.userpreferences.cookingTimeSet; + this.updateSettingsOnServer(); } setOpenBMI(isOpen: boolean) { this.isBMIModalOpen = isOpen; } + setOpenBMISave(isOpen: boolean) { + if (this.userpreferences.userHeight && this.userpreferences.userWeight) { + this.updateDisplayData(); // Update the display data when the modal is closed + this.isBMIModalOpen = isOpen; - if (this.userpreferences.BMI_set === true) { - if (!isOpen) { - //call BMI calculation fuction - this.isBMIModalOpen = isOpen; + } + + + if (this.userpreferences.bmiset === false) { + this.userpreferences.userBMI = 0; + this.isBMIModalOpen = isOpen; } + this.updateSettingsOnServer(); } - else if (this.userpreferences.BMI_set === false) { - this.userpreferences.user_BMI = 0; - this.isBMIModalOpen = isOpen; - } -} - BMI_Toggle() - { - this.userpreferences.BMI_set = !this.userpreferences.BMI_set; + BMI_Toggle() { + this.userpreferences.bmiset = !this.userpreferences.bmiset; + this.updateSettingsOnServer(); } + setOpenShopping(isOpen: boolean) { this.isShoppingModalOpen = isOpen; } setOpenShoppingSave(isOpen: boolean) { - if (this.userpreferences.shoppinginterfval_set === true ) { - if (this.shopping_interval === 'other') { - this.userpreferences.shopping_interval = this.shoppingIntervalOtherValue.toString(); - } - else if (this.shopping_interval == 'weekly' || this.shopping_interval == 'biweekly' || this.shopping_interval == 'monthly') - { - this.userpreferences.shopping_interval = this.shopping_interval; + if (this.userpreferences.shoppingIntervalSet === true) { + if (this.shoppingInterval === 'other') { + this.userpreferences.shoppingInterval = this.shoppingIntervalOtherValue.toString(); + } else if ( + this.shoppingInterval == 'weekly' || + this.shoppingInterval == 'biweekly' || + this.shoppingInterval == 'monthly' + ) { + this.userpreferences.shoppingInterval = this.shoppingInterval; } - this.isShoppingModalOpen = isOpen; + this.isShoppingModalOpen = isOpen; + } else if (this.userpreferences.shoppingIntervalSet === false) { + this.userpreferences.shoppingInterval = ''; + this.isShoppingModalOpen = isOpen; + } + this.updateSettingsOnServer(); } - else if (this.userpreferences.shoppinginterfval_set === false){ - this.userpreferences.shopping_interval = ''; - this.isShoppingModalOpen = isOpen; + + shoppingInterval_Toggle() { + this.userpreferences.shoppingIntervalSet = !this.userpreferences.shoppingIntervalSet; + this.updateSettingsOnServer(); } -} -shoppingInterval_Toggle() -{ - this.userpreferences.shoppinginterfval_set = !this.userpreferences.shoppinginterfval_set; -} -} + // Function to update display data + updateDisplayData() { + if (this.userpreferences.shoppingInterval != '') { + this.shoppingintervalToggle = true + this.shoppingInterval = this.userpreferences.shoppingInterval; + this.userpreferences.shoppingIntervalSet = true; + } + + if (this.userpreferences.foodPreferences != null) { + this.preferenceToggle = true + if (this.userpreferences.foodPreferences.includes('Vegetarian')) { + this.preferences.vegetarian = true; + } + if (this.userpreferences.foodPreferences.includes('Vegan')) { + this.preferences.vegan = true; + } + if (this.userpreferences.foodPreferences.includes('Gluten-intolerant')) { + this.preferences.glutenIntolerant = true; + } + if (this.userpreferences.foodPreferences.includes('Lactose-intolerant')) { + this.preferences.lactoseIntolerant = true; + } + this.userpreferences.foodPreferenceSet = true; + } + + if (this.userpreferences.calorieAmount != 0) { + this.calorieToggle = true + this.userpreferences.calorieSet = true; + } + if (this.userpreferences.budgetRange != '') { + this.budgetToggle = true + this.selectedPriceRange = this.userpreferences.budgetRange; + this.userpreferences.budgetSet = true; + } + + if (this.userpreferences.macroRatio != null) { + this.macroToggle = true + + this.userpreferences.macroSet = true; + } + if (this.userpreferences.allergies != null) { + this.allergiesToggle = true + if (this.userpreferences.allergies.includes('Seafood')) { + this.allergens.seafood = true; + } + if (this.userpreferences.allergies.includes('Nuts')) { + this.allergens.nuts = true; + } + if (this.userpreferences.allergies.includes('Eggs')) { + this.allergens.eggs = true; + } + if (this.userpreferences.allergies.includes('Soy')) { + this.allergens.soy = true; + } + this.userpreferences.allergiesSet = true; + } + + if (this.userpreferences.userBMI != 0) { + this.BMIToggle = true + this.userpreferences.bmiset = true; + } + + if (this.userpreferences.cookingTime != '') { + this.cookingToggle = true + this.userpreferences.cookingTimeSet = true; + } + + this.displayPreferences = this.getSelectedPreferences(); + this.displaying_Macroratio = this.getDisplayMacroratio(); + this.displayAllergies = this.getSelectedAllergens(); + } + // Function to get the displaying macro ratio + getDisplayMacroratio(): string { + console.log(this.userpreferences.macroRatio) + if(this.userpreferences && this.userpreferences.macroRatio){ + return ( + this.userpreferences.macroRatio.protein + + ' : ' + + this.userpreferences.macroRatio.carbs + + ' : ' + + this.userpreferences.macroRatio.fat + ); + } else { + return "Not available"; + } +} +calculateBMI() { + this.userpreferences.userBMI = Math.round(this.userpreferences.userHeight / + this.userpreferences.userWeight); + } +} diff --git a/frontend/src/app/pages/recipe-book/recipe-book.page.html b/frontend/src/app/pages/recipe-book/recipe-book.page.html index e26a77e0..965154dc 100644 --- a/frontend/src/app/pages/recipe-book/recipe-book.page.html +++ b/frontend/src/app/pages/recipe-book/recipe-book.page.html @@ -11,20 +11,22 @@ - - - - +
- + - - - - + +
+ No saved recipes +
+
+ \ No newline at end of file diff --git a/frontend/src/app/pages/recipe-book/recipe-book.page.scss b/frontend/src/app/pages/recipe-book/recipe-book.page.scss index 27e50251..3c020fa4 100644 --- a/frontend/src/app/pages/recipe-book/recipe-book.page.scss +++ b/frontend/src/app/pages/recipe-book/recipe-book.page.scss @@ -28,4 +28,11 @@ border-radius: 10px; z-index: 1; } - \ No newline at end of file + + .empty { + padding-top: 35vh; + text-align:center; + font-size: large; + color: gray; + font-weight: bold; + } \ No newline at end of file diff --git a/frontend/src/app/pages/recipe-book/recipe-book.page.spec.ts b/frontend/src/app/pages/recipe-book/recipe-book.page.spec.ts index 161d8a6c..0fc54835 100644 --- a/frontend/src/app/pages/recipe-book/recipe-book.page.spec.ts +++ b/frontend/src/app/pages/recipe-book/recipe-book.page.spec.ts @@ -1,17 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RecipeBookPage } from './recipe-book.page'; -import { MealGenerationService } from '../../services/meal-generation/meal-generation.service'; +import { AuthenticationService, RecipeBookApiService } from '../../services/services'; describe('RecipeBookPage', () => { let component: RecipeBookPage; let fixture: ComponentFixture; - let mockMealGenerationService: jasmine.SpyObj; + let mockRecipeBookApiService: jasmine.SpyObj; + let authServiceSpy: jasmine.SpyObj; beforeEach(async() => { await TestBed.configureTestingModule({ imports: [RecipeBookPage], providers: [ - { provide: MealGenerationService, useValue: mockMealGenerationService }, + { provide: RecipeBookApiService, useValue: mockRecipeBookApiService }, + { provide: AuthenticationService, useValue: authServiceSpy }, ], }).compileComponents(); fixture = TestBed.createComponent(RecipeBookPage); diff --git a/frontend/src/app/pages/recipe-book/recipe-book.page.ts b/frontend/src/app/pages/recipe-book/recipe-book.page.ts index f6282e32..4911d610 100644 --- a/frontend/src/app/pages/recipe-book/recipe-book.page.ts +++ b/frontend/src/app/pages/recipe-book/recipe-book.page.ts @@ -1,69 +1,147 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { IonicModule } from '@ionic/angular'; -import { ModalController } from '@ionic/angular'; +import { ActionSheetController, IonicModule } from '@ionic/angular'; import { RecipeItemComponent } from '../../components/recipe-item/recipe-item.component'; +import { AuthenticationService, ErrorHandlerService, RecipeBookApiService } from '../../services/services'; +import { AddRecipeService } from '../../services/recipe-book/add-recipe.service'; import { MealI } from '../../models/meal.model'; -import { Router } from '@angular/router'; -import { MealGenerationService } from '../../services/meal-generation/meal-generation.service'; -import { ErrorHandlerService } from '../../services/services'; @Component({ selector: 'app-recipe-book', templateUrl: './recipe-book.page.html', styleUrls: ['./recipe-book.page.scss'], standalone: true, - imports: [IonicModule, CommonModule, FormsModule, RecipeItemComponent], + imports: [IonicModule, CommonModule, FormsModule, RecipeItemComponent] }) export class RecipeBookPage implements OnInit { - // items = [ - // { url: 'https://urls.unsplash.com/photo-1519708227418-c8fd9a32b7a2?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80', name: 'Salmon' }, - // { url: '/assets/img2.jpg', name: 'Stir-fry' }, - // { url: '/assets/img4.jpg', name: 'Pancakes' }, - // { url: '/assets/img3.jpg', name: 'Raspberry Fruit Salad' } - // ]; + @ViewChild(RecipeItemComponent) recipeItem!: RecipeItemComponent; + public items: MealI[] = []; - meals: MealI[] = []; + constructor(private recipeService: RecipeBookApiService, + private errorHandlerService: ErrorHandlerService, + private auth: AuthenticationService, + private actionSheetController: ActionSheetController, + private addService: AddRecipeService) { } - constructor( - private modalController: ModalController, - public r: Router, - private mealGenerationservice: MealGenerationService, - private errorHandlerService: ErrorHandlerService - ) {} - - async openModal(item: any) { - const modal = await this.modalController.create({ - component: RecipeItemComponent, - componentProps: { - url: item.url, - name: item.name, - }, - }); - await modal.present(); + async ionViewWillEnter() { + this.getRecipes(); } - - async ngOnInit() { - for (let index = 0; index < 4; index++) { - this.mealGenerationservice.getMeal().subscribe({ - next: (data) => { - if (Array.isArray(data)) { - this.meals.push(...data); - } else { - this.meals.push(data); + async addRecipe(item: MealI) { + this.recipeService.addRecipe(item).subscribe({ + next: (response) => { + if (response.status === 200) { + if (response.body) { + this.getRecipes(); + this.errorHandlerService.presentSuccessToast(item.name + " added to Recipe Book"); + } } - - console.log(this.meals); }, error: (err) => { + if (err.status === 403) { + this.errorHandlerService.presentErrorToast( + 'Unauthorised access. Please log in again.', + err + ) + this.auth.logout(); + } else { + this.errorHandlerService.presentErrorToast( + 'Error adding item to your Recipe Book', + err + ) + } + } + }); + } + + async getRecipes() { + this.recipeService.getAllRecipes().subscribe({ + next: (response) => { + if (response.status === 200) { + if (response.body) { + this.items = response.body; + this.recipeItem.passItems(this.items); + } + } + }, + error: (err) => { + if (err.status === 403) { this.errorHandlerService.presentErrorToast( - 'Error loading recipe items', + "Unauthorised access. Please log in again", err - ); + ) + this.auth.logout(); + } else { + this.errorHandlerService.presentErrorToast( + 'Error loading saved recipes', + err + ) + } + } + }) + } + + async confirmRemove(event: Event, recipe: MealI) { + event.stopPropagation(); + + const actionSheet = await this.actionSheetController.create({ + header: `Are you sure you want to remove ${recipe.name} from your recipe book?`, + buttons: [ + { + text: 'Delete', + role: 'destructive', + handler: () => { + this.removeRecipe(recipe); + } }, - }); - } + { + text: 'Cancel', + role: 'cancel' + } + ] + }); + + await actionSheet.present(); + } + + async removeRecipe(recipe: MealI) { + this.recipeService.removeRecipe(recipe).subscribe({ + next: (response) => { + if (response.status === 200) { + this.errorHandlerService.presentSuccessToast( + `Successfully removed ${recipe.name}` + ) + this.getRecipes(); + } + }, + error: (err) => { + if (err.status === 403) { + this.errorHandlerService.presentErrorToast( + 'Unauthorised access. Please log in again', + err + ) + this.auth.logout(); + } else { + this.errorHandlerService.presentErrorToast( + 'Error removing recipe from Recipe Book', + err + ) + } + } + }); + } + + handleEvent(data: MealI) { + this.addRecipe(data); + } + + ngOnInit() { + this.addService.recipeItem$.subscribe((recipeItem) => { + if (recipeItem) { + this.addRecipe(recipeItem); + } + }); } + } diff --git a/frontend/src/app/pages/signup/signup.page.html b/frontend/src/app/pages/signup/signup.page.html index 5e056818..3659add0 100644 --- a/frontend/src/app/pages/signup/signup.page.html +++ b/frontend/src/app/pages/signup/signup.page.html @@ -1,90 +1,83 @@ - - - - - - - - - - -
-
- - +
+ +
+ +
+
+ + + + +
+ Name is required. +
+ + +
+ Please enter a valid email. +
+ -
- Name is required. +
+ Password must be at least 8 characters long.
- -
- Please enter a valid email. +
+ Passwords must match.
- - -
- Password must be at least 8 characters long. -
- - -
- Passwords must match. -
- - - - - Sign up - - - - + - Already have an account? Login + + + Sign up + + - - - - \ No newline at end of file + + + Already have an account? Login + + + + \ No newline at end of file diff --git a/frontend/src/app/pages/signup/signup.page.integration.spec.ts b/frontend/src/app/pages/signup/signup.page.integration.spec.ts new file mode 100644 index 00000000..410f8bae --- /dev/null +++ b/frontend/src/app/pages/signup/signup.page.integration.spec.ts @@ -0,0 +1,115 @@ +import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing" +import { AuthenticationService, ErrorHandlerService } from "../../services/services"; +import { SignupPage } from "./signup.page"; +import { UserI } from "../../models/user.model"; +import { TestBed } from "@angular/core/testing"; +import { IonicModule } from "@ionic/angular"; +import { RouterTestingModule } from "@angular/router/testing"; +import { Router } from "@angular/router"; +import { Component } from "@angular/core"; + +describe('SignupPageIntegration', () => { + let httpMock: HttpTestingController; + let auth: AuthenticationService; + let errorHandler: ErrorHandlerService; + let component: SignupPage; + let routerSpy = {navigate: jasmine.createSpy('navigate')}; + let apiUrl = 'http://localhost:8080'; + let mockUser: UserI; + let mockForm: any; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [IonicModule.forRoot(), HttpClientTestingModule, RouterTestingModule.withRoutes([ + {path: 'app/tabs/home', component: DummyComponent} + ])], + providers: [ + AuthenticationService, + ErrorHandlerService, + { provide: Router, useValue: routerSpy }, + SignupPage + ] + }).compileComponents(); + + httpMock = TestBed.inject(HttpTestingController); + auth = TestBed.inject(AuthenticationService); + errorHandler = TestBed.inject(ErrorHandlerService); + component = TestBed.inject(SignupPage); + + mockUser = { + username: 'test', + password: 'test', + email: 'test@test.com' + }; + + mockForm = { + username: 'test', + initial: 'test', + verify: 'test', + email: 'test@test.com' + } + }) + + afterEach(() => { + httpMock.verify(); + }); + + it('should signup a user and navigate to home', async () => { + spyOn(auth, 'register').and.callThrough(); + spyOn(errorHandler, 'presentSuccessToast').and.callThrough(); + + await component.signup(mockForm); + + const req = httpMock.expectOne(apiUrl + '/register'); + expect(req.request.method).toBe('POST'); + req.flush({token: 'testToken'}, {status: 200, statusText: 'OK'}); + + expect(auth.register).toHaveBeenCalledWith(mockUser); + expect(errorHandler.presentSuccessToast).toHaveBeenCalled(); + expect(routerSpy.navigate).toHaveBeenCalledWith(['app/tabs/home']); + }); + + it('should display an error if the user entered passwords that do not match', async () => { + spyOn(errorHandler, 'presentErrorToast').and.callThrough(); + spyOn(auth, 'register').and.callThrough(); + + mockForm.verify = 'notTest'; + + await component.signup(mockForm); + + expect(errorHandler.presentErrorToast).toHaveBeenCalled(); + expect(auth.register).not.toHaveBeenCalled(); + }); + + it('should display an error if the email already exists', async () => { + spyOn(errorHandler, 'presentErrorToast').and.callThrough(); + spyOn(auth, 'register').and.callThrough(); + + await component.signup(mockForm); + + const req = httpMock.expectOne(apiUrl + '/register'); + expect(req.request.method).toBe('POST'); + req.flush({error: 'Email already exists'}, {status: 400, statusText: 'Bad Request'}); + + expect(errorHandler.presentErrorToast).toHaveBeenCalled(); + expect(auth.register).toHaveBeenCalledWith(mockUser); + }); + + it('should display an error if there is a server error', async () => { + spyOn(errorHandler, 'presentErrorToast').and.callThrough(); + spyOn(auth, 'register').and.callThrough(); + + await component.signup(mockForm); + + const req = httpMock.expectOne(apiUrl + '/register'); + expect(req.request.method).toBe('POST'); + req.flush({error: 'Server error'}, {status: 500, statusText: 'Internal Server Error'}); + + expect(errorHandler.presentErrorToast).toHaveBeenCalled(); + expect(auth.register).toHaveBeenCalledWith(mockUser); + }); + +}); + +@Component({template: ''}) +class DummyComponent {} \ No newline at end of file diff --git a/frontend/src/app/pages/signup/signup.page.scss b/frontend/src/app/pages/signup/signup.page.scss index 42c826bf..ce3f1e05 100644 --- a/frontend/src/app/pages/signup/signup.page.scss +++ b/frontend/src/app/pages/signup/signup.page.scss @@ -1,10 +1,39 @@ -.logo { +.logo-container { + display: flex; + justify-content: center; align-items: center; - margin-top: 7vh; - margin-left: 5vw; - // border-radius: 50%; - height: 17vh; - width: 37vw; + margin-top: 5vh; +} + +.logo{ + width: 200px; + height: 200px; + z-index: 1; +} + +.background-image { + position: fixed; + animation: slide 2s linear infinite; + + background: radial-gradient( + circle, rgba(255,255,255,0) 0%, + rgba(255,255,255,0) 10%, + rgba(255,127,80,0.3) 13%, + rgba(255,127,80,0.3) 15%, + rgba(255,255,255,0) 19% + ); + background-size: 40px 40px; + height: 100vh; + width: 100vw; +} + +@keyframes slide{ + 0%{ + background-position: 40px 0; + } + 100%{ + background-position: 0 40px; + } } .firstinput { @@ -23,13 +52,12 @@ } ion-input{ - //--background: #8a8a8a; - --background : var(--ion-input-background); + --background: #d3d3d3be; + // --background : var(--ion-input-background); --border-radius: 20px; - --border-width: 0px; --color: black; --padding-bottom: 20px; - --padding-top: 15px; + --padding-top: 20px; --padding-start: 20px; max-width: 85%; margin: 0 auto; @@ -45,7 +73,7 @@ ion-button { --padding-start: 20px; width: 85vw; height: 7vh; - margin-top: 8vh; + // margin-top: 8vh; font-size: 2.5vh; } @@ -58,4 +86,5 @@ a { .loginlink { margin-top: 3vh; + z-index: 10; } \ No newline at end of file diff --git a/frontend/src/app/pages/signup/signup.page.spec.ts b/frontend/src/app/pages/signup/signup.page.spec.ts index 3e2cbd24..e3177bab 100644 --- a/frontend/src/app/pages/signup/signup.page.spec.ts +++ b/frontend/src/app/pages/signup/signup.page.spec.ts @@ -1,6 +1,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SignupPage } from './signup.page'; import { AuthenticationService } from '../../services/services'; +import { AuthResponseI } from '../../models/authResponse.model'; +import { HttpResponse } from '@angular/common/http'; +import { IonicModule } from '@ionic/angular'; +import { RouterTestingModule } from '@angular/router/testing'; +import { of } from 'rxjs'; +import { Component } from '@angular/core'; describe('SignupPage', () => { let component: SignupPage; @@ -8,10 +14,17 @@ describe('SignupPage', () => { let mockAuthenicationService: jasmine.SpyObj; beforeEach(async () => { - mockAuthenicationService = jasmine.createSpyObj('AuthenticationService', ['signup', 'checkUser']); + mockAuthenicationService = jasmine.createSpyObj('AuthenticationService', ['register', 'setToken']); + + const response = new HttpResponse({ body: { token: 'test' }, status: 200 }); + + mockAuthenicationService.register.and.returnValue(of(response)); await TestBed.configureTestingModule({ - imports: [SignupPage], + imports: [SignupPage, IonicModule, RouterTestingModule.withRoutes([ + {path: 'app/tabs/home', component: DummyComponent} + ]) + ], providers: [ { provide: AuthenticationService, useValue: mockAuthenicationService }, ], @@ -25,4 +38,18 @@ describe('SignupPage', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should call register', () => { + let mockUser = { + username: 'test', + password: 'test', + email: 'test@test.com' + }; + component.signup(mockUser); + expect(mockAuthenicationService.register).toHaveBeenCalled(); + }); }); + + +@Component({template: ''}) +class DummyComponent {} \ No newline at end of file diff --git a/frontend/src/app/pages/signup/signup.page.ts b/frontend/src/app/pages/signup/signup.page.ts index abb6fcde..8969a4c6 100644 --- a/frontend/src/app/pages/signup/signup.page.ts +++ b/frontend/src/app/pages/signup/signup.page.ts @@ -25,7 +25,6 @@ export class SignupPage { constructor(private router: Router, private errorHandlerService: ErrorHandlerService, private auth: AuthenticationService ) { } async signup(form: any) { - console.log(form); if (form.initial !== form.verify) { this.errorHandlerService.presentErrorToast('Passwords do not match', 'Passwords do not match'); return; @@ -37,29 +36,28 @@ export class SignupPage { email: form.email, } - this.auth.checkUser(newUser).subscribe({ - next: data => { - if (data) { - this.errorHandlerService.presentErrorToast('Username or email already exists', 'Username or email already exists'); - } else { - this.auth.createUser(newUser).subscribe({ - next: () => { - this.errorHandlerService.presentSuccessToast('Signup successful'); + this.auth.register(newUser).subscribe({ + next: (response) => { + if (response.status == 200) { + if (response.body) { + this.auth.setToken(response.body.token); + this.errorHandlerService.presentSuccessToast('Registration successful'); this.router.navigate(['app/tabs/home']); - }, - error: error => { - this.errorHandlerService.presentErrorToast('Signup failed', error); - } - }); - } + } + } }, - error: error => { - this.errorHandlerService.presentErrorToast('Signup failed', error); + error: (error) => { + if (error.status == 400){ + this.errorHandlerService.presentErrorToast('Email already exists', 'Email already exists'); + }else{ + this.errorHandlerService.presentErrorToast('Unexpected error. Please try again', error); + } } }); } goToLogin() { this.router.navigate(['../']); + localStorage.removeItem('token'); } } diff --git a/frontend/src/app/pages/tabs/tabs.page.html b/frontend/src/app/pages/tabs/tabs.page.html index 0ffb7d61..b4ced814 100644 --- a/frontend/src/app/pages/tabs/tabs.page.html +++ b/frontend/src/app/pages/tabs/tabs.page.html @@ -1,5 +1,5 @@ - + Recipe Book diff --git a/frontend/src/app/pages/tabs/tabs.page.scss b/frontend/src/app/pages/tabs/tabs.page.scss index 8b137891..03e8fa0a 100644 --- a/frontend/src/app/pages/tabs/tabs.page.scss +++ b/frontend/src/app/pages/tabs/tabs.page.scss @@ -1 +1,11 @@ +.bar{ + padding-top: 1vh; +} +ion-tab-button { +transition: transform 0.2s ease-in-out; +} + +ion-tab-button.tab-selected { +transform: translateY(-5px); +} \ No newline at end of file diff --git a/frontend/src/app/pages/tabs/tabs.page.ts b/frontend/src/app/pages/tabs/tabs.page.ts index a0205f33..e34d563c 100644 --- a/frontend/src/app/pages/tabs/tabs.page.ts +++ b/frontend/src/app/pages/tabs/tabs.page.ts @@ -1,3 +1,4 @@ +import { CommonModule } from '@angular/common'; import { Component, EnvironmentInjector, inject } from '@angular/core'; import { IonicModule } from '@ionic/angular'; @@ -6,7 +7,7 @@ import { IonicModule } from '@ionic/angular'; templateUrl: 'tabs.page.html', styleUrls: ['tabs.page.scss'], standalone: true, - imports: [IonicModule], + imports: [IonicModule, CommonModule], }) export class TabsPage { public environmentInjector = inject(EnvironmentInjector); diff --git a/frontend/src/app/services/authentication/authentication.service.spec.ts b/frontend/src/app/services/authentication/authentication.service.spec.ts index 3aa54a13..7cf6882b 100644 --- a/frontend/src/app/services/authentication/authentication.service.spec.ts +++ b/frontend/src/app/services/authentication/authentication.service.spec.ts @@ -1,37 +1,46 @@ import { of } from 'rxjs'; import { AuthenticationService } from './authentication.service'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpResponse } from '@angular/common/http'; import { UserI } from '../../models/interfaces'; +import { AuthResponseI } from '../../models/authResponse.model'; describe('AuthenticationService', () => { let service: AuthenticationService; let httpClientSpy: jasmine.SpyObj; + let routerSpy: jasmine.SpyObj; + let mockUser: UserI; + let mockAuthResponse: AuthResponseI; beforeEach(() => { httpClientSpy = jasmine.createSpyObj('HttpClient', ['post']); - service = new AuthenticationService(httpClientSpy as any); - }); + routerSpy = jasmine.createSpyObj('Router', ['navigate']); + service = new AuthenticationService(httpClientSpy as any, routerSpy as any); - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - it('should return true if user is able to login', (done: DoneFn) => { - const mockUser: UserI = { + mockUser = { "username": "test", "email": "test@example.com", "password": "test" }; - const expectedResponse: boolean = true; + mockAuthResponse = { + "token": "test", + } - httpClientSpy.post.and.returnValue(of(expectedResponse)); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should login user', (done: DoneFn) => { + + httpClientSpy.post.and.returnValue(of(new HttpResponse({ body: mockAuthResponse }))); service.login(mockUser).subscribe({ next: response => { - expect(response) + expect(response.body) .withContext('expected response') - .toEqual(expectedResponse); + .toEqual(mockAuthResponse); done(); }, error: done.fail @@ -42,22 +51,15 @@ describe('AuthenticationService', () => { .toBe(1); }); - it('#checkUser should return true if user exists', (done: DoneFn) => { - const mockUser: UserI = { - "username": "test", - "email": "test@example.com", - "password": "test" - }; + it('should register user', (done: DoneFn) => { - const expectedResponse: boolean = true; + httpClientSpy.post.and.returnValue(of(new HttpResponse({ body: mockAuthResponse }))); - httpClientSpy.post.and.returnValue(of(expectedResponse)); - - service.checkUser(mockUser).subscribe({ + service.register(mockUser).subscribe({ next: response => { - expect(response) + expect(response.body) .withContext('expected response') - .toEqual(expectedResponse); + .toEqual(mockAuthResponse); done(); }, error: done.fail @@ -68,22 +70,15 @@ describe('AuthenticationService', () => { .toBe(1); }); - it('#createUser should return void', (done: DoneFn) => { - const mockUser: UserI = { - "username": "test", - "email": "test@example.com", - "password": "test" - }; - - const expectedResponse: void = undefined; + it('should find user', (done: DoneFn) => { - httpClientSpy.post.and.returnValue(of(expectedResponse)); + httpClientSpy.post.and.returnValue(of(new HttpResponse({ body: mockUser }))); - service.createUser(mockUser).subscribe({ + service.findUser(mockUser.email).subscribe({ next: response => { - expect(response) + expect(response.body) .withContext('expected response') - .toEqual(expectedResponse); + .toEqual(mockUser); done(); }, error: done.fail @@ -94,4 +89,13 @@ describe('AuthenticationService', () => { .toBe(1); }); + it('should set token', () => { + const token = 'test'; + service.setToken(token); + + expect(localStorage.getItem('token')) + .withContext('token set') + .toEqual(token); + }); + }); diff --git a/frontend/src/app/services/authentication/authentication.service.ts b/frontend/src/app/services/authentication/authentication.service.ts index faa21a73..a8961dce 100644 --- a/frontend/src/app/services/authentication/authentication.service.ts +++ b/frontend/src/app/services/authentication/authentication.service.ts @@ -1,7 +1,9 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { UserI } from '../../models/interfaces'; +import { AuthResponseI } from '../../models/authResponse.model'; +import { Router } from '@angular/router'; @Injectable({ providedIn: 'root' @@ -10,45 +12,66 @@ export class AuthenticationService { url : String = 'http://localhost:8080'; - constructor(private http: HttpClient) { } + constructor(private http: HttpClient, public r: Router) { } - login(user: UserI): Observable { - return this.http.post( - this.url+'/login', + login(user: UserI): Observable> { + return this.http.post( + this.url+'/authenticate', { - "username": user.username, "email":user.email, "password": user.password - }); + }, + {observe: 'response'}); } - checkUser(user: UserI): Observable { - return this.http.post( - this.url+'/checkUser', + register(user: UserI): Observable> { + return this.http.post( + this.url+'/register', { "username": user.username, "email":user.email, "password": user.password - }); + }, + {observe: 'response'}); } - createUser(user: UserI): Observable { - return this.http.post( - this.url+'/createUser', + findUser(email: string): Observable> { + return this.http.post( + this.url+'/findByEmail', { - "username": user.username, - "email":user.email, - "password": user.password - }); + "username": '', + "email": email, + "password": '' + }, + {observe: 'response'}); } - getUser(email: string): Observable { + updateUser(user: UserI): Observable> { return this.http.post( - this.url+'/getUser', + this.url+'/updateUser', { - "username": '', - "email": email, + "username": user.username, + "email": '', "password": '' - }); + }, + {observe: 'response'}); + } + + getUser(): Observable> { + return this.http.get( + this.url+'/getUser', + {observe: 'response'}); } + + setToken(token: string): void { + if (token){ + localStorage.setItem('token', token); + } + } + + logout(): void { + localStorage.removeItem('token'); + this.r.navigate(['../']); + } + } diff --git a/frontend/src/app/services/authentication/jwt.interceptor.spec.ts b/frontend/src/app/services/authentication/jwt.interceptor.spec.ts new file mode 100644 index 00000000..8129966f --- /dev/null +++ b/frontend/src/app/services/authentication/jwt.interceptor.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { JwtInterceptor } from './jwt.interceptor'; + +describe('JwtInterceptor', () => { + beforeEach(() => TestBed.configureTestingModule({ + providers: [ + JwtInterceptor + ] + })); + + it('should be created', () => { + const interceptor: JwtInterceptor = TestBed.inject(JwtInterceptor); + expect(interceptor).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/authentication/jwt.interceptor.ts b/frontend/src/app/services/authentication/jwt.interceptor.ts new file mode 100644 index 00000000..7bd2b529 --- /dev/null +++ b/frontend/src/app/services/authentication/jwt.interceptor.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; +import { + HttpRequest, + HttpHandler, + HttpEvent, + HttpInterceptor +} from '@angular/common/http'; +import { Observable } from 'rxjs'; + +@Injectable() +export class JwtInterceptor implements HttpInterceptor { + + constructor() {} + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + const token = localStorage.getItem('token'); + if (token) { + const cloned = request.clone({ + setHeaders: { + Authorization: `Bearer ${token}` + } + }); + return next.handle(cloned); + } else { + return next.handle(request); + } + } +} diff --git a/frontend/src/app/services/meal-generation/meal-generation.service.ts b/frontend/src/app/services/meal-generation/meal-generation.service.ts index 787065e7..8622fca4 100644 --- a/frontend/src/app/services/meal-generation/meal-generation.service.ts +++ b/frontend/src/app/services/meal-generation/meal-generation.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable, catchError, concatMap, forkJoin, from, map, switchMap, tap } from 'rxjs'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable, catchError, map, tap } from 'rxjs'; import { MealI } from '../../models/meal.model'; -import { DaysMealsI, FoodItemI, UserI } from '../../models/interfaces'; +import { DaysMealsI, FoodItemI, UserI, MealBrowseI } from '../../models/interfaces'; import { title } from 'process'; +import { request } from 'http'; @Injectable({ providedIn: 'root' @@ -16,30 +17,57 @@ export class MealGenerationService { password: '', } + url : String = 'http://localhost:8080'; - url : String = 'http://localhost:8080'; constructor(private http: HttpClient) { } - getDailyMeals():Observable { - return this.http.get( - this.url+'/getDaysMeals') - // .pipe( - // // Adjust the property name according to the actual response structure - // map((daysMeals: DaysMealsI[]) => { - // return this.retrieveImageUrls(daysMeals).pipe( - // map((updatedUrls: string[]) => this.updateMealUrls(daysMeals, updatedUrls)) - // ); - // }) - // ); + getDailyMeals(dayOfWeek : String):Observable { + return this.http.post( + this.url+'/getDaysMeals', + { + "dayOfWeek" : dayOfWeek.toUpperCase(), + } + ); } + + // handleArchive(daysMeals: DaysMealsI, meal: string): Observable { + // // const headers = new HttpHeaders({ + // // 'Content-Type': 'application/json' + // // }); + // return this.http.post(this.url + '/regenerate', daysMeals).pipe( + // catchError((error) => { + // // Handle errors if the request fails + // console.error('Error:', error); + // throw error; + // }), + // map((response) => { + // // Return the updated JSON object from the server + // return response; + // }) + // ); + // } + handleArchive(daysMeal: DaysMealsI, meal: String): Observable { + return this.http.post( + this.url+'/regenerate', + { + "breakfast": daysMeal.breakfast, + "lunch": daysMeal.lunch, + "dinner": daysMeal.dinner, + "mealDate": daysMeal?.mealDate?.toUpperCase(), + "meal": meal + }); + } - getMeal():Observable { - return this.http.get( - this.url+'/getMeal' - ); + // Helper function to get the headers (if needed) + private getHeaders() { + return new HttpHeaders({ + 'Content-Type': 'application/json' // Set the content type of the request + // Add any other headers if required + }); } + // private retrieveImageUrls(daysMeals: DaysMealsI[]): Observable { // const imageRequests: Observable[] = []; @@ -74,4 +102,29 @@ export class MealGenerationService { // } + getMeal():Observable { + return this.http.get( + this.url+'/getMeal' + ); + } + + getPopularMeals():Observable { + return this.http.get( + this.url+'/getPopularMeals', + // {}, + // {observe: 'response'} + ); + } + + getSearchedMeals(query: string): Observable { + const params = { query: query }; // backend expects the query parameter + return this.http.get( + this.url + '/getSearchedMeals', + { params: params }); + } + + + + + } diff --git a/frontend/src/app/services/pantry-api/pantry-api.service.spec.ts b/frontend/src/app/services/pantry-api/pantry-api.service.spec.ts index d4bd2a2e..eee0cac3 100644 --- a/frontend/src/app/services/pantry-api/pantry-api.service.spec.ts +++ b/frontend/src/app/services/pantry-api/pantry-api.service.spec.ts @@ -1,7 +1,7 @@ import { of } from 'rxjs'; import { FoodItemI } from '../../models/interfaces'; import { PantryApiService } from './pantry-api.service'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpResponse } from '@angular/common/http'; describe('PantryApiService', () => { let service: PantryApiService; @@ -35,11 +35,11 @@ describe('PantryApiService', () => { } ]; - httpClientSpy.post.and.returnValue(of(expectedItems)); + httpClientSpy.post.and.returnValue(of(new HttpResponse({body: expectedItems}))); service.getPantryItems().subscribe({ - next: items => { - expect(items) + next: res => { + expect(res.body) .withContext('expected items') .toEqual(expectedItems); done(); @@ -59,11 +59,11 @@ describe('PantryApiService', () => { "weight": 0.1, }; - httpClientSpy.post.and.returnValue(of(expectedItem)); + httpClientSpy.post.and.returnValue(of(new HttpResponse({body: expectedItem}))); service.addToPantry(expectedItem).subscribe({ - next: item => { - expect(item) + next: res => { + expect(res.body) .withContext('expected item') .toEqual(expectedItem); done(); @@ -83,13 +83,13 @@ describe('PantryApiService', () => { "weight": 0.1, }; - httpClientSpy.post.and.returnValue(of(expectedItem)); + httpClientSpy.post.and.returnValue(of(new HttpResponse({status: 200}))); service.deletePantryItem(expectedItem).subscribe({ - next: item => { - expect(item) - .withContext('expected item') - .toEqual(expectedItem); + next: res => { + expect(res.status) + .withContext('expected HTTP status code 200') + .toEqual(200); done(); }, error: done.fail @@ -107,13 +107,13 @@ describe('PantryApiService', () => { "weight": 0.1, }; - httpClientSpy.post.and.returnValue(of(expectedItem)); + httpClientSpy.post.and.returnValue(of(new HttpResponse({status: 200}))); service.updatePantryItem(expectedItem).subscribe({ - next: item => { - expect(item) - .withContext('expected item') - .toEqual(expectedItem); + next: res => { + expect(res.status) + .withContext('expected HTTP status code 200') + .toEqual(200); done(); }, error: done.fail diff --git a/frontend/src/app/services/pantry-api/pantry-api.service.ts b/frontend/src/app/services/pantry-api/pantry-api.service.ts index 02a828a9..7baf460b 100644 --- a/frontend/src/app/services/pantry-api/pantry-api.service.ts +++ b/frontend/src/app/services/pantry-api/pantry-api.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { FoodItemI, UserI } from '../../models/interfaces'; +import { FoodItemI } from '../../models/interfaces'; @Injectable({ @@ -9,71 +9,48 @@ import { FoodItemI, UserI } from '../../models/interfaces'; }) export class PantryApiService { - user: UserI = { - username: localStorage.getItem('user') ?? '', - email: localStorage.getItem('email') ?? '', - password: '', - } - url : String = 'http://localhost:8080'; constructor(private http: HttpClient) { } - getPantryItems(): Observable { + getPantryItems(): Observable> { return this.http.post( this.url+'/getPantry', - { - "username": this.user.username, - "email": this.user.email - }); + {}, + {observe: 'response'}); } - addToPantry(item: FoodItemI): Observable { + addToPantry(item: FoodItemI): Observable> { return this.http.post( this.url+'/addToPantry', { - "food": { - "name": item.name, - "quantity": item.quantity, - "weight": item.weight, - }, - "user": { - "username": this.user.username, - "email": this.user.email - } - }); + "name": item.name, + "quantity": item.quantity, + "weight": item.weight, + }, + {observe: 'response'}); } - updatePantryItem(item: FoodItemI): Observable { - return this.http.post( + updatePantryItem(item: FoodItemI): Observable> { + return this.http.post( this.url+'/updatePantry', { - "food": { - "name": item.name, - "quantity": item.quantity, - "weight": item.weight, - }, - "user": { - "username": this.user.username, - "email": this.user.email - } - }); + "name": item.name, + "quantity": item.quantity, + "weight": item.weight, + }, + {observe: 'response'}); } - deletePantryItem(item: FoodItemI): Observable { - return this.http.post( + deletePantryItem(item: FoodItemI): Observable> { + return this.http.post( this.url+'/removeFromPantry', { - "food": { - "name": item.name, - "quantity": item.quantity, - "weight": item.weight, - }, - "user": { - "username": this.user.username, - "email": this.user.email - } - }); + "name": item.name, + "quantity": item.quantity, + "weight": item.weight, + }, + {observe: 'response'}); } } \ No newline at end of file diff --git a/frontend/src/app/services/recipe-book/add-recipe.service.spec.ts b/frontend/src/app/services/recipe-book/add-recipe.service.spec.ts new file mode 100644 index 00000000..744e936c --- /dev/null +++ b/frontend/src/app/services/recipe-book/add-recipe.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AddRecipeService } from './add-recipe.service'; + +describe('AddRecipeService', () => { + let service: AddRecipeService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AddRecipeService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/recipe-book/add-recipe.service.ts b/frontend/src/app/services/recipe-book/add-recipe.service.ts new file mode 100644 index 00000000..e29f5aee --- /dev/null +++ b/frontend/src/app/services/recipe-book/add-recipe.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { MealI } from '../../models/meal.model'; + +@Injectable({ + providedIn: 'root' +}) +export class AddRecipeService { + private recipeSource: BehaviorSubject = new BehaviorSubject(undefined); + constructor() { } + + recipeItem$ = this.recipeSource.asObservable(); + + setRecipeItem(recipeItem: MealI | undefined): void { + this.recipeSource.next(recipeItem); + } +} diff --git a/frontend/src/app/services/recipe-book/recipe-book-api.service.spec.ts b/frontend/src/app/services/recipe-book/recipe-book-api.service.spec.ts new file mode 100644 index 00000000..8636c936 --- /dev/null +++ b/frontend/src/app/services/recipe-book/recipe-book-api.service.spec.ts @@ -0,0 +1,18 @@ +import { TestBed } from '@angular/core/testing'; + +import { RecipeBookApiService } from './recipe-book-api.service'; +import { HttpClient } from '@angular/common/http'; + +describe('RecipeBookApiService', () => { + let service: RecipeBookApiService; + let httpClientSpy: jasmine.SpyObj; + + beforeEach(() => { + httpClientSpy = jasmine.createSpyObj('HttpClient', ['post']); + service = new RecipeBookApiService(httpClientSpy as any); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/recipe-book/recipe-book-api.service.ts b/frontend/src/app/services/recipe-book/recipe-book-api.service.ts new file mode 100644 index 00000000..1a562230 --- /dev/null +++ b/frontend/src/app/services/recipe-book/recipe-book-api.service.ts @@ -0,0 +1,58 @@ +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { UserI, RecipeItemI, MealI } from '../../models/interfaces'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class RecipeBookApiService { + + user: UserI = { + username: localStorage.getItem('user') ?? '', + email: localStorage.getItem('email') ?? '', + password: '', + } + + url : String = 'http://localhost:8080'; + + constructor(private http: HttpClient) { } + + getAllRecipes(): Observable> { + return this.http.post( + this.url+'/getAllRecipes', + {}, + {observe: 'response'} + ); + } + + addRecipe(item: MealI): Observable> { + return this.http.post( + this.url+'/addRecipe', + { + "name":item.name, + "description":item.description, + "image":item.image, + "ingredients":item.ingredients, + "instructions":item.instructions, + "cookingTime":item.cookingTime + }, + {observe: 'response'} + ); + } + + removeRecipe(item: MealI): Observable> { + return this.http.post( + this.url+'/removeRecipe', + { + "name":item.name, + "description":item.description, + "image":item.image, + "ingredients":item.ingredients, + "instructions":item.instructions, + "cookingTime":item.cookingTime + }, + {observe: 'response'} + ); + } +} diff --git a/frontend/src/app/services/services.ts b/frontend/src/app/services/services.ts index a31c89ae..49d46e6c 100644 --- a/frontend/src/app/services/services.ts +++ b/frontend/src/app/services/services.ts @@ -1,4 +1,5 @@ export { ShoppingListApiService } from './shopping-list-api/shopping-list-api.service'; export { PantryApiService } from './pantry-api/pantry-api.service'; export { ErrorHandlerService } from './error-handler/error-handler.service'; -export { AuthenticationService } from './authentication/authentication.service'; \ No newline at end of file +export { AuthenticationService } from './authentication/authentication.service'; +export { RecipeBookApiService } from './recipe-book/recipe-book-api.service'; \ No newline at end of file diff --git a/frontend/src/app/services/settings-api/settings-api.service.spec.ts b/frontend/src/app/services/settings-api/settings-api.service.spec.ts new file mode 100644 index 00000000..3c3579e3 --- /dev/null +++ b/frontend/src/app/services/settings-api/settings-api.service.spec.ts @@ -0,0 +1,104 @@ +//write unit test for the settings-api.service.ts +// Path: frontend/src/app/services/settings-api/settings-api.service.spec.ts +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { SettingsApiService } from './settings-api.service'; +import { UserPreferencesI } from '../../models/userpreference.model'; + +describe('SettingsApiService', () => { + let service: SettingsApiService; + let httpMock: HttpTestingController; + let settings: UserPreferencesI; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [SettingsApiService] + }); + + service = TestBed.inject(SettingsApiService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should get settings', () => { + settings = { + goal: 'lose', + shoppingInterval: 'weekly', + foodPreferences: ['vegan'], + calorieAmount: 2000, + budgetRange: 'low', + macroRatio: {protein: 0.3, carbs: 0.4, fat: 0.3}, + allergies: ['dairy'], + cookingTime: '30', + userHeight: 180, + userWeight: 80, + userBMI: 24.7, + + bmiset : true, + cookingTimeSet : true, + allergiesSet : true, + macroSet : true, + budgetSet : true, + calorieSet : true, + foodPreferenceSet : true, + shoppingIntervalSet : true, + + }; + + service.getSettings().subscribe((res) => { + expect(res.status).toBe(200); + expect(res.body).toEqual(settings); + }); + + const req = httpMock.expectOne(`${service.url}/getSettings`); + expect(req.request.method).toBe('POST'); + req.flush(settings); + }); + + + it('should update settings', () => { + settings = { + goal: 'lose', + shoppingInterval: 'weekly', + foodPreferences: ['vegan'], + calorieAmount: 2000, + budgetRange: 'low', + + macroRatio: {protein: 0.3, carbs: 0.4, fat: 0.3}, + allergies: ['dairy'], + cookingTime: '30', + userHeight: 180, + userWeight: 80, + userBMI: 24.7, + + bmiset : true, + cookingTimeSet : true, + allergiesSet : true, + macroSet : true, + budgetSet : true, + calorieSet : true, + foodPreferenceSet : true, + shoppingIntervalSet : true, + }; + + + service.updateSettings(settings).subscribe((res) => { + expect(res.status).toBe(200); + }); + + const req = httpMock.expectOne(`${service.url}/updateSettings`); + expect(req.request.method).toBe('POST'); + req.flush(settings); + }); +}); + + + diff --git a/frontend/src/app/services/settings-api/settings-api.service.ts b/frontend/src/app/services/settings-api/settings-api.service.ts new file mode 100644 index 00000000..1d04c816 --- /dev/null +++ b/frontend/src/app/services/settings-api/settings-api.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { UserPreferencesI } from '../../models/userpreference.model'; + +@Injectable({ + providedIn: 'root' +}) +export class SettingsApiService { + + url: string = 'http://localhost:8080'; + + constructor(private http: HttpClient) { } + + getSettings(): Observable> { + return this.http.post( + `${this.url}/getSettings`, + {}, + { observe: 'response' } + ); + } + + updateSettings(settings: UserPreferencesI): Observable> { + return this.http.post( + `${this.url}/updateSettings`, + settings, + { observe: 'response' } + ); + } + + + + +} diff --git a/frontend/src/app/services/shopping-list-api/shopping-list-api.service.spec.ts b/frontend/src/app/services/shopping-list-api/shopping-list-api.service.spec.ts index 1ac72b7f..5d58054e 100644 --- a/frontend/src/app/services/shopping-list-api/shopping-list-api.service.spec.ts +++ b/frontend/src/app/services/shopping-list-api/shopping-list-api.service.spec.ts @@ -1,5 +1,5 @@ import { ShoppingListApiService } from './shopping-list-api.service'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpResponse } from '@angular/common/http'; import { FoodItemI } from '../../models/interfaces'; import { of } from 'rxjs'; @@ -35,11 +35,11 @@ describe('ShoppingListApiService', () => { } ]; - httpClientSpy.post.and.returnValue(of(expectedItems)); + httpClientSpy.post.and.returnValue(of(new HttpResponse({body: expectedItems}))); service.getShoppingListItems().subscribe({ - next: items => { - expect(items) + next: res => { + expect(res.body) .withContext('expected items') .toEqual(expectedItems); done(); @@ -59,11 +59,11 @@ describe('ShoppingListApiService', () => { "weight": 0.1, }; - httpClientSpy.post.and.returnValue(of(expectedItem)); + httpClientSpy.post.and.returnValue(of(new HttpResponse({body: expectedItem}))); service.addToShoppingList(expectedItem).subscribe({ - next: item => { - expect(item) + next: res => { + expect(res.body) .withContext('expected item') .toEqual(expectedItem); done(); @@ -83,13 +83,13 @@ describe('ShoppingListApiService', () => { "weight": 0.1, }; - httpClientSpy.post.and.returnValue(of(expectedItem)); + httpClientSpy.post.and.returnValue(of(new HttpResponse({status: 200}))); service.updateShoppingListItem(expectedItem).subscribe({ - next: item => { - expect(item) - .withContext('expected item') - .toEqual(expectedItem); + next: res => { + expect(res.status) + .withContext('expected HTTP status code 200') + .toEqual(200); done(); }, error: done.fail @@ -107,13 +107,13 @@ describe('ShoppingListApiService', () => { "weight": 0.1, }; - httpClientSpy.post.and.returnValue(of(expectedItem)); + httpClientSpy.post.and.returnValue(of(new HttpResponse({status: 200}))); service.deleteShoppingListItem(expectedItem).subscribe({ - next: item => { - expect(item) - .withContext('expected item') - .toEqual(expectedItem); + next: res => { + expect(res.status) + .withContext('expected HTTP status code 200') + .toEqual(200); done(); }, error: done.fail diff --git a/frontend/src/app/services/shopping-list-api/shopping-list-api.service.ts b/frontend/src/app/services/shopping-list-api/shopping-list-api.service.ts index 12af296c..dd19595f 100644 --- a/frontend/src/app/services/shopping-list-api/shopping-list-api.service.ts +++ b/frontend/src/app/services/shopping-list-api/shopping-list-api.service.ts @@ -1,77 +1,65 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { FoodItemI, UserI } from '../../models/interfaces'; +import { FoodItemI } from '../../models/interfaces'; @Injectable({ providedIn: 'root' }) export class ShoppingListApiService { - user: UserI = { - username: localStorage.getItem('user') ?? '', - email: localStorage.getItem('email') ?? '', - password: '', - } - url: String = 'http://localhost:8080'; constructor(private http: HttpClient) { } - getShoppingListItems(): Observable { + getShoppingListItems(): Observable> { return this.http.post( this.url + '/getShoppingList', - { - "username": this.user.username, - "email": this.user.email - }); + {}, + { observe: 'response' }); } - addToShoppingList(item: FoodItemI): Observable { + addToShoppingList(item: FoodItemI): Observable> { return this.http.post( this.url + '/addToShoppingList', { - "food": { - "name": item.name, - "quantity": item.quantity, - "weight": item.weight, - }, - "user": { - "username": this.user.username, - "email": this.user.email - } - }); + "name": item.name, + "quantity": item.quantity, + "weight": item.weight, + }, + { observe: 'response' }); } - updateShoppingListItem(item: FoodItemI): Observable { - return this.http.post( + updateShoppingListItem(item: FoodItemI): Observable> { + return this.http.post( this.url + '/updateShoppingList', { - "food": { - "name": item.name, - "quantity": item.quantity, - "weight": item.weight, - }, - "user": { - "username": this.user.username, - "email": this.user.email - } - }); + "name": item.name, + "quantity": item.quantity, + "weight": item.weight, + }, + { observe: 'response' }); } - deleteShoppingListItem(item: FoodItemI): Observable { - return this.http.post( + deleteShoppingListItem(item: FoodItemI): Observable> { + return this.http.post( this.url + '/removeFromShoppingList', { - "food": { - "name": item.name, - "quantity": item.quantity, - "weight": item.weight, - }, - "user": { - "username": this.user.username, - "email": this.user.email - } - }); + "name": item.name, + "quantity": item.quantity, + "weight": item.weight, + }, + { observe: 'response' }); + } + + buyItem(item: FoodItemI): Observable> { + return this.http.post( + this.url + '/buyItem', + { + "name": item.name, + "quantity": item.quantity, + "weight": item.weight, + }, + { observe: 'response' }); } } diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg index decb1f2e..c31fe750 100644 --- a/frontend/src/assets/logo.svg +++ b/frontend/src/assets/logo.svg @@ -1,3 +1 @@ - - - \ No newline at end of file + diff --git a/frontend/src/global.scss b/frontend/src/global.scss index d89e39b9..d7310460 100644 --- a/frontend/src/global.scss +++ b/frontend/src/global.scss @@ -24,3 +24,8 @@ @import "@ionic/angular/css/text-alignment.css"; @import "@ionic/angular/css/text-transformation.css"; @import "@ionic/angular/css/flex-utils.css"; + +div.item-inner { + padding-right: 0px !important; + padding-inline-end:0px !important; + } diff --git a/frontend/src/main.ts b/frontend/src/main.ts index e3ded946..ce959c6a 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -6,8 +6,9 @@ import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; import { routes } from './app/app.routes'; import { AppComponent } from './app/app.component'; import { environment } from './environments/environment'; -import { HttpClient, HttpClientModule } from '@angular/common/http'; +import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http'; import { FormsModule } from '@angular/forms'; +import { JwtInterceptor } from './app/services/authentication/jwt.interceptor'; if (environment.production) { enableProdMode(); @@ -20,5 +21,6 @@ bootstrapApplication(AppComponent, { importProvidersFrom(HttpClientModule), importProvidersFrom(FormsModule), provideRouter(routes), + { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, ], }); diff --git a/frontend/src/theme/variables.scss b/frontend/src/theme/variables.scss index fc112894..4183d5f2 100644 --- a/frontend/src/theme/variables.scss +++ b/frontend/src/theme/variables.scss @@ -8,7 +8,7 @@ --ion-color-primary-rgb: 255, 166, 0; --ion-color-primary-contrast: #ffffff; --ion-color-primary-contrast-rgb: 0, 0, 0; - --ion-color-primary-shade: #ff7f50; + --ion-color-primary-shade: #74c781; --ion-color-primary-tint: #8B572A; /** secondary **/ @@ -76,11 +76,13 @@ --ion-color-light-tint: #f5f6f9; --ion-toolbar-background: #f8f8f8; - --ion-toolbar-color: #000000; --ion-background-color: #eaeaea; - + --ion-input-background: var(--ion-color-light-shade); + + // --ion-toolbar-segment-color: #000000; + // --ion-toolbar-segment-color-checked: #f4f4f4; } @media (prefers-color-scheme: light) { @@ -201,6 +203,8 @@ color: var(--ion-background-color); } + + /* * Material Design Dark Theme * ------------------------------------------- @@ -245,9 +249,3 @@ } } -// .ios ion-toolbar, .md ion-toolbar { -// --ion-background-color: #8A4F2D; -// --ion-color-primary: #FFFFFF; -// --ion-color-primary-contrast: #000000; - -// } diff --git a/package-lock.json b/package-lock.json index 4638a107..2414920f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,10 +22,11 @@ "@capacitor/status-bar": "5.0.4", "@ionic/angular": "^7.0.0", "@types/chart.js": "^2.9.37", - "axios": "^1.4.0", "chart.js": "^4.3.0", "cordova-plugin-advanced-http": "^3.3.1", + "dotenv": "^16.3.1", "ionicons": "^7.0.0", + "neo4j-driver": "^5.10.0", "openai": "^3.3.0", "rxjs": "~7.5.0", "tslib": "^2.3.0", @@ -65,6 +66,15 @@ "typescript": "~4.8.4" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -79,12 +89,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1502.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.8.tgz", - "integrity": "sha512-rTltw2ABHrcKc8EGimALvXmrDTP5hlNbEy6nYolJoXEI9EwHgriWrVLVPs3OEF+/ed47dbJi9EGOXUOgzgpB5A==", + "version": "0.1502.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.9.tgz", + "integrity": "sha512-CFn+LbtYeLG7WqO+BBSjogl764StHpwgfJnNAXQ/3UouUktZ92z4lxhUm0PwIPb5k0lILsf81ubcS1vzwoXEEg==", "dev": true, "dependencies": { - "@angular-devkit/core": "15.2.8", + "@angular-devkit/core": "15.2.9", "rxjs": "6.6.7" }, "engines": { @@ -112,15 +122,15 @@ "dev": true }, "node_modules/@angular-devkit/build-angular": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-15.2.8.tgz", - "integrity": "sha512-TGDnXhhOG6h6TOrWWzfnkha7wYBOXi7iJc1o1w1VKCayE3T6TZZdF847aK66vL9KG7AKYVdGhWEGw2WBHUBUpg==", + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-15.2.9.tgz", + "integrity": "sha512-djOo2Q22zLrxPccSbINz93hD+pES/nNPoze4Ys/0IdtMlLmxO/YGsA+FG5eNeNAf2jK/JRoNydaYOh7XpGoCzA==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.0", - "@angular-devkit/architect": "0.1502.8", - "@angular-devkit/build-webpack": "0.1502.8", - "@angular-devkit/core": "15.2.8", + "@angular-devkit/architect": "0.1502.9", + "@angular-devkit/build-webpack": "0.1502.9", + "@angular-devkit/core": "15.2.9", "@babel/core": "7.20.12", "@babel/generator": "7.20.14", "@babel/helper-annotate-as-pure": "7.18.6", @@ -132,7 +142,7 @@ "@babel/runtime": "7.20.13", "@babel/template": "7.20.7", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "15.2.8", + "@ngtools/webpack": "15.2.9", "ansi-colors": "4.1.3", "autoprefixer": "10.4.13", "babel-loader": "9.1.2", @@ -165,7 +175,7 @@ "rxjs": "6.6.7", "sass": "1.58.1", "sass-loader": "13.2.0", - "semver": "7.3.8", + "semver": "7.5.3", "source-map-loader": "4.0.1", "source-map-support": "0.5.21", "terser": "5.16.3", @@ -240,12 +250,12 @@ "dev": true }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1502.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1502.8.tgz", - "integrity": "sha512-jWtNv+S03FFLDe/C8SPCcRvkz3bSb2R+919IT086Q9axIPQ1VowOEwzt2k3qXPSSrC7GSYuASM+X92dB47NTQQ==", + "version": "0.1502.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1502.9.tgz", + "integrity": "sha512-VzMXoZjrbL1XlcSegqpZCBDbVvKFGPs3cKp4bXDD5ht95jcCyJPk5FA/wrh0pGGwbOF8ae/XOWFcPRzctC35iA==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1502.8", + "@angular-devkit/architect": "0.1502.9", "rxjs": "6.6.7" }, "engines": { @@ -277,9 +287,9 @@ "dev": true }, "node_modules/@angular-devkit/core": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.8.tgz", - "integrity": "sha512-Lo4XrbDMtXarKnMrFgWLmQdSX+3QPNAg4otG8cmp/U4jJyjV4dAYKEAsb1sCNGUSM4h4v09EQU/5ugVjDU29lQ==", + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.9.tgz", + "integrity": "sha512-6u44YJ9tEG2hiWITL1rwA9yP6ot4a3cyN/UOMRkYSa/XO2Gz5/dM3U74E2kwg+P1NcxLXffBWl0rz8/Y/lSZyQ==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -321,12 +331,12 @@ "dev": true }, "node_modules/@angular-devkit/schematics": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.8.tgz", - "integrity": "sha512-w6EUGC96kVsH9f8sEzajzbONMawezyVBiSo+JYp5r25rQArAz/a+KZntbuETWHQ0rQOEsKmUNKxwmr11BaptSQ==", + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.9.tgz", + "integrity": "sha512-o08nE8sTpfq/Fknrr1rzBsM8vY36BDox+8dOo9Zc/KqcVPwDy94YKRzHb+xxVaU9jy1VYeCjy63mkyELy7Z3zQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "15.2.8", + "@angular-devkit/core": "15.2.9", "jsonc-parser": "3.2.0", "magic-string": "0.29.0", "ora": "5.4.1", @@ -449,15 +459,15 @@ } }, "node_modules/@angular/cli": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-15.2.8.tgz", - "integrity": "sha512-3VlTfm6DUZfFHBY43vQSAaqmFTxy3VtRd/iDBCHcEPhHwYLWBvNwReJuJfNja8O105QQ6DBiYVBExEBtPmjQ4w==", + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-15.2.9.tgz", + "integrity": "sha512-mI6hkGyIJDKd8MRiBl3p5chsUhgnluwmpsq3g1FFPw+wv+eXsPYgCiHqXS/OsK+shFxii9XMxoZQO28bJ4NAOQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1502.8", - "@angular-devkit/core": "15.2.8", - "@angular-devkit/schematics": "15.2.8", - "@schematics/angular": "15.2.8", + "@angular-devkit/architect": "0.1502.9", + "@angular-devkit/core": "15.2.9", + "@angular-devkit/schematics": "15.2.9", + "@schematics/angular": "15.2.9", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "3.0.1", @@ -469,7 +479,7 @@ "ora": "5.4.1", "pacote": "15.1.0", "resolve": "1.22.1", - "semver": "7.3.8", + "semver": "7.5.3", "symbol-observable": "4.0.0", "yargs": "17.6.2" }, @@ -577,9 +587,9 @@ } }, "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -775,9 +785,9 @@ } }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -855,9 +865,9 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -911,9 +921,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -949,9 +959,9 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -975,9 +985,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -2223,9 +2233,9 @@ } }, "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -2428,9 +2438,9 @@ } }, "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -3584,9 +3594,9 @@ "dev": true }, "node_modules/@ngtools/webpack": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-15.2.8.tgz", - "integrity": "sha512-BJexeT4FxMtToVBGa3wdl6rrkYXgilP0kkSH4Qzu4MPlLPbeBSr4XQalQriewlpC2uzG0r2SJfrAe2eDhtSykA==", + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-15.2.9.tgz", + "integrity": "sha512-nOXUGqKkAEMlCcrhkDwWDzcVdKNH7MNRUXfNzsFc9zdeR/5p3qt6SVMN7OOE3NREyI7P6nzARc3S+6QDBjf3Jg==", "dev": true, "engines": { "node": "^14.20.0 || ^16.13.0 || >=18.10.0", @@ -3809,13 +3819,13 @@ } }, "node_modules/@schematics/angular": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-15.2.8.tgz", - "integrity": "sha512-F49IEzCFxQlpaMIgTO/wF1l/CLQKif7VaiDdyiTKOeT22IMmyd61FUmWDyZYfCBqMlvBmvDGx64HaHWes1HYCg==", + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-15.2.9.tgz", + "integrity": "sha512-0Lit6TLNUwcAYiEkXgZp3vY9xAO1cnZCBXuUcp+6v+Ddnrt2w/YOiGe74p21cYe0StkTpTljsqsKBTiX7TMjQg==", "dev": true, "dependencies": { - "@angular-devkit/core": "15.2.8", - "@angular-devkit/schematics": "15.2.8", + "@angular-devkit/core": "15.2.9", + "@angular-devkit/schematics": "15.2.9", "jsonc-parser": "3.2.0" }, "engines": { @@ -4011,9 +4021,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.34", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.34.tgz", - "integrity": "sha512-fvr49XlCGoUj2Pp730AItckfjat4WNb0lb3kfrLWffd+RLeoGAMsq7UOy04PAPtoL01uKwcp6u8nhzpgpDYr3w==", + "version": "4.17.35", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", + "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", "dev": true, "dependencies": { "@types/node": "*", @@ -4031,6 +4041,12 @@ "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==", + "dev": true + }, "node_modules/@types/http-proxy": { "version": "1.17.11", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz", @@ -4120,11 +4136,12 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", - "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz", + "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==", "dev": true, "dependencies": { + "@types/http-errors": "*", "@types/mime": "*", "@types/node": "*" } @@ -4145,9 +4162,9 @@ } }, "node_modules/@types/ws": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", - "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", + "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", "dev": true, "dependencies": { "@types/node": "*" @@ -5116,13 +5133,11 @@ } }, "node_modules/axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.14.8" } }, "node_modules/axobject-query": { @@ -5182,9 +5197,9 @@ } }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -5225,7 +5240,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -6561,6 +6575,17 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -8493,10 +8518,20 @@ } }, "node_modules/html-entities": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", - "dev": true + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", + "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] }, "node_modules/html-escaper": { "version": "2.0.2", @@ -8658,7 +8693,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -8949,9 +8983,9 @@ "dev": true }, "node_modules/ipaddr.js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", - "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", "dev": true, "engines": { "node": ">= 10" @@ -9422,9 +9456,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -9840,9 +9874,9 @@ } }, "node_modules/karma-coverage-istanbul-reporter/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -10070,9 +10104,9 @@ } }, "node_modules/less/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "optional": true, "bin": { @@ -10345,9 +10379,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -11038,6 +11072,62 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/neo4j-driver": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/neo4j-driver/-/neo4j-driver-5.10.0.tgz", + "integrity": "sha512-xvexTGrMxS3Nj/vU/OO5FA0wMcmdOJOOqHgztydw8iSFnKBgxxAo3giiH1UKGwP4k12BnOSXSQeZGXT3faVHJQ==", + "dependencies": { + "neo4j-driver-bolt-connection": "5.10.0", + "neo4j-driver-core": "5.10.0", + "rxjs": "^7.8.1" + } + }, + "node_modules/neo4j-driver-bolt-connection": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-5.10.0.tgz", + "integrity": "sha512-TgxaQ1kRtd4hP2iromtun3twx+tLP9I0F7CIhDpYaUix8Z1nHcI6z9y+uJ1+YU/doyzgS+R/ZP8h2C4S75iClw==", + "dependencies": { + "buffer": "^6.0.3", + "neo4j-driver-core": "5.10.0", + "string_decoder": "^1.3.0" + } + }, + "node_modules/neo4j-driver-bolt-connection/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/neo4j-driver-core": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/neo4j-driver-core/-/neo4j-driver-core-5.10.0.tgz", + "integrity": "sha512-Wf50GRvEqG2R0PWMJg3tF7YxILJb4QjYgpoC7g/2OCjmQQUuSy+wj65aUlV66XB4I9J07FElGWy6Xrm/rDyA5A==" + }, + "node_modules/neo4j-driver/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", @@ -11547,26 +11637,18 @@ "form-data": "^4.0.0" } }, - "node_modules/openai/node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "dependencies": { - "follow-redirects": "^1.14.8" - } - }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -12308,11 +12390,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -12632,9 +12709,9 @@ } }, "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -12993,7 +13070,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -13128,9 +13204,9 @@ } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -13831,7 +13907,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -15290,15 +15365,6 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 2cc0c9de..0fec9180 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test:dev": "ng test --browsers=ChromeHeadless --watch=false --code-coverage ", - "test:unit": ".\\backend\\gradlew.bat -p backend test" + "test:unit": "./gradlew test" }, "private": true, "dependencies": { @@ -31,10 +31,11 @@ "@capacitor/status-bar": "5.0.4", "@ionic/angular": "^7.0.0", "@types/chart.js": "^2.9.37", - "axios": "^1.4.0", "chart.js": "^4.3.0", "cordova-plugin-advanced-http": "^3.3.1", + "dotenv": "^16.3.1", "ionicons": "^7.0.0", + "neo4j-driver": "^5.10.0", "openai": "^3.3.0", "rxjs": "~7.5.0", "tslib": "^2.3.0",