Compare commits
19 Commits
develop/cm
...
a686dd55dd
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a686dd55dd | ||
![]() |
8d52be9b96 | ||
![]() |
db16b2d5dd | ||
![]() |
7370316ea0 | ||
![]() |
77f7211317 | ||
![]() |
dfc9873815 | ||
![]() |
53b112e488 | ||
![]() |
a9dd265658 | ||
![]() |
ae39b7ca67 | ||
![]() |
4c2a06585d | ||
![]() |
816d88c2c1 | ||
![]() |
843e459527 | ||
![]() |
cd0ae5ba62 | ||
![]() |
ecdbc45c4a | ||
![]() |
4b64778b59 | ||
![]() |
a88de2f17f | ||
![]() |
d987b0fe36 | ||
![]() |
0142c9ba24 | ||
![]() |
48e1f821ad |
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
tags: frontend, product, update
|
tags: frontend, party-event
|
||||||
---
|
---
|
||||||
|
|
||||||
# REQ0185 frontend product update
|
# REQ0185 frontend party-event
|
||||||
|
|
||||||
frontend page to update product
|
frontend page to handle party-event (CRUD)
|
||||||
|
|
||||||
edit page T.B.A.
|
edit page T.B.A.
|
||||||
|
|
||||||
@@ -14,4 +14,5 @@ T.B.A.
|
|||||||
|
|
||||||
## branch
|
## branch
|
||||||
|
|
||||||
|
develop/frontend/party-event/trunk
|
||||||
develop/requirements/REQ0185
|
develop/requirements/REQ0185
|
||||||
|
17
01_Requirements/REQ0186/index.md
Normal file
17
01_Requirements/REQ0186/index.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
tags: frontend, product, update
|
||||||
|
---
|
||||||
|
|
||||||
|
# REQ0185 frontend product update
|
||||||
|
|
||||||
|
frontend page to update product
|
||||||
|
|
||||||
|
edit page T.B.A.
|
||||||
|
|
||||||
|
## sources
|
||||||
|
|
||||||
|
T.B.A.
|
||||||
|
|
||||||
|
## branch
|
||||||
|
|
||||||
|
develop/requirements/REQ0185
|
23
01_Requirements/REQ0187/index.md
Normal file
23
01_Requirements/REQ0187/index.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
tags: frontend, party-order
|
||||||
|
---
|
||||||
|
|
||||||
|
# REQ0185 frontend party-order
|
||||||
|
|
||||||
|
frontend page to handle party-order (CRUD)
|
||||||
|
|
||||||
|
edit page T.B.A.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- remove detail in left nav bar
|
||||||
|
- implement `changeStatus`
|
||||||
|
|
||||||
|
## sources
|
||||||
|
|
||||||
|
T.B.A.
|
||||||
|
|
||||||
|
## branch
|
||||||
|
|
||||||
|
develop/frontend/party-order/trunk
|
||||||
|
develop/requirements/REQ0187
|
@@ -21,7 +21,7 @@
|
|||||||
"re:build-npm": "npm run clean && npm install && npm run build",
|
"re:build-npm": "npm run clean && npm install && npm run build",
|
||||||
"tsc:dev": "yarn dev & yarn tsc:watch",
|
"tsc:dev": "yarn dev & yarn tsc:watch",
|
||||||
"tsc:print": "npx tsc --showConfig",
|
"tsc:print": "npx tsc --showConfig",
|
||||||
"tsc:w": "npx nodemon --delay 3 --ext ts,tsx --exec \"yarn tsc\"",
|
"tsc:w": "npx nodemon --delay 1 --ext ts,tsx --exec \"yarn tsc\"",
|
||||||
"tsc:watch": "tsc --noEmit --watch",
|
"tsc:watch": "tsc --noEmit --watch",
|
||||||
"tsc": "tsc --noEmit",
|
"tsc": "tsc --noEmit",
|
||||||
"migrate": "npx prisma migrate dev --skip-seed",
|
"migrate": "npx prisma migrate dev --skip-seed",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"db:studio": "prisma studio",
|
"db:studio": "prisma studio",
|
||||||
"db:studio:w": "npx nodemon --delay 1 --watch prisma --ext \"prisma\" --exec \"yarn db:studio\"",
|
"db:studio:w": "npx nodemon --delay 1 --watch prisma --ext \"prisma\" --exec \"yarn db:studio\"",
|
||||||
"db:dev": "yarn db:push && yarn seed && yarn dev",
|
"db:dev": "yarn db:push && yarn seed && yarn dev",
|
||||||
"db:dev:w": "npx nodemon --delay 1 --ext \"ts,tsx,prisma\" --exec \"yarn db:dev\""
|
"db:dev:w": "npx nodemon --delay 3 --ext \"ts,tsx,prisma\" --exec \"yarn db:dev\""
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
|
@@ -1230,3 +1230,30 @@ model AccessLog {
|
|||||||
@@index([timestamp])
|
@@index([timestamp])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model PartyOrderItem {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
//
|
||||||
|
taxes Float
|
||||||
|
status String
|
||||||
|
shipping Float
|
||||||
|
discount Float
|
||||||
|
subtotal Float
|
||||||
|
orderNumber String
|
||||||
|
totalAmount Float
|
||||||
|
totalQuantity Float
|
||||||
|
history Json
|
||||||
|
payment Json
|
||||||
|
customer Json
|
||||||
|
delivery Json
|
||||||
|
items Json[]
|
||||||
|
shippingAddress Json
|
||||||
|
// OrderProductItem OrderProductItem[]
|
||||||
|
// OrderHistory OrderHistory[]
|
||||||
|
// OrderDelivery OrderDelivery[]
|
||||||
|
// OrderCustomer OrderCustomer[]
|
||||||
|
// OrderPayment OrderPayment[]
|
||||||
|
// OrderShippingAddress OrderShippingAddress[]
|
||||||
|
}
|
||||||
|
@@ -31,6 +31,7 @@ import { EventReviewSeed } from './seeds/eventReview';
|
|||||||
import { appLogSeed } from './seeds/AppLog';
|
import { appLogSeed } from './seeds/AppLog';
|
||||||
import { accessLogSeed } from './seeds/AccessLog';
|
import { accessLogSeed } from './seeds/AccessLog';
|
||||||
import { userMetaSeed } from './seeds/userMeta';
|
import { userMetaSeed } from './seeds/userMeta';
|
||||||
|
import { partyOrderItemSeed } from './seeds/partyOrderItem';
|
||||||
|
|
||||||
//
|
//
|
||||||
// import { Blog } from './seeds/blog';
|
// import { Blog } from './seeds/blog';
|
||||||
@@ -60,6 +61,8 @@ import { userMetaSeed } from './seeds/userMeta';
|
|||||||
await appLogSeed;
|
await appLogSeed;
|
||||||
await accessLogSeed;
|
await accessLogSeed;
|
||||||
|
|
||||||
|
await partyOrderItemSeed;
|
||||||
|
|
||||||
// await Blog;
|
// await Blog;
|
||||||
// await Mail;
|
// await Mail;
|
||||||
// await File;
|
// await File;
|
||||||
|
@@ -125,7 +125,7 @@ const generateRatings = () =>
|
|||||||
const generateImages = () => Array.from({ length: 8 }, (_, index) => _mock.image.event(index));
|
const generateImages = () => Array.from({ length: 8 }, (_, index) => _mock.image.event(index));
|
||||||
|
|
||||||
const _events = () =>
|
const _events = () =>
|
||||||
Array.from({ length: 2 }, (_, index) => {
|
Array.from({ length: 5 }, (_, index) => {
|
||||||
const reviews = generateReviews();
|
const reviews = generateReviews();
|
||||||
const images = generateImages();
|
const images = generateImages();
|
||||||
const ratings = generateRatings();
|
const ratings = generateRatings();
|
||||||
|
94
03_source/cms_backend/prisma/seeds/partyOrderItem.ts
Normal file
94
03_source/cms_backend/prisma/seeds/partyOrderItem.ts
Normal 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 partyOrderItem() {
|
||||||
|
await prisma.partyOrderItem.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.partyOrderItem.upsert({
|
||||||
|
where: { id: index.toString() },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: _mock.id(index),
|
||||||
|
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 partyOrderItemSeed done');
|
||||||
|
}
|
||||||
|
|
||||||
|
const partyOrderItemSeed = partyOrderItem()
|
||||||
|
.then(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
})
|
||||||
|
.catch(async (e) => {
|
||||||
|
console.error(e);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
export { partyOrderItemSeed };
|
@@ -5,12 +5,9 @@ yarn --dev
|
|||||||
clear
|
clear
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
# yarn db:push
|
|
||||||
# yarn seed
|
|
||||||
|
|
||||||
yarn db:studio &
|
yarn db:studio &
|
||||||
|
|
||||||
npx nodemon --ext ts,tsx,prisma --exec "yarn dev"
|
npx nodemon --ext ts,tsx,prisma --exec "yarn db:push && yarn seed && yarn dev"
|
||||||
# yarn dev
|
# yarn dev
|
||||||
|
|
||||||
echo "restarting..."
|
echo "restarting..."
|
||||||
|
14
03_source/cms_backend/src/app/api/_PROMPTS/clone_srevice.md
Normal file
14
03_source/cms_backend/src/app/api/_PROMPTS/clone_srevice.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Hi,
|
||||||
|
|
||||||
|
i copied from
|
||||||
|
`03_source/cms_backend/src/app/api/party-event`
|
||||||
|
to
|
||||||
|
`03_source/cms_backend/src/app/api/party-order`
|
||||||
|
|
||||||
|
with knowledge in `schema.prisma` file, and take a look into the sibling files in the same directory.
|
||||||
|
|
||||||
|
i want you to update `03_source/cms_backend/src/app/api/party-order` content to handle `party-order` (the purchase order of the party)
|
||||||
|
`party-order.service.ts` is already prepared and you can use it
|
||||||
|
please maintain same format and level of detail when you edit.
|
||||||
|
|
||||||
|
thanks.
|
35
03_source/cms_backend/src/app/api/party-event/_GUIDELINES.md
Normal file
35
03_source/cms_backend/src/app/api/party-event/_GUIDELINES.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<!-- AI: please maintain same format and level of detail when edit this file -->
|
||||||
|
|
||||||
|
# GUIDELINE
|
||||||
|
|
||||||
|
- Party Event API endpoint for managing party events
|
||||||
|
- Handles CRUD operations for party events
|
||||||
|
- Follows single file for single db table/collection pattern
|
||||||
|
|
||||||
|
## `route.ts`
|
||||||
|
|
||||||
|
Handles HTTP methods:
|
||||||
|
|
||||||
|
- `GET` - Retrieve party events
|
||||||
|
- `POST` - Create new party event
|
||||||
|
- `PUT` - Update existing party event
|
||||||
|
- `DELETE` - Remove party event
|
||||||
|
|
||||||
|
## `test.http`
|
||||||
|
|
||||||
|
Contains test requests for:
|
||||||
|
|
||||||
|
- Listing all party events
|
||||||
|
- Creating new party event
|
||||||
|
- Updating existing party event
|
||||||
|
- Deleting party event
|
||||||
|
|
||||||
|
## `../../services/party-event.service.ts`
|
||||||
|
|
||||||
|
Party Event CRUD operations:
|
||||||
|
|
||||||
|
`listPartyEvents` - List all party events with optional filters
|
||||||
|
`getPartyEvent` - Get single party event by ID
|
||||||
|
`createNewPartyEvent` - Create new party event record
|
||||||
|
`updatePartyEvent` - Update existing party event by ID
|
||||||
|
`deletePartyEvent` - Delete party event by ID
|
@@ -9,13 +9,13 @@ import { deleteEvent } from 'src/app/services/party-event.service';
|
|||||||
*************************************** */
|
*************************************** */
|
||||||
export async function PATCH(req: NextRequest) {
|
export async function PATCH(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { eventId } = await req.json();
|
const { partyEventId } = await req.json();
|
||||||
|
|
||||||
if (!eventId) throw new Error('eventId cannot be null');
|
if (!partyEventId) throw new Error('partyEventId cannot be null');
|
||||||
|
|
||||||
await deleteEvent(eventId);
|
await deleteEvent(partyEventId);
|
||||||
|
|
||||||
return response({ eventId }, STATUS.OK);
|
return response({ partyEventId }, STATUS.OK);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleError('PartyEvent - Delete', error);
|
return handleError('PartyEvent - Delete', error);
|
||||||
}
|
}
|
||||||
|
@@ -4,5 +4,5 @@ PATCH http://localhost:7272/api/party-event/delete
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"eventId": "e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01"
|
"partyEventId": "e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01"
|
||||||
}
|
}
|
||||||
|
@@ -31,23 +31,23 @@ export async function GET(req: NextRequest) {
|
|||||||
const { searchParams } = req.nextUrl;
|
const { searchParams } = req.nextUrl;
|
||||||
|
|
||||||
// RULES: eventId must exist
|
// RULES: eventId must exist
|
||||||
const eventId = searchParams.get('eventId');
|
const partyEventId = searchParams.get('partyEventId');
|
||||||
if (!eventId) {
|
if (!partyEventId) {
|
||||||
return response({ message: 'Event ID is required!' }, STATUS.BAD_REQUEST);
|
return response({ message: 'PartyEvent ID is required!' }, STATUS.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: eventId confirmed exist, run below
|
// NOTE: eventId confirmed exist, run below
|
||||||
const event = await getEvent(eventId);
|
const partyEvent = await getEvent(partyEventId);
|
||||||
|
|
||||||
if (!event) {
|
if (!partyEvent) {
|
||||||
return response({ message: 'Event not found!' }, STATUS.NOT_FOUND);
|
return response({ message: 'PartyEvent not found!' }, STATUS.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger('[PartyEvent] details', event.id);
|
logger('[PartyEvent] details', partyEvent.id);
|
||||||
|
|
||||||
createAppLog(L_INFO, 'Get event detail OK', debug);
|
createAppLog(L_INFO, 'Get event detail OK', debug);
|
||||||
|
|
||||||
return response({ event }, STATUS.OK);
|
return response({ partyEvent }, STATUS.OK);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
createAppLog(L_ERROR, 'event detail error', debug);
|
createAppLog(L_ERROR, 'event detail error', debug);
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
###
|
###
|
||||||
|
|
||||||
# Get details for a specific party event
|
# Get details for a specific party event
|
||||||
GET http://localhost:7272/api/party-event/details?eventId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01
|
GET http://localhost:7272/api/party-event/details?partyEventId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
|
@@ -25,11 +25,11 @@ export async function GET(req: NextRequest) {
|
|||||||
const debug = { 'req.headers': flattenNextjsRequest(req) };
|
const debug = { 'req.headers': flattenNextjsRequest(req) };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const events = await listEvents();
|
const partyEvents = await listEvents();
|
||||||
|
|
||||||
createAppLog(L_INFO, 'party-event list ok', {});
|
createAppLog(L_INFO, 'party-event list ok', {});
|
||||||
|
|
||||||
return response({ events }, STATUS.OK);
|
return response({ partyEvents }, STATUS.OK);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
createAppLog(L_ERROR, 'party-event list error', debug);
|
createAppLog(L_ERROR, 'party-event list error', debug);
|
||||||
|
|
||||||
|
@@ -6,20 +6,15 @@ Content-Type: application/json
|
|||||||
{
|
{
|
||||||
"partyEventData": {
|
"partyEventData": {
|
||||||
"id":"e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01",
|
"id":"e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01",
|
||||||
"title": "Summer Music Festival",
|
"title": "Summer Music Festival 111",
|
||||||
|
"name": "Summer Music Festival 111",
|
||||||
"description": "Annual summer music festival featuring local bands and artists",
|
"description": "Annual summer music festival featuring local bands and artists",
|
||||||
"startDate": "2024-07-15T18:00:00Z",
|
"startDate": "2024-07-15T18:00:00Z",
|
||||||
"endDate": "2024-07-15T23:00:00Z",
|
"endDate": "2024-07-15T23:00:00Z",
|
||||||
"location": "Central Park, Hong Kong",
|
"location": "Central Park, Hong Kong",
|
||||||
"coverUrl": "",
|
"coverUrl": "",
|
||||||
"images": [
|
"images": [ "data:image/png;base64,C", "data:image/png;base64,C" ],
|
||||||
"data:image/png;base64,C",
|
"tags": [ "Music", "Festival" ],
|
||||||
"data:image/png;base64,C"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Music",
|
|
||||||
"Festival"
|
|
||||||
],
|
|
||||||
"status": "upcoming",
|
"status": "upcoming",
|
||||||
"capacity": 500,
|
"capacity": 500,
|
||||||
"price": 150.00,
|
"price": 150.00,
|
||||||
@@ -30,6 +25,7 @@ Content-Type: application/json
|
|||||||
"requirements": "Age 18+",
|
"requirements": "Age 18+",
|
||||||
"schedule": "18:00 Doors open\n19:00 First performance\n21:00 Main act",
|
"schedule": "18:00 Doors open\n19:00 First performance\n21:00 Main act",
|
||||||
"speakers": ["DJ Lee", "Band XYZ"],
|
"speakers": ["DJ Lee", "Band XYZ"],
|
||||||
"sponsors": ["HK Radio", "Music Magazine"]
|
"sponsors": ["HK Radio", "Music Magazine"],
|
||||||
|
"reviews":[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
35
03_source/cms_backend/src/app/api/party-order/_GUIDELINES.md
Normal file
35
03_source/cms_backend/src/app/api/party-order/_GUIDELINES.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<!-- AI: please maintain same format and level of detail when edit this file -->
|
||||||
|
|
||||||
|
# GUIDELINE
|
||||||
|
|
||||||
|
- Party Order API endpoint for managing party orders
|
||||||
|
- Handles CRUD operations for party orders
|
||||||
|
- Follows single file for single db table/collection pattern
|
||||||
|
|
||||||
|
## `route.ts`
|
||||||
|
|
||||||
|
Handles HTTP methods:
|
||||||
|
|
||||||
|
- `GET` - Retrieve party orders
|
||||||
|
- `POST` - Create new party order
|
||||||
|
- `PUT` - Update existing party order
|
||||||
|
- `DELETE` - Remove party order
|
||||||
|
|
||||||
|
## `test.http`
|
||||||
|
|
||||||
|
Contains test requests for:
|
||||||
|
|
||||||
|
- Listing all party orders
|
||||||
|
- Creating new party order
|
||||||
|
- Updating existing party order
|
||||||
|
- Deleting party order
|
||||||
|
|
||||||
|
## `../../services/party-order.service.ts`
|
||||||
|
|
||||||
|
Party Order CRUD operations:
|
||||||
|
|
||||||
|
`listPartyOrders` - List all party orders with optional filters
|
||||||
|
`getPartyOrder` - Get single party order by ID
|
||||||
|
`createNewPartyOrder` - Create new party order record
|
||||||
|
`updatePartyOrder` - Update existing party order by ID
|
||||||
|
`deletePartyOrder` - Delete party order by ID
|
@@ -0,0 +1,40 @@
|
|||||||
|
// src/app/api/party-order/create/route.ts
|
||||||
|
//
|
||||||
|
// PURPOSE:
|
||||||
|
// Create new party order in db
|
||||||
|
//
|
||||||
|
// RULES:
|
||||||
|
// T.B.A.
|
||||||
|
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
import { isDev } from 'src/constants';
|
||||||
|
import { createOrder } from 'src/app/services/party-order.service';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* POST - Create PartyOrder
|
||||||
|
*************************************** */
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const { partyOrderData } = await req.json();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isDev) {
|
||||||
|
console.log({ partyOrderData });
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await createOrder(partyOrderData);
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
console.log('Order created successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(created, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating order:', { partyOrderData });
|
||||||
|
return handleError('PartyOrder - Create', error);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost:7272/api/party-order/create
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"partyOrderData": {
|
||||||
|
"taxes": 15.00,
|
||||||
|
"status": "pending",
|
||||||
|
"shipping": 10.00,
|
||||||
|
"discount": 20.00,
|
||||||
|
"subtotal": 290.00,
|
||||||
|
"orderNumber": "ORD_20231115001",
|
||||||
|
"totalAmount": 300.00,
|
||||||
|
"totalQuantity": 2.0,
|
||||||
|
"history": "{\"actions\":[{\"timestamp\":\"2023-11-15T10:00:00Z\",\"action\":\"order_created\"}]}",
|
||||||
|
"payment": "{\"method\":\"credit_card\",\"status\":\"unpaid\",\"transaction_id\":\"txn_123456\"}",
|
||||||
|
"customer": "{\"name\":\"John Doe\",\"email\":\"john.doe@example.com\",\"phone\":\"+1234567890\"}",
|
||||||
|
"delivery": "{\"method\":\"courier\",\"estimated_delivery\":\"2023-11-18\"}",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "ticket_001",
|
||||||
|
"name": "General Admission",
|
||||||
|
"quantity": 2,
|
||||||
|
"price": 150.00
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shippingAddress": "{\"street\":\"123 Main St\",\"city\":\"New York\",\"state\":\"NY\",\"zip\":\"10001\",\"country\":\"USA\"}"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
import { deleteOrder } from 'src/app/services/party-order.service';
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* PATCH - Delete PartyOrder
|
||||||
|
*************************************** */
|
||||||
|
export async function PATCH(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { partyOrderId } = await req.json();
|
||||||
|
|
||||||
|
if (!partyOrderId) throw new Error('orderId cannot be null');
|
||||||
|
|
||||||
|
await deleteOrder(partyOrderId);
|
||||||
|
|
||||||
|
return response({ partyOrderId }, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError('PartyOrder - Delete', error);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
###
|
||||||
|
|
||||||
|
PATCH http://localhost:7272/api/party-order/delete
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"partyOrderId": "e99f09a7-dd88-49d5-b1c8-1daf80c2d7b02"
|
||||||
|
}
|
@@ -0,0 +1,56 @@
|
|||||||
|
// src/app/api/party-order/details/route.ts
|
||||||
|
//
|
||||||
|
// PURPOSE:
|
||||||
|
// Get party order from db by id
|
||||||
|
//
|
||||||
|
// RULES:
|
||||||
|
// Do not modify the format and level of details thanks.
|
||||||
|
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
import { logger } from 'src/utils/logger';
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
import { L_INFO, L_ERROR } from 'src/constants';
|
||||||
|
import { getOrder } from 'src/app/services/party-order.service';
|
||||||
|
import { createAppLog } from 'src/app/services/app-log.service';
|
||||||
|
|
||||||
|
import { flattenNextjsRequest } from '../../auth/sign-in/flattenNextjsRequest';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
**************************************
|
||||||
|
* GET PartyOrder detail
|
||||||
|
***************************************
|
||||||
|
*/
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const debug = { 'req.headers': flattenNextjsRequest(req) };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { searchParams } = req.nextUrl;
|
||||||
|
|
||||||
|
// RULES: partyOrderId must exist
|
||||||
|
const partyOrderId = searchParams.get('partyOrderId');
|
||||||
|
if (!partyOrderId) {
|
||||||
|
return response({ message: 'Order ID is required!' }, STATUS.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: partyOrderId confirmed exist, run below
|
||||||
|
const partyOrder = await getOrder(partyOrderId);
|
||||||
|
|
||||||
|
if (!partyOrder) {
|
||||||
|
return response({ message: 'Order not found!' }, STATUS.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger('[PartyOrder] details', partyOrder.id);
|
||||||
|
|
||||||
|
createAppLog(L_INFO, 'Get order detail OK', debug);
|
||||||
|
|
||||||
|
return response({ partyOrder }, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
createAppLog(L_ERROR, 'order detail error', debug);
|
||||||
|
|
||||||
|
return handleError('PartyOrder - Get details', error);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
###
|
||||||
|
|
||||||
|
# Get details for a specific party order
|
||||||
|
GET http://localhost:7272/api/party-order/details?partyOrderId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b13
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# Alternative format with different ID
|
||||||
|
GET http://localhost:7272/api/party-order/details?id=ord_987654321
|
@@ -0,0 +1,16 @@
|
|||||||
|
import type { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
/**
|
||||||
|
***************************************
|
||||||
|
* GET - helloworld
|
||||||
|
***************************************
|
||||||
|
*/
|
||||||
|
export async function GET(req: NextRequest, res: NextResponse) {
|
||||||
|
try {
|
||||||
|
return response({ helloworld: 'party-order' }, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError('Helloworld - Get all', error);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,2 @@
|
|||||||
|
###
|
||||||
|
GET http://localhost:7272/api/party-order/helloworld
|
38
03_source/cms_backend/src/app/api/party-order/list/route.ts
Normal file
38
03_source/cms_backend/src/app/api/party-order/list/route.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// src/app/api/party-order/list/route.ts
|
||||||
|
//
|
||||||
|
// PURPOSE:
|
||||||
|
// List all party orders from db
|
||||||
|
//
|
||||||
|
// RULES:
|
||||||
|
// T.B.A.
|
||||||
|
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
import { L_INFO, L_ERROR } from 'src/constants';
|
||||||
|
import { createAppLog } from 'src/app/services/app-log.service';
|
||||||
|
import { listPartyOrders } from 'src/app/services/party-order.service';
|
||||||
|
|
||||||
|
import { flattenNextjsRequest } from '../../auth/sign-in/flattenNextjsRequest';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* GET - PartyOrders list
|
||||||
|
*************************************** */
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const debug = { 'req.headers': flattenNextjsRequest(req) };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const partyOrders = await listPartyOrders();
|
||||||
|
|
||||||
|
createAppLog(L_INFO, 'party-order list ok', {});
|
||||||
|
|
||||||
|
return response({ partyOrders }, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
createAppLog(L_ERROR, 'party-order list error', debug);
|
||||||
|
|
||||||
|
return handleError('PartyOrder - Get list', error);
|
||||||
|
}
|
||||||
|
}
|
19
03_source/cms_backend/src/app/api/party-order/list/test.http
Normal file
19
03_source/cms_backend/src/app/api/party-order/list/test.http
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
###
|
||||||
|
|
||||||
|
# Basic list all orders
|
||||||
|
GET http://localhost:7272/api/party-order/list
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# List orders by status
|
||||||
|
GET http://localhost:7272/api/party-order/list?status=completed
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# List orders by user
|
||||||
|
GET http://localhost:7272/api/party-order/list?userId=usr_987654321
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# List orders by payment status
|
||||||
|
GET http://localhost:7272/api/party-order/list?paymentStatus=paid
|
75
03_source/cms_backend/src/app/api/party-order/route.ts
Normal file
75
03_source/cms_backend/src/app/api/party-order/route.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import type { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
import { listPartyOrders, deleteOrder, updateOrder, createOrder } from 'src/app/services/party-order.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
**************************************
|
||||||
|
* GET - PartyOrder
|
||||||
|
***************************************
|
||||||
|
*/
|
||||||
|
export async function GET(req: NextRequest, res: NextResponse) {
|
||||||
|
try {
|
||||||
|
const orders = await listPartyOrders();
|
||||||
|
return response(orders, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError('PartyOrder - Get list', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
***************************************
|
||||||
|
* POST - Create PartyOrder
|
||||||
|
***************************************
|
||||||
|
*/
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const OPERATION = 'PartyOrder - Create';
|
||||||
|
const { data } = await req.json();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const order = await createOrder(data);
|
||||||
|
return response(OPERATION, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(OPERATION, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
***************************************
|
||||||
|
* PUT - Update PartyOrder
|
||||||
|
***************************************
|
||||||
|
*/
|
||||||
|
export async function PUT(req: NextRequest) {
|
||||||
|
const { searchParams } = req.nextUrl;
|
||||||
|
const orderId = searchParams.get('orderId');
|
||||||
|
const { data } = await req.json();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!orderId) throw new Error('orderId cannot be null');
|
||||||
|
|
||||||
|
const result = await updateOrder(orderId, data);
|
||||||
|
return response(result, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError('PartyOrder - Update', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
***************************************
|
||||||
|
* DELETE - Delete PartyOrder
|
||||||
|
***************************************
|
||||||
|
*/
|
||||||
|
export async function DELETE(req: NextRequest) {
|
||||||
|
const { searchParams } = req.nextUrl;
|
||||||
|
const orderId = searchParams.get('orderId');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!orderId) throw new Error('orderId cannot be null');
|
||||||
|
|
||||||
|
await deleteOrder(orderId);
|
||||||
|
return response({ success: true }, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError('PartyOrder - Delete', error);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
import { logger } from 'src/utils/logger';
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
import type { IOrderItem } from '../update/route';
|
||||||
|
|
||||||
|
// import { searchEvents } from 'src/app/services/party-event.service';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* GET - Search PartyEvents
|
||||||
|
*************************************** */
|
||||||
|
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 results: IOrderItem[] = [];
|
||||||
|
// TODO: search party event not implemented
|
||||||
|
console.log('search party event not implemented');
|
||||||
|
// const results = await searchEvents(query);
|
||||||
|
|
||||||
|
logger('[PartyEvent] search-results', results?.length);
|
||||||
|
|
||||||
|
return response({ results }, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError('PartyEvent - Get search', error);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,24 @@
|
|||||||
|
###
|
||||||
|
|
||||||
|
# Search party events by title
|
||||||
|
GET http://localhost:7272/api/party-event/search?query=Music
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# Search party events by location
|
||||||
|
GET http://localhost:7272/api/party-event/search?query=Central+Park
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# Search party events by tag
|
||||||
|
GET http://localhost:7272/api/party-event/search?query=Festival
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# Combined search with multiple parameters
|
||||||
|
GET http://localhost:7272/api/party-event/search?query=Summer&location=Hong+Kong&category=Music
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# No results expected
|
||||||
|
GET http://localhost:7272/api/party-event/search?query=zzzzzz
|
@@ -0,0 +1,51 @@
|
|||||||
|
// src/app/api/party-order/update/route.ts
|
||||||
|
//
|
||||||
|
// PURPOSE:
|
||||||
|
// Update party order in db by id
|
||||||
|
//
|
||||||
|
// RULES:
|
||||||
|
// T.B.A.
|
||||||
|
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
import { updateOrder } from 'src/app/services/party-order.service';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* PUT - Update PartyOrder
|
||||||
|
*************************************** */
|
||||||
|
export async function PUT(req: NextRequest) {
|
||||||
|
const { orderData } = await req.json();
|
||||||
|
const { id } = orderData;
|
||||||
|
|
||||||
|
if (!id) return response({ message: 'id not found' }, STATUS.ERROR);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await updateOrder(id, orderData);
|
||||||
|
|
||||||
|
return response({ result }, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating order:', { orderData });
|
||||||
|
return handleError('PartyOrder - Update', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IOrderItem = {
|
||||||
|
id: string;
|
||||||
|
eventId: string;
|
||||||
|
userId: string;
|
||||||
|
status: string;
|
||||||
|
paymentStatus: string;
|
||||||
|
totalAmount: number;
|
||||||
|
items: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
quantity: number;
|
||||||
|
price: number;
|
||||||
|
}[];
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
@@ -0,0 +1,51 @@
|
|||||||
|
###
|
||||||
|
|
||||||
|
PUT http://localhost:7272/api/party-order/update
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"orderData": {
|
||||||
|
"id":"e99f09a7-dd88-49d5-b1c8-1daf80c2d7b02",
|
||||||
|
"taxes": 10.50,
|
||||||
|
"status": "completed",
|
||||||
|
"shipping": 5.00,
|
||||||
|
"discount": 20.00,
|
||||||
|
"subtotal": 290.00,
|
||||||
|
"orderNumber": "ORD-2023-001",
|
||||||
|
"totalAmount": 300.00,
|
||||||
|
"totalQuantity": 2,
|
||||||
|
"history": {
|
||||||
|
"payment": "2023-01-01",
|
||||||
|
"status_changes": [
|
||||||
|
"pending",
|
||||||
|
"paid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"payment": {
|
||||||
|
"method": "credit_card",
|
||||||
|
"card_last4": "4321"
|
||||||
|
},
|
||||||
|
"customer": {
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com"
|
||||||
|
},
|
||||||
|
"delivery": {
|
||||||
|
"method": "express",
|
||||||
|
"tracking_number": "TRK123456"
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "ticket_001",
|
||||||
|
"name": "General Admission",
|
||||||
|
"quantity": 2,
|
||||||
|
"price": 150.00,
|
||||||
|
"status": "used"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shippingAddress": {
|
||||||
|
"street": "123 Main St",
|
||||||
|
"city": "New York",
|
||||||
|
"zip": "10001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,13 @@
|
|||||||
with knowledge in schema.prisma file,
|
Hi,
|
||||||
please refer the below helloworld example `helloworld.service.ts`
|
|
||||||
and create `user.service.ts` to cover user record
|
|
||||||
|
|
||||||
thanks
|
i copied from
|
||||||
|
`03_source/cms_backend/src/app/services/party-event.service.ts`
|
||||||
|
to
|
||||||
|
`03_source/cms_backend/src/app/services/party-order.service.ts`
|
||||||
|
|
||||||
`/home/logic/_wsl_workspace/001_github_ws/HKSingleParty-ws/HKSingleParty/03_source/cms_backend/src/app/services/helloworld.service.ts`
|
with knowledge in `schema.prisma` file, and reference to the sibling files in same folder
|
||||||
|
|
||||||
|
i want you to update `party-order.service.ts` content to handle party order (the purchase order of the party)
|
||||||
|
please use the model `PartyOrderItem` to handle it.
|
||||||
|
|
||||||
|
thanks.
|
||||||
|
@@ -76,10 +76,57 @@ async function createEvent(eventData: any) {
|
|||||||
return await prisma.eventItem.create({ data: eventData });
|
return await prisma.eventItem.create({ data: eventData });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateEvent(eventId: string, updateForm: UpdateEvent) {
|
async function updateEvent(eventId: string, updateForm: any) {
|
||||||
return prisma.eventItem.update({
|
return prisma.eventItem.update({
|
||||||
where: { id: eventId },
|
where: { id: eventId },
|
||||||
data: updateForm,
|
data: {
|
||||||
|
available: updateForm.available,
|
||||||
|
category: updateForm.category,
|
||||||
|
code: updateForm.code,
|
||||||
|
colors: updateForm.colors,
|
||||||
|
coverUrl: updateForm.coverUrl,
|
||||||
|
description: updateForm.description,
|
||||||
|
gender: updateForm.gender,
|
||||||
|
images: updateForm.images,
|
||||||
|
inventoryType: updateForm.inventoryType,
|
||||||
|
name: updateForm.name,
|
||||||
|
newLabel: updateForm.newLabel,
|
||||||
|
price: updateForm.price,
|
||||||
|
priceSale: updateForm.priceSale,
|
||||||
|
publish: updateForm.publish,
|
||||||
|
quantity: updateForm.quantity,
|
||||||
|
ratings: updateForm.ratings,
|
||||||
|
saleLabel: updateForm.saleLabel,
|
||||||
|
sizes: updateForm.sizes,
|
||||||
|
sku: updateForm.sku,
|
||||||
|
subDescription: updateForm.subDescription,
|
||||||
|
tags: updateForm.tags,
|
||||||
|
taxes: updateForm.taxes,
|
||||||
|
totalRatings: updateForm.totalRatings,
|
||||||
|
totalReviews: updateForm.totalReviews,
|
||||||
|
totalSold: updateForm.totalSold,
|
||||||
|
//
|
||||||
|
ageBottom: updateForm.ageBottom,
|
||||||
|
ageTop: updateForm.ageTop,
|
||||||
|
avatar: updateForm.avatar,
|
||||||
|
currency: updateForm.currency,
|
||||||
|
capacity: updateForm.capacity,
|
||||||
|
duration_m: updateForm.duration_m,
|
||||||
|
endDate: updateForm.endDate,
|
||||||
|
eventDate: updateForm.eventDate,
|
||||||
|
isFeatured: updateForm.isFeatured,
|
||||||
|
joinMembers: updateForm.joinMembers,
|
||||||
|
location: updateForm.location,
|
||||||
|
organizer: updateForm.organizer,
|
||||||
|
registrationDeadline: updateForm.registrationDeadline,
|
||||||
|
requirements: updateForm.requirements,
|
||||||
|
schedule: updateForm.schedule,
|
||||||
|
speakers: updateForm.speakers,
|
||||||
|
sponsors: updateForm.sponsors,
|
||||||
|
startDate: updateForm.startDate,
|
||||||
|
status: updateForm.status,
|
||||||
|
title: updateForm.title,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,66 @@
|
|||||||
|
// src/app/services/party-order.service.ts
|
||||||
|
//
|
||||||
|
// PURPOSE:
|
||||||
|
// - Service for handling PartyOrderItem Record
|
||||||
|
//
|
||||||
|
|
||||||
|
import type { PartyOrderItem } from '@prisma/client';
|
||||||
|
|
||||||
|
import prisma from '../lib/prisma';
|
||||||
|
|
||||||
|
type CreateOrder = {
|
||||||
|
status: string;
|
||||||
|
taxes: number;
|
||||||
|
shipping: number;
|
||||||
|
discount: number;
|
||||||
|
items: any[];
|
||||||
|
customer: any;
|
||||||
|
payment: any;
|
||||||
|
delivery: any;
|
||||||
|
shippingAddress: any;
|
||||||
|
billingAddress: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UpdateOrder = {
|
||||||
|
status?: string;
|
||||||
|
taxes?: number;
|
||||||
|
shipping?: number;
|
||||||
|
discount?: number;
|
||||||
|
items?: any[];
|
||||||
|
customer?: any;
|
||||||
|
payment?: any;
|
||||||
|
delivery?: any;
|
||||||
|
shippingAddress?: any;
|
||||||
|
billingAddress?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function listPartyOrders(): Promise<PartyOrderItem[]> {
|
||||||
|
return prisma.partyOrderItem.findMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPartyOrder(partyOrderId: string): Promise<PartyOrderItem | null> {
|
||||||
|
return prisma.partyOrderItem.findUnique({
|
||||||
|
where: { id: partyOrderId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createOrder(orderData: any) {
|
||||||
|
return prisma.partyOrderItem.create({
|
||||||
|
data: orderData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateOrder(orderId: string, updateData: UpdateOrder) {
|
||||||
|
return prisma.partyOrderItem.update({
|
||||||
|
where: { id: orderId },
|
||||||
|
data: updateData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteOrder(orderId: string) {
|
||||||
|
return prisma.partyOrderItem.delete({
|
||||||
|
where: { id: orderId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getPartyOrder as getOrder, createOrder, updateOrder, deleteOrder, listPartyOrders, type CreateOrder, type UpdateOrder };
|
@@ -23,7 +23,7 @@
|
|||||||
"re:build-npm": "npm run clean && npm install && npm run build",
|
"re:build-npm": "npm run clean && npm install && npm run build",
|
||||||
"tsc:dev": "yarn dev & yarn tsc:watch",
|
"tsc:dev": "yarn dev & yarn tsc:watch",
|
||||||
"tsc:print": "npx tsc --showConfig",
|
"tsc:print": "npx tsc --showConfig",
|
||||||
"tsc:w": "npx nodemon --delay 3 --ext ts,tsx --exec \"yarn tsc\"",
|
"tsc:w": "npx nodemon --delay 1 --ext ts,tsx --exec \"yarn tsc\"",
|
||||||
"tsc:watch": "tsc --noEmit --watch",
|
"tsc:watch": "tsc --noEmit --watch",
|
||||||
"tsc": "tsc --noEmit"
|
"tsc": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
70
03_source/frontend/src/_mock/_party-event.ts
Normal file
70
03_source/frontend/src/_mock/_party-event.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
export const PARTY_EVENT_GENDER_OPTIONS = [
|
||||||
|
{ label: 'Men', value: 'Men' },
|
||||||
|
{ label: 'Women', value: 'Women' },
|
||||||
|
{ label: 'Kids', value: 'Kids' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PARTY_EVENT_CATEGORY_OPTIONS = ['Shose', 'Apparel', 'Accessories'];
|
||||||
|
|
||||||
|
export const PARTY_EVENT_RATING_OPTIONS = ['up4Star', 'up3Star', 'up2Star', 'up1Star'];
|
||||||
|
|
||||||
|
export const PARTY_EVENT_COLOR_OPTIONS = [
|
||||||
|
'#FF4842',
|
||||||
|
'#1890FF',
|
||||||
|
'#FFC0CB',
|
||||||
|
'#00AB55',
|
||||||
|
'#FFC107',
|
||||||
|
'#7F00FF',
|
||||||
|
'#000000',
|
||||||
|
'#FFFFFF',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PARTY_EVENT_COLOR_NAME_OPTIONS = [
|
||||||
|
{ value: '#FF4842', label: 'Red' },
|
||||||
|
{ value: '#1890FF', label: 'Blue' },
|
||||||
|
{ value: '#FFC0CB', label: 'Pink' },
|
||||||
|
{ value: '#00AB55', label: 'Green' },
|
||||||
|
{ value: '#FFC107', label: 'Yellow' },
|
||||||
|
{ value: '#7F00FF', label: 'Violet' },
|
||||||
|
{ value: '#000000', label: 'Black' },
|
||||||
|
{ value: '#FFFFFF', label: 'White' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PARTY_EVENT_SIZE_OPTIONS = [
|
||||||
|
{ value: '7', label: '7' },
|
||||||
|
{ value: '8', label: '8' },
|
||||||
|
{ value: '8.5', label: '8.5' },
|
||||||
|
{ value: '9', label: '9' },
|
||||||
|
{ value: '9.5', label: '9.5' },
|
||||||
|
{ value: '10', label: '10' },
|
||||||
|
{ value: '10.5', label: '10.5' },
|
||||||
|
{ value: '11', label: '11' },
|
||||||
|
{ value: '11.5', label: '11.5' },
|
||||||
|
{ value: '12', label: '12' },
|
||||||
|
{ value: '13', label: '13' },
|
||||||
|
];
|
||||||
|
|
||||||
|
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 PARTY_EVENT_PUBLISH_OPTIONS = [
|
||||||
|
{ value: 'published', label: 'Published' },
|
||||||
|
{ value: 'draft', label: 'Draft' },
|
||||||
|
];
|
||||||
|
|
||||||
|
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 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 './_overview';
|
||||||
|
|
||||||
export * from './_calendar';
|
export * from './_calendar';
|
||||||
|
|
||||||
|
export * from './_party-event';
|
||||||
|
|
||||||
|
export * from './_party-order';
|
||||||
|
178
03_source/frontend/src/actions/party-event.ts
Normal file
178
03_source/frontend/src/actions/party-event.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// src/actions/product.ts
|
||||||
|
//
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
|
||||||
|
import type { IPartyEventItem } from 'src/types/party-event';
|
||||||
|
import type { SWRConfiguration } from 'swr';
|
||||||
|
import useSWR, { mutate } from 'swr';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const swrOptions: SWRConfiguration = {
|
||||||
|
revalidateIfStale: false,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type PartyEventsData = {
|
||||||
|
partyEvents: IPartyEventItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useGetPartyEvents() {
|
||||||
|
const url = endpoints.partyEvent.list;
|
||||||
|
|
||||||
|
const { data, isLoading, error, isValidating } = useSWR<PartyEventsData>(
|
||||||
|
url,
|
||||||
|
fetcher,
|
||||||
|
swrOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
const memoizedValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
partyEvents: data?.partyEvents || [],
|
||||||
|
partyEventsLoading: isLoading,
|
||||||
|
partyEventsError: error,
|
||||||
|
partyEventsValidating: isValidating,
|
||||||
|
partyEventsEmpty: !isLoading && !isValidating && !data?.partyEvents.length,
|
||||||
|
}),
|
||||||
|
[data?.partyEvents, error, isLoading, isValidating]
|
||||||
|
);
|
||||||
|
|
||||||
|
return memoizedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type PartyEventData = {
|
||||||
|
partyEvent: IPartyEventItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useGetPartyEvent(partyEventId: string) {
|
||||||
|
const url = partyEventId ? [endpoints.partyEvent.details, { params: { partyEventId } }] : '';
|
||||||
|
|
||||||
|
const { data, isLoading, error, isValidating } = useSWR<PartyEventData>(url, fetcher, swrOptions);
|
||||||
|
|
||||||
|
const memoizedValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
partyEvent: data?.partyEvent,
|
||||||
|
partyEventLoading: isLoading,
|
||||||
|
partyEventError: error,
|
||||||
|
partyEventValidating: isValidating,
|
||||||
|
mutate,
|
||||||
|
}),
|
||||||
|
[data?.partyEvent, error, isLoading, isValidating]
|
||||||
|
);
|
||||||
|
|
||||||
|
return memoizedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type SearchResultsData = {
|
||||||
|
results: IPartyEventItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function createPartyEvent(partyEventData: IPartyEventItem) {
|
||||||
|
/**
|
||||||
|
* Work on server
|
||||||
|
*/
|
||||||
|
const data = { partyEventData };
|
||||||
|
const {
|
||||||
|
data: { id },
|
||||||
|
} = await axiosInstance.post(endpoints.partyEvent.create, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Work in local
|
||||||
|
*/
|
||||||
|
mutate(
|
||||||
|
endpoints.partyEvent.list,
|
||||||
|
(currentData: any) => {
|
||||||
|
const currentPartyEvents: IPartyEventItem[] = currentData?.partyEvents;
|
||||||
|
|
||||||
|
const partyEvents = [...currentPartyEvents, { ...partyEventData, id }];
|
||||||
|
|
||||||
|
return { ...currentData, partyEvents };
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function updatePartyEvent(partyEventData: Partial<IPartyEventItem>) {
|
||||||
|
/**
|
||||||
|
* Work on server
|
||||||
|
*/
|
||||||
|
const data = { partyEventData };
|
||||||
|
await axiosInstance.put(endpoints.partyEvent.update, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Work in local
|
||||||
|
*/
|
||||||
|
|
||||||
|
mutate(
|
||||||
|
endpoints.partyEvent.list,
|
||||||
|
(currentData: any) => {
|
||||||
|
const currentPartyEvents: IPartyEventItem[] = currentData?.partyEvents;
|
||||||
|
|
||||||
|
const partyEvents = currentPartyEvents.map((partyEvent) =>
|
||||||
|
partyEvent.id === partyEventData.id ? { ...partyEvent, ...partyEventData } : partyEvent
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...currentData, partyEvents };
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function deletePartyEvent(partyEventId: string) {
|
||||||
|
/**
|
||||||
|
* Work on server
|
||||||
|
*/
|
||||||
|
const data = { partyEventId };
|
||||||
|
await axiosInstance.patch(endpoints.partyEvent.delete, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Work in local
|
||||||
|
*/
|
||||||
|
|
||||||
|
mutate(
|
||||||
|
endpoints.partyEvent.list,
|
||||||
|
(currentData: any) => {
|
||||||
|
const currentProducts: IPartyEventItem[] = currentData?.partyEvents;
|
||||||
|
|
||||||
|
const partyEvents = currentProducts.filter((partyEvent) => partyEvent.id !== partyEventId);
|
||||||
|
|
||||||
|
return { ...currentData, partyEvents };
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
208
03_source/frontend/src/actions/party-order.ts
Normal file
208
03_source/frontend/src/actions/party-order.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
// src/actions/party-order.ts
|
||||||
|
//
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
|
||||||
|
import type { IPartyOrderItem } from 'src/types/party-order';
|
||||||
|
import type { SWRConfiguration } from 'swr';
|
||||||
|
import useSWR, { mutate } from 'swr';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const swrOptions: SWRConfiguration = {
|
||||||
|
revalidateIfStale: false,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type PartyOrdersData = {
|
||||||
|
partyOrders: IPartyOrderItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useGetPartyOrders() {
|
||||||
|
const url = endpoints.partyOrder.list;
|
||||||
|
|
||||||
|
const { data, isLoading, error, isValidating } = useSWR<PartyOrdersData>(
|
||||||
|
url,
|
||||||
|
fetcher,
|
||||||
|
swrOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
const memoizedValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
partyOrders: data?.partyOrders || [],
|
||||||
|
partyOrdersLoading: isLoading,
|
||||||
|
partyOrdersError: error,
|
||||||
|
partyOrdersValidating: isValidating,
|
||||||
|
partyOrdersEmpty: !isLoading && !isValidating && !data?.partyOrders.length,
|
||||||
|
}),
|
||||||
|
[data?.partyOrders, error, isLoading, isValidating]
|
||||||
|
);
|
||||||
|
|
||||||
|
return memoizedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type PartyOrderData = {
|
||||||
|
partyOrder: IPartyOrderItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useGetPartyOrder(partyOrderId: string) {
|
||||||
|
const url = partyOrderId ? [endpoints.partyOrder.details, { params: { partyOrderId } }] : '';
|
||||||
|
|
||||||
|
const { data, isLoading, error, isValidating } = useSWR<PartyOrderData>(url, fetcher, swrOptions);
|
||||||
|
|
||||||
|
const memoizedValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
partyOrder: data?.partyOrder,
|
||||||
|
partyOrderLoading: isLoading,
|
||||||
|
partyOrderError: error,
|
||||||
|
partyOrderValidating: isValidating,
|
||||||
|
}),
|
||||||
|
[data?.partyOrder, error, isLoading, isValidating]
|
||||||
|
);
|
||||||
|
|
||||||
|
return memoizedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type SearchResultsData = {
|
||||||
|
results: IPartyOrderItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function createPartyOrder(partyOrderData: IPartyOrderItem) {
|
||||||
|
/**
|
||||||
|
* Work on server
|
||||||
|
*/
|
||||||
|
const data = { partyOrderData };
|
||||||
|
const {
|
||||||
|
data: { id },
|
||||||
|
} = await axiosInstance.post(endpoints.partyOrder.create, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Work in local
|
||||||
|
*/
|
||||||
|
mutate(
|
||||||
|
endpoints.partyOrder.list,
|
||||||
|
(currentData: any) => {
|
||||||
|
const currentPartyOrders: IPartyOrderItem[] = currentData?.partyOrders;
|
||||||
|
|
||||||
|
const partyOrders = [...currentPartyOrders, { ...partyOrderData, id }];
|
||||||
|
|
||||||
|
return { ...currentData, partyOrders };
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function updatePartyOrder(partyOrderData: Partial<IPartyOrderItem>) {
|
||||||
|
/**
|
||||||
|
* Work on server
|
||||||
|
*/
|
||||||
|
const data = { partyOrderData };
|
||||||
|
await axiosInstance.put(endpoints.partyOrder.update, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Work in local
|
||||||
|
*/
|
||||||
|
|
||||||
|
mutate(
|
||||||
|
endpoints.partyOrder.list,
|
||||||
|
(currentData: any) => {
|
||||||
|
const currentPartyOrders: IPartyOrderItem[] = currentData?.partyOrders;
|
||||||
|
|
||||||
|
const partyOrders = currentPartyOrders.map((partyOrder) =>
|
||||||
|
partyOrder.id === partyOrderData.id ? { ...partyOrder, ...partyOrderData } : partyOrder
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...currentData, partyOrders };
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function deletePartyOrder(partyOrderId: string) {
|
||||||
|
/**
|
||||||
|
* Work on server
|
||||||
|
*/
|
||||||
|
const data = { partyOrderId };
|
||||||
|
await axiosInstance.patch(endpoints.partyOrder.delete, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Work in local
|
||||||
|
*/
|
||||||
|
|
||||||
|
mutate(
|
||||||
|
endpoints.partyOrder.list,
|
||||||
|
(currentData: any) => {
|
||||||
|
const currentProducts: IPartyOrderItem[] = currentData?.partyOrders;
|
||||||
|
|
||||||
|
const partyOrders = currentProducts.filter((partyOrder) => partyOrder.id !== partyOrderId);
|
||||||
|
|
||||||
|
return { ...currentData, partyOrders };
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
// TODO: implement partyOrder changeStatus with url below
|
||||||
|
// const url = endpoints.order.changeStatus(orderId);
|
||||||
|
export async function changeStatus(partyOrderData: any, dummy: any) {
|
||||||
|
return true;
|
||||||
|
// /**
|
||||||
|
// * Work on server
|
||||||
|
// */
|
||||||
|
// const data = { partyOrderData };
|
||||||
|
// await axiosInstance.put(endpoints.partyOrder.update, data);
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Work in local
|
||||||
|
// */
|
||||||
|
|
||||||
|
// mutate(
|
||||||
|
// endpoints.partyOrder.list,
|
||||||
|
// (currentData: any) => {
|
||||||
|
// const currentPartyOrders: IPartyOrderItem[] = currentData?.partyOrders;
|
||||||
|
|
||||||
|
// const partyOrders = currentPartyOrders.map((partyOrder) =>
|
||||||
|
// partyOrder.id === partyOrderData.id ? { ...partyOrder, ...partyOrderData } : partyOrder
|
||||||
|
// );
|
||||||
|
|
||||||
|
// return { ...currentData, partyOrders };
|
||||||
|
// },
|
||||||
|
// false
|
||||||
|
// );
|
||||||
|
}
|
@@ -91,6 +91,26 @@ export const navData: NavSectionProps['data'] = [
|
|||||||
{ title: 'Account', path: paths.dashboard.user.account },
|
{ title: 'Account', path: paths.dashboard.user.account },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'party-event',
|
||||||
|
path: paths.dashboard.partyEvent.root,
|
||||||
|
icon: ICONS.product,
|
||||||
|
children: [
|
||||||
|
{ title: 'List', path: paths.dashboard.partyEvent.root },
|
||||||
|
{ title: 'Details', path: paths.dashboard.partyEvent.demo.details },
|
||||||
|
{ title: 'Create', path: paths.dashboard.partyEvent.new },
|
||||||
|
{ title: 'Edit', path: paths.dashboard.partyEvent.demo.edit },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'party-order',
|
||||||
|
path: paths.dashboard.partyOrder.root,
|
||||||
|
icon: ICONS.order,
|
||||||
|
children: [
|
||||||
|
{ title: 'List', path: paths.dashboard.partyOrder.root },
|
||||||
|
{ title: 'Details', path: paths.dashboard.partyOrder.demo.details },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Product',
|
title: 'Product',
|
||||||
path: paths.dashboard.product.root,
|
path: paths.dashboard.product.root,
|
||||||
|
@@ -84,4 +84,26 @@ export const endpoints = {
|
|||||||
changeStatus: (invoiceId: string) => `/api/invoice/changeStatus?invoiceId=${invoiceId}`,
|
changeStatus: (invoiceId: string) => `/api/invoice/changeStatus?invoiceId=${invoiceId}`,
|
||||||
search: '/api/invoice/search',
|
search: '/api/invoice/search',
|
||||||
},
|
},
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
partyEvent: {
|
||||||
|
list: '/api/party-event/list',
|
||||||
|
details: '/api/party-event/details',
|
||||||
|
search: '/api/party-event/search',
|
||||||
|
create: '/api/party-event/create',
|
||||||
|
update: '/api/party-event/update',
|
||||||
|
delete: '/api/party-event/delete',
|
||||||
|
},
|
||||||
|
partyOrder: {
|
||||||
|
create: '/api/party-order/create',
|
||||||
|
delete: '/api/party-order/delete',
|
||||||
|
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,28 @@
|
|||||||
|
// src/pages/dashboard/party-event/details.tsx
|
||||||
|
|
||||||
|
import { useGetPartyEvent } from 'src/actions/party-event';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
import { useParams } from 'src/routes/hooks';
|
||||||
|
import { PartyEventDetailsView } from 'src/sections/party-event/view';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `PartyEvent details | Dashboard - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const { id = '' } = useParams();
|
||||||
|
|
||||||
|
const { partyEvent, partyEventLoading, partyEventError } = useGetPartyEvent(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<PartyEventDetailsView
|
||||||
|
partyEvent={partyEvent}
|
||||||
|
loading={partyEventLoading}
|
||||||
|
error={partyEventError}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
22
03_source/frontend/src/pages/dashboard/party-event/edit.tsx
Normal file
22
03_source/frontend/src/pages/dashboard/party-event/edit.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { useGetPartyEvent } from 'src/actions/party-event';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
import { useParams } from 'src/routes/hooks';
|
||||||
|
import { PartyEventEditView } from 'src/sections/party-event/view';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `PartyEvent edit | Dashboard - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const { id = '' } = useParams();
|
||||||
|
|
||||||
|
const { partyEvent } = useGetPartyEvent(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<PartyEventEditView partyEvent={partyEvent} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
18
03_source/frontend/src/pages/dashboard/party-event/list.tsx
Normal file
18
03_source/frontend/src/pages/dashboard/party-event/list.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// src/pages/dashboard/party-event/list.tsx
|
||||||
|
//
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
import { PartyEventListView } from 'src/sections/party-event/view';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `PartyEvent list | Dashboard - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<PartyEventListView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
16
03_source/frontend/src/pages/dashboard/party-event/new.tsx
Normal file
16
03_source/frontend/src/pages/dashboard/party-event/new.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
import { PartyEventCreateView } from 'src/sections/party-event/view';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Create a new party-event | Dashboard - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<PartyEventCreateView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,28 @@
|
|||||||
|
// src/pages/dashboard/order/details.tsx
|
||||||
|
//
|
||||||
|
import { useGetPartyOrder } from 'src/actions/party-order';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
import { useParams } from 'src/routes/hooks';
|
||||||
|
import { PartyOrderDetailsView } from 'src/sections/party-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 { partyOrder, partyOrderLoading, partyOrderError } = useGetPartyOrder(id);
|
||||||
|
|
||||||
|
if (!partyOrder) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<PartyOrderDetailsView partyOrder={partyOrder} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
16
03_source/frontend/src/pages/party-event/checkout.tsx
Normal file
16
03_source/frontend/src/pages/party-event/checkout.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
import { CheckoutView } from 'src/sections/checkout/view';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Checkout - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<CheckoutView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
22
03_source/frontend/src/pages/party-event/details.tsx
Normal file
22
03_source/frontend/src/pages/party-event/details.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { useGetProduct } from 'src/actions/product';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
import { useParams } from 'src/routes/hooks';
|
||||||
|
import { ProductShopDetailsView } from 'src/sections/product/view';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Product details - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const { id = '' } = useParams();
|
||||||
|
|
||||||
|
const { product, productLoading, productError } = useGetProduct(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<ProductShopDetailsView product={product} loading={productLoading} error={productError} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
5
03_source/frontend/src/pages/party-event/helloworld.tsx
Normal file
5
03_source/frontend/src/pages/party-event/helloworld.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
function Helloworld() {
|
||||||
|
return <>helloworld</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Helloworld;
|
19
03_source/frontend/src/pages/party-event/list.tsx
Normal file
19
03_source/frontend/src/pages/party-event/list.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useGetPartyEvents } from 'src/actions/party-event';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
import { PartyEventShopView } from 'src/sections/party-event/view';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Product shop - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const { partyEvents, partyEventsLoading } = useGetPartyEvents();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<PartyEventShopView products={partyEvents} loading={partyEventsLoading} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -87,6 +87,13 @@ export const paths = {
|
|||||||
verify: `${ROOTS.AUTH_DEMO}/centered/verify`,
|
verify: `${ROOTS.AUTH_DEMO}/centered/verify`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
//
|
||||||
|
partyEvent: {
|
||||||
|
root: `/party-event`,
|
||||||
|
checkout: `/party-event/checkout`,
|
||||||
|
details: (id: string) => `/party-event/${id}`,
|
||||||
|
demo: { details: `/party-event/${MOCK_ID}` },
|
||||||
|
},
|
||||||
// DASHBOARD
|
// DASHBOARD
|
||||||
dashboard: {
|
dashboard: {
|
||||||
root: ROOTS.DASHBOARD,
|
root: ROOTS.DASHBOARD,
|
||||||
@@ -171,5 +178,23 @@ export const paths = {
|
|||||||
edit: `${ROOTS.DASHBOARD}/tour/${MOCK_ID}/edit`,
|
edit: `${ROOTS.DASHBOARD}/tour/${MOCK_ID}/edit`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
partyEvent: {
|
||||||
|
root: `${ROOTS.DASHBOARD}/party-event`,
|
||||||
|
new: `${ROOTS.DASHBOARD}/party-event/new`,
|
||||||
|
details: (id: string) => `${ROOTS.DASHBOARD}/party-event/${id}`,
|
||||||
|
edit: (id: string) => `${ROOTS.DASHBOARD}/party-event/${id}/edit`,
|
||||||
|
demo: {
|
||||||
|
details: `${ROOTS.DASHBOARD}/party-event/${MOCK_ID}`,
|
||||||
|
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}` },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
// src/routes/sections/dashboard.tsx
|
||||||
|
//
|
||||||
import { lazy, Suspense } from 'react';
|
import { lazy, Suspense } from 'react';
|
||||||
import type { RouteObject } from 'react-router';
|
import type { RouteObject } from 'react-router';
|
||||||
import { Outlet } from 'react-router';
|
import { Outlet } from 'react-router';
|
||||||
@@ -75,6 +77,16 @@ const PermissionDeniedPage = lazy(() => import('src/pages/dashboard/permission')
|
|||||||
const ParamsPage = lazy(() => import('src/pages/dashboard/params'));
|
const ParamsPage = lazy(() => import('src/pages/dashboard/params'));
|
||||||
const BlankPage = lazy(() => import('src/pages/dashboard/blank'));
|
const BlankPage = lazy(() => import('src/pages/dashboard/blank'));
|
||||||
|
|
||||||
|
// PartyEvent
|
||||||
|
const PartyEventDetailsPage = lazy(() => import('src/pages/dashboard/party-event/details'));
|
||||||
|
const PartyEventListPage = lazy(() => import('src/pages/dashboard/party-event/list'));
|
||||||
|
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() {
|
function SuspenseOutlet() {
|
||||||
@@ -198,6 +210,25 @@ export const dashboardRoutes: RouteObject[] = [
|
|||||||
{ path: 'permission', element: <PermissionDeniedPage /> },
|
{ path: 'permission', element: <PermissionDeniedPage /> },
|
||||||
{ path: 'params', element: <ParamsPage /> },
|
{ path: 'params', element: <ParamsPage /> },
|
||||||
{ path: 'blank', element: <BlankPage /> },
|
{ path: 'blank', element: <BlankPage /> },
|
||||||
|
//
|
||||||
|
{
|
||||||
|
path: 'party-event',
|
||||||
|
children: [
|
||||||
|
{ index: true, element: <PartyEventListPage /> },
|
||||||
|
{ path: 'list', element: <PartyEventListPage /> },
|
||||||
|
{ path: ':id', element: <PartyEventDetailsPage /> },
|
||||||
|
{ path: 'new', element: <PartyEventCreatePage /> },
|
||||||
|
{ path: ':id/edit', element: <PartyEventEditPage /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'party-order',
|
||||||
|
children: [
|
||||||
|
{ index: true, element: <PartyOrderListPage /> },
|
||||||
|
{ path: 'list', element: <PartyOrderListPage /> },
|
||||||
|
{ path: ':id', element: <PartyOrderDetailsPage /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@@ -27,6 +27,10 @@ const Page403 = lazy(() => import('src/pages/error/403'));
|
|||||||
const Page404 = lazy(() => import('src/pages/error/404'));
|
const Page404 = lazy(() => import('src/pages/error/404'));
|
||||||
// Blank
|
// Blank
|
||||||
const BlankPage = lazy(() => import('src/pages/blank'));
|
const BlankPage = lazy(() => import('src/pages/blank'));
|
||||||
|
// PartyEvent
|
||||||
|
const PartyEventDetailsPage = lazy(() => import('src/pages/dashboard/party-event/details'));
|
||||||
|
const PartyEventListPage = lazy(() => import('src/pages/dashboard/party-event/list'));
|
||||||
|
const PartyEventCheckoutPage = lazy(() => import('src/pages/party-event/checkout'));
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -58,6 +62,15 @@ export const mainRoutes: RouteObject[] = [
|
|||||||
{ path: 'checkout', element: <ProductCheckoutPage /> },
|
{ path: 'checkout', element: <ProductCheckoutPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'party-event',
|
||||||
|
children: [
|
||||||
|
{ index: true, element: <PartyEventListPage /> },
|
||||||
|
{ path: 'list', element: <PartyEventListPage /> },
|
||||||
|
{ path: ':id', element: <PartyEventDetailsPage /> },
|
||||||
|
{ path: 'checkout', element: <PartyEventCheckoutPage /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'post',
|
path: 'post',
|
||||||
children: [
|
children: [
|
||||||
|
@@ -13,7 +13,7 @@ import { useBoolean, useSetState } from 'minimal-shared/hooks';
|
|||||||
import { varAlpha } from 'minimal-shared/utils';
|
import { varAlpha } from 'minimal-shared/utils';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { deleteOrder, useGetOrders } from 'src/actions/order';
|
||||||
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
|
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
|
||||||
import { ConfirmDialog } from 'src/components/custom-dialog';
|
import { ConfirmDialog } from 'src/components/custom-dialog';
|
||||||
|
45
03_source/frontend/src/sections/party-event/cart-icon.tsx
Normal file
45
03_source/frontend/src/sections/party-event/cart-icon.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import Badge from '@mui/material/Badge';
|
||||||
|
import type { BoxProps } from '@mui/material/Box';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import { RouterLink } from 'src/routes/components';
|
||||||
|
import { paths } from 'src/routes/paths';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = BoxProps<'a'> & {
|
||||||
|
totalItems: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CartIcon({ totalItems, sx, ...other }: Props) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
component={RouterLink}
|
||||||
|
href={paths.product.checkout}
|
||||||
|
sx={[
|
||||||
|
(theme) => ({
|
||||||
|
right: 0,
|
||||||
|
top: 112,
|
||||||
|
zIndex: 999,
|
||||||
|
display: 'flex',
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'fixed',
|
||||||
|
color: 'text.primary',
|
||||||
|
borderTopLeftRadius: 16,
|
||||||
|
borderBottomLeftRadius: 16,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
padding: theme.spacing(1, 3, 1, 2),
|
||||||
|
boxShadow: theme.vars.customShadows.dropdown,
|
||||||
|
transition: theme.transitions.create(['opacity']),
|
||||||
|
'&:hover': { opacity: 0.72 },
|
||||||
|
}),
|
||||||
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
|
]}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
<Badge showZero badgeContent={totalItems} color="error" max={99}>
|
||||||
|
<Iconify icon="solar:cart-3-bold" width={24} />
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,86 @@
|
|||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Carousel,
|
||||||
|
CarouselArrowNumberButtons,
|
||||||
|
CarouselThumb,
|
||||||
|
CarouselThumbs,
|
||||||
|
useCarousel,
|
||||||
|
} from 'src/components/carousel';
|
||||||
|
import { Image } from 'src/components/image';
|
||||||
|
import { Lightbox, useLightBox } from 'src/components/lightbox';
|
||||||
|
import type { IPartyEventItem } from 'src/types/party-event';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
images?: IPartyEventItem['images'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProductDetailsCarousel({ images }: Props) {
|
||||||
|
const carousel = useCarousel({ thumbs: { slidesToShow: 'auto' } });
|
||||||
|
|
||||||
|
const slides = images?.map((img) => ({ src: img })) || [];
|
||||||
|
|
||||||
|
const lightbox = useLightBox(slides);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lightbox.open) {
|
||||||
|
carousel.mainApi?.scrollTo(lightbox.selected, true);
|
||||||
|
}
|
||||||
|
}, [carousel.mainApi, lightbox.open, lightbox.selected]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Box sx={{ mb: 2.5, position: 'relative' }}>
|
||||||
|
<CarouselArrowNumberButtons
|
||||||
|
{...carousel.arrows}
|
||||||
|
options={carousel.options}
|
||||||
|
totalSlides={carousel.dots.dotCount}
|
||||||
|
selectedIndex={carousel.dots.selectedIndex + 1}
|
||||||
|
sx={{ right: 16, bottom: 16, position: 'absolute' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Carousel carousel={carousel} sx={{ borderRadius: 2 }}>
|
||||||
|
{slides.map((slide) => (
|
||||||
|
<Image
|
||||||
|
key={slide.src}
|
||||||
|
alt={slide.src}
|
||||||
|
src={slide.src}
|
||||||
|
ratio="1/1"
|
||||||
|
onClick={() => lightbox.onOpen(slide.src)}
|
||||||
|
sx={{ cursor: 'zoom-in', minWidth: 320 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<CarouselThumbs
|
||||||
|
ref={carousel.thumbs.thumbsRef}
|
||||||
|
options={carousel.options?.thumbs}
|
||||||
|
slotProps={{ disableMask: true }}
|
||||||
|
sx={{ width: 360 }}
|
||||||
|
>
|
||||||
|
{slides.map((item, index) => (
|
||||||
|
<CarouselThumb
|
||||||
|
key={item.src}
|
||||||
|
index={index}
|
||||||
|
src={item.src}
|
||||||
|
selected={index === carousel.thumbs.selectedIndex}
|
||||||
|
onClick={() => carousel.thumbs.onClickThumb(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CarouselThumbs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Lightbox
|
||||||
|
index={lightbox.selected}
|
||||||
|
slides={slides}
|
||||||
|
open={lightbox.open}
|
||||||
|
close={lightbox.onClose}
|
||||||
|
onGetCurrentIndex={(index) => lightbox.setSelected(index)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import { Markdown } from 'src/components/markdown';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
description?: string;
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProductDetailsDescription({ description, sx }: Props) {
|
||||||
|
return (
|
||||||
|
<Markdown
|
||||||
|
children={description}
|
||||||
|
sx={[
|
||||||
|
() => ({
|
||||||
|
p: 3,
|
||||||
|
'& p, li, ol, table': { typography: 'body2' },
|
||||||
|
'& table': {
|
||||||
|
mt: 2,
|
||||||
|
maxWidth: 640,
|
||||||
|
'& td': { px: 2 },
|
||||||
|
'& td:first-of-type': { color: 'text.secondary' },
|
||||||
|
'tbody tr:nth-of-type(odd)': { bgcolor: 'transparent' },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,125 @@
|
|||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import LinearProgress from '@mui/material/LinearProgress';
|
||||||
|
import Rating from '@mui/material/Rating';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { sumBy } from 'es-toolkit';
|
||||||
|
import { useBoolean } from 'minimal-shared/hooks';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import type { IProductReview } from 'src/types/party-event';
|
||||||
|
import { fShortenNumber } from 'src/utils/format-number';
|
||||||
|
import { ProductReviewList } from './party-event-review-list';
|
||||||
|
import { ProductReviewNewForm } from './party-event-review-new-form';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
totalRatings?: number;
|
||||||
|
totalReviews?: number;
|
||||||
|
reviews?: IProductReview[];
|
||||||
|
ratings?: { name: string; starCount: number; reviewCount: number }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProductDetailsReview({
|
||||||
|
totalRatings,
|
||||||
|
totalReviews,
|
||||||
|
ratings = [],
|
||||||
|
reviews = [],
|
||||||
|
}: Props) {
|
||||||
|
const review = useBoolean();
|
||||||
|
|
||||||
|
const total = sumBy(ratings, (star) => star.starCount);
|
||||||
|
|
||||||
|
const renderSummary = () => (
|
||||||
|
<Stack spacing={1} sx={{ alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<Typography variant="subtitle2">Average rating</Typography>
|
||||||
|
|
||||||
|
<Typography variant="h2">
|
||||||
|
{totalRatings}
|
||||||
|
/5
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Rating readOnly value={totalRatings} precision={0.1} />
|
||||||
|
|
||||||
|
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||||
|
({fShortenNumber(totalReviews)} reviews)
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderProgress = () => (
|
||||||
|
<Stack
|
||||||
|
spacing={1.5}
|
||||||
|
sx={[
|
||||||
|
(theme) => ({
|
||||||
|
py: 5,
|
||||||
|
px: { xs: 3, md: 5 },
|
||||||
|
borderLeft: { md: `dashed 1px ${theme.vars.palette.divider}` },
|
||||||
|
borderRight: { md: `dashed 1px ${theme.vars.palette.divider}` },
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{ratings
|
||||||
|
.slice(0)
|
||||||
|
.reverse()
|
||||||
|
.map((rating) => (
|
||||||
|
<Box key={rating.name} sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Typography variant="subtitle2" component="span" sx={{ width: 42 }}>
|
||||||
|
{rating.name}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<LinearProgress
|
||||||
|
color="inherit"
|
||||||
|
variant="determinate"
|
||||||
|
value={(rating.starCount / total) * 100}
|
||||||
|
sx={{ mx: 2, flexGrow: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
component="span"
|
||||||
|
sx={{ minWidth: 48, color: 'text.secondary' }}
|
||||||
|
>
|
||||||
|
{fShortenNumber(rating.reviewCount)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderReviewButton = () => (
|
||||||
|
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="soft"
|
||||||
|
color="inherit"
|
||||||
|
onClick={review.onTrue}
|
||||||
|
startIcon={<Iconify icon="solar:pen-bold" />}
|
||||||
|
>
|
||||||
|
Write your review
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
py: { xs: 5, md: 0 },
|
||||||
|
gridTemplateColumns: { xs: 'repeat(1, 1fr)', md: 'repeat(3, 1fr)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderSummary()}
|
||||||
|
{renderProgress()}
|
||||||
|
{renderReviewButton()}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||||
|
<ProductReviewList reviews={reviews} />
|
||||||
|
<ProductReviewNewForm open={review.value} onClose={review.onFalse} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,321 @@
|
|||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import { formHelperTextClasses } from '@mui/material/FormHelperText';
|
||||||
|
import Link, { linkClasses } from '@mui/material/Link';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import Rating from '@mui/material/Rating';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { ColorPicker } from 'src/components/color-utils';
|
||||||
|
import { Field, Form } from 'src/components/hook-form';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import { Label } from 'src/components/label';
|
||||||
|
import { NumberInput } from 'src/components/number-input';
|
||||||
|
import { useRouter } from 'src/routes/hooks';
|
||||||
|
import { paths } from 'src/routes/paths';
|
||||||
|
import type { CheckoutContextValue } from 'src/types/checkout';
|
||||||
|
import type { IPartyEventItem } from 'src/types/party-event';
|
||||||
|
import { fCurrency, fShortenNumber } from 'src/utils/format-number';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
partyEvent: IPartyEventItem;
|
||||||
|
disableActions?: boolean;
|
||||||
|
items?: CheckoutContextValue['state']['items'];
|
||||||
|
onAddToCart?: CheckoutContextValue['onAddToCart'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PartyEventDetailsSummary({
|
||||||
|
items,
|
||||||
|
partyEvent,
|
||||||
|
onAddToCart,
|
||||||
|
disableActions,
|
||||||
|
...other
|
||||||
|
}: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
sizes,
|
||||||
|
price,
|
||||||
|
colors,
|
||||||
|
coverUrl,
|
||||||
|
newLabel,
|
||||||
|
available,
|
||||||
|
priceSale,
|
||||||
|
saleLabel,
|
||||||
|
totalRatings,
|
||||||
|
totalReviews,
|
||||||
|
inventoryType,
|
||||||
|
subDescription,
|
||||||
|
} = partyEvent;
|
||||||
|
|
||||||
|
const existProduct = !!items?.length && items.map((item) => item.id).includes(id);
|
||||||
|
|
||||||
|
const isMaxQuantity =
|
||||||
|
!!items?.length &&
|
||||||
|
items.filter((item) => item.id === id).map((item) => item.quantity)[0] >= available;
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
coverUrl,
|
||||||
|
available,
|
||||||
|
price,
|
||||||
|
colors: colors[0],
|
||||||
|
size: sizes[4],
|
||||||
|
quantity: available < 1 ? 0 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const methods = useForm<typeof defaultValues>({
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { watch, control, setValue, handleSubmit } = methods;
|
||||||
|
|
||||||
|
const values = watch();
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
|
console.info('DATA', JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!existProduct) {
|
||||||
|
onAddToCart?.({ ...data, colors: [values.colors] });
|
||||||
|
}
|
||||||
|
router.push(paths.product.checkout);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddCart = useCallback(() => {
|
||||||
|
try {
|
||||||
|
onAddToCart?.({
|
||||||
|
...values,
|
||||||
|
colors: [values.colors],
|
||||||
|
subtotal: values.price * values.quantity,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}, [onAddToCart, values]);
|
||||||
|
|
||||||
|
const renderPrice = () => (
|
||||||
|
<Box sx={{ typography: 'h5' }}>
|
||||||
|
{priceSale && (
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{ color: 'text.disabled', textDecoration: 'line-through', mr: 0.5 }}
|
||||||
|
>
|
||||||
|
{fCurrency(priceSale)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fCurrency(price)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderShare = () => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
gap: 3,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
[`& .${linkClasses.root}`]: {
|
||||||
|
gap: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'inline-flex',
|
||||||
|
color: 'text.secondary',
|
||||||
|
typography: 'subtitle2',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link>
|
||||||
|
<Iconify icon="mingcute:add-line" width={16} />
|
||||||
|
Compare
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link>
|
||||||
|
<Iconify icon="solar:heart-bold" width={16} />
|
||||||
|
Favorite
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link>
|
||||||
|
<Iconify icon="solar:share-bold" width={16} />
|
||||||
|
Share
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderColorOptions = () => (
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ flexGrow: 1 }}>
|
||||||
|
Color
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="colors"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<ColorPicker
|
||||||
|
options={colors}
|
||||||
|
value={field.value}
|
||||||
|
onChange={(color) => field.onChange(color as string)}
|
||||||
|
limit={4}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSizeOptions = () => (
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ flexGrow: 1 }}>
|
||||||
|
Size
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Field.Select
|
||||||
|
name="size"
|
||||||
|
size="small"
|
||||||
|
helperText={
|
||||||
|
<Link underline="always" color="text.primary">
|
||||||
|
Size chart
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
maxWidth: 88,
|
||||||
|
[`& .${formHelperTextClasses.root}`]: { mx: 0, mt: 1, textAlign: 'right' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sizes.map((size) => (
|
||||||
|
<MenuItem key={size} value={size}>
|
||||||
|
{size}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Field.Select>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderQuantity = () => (
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ flexGrow: 1 }}>
|
||||||
|
Quantity
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<NumberInput
|
||||||
|
hideDivider
|
||||||
|
value={values.quantity}
|
||||||
|
onChange={(event, quantity: number) => setValue('quantity', quantity)}
|
||||||
|
max={available}
|
||||||
|
sx={{ maxWidth: 112 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
component="div"
|
||||||
|
sx={{ textAlign: 'right', color: 'text.secondary' }}
|
||||||
|
>
|
||||||
|
Available: {available}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderActions = () => (
|
||||||
|
<Box sx={{ gap: 2, display: 'flex' }}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
disabled={isMaxQuantity || disableActions}
|
||||||
|
size="large"
|
||||||
|
color="warning"
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<Iconify icon="solar:cart-plus-bold" width={24} />}
|
||||||
|
onClick={handleAddCart}
|
||||||
|
sx={{ whiteSpace: 'nowrap' }}
|
||||||
|
>
|
||||||
|
Add to cart
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button fullWidth size="large" type="submit" variant="contained" disabled={disableActions}>
|
||||||
|
Buy now
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSubDescription = () => (
|
||||||
|
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||||
|
{subDescription}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderRating = () => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
typography: 'body2',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: 'text.disabled',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Rating size="small" value={totalRatings} precision={0.1} readOnly sx={{ mr: 1 }} />
|
||||||
|
{`(${fShortenNumber(totalReviews)} reviews)`}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderLabels = () =>
|
||||||
|
(newLabel.enabled || saleLabel.enabled) && (
|
||||||
|
<Box sx={{ gap: 1, display: 'flex', alignItems: 'center' }}>
|
||||||
|
{newLabel.enabled && <Label color="info">{newLabel.content}</Label>}
|
||||||
|
{saleLabel.enabled && <Label color="error">{saleLabel.content}</Label>}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderInventoryType = () => (
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
typography: 'overline',
|
||||||
|
color:
|
||||||
|
(inventoryType === 'out of stock' && 'error.main') ||
|
||||||
|
(inventoryType === 'low stock' && 'warning.main') ||
|
||||||
|
'success.main',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{inventoryType}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form methods={methods} onSubmit={onSubmit}>
|
||||||
|
<Stack spacing={3} sx={{ pt: 3 }} {...other}>
|
||||||
|
<Stack spacing={2} alignItems="flex-start">
|
||||||
|
{renderLabels()}
|
||||||
|
{renderInventoryType()}
|
||||||
|
|
||||||
|
<Typography variant="h5">{name}</Typography>
|
||||||
|
|
||||||
|
{renderRating()}
|
||||||
|
{renderPrice()}
|
||||||
|
{renderSubDescription()}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||||
|
|
||||||
|
{renderColorOptions()}
|
||||||
|
{renderSizeOptions()}
|
||||||
|
{renderQuantity()}
|
||||||
|
|
||||||
|
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||||
|
|
||||||
|
{renderActions()}
|
||||||
|
{renderShare()}
|
||||||
|
</Stack>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,113 @@
|
|||||||
|
import type { BoxProps } from '@mui/material/Box';
|
||||||
|
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 Tooltip from '@mui/material/Tooltip';
|
||||||
|
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 { RouterLink } from 'src/routes/components';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = BoxProps & {
|
||||||
|
backHref: string;
|
||||||
|
editHref: string;
|
||||||
|
liveHref: string;
|
||||||
|
publish: string;
|
||||||
|
onChangePublish: (newValue: string) => void;
|
||||||
|
publishOptions: { value: string; label: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProductDetailsToolbar({
|
||||||
|
sx,
|
||||||
|
publish,
|
||||||
|
backHref,
|
||||||
|
editHref,
|
||||||
|
liveHref,
|
||||||
|
publishOptions,
|
||||||
|
onChangePublish,
|
||||||
|
...other
|
||||||
|
}: Props) {
|
||||||
|
const menuActions = usePopover();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const renderMenuActions = () => (
|
||||||
|
<CustomPopover
|
||||||
|
open={menuActions.open}
|
||||||
|
anchorEl={menuActions.anchorEl}
|
||||||
|
onClose={menuActions.onClose}
|
||||||
|
slotProps={{ arrow: { placement: 'top-right' } }}
|
||||||
|
>
|
||||||
|
<MenuList>
|
||||||
|
{publishOptions.map((option) => (
|
||||||
|
<MenuItem
|
||||||
|
key={option.value}
|
||||||
|
selected={option.value === publish}
|
||||||
|
onClick={() => {
|
||||||
|
menuActions.onClose();
|
||||||
|
onChangePublish(option.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.value === 'published' && <Iconify icon="eva:cloud-upload-fill" />}
|
||||||
|
{option.value === 'draft' && <Iconify icon="solar:file-text-bold" />}
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</MenuList>
|
||||||
|
</CustomPopover>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
sx={[
|
||||||
|
{ gap: 1.5, display: 'flex', mb: { xs: 3, md: 5 } },
|
||||||
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
|
]}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
component={RouterLink}
|
||||||
|
href={backHref}
|
||||||
|
startIcon={<Iconify icon="eva:arrow-ios-back-fill" width={16} />}
|
||||||
|
>
|
||||||
|
{t('back')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
|
||||||
|
{publish === 'published' && (
|
||||||
|
<Tooltip title="Go Live">
|
||||||
|
<IconButton component={RouterLink} href={liveHref}>
|
||||||
|
<Iconify icon="eva:external-link-fill" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip title="Edit">
|
||||||
|
<IconButton component={RouterLink} href={editHref}>
|
||||||
|
<Iconify icon="solar:pen-bold" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="inherit"
|
||||||
|
variant="contained"
|
||||||
|
loading={!publish}
|
||||||
|
loadingIndicator="Loading…"
|
||||||
|
endIcon={<Iconify icon="eva:arrow-ios-downward-fill" />}
|
||||||
|
onClick={menuActions.onOpen}
|
||||||
|
sx={{ textTransform: 'capitalize' }}
|
||||||
|
>
|
||||||
|
{publish}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{renderMenuActions()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,334 @@
|
|||||||
|
import Badge from '@mui/material/Badge';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import Drawer from '@mui/material/Drawer';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import { inputBaseClasses } from '@mui/material/InputBase';
|
||||||
|
import Radio from '@mui/material/Radio';
|
||||||
|
import Rating from '@mui/material/Rating';
|
||||||
|
import Slider from '@mui/material/Slider';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import type { UseSetStateReturn } from 'minimal-shared/hooks';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { ColorPicker } from 'src/components/color-utils';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import { NumberInput } from 'src/components/number-input';
|
||||||
|
import { Scrollbar } from 'src/components/scrollbar';
|
||||||
|
import type { IProductFilters } from 'src/types/party-event';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean;
|
||||||
|
canReset: boolean;
|
||||||
|
onOpen: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
filters: UseSetStateReturn<IProductFilters>;
|
||||||
|
options: {
|
||||||
|
colors: string[];
|
||||||
|
ratings: string[];
|
||||||
|
categories: string[];
|
||||||
|
genders: { value: string; label: string }[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_AMOUNT = 200;
|
||||||
|
|
||||||
|
const marksLabel = Array.from({ length: 21 }, (_, index) => {
|
||||||
|
const value = index * 10;
|
||||||
|
const firstValue = index === 0 ? `$${value}` : `${value}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label: index % 4 ? '' : firstValue,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export function ProductFiltersDrawer({ open, onOpen, onClose, canReset, filters, options }: Props) {
|
||||||
|
const { state: currentFilters, setState: updateFilters, resetState: resetFilters } = filters;
|
||||||
|
|
||||||
|
const handleFilterGender = useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
|
const checked = currentFilters.gender.includes(newValue)
|
||||||
|
? currentFilters.gender.filter((value) => value !== newValue)
|
||||||
|
: [...currentFilters.gender, newValue];
|
||||||
|
|
||||||
|
updateFilters({ gender: checked });
|
||||||
|
},
|
||||||
|
[updateFilters, currentFilters.gender]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterCategory = useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
|
updateFilters({ category: newValue });
|
||||||
|
},
|
||||||
|
[updateFilters]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterColors = useCallback(
|
||||||
|
(newValue: string[]) => {
|
||||||
|
updateFilters({ colors: newValue });
|
||||||
|
},
|
||||||
|
[updateFilters]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterPriceRange = useCallback(
|
||||||
|
(event: Event, newValue: number | number[]) => {
|
||||||
|
updateFilters({ priceRange: newValue as number[] });
|
||||||
|
},
|
||||||
|
[updateFilters]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterRating = useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
|
updateFilters({ rating: newValue });
|
||||||
|
},
|
||||||
|
[updateFilters]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderHead = () => (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: 2,
|
||||||
|
pr: 1,
|
||||||
|
pl: 2.5,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" sx={{ flexGrow: 1 }}>
|
||||||
|
Filters
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Tooltip title="Reset">
|
||||||
|
<IconButton onClick={() => resetFilters()}>
|
||||||
|
<Badge color="error" variant="dot" invisible={!canReset}>
|
||||||
|
<Iconify icon="solar:restart-bold" />
|
||||||
|
</Badge>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<IconButton onClick={onClose}>
|
||||||
|
<Iconify icon="mingcute:close-line" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderGender = () => (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ mb: 1 }}>
|
||||||
|
Gender
|
||||||
|
</Typography>
|
||||||
|
{options.genders.map((option) => (
|
||||||
|
<FormControlLabel
|
||||||
|
key={option.value}
|
||||||
|
label={option.label}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={currentFilters.gender.includes(option.label)}
|
||||||
|
onClick={() => handleFilterGender(option.label)}
|
||||||
|
slotProps={{ input: { id: `${option.value}-checkbox` } }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderCategory = () => (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ mb: 1 }}>
|
||||||
|
Category
|
||||||
|
</Typography>
|
||||||
|
{options.categories.map((option) => (
|
||||||
|
<FormControlLabel
|
||||||
|
key={option}
|
||||||
|
label={option}
|
||||||
|
control={
|
||||||
|
<Radio
|
||||||
|
checked={option === currentFilters.category}
|
||||||
|
onClick={() => handleFilterCategory(option)}
|
||||||
|
slotProps={{ input: { id: `${option}-radio` } }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
sx={{ ...(option === 'all' && { textTransform: 'capitalize' }) }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderColor = () => (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ mb: 1 }}>
|
||||||
|
Color
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<ColorPicker
|
||||||
|
options={options.colors}
|
||||||
|
value={currentFilters.colors}
|
||||||
|
onChange={(colors) => handleFilterColors(colors as string[])}
|
||||||
|
limit={6}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPrice = () => (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Typography variant="subtitle2">Price</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ my: 2, gap: 5, display: 'flex' }}>
|
||||||
|
<InputRange type="min" value={currentFilters.priceRange} onChange={updateFilters} />
|
||||||
|
<InputRange type="max" value={currentFilters.priceRange} onChange={updateFilters} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Slider
|
||||||
|
value={currentFilters.priceRange}
|
||||||
|
onChange={handleFilterPriceRange}
|
||||||
|
step={10}
|
||||||
|
min={0}
|
||||||
|
max={MAX_AMOUNT}
|
||||||
|
marks={marksLabel}
|
||||||
|
getAriaValueText={(value) => `$${value}`}
|
||||||
|
valueLabelFormat={(value) => `$${value}`}
|
||||||
|
sx={{ alignSelf: 'center', width: `calc(100% - 24px)` }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderRating = () => (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ mb: 2 }}>
|
||||||
|
Rating
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{options.ratings.map((item, index) => (
|
||||||
|
<Box
|
||||||
|
key={item}
|
||||||
|
onClick={() => handleFilterRating(item)}
|
||||||
|
sx={{
|
||||||
|
mb: 1,
|
||||||
|
gap: 1,
|
||||||
|
ml: -1,
|
||||||
|
p: 0.5,
|
||||||
|
display: 'flex',
|
||||||
|
borderRadius: 1,
|
||||||
|
cursor: 'pointer',
|
||||||
|
typography: 'body2',
|
||||||
|
alignItems: 'center',
|
||||||
|
'&:hover': { opacity: 0.48 },
|
||||||
|
...(currentFilters.rating === item && { bgcolor: 'action.selected' }),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Rating readOnly value={4 - index} /> & Up
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
disableRipple
|
||||||
|
color="inherit"
|
||||||
|
endIcon={
|
||||||
|
<Badge color="error" variant="dot" invisible={!canReset}>
|
||||||
|
<Iconify icon="ic:round-filter-list" />
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
|
onClick={onOpen}
|
||||||
|
>
|
||||||
|
Filters
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
anchor="right"
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
slotProps={{
|
||||||
|
backdrop: { invisible: true },
|
||||||
|
paper: { sx: { width: 320 } },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderHead()}
|
||||||
|
|
||||||
|
<Scrollbar sx={{ px: 2.5, py: 3 }}>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
{renderGender()}
|
||||||
|
{renderCategory()}
|
||||||
|
{renderColor()}
|
||||||
|
{renderPrice()}
|
||||||
|
{renderRating()}
|
||||||
|
</Stack>
|
||||||
|
</Scrollbar>
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type InputRangeProps = {
|
||||||
|
value: number[];
|
||||||
|
type: 'min' | 'max';
|
||||||
|
onChange: UseSetStateReturn<IProductFilters>['setState'];
|
||||||
|
};
|
||||||
|
|
||||||
|
function InputRange({ type, value, onChange: onFilters }: InputRangeProps) {
|
||||||
|
const minValue = value[0];
|
||||||
|
const maxValue = value[1];
|
||||||
|
|
||||||
|
const handleBlur = useCallback(() => {
|
||||||
|
const newMin = Math.max(0, Math.min(minValue, MAX_AMOUNT));
|
||||||
|
const newMax = Math.max(0, Math.min(maxValue, MAX_AMOUNT));
|
||||||
|
|
||||||
|
if (newMin !== minValue || newMax !== maxValue) {
|
||||||
|
onFilters({ priceRange: [newMin, newMax] });
|
||||||
|
}
|
||||||
|
}, [minValue, maxValue, onFilters]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: 1, display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
color: 'text.disabled',
|
||||||
|
textTransform: 'capitalize',
|
||||||
|
fontWeight: 'fontWeightSemiBold',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`${type} ($)`}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
hideButtons
|
||||||
|
max={MAX_AMOUNT}
|
||||||
|
value={type === 'min' ? minValue : maxValue}
|
||||||
|
onChange={(event, newValue) =>
|
||||||
|
onFilters({ priceRange: type === 'min' ? [newValue, maxValue] : [minValue, newValue] })
|
||||||
|
}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
sx={{ maxWidth: 64 }}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
sx: {
|
||||||
|
[`& .${inputBaseClasses.input}`]: {
|
||||||
|
pr: 1,
|
||||||
|
textAlign: 'right',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,101 @@
|
|||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import type { UseSetStateReturn } from 'minimal-shared/hooks';
|
||||||
|
import { varAlpha } from 'minimal-shared/utils';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import type { FiltersResultProps } from 'src/components/filters-result';
|
||||||
|
import { chipProps, FiltersBlock, FiltersResult } from 'src/components/filters-result';
|
||||||
|
import type { IProductFilters } from 'src/types/party-event';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = FiltersResultProps & {
|
||||||
|
filters: UseSetStateReturn<IProductFilters>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProductFiltersResult({ filters, totalResults, sx }: Props) {
|
||||||
|
const { state: currentFilters, setState: updateFilters, resetState: resetFilters } = filters;
|
||||||
|
|
||||||
|
const handleRemoveGender = useCallback(
|
||||||
|
(inputValue: string) => {
|
||||||
|
const newValue = currentFilters.gender.filter((item) => item !== inputValue);
|
||||||
|
|
||||||
|
updateFilters({ gender: newValue });
|
||||||
|
},
|
||||||
|
[updateFilters, currentFilters.gender]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveCategory = useCallback(() => {
|
||||||
|
updateFilters({ category: 'all' });
|
||||||
|
}, [updateFilters]);
|
||||||
|
|
||||||
|
const handleRemoveColor = useCallback(
|
||||||
|
(inputValue: string | string[]) => {
|
||||||
|
const newValue = currentFilters.colors.filter((item: string) => item !== inputValue);
|
||||||
|
|
||||||
|
updateFilters({ colors: newValue });
|
||||||
|
},
|
||||||
|
[updateFilters, currentFilters.colors]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemovePrice = useCallback(() => {
|
||||||
|
updateFilters({ priceRange: [0, 200] });
|
||||||
|
}, [updateFilters]);
|
||||||
|
|
||||||
|
const handleRemoveRating = useCallback(() => {
|
||||||
|
updateFilters({ rating: '' });
|
||||||
|
}, [updateFilters]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FiltersResult totalResults={totalResults} onReset={() => resetFilters()} sx={sx}>
|
||||||
|
<FiltersBlock label="Gender:" isShow={!!currentFilters.gender.length}>
|
||||||
|
{currentFilters.gender.map((item) => (
|
||||||
|
<Chip {...chipProps} key={item} label={item} onDelete={() => handleRemoveGender(item)} />
|
||||||
|
))}
|
||||||
|
</FiltersBlock>
|
||||||
|
|
||||||
|
<FiltersBlock label="Category:" isShow={currentFilters.category !== 'all'}>
|
||||||
|
<Chip {...chipProps} label={currentFilters.category} onDelete={handleRemoveCategory} />
|
||||||
|
</FiltersBlock>
|
||||||
|
|
||||||
|
<FiltersBlock label="Colors:" isShow={!!currentFilters.colors.length}>
|
||||||
|
{currentFilters.colors.map((item) => (
|
||||||
|
<Chip
|
||||||
|
{...chipProps}
|
||||||
|
key={item}
|
||||||
|
label={
|
||||||
|
<Box
|
||||||
|
sx={[
|
||||||
|
(theme) => ({
|
||||||
|
ml: -0.5,
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
bgcolor: item,
|
||||||
|
borderRadius: '50%',
|
||||||
|
border: `solid 1px ${varAlpha(theme.vars.palette.common.whiteChannel, 0.24)}`,
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onDelete={() => handleRemoveColor(item)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FiltersBlock>
|
||||||
|
|
||||||
|
<FiltersBlock
|
||||||
|
label="Price:"
|
||||||
|
isShow={currentFilters.priceRange[0] !== 0 || currentFilters.priceRange[1] !== 200}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
{...chipProps}
|
||||||
|
label={`$${currentFilters.priceRange[0]} - ${currentFilters.priceRange[1]}`}
|
||||||
|
onDelete={handleRemovePrice}
|
||||||
|
/>
|
||||||
|
</FiltersBlock>
|
||||||
|
|
||||||
|
<FiltersBlock label="Rating:" isShow={!!currentFilters.rating}>
|
||||||
|
<Chip {...chipProps} label={currentFilters.rating} onDelete={handleRemoveRating} />
|
||||||
|
</FiltersBlock>
|
||||||
|
</FiltersResult>
|
||||||
|
);
|
||||||
|
}
|
147
03_source/frontend/src/sections/party-event/party-event-item.tsx
Normal file
147
03_source/frontend/src/sections/party-event/party-event-item.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import Fab, { fabClasses } from '@mui/material/Fab';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
import { ColorPreview } from 'src/components/color-utils';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import { Image } from 'src/components/image';
|
||||||
|
import { Label } from 'src/components/label';
|
||||||
|
import { RouterLink } from 'src/routes/components';
|
||||||
|
import type { IPartyEventItem } from 'src/types/party-event';
|
||||||
|
import { fCurrency } from 'src/utils/format-number';
|
||||||
|
import { useCheckoutContext } from '../checkout/context';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
partyEvent: IPartyEventItem;
|
||||||
|
detailsHref: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProductItem({ partyEvent, detailsHref }: Props) {
|
||||||
|
const { onAddToCart } = useCheckoutContext();
|
||||||
|
|
||||||
|
const { id, name, coverUrl, price, colors, available, sizes, priceSale, newLabel, saleLabel } =
|
||||||
|
partyEvent;
|
||||||
|
|
||||||
|
const handleAddCart = async () => {
|
||||||
|
const newProduct = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
coverUrl,
|
||||||
|
available,
|
||||||
|
price,
|
||||||
|
colors: [colors[0]],
|
||||||
|
size: sizes[0],
|
||||||
|
quantity: 1,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
onAddToCart(newProduct);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderLabels = () =>
|
||||||
|
(newLabel.enabled || saleLabel.enabled) && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
gap: 1,
|
||||||
|
top: 16,
|
||||||
|
zIndex: 9,
|
||||||
|
right: 16,
|
||||||
|
display: 'flex',
|
||||||
|
position: 'absolute',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{newLabel.enabled && (
|
||||||
|
<Label variant="filled" color="info">
|
||||||
|
{newLabel.content}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
{saleLabel.enabled && (
|
||||||
|
<Label variant="filled" color="error">
|
||||||
|
{saleLabel.content}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderImage = () => (
|
||||||
|
<Box sx={{ position: 'relative', p: 1 }}>
|
||||||
|
{!!available && (
|
||||||
|
<Fab
|
||||||
|
size="medium"
|
||||||
|
color="warning"
|
||||||
|
onClick={handleAddCart}
|
||||||
|
sx={[
|
||||||
|
(theme) => ({
|
||||||
|
right: 16,
|
||||||
|
zIndex: 9,
|
||||||
|
bottom: 16,
|
||||||
|
opacity: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
transform: 'scale(0)',
|
||||||
|
transition: theme.transitions.create(['opacity', 'transform'], {
|
||||||
|
easing: theme.transitions.easing.easeInOut,
|
||||||
|
duration: theme.transitions.duration.shorter,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Iconify icon="solar:cart-plus-bold" width={24} />
|
||||||
|
</Fab>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip title={!available && 'Out of stock'} placement="bottom-end">
|
||||||
|
<Image
|
||||||
|
alt={name}
|
||||||
|
src={coverUrl}
|
||||||
|
ratio="1/1"
|
||||||
|
sx={{ borderRadius: 1.5, ...(!available && { opacity: 0.48, filter: 'grayscale(1)' }) }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderContent = () => (
|
||||||
|
<Stack spacing={2.5} sx={{ p: 3, pt: 2 }}>
|
||||||
|
<Link component={RouterLink} href={detailsHref} color="inherit" variant="subtitle2" noWrap>
|
||||||
|
{name}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Tooltip title="Color">
|
||||||
|
<ColorPreview colors={colors} />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Box sx={{ gap: 0.5, display: 'flex', typography: 'subtitle1' }}>
|
||||||
|
{priceSale && (
|
||||||
|
<Box component="span" sx={{ color: 'text.disabled', textDecoration: 'line-through' }}>
|
||||||
|
{fCurrency(priceSale)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box component="span">{fCurrency(price)}</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
'&:hover': {
|
||||||
|
[`& .${fabClasses.root}`]: { opacity: 1, transform: 'scale(1)' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderLabels()}
|
||||||
|
{renderImage()}
|
||||||
|
{renderContent()}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,60 @@
|
|||||||
|
import type { BoxProps } from '@mui/material/Box';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Pagination, { paginationClasses } from '@mui/material/Pagination';
|
||||||
|
import { paths } from 'src/routes/paths';
|
||||||
|
import type { IPartyEventItem } from 'src/types/party-event';
|
||||||
|
import { ProductItem } from './party-event-item';
|
||||||
|
import { ProductItemSkeleton } from './party-event-skeleton';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = BoxProps & {
|
||||||
|
loading?: boolean;
|
||||||
|
partyEvents: IPartyEventItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PartyEventList({ partyEvents, loading, sx, ...other }: Props) {
|
||||||
|
const renderLoading = () => <ProductItemSkeleton />;
|
||||||
|
|
||||||
|
const renderList = () =>
|
||||||
|
partyEvents.map((partyEvent) => (
|
||||||
|
<ProductItem
|
||||||
|
key={partyEvent.id}
|
||||||
|
partyEvent={partyEvent}
|
||||||
|
detailsHref={paths.partyEvent.details(partyEvent.id)}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
sx={[
|
||||||
|
() => ({
|
||||||
|
gap: 3,
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: {
|
||||||
|
xs: 'repeat(1, 1fr)',
|
||||||
|
sm: 'repeat(2, 1fr)',
|
||||||
|
md: 'repeat(3, 1fr)',
|
||||||
|
lg: 'repeat(4, 1fr)',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
|
]}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
{loading ? renderLoading() : renderList()}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{partyEvents.length > 8 && (
|
||||||
|
<Pagination
|
||||||
|
count={8}
|
||||||
|
sx={{
|
||||||
|
mt: { xs: 5, md: 8 },
|
||||||
|
[`& .${paginationClasses.ul}`]: { justifyContent: 'center' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,560 @@
|
|||||||
|
// src/sections/product/product-new-edit-form.tsx
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardHeader from '@mui/material/CardHeader';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Collapse from '@mui/material/Collapse';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Switch from '@mui/material/Switch';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { useBoolean } from 'minimal-shared/hooks';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
PRODUCT_CATEGORY_GROUP_OPTIONS,
|
||||||
|
PRODUCT_COLOR_NAME_OPTIONS,
|
||||||
|
PRODUCT_SIZE_OPTIONS,
|
||||||
|
} from 'src/_mock';
|
||||||
|
import { createPartyEvent, updatePartyEvent } from 'src/actions/party-event';
|
||||||
|
import { Field, Form, schemaHelper } from 'src/components/hook-form';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import { toast } from 'src/components/snackbar';
|
||||||
|
import { useRouter } from 'src/routes/hooks';
|
||||||
|
import { paths } from 'src/routes/paths';
|
||||||
|
import type { IPartyEventItem } from 'src/types/party-event';
|
||||||
|
import { fileToBase64 } from 'src/utils/file-to-base64';
|
||||||
|
import { z as zod } from 'zod';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
const PRODUCT_PUBLISH_OPTIONS = [
|
||||||
|
{ value: 'published', label: 'Published' },
|
||||||
|
{ value: 'draft', label: 'Draft' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PRODUCT_COLOR_OPTIONS = [
|
||||||
|
'#FF4842',
|
||||||
|
'#1890FF',
|
||||||
|
'#FFC0CB',
|
||||||
|
'#00AB55',
|
||||||
|
'#FFC107',
|
||||||
|
'#7F00FF',
|
||||||
|
'#000000',
|
||||||
|
'#FFFFFF',
|
||||||
|
];
|
||||||
|
|
||||||
|
const _tags = [
|
||||||
|
`Technology`,
|
||||||
|
`Health and Wellness`,
|
||||||
|
`Travel`,
|
||||||
|
`Finance`,
|
||||||
|
`Education`,
|
||||||
|
`Food and Beverage`,
|
||||||
|
`Fashion`,
|
||||||
|
`Home and Garden`,
|
||||||
|
`Sports`,
|
||||||
|
`Entertainment`,
|
||||||
|
`Business`,
|
||||||
|
`Science`,
|
||||||
|
`Automotive`,
|
||||||
|
`Beauty`,
|
||||||
|
`Fitness`,
|
||||||
|
`Lifestyle`,
|
||||||
|
`Real Estate`,
|
||||||
|
`Parenting`,
|
||||||
|
`Pet Care`,
|
||||||
|
`Environmental`,
|
||||||
|
`DIY and Crafts`,
|
||||||
|
`Gaming`,
|
||||||
|
`Photography`,
|
||||||
|
`Music`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const PRODUCT_GENDER_OPTIONS = [
|
||||||
|
{ label: 'Men', value: 'Men' },
|
||||||
|
{ label: 'Women', value: 'Women' },
|
||||||
|
{ label: 'Kids', value: 'Kids' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export type NewPartyEventSchemaType = zod.infer<typeof NewProductSchema>;
|
||||||
|
|
||||||
|
export const NewProductSchema = zod.object({
|
||||||
|
sku: zod.string().min(1, { message: 'Product sku is required!' }),
|
||||||
|
name: zod.string().min(1, { message: 'Name is required!' }),
|
||||||
|
code: zod.string().min(1, { message: 'Product code is required!' }),
|
||||||
|
price: schemaHelper.nullableInput(
|
||||||
|
zod.number({ coerce: true }).min(1, { message: 'Price is required!' }),
|
||||||
|
{
|
||||||
|
// message for null value
|
||||||
|
message: 'Price is required!',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
taxes: zod.number({ coerce: true }).nullable(),
|
||||||
|
tags: zod.string().array().min(2, { message: 'Must have at least 2 items!' }),
|
||||||
|
sizes: zod.string().array().min(1, { message: 'Choose at least one option!' }),
|
||||||
|
publish: zod.string(),
|
||||||
|
gender: zod.array(zod.string()).min(1, { message: 'Choose at least one option!' }),
|
||||||
|
coverUrl: zod.string(),
|
||||||
|
images: schemaHelper.files({ message: 'Images is required!' }),
|
||||||
|
colors: zod.string().array().min(1, { message: 'Choose at least one option!' }),
|
||||||
|
quantity: schemaHelper.nullableInput(
|
||||||
|
zod.number({ coerce: true }).min(1, { message: 'Quantity is required!' }),
|
||||||
|
{
|
||||||
|
// message for null value
|
||||||
|
message: 'Quantity is required!',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
category: zod.string(),
|
||||||
|
available: zod.number(),
|
||||||
|
totalSold: zod.number(),
|
||||||
|
description: schemaHelper
|
||||||
|
.editor({ message: 'Description is required!' })
|
||||||
|
.min(10, { message: 'Description must be at least 10 characters' })
|
||||||
|
.max(50000, { message: 'Description must be less than 50000 characters' }),
|
||||||
|
totalRatings: zod.number(),
|
||||||
|
totalReviews: zod.number(),
|
||||||
|
inventoryType: zod.string(),
|
||||||
|
subDescription: zod.string(),
|
||||||
|
priceSale: zod.number({ coerce: true }).nullable(),
|
||||||
|
newLabel: zod.object({ enabled: zod.boolean(), content: zod.string() }),
|
||||||
|
saleLabel: zod.object({ enabled: zod.boolean(), content: zod.string() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
currentPartyEvent?: IPartyEventItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PartyEventNewEditForm({ currentPartyEvent }: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const openDetails = useBoolean(true);
|
||||||
|
const openProperties = useBoolean(true);
|
||||||
|
const openPricing = useBoolean(true);
|
||||||
|
|
||||||
|
const [includeTaxes, setIncludeTaxes] = useState(false);
|
||||||
|
|
||||||
|
const defaultValues: NewPartyEventSchemaType = {
|
||||||
|
sku: '321',
|
||||||
|
name: 'hello party event',
|
||||||
|
code: '123',
|
||||||
|
price: 1.1,
|
||||||
|
taxes: 1.1,
|
||||||
|
tags: [_tags[0], _tags[1]],
|
||||||
|
sizes: ['9'],
|
||||||
|
publish: PRODUCT_PUBLISH_OPTIONS[0].value,
|
||||||
|
gender: [
|
||||||
|
PRODUCT_GENDER_OPTIONS[0].value,
|
||||||
|
PRODUCT_GENDER_OPTIONS[1].value,
|
||||||
|
PRODUCT_GENDER_OPTIONS[2].value,
|
||||||
|
],
|
||||||
|
coverUrl: '',
|
||||||
|
images: [],
|
||||||
|
colors: [PRODUCT_COLOR_OPTIONS[0], PRODUCT_COLOR_OPTIONS[1]],
|
||||||
|
quantity: 3,
|
||||||
|
category: PRODUCT_CATEGORY_GROUP_OPTIONS[0].classify[1],
|
||||||
|
available: 0,
|
||||||
|
totalSold: 0,
|
||||||
|
description: 'hello description',
|
||||||
|
totalRatings: 0,
|
||||||
|
totalReviews: 0,
|
||||||
|
inventoryType: '',
|
||||||
|
subDescription: '',
|
||||||
|
priceSale: 0.9,
|
||||||
|
newLabel: { enabled: false, content: '' },
|
||||||
|
saleLabel: { enabled: false, content: '' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const methods = useForm<NewPartyEventSchemaType>({
|
||||||
|
resolver: zodResolver(NewProductSchema),
|
||||||
|
defaultValues,
|
||||||
|
values: currentPartyEvent,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
reset,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = methods;
|
||||||
|
|
||||||
|
const values = watch();
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
|
const updatedData = {
|
||||||
|
...data,
|
||||||
|
taxes: includeTaxes ? defaultValues.taxes : data.taxes,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// sanitize file field
|
||||||
|
for (let i = 0; i < values.images.length; i++) {
|
||||||
|
const temp: any = values.images[i];
|
||||||
|
if (temp instanceof File) {
|
||||||
|
values.images[i] = await fileToBase64(temp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedValues: IPartyEventItem = values as unknown as IPartyEventItem;
|
||||||
|
|
||||||
|
if (currentPartyEvent) {
|
||||||
|
// perform save
|
||||||
|
updatePartyEvent(sanitizedValues);
|
||||||
|
} else {
|
||||||
|
// perform create
|
||||||
|
createPartyEvent(sanitizedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(currentPartyEvent ? 'update-success!' : 'create-success!');
|
||||||
|
|
||||||
|
router.push(paths.dashboard.partyEvent.root);
|
||||||
|
|
||||||
|
// console.info('DATA', updatedData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRemoveFile = useCallback(
|
||||||
|
(inputFile: File | string) => {
|
||||||
|
const filtered = values.images && values.images?.filter((file) => file !== inputFile);
|
||||||
|
setValue('images', filtered);
|
||||||
|
},
|
||||||
|
[setValue, values.images]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveAllFiles = useCallback(() => {
|
||||||
|
setValue('images', [], { shouldValidate: true });
|
||||||
|
}, [setValue]);
|
||||||
|
|
||||||
|
const handleChangeIncludeTaxes = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setIncludeTaxes(event.target.checked);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderCollapseButton = (value: boolean, onToggle: () => void) => (
|
||||||
|
<IconButton onClick={onToggle}>
|
||||||
|
<Iconify icon={value ? 'eva:arrow-ios-downward-fill' : 'eva:arrow-ios-forward-fill'} />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleProductImageUpload() {
|
||||||
|
console.log(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [disableUserInput, setDisableUserInput] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDisableUserInput(isSubmitting);
|
||||||
|
}, [isSubmitting]);
|
||||||
|
|
||||||
|
const renderDetails = () => (
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
title="Details"
|
||||||
|
subheader="Title, short description, image..."
|
||||||
|
action={renderCollapseButton(openDetails.value, openDetails.onToggle)}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Collapse in={openDetails.value}>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Stack spacing={3} sx={{ p: 3 }}>
|
||||||
|
<Field.Text disabled={disableUserInput} name="name" label="產品名稱 / Product name" />
|
||||||
|
|
||||||
|
<Field.Text
|
||||||
|
disabled={disableUserInput}
|
||||||
|
name="subDescription"
|
||||||
|
label="Sub description"
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
<Typography variant="subtitle2">Content</Typography>
|
||||||
|
<Field.Editor name="description" sx={{ maxHeight: 480 }} />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
<Typography variant="subtitle2">Images</Typography>
|
||||||
|
<Field.Upload
|
||||||
|
multiple
|
||||||
|
thumbnail
|
||||||
|
name="images"
|
||||||
|
maxSize={3145728}
|
||||||
|
onRemove={handleRemoveFile}
|
||||||
|
onRemoveAll={handleRemoveAllFiles}
|
||||||
|
onUpload={handleProductImageUpload}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Collapse>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderProperties = () => (
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
title="Properties"
|
||||||
|
subheader="Additional functions and attributes..."
|
||||||
|
action={renderCollapseButton(openProperties.value, openProperties.onToggle)}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Collapse in={openProperties.value}>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Stack spacing={3} sx={{ p: 3 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
rowGap: 3,
|
||||||
|
columnGap: 2,
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: { xs: 'repeat(1, 1fr)', md: 'repeat(2, 1fr)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Field.Text disabled={disableUserInput} name="code" label="Product code" />
|
||||||
|
|
||||||
|
<Field.Text disabled={disableUserInput} name="sku" label="Product SKU" />
|
||||||
|
|
||||||
|
<Field.Text
|
||||||
|
disabled={disableUserInput}
|
||||||
|
name="quantity"
|
||||||
|
label="Quantity"
|
||||||
|
placeholder="0"
|
||||||
|
type="number"
|
||||||
|
slotProps={{ inputLabel: { shrink: true } }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field.Select
|
||||||
|
disabled={disableUserInput}
|
||||||
|
name="category"
|
||||||
|
label="Category"
|
||||||
|
slotProps={{
|
||||||
|
select: { native: true },
|
||||||
|
inputLabel: { shrink: true },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{PRODUCT_CATEGORY_GROUP_OPTIONS.map((category) => (
|
||||||
|
<optgroup key={category.group} label={category.group}>
|
||||||
|
{category.classify.map((classify) => (
|
||||||
|
<option key={classify} value={classify}>
|
||||||
|
{classify}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</optgroup>
|
||||||
|
))}
|
||||||
|
</Field.Select>
|
||||||
|
|
||||||
|
<Field.MultiSelect
|
||||||
|
checkbox
|
||||||
|
name="colors"
|
||||||
|
label="Colors"
|
||||||
|
options={PRODUCT_COLOR_NAME_OPTIONS}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field.MultiSelect checkbox name="sizes" label="Sizes" options={PRODUCT_SIZE_OPTIONS} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Field.Autocomplete
|
||||||
|
disabled={disableUserInput}
|
||||||
|
name="tags"
|
||||||
|
label="Tags"
|
||||||
|
placeholder="+ Tags"
|
||||||
|
multiple
|
||||||
|
freeSolo
|
||||||
|
disableCloseOnSelect
|
||||||
|
options={_tags.map((option) => option)}
|
||||||
|
getOptionLabel={(option) => option}
|
||||||
|
renderOption={(props, option) => (
|
||||||
|
<li {...props} key={option}>
|
||||||
|
{option}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
renderTags={(selected, getTagProps) =>
|
||||||
|
selected.map((option, index) => (
|
||||||
|
<Chip
|
||||||
|
{...getTagProps({ index })}
|
||||||
|
key={option}
|
||||||
|
label={option}
|
||||||
|
size="small"
|
||||||
|
color="info"
|
||||||
|
variant="soft"
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Typography variant="subtitle2">Gender</Typography>
|
||||||
|
<Field.MultiCheckbox
|
||||||
|
row
|
||||||
|
name="gender"
|
||||||
|
options={PRODUCT_GENDER_OPTIONS}
|
||||||
|
sx={{ gap: 2 }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||||
|
|
||||||
|
<Box sx={{ gap: 3, display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Field.Switch name="saleLabel.enabled" label={null} sx={{ m: 0 }} />
|
||||||
|
<Field.Text
|
||||||
|
name="saleLabel.content"
|
||||||
|
label="Sale label"
|
||||||
|
fullWidth
|
||||||
|
disabled={!values.saleLabel.enabled}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ gap: 3, display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Field.Switch name="newLabel.enabled" label={null} sx={{ m: 0 }} />
|
||||||
|
<Field.Text
|
||||||
|
name="newLabel.content"
|
||||||
|
label="New label"
|
||||||
|
fullWidth
|
||||||
|
disabled={!values.newLabel.enabled}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Collapse>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPricing = () => (
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
title="Pricing"
|
||||||
|
subheader="Price related inputs"
|
||||||
|
action={renderCollapseButton(openPricing.value, openPricing.onToggle)}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Collapse in={openPricing.value}>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Stack spacing={3} sx={{ p: 3 }}>
|
||||||
|
<Field.Text
|
||||||
|
disabled={disableUserInput}
|
||||||
|
name="price"
|
||||||
|
label="Regular price"
|
||||||
|
placeholder="0.00"
|
||||||
|
type="number"
|
||||||
|
slotProps={{
|
||||||
|
inputLabel: { shrink: true },
|
||||||
|
input: {
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start" sx={{ mr: 0.75 }}>
|
||||||
|
<Box component="span" sx={{ color: 'text.disabled' }}>
|
||||||
|
$
|
||||||
|
</Box>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field.Text
|
||||||
|
disabled={disableUserInput}
|
||||||
|
name="priceSale"
|
||||||
|
label="Sale price"
|
||||||
|
placeholder="0.00"
|
||||||
|
type="number"
|
||||||
|
slotProps={{
|
||||||
|
inputLabel: { shrink: true },
|
||||||
|
input: {
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start" sx={{ mr: 0.75 }}>
|
||||||
|
<Box component="span" sx={{ color: 'text.disabled' }}>
|
||||||
|
$
|
||||||
|
</Box>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
disabled={disableUserInput}
|
||||||
|
id="toggle-taxes"
|
||||||
|
checked={includeTaxes}
|
||||||
|
onChange={handleChangeIncludeTaxes}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Price includes taxes"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!includeTaxes && (
|
||||||
|
<Field.Text
|
||||||
|
disabled={disableUserInput}
|
||||||
|
name="taxes"
|
||||||
|
label="Tax (%)"
|
||||||
|
placeholder="0.00"
|
||||||
|
type="number"
|
||||||
|
slotProps={{
|
||||||
|
inputLabel: { shrink: true },
|
||||||
|
input: {
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start" sx={{ mr: 0.75 }}>
|
||||||
|
<Box component="span" sx={{ color: 'text.disabled' }}>
|
||||||
|
%
|
||||||
|
</Box>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Collapse>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderActions = () => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
gap: 3,
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{JSON.stringify({ errors })}</div>
|
||||||
|
<FormControlLabel
|
||||||
|
label="Publish"
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
disabled={disableUserInput}
|
||||||
|
defaultChecked
|
||||||
|
slotProps={{ input: { id: 'publish-switch' } }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
sx={{ pl: 3, flexGrow: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" variant="contained" size="large" loading={isSubmitting}>
|
||||||
|
{!currentPartyEvent ? 'create-party' : 'save-edit'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form methods={methods} onSubmit={onSubmit}>
|
||||||
|
<Stack spacing={{ xs: 3, md: 5 }} sx={{ mx: 'auto', maxWidth: { xs: 720, xl: 880 } }}>
|
||||||
|
{renderDetails()}
|
||||||
|
{renderProperties()}
|
||||||
|
{renderPricing()}
|
||||||
|
{renderActions()}
|
||||||
|
</Stack>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,124 @@
|
|||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import ButtonBase from '@mui/material/ButtonBase';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import Rating from '@mui/material/Rating';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import type { IProductReview } from 'src/types/party-event';
|
||||||
|
import { fDate } from 'src/utils/format-time';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
review: IProductReview;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProductReviewItem({ review }: Props) {
|
||||||
|
const renderInfo = () => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
gap: 2,
|
||||||
|
display: 'flex',
|
||||||
|
width: { md: 240 },
|
||||||
|
alignItems: 'center',
|
||||||
|
textAlign: { md: 'center' },
|
||||||
|
flexDirection: { xs: 'row', md: 'column' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
src={review.avatarUrl}
|
||||||
|
sx={{ width: { xs: 48, md: 64 }, height: { xs: 48, md: 64 } }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListItemText
|
||||||
|
primary={review.name}
|
||||||
|
secondary={fDate(review.postedAt)}
|
||||||
|
slotProps={{
|
||||||
|
primary: { noWrap: true },
|
||||||
|
secondary: {
|
||||||
|
noWrap: true,
|
||||||
|
sx: { mt: 0.5, typography: 'caption' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderContent = () => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
gap: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flex: '1 1 auto',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Rating size="small" value={review.rating} precision={0.1} readOnly />
|
||||||
|
|
||||||
|
{review.isPurchased && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: 'success.main',
|
||||||
|
typography: 'caption',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Iconify icon="solar:verified-check-bold" width={16} sx={{ mr: 0.5 }} />
|
||||||
|
Verified purchase
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Typography variant="body2">{review.comment}</Typography>
|
||||||
|
|
||||||
|
{!!review.attachments?.length && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
pt: 1,
|
||||||
|
gap: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{review.attachments.map((attachment) => (
|
||||||
|
<Box
|
||||||
|
key={attachment}
|
||||||
|
component="img"
|
||||||
|
alt={attachment}
|
||||||
|
src={attachment}
|
||||||
|
sx={{ width: 64, height: 64, borderRadius: 1.5 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ gap: 2, pt: 1.5, display: 'flex' }}>
|
||||||
|
<ButtonBase disableRipple sx={{ gap: 0.5, typography: 'caption' }}>
|
||||||
|
<Iconify icon="solar:like-outline" width={16} />
|
||||||
|
123
|
||||||
|
</ButtonBase>
|
||||||
|
|
||||||
|
<ButtonBase disableRipple sx={{ gap: 0.5, typography: 'caption' }}>
|
||||||
|
<Iconify icon="solar:dislike-outline" width={16} />
|
||||||
|
34
|
||||||
|
</ButtonBase>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 5,
|
||||||
|
gap: 2,
|
||||||
|
display: 'flex',
|
||||||
|
px: { xs: 2.5, md: 0 },
|
||||||
|
flexDirection: { xs: 'column', md: 'row' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderInfo()}
|
||||||
|
{renderContent()}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
import Pagination, { paginationClasses } from '@mui/material/Pagination';
|
||||||
|
import type { IProductReview } from 'src/types/party-event';
|
||||||
|
import { ProductReviewItem } from './party-event-review-item';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
reviews: IProductReview[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProductReviewList({ reviews }: Props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{reviews.map((review) => (
|
||||||
|
<ProductReviewItem key={review.id} review={review} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
count={10}
|
||||||
|
sx={{
|
||||||
|
mx: 'auto',
|
||||||
|
[`& .${paginationClasses.ul}`]: { my: 5, mx: 'auto', justifyContent: 'center' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,102 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import type { DialogProps } from '@mui/material/Dialog';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { Field, Form } from 'src/components/hook-form';
|
||||||
|
import { z as zod } from 'zod';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type ReviewSchemaType = zod.infer<typeof ReviewSchema>;
|
||||||
|
|
||||||
|
export const ReviewSchema = zod.object({
|
||||||
|
rating: zod.number().min(1, 'Rating must be greater than or equal to 1!'),
|
||||||
|
name: zod.string().min(1, { message: 'Name is required!' }),
|
||||||
|
review: zod.string().min(1, { message: 'Review is required!' }),
|
||||||
|
email: zod
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Email is required!' })
|
||||||
|
.email({ message: 'Email must be a valid email address!' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = DialogProps & {
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProductReviewNewForm({ onClose, ...other }: Props) {
|
||||||
|
const defaultValues: ReviewSchemaType = {
|
||||||
|
rating: 0,
|
||||||
|
review: '',
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const methods = useForm<ReviewSchemaType>({
|
||||||
|
mode: 'all',
|
||||||
|
resolver: zodResolver(ReviewSchema),
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
reset,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
} = methods;
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
reset();
|
||||||
|
onClose();
|
||||||
|
console.info('DATA', data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onCancel = useCallback(() => {
|
||||||
|
onClose();
|
||||||
|
reset();
|
||||||
|
}, [onClose, reset]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog onClose={onClose} {...other}>
|
||||||
|
<Form methods={methods} onSubmit={onSubmit}>
|
||||||
|
<DialogTitle> Add Review </DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<div>
|
||||||
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||||
|
Your review about this product:
|
||||||
|
</Typography>
|
||||||
|
<Field.Rating name="rating" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Field.Text name="review" label="Review *" multiline rows={3} sx={{ mt: 3 }} />
|
||||||
|
|
||||||
|
<Field.Text name="name" label="Name *" sx={{ mt: 3 }} />
|
||||||
|
|
||||||
|
<Field.Text name="email" label="Email *" sx={{ mt: 3 }} />
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button color="inherit" variant="outlined" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="submit" variant="contained" loading={isSubmitting}>
|
||||||
|
Post
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Form>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,150 @@
|
|||||||
|
import Autocomplete, { autocompleteClasses, createFilterOptions } from '@mui/material/Autocomplete';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
|
import Link, { linkClasses } from '@mui/material/Link';
|
||||||
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import match from 'autosuggest-highlight/match';
|
||||||
|
import parse from 'autosuggest-highlight/parse';
|
||||||
|
import { useDebounce } from 'minimal-shared/hooks';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useSearchProducts } from 'src/actions/party-event';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import { SearchNotFound } from 'src/components/search-not-found';
|
||||||
|
import { RouterLink } from 'src/routes/components';
|
||||||
|
import { useRouter } from 'src/routes/hooks';
|
||||||
|
import type { IPartyEventItem } from 'src/types/party-event';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
redirectPath: (id: string) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProductSearch({ redirectPath, sx }: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [selectedItem, setSelectedItem] = useState<IPartyEventItem | null>(null);
|
||||||
|
|
||||||
|
const debouncedQuery = useDebounce(searchQuery);
|
||||||
|
const { searchResults: options, searchLoading: loading } = useSearchProducts(debouncedQuery);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(item: IPartyEventItem | null) => {
|
||||||
|
setSelectedItem(item);
|
||||||
|
if (item) {
|
||||||
|
router.push(redirectPath(item.id));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[redirectPath, router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterOptions = createFilterOptions({
|
||||||
|
matchFrom: 'any',
|
||||||
|
stringify: (option: IPartyEventItem) => `${option.name} ${option.sku}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const paperStyles: SxProps<Theme> = {
|
||||||
|
width: 320,
|
||||||
|
[`& .${autocompleteClasses.listbox}`]: {
|
||||||
|
[`& .${autocompleteClasses.option}`]: {
|
||||||
|
p: 0,
|
||||||
|
[`& .${linkClasses.root}`]: {
|
||||||
|
p: 0.75,
|
||||||
|
gap: 1.5,
|
||||||
|
width: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
autoHighlight
|
||||||
|
popupIcon={null}
|
||||||
|
loading={loading}
|
||||||
|
options={options}
|
||||||
|
value={selectedItem}
|
||||||
|
filterOptions={filterOptions}
|
||||||
|
onChange={(event, newValue) => handleChange(newValue)}
|
||||||
|
onInputChange={(event, newValue) => setSearchQuery(newValue)}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
noOptionsText={<SearchNotFound query={debouncedQuery} />}
|
||||||
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
|
slotProps={{ paper: { sx: paperStyles } }}
|
||||||
|
sx={[{ width: { xs: 1, sm: 260 } }, ...(Array.isArray(sx) ? sx : [sx])]}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
placeholder="Search..."
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
...params.InputProps,
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<Iconify icon="eva:search-fill" sx={{ ml: 1, color: 'text.disabled' }} />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
endAdornment: (
|
||||||
|
<>
|
||||||
|
{loading ? <CircularProgress size={18} color="inherit" sx={{ mr: -3 }} /> : null}
|
||||||
|
{params.InputProps.endAdornment}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderOption={(props, product, { inputValue }) => {
|
||||||
|
const matches = match(product.name, inputValue);
|
||||||
|
const parts = parse(product.name, matches);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li {...props} key={product.id}>
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
href={redirectPath(product.id)}
|
||||||
|
color="inherit"
|
||||||
|
underline="none"
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
key={product.id}
|
||||||
|
alt={product.name}
|
||||||
|
src={product.coverUrl}
|
||||||
|
variant="rounded"
|
||||||
|
sx={{
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
flexShrink: 0,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div key={inputValue}>
|
||||||
|
{parts.map((part, index) => (
|
||||||
|
<Typography
|
||||||
|
key={index}
|
||||||
|
component="span"
|
||||||
|
color={part.highlight ? 'primary' : 'textPrimary'}
|
||||||
|
sx={{
|
||||||
|
typography: 'body2',
|
||||||
|
fontWeight: part.highlight ? 'fontWeightSemiBold' : 'fontWeightMedium',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{part.text}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,85 @@
|
|||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import type { GridProps } from '@mui/material/Grid';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import type { PaperProps } from '@mui/material/Paper';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Skeleton from '@mui/material/Skeleton';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type ProductItemSkeletonProps = PaperProps & {
|
||||||
|
itemCount?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProductItemSkeleton({ sx, itemCount = 16, ...other }: ProductItemSkeletonProps) {
|
||||||
|
return Array.from({ length: itemCount }, (_, index) => (
|
||||||
|
<Paper
|
||||||
|
key={index}
|
||||||
|
variant="outlined"
|
||||||
|
sx={[{ borderRadius: 2 }, ...(Array.isArray(sx) ? sx : [sx])]}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<Skeleton sx={{ pt: '100%' }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack spacing={2} sx={{ p: 3, pt: 2 }}>
|
||||||
|
<Skeleton sx={{ width: 0.5, height: 16 }} />
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<Skeleton variant="circular" sx={{ width: 16, height: 16 }} />
|
||||||
|
<Skeleton variant="circular" sx={{ width: 16, height: 16 }} />
|
||||||
|
<Skeleton variant="circular" sx={{ width: 16, height: 16 }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Skeleton sx={{ width: 40, height: 16 }} />
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function ProductDetailsSkeleton({ ...other }: GridProps) {
|
||||||
|
return (
|
||||||
|
<Grid container spacing={8} {...other}>
|
||||||
|
<Grid size={{ xs: 12, md: 6, lg: 7 }}>
|
||||||
|
<Skeleton sx={{ pt: '100%' }} />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, md: 6, lg: 5 }}>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Skeleton sx={{ height: 16, width: 48 }} />
|
||||||
|
<Skeleton sx={{ height: 16, width: 80 }} />
|
||||||
|
<Skeleton sx={{ height: 16, width: 0.5 }} />
|
||||||
|
<Skeleton sx={{ height: 16, width: 0.75 }} />
|
||||||
|
<Skeleton sx={{ height: 120 }} />
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={12}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
{Array.from({ length: 3 }, (_, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
gap: 2,
|
||||||
|
width: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Skeleton variant="circular" sx={{ width: 80, height: 80 }} />
|
||||||
|
<Skeleton sx={{ height: 16, width: 160 }} />
|
||||||
|
<Skeleton sx={{ height: 16, width: 80 }} />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,70 @@
|
|||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import MenuList from '@mui/material/MenuList';
|
||||||
|
import { usePopover } from 'minimal-shared/hooks';
|
||||||
|
import { CustomPopover } from 'src/components/custom-popover';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
sort: string;
|
||||||
|
onSort: (newValue: string) => void;
|
||||||
|
sortOptions: {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProductSort({ sort, onSort, sortOptions }: Props) {
|
||||||
|
const menuActions = usePopover();
|
||||||
|
|
||||||
|
const sortLabel = sortOptions.find((option) => option.value === sort)?.label;
|
||||||
|
|
||||||
|
const renderMenuActions = () => (
|
||||||
|
<CustomPopover
|
||||||
|
open={menuActions.open}
|
||||||
|
anchorEl={menuActions.anchorEl}
|
||||||
|
onClose={menuActions.onClose}
|
||||||
|
>
|
||||||
|
<MenuList>
|
||||||
|
{sortOptions.map((option) => (
|
||||||
|
<MenuItem
|
||||||
|
key={option.value}
|
||||||
|
selected={option.value === sort}
|
||||||
|
onClick={() => {
|
||||||
|
menuActions.onClose();
|
||||||
|
onSort(option.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</MenuList>
|
||||||
|
</CustomPopover>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
disableRipple
|
||||||
|
color="inherit"
|
||||||
|
onClick={menuActions.onOpen}
|
||||||
|
endIcon={
|
||||||
|
<Iconify
|
||||||
|
icon={menuActions.open ? 'eva:arrow-ios-upward-fill' : 'eva:arrow-ios-downward-fill'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
sx={{ fontWeight: 'fontWeightSemiBold' }}
|
||||||
|
>
|
||||||
|
Sort by:
|
||||||
|
<Box component="span" sx={{ ml: 0.5, fontWeight: 'fontWeightBold' }}>
|
||||||
|
{sortLabel}
|
||||||
|
</Box>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{renderMenuActions()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,61 @@
|
|||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import { upperFirst } from 'es-toolkit';
|
||||||
|
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 { IProductTableFilters } from 'src/types/party-event';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = FiltersResultProps & {
|
||||||
|
filters: UseSetStateReturn<IProductTableFilters>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProductTableFiltersResult({ filters, totalResults, sx }: Props) {
|
||||||
|
const { state: currentFilters, setState: updateFilters, resetState: resetFilters } = filters;
|
||||||
|
|
||||||
|
const handleRemoveStock = useCallback(
|
||||||
|
(inputValue: string) => {
|
||||||
|
const newValue = currentFilters.stock.filter((item) => item !== inputValue);
|
||||||
|
|
||||||
|
updateFilters({ stock: newValue });
|
||||||
|
},
|
||||||
|
[updateFilters, currentFilters.stock]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemovePublish = useCallback(
|
||||||
|
(inputValue: string) => {
|
||||||
|
const newValue = currentFilters.publish.filter((item) => item !== inputValue);
|
||||||
|
|
||||||
|
updateFilters({ publish: newValue });
|
||||||
|
},
|
||||||
|
[updateFilters, currentFilters.publish]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FiltersResult totalResults={totalResults} onReset={() => resetFilters()} sx={sx}>
|
||||||
|
<FiltersBlock label="Stock:" isShow={!!currentFilters.stock.length}>
|
||||||
|
{currentFilters.stock.map((item) => (
|
||||||
|
<Chip
|
||||||
|
{...chipProps}
|
||||||
|
key={item}
|
||||||
|
label={upperFirst(item)}
|
||||||
|
onDelete={() => handleRemoveStock(item)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FiltersBlock>
|
||||||
|
|
||||||
|
<FiltersBlock label="Publish:" isShow={!!currentFilters.publish.length}>
|
||||||
|
{currentFilters.publish.map((item) => (
|
||||||
|
<Chip
|
||||||
|
{...chipProps}
|
||||||
|
key={item}
|
||||||
|
label={upperFirst(item)}
|
||||||
|
onDelete={() => handleRemovePublish(item)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FiltersBlock>
|
||||||
|
</FiltersResult>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,92 @@
|
|||||||
|
// src/sections/product/product-table-row.tsx
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import LinearProgress from '@mui/material/LinearProgress';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import type { GridCellParams } from '@mui/x-data-grid';
|
||||||
|
import { Label } from 'src/components/label';
|
||||||
|
import { RouterLink } from 'src/routes/components';
|
||||||
|
import { fCurrency } from 'src/utils/format-number';
|
||||||
|
import { fDate, fTime } from 'src/utils/format-time';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type ParamsProps = {
|
||||||
|
params: GridCellParams;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RenderCellPrice({ params }: ParamsProps) {
|
||||||
|
return fCurrency(params.row.price);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RenderCellPublish({ params }: ParamsProps) {
|
||||||
|
return (
|
||||||
|
<Label variant="soft" color={params.row.publish === 'published' ? 'info' : 'default'}>
|
||||||
|
{params.row.publish}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RenderCellCreatedAt({ params }: ParamsProps) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ gap: 0.5, display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<span>{fDate(params.row.createdAt)}</span>
|
||||||
|
<Box component="span" sx={{ typography: 'caption', color: 'text.secondary' }}>
|
||||||
|
{fTime(params.row.createdAt)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RenderCellStock({ params }: ParamsProps) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: 1, typography: 'caption', color: 'text.secondary' }}>
|
||||||
|
<LinearProgress
|
||||||
|
value={(params.row.available * 100) / params.row.quantity}
|
||||||
|
variant="determinate"
|
||||||
|
color={
|
||||||
|
(params.row.inventoryType === 'out of stock' && 'error') ||
|
||||||
|
(params.row.inventoryType === 'low stock' && 'warning') ||
|
||||||
|
'success'
|
||||||
|
}
|
||||||
|
sx={{ mb: 1, height: 6, width: 80 }}
|
||||||
|
/>
|
||||||
|
{!!params.row.available && params.row.available} {params.row.inventoryType}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RenderCellProduct({ params, href }: ParamsProps & { href: string }) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: 2,
|
||||||
|
gap: 2,
|
||||||
|
width: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
alt={params.row.name}
|
||||||
|
src={params.row.coverUrl}
|
||||||
|
variant="rounded"
|
||||||
|
sx={{ width: 64, height: 64 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<Link component={RouterLink} href={href} color="inherit">
|
||||||
|
{params.row.name}
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
secondary={params.row.category}
|
||||||
|
slotProps={{
|
||||||
|
primary: { noWrap: true },
|
||||||
|
secondary: { sx: { color: 'text.disabled' } },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,184 @@
|
|||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import MenuList from '@mui/material/MenuList';
|
||||||
|
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||||
|
import type { SelectChangeEvent } from '@mui/material/Select';
|
||||||
|
import Select from '@mui/material/Select';
|
||||||
|
import type { UseSetStateReturn } from 'minimal-shared/hooks';
|
||||||
|
import { usePopover } from 'minimal-shared/hooks';
|
||||||
|
import { varAlpha } from 'minimal-shared/utils';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { CustomPopover } from 'src/components/custom-popover';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import type { IProductTableFilters } from 'src/types/party-event';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
filters: UseSetStateReturn<IProductTableFilters>;
|
||||||
|
options: {
|
||||||
|
stocks: { value: string; label: string }[];
|
||||||
|
publishs: { value: string; label: string }[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PartyEventTableToolbar({ filters, options }: Props) {
|
||||||
|
const menuActions = usePopover();
|
||||||
|
|
||||||
|
const { state: currentFilters, setState: updateFilters } = filters;
|
||||||
|
|
||||||
|
const [stock, setStock] = useState(currentFilters.stock);
|
||||||
|
const [publish, setPublish] = useState(currentFilters.publish);
|
||||||
|
|
||||||
|
const handleChangeStock = useCallback((event: SelectChangeEvent<string[]>) => {
|
||||||
|
const {
|
||||||
|
target: { value },
|
||||||
|
} = event;
|
||||||
|
|
||||||
|
setStock(typeof value === 'string' ? value.split(',') : value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleChangePublish = useCallback((event: SelectChangeEvent<string[]>) => {
|
||||||
|
const {
|
||||||
|
target: { value },
|
||||||
|
} = event;
|
||||||
|
|
||||||
|
setPublish(typeof value === 'string' ? value.split(',') : value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFilterStock = useCallback(() => {
|
||||||
|
updateFilters({ stock });
|
||||||
|
}, [updateFilters, stock]);
|
||||||
|
|
||||||
|
const handleFilterPublish = useCallback(() => {
|
||||||
|
updateFilters({ publish });
|
||||||
|
}, [publish, updateFilters]);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
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" />
|
||||||
|
Print
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem onClick={() => menuActions.onClose()}>
|
||||||
|
<Iconify icon="solar:import-bold" />
|
||||||
|
Import
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem onClick={() => menuActions.onClose()}>
|
||||||
|
<Iconify icon="solar:export-bold" />
|
||||||
|
Export
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</CustomPopover>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormControl sx={{ flexShrink: 0, width: { xs: 1, md: 200 } }}>
|
||||||
|
<InputLabel htmlFor="filter-stock-select">{t('Stock')}</InputLabel>
|
||||||
|
<Select
|
||||||
|
multiple
|
||||||
|
value={stock}
|
||||||
|
onChange={handleChangeStock}
|
||||||
|
onClose={handleFilterStock}
|
||||||
|
input={<OutlinedInput label="Stock" />}
|
||||||
|
renderValue={(selected) => selected.map((value) => t(value)).join(', ')}
|
||||||
|
inputProps={{ id: 'filter-stock-select' }}
|
||||||
|
sx={{ textTransform: 'capitalize' }}
|
||||||
|
>
|
||||||
|
{options.stocks.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
<Checkbox
|
||||||
|
disableRipple
|
||||||
|
size="small"
|
||||||
|
checked={stock.includes(option.value)}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
id: `${option.value}-checkbox`,
|
||||||
|
'aria-label': `${option.label} checkbox`,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleFilterStock}
|
||||||
|
sx={[
|
||||||
|
(theme) => ({
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontWeight: theme.typography.button,
|
||||||
|
bgcolor: varAlpha(theme.vars.palette.grey['500Channel'], 0.08),
|
||||||
|
border: `solid 1px ${varAlpha(theme.vars.palette.grey['500Channel'], 0.16)}`,
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{t('Apply')}
|
||||||
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl sx={{ flexShrink: 0, width: { xs: 1, md: 200 } }}>
|
||||||
|
<InputLabel htmlFor="filter-publish-select">{t('Publish')}</InputLabel>
|
||||||
|
<Select
|
||||||
|
multiple
|
||||||
|
value={publish}
|
||||||
|
onChange={handleChangePublish}
|
||||||
|
onClose={handleFilterPublish}
|
||||||
|
input={<OutlinedInput label={t('Publish')} />}
|
||||||
|
renderValue={(selected) => selected.map((value) => t(value)).join(', ')}
|
||||||
|
inputProps={{ id: 'filter-publish-select' }}
|
||||||
|
sx={{ textTransform: 'capitalize' }}
|
||||||
|
>
|
||||||
|
{options.publishs.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
<Checkbox
|
||||||
|
disableRipple
|
||||||
|
size="small"
|
||||||
|
checked={publish.includes(option.value)}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
id: `${option.value}-checkbox`,
|
||||||
|
'aria-label': `${option.label} checkbox`,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<MenuItem
|
||||||
|
disableGutters
|
||||||
|
disableTouchRipple
|
||||||
|
onClick={handleFilterPublish}
|
||||||
|
sx={[
|
||||||
|
(theme) => ({
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontWeight: theme.typography.button,
|
||||||
|
bgcolor: varAlpha(theme.vars.palette.grey['500Channel'], 0.08),
|
||||||
|
border: `solid 1px ${varAlpha(theme.vars.palette.grey['500Channel'], 0.16)}`,
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{t('Apply')}
|
||||||
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{renderMenuActions()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import { useBoolean } from 'minimal-shared/hooks';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function ConfirmDeleteProductDialog() {
|
||||||
|
const openDialog = useBoolean();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button color="info" variant="outlined" onClick={openDialog.onTrue}>
|
||||||
|
Open alert dialog
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dialog open onClose={openDialog.onFalse}>
|
||||||
|
<DialogTitle>Are you sure delete product ?</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent sx={{ color: 'text.secondary' }}>
|
||||||
|
Are you sure delete product ?
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="outlined" onClick={openDialog.onFalse}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button loading variant="contained" onClick={openDialog.onFalse} autoFocus>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
11
03_source/frontend/src/sections/party-event/view/index.ts
Normal file
11
03_source/frontend/src/sections/party-event/view/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export * from './party-event-edit-view';
|
||||||
|
|
||||||
|
export * from './party-event-shop-view';
|
||||||
|
|
||||||
|
export * from './party-event-list-view';
|
||||||
|
|
||||||
|
export * from './party-event-create-view';
|
||||||
|
|
||||||
|
export * from './party-event-details-view';
|
||||||
|
|
||||||
|
export * from './party-event-shop-details-view';
|
@@ -0,0 +1,24 @@
|
|||||||
|
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
|
||||||
|
import { DashboardContent } from 'src/layouts/dashboard';
|
||||||
|
import { paths } from 'src/routes/paths';
|
||||||
|
import { PartyEventNewEditForm } from '../party-event-new-edit-form';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function PartyEventCreateView() {
|
||||||
|
return (
|
||||||
|
<DashboardContent>
|
||||||
|
<CustomBreadcrumbs
|
||||||
|
heading="Create a new party event"
|
||||||
|
links={[
|
||||||
|
{ name: 'Dashboard', href: paths.dashboard.root },
|
||||||
|
{ name: 'Party Event', href: paths.dashboard.product.root },
|
||||||
|
{ name: 'New party event' },
|
||||||
|
]}
|
||||||
|
sx={{ mb: { xs: 3, md: 5 } }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PartyEventNewEditForm />
|
||||||
|
</DashboardContent>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,186 @@
|
|||||||
|
// src/sections/product/view/product-details-view.tsx
|
||||||
|
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Tab from '@mui/material/Tab';
|
||||||
|
import Tabs from '@mui/material/Tabs';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { useTabs } from 'minimal-shared/hooks';
|
||||||
|
import { varAlpha } from 'minimal-shared/utils';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
// import { PRODUCT_PUBLISH_OPTIONS } from 'src/_mock';
|
||||||
|
import { EmptyContent } from 'src/components/empty-content';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import { DashboardContent } from 'src/layouts/dashboard';
|
||||||
|
import { RouterLink } from 'src/routes/components';
|
||||||
|
import { paths } from 'src/routes/paths';
|
||||||
|
import type { IPartyEventItem } from 'src/types/party-event';
|
||||||
|
import { ProductDetailsCarousel } from '../party-event-details-carousel';
|
||||||
|
import { ProductDetailsDescription } from '../party-event-details-description';
|
||||||
|
import { ProductDetailsReview } from '../party-event-details-review';
|
||||||
|
import { PartyEventDetailsSummary } from '../party-event-details-summary';
|
||||||
|
import { ProductDetailsToolbar } from '../party-event-details-toolbar';
|
||||||
|
import { ProductDetailsSkeleton } from '../party-event-skeleton';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const SUMMARY = [
|
||||||
|
{
|
||||||
|
title: '100% original',
|
||||||
|
description: 'Chocolate bar candy canes ice cream toffee cookie halvah.',
|
||||||
|
icon: 'solar:verified-check-bold',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '10 days replacement',
|
||||||
|
description: 'Marshmallow biscuit donut dragée fruitcake wafer.',
|
||||||
|
icon: 'solar:clock-circle-bold',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Year warranty',
|
||||||
|
description: 'Cotton candy gingerbread cake I love sugar sweet.',
|
||||||
|
icon: 'solar:shield-check-bold',
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
partyEvent?: IPartyEventItem;
|
||||||
|
loading?: boolean;
|
||||||
|
error?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PartyEventDetailsView({ partyEvent, error, loading }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const tabs = useTabs('description');
|
||||||
|
|
||||||
|
const [publish, setPublish] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (partyEvent) {
|
||||||
|
setPublish(partyEvent?.publish);
|
||||||
|
}
|
||||||
|
}, [partyEvent]);
|
||||||
|
|
||||||
|
const handleChangePublish = useCallback((newValue: string) => {
|
||||||
|
setPublish(newValue);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const PRODUCT_PUBLISH_OPTIONS = [
|
||||||
|
{ value: 'published', label: t('Published') },
|
||||||
|
{ value: 'draft', label: t('Draft') },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<DashboardContent sx={{ pt: 5 }}>
|
||||||
|
<ProductDetailsSkeleton />
|
||||||
|
</DashboardContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<DashboardContent sx={{ pt: 5 }}>
|
||||||
|
<EmptyContent
|
||||||
|
filled
|
||||||
|
title={t('Party event not found!')}
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
component={RouterLink}
|
||||||
|
href={paths.dashboard.partyEvent.root}
|
||||||
|
startIcon={<Iconify width={16} icon="eva:arrow-ios-back-fill" />}
|
||||||
|
sx={{ mt: 3 }}
|
||||||
|
>
|
||||||
|
{t('Back to list')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
sx={{ py: 10, height: 'auto', flexGrow: 'unset' }}
|
||||||
|
/>
|
||||||
|
</DashboardContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardContent>
|
||||||
|
<ProductDetailsToolbar
|
||||||
|
backHref={paths.dashboard.partyEvent.root}
|
||||||
|
liveHref={paths.partyEvent.details(`${partyEvent?.id}`)}
|
||||||
|
editHref={paths.dashboard.partyEvent.edit(`${partyEvent?.id}`)}
|
||||||
|
publish={publish}
|
||||||
|
onChangePublish={handleChangePublish}
|
||||||
|
publishOptions={PRODUCT_PUBLISH_OPTIONS}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Grid container spacing={{ xs: 3, md: 5, lg: 8 }}>
|
||||||
|
<Grid size={{ xs: 12, md: 6, lg: 7 }}>
|
||||||
|
<ProductDetailsCarousel images={partyEvent?.images ?? []} />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, md: 6, lg: 5 }}>
|
||||||
|
{partyEvent && <PartyEventDetailsSummary disableActions partyEvent={partyEvent} />}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
gap: 5,
|
||||||
|
my: 10,
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: { xs: 'repeat(1, 1fr)', md: 'repeat(3, 1fr)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{SUMMARY.map((item) => (
|
||||||
|
<Box key={item.title} sx={{ textAlign: 'center', px: 5 }}>
|
||||||
|
<Iconify icon={item.icon} width={32} sx={{ color: 'primary.main' }} />
|
||||||
|
|
||||||
|
<Typography variant="subtitle1" sx={{ mb: 1, mt: 2 }}>
|
||||||
|
{item.title}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||||
|
{item.description}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Tabs
|
||||||
|
value={tabs.value}
|
||||||
|
onChange={tabs.onChange}
|
||||||
|
sx={[
|
||||||
|
(theme) => ({
|
||||||
|
px: 3,
|
||||||
|
boxShadow: `inset 0 -2px 0 0 ${varAlpha(theme.vars.palette.grey['500Channel'], 0.08)}`,
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
{ value: 'description', label: 'Description' },
|
||||||
|
{ value: 'reviews', label: `Reviews (${partyEvent?.reviews.length})` },
|
||||||
|
].map((tab) => (
|
||||||
|
<Tab key={tab.value} value={tab.value} label={tab.label} />
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{tabs.value === 'description' && (
|
||||||
|
<ProductDetailsDescription description={partyEvent?.description ?? ''} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tabs.value === 'reviews' && (
|
||||||
|
<ProductDetailsReview
|
||||||
|
ratings={partyEvent?.ratings ?? []}
|
||||||
|
reviews={partyEvent?.reviews ?? []}
|
||||||
|
totalRatings={partyEvent?.totalRatings ?? 0}
|
||||||
|
totalReviews={partyEvent?.totalReviews ?? 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</DashboardContent>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
|
||||||
|
import { DashboardContent } from 'src/layouts/dashboard';
|
||||||
|
import { paths } from 'src/routes/paths';
|
||||||
|
import type { IPartyEventItem } from 'src/types/party-event';
|
||||||
|
import { PartyEventNewEditForm } from '../party-event-new-edit-form';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
partyEvent?: IPartyEventItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PartyEventEditView({ partyEvent }: Props) {
|
||||||
|
return (
|
||||||
|
<DashboardContent>
|
||||||
|
<CustomBreadcrumbs
|
||||||
|
heading="Edit"
|
||||||
|
backHref={paths.dashboard.partyEvent.root}
|
||||||
|
links={[
|
||||||
|
{ name: 'Dashboard', href: paths.dashboard.root },
|
||||||
|
{ name: 'Party Event', href: paths.dashboard.partyEvent.root },
|
||||||
|
{ name: partyEvent?.name },
|
||||||
|
]}
|
||||||
|
sx={{ mb: { xs: 3, md: 5 } }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PartyEventNewEditForm currentPartyEvent={partyEvent} />
|
||||||
|
</DashboardContent>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,497 @@
|
|||||||
|
// src/sections/party-event/view/party-event-list-view.tsx
|
||||||
|
//
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import type {
|
||||||
|
GridActionsCellItemProps,
|
||||||
|
GridColDef,
|
||||||
|
GridColumnVisibilityModel,
|
||||||
|
GridRowSelectionModel,
|
||||||
|
GridSlotProps,
|
||||||
|
} from '@mui/x-data-grid';
|
||||||
|
import {
|
||||||
|
DataGrid,
|
||||||
|
GridActionsCellItem,
|
||||||
|
gridClasses,
|
||||||
|
GridToolbarColumnsButton,
|
||||||
|
GridToolbarContainer,
|
||||||
|
GridToolbarExport,
|
||||||
|
GridToolbarFilterButton,
|
||||||
|
GridToolbarQuickFilter,
|
||||||
|
} from '@mui/x-data-grid';
|
||||||
|
import type { UseSetStateReturn } from 'minimal-shared/hooks';
|
||||||
|
import { useBoolean, useSetState } from 'minimal-shared/hooks';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
// import { PRODUCT_STOCK_OPTIONS } from 'src/_mock';
|
||||||
|
import { deletePartyEvent, useGetPartyEvents } from 'src/actions/party-event';
|
||||||
|
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
|
||||||
|
import { ConfirmDialog } from 'src/components/custom-dialog';
|
||||||
|
import { EmptyContent } from 'src/components/empty-content';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import { toast } from 'src/components/snackbar';
|
||||||
|
import { DashboardContent } from 'src/layouts/dashboard';
|
||||||
|
import { endpoints } from 'src/lib/axios';
|
||||||
|
import { RouterLink } from 'src/routes/components';
|
||||||
|
import { paths } from 'src/routes/paths';
|
||||||
|
import type { IPartyEventItem, IProductTableFilters } from 'src/types/party-event';
|
||||||
|
import { mutate } from 'swr';
|
||||||
|
import { ProductTableFiltersResult } from '../party-event-table-filters-result';
|
||||||
|
import {
|
||||||
|
RenderCellCreatedAt,
|
||||||
|
RenderCellPrice,
|
||||||
|
RenderCellProduct,
|
||||||
|
RenderCellPublish,
|
||||||
|
RenderCellStock,
|
||||||
|
} from '../party-event-table-row';
|
||||||
|
import { PartyEventTableToolbar } from '../party-event-table-toolbar';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const HIDE_COLUMNS = { category: false };
|
||||||
|
|
||||||
|
const HIDE_COLUMNS_TOGGLABLE = ['category', 'actions'];
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function PartyEventListView() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const confirmDialog = useBoolean();
|
||||||
|
|
||||||
|
const PRODUCT_STOCK_OPTIONS = [
|
||||||
|
{ value: 'in stock', label: t('In stock') },
|
||||||
|
{ value: 'low stock', label: t('Low stock') },
|
||||||
|
{ value: 'out of stock', label: t('Out of stock') },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PUBLISH_OPTIONS = [
|
||||||
|
{ value: 'published', label: t('Published') },
|
||||||
|
{ value: 'draft', label: t('Draft') },
|
||||||
|
];
|
||||||
|
|
||||||
|
const confirmDeleteMultiItemsDialog = useBoolean();
|
||||||
|
|
||||||
|
const confirmDeleteSingleItemDialog = useBoolean();
|
||||||
|
const [idToDelete, setIdToDelete] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { partyEvents, partyEventsLoading } = useGetPartyEvents();
|
||||||
|
|
||||||
|
const [tableData, setTableData] = useState<IPartyEventItem[]>(partyEvents);
|
||||||
|
const [selectedRowIds, setSelectedRowIds] = useState<GridRowSelectionModel>([]);
|
||||||
|
const [filterButtonEl, setFilterButtonEl] = useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
const filters = useSetState<IProductTableFilters>({ publish: [], stock: [] });
|
||||||
|
const { state: currentFilters } = filters;
|
||||||
|
|
||||||
|
const [columnVisibilityModel, setColumnVisibilityModel] =
|
||||||
|
useState<GridColumnVisibilityModel>(HIDE_COLUMNS);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (partyEvents.length) {
|
||||||
|
setTableData(partyEvents);
|
||||||
|
}
|
||||||
|
}, [partyEvents]);
|
||||||
|
|
||||||
|
const canReset = currentFilters.publish.length > 0 || currentFilters.stock.length > 0;
|
||||||
|
|
||||||
|
const dataFiltered = applyFilter({
|
||||||
|
inputData: tableData,
|
||||||
|
filters: currentFilters,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteSingleRow = useCallback(async () => {
|
||||||
|
// const deleteRow = tableData.filter((row) => row.id !== id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (idToDelete) {
|
||||||
|
await deletePartyEvent(idToDelete);
|
||||||
|
toast.success('Delete success!');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error('Delete failed!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// setTableData(deleteRow);
|
||||||
|
setDeleteInProgress(false);
|
||||||
|
}, [idToDelete, mutate]);
|
||||||
|
|
||||||
|
// NOTE: this is working using example from calendar
|
||||||
|
const handleDeleteRow = useCallback(
|
||||||
|
async (id: string) => {
|
||||||
|
try {
|
||||||
|
await deletePartyEvent(id);
|
||||||
|
|
||||||
|
// invalidate cache to reload list
|
||||||
|
await mutate(endpoints.partyEvent.list);
|
||||||
|
|
||||||
|
toast.success('Delete success!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
toast.error('Delete error!');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[tableData]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeleteRows = useCallback(() => {
|
||||||
|
const deleteRows = tableData.filter((row) => !selectedRowIds.includes(row.id));
|
||||||
|
|
||||||
|
toast.success('Delete success!');
|
||||||
|
|
||||||
|
setTableData(deleteRows);
|
||||||
|
}, [selectedRowIds, tableData]);
|
||||||
|
|
||||||
|
const CustomToolbarCallback = useCallback(
|
||||||
|
() => (
|
||||||
|
<CustomToolbar
|
||||||
|
filters={filters}
|
||||||
|
canReset={canReset}
|
||||||
|
selectedRowIds={selectedRowIds}
|
||||||
|
setFilterButtonEl={setFilterButtonEl}
|
||||||
|
filteredResults={dataFiltered.length}
|
||||||
|
onOpenConfirmDeleteRows={confirmDeleteMultiItemsDialog.onTrue}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[currentFilters, selectedRowIds]
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns: GridColDef[] = [
|
||||||
|
{ field: 'category', headerName: t('Category'), filterable: false },
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
headerName: t('Party Event'),
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 360,
|
||||||
|
hideable: false,
|
||||||
|
renderCell: (params) => (
|
||||||
|
<RenderCellProduct
|
||||||
|
params={params}
|
||||||
|
href={paths.dashboard.partyEvent.details(params.row.id)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createdAt',
|
||||||
|
headerName: t('Create-at'),
|
||||||
|
width: 160,
|
||||||
|
renderCell: (params) => <RenderCellCreatedAt params={params} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'inventoryType',
|
||||||
|
headerName: t('Stock'),
|
||||||
|
width: 160,
|
||||||
|
type: 'singleSelect',
|
||||||
|
valueOptions: PRODUCT_STOCK_OPTIONS,
|
||||||
|
renderCell: (params) => <RenderCellStock params={params} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'price',
|
||||||
|
headerName: t('Price'),
|
||||||
|
width: 140,
|
||||||
|
editable: true,
|
||||||
|
renderCell: (params) => <RenderCellPrice params={params} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'publish',
|
||||||
|
headerName: t('Publish'),
|
||||||
|
width: 110,
|
||||||
|
type: 'singleSelect',
|
||||||
|
editable: true,
|
||||||
|
valueOptions: PUBLISH_OPTIONS,
|
||||||
|
renderCell: (params) => <RenderCellPublish params={params} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'actions',
|
||||||
|
field: 'actions',
|
||||||
|
headerName: ' ',
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right',
|
||||||
|
width: 80,
|
||||||
|
sortable: false,
|
||||||
|
filterable: false,
|
||||||
|
disableColumnMenu: true,
|
||||||
|
getActions: (params) => [
|
||||||
|
<GridActionsLinkItem
|
||||||
|
showInMenu
|
||||||
|
icon={<Iconify icon="solar:eye-bold" />}
|
||||||
|
label="View"
|
||||||
|
href={paths.dashboard.partyEvent.details(params.row.id)}
|
||||||
|
/>,
|
||||||
|
<GridActionsLinkItem
|
||||||
|
showInMenu
|
||||||
|
icon={<Iconify icon="solar:pen-bold" />}
|
||||||
|
label="Edit"
|
||||||
|
href={paths.dashboard.partyEvent.edit(params.row.id)}
|
||||||
|
/>,
|
||||||
|
<GridActionsCellItem
|
||||||
|
showInMenu
|
||||||
|
icon={<Iconify icon="solar:trash-bin-trash-bold" />}
|
||||||
|
label="Delete"
|
||||||
|
onClick={() => {
|
||||||
|
setIdToDelete(params.row.id);
|
||||||
|
confirmDeleteSingleItemDialog.onTrue();
|
||||||
|
}}
|
||||||
|
sx={{ color: 'error.main' }}
|
||||||
|
/>,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getTogglableColumns = () =>
|
||||||
|
columns
|
||||||
|
.filter((column) => !HIDE_COLUMNS_TOGGLABLE.includes(column.field))
|
||||||
|
.map((column) => column.field);
|
||||||
|
|
||||||
|
const renderDeleteMultipleItemsConfirmDialog = () => (
|
||||||
|
<ConfirmDialog
|
||||||
|
open={confirmDeleteMultiItemsDialog.value}
|
||||||
|
onClose={confirmDeleteMultiItemsDialog.onFalse}
|
||||||
|
title="Delete multiple party events"
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
Are you sure want to delete <strong> {selectedRowIds.length} </strong> items?
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
onClick={() => {
|
||||||
|
handleDeleteRows();
|
||||||
|
confirmDeleteMultiItemsDialog.onFalse();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Delete')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const [deleteInProgress, setDeleteInProgress] = useState<boolean>(false);
|
||||||
|
const renderDeleteSingleItemConfirmDialog = () => (
|
||||||
|
<ConfirmDialog
|
||||||
|
open={confirmDeleteSingleItemDialog.value}
|
||||||
|
onClose={confirmDeleteSingleItemDialog.onFalse}
|
||||||
|
title="Delete party event"
|
||||||
|
content={<>Are you sure want to delete item?</>}
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
loading={deleteInProgress}
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteInProgress(true);
|
||||||
|
handleDeleteSingleRow();
|
||||||
|
confirmDeleteSingleItemDialog.onFalse();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Delete')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DashboardContent sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<CustomBreadcrumbs
|
||||||
|
heading={t('Party Event List')}
|
||||||
|
links={[
|
||||||
|
{ name: t('Dashboard'), href: paths.dashboard.root },
|
||||||
|
{ name: t('Party Event'), href: paths.dashboard.partyEvent.root },
|
||||||
|
{ name: t('List') },
|
||||||
|
]}
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
component={RouterLink}
|
||||||
|
href={paths.dashboard.partyEvent.new}
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<Iconify icon="mingcute:add-line" />}
|
||||||
|
>
|
||||||
|
{t('new-party-event')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
sx={{ mb: { xs: 3, md: 5 } }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
minHeight: 640,
|
||||||
|
flexGrow: { md: 1 },
|
||||||
|
display: { md: 'flex' },
|
||||||
|
height: { xs: 800, md: '1px' },
|
||||||
|
flexDirection: { md: 'column' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DataGrid
|
||||||
|
checkboxSelection
|
||||||
|
disableRowSelectionOnClick
|
||||||
|
rows={dataFiltered}
|
||||||
|
columns={columns}
|
||||||
|
loading={partyEventsLoading}
|
||||||
|
getRowHeight={() => 'auto'}
|
||||||
|
pageSizeOptions={[5, 10, 20, { value: -1, label: 'All' }]}
|
||||||
|
initialState={{ pagination: { paginationModel: { pageSize: 10 } } }}
|
||||||
|
onRowSelectionModelChange={(newSelectionModel) => setSelectedRowIds(newSelectionModel)}
|
||||||
|
columnVisibilityModel={columnVisibilityModel}
|
||||||
|
onColumnVisibilityModelChange={(newModel) => setColumnVisibilityModel(newModel)}
|
||||||
|
slots={{
|
||||||
|
toolbar: CustomToolbarCallback,
|
||||||
|
noRowsOverlay: () => <EmptyContent />,
|
||||||
|
noResultsOverlay: () => <EmptyContent title="No results found" />,
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
toolbar: { setFilterButtonEl },
|
||||||
|
panel: { anchorEl: filterButtonEl },
|
||||||
|
columnsManagement: { getTogglableColumns },
|
||||||
|
}}
|
||||||
|
sx={{ [`& .${gridClasses.cell}`]: { alignItems: 'center', display: 'inline-flex' } }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</DashboardContent>
|
||||||
|
|
||||||
|
{renderDeleteMultipleItemsConfirmDialog()}
|
||||||
|
{renderDeleteSingleItemConfirmDialog()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
declare module '@mui/x-data-grid' {
|
||||||
|
interface ToolbarPropsOverrides {
|
||||||
|
setFilterButtonEl: React.Dispatch<React.SetStateAction<HTMLButtonElement | null>>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomToolbarProps = GridSlotProps['toolbar'] & {
|
||||||
|
canReset: boolean;
|
||||||
|
filteredResults: number;
|
||||||
|
selectedRowIds: GridRowSelectionModel;
|
||||||
|
filters: UseSetStateReturn<IProductTableFilters>;
|
||||||
|
|
||||||
|
onOpenConfirmDeleteRows: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function CustomToolbar({
|
||||||
|
filters,
|
||||||
|
canReset,
|
||||||
|
selectedRowIds,
|
||||||
|
filteredResults,
|
||||||
|
setFilterButtonEl,
|
||||||
|
onOpenConfirmDeleteRows,
|
||||||
|
}: CustomToolbarProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const PRODUCT_STOCK_OPTIONS = [
|
||||||
|
{ value: 'in stock', label: t('In stock') },
|
||||||
|
{ value: 'low stock', label: t('Low stock') },
|
||||||
|
{ value: 'out of stock', label: t('Out of stock') },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PUBLISH_OPTIONS = [
|
||||||
|
{ value: 'published', label: t('Published') },
|
||||||
|
{ value: 'draft', label: t('Draft') },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GridToolbarContainer>
|
||||||
|
<PartyEventTableToolbar
|
||||||
|
filters={filters}
|
||||||
|
options={{ stocks: PRODUCT_STOCK_OPTIONS, publishs: PUBLISH_OPTIONS }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GridToolbarQuickFilter />
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
gap: 1,
|
||||||
|
flexGrow: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!!selectedRowIds.length && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
startIcon={<Iconify icon="solar:trash-bin-trash-bold" />}
|
||||||
|
onClick={onOpenConfirmDeleteRows}
|
||||||
|
>
|
||||||
|
Delete ({selectedRowIds.length})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<GridToolbarColumnsButton />
|
||||||
|
<GridToolbarFilterButton ref={setFilterButtonEl} />
|
||||||
|
<GridToolbarExport />
|
||||||
|
</Box>
|
||||||
|
</GridToolbarContainer>
|
||||||
|
|
||||||
|
{canReset && (
|
||||||
|
<ProductTableFiltersResult
|
||||||
|
filters={filters}
|
||||||
|
totalResults={filteredResults}
|
||||||
|
sx={{ p: 2.5, pt: 0 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type GridActionsLinkItemProps = Pick<GridActionsCellItemProps, 'icon' | 'label' | 'showInMenu'> & {
|
||||||
|
href: string;
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
ref?: React.RefObject<HTMLLIElement | null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GridActionsLinkItem({ ref, href, label, icon, sx }: GridActionsLinkItemProps) {
|
||||||
|
return (
|
||||||
|
<MenuItem ref={ref} sx={sx}>
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
href={href}
|
||||||
|
underline="none"
|
||||||
|
color="inherit"
|
||||||
|
sx={{ width: 1, display: 'flex', alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
{icon && <ListItemIcon>{icon}</ListItemIcon>}
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type ApplyFilterProps = {
|
||||||
|
inputData: IPartyEventItem[];
|
||||||
|
filters: IProductTableFilters;
|
||||||
|
};
|
||||||
|
|
||||||
|
function applyFilter({ inputData, filters }: ApplyFilterProps) {
|
||||||
|
const { stock, publish } = filters;
|
||||||
|
|
||||||
|
if (stock.length) {
|
||||||
|
inputData = inputData.filter((partyEvent) => stock.includes(partyEvent.inventoryType));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publish.length) {
|
||||||
|
inputData = inputData.filter((partyEvent) => publish.includes(partyEvent.publish));
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputData;
|
||||||
|
}
|
@@ -0,0 +1,182 @@
|
|||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import Container from '@mui/material/Container';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import Tab from '@mui/material/Tab';
|
||||||
|
import Tabs from '@mui/material/Tabs';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { useTabs } from 'minimal-shared/hooks';
|
||||||
|
import { varAlpha } from 'minimal-shared/utils';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
|
||||||
|
import { EmptyContent } from 'src/components/empty-content';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import { RouterLink } from 'src/routes/components';
|
||||||
|
import { paths } from 'src/routes/paths';
|
||||||
|
import type { IPartyEventItem } from 'src/types/party-event';
|
||||||
|
import { useCheckoutContext } from '../../checkout/context';
|
||||||
|
import { CartIcon } from '../cart-icon';
|
||||||
|
import { ProductDetailsCarousel } from '../party-event-details-carousel';
|
||||||
|
import { ProductDetailsDescription } from '../party-event-details-description';
|
||||||
|
import { ProductDetailsReview } from '../party-event-details-review';
|
||||||
|
import { PartyEventDetailsSummary } from '../party-event-details-summary';
|
||||||
|
import { ProductDetailsSkeleton } from '../party-event-skeleton';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const SUMMARY = [
|
||||||
|
{
|
||||||
|
title: '100% original',
|
||||||
|
description: 'Chocolate bar candy canes ice cream toffee cookie halvah.',
|
||||||
|
icon: 'solar:verified-check-bold',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '10 days replacement',
|
||||||
|
description: 'Marshmallow biscuit donut dragée fruitcake wafer.',
|
||||||
|
icon: 'solar:clock-circle-bold',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Year warranty',
|
||||||
|
description: 'Cotton candy gingerbread cake I love sugar sweet.',
|
||||||
|
icon: 'solar:shield-check-bold',
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
product?: IPartyEventItem;
|
||||||
|
loading?: boolean;
|
||||||
|
error?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ProductShopDetailsView({ product, error, loading }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { state: checkoutState, onAddToCart } = useCheckoutContext();
|
||||||
|
|
||||||
|
const containerStyles: SxProps<Theme> = {
|
||||||
|
mt: 5,
|
||||||
|
mb: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = useTabs('description');
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Container sx={containerStyles}>
|
||||||
|
<ProductDetailsSkeleton />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Container sx={containerStyles}>
|
||||||
|
<EmptyContent
|
||||||
|
filled
|
||||||
|
title={t('Product not found!')}
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
component={RouterLink}
|
||||||
|
href={paths.product.root}
|
||||||
|
startIcon={<Iconify width={16} icon="eva:arrow-ios-back-fill" />}
|
||||||
|
sx={{ mt: 3 }}
|
||||||
|
>
|
||||||
|
Back to list
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
sx={{ py: 10 }}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container sx={containerStyles}>
|
||||||
|
<CartIcon totalItems={checkoutState.totalItems} />
|
||||||
|
|
||||||
|
<CustomBreadcrumbs
|
||||||
|
links={[
|
||||||
|
{ name: 'Home', href: '/' },
|
||||||
|
{ name: 'Shop', href: paths.product.root },
|
||||||
|
{ name: product?.name },
|
||||||
|
]}
|
||||||
|
sx={{ mb: 5 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Grid container spacing={{ xs: 3, md: 5, lg: 8 }}>
|
||||||
|
<Grid size={{ xs: 12, md: 6, lg: 7 }}>
|
||||||
|
<ProductDetailsCarousel images={product?.images} />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, md: 6, lg: 5 }}>
|
||||||
|
{product && (
|
||||||
|
<PartyEventDetailsSummary
|
||||||
|
partyEvent={product}
|
||||||
|
items={checkoutState.items}
|
||||||
|
onAddToCart={onAddToCart}
|
||||||
|
disableActions={!product?.available}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
gap: 5,
|
||||||
|
my: 10,
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: { xs: 'repeat(1, 1fr)', md: 'repeat(3, 1fr)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{SUMMARY.map((item) => (
|
||||||
|
<Box key={item.title} sx={{ textAlign: 'center', px: 5 }}>
|
||||||
|
<Iconify icon={item.icon} width={32} sx={{ color: 'primary.main' }} />
|
||||||
|
|
||||||
|
<Typography variant="subtitle1" sx={{ mb: 1, mt: 2 }}>
|
||||||
|
{item.title}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||||
|
{item.description}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Tabs
|
||||||
|
value={tabs.value}
|
||||||
|
onChange={tabs.onChange}
|
||||||
|
sx={[
|
||||||
|
(theme) => ({
|
||||||
|
px: 3,
|
||||||
|
boxShadow: `inset 0 -2px 0 0 ${varAlpha(theme.vars.palette.grey['500Channel'], 0.08)}`,
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
{ value: 'description', label: 'Description' },
|
||||||
|
{ value: 'reviews', label: `Reviews (${product?.reviews.length})` },
|
||||||
|
].map((tab) => (
|
||||||
|
<Tab key={tab.value} value={tab.value} label={tab.label} />
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{tabs.value === 'description' && (
|
||||||
|
<ProductDetailsDescription description={product?.description} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tabs.value === 'reviews' && (
|
||||||
|
<ProductDetailsReview
|
||||||
|
ratings={product?.ratings}
|
||||||
|
reviews={product?.reviews}
|
||||||
|
totalRatings={product?.totalRatings}
|
||||||
|
totalReviews={product?.totalReviews}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,193 @@
|
|||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Container from '@mui/material/Container';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { orderBy } from 'es-toolkit';
|
||||||
|
import { useBoolean, useSetState } from 'minimal-shared/hooks';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
PRODUCT_CATEGORY_OPTIONS,
|
||||||
|
PRODUCT_COLOR_OPTIONS,
|
||||||
|
PRODUCT_GENDER_OPTIONS,
|
||||||
|
PRODUCT_RATING_OPTIONS,
|
||||||
|
PRODUCT_SORT_OPTIONS,
|
||||||
|
} from 'src/_mock';
|
||||||
|
import { EmptyContent } from 'src/components/empty-content';
|
||||||
|
import { paths } from 'src/routes/paths';
|
||||||
|
import type { IPartyEventItem, IProductFilters } from 'src/types/party-event';
|
||||||
|
import { useCheckoutContext } from '../../checkout/context';
|
||||||
|
import { CartIcon } from '../cart-icon';
|
||||||
|
import { ProductFiltersDrawer } from '../party-event-filters-drawer';
|
||||||
|
import { ProductFiltersResult } from '../party-event-filters-result';
|
||||||
|
import { PartyEventList } from '../party-event-list';
|
||||||
|
import { ProductSearch } from '../party-event-search';
|
||||||
|
import { ProductSort } from '../party-event-sort';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
products: IPartyEventItem[];
|
||||||
|
loading?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PartyEventShopView({ products, loading }: Props) {
|
||||||
|
const { state: checkoutState } = useCheckoutContext();
|
||||||
|
|
||||||
|
const openFilters = useBoolean();
|
||||||
|
|
||||||
|
const [sortBy, setSortBy] = useState('featured');
|
||||||
|
|
||||||
|
const filters = useSetState<IProductFilters>({
|
||||||
|
gender: [],
|
||||||
|
colors: [],
|
||||||
|
rating: '',
|
||||||
|
category: 'all',
|
||||||
|
priceRange: [0, 200],
|
||||||
|
});
|
||||||
|
const { state: currentFilters } = filters;
|
||||||
|
|
||||||
|
const dataFiltered = applyFilter({
|
||||||
|
inputData: products,
|
||||||
|
filters: currentFilters,
|
||||||
|
sortBy,
|
||||||
|
});
|
||||||
|
|
||||||
|
const canReset =
|
||||||
|
currentFilters.gender.length > 0 ||
|
||||||
|
currentFilters.colors.length > 0 ||
|
||||||
|
currentFilters.rating !== '' ||
|
||||||
|
currentFilters.category !== 'all' ||
|
||||||
|
currentFilters.priceRange[0] !== 0 ||
|
||||||
|
currentFilters.priceRange[1] !== 200;
|
||||||
|
|
||||||
|
const notFound = !dataFiltered.length && canReset;
|
||||||
|
const isEmpty = !loading && !products.length;
|
||||||
|
|
||||||
|
const renderFilters = () => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
gap: 3,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
|
alignItems: { xs: 'flex-end', sm: 'center' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProductSearch redirectPath={(id: string) => paths.product.details(id)} />
|
||||||
|
|
||||||
|
<Box sx={{ gap: 1, flexShrink: 0, display: 'flex' }}>
|
||||||
|
<ProductFiltersDrawer
|
||||||
|
filters={filters}
|
||||||
|
canReset={canReset}
|
||||||
|
open={openFilters.value}
|
||||||
|
onOpen={openFilters.onTrue}
|
||||||
|
onClose={openFilters.onFalse}
|
||||||
|
options={{
|
||||||
|
colors: PRODUCT_COLOR_OPTIONS,
|
||||||
|
ratings: PRODUCT_RATING_OPTIONS,
|
||||||
|
genders: PRODUCT_GENDER_OPTIONS,
|
||||||
|
categories: ['all', ...PRODUCT_CATEGORY_OPTIONS],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProductSort
|
||||||
|
sort={sortBy}
|
||||||
|
onSort={(newValue: string) => setSortBy(newValue)}
|
||||||
|
sortOptions={PRODUCT_SORT_OPTIONS}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderResults = () => (
|
||||||
|
<ProductFiltersResult filters={filters} totalResults={dataFiltered.length} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderNotFound = () => <EmptyContent filled sx={{ py: 10 }} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container sx={{ mb: 10 }}>
|
||||||
|
<CartIcon totalItems={checkoutState.totalItems} />
|
||||||
|
|
||||||
|
<Typography variant="h4" sx={{ my: { xs: 3, md: 5 } }}>
|
||||||
|
Shop
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stack spacing={2.5} sx={{ mb: { xs: 3, md: 5 } }}>
|
||||||
|
{renderFilters()}
|
||||||
|
{canReset && renderResults()}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{notFound || isEmpty ? (
|
||||||
|
renderNotFound()
|
||||||
|
) : (
|
||||||
|
<PartyEventList partyEvents={dataFiltered} loading={loading} />
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type ApplyFilterProps = {
|
||||||
|
sortBy: string;
|
||||||
|
filters: IProductFilters;
|
||||||
|
inputData: IPartyEventItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function applyFilter({ inputData, filters, sortBy }: ApplyFilterProps) {
|
||||||
|
const { gender, category, colors, priceRange, rating } = filters;
|
||||||
|
|
||||||
|
const min = priceRange[0];
|
||||||
|
const max = priceRange[1];
|
||||||
|
|
||||||
|
// Sort by
|
||||||
|
if (sortBy === 'featured') {
|
||||||
|
inputData = orderBy(inputData, ['totalSold'], ['desc']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortBy === 'newest') {
|
||||||
|
inputData = orderBy(inputData, ['createdAt'], ['desc']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortBy === 'priceDesc') {
|
||||||
|
inputData = orderBy(inputData, ['price'], ['desc']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortBy === 'priceAsc') {
|
||||||
|
inputData = orderBy(inputData, ['price'], ['asc']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// filters
|
||||||
|
if (gender.length) {
|
||||||
|
inputData = inputData.filter((product) => product.gender.some((i) => gender.includes(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category !== 'all') {
|
||||||
|
inputData = inputData.filter((product) => product.category === category);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colors.length) {
|
||||||
|
inputData = inputData.filter((product) =>
|
||||||
|
product.colors.some((color) => colors.includes(color))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (min !== 0 || max !== 200) {
|
||||||
|
inputData = inputData.filter((product) => product.price >= min && product.price <= max);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rating) {
|
||||||
|
inputData = inputData.filter((product) => {
|
||||||
|
const convertRating = (value: string) => {
|
||||||
|
if (value === 'up4Star') return 4;
|
||||||
|
if (value === 'up3Star') return 3;
|
||||||
|
if (value === 'up2Star') return 2;
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
return product.totalRatings > convertRating(rating);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputData;
|
||||||
|
}
|
@@ -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 PartyOrderDetailsCustomer({ 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 PartyOrderDetailsDelivery({ 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 PartyOrderDetailsHistory({ 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 PartyOrderDetailsItems({
|
||||||
|
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 PartyOrderDetailsPayment({ 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 PartyOrderDetailsShipping({ 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 PartyOrderDetailsToolbar({
|
||||||
|
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 { IPartyOrderTableFilters } from 'src/types/party-order';
|
||||||
|
import { fDateRangeShortLabel } from 'src/utils/format-time';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = FiltersResultProps & {
|
||||||
|
onResetPage: () => void;
|
||||||
|
filters: UseSetStateReturn<IPartyOrderTableFilters>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 { IPartyOrderItem } from 'src/types/party-order';
|
||||||
|
import { fCurrency } from 'src/utils/format-number';
|
||||||
|
import { fDate, fTime } from 'src/utils/format-time';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
row: IPartyOrderItem;
|
||||||
|
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 { IPartyOrderTableFilters } from 'src/types/party-order';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
dateError: boolean;
|
||||||
|
onResetPage: () => void;
|
||||||
|
filters: UseSetStateReturn<IPartyOrderTableFilters>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 { IPartyOrderItem } from 'src/types/party-order';
|
||||||
|
import { PartyOrderDetailsCustomer } from '../party-order-details-customer';
|
||||||
|
import { PartyOrderDetailsDelivery } from '../party-order-details-delivery';
|
||||||
|
import { PartyOrderDetailsHistory } from '../party-order-details-history';
|
||||||
|
import { PartyOrderDetailsItems } from '../party-order-details-items';
|
||||||
|
import { PartyOrderDetailsPayment } from '../party-order-details-payment';
|
||||||
|
import { PartyOrderDetailsShipping } from '../party-order-details-shipping';
|
||||||
|
import { PartyOrderDetailsToolbar } from '../party-order-details-toolbar';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
partyOrder: IPartyOrderItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PartyOrderDetailsView({ partyOrder }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [status, setStatus] = useState(partyOrder.status);
|
||||||
|
|
||||||
|
const handleChangeStatus = useCallback(
|
||||||
|
async (newValue: string) => {
|
||||||
|
setStatus(newValue);
|
||||||
|
// change order status
|
||||||
|
try {
|
||||||
|
if (partyOrder?.id) {
|
||||||
|
await changeStatus(partyOrder.id, newValue);
|
||||||
|
|
||||||
|
toast.success('order status updated');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.warning('error during update order status');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[partyOrder.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardContent>
|
||||||
|
<PartyOrderDetailsToolbar
|
||||||
|
status={status}
|
||||||
|
createdAt={partyOrder?.createdAt}
|
||||||
|
orderNumber={partyOrder?.orderNumber}
|
||||||
|
backHref={paths.dashboard.partyOrder.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' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PartyOrderDetailsItems
|
||||||
|
items={partyOrder?.items}
|
||||||
|
taxes={partyOrder?.taxes}
|
||||||
|
shipping={partyOrder?.shipping}
|
||||||
|
discount={partyOrder?.discount}
|
||||||
|
subtotal={partyOrder?.subtotal}
|
||||||
|
totalAmount={partyOrder?.totalAmount}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PartyOrderDetailsHistory history={partyOrder?.history} />
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, md: 4 }}>
|
||||||
|
<Card>
|
||||||
|
<PartyOrderDetailsCustomer customer={partyOrder?.customer} />
|
||||||
|
|
||||||
|
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||||
|
<PartyOrderDetailsDelivery delivery={partyOrder?.delivery} />
|
||||||
|
|
||||||
|
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||||
|
<PartyOrderDetailsShipping shippingAddress={partyOrder?.shippingAddress} />
|
||||||
|
|
||||||
|
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||||
|
<PartyOrderDetailsPayment payment={partyOrder?.payment} />
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DashboardContent>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,359 @@
|
|||||||
|
// 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 { IPartyOrderItem, IPartyOrderTableFilters } 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 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 { partyOrders, partyOrdersLoading } = useGetPartyOrders();
|
||||||
|
|
||||||
|
const table = useTable({ defaultOrderBy: 'orderNumber' });
|
||||||
|
|
||||||
|
const confirmDialog = useBoolean();
|
||||||
|
|
||||||
|
const [tableData, setTableData] = useState<IPartyOrderItem[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableData(partyOrders);
|
||||||
|
}, [partyOrders]);
|
||||||
|
|
||||||
|
const filters = useSetState<IPartyOrderTableFilters>({
|
||||||
|
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!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error('Delete failed!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// toast.success('Delete success!');
|
||||||
|
// setTableData(deleteRow);
|
||||||
|
// table.onUpdatePageDeleteRow(dataInPage.length);
|
||||||
|
},
|
||||||
|
[table, tableData]
|
||||||
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: remove below loading screen as mutate is not used
|
||||||
|
if (!partyOrders) return <>loading</>;
|
||||||
|
if (partyOrdersLoading) 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.partyOrder.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: IPartyOrderItem[];
|
||||||
|
filters: IPartyOrderTableFilters;
|
||||||
|
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;
|
||||||
|
}
|
60
03_source/frontend/src/types/party-event.ts
Normal file
60
03_source/frontend/src/types/party-event.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { IDateValue } from './common';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type IProductFilters = {
|
||||||
|
rating: string;
|
||||||
|
gender: string[];
|
||||||
|
category: string;
|
||||||
|
colors: string[];
|
||||||
|
priceRange: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IProductTableFilters = {
|
||||||
|
stock: string[];
|
||||||
|
publish: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IProductReview = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
rating: number;
|
||||||
|
comment: string;
|
||||||
|
helpful: number;
|
||||||
|
avatarUrl: string;
|
||||||
|
postedAt: IDateValue;
|
||||||
|
isPurchased: boolean;
|
||||||
|
attachments?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IPartyEventItem = {
|
||||||
|
id: string;
|
||||||
|
createdAt: IDateValue;
|
||||||
|
//
|
||||||
|
available: number;
|
||||||
|
category: string;
|
||||||
|
code: string;
|
||||||
|
colors: string[];
|
||||||
|
coverUrl: string;
|
||||||
|
description: string;
|
||||||
|
gender: string[];
|
||||||
|
images: string[];
|
||||||
|
inventoryType: string;
|
||||||
|
name: string;
|
||||||
|
newLabel: { content: string; enabled: boolean };
|
||||||
|
price: number;
|
||||||
|
priceSale: number | null;
|
||||||
|
publish: string;
|
||||||
|
quantity: number;
|
||||||
|
ratings: { name: string; starCount: number; reviewCount: number }[];
|
||||||
|
reviews: IProductReview[];
|
||||||
|
saleLabel: { content: string; enabled: boolean };
|
||||||
|
sizes: string[];
|
||||||
|
sku: string;
|
||||||
|
subDescription: string;
|
||||||
|
tags: string[];
|
||||||
|
taxes: number;
|
||||||
|
totalRatings: number;
|
||||||
|
totalReviews: number;
|
||||||
|
totalSold: number;
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user