"feat: enhance order management with new APIs and schema changes"

This commit is contained in:
louiscklaw
2025-05-30 11:40:25 +08:00
parent 834f58bde1
commit 5a707427c6
32 changed files with 1004 additions and 122 deletions

View File

@@ -27,7 +27,7 @@
"unseed": "tsx ./prisma/unseed.ts", "unseed": "tsx ./prisma/unseed.ts",
"db:generate": "prisma generate", "db:generate": "prisma generate",
"db:push": "prisma db push --force-reset", "db:push": "prisma db push --force-reset",
"db:push:w": "npx nodemon --delay 5 --ext \"ts,tsx,prisma\" --exec \"yarn db:push && yarn seed && yarn db:studio\"", "db:push:w": "npx nodemon --delay 5 --ext \"ts,tsx,prisma\" --exec \"yarn db:push && yarn seed\"",
"db:studio": "prisma studio" "db:studio": "prisma studio"
}, },
"engines": { "engines": {

View File

@@ -266,85 +266,85 @@ model Mail {
// attachments MailAttachment[] // attachments MailAttachment[]
} }
model OrderHistory { // model OrderHistory {
id Int @id @default(autoincrement()) // id Int @id @default(autoincrement())
createdAt DateTime @default(now()) // createdAt DateTime @default(now())
updatedAt DateTime @updatedAt // updatedAt DateTime @updatedAt
// // //
orderTime DateTime // orderTime DateTime @default(now())
paymentTime DateTime // paymentTime DateTime
deliveryTime DateTime // deliveryTime DateTime
completionTime DateTime // completionTime DateTime
timeline Json[] // timeline Json[]
OrderItem OrderItem? @relation(fields: [orderItemId], references: [id]) // OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
orderItemId Int? // orderItemId Int?
} // }
model OrderShippingAddress { // model OrderShippingAddress {
id Int @id @default(autoincrement()) // id Int @id @default(autoincrement())
createdAt DateTime @default(now()) // createdAt DateTime @default(now())
updatedAt DateTime @updatedAt // updatedAt DateTime @updatedAt
// // //
fullAddress String // fullAddress String
phoneNumber String // phoneNumber String
OrderItem OrderItem? @relation(fields: [orderItemId], references: [id]) // OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
orderItemId Int? // orderItemId Int?
} // }
model OrderPayment { // model OrderPayment {
id Int @id @default(autoincrement()) // id Int @id @default(autoincrement())
createdAt DateTime @default(now()) // createdAt DateTime @default(now())
updatedAt DateTime @updatedAt // updatedAt DateTime @updatedAt
// // //
cardType String // cardType String
cardNumber String // cardNumber String
OrderItem OrderItem? @relation(fields: [orderItemId], references: [id]) // OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
orderItemId Int? // orderItemId Int?
} // }
model OrderDelivery { // model OrderDelivery {
id Int @id @default(autoincrement()) // id Int @id @default(autoincrement())
createdAt DateTime @default(now()) // createdAt DateTime @default(now())
updatedAt DateTime @updatedAt // updatedAt DateTime @updatedAt
// // //
shipBy String // shipBy String
speedy String // speedy String
trackingNumber String // trackingNumber String
OrderItem OrderItem? @relation(fields: [orderItemId], references: [id]) // OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
orderItemId Int? // orderItemId Int?
} // }
model OrderCustomer { // model OrderCustomer {
id Int @id @default(autoincrement()) // id Int @id @default(autoincrement())
createdAt DateTime @default(now()) // createdAt DateTime @default(now())
updatedAt DateTime @updatedAt // updatedAt DateTime @updatedAt
// // //
name String // name String
email String // email String
avatarUrl String // avatarUrl String
ipAddress String // ipAddress String
OrderItem OrderItem? @relation(fields: [orderItemId], references: [id]) // OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
orderItemId Int? // orderItemId Int?
} // }
model OrderProductItem { // model OrderProductItem {
id Int @id @default(autoincrement()) // id Int @id @default(autoincrement())
createdAt DateTime @default(now()) // createdAt DateTime @default(now())
updatedAt DateTime @updatedAt // updatedAt DateTime @updatedAt
// // //
sku String // sku String
name String // name String
price Float // price Float
coverUrl String // coverUrl String
quantity Float // quantity Float
OrderItem OrderItem? @relation(fields: [orderItemId], references: [id]) // OrderItem OrderItem? @relation(fields: [orderItemId], references: [id])
orderItemId Int? // orderItemId Int?
} // }
model OrderItem { model OrderItem {
id Int @id @default(autoincrement()) id String @id @default(uuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
// //
taxes Float taxes Float
status String status String
@@ -354,12 +354,18 @@ model OrderItem {
orderNumber String orderNumber String
totalAmount Float totalAmount Float
totalQuantity Float totalQuantity Float
history OrderHistory[] history Json
payment OrderPayment[] payment Json
customer OrderCustomer[] customer Json
delivery OrderDelivery[] delivery Json
items OrderProductItem[] items Json[]
shippingAddress OrderShippingAddress[] shippingAddress Json
// OrderProductItem OrderProductItem[]
// OrderHistory OrderHistory[]
// OrderDelivery OrderDelivery[]
// OrderCustomer OrderCustomer[]
// OrderPayment OrderPayment[]
// OrderShippingAddress OrderShippingAddress[]
} }
// src/types/tour.ts // src/types/tour.ts

View File

@@ -23,6 +23,8 @@ import { ProductReview } from './seeds/productReview';
import { ProductItem } from './seeds/productItem'; import { ProductItem } from './seeds/productItem';
import { FileStore } from './seeds/fileStore'; import { FileStore } from './seeds/fileStore';
import { userItemSeed } from './seeds/userItem'; import { userItemSeed } from './seeds/userItem';
import { orderItemSeed } from './seeds/orderItem';
// //
// import { Blog } from './seeds/blog'; // import { Blog } from './seeds/blog';
// import { Mail } from './seeds/mail'; // import { Mail } from './seeds/mail';
@@ -39,6 +41,7 @@ import { userItemSeed } from './seeds/userItem';
await FileStore; await FileStore;
await ProductItem; await ProductItem;
await userItemSeed; await userItemSeed;
await orderItemSeed;
// await Blog; // await Blog;
// await Mail; // await Mail;
// await File; // await File;

View File

@@ -10,8 +10,8 @@ async function order() {
title: 'Single Party with Dating', title: 'Single Party with Dating',
order_time: new Date(), order_time: new Date(),
last_payment_date: new Date(), last_payment_date: new Date(),
status: 'Pending' status: 'Pending',
} },
}); });
} }

