diff --git a/app/selfcheckout/CheckoutScreen.tsx b/app/selfcheckout/CheckoutScreen.tsx deleted file mode 100644 index 07be360..0000000 --- a/app/selfcheckout/CheckoutScreen.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client" -import { Box, Container } from "@mui/material" -import Grid from "@mui/material/Grid2" // Usando Grid2 -import ButtonPanel from "@organisms/ButtonPanel" -import Header from "@organisms/Header" -import MainPanel from "@organisms/MainPanel" -import ProductList from "@organisms/ProductList" -import React from "react" - -const CheckoutScreen: React.FC = () => { - return ( - -
- - - - - - - - - - - - - - - - - - ) -} - -export default CheckoutScreen diff --git a/app/selfcheckout/LoyaltyProgramScreen.tsx b/app/selfcheckout/LoyaltyProgramScreen.tsx deleted file mode 100644 index f446214..0000000 --- a/app/selfcheckout/LoyaltyProgramScreen.tsx +++ /dev/null @@ -1,99 +0,0 @@ -"use client" -import CheckoutScreen from "@app/selfcheckout/CheckoutScreen" -import AskForAffiliation from "@layouts/AskForAffiliation" -import IdentityDocumentNumber from "@layouts/IdentityDocumentNumber" -import TypeIdentityDocument from "@layouts/TypeIdentityDocument" -import { Box, Card, CardContent, Container } from "@mui/material" -import Grid from "@mui/material/Grid2" -import Header from "@organisms/Header" -import { getDocumentsByCountry } from "@package/utils/getDocumentsByCountry" -import { useEffect, useState } from "react" - -const LoyaltyProgramScreen: React.FC = () => { - const [isAffiliated, setIsAffiliated] = useState(null) - const [documentType, setDocumentType] = useState(null) - const [documentNumber, setDocumentNumber] = useState(null) - const [goToCheckout, setGoToCheckout] = useState(false) - const [countryDocumentTypes, setCountryDocumentTypes] = useState([]) - - useEffect(() => { - const documents = getDocumentsByCountry() - setCountryDocumentTypes(documents) - }, []) - - const handleAffiliationSelect = (isAffiliated: boolean) => { - setIsAffiliated(isAffiliated) - if (!isAffiliated) { - setDocumentType("13") - setDocumentNumber("222222222222") - setGoToCheckout(true) - } - } - - const handleDocumentSelect = (selectedDocumentType: string) => { - setDocumentType(selectedDocumentType) - } - - const handleDocumentNumberSubmit = (enteredDocumentNumber: string) => { - setDocumentNumber(enteredDocumentNumber) - setGoToCheckout(true) - } - - if (goToCheckout) { - return - } - - return ( - - -
- - - - - - - - - - {isAffiliated === null && } - {isAffiliated && !documentType && ( - - )} - {isAffiliated && documentType && ( - - )} - - - - - - ) -} - -export default LoyaltyProgramScreen diff --git a/app/selfcheckout/PreCheckoutScreen.tsx b/app/selfcheckout/PreCheckoutScreen.tsx deleted file mode 100644 index ee22514..0000000 --- a/app/selfcheckout/PreCheckoutScreen.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client" -import Title from "@atoms/Title" -import { Tooltip } from "@atoms/Tooltip" -import { Box, Button, Container } from "@mui/material" -import React from "react" - -const PreCheckoutScreen: React.FC = () => { - const handleStartCheckout = () => { - console.log("Comenzar checkout") - } - - return ( - - - - <Title text="Abierta" size="h2" /> - - <Tooltip explainer="Escanee un producto o pulse el botón para comenzar" withArrow side="top"> - <Title text="Escanee un producto o pulse aquí para comenzar" size="h6" /> - </Tooltip> - - <Button - variant="contained" - color="success" - size="large" - sx={{ marginTop: "40px" }} - onClick={handleStartCheckout} - > - Comenzar - </Button> - </Box> - </Container> - ) -} - -export default PreCheckoutScreen diff --git a/app/selfcheckout/checkout/page.tsx b/app/selfcheckout/checkout/page.tsx new file mode 100644 index 0000000..2734c3b --- /dev/null +++ b/app/selfcheckout/checkout/page.tsx @@ -0,0 +1,22 @@ +"use client"; +import { Box } from "@mui/material"; +import React from "react"; +import MainLayout from "@layouts/MainLayout"; + +const CheckoutScreen: React.FC = () => { + return ( + <Box + sx={{ + width: '100vw', + height: '100vh', + overflow: 'hidden', + bgcolor: "background.default", + boxShadow: '4' + }} + > + <MainLayout /> + </Box> + ); +}; + +export default CheckoutScreen; \ No newline at end of file diff --git a/app/selfcheckout/clientSearch/page.tsx b/app/selfcheckout/clientSearch/page.tsx new file mode 100644 index 0000000..6b4cca4 --- /dev/null +++ b/app/selfcheckout/clientSearch/page.tsx @@ -0,0 +1,242 @@ +'use client'; +import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Paper, Typography } from '@mui/material'; +import Grid from '@mui/material/Grid2'; +import { useSetAtom } from 'jotai'; +import { useRouter } from 'next/navigation'; +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 CustomerAtom from '@atoms/states/CustomerAtom'; +import Title from '@atoms/Title'; +import { Tooltip } from '@atoms/Tooltip'; +import { useClientSearchLogic } from '@hooks/useClientSearchLogic'; +import { CustomerDniTypeInterface } from '@interfaces/ResponseInterfaces/Customer/DNITypeResponseInterface'; + +const ClientSearch: React.FC = () => { + const { + idTypes, + loading, + error, + result, + formData, + handleInputChange, + handleSubmitForm, + isAffiliated, + } = useClientSearchLogic(); + const { t } = useTranslation(); + const [openDialog, setOpenDialog] = useState(false); + const setCustomer = useSetAtom(CustomerAtom); + const router = useRouter(); + + const handleOpenDialog = () => { + setOpenDialog(true); + }; + + const handleCloseDialog = () => { + setOpenDialog(false); + }; + + const handleUseGenericCustomer = () => { + setCustomer({ + id: '', + idCompany: 'default-company', + companyName: 'default-company-name', + idSubsidiary: 0, + nameSubsidiary: 'default-subsidiary', + externalClientId: null, + name: 'Cliente', + secondName: '', + firstSurname: 'Generico', + secondSurname: '', + idTypeCustomer: 'Tarjeta de identidad', + nameTypeCustomer: 'Tarjeta', + 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: '' + }); + + router.push("/selfcheckout/checkout"); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); // Evita el comportamiento por defecto del formulario + handleSubmitForm(); // Ejecuta la función de búsqueda con los datos actuales + }; + + 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} // Asegura que el estado del input cambie + 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} // Actualiza el valor del input al escribir + 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={() => { + // Lógica para crear nuevo cliente + }} + /> + </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={() => { + router.push("/selfcheckout/checkout"); + }} + /> + </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; diff --git a/app/selfcheckout/precheckout/page.tsx b/app/selfcheckout/precheckout/page.tsx new file mode 100644 index 0000000..ba88edd --- /dev/null +++ b/app/selfcheckout/precheckout/page.tsx @@ -0,0 +1,57 @@ +"use client"; +import { Box, Button, Container } from "@mui/material"; +import Grid from "@mui/material/Grid2"; +import { useRouter } from "next/navigation"; +import React, { useCallback, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import Title from "@atoms/Title"; + +const PreCheckout: React.FC = () => { + const { t } = useTranslation(); + const router = useRouter(); + + const handleStartCheckout = useCallback(() => { + router.push("/selfcheckout/verifyAffiliation"); + }, [router]); + + useEffect(() => { + const handleKeyPress = () => { + handleStartCheckout(); + }; + + document.addEventListener("keydown", handleKeyPress); + + // Cleanup the event listener + return () => { + document.removeEventListener("keydown", handleKeyPress); + }; + }, [handleStartCheckout]); + + return ( + <Container maxWidth="md" sx={{ textAlign: "center", marginTop: "20vh" }}> + <Box sx={{ backgroundColor: "#f1f1f1", padding: "80px", borderRadius: "10px", boxShadow: 6 }}> + <Grid size={{ xs: 4, md: 6 }}> + <Title text={t("msj_this_box_is")} size="h3" /> + </Grid> + <Grid size={{ xs: 4, md: 6 }}> + <Title text={t("lb_open")} size="h1" /> + </Grid> + <Grid size={{ xs: 4, md: 6 }}> + <Title text={t("msj_press_any_key_button_start")} size="h5" /> + </Grid> + + <Button + variant="contained" + color="primary" + size="large" + sx={{ marginTop: "40px" }} + onClick={handleStartCheckout} + > + {t("lbl_start")} + </Button> + </Box> + </Container> + ); +}; + +export default PreCheckout; diff --git a/app/selfcheckout/verifyAffiliation/page.tsx b/app/selfcheckout/verifyAffiliation/page.tsx new file mode 100644 index 0000000..0ec9d89 --- /dev/null +++ b/app/selfcheckout/verifyAffiliation/page.tsx @@ -0,0 +1,61 @@ +"use client"; +import { Box, Card, CardContent, Container } from "@mui/material"; +import Grid from "@mui/material/Grid2"; +import Image from "next/image"; +import React from "react"; +import CustomLoading from "@atoms/CustomLoading"; + +const ClientSearch = React.lazy(() => import("@app/selfcheckout/clientSearch/page")); + +const VerifyAffiliation: React.FC = () => { + return ( + <Container + maxWidth={false} + sx={{ + height: "100vh", + display: "flex", + justifyContent: "center", + alignItems: "center", + padding: 0, + }} + > + <Card sx={{ width: "100%", p: 2 }}> + + <CardContent sx={{ padding: "4px", height: "100%" }}> + <Grid container spacing={2} sx={{ height: "100%" }} alignItems="center"> + <Grid size = {{xs:12, md:6}}> + <Box + sx={{ + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + height: "100%", + bgcolor: "#f5f5f5", + p: 5, + borderRadius: 5, + }} + > + <Image + src="/img/logo.png" + alt="Rhiscom Logo" + width={600} + height={400} + priority={true} + style={{ marginBottom: 15 }} + /> + </Box> + </Grid> + <Grid size = {{xs:12, md:6}}> + <React.Suspense fallback={<CustomLoading />}> + <ClientSearch /> + </React.Suspense> + </Grid> + </Grid> + </CardContent> + </Card> + </Container> + ); +}; + +export default VerifyAffiliation; diff --git a/package/components/atoms/Button.tsx b/package/components/atoms/Button.tsx index 4ec0b44..ee789e2 100644 --- a/package/components/atoms/Button.tsx +++ b/package/components/atoms/Button.tsx @@ -1,45 +1,56 @@ -"use client" -import { Button as MUIButton } from "@mui/material" -import React from "react" +"use client"; +import { Button as MUIButton } from "@mui/material"; +import React, { forwardRef } from "react"; interface ButtonProps { - label: string - variant?: "primary" | "secondary" | "success" | "danger" | "warning" | "info" - onClick?: () => void - disabled?: boolean - className?: string - startIcon?: React.ReactNode + label: string; + type?: "button" | "submit" | "reset"; + variant?: "primary" | "secondary" | "success" | "danger" | "warning" | "info"; + onClick?: () => void; + disabled?: boolean; + className?: string; + startIcon?: React.ReactNode; } -const Button: React.FC<ButtonProps> = ({ - label, - variant = "primary", - onClick, - disabled, - className = "", - startIcon, -}) => { - const colorMap: Record<string, "primary" | "secondary" | "error" | "warning" | "info" | "success"> = { - primary: "primary", - secondary: "secondary", - success: "success", - danger: "error", - warning: "warning", - info: "info", +const Button = forwardRef<HTMLButtonElement, ButtonProps>( + ( + { + label, + type = "button", + variant = "primary", + onClick, + disabled, + className = "", + startIcon, + }, + ref + ) => { + const colorMap: Record<string, "primary" | "secondary" | "error" | "warning" | "info" | "success"> = { + primary: "primary", + secondary: "secondary", + success: "success", + danger: "error", + warning: "warning", + info: "info", + }; + + return ( + <MUIButton + ref={ref} + type={type} + onClick={onClick} + disabled={disabled} + color={colorMap[variant ?? "primary"]} + className={className} + variant="contained" + startIcon={startIcon} + > + {label} + </MUIButton> + ); } +); - return ( - <MUIButton - onClick={onClick} - disabled={disabled} - color={colorMap[variant ?? "primary"]} - className={className} - variant="contained" - startIcon={startIcon} - > - {label} - </MUIButton> - ) -} +Button.displayName = "Button"; -export default Button +export default Button; diff --git a/package/components/atoms/CustomLoading.tsx b/package/components/atoms/CustomLoading.tsx new file mode 100644 index 0000000..748f55a --- /dev/null +++ b/package/components/atoms/CustomLoading.tsx @@ -0,0 +1,43 @@ +'use client'; +import { CircularProgress } from '@mui/material'; +import * as RadixTooltip from '@radix-ui/react-tooltip'; +import React from 'react'; + +const CustomLoading: React.FC = () => { + return ( + <div className="flex flex-col items-center justify-center h-full space-y-4"> + <CircularProgress + size={60} + thickness={4} + className="text-blue-600 animate-spin" + /> + + <RadixTooltip.Provider> + <RadixTooltip.Root> + <RadixTooltip.Trigger asChild> + <div className="text-gray-600 font-semibold text-lg cursor-pointer"> + Loading, please wait... + </div> + </RadixTooltip.Trigger> + <RadixTooltip.Portal> + <RadixTooltip.Content + side="top" + sideOffset={5} + className="bg-gray-800 text-white rounded-md p-2 shadow-md text-sm" + > + Fetching data from the server. This may take a moment. + <RadixTooltip.Arrow className="fill-gray-800" /> + </RadixTooltip.Content> + </RadixTooltip.Portal> + </RadixTooltip.Root> + </RadixTooltip.Provider> + + {/* Tailwind CSS animation for visual effect */} + <div className="relative mt-2"> + <div className="size-16 border-4 border-dashed rounded-full animate-spin border-blue-500"></div> + </div> + </div> + ); +}; + +export default CustomLoading; diff --git a/package/components/atoms/Icon.tsx b/package/components/atoms/Icon.tsx new file mode 100644 index 0000000..14a33a5 --- /dev/null +++ b/package/components/atoms/Icon.tsx @@ -0,0 +1,15 @@ +"use client" +import * as MUIIcons from "@mui/icons-material" +import React from "react" + +interface IconProps { + name: keyof typeof MUIIcons + className?: string +} + +const Icon: React.FC<IconProps> = ({ name, className }) => { + const IconComponent = MUIIcons[name] + return <IconComponent className={className} /> +} + +export default Icon \ No newline at end of file diff --git a/package/components/atoms/Input.tsx b/package/components/atoms/Input.tsx index 7ec352b..260e684 100644 --- a/package/components/atoms/Input.tsx +++ b/package/components/atoms/Input.tsx @@ -4,20 +4,33 @@ import React from "react" interface InputProps { type: string + name?: string; placeholder?: string value: string onChange: (e: React.ChangeEvent<HTMLInputElement>) => void + onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void; autocomplete?: string disabled?: boolean } -const Input: React.FC<InputProps> = ({ type, placeholder, value, onChange, autocomplete, disabled }) => { +const Input: React.FC<InputProps> = ({ + type, + name, + placeholder, + value, + onChange, + onKeyDown, + autocomplete, + disabled +}) => { return ( <TextField type={type} + name={name} placeholder={placeholder} value={value} onChange={onChange} + onKeyDown={onKeyDown} autoComplete={autocomplete} fullWidth variant="outlined" diff --git a/package/components/atoms/Label.tsx b/package/components/atoms/Label.tsx index baa3b4e..c84fb24 100644 --- a/package/components/atoms/Label.tsx +++ b/package/components/atoms/Label.tsx @@ -7,15 +7,16 @@ interface LabelProps { text: string } -const Label: React.FC<LabelProps> = ({ htmlFor, text }) => { +const Label = React.memo(({ htmlFor, text }: LabelProps) => { return ( <label htmlFor={htmlFor} className="mb-2 block"> - <Typography variant="subtitle1"> - {text} - </Typography> + <Typography + variant="subtitle1">{text}</Typography> </label> - ) -} + ); +}); + +Label.displayName = "Label"; export default Label diff --git a/package/components/atoms/states/CustomerAtom.tsx b/package/components/atoms/states/CustomerAtom.tsx new file mode 100644 index 0000000..dd5f85b --- /dev/null +++ b/package/components/atoms/states/CustomerAtom.tsx @@ -0,0 +1,7 @@ +import { atom } from 'jotai'; +import { CustomerInterface } from '@interfaces/ResponseInterfaces/Customer/CustomerResponseInterface'; + + +const CustomerAtom = atom<CustomerInterface | null>(null); // Estado inicial vacío + +export default CustomerAtom; diff --git a/package/components/atoms/states/UserAtom.tsx b/package/components/atoms/states/UserAtom.tsx new file mode 100644 index 0000000..5ca7f10 --- /dev/null +++ b/package/components/atoms/states/UserAtom.tsx @@ -0,0 +1,6 @@ +import { atom } from 'jotai'; +import { UserResposeInterface } from '@interfaces/ResponseInterfaces/Auth/UserResponseInterface'; + +const UserAtom = atom<UserResposeInterface | null>(null); + +export default UserAtom; diff --git a/package/components/layouts/AskForAffiliation.tsx b/package/components/layouts/AskForAffiliation.tsx index de9c59f..ba6fcb0 100644 --- a/package/components/layouts/AskForAffiliation.tsx +++ b/package/components/layouts/AskForAffiliation.tsx @@ -1,6 +1,7 @@ "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" @@ -10,6 +11,9 @@ interface AskForAffiliationProps { } const AskForAffiliation: React.FC<AskForAffiliationProps> = ({ onAffiliationSelect }) => { + + const { t } = useTranslation() + const handleYesClick = () => { onAffiliationSelect(true) } @@ -31,14 +35,14 @@ const AskForAffiliation: React.FC<AskForAffiliationProps> = ({ onAffiliationSele p: 2, }} > - <Title text="Se requiere Información" size="h3" /> - <Label htmlFor="loyalty" text="¿Está registrado como afiliado?" /> + <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="Sí" onClick={handleYesClick} variant="primary" className="h-16 w-full" /> + <Button label={t("lbl_yes")} onClick={handleYesClick} variant="primary" className="h-16 w-full" /> </Grid> <Grid size={{ xs: 6 }} sx={{ minWidth: "200px" }}> - <Button label="No" onClick={handleNoClick} variant="primary" className="h-16 w-full" /> + <Button label={t('lbl_no')} onClick={handleNoClick} variant="primary" className="h-16 w-full" /> </Grid> </Box> </Box> diff --git a/package/components/layouts/IdentityDocumentNumber.tsx b/package/components/layouts/IdentityDocumentNumber.tsx index 92affde..6cba5cd 100644 --- a/package/components/layouts/IdentityDocumentNumber.tsx +++ b/package/components/layouts/IdentityDocumentNumber.tsx @@ -2,6 +2,7 @@ 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" @@ -15,9 +16,11 @@ const IdentityDocumentNumber: React.FC<IdentityDocumentNumberProps> = ({ documen 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"] // Documentos que contienen letras + const alphaDocuments = ["PASAPORTE", "CURP", "RFC", "NIE", "RUT"] setIsAlphaEnabled(alphaDocuments.includes(documentType.toUpperCase())) setKeyboardMode(isAlphaEnabled ? "alpha" : "numeric") }, [documentType, isAlphaEnabled]) @@ -30,7 +33,6 @@ const IdentityDocumentNumber: React.FC<IdentityDocumentNumberProps> = ({ documen setInputValue((prev) => prev.slice(0, -1)) } - // Renderiza el teclado basado en el modo seleccionado const renderKeyboard = () => { if (keyboardMode === "numeric") { return ( @@ -69,33 +71,32 @@ const IdentityDocumentNumber: React.FC<IdentityDocumentNumberProps> = ({ documen }} > {/* Título */} - <Title text={`Ingrese el número de ${documentType}`} size="h4" /> + <Title text={`${t('msj_enter_number_of')} ${documentType}`} size="h4" /> - {/* Input que muestra el valor actual */} <Input type="text" - placeholder="Ingrese el número de documento" + 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="Borrar" onClick={handleDelete} variant="danger" className="h-16 w-full" /> + <Button label= {t('lbl_delete')} onClick={handleDelete} variant="danger" className="h-16 w-full" /> </Grid> <Grid size={{ xs: 4 }}> - <Button label="Cancelar" onClick={() => setInputValue("")} variant="danger" className="h-16 w-full" /> + <Button label={t('lbl_cancel')} onClick={() => setInputValue("")} variant="danger" className="h-16 w-full" /> </Grid> {renderKeyboard()} <Grid size={{ xs: 4 }}> - <Button label="Ingresar" onClick={() => onSubmit(inputValue)} variant="success" className="h-16 w-full" /> + <Button label={t('lbl_enter')} onClick={() => onSubmit(inputValue)} variant="success" className="h-16 w-full" /> </Grid> {isAlphaEnabled && ( <Grid size={{ xs: 4 }}> <Button - label={`Cambiar a ${keyboardMode === "numeric" ? "Alfabético" : "Numérico"}`} + label={`${t('msj_change_to')} ${keyboardMode === "numeric" ? t('lbl_alphabetical') : t('lbl_numeric')}`} onClick={() => setKeyboardMode(keyboardMode === "numeric" ? "alpha" : "numeric")} - variant="secondary" + variant="info" className="h-16 w-full" /> </Grid> diff --git a/package/components/layouts/MainLayout.tsx b/package/components/layouts/MainLayout.tsx new file mode 100644 index 0000000..ebe443b --- /dev/null +++ b/package/components/layouts/MainLayout.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { Box, Container, Paper } from '@mui/material'; +import Grid from '@mui/material/Grid2'; +import React from 'react'; +import ControlPanel from '@organisms/Checkout/ControlPanel'; +import ProductTable from '@organisms/Checkout/ProductTable'; + +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 }, +]; + +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); + + 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} /> + </Paper> + </Grid> + <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} + /> + </Paper> + </Grid> + </Grid> + </Container> + </Box> + ); +}; + +export default MainLayout; \ No newline at end of file diff --git a/package/components/layouts/TypeIdentityDocument.tsx b/package/components/layouts/TypeIdentityDocument.tsx index d226b38..0659268 100644 --- a/package/components/layouts/TypeIdentityDocument.tsx +++ b/package/components/layouts/TypeIdentityDocument.tsx @@ -1,6 +1,7 @@ "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" @@ -11,6 +12,8 @@ interface TypesDocumentProps { } const TypeIdentityDocument: React.FC<TypesDocumentProps> = ({ typesDocument, onDocumentSelect }) => { + const { t } = useTranslation() + const handleDocumentClick = (documentType: string) => { onDocumentSelect(documentType) } @@ -25,15 +28,15 @@ const TypeIdentityDocument: React.FC<TypesDocumentProps> = ({ typesDocument, onD justifyContent: "center", alignItems: "center", height: "100%", - p: 2, + p: 1, }} > - <Title text="Se requiere Información" size="h4" /> - <Label htmlFor="loyalty" text="Seleccione el tipo de documento de identidad" /> + <Title text= {t("lbl_information_required")} size="h4" /> + <Label htmlFor="loyalty" text={t('msj_select_type_identity_document')} /> - <Grid container spacing={2} justifyContent="center" sx={{ mt: 3 }}> + <Grid container spacing={1} justifyContent="center" sx={{ mt: 2 }}> {typesDocument.map((documentType, index) => ( - <Grid key={index} size={{ xs: 6, md: 4 }} sx={{ minWidth: "200px" }}> + <Grid key={index} size={{ xs: 4, md: 6 }} sx={{ minWidth: "100px" }}> <Button label={documentType} onClick={() => handleDocumentClick(documentType)} diff --git a/package/components/molecules/ActionButtons.tsx b/package/components/molecules/ActionButtons.tsx new file mode 100644 index 0000000..5f5043b --- /dev/null +++ b/package/components/molecules/ActionButtons.tsx @@ -0,0 +1,63 @@ +"use client"; +import { Box } from '@mui/material'; +import Grid from '@mui/material/Grid2'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Button from '@atoms/Button'; + +interface ActionButtonsProps { + onVoidOrder: () => void; + onVoidLast: () => void; + onResume: () => void; +} + +const ActionButtons: React.FC<ActionButtonsProps> = ({ onVoidOrder, onVoidLast, onResume }) => { + const { t } = useTranslation() + + return ( + <Box + sx={{ + mb: 1, + p: 1, + boxShadow: 4, + display: 'flex', + justifyContent: 'center', + alignItems: 'center' + }} + > + <Grid + container + spacing={2} + justifyContent="center" + alignItems="center" + > + <Grid size={{xs:4}}> + <Button + label={t("lbl_void_order")} + variant="danger" + onClick={onVoidOrder} + className="full-width-button" + /> + </Grid> + <Grid size={{xs:4}}> + <Button + label={t("lbl_void_last")} + variant="danger" + onClick={onVoidLast} + className="full-width-button" + /> + </Grid> + <Grid size={{xs:4}}> + <Button + label={t("lbl_resume")} + variant="primary" + onClick={onResume} + className="full-width-button" + /> + </Grid> + </Grid> + </Box> + ); +}; + +export default ActionButtons; diff --git a/package/components/molecules/CustomCard.tsx b/package/components/molecules/CustomCard.tsx new file mode 100644 index 0000000..f5bba8a --- /dev/null +++ b/package/components/molecules/CustomCard.tsx @@ -0,0 +1,20 @@ +"use client" +import { CardContent, CardHeader, Card as MUICard } from "@mui/material" +import React from "react" + +interface CardProps { + title?: string + children: React.ReactNode + className?: string +} + +const CustomCard: React.FC<CardProps> = ({ title, children, className }) => { + return ( + <MUICard className={className}> + {title && <CardHeader title={title} />} + <CardContent>{children}</CardContent> + </MUICard> + ) +} + +export default CustomCard \ No newline at end of file diff --git a/package/components/molecules/NumericKey.tsx b/package/components/molecules/NumericKey.tsx index d7682e6..e84c323 100644 --- a/package/components/molecules/NumericKey.tsx +++ b/package/components/molecules/NumericKey.tsx @@ -1,20 +1,24 @@ -"use client" -import Grid2 from "@mui/material/Grid2" -import React from "react" -import Button from "@atoms/Button" +"use client"; +import Grid2 from "@mui/material/Grid2"; +import React from "react"; +import Button from "@atoms/Button"; interface NumericKeyProps { - label: string | number - onClick: () => void - className?: string + label: string | number; + onClick: () => void; + className?: string; } const NumericKey: React.FC<NumericKeyProps> = ({ label, onClick, className }) => { return ( <Grid2 container justifyContent="center" component="div"> - <Button label={label.toString()} onClick={onClick} className={`h-16 w-full ${className}`} /> + <Button + label={label.toString()} + onClick={onClick} + className={`h-16 w-full text-3xl font-bold ${className}`} + /> </Grid2> - ) -} + ); +}; -export default NumericKey +export default NumericKey; diff --git a/package/components/molecules/NumericPad.tsx b/package/components/molecules/NumericPad.tsx index b5c497a..bc4e3b5 100644 --- a/package/components/molecules/NumericPad.tsx +++ b/package/components/molecules/NumericPad.tsx @@ -1,69 +1,57 @@ -"use client" -import Grid from "@mui/material/Grid2" -import React from "react" -import NumericKey from "@molecules/NumericKey" +"use client"; +import Grid from "@mui/material/Grid2"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import NumericKey from "@molecules/NumericKey"; -const NumericPad: React.FC = () => { - const handleKeyClick = (key: number) => { - console.log(`Key pressed: ${key}`) - } +interface NumericPadProps { + onKeyClick: (key: string) => void; +} + +const NumericPad: React.FC<NumericPadProps> = ({ onKeyClick }) => { + const { t } = useTranslation(); return ( <Grid container spacing={0.4} justifyContent="center" alignItems="center"> <Grid size={{ xs: 6 }}> <NumericKey - label="Cancel" - onClick={() => console.log("Cancel pressed")} + label={t("lbl_cancel")} + onClick={() => onKeyClick("cancel")} className="h-12 w-full bg-danger-light text-white hover:bg-danger-dark" /> </Grid> <Grid size={{ xs: 6 }}> - <NumericKey label="clear" onClick={() => console.log("Clear pressed")} className="size-12 bg-secondary-light" /> - </Grid> - <Grid size={{ xs: 3 }} sx={{ minWidth: "100px" }}> <NumericKey - label="0" - onClick={() => console.log("0")} - className="size-24 bg-primary-500 text-white hover:bg-primary-600" + label={t("lbl_clear")} + onClick={() => onKeyClick("clear")} + className="h-12 bg-secondary-light text-black" /> </Grid> - {[...Array(9).keys()].map((i) => ( - <Grid key={i + 1} size={{ xs: 3 }} sx={{ minWidth: "100px" }}> - {" "} + {[...Array(10).keys()].map((i) => ( + <Grid key={i} size={{ xs: 4 }} sx={{ minWidth: "100px" }}> <NumericKey - label={i + 1} - onClick={() => handleKeyClick(i + 1)} - className="size-24 bg-primary-500 text-white hover:bg-primary-600" + label={i} + onClick={() => onKeyClick(i.toString())} + className="h-16 bg-primary-500 text-white hover:bg-primary-600" /> </Grid> ))} - <Grid size={{ xs: 3 }} sx={{ minWidth: "100px" }}> + <Grid size={{ xs: 4 }}> <NumericKey label="." - onClick={() => console.log(".")} - className="size-24 bg-primary-500 text-white hover:bg-primary-600" + onClick={() => onKeyClick(".")} + className="h-16 bg-primary-500 text-white hover:bg-primary-600" /> </Grid> - <Grid size={{ xs: 3 }} sx={{ minWidth: "100px" }}> - <NumericKey label="/" onClick={() => console.log("Divide pressed")} className="size-12 bg-secondary-light" /> - </Grid> - <Grid size={{ xs: 3 }} sx={{ minWidth: "100px" }}> - <NumericKey label="QTY" onClick={() => console.log("QTY pressed")} className="size-12 bg-secondary-light" /> - </Grid> - <Grid size={{ xs: 3 }} sx={{ minWidth: "100px" }}> - <NumericKey label="Price" onClick={() => console.log("Price pressed")} className="size-12 bg-secondary-light" /> - </Grid> - - - <Grid size={{ xs: 6 }}> + <Grid size={{ xs: 4 }}> <NumericKey - label="Enter" - onClick={() => console.log("Enter pressed")} + label={t("lbl_enter")} + onClick={() => onKeyClick("enter")} className="h-12 w-full bg-success-light text-white hover:bg-success-dark" /> </Grid> </Grid> - ) -} + ); +}; -export default NumericPad +export default NumericPad; diff --git a/package/components/molecules/PluEntry.tsx b/package/components/molecules/PluEntry.tsx deleted file mode 100644 index 6b12568..0000000 --- a/package/components/molecules/PluEntry.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client" -import { Box } from "@mui/material" -import React, { useState } from "react" -import { useTranslation } from "react-i18next" -import Input from "@atoms/Input" - -const PluEntry: React.FC = () => { - const [plu, setPlu] = useState("") - const { t } = useTranslation() - - const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { - setPlu(e.target.value) - } - - return ( - <Box sx={{ p: 1, backgroundColor: "#f5f5f5", borderRadius: 2 }}> - <Input - type="text" - value={plu} - onChange={handleInputChange} - placeholder={t("lbl_enter_PLU")} - autocomplete="off" - /> - </Box> - ) -} - -export default PluEntry diff --git a/package/components/molecules/ProductMatch.tsx b/package/components/molecules/ProductMatch.tsx deleted file mode 100644 index d77848e..0000000 --- a/package/components/molecules/ProductMatch.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client" -import { Box, Typography } from "@mui/material" -import React from "react" - -const ProductMatch: React.FC = () => { - return ( - <Box sx={{ p: 2, backgroundColor: "#f5f5f5", borderRadius: 1 }}> - <Typography variant="h6">Coincidencia de productos:</Typography> {/* TODO: Aquí irán los productos que coincidan con el PLU */} - </Box> - ) -} - -export default ProductMatch diff --git a/package/components/molecules/ProductRow.tsx b/package/components/molecules/ProductRow.tsx new file mode 100644 index 0000000..0d56166 --- /dev/null +++ b/package/components/molecules/ProductRow.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { TableCell, TableRow } from '@mui/material'; +import React from 'react'; + +interface ProductRowProps { + amount: number; + name: string; + unitValue: number; + tax: number; + discount: number; + total: number; +} + +const ProductRow: React.FC<ProductRowProps> = ({ amount, name, unitValue, tax, discount, total }) => { + return ( + <TableRow> + <TableCell>{amount}</TableCell> + <TableCell>{name}</TableCell> + <TableCell>${unitValue.toFixed(2)}</TableCell> + <TableCell>${tax.toFixed(2)}</TableCell> + <TableCell>${discount.toFixed(2)}</TableCell> + <TableCell>${total.toFixed(2)}</TableCell> + </TableRow> + ); +}; + +export default ProductRow; diff --git a/package/components/molecules/ProductSearch.tsx b/package/components/molecules/ProductSearch.tsx new file mode 100644 index 0000000..68408a2 --- /dev/null +++ b/package/components/molecules/ProductSearch.tsx @@ -0,0 +1,50 @@ +// components/molecules/ProductSearch.tsx +"use client"; + +import { Box } from '@mui/material'; +import Grid from '@mui/material/Grid2'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import Button from '@atoms/Button'; +import Input from '@atoms/Input'; + +interface ProductSearchProps { + onSearch: (plu: string) => void; +} + +const ProductSearch: React.FC<ProductSearchProps> = ({ onSearch }) => { + const [plu, setPlu] = useState(''); + const { t } = useTranslation() + + + const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { + setPlu(e.target.value); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + onSearch(plu); + } + }; + + return ( + <Box sx={{ p:1, mb: 1, boxShadow: '4' }}> + <Grid container spacing={3} alignItems="center"> + <Grid size={{xs:8}}> + <Input + type="text" + value={plu} + placeholder={t("msj_enter_plu")} + onChange={handleInputChange} + onKeyDown={handleKeyDown} + /> + </Grid> + <Grid size={{xs:4}}> + <Button label={t("lbl_search")} variant="primary" onClick={() => onSearch(plu)} /> + </Grid> + </Grid> + </Box> + ); +}; + +export default ProductSearch; diff --git a/package/components/molecules/SaleSummary.tsx b/package/components/molecules/SaleSummary.tsx new file mode 100644 index 0000000..6388dd7 --- /dev/null +++ b/package/components/molecules/SaleSummary.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { Box, Table, TableBody, TableCell, TableHead, TableRow, Typography } from '@mui/material'; +import Grid from '@mui/material/Grid2'; + +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface SaleSummaryProps { + subtotal: number; + discount: number; + tax: number; + total: number; +} + +const SaleSummary: React.FC<SaleSummaryProps> = ({ subtotal, discount, tax, total }) => { + const { t } = useTranslation() + + return ( + <Box sx={{ + + color: 'black', + borderRadius: '5px', + boxShadow: '2', + p: 1, + mb: 1 + }}> + <Box sx={{mb: 1, p: 1, boxShadow: '2', }}> + <Table size="small"> + <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> + </TableRow> + </TableBody> + </Table> + <Grid container justifyContent='right'> + <Box sx={{ mt: 5, textAlign: 'right' }}> + <Grid container> + <Grid size={{xs:7}} sx={{ pr: 10 }}> + <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> + </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> + </Grid> + </Grid> + </Box> + </Grid> + </Box> + <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> + </Typography> + </Box> + </Box> + ); +}; + +export default SaleSummary; \ No newline at end of file diff --git a/package/components/molecules/Screen.tsx b/package/components/molecules/Screen.tsx deleted file mode 100644 index 7b952eb..0000000 --- a/package/components/molecules/Screen.tsx +++ /dev/null @@ -1,15 +0,0 @@ -"use client" -import { Box, Typography } from "@mui/material" -import React from "react" - -const Screen: React.FC = () => { - return ( - <Box sx={{ p: 2, backgroundColor: "#f5f5f5", borderRadius: 1, textAlign: "center" }}>{/* TODO: Logica para valores */} - <Typography variant="h6">Item Subtotal: $0.00</Typography> - <Typography variant="body1">Total Savings: $0.00</Typography> - <Typography variant="body1">Tax: $0.00</Typography> - </Box> - ) -} - -export default Screen diff --git a/package/components/molecules/UserHeader.tsx b/package/components/molecules/UserHeader.tsx new file mode 100644 index 0000000..835403b --- /dev/null +++ b/package/components/molecules/UserHeader.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { Box } from '@mui/material'; +import Grid2 from '@mui/material/Grid2'; +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'; + +const UserHeader: React.FC = () => { + const [user] = useAtom(UserAtom); + const { t } = useTranslation() + + + return ( + <Box sx={{mb: 1, boxShadow: '4', }}> + <Grid2 + container + justifyContent="space-between" + alignItems="center" + className="bg-blue-600 text-white p-4" + > + <Grid2> + <Typography variant="h6" className="font-bold"> + {user ? `${t("lbl_name_customer")}: ${user.name} ${user.lastName}` : `${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`} + </Typography> + </Grid2> + + <Grid2> + <Typography variant="body2" className="text-white"> + {user ? `${t("lbl_email_customer")}: ${user.email}` : `${t("lbl_email_customer")}: generic@example.com`} + </Typography> + </Grid2> + </Grid2> + </Box> + ); +}; + +export default UserHeader; diff --git a/package/components/organisms/ForgotPasswordForm.tsx b/package/components/organisms/Auth/ForgotPasswordForm.tsx similarity index 100% rename from package/components/organisms/ForgotPasswordForm.tsx rename to package/components/organisms/Auth/ForgotPasswordForm.tsx diff --git a/package/components/organisms/Auth/LoginForm.tsx b/package/components/organisms/Auth/LoginForm.tsx new file mode 100644 index 0000000..7d04b0f --- /dev/null +++ b/package/components/organisms/Auth/LoginForm.tsx @@ -0,0 +1,77 @@ +"use client"; +import { Box, Typography } from "@mui/material"; +import { useSetAtom } from "jotai"; +import { useRouter } from "next/navigation"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import Button from "@atoms/Button"; +import Input from "@atoms/Input"; +import Label from "@atoms/Label"; +import UserAtom from "@atoms/states/UserAtom"; +import { LoginResponse } from "@interfaces/ResponseInterfaces/Auth/UserResponseInterface"; +import { Login } from "@package/config/api/Admin/auth/Login/page"; + +const LoginForm: React.FC = () => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState<string | null>(null); + const router = useRouter(); + const { t } = useTranslation(); + const setUser = useSetAtom(UserAtom); // Hook para actualizar el estado global del usuario + + const handleLogin = async () => { + try { + const response: LoginResponse = await Login(email, password); + if (response.correct) { + // Almacenamos los datos del usuario logueado en el átomo global + setUser(response.object); + + // Redireccionamos a la página deseada + router.push("/selfcheckout/precheckout"); + } else { + setError(response.message || t("msj_Incorrect_credentials_try_again")); + } + } catch (err) { + setError(t("msj_Incorrect_credentials_try_again")); + } + }; + + return ( + <Box + component="form" + onSubmit={(e) => { + e.preventDefault(); + handleLogin(); + }} + sx={{ display: "flex", flexDirection: "column", gap: 3, width: "100%" }} + > + <Box> + <Label htmlFor={t("lbl_email")} text={t("lbl_email")} /> + <Input + type="email" + placeholder={t("msj_enter_your_email")} + value={email} + onChange={(e) => setEmail(e.target.value)} + autocomplete="email" + /> + </Box> + + <Box> + <Label htmlFor={t("lbl_password")} text={t("lbl_password")} /> + <Input + type="password" + placeholder={t("msj_enter_your_password")} + value={password} + onChange={(e) => setPassword(e.target.value)} + autocomplete="current-password" + /> + </Box> + + {error && <Typography color="error">{error}</Typography>} + + <Button label={t("lbl_login")} onClick={handleLogin} variant="primary" /> + </Box> + ); +}; + +export default LoginForm; diff --git a/package/components/organisms/ButtonPanel.tsx b/package/components/organisms/Checkout/ButtonPanel.tsx similarity index 98% rename from package/components/organisms/ButtonPanel.tsx rename to package/components/organisms/Checkout/ButtonPanel.tsx index dd30df4..aacb4d9 100644 --- a/package/components/organisms/ButtonPanel.tsx +++ b/package/components/organisms/Checkout/ButtonPanel.tsx @@ -170,14 +170,15 @@ const ButtonPanel: React.FC = () => { </Tooltip> </Grid> <Grid size={{ xs: 4 }}> - <Tooltip explainer={t("lbl_bag_fee")} side="top" withArrow> + <Tooltip explainer={t("msj_bag_fee")} side="top" withArrow> <Box sx={{ width: "100%" }}> <Button - label={t("msj_bag_fee")} + label= {t("lbl_bag_fee")} onClick={() => {}} variant="primary" className="full-size-button" startIcon={<ShoppingBagIcon />} + /> </Box> </Tooltip> diff --git a/package/components/organisms/Checkout/ControlPanel.tsx b/package/components/organisms/Checkout/ControlPanel.tsx new file mode 100644 index 0000000..c12cfbc --- /dev/null +++ b/package/components/organisms/Checkout/ControlPanel.tsx @@ -0,0 +1,47 @@ +"use client"; + +import Grid2 from '@mui/material/Grid2'; +import React from 'react'; +import ActionButtons from '@molecules/ActionButtons'; +import ProductSearch from '@molecules/ProductSearch'; +import SaleSummary from '@molecules/SaleSummary'; + +interface ControlPanelProps { + onVoidOrder: () => void; + onVoidLast: () => void; + onResume: () => void; + onSearchPLU: (plu: string) => void; + subtotal: number; + discount: number; + tax: number; + total: number; +} + +const ControlPanel: React.FC<ControlPanelProps> = ({ + onVoidOrder, + onVoidLast, + onResume, + onSearchPLU, + subtotal, + discount, + tax, + total +}) => { + return ( + <Grid2 container spacing={4}> + <Grid2 size={{xs:12}}> + <ActionButtons onVoidOrder={onVoidOrder} onVoidLast={onVoidLast} onResume={onResume} /> + </Grid2> + + <Grid2 size={{xs:12}}> + <ProductSearch onSearch={onSearchPLU} /> + </Grid2> + + <Grid2 size={{xs:12}}> + <SaleSummary subtotal={subtotal} discount={discount} tax={tax} total={total} /> + </Grid2> + </Grid2> + ); +}; + +export default ControlPanel; diff --git a/package/components/organisms/Checkout/ProductTable.tsx b/package/components/organisms/Checkout/ProductTable.tsx new file mode 100644 index 0000000..a63c5c5 --- /dev/null +++ b/package/components/organisms/Checkout/ProductTable.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { Box, Table, TableBody, TableCell, TableHead, TableRow } from '@mui/material'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +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[]; +} + +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() + + return ( + <Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}> + <UserHeader /> + <Box sx={{ flexGrow: 1, overflow: 'auto', p: 2, boxShadow: '2'}}> + <Table stickyHeader sx={{ + '& .MuiTableCell-root': { + padding: '8px 16px', + } + }}> + <TableHead> + <TableRow> + <TableCell>{t("tle_amount")}</TableCell> + <TableCell>{t("tle_product")}</TableCell> + <TableCell>{t("tle_unit_value")}</TableCell> + <TableCell>{t("tle_tax")}</TableCell> + <TableCell>{t("tle_discount")}</TableCell> + <TableCell>{t("tle_total")}</TableCell> + </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> + </TableRow> + ))} + </TableBody> + </Table> + </Box> + <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> + </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> + </TableRow> + </TableBody> + </Table> + </CustomCard> + </Box> + </Box> + ); +}; + +export default ProductTable; \ No newline at end of file diff --git a/package/components/organisms/Header.tsx b/package/components/organisms/Header.tsx deleted file mode 100644 index 1a4cbea..0000000 --- a/package/components/organisms/Header.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import MoreVertIcon from "@mui/icons-material/MoreVert"; -import PersonIcon from "@mui/icons-material/Person"; -import { Card, CardContent, IconButton, Typography } from "@mui/material"; -import Grid from "@mui/material/Grid2"; -import React, { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -interface HeaderProps { - sx?: object; - onToggleButtonPanel: () => void; -} - -const Header: React.FC<HeaderProps> = ({ sx, onToggleButtonPanel }) => { - const { t } = useTranslation(); - - const [currentDateTime, setCurrentDateTime] = useState(new Date()); - - useEffect(() => { - const interval = setInterval(() => { - setCurrentDateTime(new Date()); - }, 60000); - - return () => clearInterval(interval); - }, []); - - const formattedDate = currentDateTime.toLocaleDateString(undefined, { - weekday: "short", - day: "numeric", - month: "short", - }); - const formattedTime = currentDateTime.toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - }); - - return ( - <Card sx={{ ...sx, p: 0 }}> - <CardContent sx={{ padding: "1px 8px" }}> - <Grid container alignItems="center" justifyContent="space-between"> - <Grid> - <Typography variant="body2" color="text.primary" sx={{ fontWeight: "bold" }}> - Generic user - </Typography> - <Typography variant="body2" color="text.secondary"> - Add Member - </Typography> - </Grid> - - <Grid> - <Typography variant="body2" color="text.secondary"> - {formattedDate} - {formattedTime} - </Typography> - </Grid> - - <Grid> - <Typography variant="body2" color="text.secondary"> - {t("lbl_Store")}: <strong>8004</strong> | {t("lbl_register")}: <strong>317</strong> - </Typography> - </Grid> - - <Grid> - <Grid container alignItems="center"> - <PersonIcon sx={{ color: "text.secondary", fontSize: "18px", mr: 0.5 }} /> - <Typography variant="body2" color="text.secondary"> - {t("lbl_associate")} <strong>2</strong> - </Typography> - </Grid> - </Grid> - - <Grid> - <IconButton size="small" onClick={onToggleButtonPanel}> - <MoreVertIcon color="action" sx={{ fontSize: "18px" }} /> - </IconButton> - </Grid> - </Grid> - </CardContent> - </Card> - ); -}; - -export default Header; diff --git a/package/components/organisms/LoginForm.tsx b/package/components/organisms/LoginForm.tsx deleted file mode 100644 index 94e2a0f..0000000 --- a/package/components/organisms/LoginForm.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client" -import { Box, Typography } from "@mui/material" -import { useRouter } from "next/navigation" -import React, { useState } from "react" -import { useTranslation } from "react-i18next" -import Button from "@atoms/Button" -import Input from "@atoms/Input" -import Label from "@atoms/Label" -import {Login} from "@package/config/api/Admin/auth/Login/page" - -const LoginForm: React.FC = () => { - const [email, setEmail] = useState("") - const [password, setPassword] = useState("") - const [error, setError] = useState<string | null>(null) - const router = useRouter() - const { t } = useTranslation() - - const handleLogin = async () => { - try { - const response = await Login(email, password) - if (response.correct) { - router.push("/selfcheckout/precheckout") - } else { - setError(response.message || t("msj_Incorrect_credentials_try_again")) - } - } catch (err) { - setError(t("msj_Incorrect_credentials_try_again")) - } - } - - return ( - <Box - component="form" - onSubmit={(e) => { - e.preventDefault() - handleLogin() - }} - sx={{ display: "flex", flexDirection: "column", gap: 3, width: "100%" }} - > - <Box> - <Label htmlFor={t("lbl_email")} text={t("lbl_email")} /> - <Input - type="email" - placeholder={t("msj_enter_your_email")} - value={email} - onChange={(e) => setEmail(e.target.value)} - autocomplete="email" - /> - </Box> - - <Box> - <Label htmlFor={t("lbl_password")} text={t("lbl_password")} /> - <Input - type="password" - placeholder={t("msj_enter_your_password")} - value={password} - onChange={(e) => setPassword(e.target.value)} - autocomplete="current-password" - /> - </Box> - - {error && <Typography color="error">{error}</Typography>} - - <Button label={t("lbl_login")} onClick={handleLogin} variant="primary" /> - </Box> - ) -} - -export default LoginForm diff --git a/package/components/organisms/MainPanel.tsx b/package/components/organisms/MainPanel.tsx deleted file mode 100644 index f2ecaa1..0000000 --- a/package/components/organisms/MainPanel.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client" -import { Card, CardContent } from "@mui/material" -import Grid from "@mui/material/Grid2" -import React from "react" -import NumericPad from "@molecules/NumericPad" -import PluEntry from "@molecules/PluEntry" -import ProductMatch from "@molecules/ProductMatch" -import Screen from "@molecules/Screen" - -const MainPanel: React.FC = () => { - return ( - <Card sx={{ height: "100%" }}> - <CardContent sx={{ padding: "8px" }}> - <Grid container spacing={1} justifyContent="center" alignItems="stretch"> - <Grid size={{ xs: 12 }}> - <Screen /> - </Grid> - <Grid size={{ xs: 12 }}> - <ProductMatch /> - </Grid> - <Grid size={{ xs: 12 }}> - <PluEntry /> - </Grid> - <Grid size={{ xs: 12 }}> - <NumericPad /> - </Grid> - </Grid> - </CardContent> - </Card> - ) -} - -export default MainPanel diff --git a/package/components/organisms/Navbar.tsx b/package/components/organisms/Navbar.tsx index a1f247b..dd2495d 100644 --- a/package/components/organisms/Navbar.tsx +++ b/package/components/organisms/Navbar.tsx @@ -1,17 +1,23 @@ "use client"; import LanguageIcon from '@mui/icons-material/Language'; +import MoreVertIcon from "@mui/icons-material/MoreVert"; // Botón de 3 puntos verticales +import { Typography } from '@mui/material'; import Grid from '@mui/material/Grid2'; import IconButton from '@mui/material/IconButton'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; +import { useAtom } from 'jotai'; import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import UserAtom from '@atoms/states/UserAtom'; import i18n from '@package/utils/language/i18n'; const Navbar: React.FC = () => { const { t } = useTranslation(); const [language, setLanguage] = useState(i18n.language || 'en'); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); + const [user] = useAtom(UserAtom); + const [currentDateTime, setCurrentDateTime] = useState<Date | null>(null); const changeLanguage = (lng: string) => { i18n.changeLanguage(lng); @@ -29,8 +35,29 @@ const Navbar: React.FC = () => { useEffect(() => { setLanguage(i18n.language); + const interval = setInterval(() => { + setCurrentDateTime(new Date()); + }, 60000); + + setCurrentDateTime(new Date()); + + return () => clearInterval(interval); }, []); + const formattedDate = currentDateTime + ? currentDateTime.toLocaleDateString(undefined, { + weekday: "short", + day: "numeric", + month: "short", + }) + : ''; + const formattedTime = currentDateTime + ? currentDateTime.toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + }) + : ''; + return ( <Grid container @@ -39,10 +66,21 @@ const Navbar: React.FC = () => { className="bg-blue-600 text-white p-4" > <Grid> - <h1 className="text-white font-bold">{t('lbl_wellcome')}</h1> + <h1 className="text-white font-bold"> + {user ? `${t('lbl_wellcome')} ${user.name} ${user.lastName}` : t('lbl_wellcome')} + </h1> </Grid> + <Grid> - {/* IconButton with globe icon to open language menu */} + <Typography variant="body2" color="text.secondary" className="text-white"> + {formattedDate} - {formattedTime} + </Typography> + <Typography variant="body2" color="text.secondary" className="text-white"> + {t("lbl_Store")}: <strong>8004</strong> | {t("lbl_register")}: <strong>317</strong> + </Typography> + </Grid> + + <Grid container alignItems="center"> <IconButton aria-controls="language-menu" aria-haspopup="true" @@ -60,6 +98,17 @@ const Navbar: React.FC = () => { <MenuItem onClick={() => changeLanguage('en')}>English</MenuItem> <MenuItem onClick={() => changeLanguage('es')}>Español</MenuItem> </Menu> + + {/* Botón de 3 puntos verticales */} + <IconButton + size="small" + className="text-white ml-4" + onClick={() => { + console.log('Menú de opciones abierto'); + }} + > + <MoreVertIcon /> + </IconButton> </Grid> </Grid> ); diff --git a/package/components/organisms/ProductList.tsx b/package/components/organisms/ProductList.tsx deleted file mode 100644 index cd812a5..0000000 --- a/package/components/organisms/ProductList.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client" -import { Card, CardContent, Typography } from "@mui/material" -import React from "react" - -const ProductList: React.FC = () => { - return ( - <Card sx={{ height: "100%" }}> - {" "} - {/* Ajustar la altura de la Card */} - <CardContent> - <Typography variant="h6" color="text.primary"> - Lista de productos - </Typography> - <Typography variant="body1" color="text.secondary"> - Producto 1 - </Typography> - <Typography variant="body1" color="text.secondary"> - Producto 2 - </Typography> - <Typography variant="body1" color="text.secondary"> - Producto 3 - </Typography> - </CardContent> - </Card> - ) -} - -export default ProductList diff --git a/package/hooks/useClientSearchLogic.tsx b/package/hooks/useClientSearchLogic.tsx new file mode 100644 index 0000000..f266a17 --- /dev/null +++ b/package/hooks/useClientSearchLogic.tsx @@ -0,0 +1,96 @@ +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'; +import { GetTypeDni } from '@package/config/api/Customer/TypeDNI/GetTypeDNI/page'; + +export const useClientSearchLogic = () => { + const [idTypes, setIdTypes] = useState<CustomerDniTypeInterface[]>([]); + const [loading, setLoading] = useState(false); + const [isAffiliated, setIsAffiliated] = useState<boolean>(true); + const [error, setError] = useState<string | null>(null); + const [result, setResult] = useState<CustomerInterface[]>([]); + const [formData, setFormData] = useState({ + idType: '', + idNumber: '' + }); + + const setCustomer = useSetAtom(CustomerAtom); + + const fetchIdTypes = async () => { + setLoading(true); + try { + const response = await GetTypeDni({ + size: 100, + page: 0, + }) as CustomerDniTypeResponseInterface; + if (response?.object.list) { + setIdTypes(response.object.list); + } else { + setError('No se pudieron cargar los tipos de identificación'); + } + } catch (err) { + setError('Error al obtener los tipos de identificación'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchIdTypes(); + }, []); + + const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }); + setResult([]); + setIsAffiliated(true); + }; + + const handleSubmitForm = async (e?: React.FormEvent) => { + if (e) e.preventDefault(); + setLoading(true); + setError(null); + setResult([]); + + const response = await GetCustomer({ + idTypeCustomer: formData.idType, + dni: formData.idNumber + }) as CustomerResponseInterface; + + setLoading(false); + + if (response.correct && response.object.list.length > 0) { + const customerData = response.object.list[0]; + + if (customerData) { + setIsAffiliated(true); + setCustomer({ + ...customerData, + idTypeCustomer: formData.idType, + dni: formData.idNumber, + id: customerData.id || '', + }); + setResult(response.object.list); + } + } else { + setIsAffiliated(false); + setCustomer(null); + } + }; + + return { + idTypes, + loading, + error, + result, + formData, + handleInputChange, + handleSubmitForm, + isAffiliated + }; +};