diff --git a/web/public/images/warehouse-pin.svg b/web/public/images/warehouse-pin.svg new file mode 100644 index 00000000..773745b2 --- /dev/null +++ b/web/public/images/warehouse-pin.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/web/src/App.svelte b/web/src/App.svelte index 0e9f85cf..0b403591 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -5,7 +5,6 @@ import Dashboard from "./routes/backOffice/dashboard/Dashboard.svelte"; import Map from "./routes/backOffice/map/Map.svelte"; import Routes from "./routes/backOffice/routes/Routes.svelte"; - import Warehouses from "./routes/backOffice/warehouses/Warehouses.svelte"; import Trucks from "./routes/backOffice/trucks/Trucks.svelte"; import Reports from "./routes/backOffice/reports/Reports.svelte"; import Employees from "./routes/backOffice/employees/Employees.svelte"; @@ -20,6 +19,7 @@ import url from "./lib/stores/url"; import ContainersRouter from "./routes/backOffice/containers/ContainersRouter.svelte"; import Toast from "./lib/components/Toast.svelte"; + import WarehousesRouter from "./routes/backOffice/warehouses/WarehousesRouter.svelte"; onMount(() => { if ($url.pathname === "/") { @@ -36,10 +36,9 @@ - + + + diff --git a/web/src/domain/warehouse.ts b/web/src/domain/warehouse.ts new file mode 100644 index 00000000..9e2f14a1 --- /dev/null +++ b/web/src/domain/warehouse.ts @@ -0,0 +1,19 @@ +import type { components } from "../../api/ecomap/http"; + +/** + * Warehouse. + */ +export type Warehouse = components["schemas"]["Warehouse"]; + +/** + * Paginated warehouses. + */ +export type PaginatedWarehouses = components["schemas"]["WarehousesPaginated"]; + +/** + * Filters of warehouses. + */ +export interface WarehousesFilters { + pageIndex: number; + location: string; +} diff --git a/web/src/lib/components/Field.svelte b/web/src/lib/components/Field.svelte index 63a7b60b..3488ce31 100644 --- a/web/src/lib/components/Field.svelte +++ b/web/src/lib/components/Field.svelte @@ -10,7 +10,7 @@ * The value of the field. * @default null */ - export let value: string | null = null; + export let value: number | string | null = null;
diff --git a/web/src/lib/components/Input.svelte b/web/src/lib/components/Input.svelte index 6c4efa7c..fb3c28d2 100644 --- a/web/src/lib/components/Input.svelte +++ b/web/src/lib/components/Input.svelte @@ -2,6 +2,7 @@ import type { ChangeEventHandler, HTMLInputAttributes, + KeyboardEventHandler, MouseEventHandler, } from "svelte/elements"; import Icon from "./Icon.svelte"; @@ -30,6 +31,18 @@ */ export let id: HTMLInputAttributes["id"] = null; + /** + * Defines the maximum value that is acceptable and valid for the input. + * @default null + */ + export let max: HTMLInputAttributes["max"] = null; + + /** + * Defines the minimum value that is acceptable and valid for the input. + * @default null + */ + export let min: HTMLInputAttributes["min"] = null; + /** * The name of the form control. Submitted with the form as part of a name/value pair. * @default null @@ -48,6 +61,12 @@ */ export let onInput: ChangeEventHandler | null = null; + /** + * Callback fired when a key is pressed. + * @default null + */ + export let onKeyDown: KeyboardEventHandler | null = null; + /** * The text that appears in the form control when it has no value set. * @default null @@ -60,6 +79,18 @@ */ export let readonly: boolean = false; + /** + * Indicates that the user must specify a value for the input before the owning form can be submitted. + * @default false + */ + export let required: boolean = false; + + /** + * CSS styling declarations to be applied to the element. + * @default null + */ + export let style: string | null = null; + /** * The type of control to render. * @default null @@ -70,7 +101,7 @@ * The value of the input. * @default null */ - export let value: string | null = null; + export let value: number | string | null = null;
@@ -83,6 +114,11 @@ {type} {value} {readonly} + {required} + {max} + {min} + {style} + on:keydown={onKeyDown} on:input={onInput} on:click={onClick} data-endIcon={endIcon} diff --git a/web/src/lib/components/LocationInput.svelte b/web/src/lib/components/LocationInput.svelte new file mode 100644 index 00000000..467c16d0 --- /dev/null +++ b/web/src/lib/components/LocationInput.svelte @@ -0,0 +1,72 @@ + + + diff --git a/web/src/lib/components/Select.svelte b/web/src/lib/components/Select.svelte index b34bb8a2..56f2a223 100644 --- a/web/src/lib/components/Select.svelte +++ b/web/src/lib/components/Select.svelte @@ -38,6 +38,12 @@ */ export let placeholder: string | null = null; + /** + * Indicates that the user must specify a value for the input before the owning form can be submitted. + * @default false + */ + export let required: boolean = false; + /** * The value of the select. * @default "" @@ -50,6 +56,7 @@ {name} {value} {placeholder} + {required} on:change={onChange} class={`${className} ${error ? "error" : ""}`} > diff --git a/web/src/lib/components/SelectLocation.svelte b/web/src/lib/components/SelectLocation.svelte index cb3b79d2..39ae1d4b 100644 --- a/web/src/lib/components/SelectLocation.svelte +++ b/web/src/lib/components/SelectLocation.svelte @@ -19,6 +19,7 @@ import { DEFAULT_ANIMATION_DURATION, DEFAULT_MAX_ZOOM, + DEFAULT_PIN_ICON_SRC, } from "../constants/map"; /** @@ -51,6 +52,12 @@ */ export let onCancel: (() => void) | null = null; + /** + * Map layer style. + * @default "/images/pin.svg" + */ + export let iconSrc: string = DEFAULT_PIN_ICON_SRC; + /** * Open Layers map. */ @@ -67,7 +74,7 @@ const layer = new VectorLayer({ source: new VectorSource>({ features: [] }), style: { - "icon-src": "/images/pin.svg", + "icon-src": iconSrc, }, }); diff --git a/web/src/lib/components/map/mapUtils.ts b/web/src/lib/components/map/mapUtils.ts index 324c79c5..d06e4e5d 100644 --- a/web/src/lib/components/map/mapUtils.ts +++ b/web/src/lib/components/map/mapUtils.ts @@ -24,6 +24,7 @@ import { colorLayerKey, nameLayerKey, DEFAULT_MAX_ZOOM, + CONTAINER_ICON_SRC, } from "../../constants/map"; import type { Geometry } from "ol/geom"; import type Feature from "ol/Feature"; @@ -53,7 +54,7 @@ const defaultVectorStyle: VectorStyle = { * Default style for WebGl point layer. */ const defaultIconStyle: WebGLStyle = { - "icon-src": "/images/logo.svg", + "icon-src": CONTAINER_ICON_SRC, }; /** @@ -61,7 +62,7 @@ const defaultIconStyle: WebGLStyle = { */ const defaultClusterIcon = new Style({ image: new Icon({ - src: "/images/logo.svg", + src: CONTAINER_ICON_SRC, }), }); diff --git a/web/src/lib/constants/map.ts b/web/src/lib/constants/map.ts index 4652b21d..a8752a84 100644 --- a/web/src/lib/constants/map.ts +++ b/web/src/lib/constants/map.ts @@ -32,3 +32,18 @@ export const OL_PROJECTION = "EPSG:3857"; * Resource geometry projection. */ export const RESOURCE_PROJECTION = "EPSG:4326"; + +/** + * Source location of the default pin icon. + */ +export const DEFAULT_PIN_ICON_SRC = "/images/pin.svg"; + +/** + * Source location of the container pin icon. + */ +export const CONTAINER_ICON_SRC = "/images/logo.svg"; + +/** + * Source location of the warehouse pin icon. + */ +export const WAREHOUSE_ICON_SRC = "/images/warehouse-pin.svg"; diff --git a/web/src/lib/utils/i8n.ts b/web/src/lib/utils/i8n.ts index 6ae9b0ee..5013dc16 100644 --- a/web/src/lib/utils/i8n.ts +++ b/web/src/lib/utils/i8n.ts @@ -73,6 +73,9 @@ function _locale(): Writable { localStorage.setItem("locale", supportedLocale); + // Set the supported locale in the lang attribute of the HTML element. + document.documentElement.setAttribute("lang", supportedLocale); + const store = writable(supportedLocale); const { subscribe, set } = store; @@ -82,6 +85,9 @@ function _locale(): Writable { const supportedLocale = getSupportedLocale(locale); localStorage.setItem("locale", supportedLocale); + // Set the supported locale in the lang attribute of the HTML element. + document.documentElement.setAttribute("lang", supportedLocale); + // Refresh current page when switching locales. location.reload(); @@ -93,6 +99,9 @@ function _locale(): Writable { const supportedLocale = getSupportedLocale(updatedLocale); localStorage.setItem("locale", supportedLocale); + // Set the supported locale in the lang attribute of the HTML element. + document.documentElement.setAttribute("lang", supportedLocale); + // Refresh current page when switching locales. location.reload(); diff --git a/web/src/locales/en.json b/web/src/locales/en.json index c950bc8e..02e54a85 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -11,16 +11,17 @@ "save": "Save", "preview": "Preview", "selectLocation": "Select location", + "location": "Location", + "location.placeholder": "Select a location", + "location.search": "Search by location", "location.unknownWay": "Unknown way", "containers.title": "Containers", - "containers.search": "Search by location", "containers.notFound.title": "Container not found", "containers.notFound.description": "The container you are looking for does not exist.", "containers.create.title": "Add container", "containers.create.success": "Container added successfully", "containers.category": "Category", - "containers.location": "Location", - "containers.location.placeholder": "Select a location", + "truckCapacity.placeholder": "Enter the maximum truck capacity", "containers.category.general": "General", "containers.category.glass": "Glass", "containers.category.hazardous": "Hazardous", @@ -33,7 +34,26 @@ "containers.delete.success": "Container deleted successfully", "containers.delete.conflict.title": "Conflict", "containers.delete.conflict.description": "This container cannot be deleted because it's associated with a report, bookmark, or route.", - "error.requiredField": "Required field", + "warehouses": "Warehouses", + "truckCapacity": "Maximum truck capacity", + "warehouses.create.title": "Add warehouse", + "warehouses.create.success": "Warehouse added successfully", + "warehouses.update.success": "Warehouse updated successfully", + "warehouses.update.conflict.title": "Conflict", + "warehouses.update.conflict.description": "The warehouse already has more trucks than the new capacity.", + "warehouses.notFound.title": "Warehouse not found", + "warehouses.notFound.description": "The warehouse you are looking for does not exist.", + "warehouses.delete.success": "Warehouse deleted successfully", + "warehouses.delete.conflict.title": "Conflict", + "warehouses.delete.conflict.description": "This warehouse cannot be deleted because it's associated with a truck or a route.", + "error.valueMissing": "Required field", + "error.typeMismatch.number": "Invalid number", + "error.patternMismatch": "Invalid format", + "error.tooLong": "Value must be {{maxLength}} characters or less", + "error.tooShort": "Value must be {{minLength}} characters or more", + "error.rangeUnderflow": "Value must not be less than {{min}}", + "error.rangeOverflow": "Value must not be greater than {{max}}", + "error.stepMismatch": "Invalid value", "error.unexpected": "An unexpected error occurred. Please try again later or contact the support team.", "error.unexpected.title": "Oops! An unexpected error occurred", "error.unexpected.description": "Please try again later or contact the support team.", diff --git a/web/src/locales/pt.json b/web/src/locales/pt.json index 0a652407..2f32302a 100644 --- a/web/src/locales/pt.json +++ b/web/src/locales/pt.json @@ -11,16 +11,17 @@ "save": "Guardar", "preview": "Pré-visualização", "selectLocation": "Selecionar localização", + "location": "Localização", + "location.placeholder": "Selecione uma localização", + "location.search": "Pesquise por localização", "location.unknownWay": "Rua desconhecida", "containers.title": "Contentores", "containers.notFound.title": "Contentor não encontrado", "containers.notFound.description": "O contentor que procura não existe.", "containers.create.title": "Adicionar contentor", "containers.create.success": "Contentor adicionado com sucesso", - "containers.search": "Pesquise por localização", "containers.category": "Categoria", - "containers.location": "Localização", - "containers.location.placeholder": "Selecione uma localização", + "truckCapacity.placeholder": "Introduza a capacidade máxima de camiões", "containers.category.general": "Geral", "containers.category.glass": "Vidro", "containers.category.hazardous": "Perigoso", @@ -33,7 +34,26 @@ "containers.delete.success": "Contentor eliminado com sucesso", "containers.delete.conflict.title": "Conflito", "containers.delete.conflict.description": "Este contentor não pode ser eliminado porque está associado a um reporte, marcador ou rota.", - "error.requiredField": "Campo obrigatório", + "warehouses": "Armazéns", + "truckCapacity": "Capacidade máxima de camiões", + "warehouses.create.title": "Adicionar armazém", + "warehouses.create.success": "Armazém adicionado com sucesso", + "warehouses.update.success": "Armazém atualizado com sucesso", + "warehouses.update.conflict.title": "Conflicto", + "warehouses.update.conflict.description": "O armazém já tem mais camiões do que a nova capacidade.", + "warehouses.notFound.title": "Armazém não encontrado", + "warehouses.notFound.description": "O armazém que procura não existe.", + "warehouses.delete.success": "Armazém eliminado com sucesso", + "warehouses.delete.conflict.title": "Conflito", + "warehouses.delete.conflict.description": "Este armazém não pode ser eliminado porque está associado a um camião ou a uma rota.", + "error.valueMissing": "Campo obrigatório", + "error.typeMismatch.number": "Número inválido", + "error.patternMismatch": "Formato inválido", + "error.tooLong": "Valor tem de ter {{maxLength}} caracteres ou menos", + "error.tooShort": "Valor tem de ter {{minLength}} caracteres ou mais", + "error.rangeUnderflow": "Valor não deve ser inferior a {{min}}", + "error.rangeOverflow": "Valor não deve ser superior a {{max}}", + "error.stepMismatch": "Valor inválido", "error.unexpected": "Ocorreu um erro inesperado. Tente novamente mais tarde ou contacte a equipa de apoio.", "error.unexpected.title": "Oops! Ocorreu um erro inesperado", "error.unexpected.description": "Tente novamente mais tarde ou contacte a equipa de apoio.", diff --git a/web/src/locales/schema.json b/web/src/locales/schema.json index 4b2e75b1..c007194f 100644 --- a/web/src/locales/schema.json +++ b/web/src/locales/schema.json @@ -34,6 +34,15 @@ "selectLocation": { "type": "string" }, + "location": { + "type": "string" + }, + "location.placeholder": { + "type": "string" + }, + "location.search": { + "type": "string" + }, "location.unknownWay": { "type": "string" }, @@ -52,16 +61,10 @@ "containers.create.success": { "type": "string" }, - "containers.search": { - "type": "string" - }, "containers.category": { "type": "string" }, - "containers.location": { - "type": "string" - }, - "containers.location.placeholder": { + "truckCapacity.placeholder": { "type": "string" }, "containers.category.general": { @@ -100,7 +103,64 @@ "containers.delete.conflict.description": { "type": "string" }, - "error.requiredField": { + "warehouses": { + "type": "string" + }, + "truckCapacity": { + "type": "string" + }, + "warehouses.create.title": { + "type": "string" + }, + "warehouses.create.success": { + "type": "string" + }, + "warehouses.update.success": { + "type": "string" + }, + "warehouses.update.conflict.title": { + "type": "string" + }, + "warehouses.update.conflict.description": { + "type": "string" + }, + "warehouses.notFound.title": { + "type": "string" + }, + "warehouses.notFound.description": { + "type": "string" + }, + "warehouses.delete.success": { + "type": "string" + }, + "warehouses.delete.conflict.title": { + "type": "string" + }, + "warehouses.delete.conflict.description": { + "type": "string" + }, + "error.valueMissing": { + "type": "string" + }, + "error.typeMismatch.number": { + "type": "string" + }, + "error.patternMismatch": { + "type": "string" + }, + "error.tooLong": { + "type": "string" + }, + "error.tooShort": { + "type": "string" + }, + "error.rangeUnderflow": { + "type": "string" + }, + "error.rangeOverflow": { + "type": "string" + }, + "error.stepMismatch": { "type": "string" }, "error.unexpected": { diff --git a/web/src/routes/backOffice/containers/components/ContainerForm.svelte b/web/src/routes/backOffice/containers/components/ContainerForm.svelte index dd43b15c..a938cc5c 100644 --- a/web/src/routes/backOffice/containers/components/ContainerForm.svelte +++ b/web/src/routes/backOffice/containers/components/ContainerForm.svelte @@ -8,7 +8,6 @@ import DetailsFields from "../../../../lib/components/details/DetailsFields.svelte"; import DetailsSection from "../../../../lib/components/details/DetailsSection.svelte"; import DetailsContent from "../../../../lib/components/details/DetailsContent.svelte"; - import Input from "../../../../lib/components/Input.svelte"; import Map from "../../../../lib/components/map/Map.svelte"; import OlMap from "ol/Map"; import Select from "../../../../lib/components/Select.svelte"; @@ -30,6 +29,8 @@ } from "../../../../lib/utils/map"; import { isValidContainerCategory } from "../utils/category"; import { getLocationName } from "../../../../lib/utils/location"; + import { CONTAINER_ICON_SRC } from "../../../../lib/constants/map"; + import LocationInput from "../../../../lib/components/LocationInput.svelte"; /** * The back route. @@ -66,7 +67,7 @@ const layer = new VectorLayer({ source: new VectorSource>({ features: [] }), style: { - "icon-src": "/images/logo.svg", + "icon-src": CONTAINER_ICON_SRC, }, }); @@ -130,13 +131,13 @@ */ function validateForm(category: string, location: string) { if (!category) { - formErrorMessages.category = $t("error.requiredField"); + formErrorMessages.category = $t("error.valueMissing"); } else { formErrorMessages.category = ""; } if (!location) { - formErrorMessages.location = $t("error.requiredField"); + formErrorMessages.location = $t("error.valueMissing"); } else { formErrorMessages.location = ""; } @@ -178,7 +179,7 @@ } -
+ @@ -194,6 +195,7 @@ helperText={formErrorMessages.category} > (openSelectLocation = true)} /> @@ -240,6 +241,7 @@ (openSelectLocation = open)} onSave={(coordinate, name) => { addContainerToMap(coordinate); diff --git a/web/src/routes/backOffice/containers/details/Container.svelte b/web/src/routes/backOffice/containers/details/Container.svelte index f331bd75..d6f1e723 100644 --- a/web/src/routes/backOffice/containers/details/Container.svelte +++ b/web/src/routes/backOffice/containers/details/Container.svelte @@ -117,7 +117,7 @@ label={$t("containers.category")} value={$t(`containers.category.${container.category}`)} /> - + diff --git a/web/src/routes/backOffice/containers/details/ContainerMap.svelte b/web/src/routes/backOffice/containers/details/ContainerMap.svelte index 83b5d255..20b1a053 100644 --- a/web/src/routes/backOffice/containers/details/ContainerMap.svelte +++ b/web/src/routes/backOffice/containers/details/ContainerMap.svelte @@ -16,6 +16,7 @@ import { formatDate } from "../../../../lib/utils/date"; import { DateFormats } from "../../../../lib/constants/date"; import { getLocationName } from "../../../../lib/utils/location"; + import { CONTAINER_ICON_SRC } from "../../../../lib/constants/map"; /** * Container ID. @@ -42,7 +43,9 @@ const feature = new Feature(point); const mapHelper = new MapHelper(map); - mapHelper.addPointLayer({ features: [feature] }, "container"); + mapHelper.addPointLayer({ features: [feature] }, "container", "#fff", { + "icon-src": CONTAINER_ICON_SRC, + }); const view = map.getView(); view.fit(point); diff --git a/web/src/routes/backOffice/containers/list/ContainersTable.svelte b/web/src/routes/backOffice/containers/list/ContainersTable.svelte index 7774452e..6e594ff2 100644 --- a/web/src/routes/backOffice/containers/list/ContainersTable.svelte +++ b/web/src/routes/backOffice/containers/list/ContainersTable.svelte @@ -42,7 +42,7 @@ { type: "accessor", field: "geoJson", - header: $t("containers.location"), + header: $t("location"), enableSorting: false, enableFiltering: false, cell(geoJson) { diff --git a/web/src/routes/backOffice/containers/list/SearchContainers.svelte b/web/src/routes/backOffice/containers/list/SearchContainers.svelte index 57fd0cff..3d335293 100644 --- a/web/src/routes/backOffice/containers/list/SearchContainers.svelte +++ b/web/src/routes/backOffice/containers/list/SearchContainers.svelte @@ -27,7 +27,7 @@ diff --git a/web/src/routes/backOffice/warehouses/Warehouses.svelte b/web/src/routes/backOffice/warehouses/Warehouses.svelte deleted file mode 100644 index 5df9ce5e..00000000 --- a/web/src/routes/backOffice/warehouses/Warehouses.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - -
{$t("sidebar.warehouses")}
diff --git a/web/src/routes/backOffice/warehouses/WarehousesRouter.svelte b/web/src/routes/backOffice/warehouses/WarehousesRouter.svelte new file mode 100644 index 00000000..576abfa4 --- /dev/null +++ b/web/src/routes/backOffice/warehouses/WarehousesRouter.svelte @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/web/src/routes/backOffice/warehouses/components/WarehouseForm.svelte b/web/src/routes/backOffice/warehouses/components/WarehouseForm.svelte new file mode 100644 index 00000000..cae696a1 --- /dev/null +++ b/web/src/routes/backOffice/warehouses/components/WarehouseForm.svelte @@ -0,0 +1,297 @@ + + + + + + + + + + + + + + (openSelectLocation = true)} + /> + + + + + + + + { + const warehouseCoordinate = warehouse?.geoJson.geometry.coordinates; + if (!warehouseCoordinate) { + return; + } + + const mapCoordinate = convertToMapProjection(warehouseCoordinate); + addWarehouseToMap(mapCoordinate); + }} + /> + + (openSelectLocation = open)} + onSave={(coordinate, name) => { + addWarehouseToMap(coordinate); + selectedCoordinate = convertToResourceProjection(coordinate); + locationName = name; + }} + /> + + + + diff --git a/web/src/routes/backOffice/warehouses/create/CreateWarehouse.svelte b/web/src/routes/backOffice/warehouses/create/CreateWarehouse.svelte new file mode 100644 index 00000000..858f81c9 --- /dev/null +++ b/web/src/routes/backOffice/warehouses/create/CreateWarehouse.svelte @@ -0,0 +1,57 @@ + + + + + diff --git a/web/src/routes/backOffice/warehouses/details/Warehouse.svelte b/web/src/routes/backOffice/warehouses/details/Warehouse.svelte new file mode 100644 index 00000000..3366a89f --- /dev/null +++ b/web/src/routes/backOffice/warehouses/details/Warehouse.svelte @@ -0,0 +1,160 @@ + + + + {#await warehousePromise} +
+ +
+ {:then warehouse} + {@const locationName = getLocationName( + warehouse.geoJson.properties.wayName, + warehouse.geoJson.properties.municipalityName, + )} + + + + + + + + + + + + + + + + + + + + + + {:catch} +
+

{$t("warehouses.notFound.title")}

+

{$t("warehouses.notFound.description")}

+
+ {/await} +
+ + diff --git a/web/src/routes/backOffice/warehouses/details/WarehouseMap.svelte b/web/src/routes/backOffice/warehouses/details/WarehouseMap.svelte new file mode 100644 index 00000000..16b3e0a4 --- /dev/null +++ b/web/src/routes/backOffice/warehouses/details/WarehouseMap.svelte @@ -0,0 +1,159 @@ + + +
+ + + {#await warehousePromise} +
+ +
+ {:then warehouse} + {@const { wayName, municipalityName } = warehouse.geoJson.properties} + +
+
+ + + + + + + {:catch} +
+

{$t("warehouses.notFound.title")}

+

{$t("warehouses.notFound.description")}

+
+ {/await} +
+ + diff --git a/web/src/routes/backOffice/warehouses/edit/EditWarehouse.svelte b/web/src/routes/backOffice/warehouses/edit/EditWarehouse.svelte new file mode 100644 index 00000000..102cb13f --- /dev/null +++ b/web/src/routes/backOffice/warehouses/edit/EditWarehouse.svelte @@ -0,0 +1,133 @@ + + + + {#await warehousePromise} +
+ +
+ {:then warehouse} + {@const locationName = getLocationName( + warehouse.geoJson.properties.wayName, + warehouse.geoJson.properties.municipalityName, + )} + + {:catch} +
+

{$t("warehouses.notFound.title")}

+

{$t("warehouses.notFound.description")}

+
+ {/await} +
+ + diff --git a/web/src/routes/backOffice/warehouses/list/SearchWarehouses.svelte b/web/src/routes/backOffice/warehouses/list/SearchWarehouses.svelte new file mode 100644 index 00000000..f5ac0786 --- /dev/null +++ b/web/src/routes/backOffice/warehouses/list/SearchWarehouses.svelte @@ -0,0 +1,39 @@ + + + + + diff --git a/web/src/routes/backOffice/warehouses/list/Warehouses.svelte b/web/src/routes/backOffice/warehouses/list/Warehouses.svelte new file mode 100644 index 00000000..77a74347 --- /dev/null +++ b/web/src/routes/backOffice/warehouses/list/Warehouses.svelte @@ -0,0 +1,31 @@ + + + +

{$t("warehouses")}

+
+ + + + +
+ +
+ + diff --git a/web/src/routes/backOffice/warehouses/list/WarehousesTable.svelte b/web/src/routes/backOffice/warehouses/list/WarehousesTable.svelte new file mode 100644 index 00000000..2d9d367a --- /dev/null +++ b/web/src/routes/backOffice/warehouses/list/WarehousesTable.svelte @@ -0,0 +1,80 @@ + + + diff --git a/web/src/routes/backOffice/warehouses/list/warehousesStore.ts b/web/src/routes/backOffice/warehouses/list/warehousesStore.ts new file mode 100644 index 00000000..7d84cdbe --- /dev/null +++ b/web/src/routes/backOffice/warehouses/list/warehousesStore.ts @@ -0,0 +1,123 @@ +import type { + PaginatedWarehouses, + WarehousesFilters, +} from "../../../../domain/warehouse"; +import ecomapHttpClient from "../../../../lib/clients/ecomap/http"; +import { DEFAULT_PAGE_SIZE } from "../../../../lib/constants/pagination"; +import { createTableStore } from "../../../../lib/stores/table"; +import { BackOfficeRoutes } from "../../../constants/routes"; + +/** + * The search parameter names for each filter of the warehouses table. + */ +const FILTERS_PARAMS_NAMES: Record = { + pageIndex: "pageIndex", + location: "location", +}; + +/** + * The initial data of the warehouses table. + */ +const initialData: PaginatedWarehouses = { + warehouses: [], + total: 0, +}; + +/** + * The initial filters of the warehouses table. + */ +export const initialFilters: WarehousesFilters = { + pageIndex: 0, + location: "", +}; + +/** + * Maps URL search params to warehouses filters. + * @param searchParams URL search params. + * @returns Warehouses filters. + */ +function searchParamsToFilters( + searchParams: URLSearchParams, +): WarehousesFilters { + let pageIndex = initialFilters.pageIndex; + let location = initialFilters.location; + + const pageIndexParam = Number( + searchParams.get(FILTERS_PARAMS_NAMES.pageIndex), + ); + const locationParam = searchParams.get(FILTERS_PARAMS_NAMES.location); + + // Update page index when it's a valid number. + if (!Number.isNaN(pageIndexParam)) { + pageIndex = pageIndexParam; + } + + // Update location when it's a non empty value. + if (locationParam) { + location = locationParam; + } + + return { + pageIndex, + location, + }; +} + +/** + * Maps filters of the warehouses table to URL search params. + * @param filters Warehouses filters. + * @returns URL search params. + */ +function filtersToSearchParams(filters: WarehousesFilters): URLSearchParams { + const { pageIndex, location } = filters; + + const searchParams = new URLSearchParams(window.location.search); + searchParams.set(FILTERS_PARAMS_NAMES.pageIndex, pageIndex.toString()); + + if (location) { + searchParams.set(FILTERS_PARAMS_NAMES.location, location); + } else { + searchParams.delete(FILTERS_PARAMS_NAMES.location); + } + + return searchParams; +} + +/** + * Retrieves warehouses to be displayed in the warehouses table. + * @param filters Warehouses filters. + * @returns Warehouses. + */ +async function getWarehouses( + filters: WarehousesFilters, +): Promise { + const { pageIndex, location } = filters; + + const res = await ecomapHttpClient.GET("/warehouses", { + params: { + query: { + offset: pageIndex * DEFAULT_PAGE_SIZE, + limit: DEFAULT_PAGE_SIZE, + sort: "createdAt", + order: "desc", + locationName: location, + }, + }, + }); + + if (res.error) { + return { total: 0, warehouses: [] }; + } + + return res.data; +} + +const warehousesStore = createTableStore( + BackOfficeRoutes.WAREHOUSES, + initialData, + filtersToSearchParams, + searchParamsToFilters, + getWarehouses, +); + +export default warehousesStore; diff --git a/web/src/routes/signIn/SignIn.svelte b/web/src/routes/signIn/SignIn.svelte index 2552b5a0..6bd85138 100644 --- a/web/src/routes/signIn/SignIn.svelte +++ b/web/src/routes/signIn/SignIn.svelte @@ -36,13 +36,13 @@ */ function validateForm(username: string, password: string) { if (!username) { - formErrorMessages.username = $t("error.requiredField"); + formErrorMessages.username = $t("error.valueMissing"); } else { formErrorMessages.username = ""; } if (!password) { - formErrorMessages.password = $t("error.requiredField"); + formErrorMessages.password = $t("error.valueMissing"); } else { formErrorMessages.password = ""; } @@ -137,7 +137,7 @@

{$t("signin.title")}

-
+