feat: add sale flow with Boopos Cloud
This commit is contained in:
parent
0a14fe8882
commit
9cb4302fc9
19 changed files with 449 additions and 398 deletions
|
|
@ -1,54 +0,0 @@
|
|||
"use client"
|
||||
import { Box, Card, CardContent } from "@mui/material"
|
||||
import Grid from "@mui/material/Grid2"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import Button from "@atoms/Button"
|
||||
import Label from "@atoms/Label"
|
||||
import Title from "@atoms/Title"
|
||||
|
||||
interface AskForAffiliationProps {
|
||||
onAffiliationSelect: (isAffiliated: boolean) => void
|
||||
}
|
||||
|
||||
const AskForAffiliation: React.FC<AskForAffiliationProps> = ({ onAffiliationSelect }) => {
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleYesClick = () => {
|
||||
onAffiliationSelect(true)
|
||||
}
|
||||
|
||||
const handleNoClick = () => {
|
||||
onAffiliationSelect(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card sx={{ width: "100%", maxWidth: "100%" }}>
|
||||
<CardContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<Title text={t("lbl_information_required")} size="h3" />
|
||||
<Label htmlFor="loyalty" text= {t("msj_are_you_registered_as_affiliate")} />
|
||||
<Box sx={{ mt: 3, display: "flex", gap: 3 }}>
|
||||
<Grid size={{ xs: 6 }} sx={{ minWidth: "200px" }}>
|
||||
<Button label={t("lbl_yes")} onClick={handleYesClick} variant="primary" className="h-16 w-full" />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6 }} sx={{ minWidth: "200px" }}>
|
||||
<Button label={t('lbl_no')} onClick={handleNoClick} variant="primary" className="h-16 w-full" />
|
||||
</Grid>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default AskForAffiliation
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
"use client"
|
||||
import { Box, Card, CardContent } from "@mui/material"
|
||||
import Grid from "@mui/material/Grid2"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import Button from "@atoms/Button"
|
||||
import Input from "@atoms/Input"
|
||||
import Title from "@atoms/Title"
|
||||
|
||||
interface IdentityDocumentNumberProps {
|
||||
documentType: string
|
||||
onSubmit: (documentNumber: string) => void
|
||||
}
|
||||
|
||||
const IdentityDocumentNumber: React.FC<IdentityDocumentNumberProps> = ({ documentType, onSubmit }) => {
|
||||
const [inputValue, setInputValue] = useState<string>("")
|
||||
const [keyboardMode, setKeyboardMode] = useState<"numeric" | "alpha">("numeric")
|
||||
const [isAlphaEnabled, setIsAlphaEnabled] = useState<boolean>(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const alphaDocuments = ["PASAPORTE", "CURP", "RFC", "NIE", "RUT"]
|
||||
setIsAlphaEnabled(alphaDocuments.includes(documentType.toUpperCase()))
|
||||
setKeyboardMode(isAlphaEnabled ? "alpha" : "numeric")
|
||||
}, [documentType, isAlphaEnabled])
|
||||
|
||||
const handleKeyPress = (key: string) => {
|
||||
setInputValue((prev) => prev + key)
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
setInputValue((prev) => prev.slice(0, -1))
|
||||
}
|
||||
|
||||
const renderKeyboard = () => {
|
||||
if (keyboardMode === "numeric") {
|
||||
return (
|
||||
<>
|
||||
{["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"].map((num) => (
|
||||
<Grid key={num} size={{ xs: 4 }}>
|
||||
<Button label={num} onClick={() => handleKeyPress(num)} variant="primary" className="h-16 w-full" />
|
||||
</Grid>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{"ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((letter) => (
|
||||
<Grid key={letter} size={{ xs: 2 }}>
|
||||
<Button label={letter} onClick={() => handleKeyPress(letter)} variant="primary" className="h-16 w-full" />
|
||||
</Grid>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card sx={{ width: "100%", maxWidth: "100%" }}>
|
||||
<CardContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
{/* Título */}
|
||||
<Title text={`${t('msj_enter_number_of')} ${documentType}`} size="h4" />
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder= {t('msj_enter_document_number')}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2} justifyContent="center" alignItems="center" sx={{ mt: 3 }}>
|
||||
<Grid size={{ xs: 4 }}>
|
||||
<Button label= {t('lbl_delete')} onClick={handleDelete} variant="danger" className="h-16 w-full" />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 4 }}>
|
||||
<Button label={t('lbl_cancel')} onClick={() => setInputValue("")} variant="danger" className="h-16 w-full" />
|
||||
</Grid>
|
||||
{renderKeyboard()}
|
||||
<Grid size={{ xs: 4 }}>
|
||||
<Button label={t('lbl_enter')} onClick={() => onSubmit(inputValue)} variant="success" className="h-16 w-full" />
|
||||
</Grid>
|
||||
{isAlphaEnabled && (
|
||||
<Grid size={{ xs: 4 }}>
|
||||
<Button
|
||||
label={`${t('msj_change_to')} ${keyboardMode === "numeric" ? t('lbl_alphabetical') : t('lbl_numeric')}`}
|
||||
onClick={() => setKeyboardMode(keyboardMode === "numeric" ? "alpha" : "numeric")}
|
||||
variant="info"
|
||||
className="h-16 w-full"
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default IdentityDocumentNumber
|
||||
|
|
@ -1,55 +1,207 @@
|
|||
"use client";
|
||||
|
||||
import { Box, Container, Paper } from '@mui/material';
|
||||
import { Box, Container, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Paper } from '@mui/material';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
import React from 'react';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@atoms/Button';
|
||||
import { storeNumberAtom, storePosAtom } from '@atoms/states/StoreAtom';
|
||||
import { EndSaleOutputInterface } from '@interfaces/OutputInterfaces/Sale/EndSaleOutputInterface';
|
||||
import { GetSaleOutputInterface } from '@interfaces/OutputInterfaces/Sale/GetSaleOutputInterface';
|
||||
import { CustomerInterface } from '@interfaces/ResponseInterfaces/Customer/CustomerResponseInterface';
|
||||
import { EndSaleResponseInterface } from '@interfaces/ResponseInterfaces/Sale/EndSaleResponseInterface';
|
||||
import { GetSaleResponseInterface, SaleTransactionObjectInterface } from '@interfaces/ResponseInterfaces/Sale/GetSaleResponseInterface';
|
||||
import { StartSaleResponseInterface } from '@interfaces/ResponseInterfaces/Sale/StartSaleResponseInterface';
|
||||
import ClientSearch from '@organisms/Checkout/ClientSearch';
|
||||
import ControlPanel from '@organisms/Checkout/ControlPanel';
|
||||
import ProductTable from '@organisms/Checkout/ProductTable';
|
||||
import EndSale from '@package/config/api/Sale/Transaction/EndSale/page';
|
||||
import GetSale from '@package/config/api/Sale/Transaction/GetSale/page';
|
||||
import { getUser } from '@utils/Utilities';
|
||||
|
||||
const products = [
|
||||
{ id: 1, amount: 2, name: "Product X", unitValue: 2500, tax: 110, discount: 0, total: 5220 },
|
||||
{ id: 2, amount: 1, name: "Product Y", unitValue: 3500, tax: 230, discount: 0, total: 3730 },
|
||||
{ id: 3, amount: 3, name: "Product Z", unitValue: 1000, tax: 100, discount: 0, total: 3300 },
|
||||
];
|
||||
interface MainLayoutProps {
|
||||
onEndTransaction: () => void; // Callback para notificar cuando la transacción termine
|
||||
}
|
||||
|
||||
const MainLayout: React.FC<MainLayoutProps> = ({ onEndTransaction }) => {
|
||||
const [transaction, setTransaction] = useState<StartSaleResponseInterface | null>(null);
|
||||
const [client, setClient] = useState<CustomerInterface | null>(null);
|
||||
const [productsList, setProductsList] = useState<SaleTransactionObjectInterface | null>(null);
|
||||
const [endSaleResponse, setEndSaleResponse] = useState<EndSaleResponseInterface | null>(null);
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
|
||||
const [idUser] = useState(getUser() && getUser().id ? getUser().id : '');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [storeNumber] = useAtom(storeNumberAtom);
|
||||
const [storePos] = useAtom(storePosAtom);
|
||||
|
||||
const handleTransactionComplete = (transaction: StartSaleResponseInterface, client: CustomerInterface) => {
|
||||
setTransaction(transaction);
|
||||
setClient(client);
|
||||
};
|
||||
|
||||
const fetchSaleDetails = useCallback(async () => {
|
||||
if (!transaction || !client) return;
|
||||
|
||||
try {
|
||||
const saleRequest: GetSaleOutputInterface = {
|
||||
idTransaction: transaction.object?.id,
|
||||
storeNumber: storeNumber,
|
||||
idSubsidiary: client.idSubsidiary,
|
||||
idUser: client.idUser,
|
||||
pos: storePos,
|
||||
};
|
||||
|
||||
const saleResponse = await GetSale(saleRequest) as GetSaleResponseInterface;
|
||||
|
||||
if (saleResponse && saleResponse.correct) {
|
||||
setProductsList(saleResponse.object);
|
||||
} else {
|
||||
console.error("Error fetching sale details: ", saleResponse?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during GetSale API call", error);
|
||||
}
|
||||
}, [client, storeNumber, storePos, transaction]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSaleDetails();
|
||||
}, [fetchSaleDetails, transaction]);
|
||||
|
||||
const handleProductAdded = () => {
|
||||
fetchSaleDetails();
|
||||
};
|
||||
|
||||
const MainLayout: React.FC = () => {
|
||||
const handleVoidOrder = () => console.log("Void Order");
|
||||
const handleVoidLast = () => console.log("Void Last");
|
||||
const handleResume = () => console.log("Resume");
|
||||
const handleSearchPLU = (plu: string) => console.log("Search PLU:", plu);
|
||||
|
||||
const subtotal = products.reduce((acc, product) => acc + product.unitValue * product.amount, 0);
|
||||
const discount = products.reduce((acc, product) => acc + product.discount, 0);
|
||||
const tax = products.reduce((acc, product) => acc + product.tax, 0);
|
||||
const total = products.reduce((acc, product) => acc + product.total, 0);
|
||||
const handleEndSale = async () => {
|
||||
if (!transaction || !client || !productsList) return;
|
||||
|
||||
if (!transaction.object?.id) {
|
||||
console.error("Transaction ID is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (client.idSubsidiary === undefined) {
|
||||
console.error("Client subsidiary ID is missing");
|
||||
return;
|
||||
}
|
||||
const storeAddress = process.env.NEXT_PUBLIC_STORE_ADDRESS || '';
|
||||
const codeMunPos = process.env.NEXT_PUBLIC_STORE_CODE_CITY || '';
|
||||
const nameMunPos = process.env.NEXT_PUBLIC_STORE_CITY || '';
|
||||
const codeDepPos = process.env.NEXT_PUBLIC_STORE_CODE_DEPARTMENT || '';
|
||||
const nameDepPos = process.env.NEXT_PUBLIC_STORE_DEPARTMENT || '';
|
||||
|
||||
if (!storeAddress || !codeMunPos || !nameMunPos || !codeDepPos || !nameDepPos) {
|
||||
console.error("Missing store address information from environment variables");
|
||||
return;
|
||||
}
|
||||
const endSaleRequest: EndSaleOutputInterface = {
|
||||
idTransaction: transaction.object.id,
|
||||
storeNumber: storeNumber,
|
||||
idSubsidiary: client.idSubsidiary,
|
||||
idUser: idUser,
|
||||
localDate: new Date().toISOString(),
|
||||
pos: storePos,
|
||||
comment: 'Test End Sale',
|
||||
payment: [
|
||||
{
|
||||
type: "CASH",
|
||||
approvalCode: "0001",
|
||||
receiptNumber: "10001",
|
||||
value: productsList.totalWithPromotion+ productsList.totalTaxes,
|
||||
cardNumber: "0032",
|
||||
cardType: "DEBIT",
|
||||
comment: "Test"
|
||||
}
|
||||
],
|
||||
infoPos: {
|
||||
addressPos: storeAddress,
|
||||
codeMunPos: codeMunPos,
|
||||
nameMunPos: nameMunPos,
|
||||
codeDepPos: codeDepPos,
|
||||
nameDepPos: nameDepPos,
|
||||
pos: storePos
|
||||
}
|
||||
};
|
||||
console.log("End Sale Request: ", endSaleRequest);
|
||||
try {
|
||||
const endSaleResponse = await EndSale(endSaleRequest) as EndSaleResponseInterface;
|
||||
|
||||
console.log("End Sale Response: ", endSaleResponse);
|
||||
if (endSaleResponse && endSaleResponse.correct) {
|
||||
setEndSaleResponse(endSaleResponse);
|
||||
setOpenDialog(true);
|
||||
} else {
|
||||
console.error("Error ending sale:", endSaleResponse?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during EndSale API call", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
setOpenDialog(false);
|
||||
onEndTransaction();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', bgcolor: 'background.default', boxShadow: '4' }}>
|
||||
<Container maxWidth={false} sx={{ flexGrow: 1, display: 'flex', py: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{xs:12, md:8}}>
|
||||
<Paper elevation={3} sx={{ height: '100%', p: 2}}>
|
||||
<ProductTable products={products} />
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<Paper elevation={3} sx={{ height: '100%', p: 2 }}>
|
||||
<ProductTable products={productsList} />
|
||||
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid size={{xs:12, md:4}}>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Paper elevation={3} sx={{ height: '100%', p: 2 }}>
|
||||
<ControlPanel
|
||||
onVoidOrder={handleVoidOrder}
|
||||
onVoidLast={handleVoidLast}
|
||||
onResume={handleResume}
|
||||
onSearchPLU={handleSearchPLU}
|
||||
subtotal={subtotal}
|
||||
discount={discount}
|
||||
tax={tax}
|
||||
total={total}
|
||||
/>
|
||||
{!transaction && !client ? (
|
||||
<ClientSearch onTransactionComplete={handleTransactionComplete} />
|
||||
) : (
|
||||
<ControlPanel
|
||||
onVoidOrder={handleVoidOrder}
|
||||
onVoidLast={handleVoidLast}
|
||||
onResume={handleResume}
|
||||
transaction={transaction!}
|
||||
client={client!}
|
||||
onProductAdded={handleProductAdded}
|
||||
/>
|
||||
|
||||
)}
|
||||
{productsList && productsList.list.length > 0 && (
|
||||
<Button
|
||||
label={t("lbl_resume")}
|
||||
variant="primary"
|
||||
onClick={handleEndSale}
|
||||
className="full-width-button"
|
||||
disabled={!productsList || productsList.list.length === 0}
|
||||
/>
|
||||
)}
|
||||
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
|
||||
<Dialog open={openDialog} onClose={handleDialogClose}>
|
||||
<DialogTitle>{t("transaction_complete")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{endSaleResponse?.object?.description || t("transaction_successful")}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDialogClose} label="Aceptar" />
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainLayout;
|
||||
export default MainLayout;
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
"use client"
|
||||
import { Box, Card, CardContent } from "@mui/material"
|
||||
import Grid from "@mui/material/Grid2"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import Button from "@atoms/Button"
|
||||
import Label from "@atoms/Label"
|
||||
import Title from "@atoms/Title"
|
||||
|
||||
interface TypesDocumentProps {
|
||||
typesDocument: string[]
|
||||
onDocumentSelect: (documentType: string) => void
|
||||
}
|
||||
|
||||
const TypeIdentityDocument: React.FC<TypesDocumentProps> = ({ typesDocument, onDocumentSelect }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleDocumentClick = (documentType: string) => {
|
||||
onDocumentSelect(documentType)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card sx={{ width: "100%", maxWidth: "100%" }}>
|
||||
<CardContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<Title text= {t("lbl_information_required")} size="h4" />
|
||||
<Label htmlFor="loyalty" text={t('msj_select_type_identity_document')} />
|
||||
|
||||
<Grid container spacing={1} justifyContent="center" sx={{ mt: 2 }}>
|
||||
{typesDocument.map((documentType, index) => (
|
||||
<Grid key={index} size={{ xs: 4, md: 6 }} sx={{ minWidth: "100px" }}>
|
||||
<Button
|
||||
label={documentType}
|
||||
onClick={() => handleDocumentClick(documentType)}
|
||||
variant="primary"
|
||||
className="h-16 w-full"
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default TypeIdentityDocument
|
||||
Loading…
Add table
Add a link
Reference in a new issue