View File

@@ -0,0 +1,94 @@
import { PrismaClient } from '@prisma/client';
import { _mock } from './_mock';
const prisma = new PrismaClient();
const ITEMS = Array.from({ length: 3 }, (_, index) => ({
id: _mock.id(index),
sku: `16H9UR${index}`,
quantity: index + 1,
name: _mock.productName(index),
coverUrl: _mock.image.product(index),
price: _mock.number.price(index),
}));
async function orderItem() {
await prisma.orderItem.deleteMany({});
for (let index = 1; index < 20 + 1; index++) {
const shipping = 10;
const discount = 10;
const taxes = 10;
const items = (index % 2 && ITEMS.slice(0, 1)) || (index % 3 && ITEMS.slice(1, 3)) || ITEMS;
const totalQuantity = items.reduce((accumulator, item) => accumulator + item.quantity, 0);
const subtotal = items.reduce((accumulator, item) => accumulator + item.price * item.quantity, 0);
const totalAmount = subtotal - shipping - discount + taxes;
const customer = {
id: _mock.id(index),
name: _mock.fullName(index),
email: _mock.email(index),
avatarUrl: _mock.image.avatar(index),
ipAddress: '192.158.1.38',
};
const delivery = { shipBy: 'DHL', speedy: 'Standard', trackingNumber: 'SPX037739199373' };
const history = {
orderTime: _mock.time(1),
paymentTime: _mock.time(2),
deliveryTime: _mock.time(3),
completionTime: _mock.time(4),
timeline: [
{ title: 'Delivery successful', time: _mock.time(1) },
{ title: 'Transporting to [2]', time: _mock.time(2) },
{ title: 'Transporting to [1]', time: _mock.time(3) },
{ title: 'The shipping unit has picked up the goods', time: _mock.time(4) },
{ title: 'Order has been created', time: _mock.time(5) },
],
};
const temp = await prisma.orderItem.upsert({
where: { id: index.toString() },
update: {},
create: {
id: index.toString(),
orderNumber: `#601${index}`,
taxes,
items,
history,
subtotal: items.reduce((accumulator, item) => accumulator + item.price * item.quantity, 0),
shipping,
discount,
customer,
delivery,
totalAmount,
totalQuantity,
shippingAddress: {
fullAddress: '19034 Verna Unions Apt. 164 - Honolulu, RI / 87535',
phoneNumber: '365-374-4961',
},
payment: {
//
cardType: 'mastercard',
cardNumber: '4111 1111 1111 1111',
},
status: (index % 2 && 'completed') || (index % 3 && 'pending') || (index % 4 && 'cancelled') || 'refunded',
},
});
}
console.log('seed orderItem done');
}
const orderItemSeed = orderItem()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
export { orderItemSeed };

View File

