Auto commit: 2026-06-04T18:38:09.342Z
This commit is contained in:
parent
5226283cc7
commit
6c0b5d5c8b
@ -154,6 +154,12 @@ router.post('/bulk-import', wrapAsync(async (req, res) => {
|
|||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
router.put('/:id/status', wrapAsync(async (req, res) => {
|
||||||
|
const payload = await OrdersService.changeStatus(req.params.id, req.body.data || {}, req.currentUser);
|
||||||
|
res.status(200).send(payload);
|
||||||
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/orders/{id}:
|
* /api/orders/{id}:
|
||||||
|
|||||||
@ -3,11 +3,60 @@ const OrdersDBApi = require('../db/api/orders');
|
|||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
const axios = require('axios');
|
|
||||||
const config = require('../config');
|
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
|
||||||
|
|
||||||
|
const ORDER_STATUS_TRANSITIONS = {
|
||||||
|
pending: ['assigned', 'cancelled'],
|
||||||
|
assigned: ['picked_up'],
|
||||||
|
picked_up: ['out_for_delivery'],
|
||||||
|
out_for_delivery: ['delivered', 'failed_attempt'],
|
||||||
|
failed_attempt: ['rescheduled'],
|
||||||
|
rescheduled: ['out_for_delivery'],
|
||||||
|
delivered: [],
|
||||||
|
cancelled: [],
|
||||||
|
returned: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_ROLE_RULES = {
|
||||||
|
assigned: ['Administrator', 'System Owner', 'Operations Manager', 'Dispatch Supervisor'],
|
||||||
|
picked_up: ['Administrator', 'System Owner', 'Operations Manager', 'Dispatch Supervisor', 'Driver User'],
|
||||||
|
out_for_delivery: ['Administrator', 'System Owner', 'Operations Manager', 'Dispatch Supervisor', 'Driver User'],
|
||||||
|
delivered: ['Administrator', 'System Owner', 'Operations Manager', 'Dispatch Supervisor', 'Driver User'],
|
||||||
|
failed_attempt: ['Administrator', 'System Owner', 'Operations Manager', 'Dispatch Supervisor', 'Driver User', 'Customer Support Agent'],
|
||||||
|
rescheduled: ['Administrator', 'System Owner', 'Operations Manager', 'Dispatch Supervisor', 'Customer Support Agent'],
|
||||||
|
cancelled: ['Administrator', 'System Owner', 'Operations Manager', 'Dispatch Supervisor', 'Customer Support Agent'],
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRoleName(currentUser) {
|
||||||
|
return currentUser?.app_role?.name || currentUser?.role || 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateStatusTransition(order, nextStatus, currentUser) {
|
||||||
|
if (!nextStatus || !ORDER_STATUS_TRANSITIONS[nextStatus]) {
|
||||||
|
throw new ValidationError('orders.invalidStatus', `Invalid order status '${nextStatus}'.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStatus = order.current_status || 'pending';
|
||||||
|
const allowedNext = ORDER_STATUS_TRANSITIONS[currentStatus] || [];
|
||||||
|
if (!allowedNext.includes(nextStatus)) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'orders.invalidTransition',
|
||||||
|
`Cannot transition order from '${currentStatus}' to '${nextStatus}'.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleName = getRoleName(currentUser);
|
||||||
|
const allowedRoles = STATUS_ROLE_RULES[nextStatus] || [];
|
||||||
|
if (!allowedRoles.includes(roleName)) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'orders.statusRoleForbidden',
|
||||||
|
`Role '${roleName}' cannot change an order to '${nextStatus}'.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -28,9 +77,9 @@ module.exports = class OrdersService {
|
|||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
static async bulkImport(req, res) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -65,10 +114,71 @@ module.exports = class OrdersService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static async changeStatus(id, data, currentUser) {
|
||||||
|
const transaction = await db.sequelize.transaction();
|
||||||
|
try {
|
||||||
|
const order = await db.orders.findByPk(id, { transaction });
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
throw new ValidationError('ordersNotFound');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStatus = data.current_status;
|
||||||
|
validateStatusTransition(order, nextStatus, currentUser);
|
||||||
|
|
||||||
|
if (nextStatus === 'assigned' && !data.assigned_driver && !order.assigned_driverId) {
|
||||||
|
throw new ValidationError('orders.driverRequired', 'Assigned status requires a driver.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousStatus = order.current_status || 'pending';
|
||||||
|
const updatePayload = {
|
||||||
|
previous_status: previousStatus,
|
||||||
|
current_status: nextStatus,
|
||||||
|
last_status_at: new Date(),
|
||||||
|
updatedById: currentUser?.id || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.assigned_driver !== undefined) {
|
||||||
|
updatePayload.assigned_driverId = data.assigned_driver || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.delivery_lat !== undefined) {
|
||||||
|
updatePayload.delivery_lat = data.delivery_lat || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.delivery_lng !== undefined) {
|
||||||
|
updatePayload.delivery_lng = data.delivery_lng || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await order.update(updatePayload, { transaction });
|
||||||
|
|
||||||
|
await db.order_status_logs.create(
|
||||||
|
{
|
||||||
|
orderId: id,
|
||||||
|
from_status: previousStatus,
|
||||||
|
to_status: nextStatus,
|
||||||
|
comment: data.comment || null,
|
||||||
|
changed_at: new Date(),
|
||||||
|
changed_by_userId: currentUser?.id || null,
|
||||||
|
createdById: currentUser?.id || null,
|
||||||
|
updatedById: currentUser?.id || null,
|
||||||
|
},
|
||||||
|
{ transaction },
|
||||||
|
);
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
return OrdersDBApi.findBy({ id });
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async update(data, id, currentUser) {
|
static async update(data, id, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
let orders = await OrdersDBApi.findBy(
|
const orders = await OrdersDBApi.findBy(
|
||||||
{id},
|
{id},
|
||||||
{transaction},
|
{transaction},
|
||||||
);
|
);
|
||||||
@ -95,7 +205,7 @@ module.exports = class OrdersService {
|
|||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
static async deleteByIds(ids, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export default function AsideMenu({
|
|||||||
<>
|
<>
|
||||||
<AsideMenuLayer
|
<AsideMenuLayer
|
||||||
menu={props.menu}
|
menu={props.menu}
|
||||||
className={`${isAsideMobileExpanded ? 'left-0' : '-left-60 lg:left-0'} ${
|
className={`${isAsideMobileExpanded ? 'right-0' : '-right-60 lg:right-0'} ${
|
||||||
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
|
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
|
||||||
}`}
|
}`}
|
||||||
onAsideLgCloseClick={props.onAsideLgClose}
|
onAsideLgCloseClick={props.onAsideLgClose}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
|
|||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
id='asideMenu'
|
id='asideMenu'
|
||||||
className={`${className} zzz lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
|
className={`${className} zzz lg:py-2 lg:pr-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
|
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
|
||||||
@ -37,7 +37,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
|
|||||||
<div
|
<div
|
||||||
className={`flex flex-row h-14 items-center justify-between ${asideBrandStyle}`}
|
className={`flex flex-row h-14 items-center justify-between ${asideBrandStyle}`}
|
||||||
>
|
>
|
||||||
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
|
<div className="text-center flex-1 lg:text-right lg:pr-6 xl:text-center xl:pr-0">
|
||||||
|
|
||||||
<b className="font-black">Parcel Ops RTL</b>
|
<b className="font-black">Parcel Ops RTL</b>
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export default function FooterBar({ children }: Props) {
|
|||||||
return (
|
return (
|
||||||
<footer className={`py-2 px-6 ${containerMaxW}`}>
|
<footer className={`py-2 px-6 ${containerMaxW}`}>
|
||||||
<div className="block md:flex items-center justify-between">
|
<div className="block md:flex items-center justify-between">
|
||||||
<div className="text-center md:text-left mb-6 md:mb-0">
|
<div className="text-center md:text-right mb-6 md:mb-0">
|
||||||
<b>
|
<b>
|
||||||
©{year},{` `}
|
©{year},{` `}
|
||||||
<a href="https://flatlogic.com/" rel="noreferrer" target="_blank">
|
<a href="https://flatlogic.com/" rel="noreferrer" target="_blank">
|
||||||
|
|||||||
@ -47,7 +47,7 @@ export default function NavBar({ menu, className = '', children }: Props) {
|
|||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
isMenuNavBarActive ? 'block' : 'hidden'
|
isMenuNavBarActive ? 'block' : 'hidden'
|
||||||
} flex items-center max-h-screen-menu overflow-y-auto lg:overflow-visible absolute w-screen top-14 left-0 ${bgColor} shadow-lg lg:w-auto lg:flex lg:static lg:shadow-none dark:bg-dark-800`}
|
} flex items-center max-h-screen-menu overflow-y-auto lg:overflow-visible absolute w-screen top-14 right-0 ${bgColor} shadow-lg lg:w-auto lg:flex lg:static lg:shadow-none dark:bg-dark-800`}
|
||||||
>
|
>
|
||||||
<NavBarMenuList menu={menu} />
|
<NavBarMenuList menu={menu} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React, {useEffect, useRef} from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
|
||||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||||
import BaseDivider from './BaseDivider'
|
import BaseDivider from './BaseDivider'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
@ -66,9 +65,12 @@ export default function NavBarItem({ item }: Props) {
|
|||||||
|
|
||||||
const getItemId = (label) => {
|
const getItemId = (label) => {
|
||||||
switch (label) {
|
switch (label) {
|
||||||
|
case 'وضع الإضاءة':
|
||||||
case 'Light/Dark':
|
case 'Light/Dark':
|
||||||
return 'themeToggle';
|
return 'themeToggle';
|
||||||
|
case 'تسجيل الخروج':
|
||||||
case 'Log out':
|
case 'Log out':
|
||||||
|
case 'Log Out':
|
||||||
return 'logout';
|
return 'logout';
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -94,7 +96,7 @@ export default function NavBarItem({ item }: Props) {
|
|||||||
>
|
>
|
||||||
{itemLabel}
|
{itemLabel}
|
||||||
</span>
|
</span>
|
||||||
{item.isCurrentUser && <UserAvatarCurrentUser className="w-6 h-6 mr-3 inline-flex" />}
|
{item.isCurrentUser && <UserAvatarCurrentUser className="w-6 h-6 ml-3 inline-flex" />}
|
||||||
{item.menu && (
|
{item.menu && (
|
||||||
<BaseIcon
|
<BaseIcon
|
||||||
path={isDropdownActive ? mdiChevronUp : mdiChevronDown}
|
path={isDropdownActive ? mdiChevronUp : mdiChevronDown}
|
||||||
@ -106,7 +108,7 @@ export default function NavBarItem({ item }: Props) {
|
|||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
!isDropdownActive ? 'lg:hidden' : ''
|
!isDropdownActive ? 'lg:hidden' : ''
|
||||||
} text-sm border-b border-gray-100 lg:border lg:bg-white lg:absolute lg:top-full lg:left-0 lg:min-w-full lg:z-20 lg:rounded-lg lg:shadow-lg lg:dark:bg-dark-900 dark:border-dark-700`}
|
} text-sm border-b border-gray-100 lg:border lg:bg-white lg:absolute lg:top-full lg:right-0 lg:min-w-full lg:z-20 lg:rounded-lg lg:shadow-lg lg:dark:bg-dark-900 dark:border-dark-700`}
|
||||||
>
|
>
|
||||||
<ClickOutside onClickOutside={() => setIsDropdownActive(false)} excludedElements={[excludedRef]}>
|
<ClickOutside onClickOutside={() => setIsDropdownActive(false)} excludedElements={[excludedRef]}>
|
||||||
<NavBarMenuList menu={item.menu} />
|
<NavBarMenuList menu={item.menu} />
|
||||||
|
|||||||
@ -49,16 +49,16 @@ export default function PasswordSetOrReset() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
{isInvitation && <title>{getPageTitle('Set Password')}</title>}
|
{isInvitation && <title>{getPageTitle('تعيين كلمة المرور')}</title>}
|
||||||
{!isInvitation && <title>{getPageTitle('Reset Password')}</title>}
|
{!isInvitation && <title>{getPageTitle('إعادة تعيين كلمة المرور')}</title>}
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<SectionFullScreen bg='violet'>
|
||||||
<div className='w-full flex flex-col items-center justify-center'>
|
<div className='w-full flex flex-col items-center justify-center'>
|
||||||
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
|
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
|
||||||
{isInvitation && <p className='text-xl mb-2'>Set Password</p>}
|
{isInvitation && <p className='text-xl mb-2'>تعيين كلمة المرور</p>}
|
||||||
{!isInvitation && <p className='text-xl mb-2'>Reset Password</p>}
|
{!isInvitation && <p className='text-xl mb-2'>إعادة تعيين كلمة المرور</p>}
|
||||||
<p className='text-base mb-4'>Enter your new password</p>
|
<p className='text-base mb-4'>أدخل كلمة المرور الجديدة</p>
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
@ -74,7 +74,7 @@ export default function PasswordSetOrReset() {
|
|||||||
<Field
|
<Field
|
||||||
type='password'
|
type='password'
|
||||||
name='password'
|
name='password'
|
||||||
placeholder='Password'
|
placeholder='كلمة المرور'
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField
|
<FormField
|
||||||
@ -82,7 +82,7 @@ export default function PasswordSetOrReset() {
|
|||||||
<Field
|
<Field
|
||||||
type='password'
|
type='password'
|
||||||
name='confirm'
|
name='confirm'
|
||||||
placeholder='Confirm Password'
|
placeholder='تأكيد كلمة المرور'
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
@ -93,10 +93,10 @@ export default function PasswordSetOrReset() {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
label={
|
label={
|
||||||
loading
|
loading
|
||||||
? 'Loading...'
|
? 'جارٍ التحميل...'
|
||||||
: isInvitation
|
: isInvitation
|
||||||
? 'Set Password'
|
? 'تعيين كلمة المرور'
|
||||||
: 'Reset Password'
|
: 'إعادة تعيين كلمة المرور'
|
||||||
}
|
}
|
||||||
color='info'
|
color='info'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -11,9 +11,9 @@ const Search = () => {
|
|||||||
const validateSearch = (value) => {
|
const validateSearch = (value) => {
|
||||||
let error;
|
let error;
|
||||||
if (!value) {
|
if (!value) {
|
||||||
error = 'Required';
|
error = 'مطلوب';
|
||||||
} else if (value.length < 2) {
|
} else if (value.length < 2) {
|
||||||
error = 'Minimum length: 2 characters';
|
error = 'الحد الأدنى حرفان';
|
||||||
}
|
}
|
||||||
return error;
|
return error;
|
||||||
};
|
};
|
||||||
@ -36,11 +36,11 @@ const Search = () => {
|
|||||||
id='search'
|
id='search'
|
||||||
name='search'
|
name='search'
|
||||||
validate={validateSearch}
|
validate={validateSearch}
|
||||||
placeholder='Search'
|
placeholder='بحث'
|
||||||
className={` ${corners} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-2 relative ml-2 w-full dark:placeholder-dark-600 ${focusRing} shadow-none`}
|
className={` ${corners} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-2 relative mr-2 w-full dark:placeholder-dark-600 ${focusRing} shadow-none`}
|
||||||
/>
|
/>
|
||||||
{errors.search && touched.search && values.search.length < 2 ? (
|
{errors.search && touched.search && values.search.length < 2 ? (
|
||||||
<div className='text-red-500 text-sm ml-2 absolute'>{errors.search}</div>
|
<div className='text-red-500 text-sm mr-2 absolute'>{errors.search}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -12,6 +12,17 @@
|
|||||||
@import "_theme.css";
|
@import "_theme.css";
|
||||||
@import '_rich-text.css';
|
@import '_rich-text.css';
|
||||||
|
|
||||||
|
html[dir='rtl'] body {
|
||||||
|
direction: rtl;
|
||||||
|
font-family: Cairo, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir='rtl'] input,
|
||||||
|
html[dir='rtl'] textarea,
|
||||||
|
html[dir='rtl'] select {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.introjs-tooltip {
|
.introjs-tooltip {
|
||||||
@apply min-w-[400px] max-w-[480px] p-2 !important;
|
@apply min-w-[400px] max-w-[480px] p-2 !important;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import React, { ReactNode, useEffect } from 'react'
|
import React, { ReactNode, useEffect, useState } from 'react'
|
||||||
import { useState } from 'react'
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||||
import menuAside from '../menuAside'
|
import menuAside from '../menuAside'
|
||||||
@ -86,24 +85,24 @@ export default function LayoutAuthenticated({
|
|||||||
}, [router.events, dispatch])
|
}, [router.events, dispatch])
|
||||||
|
|
||||||
|
|
||||||
const layoutAsidePadding = 'xl:pl-60'
|
const layoutAsidePadding = 'xl:pr-60'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
||||||
<div
|
<div
|
||||||
className={`${layoutAsidePadding} ${
|
className={`${layoutAsidePadding} ${
|
||||||
isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''
|
isAsideMobileExpanded ? 'mr-60 lg:mr-0' : ''
|
||||||
} pt-14 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
|
} pt-14 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
|
||||||
>
|
>
|
||||||
<NavBar
|
<NavBar
|
||||||
menu={menuNavBar}
|
menu={menuNavBar}
|
||||||
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''}`}
|
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'mr-60 lg:mr-0' : ''}`}
|
||||||
>
|
>
|
||||||
<NavBarItemPlain
|
<NavBarItemPlain
|
||||||
display="flex lg:hidden"
|
display="flex lg:hidden"
|
||||||
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
|
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
|
||||||
>
|
>
|
||||||
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" />
|
<BaseIcon path={isAsideMobileExpanded ? mdiForwardburger : mdiBackburger} size="24" />
|
||||||
</NavBarItemPlain>
|
</NavBarItemPlain>
|
||||||
<NavBarItemPlain
|
<NavBarItemPlain
|
||||||
display="hidden lg:flex xl:hidden"
|
display="hidden lg:flex xl:hidden"
|
||||||
@ -122,7 +121,7 @@ export default function LayoutAuthenticated({
|
|||||||
onAsideLgClose={() => setIsAsideLgActive(false)}
|
onAsideLgClose={() => setIsAsideLgActive(false)}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
<FooterBar>Hand-crafted & Made with ❤️</FooterBar>
|
<FooterBar>صُنع بعناية و ❤️</FooterBar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,15 +2,22 @@ import * as icon from '@mdi/js';
|
|||||||
import { MenuAsideItem } from './interfaces'
|
import { MenuAsideItem } from './interfaces'
|
||||||
|
|
||||||
const menuAside: MenuAsideItem[] = [
|
const menuAside: MenuAsideItem[] = [
|
||||||
|
{
|
||||||
|
href: '/operations/orders',
|
||||||
|
icon: icon.mdiPackageVariantClosed,
|
||||||
|
label: 'عمليات التوصيل',
|
||||||
|
permissions: 'READ_ORDERS'
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
href: '/dashboard',
|
href: '/dashboard',
|
||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiViewDashboardOutline,
|
||||||
label: 'Dashboard',
|
label: 'لوحة التحكم',
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
href: '/users/users-list',
|
href: '/users/users-list',
|
||||||
label: 'Users',
|
label: 'المستخدمون',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: icon.mdiAccountGroup ?? icon.mdiTable,
|
icon: icon.mdiAccountGroup ?? icon.mdiTable,
|
||||||
@ -18,7 +25,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/roles/roles-list',
|
href: '/roles/roles-list',
|
||||||
label: 'Roles',
|
label: 'الأدوار',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
|
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
|
||||||
@ -26,7 +33,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/permissions/permissions-list',
|
href: '/permissions/permissions-list',
|
||||||
label: 'Permissions',
|
label: 'الصلاحيات',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
||||||
@ -34,7 +41,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/merchants/merchants-list',
|
href: '/merchants/merchants-list',
|
||||||
label: 'Merchants',
|
label: 'التجار',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiStore' in icon ? icon['mdiStore' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiStore' in icon ? icon['mdiStore' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -42,7 +49,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/drivers/drivers-list',
|
href: '/drivers/drivers-list',
|
||||||
label: 'Drivers',
|
label: 'السائقون',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiTruckFast' in icon ? icon['mdiTruckFast' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiTruckFast' in icon ? icon['mdiTruckFast' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -50,7 +57,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/countries/countries-list',
|
href: '/countries/countries-list',
|
||||||
label: 'Countries',
|
label: 'الدول',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiEarth' in icon ? icon['mdiEarth' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiEarth' in icon ? icon['mdiEarth' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -58,7 +65,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/cities/cities-list',
|
href: '/cities/cities-list',
|
||||||
label: 'Cities',
|
label: 'المدن',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiCity' in icon ? icon['mdiCity' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiCity' in icon ? icon['mdiCity' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -66,7 +73,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/regions/regions-list',
|
href: '/regions/regions-list',
|
||||||
label: 'Regions',
|
label: 'المناطق',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiMapMarker' in icon ? icon['mdiMapMarker' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiMapMarker' in icon ? icon['mdiMapMarker' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -74,7 +81,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/merchant_pricing_rules/merchant_pricing_rules-list',
|
href: '/merchant_pricing_rules/merchant_pricing_rules-list',
|
||||||
label: 'Merchant pricing rules',
|
label: 'قواعد تسعير التجار',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiCashMultiple' in icon ? icon['mdiCashMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiCashMultiple' in icon ? icon['mdiCashMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -82,7 +89,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/orders/orders-list',
|
href: '/orders/orders-list',
|
||||||
label: 'Orders',
|
label: 'الطلبات',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiPackageVariantClosed' in icon ? icon['mdiPackageVariantClosed' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiPackageVariantClosed' in icon ? icon['mdiPackageVariantClosed' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -90,7 +97,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/order_status_logs/order_status_logs-list',
|
href: '/order_status_logs/order_status_logs-list',
|
||||||
label: 'Order status logs',
|
label: 'سجل حالات الطلب',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiTimelineText' in icon ? icon['mdiTimelineText' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiTimelineText' in icon ? icon['mdiTimelineText' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -98,7 +105,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/driver_locations/driver_locations-list',
|
href: '/driver_locations/driver_locations-list',
|
||||||
label: 'Driver locations',
|
label: 'مواقع السائقين',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiCrosshairsGps' in icon ? icon['mdiCrosshairsGps' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiCrosshairsGps' in icon ? icon['mdiCrosshairsGps' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -106,7 +113,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/driver_payment_slips/driver_payment_slips-list',
|
href: '/driver_payment_slips/driver_payment_slips-list',
|
||||||
label: 'Driver payment slips',
|
label: 'كشوفات دفع السائقين',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiReceiptText' in icon ? icon['mdiReceiptText' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiReceiptText' in icon ? icon['mdiReceiptText' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -114,7 +121,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/merchant_payment_slips/merchant_payment_slips-list',
|
href: '/merchant_payment_slips/merchant_payment_slips-list',
|
||||||
label: 'Merchant payment slips',
|
label: 'كشوفات دفع التجار',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiFileDocumentCheck' in icon ? icon['mdiFileDocumentCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiFileDocumentCheck' in icon ? icon['mdiFileDocumentCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -122,7 +129,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/driver_return_slips/driver_return_slips-list',
|
href: '/driver_return_slips/driver_return_slips-list',
|
||||||
label: 'Driver return slips',
|
label: 'مرتجعات السائقين',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiPackageReturn' in icon ? icon['mdiPackageReturn' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiPackageReturn' in icon ? icon['mdiPackageReturn' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -130,7 +137,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/merchant_return_slips/merchant_return_slips-list',
|
href: '/merchant_return_slips/merchant_return_slips-list',
|
||||||
label: 'Merchant return slips',
|
label: 'مرتجعات التجار',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiClipboardArrowLeft' in icon ? icon['mdiClipboardArrowLeft' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiClipboardArrowLeft' in icon ? icon['mdiClipboardArrowLeft' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -138,7 +145,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/notifications/notifications-list',
|
href: '/notifications/notifications-list',
|
||||||
label: 'Notifications',
|
label: 'الإشعارات',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiBell' in icon ? icon['mdiBell' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiBell' in icon ? icon['mdiBell' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -146,7 +153,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/system_settings/system_settings-list',
|
href: '/system_settings/system_settings-list',
|
||||||
label: 'System settings',
|
label: 'إعدادات النظام',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiCog' in icon ? icon['mdiCog' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiCog' in icon ? icon['mdiCog' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -154,7 +161,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/profile',
|
href: '/profile',
|
||||||
label: 'Profile',
|
label: 'الملف الشخصي',
|
||||||
icon: icon.mdiAccountCircle,
|
icon: icon.mdiAccountCircle,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -162,7 +169,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
{
|
{
|
||||||
href: '/api-docs',
|
href: '/api-docs',
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
label: 'Swagger API',
|
label: 'توثيق Swagger API',
|
||||||
icon: icon.mdiFileCode,
|
icon: icon.mdiFileCode,
|
||||||
permissions: 'READ_API_DOCS'
|
permissions: 'READ_API_DOCS'
|
||||||
},
|
},
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const menuNavBar: MenuNavBarItem[] = [
|
|||||||
menu: [
|
menu: [
|
||||||
{
|
{
|
||||||
icon: mdiAccount,
|
icon: mdiAccount,
|
||||||
label: 'My Profile',
|
label: 'الملف الشخصي',
|
||||||
href: '/profile',
|
href: '/profile',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -27,20 +27,20 @@ const menuNavBar: MenuNavBarItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: mdiLogout,
|
icon: mdiLogout,
|
||||||
label: 'Log Out',
|
label: 'تسجيل الخروج',
|
||||||
isLogout: true,
|
isLogout: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: mdiThemeLightDark,
|
icon: mdiThemeLightDark,
|
||||||
label: 'Light/Dark',
|
label: 'وضع الإضاءة',
|
||||||
isDesktopNoLabel: true,
|
isDesktopNoLabel: true,
|
||||||
isToggleLightDark: true,
|
isToggleLightDark: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: mdiLogout,
|
icon: mdiLogout,
|
||||||
label: 'Log out',
|
label: 'تسجيل الخروج',
|
||||||
isDesktopNoLabel: true,
|
isDesktopNoLabel: true,
|
||||||
isLogout: true,
|
isLogout: true,
|
||||||
},
|
},
|
||||||
|
|||||||
13
frontend/src/pages/_document.tsx
Normal file
13
frontend/src/pages/_document.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Head, Html, Main, NextScript } from 'next/document'
|
||||||
|
|
||||||
|
export default function Document() {
|
||||||
|
return (
|
||||||
|
<Html lang="ar" dir="rtl">
|
||||||
|
<Head />
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -24,21 +24,21 @@ export default function Forgot() {
|
|||||||
try {
|
try {
|
||||||
const { data: response } = await axios.post('/auth/send-password-reset-email', value);
|
const { data: response } = await axios.post('/auth/send-password-reset-email', value);
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
notify('success', 'Please check your email for verification link');
|
notify('success', 'تحقق من بريدك الإلكتروني لرابط إعادة التعيين');
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await router.push('/login')
|
await router.push('/login')
|
||||||
}, 3000)
|
}, 3000)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
console.log('error: ', error)
|
console.log('error: ', error)
|
||||||
notify('error', 'Something was wrong. Try again')
|
notify('error', 'حدث خطأ. حاول مرة أخرى')
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Login')}</title>
|
<title>{getPageTitle('نسيت كلمة المرور')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<SectionFullScreen bg='violet'>
|
||||||
@ -50,7 +50,7 @@ export default function Forgot() {
|
|||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<FormField label='Email' help='Please enter your email'>
|
<FormField label='البريد الإلكتروني' help='الرجاء إدخال البريد الإلكتروني'>
|
||||||
<Field name='email' />
|
<Field name='email' />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
@ -59,12 +59,12 @@ export default function Forgot() {
|
|||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
type='submit'
|
type='submit'
|
||||||
label={loading ? 'Loading...' : 'Submit' }
|
label={loading ? 'جارٍ التحميل...' : 'إرسال' }
|
||||||
color='info'
|
color='info'
|
||||||
/>
|
/>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
href={'/login'}
|
href={'/login'}
|
||||||
label={'Login'}
|
label={'تسجيل الدخول'}
|
||||||
color='info'
|
color='info'
|
||||||
/>
|
/>
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
|
|||||||
@ -109,8 +109,8 @@ export default function Login() {
|
|||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
}}>
|
}}>
|
||||||
<div className="flex justify-center w-full bg-blue-300/20">
|
<div className="flex justify-center w-full bg-blue-300/20">
|
||||||
<a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer">Photo
|
<a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer">صورة
|
||||||
by {image?.photographer} on Pexels</a>
|
بواسطة {image?.photographer} على Pexels</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -126,7 +126,7 @@ export default function Login() {
|
|||||||
muted
|
muted
|
||||||
>
|
>
|
||||||
<source src={video.video_files[0]?.link} type='video/mp4'/>
|
<source src={video.video_files[0]?.link} type='video/mp4'/>
|
||||||
Your browser does not support the video tag.
|
المتصفح الحالي لا يدعم عرض الفيديو.
|
||||||
</video>
|
</video>
|
||||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
||||||
<a
|
<a
|
||||||
@ -135,7 +135,7 @@ export default function Login() {
|
|||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
>
|
>
|
||||||
Video by {video.user.name} on Pexels
|
فيديو بواسطة {video.user.name} على Pexels
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>)
|
</div>)
|
||||||
@ -154,7 +154,7 @@ export default function Login() {
|
|||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
} : {}}>
|
} : {}}>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Login')}</title>
|
<title>{getPageTitle('تسجيل الدخول')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<SectionFullScreen bg='violet'>
|
||||||
@ -170,18 +170,18 @@ export default function Login() {
|
|||||||
<div className='flex flex-row text-gray-500 justify-between'>
|
<div className='flex flex-row text-gray-500 justify-between'>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<p className='mb-2'>Use{' '}
|
<p className='mb-2'>استخدم{' '}
|
||||||
<code className={`cursor-pointer ${textColor} `}
|
<code className={`cursor-pointer ${textColor} `}
|
||||||
data-password="2fa67594"
|
data-password="2fa67594"
|
||||||
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
|
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
|
||||||
<code className={`${textColor}`}>2fa67594</code>{' / '}
|
<code className={`${textColor}`}>2fa67594</code>{' / '}
|
||||||
to login as Admin</p>
|
لتسجيل الدخول كمدير</p>
|
||||||
<p>Use <code
|
<p>استخدم <code
|
||||||
className={`cursor-pointer ${textColor} `}
|
className={`cursor-pointer ${textColor} `}
|
||||||
data-password="61f389762427"
|
data-password="61f389762427"
|
||||||
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
|
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
|
||||||
<code className={`${textColor}`}>61f389762427</code>{' / '}
|
<code className={`${textColor}`}>61f389762427</code>{' / '}
|
||||||
to login as User</p>
|
لتسجيل الدخول كمستخدم</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<BaseIcon
|
<BaseIcon
|
||||||
@ -203,19 +203,19 @@ export default function Login() {
|
|||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<FormField
|
<FormField
|
||||||
label='Login'
|
label='البريد الإلكتروني'
|
||||||
help='Please enter your login'>
|
help='الرجاء إدخال البريد الإلكتروني'>
|
||||||
<Field name='email' />
|
<Field name='email' />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<FormField
|
<FormField
|
||||||
label='Password'
|
label='كلمة المرور'
|
||||||
help='Please enter your password'>
|
help='الرجاء إدخال كلمة المرور'>
|
||||||
<Field name='password' type={showPassword ? 'text' : 'password'} />
|
<Field name='password' type={showPassword ? 'text' : 'password'} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<div
|
<div
|
||||||
className='absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer'
|
className='absolute bottom-8 left-0 pl-3 flex items-center cursor-pointer'
|
||||||
onClick={togglePasswordVisibility}
|
onClick={togglePasswordVisibility}
|
||||||
>
|
>
|
||||||
<BaseIcon
|
<BaseIcon
|
||||||
@ -227,12 +227,12 @@ export default function Login() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'flex justify-between'}>
|
<div className={'flex justify-between'}>
|
||||||
<FormCheckRadio type='checkbox' label='Remember'>
|
<FormCheckRadio type='checkbox' label='تذكرني'>
|
||||||
<Field type='checkbox' name='remember' />
|
<Field type='checkbox' name='remember' />
|
||||||
</FormCheckRadio>
|
</FormCheckRadio>
|
||||||
|
|
||||||
<Link className={`${textColor} text-blue-600`} href={'/forgot'}>
|
<Link className={`${textColor} text-blue-600`} href={'/forgot'}>
|
||||||
Forgot password?
|
نسيت كلمة المرور؟
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -242,16 +242,16 @@ export default function Login() {
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
className={'w-full'}
|
className={'w-full'}
|
||||||
type='submit'
|
type='submit'
|
||||||
label={isFetching ? 'Loading...' : 'Login'}
|
label={isFetching ? 'جارٍ التحميل...' : 'تسجيل الدخول'}
|
||||||
color='info'
|
color='info'
|
||||||
disabled={isFetching}
|
disabled={isFetching}
|
||||||
/>
|
/>
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
<br />
|
<br />
|
||||||
<p className={'text-center'}>
|
<p className={'text-center'}>
|
||||||
Don’t have an account yet?{' '}
|
ليس لديك حساب بعد؟{' '}
|
||||||
<Link className={`${textColor}`} href={'/register'}>
|
<Link className={`${textColor}`} href={'/register'}>
|
||||||
New Account
|
إنشاء حساب جديد
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</Form>
|
</Form>
|
||||||
@ -261,9 +261,9 @@ export default function Login() {
|
|||||||
</div>
|
</div>
|
||||||
</SectionFullScreen>
|
</SectionFullScreen>
|
||||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. © All rights reserved</p>
|
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. جميع الحقوق محفوظة</p>
|
||||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
<Link className='py-6 mr-4 text-sm' href='/privacy-policy/'>
|
||||||
Privacy Policy
|
سياسة الخصوصية
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|||||||
589
frontend/src/pages/operations/orders.tsx
Normal file
589
frontend/src/pages/operations/orders.tsx
Normal file
@ -0,0 +1,589 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import Head from 'next/head'
|
||||||
|
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
|
import SectionMain from '../../components/SectionMain'
|
||||||
|
import CardBox from '../../components/CardBox'
|
||||||
|
import BaseButton from '../../components/BaseButton'
|
||||||
|
import { getPageTitle } from '../../config'
|
||||||
|
import { useAppSelector } from '../../stores/hooks'
|
||||||
|
|
||||||
|
type Option = {
|
||||||
|
id: string
|
||||||
|
label?: string
|
||||||
|
name?: string
|
||||||
|
company_name?: string
|
||||||
|
name_ar?: string
|
||||||
|
name_en?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Order = {
|
||||||
|
id: string
|
||||||
|
order_code?: string
|
||||||
|
customer_name?: string
|
||||||
|
customer_phone?: string
|
||||||
|
delivery_address?: string
|
||||||
|
package_description?: string
|
||||||
|
weight_size?: string
|
||||||
|
cod_amount?: string | number
|
||||||
|
current_status?: string
|
||||||
|
previous_status?: string
|
||||||
|
notes?: string
|
||||||
|
delivery_lat?: string | number
|
||||||
|
delivery_lng?: string | number
|
||||||
|
createdAt?: string
|
||||||
|
last_status_at?: string
|
||||||
|
merchant?: Option
|
||||||
|
delivery_city?: Option
|
||||||
|
delivery_region?: Option
|
||||||
|
assigned_driver?: Option
|
||||||
|
order_status_logs_order?: StatusLog[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusLog = {
|
||||||
|
id: string
|
||||||
|
from_status?: string
|
||||||
|
to_status?: string
|
||||||
|
comment?: string
|
||||||
|
changed_at?: string
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormState = {
|
||||||
|
merchant: string
|
||||||
|
customer_name: string
|
||||||
|
customer_phone: string
|
||||||
|
delivery_city: string
|
||||||
|
delivery_region: string
|
||||||
|
delivery_address: string
|
||||||
|
package_description: string
|
||||||
|
weight_size: string
|
||||||
|
cod_amount: string
|
||||||
|
notes: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const teal = '#01696f'
|
||||||
|
|
||||||
|
const statusMeta = {
|
||||||
|
pending: { label: 'قيد الانتظار', color: 'bg-slate-100 text-slate-700 ring-slate-200' },
|
||||||
|
assigned: { label: 'تم التعيين', color: 'bg-blue-100 text-blue-700 ring-blue-200' },
|
||||||
|
picked_up: { label: 'تم الاستلام', color: 'bg-yellow-100 text-yellow-800 ring-yellow-200' },
|
||||||
|
out_for_delivery: { label: 'في الطريق للتسليم', color: 'bg-orange-100 text-orange-700 ring-orange-200' },
|
||||||
|
delivered: { label: 'تم التسليم', color: 'bg-emerald-100 text-emerald-700 ring-emerald-200' },
|
||||||
|
failed_attempt: { label: 'محاولة فاشلة', color: 'bg-red-100 text-red-700 ring-red-200' },
|
||||||
|
rescheduled: { label: 'أعيدت الجدولة', color: 'bg-cyan-100 text-cyan-700 ring-cyan-200' },
|
||||||
|
cancelled: { label: 'ملغي', color: 'bg-zinc-200 text-zinc-800 ring-zinc-300' },
|
||||||
|
returned: { label: 'مرتجع', color: 'bg-purple-100 text-purple-700 ring-purple-200' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStatuses = {
|
||||||
|
pending: ['assigned', 'cancelled'],
|
||||||
|
assigned: ['picked_up'],
|
||||||
|
picked_up: ['out_for_delivery'],
|
||||||
|
out_for_delivery: ['delivered', 'failed_attempt'],
|
||||||
|
failed_attempt: ['rescheduled'],
|
||||||
|
rescheduled: ['out_for_delivery'],
|
||||||
|
delivered: [],
|
||||||
|
cancelled: [],
|
||||||
|
returned: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyForm: FormState = {
|
||||||
|
merchant: '',
|
||||||
|
customer_name: '',
|
||||||
|
customer_phone: '',
|
||||||
|
delivery_city: '',
|
||||||
|
delivery_region: '',
|
||||||
|
delivery_address: '',
|
||||||
|
package_description: '',
|
||||||
|
weight_size: '',
|
||||||
|
cod_amount: '',
|
||||||
|
notes: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelFor = (status?: string) => statusMeta[status || 'pending']?.label || status || '—'
|
||||||
|
|
||||||
|
const badgeClass = (status?: string) =>
|
||||||
|
`inline-flex items-center rounded-full px-3 py-1 text-xs font-bold ring-1 ${
|
||||||
|
statusMeta[status || 'pending']?.color || statusMeta.pending.color
|
||||||
|
}`
|
||||||
|
|
||||||
|
const optionLabel = (option?: Option) =>
|
||||||
|
option?.company_name || option?.name_ar || option?.name || option?.label || option?.name_en || '—'
|
||||||
|
|
||||||
|
const todayIso = () => new Date().toISOString().slice(0, 10)
|
||||||
|
|
||||||
|
const OperationsOrdersPage = () => {
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth)
|
||||||
|
const [orders, setOrders] = useState<Order[]>([])
|
||||||
|
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null)
|
||||||
|
const [merchants, setMerchants] = useState<Option[]>([])
|
||||||
|
const [drivers, setDrivers] = useState<Option[]>([])
|
||||||
|
const [cities, setCities] = useState<Option[]>([])
|
||||||
|
const [regions, setRegions] = useState<Option[]>([])
|
||||||
|
const [form, setForm] = useState<FormState>(emptyForm)
|
||||||
|
const [filters, setFilters] = useState({ query: '', status: '' })
|
||||||
|
const [nextStatus, setNextStatus] = useState('')
|
||||||
|
const [statusDriver, setStatusDriver] = useState('')
|
||||||
|
const [statusComment, setStatusComment] = useState('')
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [notice, setNotice] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
||||||
|
|
||||||
|
const fetchOrders = useCallback(async () => {
|
||||||
|
const params: Record<string, string | number> = { page: 0, limit: 50, field: 'createdAt', sort: 'desc' }
|
||||||
|
if (filters.status) params.current_status = filters.status
|
||||||
|
if (filters.query) {
|
||||||
|
if (/^[0-9+\-\s]+$/.test(filters.query)) {
|
||||||
|
params.customer_phone = filters.query
|
||||||
|
} else {
|
||||||
|
params.order_code = filters.query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await axios.get('orders', { params })
|
||||||
|
setOrders(Array.isArray(data.rows) ? data.rows : [])
|
||||||
|
}, [filters.query, filters.status])
|
||||||
|
|
||||||
|
const fetchLookups = useCallback(async () => {
|
||||||
|
const [merchantRes, driverRes, cityRes, regionRes] = await Promise.all([
|
||||||
|
axios.get('merchants', { params: { page: 0, limit: 100 } }),
|
||||||
|
axios.get('drivers', { params: { page: 0, limit: 100 } }),
|
||||||
|
axios.get('cities', { params: { page: 0, limit: 100 } }),
|
||||||
|
axios.get('regions', { params: { page: 0, limit: 100 } }),
|
||||||
|
])
|
||||||
|
|
||||||
|
setMerchants(merchantRes.data.rows || [])
|
||||||
|
setDrivers(driverRes.data.rows || [])
|
||||||
|
setCities(cityRes.data.rows || [])
|
||||||
|
setRegions(regionRes.data.rows || [])
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
await Promise.all([fetchOrders(), fetchLookups()])
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load parcel operations data', error)
|
||||||
|
setNotice({ type: 'error', text: 'تعذر تحميل بيانات العمليات. تحقق من الصلاحيات أو الاتصال.' })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [fetchLookups, fetchOrders])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentUser?.id) {
|
||||||
|
setLoading(false)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh().catch((error) => {
|
||||||
|
console.error('Initial operations refresh failed', error)
|
||||||
|
})
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
fetchOrders().catch((error) => {
|
||||||
|
console.error('Auto refresh failed', error)
|
||||||
|
})
|
||||||
|
}, 30000)
|
||||||
|
|
||||||
|
return () => window.clearInterval(timer)
|
||||||
|
}, [currentUser?.id, fetchOrders, refresh])
|
||||||
|
|
||||||
|
const selectedAllowedStatuses = useMemo(() => {
|
||||||
|
if (!selectedOrder) return []
|
||||||
|
return nextStatuses[selectedOrder.current_status || 'pending'] || []
|
||||||
|
}, [selectedOrder])
|
||||||
|
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
const today = todayIso()
|
||||||
|
const todaysOrders = orders.filter((order) => order.createdAt?.slice(0, 10) === today)
|
||||||
|
return [
|
||||||
|
{ label: 'طلبات اليوم', value: todaysOrders.length, hint: 'تحديث تلقائي كل 30 ثانية' },
|
||||||
|
{ label: 'قيد الانتظار', value: orders.filter((order) => order.current_status === 'pending').length, hint: 'تحتاج إجراء' },
|
||||||
|
{ label: 'تم التسليم', value: orders.filter((order) => order.current_status === 'delivered').length, hint: 'طلبات مكتملة' },
|
||||||
|
{ label: 'فشل التسليم', value: orders.filter((order) => order.current_status === 'failed_attempt').length, hint: 'تحتاج متابعة' },
|
||||||
|
]
|
||||||
|
}, [orders])
|
||||||
|
|
||||||
|
const revenueToday = useMemo(
|
||||||
|
() =>
|
||||||
|
orders
|
||||||
|
.filter((order) => order.createdAt?.slice(0, 10) === todayIso())
|
||||||
|
.reduce((sum, order) => sum + Number(order.cod_amount || 0), 0),
|
||||||
|
[orders],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleFormChange = (field: keyof FormState, value: string) => {
|
||||||
|
setForm((current) => ({ ...current, [field]: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateOrder = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (!form.customer_name.trim() || !form.customer_phone.trim() || !form.delivery_address.trim()) {
|
||||||
|
setNotice({ type: 'error', text: 'اسم العميل، الهاتف، والعنوان حقول مطلوبة.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSaving(true)
|
||||||
|
setNotice(null)
|
||||||
|
try {
|
||||||
|
const orderCode = `ORD-${Date.now().toString().slice(-8)}`
|
||||||
|
await axios.post('orders', {
|
||||||
|
data: {
|
||||||
|
...form,
|
||||||
|
order_code: orderCode,
|
||||||
|
current_status: 'pending',
|
||||||
|
previous_status: 'pending',
|
||||||
|
placed_at: new Date().toISOString(),
|
||||||
|
last_status_at: new Date().toISOString(),
|
||||||
|
merchant: form.merchant || null,
|
||||||
|
delivery_city: form.delivery_city || null,
|
||||||
|
delivery_region: form.delivery_region || null,
|
||||||
|
cod_amount: form.cod_amount || 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
setForm(emptyForm)
|
||||||
|
await fetchOrders()
|
||||||
|
setNotice({ type: 'success', text: `تم إنشاء الطلب ${orderCode} وأصبح جاهزاً للتعيين.` })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create order failed', error)
|
||||||
|
setNotice({ type: 'error', text: 'تعذر إنشاء الطلب. تأكد من البيانات والصلاحيات.' })
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openOrder = async (order: Order) => {
|
||||||
|
setSelectedOrder(order)
|
||||||
|
setNextStatus('')
|
||||||
|
setStatusDriver(order.assigned_driver?.id || '')
|
||||||
|
setStatusComment('')
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`orders/${order.id}`)
|
||||||
|
setSelectedOrder(data)
|
||||||
|
setStatusDriver(data?.assigned_driver?.id || '')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Load order detail failed', error)
|
||||||
|
setNotice({ type: 'error', text: 'تعذر فتح تفاصيل الطلب.' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStatusChange = async () => {
|
||||||
|
if (!selectedOrder || !nextStatus) {
|
||||||
|
setNotice({ type: 'error', text: 'اختر الحالة التالية أولاً.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextStatus === 'assigned' && !statusDriver) {
|
||||||
|
setNotice({ type: 'error', text: 'يجب اختيار سائق قبل تحويل الطلب إلى تم التعيين.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSaving(true)
|
||||||
|
setNotice(null)
|
||||||
|
try {
|
||||||
|
const { data } = await axios.put(`orders/${selectedOrder.id}/status`, {
|
||||||
|
data: {
|
||||||
|
current_status: nextStatus,
|
||||||
|
assigned_driver: statusDriver || undefined,
|
||||||
|
comment: statusComment,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
setSelectedOrder(data)
|
||||||
|
setNextStatus('')
|
||||||
|
setStatusComment('')
|
||||||
|
await fetchOrders()
|
||||||
|
setNotice({ type: 'success', text: 'تم تحديث حالة الطلب وتسجيلها في السجل.' })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Status update failed', error)
|
||||||
|
setNotice({ type: 'error', text: 'رفض الخادم هذا الانتقال. تأكد من التسلسل والصلاحيات.' })
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedLogs = [...(selectedOrder?.order_status_logs_order || [])].sort(
|
||||||
|
(a, b) => new Date(b.changed_at || b.createdAt || '').getTime() - new Date(a.changed_at || a.createdAt || '').getTime(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Parcel Operations')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<div dir="rtl" className="min-h-screen space-y-6 text-slate-900" style={{ fontFamily: 'Cairo, ui-sans-serif, system-ui' }}>
|
||||||
|
<section className="overflow-hidden rounded-[2rem] bg-gradient-to-br from-[#01696f] via-[#07575d] to-[#0f2730] p-6 text-white shadow-2xl shadow-teal-900/20 md:p-8">
|
||||||
|
<div className="grid gap-6 lg:grid-cols-[1.4fr_0.8fr]">
|
||||||
|
<div>
|
||||||
|
<div className="mb-5 inline-flex rounded-full border border-white/15 bg-white/10 px-4 py-2 text-sm font-semibold backdrop-blur">
|
||||||
|
مركز عمليات الطرود · [اسم شركتك]
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-extrabold leading-tight md:text-5xl">إدارة طلبات التوصيل من الإدخال حتى تغيير الحالة</h1>
|
||||||
|
<p className="mt-4 max-w-2xl text-sm leading-7 text-teal-50 md:text-base">
|
||||||
|
شريحة تشغيلية أولى تربط إنشاء الطلب، قائمة الطلبات، تفاصيل الطلب، وتحديث الحالة وفق مسار العمل المعتمد وبصلاحيات الخادم.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 flex flex-wrap gap-3">
|
||||||
|
<BaseButton label="إنشاء طلب جديد" color="info" className="border-white/20 bg-white text-[#01696f] hover:bg-teal-50" onClick={() => document.getElementById('create-order-form')?.scrollIntoView({ behavior: 'smooth' })} />
|
||||||
|
<BaseButton label="العودة للوحة الإدارة" color="white" outline href="/dashboard" className="border-white/30 text-white hover:bg-white/10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-3xl border border-white/10 bg-white/10 p-5 backdrop-blur">
|
||||||
|
<p className="text-sm text-teal-50">المستخدم الحالي</p>
|
||||||
|
<p className="mt-2 text-xl font-bold">{currentUser?.firstName || currentUser?.email || 'مشغل النظام'}</p>
|
||||||
|
<p className="text-sm text-teal-100">الدور: {currentUser?.app_role?.name || 'غير محدد'}</p>
|
||||||
|
<div className="mt-6 rounded-2xl bg-white p-4 text-slate-900">
|
||||||
|
<p className="text-sm text-slate-500">تحصيل اليوم COD</p>
|
||||||
|
<p className="mt-1 text-3xl font-extrabold" style={{ color: teal }}>{revenueToday.toLocaleString()} د.أ</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{notice && (
|
||||||
|
<div className={`rounded-2xl border px-4 py-3 text-sm font-semibold ${notice.type === 'success' ? 'border-emerald-200 bg-emerald-50 text-emerald-800' : 'border-red-200 bg-red-50 text-red-800'}`}>
|
||||||
|
{notice.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-4">
|
||||||
|
{stats.map((stat) => (
|
||||||
|
<CardBox key={stat.label} className="border-0 bg-white shadow-sm ring-1 ring-slate-100">
|
||||||
|
<p className="text-sm font-semibold text-slate-500">{stat.label}</p>
|
||||||
|
<p className="mt-2 text-3xl font-extrabold" style={{ color: teal }}>{loading ? '…' : stat.value}</p>
|
||||||
|
<p className="mt-2 text-xs text-slate-400">{stat.hint}</p>
|
||||||
|
</CardBox>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 xl:grid-cols-[0.95fr_1.35fr]">
|
||||||
|
<CardBox className="border-0 bg-white shadow-sm ring-1 ring-slate-100">
|
||||||
|
<div id="create-order-form" className="mb-5 flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-teal-700">طلب جديد</p>
|
||||||
|
<h2 className="text-2xl font-extrabold text-slate-900">إدخال طلب يدوي</h2>
|
||||||
|
</div>
|
||||||
|
<span className="rounded-full bg-teal-50 px-3 py-1 text-xs font-bold text-teal-700">الحالة الافتراضية: قيد الانتظار</span>
|
||||||
|
</div>
|
||||||
|
<form className="grid gap-4" onSubmit={handleCreateOrder}>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
<FieldLabel label="التاجر">
|
||||||
|
<select className="ops-input" value={form.merchant} onChange={(event) => handleFormChange('merchant', event.target.value)}>
|
||||||
|
<option value="">بدون تاجر</option>
|
||||||
|
{merchants.map((merchant) => <option key={merchant.id} value={merchant.id}>{optionLabel(merchant)}</option>)}
|
||||||
|
</select>
|
||||||
|
</FieldLabel>
|
||||||
|
<FieldLabel label="قيمة التحصيل COD">
|
||||||
|
<input className="ops-input" inputMode="decimal" value={form.cod_amount} onChange={(event) => handleFormChange('cod_amount', event.target.value)} placeholder="0.00" />
|
||||||
|
</FieldLabel>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
<FieldLabel label="اسم العميل *">
|
||||||
|
<input className="ops-input" value={form.customer_name} onChange={(event) => handleFormChange('customer_name', event.target.value)} placeholder="مثال: أحمد محمد" />
|
||||||
|
</FieldLabel>
|
||||||
|
<FieldLabel label="هاتف العميل *">
|
||||||
|
<input className="ops-input" value={form.customer_phone} onChange={(event) => handleFormChange('customer_phone', event.target.value)} placeholder="07xxxxxxxx" />
|
||||||
|
</FieldLabel>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
<FieldLabel label="المدينة">
|
||||||
|
<select className="ops-input" value={form.delivery_city} onChange={(event) => handleFormChange('delivery_city', event.target.value)}>
|
||||||
|
<option value="">اختر المدينة</option>
|
||||||
|
{cities.map((city) => <option key={city.id} value={city.id}>{optionLabel(city)}</option>)}
|
||||||
|
</select>
|
||||||
|
</FieldLabel>
|
||||||
|
<FieldLabel label="المنطقة">
|
||||||
|
<select className="ops-input" value={form.delivery_region} onChange={(event) => handleFormChange('delivery_region', event.target.value)}>
|
||||||
|
<option value="">اختر المنطقة</option>
|
||||||
|
{regions.map((region) => <option key={region.id} value={region.id}>{optionLabel(region)}</option>)}
|
||||||
|
</select>
|
||||||
|
</FieldLabel>
|
||||||
|
</div>
|
||||||
|
<FieldLabel label="العنوان الكامل *">
|
||||||
|
<textarea className="ops-input min-h-20" value={form.delivery_address} onChange={(event) => handleFormChange('delivery_address', event.target.value)} placeholder="الحي، الشارع، أقرب معلم" />
|
||||||
|
</FieldLabel>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
<FieldLabel label="وصف الطرد">
|
||||||
|
<input className="ops-input" value={form.package_description} onChange={(event) => handleFormChange('package_description', event.target.value)} placeholder="ملابس، مستندات، إلكترونيات..." />
|
||||||
|
</FieldLabel>
|
||||||
|
<FieldLabel label="الوزن/الحجم">
|
||||||
|
<input className="ops-input" value={form.weight_size} onChange={(event) => handleFormChange('weight_size', event.target.value)} placeholder="اختياري" />
|
||||||
|
</FieldLabel>
|
||||||
|
</div>
|
||||||
|
<FieldLabel label="ملاحظات داخلية">
|
||||||
|
<input className="ops-input" value={form.notes} onChange={(event) => handleFormChange('notes', event.target.value)} placeholder="أي تعليمات خاصة للتسليم" />
|
||||||
|
</FieldLabel>
|
||||||
|
<BaseButton type="submit" color="info" label={saving ? 'جارٍ الحفظ...' : 'إنشاء الطلب'} disabled={saving} className="w-full border-[#01696f] bg-[#01696f] py-3 text-white hover:bg-[#07575d]" />
|
||||||
|
</form>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox className="border-0 bg-white shadow-sm ring-1 ring-slate-100" hasComponentLayout>
|
||||||
|
<div className="border-b border-slate-100 p-5">
|
||||||
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-teal-700">قائمة التشغيل</p>
|
||||||
|
<h2 className="text-2xl font-extrabold">آخر 50 طلب</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input className="ops-input w-full md:w-56" value={filters.query} onChange={(event) => setFilters((current) => ({ ...current, query: event.target.value }))} placeholder="بحث برقم الطلب أو الهاتف" />
|
||||||
|
<select className="ops-input w-40" value={filters.status} onChange={(event) => setFilters((current) => ({ ...current, status: event.target.value }))}>
|
||||||
|
<option value="">كل الحالات</option>
|
||||||
|
{Object.entries(statusMeta).map(([value, meta]) => <option key={value} value={value}>{meta.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full text-right text-sm">
|
||||||
|
<thead className="bg-slate-50 text-xs font-bold text-slate-500">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3">رقم الطلب</th>
|
||||||
|
<th className="px-4 py-3">العميل</th>
|
||||||
|
<th className="px-4 py-3">التاجر</th>
|
||||||
|
<th className="px-4 py-3">السائق</th>
|
||||||
|
<th className="px-4 py-3">الحالة</th>
|
||||||
|
<th className="px-4 py-3">COD</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-100">
|
||||||
|
{orders.map((order) => (
|
||||||
|
<tr key={order.id} className="cursor-pointer transition hover:bg-teal-50/60" onClick={() => openOrder(order)}>
|
||||||
|
<td className="px-4 py-4 font-extrabold text-teal-700">{order.order_code || order.id.slice(0, 8)}</td>
|
||||||
|
<td className="px-4 py-4"><p className="font-bold">{order.customer_name || '—'}</p><p className="text-xs text-slate-400">{order.customer_phone || '—'}</p></td>
|
||||||
|
<td className="px-4 py-4 text-slate-600">{optionLabel(order.merchant)}</td>
|
||||||
|
<td className="px-4 py-4 text-slate-600">{optionLabel(order.assigned_driver)}</td>
|
||||||
|
<td className="px-4 py-4"><span className={badgeClass(order.current_status)}>{labelFor(order.current_status)}</span></td>
|
||||||
|
<td className="px-4 py-4 font-bold">{Number(order.cod_amount || 0).toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!orders.length && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="px-4 py-12 text-center text-slate-500">
|
||||||
|
لا توجد طلبات مطابقة حالياً. ابدأ بإنشاء أول طلب من النموذج المجاور.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardBox className="border-0 bg-white shadow-sm ring-1 ring-slate-100">
|
||||||
|
{!selectedOrder ? (
|
||||||
|
<div className="rounded-3xl border border-dashed border-slate-200 bg-slate-50 p-10 text-center">
|
||||||
|
<p className="text-xl font-extrabold">اختر طلباً من القائمة</p>
|
||||||
|
<p className="mt-2 text-sm text-slate-500">ستظهر هنا تفاصيل الطلب، الإجراءات التالية المسموحة، وسجل الحالات.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-6 lg:grid-cols-[1fr_0.9fr]">
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-teal-700">تفاصيل الطلب</p>
|
||||||
|
<h2 className="text-3xl font-extrabold">{selectedOrder.order_code || selectedOrder.id.slice(0, 8)}</h2>
|
||||||
|
</div>
|
||||||
|
<span className={badgeClass(selectedOrder.current_status)}>{labelFor(selectedOrder.current_status)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 grid gap-3 md:grid-cols-2">
|
||||||
|
<Info label="العميل" value={selectedOrder.customer_name} />
|
||||||
|
<Info label="الهاتف" value={selectedOrder.customer_phone} />
|
||||||
|
<Info label="التاجر" value={optionLabel(selectedOrder.merchant)} />
|
||||||
|
<Info label="السائق" value={optionLabel(selectedOrder.assigned_driver)} />
|
||||||
|
<Info label="المدينة" value={optionLabel(selectedOrder.delivery_city)} />
|
||||||
|
<Info label="المنطقة" value={optionLabel(selectedOrder.delivery_region)} />
|
||||||
|
<Info label="العنوان" value={selectedOrder.delivery_address} wide />
|
||||||
|
<Info label="وصف الطرد" value={selectedOrder.package_description} />
|
||||||
|
<Info label="COD" value={`${Number(selectedOrder.cod_amount || 0).toLocaleString()} د.أ`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-3xl bg-slate-50 p-5">
|
||||||
|
<h3 className="text-xl font-extrabold">تغيير الحالة</h3>
|
||||||
|
<p className="mt-1 text-sm text-slate-500">الواجهة تعرض فقط الانتقالات التالية، والخادم يرفض أي انتقال غير مسموح.</p>
|
||||||
|
<div className="mt-4 grid gap-3">
|
||||||
|
<FieldLabel label="الحالة التالية">
|
||||||
|
<select className="ops-input bg-white" value={nextStatus} onChange={(event) => setNextStatus(event.target.value)}>
|
||||||
|
<option value="">اختر الإجراء التالي</option>
|
||||||
|
{selectedAllowedStatuses.map((status) => <option key={status} value={status}>{labelFor(status)}</option>)}
|
||||||
|
</select>
|
||||||
|
</FieldLabel>
|
||||||
|
{nextStatus === 'assigned' && (
|
||||||
|
<FieldLabel label="السائق المعيّن *">
|
||||||
|
<select className="ops-input bg-white" value={statusDriver} onChange={(event) => setStatusDriver(event.target.value)}>
|
||||||
|
<option value="">اختر السائق</option>
|
||||||
|
{drivers.map((driver) => <option key={driver.id} value={driver.id}>{optionLabel(driver)}</option>)}
|
||||||
|
</select>
|
||||||
|
</FieldLabel>
|
||||||
|
)}
|
||||||
|
<FieldLabel label="تعليق على الحركة">
|
||||||
|
<textarea className="ops-input min-h-20 bg-white" value={statusComment} onChange={(event) => setStatusComment(event.target.value)} placeholder="مثال: تم التواصل مع العميل وتأكيد الموعد" />
|
||||||
|
</FieldLabel>
|
||||||
|
<BaseButton color="info" label={saving ? 'جارٍ التحديث...' : 'تطبيق تغيير الحالة'} disabled={saving || !selectedAllowedStatuses.length} onClick={handleStatusChange} className="border-[#01696f] bg-[#01696f] py-3 text-white hover:bg-[#07575d]" />
|
||||||
|
{!selectedAllowedStatuses.length && <p className="text-sm font-semibold text-slate-500">هذه حالة نهائية ولا توجد انتقالات متاحة.</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<h3 className="mb-4 text-xl font-extrabold">سجل الحالات</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{sortedLogs.map((log) => (
|
||||||
|
<div key={log.id} className="rounded-2xl border border-slate-100 bg-white p-4 shadow-sm">
|
||||||
|
<div className="flex flex-wrap items-center gap-2 text-sm font-bold">
|
||||||
|
<span className={badgeClass(log.from_status)}>{labelFor(log.from_status)}</span>
|
||||||
|
<span className="text-slate-300">←</span>
|
||||||
|
<span className={badgeClass(log.to_status)}>{labelFor(log.to_status)}</span>
|
||||||
|
<span className="mr-auto text-xs text-slate-400">{new Date(log.changed_at || log.createdAt || '').toLocaleString('ar')}</span>
|
||||||
|
</div>
|
||||||
|
{log.comment && <p className="mt-2 text-sm text-slate-600">{log.comment}</p>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!sortedLogs.length && <p className="rounded-2xl bg-slate-50 p-5 text-center text-sm text-slate-500">لا توجد حركات مسجلة بعد. أول تغيير حالة سيظهر هنا.</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
<style jsx global>{`
|
||||||
|
.ops-input {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid rgb(226 232 240);
|
||||||
|
background: rgb(248 250 252);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgb(15 23 42);
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 150ms ease, box-shadow 150ms ease, background 150ms ease;
|
||||||
|
}
|
||||||
|
.ops-input:focus {
|
||||||
|
border-color: #01696f;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 0 0 3px rgba(1, 105, 111, 0.14);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||||
|
<label className="block">
|
||||||
|
<span className="mb-2 block text-sm font-extrabold text-slate-700">{label}</span>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
|
||||||
|
const Info = ({ label, value, wide }: { label: string; value?: string | number; wide?: boolean }) => (
|
||||||
|
<div className={`rounded-2xl bg-slate-50 p-4 ${wide ? 'md:col-span-2' : ''}`}>
|
||||||
|
<p className="text-xs font-bold text-slate-400">{label}</p>
|
||||||
|
<p className="mt-1 font-extrabold text-slate-800">{value || '—'}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
OperationsOrdersPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OperationsOrdersPage
|
||||||
@ -28,18 +28,18 @@ export default function Register() {
|
|||||||
const { data: response } = await axios.post('/auth/signup',value);
|
const { data: response } = await axios.post('/auth/signup',value);
|
||||||
await router.push('/login')
|
await router.push('/login')
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
notify('success', 'Please check your email for verification link')
|
notify('success', 'تحقق من بريدك الإلكتروني لتأكيد الحساب')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
console.log('error: ', error)
|
console.log('error: ', error)
|
||||||
notify('error', 'Something was wrong. Try again')
|
notify('error', 'حدث خطأ. حاول مرة أخرى')
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Login')}</title>
|
<title>{getPageTitle('إنشاء حساب')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<SectionFullScreen bg='violet'>
|
||||||
@ -54,13 +54,13 @@ export default function Register() {
|
|||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
|
|
||||||
<FormField label='Email' help='Please enter your email'>
|
<FormField label='البريد الإلكتروني' help='الرجاء إدخال البريد الإلكتروني'>
|
||||||
<Field type='email' name='email' />
|
<Field type='email' name='email' />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label='Password' help='Please enter your password'>
|
<FormField label='كلمة المرور' help='الرجاء إدخال كلمة المرور'>
|
||||||
<Field type='password' name='password' />
|
<Field type='password' name='password' />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label='Confirm Password' help='Please confirm your password'>
|
<FormField label='تأكيد كلمة المرور' help='الرجاء تأكيد كلمة المرور'>
|
||||||
<Field type='password' name='confirm' />
|
<Field type='password' name='confirm' />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
@ -69,12 +69,12 @@ export default function Register() {
|
|||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
type='submit'
|
type='submit'
|
||||||
label={loading ? 'Loading...' : 'Register' }
|
label={loading ? 'جارٍ التحميل...' : 'إنشاء حساب' }
|
||||||
color='info'
|
color='info'
|
||||||
/>
|
/>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
href={'/login'}
|
href={'/login'}
|
||||||
label={'Login'}
|
label={'تسجيل الدخول'}
|
||||||
color='info'
|
color='info'
|
||||||
/>
|
/>
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
|
|||||||
@ -28,12 +28,12 @@ export default function Verify() {
|
|||||||
}).then(verified => {
|
}).then(verified => {
|
||||||
if (verified) {
|
if (verified) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
notify('success', 'Your email was verified');
|
notify('success', 'تم تأكيد البريد الإلكتروني بنجاح');
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
console.log('error: ', error);
|
console.log('error: ', error);
|
||||||
notify('error', error.response);
|
notify('error', 'تعذر تأكيد البريد الإلكتروني');
|
||||||
}).finally(async () => {
|
}).finally(async () => {
|
||||||
await router.push('/login');
|
await router.push('/login');
|
||||||
});
|
});
|
||||||
@ -44,11 +44,11 @@ export default function Verify() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Verify Email')}</title>
|
<title>{getPageTitle('تأكيد البريد الإلكتروني')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionFullScreen bg='violet'>
|
<SectionFullScreen bg='violet'>
|
||||||
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
|
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
|
||||||
<p>{loading ? 'Loading...' : ''}</p>
|
<p>{loading ? 'جارٍ التحقق...' : ''}</p>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionFullScreen>
|
</SectionFullScreen>
|
||||||
|
|
||||||
|
|||||||
@ -123,6 +123,23 @@ export const update = createAsyncThunk('orders/updateOrders', async (payload: an
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const changeStatus = createAsyncThunk('orders/changeStatus', async (payload: any, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const result = await axios.put(
|
||||||
|
`orders/${payload.id}/status`,
|
||||||
|
{ data: payload.data }
|
||||||
|
)
|
||||||
|
return result.data
|
||||||
|
} catch (error) {
|
||||||
|
if (!error.response) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rejectWithValue(error.response.data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export const ordersSlice = createSlice({
|
export const ordersSlice = createSlice({
|
||||||
name: 'orders',
|
name: 'orders',
|
||||||
initialState,
|
initialState,
|
||||||
@ -208,6 +225,20 @@ export const ordersSlice = createSlice({
|
|||||||
rejectNotify(state, action);
|
rejectNotify(state, action);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
builder.addCase(changeStatus.pending, (state) => {
|
||||||
|
state.loading = true
|
||||||
|
resetNotify(state);
|
||||||
|
})
|
||||||
|
builder.addCase(changeStatus.fulfilled, (state, action) => {
|
||||||
|
state.loading = false
|
||||||
|
state.orders = action.payload
|
||||||
|
fulfilledNotify(state, 'Order status has been updated');
|
||||||
|
})
|
||||||
|
builder.addCase(changeStatus.rejected, (state, action) => {
|
||||||
|
state.loading = false
|
||||||
|
rejectNotify(state, action);
|
||||||
|
})
|
||||||
|
|
||||||
builder.addCase(uploadCsv.pending, (state) => {
|
builder.addCase(uploadCsv.pending, (state) => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
resetNotify(state);
|
resetNotify(state);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user