diff --git a/src/backend/services/catalog-api/src/handlers/catalog.rs b/src/backend/services/catalog-api/src/handlers/catalog.rs index dea4b34cd..d4ccc372c 100644 --- a/src/backend/services/catalog-api/src/handlers/catalog.rs +++ b/src/backend/services/catalog-api/src/handlers/catalog.rs @@ -1,8 +1,9 @@ -use crate::models::catalog::{Catalog, CatalogRequest, CatalogResponse, NewCatalog, UpdateCatalog}; +use crate::models::catalog::{Catalog, CatalogItem, CatalogRequest, CatalogsResponse, NewCatalog, UpdateCatalog}; use crate::db::db::establish_connection; use crate::schema::{self}; use bigdecimal::ToPrimitive; +use diesel::dsl::Limit; use diesel::prelude::*; use opentelemetry::global::{self, ObjectSafeSpan}; use opentelemetry::trace::Tracer; @@ -72,8 +73,12 @@ pub fn delete(catalog_id: i32) { } #[openapi] -#[get("/all?", format = "json")] -pub fn get_catalogs(category_name: Option) -> Result>> { +#[get("/all?&&", format = "json")] +pub fn get_catalogs( + category_name: Option, + offset: Option, + limit: Option, +) -> Result> { use schema::catalog::dsl::*; let connection = &mut establish_connection(); @@ -81,11 +86,18 @@ pub fn get_catalogs(category_name: Option) -> Result = query + .offset(offset) + .limit(limit) .load::(connection) .expect("failed to loading catalogs") .into_iter() - .map(|c| CatalogResponse { + .map(|c| CatalogItem { id: c.id, name: c.name, description: c.description, @@ -95,12 +107,20 @@ pub fn get_catalogs(category_name: Option) -> Result", format = "json")] -pub fn get_catalog(catalog_id: i32) -> Result> { +pub fn get_catalog(catalog_id: i32) -> Result> { use schema::catalog::dsl::*; let connection = &mut establish_connection(); @@ -109,7 +129,7 @@ pub fn get_catalog(catalog_id: i32) -> Result> { .first::(connection) .expect("failed to loading catalogs"); - let res = CatalogResponse { + let res = CatalogItem { id: c.id, name: c.name, description: c.description, diff --git a/src/backend/services/catalog-api/src/models/catalog.rs b/src/backend/services/catalog-api/src/models/catalog.rs index dabe22205..45fd98b62 100644 --- a/src/backend/services/catalog-api/src/models/catalog.rs +++ b/src/backend/services/catalog-api/src/models/catalog.rs @@ -56,7 +56,7 @@ pub struct CatalogRequest { #[derive(Serialize, Deserialize, JsonSchema)] #[serde(crate = "rocket::serde")] -pub struct CatalogResponse { +pub struct CatalogItem { pub id: i32, pub name: String, pub description: Option, @@ -65,3 +65,12 @@ pub struct CatalogResponse { pub currency: String, pub category: String, } + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(crate = "rocket::serde")] +pub struct CatalogsResponse { + pub catalog_items: Vec, + pub total: i64, + pub offset: i64, + pub limit: i64, +} diff --git a/src/backend/services/web-app/src/app/foods/foods.tsx b/src/backend/services/web-app/src/app/foods/foods.tsx index 079a5b949..f7a76858d 100644 --- a/src/backend/services/web-app/src/app/foods/foods.tsx +++ b/src/backend/services/web-app/src/app/foods/foods.tsx @@ -17,7 +17,7 @@ export const FoodsPage = ({
- {foodItems.map((item) => ( + {foodItems.catalog_items.map((item) => ( ))}
diff --git a/src/backend/services/web-app/src/lib/fetch.ts b/src/backend/services/web-app/src/lib/fetch.ts index 79ddf60e7..01eec4dfe 100644 --- a/src/backend/services/web-app/src/lib/fetch.ts +++ b/src/backend/services/web-app/src/lib/fetch.ts @@ -20,14 +20,15 @@ async function fetchItems(apiUrl: string) { } const items: FoodItems = FoodItemsScheme.parse(await res.json()); - const updatedItems = items.map((item) => { + const catalog_items = items.catalog_items.map((item) => { return { ...item, image: process.env.INTERNAL_API_BASE_URL + item.image }; }); - return updatedItems; + + return {...items, catalog_items} } export async function fetchCategories(): Promise { diff --git a/src/backend/services/web-app/src/lib/types/food-item.ts b/src/backend/services/web-app/src/lib/types/food-item.ts index 7a857b52c..e77712091 100644 --- a/src/backend/services/web-app/src/lib/types/food-item.ts +++ b/src/backend/services/web-app/src/lib/types/food-item.ts @@ -9,7 +9,12 @@ export const FoodItemScheme = z.object({ currency: z.string() }); -export const FoodItemsScheme = z.array(FoodItemScheme); +export const FoodItemsScheme = z.object({ + catalog_items: z.array(FoodItemScheme), + total: z.number(), + offset: z.number(), + limit: z.number() +}); export const CategoriesScheme = z.array( z.object({ diff --git a/src/backend/services/web.admin/dashboard-app/next.config.mjs b/src/backend/services/web.admin/dashboard-app/next.config.mjs index 095334c6f..800603fb7 100644 --- a/src/backend/services/web.admin/dashboard-app/next.config.mjs +++ b/src/backend/services/web.admin/dashboard-app/next.config.mjs @@ -10,6 +10,18 @@ const nextConfig = { protocol: "https", hostname: "*.public.blob.vercel-storage.com", }, + { + protocol: 'http', + hostname: 'localhost' + }, + { + protocol: 'http', + hostname: 'traefik' + }, + { + protocol: 'http', + hostname: 'host.docker.internal' + } ], }, }; diff --git a/src/backend/services/web.admin/dashboard-app/package-lock.json b/src/backend/services/web.admin/dashboard-app/package-lock.json index 79d471cf2..68e1ea662 100644 --- a/src/backend/services/web.admin/dashboard-app/package-lock.json +++ b/src/backend/services/web.admin/dashboard-app/package-lock.json @@ -23,10 +23,11 @@ "react": "^18", "react-dom": "^18", "tailwind-merge": "^2.2.1", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.23.8" }, "devDependencies": { - "@types/node": "^20", + "@types/node": "^20.14.10", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", @@ -1118,9 +1119,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", - "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -5726,6 +5727,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/src/backend/services/web.admin/dashboard-app/package.json b/src/backend/services/web.admin/dashboard-app/package.json index 9719486ee..5d94eaf67 100644 --- a/src/backend/services/web.admin/dashboard-app/package.json +++ b/src/backend/services/web.admin/dashboard-app/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@heroicons/react": "^2.1.1", - "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", @@ -24,10 +24,11 @@ "react": "^18", "react-dom": "^18", "tailwind-merge": "^2.2.1", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.23.8" }, "devDependencies": { - "@types/node": "^20", + "@types/node": "^20.14.10", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", diff --git a/src/backend/services/web.admin/dashboard-app/src/app/(dashboard)/products/page.tsx b/src/backend/services/web.admin/dashboard-app/src/app/(dashboard)/products/page.tsx index 87d4c9b58..e5e60fa04 100644 --- a/src/backend/services/web.admin/dashboard-app/src/app/(dashboard)/products/page.tsx +++ b/src/backend/services/web.admin/dashboard-app/src/app/(dashboard)/products/page.tsx @@ -2,7 +2,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { File, PlusCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { ProductsTable } from './products-table'; -import { getProducts } from '@/lib/db'; +import { fetchCatalogs } from '@/lib/fetch'; export default async function ProductsPage({ @@ -12,9 +12,11 @@ export default async function ProductsPage({ }) { const search = searchParams.q ?? ''; const offset = searchParams.offset ?? 0; - const { products, newOffset, totalProducts } = await getProducts( + const productsPerPage = 10; + const { products, newOffset, totalProducts } = await fetchCatalogs( search, - Number(offset) + Number(offset), + productsPerPage, ); return ( @@ -48,6 +50,7 @@ export default async function ProductsPage({ products={products} offset={newOffset ?? 0} totalProducts={totalProducts} + productsPerPage={productsPerPage} /> diff --git a/src/backend/services/web.admin/dashboard-app/src/app/(dashboard)/products/product.tsx b/src/backend/services/web.admin/dashboard-app/src/app/(dashboard)/products/product.tsx index c5869082b..f13cf32aa 100644 --- a/src/backend/services/web.admin/dashboard-app/src/app/(dashboard)/products/product.tsx +++ b/src/backend/services/web.admin/dashboard-app/src/app/(dashboard)/products/product.tsx @@ -10,8 +10,8 @@ import { } from '@/components/ui/dropdown-menu'; import { MoreHorizontal } from 'lucide-react'; import { TableCell, TableRow } from '@/components/ui/table'; -import { SelectProduct } from '@/lib/db'; import { deleteProduct } from './actions'; +import { SelectProduct } from '@/lib/fetch'; export function Product({ product }: { product: SelectProduct }) { return ( @@ -32,7 +32,7 @@ export function Product({ product }: { product: SelectProduct }) { {`$${product.price}`} - {product.stock} + {product.currency} {product.availableAt.toLocaleDateString()} diff --git a/src/backend/services/web.admin/dashboard-app/src/app/(dashboard)/products/products-table.tsx b/src/backend/services/web.admin/dashboard-app/src/app/(dashboard)/products/products-table.tsx index 975b79f10..bbcdafa29 100644 --- a/src/backend/services/web.admin/dashboard-app/src/app/(dashboard)/products/products-table.tsx +++ b/src/backend/services/web.admin/dashboard-app/src/app/(dashboard)/products/products-table.tsx @@ -19,26 +19,26 @@ import { Product } from './product'; import { useRouter } from 'next/navigation'; import { ChevronLeft, ChevronRight } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { SelectProduct } from '@/lib/db'; +import { SelectProduct } from '@/lib/fetch'; export function ProductsTable({ products, offset, - totalProducts + totalProducts, + productsPerPage, }: { products: SelectProduct[]; offset: number; totalProducts: number; + productsPerPage: number; }) { let router = useRouter(); - let productsPerPage = 5; - function prevPage() { router.back(); } function nextPage() { - router.push(`/?offset=${offset}`, { scroll: false }); + router.push(`/products?offset=${offset}`, { scroll: false }); } return ( @@ -46,7 +46,7 @@ export function ProductsTable({ Products - Manage your products and view their sales performance. + Manage your products. @@ -59,9 +59,7 @@ export function ProductsTable({ Name Status Price - - Total Sales - + Currency Created at Actions diff --git a/src/backend/services/web.admin/dashboard-app/src/lib/db.ts b/src/backend/services/web.admin/dashboard-app/src/lib/db.ts deleted file mode 100644 index 38026afba..000000000 --- a/src/backend/services/web.admin/dashboard-app/src/lib/db.ts +++ /dev/null @@ -1,39 +0,0 @@ -export function getProducts(search: string, - offset: number) { - return { - newOffset: null, - totalProducts: 0, - products: [ - { - id: 1, - imageUrl: - 'https://uwja77bygk2kgfqe.public.blob.vercel-storage.com/smartphone-gaPvyZW6aww0IhD3dOpaU6gBGILtcJ.webp', - name: 'Smartphone X Pro', - status: 'active', - price: '999.00', - stock: 150, - availableAt: new Date() - }, - { - id: 2, - imageUrl: - 'https://uwja77bygk2kgfqe.public.blob.vercel-storage.com/earbuds-3rew4JGdIK81KNlR8Edr8NBBhFTOtX.webp', - name: 'Wireless Earbuds Ultra', - status: 'active', - price: '199.00', - stock: 300, - availableAt: new Date() - } - ] as SelectProduct[] - } -} - -export type SelectProduct = { - status: "active" | "inactive" | "archived"; - id: number; - imageUrl: string; - name: string; - price: string; - stock: number; - availableAt: Date; -} \ No newline at end of file diff --git a/src/backend/services/web.admin/dashboard-app/src/lib/fetch.ts b/src/backend/services/web.admin/dashboard-app/src/lib/fetch.ts new file mode 100644 index 000000000..c1bda9f84 --- /dev/null +++ b/src/backend/services/web.admin/dashboard-app/src/lib/fetch.ts @@ -0,0 +1,73 @@ +import { CatalogItems, CatalogItemsScheme, Categories, CategoriesScheme } from "./types/catalog"; + +export type SelectProduct = { + status: "active" | "inactive" | "archived"; + id: number; + imageUrl: string; + name: string; + price: number; + currency: string; + availableAt: Date; +} + +export type SelectProducts = { + newOffset: number | null; + totalProducts: number; + products: SelectProduct[]; +} + + +export async function fetchCatalogs(search: string, offset: number, productsPerPage: number): Promise { + const apiUrl = `${process.env.INTERNAL_API_BASE_URL}/catalog/items/all?offset=${offset}&limit=${productsPerPage}`; + + const items = await fetchItems(apiUrl); + + return { + newOffset: items.offset + items.limit, + totalProducts: items.total, + products: items.catalog_items.map((item) => { + return { + status: 'active', + id: item.id, + imageUrl: item.image, + name: item.name, + price: item.price, + currency: item.currency, + availableAt: new Date(), + }; + }), + }; +} + +export async function fetchCatalogItemsByCategory(category: string): Promise { + const apiUrl = process.env.INTERNAL_API_BASE_URL + `/catalog/items/all?category_name=${category}`; + return await fetchItems(apiUrl); +} + +async function fetchItems(apiUrl: string) { + const res = await fetch(apiUrl); + if (!res.ok) { + throw new Error('Failed to fetch catalog items data'); + } + + const items: CatalogItems = CatalogItemsScheme.parse(await res.json()); + const catalog_items = items.catalog_items.map((item) => { + return { + ...item, + image: process.env.INTERNAL_API_BASE_URL + item.image + }; + }); + + return { ...items, catalog_items }; +} + +export async function fetchCategories(): Promise { + const apiUrl = process.env.INTERNAL_API_BASE_URL + '/catalog/categories'; + const res = await fetch(apiUrl); + if (!res.ok) { + throw new Error('Failed to fetch categories data'); + } + + const categories: Categories = CategoriesScheme.parse(await res.json()); + return categories; +} diff --git a/src/backend/services/web.admin/dashboard-app/src/lib/types/catalog.ts b/src/backend/services/web.admin/dashboard-app/src/lib/types/catalog.ts new file mode 100644 index 000000000..b457d3a8c --- /dev/null +++ b/src/backend/services/web.admin/dashboard-app/src/lib/types/catalog.ts @@ -0,0 +1,28 @@ +import { z } from 'zod'; + +export const CatalogItemScheme = z.object({ + id: z.number(), + name: z.string(), + description: z.string(), + price: z.number(), + image: z.string(), + currency: z.string() +}); + +export const CatalogItemsScheme = z.object({ + catalog_items: z.array(CatalogItemScheme), + total: z.number(), + offset: z.number(), + limit: z.number() +}); + +export const CategoriesScheme = z.array( + z.object({ + id: z.number(), + name: z.string() + }) +); + +export type CatalogItem = z.infer; +export type CatalogItems = z.infer; +export type Categories = z.infer;