@@ -0,0 +1,98 @@
// 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';
// ----------------------------------------------------------------------
/** **************************************
* PUT - change order status
*************************************** */
export async function PUT(req: NextRequest) {
// logger('[Order] list', products.length);
const { searchParams } = req.nextUrl;
const orderId = searchParams.get('orderId');
// RULES: orderId must exist
if (!orderId) {
return response({ message: 'Order ID is required!' }, STATUS.BAD_REQUEST);
}
const { data } = await req.json();
try {
const order = await prisma.orderItem.updateMany({
where: { id: orderId },
data: { status: data.status },
});
return response({ order }, STATUS.OK);
} catch (error) {
console.log({ data });
return handleError('Order - 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[];
};

View File

@@ -0,0 +1,9 @@
###
PUT http://localhost:7272/api/order/changeStatus?orderId=1
content-type: application/json
{
"data":{"status": "helloworld"}
}

View File

@@ -0,0 +1,53 @@
// src/app/api/user/createUser/route.ts
//
// PURPOSE:
// create user to db
//
// RULES:
// T.B.A.
//
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/**
***************************************
* POST - create User
***************************************
*/
export async function POST(req: NextRequest) {
// logger('[User] list', users.length);
const { data } = await req.json();
const createForm: CreateUserData = data as unknown as CreateUserData;
try {
const user = await prisma.userItem.create({ data: createForm });
return response({ user }, STATUS.OK);
} catch (error) {
return handleError('User - Create', error);
}
}
type CreateUserData = {
name: string;
city: string;
role: string;
email: string;
state: string;
status: string;
address: string;
country: string;
zipCode: string;
company: string;
avatarUrl: string;
phoneNumber: string;
isVerified: boolean;
//
username: string;
password: string;
};

View File

@@ -0,0 +1,4 @@
###
POST http://localhost:7272/api/user/createUser

View File

@@ -0,0 +1,47 @@
// src/app/api/product/deleteUser/route.ts
//
// PURPOSE:
// delete product from db by id
//
// RULES:
// T.B.A.
import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/** **************************************
* handle Delete Users
*************************************** */
export async function DELETE(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
// RULES: userId must exist
const userId = searchParams.get('userId');
if (!userId) {
return response({ message: 'User ID is required!' }, STATUS.BAD_REQUEST);
}
// NOTE: userId confirmed exist, run below
const user = await prisma.userItem.delete({
//
where: { id: userId },
});
if (!user) {
return response({ message: 'User not found!' }, STATUS.NOT_FOUND);
}
logger('[User] details', user.id);
return response({ user }, STATUS.OK);
} catch (error) {
return handleError('User - Get details', error);
}
}

View File

@@ -0,0 +1,3 @@
###
DELETE http://localhost:7272/api/user/deleteUser?userId=3f431e6f-ad05-4d60-9c25-6a7e92a954ad

View File

@@ -0,0 +1,47 @@
// src/app/api/order/details/route.ts
//
// PURPOSE:
// read order from db by id
//
// RULES:
// T.B.A.
import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/** **************************************
* GET Order detail
*************************************** */
export async function GET(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
// RULES: orderId must exist
const orderId = searchParams.get('orderId');
if (!orderId) {
return response({ message: 'orderId is required!' }, STATUS.BAD_REQUEST);
}
// NOTE: orderId confirmed exist, run below
const order = await prisma.orderItem.findFirst({
// include: { reviews: true },
where: { id: orderId.toString() },
});
if (!order) {
return response({ message: 'Order not found!' }, STATUS.NOT_FOUND);
}
logger('[Order] details', order.id);
return response({ order }, STATUS.OK);
} catch (error) {
return handleError('Product - Get details', error);
}
}

View File

@@ -0,0 +1,4 @@
###
GET http://localhost:7272/api/order/details?orderId=1

View File

@@ -0,0 +1,30 @@
// src/app/api/product/image/upload/route.ts
//
// PURPOSE:
// handle upload product image
//
// 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 POST(req: NextRequest) {
try {
const { data } = await req.json();
console.log('helloworld');
return response({ hello: 'world' }, STATUS.OK);
} catch (error) {
console.log({ hello: 'world' });
return handleError('Product - store product image', error);
}
}

View File

@@ -0,0 +1,22 @@
// src/app/api/order/list/route.ts
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/** **************************************
* GET - OrderItem
*************************************** */
export async function GET() {
try {
const orders = await prisma.orderItem.findMany();
logger('[Order] list', orders.length);
return response({ orders }, STATUS.OK);
} catch (error) {
return handleError('OrderItem - Get list', error);
}
}

View File

@@ -0,0 +1,3 @@
###
GET http://localhost:7272/api/order/list

View File

