feat: add sale flow with Boopos Cloud

This commit is contained in:
Luis Felipe Cadavid 2024-10-01 13:45:34 -05:00
parent 0a14fe8882
commit 9cb4302fc9
19 changed files with 449 additions and 398 deletions

View file

@ -0,0 +1,11 @@
import { atom } from 'jotai';
export const storeNumberAtom = atom<string>(process.env.NEXT_PUBLIC_STORE_NUMBER || '');
export const storePosAtom = atom<string>(process.env.NEXT_PUBLIC_POS || '');
export const storeInfoPosAtom = atom({
addressPos: process.env.NEXT_PUBLIC_STORE_ADDRESS || '',
codeMunPos: process.env.NEXT_PUBLIC_STORE_CODE_CITY || '',
nameMunPos: process.env.NEXT_PUBLIC_STORE_CITY || '',
codeDepPos: process.env.NEXT_PUBLIC_STORE_CODE_DEPARTMENT || '',
nameDepPos: process.env.NEXT_PUBLIC_STORE_DEPARTMENT || '',
});

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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

View file

@ -5,15 +5,14 @@ import Grid from '@mui/material/Grid2';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ItemObject } from '@interfaces/ResponseInterfaces/Sale/AddProductsResponseInterface';
interface SaleSummaryProps {
subtotal: number;
discount: number;
tax: number;
total: number;
product: ItemObject;
}
const SaleSummary: React.FC<SaleSummaryProps> = ({ subtotal, discount, tax, total }) => {
const SaleSummary: React.FC<SaleSummaryProps> = ({ product }) => {
const { t } = useTranslation()
return (
@ -30,15 +29,13 @@ const SaleSummary: React.FC<SaleSummaryProps> = ({ subtotal, discount, tax, tota
<TableHead>
<TableRow>
<TableCell sx={{ color: 'primary', borderColor: 'black', fontWeight: 'bold', width: '30%' }}>{t("tle_product")}</TableCell>
<TableCell sx={{ color: 'primary', borderColor: 'black', fontWeight: 'bold', width: '50%' }}>{t("tle_description")}</TableCell>
<TableCell sx={{ color: 'primary', borderColor: 'black', fontWeight: 'bold', width: '20%', textAlign: 'right' }}>{t("tle_amount")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell sx={{ color: 'black', borderColor: 'black' }}>Product R</TableCell>
<TableCell sx={{ color: 'black', borderColor: 'black' }}>Producto para r uso</TableCell>
<TableCell sx={{ color: 'black', borderColor: 'black', textAlign: 'right' }}>3</TableCell>
<TableCell sx={{ color: 'black', borderColor: 'black' }}>{product.name}</TableCell>
<TableCell sx={{ color: 'black', borderColor: 'black', textAlign: 'right' }}>1</TableCell>
</TableRow>
</TableBody>
</Table>
@ -49,31 +46,16 @@ const SaleSummary: React.FC<SaleSummaryProps> = ({ subtotal, discount, tax, tota
<Typography variant="body2" align="right">{t("lbl_subtotal")}:</Typography>
</Grid>
<Grid size={{xs:5}}>
<Typography variant="body2" align="left">$ 1.400</Typography>
</Grid>
</Grid>
<Grid container>
<Grid size={{xs:7}} sx={{ pr: 10 }}>
<Typography variant="body2" align="right">{t("lbl_discount")}:</Typography>
</Grid>
<Grid size={{xs:5}}>
<Typography variant="body2" align="left">$ 12</Typography>
<Typography variant="body2" align="left">{product.regularUnitPrice}</Typography>
</Grid>
</Grid>
<Grid container>
<Grid size={{xs:7}} sx={{ pr: 5.5 }}>
<Typography variant="body2" align="right">{t("lbl_tax")}:</Typography>
</Grid>
<Grid size={{xs:5}}>
<Typography variant="body2" align="left">$ 123</Typography>
</Grid>
</Grid>
<Grid container>
<Grid size={{xs:7}} sx={{ pr: 5.5 }}>
<Typography variant="body2" align="right">{t("lbl_total")}:</Typography>
</Grid>
<Grid size={{xs:5}}>
<Typography variant="body2" align="left">$ 1.511</Typography>
<Typography variant="body2" align="left">{product.totalTaxes}</Typography>
</Grid>
</Grid>
</Box>
@ -82,7 +64,7 @@ const SaleSummary: React.FC<SaleSummaryProps> = ({ subtotal, discount, tax, tota
<Box sx={{ p: 3, boxShadow: '2' }}>
<Typography variant="h6" sx={{ color: 'primary', display: 'flex', justifyContent: 'space-between' }}>
<span>{t("tle_total")}:</span>
<span>$ 12.250</span>
<span>{product.extendedPrice}</span>
</Typography>
</Box>
</Box>

View file

@ -6,10 +6,10 @@ import Typography from '@mui/material/Typography';
import { useAtom } from 'jotai';
import React from 'react';
import { useTranslation } from 'react-i18next';
import UserAtom from '@atoms/states/UserAtom';
import CustomerAtom from '@atoms/states/CustomerAtom';
const UserHeader: React.FC = () => {
const [user] = useAtom(UserAtom);
const CustomerHeader: React.FC = () => {
const [customer] = useAtom(CustomerAtom);
const { t } = useTranslation()
@ -23,19 +23,19 @@ const UserHeader: React.FC = () => {
>
<Grid2>
<Typography variant="h6" className="font-bold">
{user ? `${t("lbl_name_customer")}: ${user.name} ${user.lastName}` : `${t("lbl_name_customer")}: Anónimo`}
{customer ? `${t("lbl_name_customer")}: ${customer.name} ${customer.firstSurname}` : `${t("lbl_name_customer")}: Anónimo`}
</Typography>
</Grid2>
<Grid2>
<Typography variant="body2" color="textSecondary" className="text-white">
{user ? `${t("lbl_dni_customer")}: ${user.idTypeUser}` : `${t("lbl_dni_customer")}: 222222222222`}
{customer ? `${t("lbl_dni_customer")}: ${customer.dni}` : `${t("lbl_dni_customer")}: 222222222222`}
</Typography>
</Grid2>
<Grid2>
<Typography variant="body2" className="text-white">
{user ? `${t("lbl_email_customer")}: ${user.email}` : `${t("lbl_email_customer")}: generic@example.com`}
{customer ? `${t("lbl_email_customer")}: ${customer.mail}` : `${t("lbl_email_customer")}: generic@example.com`}
</Typography>
</Grid2>
</Grid2>
@ -43,4 +43,4 @@ const UserHeader: React.FC = () => {
);
};
export default UserHeader;
export default CustomerHeader;

View file

@ -0,0 +1,310 @@
'use client';
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Paper, Typography } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { useAtom } from 'jotai';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import Button from '@atoms/Button';
import CustomLoading from '@atoms/CustomLoading';
import Icon from '@atoms/Icon';
import Input from '@atoms/Input';
import Label from '@atoms/Label';
import { storeNumberAtom, storePosAtom } from '@atoms/states/StoreAtom';
import Title from '@atoms/Title';
import { Tooltip } from '@atoms/Tooltip';
import { useClientSearchLogic } from '@hooks/useClientSearchLogic';
import { CustomerInterface } from '@interfaces/ResponseInterfaces/Customer/CustomerResponseInterface';
import { CustomerDniTypeInterface } from '@interfaces/ResponseInterfaces/Customer/DNITypeResponseInterface';
import { StartSaleResponseInterface } from '@interfaces/ResponseInterfaces/Sale/StartSaleResponseInterface';
import StartSale from '@package/config/api/Sale/Transaction/StartSale/page';
import { getUser } from '@utils/Utilities';
interface ClientSearchProps {
onTransactionComplete: (transaction: StartSaleResponseInterface, client: CustomerInterface) => void;
}
const ClientSearch: React.FC<ClientSearchProps> = ({ onTransactionComplete }) => {
const {
idTypes,
loading,
error,
result,
formData,
handleInputChange,
handleSubmitForm,
isAffiliated,
customer,
} = useClientSearchLogic();
const { t } = useTranslation();
const [openDialog, setOpenDialog] = useState(false);
const [idUser] = useState(getUser() && getUser().id ? getUser().id : '');
const [storeNumber] = useAtom(storeNumberAtom);
const [storePos] = useAtom(storePosAtom);
const handleOpenDialog = () => {
setOpenDialog(true);
};
const handleCloseDialog = () => {
setOpenDialog(false);
};
const handleUseGenericCustomer = async () => {
const genericCustomer = {
id: '',
idCompany: 'default-company',
companyName: 'default-company-name',
idSubsidiary: 3,
nameSubsidiary: 'default-subsidiary',
externalClientId: null,
name: 'Cliente',
secondName: '',
firstSurname: 'Generico',
secondSurname: '',
idTypeCustomer: '12',
nameTypeCustomer: 'Tarjeta de identidad',
dni: '222222222222',
idGender: '',
nameGender: '',
idMaritalStatus: null,
nameMaritalStatus: null,
address: '',
idCity: null,
nameCity: null,
idCountry: null,
nameCountry: null,
mail: '',
phone: '',
idUser: '',
nameUser: '',
status: true,
birthdate: null,
group: [],
NameSurname: '',
SurnameName: ''
};
const saleData = {
storeNumber: storeNumber,
idSubsidiary: genericCustomer.idSubsidiary,
idClient: genericCustomer.id,
clientDni: genericCustomer.dni,
idType: genericCustomer.idTypeCustomer,
idGroup: '',
invoiceName: genericCustomer.name,
invoiceTypeNit: genericCustomer.idTypeCustomer,
invoiceNit: genericCustomer.dni,
invoiceDirection: genericCustomer.address,
invoiceCellPhone: genericCustomer.phone,
invoiceEmail: genericCustomer.mail,
idUser: idUser,
localDate: new Date().toISOString(),
pos: storePos,
};
const saleResponse = await StartSale(saleData) as StartSaleResponseInterface;
if (saleResponse && saleResponse.correct) {
if (customer) {
onTransactionComplete(saleResponse, customer);
} else {
console.error("Customer is null");
}
} else {
console.error(saleResponse.message);
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
handleSubmitForm();
};
const handleContinue = async () => {
if (customer) {
const saleData = {
storeNumber: storeNumber,
idSubsidiary: customer.idSubsidiary || 0,
idClient: customer.id,
clientDni: customer.dni,
idType: customer.idTypeCustomer,
idGroup: '',
invoiceName: customer.name,
invoiceTypeNit: customer.idTypeCustomer,
invoiceNit: customer.dni,
invoiceDirection: customer.address,
invoiceCellPhone: customer.phone,
invoiceEmail: customer.mail,
idUser: idUser,
localDate: new Date().toISOString(),
pos: storePos,
};
console.log("VALOR DE SALEDATA",saleData);
const saleResponse = await StartSale(saleData) as StartSaleResponseInterface;
console.log("VALOR DE SALESRESPONSE",saleResponse);
if (saleResponse && saleResponse.correct) {
onTransactionComplete(saleResponse, customer);
} else {
console.error(saleResponse.message);
}
} else {
handleOpenDialog();
}
};
return (
<Paper elevation={3} className="p-6 max-w-2xl mx-auto">
<Title text={t("lbl_client_search")} size="h2" />
<form onSubmit={handleSubmit} className="space-y-4 mt-4">
<Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 6 }}>
<Label htmlFor="idType" text={t("lbl_id_type")} />
<select
name="idType"
value={formData.idType}
onChange={handleInputChange}
className="w-full p-2 border rounded"
disabled={loading}
>
<option value="">{t("lbl_selectid_type")}</option>
{idTypes.length > 0 ? (
idTypes.map((type: CustomerDniTypeInterface) => (
<option key={type.id} value={type.id}>
{type.name}
</option>
))
) : (
<option disabled>{t("msj_loading_id_types")}</option>
)}
</select>
</Grid>
<Grid size={{ xs: 12 }}>
<Label htmlFor="idNumber" text={t("lbl_id_number")} />
<Input
type="text"
name="idNumber"
placeholder={t("msj_enter_id_number")}
value={formData.idNumber}
onChange={handleInputChange}
disabled={loading}
/>
</Grid>
</Grid>
<div className="flex justify-end">
<Tooltip explainer={t("mjs_search_for_client")} withArrow>
<Button
label={t("lbl_search")}
variant="primary"
type="submit"
disabled={loading}
startIcon={<Icon name="Search" />}
/>
</Tooltip>
</div>
</form>
{loading && (
<div className="flex justify-center mt-4">
<CustomLoading />
</div>
)}
{error && (
<Typography color="error" className="mt-4">
{error}
</Typography>
)}
{result.length === 0 && !isAffiliated && (
<Paper elevation={2} className="mt-6 p-4">
<Title text={t("msj_no_affiliate")} size="h3" />
<Typography variant="body1" className="mt-2">
{t("msj_choose_option_for_non_affiliate")}
</Typography>
<div className="flex justify-center gap-4 mt-4">
<Button
label={t("lbl_use_generic_customer")}
variant="primary"
onClick={handleOpenDialog}
/>
<Button
label={t("lbl_create_new_customer")}
variant="primary"
onClick={() => {
}}
/>
</div>
</Paper>
)}
{result.length > 0 && (
<Paper elevation={2} className="mt-6 p-4">
<Title text={t("lbl_search_result")} size="h3" />
{result.map((customer) => (
<Grid container spacing={2} className="mt-2" key={customer.id}>
<Grid size={{ xs: 6 }}>
<Typography variant="body1">
<strong>{t("lbl_name")}:</strong> {customer.name}
</Typography>
</Grid>
<Grid size={{ xs: 6 }}>
<Typography variant="body1">
<strong>{t("lbl_last_name")}:</strong> {customer.firstSurname}
</Typography>
</Grid>
<Grid size={{ xs: 6 }}>
<Typography variant="body1">
<strong>{t("lbl_email")}:</strong> {customer.mail}
</Typography>
</Grid>
<Grid size={{ xs: 6 }}>
<Typography variant="body1">
<strong>{t("lbl_phone")}:</strong> {customer.phone}
</Typography>
</Grid>
<Grid size={{ xs: 12 }}>
<Typography variant="body1">
<strong>{t("lbl_address")}:</strong> {customer.address}
</Typography>
</Grid>
</Grid>
))}
<div className="flex justify-end mt-4">
<Button
label={t("lbl_continue")}
variant="primary"
onClick={handleContinue}
/>
</div>
</Paper>
)}
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="xs" fullWidth>
<DialogTitle sx={{ textAlign: 'center' }}>
{t("msj_confirm_generic_customer")}
</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
<DialogContentText>
{t("msj_are_you_sure_generic_customer")}
</DialogContentText>
</DialogContent>
<DialogActions sx={{ justifyContent: 'center' }}>
<Button onClick={handleCloseDialog} label={t("lbl_cancel")} />
<Button
onClick={handleUseGenericCustomer}
label={t("lbl_confirm")}
variant="primary"
/>
</DialogActions>
</Dialog>
</Paper>
);
};
export default ClientSearch;

View file

@ -1,32 +1,78 @@
"use client";
import Grid2 from '@mui/material/Grid2';
import React from 'react';
import { useAtom } from 'jotai';
import React, { useState } from 'react';
import { storeNumberAtom, storePosAtom } from '@atoms/states/StoreAtom';
import { AddProductsOutputInterface } from '@interfaces/OutputInterfaces/Sale/AddProductsOutputInterfae';
import { CustomerInterface } from '@interfaces/ResponseInterfaces/Customer/CustomerResponseInterface';
import { AddProductsResponseInterface, ItemObject } from '@interfaces/ResponseInterfaces/Sale/AddProductsResponseInterface';
import { StartSaleResponseInterface } from '@interfaces/ResponseInterfaces/Sale/StartSaleResponseInterface';
import ActionButtons from '@molecules/ActionButtons';
import ProductSearch from '@molecules/ProductSearch';
import SaleSummary from '@molecules/SaleSummary';
import AddProducts from "@package/config/api/Sale/Transaction/AddProducts/page";
interface ControlPanelProps {
onVoidOrder: () => void;
onVoidLast: () => void;
onResume: () => void;
onSearchPLU: (plu: string) => void;
subtotal: number;
discount: number;
tax: number;
total: number;
transaction: StartSaleResponseInterface;
client: CustomerInterface;
onProductAdded: () => void;
}
const ControlPanel: React.FC<ControlPanelProps> = ({
onVoidOrder,
onVoidLast,
onResume,
onSearchPLU,
subtotal,
discount,
tax,
total
transaction,
client,
onProductAdded,
}) => {
const [addedProducts, setAddedProducts] = useState<ItemObject>({} as ItemObject);
const [loading, setLoading] = useState(false);
const [storeNumber] = useAtom(storeNumberAtom);
const [storePos] = useAtom(storePosAtom);
const handleSearchSKU = async (sku: string) => {
setLoading(true);
try {
const addProductRequest: AddProductsOutputInterface = {
idTransaction: transaction.object?.id,
storeNumber: storeNumber,
idSubsidiary: client.idSubsidiary,
clientNit: client.dni,
idUser: client.idUser,
localDate: new Date(),
pos: storePos,
products: [
{
sku: sku,
value: 1,
},
],
};
const productResponse = await AddProducts(addProductRequest) as AddProductsResponseInterface;
if (productResponse && productResponse.correct) {
const newProduct = productResponse.object;
setAddedProducts(newProduct)
onProductAdded();
} else {
console.error("Error adding product: ", productResponse?.message);
}
} catch (error) {
console.error("Error during AddProducts API call", error);
} finally {
setLoading(false);
}
};
return (
<Grid2 container spacing={4}>
<Grid2 size={{xs:12}}>
@ -34,11 +80,11 @@ const ControlPanel: React.FC<ControlPanelProps> = ({
</Grid2>
<Grid2 size={{xs:12}}>
<ProductSearch onSearch={onSearchPLU} />
<ProductSearch onSearch={handleSearchSKU} />
</Grid2>
<Grid2 size={{xs:12}}>
<SaleSummary subtotal={subtotal} discount={discount} tax={tax} total={total} />
<SaleSummary product={addedProducts} />
</Grid2>
</Grid2>
);

View file

@ -3,30 +3,16 @@
import { Box, Table, TableBody, TableCell, TableHead, TableRow } from '@mui/material';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SaleTransactionObjectInterface } from '@interfaces/ResponseInterfaces/Sale/GetSaleResponseInterface';
import CustomCard from '@molecules/CustomCard';
import UserHeader from '@molecules/UserHeader';
interface Product {
id: number;
amount: number;
name: string;
unitValue: number;
tax: number;
discount: number;
total: number;
}
interface ProductTableProps {
products: Product[];
products: SaleTransactionObjectInterface | null;
}
const ProductTable: React.FC<ProductTableProps> = ({ products }) => {
const totalAmount = products.reduce((acc, product) => acc + product.amount, 0);
const totalUnitValue = products.reduce((acc, product) => acc + product.unitValue * product.amount, 0);
const totalTax = products.reduce((acc, product) => acc + product.tax, 0);
const totalDiscount = products.reduce((acc, product) => acc + product.discount, 0);
const grandTotal = products.reduce((acc, product) => acc + product.total, 0);
const { t } = useTranslation()
const { t } = useTranslation();
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
@ -48,38 +34,60 @@ const ProductTable: React.FC<ProductTableProps> = ({ products }) => {
</TableRow>
</TableHead>
<TableBody>
{products.map((product) => (
<TableRow key={product.id}>
<TableCell>{product.amount}</TableCell>
<TableCell>{product.name}</TableCell>
<TableCell>${product.unitValue.toFixed(2)}</TableCell>
<TableCell>${product.tax.toFixed(2)}</TableCell>
<TableCell>${product.discount.toFixed(2)}</TableCell>
<TableCell>${product.total.toFixed(2)}</TableCell>
{products?.list?.length ? (
products.list.map((product) => (
<TableRow key={product.idDetail}>
<TableCell>{product.quantity}</TableCell>
<TableCell>{product.name}</TableCell>
<TableCell>${product.regularUnitPrice.toFixed(2)}</TableCell>
<TableCell>${product.totalTaxes.toFixed(2)}</TableCell>
<TableCell>${product.discount.toFixed(2)}</TableCell>
<TableCell>${product.extendedPrice.toFixed(2)}</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={6} align="center">
{t("msj_no_products_added")} {/* Mensaje de no productos */}
</TableCell>
</TableRow>
))}
)}
</TableBody>
</Table>
</Box>
<Box sx={{ p:0.2, boxShadow: '2', }}>
<Box sx={{ p: 0.2, boxShadow: '2' }}>
<CustomCard className="bg-gray-700 text-white">
<Table>
<TableHead>
<TableRow>
<TableCell sx={{ color: 'RGB(255,255,255)', borderColor: 'with', fontWeight: 'bold', width: '20%' }}>{t("tle_item_count")}</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)', borderColor: 'with', fontWeight: 'bold', width: '20%' }}>{t("tle_total_unit")}</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)', borderColor: 'with', fontWeight: 'bold', width: '20%' }}>{t("tle_total_taxes")}</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)', borderColor: 'with', fontWeight: 'bold', width: '20%' }}>{t("tle_total_discount")}</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)', borderColor: 'with', fontWeight: 'bold', width: '20%' }}>{t("tle_total")}</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)', fontWeight: 'bold', width: '20%' }}>
{t("tle_total_unit")}
</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)', fontWeight: 'bold', width: '20%' }}>
{t("tle_total_taxes")}
</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)', fontWeight: 'bold', width: '20%' }}>
{t("tle_total_discount")}
</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)', fontWeight: 'bold', width: '20%' }}>
{t("tle_total")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell sx={{ color: 'RGB(255,255,255)', borderColor: 'with' }}>{totalAmount}</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)', borderColor: 'with' }}>${totalUnitValue.toFixed(2)}</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)', borderColor: 'with' }}>${totalTax.toFixed(2)}</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)', borderColor: 'with' }}>${totalDiscount.toFixed(2)}</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)', borderColor: 'with' }}>${grandTotal.toFixed(2)}</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)' }}>
${products?.totalSale?.toFixed(2) || '0.00'}
</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)' }}>
${products?.totalTaxes?.toFixed(2) || '0.00'}
</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)' }}>
$0.00 {/* Descuento total si aplica */}
</TableCell>
<TableCell sx={{ color: 'RGB(255,255,255)' }}>
${((products?.totalWithPromotion ?? 0) + (products?.totalTaxes ?? 0)).toFixed(2)}
</TableCell>
</TableRow>
</TableBody>
</Table>
@ -89,4 +97,4 @@ const ProductTable: React.FC<ProductTableProps> = ({ products }) => {
);
};
export default ProductTable;
export default ProductTable;

