diff --git a/.env b/.env index 88e4c465e92e7fba735ed2cf9323364f1c4390a6..d1fbc508362bb8adbadc837d400a90f3711656df 100644 --- a/.env +++ b/.env @@ -39,5 +39,3 @@ NEXT_PUBLIC_DEFAULT_IMAGE_URL=https://mydressin-rec.s3.eu-west-3.amazonaws.com/I #GATEWAY API URL NEXT_PUBLIC_MYDRESSIN_GATEWAY_API_URL=https://mydressin-api.mc-test.xyz - - diff --git a/src/app/dashboard/shipping/general/shipments/[id]/page.tsx b/src/app/dashboard/shipping/general/shipments/[id]/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d6c1d0b9a029c7fffb1a38c76ab4e3ff9a047d93 --- /dev/null +++ b/src/app/dashboard/shipping/general/shipments/[id]/page.tsx @@ -0,0 +1,10 @@ +import ShippingMainView from "@/shared/sections/shipping/general/ShippingMainView"; + + +export const metadata = { + title: 'Dashboard: Livraison', +}; + +export default function ListShipmentPage() { + return <ShippingMainView defaultValue='5'/>; +} diff --git a/src/app/dashboard/shipping/general/shipments/page.tsx b/src/app/dashboard/shipping/general/shipments/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d6c1d0b9a029c7fffb1a38c76ab4e3ff9a047d93 --- /dev/null +++ b/src/app/dashboard/shipping/general/shipments/page.tsx @@ -0,0 +1,10 @@ +import ShippingMainView from "@/shared/sections/shipping/general/ShippingMainView"; + + +export const metadata = { + title: 'Dashboard: Livraison', +}; + +export default function ListShipmentPage() { + return <ShippingMainView defaultValue='5'/>; +} diff --git a/src/contexts/types/shipment.ts b/src/contexts/types/shipment.ts new file mode 100644 index 0000000000000000000000000000000000000000..9623dd27673aad635388022483154ae0febe9ddb --- /dev/null +++ b/src/contexts/types/shipment.ts @@ -0,0 +1,75 @@ + +import { MondialRelayMode, ShippingMethodType } from "@/contexts/types/Shipping"; + +export enum ShipmentStatus { + FAILED = 'FAILED', + WAITING_FOR_PAYMENT = 'WAITING_FOR_PAYMENT', + PENDING = 'PENDING', + IN_PROGRESS = 'IN_PROGRESS', + SHIPPED = 'SHIPPED', + DELIVERED = 'DELIVERED', + CANCELED = 'CANCELED', +} + +export const shipmentStatusLabels = { + [ShipmentStatus.FAILED]: 'Échec de l\'expédition', + [ShipmentStatus.WAITING_FOR_PAYMENT]: 'En attente de paiement', + [ShipmentStatus.PENDING]: 'En attente', + [ShipmentStatus.IN_PROGRESS]: 'En cours', + [ShipmentStatus.SHIPPED]: 'Expédié', + [ShipmentStatus.DELIVERED]: 'Livré', + [ShipmentStatus.CANCELED]: 'Annulé', +}; + +export enum MondialRelayShipmentStatus { + SHIPMENT_CREATED = 'SHIPMENT_CREATED', + REGISTERED_PACKAGE = 'REGISTERED_PACKAGE', +} + +export const mondialRelayShipmentStatusLabels = { + [MondialRelayShipmentStatus.SHIPMENT_CREATED]: 'Expédition créée', + [MondialRelayShipmentStatus.REGISTERED_PACKAGE]: 'Colis enregistré', +}; + +export interface Shipment { + id: number; + createdAt: Date; + updatedAt: Date; + clientId: number; + userId: number; + orderId: number; + shipmentStatus: ShipmentStatus; + total: number; + shippingMethodType: ShippingMethodType; + shippingFirstName: string; + shippingLastName: string; + shippingCompanyName?: string; + shippingEmail: string; + shippingPhoneNumber: string; + shippingStreet: string; + shippingHome: string; + shippingCity: string; + shippingZipCode: string; + shippingCountry: string; + trackingNumber: string; +} + +export interface ShipmentMondialRelay extends Shipment { + mondialRelayMode: MondialRelayMode; + mondialRelayShipmentStatus: MondialRelayShipmentStatus; +} + +export interface ShipmentColissimo extends Shipment { + labelCreatedAt: Date; +} + + +export type ShipmentTableFilterValue = string | string[] | Date | null; + +export type ShipmentTableFilters = { + country: string[]; + shippingMethod: string[]; + status: string[]; + startDate: Date | null; + endDate: Date | null; +}; diff --git a/src/routes/paths.ts b/src/routes/paths.ts index 87ff0081822eab5e8b405dd5c9f925c6b5fc6cee..668ebdb4c79d48e65df5bd612458944682519a98 100644 --- a/src/routes/paths.ts +++ b/src/routes/paths.ts @@ -32,14 +32,14 @@ export const paths = { termeChoix: { root: (attributId: string) => `${ROOTS.DASHBOARD}/product/attribut/${attributId}/termeChoix`, - editChoix : (attributId: string, choiceId: string )=> + editChoix: (attributId: string, choiceId: string) => `${ROOTS.DASHBOARD}/product/attribut/${attributId}/termeChoix/${choiceId}/edit`, }, termeImage: { root: (id: string) => `${ROOTS.DASHBOARD}/product/attribut/${id}/termeImage`, - editImage : (attributId: string, imageId: string )=> + editImage: (attributId: string, imageId: string) => `${ROOTS.DASHBOARD}/product/attribut/${attributId}/termeImage/${imageId}/edit`, }, termeLabel: { @@ -122,6 +122,10 @@ export const paths = { shippingClass: { shippingClassList: `${ROOTS.DASHBOARD}/shipping/general/shippingClass`, }, + shipments: { + shipmentList: `${ROOTS.DASHBOARD}/shipping/general/shipments`, + details: (id: string) => `${ROOTS.DASHBOARD}/shipping/general/shipments/${id}`, + }, }, colissimo: `${ROOTS.DASHBOARD}/shipping/colissimo`, mondialRelayInPost: { @@ -167,7 +171,7 @@ export const paths = { listSupplierOrder: `${ROOTS.DASHBOARD}/driver/supplierOrder`, details: (id: string) => `${ROOTS.DASHBOARD}/driver/supplierOrder/${id}/details`, supplier: `${ROOTS.DASHBOARD}/driver/supplier`, - supplierDetails : (id: string) => `${ROOTS.DASHBOARD}/driver/supplier/${id}/details` + supplierDetails: (id: string) => `${ROOTS.DASHBOARD}/driver/supplier/${id}/details` }, }, promo: { @@ -177,7 +181,7 @@ export const paths = { all_promo: `${ROOTS.DASHBOARD}/promo`, details: (id: string) => `${ROOTS.DASHBOARD}/promo/${id}`, edit: (id: number) => `${ROOTS.DASHBOARD}/promo/${id}/edit`, - + Duplicatepromo: (id: string) => `${ROOTS.DASHBOARD}/promo/duplicate/${id}`, }, giftcard: { diff --git a/src/shared/api/server.ts b/src/shared/api/server.ts index cf674fd1f6b41e611f7719c7261286baeb1d9106..48fdb112a07f4c9e0fcff130bc0422ce3a53c264 100644 --- a/src/shared/api/server.ts +++ b/src/shared/api/server.ts @@ -175,6 +175,11 @@ export const endpoints = { delete: (shippingAreaId: string) => `/api/shipping/area/${shippingAreaId}`, }, + shipments: { + getAll: `/api/shipping/shipment/all_shipments`, + getById: (shipmentId: string) => + `/api/shipping/shipment/${shipmentId}`, + }, }, media: { getAll: `/api/medias/all-medias`, diff --git a/src/shared/api/shipment.ts b/src/shared/api/shipment.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e7e33997659e3d3b6d0c0eac94667bd7e33f24e --- /dev/null +++ b/src/shared/api/shipment.ts @@ -0,0 +1,57 @@ +import { useMemo } from "react"; +import useSWR, { mutate } from "swr"; +import axiosInstance, { endpoints, fetcher } from "./server"; +import { Order, OrderStatus } from "@/contexts/types/main-order"; +import { Shipment } from "@/contexts/types/shipment"; + + +const options = { + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, +}; + +///////////////////-----------Order Management----------/////////////////////// +////////////////////////////////////////////////////////////////////////////// +export function getAllShipments() { + const URL = [endpoints.shipping.shipments.getAll]; + const { data, error, isLoading, isValidating, mutate } = useSWR<Shipment[]>(URL, fetcher, options); + + console.log("data of order : "+data); + + const memoizedValue = useMemo( + () => ({ + shipmentsData: (data as Shipment[]) || [], + shipmentsError: error, + shipmentsLoading: isLoading, + shipmentsValidating: isValidating, + mutateShipments: mutate, + }), + [data, error] + ); + + return memoizedValue; +} + +export function getShipmentById(shipmentId: string) { + const { data, error, isLoading, isValidating, mutate } = useSWR<Shipment>( + shipmentId ? endpoints.shipping.shipments.getById(shipmentId) : null, + fetcher, + options + ); + + const memoizedValue = useMemo( + () => ({ + shipmentData: data, + shipmentError: error, + shipmentLoading: isLoading, + shipmentValidating: isValidating, + mutateShipment: mutate, + }), + [data, error] + ); + + return memoizedValue; +} + + diff --git a/src/shared/sections/shipping/general/ShippingMainView.tsx b/src/shared/sections/shipping/general/ShippingMainView.tsx index cf8597b3db058d5f2eaed155d17f8e7e8fd95323..d9c84eb7142a53535d344a9750aff8484a655f74 100644 --- a/src/shared/sections/shipping/general/ShippingMainView.tsx +++ b/src/shared/sections/shipping/general/ShippingMainView.tsx @@ -18,6 +18,7 @@ import ShippingAreaTableView from './shippingAreas/ShippingAreaTableView'; import DeliveryRulesView from './deliveryRules/DeliveryRulesView'; import ShippingClassTableView from './shippingClass/ShippingClassTableView'; import ShippingStatisticsView from './statistics/ShippingStatisticsView'; +import ShipmentListView from './shipments/ShipmentListView'; // ---------------------------------------------------------------------- @@ -38,9 +39,13 @@ const TABS = [ { value: '4', label: "Classes d'expédition", - },, + }, { value: '5', + label: 'Expéditions', + }, + { + value: '6', label: 'Statistiques', }, ]; @@ -83,7 +88,10 @@ export default function ShippingMainView({ defaultValue }: ShippingMainViewProps {currentTab === '4' && <ShippingClassTableView />} - {currentTab === '5' && <ShippingStatisticsView />} + {currentTab === '5' && <ShipmentListView />} + + {currentTab === '6' && <ShippingStatisticsView />} + </Container> ); } diff --git a/src/shared/sections/shipping/general/shipments/ShipmentListView.tsx b/src/shared/sections/shipping/general/shipments/ShipmentListView.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9b63b2e5e55a8946d96b46828707d38756de7afa --- /dev/null +++ b/src/shared/sections/shipping/general/shipments/ShipmentListView.tsx @@ -0,0 +1,341 @@ +"use client"; + +import isEqual from "lodash/isEqual"; +import { useState, useEffect, useCallback } from "react"; +import Stack from "@mui/material/Stack"; +import Button from "@mui/material/Button"; +import Card from "@mui/material/Card"; +import Box from "@mui/material/Box"; +import { + DataGrid, + GridColDef, + GridToolbarContainer, + GridRowSelectionModel, + GridToolbarQuickFilter, + GridToolbarColumnsButton, + GridColumnVisibilityModel, +} from "@mui/x-data-grid"; +import { paths } from "@/routes/paths"; +import { useRouter } from "@/hooks"; +import { useBoolean } from "@/hooks/use-boolean"; +import { isAfter, isBetween } from '@/utils/format-time'; +import { useSnackbar } from "@/shared/components/snackbar"; +import EmptyContent from "@/shared/components/empty-content"; +import { useSettingsContext } from "@/shared/components/settings"; +import CustomBreadcrumbs from "@/shared/components/custom-breadcrumbs"; +import Select, { SelectChangeEvent } from "@mui/material/Select"; +import FormControl from "@mui/material/FormControl"; +import InputLabel from "@mui/material/InputLabel"; +import MenuItem from "@mui/material/MenuItem"; +import { + RenderCellID, + RenderCellCreatedAt, + RenderCellCustomer, + RenderCellAdress, + RenderCellCountry, + RenderCellShippingMethod, + RenderCellStatus, + RenderCellTrackingNumber +} from "./shipment-table-row"; +import { ShippingMethodType, shippingMethodTypeLabels } from "@/contexts/types/Shipping"; +import { countries } from "@/shared/assets/data"; +import { Shipment, ShipmentStatus, shipmentStatusLabels, ShipmentTableFilters, ShipmentTableFilterValue } from "@/contexts/types/shipment"; +import ShipmentTableToolbar from "./shipment-table-toolbar"; +import ShipmentTableFiltersResult from "./shipment-table-filters-result"; +import { getAllShipments } from "@/shared/api/shipment"; + +// ---------------------------------------------------------------------- + + +const Country_OPTIONS = countries + .filter(country => country.label && country.code) + .map(country => ({ + value: country.label, + label: country.label, + })); + +const ShippingMethod_OPTIONS = Object.keys(ShippingMethodType).map((key) => ({ + value: ShippingMethodType[key as keyof typeof ShippingMethodType], + label: shippingMethodTypeLabels[ShippingMethodType[key as keyof typeof ShippingMethodType]], +})); + +const Status_OPTIONS = Object.keys(ShipmentStatus).map((key) => ({ + value: ShipmentStatus[key as keyof typeof ShipmentStatus], + label: shipmentStatusLabels[ShipmentStatus[key as keyof typeof ShipmentStatus]], +})); + +const defaultFilters: ShipmentTableFilters = { + country: [], + shippingMethod: [], + status: [], + startDate: null, + endDate: null, +}; + +const HIDE_COLUMNS = { + category: false, +}; + +const HIDE_COLUMNS_TOGGLABLE = ["category", "actions"]; + +// ---------------------------------------------------------------------- + +export default function ShipmentListView() { + const { enqueueSnackbar } = useSnackbar(); + const confirmRows = useBoolean(); + const router = useRouter(); + const settings = useSettingsContext(); + const { shipmentsData, shipmentsError, shipmentsLoading } = getAllShipments(); + const [tableData, setTableData] = useState<Shipment[]>(shipmentsData); + const [filters, setFilters] = useState(defaultFilters); + const [selectedRowIds, setSelectedRowIds] = useState<GridRowSelectionModel>( + [] + ); + + const [columnVisibilityModel, setColumnVisibilityModel] = + useState<GridColumnVisibilityModel>(HIDE_COLUMNS); + + useEffect(() => { + if (shipmentsData.length) { + setTableData(shipmentsData); + } + }, [shipmentsData]); + + const dateError = isAfter(filters.startDate, filters.endDate); + + const dataFiltered = applyFilter({ + inputData: tableData, + filters, + dateError, + }); + + const canReset = !isEqual(defaultFilters, filters); + + const handleFilters = useCallback( + (name: string, value: ShipmentTableFilterValue) => { + setFilters((prevState) => ({ + ...prevState, + [name]: value, + })); + }, + [] + ); + + const handleResetFilters = useCallback(() => { + setFilters(defaultFilters); + }, []); + + const handleViewRow = useCallback( + (id: string) => { + router.push(paths.dashboard.admin.shipping.general.shipments.details(id)); + }, + [router] + ); + + const columns: GridColDef[] = [ + { + field: "id", + headerName: "ID", + flex: 1, + width: 40, + filterable: true, + hideable: false, + renderCell: (params) => <RenderCellID params={params} />, + }, + { + field: "createdAt", + headerName: "Date Création", + width: 120, + hideable: false, + renderCell: (params) => <RenderCellCreatedAt params={params} />, + }, + { + field: "customer", + headerName: "Client", + minWidth: 190, + renderCell: (params) => <RenderCellCustomer params={params} />, + }, + { + field: "adress", + headerName: "Adresse", + minWidth: 180, + renderCell: (params) => <RenderCellAdress params={params} />, + }, + { + field: "country", + headerName: "Pays", + minWidth: 100, + renderCell: (params) => <RenderCellCountry params={params} />, + }, + { + field: "shippingMethod", + headerName: "Méthode de livraison", + minWidth: 200, + renderCell: (params) => <RenderCellShippingMethod params={params} />, + }, + { + field: "status", + headerName: "Status", + minWidth: 110, + renderCell: (params) => <RenderCellStatus params={params} />, + }, + { + field: "trackingNumber", + headerName: "Numéro de suivi", + minWidth: 130, + align: 'right', + renderCell: (params) => <RenderCellTrackingNumber params={params} />, + }, + ]; + + const getTogglableColumns = () => + columns + .filter((column) => !HIDE_COLUMNS_TOGGLABLE.includes(column.field)) + .map((column) => column.field); + + const [actionGroupe, setActionGroupe] = useState(""); + + const handleChangeActionGroupe = (event: SelectChangeEvent) => { + setActionGroupe(event.target.value as string); + }; + + return ( + <> + <CustomBreadcrumbs + heading="Liste des expéditions" + links={[ + { name: 'Dashboard', href: paths.dashboard.root, }, + { name: 'Livraison', href: paths.dashboard.admin.shipping.general.root, }, + { name: "Expéditions" }, + ]} + sx={{ + mb: { + xs: 3, + md: 5, + }, + }} + /> + + <Card> + <Box + sx={{ + width: "100%", + flexGrow: { md: 1 }, + display: { md: "flex" }, + flexDirection: { md: "column" }, + }} + > + <DataGrid + checkboxSelection + disableRowSelectionOnClick + rows={dataFiltered} + columns={columns} + getRowHeight={() => "auto"} + pageSizeOptions={[5, 10, 25]} + initialState={{ + pagination: { + paginationModel: { pageSize: 10 }, + }, + }} + onRowSelectionModelChange={(newSelectionModel) => { + setSelectedRowIds(newSelectionModel); + }} + columnVisibilityModel={columnVisibilityModel} + onColumnVisibilityModelChange={(newModel) => + setColumnVisibilityModel(newModel) + } + slots={{ + toolbar: () => ( + <> + <GridToolbarContainer> + <ShipmentTableToolbar + filters={filters} + onFilters={handleFilters} + countryOptions={Country_OPTIONS} + shippingMethodOptions={ShippingMethod_OPTIONS} + statusOptions={Status_OPTIONS} + dateError={dateError} + /> + + <GridToolbarQuickFilter /> + + <Stack + spacing={1} + flexGrow={1} + direction="row" + alignItems="center" + justifyContent="flex-end" + > + + <GridToolbarColumnsButton /> + </Stack> + </GridToolbarContainer> + + {canReset && ( + <ShipmentTableFiltersResult + filters={filters} + onFilters={handleFilters} + onResetFilters={handleResetFilters} + results={dataFiltered.length} + sx={{ p: 2.5, pt: 0 }} + shippingMethodOptions={ShippingMethod_OPTIONS} + statusOptions={Status_OPTIONS} + /> + )} + </> + ), + noRowsOverlay: () => <EmptyContent title="No Data" />, + noResultsOverlay: () => <EmptyContent title="No results found" />, + }} + slotProps={{ + columnsPanel: { + getTogglableColumns, + }, + }} + /> + </Box> + </Card> + </> + ); +} + +// ---------------------------------------------------------------------- + +function applyFilter({ + inputData, + filters, + dateError, + +}: { + inputData: Shipment[]; + filters: ShipmentTableFilters; + dateError: boolean; +}) { + const { country, shippingMethod, status, startDate, endDate } = filters; + + if (country.length) { + inputData = inputData.filter((shipment) => + country.includes(shipment.shippingCountry) + ); + } + + if (shippingMethod.length) { + inputData = inputData.filter((shipment) => + shippingMethod.includes(shipment.shippingMethodType) + ); + } + + if (status.length) { + inputData = inputData.filter((shipment) => + status.includes(shipment.shipmentStatus) + ); + } + + if (!dateError) { + if (startDate && endDate) { + inputData = inputData.filter((shipment) => isBetween(shipment.createdAt, startDate, endDate)); + } + } + + return inputData; +} diff --git a/src/shared/sections/shipping/general/shipments/shipment-table-filters-result.tsx b/src/shared/sections/shipping/general/shipments/shipment-table-filters-result.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5f71482821a6a30ce9ba25fe9b7111ce6ab6f6f9 --- /dev/null +++ b/src/shared/sections/shipping/general/shipments/shipment-table-filters-result.tsx @@ -0,0 +1,170 @@ +import { useCallback } from "react"; +import Box from "@mui/material/Box"; +import Chip from "@mui/material/Chip"; +import Paper from "@mui/material/Paper"; +import Button from "@mui/material/Button"; +import Stack, { StackProps } from "@mui/material/Stack"; +import Iconify from "@/shared/components/iconify"; +import { ShipmentTableFilters, ShipmentTableFilterValue } from "@/contexts/types/shipment"; + +// ---------------------------------------------------------------------- + +type Props = StackProps & { + filters: ShipmentTableFilters; + onFilters: (name: string, value: ShipmentTableFilterValue) => void; + // + onResetFilters: VoidFunction; + // + results: number; + shippingMethodOptions: { + value: string; + label: string; + }[]; + statusOptions: { + value: string; + label: string; + }[]; +}; + +export default function ShipmentTableFiltersResult({ + filters, + onFilters, + // + onResetFilters, + // + results, + shippingMethodOptions, + statusOptions, + ...other +}: Props) { + const handleRemoveCountry = useCallback( + (inputValue: string) => { + const newValue = filters.country.filter((item) => item !== inputValue); + + onFilters("country", newValue); + }, + [filters.country, onFilters] + ); + + const handleRemoveShippingMethod = useCallback( + (inputValue: string) => { + const newValue = filters.shippingMethod.filter((item) => item !== inputValue); + + onFilters("shippingMethod", newValue); + }, + [filters.shippingMethod, onFilters] + ); + + const handleRemoveStatus = useCallback( + (inputValue: string) => { + const newValue = filters.status.filter((item) => item !== inputValue); + + onFilters("status", newValue); + }, + [filters.status, onFilters] + ); + + return ( + <Stack spacing={1.5} {...other}> + <Box sx={{ typography: "body2" }}> + <strong>{results}</strong> + <Box component="span" sx={{ color: "text.secondary", ml: 0.25 }}> + results found + </Box> + </Box> + + <Stack + flexGrow={1} + spacing={1} + direction="row" + flexWrap="wrap" + alignItems="center" + > + {!!filters.country.length && ( + <Block label="Pays:"> + {filters.country.map((item) => ( + <Chip + key={item} + label={item} + size="small" + onDelete={() => handleRemoveCountry(item)} + /> + ))} + </Block> + )} + + {!!filters.shippingMethod.length && ( + <Block label="Méthode d'expédition:"> + {filters.shippingMethod.map((item) => { + const label = shippingMethodOptions.find((option) => option.value === item)?.label || item; + return ( + <Chip + key={item} + label={label} + size="small" + onDelete={() => handleRemoveShippingMethod(item)} + /> + ); + })} + </Block> + )} + + {!!filters.status.length && ( + <Block label="Status:"> + {filters.status.map((item) => { + const label = statusOptions.find((option) => option.value === item)?.label || item; + return ( + <Chip + key={item} + label={label} + size="small" + onDelete={() => handleRemoveStatus(item)} + /> + ); + })} + </Block> + )} + <Button + color="error" + onClick={onResetFilters} + startIcon={<Iconify icon="solar:trash-bin-trash-bold" />} + > + Clear + </Button> + </Stack> + </Stack> + ); +} + +// ---------------------------------------------------------------------- + +type BlockProps = StackProps & { + label: string; +}; + +function Block({ label, children, sx, ...other }: BlockProps) { + return ( + <Stack + component={Paper} + variant="outlined" + spacing={1} + direction="row" + sx={{ + p: 1, + borderRadius: 1, + overflow: "hidden", + borderStyle: "dashed", + ...sx, + }} + {...other} + > + <Box component="span" sx={{ typography: "subtitle2" }}> + {label} + </Box> + + <Stack spacing={1} direction="row" flexWrap="wrap"> + {children} + </Stack> + </Stack> + ); +} diff --git a/src/shared/sections/shipping/general/shipments/shipment-table-row.tsx b/src/shared/sections/shipping/general/shipments/shipment-table-row.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e6eac16c1c1e5c7605e464b3c699ec74a0a52fba --- /dev/null +++ b/src/shared/sections/shipping/general/shipments/shipment-table-row.tsx @@ -0,0 +1,111 @@ +import { GridCellParams } from "@mui/x-data-grid"; +import ListItemText from "@mui/material/ListItemText"; +import { fTime, fDate } from "@/utils/format-time"; +import Label from "@/shared/components/label"; +import { Button } from "@mui/material"; +import { IOrderColissimoStatus } from "@/shared/types/shipping"; +import { ShipmentStatus, shipmentStatusLabels } from "@/contexts/types/shipment"; +import { useEffect, useState } from "react"; +import { IClientItem } from "@/shared/types/user"; +import { useGetUserById } from "@/shared/api/user"; +import { ShippingMethodType, shippingMethodTypeLabels } from "@/contexts/types/Shipping"; + +// ---------------------------------------------------------------------- +const ShippingMethod_OPTIONS = Object.keys(ShippingMethodType).map((key) => ({ + value: ShippingMethodType[key as keyof typeof ShippingMethodType], + label: shippingMethodTypeLabels[ShippingMethodType[key as keyof typeof ShippingMethodType]], +})); + +const Status_OPTIONS = Object.keys(ShipmentStatus).map((key) => ({ + value: ShipmentStatus[key as keyof typeof ShipmentStatus], + label: shipmentStatusLabels[ShipmentStatus[key as keyof typeof ShipmentStatus]], +})); + +type ParamsProps = { + params: GridCellParams; +}; + +export function RenderCellID({ params }: ParamsProps) { + return <>{params.row.id}</>; +} + +export function RenderCellCreatedAt({ params }: ParamsProps) { + return ( + <ListItemText + primary={fDate(params.row.createdAt)} + secondary={fTime(params.row.createdAt)} + primaryTypographyProps={{ typography: "body2", noWrap: true }} + secondaryTypographyProps={{ + mt: 0.5, + component: "span", + typography: "caption", + }} + /> + ); +} + +export function RenderCellCustomer({ params }: ParamsProps) { + const { userData, userError } = useGetUserById(Number(params.row.clientId)); + const [clientData, setClientData] = useState<IClientItem | null>(null); + + useEffect(() => { + if (userData && userData.roles.name === "CLIENT") { + setClientData(userData as IClientItem); + } + }, [userData]); + return ( + <> + {clientData ? ( + <ListItemText + primary={clientData.firstName + ' ' + clientData.lastName} + secondary={clientData.email} + /> + ) : ( + <></> + )} + </> + ); +} + +export function RenderCellAdress({ params }: ParamsProps) { + return <>{params.row.shippingStreet + ` ` + params.row.shippingHome}</>; +} + +export function RenderCellCountry({ params }: ParamsProps) { + return <>{params.row.shippingCountry}</>; +} + +export function RenderCellShippingMethod({ params }: ParamsProps) { + const shippingMethodLabel = ShippingMethod_OPTIONS.find( + (option) => option.value === params.row.shippingMethodType + )?.label || params.row.shippingMethodType; + + return <>{shippingMethodLabel}</>; +} + +export function RenderCellStatus({ params }: ParamsProps) { + const statusLabel = Status_OPTIONS.find( + (option) => option.value === params.row.shipmentStatus + )?.label || params.row.shipmentStatus; + + return ( + <Label + variant="soft" + color={ + (params.row.shipmentStatus === ShipmentStatus.CANCELED && "error") || + (params.row.shipmentStatus === ShipmentStatus.WAITING_FOR_PAYMENT && "info") || + (params.row.shipmentStatus === ShipmentStatus.DELIVERED && "success") || + (params.row.shipmentStatus === ShipmentStatus.FAILED && "default") || + (params.row.shipmentStatus === ShipmentStatus.PENDING && "secondary") || + (params.row.shipmentStatus === ShipmentStatus.SHIPPED && "primary") || + "default" + } + > + {statusLabel} + </Label> + ); +} + +export function RenderCellTrackingNumber({ params }: ParamsProps) { + return <>{params.row.trackingNumber}</>; +} \ No newline at end of file diff --git a/src/shared/sections/shipping/general/shipments/shipment-table-toolbar.tsx b/src/shared/sections/shipping/general/shipments/shipment-table-toolbar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..114c2c0301134ad8f1f23c41d4d7de8b5a3e35c6 --- /dev/null +++ b/src/shared/sections/shipping/general/shipments/shipment-table-toolbar.tsx @@ -0,0 +1,259 @@ +import { useState, useCallback } from "react"; +import MenuItem from "@mui/material/MenuItem"; +import Checkbox from "@mui/material/Checkbox"; +import Box from "@mui/material/Box"; +import InputLabel from "@mui/material/InputLabel"; +import FormControl from "@mui/material/FormControl"; +import OutlinedInput from "@mui/material/OutlinedInput"; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import Select, { SelectChangeEvent } from "@mui/material/Select"; +import { formHelperTextClasses } from '@mui/material/FormHelperText'; +import { IOrderColissimoTableFilters, IOrderColissimoTableFilterValue } from "@/shared/types/shipping"; +import { IconButton, Menu, Typography } from '@mui/material'; +import { GridFilterAltIcon } from "@mui/x-data-grid"; +import { ShipmentTableFilters, ShipmentTableFilterValue } from "@/contexts/types/shipment"; + + +// ---------------------------------------------------------------------- + +type Props = { + filters: ShipmentTableFilters; + onFilters: (name: string, value: ShipmentTableFilterValue) => void; + dateError: boolean; + // + countryOptions: { + value: string; + label: string; + }[]; + shippingMethodOptions: { + value: string; + label: string; + }[]; + statusOptions: { + value: string; + label: string; + }[]; +}; + +export default function ShipmentTableToolbar({ + filters, + onFilters, + // + countryOptions, + shippingMethodOptions, + statusOptions, + dateError +}: Props) { + + const [country, setCountry] = useState<string[]>(filters.country); + + const [shippingMethod, setShippingMethod] = useState<string[]>(filters.shippingMethod); + + const [status, setStatus] = useState<string[]>(filters.status); + + const handleChangeCountry = useCallback( + (event: SelectChangeEvent<string[]>) => { + const { + target: { value }, + } = event; + setCountry(typeof value === "string" ? value.split(",") : value); + }, + [] + ); + + const handleChangeShippingMethod = useCallback( + (event: SelectChangeEvent<string[]>) => { + const { + target: { value }, + } = event; + setShippingMethod(typeof value === "string" ? value.split(",") : value); + }, + [] + ); + + const handleChangeStatus = useCallback( + (event: SelectChangeEvent<string[]>) => { + const { + target: { value }, + } = event; + setStatus(typeof value === "string" ? value.split(",") : value); + }, + [] + ); + + const handleCloseCountry = useCallback(() => { + onFilters("country", country); + }, [onFilters, country]); + + const handleCloseShippingMethod = useCallback(() => { + onFilters("shippingMethod", shippingMethod); + }, [onFilters, shippingMethod]); + + const handleCloseStatus = useCallback(() => { + onFilters("status", status); + }, [onFilters, status]); + + const handleFilterStartDate = useCallback( + (newValue: Date | null) => { + onFilters('startDate', newValue); + }, + [onFilters] + ); + + const handleFilterEndDate = useCallback( + (newValue: Date | null) => { + onFilters('endDate', newValue); + }, + [onFilters] + ); + + const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null); + + const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + <> + <Box> + <Box + sx={{ + display: 'grid', + gridTemplateColumns: 'repeat(5, 1fr)', + gap: 2, + width: '100%', + }} + > + <FormControl + sx={{ + flexShrink: 0, + width: { xs: 1, md: 220 }, + }} + > + <InputLabel>Pays</InputLabel> + <Select + multiple + value={country} + onChange={handleChangeCountry} + input={<OutlinedInput label="Pays" />} + renderValue={(selected) => selected.map((value) => value).join(", ")} + onClose={handleCloseCountry} + sx={{ textTransform: "capitalize" }} + > + {countryOptions.map((option) => ( + <MenuItem key={option.value} value={option.value}> + <Checkbox + disableRipple + size="small" + checked={country.includes(option.value)} + /> + {option.label} + </MenuItem> + ))} + </Select> + </FormControl> + <FormControl + sx={{ + flexShrink: 0, + width: { xs: 1, md: 220 }, + }} + > + <InputLabel>Méthode de livraison</InputLabel> + <Select + multiple + value={shippingMethod} + onChange={handleChangeShippingMethod} + input={<OutlinedInput label="Méthode de livraison" />} + renderValue={(selected) => + selected + .map((value) => shippingMethodOptions.find((option) => option.value === value)?.label) + .join(", ") + } + onClose={handleCloseShippingMethod} + sx={{ textTransform: "capitalize" }} + > + {shippingMethodOptions.map((option) => ( + <MenuItem key={option.value} value={option.value}> + <Checkbox + disableRipple + size="small" + checked={shippingMethod.includes(option.value)} + /> + {option.label} + </MenuItem> + ))} + </Select> + </FormControl> + <FormControl + sx={{ + flexShrink: 0, + width: { xs: 1, md: 200 }, + }} + > + <InputLabel>Status</InputLabel> + <Select + multiple + value={status} + onChange={handleChangeStatus} + input={<OutlinedInput label="Status" />} + renderValue={(selected) => + selected + .map((value) => statusOptions.find((option) => option.value === value)?.label) + .join(", ") + } + onClose={handleCloseStatus} + sx={{ textTransform: "capitalize" }} + > + {statusOptions.map((option) => ( + <MenuItem key={option.value} value={option.value}> + <Checkbox + disableRipple + size="small" + checked={status.includes(option.value)} + /> + {option.label} + </MenuItem> + ))} + </Select> + </FormControl> + <DatePicker + label="Date début" + value={filters.startDate} + onChange={handleFilterStartDate} + slotProps={{ + textField: { + fullWidth: true, + }, + }} + sx={{ + maxWidth: { md: 200 }, + }} + /> + <DatePicker + label="Date fin" + value={filters.endDate} + onChange={handleFilterEndDate} + slotProps={{ + textField: { + fullWidth: true, + error: dateError, + helperText: dateError && 'La date de fin doit être ultérieure à la date de début.', + }, + }} + sx={{ + maxWidth: { md: 200 }, + [`& .${formHelperTextClasses.root}`]: { + position: { md: 'absolute' }, + bottom: { md: -40 }, + }, + }} + /> + </Box> + </Box> + </> + ); +} diff --git a/src/shared/sections/supplier/All-OrderSaleSupplier/Order-table-row.tsx b/src/shared/sections/supplier/All-OrderSaleSupplier/Order-table-row.tsx index f7ea2cbac9cdd13af8e1cc912873be638414d75c..8fe0d8291cc6bbfd79894d9893692b113296dd40 100644 --- a/src/shared/sections/supplier/All-OrderSaleSupplier/Order-table-row.tsx +++ b/src/shared/sections/supplier/All-OrderSaleSupplier/Order-table-row.tsx @@ -204,6 +204,7 @@ export default function InvoiceTableRow({ .then((response) => { setConfirmLoading(false); setOpenConfirmDialog(false); + onDeleteRow(); enqueueSnackbar("Commande de vente confirmée avec succès !", { variant: "success" }); onEditRow(); })