@@ -0,0 +1,115 @@
// 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 POST(req: NextRequest) {
// logger('[Product] list', products.length);
const { searchParams } = req.nextUrl;
const userId = searchParams.get('userId');
// RULES: userId must exist
if (!userId) {
return response({ message: 'Product ID is required!' }, STATUS.BAD_REQUEST);
}
const { data } = await req.json();
try {
const user = await prisma.userItem.updateMany({
where: { id: userId },
data: {
status: data.status,
avatarUrl: data.avatarUrl,
isVerified: data.isVerified,
name: data.name,
email: data.email,
phoneNumber: data.phoneNumber,
country: data.country,
state: data.state,
city: data.city,
address: data.address,
zipCode: data.zipCode,
company: data.company,
role: data.role,
//
username: data.username,
password: data.password,
},
});
return response({ user }, STATUS.OK);
} catch (error) {
console.log({ hello: 'world', data });
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[];
};

View File

@@ -0,0 +1,3 @@
###
POST http://localhost:7272/api/user/list

View File

@@ -0,0 +1,37 @@
import type { NextRequest } from 'next/server';
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 - Search products
*************************************** */
export async function GET(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
const query = searchParams.get('query')?.trim().toLowerCase();
if (!query) {
return response({ results: [] }, STATUS.OK);
}
const products = _products();
// Accept search by name or sku
const results = products.filter(
({ name, sku }) => name.toLowerCase().includes(query) || sku?.toLowerCase().includes(query)
);
logger('[Product] search-results', results.length);
return response({ results }, STATUS.OK);
} catch (error) {
return handleError('Product - Get search', error);
}
}

View File

@@ -75,7 +75,11 @@ export const _orders = Array.from({ length: 20 }, (_, index) => {
fullAddress: '19034 Verna Unions Apt. 164 - Honolulu, RI / 87535', fullAddress: '19034 Verna Unions Apt. 164 - Honolulu, RI / 87535',
phoneNumber: '365-374-4961', phoneNumber: '365-374-4961',
}, },
payment: { cardType: 'mastercard', cardNumber: '**** **** **** 5678' }, payment: {
//
cardType: 'mastercard',
cardNumber: '**** **** **** 5678',
},
status: status:
(index % 2 && 'completed') || (index % 2 && 'completed') ||
(index % 3 && 'pending') || (index % 3 && 'pending') ||

View File

