diff --git a/README.md b/README.md index cf5b0a1..7571427 100644 --- a/README.md +++ b/README.md @@ -95,13 +95,13 @@ https://start.spring.io/#!type=maven-project&language=java&platformVersion=3.0.4 #### Create backend docker container 1. First build the image with `cd diogenes && ./mvnw install`. -1. Build the container with `cd .. && docker build -f docker/Dockerfile -t nck974/diogenes:0.0.5 .` +1. Build the container with `cd .. && docker build -f docker/Dockerfile -t nck974/diogenes:0.0.7 .` 1. Generate a token in `https://hub.docker.com` and login with `docker login -u `. Paste the generated token as password. -1. Push the generated container with `docker push nck974/diogenes:0.0.5`. +1. Push the generated container with `docker push nck974/diogenes:0.0.7`. 1. Create the `latest` tag and push it: ```bash - docker tag nck974/diogenes:0.0.5 nck974/diogenes:latest + docker tag nck974/diogenes:0.0.7 nck974/diogenes:latest docker push nck974/diogenes:latest ``` @@ -113,13 +113,13 @@ To run the app in development mode just access the folder `diogenes-ng` and star #### Create frontend docker container -1. Build the container with `docker build -f docker/Dockerfile.angular -t nck974/diogenes-ng:0.0.7 .` +1. Build the container with `docker build -f docker/Dockerfile.angular -t nck974/diogenes-ng:0.0.8 .` 1. Generate a token in `https://hub.docker.com` and login with `docker login -u `. Paste the generated token as password. -1. Push the generated container with `docker push nck974/diogenes-ng:0.0.7`. +1. Push the generated container with `docker push nck974/diogenes-ng:0.0.8`. 1. Create the `latest` tag and push it: ```bash - docker tag nck974/diogenes-ng:0.0.7 nck974/diogenes-ng:latest + docker tag nck974/diogenes-ng:0.0.8 nck974/diogenes-ng:latest docker push nck974/diogenes-ng:latest ``` diff --git a/diogenes/pom.xml b/diogenes/pom.xml index 64fb1b6..c3b84ac 100644 --- a/diogenes/pom.xml +++ b/diogenes/pom.xml @@ -6,12 +6,12 @@ org.springframework.boot spring-boot-starter-parent - 3.1.5 + 3.2.0 dev.nichoko diogenes - 0.0.5 + 0.0.7 diogenes Software to manage the personal inventary of items that you own. @@ -65,7 +65,7 @@ org.postgresql postgresql - 42.6.0 + 42.7.0 diff --git a/diogenes/src/main/java/dev/nichoko/diogenes/controller/LocationController.java b/diogenes/src/main/java/dev/nichoko/diogenes/controller/LocationController.java new file mode 100644 index 0000000..745a1ea --- /dev/null +++ b/diogenes/src/main/java/dev/nichoko/diogenes/controller/LocationController.java @@ -0,0 +1,67 @@ +package dev.nichoko.diogenes.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import dev.nichoko.diogenes.model.LocationSummary; +import dev.nichoko.diogenes.model.domain.Location; +import dev.nichoko.diogenes.service.LocationService; +import jakarta.validation.Valid; + +@RestController +@RequestMapping("/api/v1/locations") +public class LocationController { + private LocationService locationService; + + @Autowired + public LocationController(LocationService locationService) { + this.locationService = locationService; + } + + @PostMapping("/") + public ResponseEntity createLocation(@Valid @RequestBody Location location) { + Location createdLocation = locationService.createLocation(location); + return ResponseEntity.status(HttpStatus.CREATED).body(createdLocation); + } + + @GetMapping("/") + public ResponseEntity> getAllLocations() { + return ResponseEntity + .ok(locationService.getAllLocations()); + } + + @GetMapping("/{id}") + public ResponseEntity getLocationById(@Valid @PathVariable int id) { + return ResponseEntity.ok(locationService.getLocationById(id)); + } + + @PutMapping("/{id}") + public ResponseEntity updateLocation(@Valid @RequestBody Location updateLocation, @PathVariable int id) { + return ResponseEntity.ok().body(locationService.updateLocation(id, updateLocation)); + } + + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping("/{id}") + public ResponseEntity deleteLocation(@Valid @PathVariable int id) { + locationService.deleteLocation(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/summary") + public ResponseEntity> getLocationsSummary() { + return ResponseEntity + .ok(locationService.getLocationsSummary()); + } +} diff --git a/diogenes/src/main/java/dev/nichoko/diogenes/exception/InvalidLocationException.java b/diogenes/src/main/java/dev/nichoko/diogenes/exception/InvalidLocationException.java new file mode 100644 index 0000000..6e42869 --- /dev/null +++ b/diogenes/src/main/java/dev/nichoko/diogenes/exception/InvalidLocationException.java @@ -0,0 +1,8 @@ +package dev.nichoko.diogenes.exception; + +public class InvalidLocationException extends RuntimeException { + + public InvalidLocationException(int id) { + super("Invalid location with id '" + id + "'."); + } +} \ No newline at end of file diff --git a/diogenes/src/main/java/dev/nichoko/diogenes/exception/MissingLocationException.java b/diogenes/src/main/java/dev/nichoko/diogenes/exception/MissingLocationException.java new file mode 100644 index 0000000..ec47f34 --- /dev/null +++ b/diogenes/src/main/java/dev/nichoko/diogenes/exception/MissingLocationException.java @@ -0,0 +1,8 @@ +package dev.nichoko.diogenes.exception; + +public class MissingLocationException extends RuntimeException { + + public MissingLocationException() { + super("An item needs to have a location assigned."); + } +} \ No newline at end of file diff --git a/diogenes/src/main/java/dev/nichoko/diogenes/exception/handler/ResourceNotFoundAdviceController.java b/diogenes/src/main/java/dev/nichoko/diogenes/exception/handler/ResourceNotFoundAdviceController.java index 4b0f852..0b26042 100644 --- a/diogenes/src/main/java/dev/nichoko/diogenes/exception/handler/ResourceNotFoundAdviceController.java +++ b/diogenes/src/main/java/dev/nichoko/diogenes/exception/handler/ResourceNotFoundAdviceController.java @@ -7,7 +7,9 @@ import dev.nichoko.diogenes.exception.ImageCouldNotBeSavedException; import dev.nichoko.diogenes.exception.InvalidCategoryException; +import dev.nichoko.diogenes.exception.InvalidLocationException; import dev.nichoko.diogenes.exception.MissingCategoryException; +import dev.nichoko.diogenes.exception.MissingLocationException; import dev.nichoko.diogenes.exception.NameAlreadyExistsException; import dev.nichoko.diogenes.exception.ResourceNotFoundException; import dev.nichoko.diogenes.exception.UnsupportedImageFormatException; @@ -33,12 +35,24 @@ public ErrorResponse handleMissingCategory(MissingCategoryException ex) { return new ErrorResponse(ex.getMessage()); } + @ExceptionHandler(MissingLocationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleMissingLocation(MissingLocationException ex) { + return new ErrorResponse(ex.getMessage()); + } + @ExceptionHandler(InvalidCategoryException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleInvalidCategory(InvalidCategoryException ex) { return new ErrorResponse(ex.getMessage()); } + @ExceptionHandler(InvalidLocationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleInvalidLocation(InvalidLocationException ex) { + return new ErrorResponse(ex.getMessage()); + } + @ExceptionHandler(ImageCouldNotBeSavedException.class) @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) public ErrorResponse handleImageCouldNotBeSaved(ImageCouldNotBeSavedException ex) { diff --git a/diogenes/src/main/java/dev/nichoko/diogenes/model/ItemFilter.java b/diogenes/src/main/java/dev/nichoko/diogenes/model/ItemFilter.java index 3686b31..e8e0339 100644 --- a/diogenes/src/main/java/dev/nichoko/diogenes/model/ItemFilter.java +++ b/diogenes/src/main/java/dev/nichoko/diogenes/model/ItemFilter.java @@ -5,12 +5,14 @@ public class ItemFilter { private String description; private Integer number; private Integer categoryId; + private Integer locationId; - public ItemFilter(String name, String description, Integer number, Integer categoryId) { + public ItemFilter(String name, String description, Integer number, Integer categoryId, Integer locationId) { this.name = name; this.description = description; this.number = number; this.categoryId = categoryId; + this.locationId = locationId; } public String getName() { @@ -29,10 +31,14 @@ public Integer getCategoryId() { return categoryId; } + public Integer getLocationId() { + return locationId; + } + @Override public String toString() { return "ItemFilter [name=" + name + ", description=" + description + ", number=" + number + ", categoryId=" - + categoryId + "]"; + + categoryId + ", locationId=" + locationId + "]"; } } diff --git a/diogenes/src/main/java/dev/nichoko/diogenes/model/LocationSummary.java b/diogenes/src/main/java/dev/nichoko/diogenes/model/LocationSummary.java new file mode 100644 index 0000000..447de78 --- /dev/null +++ b/diogenes/src/main/java/dev/nichoko/diogenes/model/LocationSummary.java @@ -0,0 +1,29 @@ +package dev.nichoko.diogenes.model; + +import dev.nichoko.diogenes.model.domain.Location; + +public class LocationSummary { + private Location location; + private int itemsNumber; + + public LocationSummary(Location location, int itemsNumber) { + this.location = location; + this.itemsNumber = itemsNumber; + } + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + + public int getItemsNumber() { + return itemsNumber; + } + + public void setItemsNumber(int itemsNumber) { + this.itemsNumber = itemsNumber; + } +} diff --git a/diogenes/src/main/java/dev/nichoko/diogenes/model/domain/Item.java b/diogenes/src/main/java/dev/nichoko/diogenes/model/domain/Item.java index c02ab41..d79f4f7 100644 --- a/diogenes/src/main/java/dev/nichoko/diogenes/model/domain/Item.java +++ b/diogenes/src/main/java/dev/nichoko/diogenes/model/domain/Item.java @@ -70,6 +70,14 @@ public class Item { @Transient private int categoryId; + @OneToOne() + @JoinColumn(name = "location_id") + @JsonProperty(access = Access.READ_ONLY) + private Location location; + + @Transient + private int locationId; + public Item() { } @@ -142,6 +150,17 @@ public void setCategory(Category category) { } } + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + if (location != null) { + this.locationId = location.getId(); + } + } + public int getCategoryId() { return categoryId; } @@ -150,6 +169,14 @@ public void setCategoryId(int categoryId) { this.categoryId = categoryId; } + public int getLocationId() { + return locationId; + } + + public void setLocationId(int locationId) { + this.locationId = locationId; + } + public LocalDateTime getUpdatedOn() { return updatedOn; } @@ -170,7 +197,8 @@ public void setCreatedOn(LocalDateTime createdOn) { public String toString() { return "Item [id=" + id + ", name=" + name + ", description=" + description + ", number=" + number + ", updatedOn=" + updatedOn + ", createdOn=" + createdOn + ", imagePath=" + imagePath + ", category=" - + category + ", categoryId=" + categoryId + "]"; + + category + ", categoryId=" + categoryId + ", location=" + location + ", locationId=" + locationId + + "]"; } } \ No newline at end of file diff --git a/diogenes/src/main/java/dev/nichoko/diogenes/model/domain/Location.java b/diogenes/src/main/java/dev/nichoko/diogenes/model/domain/Location.java new file mode 100644 index 0000000..ff9b264 --- /dev/null +++ b/diogenes/src/main/java/dev/nichoko/diogenes/model/domain/Location.java @@ -0,0 +1,142 @@ +package dev.nichoko.diogenes.model.domain; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonProperty.Access; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; + +@Entity +@Table(name = "location") +public class Location { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @JsonProperty(access = Access.READ_ONLY) + private int id; + + @Column(nullable = false, unique = true) + @Schema(description = "The name of the location", nullable = false) + @NotEmpty + @NotBlank(message = "Name is mandatory") + @Size(max = 50, message = "Name cannot exceed 50 characters") + private String name; + + @Column(nullable = false) + @Schema(description = "Description of the item") + @Size(max = 2000, message = "Description cannot exceed 2000 characters") + private String description; + + @Column(nullable = false) + @Schema(description = "Icon of the item as just the HEX number without #") + @Size(max = 50, message = "Icon cannot exceed 6 characters") + private String icon; + + @Column(name = "updated_on") + @JsonProperty(access = Access.READ_ONLY) + private LocalDateTime updatedOn; + + @Column(name = "created_on") + @JsonProperty(access = Access.READ_ONLY) + private LocalDateTime createdOn; + + public Location(int id, + String name, + String description, + String icon) { + this.id = id; + this.name = name; + this.description = description; + this.icon = icon; + } + + public Location( + String name, + String description, + String icon) { + this.name = name; + this.description = description; + this.icon = icon; + } + + public Location() { + } + + @PrePersist + protected void onCreate() { + createdOn = LocalDateTime.now(); + updatedOn = createdOn; + } + + @PreUpdate + protected void onUpdate() { + updatedOn = LocalDateTime.now(); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + @Override + public String toString() { + return "Location [id=" + id + ", name=" + name + ", description=" + description + ", icon=" + icon + + ", updatedOn=" + updatedOn + ", createdOn=" + createdOn + "]"; + } + +} diff --git a/diogenes/src/main/java/dev/nichoko/diogenes/repository/LocationRepository.java b/diogenes/src/main/java/dev/nichoko/diogenes/repository/LocationRepository.java new file mode 100644 index 0000000..54da526 --- /dev/null +++ b/diogenes/src/main/java/dev/nichoko/diogenes/repository/LocationRepository.java @@ -0,0 +1,18 @@ +package dev.nichoko.diogenes.repository; + +import java.util.Optional; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import dev.nichoko.diogenes.model.domain.Location; + +@Repository +public interface LocationRepository extends JpaRepository { + Optional findById(int itemId); + Page findAll(Specification spec, Pageable pageable); + boolean existsByName(String name); +} diff --git a/diogenes/src/main/java/dev/nichoko/diogenes/service/ItemServiceImpl.java b/diogenes/src/main/java/dev/nichoko/diogenes/service/ItemServiceImpl.java index 2f0a03e..470ae8c 100644 --- a/diogenes/src/main/java/dev/nichoko/diogenes/service/ItemServiceImpl.java +++ b/diogenes/src/main/java/dev/nichoko/diogenes/service/ItemServiceImpl.java @@ -14,29 +14,36 @@ import org.springframework.web.multipart.MultipartFile; import dev.nichoko.diogenes.exception.MissingCategoryException; +import dev.nichoko.diogenes.exception.MissingLocationException; import dev.nichoko.diogenes.exception.ResourceNotFoundException; import dev.nichoko.diogenes.config.FileStorageConfig; import dev.nichoko.diogenes.exception.InvalidCategoryException; +import dev.nichoko.diogenes.exception.InvalidLocationException; import dev.nichoko.diogenes.model.ItemFilter; import dev.nichoko.diogenes.model.domain.Category; import dev.nichoko.diogenes.model.domain.Item; +import dev.nichoko.diogenes.model.domain.Location; import dev.nichoko.diogenes.model.enums.SortDirection; import dev.nichoko.diogenes.repository.CategoryRepository; import dev.nichoko.diogenes.repository.ItemRepository; +import dev.nichoko.diogenes.repository.LocationRepository; @Service public class ItemServiceImpl implements ItemService { private final ItemRepository itemRepository; private final CategoryRepository categoryRepository; + private final LocationRepository locationRepository; private final FileStorageConfig fileStorageConfig; private final FileStorageService fileStorageService; @Autowired public ItemServiceImpl(ItemRepository itemRepository, CategoryRepository categoryRepository, - FileStorageConfig fileStorageConfig, FileStorageService fileStorageService) { + FileStorageConfig fileStorageConfig, FileStorageService fileStorageService, + LocationRepository locationRepository) { this.itemRepository = itemRepository; this.categoryRepository = categoryRepository; + this.locationRepository = locationRepository; this.fileStorageConfig = fileStorageConfig; this.fileStorageService = fileStorageService; } @@ -64,6 +71,18 @@ private void setCategoryId(Item item) { } } + /** + * For some reason the locationId does not seem to be automatically filled by + * JPA + * + * @param item + */ + private void setLocationId(Item item) { + if (item.getLocation() != null) { + item.setLocationId(item.getLocation().getId()); + } + } + /** * Set fields that will be returned to the user and are not stored in the * database as such @@ -73,6 +92,7 @@ private void setCategoryId(Item item) { private void setTransientFields(Item createdItem) { setImageBasePath(createdItem); setCategoryId(createdItem); + setLocationId(createdItem); } /* @@ -119,6 +139,11 @@ private Specification filterItems(ItemFilter filter) { spec = spec.and((root, query, cb) -> cb.equal(root.get("category").get("id"), filter.getCategoryId())); } + // Filter by location + if (filter.getLocationId() != null) { + spec = spec.and((root, query, cb) -> cb.equal(root.get("location").get("id"), filter.getLocationId())); + } + return spec; } @@ -180,6 +205,25 @@ private Category findCategory(Item item) throws MissingCategoryException, Invali .orElseThrow(() -> new InvalidCategoryException(categoryId)); } + /** + * Checks that the provided location exists and returns it + * + * @param item + * @return + * @throws MissingLocationException + * @throws InvalidLocationException + */ + private Location findLocation(Item item) throws MissingLocationException, InvalidLocationException { + final int locationId = item.getLocationId(); + + if (locationId == 0) { + throw new MissingLocationException(); + } + + return locationRepository.findById(locationId) + .orElseThrow(() -> new InvalidLocationException(locationId)); + } + /* * Create a new item */ @@ -187,6 +231,7 @@ private Category findCategory(Item item) throws MissingCategoryException, Invali public Item createItem(Item item, MultipartFile imageFile) { item.setCategory(this.findCategory(item)); + item.setLocation(this.findLocation(item)); if (imageFile != null) { String imagePath = fileStorageService.saveItemImage(imageFile); @@ -228,6 +273,7 @@ public Item updateItem(int id, Item item, MultipartFile imageFile) { // Set fields that shall not be changed item.setId(existingItem.getId()); item.setCategory(this.findCategory(item)); + item.setLocation(this.findLocation(item)); item.setCreatedOn(existingItem.getCreatedOn()); // Store the image and update the database diff --git a/diogenes/src/main/java/dev/nichoko/diogenes/service/LocationService.java b/diogenes/src/main/java/dev/nichoko/diogenes/service/LocationService.java new file mode 100644 index 0000000..d571451 --- /dev/null +++ b/diogenes/src/main/java/dev/nichoko/diogenes/service/LocationService.java @@ -0,0 +1,20 @@ +package dev.nichoko.diogenes.service; + +import java.util.List; + +import dev.nichoko.diogenes.model.LocationSummary; +import dev.nichoko.diogenes.model.domain.Location; + +public interface LocationService { + Location getLocationById(int id); + + List getAllLocations(); + + List getLocationsSummary(); + + Location createLocation(Location item); + + Location updateLocation(int id, Location item); + + void deleteLocation(int id); +} diff --git a/diogenes/src/main/java/dev/nichoko/diogenes/service/LocationServiceImpl.java b/diogenes/src/main/java/dev/nichoko/diogenes/service/LocationServiceImpl.java new file mode 100644 index 0000000..76cbd63 --- /dev/null +++ b/diogenes/src/main/java/dev/nichoko/diogenes/service/LocationServiceImpl.java @@ -0,0 +1,117 @@ +package dev.nichoko.diogenes.service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; + +import dev.nichoko.diogenes.exception.NameAlreadyExistsException; +import dev.nichoko.diogenes.exception.ResourceNotFoundException; +import dev.nichoko.diogenes.model.LocationSummary; +import dev.nichoko.diogenes.model.domain.Location; +import dev.nichoko.diogenes.model.domain.Item; +import dev.nichoko.diogenes.repository.LocationRepository; + +@Service +public class LocationServiceImpl implements LocationService { + + private LocationRepository locationRepository; + private ItemService itemService; + + @Autowired + public LocationServiceImpl(LocationRepository locationRepository, ItemService itemService) { + this.locationRepository = locationRepository; + this.itemService = itemService; + } + + public Location getLocationById(int id) { + return locationRepository.findById(id) + .orElseThrow( + () -> new ResourceNotFoundException(id)); + + } + + /** + * Check if another item with the same name already exists in the database + * + * @param location + */ + private void validateName(Location location) { + String locationName = location.getName(); + if (locationRepository.existsByName(locationName)) { + throw new NameAlreadyExistsException( + "Location with the name " + locationName + " already exists."); + } + } + + /* + * Return all locations + */ + @Override + public List getAllLocations() { + return locationRepository.findAll(Sort.by("name")); + } + + /* + * Create a new location + */ + @Override + public Location createLocation(Location location) { + validateName(location); + + return locationRepository.save(location); + } + + /* + * Update an existing location or throw a not found exception + */ + @Override + public Location updateLocation(int id, Location location) { + return locationRepository.findById(id) + .map(existingLocation -> { + location.setId(existingLocation.getId()); + location.setCreatedOn(existingLocation.getCreatedOn()); + if (!location.getName().equals(existingLocation.getName())) { + validateName(location); + } + return locationRepository.save(location); + }) + .orElseThrow(() -> new ResourceNotFoundException(id)); + } + + /* + * Delete an existing location or throw a not found exception + */ + @Override + public void deleteLocation(int id) { + Location location = locationRepository.findById(id) + .orElseThrow( + () -> new ResourceNotFoundException(id)); + locationRepository.delete(location); + } + + /* + * Get all locations and items and count the number of items in each location + */ + @Override + public List getLocationsSummary() { + + // Count the locations from the items + List items = itemService.getAllItems(); + Map locationItemCountMap = items.stream() + .collect( + Collectors.groupingBy( + Item::getLocationId, + Collectors.reducing(0, e -> 1, (a, b) -> a + b))); + + // Return the locations with the value + List locations = getAllLocations(); + return locations.stream() + .map(location -> new LocationSummary(location, locationItemCountMap.getOrDefault(location.getId(), 0))) + .collect(Collectors.toList()); + } + +} diff --git a/diogenes/src/main/resources/db/migration/V7.20231220__add_item_location.sql b/diogenes/src/main/resources/db/migration/V7.20231220__add_item_location.sql new file mode 100644 index 0000000..fbf8431 --- /dev/null +++ b/diogenes/src/main/resources/db/migration/V7.20231220__add_item_location.sql @@ -0,0 +1,16 @@ +-- Create Category +CREATE TABLE location ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + icon VARCHAR(50) NOT NULL, + created_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON TABLE location IS 'Location of the items'; + +-- Add link to location +ALTER TABLE item ADD location_id INT; + +ALTER TABLE item ADD CONSTRAINT fk_location_id FOREIGN KEY (location_id) REFERENCES location(id); diff --git a/diogenes/src/test/java/dev/nichoko/diogenes/common/ItemManager.java b/diogenes/src/test/java/dev/nichoko/diogenes/common/ItemManager.java index 44c0cd9..14e702e 100644 --- a/diogenes/src/test/java/dev/nichoko/diogenes/common/ItemManager.java +++ b/diogenes/src/test/java/dev/nichoko/diogenes/common/ItemManager.java @@ -45,12 +45,39 @@ private static void tryCreateCategoryIfNotExists(MockMvc mockMvc, Item item) } } + /** + * Create first the location and assign the id to the item + * + * @param item + * @throws UnsupportedEncodingException + * @throws Exception + * @throws JsonProcessingException + */ + private static void tryCreateLocationIfNotExists(MockMvc mockMvc, Item item) + throws UnsupportedEncodingException, Exception, JsonProcessingException { + try { + String locationString = LocationManager.createLocation(mockMvc, item.getLocation()).andReturn() + .getResponse() + .getContentAsString(); + if (locationString != null) { + int locationId = JsonProcessor + .readJsonString(locationString) + .get("id") + .asInt(0); + item.setLocationId(locationId); + } + } catch (java.lang.NullPointerException e) { + + } + } + /* * Sends the provided item to the API */ public static ResultActions createItem(MockMvc mockMvc, Item item) throws Exception { tryCreateCategoryIfNotExists(mockMvc, item); + tryCreateLocationIfNotExists(mockMvc, item); return mockMvc.perform( post("/api/v1/item/") @@ -65,6 +92,7 @@ public static ResultActions createItem(MockMvc mockMvc, Item item) throws Except public static ResultActions updateItem(MockMvc mockMvc, Item item) throws Exception { tryCreateCategoryIfNotExists(mockMvc, item); + tryCreateLocationIfNotExists(mockMvc, item); return mockMvc.perform( put("/api/v1/item/" + Integer.toString(item.getId())) @@ -80,6 +108,7 @@ public static ResultActions createItemWithImage(MockMvc mockMvc, Item item, Mock throws Exception { tryCreateCategoryIfNotExists(mockMvc, item); + tryCreateLocationIfNotExists(mockMvc, item); // Create a MockMultipartFile for the JSON content MockMultipartFile itemPart = new MockMultipartFile( @@ -106,7 +135,8 @@ public static ResultActions createItemWithImage(MockMvc mockMvc, Item item, Mock /* * Updates the provided item to the API with the given image */ - public static ResultActions updateItemWithImage(MockMvc mockMvc, String id, Item item, MockMultipartFile imagePart) + public static ResultActions updateItemWithImage(MockMvc mockMvc, String id, Item item, + MockMultipartFile imagePart) throws Exception { // Create a MockMultipartFile for the JSON content diff --git a/diogenes/src/test/java/dev/nichoko/diogenes/common/LocationManager.java b/diogenes/src/test/java/dev/nichoko/diogenes/common/LocationManager.java new file mode 100644 index 0000000..23f372d --- /dev/null +++ b/diogenes/src/test/java/dev/nichoko/diogenes/common/LocationManager.java @@ -0,0 +1,36 @@ +package dev.nichoko.diogenes.common; + +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; + +import dev.nichoko.diogenes.model.domain.Location; +import dev.nichoko.diogenes.utils.JsonProcessor; + +public class LocationManager { + /* + * Sends the provided location API + */ + public static ResultActions createLocation(MockMvc mockMvc, Location location) throws Exception { + return mockMvc.perform( + post("/api/v1/locations/") + .content(JsonProcessor.stringifyClass(location)) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)); + } + + /* + * Sends the provided location to the API + */ + public static ResultActions updateLocation(MockMvc mockMvc, Location location, int locationId) throws Exception { + return mockMvc.perform( + put("/api/v1/locations/" + Integer.toString(locationId)) + .content(JsonProcessor.stringifyClass(location)) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)); + } + +} diff --git a/diogenes/src/test/java/dev/nichoko/diogenes/controller/ItemControllerTest.java b/diogenes/src/test/java/dev/nichoko/diogenes/controller/ItemControllerTest.java index 501b1db..436a2dd 100644 --- a/diogenes/src/test/java/dev/nichoko/diogenes/controller/ItemControllerTest.java +++ b/diogenes/src/test/java/dev/nichoko/diogenes/controller/ItemControllerTest.java @@ -346,6 +346,20 @@ void canNotCreateNewWithoutValidCategoryId() throws Exception { .andExpect(status().isBadRequest()); } + /** + * Verify create item validation: Invalid location + * + * @throws Exception + */ + @Test + void canNotCreateNewWithoutValidLocationId() throws Exception { + Item item = ItemMock.getMockItem(1); + item.setLocation(null); + + ItemManager.createItem(this.mockMvc, item) + .andExpect(status().isBadRequest()); + } + /** * Verify create item validation: Category 0 * @@ -364,6 +378,24 @@ void canNotCreateNewWithoutCategoryId() throws Exception { .andExpect(status().isBadRequest()); } + /** + * Verify create item validation: Location 0 + * + * @throws Exception + */ + @Test + void canNotCreateNewWithoutLocationId() throws Exception { + Item item = ItemMock.getMockItem(1); + item.setLocationId(0); + + this.mockMvc.perform( + post("/api/v1/item/") + .content(JsonProcessor.stringifyClass(item)) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + /** * Verify create item validation: Empty name * @@ -497,7 +529,8 @@ void canNotDeleteNonExistingItem() throws Exception { "name", "number", "description", - "categoryId" + "categoryId", + "locationId", }) void canFilterByTheAvailableParameters(String filterName) throws Exception { IntStream.range(0, 10).forEachOrdered(n -> { diff --git a/diogenes/src/test/java/dev/nichoko/diogenes/controller/LocationControllerTest.java b/diogenes/src/test/java/dev/nichoko/diogenes/controller/LocationControllerTest.java new file mode 100644 index 0000000..8aaada4 --- /dev/null +++ b/diogenes/src/test/java/dev/nichoko/diogenes/controller/LocationControllerTest.java @@ -0,0 +1,362 @@ +package dev.nichoko.diogenes.controller; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +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.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import dev.nichoko.diogenes.common.LocationManager; +import dev.nichoko.diogenes.common.ItemManager; +import dev.nichoko.diogenes.mock.LocationMock; +import dev.nichoko.diogenes.mock.ItemMock; +import dev.nichoko.diogenes.model.domain.Location; +import dev.nichoko.diogenes.model.domain.Item; +import dev.nichoko.diogenes.utils.JsonProcessor; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@ActiveProfiles("test") +class LocationControllerTest { + @Autowired + private LocationController controller; + + @Autowired + private MockMvc mockMvc; + + @Autowired + Flyway flyway; + + /* + * Clean up the database before each test + */ + @BeforeEach + public void cleanUp() { + flyway.clean(); + flyway.migrate(); + } + + /* + * Test the app can load + */ + @Test + void contextLoads() throws Exception { + assertThat(controller).isNotNull(); + } + + /** + * Verify that when an location is not found a + * + * @throws Exception + */ + @Test + void canGetErrorLocationNotFound() throws Exception { + this.mockMvc.perform(get("/api/v1/locations/25")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value("The following id could not be found: 25")); + } + + /** + * Verify that an location can be retrieved with all its parameters + * + * @throws Exception + */ + @Test + void canSearchLocationById() throws Exception { + Location location = LocationMock.getMockLocation(1); + LocationManager.createLocation(this.mockMvc, location); + + this.mockMvc.perform(get("/api/v1/locations/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(location.getId())) + .andExpect(jsonPath("$.name").value(location.getName())) + .andExpect(jsonPath("$.description").value(location.getDescription())) + .andExpect(jsonPath("$.icon").value(location.getIcon())); + } + + /** + * Can create a new location + * + * @throws Exception + */ + @Test + void canCreateNewLocation() throws Exception { + Location location = LocationMock.getMockLocation(1); + + LocationManager.createLocation(this.mockMvc, location) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").isNumber()) + .andExpect(jsonPath("$.name").value(location.getName())) + .andExpect(jsonPath("$.description").value(location.getDescription())) + .andExpect(jsonPath("$.icon").value(location.getIcon())); + } + + /** + * Can not create a new location with a duplicated name + * + * @throws Exception + */ + @Test + void canNotCreateLocationWithTheSameName() throws Exception { + Location location = LocationMock.getMockLocation(1); + + LocationManager.createLocation(this.mockMvc, location); + LocationManager.createLocation(this.mockMvc, location) + .andExpect(status().isConflict()) + .andExpect( + jsonPath("message").value("Location with the name " + location.getName() + " already exists.")); + } + + /** + * Can not create a update a location with a duplicated name + * + * @throws Exception + */ + @Test + void canNotUpdateLocationWithTheSameName() throws Exception { + Location location = LocationMock.getMockLocation(1); + Location locationUpdate = LocationMock.getMockLocation(2); + + LocationManager.createLocation(this.mockMvc, location); + LocationManager.createLocation(this.mockMvc, locationUpdate); + locationUpdate.setName(location.getName()); + LocationManager.updateLocation(this.mockMvc, locationUpdate, locationUpdate.getId()) + .andExpect(status().isConflict()) + .andExpect( + jsonPath("message").value("Location with the name " + location.getName() + " already exists.")); + } + + /** + * Can create a new location + * + * @throws Exception + */ + @Test + void canCreateLocationWithoutDescription() throws Exception { + Location location = LocationMock.getMockLocation(1); + location.setDescription(null); + + LocationManager.createLocation(this.mockMvc, location) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").isNumber()) + .andExpect(jsonPath("$.name").value(location.getName())) + .andExpect(jsonPath("$.description").value(location.getDescription())) + .andExpect(jsonPath("$.icon").value(location.getIcon())); + } + + /** + * Can get all locations + * + * @throws Exception + */ + @Test + void canGetAllLocations() throws Exception { + List locations = List.of(LocationMock.getMockLocation(2), LocationMock.getMockLocation(3), + LocationMock.getMockLocation(4)); + for (Location location : locations) { + LocationManager.createLocation(this.mockMvc, location); + } + this.mockMvc.perform(get("/api/v1/locations/")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(locations.size())) + .andExpect(jsonPath("$[0].name").value(locations.get(0).getName())) + .andExpect(jsonPath("$[1].name").value(locations.get(1).getName())) + .andExpect(jsonPath("$[2].name").value(locations.get(2).getName())); + } + + /** + * Can get the locations summary + * + * @throws Exception + */ + @Test + void canGetLocationsSummary() throws Exception { + List locations = List.of(LocationMock.getMockLocation(1), LocationMock.getMockLocation(2)); + for (Location location : locations) { + LocationManager.createLocation(this.mockMvc, location); + } + + // Add two items + List items = List.of(ItemMock.getMockItem(1), ItemMock.getMockItem(2)); + for (Item item : items) { + item.setLocation(locations.get(0)); + ItemManager.createItem(this.mockMvc, item); + } + + this.mockMvc.perform(get("/api/v1/locations/summary")) + .andDo(res -> System.out.println(res)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(locations.size())) + .andExpect(jsonPath("$[0].location.name").value(locations.get(0).getName())) + .andExpect(jsonPath("$[0].itemsNumber").value(2)) + .andExpect(jsonPath("$[1].location.name").value(locations.get(1).getName())) + .andExpect(jsonPath("$[1].itemsNumber").value(0)); + } + + /** + * Verify create location validation: No name + * + * @throws Exception + */ + @Test + void canNotCreateNewLocationWithoutName() throws Exception { + Location location = LocationMock.getMockLocation(1); + location.setName(null); + location.setDescription(null); + + LocationManager.createLocation(this.mockMvc, location) + .andExpect(status().isBadRequest()); + } + + /** + * Verify create location validation: Empty name + * + * @throws Exception + */ + @Test + void canNotCreateNewEmptyName() throws Exception { + Location location = LocationMock.getMockLocation(1); + location.setName(""); + location.setDescription(""); + + LocationManager.createLocation(this.mockMvc, location) + .andExpect(status().isBadRequest()); + } + + /** + * Verify create location validation: Too long description + * + * @throws Exception + */ + @Test + void canNotCreateNewLocationTooLongDescription() throws Exception { + Location location = LocationMock.getMockLocation(1); + location.setDescription("a".repeat(2001)); + + LocationManager.createLocation(this.mockMvc, location) + .andExpect(status().isBadRequest()); + } + + /** + * Verify create location validation: Name too long + * + * @throws Exception + */ + @Test + void canNotCreateNewWithNameTooLong() throws Exception { + Location location = LocationMock.getMockLocation(1); + location.setName("a".repeat(51)); + + LocationManager.createLocation(this.mockMvc, location) + .andExpect(status().isBadRequest()); + } + + /** + * Verify that a new location can be updated + * + * @throws Exception + */ + @Test + void canUpdateLocation() throws Exception { + Location location = LocationMock.getMockLocation(1); + Location updatedLocation = LocationMock.getMockLocation(2); + updatedLocation.setId(location.getId()); + LocationManager.createLocation(this.mockMvc, location); + + LocationManager.updateLocation(this.mockMvc, updatedLocation, location.getId()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(updatedLocation.getId())) + .andExpect(jsonPath("$.name").value(updatedLocation.getName())) + .andExpect(jsonPath("$.description").value(updatedLocation.getDescription())) + .andExpect(jsonPath("$.icon").value(updatedLocation.getIcon())); + } + + /** + * Verify that a location can be updated and there are no conflicts with the + * name. + * + * This is added because of the validateName(...) method in item + * + * @throws Exception + */ + @Test + void canUpdateLocationSameName() throws Exception { + Location location = LocationMock.getMockLocation(1); + Location updatedLocation = LocationMock.getMockLocation(2); + updatedLocation.setId(location.getId()); + updatedLocation.setName(location.getName()); + LocationManager.createLocation(this.mockMvc, location); + + LocationManager.updateLocation(this.mockMvc, updatedLocation, location.getId()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(updatedLocation.getId())) + .andExpect(jsonPath("$.name").value(updatedLocation.getName())) + .andExpect(jsonPath("$.description").value(updatedLocation.getDescription())) + .andExpect(jsonPath("$.icon").value(updatedLocation.getIcon())); + } + + /** + * Verify that a non existing location can not be updated + * + * @throws Exception + */ + @Test + void canNotUpdateNotExistingLocation() throws Exception { + Location location = LocationMock.getMockLocation(1); + this.mockMvc.perform( + put("/api/v1/locations/" + Integer.toString(location.getId())) + .content(JsonProcessor.stringifyClass(location)) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + /** + * Verify that an location can be deleted + * + * @throws Exception + */ + @Test + void canDeleteLocation() throws Exception { + Location location = LocationMock.getMockLocation(1); + LocationManager.createLocation(this.mockMvc, location); + + this.mockMvc.perform( + delete("/api/v1/locations/" + Integer.toString(location.getId())) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + } + + /** + * Verify that a non existing location can not be deleted + * + * @throws Exception + */ + @Test + void canNotDeleteNonExistingLocation() throws Exception { + Location location = LocationMock.getMockLocation(1); + + this.mockMvc.perform( + delete("/api/v1/locations/" + Integer.toString(location.getId())) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + +} diff --git a/diogenes/src/test/java/dev/nichoko/diogenes/mock/ItemMock.java b/diogenes/src/test/java/dev/nichoko/diogenes/mock/ItemMock.java index 53565c0..62b1955 100644 --- a/diogenes/src/test/java/dev/nichoko/diogenes/mock/ItemMock.java +++ b/diogenes/src/test/java/dev/nichoko/diogenes/mock/ItemMock.java @@ -2,6 +2,7 @@ import dev.nichoko.diogenes.model.domain.Category; import dev.nichoko.diogenes.model.domain.Item; +import dev.nichoko.diogenes.model.domain.Location; public class ItemMock { @@ -18,7 +19,12 @@ public static Item getMockItem(Integer number) { number, "name" + number, "description" + number, - "col" + number)); + "col" + number)); + item.setLocation(new Location( + number, + "name" + number, + "description" + number, + "icon" + number)); return item; } } diff --git a/diogenes/src/test/java/dev/nichoko/diogenes/mock/LocationMock.java b/diogenes/src/test/java/dev/nichoko/diogenes/mock/LocationMock.java new file mode 100644 index 0000000..161e0ad --- /dev/null +++ b/diogenes/src/test/java/dev/nichoko/diogenes/mock/LocationMock.java @@ -0,0 +1,17 @@ +package dev.nichoko.diogenes.mock; + +import dev.nichoko.diogenes.model.domain.Location; + +public class LocationMock { + /* + * Return a mock of an location + */ + public static Location getMockLocation(Integer number) { + return new Location( + number, + "TestName" + number.toString(), + "Description" + number.toString(), + "AB02" + number.toString()); + } + +} diff --git a/e2e/generate_test_data.py b/e2e/generate_test_data.py index a3542fe..55c3f1f 100644 --- a/e2e/generate_test_data.py +++ b/e2e/generate_test_data.py @@ -11,34 +11,51 @@ SERVER = "http://localhost:8080/diogenes" BASE_URL = f"{SERVER}/api/v1" +CONTENT_TYPE_JSON = "application/json" + class Item(BaseModel): """ Representation of an item """ + name: str description: str category_id: int = Field(alias="categoryId") + location_id: int = Field(alias="locationId") number: int + class Category(BaseModel): """ - Representation of a class + Representation of a category """ + id: int name: str description: str color: str +class Location(BaseModel): + """ + Representation of a location + """ + + id: int + name: str + description: str + icon: str + + def generate_random_color() -> str: """ Generate a random color """ - r = random.randint(0,255) - g = random.randint(0,255) - b = random.randint(0,255) - return '%02X%02X%02X' % (r,g,b) + r = random.randint(0, 255) + g = random.randint(0, 255) + b = random.randint(0, 255) + return "%02X%02X%02X" % (r, g, b) def post_categories(token: str, categories: list[Category]): @@ -46,14 +63,11 @@ def post_categories(token: str, categories: list[Category]): Create a set of categories """ url = f"{BASE_URL}/categories/" - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {token}" - } + headers = {"Content-Type": CONTENT_TYPE_JSON, "Authorization": f"Bearer {token}"} for category in categories: data = category.model_dump(by_alias=True) try: - res = requests.post(url, json = data, headers = headers, timeout=30) + res = requests.post(url, json=data, headers=headers, timeout=30) res.raise_for_status() except requests.exceptions.HTTPError as err: if err.response: @@ -61,19 +75,35 @@ def post_categories(token: str, categories: list[Category]): print(f"{category.name} could not be created {code}") continue + +def post_locations(token: str, locations: list[Location]): + """ + Create a set of locations + """ + url = f"{BASE_URL}/locations/" + headers = {"Content-Type": CONTENT_TYPE_JSON, "Authorization": f"Bearer {token}"} + for location in locations: + data = location.model_dump(by_alias=True) + try: + res = requests.post(url, json=data, headers=headers, timeout=30) + res.raise_for_status() + except requests.exceptions.HTTPError as err: + if err.response: + code = err.response.status_code + print(f"{location.name} could not be created {code}") + continue + + def post_items(token: str, items: list[Item]): """ Create a set of items """ url = f"{BASE_URL}/item/" - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {token}" - } + headers = {"Content-Type": CONTENT_TYPE_JSON, "Authorization": f"Bearer {token}"} for item in items: data = item.model_dump(by_alias=True) try: - res = requests.post(url, json = data, headers = headers, timeout=30) + res = requests.post(url, json=data, headers=headers, timeout=30) res.raise_for_status() except requests.exceptions.HTTPError as err: if err.response: @@ -87,21 +117,38 @@ def get_all_categories(token: str) -> list[Category]: """ url = f"{BASE_URL}/categories/" - headers = { - "Authorization": f"Bearer {token}" - } + headers = {"Authorization": f"Bearer {token}"} res = requests.get(url, headers=headers, timeout=30) res.raise_for_status() raw_categories = json.loads(res.content) - categories = [] + categories: list[Category] = [] for raw_category in raw_categories: category = Category.model_validate(raw_category) categories.append(category) return categories +def get_all_locations(token: str) -> list[Location]: + """ + Retrieve all locations + """ + url = f"{BASE_URL}/locations/" + + headers = {"Authorization": f"Bearer {token}"} + + res = requests.get(url, headers=headers, timeout=30) + res.raise_for_status() + raw_locations = json.loads(res.content) + + locations: list[Location] = [] + for raw_location in raw_locations: + location = Location.model_validate(raw_location) + locations.append(location) + return locations + + def read_names(file: str) -> list[str]: """ Read the locally stored categories @@ -114,7 +161,7 @@ def create_categories() -> list[Category]: """ Build categories """ - categories = [] + categories: list[Category] = [] fake = Faker() categories_provider = DynamicProvider( provider_name="categories", @@ -123,20 +170,48 @@ def create_categories() -> list[Category]: fake.add_provider(categories_provider) for counter in range(20): category = Category( - id = counter, - name = fake.categories().strip(), - description= fake.text(), - color=generate_random_color() + id=counter, + name=fake.categories().strip(), + description=fake.text(), + color=generate_random_color(), ) categories.append(category) return categories -def create_items(categories: list[Category]) -> list[Item]: +def create_locations() -> list[Location]: + """ + Build locations + """ + locations: list[Location] = [] + fake = Faker() + locations_provider = DynamicProvider( + provider_name="locations", + elements=read_names("locations.txt"), + ) + fake.add_provider(locations_provider) + fake_2 = Faker() + icons_provider = DynamicProvider( + provider_name="icons", + elements=read_names("icons.txt"), + ) + fake_2.add_provider(icons_provider) + for counter in range(20): + location = Location( + id=counter, + name=fake.locations().strip(), + description=fake.text(), + icon=fake_2.icons().strip(), + ) + locations.append(location) + return locations + + +def create_items(categories: list[Category], locations: list[Location]) -> list[Item]: """ Build items """ - items = [] + items: list[Item] = [] fake = Faker() items_provider = DynamicProvider( provider_name="random_item", @@ -145,24 +220,23 @@ def create_items(categories: list[Category]) -> list[Item]: fake.add_provider(items_provider) for _ in range(20): item = Item( - name = fake.random_item().strip(), - categoryId=categories[random.randint(0, len(categories)-1)].id, - description= fake.text(), - number=random.randint(0,1000), + name=fake.random_item().strip(), + categoryId=categories[random.randint(0, len(categories) - 1)].id, + locationId=locations[random.randint(0, len(locations) - 1)].id, + description=fake.text(), + number=random.randint(0, 1000), ) items.append(item) return items + def get_token(): """ Query to get the token """ url = f"{SERVER}/authenticate" - data = { - "username": "test1", - "password": "test1" - } - res = requests.post(url, json = data, timeout=30) + data = {"username": "test1", "password": "test1"} + res = requests.post(url, json=data, timeout=30) res.raise_for_status() return res.content.decode() @@ -176,8 +250,14 @@ def main(): categories = create_categories() post_categories(token, categories) categories = get_all_categories(token) - items = create_items(categories) + + locations = create_locations() + post_locations(token, locations) + locations = get_all_locations(token) + + items = create_items(categories, locations) post_items(token, items) + if __name__ == "__main__": main() diff --git a/e2e/icons.txt b/e2e/icons.txt new file mode 100644 index 0000000..44f9bbf --- /dev/null +++ b/e2e/icons.txt @@ -0,0 +1,20 @@ +account_box +alarm +attach_file +bookmark +check_circle +dashboard +event +favorite +home +info +language +mail +notifications +payment +search +settings +thumb_up +verified_user +wifi +zoom_out_map \ No newline at end of file diff --git a/e2e/locations.txt b/e2e/locations.txt new file mode 100644 index 0000000..6060425 --- /dev/null +++ b/e2e/locations.txt @@ -0,0 +1,20 @@ +Serenity Suite +Whispering Willow Room +Enchanting Ember Chamber +Celestial Oasis +Radiant Retreat +Velvet Vista Lounge +Tranquil Twilight Haven +Harmonious Hideaway +Mystical Moonlit Quarters +Blissful Breeze Den +Ethereal Echo Nook +Secret Garden Oasis +Starlit Symphony Chamber +Azure Ascension Alcove +Golden Glow Sanctuary +Opulent Orchid Parlor +Cosmic Cascade Suite +Luminous Lagoon Lounge +Emerald Enclave Retreat +Aurora Aura Chamber \ No newline at end of file diff --git a/ng-diogenes/package.json b/ng-diogenes/package.json index 8759a7b..445237b 100644 --- a/ng-diogenes/package.json +++ b/ng-diogenes/package.json @@ -1,6 +1,6 @@ { "name": "ng-diogenes", - "version": "0.0.7", + "version": "0.0.8", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/ng-diogenes/src/app/app-routing.module.ts b/ng-diogenes/src/app/app-routing.module.ts index a9a9a91..d0c51cb 100644 --- a/ng-diogenes/src/app/app-routing.module.ts +++ b/ng-diogenes/src/app/app-routing.module.ts @@ -7,15 +7,23 @@ import { DashboardComponent } from './pages/dashboard/dashboard.component'; import { EditCategoryComponent } from './pages/edit-category/edit-category.component'; import { EditSingleItemComponent } from './pages/edit-item/components/edit-single-item/edit-single-item.component'; import { EditItemComponent } from './pages/edit-item/edit-item.component'; +import { EditLocationComponent } from './pages/edit-location/edit-location.component'; import { InventoryComponent } from './pages/inventory/inventory.component'; import { ItemDetailComponent } from './pages/item-detail/item-detail.component'; +import { LocationDetailComponent } from './pages/location-detail/location-detail.component'; +import { LocationsSummaryComponent } from './pages/locations-summary/locations-summary.component'; +import { LocationsComponent } from './pages/locations/locations.component'; import { LoginComponent } from './pages/login/login.component'; const routes: Routes = [ { path: 'categories/new', component: EditCategoryComponent }, { path: 'categories/:id/edit', component: EditCategoryComponent }, + { path: 'locations/new', component: EditLocationComponent }, + { path: 'locations/:id/edit', component: EditLocationComponent }, { path: 'categories/:id', component: CategoryDetailComponent }, + { path: 'locations/:id', component: LocationDetailComponent }, { path: 'categories', component: CategoriesComponent }, + { path: 'locations', component: LocationsComponent }, { path: 'items/edit', component: EditItemComponent, children: [ @@ -28,6 +36,7 @@ const routes: Routes = [ { path: 'items/:id', component: ItemDetailComponent }, { path: 'items', component: InventoryComponent }, { path: 'summary-categories', component: CategoriesSummaryComponent }, + { path: 'summary-locations', component: LocationsSummaryComponent }, { path: 'dashboard', component: DashboardComponent }, { path: 'home', redirectTo: "dashboard" }, { path: '', component: LoginComponent }, diff --git a/ng-diogenes/src/app/app.module.ts b/ng-diogenes/src/app/app.module.ts index 4a144f7..f2b8d48 100644 --- a/ng-diogenes/src/app/app.module.ts +++ b/ng-diogenes/src/app/app.module.ts @@ -18,6 +18,10 @@ import { SharedComponentsModule } from './shared/components/shared.components.mo import { AuthenticationInterceptor } from './shared/interceptors/authentication.interceptor'; import { CategoriesSummaryModule } from './pages/categories-summary/categories-summary.module'; import { DashboardModule } from './pages/dashboard/dashboard.module'; +import { LocationsModule } from './pages/locations/locations.module'; +import { LocationDetailModule } from './pages/location-detail/location-detail.module'; +import { LocationsSummaryModule } from './pages/locations-summary/locations-summary.module'; +import { EditLocationModule } from './pages/edit-location/edit-location.module'; @NgModule({ declarations: [ @@ -39,6 +43,10 @@ import { DashboardModule } from './pages/dashboard/dashboard.module'; LoginModule, CategoriesSummaryModule, DashboardModule, + LocationsModule, + LocationDetailModule, + LocationsSummaryModule, + EditLocationModule, SharedComponentsModule, diff --git a/ng-diogenes/src/app/models/Item.ts b/ng-diogenes/src/app/models/Item.ts index 9e4bbfa..0ff9a5a 100644 --- a/ng-diogenes/src/app/models/Item.ts +++ b/ng-diogenes/src/app/models/Item.ts @@ -1,4 +1,5 @@ import { Category } from "./Category"; +import { Location } from "./Location"; export interface Item { id: number; @@ -6,6 +7,7 @@ export interface Item { description: string; number: number; category: Category; + location: Location; createdOn: Date; updatedOn: Date; imagePath: string | undefined; diff --git a/ng-diogenes/src/app/models/ItemFilter.ts b/ng-diogenes/src/app/models/ItemFilter.ts index 8e9e009..dc0a400 100644 --- a/ng-diogenes/src/app/models/ItemFilter.ts +++ b/ng-diogenes/src/app/models/ItemFilter.ts @@ -3,4 +3,5 @@ export interface ItemFilter { description?: string; number?: number; categoryId?: number; + locationId?: number; } \ No newline at end of file diff --git a/ng-diogenes/src/app/models/Location.ts b/ng-diogenes/src/app/models/Location.ts new file mode 100644 index 0000000..5917bcd --- /dev/null +++ b/ng-diogenes/src/app/models/Location.ts @@ -0,0 +1,8 @@ +export interface Location { + id: number; + name: string; + description: string; + icon: string; + updatedOn: Date; + createdOn: Date; +} \ No newline at end of file diff --git a/ng-diogenes/src/app/models/LocationSummary.ts b/ng-diogenes/src/app/models/LocationSummary.ts new file mode 100644 index 0000000..cd6d135 --- /dev/null +++ b/ng-diogenes/src/app/models/LocationSummary.ts @@ -0,0 +1,6 @@ +import { Location } from "./Location"; + +export interface LocationSummary { + location: Location; + itemsNumber: number; +} \ No newline at end of file diff --git a/ng-diogenes/src/app/pages/dashboard/dashboard.component.ts b/ng-diogenes/src/app/pages/dashboard/dashboard.component.ts index d7779dd..bd8426e 100644 --- a/ng-diogenes/src/app/pages/dashboard/dashboard.component.ts +++ b/ng-diogenes/src/app/pages/dashboard/dashboard.component.ts @@ -20,6 +20,11 @@ export class DashboardComponent { name: "Categories overview", route: "/summary-categories", icon: "category" + }, + { + name: "Locations overview", + route: "/summary-locations", + icon: "location_on" } ] diff --git a/ng-diogenes/src/app/pages/edit-item/components/edit-single-item/edit-single-item.component.html b/ng-diogenes/src/app/pages/edit-item/components/edit-single-item/edit-single-item.component.html index 86a07c4..d00426a 100644 --- a/ng-diogenes/src/app/pages/edit-item/components/edit-single-item/edit-single-item.component.html +++ b/ng-diogenes/src/app/pages/edit-item/components/edit-single-item/edit-single-item.component.html @@ -51,6 +51,16 @@ + + + Location + + + {{ location.name }} + + + Description diff --git a/ng-diogenes/src/app/pages/edit-item/components/edit-single-item/edit-single-item.component.ts b/ng-diogenes/src/app/pages/edit-item/components/edit-single-item/edit-single-item.component.ts index 9b3fcf5..e50317b 100644 --- a/ng-diogenes/src/app/pages/edit-item/components/edit-single-item/edit-single-item.component.ts +++ b/ng-diogenes/src/app/pages/edit-item/components/edit-single-item/edit-single-item.component.ts @@ -6,8 +6,10 @@ import { Observable, Subscription, catchError, combineLatest, finalize, map, of, import { Category } from 'src/app/models/Category'; import { ImageTransfer } from 'src/app/models/ImageTransfer'; import { Item } from 'src/app/models/Item'; +import { Location } from 'src/app/models/Location'; import { CategoryService } from 'src/app/shared/services/category.service'; import { ItemService } from 'src/app/shared/services/item.service'; +import { LocationService } from 'src/app/shared/services/location.service'; import { MessageService } from 'src/app/shared/services/message.service'; @Component({ @@ -23,6 +25,7 @@ export class EditSingleItemComponent implements OnDestroy, OnInit { itemForm: FormGroup; categories?: Category[]; + locations?: Location[]; item?: Item; initializationError?: string; isNewItem: boolean = true; @@ -35,6 +38,7 @@ export class EditSingleItemComponent implements OnDestroy, OnInit { private fb: FormBuilder, private itemService: ItemService, private categoryService: CategoryService, + private locationService: LocationService, private route: ActivatedRoute, private messageService: MessageService, public dialogService: MatDialog) { @@ -42,7 +46,8 @@ export class EditSingleItemComponent implements OnDestroy, OnInit { name: ['', [Validators.required, Validators.maxLength(50)]], description: ['', [Validators.maxLength(2000)]], number: [1, [Validators.required, Validators.pattern(/^\d+$/)]], - categoryId: [null, Validators.required] + categoryId: [null, Validators.required], + locationId: [null, Validators.required], }); } @@ -53,6 +58,7 @@ export class EditSingleItemComponent implements OnDestroy, OnInit { ngOnInit(): void { combineLatest({ categories: this.categoryService.getCategories(), + locations: this.locationService.getLocations(), item: this.getItemToEdit(), }) .pipe(finalize( @@ -60,8 +66,9 @@ export class EditSingleItemComponent implements OnDestroy, OnInit { this.isLoading = false; } )) - .subscribe(({ categories, item }) => { + .subscribe(({ categories, locations, item }) => { this.categories = categories; + this.locations = locations; if (item) { this.isNewItem = false; this.item = item; @@ -70,6 +77,7 @@ export class EditSingleItemComponent implements OnDestroy, OnInit { this.selectedImageBlob = this.createFromImage.file; } this.preselectPreviousCategory(); + this.preselectPreviousLocation(); }); } @@ -92,6 +100,25 @@ export class EditSingleItemComponent implements OnDestroy, OnInit { } } + /// If there is category saved in the browser and it is a new item auto-select the category + /// as it is likely to be creating items from a similar category + private preselectPreviousLocation(): void { + + if (!this.isNewItem) { + return; + } + + let lastLocation = localStorage.getItem("last-location"); + if (lastLocation && this.locations) { + for (let location of this.locations) { + if (location.name == lastLocation) { + this.itemForm.get("locationId")?.setValue(location.id); + break; + } + } + } + } + onAddNewImage(newValue?: Blob) { this.selectedImageBlob = newValue; } @@ -147,8 +174,13 @@ export class EditSingleItemComponent implements OnDestroy, OnInit { name: item.name, description: item.description, number: item.number, - categoryId: item.category.id, - }) + categoryId: item.category.id || null, + }); + if (item.location) { + this.itemForm.patchValue({ + locationId: item.location.id || null, + }); + } } onSubmit(): void { @@ -178,9 +210,11 @@ export class EditSingleItemComponent implements OnDestroy, OnInit { .subscribe( item => { this.messageService.add(`Item ${item.name} was ${itemTypeMessage}`); + console.log(item); - // Save the current category for the next item creation + // Save the current values for the next item creation localStorage.setItem("last-category", item.category.name); + localStorage.setItem("last-location", item.location.name); this.onItemCreate.emit(true); } diff --git a/ng-diogenes/src/app/pages/edit-item/edit-item.component.ts b/ng-diogenes/src/app/pages/edit-item/edit-item.component.ts index f40a2c5..2dc1b92 100644 --- a/ng-diogenes/src/app/pages/edit-item/edit-item.component.ts +++ b/ng-diogenes/src/app/pages/edit-item/edit-item.component.ts @@ -1,3 +1,4 @@ +import { Location } from '@angular/common'; import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @@ -10,7 +11,7 @@ export class EditItemComponent implements OnInit { singleItemEdit = true; - constructor(private router: Router) { } + constructor(private router: Router, private location: Location) { } ngOnInit(): void { this.checkBulkUrl(); @@ -25,7 +26,7 @@ export class EditItemComponent implements OnInit { returnToInventory(itemCreated: boolean): void { if (itemCreated) { - this.router.navigateByUrl("/items"); + this.location.back(); } } diff --git a/ng-diogenes/src/app/pages/edit-location/edit-location.component.html b/ng-diogenes/src/app/pages/edit-location/edit-location.component.html new file mode 100644 index 0000000..76d5ca9 --- /dev/null +++ b/ng-diogenes/src/app/pages/edit-location/edit-location.component.html @@ -0,0 +1,70 @@ + + + + Edit location + + + +
+ +
+ + + +
+ +
+ + + +
+ + + + + {{isNewLocation ? "Create location" : "Edit location"}} + + +
+ + + + Name + + + + + Description + + + + + Select Icon + + + {{ icon }} {{ icon }} + + + + + +
+ +
+ +
+
+
+ +
+ +
\ No newline at end of file diff --git a/ng-diogenes/src/app/pages/edit-location/edit-location.component.scss b/ng-diogenes/src/app/pages/edit-location/edit-location.component.scss new file mode 100644 index 0000000..2a10b00 --- /dev/null +++ b/ng-diogenes/src/app/pages/edit-location/edit-location.component.scss @@ -0,0 +1,3 @@ +.textarea-height { + min-height: 150px; +} \ No newline at end of file diff --git a/ng-diogenes/src/app/pages/edit-location/edit-location.component.spec.ts b/ng-diogenes/src/app/pages/edit-location/edit-location.component.spec.ts new file mode 100644 index 0000000..d73267d --- /dev/null +++ b/ng-diogenes/src/app/pages/edit-location/edit-location.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditLocationComponent } from './edit-location.component'; + +describe('EditLocationComponent', () => { + let component: EditLocationComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [EditLocationComponent] + }); + fixture = TestBed.createComponent(EditLocationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ng-diogenes/src/app/pages/edit-location/edit-location.component.ts b/ng-diogenes/src/app/pages/edit-location/edit-location.component.ts new file mode 100644 index 0000000..f65bc14 --- /dev/null +++ b/ng-diogenes/src/app/pages/edit-location/edit-location.component.ts @@ -0,0 +1,143 @@ +import { Component } from '@angular/core'; +import { Location as LocationCommon } from '@angular/common'; +import { Observable, Subscription, catchError, finalize, map, of, switchMap, take, throwError } from 'rxjs'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Location } from 'src/app/models/Location'; +import { LocationService } from 'src/app/shared/services/location.service'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { MessageService } from 'src/app/shared/services/message.service'; +import { getMaterialIcons } from 'src/app/utils/material-icons'; + +@Component({ + selector: 'app-edit-location', + templateUrl: './edit-location.component.html', + styleUrls: ['./edit-location.component.scss'] +}) +export class EditLocationComponent { + initializationSubscription?: Subscription; + + iconList: string[] = getMaterialIcons(); + + locationForm: FormGroup; + locations?: Location[]; + location?: Location; + initializationError?: string; + isNewLocation: boolean = true; + isLoading: boolean = true; + + constructor( + private fb: FormBuilder, + private locationService: LocationService, + private route: ActivatedRoute, + private router: Router, + private messageService: MessageService, + private locationCommon: LocationCommon) { + this.locationForm = this.fb.group({ + name: ['', [Validators.required, Validators.maxLength(50)]], + description: ['', [Validators.maxLength(2000)]], + icon: ['', [Validators.required]], + }); + + } + ngOnDestroy(): void { + this.initializationSubscription?.unsubscribe(); + } + + ngOnInit(): void { + this.initializationSubscription = this.getLocationToEdit() + .pipe(finalize( + () => { + console.log("Finalize"); + this.isLoading = false; + } + )) + .subscribe((location: Location | undefined) => { + if (location) { + this.isNewLocation = false; + this.location = location; + this.initializeEditFormWithLocationData(location); + } + }); + } + + private getLocationToEdit(): Observable { + return this.route.paramMap.pipe( + take(1), // paramMap keeps emitting values + map(params => this.getRouteParameters(params)), + switchMap(locationId => { + if (locationId) { + return this.locationService.getLocation(locationId) + } + return throwError(() => new Error("Location ID not provided")); + } + ), + catchError(error => { + if (error.message !== "Location ID not provided") { + this.initializationError = "The location to edit could not be found."; + this.isNewLocation = true; + } + return of(undefined); + }) + ); + } + + private getRouteParameters(params: ParamMap): number | undefined { + const idString = params.get("id"); + if (idString) { + const id = parseInt(idString); + if (isNaN(id)) { + this.initializationError = `Error: Invalid id ${idString} received to edit the location`; + throwError(() => new Error(this.initializationError)); + } + return id; + } + + return undefined; + } + + + private initializeEditFormWithLocationData(location: Location): void { + this.locationForm.patchValue({ + name: location.name, + description: location.description, + icon: location.icon, + }); + } + + onSubmit(): void { + if (this.locationForm.valid) { + + this.isLoading = true; + + const newLocation: Location = this.locationForm.value as Location; + + // Select service + let manageLocationObservable = this.locationService.postLocation(newLocation); + let locationTypeMessage = "created"; + if (!this.isNewLocation) { + newLocation.id = this.location?.id!; + manageLocationObservable = this.locationService.updateLocation(newLocation); + locationTypeMessage = "updated"; + } + + manageLocationObservable + .pipe( + catchError(error => { + this.messageService.add(`Location ${newLocation.name} could not be ${locationTypeMessage}. ${error}`) + return throwError(() => error); + }), + finalize(() => this.isLoading = false) + ) + .subscribe( + location => { + this.messageService.add(`Location ${location.name} was ${locationTypeMessage}`); + this.router.navigateByUrl("/locations"); + } + ); + } + } + + onNavigateBack(): void { + this.locationCommon.back(); + } +} diff --git a/ng-diogenes/src/app/pages/edit-location/edit-location.module.ts b/ng-diogenes/src/app/pages/edit-location/edit-location.module.ts new file mode 100644 index 0000000..a6ad8bc --- /dev/null +++ b/ng-diogenes/src/app/pages/edit-location/edit-location.module.ts @@ -0,0 +1,42 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { EditLocationComponent } from './edit-location.component'; +import { MAT_COLOR_FORMATS, NGX_MAT_COLOR_FORMATS, NgxMatColorPickerModule } from '@angular-material-components/color-picker'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatCardModule } from '@angular/material/card'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatInputModule } from '@angular/material/input'; +import { SharedComponentsModule } from 'src/app/shared/components/shared.components.module'; +import { MatSelectModule } from '@angular/material/select'; + + + +@NgModule({ + declarations: [EditLocationComponent], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + + // Internal + SharedComponentsModule, + + // Material + MatFormFieldModule, + MatCardModule, + MatToolbarModule, + MatIconModule, + MatButtonModule, + MatInputModule, + MatSelectModule, + + ], + exports: [ + EditLocationComponent + ], + providers: [{ provide: MAT_COLOR_FORMATS, useValue: NGX_MAT_COLOR_FORMATS }], +}) +export class EditLocationModule { } diff --git a/ng-diogenes/src/app/pages/inventory/components/inventory-filter/inventory-filter.component.html b/ng-diogenes/src/app/pages/inventory/components/inventory-filter/inventory-filter.component.html index 8ae3290..cfa19d4 100644 --- a/ng-diogenes/src/app/pages/inventory/components/inventory-filter/inventory-filter.component.html +++ b/ng-diogenes/src/app/pages/inventory/components/inventory-filter/inventory-filter.component.html @@ -16,6 +16,9 @@

+
diff --git a/ng-diogenes/src/app/pages/inventory/components/inventory-filter/inventory-filter.component.ts b/ng-diogenes/src/app/pages/inventory/components/inventory-filter/inventory-filter.component.ts index 424f206..ac8f6cb 100644 --- a/ng-diogenes/src/app/pages/inventory/components/inventory-filter/inventory-filter.component.ts +++ b/ng-diogenes/src/app/pages/inventory/components/inventory-filter/inventory-filter.component.ts @@ -1,10 +1,12 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Subscription } from 'rxjs'; +import { Subscription, combineLatest } from 'rxjs'; import { Category } from 'src/app/models/Category'; +import { Location } from 'src/app/models/Location'; import { ItemFilter } from 'src/app/models/ItemFilter'; import { CategoryService } from 'src/app/shared/services/category.service'; +import { LocationService } from 'src/app/shared/services/location.service'; import { isNumberValidator } from 'src/app/utils/form-validator/number'; @@ -23,10 +25,12 @@ export class InventoryFilterComponent implements OnInit, OnDestroy { descriptionFilterName: string = 'description'; numberFilterName: string = 'number'; categoryIdFilterName: string = 'categoryId'; + locationIdFilterName: string = 'locationId'; // Dropbox options categorySubscription?: Subscription; categories: Category[] = []; + locations: Location[] = []; // Status to clear filters filterIsActive = false; @@ -34,13 +38,15 @@ export class InventoryFilterComponent implements OnInit, OnDestroy { constructor( private fb: FormBuilder, private categoryService: CategoryService, + private locationService: LocationService, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public previousFilter?: ItemFilter) { this.form = this.fb.group({ name: new FormControl("", [Validators.maxLength(50)]), number: new FormControl("", [isNumberValidator()]), description: new FormControl("", [Validators.maxLength(200)]), - categoryId: new FormControl("") + categoryId: new FormControl(""), + locationId: new FormControl(""), }); } @@ -49,8 +55,12 @@ export class InventoryFilterComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.categorySubscription = this.categoryService.getCategories().subscribe(categories => { + combineLatest({ + categories: this.categoryService.getCategories(), + locations: this.locationService.getLocations(), + }).subscribe(({categories, locations}) => { this.categories = categories; + this.locations = locations; this.prefillPreviousFilter(); }); } @@ -70,6 +80,7 @@ export class InventoryFilterComponent implements OnInit, OnDestroy { "number": this.previousFilter.number, "description": this.previousFilter.description, "categoryId": this.previousFilter.categoryId, + "locationId": this.previousFilter.locationId, } ); this.filterIsActive = true; diff --git a/ng-diogenes/src/app/pages/inventory/components/item-in-list/item-in-list.component.html b/ng-diogenes/src/app/pages/inventory/components/item-in-list/item-in-list.component.html index bba8431..1a931e4 100644 --- a/ng-diogenes/src/app/pages/inventory/components/item-in-list/item-in-list.component.html +++ b/ng-diogenes/src/app/pages/inventory/components/item-in-list/item-in-list.component.html @@ -3,8 +3,13 @@ - -
+ +
+ +
- image + image - + { this.itemFilter = { categoryId: params['categoryId'] || undefined, + locationId: params['locationId'] || undefined, name: params['name'] || undefined, description: params['description'] || undefined, number: params['number'] || undefined @@ -144,7 +145,7 @@ export class InventoryComponent implements OnInit, OnDestroy { for (let urlParameters of [this.itemFilter, this.itemSorter]) { if (urlParameters) { const filterQueryString = Object.entries(urlParameters) - .filter(([_key, value]) => value !== undefined && value != null) + .filter(([_key, value]) => value != undefined && value != null && value != "") .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); url = `${url}&${filterQueryString}`; diff --git a/ng-diogenes/src/app/pages/item-detail/item-detail.component.html b/ng-diogenes/src/app/pages/item-detail/item-detail.component.html index 5d25847..3388da5 100644 --- a/ng-diogenes/src/app/pages/item-detail/item-detail.component.html +++ b/ng-diogenes/src/app/pages/item-detail/item-detail.component.html @@ -13,6 +13,7 @@
+ @@ -22,10 +23,16 @@ - {{item.category.name}} + {{item.category.name}} ({{item.location.name}}) -
+ +
+ +
@@ -41,13 +48,13 @@
-

+

{{ item.description }}


-
diff --git a/ng-diogenes/src/app/pages/location-detail/location-detail.component.html b/ng-diogenes/src/app/pages/location-detail/location-detail.component.html new file mode 100644 index 0000000..171f03e --- /dev/null +++ b/ng-diogenes/src/app/pages/location-detail/location-detail.component.html @@ -0,0 +1,66 @@ + + + + Location detail + + + +
+ +
+ + + +
+ + + + + + {{ location.name }} + + + +
+ +
+ +
+ + + +

+ {{ location.description }} +

+
+

Last Updated: {{ location.updatedOn | date:'medium' + }}

+

Created On: {{ location.createdOn | date:'medium' }}

+
+
+ + + + + + + + + +
+
+ + + + + +
\ No newline at end of file diff --git a/ng-diogenes/src/app/pages/location-detail/location-detail.component.scss b/ng-diogenes/src/app/pages/location-detail/location-detail.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/ng-diogenes/src/app/pages/location-detail/location-detail.component.spec.ts b/ng-diogenes/src/app/pages/location-detail/location-detail.component.spec.ts new file mode 100644 index 0000000..cbd780e --- /dev/null +++ b/ng-diogenes/src/app/pages/location-detail/location-detail.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LocationDetailComponent } from './location-detail.component'; + +describe('LocationDetailComponent', () => { + let component: LocationDetailComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [LocationDetailComponent] + }); + fixture = TestBed.createComponent(LocationDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ng-diogenes/src/app/pages/location-detail/location-detail.component.ts b/ng-diogenes/src/app/pages/location-detail/location-detail.component.ts new file mode 100644 index 0000000..1b47e8a --- /dev/null +++ b/ng-diogenes/src/app/pages/location-detail/location-detail.component.ts @@ -0,0 +1,115 @@ +import { Location as CommonLocation, } from '@angular/common'; +import { Component, OnDestroy, OnInit, } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Observable, Subscription, catchError, finalize, of } from 'rxjs'; +import { Location } from 'src/app/models/Location'; +import { ConfirmationDialogComponent } from 'src/app/shared/components/confirmation-dialog/confirmation-dialog.component'; +import { LocationService } from 'src/app/shared/services/location.service'; +import { MessageService } from 'src/app/shared/services/message.service'; + +@Component({ + selector: 'app-location-detail', + templateUrl: './location-detail.component.html', + styleUrls: ['./location-detail.component.scss'] +}) +export class LocationDetailComponent implements OnInit, OnDestroy { + + locationServiceSubscription?: Subscription; + paramsSubscription?: Subscription; + location?: Location; + isLoading = false; + constructor( + private locationService: LocationService, + private messageService: MessageService, + private route: ActivatedRoute, + private commonLocation: CommonLocation, + public dialogService: MatDialog, + private router: Router) { + + } + + ngOnInit(): void { + this.getLocation(); + } + + ngOnDestroy(): void { + this.locationServiceSubscription?.unsubscribe(); + this.paramsSubscription?.unsubscribe(); + } + + private getLocation(): void { + this.isLoading = true; + this.paramsSubscription = this.route.paramMap + .pipe( + finalize(() => this.isLoading = false), + ) + .subscribe(param => { + const idParam = param.get("id"); + if (idParam) { + const id = parseInt(idParam); + this.queryItem(id) + } + }); + } + + private queryItem(id: number): void { + this.locationServiceSubscription = this.locationService.getLocation(id) + .pipe( + finalize(() => this.isLoading = false), + ) + .subscribe(location => { + this.location = location; + }) + } + + private openDialog(): Observable { + const dialogRef = this.dialogService.open(ConfirmationDialogComponent, { + data: { title: "Delete location?", content: `Do you really want to delete ${this.location!.name}?` }, + }); + + return dialogRef.afterClosed(); + } + + onNavigateBack(): void { + this.commonLocation.back(); + } + + onEditLocation(): void { + this.router.navigateByUrl(`/locations/${this.location?.id}/edit`); + } + + private deleteLocation() { + this.isLoading = true; + this.locationService.deleteLocation(this.location!.id) + .pipe( + finalize(() => this.isLoading = false), + catchError((err) => { + const errorMessage = err["error"]["message"]; + this.messageService.add(`The location could not be deleted. ${errorMessage}`); + return of(1); + }) + ) + .subscribe((result) => { + if (result != 1) { + this.messageService.add(`Location ${this.location!.name} was deleted`); + + this.onNavigateBack(); + } + }); + } + + onDeleteLocation(): void { + if (!this.location) { + console.error("Trying to delete an location that is not defined"); + } + + this.openDialog().subscribe(result => { + console.log('The dialog was closed: ' + result); + if (result as boolean) { + this.deleteLocation(); + } + }); + + } +} diff --git a/ng-diogenes/src/app/pages/location-detail/location-detail.module.ts b/ng-diogenes/src/app/pages/location-detail/location-detail.module.ts new file mode 100644 index 0000000..f880ebc --- /dev/null +++ b/ng-diogenes/src/app/pages/location-detail/location-detail.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { LocationDetailComponent } from './location-detail.component'; +import { MatIconModule } from '@angular/material/icon'; +import { SharedComponentsModule } from 'src/app/shared/components/shared.components.module'; +import { MatCardModule } from '@angular/material/card'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatButtonModule } from '@angular/material/button'; + + + +@NgModule({ + declarations: [LocationDetailComponent], + imports: [ + CommonModule, + + // Local + SharedComponentsModule, + + // Material + MatIconModule, + MatCardModule, + MatToolbarModule, + MatButtonModule, + ], + exports: [ + LocationDetailComponent + ] +}) +export class LocationDetailModule { } diff --git a/ng-diogenes/src/app/pages/locations-summary/locations-summary-menu/locations-summary-menu.component.html b/ng-diogenes/src/app/pages/locations-summary/locations-summary-menu/locations-summary-menu.component.html new file mode 100644 index 0000000..f368bb9 --- /dev/null +++ b/ng-diogenes/src/app/pages/locations-summary/locations-summary-menu/locations-summary-menu.component.html @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/ng-diogenes/src/app/pages/locations-summary/locations-summary-menu/locations-summary-menu.component.scss b/ng-diogenes/src/app/pages/locations-summary/locations-summary-menu/locations-summary-menu.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/ng-diogenes/src/app/pages/locations-summary/locations-summary-menu/locations-summary-menu.component.spec.ts b/ng-diogenes/src/app/pages/locations-summary/locations-summary-menu/locations-summary-menu.component.spec.ts new file mode 100644 index 0000000..4786289 --- /dev/null +++ b/ng-diogenes/src/app/pages/locations-summary/locations-summary-menu/locations-summary-menu.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LocationsSummaryMenuComponent } from './locations-summary-menu.component'; + +describe('LocationsSummaryMenuComponent', () => { + let component: LocationsSummaryMenuComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LocationsSummaryMenuComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LocationsSummaryMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ng-diogenes/src/app/pages/locations-summary/locations-summary-menu/locations-summary-menu.component.ts b/ng-diogenes/src/app/pages/locations-summary/locations-summary-menu/locations-summary-menu.component.ts new file mode 100644 index 0000000..c283fdf --- /dev/null +++ b/ng-diogenes/src/app/pages/locations-summary/locations-summary-menu/locations-summary-menu.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthenticationService } from 'src/app/shared/services/authentication.service'; + +@Component({ + selector: 'app-locations-summary-menu', + standalone: false, + templateUrl: './locations-summary-menu.component.html', + styleUrl: './locations-summary-menu.component.scss' +}) +export class LocationsSummaryMenuComponent { + + constructor(private router: Router, private authenticationService: AuthenticationService) { } + + onNavigateToLocations(): void { + this.router.navigateByUrl("/locations"); + } + + onLogout(): void { + this.authenticationService.logout(); + this.router.navigateByUrl("/"); + } + +} + diff --git a/ng-diogenes/src/app/pages/locations-summary/locations-summary.component.html b/ng-diogenes/src/app/pages/locations-summary/locations-summary.component.html new file mode 100644 index 0000000..1a94d44 --- /dev/null +++ b/ng-diogenes/src/app/pages/locations-summary/locations-summary.component.html @@ -0,0 +1,80 @@ + + + + + + +
+ + Locations +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+ +
+ + + + {{ + summary.location.name }} + + + + + + + Nr. of items: + + + + x + {{ summary.itemsNumber}} + + + +
+ +
+
+ + + + + + + +
\ No newline at end of file diff --git a/ng-diogenes/src/app/pages/locations-summary/locations-summary.component.scss b/ng-diogenes/src/app/pages/locations-summary/locations-summary.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/ng-diogenes/src/app/pages/locations-summary/locations-summary.component.spec.ts b/ng-diogenes/src/app/pages/locations-summary/locations-summary.component.spec.ts new file mode 100644 index 0000000..ac891e5 --- /dev/null +++ b/ng-diogenes/src/app/pages/locations-summary/locations-summary.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LocationsSummaryComponent } from './locations-summary.component'; + +describe('LocationsSummaryComponent', () => { + let component: LocationsSummaryComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LocationsSummaryComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LocationsSummaryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ng-diogenes/src/app/pages/locations-summary/locations-summary.component.ts b/ng-diogenes/src/app/pages/locations-summary/locations-summary.component.ts new file mode 100644 index 0000000..7cb9e48 --- /dev/null +++ b/ng-diogenes/src/app/pages/locations-summary/locations-summary.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { LocationService } from 'src/app/shared/services/location.service'; +import { Subscription, finalize } from 'rxjs'; +import { Location } from 'src/app/models/Location'; +import { LocationSummary } from 'src/app/models/LocationSummary'; + +@Component({ + selector: 'app-locations-summary', + standalone: false, + templateUrl: './locations-summary.component.html', + styleUrl: './locations-summary.component.scss' +}) +export class LocationsSummaryComponent implements OnInit { + + isLoading = true; + locationSubscription?: Subscription; + locationsSummary?: LocationSummary[]; + + constructor(private router: Router, private locationService: LocationService) { }; + + ngOnDestroy(): void { + this.locationSubscription?.unsubscribe(); + } + + ngOnInit(): void { + this.locationSubscription = this.locationService.getLocationsSummary() + .pipe( + finalize(() => { + this.isLoading = false; + }) + ) + .subscribe(locationsSummary => { + this.locationsSummary = locationsSummary; + }); + } + + onCreateNewItem(): void { + this.router.navigateByUrl("/items/edit/new"); + } + + onOpenLocation(location: Location): void { + this.router.navigateByUrl(`/items?locationId=${location.id}`,); + } + +} diff --git a/ng-diogenes/src/app/pages/locations-summary/locations-summary.module.ts b/ng-diogenes/src/app/pages/locations-summary/locations-summary.module.ts new file mode 100644 index 0000000..c5f46d4 --- /dev/null +++ b/ng-diogenes/src/app/pages/locations-summary/locations-summary.module.ts @@ -0,0 +1,52 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatSelectModule } from '@angular/material/select'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { InfiniteScrollModule } from 'ngx-infinite-scroll'; +import { SharedComponentsModule } from 'src/app/shared/components/shared.components.module'; +import { SharedPipesModule } from 'src/app/shared/pipes/shared.pipes.module'; +import { LocationsSummaryComponent } from './locations-summary.component'; +import { LocationsSummaryMenuComponent } from './locations-summary-menu/locations-summary-menu.component'; + + + +@NgModule({ + declarations: [ + LocationsSummaryComponent, + LocationsSummaryMenuComponent + ], + imports: [ + CommonModule, + ReactiveFormsModule, + FormsModule, + + // Local + SharedComponentsModule, + SharedPipesModule, + + // External + InfiniteScrollModule, + + // Material + MatIconModule, + MatButtonModule, + MatToolbarModule, + MatSelectModule, + MatInputModule, + MatCardModule, + MatMenuModule, + MatDialogModule, + + ], + exports: [ + LocationsSummaryComponent + ] +}) +export class LocationsSummaryModule { } diff --git a/ng-diogenes/src/app/pages/locations/components/location-in-list/location-in-list.component.html b/ng-diogenes/src/app/pages/locations/components/location-in-list/location-in-list.component.html new file mode 100644 index 0000000..882ed34 --- /dev/null +++ b/ng-diogenes/src/app/pages/locations/components/location-in-list/location-in-list.component.html @@ -0,0 +1,23 @@ + + + + + +
+ +
+ + + + + + {{ location.name }} + + + + +
+ +
diff --git a/ng-diogenes/src/app/pages/locations/components/location-in-list/location-in-list.component.scss b/ng-diogenes/src/app/pages/locations/components/location-in-list/location-in-list.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/ng-diogenes/src/app/pages/locations/components/location-in-list/location-in-list.component.spec.ts b/ng-diogenes/src/app/pages/locations/components/location-in-list/location-in-list.component.spec.ts new file mode 100644 index 0000000..581a800 --- /dev/null +++ b/ng-diogenes/src/app/pages/locations/components/location-in-list/location-in-list.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CategoryInListComponent } from './location-in-list.component'; + +describe('CategoryInListComponent', () => { + let component: CategoryInListComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [CategoryInListComponent] + }); + fixture = TestBed.createComponent(CategoryInListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ng-diogenes/src/app/pages/locations/components/location-in-list/location-in-list.component.ts b/ng-diogenes/src/app/pages/locations/components/location-in-list/location-in-list.component.ts new file mode 100644 index 0000000..9f26c9b --- /dev/null +++ b/ng-diogenes/src/app/pages/locations/components/location-in-list/location-in-list.component.ts @@ -0,0 +1,22 @@ +import { Component, Input } from '@angular/core'; +import { Router } from '@angular/router'; +import { Location } from 'src/app/models/Location'; + +@Component({ + selector: 'app-location-in-list', + templateUrl: './location-in-list.component.html', + styleUrls: ['./location-in-list.component.scss'] +}) +export class LocationInListComponent { + + @Input() location!: Location; + + constructor(private router: Router) { + + } + + onOpenDetails() { + this.router.navigate(["locations", this.location.id]); + } + +} diff --git a/ng-diogenes/src/app/pages/locations/locations.component.html b/ng-diogenes/src/app/pages/locations/locations.component.html new file mode 100644 index 0000000..2b21f86 --- /dev/null +++ b/ng-diogenes/src/app/pages/locations/locations.component.html @@ -0,0 +1,27 @@ + + + +
+ Locations +
+
+
+ + +
+ +
+ + +
+
+ +
+
+ + + + + diff --git a/ng-diogenes/src/app/pages/locations/locations.component.scss b/ng-diogenes/src/app/pages/locations/locations.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/ng-diogenes/src/app/pages/locations/locations.component.spec.ts b/ng-diogenes/src/app/pages/locations/locations.component.spec.ts new file mode 100644 index 0000000..26ff216 --- /dev/null +++ b/ng-diogenes/src/app/pages/locations/locations.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LocationsComponent } from './locations.component'; + +describe('LocationsComponent', () => { + let component: LocationsComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [LocationsComponent] + }); + fixture = TestBed.createComponent(LocationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ng-diogenes/src/app/pages/locations/locations.component.ts b/ng-diogenes/src/app/pages/locations/locations.component.ts new file mode 100644 index 0000000..016ea2b --- /dev/null +++ b/ng-diogenes/src/app/pages/locations/locations.component.ts @@ -0,0 +1,32 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { finalize } from 'rxjs'; +import { Location } from 'src/app/models/Location'; +import { LocationService } from 'src/app/shared/services/location.service'; + +@Component({ + selector: 'app-locations', + templateUrl: './locations.component.html', + styleUrls: ['./locations.component.scss'] +}) +export class LocationsComponent implements OnInit { + + locations: Location[] = []; + isLoading: boolean = false; + constructor(private locationService: LocationService, private router: Router) { } + + ngOnInit(): void { + this.fetchLocations(); + } + + private fetchLocations() { + this.isLoading = true; + this.locationService.getLocations() + .pipe(finalize(() => this.isLoading = false)) + .subscribe((locations) => this.locations = locations); + } + + onCreateNewLocation(): void { + this.router.navigateByUrl("/locations/new"); + } +} diff --git a/ng-diogenes/src/app/pages/locations/locations.module.ts b/ng-diogenes/src/app/pages/locations/locations.module.ts new file mode 100644 index 0000000..3d4758b --- /dev/null +++ b/ng-diogenes/src/app/pages/locations/locations.module.ts @@ -0,0 +1,34 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { LocationsComponent } from './locations.component'; +import { LocationInListComponent } from './components/location-in-list/location-in-list.component'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatCardModule } from '@angular/material/card'; +import { SharedComponentsModule } from 'src/app/shared/components/shared.components.module'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; + + + +@NgModule({ + declarations: [ + LocationsComponent, + LocationInListComponent + ], + imports: [ + CommonModule, + + // Local + SharedComponentsModule, + + // Material + MatToolbarModule, + MatCardModule, + MatIconModule, + MatButtonModule, + ], + exports: [ + LocationsComponent + ] +}) +export class LocationsModule { } diff --git a/ng-diogenes/src/app/shared/pipes/generate-select-box-options.pipe.ts b/ng-diogenes/src/app/shared/pipes/generate-select-box-options.pipe.ts index 81ce0c4..21c4ad8 100644 --- a/ng-diogenes/src/app/shared/pipes/generate-select-box-options.pipe.ts +++ b/ng-diogenes/src/app/shared/pipes/generate-select-box-options.pipe.ts @@ -1,13 +1,14 @@ import { Pipe, PipeTransform } from '@angular/core'; import { Category } from '../../models/Category'; +import { Location } from 'src/app/models/Location'; @Pipe({ name: 'generateSelectBoxOptions' }) export class GenerateSelectBoxOptionsPipe implements PipeTransform { - transform(values: Category[], ...args: unknown[]): [string, string | number][] { - return values.map(category => [category.name, category.id]); + transform(values: Category[] | Location[], ...args: unknown[]): [string, string | number][] { + return values.map(value => [value.name, value.id]); } } diff --git a/ng-diogenes/src/app/shared/services/location.service.spec.ts b/ng-diogenes/src/app/shared/services/location.service.spec.ts new file mode 100644 index 0000000..49dca00 --- /dev/null +++ b/ng-diogenes/src/app/shared/services/location.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { LocationService } from './location.service'; + +describe('LocationService', () => { + let service: LocationService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LocationService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/ng-diogenes/src/app/shared/services/location.service.ts b/ng-diogenes/src/app/shared/services/location.service.ts new file mode 100644 index 0000000..bde9e0a --- /dev/null +++ b/ng-diogenes/src/app/shared/services/location.service.ts @@ -0,0 +1,114 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, catchError, map, tap } from 'rxjs'; + +import { Location } from '../../models/Location'; +import { ErrorHandlerService } from './error-handler.service'; +import { environment } from 'src/environments/environment'; +import { LocationSummary } from 'src/app/models/LocationSummary'; + + +@Injectable({ + providedIn: 'root' +}) +export class LocationService { + private backendUrl = environment.diogenesBackendURL; + private urlPath = "api/v1/locations"; + private url = `${this.backendUrl}/${this.urlPath}`; + + + constructor(private httpClient: HttpClient, private errorHandler: ErrorHandlerService) { } + + getLocations(): Observable { + let url = `${this.url}/`; + + console.log(url); + return this.httpClient.get(url) + .pipe( + map((rawLocations) => { + const locations: Location[] = rawLocations.map((location) => ({ ...location })); + return locations; + }), + tap((locations) => console.log(locations)), + catchError(this.errorHandler.handleError("getLocations", [])) + ); + } + + getLocationsSummary(): Observable { + let url = `${this.url}/summary`; + + console.log(url); + return this.httpClient.get(url) + .pipe( + map((rawSummary) => { + const locations: LocationSummary[] = rawSummary.map((summary) => ({ ...summary })); + return locations; + }), + tap((locations) => console.log(locations)), + catchError(this.errorHandler.handleError("getLocationsSummary", [])) + ); + } + + getLocation(locationId: number): Observable { + let url = `${this.url}/${locationId}`; + + console.log(url); + + return this.httpClient.get(url) + .pipe( + map((rawLocation) => { + const location: Location = { ...rawLocation }; + return location; + }), + tap((location) => console.log(location)), + catchError(this.errorHandler.handleError("getLocation")) + ); + } + + deleteLocation(id: number): Observable { + const url = `${this.url}/${id}`; + + console.log(url); + + return this.httpClient.delete(url) + .pipe( + tap(_ => console.log(`Location ${id} deleted`)), + catchError(this.errorHandler.handleError("deleteLocation"))); + } + + postLocation(location: Location): Observable { + let url = `${this.url}/`; + + console.log(url); + + let data = { ...location }; + + return this.httpClient.post(url, data) + .pipe( + map((rawLocation) => { + const location: Location = { ...rawLocation }; + return location; + }), + tap((location) => console.log(location)), + catchError(this.errorHandler.handleError("postLocation")) + ); + } + + updateLocation(location: Location): Observable { + let url = `${this.url}/${location.id}`; + + console.log(url); + + let data = { ...location }; + + return this.httpClient.put(url, data) + .pipe( + map((rawLocation) => { + const location: Location = { ...rawLocation }; + return location; + }), + tap((location) => console.log(location)), + catchError(this.errorHandler.handleError("updateLocation")) + ); + } +} diff --git a/ng-diogenes/src/app/utils/material-icons.ts b/ng-diogenes/src/app/utils/material-icons.ts new file mode 100644 index 0000000..81b4092 --- /dev/null +++ b/ng-diogenes/src/app/utils/material-icons.ts @@ -0,0 +1,2241 @@ +// List extracted from: +// https://raw.githubusercontent.com/google/material-design-icons/master/font/MaterialIcons-Regular.codepoints +export function getMaterialIcons(): string[] { + return [ + "10k", + "10mp", + "11mp", + "123", + "12mp", + "13mp", + "14mp", + "15mp", + "16mp", + "17mp", + "18_up_rating", + "18mp", + "19mp", + "1k", + "1k_plus", + "1x_mobiledata", + "20mp", + "21mp", + "22mp", + "23mp", + "24mp", + "2k", + "2k_plus", + "2mp", + "30fps", + "30fps_select", + "360", + "3d_rotation", + "3g_mobiledata", + "3k", + "3k_plus", + "3mp", + "3p", + "4g_mobiledata", + "4g_plus_mobiledata", + "4k", + "4k_plus", + "4mp", + "5g", + "5k", + "5k_plus", + "5mp", + "60fps", + "60fps_select", + "6_ft_apart", + "6k", + "6k_plus", + "6mp", + "7k", + "7k_plus", + "7mp", + "8k", + "8k_plus", + "8mp", + "9k", + "9k_plus", + "9mp", + "abc", + "ac_unit", + "access_alarm", + "access_alarms", + "access_time", + "access_time_filled", + "accessibility", + "accessibility_new", + "accessible", + "accessible_forward", + "account_balance", + "account_balance_wallet", + "account_box", + "account_circle", + "account_tree", + "ad_units", + "adb", + "add", + "add_a_photo", + "add_alarm", + "add_alert", + "add_box", + "add_business", + "add_call", + "add_card", + "add_chart", + "add_circle", + "add_circle_outline", + "add_comment", + "add_home", + "add_home_work", + "add_ic_call", + "add_link", + "add_location", + "add_location_alt", + "add_moderator", + "add_photo_alternate", + "add_reaction", + "add_road", + "add_shopping_cart", + "add_task", + "add_to_drive", + "add_to_home_screen", + "add_to_photos", + "add_to_queue", + "addchart", + "adf_scanner", + "adjust", + "admin_panel_settings", + "adobe", + "ads_click", + "agriculture", + "air", + "airline_seat_flat", + "airline_seat_flat_angled", + "airline_seat_individual_suite", + "airline_seat_legroom_extra", + "airline_seat_legroom_normal", + "airline_seat_legroom_reduced", + "airline_seat_recline_extra", + "airline_seat_recline_normal", + "airline_stops", + "airlines", + "airplane_ticket", + "airplanemode_active", + "airplanemode_inactive", + "airplanemode_off", + "airplanemode_on", + "airplay", + "airport_shuttle", + "alarm", + "alarm_add", + "alarm_off", + "alarm_on", + "album", + "align_horizontal_center", + "align_horizontal_left", + "align_horizontal_right", + "align_vertical_bottom", + "align_vertical_center", + "align_vertical_top", + "all_inbox", + "all_inclusive", + "all_out", + "alt_route", + "alternate_email", + "amp_stories", + "analytics", + "anchor", + "android", + "animation", + "announcement", + "aod", + "apartment", + "api", + "app_blocking", + "app_registration", + "app_settings_alt", + "app_shortcut", + "apple", + "approval", + "apps", + "apps_outage", + "architecture", + "archive", + "area_chart", + "arrow_back", + "arrow_back_ios", + "arrow_back_ios_new", + "arrow_circle_down", + "arrow_circle_left", + "arrow_circle_right", + "arrow_circle_up", + "arrow_downward", + "arrow_drop_down", + "arrow_drop_down_circle", + "arrow_drop_up", + "arrow_forward", + "arrow_forward_ios", + "arrow_left", + "arrow_outward", + "arrow_right", + "arrow_right_alt", + "arrow_upward", + "art_track", + "article", + "aspect_ratio", + "assessment", + "assignment", + "assignment_add", + "assignment_ind", + "assignment_late", + "assignment_return", + "assignment_returned", + "assignment_turned_in", + "assist_walker", + "assistant", + "assistant_direction", + "assistant_navigation", + "assistant_photo", + "assured_workload", + "atm", + "attach_email", + "attach_file", + "attach_money", + "attachment", + "attractions", + "attribution", + "audio_file", + "audiotrack", + "auto_awesome", + "auto_awesome_mosaic", + "auto_awesome_motion", + "auto_delete", + "auto_fix_high", + "auto_fix_normal", + "auto_fix_off", + "auto_graph", + "auto_mode", + "auto_stories", + "autofps_select", + "autorenew", + "av_timer", + "baby_changing_station", + "back_hand", + "backpack", + "backspace", + "backup", + "backup_table", + "badge", + "bakery_dining", + "balance", + "balcony", + "ballot", + "bar_chart", + "barcode_reader", + "batch_prediction", + "bathroom", + "bathtub", + "battery_0_bar", + "battery_1_bar", + "battery_2_bar", + "battery_3_bar", + "battery_4_bar", + "battery_5_bar", + "battery_6_bar", + "battery_alert", + "battery_charging_full", + "battery_full", + "battery_saver", + "battery_std", + "battery_unknown", + "beach_access", + "bed", + "bedroom_baby", + "bedroom_child", + "bedroom_parent", + "bedtime", + "bedtime_off", + "beenhere", + "bento", + "bike_scooter", + "biotech", + "blender", + "blind", + "blinds", + "blinds_closed", + "block", + "block_flipped", + "bloodtype", + "bluetooth", + "bluetooth_audio", + "bluetooth_connected", + "bluetooth_disabled", + "bluetooth_drive", + "bluetooth_searching", + "blur_circular", + "blur_linear", + "blur_off", + "blur_on", + "bolt", + "book", + "book_online", + "bookmark", + "bookmark_add", + "bookmark_added", + "bookmark_border", + "bookmark_outline", + "bookmark_remove", + "bookmarks", + "border_all", + "border_bottom", + "border_clear", + "border_color", + "border_horizontal", + "border_inner", + "border_left", + "border_outer", + "border_right", + "border_style", + "border_top", + "border_vertical", + "boy", + "branding_watermark", + "breakfast_dining", + "brightness_1", + "brightness_2", + "brightness_3", + "brightness_4", + "brightness_5", + "brightness_6", + "brightness_7", + "brightness_auto", + "brightness_high", + "brightness_low", + "brightness_medium", + "broadcast_on_home", + "broadcast_on_personal", + "broken_image", + "browse_gallery", + "browser_not_supported", + "browser_updated", + "brunch_dining", + "brush", + "bubble_chart", + "bug_report", + "build", + "build_circle", + "bungalow", + "burst_mode", + "bus_alert", + "business", + "business_center", + "cabin", + "cable", + "cached", + "cake", + "calculate", + "calendar_month", + "calendar_today", + "calendar_view_day", + "calendar_view_month", + "calendar_view_week", + "call", + "call_end", + "call_made", + "call_merge", + "call_missed", + "call_missed_outgoing", + "call_received", + "call_split", + "call_to_action", + "camera", + "camera_alt", + "camera_enhance", + "camera_front", + "camera_indoor", + "camera_outdoor", + "camera_rear", + "camera_roll", + "cameraswitch", + "campaign", + "cancel", + "cancel_presentation", + "cancel_schedule_send", + "candlestick_chart", + "car_crash", + "car_rental", + "car_repair", + "card_giftcard", + "card_membership", + "card_travel", + "carpenter", + "cases", + "casino", + "cast", + "cast_connected", + "cast_for_education", + "castle", + "catching_pokemon", + "category", + "celebration", + "cell_tower", + "cell_wifi", + "center_focus_strong", + "center_focus_weak", + "chair", + "chair_alt", + "chalet", + "change_circle", + "change_history", + "charging_station", + "chat", + "chat_bubble", + "chat_bubble_outline", + "check", + "check_box", + "check_box_outline_blank", + "check_circle", + "check_circle_outline", + "checklist", + "checklist_rtl", + "checkroom", + "chevron_left", + "chevron_right", + "child_care", + "child_friendly", + "chrome_reader_mode", + "church", + "circle", + "circle_notifications", + "class", + "clean_hands", + "cleaning_services", + "clear", + "clear_all", + "close", + "close_fullscreen", + "closed_caption", + "closed_caption_disabled", + "closed_caption_off", + "cloud", + "cloud_circle", + "cloud_done", + "cloud_download", + "cloud_off", + "cloud_queue", + "cloud_sync", + "cloud_upload", + "cloudy_snowing", + "co2", + "co_present", + "code", + "code_off", + "coffee", + "coffee_maker", + "collections", + "collections_bookmark", + "color_lens", + "colorize", + "comment", + "comment_bank", + "comments_disabled", + "commit", + "commute", + "compare", + "compare_arrows", + "compass_calibration", + "compost", + "compress", + "computer", + "confirmation_num", + "confirmation_number", + "connect_without_contact", + "connected_tv", + "connecting_airports", + "construction", + "contact_emergency", + "contact_mail", + "contact_page", + "contact_phone", + "contact_support", + "contactless", + "contacts", + "content_copy", + "content_cut", + "content_paste", + "content_paste_go", + "content_paste_off", + "content_paste_search", + "contrast", + "control_camera", + "control_point", + "control_point_duplicate", + "conveyor_belt", + "cookie", + "copy_all", + "copyright", + "coronavirus", + "corporate_fare", + "cottage", + "countertops", + "create", + "create_new_folder", + "credit_card", + "credit_card_off", + "credit_score", + "crib", + "crisis_alert", + "crop", + "crop_16_9", + "crop_3_2", + "crop_5_4", + "crop_7_5", + "crop_din", + "crop_free", + "crop_landscape", + "crop_original", + "crop_portrait", + "crop_rotate", + "crop_square", + "cruelty_free", + "css", + "currency_bitcoin", + "currency_exchange", + "currency_franc", + "currency_lira", + "currency_pound", + "currency_ruble", + "currency_rupee", + "currency_yen", + "currency_yuan", + "curtains", + "curtains_closed", + "cyclone", + "dangerous", + "dark_mode", + "dashboard", + "dashboard_customize", + "data_array", + "data_exploration", + "data_object", + "data_saver_off", + "data_saver_on", + "data_thresholding", + "data_usage", + "dataset", + "dataset_linked", + "date_range", + "deblur", + "deck", + "dehaze", + "delete", + "delete_forever", + "delete_outline", + "delete_sweep", + "delivery_dining", + "density_large", + "density_medium", + "density_small", + "departure_board", + "description", + "deselect", + "design_services", + "desk", + "desktop_access_disabled", + "desktop_mac", + "desktop_windows", + "details", + "developer_board", + "developer_board_off", + "developer_mode", + "device_hub", + "device_thermostat", + "device_unknown", + "devices", + "devices_fold", + "devices_other", + "dew_point", + "dialer_sip", + "dialpad", + "diamond", + "difference", + "dining", + "dinner_dining", + "directions", + "directions_bike", + "directions_boat", + "directions_boat_filled", + "directions_bus", + "directions_bus_filled", + "directions_car", + "directions_car_filled", + "directions_ferry", + "directions_off", + "directions_railway", + "directions_railway_filled", + "directions_run", + "directions_subway", + "directions_subway_filled", + "directions_train", + "directions_transit", + "directions_transit_filled", + "directions_walk", + "dirty_lens", + "disabled_by_default", + "disabled_visible", + "disc_full", + "discord", + "discount", + "display_settings", + "diversity_1", + "diversity_2", + "diversity_3", + "dnd_forwardslash", + "dns", + "do_disturb", + "do_disturb_alt", + "do_disturb_off", + "do_disturb_on", + "do_not_disturb", + "do_not_disturb_alt", + "do_not_disturb_off", + "do_not_disturb_on", + "do_not_disturb_on_total_silence", + "do_not_step", + "do_not_touch", + "dock", + "document_scanner", + "domain", + "domain_add", + "domain_disabled", + "domain_verification", + "done", + "done_all", + "done_outline", + "donut_large", + "donut_small", + "door_back", + "door_front", + "door_sliding", + "doorbell", + "double_arrow", + "downhill_skiing", + "download", + "download_done", + "download_for_offline", + "downloading", + "drafts", + "drag_handle", + "drag_indicator", + "draw", + "drive_eta", + "drive_file_move", + "drive_file_move_outline", + "drive_file_move_rtl", + "drive_file_rename_outline", + "drive_folder_upload", + "dry", + "dry_cleaning", + "duo", + "dvr", + "dynamic_feed", + "dynamic_form", + "e_mobiledata", + "earbuds", + "earbuds_battery", + "east", + "eco", + "edgesensor_high", + "edgesensor_low", + "edit", + "edit_attributes", + "edit_calendar", + "edit_document", + "edit_location", + "edit_location_alt", + "edit_note", + "edit_notifications", + "edit_off", + "edit_road", + "edit_square", + "egg", + "egg_alt", + "eject", + "elderly", + "elderly_woman", + "electric_bike", + "electric_bolt", + "electric_car", + "electric_meter", + "electric_moped", + "electric_rickshaw", + "electric_scooter", + "electrical_services", + "elevator", + "email", + "emergency", + "emergency_recording", + "emergency_share", + "emoji_emotions", + "emoji_events", + "emoji_flags", + "emoji_food_beverage", + "emoji_nature", + "emoji_objects", + "emoji_people", + "emoji_symbols", + "emoji_transportation", + "energy_savings_leaf", + "engineering", + "enhance_photo_translate", + "enhanced_encryption", + "equalizer", + "error", + "error_outline", + "escalator", + "escalator_warning", + "euro", + "euro_symbol", + "ev_station", + "event", + "event_available", + "event_busy", + "event_note", + "event_repeat", + "event_seat", + "exit_to_app", + "expand", + "expand_circle_down", + "expand_less", + "expand_more", + "explicit", + "explore", + "explore_off", + "exposure", + "exposure_minus_1", + "exposure_minus_2", + "exposure_neg_1", + "exposure_neg_2", + "exposure_plus_1", + "exposure_plus_2", + "exposure_zero", + "extension", + "extension_off", + "face", + "face_2", + "face_3", + "face_4", + "face_5", + "face_6", + "face_retouching_natural", + "face_retouching_off", + "facebook", + "fact_check", + "factory", + "family_restroom", + "fast_forward", + "fast_rewind", + "fastfood", + "favorite", + "favorite_border", + "favorite_outline", + "fax", + "featured_play_list", + "featured_video", + "feed", + "feedback", + "female", + "fence", + "festival", + "fiber_dvr", + "fiber_manual_record", + "fiber_new", + "fiber_pin", + "fiber_smart_record", + "file_copy", + "file_download", + "file_download_done", + "file_download_off", + "file_open", + "file_present", + "file_upload", + "file_upload_off", + "filter", + "filter_1", + "filter_2", + "filter_3", + "filter_4", + "filter_5", + "filter_6", + "filter_7", + "filter_8", + "filter_9", + "filter_9_plus", + "filter_alt", + "filter_alt_off", + "filter_b_and_w", + "filter_center_focus", + "filter_drama", + "filter_frames", + "filter_hdr", + "filter_list", + "filter_list_alt", + "filter_list_off", + "filter_none", + "filter_tilt_shift", + "filter_vintage", + "find_in_page", + "find_replace", + "fingerprint", + "fire_extinguisher", + "fire_hydrant", + "fire_hydrant_alt", + "fire_truck", + "fireplace", + "first_page", + "fit_screen", + "fitbit", + "fitness_center", + "flag", + "flag_circle", + "flaky", + "flare", + "flash_auto", + "flash_off", + "flash_on", + "flashlight_off", + "flashlight_on", + "flatware", + "flight", + "flight_class", + "flight_land", + "flight_takeoff", + "flip", + "flip_camera_android", + "flip_camera_ios", + "flip_to_back", + "flip_to_front", + "flood", + "flourescent", + "flourescent", + "fluorescent", + "flutter_dash", + "fmd_bad", + "fmd_good", + "foggy", + "folder", + "folder_copy", + "folder_delete", + "folder_off", + "folder_open", + "folder_shared", + "folder_special", + "folder_zip", + "follow_the_signs", + "font_download", + "font_download_off", + "food_bank", + "forest", + "fork_left", + "fork_right", + "forklift", + "format_align_center", + "format_align_justify", + "format_align_left", + "format_align_right", + "format_bold", + "format_clear", + "format_color_fill", + "format_color_reset", + "format_color_text", + "format_indent_decrease", + "format_indent_increase", + "format_italic", + "format_line_spacing", + "format_list_bulleted", + "format_list_bulleted_add", + "format_list_numbered", + "format_list_numbered_rtl", + "format_overline", + "format_paint", + "format_quote", + "format_shapes", + "format_size", + "format_strikethrough", + "format_textdirection_l_to_r", + "format_textdirection_r_to_l", + "format_underline", + "format_underlined", + "fort", + "forum", + "forward", + "forward_10", + "forward_30", + "forward_5", + "forward_to_inbox", + "foundation", + "free_breakfast", + "free_cancellation", + "front_hand", + "front_loader", + "fullscreen", + "fullscreen_exit", + "functions", + "g_mobiledata", + "g_translate", + "gamepad", + "games", + "garage", + "gas_meter", + "gavel", + "generating_tokens", + "gesture", + "get_app", + "gif", + "gif_box", + "girl", + "gite", + "goat", + "golf_course", + "gpp_bad", + "gpp_good", + "gpp_maybe", + "gps_fixed", + "gps_not_fixed", + "gps_off", + "grade", + "gradient", + "grading", + "grain", + "graphic_eq", + "grass", + "grid_3x3", + "grid_4x4", + "grid_goldenratio", + "grid_off", + "grid_on", + "grid_view", + "group", + "group_add", + "group_off", + "group_remove", + "group_work", + "groups", + "groups_2", + "groups_3", + "h_mobiledata", + "h_plus_mobiledata", + "hail", + "handshake", + "handyman", + "hardware", + "hd", + "hdr_auto", + "hdr_auto_select", + "hdr_enhanced_select", + "hdr_off", + "hdr_off_select", + "hdr_on", + "hdr_on_select", + "hdr_plus", + "hdr_strong", + "hdr_weak", + "headphones", + "headphones_battery", + "headset", + "headset_mic", + "headset_off", + "healing", + "health_and_safety", + "hearing", + "hearing_disabled", + "heart_broken", + "heat_pump", + "height", + "help", + "help_center", + "help_outline", + "hevc", + "hexagon", + "hide_image", + "hide_source", + "high_quality", + "highlight", + "highlight_alt", + "highlight_off", + "highlight_remove", + "hiking", + "history", + "history_edu", + "history_toggle_off", + "hive", + "hls", + "hls_off", + "holiday_village", + "home", + "home_filled", + "home_max", + "home_mini", + "home_repair_service", + "home_work", + "horizontal_distribute", + "horizontal_rule", + "horizontal_split", + "hot_tub", + "hotel", + "hotel_class", + "hourglass_bottom", + "hourglass_disabled", + "hourglass_empty", + "hourglass_full", + "hourglass_top", + "house", + "house_siding", + "houseboat", + "how_to_reg", + "how_to_vote", + "html", + "http", + "https", + "hub", + "hvac", + "ice_skating", + "icecream", + "image", + "image_aspect_ratio", + "image_not_supported", + "image_search", + "imagesearch_roller", + "import_contacts", + "import_export", + "important_devices", + "inbox", + "incomplete_circle", + "indeterminate_check_box", + "info", + "info_outline", + "input", + "insert_chart", + "insert_chart_outlined", + "insert_comment", + "insert_drive_file", + "insert_emoticon", + "insert_invitation", + "insert_link", + "insert_page_break", + "insert_photo", + "insights", + "install_desktop", + "install_mobile", + "integration_instructions", + "interests", + "interpreter_mode", + "inventory", + "inventory_2", + "invert_colors", + "invert_colors_off", + "invert_colors_on", + "ios_share", + "iron", + "iso", + "javascript", + "join_full", + "join_inner", + "join_left", + "join_right", + "kayaking", + "kebab_dining", + "key", + "key_off", + "keyboard", + "keyboard_alt", + "keyboard_arrow_down", + "keyboard_arrow_left", + "keyboard_arrow_right", + "keyboard_arrow_up", + "keyboard_backspace", + "keyboard_capslock", + "keyboard_command", + "keyboard_command_key", + "keyboard_control", + "keyboard_control_key", + "keyboard_double_arrow_down", + "keyboard_double_arrow_left", + "keyboard_double_arrow_right", + "keyboard_double_arrow_up", + "keyboard_hide", + "keyboard_option", + "keyboard_option_key", + "keyboard_return", + "keyboard_tab", + "keyboard_voice", + "king_bed", + "kitchen", + "kitesurfing", + "label", + "label_important", + "label_important_outline", + "label_off", + "label_outline", + "lan", + "landscape", + "landslide", + "language", + "laptop", + "laptop_chromebook", + "laptop_mac", + "laptop_windows", + "last_page", + "launch", + "layers", + "layers_clear", + "leaderboard", + "leak_add", + "leak_remove", + "leave_bags_at_home", + "legend_toggle", + "lens", + "lens_blur", + "library_add", + "library_add_check", + "library_books", + "library_music", + "light", + "light_mode", + "lightbulb", + "lightbulb_circle", + "lightbulb_outline", + "line_axis", + "line_style", + "line_weight", + "linear_scale", + "link", + "link_off", + "linked_camera", + "liquor", + "list", + "list_alt", + "live_help", + "live_tv", + "living", + "local_activity", + "local_airport", + "local_atm", + "local_attraction", + "local_bar", + "local_cafe", + "local_car_wash", + "local_convenience_store", + "local_dining", + "local_drink", + "local_fire_department", + "local_florist", + "local_gas_station", + "local_grocery_store", + "local_hospital", + "local_hotel", + "local_laundry_service", + "local_library", + "local_mall", + "local_movies", + "local_offer", + "local_parking", + "local_pharmacy", + "local_phone", + "local_pizza", + "local_play", + "local_police", + "local_post_office", + "local_print_shop", + "local_printshop", + "local_restaurant", + "local_see", + "local_shipping", + "local_taxi", + "location_city", + "location_disabled", + "location_history", + "location_off", + "location_on", + "location_pin", + "location_searching", + "lock", + "lock_clock", + "lock_open", + "lock_outline", + "lock_person", + "lock_reset", + "login", + "logo_dev", + "logout", + "looks", + "looks_3", + "looks_4", + "looks_5", + "looks_6", + "looks_one", + "looks_two", + "loop", + "loupe", + "low_priority", + "loyalty", + "lte_mobiledata", + "lte_plus_mobiledata", + "luggage", + "lunch_dining", + "lyrics", + "macro_off", + "mail", + "mail_lock", + "mail_outline", + "male", + "man", + "man_2", + "man_3", + "man_4", + "manage_accounts", + "manage_history", + "manage_search", + "map", + "maps_home_work", + "maps_ugc", + "margin", + "mark_as_unread", + "mark_chat_read", + "mark_chat_unread", + "mark_email_read", + "mark_email_unread", + "mark_unread_chat_alt", + "markunread", + "markunread_mailbox", + "masks", + "maximize", + "media_bluetooth_off", + "media_bluetooth_on", + "mediation", + "medical_information", + "medical_services", + "medication", + "medication_liquid", + "meeting_room", + "memory", + "menu", + "menu_book", + "menu_open", + "merge", + "merge_type", + "message", + "messenger", + "messenger_outline", + "mic", + "mic_external_off", + "mic_external_on", + "mic_none", + "mic_off", + "microwave", + "military_tech", + "minimize", + "minor_crash", + "miscellaneous_services", + "missed_video_call", + "mms", + "mobile_friendly", + "mobile_off", + "mobile_screen_share", + "mobiledata_off", + "mode", + "mode_comment", + "mode_edit", + "mode_edit_outline", + "mode_fan_off", + "mode_night", + "mode_of_travel", + "mode_standby", + "model_training", + "monetization_on", + "money", + "money_off", + "money_off_csred", + "monitor", + "monitor_heart", + "monitor_weight", + "monochrome_photos", + "mood", + "mood_bad", + "moped", + "more", + "more_horiz", + "more_time", + "more_vert", + "mosque", + "motion_photos_auto", + "motion_photos_off", + "motion_photos_on", + "motion_photos_pause", + "motion_photos_paused", + "motorcycle", + "mouse", + "move_down", + "move_to_inbox", + "move_up", + "movie", + "movie_creation", + "movie_edit", + "movie_filter", + "moving", + "mp", + "multiline_chart", + "multiple_stop", + "multitrack_audio", + "museum", + "music_note", + "music_off", + "music_video", + "my_library_add", + "my_library_books", + "my_library_music", + "my_location", + "nat", + "nature", + "nature_people", + "navigate_before", + "navigate_next", + "navigation", + "near_me", + "near_me_disabled", + "nearby_error", + "nearby_off", + "nest_cam_wired_stand", + "network_cell", + "network_check", + "network_locked", + "network_ping", + "network_wifi", + "network_wifi_1_bar", + "network_wifi_2_bar", + "network_wifi_3_bar", + "new_label", + "new_releases", + "newspaper", + "next_plan", + "next_week", + "nfc", + "night_shelter", + "nightlife", + "nightlight", + "nightlight_round", + "nights_stay", + "no_accounts", + "no_adult_content", + "no_backpack", + "no_cell", + "no_crash", + "no_drinks", + "no_encryption", + "no_encryption_gmailerrorred", + "no_flash", + "no_food", + "no_luggage", + "no_meals", + "no_meals_ouline", + "no_meeting_room", + "no_photography", + "no_sim", + "no_stroller", + "no_transfer", + "noise_aware", + "noise_control_off", + "nordic_walking", + "north", + "north_east", + "north_west", + "not_accessible", + "not_interested", + "not_listed_location", + "not_started", + "note", + "note_add", + "note_alt", + "notes", + "notification_add", + "notification_important", + "notifications", + "notifications_active", + "notifications_none", + "notifications_off", + "notifications_on", + "notifications_paused", + "now_wallpaper", + "now_widgets", + "numbers", + "offline_bolt", + "offline_pin", + "offline_share", + "oil_barrel", + "on_device_training", + "ondemand_video", + "online_prediction", + "opacity", + "open_in_browser", + "open_in_full", + "open_in_new", + "open_in_new_off", + "open_with", + "other_houses", + "outbond", + "outbound", + "outbox", + "outdoor_grill", + "outgoing_mail", + "outlet", + "outlined_flag", + "output", + "padding", + "pages", + "pageview", + "paid", + "palette", + "pallet", + "pan_tool", + "pan_tool_alt", + "panorama", + "panorama_fish_eye", + "panorama_fisheye", + "panorama_horizontal", + "panorama_horizontal_select", + "panorama_photosphere", + "panorama_photosphere_select", + "panorama_vertical", + "panorama_vertical_select", + "panorama_wide_angle", + "panorama_wide_angle_select", + "paragliding", + "park", + "party_mode", + "password", + "pattern", + "pause", + "pause_circle", + "pause_circle_filled", + "pause_circle_outline", + "pause_presentation", + "payment", + "payments", + "paypal", + "pedal_bike", + "pending", + "pending_actions", + "pentagon", + "people", + "people_alt", + "people_outline", + "percent", + "perm_camera_mic", + "perm_contact_cal", + "perm_contact_calendar", + "perm_data_setting", + "perm_device_info", + "perm_device_information", + "perm_identity", + "perm_media", + "perm_phone_msg", + "perm_scan_wifi", + "person", + "person_2", + "person_3", + "person_4", + "person_add", + "person_add_alt", + "person_add_alt_1", + "person_add_disabled", + "person_off", + "person_outline", + "person_pin", + "person_pin_circle", + "person_remove", + "person_remove_alt_1", + "person_search", + "personal_injury", + "personal_video", + "pest_control", + "pest_control_rodent", + "pets", + "phishing", + "phone", + "phone_android", + "phone_bluetooth_speaker", + "phone_callback", + "phone_disabled", + "phone_enabled", + "phone_forwarded", + "phone_in_talk", + "phone_iphone", + "phone_locked", + "phone_missed", + "phone_paused", + "phonelink", + "phonelink_erase", + "phonelink_lock", + "phonelink_off", + "phonelink_ring", + "phonelink_setup", + "photo", + "photo_album", + "photo_camera", + "photo_camera_back", + "photo_camera_front", + "photo_filter", + "photo_library", + "photo_size_select_actual", + "photo_size_select_large", + "photo_size_select_small", + "php", + "piano", + "piano_off", + "picture_as_pdf", + "picture_in_picture", + "picture_in_picture_alt", + "pie_chart", + "pie_chart_outline", + "pie_chart_outlined", + "pin", + "pin_drop", + "pin_end", + "pin_invoke", + "pinch", + "pivot_table_chart", + "pix", + "place", + "plagiarism", + "play_arrow", + "play_circle", + "play_circle_fill", + "play_circle_filled", + "play_circle_outline", + "play_disabled", + "play_for_work", + "play_lesson", + "playlist_add", + "playlist_add_check", + "playlist_add_check_circle", + "playlist_add_circle", + "playlist_play", + "playlist_remove", + "plumbing", + "plus_one", + "podcasts", + "point_of_sale", + "policy", + "poll", + "polyline", + "polymer", + "pool", + "portable_wifi_off", + "portrait", + "post_add", + "power", + "power_input", + "power_off", + "power_settings_new", + "precision_manufacturing", + "pregnant_woman", + "present_to_all", + "preview", + "price_change", + "price_check", + "print", + "print_disabled", + "priority_high", + "privacy_tip", + "private_connectivity", + "production_quantity_limits", + "propane", + "propane_tank", + "psychology", + "psychology_alt", + "public", + "public_off", + "publish", + "published_with_changes", + "punch_clock", + "push_pin", + "qr_code", + "qr_code_2", + "qr_code_scanner", + "query_builder", + "query_stats", + "question_answer", + "question_mark", + "queue", + "queue_music", + "queue_play_next", + "quick_contacts_dialer", + "quick_contacts_mail", + "quickreply", + "quiz", + "quora", + "r_mobiledata", + "radar", + "radio", + "radio_button_checked", + "radio_button_off", + "radio_button_on", + "radio_button_unchecked", + "railway_alert", + "ramen_dining", + "ramp_left", + "ramp_right", + "rate_review", + "raw_off", + "raw_on", + "read_more", + "real_estate_agent", + "rebase_edit", + "receipt", + "receipt_long", + "recent_actors", + "recommend", + "record_voice_over", + "rectangle", + "recycling", + "reddit", + "redeem", + "redo", + "reduce_capacity", + "refresh", + "remember_me", + "remove", + "remove_circle", + "remove_circle_outline", + "remove_done", + "remove_from_queue", + "remove_moderator", + "remove_red_eye", + "remove_road", + "remove_shopping_cart", + "reorder", + "repartition", + "repeat", + "repeat_on", + "repeat_one", + "repeat_one_on", + "replay", + "replay_10", + "replay_30", + "replay_5", + "replay_circle_filled", + "reply", + "reply_all", + "report", + "report_gmailerrorred", + "report_off", + "report_problem", + "request_page", + "request_quote", + "reset_tv", + "restart_alt", + "restaurant", + "restaurant_menu", + "restore", + "restore_from_trash", + "restore_page", + "reviews", + "rice_bowl", + "ring_volume", + "rocket", + "rocket_launch", + "roller_shades", + "roller_shades_closed", + "roller_skating", + "roofing", + "room", + "room_preferences", + "room_service", + "rotate_90_degrees_ccw", + "rotate_90_degrees_cw", + "rotate_left", + "rotate_right", + "roundabout_left", + "roundabout_right", + "rounded_corner", + "route", + "router", + "rowing", + "rss_feed", + "rsvp", + "rtt", + "rule", + "rule_folder", + "run_circle", + "running_with_errors", + "rv_hookup", + "safety_check", + "safety_divider", + "sailing", + "sanitizer", + "satellite", + "satellite_alt", + "save", + "save_alt", + "save_as", + "saved_search", + "savings", + "scale", + "scanner", + "scatter_plot", + "schedule", + "schedule_send", + "schema", + "school", + "science", + "score", + "scoreboard", + "screen_lock_landscape", + "screen_lock_portrait", + "screen_lock_rotation", + "screen_rotation", + "screen_rotation_alt", + "screen_search_desktop", + "screen_share", + "screenshot", + "screenshot_monitor", + "scuba_diving", + "sd", + "sd_card", + "sd_card_alert", + "sd_storage", + "search", + "search_off", + "security", + "security_update", + "security_update_good", + "security_update_warning", + "segment", + "select_all", + "self_improvement", + "sell", + "send", + "send_and_archive", + "send_time_extension", + "send_to_mobile", + "sensor_door", + "sensor_occupied", + "sensor_window", + "sensors", + "sensors_off", + "sentiment_dissatisfied", + "sentiment_neutral", + "sentiment_satisfied", + "sentiment_satisfied_alt", + "sentiment_very_dissatisfied", + "sentiment_very_satisfied", + "set_meal", + "settings", + "settings_accessibility", + "settings_applications", + "settings_backup_restore", + "settings_bluetooth", + "settings_brightness", + "settings_cell", + "settings_display", + "settings_ethernet", + "settings_input_antenna", + "settings_input_component", + "settings_input_composite", + "settings_input_hdmi", + "settings_input_svideo", + "settings_overscan", + "settings_phone", + "settings_power", + "settings_remote", + "settings_suggest", + "settings_system_daydream", + "settings_voice", + "severe_cold", + "shape_line", + "share", + "share_arrival_time", + "share_location", + "shelves", + "shield", + "shield_moon", + "shop", + "shop_2", + "shop_two", + "shopify", + "shopping_bag", + "shopping_basket", + "shopping_cart", + "shopping_cart_checkout", + "short_text", + "shortcut", + "show_chart", + "shower", + "shuffle", + "shuffle_on", + "shutter_speed", + "sick", + "sign_language", + "signal_cellular_0_bar", + "signal_cellular_4_bar", + "signal_cellular_alt", + "signal_cellular_alt_1_bar", + "signal_cellular_alt_2_bar", + "signal_cellular_connected_no_internet_0_bar", + "signal_cellular_connected_no_internet_4_bar", + "signal_cellular_no_sim", + "signal_cellular_nodata", + "signal_cellular_null", + "signal_cellular_off", + "signal_wifi_0_bar", + "signal_wifi_4_bar", + "signal_wifi_4_bar_lock", + "signal_wifi_bad", + "signal_wifi_connected_no_internet_4", + "signal_wifi_off", + "signal_wifi_statusbar_4_bar", + "signal_wifi_statusbar_connected_no_internet_4", + "signal_wifi_statusbar_null", + "signpost", + "sim_card", + "sim_card_alert", + "sim_card_download", + "single_bed", + "sip", + "skateboarding", + "skip_next", + "skip_previous", + "sledding", + "slideshow", + "slow_motion_video", + "smart_button", + "smart_display", + "smart_screen", + "smart_toy", + "smartphone", + "smoke_free", + "smoking_rooms", + "sms", + "sms_failed", + "snapchat", + "snippet_folder", + "snooze", + "snowboarding", + "snowing", + "snowmobile", + "snowshoeing", + "soap", + "social_distance", + "solar_power", + "sort", + "sort_by_alpha", + "sos", + "soup_kitchen", + "source", + "south", + "south_america", + "south_east", + "south_west", + "spa", + "space_bar", + "space_dashboard", + "spatial_audio", + "spatial_audio_off", + "spatial_tracking", + "speaker", + "speaker_group", + "speaker_notes", + "speaker_notes_off", + "speaker_phone", + "speed", + "spellcheck", + "splitscreen", + "spoke", + "sports", + "sports_bar", + "sports_baseball", + "sports_basketball", + "sports_cricket", + "sports_esports", + "sports_football", + "sports_golf", + "sports_gymnastics", + "sports_handball", + "sports_hockey", + "sports_kabaddi", + "sports_martial_arts", + "sports_mma", + "sports_motorsports", + "sports_rugby", + "sports_score", + "sports_soccer", + "sports_tennis", + "sports_volleyball", + "square", + "square_foot", + "ssid_chart", + "stacked_bar_chart", + "stacked_line_chart", + "stadium", + "stairs", + "star", + "star_border", + "star_border_purple500", + "star_half", + "star_outline", + "star_purple500", + "star_rate", + "stars", + "start", + "stay_current_landscape", + "stay_current_portrait", + "stay_primary_landscape", + "stay_primary_portrait", + "sticky_note_2", + "stop", + "stop_circle", + "stop_screen_share", + "storage", + "store", + "store_mall_directory", + "storefront", + "storm", + "straight", + "straighten", + "stream", + "streetview", + "strikethrough_s", + "stroller", + "style", + "subdirectory_arrow_left", + "subdirectory_arrow_right", + "subject", + "subscript", + "subscriptions", + "subtitles", + "subtitles_off", + "subway", + "summarize", + "sunny", + "sunny_snowing", + "superscript", + "supervised_user_circle", + "supervisor_account", + "support", + "support_agent", + "surfing", + "surround_sound", + "swap_calls", + "swap_horiz", + "swap_horizontal_circle", + "swap_vert", + "swap_vert_circle", + "swap_vertical_circle", + "swipe", + "swipe_down", + "swipe_down_alt", + "swipe_left", + "swipe_left_alt", + "swipe_right", + "swipe_right_alt", + "swipe_up", + "swipe_up_alt", + "swipe_vertical", + "switch_access_shortcut", + "switch_access_shortcut_add", + "switch_account", + "switch_camera", + "switch_left", + "switch_right", + "switch_video", + "synagogue", + "sync", + "sync_alt", + "sync_disabled", + "sync_lock", + "sync_problem", + "system_security_update", + "system_security_update_good", + "system_security_update_warning", + "system_update", + "system_update_alt", + "system_update_tv", + "tab", + "tab_unselected", + "table_bar", + "table_chart", + "table_restaurant", + "table_rows", + "table_view", + "tablet", + "tablet_android", + "tablet_mac", + "tag", + "tag_faces", + "takeout_dining", + "tap_and_play", + "tapas", + "task", + "task_alt", + "taxi_alert", + "telegram", + "temple_buddhist", + "temple_hindu", + "terminal", + "terrain", + "text_decrease", + "text_fields", + "text_format", + "text_increase", + "text_rotate_up", + "text_rotate_vertical", + "text_rotation_angledown", + "text_rotation_angleup", + "text_rotation_down", + "text_rotation_none", + "text_snippet", + "textsms", + "texture", + "theater_comedy", + "theaters", + "thermostat", + "thermostat_auto", + "thumb_down", + "thumb_down_alt", + "thumb_down_off_alt", + "thumb_up", + "thumb_up_alt", + "thumb_up_off_alt", + "thumbs_up_down", + "thunderstorm", + "tiktok", + "time_to_leave", + "timelapse", + "timeline", + "timer", + "timer_10", + "timer_10_select", + "timer_3", + "timer_3_select", + "timer_off", + "tips_and_updates", + "tire_repair", + "title", + "toc", + "today", + "toggle_off", + "toggle_on", + "token", + "toll", + "tonality", + "topic", + "tornado", + "touch_app", + "tour", + "toys", + "track_changes", + "traffic", + "train", + "tram", + "transcribe", + "transfer_within_a_station", + "transform", + "transgender", + "transit_enterexit", + "translate", + "travel_explore", + "trending_down", + "trending_flat", + "trending_neutral", + "trending_up", + "trip_origin", + "trolley", + "troubleshoot", + "try", + "tsunami", + "tty", + "tune", + "tungsten", + "turn_left", + "turn_right", + "turn_sharp_left", + "turn_sharp_right", + "turn_slight_left", + "turn_slight_right", + "turned_in", + "turned_in_not", + "tv", + "tv_off", + "two_wheeler", + "type_specimen", + "u_turn_left", + "u_turn_right", + "umbrella", + "unarchive", + "undo", + "unfold_less", + "unfold_less_double", + "unfold_more", + "unfold_more_double", + "unpublished", + "unsubscribe", + "upcoming", + "update", + "update_disabled", + "upgrade", + "upload", + "upload_file", + "usb", + "usb_off", + "vaccines", + "vape_free", + "vaping_rooms", + "verified", + "verified_user", + "vertical_align_bottom", + "vertical_align_center", + "vertical_align_top", + "vertical_distribute", + "vertical_shades", + "vertical_shades_closed", + "vertical_split", + "vibration", + "video_call", + "video_camera_back", + "video_camera_front", + "video_chat", + "video_collection", + "video_file", + "video_label", + "video_library", + "video_settings", + "video_stable", + "videocam", + "videocam_off", + "videogame_asset", + "videogame_asset_off", + "view_agenda", + "view_array", + "view_carousel", + "view_column", + "view_comfortable", + "view_comfy", + "view_comfy_alt", + "view_compact", + "view_compact_alt", + "view_cozy", + "view_day", + "view_headline", + "view_in_ar", + "view_kanban", + "view_list", + "view_module", + "view_quilt", + "view_sidebar", + "view_stream", + "view_timeline", + "view_week", + "vignette", + "villa", + "visibility", + "visibility_off", + "voice_chat", + "voice_over_off", + "voicemail", + "volcano", + "volume_down", + "volume_down_alt", + "volume_mute", + "volume_off", + "volume_up", + "volunteer_activism", + "vpn_key", + "vpn_key_off", + "vpn_lock", + "vrpano", + "wallet", + "wallet_giftcard", + "wallet_membership", + "wallet_travel", + "wallpaper", + "warehouse", + "warning", + "warning_amber", + "wash", + "watch", + "watch_later", + "watch_off", + "water", + "water_damage", + "water_drop", + "waterfall_chart", + "waves", + "waving_hand", + "wb_auto", + "wb_cloudy", + "wb_incandescent", + "wb_iridescent", + "wb_shade", + "wb_sunny", + "wb_twighlight", + "wb_twilight", + "wc", + "web", + "web_asset", + "web_asset_off", + "web_stories", + "webhook", + "wechat", + "weekend", + "west", + "whatshot", + "wheelchair_pickup", + "where_to_vote", + "widgets", + "width_full", + "width_normal", + "width_wide", + "wifi", + "wifi_1_bar", + "wifi_2_bar", + "wifi_calling", + "wifi_calling_3", + "wifi_channel", + "wifi_find", + "wifi_lock", + "wifi_off", + "wifi_password", + "wifi_protected_setup", + "wifi_tethering", + "wifi_tethering_error", + "wifi_tethering_error_rounded", + "wifi_tethering_off", + "wind_power", + "window", + "wine_bar", + "woman", + "woman_2", + "woo_commerce", + "wordpress", + "work", + "work_history", + "work_off", + "work_outline", + "workspace_premium", + "workspaces", + "workspaces_filled", + "workspaces_outline", + "wrap_text", + "wrong_location", + "wysiwyg", + "yard", + "youtube_searched_for", + "zoom_in", + "zoom_in_map", + "zoom_out", + "zoom_out_map", + ]; +} \ No newline at end of file diff --git a/ng-diogenes/src/styles/icons.scss b/ng-diogenes/src/styles/icons.scss index 0221eea..2400e4a 100644 --- a/ng-diogenes/src/styles/icons.scss +++ b/ng-diogenes/src/styles/icons.scss @@ -1,7 +1,17 @@ -.small-screen-icon{ +.small-screen-icon { display: inline-block !important; } +// Center within a mat card avatar, e.g. for locations +.icon-in-avatar { + padding-left: 0.5rem; + padding-top: 0.5rem; +} + +.icon-in-avatar-container { + background-color: #000; +} + // Full screen @media screen and (min-width: 768px) {