Ref: modification components in selfcheckout

This commit is contained in:
Luis Felipe Cadavid 2024-09-30 15:17:38 -05:00
parent 251a6a2a6a
commit ab7a02bcca
40 changed files with 1315 additions and 557 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,6 @@
import { atom } from 'jotai';
import { UserResposeInterface } from '@interfaces/ResponseInterfaces/Auth/UserResponseInterface';
const UserAtom = atom<UserResposeInterface | null>(null);
export default UserAtom;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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