View file

@ -1,4 +1,5 @@
import { GetTypeDNIOutputInterface } from "@interfaces/OutputInterfaces/Customer/GetTypeDNIOutputInterface";
import { CustomerDniTypeResponseInterface } from "@interfaces/ResponseInterfaces/Customer/DNITypeResponseInterface";
import { BASE_URL_CUSTOMER_TYPEDNI } from "@package/config/ApiPath";
import { FetchData } from "@package/config/FetchData";
import { ReturnService } from "@package/config/ReturnService"
@ -36,7 +37,7 @@ async function GetTypeDni(ae_object: GetTypeDNIOutputInterface) {
})
}
const data = await FetchData(BASE_URL_CUSTOMER_TYPEDNI, 'all', options)
const data: CustomerDniTypeResponseInterface = await FetchData(BASE_URL_CUSTOMER_TYPEDNI, 'all', options)
if (data.correct) {
return data.object

View file

@ -31,7 +31,7 @@ async function GetSale(ae_object: GetSaleOutputInterface) {
})
}
let data: GetSaleResponseInterface = await FetchData(BASE_URL_SALE, 'addProducts', options)
let data: GetSaleResponseInterface = await FetchData(BASE_URL_SALE, 'getSale', options)
if (data.correct) {
return data.object

View file

@ -42,7 +42,7 @@ async function StartSale(ae_object: StartSaleOutputInterface) {
})
}
let data : StartSaleResponseInterface = await FetchData(BASE_URL_SALE, 'addProducts', options)
let data : StartSaleResponseInterface = await FetchData(BASE_URL_SALE, 'startSale', options)
if (data.correct) {
return data.object

View file

@ -1,6 +1,4 @@
import { useSetAtom } from 'jotai';
import { useEffect, useState } from 'react';
import CustomerAtom from '@atoms/states/CustomerAtom';
import { CustomerInterface, CustomerResponseInterface } from '@interfaces/ResponseInterfaces/Customer/CustomerResponseInterface';
import { CustomerDniTypeInterface, CustomerDniTypeResponseInterface } from '@interfaces/ResponseInterfaces/Customer/DNITypeResponseInterface';
import { GetCustomer } from '@package/config/api/Customer/Customer/GetCustomer/page';
@ -17,7 +15,8 @@ export const useClientSearchLogic = () => {
idNumber: ''
});
const setCustomer = useSetAtom(CustomerAtom);
// Cliente seleccionado
const [customer, setCustomer] = useState<CustomerInterface | null>(null);
const fetchIdTypes = async () => {
setLoading(true);
@ -91,6 +90,7 @@ export const useClientSearchLogic = () => {
formData,
handleInputChange,
handleSubmitForm,
isAffiliated
isAffiliated,
customer
};
};