@@ -0,0 +1,226 @@
// src/actions/order.ts
import { useMemo } from 'react';
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
import type { IProductItem } from 'src/types/product';
import type { IOrderItem } from 'src/types/order';
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';
// ----------------------------------------------------------------------
const swrOptions: SWRConfiguration = {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
};
// ----------------------------------------------------------------------
type OrdersData = {
orders: IOrderItem[];
};
export function useGetOrders() {
const url = endpoints.order.list;
const { data, isLoading, error, isValidating, mutate } = useSWR<OrdersData>(
url,
fetcher,
swrOptions
);
const memoizedValue = useMemo(
() => ({
orders: data?.orders || [],
ordersLoading: isLoading,
ordersError: error,
ordersValidating: isValidating,
ordersEmpty: !isLoading && !isValidating && !data?.orders.length,
mutate,
}),
[data?.orders, error, isLoading, isValidating, mutate]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
type OrderData = {
order: IOrderItem;
};
export function useGetOrder(orderId: string) {
const url = orderId ? [endpoints.order.details, { params: { orderId } }] : '';
const { data, isLoading, error, isValidating } = useSWR<OrderData>(url, fetcher, swrOptions);
const memoizedValue = useMemo(
() => ({
order: data?.order,
orderLoading: isLoading,
orderError: error,
orderValidating: isValidating,
}),
[data?.order, error, isLoading, isValidating]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
type SearchResultsData = {
results: IProductItem[];
};
export function useSearchProducts(query: string) {
const url = query ? [endpoints.product.search, { params: { query } }] : '';
const { data, isLoading, error, isValidating } = useSWR<SearchResultsData>(url, fetcher, {
...swrOptions,
keepPreviousData: true,
});
const memoizedValue = useMemo(
() => ({
searchResults: data?.results || [],
searchLoading: isLoading,
searchError: error,
searchValidating: isValidating,
searchEmpty: !isLoading && !isValidating && !data?.results.length,
}),
[data?.results, error, isLoading, isValidating]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
type SaveOrderData = {
name: string;
city: string;
role: string;
email: string;
state: string;
status: string;
address: string;
country: string;
zipCode: string;
company: string;
avatarUrl: string;
phoneNumber: string;
isVerified: boolean;
//
ordername: string;
password: string;
};
export async function saveOrder(orderId: string, saveOrderData: SaveOrderData) {
// const url = orderId ? [endpoints.order.details, { params: { orderId } }] : '';
const res = await axiosInstance.post(
//
`http://localhost:7272/api/order/saveOrder?orderId=${orderId}`,
{
data: saveOrderData,
}
);
return res;
}
export async function uploadOrderImage(saveOrderData: SaveOrderData) {
console.log('uploadOrderImage ?');
// const url = orderId ? [endpoints.order.details, { params: { orderId } }] : '';
const res = await axiosInstance.get('http://localhost:7272/api/product/helloworld');
return res;
}
// ----------------------------------------------------------------------
type CreateOrderData = {
name: string;
city: string;
role: string;
email: string;
state: string;
status: string;
address: string;
country: string;
zipCode: string;
company: string;
avatarUrl: string;
phoneNumber: string;
isVerified: boolean;
//
ordername: string;
password: string;
};
export async function createOrder(createOrderData: CreateOrderData) {
console.log('create product ?');
// const url = productId ? [endpoints.product.details, { params: { productId } }] : '';
const res = await axiosInstance.post('http://localhost:7272/api/order/createOrder', {
data: createOrderData,
});
return res;
}
// ----------------------------------------------------------------------
type DeleteOrderResponse = {
success: boolean;
message?: string;
};
export async function deleteOrder(orderId: string): Promise<DeleteOrderResponse> {
const url = `http://localhost:7272/api/order/deleteOrder?orderId=${orderId}`;
try {
const res = await axiosInstance.delete(url);
return {
success: true,
message: 'Order deleted successfully',
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Failed to delete product',
};
}
}
// ----------------------------------------------------------------------
type ChangeStatusResponse = {
success: boolean;
message?: string;
};
export async function changeStatus(
orderId: string,
newOrderStatus: string
): Promise<ChangeStatusResponse> {
const url = endpoints.order.changeStatus(orderId);
try {
const res = await axiosInstance.put(url, { data: { status: newOrderStatus } });
return {
success: true,
message: 'status updated successfully',
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Failed to delete product',
};
}
}

View File

@@ -1,3 +1,4 @@
// src/actions/product.ts
import { useMemo } from 'react'; import { useMemo } from 'react';
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios'; import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
import type { IProductItem } from 'src/types/product'; import type { IProductItem } from 'src/types/product';

View File

@@ -1,7 +1,9 @@
// src/pages/dashboard/order/details.tsx
import { useParams } from 'src/routes/hooks'; import { useParams } from 'src/routes/hooks';
import { _orders } from 'src/_mock/_order'; import { _orders } from 'src/_mock/_order';
import { CONFIG } from 'src/global-config'; import { CONFIG } from 'src/global-config';
import { useGetOrder } from 'src/actions/order';
import { OrderDetailsView } from 'src/sections/order/view'; import { OrderDetailsView } from 'src/sections/order/view';
@@ -12,13 +14,18 @@ const metadata = { title: `Order details | Dashboard - ${CONFIG.appName}` };
export default function Page() { export default function Page() {
const { id = '' } = useParams(); const { id = '' } = useParams();
const currentOrder = _orders.find((order) => order.id === id); // const currentOrder = _orders.find((order) => order.id === id);
// TODO: error handling
const { order, orderLoading, orderError } = useGetOrder(id);
if (!order) return <>loading</>;
if (orderLoading) return <>loading</>;
return ( return (
<> <>
<title>{metadata.title}</title> <title>{metadata.title}</title>
<OrderDetailsView order={currentOrder} /> <OrderDetailsView order={order} />
</> </>
); );
} }

View File

@@ -1,3 +1,5 @@
// src/pages/dashboard/product/details.tsx
import { useParams } from 'src/routes/hooks'; import { useParams } from 'src/routes/hooks';
import { CONFIG } from 'src/global-config'; import { CONFIG } from 'src/global-config';

View File

@@ -1,3 +1,4 @@
// src/sections/order/order-details-history.tsx
import type { IOrderHistory } from 'src/types/order'; import type { IOrderHistory } from 'src/types/order';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
@@ -13,6 +14,7 @@ import TimelineConnector from '@mui/lab/TimelineConnector';
import TimelineItem, { timelineItemClasses } from '@mui/lab/TimelineItem'; import TimelineItem, { timelineItemClasses } from '@mui/lab/TimelineItem';
import { fDateTime } from 'src/utils/format-time'; import { fDateTime } from 'src/utils/format-time';
import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -21,6 +23,8 @@ type Props = {
}; };
export function OrderDetailsHistory({ history }: Props) { export function OrderDetailsHistory({ history }: Props) {
const { t } = useTranslation();
const renderSummary = () => ( const renderSummary = () => (
<Paper <Paper
variant="outlined" variant="outlined"
@@ -37,22 +41,22 @@ export function OrderDetailsHistory({ history }: Props) {
}} }}
> >
<div> <div>
<Box sx={{ mb: 0.5, color: 'text.disabled' }}>Order time</Box> <Box sx={{ mb: 0.5, color: 'text.disabled' }}>{t('Order time')}</Box>
{fDateTime(history?.orderTime)} {fDateTime(history?.orderTime)}
</div> </div>
<div> <div>
<Box sx={{ mb: 0.5, color: 'text.disabled' }}>Payment time</Box> <Box sx={{ mb: 0.5, color: 'text.disabled' }}>{t('Payment time')}</Box>
{fDateTime(history?.orderTime)} {fDateTime(history?.orderTime)}
</div> </div>
<div> <div>
<Box sx={{ mb: 0.5, color: 'text.disabled' }}>Delivery time for the carrier</Box> <Box sx={{ mb: 0.5, color: 'text.disabled' }}>{t('Delivery time for the carrier')}</Box>
{fDateTime(history?.orderTime)} {fDateTime(history?.orderTime)}
</div> </div>
<div> <div>
<Box sx={{ mb: 0.5, color: 'text.disabled' }}>Completion time</Box> <Box sx={{ mb: 0.5, color: 'text.disabled' }}>{t('Completion time')}</Box>
{fDateTime(history?.orderTime)} {fDateTime(history?.orderTime)}
</div> </div>
</Paper> </Paper>

View File

@@ -12,6 +12,7 @@ import { fCurrency } from 'src/utils/format-number';
import { Iconify } from 'src/components/iconify'; import { Iconify } from 'src/components/iconify';
import { Scrollbar } from 'src/components/scrollbar'; import { Scrollbar } from 'src/components/scrollbar';
import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -34,6 +35,7 @@ export function OrderDetailsItems({
totalAmount, totalAmount,
...other ...other
}: Props) { }: Props) {
const { t } = useTranslation();
const renderTotal = () => ( const renderTotal = () => (
<Box <Box
sx={{ sx={{
@@ -47,32 +49,32 @@ export function OrderDetailsItems({
}} }}
> >
<Box sx={{ display: 'flex' }}> <Box sx={{ display: 'flex' }}>
<Box sx={{ color: 'text.secondary' }}>Subtotal</Box> <Box sx={{ color: 'text.secondary' }}>{t('Subtotal')}</Box>
<Box sx={{ width: 160, typography: 'subtitle2' }}>{fCurrency(subtotal) || '-'}</Box> <Box sx={{ width: 160, typography: 'subtitle2' }}>{fCurrency(subtotal) || '-'}</Box>
</Box> </Box>
<Box sx={{ display: 'flex' }}> <Box sx={{ display: 'flex' }}>
<Box sx={{ color: 'text.secondary' }}>Shipping</Box> <Box sx={{ color: 'text.secondary' }}>{t('Shipping')}</Box>
<Box sx={{ width: 160, ...(shipping && { color: 'error.main' }) }}> <Box sx={{ width: 160, ...(shipping && { color: 'error.main' }) }}>
{shipping ? `- ${fCurrency(shipping)}` : '-'} {shipping ? `- ${fCurrency(shipping)}` : '-'}
</Box> </Box>
</Box> </Box>
<Box sx={{ display: 'flex' }}> <Box sx={{ display: 'flex' }}>
<Box sx={{ color: 'text.secondary' }}>Discount</Box> <Box sx={{ color: 'text.secondary' }}>{t('Discount')}</Box>
<Box sx={{ width: 160, ...(discount && { color: 'error.main' }) }}> <Box sx={{ width: 160, ...(discount && { color: 'error.main' }) }}>
{discount ? `- ${fCurrency(discount)}` : '-'} {discount ? `- ${fCurrency(discount)}` : '-'}
</Box> </Box>
</Box> </Box>
<Box sx={{ display: 'flex' }}> <Box sx={{ display: 'flex' }}>
<Box sx={{ color: 'text.secondary' }}>Taxes</Box> <Box sx={{ color: 'text.secondary' }}>{t('Taxes')}</Box>
<Box sx={{ width: 160 }}>{taxes ? fCurrency(taxes) : '-'}</Box> <Box sx={{ width: 160 }}>{taxes ? fCurrency(taxes) : '-'}</Box>
</Box> </Box>
<Box sx={{ display: 'flex', typography: 'subtitle1' }}> <Box sx={{ display: 'flex', typography: 'subtitle1' }}>
<div>Total</div> <div>{t('Total')}</div>
<Box sx={{ width: 160 }}>{fCurrency(totalAmount) || '-'}</Box> <Box sx={{ width: 160 }}>{fCurrency(totalAmount) || '-'}</Box>
</Box> </Box>
</Box> </Box>

View File

@@ -1,3 +1,5 @@
// src/sections/order/order-details-toolbar.tsx
import type { IDateValue } from 'src/types/common'; import type { IDateValue } from 'src/types/common';
import { usePopover } from 'minimal-shared/hooks'; import { usePopover } from 'minimal-shared/hooks';
@@ -17,6 +19,7 @@ import { fDateTime } from 'src/utils/format-time';
import { Label } from 'src/components/label'; import { Label } from 'src/components/label';
import { Iconify } from 'src/components/iconify'; import { Iconify } from 'src/components/iconify';
import { CustomPopover } from 'src/components/custom-popover'; import { CustomPopover } from 'src/components/custom-popover';
import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -37,6 +40,7 @@ export function OrderDetailsToolbar({
statusOptions, statusOptions,
onChangeStatus, onChangeStatus,
}: Props) { }: Props) {
const { t } = useTranslation();
const menuActions = usePopover(); const menuActions = usePopover();
const renderMenuActions = () => ( const renderMenuActions = () => (
@@ -56,7 +60,7 @@ export function OrderDetailsToolbar({
onChangeStatus(option.value); onChangeStatus(option.value);
}} }}
> >
{option.label} {t(option.label)}
</MenuItem> </MenuItem>
))} ))}
</MenuList> </MenuList>
@@ -124,11 +128,11 @@ export function OrderDetailsToolbar({
variant="outlined" variant="outlined"
startIcon={<Iconify icon="solar:printer-minimalistic-bold" />} startIcon={<Iconify icon="solar:printer-minimalistic-bold" />}
> >
Print {t('Print (not implemented)')}
</Button> </Button>
<Button color="inherit" variant="contained" startIcon={<Iconify icon="solar:pen-bold" />}> <Button color="inherit" variant="contained" startIcon={<Iconify icon="solar:pen-bold" />}>
Edit {t('Edit')}
</Button> </Button>
</Box> </Box>
</Box> </Box>

View File

@@ -1,6 +1,8 @@
// src/sections/order/view/order-details-view.tsx
import type { IOrderItem } from 'src/types/order'; import type { IOrderItem } from 'src/types/order';
import { useState, useCallback } from 'react'; import { useState, useCallback, useEffect } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
@@ -19,19 +21,39 @@ import { OrderDetailsPayment } from '../order-details-payment';
import { OrderDetailsCustomer } from '../order-details-customer'; import { OrderDetailsCustomer } from '../order-details-customer';
import { OrderDetailsDelivery } from '../order-details-delivery'; import { OrderDetailsDelivery } from '../order-details-delivery';
import { OrderDetailsShipping } from '../order-details-shipping'; import { OrderDetailsShipping } from '../order-details-shipping';
import { useTranslate } from 'src/locales';
import { useTranslation } from 'react-i18next';
import { changeStatus } from 'src/actions/order';
import { toast } from 'sonner';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
type Props = { type Props = {
order?: IOrderItem; order: IOrderItem;
}; };
export function OrderDetailsView({ order }: Props) { export function OrderDetailsView({ order }: Props) {
const [status, setStatus] = useState(order?.status); const { t } = useTranslation();
const handleChangeStatus = useCallback((newValue: string) => { const [status, setStatus] = useState(order.status);
setStatus(newValue);
}, []); const handleChangeStatus = useCallback(
async (newValue: string) => {
setStatus(newValue);
// change order status
try {
if (order?.id) {
await changeStatus(order.id, newValue);
toast.success('order status updated');
}
} catch (error) {
console.error(error);
toast.warning('error during update order status');
}
},
[order.id]
);
return ( return (
<DashboardContent> <DashboardContent>
@@ -47,7 +69,12 @@ export function OrderDetailsView({ order }: Props) {
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid size={{ xs: 12, md: 8 }}> <Grid size={{ xs: 12, md: 8 }}>
<Box <Box
sx={{ gap: 3, display: 'flex', flexDirection: { xs: 'column-reverse', md: 'column' } }} sx={{
//
gap: 3,
display: 'flex',
flexDirection: { xs: 'column-reverse', md: 'column' },
}}
> >
<OrderDetailsItems <OrderDetailsItems
items={order?.items} items={order?.items}

View File

@@ -3,7 +3,7 @@
import type { TableHeadCellProps } from 'src/components/table'; import type { TableHeadCellProps } from 'src/components/table';
import type { IOrderItem, IOrderTableFilters } from 'src/types/order'; import type { IOrderItem, IOrderTableFilters } from 'src/types/order';
import { useState, useCallback } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { varAlpha } from 'minimal-shared/utils'; import { varAlpha } from 'minimal-shared/utils';
import { useBoolean, useSetState } from 'minimal-shared/hooks'; import { useBoolean, useSetState } from 'minimal-shared/hooks';
@@ -46,6 +46,8 @@ import { OrderTableRow } from '../order-table-row';
import { OrderTableToolbar } from '../order-table-toolbar'; import { OrderTableToolbar } from '../order-table-toolbar';
import { OrderTableFiltersResult } from '../order-table-filters-result'; import { OrderTableFiltersResult } from '../order-table-filters-result';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRouter } from 'src/routes/hooks';
import { deleteOrder, useGetOrders } from 'src/actions/order';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -55,12 +57,7 @@ const STATUS_OPTIONS = [{ value: 'all', label: 'All' }, ...ORDER_STATUS_OPTIONS]
export function OrderListView() { export function OrderListView() {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter();
const table = useTable({ defaultOrderBy: 'orderNumber' });
const confirmDialog = useBoolean();
const [tableData, setTableData] = useState<IOrderItem[]>(_orders);
const TABLE_HEAD: TableHeadCellProps[] = [ const TABLE_HEAD: TableHeadCellProps[] = [
{ id: 'orderNumber', label: t('Order'), width: 88 }, { id: 'orderNumber', label: t('Order'), width: 88 },
@@ -72,6 +69,18 @@ export function OrderListView() {
{ id: '', width: 88 }, { id: '', width: 88 },
]; ];
const { orders, mutate, ordersLoading } = useGetOrders();
const table = useTable({ defaultOrderBy: 'orderNumber' });
const confirmDialog = useBoolean();
const [tableData, setTableData] = useState<IOrderItem[]>([]);
useEffect(() => {
setTableData(orders);
}, [orders]);
const filters = useSetState<IOrderTableFilters>({ const filters = useSetState<IOrderTableFilters>({
name: '', name: '',
status: 'all', status: 'all',
@@ -99,16 +108,23 @@ export function OrderListView() {
const notFound = (!dataFiltered.length && canReset) || !dataFiltered.length; const notFound = (!dataFiltered.length && canReset) || !dataFiltered.length;
const handleDeleteRow = useCallback( const handleDeleteRow = useCallback(
(id: string) => { async (id: string) => {
const deleteRow = tableData.filter((row) => row.id !== id); // const deleteRow = tableData.filter((row) => row.id !== id);
toast.success('Delete success!'); try {
await deleteOrder(id);
toast.success('Delete success!');
mutate();
} catch (error) {
console.error(error);
toast.error('Delete failed!');
}
setTableData(deleteRow); // toast.success('Delete success!');
// setTableData(deleteRow);
table.onUpdatePageDeleteRow(dataInPage.length); // table.onUpdatePageDeleteRow(dataInPage.length);
}, },
[dataInPage.length, table, tableData] [table, tableData, mutate]
); );
const handleDeleteRows = useCallback(() => { const handleDeleteRows = useCallback(() => {
@@ -133,7 +149,7 @@ export function OrderListView() {
<ConfirmDialog <ConfirmDialog
open={confirmDialog.value} open={confirmDialog.value}
onClose={confirmDialog.onFalse} onClose={confirmDialog.onFalse}
title="Delete" title={t('Delete')}
content={ content={
<> <>
Are you sure want to delete <strong> {table.selected.length} </strong> items? Are you sure want to delete <strong> {table.selected.length} </strong> items?
@@ -145,7 +161,7 @@ export function OrderListView() {
color="error" color="error"
onClick={() => { onClick={() => {
handleDeleteRows(); handleDeleteRows();
confirmDialog.onFalse(); // confirmDialog.onFalse();
}} }}
> >
{t('Delete')} {t('Delete')}
@@ -154,12 +170,20 @@ export function OrderListView() {
/> />
); );
useEffect(() => {
mutate();
}, []);
if (!orders) return <>loading</>;
if (ordersLoading) return <>loading</>;
return ( return (
<> <>
<DashboardContent> <DashboardContent>
<CustomBreadcrumbs <CustomBreadcrumbs
heading="List" heading="List"
links={[ links={[
//
{ name: t('Dashboard'), href: paths.dashboard.root }, { name: t('Dashboard'), href: paths.dashboard.root },
{ name: t('Order'), href: paths.dashboard.order.root }, { name: t('Order'), href: paths.dashboard.order.root },
{ name: t('List') }, { name: t('List') },

View File

@@ -1,3 +1,5 @@
// src/sections/product/view/product-details-view.tsx
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';

View File

@@ -252,7 +252,7 @@ export function UserListView() {
) )
} }
action={ action={
<Tooltip title="Delete"> <Tooltip title={t('Delete')}>
<IconButton color="primary" onClick={confirmDialog.onTrue}> <IconButton color="primary" onClick={confirmDialog.onTrue}>
<Iconify icon="solar:trash-bin-trash-bold" /> <Iconify icon="solar:trash-bin-trash-bold" />
</IconButton> </IconButton>

View File

@@ -52,6 +52,8 @@ export type IOrderProductItem = {
export type IOrderItem = { export type IOrderItem = {
id: string; id: string;
createdAt: IDateValue;
//
taxes: number; taxes: number;
status: string; status: string;
shipping: number; shipping: number;
@@ -60,8 +62,7 @@ export type IOrderItem = {
orderNumber: string; orderNumber: string;
totalAmount: number; totalAmount: number;
totalQuantity: number; totalQuantity: number;
createdAt: IDateValue; history: IOrderHistory | undefined;
history: IOrderHistory;
payment: IOrderPayment; payment: IOrderPayment;
customer: IOrderCustomer; customer: IOrderCustomer;
delivery: IOrderDelivery; delivery: IOrderDelivery;