diff --git a/03_source/cms_backend/prisma/schema.prisma b/03_source/cms_backend/prisma/schema.prisma index e030df0..ac50f16 100644 --- a/03_source/cms_backend/prisma/schema.prisma +++ b/03_source/cms_backend/prisma/schema.prisma @@ -195,32 +195,32 @@ model ProductItem { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // - sku String - name String - code String - price Float - taxes Float - tags String[] - sizes String[] - publish String - gender String[] - coverUrl String - images String[] - colors String[] - quantity Int - category String available Int - totalSold Int + category String + code String + colors String[] + coverUrl String description String - totalRatings Float - totalReviews Int + gender String[] + images String[] inventoryType String - subDescription String - priceSale Float? + name String newLabel Json - saleLabel Json + price Float + priceSale Float? + publish String + quantity Int ratings Json[] reviews ProductReview[] + saleLabel Json + sizes String[] + sku String + subDescription String + tags String[] + taxes Float + totalRatings Float + totalReviews Int + totalSold Int } model MailSender { diff --git a/03_source/cms_backend/src/app/api/product/create/route.ts b/03_source/cms_backend/src/app/api/product/create/route.ts new file mode 100644 index 0000000..9fc848e --- /dev/null +++ b/03_source/cms_backend/src/app/api/product/create/route.ts @@ -0,0 +1,43 @@ +// src/app/api/product/createProduct/route.ts +// REQ0183 frontend product new +// +// PURPOSE: +// create product to db +// +// RULES: +// T.B.A. +// + +import type { NextRequest } from 'next/server'; + +import { STATUS, response, handleError } from 'src/utils/response'; + +import { isDev } from 'src/constants'; + +import prisma from '../../../lib/prisma'; + +// ---------------------------------------------------------------------- + +/** ************************************** + * POST - Products + *************************************** */ +export async function POST(req: NextRequest) { + const { productData } = await req.json(); + + try { + if (isDev) { + console.log({ productData }); + } + + await prisma.productItem.create({ data: productData }); + + if (isDev) { + console.log('create done'); + } + + return response({ hello: 'world' }, STATUS.OK); + } catch (error) { + console.log({ hello: 'world', productData }); + return handleError('Product - Create', error); + } +} diff --git a/03_source/cms_backend/src/app/api/product/create/test.http b/03_source/cms_backend/src/app/api/product/create/test.http new file mode 100644 index 0000000..46163ae --- /dev/null +++ b/03_source/cms_backend/src/app/api/product/create/test.http @@ -0,0 +1,51 @@ +### + +POST http://localhost:7272/api/product/createProduct +Content-Type: application/json + +{ + "data": { + "available": 99, + "category": "T-shirts", + "code": "PD-12345", + "colors": [ + "Red" + ], + "coverUrl": "", + "description": "this is description, 会員管理機能は、単なるデータ管理ツール以上の価値を持ちます。 企業は顧客の情報や動向を深く理解し、長期的な顧客関係の構築やロイヤルティの確立、そして迅速な市場変動への対応に直結します。 会員管理機能は、現代のビジネスにおいて企業の競争力を高める基盤として欠かせないものとなっています。", + "gender": [ + "Men" + ], + "images": [ + "data:image/png;base64,C", + "data:image/png;base64,C" + ], + "inventoryType": "test", + "name": "hello product", + "newLabel": { + "enabled": false, + "content": "" + }, + "price": 99.99, + "priceSale": null, + "publish": "yes", + "quantity": 99, + "saleLabel": { + "enabled": false, + "content": "" + }, + "sizes": [ + "7" + ], + "sku": "SK-122345", + "subDescription": "this is test sub-description", + "tags": [ + "Travel", + "Finance" + ], + "taxes": 0, + "totalRatings": 1, + "totalReviews": 1, + "totalSold": 1 + } +} diff --git a/03_source/cms_backend/src/app/api/product/createProduct/route.ts b/03_source/cms_backend/src/app/api/product/createProduct/route.ts index 84f3bc8..1475a12 100644 --- a/03_source/cms_backend/src/app/api/product/createProduct/route.ts +++ b/03_source/cms_backend/src/app/api/product/createProduct/route.ts @@ -1,4 +1,5 @@ // src/app/api/product/createProduct/route.ts +// REQ0183 frontend product new // // PURPOSE: // create product to db @@ -11,6 +12,8 @@ import type { NextRequest } from 'next/server'; import { STATUS, response, handleError } from 'src/utils/response'; +import { isDev } from 'src/constants'; + import prisma from '../../../lib/prisma'; // ---------------------------------------------------------------------- @@ -19,57 +22,22 @@ import prisma from '../../../lib/prisma'; * POST - Products *************************************** */ export async function POST(req: NextRequest) { - // logger('[Product] list', products.length); const { data } = await req.json(); - const createForm: CreateProductData = data as unknown as CreateProductData; - - console.log({ createForm }); try { - console.log({ data }); - await prisma.productItem.create({ data: createForm }); + if (isDev) { + console.log({ data }); + } + + await prisma.productItem.create({ data }); + + if (isDev) { + console.log('create done'); + } + return response({ hello: 'world' }, STATUS.OK); } catch (error) { console.log({ hello: 'world', data }); return handleError('Product - Create', error); } } - -type CreateProductData = { - // id: string; - sku: string; - name: string; - code: string; - price: number; - taxes: number; - tags: string[]; - sizes: string[]; - publish: string; - gender: string[]; - coverUrl: string; - images: string[]; - colors: string[]; - quantity: number; - category: string; - available: number; - totalSold: number; - description: string; - totalRatings: number; - totalReviews: number; - inventoryType: string; - subDescription: string; - priceSale: number; - newLabel: { - content: string; - enabled: boolean; - }; - saleLabel: { - content: string; - enabled: boolean; - }; - // ratings: { - // name: string; - // starCount: number; - // reviewCount: number; - // }[]; -}; diff --git a/03_source/cms_backend/src/app/api/product/createProduct/test.http b/03_source/cms_backend/src/app/api/product/createProduct/test.http new file mode 100644 index 0000000..46163ae --- /dev/null +++ b/03_source/cms_backend/src/app/api/product/createProduct/test.http @@ -0,0 +1,51 @@ +### + +POST http://localhost:7272/api/product/createProduct +Content-Type: application/json + +{ + "data": { + "available": 99, + "category": "T-shirts", + "code": "PD-12345", + "colors": [ + "Red" + ], + "coverUrl": "", + "description": "this is description, 会員管理機能は、単なるデータ管理ツール以上の価値を持ちます。 企業は顧客の情報や動向を深く理解し、長期的な顧客関係の構築やロイヤルティの確立、そして迅速な市場変動への対応に直結します。 会員管理機能は、現代のビジネスにおいて企業の競争力を高める基盤として欠かせないものとなっています。", + "gender": [ + "Men" + ], + "images": [ + "data:image/png;base64,C", + "data:image/png;base64,C" + ], + "inventoryType": "test", + "name": "hello product", + "newLabel": { + "enabled": false, + "content": "" + }, + "price": 99.99, + "priceSale": null, + "publish": "yes", + "quantity": 99, + "saleLabel": { + "enabled": false, + "content": "" + }, + "sizes": [ + "7" + ], + "sku": "SK-122345", + "subDescription": "this is test sub-description", + "tags": [ + "Travel", + "Finance" + ], + "taxes": 0, + "totalRatings": 1, + "totalReviews": 1, + "totalSold": 1 + } +} diff --git a/03_source/cms_backend/src/app/api/product/delete/route.ts b/03_source/cms_backend/src/app/api/product/delete/route.ts new file mode 100644 index 0000000..8f3677c --- /dev/null +++ b/03_source/cms_backend/src/app/api/product/delete/route.ts @@ -0,0 +1,22 @@ +import type { NextRequest } from 'next/server'; + +import { STATUS, response, handleError } from 'src/utils/response'; + +import { deleteProduct } from 'src/app/services/product.service'; + +/** ************************************** + * PATCH - Delete product + *************************************** */ +export async function PATCH(req: NextRequest) { + try { + const { productId } = await req.json(); + + if (!productId) throw new Error('productId cannot be null'); + + await deleteProduct(productId); + + return response({ productId }, STATUS.OK); + } catch (error) { + return handleError('Product - Delete', error); + } +} diff --git a/03_source/cms_backend/src/app/api/product/details/route.ts b/03_source/cms_backend/src/app/api/product/details/route.ts index beb7197..a5f4158 100644 --- a/03_source/cms_backend/src/app/api/product/details/route.ts +++ b/03_source/cms_backend/src/app/api/product/details/route.ts @@ -1,5 +1,7 @@ // src/app/api/product/details/route.ts // +// REQ0182 frontend product details +// // PURPOSE: // get product from db by id // diff --git a/03_source/cms_backend/src/app/api/product/details/test.http b/03_source/cms_backend/src/app/api/product/details/test.http index da42439..66907a6 100644 --- a/03_source/cms_backend/src/app/api/product/details/test.http +++ b/03_source/cms_backend/src/app/api/product/details/test.http @@ -1,3 +1,5 @@ ### - +GET http://localhost:7272/api/product/details?productId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01 + +### GET http://localhost:7272/api/product/details?productId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01 diff --git a/03_source/cms_backend/src/app/api/product/list/route.ts.bak b/03_source/cms_backend/src/app/api/product/list/route.ts.bak deleted file mode 100644 index 24ce8d4..0000000 --- a/03_source/cms_backend/src/app/api/product/list/route.ts.bak +++ /dev/null @@ -1,23 +0,0 @@ -import { logger } from 'src/utils/logger'; -import { STATUS, response, handleError } from 'src/utils/response'; - -import { _products } from 'src/_mock/_product'; - -// ---------------------------------------------------------------------- - -export const runtime = 'edge'; - -/** ************************************** - * GET - Products - *************************************** */ -export async function GET() { - try { - const products = _products(); - - logger('[Product] list', products.length); - - return response({ products }, STATUS.OK); - } catch (error) { - return handleError('Product - Get list', error); - } -} diff --git a/03_source/cms_backend/src/app/api/product/saveProduct/route.ts b/03_source/cms_backend/src/app/api/product/saveProduct/route.ts index 6400ae5..d2a8c05 100644 --- a/03_source/cms_backend/src/app/api/product/saveProduct/route.ts +++ b/03_source/cms_backend/src/app/api/product/saveProduct/route.ts @@ -17,7 +17,7 @@ import prisma from '../../../lib/prisma'; /** ************************************** * GET - Products *************************************** */ -export async function POST(req: NextRequest) { +export async function PUT(req: NextRequest) { // logger('[Product] list', products.length); const { data } = await req.json(); diff --git a/03_source/cms_backend/src/app/api/product/saveProduct/test.http b/03_source/cms_backend/src/app/api/product/saveProduct/test.http new file mode 100644 index 0000000..2b44878 --- /dev/null +++ b/03_source/cms_backend/src/app/api/product/saveProduct/test.http @@ -0,0 +1,51 @@ +### + +PUT http://localhost:7272/api/product/createProduct +Content-Type: application/json + +{ + "data": { + "available": 99, + "category": "T-shirts", + "code": "PD-12345", + "colors": [ + "Red" + ], + "coverUrl": "", + "description": "this is description, 会員管理機能は、単なるデータ管理ツール以上の価値を持ちます。 企業は顧客の情報や動向を深く理解し、長期的な顧客関係の構築やロイヤルティの確立、そして迅速な市場変動への対応に直結します。 会員管理機能は、現代のビジネスにおいて企業の競争力を高める基盤として欠かせないものとなっています。", + "gender": [ + "Men" + ], + "images": [ + "data:image/png;base64,C", + "data:image/png;base64,C" + ], + "inventoryType": "test", + "name": "hello product", + "newLabel": { + "enabled": false, + "content": "" + }, + "price": 99.99, + "priceSale": null, + "publish": "yes", + "quantity": 99, + "saleLabel": { + "enabled": false, + "content": "" + }, + "sizes": [ + "7" + ], + "sku": "SK-122345", + "subDescription": "this is test sub-description", + "tags": [ + "Travel", + "Finance" + ], + "taxes": 0, + "totalRatings": 1, + "totalReviews": 1, + "totalSold": 1 + } +} diff --git a/03_source/cms_backend/src/app/api/product/update/route.ts b/03_source/cms_backend/src/app/api/product/update/route.ts new file mode 100644 index 0000000..ad43207 --- /dev/null +++ b/03_source/cms_backend/src/app/api/product/update/route.ts @@ -0,0 +1,129 @@ +// src/app/api/product/saveProduct/route.ts +// +// PURPOSE: +// save product to db by id +// +// RULES: +// T.B.A. + +import type { NextRequest } from 'next/server'; + +import { STATUS, response, handleError } from 'src/utils/response'; + +import prisma from '../../../lib/prisma'; + +// ---------------------------------------------------------------------- + +/** ************************************** + * GET - Products + *************************************** */ +export async function PUT(req: NextRequest) { + // logger('[Product] list', products.length); + const { productData } = await req.json(); + + try { + const products = await prisma.productItem.updateMany({ + data: { + name: productData.name, + sku: productData.sku, + code: productData.code, + price: productData.price, + taxes: productData.taxes, + tags: productData.tags, + sizes: productData.sizes, + publish: productData.publish, + gender: productData.gender, + coverUrl: productData.coverUrl, + images: productData.images, + colors: productData.colors, + quantity: productData.quantity, + category: productData.category, + available: productData.available, + totalSold: productData.totalSold, + description: productData.description, + totalRatings: productData.totalRatings, + totalReviews: productData.totalReviews, + inventoryType: productData.inventoryType, + subDescription: productData.subDescription, + priceSale: productData.priceSale, + // + newLabel: { + content: productData.newLabel?.content || '', + enabled: productData.newLabel?.enabled ?? false, + }, + saleLabel: { + content: productData.saleLabel?.content || '', + enabled: productData.saleLabel?.enabled ?? false, + }, + ratings: { + set: productData.ratings.map((rating: { name: string; starCount: number; reviewCount: number }) => ({ + name: rating.name, + starCount: rating.starCount, + reviewCount: rating.reviewCount, + })), + }, + }, + where: { id: productData.id }, + }); + + return response({ data: productData }, STATUS.OK); + } catch (error) { + console.log({ data: productData }); + return handleError('Product - Get list', error); + } +} + +export type IProductItem = { + id: string; + sku: string; + name: string; + code: string; + price: number; + taxes: number; + tags: string[]; + sizes: string[]; + publish: string; + gender: string[]; + coverUrl: string; + images: string[]; + colors: string[]; + quantity: number; + category: string; + available: number; + totalSold: number; + description: string; + totalRatings: number; + totalReviews: number; + // createdAt: IDateValue; + inventoryType: string; + subDescription: string; + priceSale: number | null; + // reviews: IProductReview[]; + newLabel: { + content: string; + enabled: boolean; + }; + saleLabel: { + content: string; + enabled: boolean; + }; + ratings: { + name: string; + starCount: number; + reviewCount: number; + }[]; +}; + +export type IDateValue = string | number | null; + +export type IProductReview = { + id: string; + name: string; + rating: number; + comment: string; + helpful: number; + avatarUrl: string; + postedAt: IDateValue; + isPurchased: boolean; + attachments?: string[]; +}; diff --git a/03_source/cms_backend/src/app/api/product/update/test.http b/03_source/cms_backend/src/app/api/product/update/test.http new file mode 100644 index 0000000..2b44878 --- /dev/null +++ b/03_source/cms_backend/src/app/api/product/update/test.http @@ -0,0 +1,51 @@ +### + +PUT http://localhost:7272/api/product/createProduct +Content-Type: application/json + +{ + "data": { + "available": 99, + "category": "T-shirts", + "code": "PD-12345", + "colors": [ + "Red" + ], + "coverUrl": "", + "description": "this is description, 会員管理機能は、単なるデータ管理ツール以上の価値を持ちます。 企業は顧客の情報や動向を深く理解し、長期的な顧客関係の構築やロイヤルティの確立、そして迅速な市場変動への対応に直結します。 会員管理機能は、現代のビジネスにおいて企業の競争力を高める基盤として欠かせないものとなっています。", + "gender": [ + "Men" + ], + "images": [ + "data:image/png;base64,C", + "data:image/png;base64,C" + ], + "inventoryType": "test", + "name": "hello product", + "newLabel": { + "enabled": false, + "content": "" + }, + "price": 99.99, + "priceSale": null, + "publish": "yes", + "quantity": 99, + "saleLabel": { + "enabled": false, + "content": "" + }, + "sizes": [ + "7" + ], + "sku": "SK-122345", + "subDescription": "this is test sub-description", + "tags": [ + "Travel", + "Finance" + ], + "taxes": 0, + "totalRatings": 1, + "totalReviews": 1, + "totalSold": 1 + } +} diff --git a/03_source/cms_backend/src/app/services/product.service.ts b/03_source/cms_backend/src/app/services/product.service.ts index b29c83f..fccf049 100644 --- a/03_source/cms_backend/src/app/services/product.service.ts +++ b/03_source/cms_backend/src/app/services/product.service.ts @@ -1,5 +1,7 @@ // src/app/services/product.service.ts // +// REQ0182 frontend product details +// // PURPOSE: // - Service for handling ProductItem Record // @@ -65,11 +67,19 @@ type UpdateProduct = { }; async function listProducts(): Promise { - return prisma.productItem.findMany(); + return prisma.productItem.findMany({ + include: { + reviews: true, + }, + }); } async function getProduct(productId: string): Promise { - return prisma.productItem.findUnique({ where: { id: productId } }); + return prisma.productItem.findUnique({ + where: { id: productId }, + include: { reviews: true }, + // + }); } async function createProduct(createForm: CreateProduct) { diff --git a/03_source/frontend/src/actions/product.ts b/03_source/frontend/src/actions/product.ts index 9851593..e818325 100644 --- a/03_source/frontend/src/actions/product.ts +++ b/03_source/frontend/src/actions/product.ts @@ -1,9 +1,11 @@ +// // src/actions/product.ts +// import { useMemo } from 'react'; import axiosInstance, { endpoints, fetcher } from 'src/lib/axios'; import type { IProductItem } from 'src/types/product'; import type { SWRConfiguration } from 'swr'; -import useSWR from 'swr'; +import useSWR, { mutate } from 'swr'; // ---------------------------------------------------------------------- @@ -22,11 +24,7 @@ type ProductsData = { export function useGetProducts() { const url = endpoints.product.list; - const { data, isLoading, error, isValidating, mutate } = useSWR( - url, - fetcher, - swrOptions - ); + const { data, isLoading, error, isValidating } = useSWR(url, fetcher, swrOptions); const memoizedValue = useMemo( () => ({ @@ -35,9 +33,8 @@ export function useGetProducts() { productsError: error, productsValidating: isValidating, productsEmpty: !isLoading && !isValidating && !data?.products.length, - mutate, }), - [data?.products, error, isLoading, isValidating, mutate] + [data?.products, error, isLoading, isValidating] ); return memoizedValue; @@ -56,10 +53,11 @@ export function useGetProduct(productId: string) { const memoizedValue = useMemo( () => ({ - currentProduct: data?.product, + product: data?.product, productLoading: isLoading, productError: error, productValidating: isValidating, + mutate, }), [data?.product, error, isLoading, isValidating] ); @@ -97,140 +95,80 @@ export function useSearchProducts(query: string) { // ---------------------------------------------------------------------- -type SaveProductData = { - // id: string; +export async function createProduct(productData: IProductItem) { + /** + * Work on server + */ + const data = { productData }; + await axiosInstance.post(endpoints.product.create, data); - sku: string; - name: string; - code: string; - price: number | null; - taxes: number | null; - tags: string[]; - sizes: string[]; - // publish: string; - gender: string[]; - // coverUrl: string; - images: (string | File)[]; - colors: string[]; - quantity: number | null; - category: string; - // available: number; - // totalSold: number; - description: string; - // totalRatings: number; - // totalReviews: number; - // inventoryType: string; - subDescription: string; - priceSale: number | null; - newLabel: { - content: string; - enabled: boolean; - }; - saleLabel: { - content: string; - enabled: boolean; - }; - // ratings: { - // name: string; - // starCount: number; - // reviewCount: number; - // }[]; -}; + /** + * Work in local + */ + mutate( + endpoints.product.list, + (currentData: any) => { + const currentProducts: IProductItem[] = currentData?.products; -export async function saveProduct(productId: string, saveProductData: SaveProductData) { - console.log('save product ?'); - // const url = productId ? [endpoints.product.details, { params: { productId } }] : ''; + const products = [...currentProducts, productData]; - const res = await axiosInstance.post('http://localhost:7272/api/product/saveProduct', { - data: saveProductData, - }); - - return res; -} - -export async function uploadProductImage(saveProductData: SaveProductData) { - console.log('save product ?'); - // const url = productId ? [endpoints.product.details, { params: { productId } }] : ''; - - const res = await axiosInstance.get('http://localhost:7272/api/product/helloworld'); - - return res; + return { ...currentData, products }; + }, + false + ); } // ---------------------------------------------------------------------- -type CreateProductData = { - // id: string; - sku: string; - name: string; - code: string; - price: number | null; - taxes: number | null; - tags: string[]; - sizes: string[]; - publish: string; - gender: string[]; - coverUrl: string; - images: (string | File)[]; - colors: string[]; - quantity: number | null; - category: string; - available: number; - totalSold: number; - description: string; - totalRatings: number; - totalReviews: number; - inventoryType: string; - subDescription: string; - priceSale: number | null; - newLabel: { - content: string; - enabled: boolean; - }; - saleLabel: { - content: string; - enabled: boolean; - }; - // ratings: { - // name: string; - // starCount: number; - // reviewCount: number; - // }[]; -}; +export async function updateProduct(productData: Partial) { + /** + * Work on server + */ + const data = { productData }; + await axiosInstance.put(endpoints.product.update, data); -export async function createProduct(createProductData: CreateProductData) { - console.log('create product ?'); - // const url = productId ? [endpoints.product.details, { params: { productId } }] : ''; + /** + * Work in local + */ - const res = await axiosInstance.post('http://localhost:7272/api/product/createProduct', { - data: createProductData, - }); + mutate( + endpoints.product.list, + (currentData: any) => { + const currentProducts: IProductItem[] = currentData?.products; - return res; + const products = currentProducts.map((product) => + product.id === productData.id ? { ...product, ...productData } : product + ); + + return { ...currentData, products }; + }, + false + ); } // ---------------------------------------------------------------------- -type DeleteProductResponse = { - success: boolean; - message?: string; -}; +export async function deleteProduct(productId: string) { + /** + * Work on server + */ + const data = { productId }; + await axiosInstance.patch(endpoints.product.delete, data); -export async function deleteProduct(productId: string): Promise { - const url = `http://localhost:7272/api/product/deleteProduct?productId=${productId}`; + /** + * Work in local + */ - try { - const res = await axiosInstance.delete(url); - console.log({ res }); + mutate( + endpoints.product.list, + (currentData: any) => { + console.log({ currentData }); + const currentProducts: IProductItem[] = currentData?.products; - return { - success: true, - message: 'Product deleted successfully', - }; - } catch (error) { - return { - success: false, - message: error instanceof Error ? error.message : 'Failed to delete product', - }; - } + const products = currentProducts.filter((product) => product.id !== productId); + + return { ...currentData, products }; + }, + false + ); } diff --git a/03_source/frontend/src/pages/dashboard/product/details.tsx b/03_source/frontend/src/pages/dashboard/product/details.tsx index b1cbdce..215c8f6 100644 --- a/03_source/frontend/src/pages/dashboard/product/details.tsx +++ b/03_source/frontend/src/pages/dashboard/product/details.tsx @@ -1,10 +1,8 @@ // src/pages/dashboard/product/details.tsx -import { useParams } from 'src/routes/hooks'; - -import { CONFIG } from 'src/global-config'; import { useGetProduct } from 'src/actions/product'; - +import { CONFIG } from 'src/global-config'; +import { useParams } from 'src/routes/hooks'; import { ProductDetailsView } from 'src/sections/product/view'; // ---------------------------------------------------------------------- @@ -14,7 +12,7 @@ const metadata = { title: `Product details | Dashboard - ${CONFIG.appName}` }; export default function Page() { const { id = '' } = useParams(); - const { currentProduct: product, productLoading, productError } = useGetProduct(id); + const { product, productLoading, productError } = useGetProduct(id); return ( <> diff --git a/03_source/frontend/src/pages/dashboard/product/edit.tsx b/03_source/frontend/src/pages/dashboard/product/edit.tsx index 5067afd..9654194 100644 --- a/03_source/frontend/src/pages/dashboard/product/edit.tsx +++ b/03_source/frontend/src/pages/dashboard/product/edit.tsx @@ -1,8 +1,6 @@ -import { useParams } from 'src/routes/hooks'; - -import { CONFIG } from 'src/global-config'; import { useGetProduct } from 'src/actions/product'; - +import { CONFIG } from 'src/global-config'; +import { useParams } from 'src/routes/hooks'; import { ProductEditView } from 'src/sections/product/view'; // ---------------------------------------------------------------------- @@ -12,13 +10,13 @@ const metadata = { title: `Product edit | Dashboard - ${CONFIG.appName}` }; export default function Page() { const { id = '' } = useParams(); - const { currentProduct } = useGetProduct(id); + const { product } = useGetProduct(id); return ( <> {metadata.title} - + ); } diff --git a/03_source/frontend/src/pages/dashboard/product/list.tsx b/03_source/frontend/src/pages/dashboard/product/list.tsx index 2cffc00..7333213 100644 --- a/03_source/frontend/src/pages/dashboard/product/list.tsx +++ b/03_source/frontend/src/pages/dashboard/product/list.tsx @@ -1,5 +1,5 @@ // src/pages/dashboard/product/list.tsx - +// import { CONFIG } from 'src/global-config'; import { ProductListView } from 'src/sections/product/view'; diff --git a/03_source/frontend/src/pages/product/details.tsx b/03_source/frontend/src/pages/product/details.tsx index b3d9ce3..f8c04c8 100644 --- a/03_source/frontend/src/pages/product/details.tsx +++ b/03_source/frontend/src/pages/product/details.tsx @@ -12,7 +12,7 @@ const metadata = { title: `Product details - ${CONFIG.appName}` }; export default function Page() { const { id = '' } = useParams(); - const { currentProduct: product, productLoading, productError } = useGetProduct(id); + const { product, productLoading, productError } = useGetProduct(id); return ( <> diff --git a/03_source/frontend/src/pages/product/helloworld.tsx b/03_source/frontend/src/pages/product/helloworld.tsx new file mode 100644 index 0000000..61d7eb4 --- /dev/null +++ b/03_source/frontend/src/pages/product/helloworld.tsx @@ -0,0 +1,5 @@ +function Helloworld() { + return <>helloworld; +} + +export default Helloworld; diff --git a/03_source/frontend/src/pages/product/list.tsx b/03_source/frontend/src/pages/product/list.tsx index 1caa9d9..afdd483 100644 --- a/03_source/frontend/src/pages/product/list.tsx +++ b/03_source/frontend/src/pages/product/list.tsx @@ -1,6 +1,5 @@ -import { CONFIG } from 'src/global-config'; import { useGetProducts } from 'src/actions/product'; - +import { CONFIG } from 'src/global-config'; import { ProductShopView } from 'src/sections/product/view'; // ---------------------------------------------------------------------- diff --git a/03_source/frontend/src/sections/product/product-item.tsx b/03_source/frontend/src/sections/product/product-item.tsx index b96d8e4..ef109a4 100644 --- a/03_source/frontend/src/sections/product/product-item.tsx +++ b/03_source/frontend/src/sections/product/product-item.tsx @@ -1,21 +1,16 @@ -import type { IProductItem } from 'src/types/product'; - import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; import Card from '@mui/material/Card'; +import Fab, { fabClasses } from '@mui/material/Fab'; +import Link from '@mui/material/Link'; import Stack from '@mui/material/Stack'; import Tooltip from '@mui/material/Tooltip'; -import Fab, { fabClasses } from '@mui/material/Fab'; - -import { RouterLink } from 'src/routes/components'; - -import { fCurrency } from 'src/utils/format-number'; - -import { Label } from 'src/components/label'; -import { Image } from 'src/components/image'; -import { Iconify } from 'src/components/iconify'; import { ColorPreview } from 'src/components/color-utils'; - +import { Iconify } from 'src/components/iconify'; +import { Image } from 'src/components/image'; +import { Label } from 'src/components/label'; +import { RouterLink } from 'src/routes/components'; +import type { IProductItem } from 'src/types/product'; +import { fCurrency } from 'src/utils/format-number'; import { useCheckoutContext } from '../checkout/context'; // ---------------------------------------------------------------------- diff --git a/03_source/frontend/src/sections/product/product-list.tsx b/03_source/frontend/src/sections/product/product-list.tsx index aa1cf40..88f37bd 100644 --- a/03_source/frontend/src/sections/product/product-list.tsx +++ b/03_source/frontend/src/sections/product/product-list.tsx @@ -1,11 +1,8 @@ import type { BoxProps } from '@mui/material/Box'; -import type { IProductItem } from 'src/types/product'; - import Box from '@mui/material/Box'; import Pagination, { paginationClasses } from '@mui/material/Pagination'; - import { paths } from 'src/routes/paths'; - +import type { IProductItem } from 'src/types/product'; import { ProductItem } from './product-item'; import { ProductItemSkeleton } from './product-skeleton'; diff --git a/03_source/frontend/src/sections/product/product-new-edit-form.tsx b/03_source/frontend/src/sections/product/product-new-edit-form.tsx index dce44c0..b8463f0 100644 --- a/03_source/frontend/src/sections/product/product-new-edit-form.tsx +++ b/03_source/frontend/src/sections/product/product-new-edit-form.tsx @@ -21,7 +21,7 @@ import { PRODUCT_COLOR_NAME_OPTIONS, PRODUCT_SIZE_OPTIONS, } from 'src/_mock'; -import { createProduct, saveProduct } from 'src/actions/product'; +import { createProduct, updateProduct } from 'src/actions/product'; import { Field, Form, schemaHelper } from 'src/components/hook-form'; import { Iconify } from 'src/components/iconify'; import { toast } from 'src/components/snackbar'; @@ -194,9 +194,6 @@ export function ProductNewEditForm({ currentProduct }: Props) { }; try { - // await new Promise((resolve) => setTimeout(resolve, 500)); - // reset(); - // sanitize file field for (let i = 0; i < values.images.length; i++) { const temp: any = values.images[i]; @@ -205,12 +202,14 @@ export function ProductNewEditForm({ currentProduct }: Props) { } } + const sanitizedValues: IProductItem = values as unknown as IProductItem; + if (currentProduct) { // perform save - await saveProduct(currentProduct.id, values); + updateProduct(sanitizedValues); } else { // perform create - await createProduct(values); + createProduct(sanitizedValues); } toast.success(currentProduct ? 'Update success!' : 'Create success!'); diff --git a/03_source/frontend/src/sections/product/product-table-row.tsx b/03_source/frontend/src/sections/product/product-table-row.tsx index 0588aff..580a97d 100644 --- a/03_source/frontend/src/sections/product/product-table-row.tsx +++ b/03_source/frontend/src/sections/product/product-table-row.tsx @@ -1,18 +1,14 @@ // src/sections/product/product-table-row.tsx -import type { GridCellParams } from '@mui/x-data-grid'; - -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; import Avatar from '@mui/material/Avatar'; -import ListItemText from '@mui/material/ListItemText'; +import Box from '@mui/material/Box'; import LinearProgress from '@mui/material/LinearProgress'; - -import { RouterLink } from 'src/routes/components'; - -import { fCurrency } from 'src/utils/format-number'; -import { fTime, fDate } from 'src/utils/format-time'; - +import Link from '@mui/material/Link'; +import ListItemText from '@mui/material/ListItemText'; +import type { GridCellParams } from '@mui/x-data-grid'; import { Label } from 'src/components/label'; +import { RouterLink } from 'src/routes/components'; +import { fCurrency } from 'src/utils/format-number'; +import { fDate, fTime } from 'src/utils/format-time'; // ---------------------------------------------------------------------- diff --git a/03_source/frontend/src/sections/product/view/product-create-view.tsx b/03_source/frontend/src/sections/product/view/product-create-view.tsx index 9f5619a..8e0624f 100644 --- a/03_source/frontend/src/sections/product/view/product-create-view.tsx +++ b/03_source/frontend/src/sections/product/view/product-create-view.tsx @@ -1,9 +1,6 @@ -import { paths } from 'src/routes/paths'; - -import { DashboardContent } from 'src/layouts/dashboard'; - import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs'; - +import { DashboardContent } from 'src/layouts/dashboard'; +import { paths } from 'src/routes/paths'; import { ProductNewEditForm } from '../product-new-edit-form'; // ---------------------------------------------------------------------- diff --git a/03_source/frontend/src/sections/product/view/product-edit-view.tsx b/03_source/frontend/src/sections/product/view/product-edit-view.tsx index d3f9183..5597164 100644 --- a/03_source/frontend/src/sections/product/view/product-edit-view.tsx +++ b/03_source/frontend/src/sections/product/view/product-edit-view.tsx @@ -1,11 +1,7 @@ -import type { IProductItem } from 'src/types/product'; - -import { paths } from 'src/routes/paths'; - -import { DashboardContent } from 'src/layouts/dashboard'; - import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs'; - +import { DashboardContent } from 'src/layouts/dashboard'; +import { paths } from 'src/routes/paths'; +import type { IProductItem } from 'src/types/product'; import { ProductNewEditForm } from '../product-new-edit-form'; // ---------------------------------------------------------------------- diff --git a/03_source/frontend/src/sections/product/view/product-list-view.tsx b/03_source/frontend/src/sections/product/view/product-list-view.tsx index 188b013..2236cd3 100644 --- a/03_source/frontend/src/sections/product/view/product-list-view.tsx +++ b/03_source/frontend/src/sections/product/view/product-list-view.tsx @@ -35,9 +35,11 @@ import { EmptyContent } from 'src/components/empty-content'; import { Iconify } from 'src/components/iconify'; import { toast } from 'src/components/snackbar'; import { DashboardContent } from 'src/layouts/dashboard'; +import { endpoints } from 'src/lib/axios'; import { RouterLink } from 'src/routes/components'; import { paths } from 'src/routes/paths'; import type { IProductItem, IProductTableFilters } from 'src/types/product'; +import { mutate } from 'swr'; import { ProductTableFiltersResult } from '../product-table-filters-result'; import { RenderCellCreatedAt, @@ -59,6 +61,8 @@ const HIDE_COLUMNS_TOGGLABLE = ['category', 'actions']; export function ProductListView() { const { t } = useTranslation(); + const confirmDialog = useBoolean(); + const PRODUCT_STOCK_OPTIONS = [ { value: 'in stock', label: t('In stock') }, { value: 'low stock', label: t('Low stock') }, @@ -75,7 +79,7 @@ export function ProductListView() { const confirmDeleteSingleItemDialog = useBoolean(); const [idToDelete, setIdToDelete] = useState(null); - const { products, productsLoading, mutate } = useGetProducts(); + const { products, productsLoading } = useGetProducts(); const [tableData, setTableData] = useState(products); const [selectedRowIds, setSelectedRowIds] = useState([]); @@ -113,13 +117,29 @@ export function ProductListView() { toast.error('Delete failed!'); } - // TODO: reload table here - mutate(); - // setTableData(deleteRow); setDeleteInProgress(false); }, [idToDelete, mutate]); + // NOTE: this is working using example from calendar + const handleDeleteRow = useCallback( + async (id: string) => { + try { + await deleteProduct(id); + + // invalidate cache to reload list + await mutate(endpoints.product.list); + + toast.success('Delete success!'); + } catch (error) { + console.error(error); + + toast.error('Delete error!'); + } + }, + [tableData] + ); + const handleDeleteRows = useCallback(() => { const deleteRows = tableData.filter((row) => !selectedRowIds.includes(row.id)); diff --git a/03_source/frontend/src/sections/product/view/product-shop-details-view.tsx b/03_source/frontend/src/sections/product/view/product-shop-details-view.tsx index 63bdfbd..97d2d0b 100644 --- a/03_source/frontend/src/sections/product/view/product-shop-details-view.tsx +++ b/03_source/frontend/src/sections/product/view/product-shop-details-view.tsx @@ -1,33 +1,28 @@ -import type { IProductItem } from 'src/types/product'; -import type { Theme, SxProps } from '@mui/material/styles'; - +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import Container from '@mui/material/Container'; +import Grid from '@mui/material/Grid'; +import type { SxProps, Theme } from '@mui/material/styles'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Typography from '@mui/material/Typography'; import { useTabs } from 'minimal-shared/hooks'; import { varAlpha } from 'minimal-shared/utils'; - -import Tab from '@mui/material/Tab'; -import Box from '@mui/material/Box'; -import Tabs from '@mui/material/Tabs'; -import Card from '@mui/material/Card'; -import Grid from '@mui/material/Grid'; -import Button from '@mui/material/Button'; -import Container from '@mui/material/Container'; -import Typography from '@mui/material/Typography'; - -import { paths } from 'src/routes/paths'; -import { RouterLink } from 'src/routes/components'; - -import { Iconify } from 'src/components/iconify'; -import { EmptyContent } from 'src/components/empty-content'; +import { useTranslation } from 'react-i18next'; import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs'; - -import { CartIcon } from '../cart-icon'; +import { EmptyContent } from 'src/components/empty-content'; +import { Iconify } from 'src/components/iconify'; +import { RouterLink } from 'src/routes/components'; +import { paths } from 'src/routes/paths'; +import type { IProductItem } from 'src/types/product'; import { useCheckoutContext } from '../../checkout/context'; -import { ProductDetailsSkeleton } from '../product-skeleton'; -import { ProductDetailsReview } from '../product-details-review'; -import { ProductDetailsSummary } from '../product-details-summary'; +import { CartIcon } from '../cart-icon'; import { ProductDetailsCarousel } from '../product-details-carousel'; import { ProductDetailsDescription } from '../product-details-description'; -import { useTranslation } from 'react-i18next'; +import { ProductDetailsReview } from '../product-details-review'; +import { ProductDetailsSummary } from '../product-details-summary'; +import { ProductDetailsSkeleton } from '../product-skeleton'; // ---------------------------------------------------------------------- diff --git a/03_source/frontend/src/types/product.ts b/03_source/frontend/src/types/product.ts index 88753bc..60c3569 100644 --- a/03_source/frontend/src/types/product.ts +++ b/03_source/frontend/src/types/product.ts @@ -29,41 +29,32 @@ export type IProductReview = { export type IProductItem = { id: string; - sku: string; - name: string; - code: string; - price: number; - taxes: number; - tags: string[]; - sizes: string[]; - publish: string; - gender: string[]; - coverUrl: string; - images: string[]; - colors: string[]; - quantity: number; - category: string; + createdAt: IDateValue; + // available: number; - totalSold: number; + category: string; + code: string; + colors: string[]; + coverUrl: string; description: string; + gender: string[]; + images: string[]; + inventoryType: string; + name: string; + newLabel: { content: string; enabled: boolean }; + price: number; + priceSale: number | null; + publish: string; + quantity: number; + ratings: { name: string; starCount: number; reviewCount: number }[]; + reviews: IProductReview[]; + saleLabel: { content: string; enabled: boolean }; + sizes: string[]; + sku: string; + subDescription: string; + tags: string[]; + taxes: number; totalRatings: number; totalReviews: number; - createdAt: IDateValue; - inventoryType: string; - subDescription: string; - priceSale: number | null; - reviews: IProductReview[]; - newLabel: { - content: string; - enabled: boolean; - }; - saleLabel: { - content: string; - enabled: boolean; - }; - ratings: { - name: string; - starCount: number; - reviewCount: number; - }[]; + totalSold: number; };