feat: add REQ0189 frontend party-order list and details pages with mock data and API integration
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
export const PRODUCT_GENDER_OPTIONS = [
|
||||
export const PARTY_EVENT_GENDER_OPTIONS = [
|
||||
{ label: 'Men', value: 'Men' },
|
||||
{ label: 'Women', value: 'Women' },
|
||||
{ label: 'Kids', value: 'Kids' },
|
||||
];
|
||||
|
||||
export const PRODUCT_CATEGORY_OPTIONS = ['Shose', 'Apparel', 'Accessories'];
|
||||
export const PARTY_EVENT_CATEGORY_OPTIONS = ['Shose', 'Apparel', 'Accessories'];
|
||||
|
||||
export const PRODUCT_RATING_OPTIONS = ['up4Star', 'up3Star', 'up2Star', 'up1Star'];
|
||||
export const PARTY_EVENT_RATING_OPTIONS = ['up4Star', 'up3Star', 'up2Star', 'up1Star'];
|
||||
|
||||
export const PRODUCT_COLOR_OPTIONS = [
|
||||
export const PARTY_EVENT_COLOR_OPTIONS = [
|
||||
'#FF4842',
|
||||
'#1890FF',
|
||||
'#FFC0CB',
|
||||
@@ -19,7 +19,7 @@ export const PRODUCT_COLOR_OPTIONS = [
|
||||
'#FFFFFF',
|
||||
];
|
||||
|
||||
export const PRODUCT_COLOR_NAME_OPTIONS = [
|
||||
export const PARTY_EVENT_COLOR_NAME_OPTIONS = [
|
||||
{ value: '#FF4842', label: 'Red' },
|
||||
{ value: '#1890FF', label: 'Blue' },
|
||||
{ value: '#FFC0CB', label: 'Pink' },
|
||||
@@ -30,7 +30,7 @@ export const PRODUCT_COLOR_NAME_OPTIONS = [
|
||||
{ value: '#FFFFFF', label: 'White' },
|
||||
];
|
||||
|
||||
export const PRODUCT_SIZE_OPTIONS = [
|
||||
export const PARTY_EVENT_SIZE_OPTIONS = [
|
||||
{ value: '7', label: '7' },
|
||||
{ value: '8', label: '8' },
|
||||
{ value: '8.5', label: '8.5' },
|
||||
@@ -44,26 +44,26 @@ export const PRODUCT_SIZE_OPTIONS = [
|
||||
{ value: '13', label: '13' },
|
||||
];
|
||||
|
||||
export const PRODUCT_STOCK_OPTIONS = [
|
||||
export const PARTY_EVENT_STOCK_OPTIONS = [
|
||||
{ value: 'in stock', label: 'In stock' },
|
||||
{ value: 'low stock', label: 'Low stock' },
|
||||
{ value: 'out of stock', label: 'Out of stock' },
|
||||
];
|
||||
|
||||
// not used due to i18n
|
||||
export const PRODUCT_PUBLISH_OPTIONS = [
|
||||
export const PARTY_EVENT_PUBLISH_OPTIONS = [
|
||||
{ value: 'published', label: 'Published' },
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
];
|
||||
|
||||
export const PRODUCT_SORT_OPTIONS = [
|
||||
export const PARTY_EVENT_SORT_OPTIONS = [
|
||||
{ value: 'featured', label: 'Featured' },
|
||||
{ value: 'newest', label: 'Newest' },
|
||||
{ value: 'priceDesc', label: 'Price: High - Low' },
|
||||
{ value: 'priceAsc', label: 'Price: Low - High' },
|
||||
];
|
||||
|
||||
export const PRODUCT_CATEGORY_GROUP_OPTIONS = [
|
||||
export const PARTY_EVENT_CATEGORY_GROUP_OPTIONS = [
|
||||
{ group: 'Clothing', classify: ['Shirts', 'T-shirts', 'Jeans', 'Leather', 'Accessories'] },
|
||||
{ group: 'Tailored', classify: ['Suits', 'Blazers', 'Trousers', 'Waistcoats', 'Apparel'] },
|
||||
{ group: 'Accessories', classify: ['Shoes', 'Backpacks and bags', 'Bracelets', 'Face masks'] },
|
||||
|
89
03_source/frontend/src/_mock/_party-order.ts
Normal file
89
03_source/frontend/src/_mock/_party-order.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { _mock } from './_mock';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export const PARTY_ORDER_STATUS_OPTIONS = [
|
||||
{ value: 'pending', label: 'Pending' },
|
||||
{ value: 'completed', label: 'Completed' },
|
||||
{ value: 'cancelled', label: 'Cancelled' },
|
||||
{ value: 'refunded', label: 'Refunded' },
|
||||
];
|
||||
|
||||
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),
|
||||
}));
|
||||
|
||||
export const _party_orders = Array.from({ length: 20 }, (_, 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) },
|
||||
],
|
||||
};
|
||||
|
||||
return {
|
||||
id: _mock.id(index),
|
||||
orderNumber: `#601${index}`,
|
||||
createdAt: _mock.time(index),
|
||||
taxes,
|
||||
items,
|
||||
history,
|
||||
subtotal,
|
||||
shipping,
|
||||
discount,
|
||||
customer,
|
||||
delivery,
|
||||
totalAmount,
|
||||
totalQuantity,
|
||||
shippingAddress: {
|
||||
fullAddress: '19034 Verna Unions Apt. 164 - Honolulu, RI / 87535',
|
||||
phoneNumber: '365-374-4961',
|
||||
},
|
||||
payment: {
|
||||
//
|
||||
cardType: 'mastercard',
|
||||
cardNumber: '**** **** **** 5678',
|
||||
},
|
||||
status:
|
||||
(index % 2 && 'completed') ||
|
||||
(index % 3 && 'pending') ||
|
||||
(index % 4 && 'cancelled') ||
|
||||
'refunded',
|
||||
};
|
||||
});
|
@@ -23,3 +23,7 @@ export * from './_product';
|
||||
export * from './_overview';
|
||||
|
||||
export * from './_calendar';
|
||||
|
||||
export * from './_party-event';
|
||||
|
||||
export * from './_party-order';
|
||||
|
226
03_source/frontend/src/actions/party-order.ts
Normal file
226
03_source/frontend/src/actions/party-order.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
// src/actions/order.ts
|
||||
import { useMemo } from 'react';
|
||||
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
|
||||
import type { IOrderItem } from 'src/types/party-order';
|
||||
import type { IProductItem } from 'src/types/product';
|
||||
import type { SWRConfiguration } from 'swr';
|
||||
import useSWR from 'swr';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const swrOptions: SWRConfiguration = {
|
||||
revalidateIfStale: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type OrdersData = {
|
||||
partyOrders: IOrderItem[];
|
||||
};
|
||||
|
||||
export function useGetPartyOrders() {
|
||||
const url = endpoints.partyOrder.list;
|
||||
|
||||
const { data, isLoading, error, isValidating, mutate } = useSWR<OrdersData>(
|
||||
url,
|
||||
fetcher,
|
||||
swrOptions
|
||||
);
|
||||
|
||||
const memoizedValue = useMemo(
|
||||
() => ({
|
||||
orders: data?.partyOrders || [],
|
||||
ordersLoading: isLoading,
|
||||
ordersError: error,
|
||||
ordersValidating: isValidating,
|
||||
ordersEmpty: !isLoading && !isValidating && !data?.partyOrders.length,
|
||||
mutate,
|
||||
}),
|
||||
[data?.partyOrders, 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 deletePartyOrder(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',
|
||||
};
|
||||
}
|
||||
}
|
@@ -84,6 +84,9 @@ export const endpoints = {
|
||||
changeStatus: (invoiceId: string) => `/api/invoice/changeStatus?invoiceId=${invoiceId}`,
|
||||
search: '/api/invoice/search',
|
||||
},
|
||||
//
|
||||
//
|
||||
//
|
||||
partyEvent: {
|
||||
list: '/api/party-event/list',
|
||||
details: '/api/party-event/details',
|
||||
@@ -92,4 +95,13 @@ export const endpoints = {
|
||||
update: '/api/party-event/update',
|
||||
delete: '/api/party-event/delete',
|
||||
},
|
||||
partyOrder: {
|
||||
list: '/api/party-order/list',
|
||||
profile: '/api/party-order/profile',
|
||||
update: '/api/party-order/update',
|
||||
settings: '/api/party-order/settings',
|
||||
details: '/api/party-order/details',
|
||||
changeStatus: (partyOrderId: string) =>
|
||||
`/api/party-order/changeStatus?partyOrderId=${partyOrderId}`,
|
||||
},
|
||||
};
|
||||
|
@@ -0,0 +1,30 @@
|
||||
// src/pages/dashboard/order/details.tsx
|
||||
//
|
||||
import { _orders } from 'src/_mock/_order';
|
||||
import { useGetOrder } from 'src/actions/order';
|
||||
import { CONFIG } from 'src/global-config';
|
||||
import { useParams } from 'src/routes/hooks';
|
||||
import { OrderDetailsView } from 'src/sections/order/view';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const metadata = { title: `Order details | Dashboard - ${CONFIG.appName}` };
|
||||
|
||||
export default function Page() {
|
||||
const { id = '' } = useParams();
|
||||
|
||||
// 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 (
|
||||
<>
|
||||
<title>{metadata.title}</title>
|
||||
|
||||
<OrderDetailsView order={order} />
|
||||
</>
|
||||
);
|
||||
}
|
18
03_source/frontend/src/pages/dashboard/party-order/list.tsx
Normal file
18
03_source/frontend/src/pages/dashboard/party-order/list.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
// src/pages/dashboard/party-order/list.tsx
|
||||
//
|
||||
import { CONFIG } from 'src/global-config';
|
||||
import { PartyOrderListView } from 'src/sections/party-order/view';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const metadata = { title: `Order list | Dashboard - ${CONFIG.appName}` };
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<title>{metadata.title}</title>
|
||||
|
||||
<PartyOrderListView />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -179,6 +179,8 @@ export const paths = {
|
||||
},
|
||||
},
|
||||
//
|
||||
//
|
||||
//
|
||||
partyEvent: {
|
||||
root: `${ROOTS.DASHBOARD}/party-event`,
|
||||
new: `${ROOTS.DASHBOARD}/party-event/new`,
|
||||
@@ -189,5 +191,10 @@ export const paths = {
|
||||
edit: `${ROOTS.DASHBOARD}/party-event/${MOCK_ID}/edit`,
|
||||
},
|
||||
},
|
||||
partyOrder: {
|
||||
root: `${ROOTS.DASHBOARD}/party-order`,
|
||||
details: (id: string) => `${ROOTS.DASHBOARD}/party-order/${id}`,
|
||||
demo: { details: `${ROOTS.DASHBOARD}/party-order/${MOCK_ID}` },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@@ -83,6 +83,10 @@ const PartyEventListPage = lazy(() => import('src/pages/dashboard/party-event/li
|
||||
const PartyEventCreatePage = lazy(() => import('src/pages/dashboard/party-event/new'));
|
||||
const PartyEventEditPage = lazy(() => import('src/pages/dashboard/party-event/edit'));
|
||||
|
||||
// PartyOrder
|
||||
const PartyOrderListPage = lazy(() => import('src/pages/dashboard/party-order/list'));
|
||||
const PartyOrderDetailsPage = lazy(() => import('src/pages/dashboard/party-order/details'));
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
function SuspenseOutlet() {
|
||||
@@ -217,6 +221,14 @@ export const dashboardRoutes: RouteObject[] = [
|
||||
{ path: ':id/edit', element: <PartyEventEditPage /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'party-order',
|
||||
children: [
|
||||
{ index: true, element: <PartyOrderListPage /> },
|
||||
{ path: 'list', element: <PartyOrderListPage /> },
|
||||
{ path: ':id', element: <PartyOrderDetailsPage /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@@ -13,7 +13,7 @@ import { useBoolean, useSetState } from 'minimal-shared/hooks';
|
||||
import { varAlpha } from 'minimal-shared/utils';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { _orders, ORDER_STATUS_OPTIONS } from 'src/_mock';
|
||||
import { _party_orders, ORDER_STATUS_OPTIONS } from 'src/_mock';
|
||||
import { deleteOrder, useGetOrders } from 'src/actions/order';
|
||||
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
|
||||
import { ConfirmDialog } from 'src/components/custom-dialog';
|
||||
|
@@ -0,0 +1,59 @@
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import type { IOrderCustomer } from 'src/types/party-order';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
customer?: IOrderCustomer;
|
||||
};
|
||||
|
||||
export function OrderDetailsCustomer({ customer }: Props) {
|
||||
return (
|
||||
<>
|
||||
<CardHeader
|
||||
title="Customer info"
|
||||
action={
|
||||
<IconButton>
|
||||
<Iconify icon="solar:pen-bold" />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<Box sx={{ p: 3, display: 'flex' }}>
|
||||
<Avatar
|
||||
alt={customer?.name}
|
||||
src={customer?.avatarUrl}
|
||||
sx={{ width: 48, height: 48, mr: 2 }}
|
||||
/>
|
||||
|
||||
<Stack spacing={0.5} sx={{ typography: 'body2', alignItems: 'flex-start' }}>
|
||||
<Typography variant="subtitle2">{customer?.name}</Typography>
|
||||
|
||||
<Box sx={{ color: 'text.secondary' }}>{customer?.email}</Box>
|
||||
|
||||
<div>
|
||||
IP address:
|
||||
<Box component="span" sx={{ color: 'text.secondary', ml: 0.25 }}>
|
||||
{customer?.ipAddress}
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
startIcon={<Iconify icon="mingcute:add-line" />}
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
Add to Blacklist
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import type { IOrderDelivery } from 'src/types/party-order';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
delivery?: IOrderDelivery;
|
||||
};
|
||||
|
||||
export function OrderDetailsDelivery({ delivery }: Props) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<CardHeader
|
||||
title={t('Delivery')}
|
||||
action={
|
||||
<IconButton>
|
||||
<Iconify icon="solar:pen-bold" />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<Stack spacing={1.5} sx={{ p: 3, typography: 'body2' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box component="span" sx={{ color: 'text.secondary', width: 120, flexShrink: 0 }}>
|
||||
{t('Ship by')}
|
||||
</Box>
|
||||
|
||||
{delivery?.shipBy}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box component="span" sx={{ color: 'text.secondary', width: 120, flexShrink: 0 }}>
|
||||
{t('Speedy')}
|
||||
</Box>
|
||||
|
||||
{delivery?.speedy}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box component="span" sx={{ color: 'text.secondary', width: 120, flexShrink: 0 }}>
|
||||
{t('Tracking No.')}
|
||||
</Box>
|
||||
|
||||
<Link underline="always" color="inherit">
|
||||
{delivery?.trackingNumber}
|
||||
</Link>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,108 @@
|
||||
// src/sections/order/order-details-history.tsx
|
||||
import Timeline from '@mui/lab/Timeline';
|
||||
import TimelineConnector from '@mui/lab/TimelineConnector';
|
||||
import TimelineContent from '@mui/lab/TimelineContent';
|
||||
import TimelineDot from '@mui/lab/TimelineDot';
|
||||
import TimelineItem, { timelineItemClasses } from '@mui/lab/TimelineItem';
|
||||
import TimelineSeparator from '@mui/lab/TimelineSeparator';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { IOrderHistory } from 'src/types/party-order';
|
||||
import { fDateTime } from 'src/utils/format-time';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
history?: IOrderHistory;
|
||||
};
|
||||
|
||||
export function OrderDetailsHistory({ history }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderSummary = () => (
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 2.5,
|
||||
gap: 2,
|
||||
minWidth: 260,
|
||||
flexShrink: 0,
|
||||
borderRadius: 2,
|
||||
display: 'flex',
|
||||
typography: 'body2',
|
||||
borderStyle: 'dashed',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Box sx={{ mb: 0.5, color: 'text.disabled' }}>{t('Order time')}</Box>
|
||||
{fDateTime(history?.orderTime)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Box sx={{ mb: 0.5, color: 'text.disabled' }}>{t('Payment time')}</Box>
|
||||
{fDateTime(history?.orderTime)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Box sx={{ mb: 0.5, color: 'text.disabled' }}>{t('Delivery time for the carrier')}</Box>
|
||||
{fDateTime(history?.orderTime)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Box sx={{ mb: 0.5, color: 'text.disabled' }}>{t('Completion time')}</Box>
|
||||
{fDateTime(history?.orderTime)}
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
const renderTimeline = () => (
|
||||
<Timeline
|
||||
sx={{ p: 0, m: 0, [`& .${timelineItemClasses.root}:before`]: { flex: 0, padding: 0 } }}
|
||||
>
|
||||
{history?.timeline.map((item, index) => {
|
||||
const firstTime = index === 0;
|
||||
const lastTime = index === history.timeline.length - 1;
|
||||
|
||||
return (
|
||||
<TimelineItem key={item.title}>
|
||||
<TimelineSeparator>
|
||||
<TimelineDot color={(firstTime && 'primary') || 'grey'} />
|
||||
{lastTime ? null : <TimelineConnector />}
|
||||
</TimelineSeparator>
|
||||
|
||||
<TimelineContent>
|
||||
<Typography variant="subtitle2">{item.title}</Typography>
|
||||
|
||||
<Box sx={{ color: 'text.disabled', typography: 'caption', mt: 0.5 }}>
|
||||
{fDateTime(item.time)}
|
||||
</Box>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
);
|
||||
})}
|
||||
</Timeline>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title="History" />
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
gap: 3,
|
||||
display: 'flex',
|
||||
alignItems: { md: 'flex-start' },
|
||||
flexDirection: { xs: 'column-reverse', md: 'row' },
|
||||
}}
|
||||
>
|
||||
{renderTimeline()}
|
||||
{renderSummary()}
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,130 @@
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import type { CardProps } from '@mui/material/Card';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Scrollbar } from 'src/components/scrollbar';
|
||||
import type { IOrderProductItem } from 'src/types/party-order';
|
||||
import { fCurrency } from 'src/utils/format-number';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = CardProps & {
|
||||
taxes?: number;
|
||||
shipping?: number;
|
||||
discount?: number;
|
||||
subtotal?: number;
|
||||
totalAmount?: number;
|
||||
items?: IOrderProductItem[];
|
||||
};
|
||||
|
||||
export function OrderDetailsItems({
|
||||
sx,
|
||||
taxes,
|
||||
shipping,
|
||||
discount,
|
||||
subtotal,
|
||||
items = [],
|
||||
totalAmount,
|
||||
...other
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const renderTotal = () => (
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
gap: 2,
|
||||
display: 'flex',
|
||||
textAlign: 'right',
|
||||
typography: 'body2',
|
||||
alignItems: 'flex-end',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box sx={{ color: 'text.secondary' }}>{t('Subtotal')}</Box>
|
||||
<Box sx={{ width: 160, typography: 'subtitle2' }}>{fCurrency(subtotal) || '-'}</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box sx={{ color: 'text.secondary' }}>{t('Shipping')}</Box>
|
||||
<Box sx={{ width: 160, ...(shipping && { color: 'error.main' }) }}>
|
||||
{shipping ? `- ${fCurrency(shipping)}` : '-'}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box sx={{ color: 'text.secondary' }}>{t('Discount')}</Box>
|
||||
<Box sx={{ width: 160, ...(discount && { color: 'error.main' }) }}>
|
||||
{discount ? `- ${fCurrency(discount)}` : '-'}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box sx={{ color: 'text.secondary' }}>{t('Taxes')}</Box>
|
||||
|
||||
<Box sx={{ width: 160 }}>{taxes ? fCurrency(taxes) : '-'}</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', typography: 'subtitle1' }}>
|
||||
<div>{t('Total')}</div>
|
||||
<Box sx={{ width: 160 }}>{fCurrency(totalAmount) || '-'}</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card sx={sx} {...other}>
|
||||
<CardHeader
|
||||
title={t('Details')}
|
||||
action={
|
||||
<IconButton>
|
||||
<Iconify icon="solar:pen-bold" />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
|
||||
<Scrollbar>
|
||||
{items.map((item) => (
|
||||
<Box
|
||||
key={item.id}
|
||||
sx={[
|
||||
(theme) => ({
|
||||
p: 3,
|
||||
minWidth: 640,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderBottom: `dashed 2px ${theme.vars.palette.background.neutral}`,
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Avatar src={item.coverUrl} variant="rounded" sx={{ width: 48, height: 48, mr: 2 }} />
|
||||
|
||||
<ListItemText
|
||||
primary={item.name}
|
||||
secondary={item.sku}
|
||||
slotProps={{
|
||||
primary: { sx: { typography: 'body2' } },
|
||||
secondary: {
|
||||
sx: { mt: 0.5, color: 'text.disabled' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box sx={{ typography: 'body2' }}>x{item.quantity}</Box>
|
||||
|
||||
<Box sx={{ width: 110, textAlign: 'right', typography: 'subtitle2' }}>
|
||||
{fCurrency(item.price)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Scrollbar>
|
||||
|
||||
{renderTotal()}
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import type { IOrderPayment } from 'src/types/party-order';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
payment?: IOrderPayment;
|
||||
};
|
||||
|
||||
export function OrderDetailsPayment({ payment }: Props) {
|
||||
return (
|
||||
<>
|
||||
<CardHeader
|
||||
title="Payment"
|
||||
action={
|
||||
<IconButton>
|
||||
<Iconify icon="solar:pen-bold" />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
gap: 0.5,
|
||||
display: 'flex',
|
||||
typography: 'body2',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
{payment?.cardNumber}
|
||||
<Iconify icon="payments:mastercard" width={36} height="auto" />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import type { IOrderShippingAddress } from 'src/types/party-order';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
shippingAddress?: IOrderShippingAddress;
|
||||
};
|
||||
|
||||
export function OrderDetailsShipping({ shippingAddress }: Props) {
|
||||
return (
|
||||
<>
|
||||
<CardHeader
|
||||
title="Shipping"
|
||||
action={
|
||||
<IconButton>
|
||||
<Iconify icon="solar:pen-bold" />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<Stack spacing={1.5} sx={{ p: 3, typography: 'body2' }}>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box component="span" sx={{ color: 'text.secondary', width: 120, flexShrink: 0 }}>
|
||||
Address
|
||||
</Box>
|
||||
|
||||
{shippingAddress?.fullAddress}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box component="span" sx={{ color: 'text.secondary', width: 120, flexShrink: 0 }}>
|
||||
Phone number
|
||||
</Box>
|
||||
|
||||
{shippingAddress?.phoneNumber}
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,138 @@
|
||||
// src/sections/order/order-details-toolbar.tsx
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { usePopover } from 'minimal-shared/hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CustomPopover } from 'src/components/custom-popover';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Label } from 'src/components/label';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
import type { IDateValue } from 'src/types/common';
|
||||
import { fDateTime } from 'src/utils/format-time';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
status?: string;
|
||||
backHref: string;
|
||||
orderNumber?: string;
|
||||
createdAt?: IDateValue;
|
||||
onChangeStatus: (newValue: string) => void;
|
||||
statusOptions: { value: string; label: string }[];
|
||||
};
|
||||
|
||||
export function OrderDetailsToolbar({
|
||||
status,
|
||||
backHref,
|
||||
createdAt,
|
||||
orderNumber,
|
||||
statusOptions,
|
||||
onChangeStatus,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const menuActions = usePopover();
|
||||
|
||||
const renderMenuActions = () => (
|
||||
<CustomPopover
|
||||
open={menuActions.open}
|
||||
anchorEl={menuActions.anchorEl}
|
||||
onClose={menuActions.onClose}
|
||||
slotProps={{ arrow: { placement: 'top-right' } }}
|
||||
>
|
||||
<MenuList>
|
||||
{statusOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
selected={option.value === status}
|
||||
onClick={() => {
|
||||
menuActions.onClose();
|
||||
onChangeStatus(option.value);
|
||||
}}
|
||||
>
|
||||
{t(option.label)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</CustomPopover>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
gap: 3,
|
||||
display: 'flex',
|
||||
mb: { xs: 3, md: 5 },
|
||||
flexDirection: { xs: 'column', md: 'row' },
|
||||
}}
|
||||
>
|
||||
<Box sx={{ gap: 1, display: 'flex', alignItems: 'flex-start' }}>
|
||||
<IconButton component={RouterLink} href={backHref}>
|
||||
<Iconify icon="eva:arrow-ios-back-fill" />
|
||||
</IconButton>
|
||||
|
||||
<Stack spacing={0.5}>
|
||||
<Box sx={{ gap: 1, display: 'flex', alignItems: 'center' }}>
|
||||
<Typography variant="h4"> Order {orderNumber} </Typography>
|
||||
<Label
|
||||
variant="soft"
|
||||
color={
|
||||
(status === 'completed' && 'success') ||
|
||||
(status === 'pending' && 'warning') ||
|
||||
(status === 'cancelled' && 'error') ||
|
||||
'default'
|
||||
}
|
||||
>
|
||||
{status}
|
||||
</Label>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" sx={{ color: 'text.disabled' }}>
|
||||
{fDateTime(createdAt)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
gap: 1.5,
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
color="inherit"
|
||||
variant="outlined"
|
||||
endIcon={<Iconify icon="eva:arrow-ios-downward-fill" />}
|
||||
onClick={menuActions.onOpen}
|
||||
sx={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
{status}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="inherit"
|
||||
variant="outlined"
|
||||
startIcon={<Iconify icon="solar:printer-minimalistic-bold" />}
|
||||
>
|
||||
{t('Print (not implemented)')}
|
||||
</Button>
|
||||
|
||||
<Button color="inherit" variant="contained" startIcon={<Iconify icon="solar:pen-bold" />}>
|
||||
{t('Edit')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{renderMenuActions()}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
import Chip from '@mui/material/Chip';
|
||||
import type { UseSetStateReturn } from 'minimal-shared/hooks';
|
||||
import { useCallback } from 'react';
|
||||
import type { FiltersResultProps } from 'src/components/filters-result';
|
||||
import { chipProps, FiltersBlock, FiltersResult } from 'src/components/filters-result';
|
||||
import type { IOrderTableFilters } from 'src/types/party-order';
|
||||
import { fDateRangeShortLabel } from 'src/utils/format-time';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = FiltersResultProps & {
|
||||
onResetPage: () => void;
|
||||
filters: UseSetStateReturn<IOrderTableFilters>;
|
||||
};
|
||||
|
||||
export function PartyOrderTableFiltersResult({ filters, totalResults, onResetPage, sx }: Props) {
|
||||
const { state: currentFilters, setState: updateFilters, resetState: resetFilters } = filters;
|
||||
|
||||
const handleRemoveKeyword = useCallback(() => {
|
||||
onResetPage();
|
||||
updateFilters({ name: '' });
|
||||
}, [onResetPage, updateFilters]);
|
||||
|
||||
const handleRemoveStatus = useCallback(() => {
|
||||
onResetPage();
|
||||
updateFilters({ status: 'all' });
|
||||
}, [onResetPage, updateFilters]);
|
||||
|
||||
const handleRemoveDate = useCallback(() => {
|
||||
onResetPage();
|
||||
updateFilters({ startDate: null, endDate: null });
|
||||
}, [onResetPage, updateFilters]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
onResetPage();
|
||||
resetFilters();
|
||||
}, [onResetPage, resetFilters]);
|
||||
|
||||
return (
|
||||
<FiltersResult totalResults={totalResults} onReset={handleReset} sx={sx}>
|
||||
<FiltersBlock label="Status:" isShow={currentFilters.status !== 'all'}>
|
||||
<Chip
|
||||
{...chipProps}
|
||||
label={currentFilters.status}
|
||||
onDelete={handleRemoveStatus}
|
||||
sx={{ textTransform: 'capitalize' }}
|
||||
/>
|
||||
</FiltersBlock>
|
||||
|
||||
<FiltersBlock
|
||||
label="Date:"
|
||||
isShow={Boolean(currentFilters.startDate && currentFilters.endDate)}
|
||||
>
|
||||
<Chip
|
||||
{...chipProps}
|
||||
label={fDateRangeShortLabel(currentFilters.startDate, currentFilters.endDate)}
|
||||
onDelete={handleRemoveDate}
|
||||
/>
|
||||
</FiltersBlock>
|
||||
|
||||
<FiltersBlock label="Keyword:" isShow={!!currentFilters.name}>
|
||||
<Chip {...chipProps} label={currentFilters.name} onDelete={handleRemoveKeyword} />
|
||||
</FiltersBlock>
|
||||
</FiltersResult>
|
||||
);
|
||||
}
|
@@ -0,0 +1,237 @@
|
||||
// src/sections/order/view/order-list-view.tsx
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Link from '@mui/material/Link';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import { useBoolean, usePopover } from 'minimal-shared/hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ConfirmDialog } from 'src/components/custom-dialog';
|
||||
import { CustomPopover } from 'src/components/custom-popover';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Label } from 'src/components/label';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
import type { IOrderItem } from 'src/types/party-order';
|
||||
import { fCurrency } from 'src/utils/format-number';
|
||||
import { fDate, fTime } from 'src/utils/format-time';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
row: IOrderItem;
|
||||
selected: boolean;
|
||||
detailsHref: string;
|
||||
onSelectRow: () => void;
|
||||
onDeleteRow: () => void;
|
||||
};
|
||||
|
||||
export function PartyOrderTableRow({
|
||||
row,
|
||||
selected,
|
||||
onSelectRow,
|
||||
onDeleteRow,
|
||||
detailsHref,
|
||||
}: Props) {
|
||||
const confirmDialog = useBoolean();
|
||||
const menuActions = usePopover();
|
||||
const collapseRow = useBoolean();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderPrimaryRow = () => (
|
||||
<TableRow hover selected={selected}>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
onClick={onSelectRow}
|
||||
slotProps={{
|
||||
input: {
|
||||
id: `${row.id}-checkbox`,
|
||||
'aria-label': `${row.id} checkbox`,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Link component={RouterLink} href={detailsHref} color="inherit" underline="always">
|
||||
{row.orderNumber}
|
||||
</Link>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Box sx={{ gap: 2, display: 'flex', alignItems: 'center' }}>
|
||||
<Avatar alt={row.customer.name} src={row.customer.avatarUrl} />
|
||||
|
||||
<Stack sx={{ typography: 'body2', flex: '1 1 auto', alignItems: 'flex-start' }}>
|
||||
<Box component="span">{row.customer.name}</Box>
|
||||
|
||||
<Box component="span" sx={{ color: 'text.disabled' }}>
|
||||
{row.customer.email}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<ListItemText
|
||||
primary={fDate(row.createdAt)}
|
||||
secondary={fTime(row.createdAt)}
|
||||
slotProps={{
|
||||
primary: {
|
||||
noWrap: true,
|
||||
sx: { typography: 'body2' },
|
||||
},
|
||||
secondary: {
|
||||
sx: { mt: 0.5, typography: 'caption' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="center"> {row.totalQuantity} </TableCell>
|
||||
|
||||
<TableCell> {fCurrency(row.subtotal)} </TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Label
|
||||
variant="soft"
|
||||
color={
|
||||
(row.status === 'completed' && 'success') ||
|
||||
(row.status === 'pending' && 'warning') ||
|
||||
(row.status === 'cancelled' && 'error') ||
|
||||
'default'
|
||||
}
|
||||
>
|
||||
{row.status}
|
||||
</Label>
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="right" sx={{ px: 1, whiteSpace: 'nowrap' }}>
|
||||
<IconButton
|
||||
color={collapseRow.value ? 'inherit' : 'default'}
|
||||
onClick={collapseRow.onToggle}
|
||||
sx={{ ...(collapseRow.value && { bgcolor: 'action.hover' }) }}
|
||||
>
|
||||
<Iconify icon="eva:arrow-ios-downward-fill" />
|
||||
</IconButton>
|
||||
|
||||
<IconButton color={menuActions.open ? 'inherit' : 'default'} onClick={menuActions.onOpen}>
|
||||
<Iconify icon="eva:more-vertical-fill" />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
const renderSecondaryRow = () => (
|
||||
<TableRow>
|
||||
<TableCell sx={{ p: 0, border: 'none' }} colSpan={8}>
|
||||
<Collapse
|
||||
in={collapseRow.value}
|
||||
timeout="auto"
|
||||
unmountOnExit
|
||||
sx={{ bgcolor: 'background.neutral' }}
|
||||
>
|
||||
<Paper sx={{ m: 1.5 }}>
|
||||
{row.items.map((item) => (
|
||||
<Box
|
||||
key={item.id}
|
||||
sx={(theme) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
p: theme.spacing(1.5, 2, 1.5, 1.5),
|
||||
'&:not(:last-of-type)': {
|
||||
borderBottom: `solid 2px ${theme.vars.palette.background.neutral}`,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Avatar
|
||||
src={item.coverUrl}
|
||||
variant="rounded"
|
||||
sx={{ width: 48, height: 48, mr: 2 }}
|
||||
/>
|
||||
|
||||
<ListItemText
|
||||
primary={item.name}
|
||||
secondary={item.sku}
|
||||
slotProps={{
|
||||
primary: {
|
||||
sx: { typography: 'body2' },
|
||||
},
|
||||
secondary: {
|
||||
sx: { mt: 0.5, color: 'text.disabled' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>x{item.quantity} </div>
|
||||
|
||||
<Box sx={{ width: 110, textAlign: 'right' }}>{fCurrency(item.price)}</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Paper>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
const renderMenuActions = () => (
|
||||
<CustomPopover
|
||||
open={menuActions.open}
|
||||
anchorEl={menuActions.anchorEl}
|
||||
onClose={menuActions.onClose}
|
||||
slotProps={{ arrow: { placement: 'right-top' } }}
|
||||
>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
confirmDialog.onTrue();
|
||||
menuActions.onClose();
|
||||
}}
|
||||
sx={{ color: 'error.main' }}
|
||||
>
|
||||
<Iconify icon="solar:trash-bin-trash-bold" />
|
||||
{t('Delete')}
|
||||
</MenuItem>
|
||||
|
||||
<li>
|
||||
<MenuItem component={RouterLink} href={detailsHref} onClick={() => menuActions.onClose()}>
|
||||
<Iconify icon="solar:eye-bold" />
|
||||
{t('View')}
|
||||
</MenuItem>
|
||||
</li>
|
||||
</MenuList>
|
||||
</CustomPopover>
|
||||
);
|
||||
|
||||
const renderConfrimDialog = () => (
|
||||
<ConfirmDialog
|
||||
open={confirmDialog.value}
|
||||
onClose={confirmDialog.onFalse}
|
||||
title="Delete"
|
||||
content="Are you sure want to delete?"
|
||||
action={
|
||||
<Button variant="contained" color="error" onClick={onDeleteRow}>
|
||||
Delete
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderPrimaryRow()}
|
||||
{renderSecondaryRow()}
|
||||
{renderMenuActions()}
|
||||
{renderConfrimDialog()}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,156 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import { formHelperTextClasses } from '@mui/material/FormHelperText';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import type { UseSetStateReturn } from 'minimal-shared/hooks';
|
||||
import { usePopover } from 'minimal-shared/hooks';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CustomPopover } from 'src/components/custom-popover';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import type { IDatePickerControl } from 'src/types/common';
|
||||
import type { IOrderTableFilters } from 'src/types/party-order';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
dateError: boolean;
|
||||
onResetPage: () => void;
|
||||
filters: UseSetStateReturn<IOrderTableFilters>;
|
||||
};
|
||||
|
||||
export function PartyOrderTableToolbar({ filters, onResetPage, dateError }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const menuActions = usePopover();
|
||||
|
||||
const { state: currentFilters, setState: updateFilters } = filters;
|
||||
|
||||
const handleFilterName = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onResetPage();
|
||||
updateFilters({ name: event.target.value });
|
||||
},
|
||||
[onResetPage, updateFilters]
|
||||
);
|
||||
|
||||
const handleFilterStartDate = useCallback(
|
||||
(newValue: IDatePickerControl) => {
|
||||
onResetPage();
|
||||
updateFilters({ startDate: newValue });
|
||||
},
|
||||
[onResetPage, updateFilters]
|
||||
);
|
||||
|
||||
const handleFilterEndDate = useCallback(
|
||||
(newValue: IDatePickerControl) => {
|
||||
onResetPage();
|
||||
updateFilters({ endDate: newValue });
|
||||
},
|
||||
[onResetPage, updateFilters]
|
||||
);
|
||||
|
||||
const renderMenuActions = () => (
|
||||
<CustomPopover
|
||||
open={menuActions.open}
|
||||
anchorEl={menuActions.anchorEl}
|
||||
onClose={menuActions.onClose}
|
||||
slotProps={{ arrow: { placement: 'right-top' } }}
|
||||
>
|
||||
<MenuList>
|
||||
<MenuItem onClick={() => menuActions.onClose()}>
|
||||
<Iconify icon="solar:printer-minimalistic-bold" />
|
||||
{t('Print')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={() => menuActions.onClose()}>
|
||||
<Iconify icon="solar:import-bold" />
|
||||
{t('Import')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={() => menuActions.onClose()}>
|
||||
<Iconify icon="solar:export-bold" />
|
||||
{t('Export')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</CustomPopover>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
p: 2.5,
|
||||
gap: 2,
|
||||
display: 'flex',
|
||||
pr: { xs: 2.5, md: 1 },
|
||||
flexDirection: { xs: 'column', md: 'row' },
|
||||
alignItems: { xs: 'flex-end', md: 'center' },
|
||||
}}
|
||||
>
|
||||
<DatePicker
|
||||
label={t('Start date')}
|
||||
value={currentFilters.startDate}
|
||||
onChange={handleFilterStartDate}
|
||||
slotProps={{ textField: { fullWidth: true } }}
|
||||
sx={{ maxWidth: { md: 200 } }}
|
||||
/>
|
||||
|
||||
<DatePicker
|
||||
label={t('End date')}
|
||||
value={currentFilters.endDate}
|
||||
onChange={handleFilterEndDate}
|
||||
slotProps={{
|
||||
textField: {
|
||||
fullWidth: true,
|
||||
error: dateError,
|
||||
helperText: dateError ? 'End date must be later than start date' : null,
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
maxWidth: { md: 200 },
|
||||
[`& .${formHelperTextClasses.root}`]: {
|
||||
position: { md: 'absolute' },
|
||||
bottom: { md: -40 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
gap: 2,
|
||||
width: 1,
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
value={currentFilters.name}
|
||||
onChange={handleFilterName}
|
||||
placeholder={t('Search customer or order number...')}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Iconify icon="eva:search-fill" sx={{ color: 'text.disabled' }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<IconButton onClick={menuActions.onOpen}>
|
||||
<Iconify icon="eva:more-vertical-fill" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{renderMenuActions()}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
export * from './party-order-list-view';
|
||||
|
||||
export * from './party-order-details-view';
|
@@ -0,0 +1,104 @@
|
||||
// src/sections/order/view/order-details-view.tsx
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { ORDER_STATUS_OPTIONS } from 'src/_mock';
|
||||
import { changeStatus } from 'src/actions/party-order';
|
||||
import { DashboardContent } from 'src/layouts/dashboard';
|
||||
import { useTranslate } from 'src/locales';
|
||||
import { paths } from 'src/routes/paths';
|
||||
import type { IOrderItem } from 'src/types/party-order';
|
||||
import { OrderDetailsCustomer } from '../party-order-details-customer';
|
||||
import { OrderDetailsDelivery } from '../party-order-details-delivery';
|
||||
import { OrderDetailsHistory } from '../party-order-details-history';
|
||||
import { OrderDetailsItems } from '../party-order-details-items';
|
||||
import { OrderDetailsPayment } from '../party-order-details-payment';
|
||||
import { OrderDetailsShipping } from '../party-order-details-shipping';
|
||||
import { OrderDetailsToolbar } from '../party-order-details-toolbar';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
order: IOrderItem;
|
||||
};
|
||||
|
||||
export function OrderDetailsView({ order }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [status, setStatus] = useState(order.status);
|
||||
|
||||
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 (
|
||||
<DashboardContent>
|
||||
<OrderDetailsToolbar
|
||||
status={status}
|
||||
createdAt={order?.createdAt}
|
||||
orderNumber={order?.orderNumber}
|
||||
backHref={paths.dashboard.order.root}
|
||||
onChangeStatus={handleChangeStatus}
|
||||
statusOptions={ORDER_STATUS_OPTIONS}
|
||||
/>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<Box
|
||||
sx={{
|
||||
//
|
||||
gap: 3,
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column-reverse', md: 'column' },
|
||||
}}
|
||||
>
|
||||
<OrderDetailsItems
|
||||
items={order?.items}
|
||||
taxes={order?.taxes}
|
||||
shipping={order?.shipping}
|
||||
discount={order?.discount}
|
||||
subtotal={order?.subtotal}
|
||||
totalAmount={order?.totalAmount}
|
||||
/>
|
||||
|
||||
<OrderDetailsHistory history={order?.history} />
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Card>
|
||||
<OrderDetailsCustomer customer={order?.customer} />
|
||||
|
||||
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||
<OrderDetailsDelivery delivery={order?.delivery} />
|
||||
|
||||
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||
<OrderDetailsShipping shippingAddress={order?.shippingAddress} />
|
||||
|
||||
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||
<OrderDetailsPayment payment={order?.payment} />
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DashboardContent>
|
||||
);
|
||||
}
|
@@ -0,0 +1,364 @@
|
||||
// src/sections/order/view/order-list-view.tsx
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { useBoolean, useSetState } from 'minimal-shared/hooks';
|
||||
import { varAlpha } from 'minimal-shared/utils';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { _party_orders, PARTY_ORDER_STATUS_OPTIONS } from 'src/_mock';
|
||||
import { deletePartyOrder, useGetPartyOrders } from 'src/actions/party-order';
|
||||
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
|
||||
import { ConfirmDialog } from 'src/components/custom-dialog';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Label } from 'src/components/label';
|
||||
import { Scrollbar } from 'src/components/scrollbar';
|
||||
import { toast } from 'src/components/snackbar';
|
||||
import type { TableHeadCellProps } from 'src/components/table';
|
||||
import {
|
||||
emptyRows,
|
||||
getComparator,
|
||||
rowInPage,
|
||||
TableEmptyRows,
|
||||
TableHeadCustom,
|
||||
TableNoData,
|
||||
TablePaginationCustom,
|
||||
TableSelectedAction,
|
||||
useTable,
|
||||
} from 'src/components/table';
|
||||
import { DashboardContent } from 'src/layouts/dashboard';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
import { paths } from 'src/routes/paths';
|
||||
import type { IOrderItem, IOrderTableFilters } from 'src/types/party-order';
|
||||
import { fIsAfter, fIsBetween } from 'src/utils/format-time';
|
||||
import { PartyOrderTableFiltersResult } from '../party-order-table-filters-result';
|
||||
import { PartyOrderTableRow } from '../party-order-table-row';
|
||||
import { PartyOrderTableToolbar } from '../party-order-table-toolbar';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const STATUS_OPTIONS = [{ value: 'all', label: 'All' }, ...PARTY_ORDER_STATUS_OPTIONS];
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function PartyOrderListView() {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const TABLE_HEAD: TableHeadCellProps[] = [
|
||||
{ id: 'orderNumber', label: t('Order'), width: 88 },
|
||||
{ id: 'name', label: t('Customer') },
|
||||
{ id: 'createdAt', label: t('Date'), width: 140 },
|
||||
{ id: 'totalQuantity', label: t('Items'), width: 120, align: 'center' },
|
||||
{ id: 'totalAmount', label: t('Price'), width: 140 },
|
||||
{ id: 'status', label: t('Status'), width: 110 },
|
||||
{ id: '', width: 88 },
|
||||
];
|
||||
|
||||
const { orders, mutate, ordersLoading } = useGetPartyOrders();
|
||||
|
||||
const table = useTable({ defaultOrderBy: 'orderNumber' });
|
||||
|
||||
const confirmDialog = useBoolean();
|
||||
|
||||
const [tableData, setTableData] = useState<IOrderItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableData(orders);
|
||||
}, [orders]);
|
||||
|
||||
const filters = useSetState<IOrderTableFilters>({
|
||||
name: '',
|
||||
status: 'all',
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
});
|
||||
const { state: currentFilters, setState: updateFilters } = filters;
|
||||
|
||||
const dateError = fIsAfter(currentFilters.startDate, currentFilters.endDate);
|
||||
|
||||
const dataFiltered = applyFilter({
|
||||
inputData: tableData,
|
||||
comparator: getComparator(table.order, table.orderBy),
|
||||
filters: currentFilters,
|
||||
dateError,
|
||||
});
|
||||
|
||||
const dataInPage = rowInPage(dataFiltered, table.page, table.rowsPerPage);
|
||||
|
||||
const canReset =
|
||||
!!currentFilters.name ||
|
||||
currentFilters.status !== 'all' ||
|
||||
(!!currentFilters.startDate && !!currentFilters.endDate);
|
||||
|
||||
const notFound = (!dataFiltered.length && canReset) || !dataFiltered.length;
|
||||
|
||||
const handleDeleteRow = useCallback(
|
||||
async (id: string) => {
|
||||
// const deleteRow = tableData.filter((row) => row.id !== id);
|
||||
|
||||
try {
|
||||
await deletePartyOrder(id);
|
||||
toast.success('Delete success!');
|
||||
mutate();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('Delete failed!');
|
||||
}
|
||||
|
||||
// toast.success('Delete success!');
|
||||
// setTableData(deleteRow);
|
||||
// table.onUpdatePageDeleteRow(dataInPage.length);
|
||||
},
|
||||
[table, tableData, mutate]
|
||||
);
|
||||
|
||||
const handleDeleteRows = useCallback(() => {
|
||||
const deleteRows = tableData.filter((row) => !table.selected.includes(row.id));
|
||||
|
||||
toast.success('Delete success!');
|
||||
|
||||
setTableData(deleteRows);
|
||||
|
||||
table.onUpdatePageDeleteRows(dataInPage.length, dataFiltered.length);
|
||||
}, [dataFiltered.length, dataInPage.length, table, tableData]);
|
||||
|
||||
const handleFilterStatus = useCallback(
|
||||
(event: React.SyntheticEvent, newValue: string) => {
|
||||
table.onResetPage();
|
||||
updateFilters({ status: newValue });
|
||||
},
|
||||
[updateFilters, table]
|
||||
);
|
||||
|
||||
const renderConfirmDialog = () => (
|
||||
<ConfirmDialog
|
||||
open={confirmDialog.value}
|
||||
onClose={confirmDialog.onFalse}
|
||||
title={t('Delete')}
|
||||
content={
|
||||
<>
|
||||
Are you sure want to delete <strong> {table.selected.length} </strong> items?
|
||||
</>
|
||||
}
|
||||
action={
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
handleDeleteRows();
|
||||
// confirmDialog.onFalse();
|
||||
}}
|
||||
>
|
||||
{t('Delete')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
if (!orders) return <>loading</>;
|
||||
if (ordersLoading) return <>loading</>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardContent>
|
||||
<CustomBreadcrumbs
|
||||
heading="List"
|
||||
links={[
|
||||
//
|
||||
{ name: t('Dashboard'), href: paths.dashboard.root },
|
||||
{ name: t('Party Order'), href: paths.dashboard.partyOrder.root },
|
||||
{ name: t('List') },
|
||||
]}
|
||||
sx={{ mb: { xs: 3, md: 5 } }}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<Tabs
|
||||
value={currentFilters.status}
|
||||
onChange={handleFilterStatus}
|
||||
sx={[
|
||||
(theme) => ({
|
||||
px: 2.5,
|
||||
boxShadow: `inset 0 -2px 0 0 ${varAlpha(theme.vars.palette.grey['500Channel'], 0.08)}`,
|
||||
}),
|
||||
]}
|
||||
>
|
||||
{STATUS_OPTIONS.map((tab) => (
|
||||
<Tab
|
||||
key={tab.value}
|
||||
iconPosition="end"
|
||||
value={tab.value}
|
||||
label={t(tab.label)}
|
||||
icon={
|
||||
<Label
|
||||
variant={
|
||||
((tab.value === 'all' || tab.value === currentFilters.status) && 'filled') ||
|
||||
'soft'
|
||||
}
|
||||
color={
|
||||
(tab.value === 'completed' && 'success') ||
|
||||
(tab.value === 'pending' && 'warning') ||
|
||||
(tab.value === 'cancelled' && 'error') ||
|
||||
'default'
|
||||
}
|
||||
>
|
||||
{['completed', 'pending', 'cancelled', 'refunded'].includes(tab.value)
|
||||
? tableData.filter((user) => user.status === tab.value).length
|
||||
: tableData.length}
|
||||
</Label>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
<PartyOrderTableToolbar
|
||||
filters={filters}
|
||||
onResetPage={table.onResetPage}
|
||||
dateError={dateError}
|
||||
/>
|
||||
|
||||
{canReset && (
|
||||
<PartyOrderTableFiltersResult
|
||||
filters={filters}
|
||||
totalResults={dataFiltered.length}
|
||||
onResetPage={table.onResetPage}
|
||||
sx={{ p: 2.5, pt: 0 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<TableSelectedAction
|
||||
dense={table.dense}
|
||||
numSelected={table.selected.length}
|
||||
rowCount={dataFiltered.length}
|
||||
onSelectAllRows={(checked) =>
|
||||
table.onSelectAllRows(
|
||||
checked,
|
||||
dataFiltered.map((row) => row.id)
|
||||
)
|
||||
}
|
||||
action={
|
||||
<Tooltip title={t('Delete')}>
|
||||
<IconButton color="primary" onClick={confirmDialog.onTrue}>
|
||||
<Iconify icon="solar:trash-bin-trash-bold" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
|
||||
<Scrollbar sx={{ minHeight: 444 }}>
|
||||
<Table size={table.dense ? 'small' : 'medium'} sx={{ minWidth: 960 }}>
|
||||
<TableHeadCustom
|
||||
order={table.order}
|
||||
orderBy={table.orderBy}
|
||||
headCells={TABLE_HEAD}
|
||||
rowCount={dataFiltered.length}
|
||||
numSelected={table.selected.length}
|
||||
onSort={table.onSort}
|
||||
onSelectAllRows={(checked) =>
|
||||
table.onSelectAllRows(
|
||||
checked,
|
||||
dataFiltered.map((row) => row.id)
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<TableBody>
|
||||
{dataFiltered
|
||||
.slice(
|
||||
table.page * table.rowsPerPage,
|
||||
table.page * table.rowsPerPage + table.rowsPerPage
|
||||
)
|
||||
.map((row) => (
|
||||
<PartyOrderTableRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
selected={table.selected.includes(row.id)}
|
||||
onSelectRow={() => table.onSelectRow(row.id)}
|
||||
onDeleteRow={() => handleDeleteRow(row.id)}
|
||||
detailsHref={paths.dashboard.order.details(row.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<TableEmptyRows
|
||||
height={table.dense ? 56 : 56 + 20}
|
||||
emptyRows={emptyRows(table.page, table.rowsPerPage, dataFiltered.length)}
|
||||
/>
|
||||
|
||||
<TableNoData notFound={notFound} />
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Scrollbar>
|
||||
</Box>
|
||||
|
||||
<TablePaginationCustom
|
||||
page={table.page}
|
||||
dense={table.dense}
|
||||
count={dataFiltered.length}
|
||||
rowsPerPage={table.rowsPerPage}
|
||||
onPageChange={table.onChangePage}
|
||||
onChangeDense={table.onChangeDense}
|
||||
onRowsPerPageChange={table.onChangeRowsPerPage}
|
||||
/>
|
||||
</Card>
|
||||
</DashboardContent>
|
||||
|
||||
{renderConfirmDialog()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type ApplyFilterProps = {
|
||||
dateError: boolean;
|
||||
inputData: IOrderItem[];
|
||||
filters: IOrderTableFilters;
|
||||
comparator: (a: any, b: any) => number;
|
||||
};
|
||||
|
||||
function applyFilter({ inputData, comparator, filters, dateError }: ApplyFilterProps) {
|
||||
const { status, name, startDate, endDate } = filters;
|
||||
|
||||
const stabilizedThis = inputData.map((el, index) => [el, index] as const);
|
||||
|
||||
stabilizedThis.sort((a, b) => {
|
||||
const order = comparator(a[0], b[0]);
|
||||
if (order !== 0) return order;
|
||||
return a[1] - b[1];
|
||||
});
|
||||
|
||||
inputData = stabilizedThis.map((el) => el[0]);
|
||||
|
||||
if (name) {
|
||||
inputData = inputData.filter(({ orderNumber, customer }) =>
|
||||
[orderNumber, customer.name, customer.email].some((field) =>
|
||||
field?.toLowerCase().includes(name.toLowerCase())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (status !== 'all') {
|
||||
inputData = inputData.filter((order) => order.status === status);
|
||||
}
|
||||
|
||||
if (!dateError) {
|
||||
if (startDate && endDate) {
|
||||
inputData = inputData.filter((order) => fIsBetween(order.createdAt, startDate, endDate));
|
||||
}
|
||||
}
|
||||
|
||||
return inputData;
|
||||
}
|
71
03_source/frontend/src/types/party-order.ts
Normal file
71
03_source/frontend/src/types/party-order.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { IDatePickerControl, IDateValue } from './common';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type IOrderTableFilters = {
|
||||
name: string;
|
||||
status: string;
|
||||
endDate: IDatePickerControl;
|
||||
startDate: IDatePickerControl;
|
||||
};
|
||||
|
||||
export type IOrderHistory = {
|
||||
orderTime: IDateValue;
|
||||
paymentTime: IDateValue;
|
||||
deliveryTime: IDateValue;
|
||||
completionTime: IDateValue;
|
||||
timeline: { title: string; time: IDateValue }[];
|
||||
};
|
||||
|
||||
export type IOrderShippingAddress = {
|
||||
fullAddress: string;
|
||||
phoneNumber: string;
|
||||
};
|
||||
|
||||
export type IOrderPayment = {
|
||||
cardType: string;
|
||||
cardNumber: string;
|
||||
};
|
||||
|
||||
export type IOrderDelivery = {
|
||||
shipBy: string;
|
||||
speedy: string;
|
||||
trackingNumber: string;
|
||||
};
|
||||
|
||||
export type IOrderCustomer = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatarUrl: string;
|
||||
ipAddress: string;
|
||||
};
|
||||
|
||||
export type IOrderProductItem = {
|
||||
id: string;
|
||||
sku: string;
|
||||
name: string;
|
||||
price: number;
|
||||
coverUrl: string;
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
export type IOrderItem = {
|
||||
id: string;
|
||||
createdAt: IDateValue;
|
||||
//
|
||||
taxes: number;
|
||||
status: string;
|
||||
shipping: number;
|
||||
discount: number;
|
||||
subtotal: number;
|
||||
orderNumber: string;
|
||||
totalAmount: number;
|
||||
totalQuantity: number;
|
||||
history: IOrderHistory | undefined;
|
||||
payment: IOrderPayment;
|
||||
customer: IOrderCustomer;
|
||||
delivery: IOrderDelivery;
|
||||
items: IOrderProductItem[];
|
||||
shippingAddress: IOrderShippingAddress;
|
||||
};
|
Reference in New Issue
Block a user