View file

@ -1,7 +1,7 @@
export interface GetSaleOutputInterface {
idTransaction?: string;
storeNumber?: number;
idSubsidiary?: string;
storeNumber?: string;
idSubsidiary?: number;
idUser?: string;
pos?: string;
}

View file

@ -1,5 +1,5 @@
export interface Tax {
export interface TaxInteface {
id: string;
idSaleDetail: string;
idProduct: string;
@ -13,7 +13,7 @@ export interface Tax {
taxIncluded: boolean;
}
export interface SaleDetail {
export interface SaleDetailInterface {
idDetail: string;
idTransaction: string;
id: string;
@ -35,19 +35,19 @@ export interface SaleDetail {
idUser: string;
creation: string;
lastUpdate: string;
taxes: Tax[];
taxes: TaxInteface[];
active: boolean;
weighable: boolean;
extendedPrice: number;
}
export interface CustomerPromotion {
export interface CustomerPromotionInterface {
groupId: string;
customerId: string;
percentageDiscount: number | null;
}
export interface ItemPromotion {
export interface ItemPromotionInterface {
index: number;
code: string;
quantity: number;
@ -58,14 +58,14 @@ export interface ItemPromotion {
weighable: boolean;
}
export interface Promotions {
customer: CustomerPromotion;
items: ItemPromotion[];
export interface PromotionsInterface {
customer: CustomerPromotionInterface;
items: ItemPromotionInterface[];
total: number;
listPromotion: any[]; // Assuming this is an empty array, can adjust if more data is provided
listPromotion: any[];
}
export interface SaleTransactionObject {
export interface SaleTransactionObjectInterface {
id: string;
idCompany: string;
idSubsidiary: number;
@ -95,13 +95,13 @@ export interface SaleTransactionObject {
localDateTime: string | null;
creation: string;
lastUpdate: string;
list: SaleDetail[];
promotions: Promotions;
list: SaleDetailInterface[];
promotions: PromotionsInterface;
}
export interface GetSaleResponseInterface {
correct: boolean;
message: string | null;
errorCode: number;
object: SaleTransactionObject;
object: SaleTransactionObjectInterface;
}

View file

@ -7,5 +7,5 @@ export interface StartSaleResponseInterface {
correct: boolean;
message: string;
errorCode: number;
object: TransactionObject;
object: TransactionObject | null;
}