feat: integrate bottom toolbar store in various organization profile pages

- Added bottom toolbar functionality to the Schedules, Services, Templates, and WAP pages.
- Ensured the bottom toolbar is cleared on component unmount.
- Improved code formatting and consistency across components.
- Minor adjustments to state management and event handling for better clarity and performance.
This commit is contained in:
2026-09-22 11:20:53 -03:00
parent 0dd2998f5c
commit f5a2c0774e
24 changed files with 1537 additions and 665 deletions
@@ -23,6 +23,7 @@ import { CLIENT_ACCOUNT_MOVEMENT_TYPES } from "@core/Models/ClientAccountMovemen
import { getPaymentIcon } from "@core/app/components/PaymentIcons/PaymentIcons"; import { getPaymentIcon } from "@core/app/components/PaymentIcons/PaymentIcons";
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan"; import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
import { motion } from "motion/react"; import { motion } from "motion/react";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
oid: string; oid: string;
@@ -36,6 +37,7 @@ export default function ClientAccount() {
const alert = useAlert(); const alert = useAlert();
const SessionInfo = useSessionStore(); const SessionInfo = useSessionStore();
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation(); const { goTo } = useNavigation();
@@ -68,6 +70,8 @@ export default function ClientAccount() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org/profile/" + id + "/clients", url: "/admin/org/profile/" + id + "/clients",
title: "Clientes", title: "Clientes",
@@ -85,6 +89,10 @@ export default function ClientAccount() {
title: "Cuenta Corriente", title: "Cuenta Corriente",
icon: <AccountBalanceOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />, icon: <AccountBalanceOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
return () => {
bottomToolbar.clear();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -127,9 +135,22 @@ export default function ClientAccount() {
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}> <SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}>
<div style={{ maxWidth: "800px", margin: "0 auto", paddingBottom: "100px" }}> <div style={{ maxWidth: "800px", margin: "0 auto", paddingBottom: "100px" }}>
{/* Top Status Card */} {/* Top Status Card */}
<div style={{ position: "sticky", top: "70px", zIndex: 100, display: "flex", flexDirection: "column", background: "white", paddingBottom: "20px", paddingTop: "5px", margin: "0 -10px 30px -10px", paddingLeft: "10px", paddingRight: "10px" }}> <div
style={{
position: "sticky",
top: "70px",
zIndex: 100,
display: "flex",
flexDirection: "column",
background: "white",
paddingBottom: "20px",
paddingTop: "5px",
margin: "0 -10px 30px -10px",
paddingLeft: "10px",
paddingRight: "10px",
}}
>
{view && ( {view && (
<motion.div <motion.div
layout layout
@@ -138,26 +159,35 @@ export default function ClientAccount() {
opacity: 1, opacity: 1,
y: 0, y: 0,
padding: isMinimized ? "25px 50px" : "40px 20px", padding: isMinimized ? "25px 50px" : "40px 20px",
borderRadius: isMinimized ? "15px" : "20px" borderRadius: isMinimized ? "15px" : "20px",
}} }}
style={{ style={{
display: "flex", display: "flex",
flexDirection: isMinimized ? "row" : "column", flexDirection: isMinimized ? "row" : "column",
alignItems: "center", alignItems: "center",
justifyContent: isMinimized ? "space-between" : "center", justifyContent: isMinimized ? "space-between" : "center",
background: view.amount >= 0 background:
view.amount >= 0
? "linear-gradient(135deg, #1d976c 0%, #93f9b9 100%)" ? "linear-gradient(135deg, #1d976c 0%, #93f9b9 100%)"
: "linear-gradient(135deg, #cb2d3e 0%, #ef473a 100%)", : "linear-gradient(135deg, #cb2d3e 0%, #ef473a 100%)",
color: "white", color: "white",
boxShadow: isMinimized ? "0 4px 15px rgba(0,0,0,0.3)" : "0 10px 30px rgba(0,0,0,0.15)", boxShadow: isMinimized
? "0 4px 15px rgba(0,0,0,0.3)"
: "0 10px 30px rgba(0,0,0,0.15)",
position: "relative", position: "relative",
overflow: "hidden", overflow: "hidden",
transition: "box-shadow 0.3s ease" transition: "box-shadow 0.3s ease",
}} }}
> >
<IconButton <IconButton
onClick={() => setForceMinimized(!forceMinimized)} onClick={() => setForceMinimized(!forceMinimized)}
style={{ position: "absolute", top: "10px", left: "10px", color: "rgba(255,255,255,0.7)", zIndex: 10 }} style={{
position: "absolute",
top: "10px",
left: "10px",
color: "rgba(255,255,255,0.7)",
zIndex: 10,
}}
size="small" size="small"
title={forceMinimized ? "Maximizar" : "Minimizar"} title={forceMinimized ? "Maximizar" : "Minimizar"}
> >
@@ -165,38 +195,73 @@ export default function ClientAccount() {
</IconButton> </IconButton>
{/* Decorative background circles */} {/* Decorative background circles */}
<div style={{ position: "absolute", top: "-50px", right: "-50px", width: "150px", height: "150px", borderRadius: "50%", background: "rgba(255,255,255,0.1)" }}></div> <div
<div style={{ position: "absolute", bottom: "-30px", left: "-30px", width: "100px", height: "100px", borderRadius: "50%", background: "rgba(255,255,255,0.1)" }}></div> style={{
position: "absolute",
top: "-50px",
right: "-50px",
width: "150px",
height: "150px",
borderRadius: "50%",
background: "rgba(255,255,255,0.1)",
}}
></div>
<div
style={{
position: "absolute",
bottom: "-30px",
left: "-30px",
width: "100px",
height: "100px",
borderRadius: "50%",
background: "rgba(255,255,255,0.1)",
}}
></div>
<motion.div <motion.div
animate={{ animate={{
fontSize: isMinimized ? "14px" : "16px", fontSize: isMinimized ? "14px" : "16px",
marginBottom: isMinimized ? "0px" : "0px", marginBottom: isMinimized ? "0px" : "0px",
marginLeft: isMinimized ? "30px" : "0px" marginLeft: isMinimized ? "30px" : "0px",
}}
style={{
fontWeight: "bold",
opacity: 0.9,
textTransform: "uppercase",
letterSpacing: "1px",
}} }}
style={{ fontWeight: "bold", opacity: 0.9, textTransform: "uppercase", letterSpacing: "1px" }}
> >
Saldo Actual Saldo Actual
</motion.div> </motion.div>
<motion.div <motion.div
animate={{ animate={{
fontSize: isMinimized ? "24px" : "56px", fontSize: isMinimized ? "24px" : "56px",
margin: isMinimized ? "0px" : "10px 0 30px 0" margin: isMinimized ? "0px" : "10px 0 30px 0",
}}
style={{
fontWeight: "900",
textShadow: "0 2px 10px rgba(0,0,0,0.1)",
}} }}
style={{ fontWeight: "900", textShadow: "0 2px 10px rgba(0,0,0,0.1)" }}
> >
{formatPrice(view.amount)} {formatPrice(view.amount)}
</motion.div> </motion.div>
{!isMinimized && ( {!isMinimized && (
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
style={{ display: "flex", gap: "15px", flexDirection: "row", zIndex: 1 }} style={{
display: "flex",
gap: "15px",
flexDirection: "row",
zIndex: 1,
}}
>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
> >
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<IconButton <IconButton
sx={{ sx={{
backgroundColor: "rgba(255,255,255,0.2)", backgroundColor: "rgba(255,255,255,0.2)",
@@ -207,16 +272,29 @@ export default function ClientAccount() {
width: "110px", width: "110px",
height: "90px", height: "90px",
borderRadius: "15px", borderRadius: "15px",
"&:hover": { backgroundColor: "rgba(255,255,255,0.3)" } "&:hover": {
backgroundColor: "rgba(255,255,255,0.3)",
},
}} }}
onClick={() => add(CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT)} onClick={() =>
add(CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT)
}
> >
<DepositarIcon sx={{ fontSize: "32px", marginBottom: "5px" }} /> <DepositarIcon
<span style={{ fontSize: "14px", fontWeight: "bold" }}>Ingresar</span> sx={{ fontSize: "32px", marginBottom: "5px" }}
/>
<span
style={{ fontSize: "14px", fontWeight: "bold" }}
>
Ingresar
</span>
</IconButton> </IconButton>
</motion.div> </motion.div>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}> <motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<IconButton <IconButton
sx={{ sx={{
backgroundColor: "rgba(255,255,255,0.2)", backgroundColor: "rgba(255,255,255,0.2)",
@@ -227,16 +305,29 @@ export default function ClientAccount() {
width: "110px", width: "110px",
height: "90px", height: "90px",
borderRadius: "15px", borderRadius: "15px",
"&:hover": { backgroundColor: "rgba(255,255,255,0.3)" } "&:hover": {
backgroundColor: "rgba(255,255,255,0.3)",
},
}} }}
onClick={() => add(CLIENT_ACCOUNT_MOVEMENT_TYPES.WITHDRAWAL)} onClick={() =>
add(CLIENT_ACCOUNT_MOVEMENT_TYPES.WITHDRAWAL)
}
> >
<RetirarIcon sx={{ fontSize: "32px", marginBottom: "5px" }} /> <RetirarIcon
<span style={{ fontSize: "14px", fontWeight: "bold" }}>Retirar</span> sx={{ fontSize: "32px", marginBottom: "5px" }}
/>
<span
style={{ fontSize: "14px", fontWeight: "bold" }}
>
Retirar
</span>
</IconButton> </IconButton>
</motion.div> </motion.div>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}> <motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<IconButton <IconButton
sx={{ sx={{
backgroundColor: "white", backgroundColor: "white",
@@ -248,12 +339,26 @@ export default function ClientAccount() {
height: "90px", height: "90px",
borderRadius: "15px", borderRadius: "15px",
boxShadow: "0 4px 15px rgba(0,0,0,0.1)", boxShadow: "0 4px 15px rgba(0,0,0,0.1)",
"&:hover": { backgroundColor: "#f8f9fa" } "&:hover": { backgroundColor: "#f8f9fa" },
}} }}
onClick={() => goTo("/admin/org/" + id + "/client/" + clientId + "/account/movements")} onClick={() =>
goTo(
"/admin/org/" +
id +
"/client/" +
clientId +
"/account/movements",
)
}
> >
<MovimientosIcon sx={{ fontSize: "32px", marginBottom: "5px" }} /> <MovimientosIcon
<span style={{ fontSize: "14px", fontWeight: "bold" }}>Historial</span> sx={{ fontSize: "32px", marginBottom: "5px" }}
/>
<span
style={{ fontSize: "14px", fontWeight: "bold" }}
>
Historial
</span>
</IconButton> </IconButton>
</motion.div> </motion.div>
</motion.div> </motion.div>
@@ -263,13 +368,40 @@ export default function ClientAccount() {
</div> </div>
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}> <div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "15px", padding: "0 10px" }}> <div
<div style={{ fontSize: "20px", fontWeight: "bold", color: "var(--black-light)" }}> style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "15px",
padding: "0 10px",
}}
>
<div
style={{
fontSize: "20px",
fontWeight: "bold",
color: "var(--black-light)",
}}
>
Últimos Movimientos Últimos Movimientos
</div> </div>
<div <div
style={{ fontSize: "14px", color: "var(--wine-dark)", cursor: "pointer", fontWeight: "bold" }} style={{
onClick={() => goTo("/admin/org/" + id + "/client/" + clientId + "/account/movements")} fontSize: "14px",
color: "var(--wine-dark)",
cursor: "pointer",
fontWeight: "bold",
}}
onClick={() =>
goTo(
"/admin/org/" +
id +
"/client/" +
clientId +
"/account/movements",
)
}
> >
Ver todos Ver todos
</div> </div>
@@ -294,42 +426,79 @@ export default function ClientAccount() {
boxShadow: "0 2px 8px rgba(0,0,0,0.05)", boxShadow: "0 2px 8px rgba(0,0,0,0.05)",
}} }}
> >
<div style={{ display: "flex", alignItems: "center", gap: "15px" }}> <div
<div style={{ style={{
display: "flex",
alignItems: "center",
gap: "15px",
}}
>
<div
style={{
width: "45px", width: "45px",
height: "45px", height: "45px",
borderRadius: "12px", borderRadius: "12px",
backgroundColor: movement.amount > 0 ? "rgba(40, 167, 69, 0.1)" : "rgba(220, 53, 69, 0.1)", backgroundColor:
movement.amount > 0
? "rgba(40, 167, 69, 0.1)"
: "rgba(220, 53, 69, 0.1)",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
color: movement.amount > 0 ? "#28a745" : "#dc3545" color:
}}> movement.amount > 0 ? "#28a745" : "#dc3545",
}}
>
{getPaymentIcon(movement.paymentMethod)} {getPaymentIcon(movement.paymentMethod)}
</div> </div>
<div> <div>
<div style={{ fontSize: "16px", fontWeight: "bold", color: "#333", marginBottom: "3px" }}> <div
style={{
fontSize: "16px",
fontWeight: "bold",
color: "#333",
marginBottom: "3px",
}}
>
{movement.description} {movement.description}
</div> </div>
<div style={{ fontSize: "13px", color: "#888" }}> <div style={{ fontSize: "13px", color: "#888" }}>
{dayjs(movement.createdAt).format("DD MMMM YYYY • HH:mm")} {dayjs(movement.createdAt).format(
"DD MMMM YYYY • HH:mm",
)}
</div> </div>
</div> </div>
</div> </div>
<div style={{ <div
style={{
fontSize: "18px", fontSize: "18px",
fontWeight: "900", fontWeight: "900",
color: movement.amount > 0 ? "#28a745" : "#dc3545", color: movement.amount > 0 ? "#28a745" : "#dc3545",
}}> }}
{movement.amount > 0 ? "+" : ""}{formatPrice(movement.amount)} >
{movement.amount > 0 ? "+" : ""}
{formatPrice(movement.amount)}
</div> </div>
</motion.div> </motion.div>
))} ))}
</div> </div>
) : ( ) : (
<div style={{ textAlign: "center", padding: "40px", backgroundColor: "#fff", borderRadius: "15px", color: "#888", border: "1px dashed #ccc" }}> <div
<AccountBalanceOutlinedIcon sx={{ fontSize: "48px", color: "#ddd", marginBottom: "10px" }} /> style={{
<div style={{ fontSize: "16px", fontWeight: "bold", color: "#666" }}> textAlign: "center",
padding: "40px",
backgroundColor: "#fff",
borderRadius: "15px",
color: "#888",
border: "1px dashed #ccc",
}}
>
<AccountBalanceOutlinedIcon
sx={{ fontSize: "48px", color: "#ddd", marginBottom: "10px" }}
/>
<div
style={{ fontSize: "16px", fontWeight: "bold", color: "#666" }}
>
Aún no hay movimientos Aún no hay movimientos
</div> </div>
<div style={{ fontSize: "14px" }}> <div style={{ fontSize: "14px" }}>
@@ -1,9 +1,6 @@
"use client"; "use client";
import { useAlert } from "@core/Store/Alert.Store"; import { useAlert } from "@core/Store/Alert.Store";
import { import { useEventHandlerStore, EVENT_TYPES } from "@core/Store/EventHandler.Store";
useEventHandlerStore,
EVENT_TYPES,
} from "@core/Store/EventHandler.Store";
import { useLocationStore } from "@core/Store/Location.Store"; import { useLocationStore } from "@core/Store/Location.Store";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -14,6 +11,7 @@ import ServiceItem from "@core/app/components/ServiceItem/ServiceItem";
import Location from "@core/app/components/Location/Location"; import Location from "@core/app/components/Location/Location";
import { useNavigation } from "@core/app/hooks/goto"; import { useNavigation } from "@core/app/hooks/goto";
import { useMenuStore } from "@core/Store/Menu.Store"; import { useMenuStore } from "@core/Store/Menu.Store";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
oid: string; oid: string;
@@ -26,12 +24,12 @@ export default function ClientAppointments() {
const clientId = data.clientId; const clientId = data.clientId;
const alert = useAlert(); const alert = useAlert();
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const menu = useMenuStore(); const menu = useMenuStore();
const { goTo } = useNavigation(); const { goTo } = useNavigation();
const [appintments, setAppointments] = const [appintments, setAppointments] = useState<AppointmentAdminByClientView>({
useState<AppointmentAdminByClientView>({
events: [], events: [],
}); });
@@ -44,6 +42,8 @@ export default function ClientAppointments() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org/profile/" + id + "/clients", url: "/admin/org/profile/" + id + "/clients",
title: "Clientes", title: "Clientes",
@@ -61,6 +61,10 @@ export default function ClientAppointments() {
title: "Turnos reservados", title: "Turnos reservados",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />, icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
return () => {
bottomToolbar.clear();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -89,15 +93,17 @@ export default function ClientAppointments() {
}} }}
> >
{appintments.events.length === 0 ? ( {appintments.events.length === 0 ? (
<div style={{ <div
style={{
gridColumn: "1 / -1", gridColumn: "1 / -1",
textAlign: "center", textAlign: "center",
padding: "40px 20px", padding: "40px 20px",
background: "#fff", background: "#fff",
borderRadius: "12px", borderRadius: "12px",
boxShadow: "0 4px 15px rgba(0,0,0,0.02)", boxShadow: "0 4px 15px rgba(0,0,0,0.02)",
color: "#7f8c8d" color: "#7f8c8d",
}}> }}
>
No hay turnos registrados para este cliente. No hay turnos registrados para este cliente.
</div> </div>
) : ( ) : (
@@ -113,7 +119,7 @@ export default function ClientAppointments() {
"/client/" + "/client/" +
clientId + clientId +
"/appointments/" + "/appointments/" +
event.id event.id,
); );
}} }}
/> />
@@ -15,6 +15,7 @@ import { validatePhone } from "@core/helpers/validatePhone";
import { useConfirmStore } from "@core/Store/Confirm.Store"; import { useConfirmStore } from "@core/Store/Confirm.Store";
import Switch from "@core/app/components/Switch/Switch"; import Switch from "@core/app/components/Switch/Switch";
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
oid: string; oid: string;
@@ -29,6 +30,7 @@ export default function ClientCreate() {
const confirm = useConfirmStore(); const confirm = useConfirmStore();
const SessionInfo = useSessionStore(); const SessionInfo = useSessionStore();
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const [firstName, setFirstName] = useState(""); const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState(""); const [lastName, setLastName] = useState("");
@@ -46,6 +48,8 @@ export default function ClientCreate() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org/profile/" + id + "/clients", url: "/admin/org/profile/" + id + "/clients",
title: "Clientes", title: "Clientes",
@@ -63,6 +67,10 @@ export default function ClientCreate() {
title: "Editar", title: "Editar",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />, icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
return () => {
bottomToolbar.clear();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -92,7 +100,7 @@ export default function ClientCreate() {
confirm.show( confirm.show(
validationPhoneResult.message, validationPhoneResult.message,
() => {}, () => {},
() => {} () => {},
); );
eventHandler.setEventType(EVENT_TYPES.SLEEP); eventHandler.setEventType(EVENT_TYPES.SLEEP);
return; return;
@@ -112,7 +120,7 @@ export default function ClientCreate() {
}) })
.then(() => { .then(() => {
alert.showSuccess( alert.showSuccess(
`Información del cliente ${firstName} ${lastName} actualizada` `Información del cliente ${firstName} ${lastName} actualizada`,
); );
setTimeout(() => { setTimeout(() => {
goTo("/admin/org/" + id + "/client/" + clientId + "/profile"); goTo("/admin/org/" + id + "/client/" + clientId + "/profile");
@@ -180,7 +188,9 @@ export default function ClientCreate() {
/> />
<div style={{ marginTop: "15px" }}></div> <div style={{ marginTop: "15px" }}></div>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}> <Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Typography variant="body1" sx={{ fontWeight: "bold", paddingLeft: "10px" }}>Baneado (No puede reservar)</Typography> <Typography variant="body1" sx={{ fontWeight: "bold", paddingLeft: "10px" }}>
Baneado (No puede reservar)
</Typography>
<Switch <Switch
checked={banned} checked={banned}
onChange={(checked) => { onChange={(checked) => {
@@ -18,6 +18,7 @@ import { ClientMergeParams, OrganizationClientView } from "@core/Models/Clients.
import Button from "@core/app/components/Button/Button"; import Button from "@core/app/components/Button/Button";
import MaterialAlert from "@mui/material/Alert"; import MaterialAlert from "@mui/material/Alert";
import { useConfirmStore } from "@core/Store/Confirm.Store"; import { useConfirmStore } from "@core/Store/Confirm.Store";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
oid: string; oid: string;
@@ -32,6 +33,7 @@ export default function ClientCreate() {
const confirm = useConfirmStore(); const confirm = useConfirmStore();
const SessionInfo = useSessionStore(); const SessionInfo = useSessionStore();
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation(); const { goTo } = useNavigation();
@@ -53,7 +55,7 @@ export default function ClientCreate() {
}); });
const [clientMergeFilterData, setClientMergeFilterData] = useState<TextObjectFilterResult[]>( const [clientMergeFilterData, setClientMergeFilterData] = useState<TextObjectFilterResult[]>(
[] [],
); );
const [clientMergeId, setClientMergeId] = useState<string>(""); const [clientMergeId, setClientMergeId] = useState<string>("");
@@ -94,7 +96,7 @@ export default function ClientCreate() {
() => { () => {
mergeClients(); mergeClients();
}, },
() => {} () => {},
); );
}; };
@@ -105,6 +107,8 @@ export default function ClientCreate() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org/profile/" + id + "/clients", url: "/admin/org/profile/" + id + "/clients",
title: "Clientes", title: "Clientes",
@@ -122,6 +126,10 @@ export default function ClientCreate() {
title: "Fusionar clientes", title: "Fusionar clientes",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />, icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
return () => {
bottomToolbar.clear();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -11,6 +11,7 @@ import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import NotificationsIcon from "@mui/icons-material/NotificationsOutlined"; import NotificationsIcon from "@mui/icons-material/NotificationsOutlined";
import MaterialAlert from "@mui/material/Alert"; import MaterialAlert from "@mui/material/Alert";
import styles from "./NotificationsPage.module.css"; import styles from "./NotificationsPage.module.css";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
oid: string; oid: string;
@@ -20,7 +21,7 @@ type UrlData = {
const CHANNELS: { key: NotificationChannel; label: string }[] = [ const CHANNELS: { key: NotificationChannel; label: string }[] = [
{ key: "whatsapp", label: "WhatsApp" }, { key: "whatsapp", label: "WhatsApp" },
{ key: "email", label: "Email" }, { key: "email", label: "Email" },
{ key: "system", label: "Sistema" } { key: "system", label: "Sistema" },
]; ];
type ChannelPreference = "default" | "preferred" | "muted"; type ChannelPreference = "default" | "preferred" | "muted";
@@ -30,34 +31,41 @@ export default function NotificationsPage() {
const oid = data.oid; const oid = data.oid;
const clientId = data.clientId; const clientId = data.clientId;
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const store = useNotificationPreferencesStore(); const store = useNotificationPreferencesStore();
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org", url: "/admin/org",
title: "Organizaciones", title: "Organizaciones",
icon: null icon: null,
}); });
location.add({ location.add({
url: "/admin/org/profile/" + oid, url: "/admin/org/profile/" + oid,
title: "Organización", title: "Organización",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" /> icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
}); });
location.add({ location.add({
url: "/admin/org/" + oid + "/client/" + clientId + "/profile", url: "/admin/org/" + oid + "/client/" + clientId + "/profile",
title: "Cliente", title: "Cliente",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} /> icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
location.add({ location.add({
url: "", url: "",
title: "Notificaciones", title: "Notificaciones",
icon: <NotificationsIcon sx={{ fontSize: "32px", mr: "10px" }} /> icon: <NotificationsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
return () => {
bottomToolbar.clear();
};
}, [oid, clientId]); }, [oid, clientId]);
const getChannelPreference = (channel: NotificationChannel): ChannelPreference => { const getChannelPreference = (channel: NotificationChannel): ChannelPreference => {
@@ -81,7 +89,7 @@ export default function NotificationsPage() {
mutedChannels: mutedChannels:
preference === "muted" preference === "muted"
? [...store.formData.mutedChannels.filter((c) => c !== channel), channel] ? [...store.formData.mutedChannels.filter((c) => c !== channel), channel]
: store.formData.mutedChannels.filter((c) => c !== channel) : store.formData.mutedChannels.filter((c) => c !== channel),
}); });
}; };
@@ -91,7 +99,9 @@ export default function NotificationsPage() {
<div className={styles.container}> <div className={styles.container}>
<MaterialAlert severity="info" sx={{ marginBottom: "0" }}> <MaterialAlert severity="info" sx={{ marginBottom: "0" }}>
<b>Preferencias del cliente:</b> Configure cómo esta organización debe comunicarse con este cliente. Si deja un canal en estándar, se usará la política general de la organización. <b>Preferencias del cliente:</b> Configure cómo esta organización debe
comunicarse con este cliente. Si deja un canal en estándar, se usará la política
general de la organización.
</MaterialAlert> </MaterialAlert>
<div className={styles.channels}> <div className={styles.channels}>
@@ -102,24 +112,32 @@ export default function NotificationsPage() {
<div> <div>
<div className={styles.channelTitle}>{channel.label}</div> <div className={styles.channelTitle}>{channel.label}</div>
<div className={styles.channelDescription}> <div className={styles.channelDescription}>
{selectedPreference === "preferred" && "Priorizar este canal para el cliente."} {selectedPreference === "preferred" &&
{selectedPreference === "muted" && "No enviar notificaciones por este canal."} "Priorizar este canal para el cliente."}
{selectedPreference === "default" && "Usar la configuración general de la organización."} {selectedPreference === "muted" &&
"No enviar notificaciones por este canal."}
{selectedPreference === "default" &&
"Usar la configuración general de la organización."}
</div> </div>
</div> </div>
<div className={styles.options}> <div className={styles.options}>
{[ {[
{ key: "default" as ChannelPreference, label: "Estándar" }, { key: "default" as ChannelPreference, label: "Estándar" },
{ key: "preferred" as ChannelPreference, label: "Preferido" }, {
{ key: "muted" as ChannelPreference, label: "Silenciado" } key: "preferred" as ChannelPreference,
label: "Preferido",
},
{ key: "muted" as ChannelPreference, label: "Silenciado" },
].map((option) => { ].map((option) => {
const isSelected = selectedPreference === option.key; const isSelected = selectedPreference === option.key;
return ( return (
<button <button
key={option.key} key={option.key}
type="button" type="button"
onClick={() => selectPreference(channel.key, option.key)} onClick={() =>
selectPreference(channel.key, option.key)
}
className={`${styles.optionButton} ${isSelected ? styles.optionButtonActive : ""}`} className={`${styles.optionButton} ${isSelected ? styles.optionButtonActive : ""}`}
> >
{option.label} {option.label}
@@ -8,6 +8,7 @@ import { useLocationStore } from "@store/Location.Store";
import { useNavigation } from "@hooks/goto"; import { useNavigation } from "@hooks/goto";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
oid: string; oid: string;
@@ -19,11 +20,14 @@ export default function ClientProfile() {
const clientId = data.clientId; const clientId = data.clientId;
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const { goTo } = useNavigation(); const { goTo } = useNavigation();
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org", url: "/admin/org",
title: "Mis organizaciones", title: "Mis organizaciones",
@@ -33,9 +37,7 @@ export default function ClientProfile() {
location.add({ location.add({
url: "/admin/org/profile/" + oid, url: "/admin/org/profile/" + oid,
title: "Organizacion", title: "Organizacion",
icon: ( icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
<StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />
),
}); });
location.add({ location.add({
@@ -43,6 +45,10 @@ export default function ClientProfile() {
title: "Perfil del cliente", title: "Perfil del cliente",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />, icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
return () => {
bottomToolbar.clear();
};
}, []); }, []);
return ( return (
@@ -59,79 +65,35 @@ export default function ClientProfile() {
<ButtonOption <ButtonOption
text="Editar" text="Editar"
icon="/profile-icon.svg" icon="/profile-icon.svg"
onClick={() => onClick={() => goTo("/admin/org/" + oid + "/client/" + clientId + "/edit")}
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/edit"
)
}
/> />
<ButtonOption <ButtonOption
text="Turnos" text="Turnos"
icon="/profile-icon.svg" icon="/profile-icon.svg"
onClick={() => onClick={() =>
goTo( goTo("/admin/org/" + oid + "/client/" + clientId + "/appointments")
"/admin/org/" +
oid +
"/client/" +
clientId +
"/appointments"
)
} }
/> />
<ButtonOption <ButtonOption
text="Fusionar Duplicados" text="Fusionar Duplicados"
icon="/client-merge-icon.svg" icon="/client-merge-icon.svg"
onClick={() => onClick={() => goTo("/admin/org/" + oid + "/client/" + clientId + "/merge")}
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/merge"
)
}
/> />
<ButtonOption <ButtonOption
text="Repeticiones" text="Repeticiones"
icon="/repeat-icon.svg" icon="/repeat-icon.svg"
onClick={() => onClick={() => goTo("/admin/org/" + oid + "/client/" + clientId + "/repeats")}
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/repeats"
)
}
/> />
<ButtonOption <ButtonOption
text="Cuenta" text="Cuenta"
icon="/account-icon.svg" icon="/account-icon.svg"
onClick={() => onClick={() => goTo("/admin/org/" + oid + "/client/" + clientId + "/account")}
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/account"
)
}
/> />
<ButtonOption <ButtonOption
text="Notificaciones" text="Notificaciones"
icon="/notifications-icon.svg" icon="/notifications-icon.svg"
onClick={() => onClick={() =>
goTo( goTo("/admin/org/" + oid + "/client/" + clientId + "/notifications")
"/admin/org/" +
oid +
"/client/" +
clientId +
"/notifications"
)
} }
/> />
</div> </div>
@@ -17,6 +17,7 @@ import { deleteRepeat, getRepeatsByClient } from "../Client.Service";
import { DeleteRepeatsParams, FindRepeatsParams, RepeatView } from "@core/Models/Repeats.model"; import { DeleteRepeatsParams, FindRepeatsParams, RepeatView } from "@core/Models/Repeats.model";
import style from "./repeats.module.css"; import style from "./repeats.module.css";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
oid: string; oid: string;
@@ -31,6 +32,7 @@ export default function ClientCreate() {
const confirm = useConfirmStore(); const confirm = useConfirmStore();
const SessionInfo = useSessionStore(); const SessionInfo = useSessionStore();
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const [repeats, setRepeats] = useState<RepeatView[]>([]); const [repeats, setRepeats] = useState<RepeatView[]>([]);
@@ -52,6 +54,8 @@ export default function ClientCreate() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org/profile/" + id + "/clients", url: "/admin/org/profile/" + id + "/clients",
title: "Clientes", title: "Clientes",
@@ -69,6 +73,10 @@ export default function ClientCreate() {
title: "Repeticiones", title: "Repeticiones",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />, icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
return () => {
bottomToolbar.clear();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -162,7 +170,7 @@ export default function ClientCreate() {
() => { () => {
handleDelete(repeat.id); handleDelete(repeat.id);
}, },
() => {} () => {},
); );
}} }}
> >
@@ -27,6 +27,7 @@ import TextObjectFilter from "@components/TextObjectFilter/TextObjectFilter";
import SelectLocationMap, { import SelectLocationMap, {
LocationCoordinates, LocationCoordinates,
} from "@core/app/components/SelectLocationMap/SelectLocationMap"; } from "@core/app/components/SelectLocationMap/SelectLocationMap";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
id: string; id: string;
@@ -36,6 +37,7 @@ export default function OrganizationEdit() {
const data = useParams<UrlData>(); const data = useParams<UrlData>();
const id = data.id; const id = data.id;
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore(); const SessionInfo = useSessionStore();
const alert = useAlert(); const alert = useAlert();
@@ -140,7 +142,7 @@ export default function OrganizationEdit() {
}); });
const [localidadesFilterData, setLocalidadesFilterData] = useState<TextObjectFilterResult[]>( const [localidadesFilterData, setLocalidadesFilterData] = useState<TextObjectFilterResult[]>(
[] [],
); );
const [selectedLocalidad, setSelectedLocalidad] = useState<string>(""); const [selectedLocalidad, setSelectedLocalidad] = useState<string>("");
@@ -188,6 +190,8 @@ export default function OrganizationEdit() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org", url: "/admin/org",
title: "Organizaciones", title: "Organizaciones",
@@ -205,6 +209,10 @@ export default function OrganizationEdit() {
title: "Ubicación", title: "Ubicación",
icon: <OrgConfigIcon sx={{ fontSize: "32px", mr: "10px" }} />, icon: <OrgConfigIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
return () => {
bottomToolbar.clear();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -14,6 +14,7 @@ import Button from "@core/app/components/Button/Button";
import { useConfirmStore } from "@core/Store/Confirm.Store"; import { useConfirmStore } from "@core/Store/Confirm.Store";
import { deleteOrganization } from "./advanced.Service"; import { deleteOrganization } from "./advanced.Service";
import { useNavigation } from "@core/app/hooks/goto"; import { useNavigation } from "@core/app/hooks/goto";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
id: string; id: string;
@@ -23,6 +24,7 @@ export default function OrganizationAdvancedEdit() {
const data = useParams<UrlData>(); const data = useParams<UrlData>();
const id = data.id; const id = data.id;
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore(); const SessionInfo = useSessionStore();
const alert = useAlert(); const alert = useAlert();
@@ -33,6 +35,8 @@ export default function OrganizationAdvancedEdit() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org", url: "/admin/org",
title: "Organizaciones", title: "Organizaciones",
@@ -50,6 +54,10 @@ export default function OrganizationAdvancedEdit() {
title: "Avanzado", title: "Avanzado",
icon: <OrgConfigIcon sx={{ fontSize: "32px", mr: "10px" }} />, icon: <OrgConfigIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
return () => {
bottomToolbar.clear();
};
}, []); }, []);
const eliminarHandler = () => { const eliminarHandler = () => {
@@ -73,7 +81,7 @@ export default function OrganizationAdvancedEdit() {
}, },
() => { () => {
eventHandler.setEventType(EVENT_TYPES.SLEEP); eventHandler.setEventType(EVENT_TYPES.SLEEP);
} },
); );
} else { } else {
alert.showError("La confirmación no es correcta"); alert.showError("La confirmación no es correcta");
@@ -12,6 +12,7 @@ import { UserAssistanceInfo } from "@core/Models/User.model";
import Link from "next/link"; import Link from "next/link";
import { getAssistanceUserInfo } from "./Assistance.Service"; import { getAssistanceUserInfo } from "./Assistance.Service";
import AssistanceIcon from "@mui/icons-material/HeadsetMicOutlined"; import AssistanceIcon from "@mui/icons-material/HeadsetMicOutlined";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
id: string; id: string;
@@ -23,6 +24,7 @@ export default function CollaboratorsAdd() {
const alert = useAlert(); const alert = useAlert();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const SessionInfo = useSessionStore(); const SessionInfo = useSessionStore();
const [user, setUser] = useState<UserAssistanceInfo | null>(null); const [user, setUser] = useState<UserAssistanceInfo | null>(null);
const [findEmail, setFindEmail] = useState<string>(""); const [findEmail, setFindEmail] = useState<string>("");
@@ -34,6 +36,8 @@ export default function CollaboratorsAdd() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org/profile/" + id, url: "/admin/org/profile/" + id,
title: "Organizacion", title: "Organizacion",
@@ -45,6 +49,10 @@ export default function CollaboratorsAdd() {
title: "Asistencia", title: "Asistencia",
icon: <AssistanceIcon sx={{ fontSize: "32px", mr: "10px" }} />, icon: <AssistanceIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
return () => {
bottomToolbar.clear();
};
}, []); }, []);
const findUser = async () => { const findUser = async () => {
@@ -10,6 +10,7 @@ import { CompanyEmployeesView } from "@models/Collaborators.model";
import { loadCollaboratorsByCompanyId } from "./Collaborators.Service"; import { loadCollaboratorsByCompanyId } from "./Collaborators.Service";
import { useAlert } from "@store/Alert.Store"; import { useAlert } from "@store/Alert.Store";
import CollaboratorItem from "@core/app/components/CollaboratorItem/CollaboratorItem"; import CollaboratorItem from "@core/app/components/CollaboratorItem/CollaboratorItem";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
id: string; id: string;
@@ -19,6 +20,7 @@ export default function OrganizationProfile() {
const data = useParams<UrlData>(); const data = useParams<UrlData>();
const id = data.id; const id = data.id;
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const alert = useAlert(); const alert = useAlert();
const { goTo } = useNavigation(); const { goTo } = useNavigation();
@@ -31,6 +33,8 @@ export default function OrganizationProfile() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org", url: "/admin/org",
title: "Mis organizaciones", title: "Mis organizaciones",
@@ -60,6 +64,10 @@ export default function OrganizationProfile() {
.finally(() => { .finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD); eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
}); });
return () => {
bottomToolbar.clear();
};
}, []); }, []);
const handleCollaboratorClick = (collaboratorId: string) => { const handleCollaboratorClick = (collaboratorId: string) => {
@@ -35,6 +35,7 @@ import TextObjectFilterItem from "@core/app/components/TextObjectFilterItem/Text
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter"; import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
import { useFilterStore } from "@core/Store/Filter.Store"; import { useFilterStore } from "@core/Store/Filter.Store";
import filterObjects from "@components/TextObjectFilter/Filter.Service"; import filterObjects from "@components/TextObjectFilter/Filter.Service";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
id: string; id: string;
@@ -112,6 +113,7 @@ export default function DiscountsEmailGrid() {
const data = useParams<UrlData>(); const data = useParams<UrlData>();
const oid = data.id; const oid = data.id;
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const SessionInfo = useSessionStore(); const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const alert = useAlert(); const alert = useAlert();
@@ -151,7 +153,9 @@ export default function DiscountsEmailGrid() {
render: (value: DiscountEmailRow) => ( render: (value: DiscountEmailRow) => (
<div style={{ paddingBottom: "10px" }}> <div style={{ paddingBottom: "10px" }}>
<div> <div>
<div style={{ paddingLeft: "10px", paddingTop: "10px", fontWeight: "bold" }}> <div
style={{ paddingLeft: "10px", paddingTop: "10px", fontWeight: "bold" }}
>
{value.discountName + " (" + value.discountCode + ")"} {value.discountName + " (" + value.discountCode + ")"}
</div> </div>
<div style={{ paddingLeft: "10px", paddingTop: "0px", fontWeight: "bold" }}> <div style={{ paddingLeft: "10px", paddingTop: "0px", fontWeight: "bold" }}>
@@ -206,8 +210,10 @@ export default function DiscountsEmailGrid() {
const [pageSize] = useState<number>( const [pageSize] = useState<number>(
parseInt( parseInt(
process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE ? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE : "10" process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
) ? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
: "10",
),
); );
const [discounts, setDiscounts] = useState<PaginateDiscountsEmailViewResults>(); const [discounts, setDiscounts] = useState<PaginateDiscountsEmailViewResults>();
const discountsRef = useRef(discounts); const discountsRef = useRef(discounts);
@@ -219,6 +225,8 @@ export default function DiscountsEmailGrid() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org", url: "/admin/org",
title: "Mis organizaciones", title: "Mis organizaciones",
@@ -238,6 +246,10 @@ export default function DiscountsEmailGrid() {
}); });
loadDiscountsEmail(1, pageSize); loadDiscountsEmail(1, pageSize);
return () => {
bottomToolbar.clear();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -267,7 +279,7 @@ export default function DiscountsEmailGrid() {
setDiscountId={setDiscountId} setDiscountId={setDiscountId}
filterEmail={filterEmail} filterEmail={filterEmail}
setFilterEmail={setFilterEmail} setFilterEmail={setFilterEmail}
/> />,
); );
filter.setOnFilter(() => { filter.setOnFilter(() => {
setSearchByFilters(true); setSearchByFilters(true);
@@ -327,7 +339,7 @@ export default function DiscountsEmailGrid() {
alert.showError(err.format()); alert.showError(err.format());
}); });
}, },
() => {} () => {},
); );
}; };
@@ -22,6 +22,7 @@ import turnosXpressTheme from "@core/app/theme/turnosXpress";
import { ButtonGroup } from "@mui/material"; import { ButtonGroup } from "@mui/material";
import ButtonMaterial from "@mui/material/Button"; import ButtonMaterial from "@mui/material/Button";
import { useNavigation } from "@core/app/hooks/goto"; import { useNavigation } from "@core/app/hooks/goto";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
id: string; id: string;
@@ -33,6 +34,7 @@ export default function DiscountsGrid() {
const data = useParams<UrlData>(); const data = useParams<UrlData>();
const oid = data.id; const oid = data.id;
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation(); const { goTo } = useNavigation();
@@ -67,7 +69,9 @@ export default function DiscountsGrid() {
color: "var(--green-darkestX2)", color: "var(--green-darkestX2)",
}} }}
> >
{value.type === DiscountType.FIXED ? formatPrice(value.value) : value.value + "%"} {value.type === DiscountType.FIXED
? formatPrice(value.value)
: value.value + "%"}
</div> </div>
</div> </div>
), ),
@@ -76,8 +80,10 @@ export default function DiscountsGrid() {
const [pageSize] = useState<number>( const [pageSize] = useState<number>(
parseInt( parseInt(
process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE ? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE : "10" process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
) ? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
: "10",
),
); );
const [discounts, setDiscounts] = useState<PaginateDiscountsResults>(); const [discounts, setDiscounts] = useState<PaginateDiscountsResults>();
@@ -88,6 +94,8 @@ export default function DiscountsGrid() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org", url: "/admin/org",
title: "Mis organizaciones", title: "Mis organizaciones",
@@ -107,6 +115,10 @@ export default function DiscountsGrid() {
}); });
loadDiscounts(1, pageSize); loadDiscounts(1, pageSize);
return () => {
bottomToolbar.clear();
};
}, []); }, []);
const loadDiscounts = (page: number = 1, pageSize: number = 30) => { const loadDiscounts = (page: number = 1, pageSize: number = 30) => {
@@ -38,6 +38,7 @@ import Switch from "@core/app/components/Switch/Switch";
import { FindTemplatesParams } from "@core/Models/Templates.model"; import { FindTemplatesParams } from "@core/Models/Templates.model";
import filterObjects from "@core/app/components/TextObjectFilter/Filter.Service"; import filterObjects from "@core/app/components/TextObjectFilter/Filter.Service";
import { CompanyServiceView } from "@models/Service.model"; import { CompanyServiceView } from "@models/Service.model";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
id: string; id: string;
@@ -47,6 +48,7 @@ export default function OrganizationEdit() {
const data = useParams<UrlData>(); const data = useParams<UrlData>();
const id = data.id; const id = data.id;
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore(); const SessionInfo = useSessionStore();
const alert = useAlert(); const alert = useAlert();
@@ -66,11 +68,12 @@ export default function OrganizationEdit() {
const [servicesShowPublicScores, setServicesShowPublicScores] = useState(true); const [servicesShowPublicScores, setServicesShowPublicScores] = useState(true);
const [servicesShowPublicOpinions, setServicesShowPublicOpinions] = useState(true); const [servicesShowPublicOpinions, setServicesShowPublicOpinions] = useState(true);
const [originalServicesShowPublicScores, setOriginalServicesShowPublicScores] = useState(true); const [originalServicesShowPublicScores, setOriginalServicesShowPublicScores] = useState(true);
const [originalServicesShowPublicOpinions, setOriginalServicesShowPublicOpinions] = useState(true); const [originalServicesShowPublicOpinions, setOriginalServicesShowPublicOpinions] =
useState(true);
const [services, setServices] = useState<CompanyServiceView[]>([]); const [services, setServices] = useState<CompanyServiceView[]>([]);
const [orgPublished, setOrgPublished] = useState<COMPANY_PUBLISHED_STATUS | undefined>( const [orgPublished, setOrgPublished] = useState<COMPANY_PUBLISHED_STATUS | undefined>(
undefined undefined,
); );
const [savePublishedStatusAvailable, setSavePublishedStatusAvailable] = useState(false); const [savePublishedStatusAvailable, setSavePublishedStatusAvailable] = useState(false);
@@ -199,6 +202,8 @@ export default function OrganizationEdit() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org", url: "/admin/org",
title: "Organizaciones", title: "Organizaciones",
@@ -218,6 +223,10 @@ export default function OrganizationEdit() {
}); });
loadCategories(); loadCategories();
return () => {
bottomToolbar.clear();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -241,25 +250,33 @@ export default function OrganizationEdit() {
setCategoryId(organization.categoryId.toString()); setCategoryId(organization.categoryId.toString());
setOrgSlug(getSlug(organization.name)); setOrgSlug(getSlug(organization.name));
setOrgPublished(organization.published); setOrgPublished(organization.published);
setShowPublicScores(organization.showPublicScores !== undefined ? organization.showPublicScores : true); setShowPublicScores(
setShowPublicOpinions(organization.showPublicOpinions !== undefined ? organization.showPublicOpinions : true); organization.showPublicScores !== undefined
? organization.showPublicScores
: true,
);
setShowPublicOpinions(
organization.showPublicOpinions !== undefined
? organization.showPublicOpinions
: true,
);
setShowPublicProfessionals( setShowPublicProfessionals(
organization.showPublicProfessionals !== undefined organization.showPublicProfessionals !== undefined
? organization.showPublicProfessionals ? organization.showPublicProfessionals
: true : true,
); );
setServices(loadedServices); setServices(loadedServices);
setServicesShowPublicScores( setServicesShowPublicScores(
loadedServices.every((service) => service.showPublicScores !== false) loadedServices.every((service) => service.showPublicScores !== false),
); );
setServicesShowPublicOpinions( setServicesShowPublicOpinions(
loadedServices.every((service) => service.showPublicOpinions !== false) loadedServices.every((service) => service.showPublicOpinions !== false),
); );
setOriginalServicesShowPublicScores( setOriginalServicesShowPublicScores(
loadedServices.every((service) => service.showPublicScores !== false) loadedServices.every((service) => service.showPublicScores !== false),
); );
setOriginalServicesShowPublicOpinions( setOriginalServicesShowPublicOpinions(
loadedServices.every((service) => service.showPublicOpinions !== false) loadedServices.every((service) => service.showPublicOpinions !== false),
); );
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -287,7 +304,7 @@ export default function OrganizationEdit() {
confirm.show( confirm.show(
validationPhoneResult.message, validationPhoneResult.message,
() => {}, () => {},
() => {} () => {},
); );
eventHandler.setEventType(EVENT_TYPES.SLEEP); eventHandler.setEventType(EVENT_TYPES.SLEEP);
return; return;
@@ -307,7 +324,8 @@ export default function OrganizationEdit() {
phoneNumber: phone, phoneNumber: phone,
templateWapNotifId: templateWapNotifId === "" ? undefined : templateWapNotifId, templateWapNotifId: templateWapNotifId === "" ? undefined : templateWapNotifId,
templateWapAltaId: templateWapAltaId === "" ? undefined : templateWapAltaId, templateWapAltaId: templateWapAltaId === "" ? undefined : templateWapAltaId,
templateWapCancellationId: templateWapCancellationId === "" ? undefined : templateWapCancellationId, templateWapCancellationId:
templateWapCancellationId === "" ? undefined : templateWapCancellationId,
...(canEditPublicVisibilityFlags ...(canEditPublicVisibilityFlags
? { ? {
showPublicScores, showPublicScores,
@@ -321,31 +339,31 @@ export default function OrganizationEdit() {
id: Yup.string().required("En este momento no es posible procesar su solicitud."), id: Yup.string().required("En este momento no es posible procesar su solicitud."),
name: Yup.string().required("Debe introducir el nombre de la organización."), name: Yup.string().required("Debe introducir el nombre de la organización."),
description: Yup.string().required( description: Yup.string().required(
"Debe introducir la descripción de la organización." "Debe introducir la descripción de la organización.",
), ),
categoryId: Yup.number().required( categoryId: Yup.number().required(
"Debe seleccionar una categoría para la organización." "Debe seleccionar una categoría para la organización.",
), ),
phoneAreaCode: Yup.number() phoneAreaCode: Yup.number()
.typeError("Código de área debe ser un número.") .typeError("Código de área debe ser un número.")
.transform((value, originalValue) => .transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value originalValue.toString().trim() === "" ? null : value,
) )
.nullable(), .nullable(),
phoneNumber: Yup.number() phoneNumber: Yup.number()
.typeError("El teléfono debe ser un número.") .typeError("El teléfono debe ser un número.")
.transform((value, originalValue) => .transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value originalValue.toString().trim() === "" ? null : value,
) )
.nullable(), .nullable(),
appointmentAlert: Yup.string().optional().default(""), appointmentAlert: Yup.string().optional().default(""),
sessionUser: Yup.string().required( sessionUser: Yup.string().required(
"Para crear una organización debe iniciar sesión." "Para crear una organización debe iniciar sesión.",
), ),
heatMapFraction: Yup.number() heatMapFraction: Yup.number()
.typeError("Fracción de tiempo debe ser un número.") .typeError("Fracción de tiempo debe ser un número.")
.transform((value, originalValue) => .transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value originalValue.toString().trim() === "" ? null : value,
) )
.nullable() .nullable()
.integer("Fracción de tiempo debe ser un número entero en minutos.") .integer("Fracción de tiempo debe ser un número entero en minutos.")
@@ -354,7 +372,7 @@ export default function OrganizationEdit() {
appointmentTime: Yup.number() appointmentTime: Yup.number()
.typeError("El Plazo Mínimo de Reserva debe ser un número.") .typeError("El Plazo Mínimo de Reserva debe ser un número.")
.transform((value, originalValue) => .transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value originalValue.toString().trim() === "" ? null : value,
) )
.nullable() .nullable()
.integer("El Plazo Mínimo de Reserva debe ser un número entero en horas.") .integer("El Plazo Mínimo de Reserva debe ser un número entero en horas.")
@@ -362,7 +380,7 @@ export default function OrganizationEdit() {
cancellationTime: Yup.number() cancellationTime: Yup.number()
.typeError("El Plazo Mínimo de Cancelación debe ser un número.") .typeError("El Plazo Mínimo de Cancelación debe ser un número.")
.transform((value, originalValue) => .transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value originalValue.toString().trim() === "" ? null : value,
) )
.nullable() .nullable()
.integer("El Plazo Mínimo de Cancelación debe ser un número entero en horas.") .integer("El Plazo Mínimo de Cancelación debe ser un número entero en horas.")
@@ -386,7 +404,8 @@ export default function OrganizationEdit() {
originalServicesShowPublicOpinions !== servicesShowPublicOpinions; originalServicesShowPublicOpinions !== servicesShowPublicOpinions;
const changedServices = const changedServices =
canEditPublicVisibilityFlags && canEditPublicVisibilityFlags &&
(servicesShowPublicScoresChanged || servicesShowPublicOpinionsChanged) (servicesShowPublicScoresChanged ||
servicesShowPublicOpinionsChanged)
? services ? services
: []; : [];
@@ -414,8 +433,8 @@ export default function OrganizationEdit() {
: service.showPublicOpinions !== undefined : service.showPublicOpinions !== undefined
? service.showPublicOpinions ? service.showPublicOpinions
: true, : true,
}) }),
) ),
).then(() => { ).then(() => {
const updatedServices = services.map((service) => ({ const updatedServices = services.map((service) => ({
...service, ...service,
@@ -434,7 +453,9 @@ export default function OrganizationEdit() {
}); });
}) })
.catch((error: ApiError | Error) => { .catch((error: ApiError | Error) => {
alert.showError(error instanceof ApiError ? error.format() : error.message); alert.showError(
error instanceof ApiError ? error.format() : error.message,
);
}) })
.finally(() => { .finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP); eventHandler.setEventType(EVENT_TYPES.SLEEP);
@@ -453,7 +474,7 @@ export default function OrganizationEdit() {
id: category.id.toString(), id: category.id.toString(),
name: category.name, name: category.name,
description: category.description, description: category.description,
})) })),
); );
}; };
@@ -533,7 +554,7 @@ export default function OrganizationEdit() {
setOrgPublished( setOrgPublished(
checked checked
? COMPANY_PUBLISHED_STATUS.PUBLISHED ? COMPANY_PUBLISHED_STATUS.PUBLISHED
: COMPANY_PUBLISHED_STATUS.PRIVATE : COMPANY_PUBLISHED_STATUS.PRIVATE,
); );
}} }}
/> />
@@ -581,15 +602,23 @@ export default function OrganizationEdit() {
/> />
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}> <MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
<b>Visibilidad pública:</b> Elegí si querés mostrar puntuaciones, opiniones y profesionales en el perfil público y las páginas de reserva. Disponible para planes pagos. <b>Visibilidad pública:</b> Elegí si querés mostrar puntuaciones, opiniones y
profesionales en el perfil público y las páginas de reserva. Disponible para planes
pagos.
</MaterialAlert> </MaterialAlert>
{!canEditPublicVisibilityFlags && ( {!canEditPublicVisibilityFlags && (
<MaterialAlert severity="warning" sx={{ marginTop: "10px" }}> <MaterialAlert severity="warning" sx={{ marginTop: "10px" }}>
Para modificar las opciones de visibilidad pública de opiniones, puntuaciones y profesionales de esta organización y sus servicios, el propietario de la organización necesita un plan pago. Para modificar las opciones de visibilidad pública de opiniones, puntuaciones y
profesionales de esta organización y sus servicios, el propietario de la
organización necesita un plan pago.
</MaterialAlert> </MaterialAlert>
)} )}
<div style={{ display: "flex", flexDirection: "column", gap: "10px", marginTop: "10px" }}> <div
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}> style={{ display: "flex", flexDirection: "column", gap: "10px", marginTop: "10px" }}
>
<label
style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}
>
<Checkbox <Checkbox
checked={showPublicScores} checked={showPublicScores}
disabled={!canEditPublicVisibilityFlags} disabled={!canEditPublicVisibilityFlags}
@@ -600,7 +629,9 @@ export default function OrganizationEdit() {
/> />
Mostrar puntuaciones públicas Mostrar puntuaciones públicas
</label> </label>
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}> <label
style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}
>
<Checkbox <Checkbox
checked={showPublicOpinions} checked={showPublicOpinions}
disabled={!canEditPublicVisibilityFlags} disabled={!canEditPublicVisibilityFlags}
@@ -611,7 +642,9 @@ export default function OrganizationEdit() {
/> />
Mostrar opiniones públicas Mostrar opiniones públicas
</label> </label>
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}> <label
style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}
>
<Checkbox <Checkbox
checked={showPublicProfessionals} checked={showPublicProfessionals}
disabled={!canEditPublicVisibilityFlags} disabled={!canEditPublicVisibilityFlags}
@@ -632,7 +665,9 @@ export default function OrganizationEdit() {
{services.length > 0 && ( {services.length > 0 && (
<> <>
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}> <MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
<b>Visibilidad pública de servicios:</b> Configurá si se muestran puntuaciones y opiniones en todos los servicios. Disponible para planes pagos. <b>Visibilidad pública de servicios:</b> Configurá si se muestran
puntuaciones y opiniones en todos los servicios. Disponible para planes
pagos.
</MaterialAlert> </MaterialAlert>
<div <div
style={{ style={{
@@ -642,7 +677,14 @@ export default function OrganizationEdit() {
marginTop: "10px", marginTop: "10px",
}} }}
> >
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}> <label
style={{
display: "flex",
alignItems: "center",
gap: "10px",
fontWeight: 600,
}}
>
<Checkbox <Checkbox
checked={servicesShowPublicScores} checked={servicesShowPublicScores}
disabled={!canEditPublicVisibilityFlags} disabled={!canEditPublicVisibilityFlags}
@@ -653,7 +695,14 @@ export default function OrganizationEdit() {
/> />
Mostrar puntuaciones públicas de los servicios Mostrar puntuaciones públicas de los servicios
</label> </label>
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}> <label
style={{
display: "flex",
alignItems: "center",
gap: "10px",
fontWeight: 600,
}}
>
<Checkbox <Checkbox
checked={servicesShowPublicOpinions} checked={servicesShowPublicOpinions}
disabled={!canEditPublicVisibilityFlags} disabled={!canEditPublicVisibilityFlags}
@@ -666,7 +715,8 @@ export default function OrganizationEdit() {
</label> </label>
{!canEditPublicVisibilityFlags && ( {!canEditPublicVisibilityFlags && (
<span style={{ color: "var(--gray-darkest)", fontSize: "13px" }}> <span style={{ color: "var(--gray-darkest)", fontSize: "13px" }}>
Para cambiar la visibilidad pública de los servicios necesitás un plan pago. Para cambiar la visibilidad pública de los servicios necesitás un
plan pago.
</span> </span>
)} )}
</div> </div>
@@ -24,6 +24,7 @@ import {
OrganizationFiscalProfileTaxCondition, OrganizationFiscalProfileTaxCondition,
upsertOrganizationFiscalProfile, upsertOrganizationFiscalProfile,
} from "./Fiscal.Service"; } from "./Fiscal.Service";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
id: string; id: string;
@@ -33,13 +34,15 @@ export default function OrganizationFiscalProfileEdit() {
const data = useParams<UrlData>(); const data = useParams<UrlData>();
const id = data.id; const id = data.id;
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore(); const SessionInfo = useSessionStore();
const alert = useAlert(); const alert = useAlert();
const [cuit, setCuit] = useState(""); const [cuit, setCuit] = useState("");
const [legalName, setLegalName] = useState(""); const [legalName, setLegalName] = useState("");
const [taxCondition, setTaxCondition] = useState<OrganizationFiscalProfileTaxCondition>("MONOTRIBUTO"); const [taxCondition, setTaxCondition] =
useState<OrganizationFiscalProfileTaxCondition>("MONOTRIBUTO");
const [pointOfSale, setPointOfSale] = useState("1"); const [pointOfSale, setPointOfSale] = useState("1");
const [activityCode, setActivityCode] = useState(""); const [activityCode, setActivityCode] = useState("");
const [activityDescription, setActivityDescription] = useState(""); const [activityDescription, setActivityDescription] = useState("");
@@ -80,6 +83,8 @@ export default function OrganizationFiscalProfileEdit() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.clear();
location.add({ location.add({
url: "/admin/org", url: "/admin/org",
title: "Organizaciones", title: "Organizaciones",
@@ -97,6 +102,10 @@ export default function OrganizationFiscalProfileEdit() {
title: "Datos fiscales", title: "Datos fiscales",
icon: <ReceiptLongOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />, icon: <ReceiptLongOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
return () => {
bottomToolbar.clear();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -118,7 +127,9 @@ export default function OrganizationFiscalProfileEdit() {
setLegalName(profile.legalName || ""); setLegalName(profile.legalName || "");
setTaxCondition(profile.taxCondition || "MONOTRIBUTO"); setTaxCondition(profile.taxCondition || "MONOTRIBUTO");
setPointOfSale(profile.pointOfSale ? String(profile.pointOfSale) : "1"); setPointOfSale(profile.pointOfSale ? String(profile.pointOfSale) : "1");
const profileActivity = activities.find((activity) => activity.id === profile.activityCode); const profileActivity = activities.find(
(activity) => activity.id === profile.activityCode,
);
setActivityCode(profile.activityCode || ""); setActivityCode(profile.activityCode || "");
setActivityDescription(profile.activityDescription || profileActivity?.name || ""); setActivityDescription(profile.activityDescription || profileActivity?.name || "");
}) })
@@ -152,7 +163,10 @@ export default function OrganizationFiscalProfileEdit() {
return; return;
} }
if (activityCode.trim() && !arcaActivities.some((activity) => activity.id === activityCode.trim())) { if (
activityCode.trim() &&
!arcaActivities.some((activity) => activity.id === activityCode.trim())
) {
alert.showError("Seleccioná una actividad ARCA/CLAE válida."); alert.showError("Seleccioná una actividad ARCA/CLAE válida.");
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE); eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
return; return;
@@ -160,7 +174,9 @@ export default function OrganizationFiscalProfileEdit() {
eventHandler.setEventType(EVENT_TYPES.LOADING); eventHandler.setEventType(EVENT_TYPES.LOADING);
const selectedActivity = arcaActivities.find((activity) => activity.id === activityCode.trim()); const selectedActivity = arcaActivities.find(
(activity) => activity.id === activityCode.trim(),
);
upsertOrganizationFiscalProfile({ upsertOrganizationFiscalProfile({
companyId: id, companyId: id,
@@ -173,7 +189,9 @@ export default function OrganizationFiscalProfileEdit() {
sessionUser: SessionInfo.userId, sessionUser: SessionInfo.userId,
}) })
.then((profile) => { .then((profile) => {
const savedActivity = arcaActivities.find((activity) => activity.id === profile.activityCode); const savedActivity = arcaActivities.find(
(activity) => activity.id === profile.activityCode,
);
setActivityCode(profile.activityCode || ""); setActivityCode(profile.activityCode || "");
setActivityDescription(profile.activityDescription || savedActivity?.name || ""); setActivityDescription(profile.activityDescription || savedActivity?.name || "");
alert.showSuccess("Datos fiscales actualizados."); alert.showSuccess("Datos fiscales actualizados.");
@@ -198,7 +216,8 @@ export default function OrganizationFiscalProfileEdit() {
<div> <div>
<h2>Configuración fiscal</h2> <h2>Configuración fiscal</h2>
<p> <p>
Estos datos se usan para emitir Factura C desde los movimientos de caja y para precargar el asistente ARCA. Estos datos se usan para emitir Factura C desde los movimientos de
caja y para precargar el asistente ARCA.
</p> </p>
</div> </div>
</section> </section>
@@ -240,7 +259,11 @@ export default function OrganizationFiscalProfileEdit() {
<h3>Condición fiscal</h3> <h3>Condición fiscal</h3>
<p>Seleccioná la condición declarada para esta organización.</p> <p>Seleccioná la condición declarada para esta organización.</p>
</div> </div>
<div className={style.optionGroup} role="radiogroup" aria-label="Condición fiscal"> <div
className={style.optionGroup}
role="radiogroup"
aria-label="Condición fiscal"
>
{fiscalConditionOptions.map((option) => { {fiscalConditionOptions.map((option) => {
const selected = taxCondition === option.id; const selected = taxCondition === option.id;
@@ -256,7 +279,9 @@ export default function OrganizationFiscalProfileEdit() {
aria-pressed={selected} aria-pressed={selected}
> >
<span className={style.optionTitle}>{option.title}</span> <span className={style.optionTitle}>{option.title}</span>
<span className={style.optionDescription}>{option.description}</span> <span className={style.optionDescription}>
{option.description}
</span>
</button> </button>
); );
})} })}
@@ -290,7 +315,7 @@ export default function OrganizationFiscalProfileEdit() {
selectedId={activityCode} selectedId={activityCode}
onChange={(selectedActivityCode) => { onChange={(selectedActivityCode) => {
const selectedActivity = arcaActivities.find( const selectedActivity = arcaActivities.find(
(activity) => activity.id === selectedActivityCode (activity) => activity.id === selectedActivityCode,
); );
setActivityCode(selectedActivity?.id || ""); setActivityCode(selectedActivity?.id || "");
@@ -13,6 +13,7 @@ import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import NotificationsIcon from "@mui/icons-material/NotificationsOutlined"; import NotificationsIcon from "@mui/icons-material/NotificationsOutlined";
import MaterialAlert from "@mui/material/Alert"; import MaterialAlert from "@mui/material/Alert";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
id: string; id: string;
@@ -21,7 +22,7 @@ type UrlData = {
const CHANNELS: { key: NotificationChannel; label: string }[] = [ const CHANNELS: { key: NotificationChannel; label: string }[] = [
{ key: "whatsapp", label: "WhatsApp" }, { key: "whatsapp", label: "WhatsApp" },
{ key: "email", label: "Email" }, { key: "email", label: "Email" },
{ key: "system", label: "Sistema" } { key: "system", label: "Sistema" },
]; ];
const REMINDER_PRESETS: { minutes: number; label: string }[] = [ const REMINDER_PRESETS: { minutes: number; label: string }[] = [
@@ -30,36 +31,43 @@ const REMINDER_PRESETS: { minutes: number; label: string }[] = [
{ minutes: 360, label: "6 horas antes" }, { minutes: 360, label: "6 horas antes" },
{ minutes: 720, label: "12 horas antes" }, { minutes: 720, label: "12 horas antes" },
{ minutes: 1440, label: "1 día antes" }, { minutes: 1440, label: "1 día antes" },
{ minutes: 2880, label: "2 días antes" } { minutes: 2880, label: "2 días antes" },
]; ];
export default function NotificationsPage() { export default function NotificationsPage() {
const data = useParams<UrlData>(); const data = useParams<UrlData>();
const id = data.id; const id = data.id;
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const store = useNotificationPreferencesStore(); const store = useNotificationPreferencesStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org", url: "/admin/org",
title: "Organizaciones", title: "Organizaciones",
icon: null icon: null,
}); });
location.add({ location.add({
url: "/admin/org/profile/" + id, url: "/admin/org/profile/" + id,
title: "Perfil", title: "Perfil",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" /> icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
}); });
location.add({ location.add({
url: "/admin/org/profile/" + id + "/notifications", url: "/admin/org/profile/" + id + "/notifications",
title: "Notificaciones", title: "Notificaciones",
icon: <NotificationsIcon sx={{ fontSize: "32px", mr: "10px" }} /> icon: <NotificationsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
return () => {
bottomToolbar.clear();
};
}, [id]); }, [id]);
const toggleChannel = (channel: NotificationChannel) => { const toggleChannel = (channel: NotificationChannel) => {
@@ -73,18 +81,22 @@ export default function NotificationsPage() {
const updateQuietHours = (field: "from" | "to", value: string) => { const updateQuietHours = (field: "from" | "to", value: string) => {
const current = store.formData.quietHours || { from: "00:00", to: "00:00" }; const current = store.formData.quietHours || { from: "00:00", to: "00:00" };
store.updateFormData({ store.updateFormData({
quietHours: { ...current, [field]: value } quietHours: { ...current, [field]: value },
}); });
}; };
const quietHourValue = (value: string | undefined, fallback: string) => { const quietHourValue = (value: string | undefined, fallback: string) => {
const [hour, minute] = (value || fallback).split(":").map(Number); const [hour, minute] = (value || fallback).split(":").map(Number);
return dayjs(new Date()).hour(hour).minute(minute || 0).second(0).millisecond(0); return dayjs(new Date())
.hour(hour)
.minute(minute || 0)
.second(0)
.millisecond(0);
}; };
const toggleQuietHours = () => { const toggleQuietHours = () => {
store.updateFormData({ store.updateFormData({
quietHours: store.formData.quietHours ? null : { from: "22:00", to: "08:00" } quietHours: store.formData.quietHours ? null : { from: "22:00", to: "08:00" },
}); });
}; };
@@ -95,7 +107,10 @@ export default function NotificationsPage() {
store.updateFormData({ reminderRules: updated }); store.updateFormData({ reminderRules: updated });
} else { } else {
store.updateFormData({ store.updateFormData({
reminderRules: [...store.formData.reminderRules, { offset: minutes, enabled: true }] reminderRules: [
...store.formData.reminderRules,
{ offset: minutes, enabled: true },
],
}); });
} }
}; };
@@ -107,10 +122,18 @@ export default function NotificationsPage() {
<div style={{ marginTop: "20px" }}> <div style={{ marginTop: "20px" }}>
{/* Channel Toggles Section */} {/* Channel Toggles Section */}
<MaterialAlert severity="info" sx={{ marginBottom: "20px" }}> <MaterialAlert severity="info" sx={{ marginBottom: "20px" }}>
<b>Canales de notificación:</b> Seleccione los canales por los cuales se enviarán las notificaciones automáticas. <b>Canales de notificación:</b> Seleccione los canales por los cuales se
enviarán las notificaciones automáticas.
</MaterialAlert> </MaterialAlert>
<div style={{ display: "flex", flexDirection: "column", gap: "8px", marginBottom: "30px" }}> <div
style={{
display: "flex",
flexDirection: "column",
gap: "8px",
marginBottom: "30px",
}}
>
{CHANNELS.map((channel) => ( {CHANNELS.map((channel) => (
<div <div
key={channel.key} key={channel.key}
@@ -121,10 +144,12 @@ export default function NotificationsPage() {
padding: "12px 16px", padding: "12px 16px",
borderRadius: "4px", borderRadius: "4px",
border: "1px solid var(--border-color)", border: "1px solid var(--border-color)",
backgroundColor: "var(--bg-primary)" backgroundColor: "var(--bg-primary)",
}} }}
> >
<span style={{ color: "var(--text-primary)", fontWeight: "500" }}>{channel.label}</span> <span style={{ color: "var(--text-primary)", fontWeight: "500" }}>
{channel.label}
</span>
<Switch <Switch
checked={store.formData.defaultChannels.includes(channel.key)} checked={store.formData.defaultChannels.includes(channel.key)}
onChange={() => toggleChannel(channel.key)} onChange={() => toggleChannel(channel.key)}
@@ -135,7 +160,8 @@ export default function NotificationsPage() {
{/* Quiet Hours Section */} {/* Quiet Hours Section */}
<MaterialAlert severity="info" sx={{ marginBottom: "20px" }}> <MaterialAlert severity="info" sx={{ marginBottom: "20px" }}>
<b>Horario silencioso:</b> Las notificaciones no se enviarán durante este rango horario. <b>Horario silencioso:</b> Las notificaciones no se enviarán durante este rango
horario.
</MaterialAlert> </MaterialAlert>
<div <div
@@ -147,7 +173,7 @@ export default function NotificationsPage() {
borderRadius: "8px", borderRadius: "8px",
border: "1px solid var(--border-color)", border: "1px solid var(--border-color)",
backgroundColor: "var(--bg-primary)", backgroundColor: "var(--bg-primary)",
marginBottom: "16px" marginBottom: "16px",
}} }}
> >
<span style={{ color: "var(--text-primary)", fontWeight: "500" }}> <span style={{ color: "var(--text-primary)", fontWeight: "500" }}>
@@ -160,13 +186,22 @@ export default function NotificationsPage() {
</div> </div>
{store.formData.quietHours && ( {store.formData.quietHours && (
<div style={{ display: "flex", gap: "16px", alignItems: "flex-end", marginBottom: "30px" }}> <div
style={{
display: "flex",
gap: "16px",
alignItems: "flex-end",
marginBottom: "30px",
}}
>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<TextTime <TextTime
placeholder="Desde" placeholder="Desde"
width="100%" width="100%"
value={quietHourValue(store.formData.quietHours.from, "22:00")} value={quietHourValue(store.formData.quietHours.from, "22:00")}
onChange={(value) => updateQuietHours("from", value.format("HH:mm"))} onChange={(value) =>
updateQuietHours("from", value.format("HH:mm"))
}
/> />
</div> </div>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
@@ -182,12 +217,15 @@ export default function NotificationsPage() {
{/* Reminder Rules Section */} {/* Reminder Rules Section */}
<MaterialAlert severity="info" sx={{ marginBottom: "20px" }}> <MaterialAlert severity="info" sx={{ marginBottom: "20px" }}>
<b>Reglas de recordatorio:</b> Active los momentos en los que desea enviar recordatorios antes del turno. <b>Reglas de recordatorio:</b> Active los momentos en los que desea enviar
recordatorios antes del turno.
</MaterialAlert> </MaterialAlert>
<div style={{ display: "flex", flexWrap: "wrap", gap: "10px" }}> <div style={{ display: "flex", flexWrap: "wrap", gap: "10px" }}>
{REMINDER_PRESETS.map((preset) => { {REMINDER_PRESETS.map((preset) => {
const isActive = store.formData.reminderRules.some((r) => r.offset === preset.minutes); const isActive = store.formData.reminderRules.some(
(r) => r.offset === preset.minutes,
);
return ( return (
<button <button
key={preset.minutes} key={preset.minutes}
@@ -198,11 +236,15 @@ export default function NotificationsPage() {
padding: "10px 16px", padding: "10px 16px",
borderRadius: "999px", borderRadius: "999px",
border: `1px solid ${isActive ? "var(--primary-color)" : "var(--border-color)"}`, border: `1px solid ${isActive ? "var(--primary-color)" : "var(--border-color)"}`,
backgroundColor: isActive ? "rgba(25, 118, 210, 0.12)" : "var(--bg-primary)", backgroundColor: isActive
color: isActive ? "var(--primary-color)" : "var(--text-secondary)", ? "rgba(25, 118, 210, 0.12)"
: "var(--bg-primary)",
color: isActive
? "var(--primary-color)"
: "var(--text-secondary)",
fontWeight: isActive ? "600" : "500", fontWeight: isActive ? "600" : "500",
fontSize: "14px", fontSize: "14px",
transition: "all 0.2s ease" transition: "all 0.2s ease",
}} }}
> >
{preset.label} {preset.label}
@@ -248,7 +248,7 @@ export default function OrganizationNotPayedAppointments() {
const [dialogPayVisible, setDialogPayVisible] = useState<boolean>(false); const [dialogPayVisible, setDialogPayVisible] = useState<boolean>(false);
const [dialogPayOneVisible, setDialogPayOneVisible] = useState<boolean>(false); const [dialogPayOneVisible, setDialogPayOneVisible] = useState<boolean>(false);
const [createPaymentData, setCreatePaymentData] = useState<PaginatePaymentsView | undefined>( const [createPaymentData, setCreatePaymentData] = useState<PaginatePaymentsView | undefined>(
undefined undefined,
); );
const [paymentMethodFilterContent] = useState<TextObjectModalContent>({ const [paymentMethodFilterContent] = useState<TextObjectModalContent>({
@@ -284,7 +284,7 @@ export default function OrganizationNotPayedAppointments() {
TextObjectFilterResult[] TextObjectFilterResult[]
>([]); >([]);
const [paymentMethodId, setPaymentMethodId] = useState<CLIENT_ACCOUNT_PAYMENT_METHODS>( const [paymentMethodId, setPaymentMethodId] = useState<CLIENT_ACCOUNT_PAYMENT_METHODS>(
CLIENT_ACCOUNT_PAYMENT_METHODS.CASH CLIENT_ACCOUNT_PAYMENT_METHODS.CASH,
); );
useEffect(() => { useEffect(() => {
@@ -326,7 +326,7 @@ export default function OrganizationNotPayedAppointments() {
TextObjectFilterResult[] TextObjectFilterResult[]
>([]); >([]);
const [paymentProcessDateId, setPaymentProcessDateId] = useState<PAYMENT_PROCCESS_DATE>( const [paymentProcessDateId, setPaymentProcessDateId] = useState<PAYMENT_PROCCESS_DATE>(
PAYMENT_PROCCESS_DATE.NOW PAYMENT_PROCCESS_DATE.NOW,
); );
useEffect(() => { useEffect(() => {
@@ -356,7 +356,7 @@ export default function OrganizationNotPayedAppointments() {
id: category.id.toString(), id: category.id.toString(),
name: category.name, name: category.name,
description: category.description, description: category.description,
})) })),
); );
}; };
@@ -370,7 +370,7 @@ export default function OrganizationNotPayedAppointments() {
const [clientIds, setClientIds] = useState<string[]>([]); const [clientIds, setClientIds] = useState<string[]>([]);
const [collaboratorFilterData, setCollaboratorFilterData] = useState<TextObjectFilterResult[]>( const [collaboratorFilterData, setCollaboratorFilterData] = useState<TextObjectFilterResult[]>(
[] [],
); );
const [collaboratorId, setCollaboratorId] = useState<string>(""); const [collaboratorId, setCollaboratorId] = useState<string>("");
@@ -484,8 +484,8 @@ export default function OrganizationNotPayedAppointments() {
parseInt( parseInt(
process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE ? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
: "10" : "10",
) ),
); );
const [payments, setPayments] = useState<PaginateNotPayedAppointmentsResult>(); const [payments, setPayments] = useState<PaginateNotPayedAppointmentsResult>();
const paymentsRef = useRef(payments); const paymentsRef = useRef(payments);
@@ -595,7 +595,6 @@ export default function OrganizationNotPayedAppointments() {
}); });
}; };
const generarReporte = async () => { const generarReporte = async () => {
eventHandler.setEventType(EVENT_TYPES.LOADING); eventHandler.setEventType(EVENT_TYPES.LOADING);
try { try {
@@ -625,27 +624,27 @@ export default function OrganizationNotPayedAppointments() {
reader.onloadend = () => resolve(reader.result as string); reader.onloadend = () => resolve(reader.result as string);
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
}); });
} catch(e) {} } catch (e) {}
const tableBody: any[] = [ const tableBody: any[] = [
[ [
{ text: "Cliente", bold: true, color: "#333333", fillColor: "#f5f5f5" }, { text: "Cliente", bold: true, color: "#333333", fillColor: "#f5f5f5" },
{ text: "Datos del Turno", bold: true, color: "#333333", fillColor: "#f5f5f5" }, { text: "Datos del Turno", bold: true, color: "#333333", fillColor: "#f5f5f5" },
{ text: "Monto", bold: true, color: "#333333", fillColor: "#f5f5f5" } { text: "Monto", bold: true, color: "#333333", fillColor: "#f5f5f5" },
] ],
]; ];
report.items.forEach(item => { report.items.forEach((item) => {
tableBody.push([ tableBody.push([
item.clientName || "-", item.clientName || "-",
{ {
text: [ text: [
{ text: `${item.serviceName || "-"}\n`, bold: true, fontSize: 11 }, { text: `${item.serviceName || "-"}\n`, bold: true, fontSize: 11 },
`Prof: ${item.employeeName || "-"}\nFecha: ${dayjs(item.appointmentDate).format("DD/MM/YYYY HH:mm")} hs` `Prof: ${item.employeeName || "-"}\nFecha: ${dayjs(item.appointmentDate).format("DD/MM/YYYY HH:mm")} hs`,
], ],
fontSize: 10 fontSize: 10,
}, },
{ text: "$" + item.price.toLocaleString("es-AR"), alignment: "right" } { text: "$" + item.price.toLocaleString("es-AR"), alignment: "right" },
]); ]);
}); });
@@ -653,52 +652,81 @@ export default function OrganizationNotPayedAppointments() {
tableBody.push([ tableBody.push([
{ text: "TOTAL ADEUDADO", bold: true, colSpan: 2, alignment: "right" }, { text: "TOTAL ADEUDADO", bold: true, colSpan: 2, alignment: "right" },
{}, {},
{ text: "$" + report.totalAmount.toLocaleString("es-AR"), bold: true, alignment: "right" } {
text: "$" + report.totalAmount.toLocaleString("es-AR"),
bold: true,
alignment: "right",
},
]); ]);
const docDefinition: unknown = { const docDefinition: unknown = {
content: [ content: [
{ {
columns: [ columns: [
{ text: "Reporte de Turnos Adeudados", fontSize: 18, bold: true, margin: [0, 0, 0, 10], width: '*' }, {
...(logoDataUrl ? [{ image: logoDataUrl, width: 45, margin: [0, 0, 0, 10], alignment: 'right' }] : []) text: "Reporte de Turnos Adeudados",
fontSize: 18,
bold: true,
margin: [0, 0, 0, 10],
width: "*",
},
...(logoDataUrl
? [
{
image: logoDataUrl,
width: 45,
margin: [0, 0, 0, 10],
alignment: "right",
},
] ]
: []),
],
}, },
{ text: `Filtros aplicados:`, bold: true, margin: [0, 0, 0, 5] }, { text: `Filtros aplicados:`, bold: true, margin: [0, 0, 0, 5] },
{ {
text: [ text: [
{ text: "Clientes Seleccionados: ", bold: true }, { text: "Clientes Seleccionados: ", bold: true },
`${clientIds.length > 0 ? clientIds.length : "Todos"}` `${clientIds.length > 0 ? clientIds.length : "Todos"}`,
], ],
margin: [0, 0, 0, 2] margin: [0, 0, 0, 2],
}, },
{ {
text: [ text: [
{ text: "Período: ", bold: true }, { text: "Período: ", bold: true },
`${dateFrom ? dateFrom.format("DD/MM/YYYY") : "Inicio"} al ${dateTo ? dateTo.format("DD/MM/YYYY") : "Fin"}` `${dateFrom ? dateFrom.format("DD/MM/YYYY") : "Inicio"} al ${dateTo ? dateTo.format("DD/MM/YYYY") : "Fin"}`,
], ],
margin: [0, 0, 0, 20] margin: [0, 0, 0, 20],
}, },
{ {
table: { table: {
headerRows: 1, headerRows: 1,
widths: [120, "*", "auto"], widths: [120, "*", "auto"],
body: tableBody body: tableBody,
}, },
layout: "lightHorizontalLines" layout: "lightHorizontalLines",
} },
] ],
}; };
if ((window as unknown as { Android: any }).Android && (window as unknown as { Android: any }).Android.savePdf) { if (
(window as unknown as { Android: any }).Android &&
(window as unknown as { Android: any }).Android.savePdf
) {
pdfMake.createPdf(docDefinition as any).getBase64((data: any) => { pdfMake.createPdf(docDefinition as any).getBase64((data: any) => {
(window as unknown as { Android: any }).Android?.savePdf(data, `reporte-adeudados.pdf`); (window as unknown as { Android: any }).Android?.savePdf(
data,
`reporte-adeudados.pdf`,
);
}); });
} else { } else {
pdfMake.createPdf(docDefinition as any).download(`reporte-adeudados.pdf`); pdfMake.createPdf(docDefinition as any).download(`reporte-adeudados.pdf`);
} }
} catch (error: unknown) { } catch (error: unknown) {
alert.showError((error as { format?: () => string }).format ? (error as { format: () => string }).format() : "Error generando PDF"); alert.showError(
(error as { format?: () => string }).format
? (error as { format: () => string }).format()
: "Error generando PDF",
);
} finally { } finally {
eventHandler.setEventType(EVENT_TYPES.CLEAR); eventHandler.setEventType(EVENT_TYPES.CLEAR);
} }
@@ -832,14 +860,24 @@ export default function OrganizationNotPayedAppointments() {
setMethodId={setMethodId} setMethodId={setMethodId}
clientData={clientFilterData} clientData={clientFilterData}
setClientIds={setClientIds} setClientIds={setClientIds}
/> />,
); );
filter.setOnFilter(() => { }); filter.setOnFilter(() => {});
return () => { return () => {
bottomToolbar.clear(); bottomToolbar.clear();
}; };
}, [paymentsTotal, collaboratorFilterData, serviceFilterData, methodFilterData, clientFilterData, clientIds, collaboratorId, serviceId, methodId]); }, [
paymentsTotal,
collaboratorFilterData,
serviceFilterData,
methodFilterData,
clientFilterData,
clientIds,
collaboratorId,
serviceId,
methodId,
]);
useEffect(() => { useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP); eventHandler.setEventType(EVENT_TYPES.SLEEP);
@@ -881,39 +919,75 @@ export default function OrganizationNotPayedAppointments() {
loadNotPayedAppointmentsTotal(); loadNotPayedAppointmentsTotal();
}, [clientIds, collaboratorId, serviceId, methodId, dateFrom, dateTo, reloadPayments]); }, [clientIds, collaboratorId, serviceId, methodId, dateFrom, dateTo, reloadPayments]);
return ( return (
<> <>
<Location /> <Location />
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}> <SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}>
<div style={{ maxWidth: "800px", margin: "0 auto", paddingBottom: "100px", marginTop: "0px" }}> <div
style={{
maxWidth: "800px",
margin: "0 auto",
paddingBottom: "100px",
marginTop: "0px",
}}
>
{/* Top Status Card */} {/* Top Status Card */}
<div style={{ position: "sticky", top: "70px", zIndex: 100, marginBottom: "50px", display: "flex", flexDirection: "column", background: "white", paddingBottom: "10px", paddingTop: "5px", margin: "0 -10px", paddingLeft: "10px", paddingRight: "10px" }}> <div
style={{
position: "sticky",
top: "70px",
zIndex: 100,
marginBottom: "50px",
display: "flex",
flexDirection: "column",
background: "white",
paddingBottom: "10px",
paddingTop: "5px",
margin: "0 -10px",
paddingLeft: "10px",
paddingRight: "10px",
}}
>
{/* Tabs Selector */} {/* Tabs Selector */}
<div style={{ display: "flex", justifyContent: "center", marginBottom: "15px" }}> <div
style={{
display: "flex",
justifyContent: "center",
marginBottom: "15px",
}}
>
<ThemeProvider theme={turnosXpressTheme}> <ThemeProvider theme={turnosXpressTheme}>
<div style={{ <div
style={{
display: "flex", display: "flex",
background: "rgba(255,255,255,0.9)", background: "rgba(255,255,255,0.9)",
backdropFilter: "blur(10px)", backdropFilter: "blur(10px)",
borderRadius: "30px", borderRadius: "30px",
padding: "5px", padding: "5px",
boxShadow: "0 4px 15px rgba(0,0,0,0.05)" boxShadow: "0 4px 15px rgba(0,0,0,0.05)",
}}> }}
>
<ButtonMaterial <ButtonMaterial
variant="text" variant="text"
onClick={() => goTo(`/admin/org/profile/${id}/payments`)} onClick={() => goTo(`/admin/org/profile/${id}/payments`)}
style={{ borderRadius: "25px", color: "#666", fontWeight: "bold", padding: "8px 25px" }} style={{
borderRadius: "25px",
color: "#666",
fontWeight: "bold",
padding: "8px 25px",
}}
> >
Pagados Pagados
</ButtonMaterial> </ButtonMaterial>
<ButtonMaterial <ButtonMaterial
variant="contained" variant="contained"
style={{ borderRadius: "25px", boxShadow: "0 2px 8px rgba(0,0,0,0.1)", fontWeight: "bold", padding: "8px 25px" }} style={{
borderRadius: "25px",
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
fontWeight: "bold",
padding: "8px 25px",
}}
> >
Adeudados Adeudados
</ButtonMaterial> </ButtonMaterial>
@@ -928,25 +1002,33 @@ export default function OrganizationNotPayedAppointments() {
opacity: 1, opacity: 1,
y: 0, y: 0,
padding: isMinimized ? "25px 50px" : "40px 20px", padding: isMinimized ? "25px 50px" : "40px 20px",
borderRadius: isMinimized ? "15px" : "20px" borderRadius: isMinimized ? "15px" : "20px",
}} }}
style={{ style={{
background: "linear-gradient(135deg, #FF416C 0%, #FF4B2B 100%)", // Red gradient for debts background: "linear-gradient(135deg, #FF416C 0%, #FF4B2B 100%)", // Red gradient for debts
color: "white", color: "white",
textAlign: "center", textAlign: "center",
boxShadow: isMinimized ? "0 4px 15px rgba(0,0,0,0.3)" : "0 10px 30px rgba(0,0,0,0.15)", boxShadow: isMinimized
? "0 4px 15px rgba(0,0,0,0.3)"
: "0 10px 30px rgba(0,0,0,0.15)",
position: "relative", position: "relative",
display: "flex", display: "flex",
flexDirection: isMinimized ? "row" : "column", flexDirection: isMinimized ? "row" : "column",
alignItems: "center", alignItems: "center",
justifyContent: isMinimized ? "space-between" : "center", justifyContent: isMinimized ? "space-between" : "center",
transition: "box-shadow 0.3s ease" transition: "box-shadow 0.3s ease",
}} }}
> >
<IconButton <IconButton
onClick={() => setForceMinimized(!forceMinimized)} onClick={() => setForceMinimized(!forceMinimized)}
style={{ position: "absolute", top: "10px", left: "10px", color: "rgba(255,255,255,0.7)", zIndex: 10 }} style={{
position: "absolute",
top: "10px",
left: "10px",
color: "rgba(255,255,255,0.7)",
zIndex: 10,
}}
size="small" size="small"
title={forceMinimized ? "Maximizar" : "Minimizar"} title={forceMinimized ? "Maximizar" : "Minimizar"}
> >
@@ -954,28 +1036,59 @@ export default function OrganizationNotPayedAppointments() {
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => filter.toggle()} onClick={() => filter.toggle()}
style={{ position: "absolute", top: "10px", right: "10px", color: "rgba(255,255,255,0.7)", zIndex: 10 }} style={{
position: "absolute",
top: "10px",
right: "10px",
color: "rgba(255,255,255,0.7)",
zIndex: 10,
}}
size="small" size="small"
title="Filtrar" title="Filtrar"
> >
<FilterAltOutlinedIcon /> <FilterAltOutlinedIcon />
</IconButton> </IconButton>
<div style={{ position: "absolute", top: "-50px", right: "-50px", width: "150px", height: "150px", borderRadius: "50%", background: "rgba(255,255,255,0.05)" }}></div> <div
<div style={{ position: "absolute", bottom: "-30px", left: "-30px", width: "100px", height: "100px", borderRadius: "50%", background: "rgba(255,255,255,0.05)" }}></div> style={{
position: "absolute",
top: "-50px",
right: "-50px",
width: "150px",
height: "150px",
borderRadius: "50%",
background: "rgba(255,255,255,0.05)",
}}
></div>
<div
style={{
position: "absolute",
bottom: "-30px",
left: "-30px",
width: "100px",
height: "100px",
borderRadius: "50%",
background: "rgba(255,255,255,0.05)",
}}
></div>
<motion.div <motion.div
animate={{ animate={{
fontSize: isMinimized ? "14px" : "16px", fontSize: isMinimized ? "14px" : "16px",
marginBottom: isMinimized ? "0px" : "10px" marginBottom: isMinimized ? "0px" : "10px",
}}
style={{
fontWeight: "bold",
textTransform: "uppercase",
letterSpacing: "2px",
opacity: 0.9,
}} }}
style={{ fontWeight: "bold", textTransform: "uppercase", letterSpacing: "2px", opacity: 0.9 }}
> >
Total Adeudado Total Adeudado
</motion.div> </motion.div>
<motion.div <motion.div
animate={{ animate={{
fontSize: isMinimized ? "24px" : "56px", fontSize: isMinimized ? "24px" : "56px",
margin: isMinimized ? "0px" : "10px 0" margin: isMinimized ? "0px" : "10px 0",
}} }}
style={{ fontWeight: "900" }} style={{ fontWeight: "900" }}
> >
@@ -989,7 +1102,13 @@ export default function OrganizationNotPayedAppointments() {
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
style={{ marginTop: "15px" }} style={{ marginTop: "15px" }}
> >
<div style={{ marginBottom: "15px", width: "250px", maxWidth: "100%" }}> <div
style={{
marginBottom: "15px",
width: "250px",
maxWidth: "100%",
}}
>
<TextRangeDateModern <TextRangeDateModern
label="Período" label="Período"
dateFrom={dateFrom} dateFrom={dateFrom}
@@ -998,7 +1117,14 @@ export default function OrganizationNotPayedAppointments() {
setDateTo={setDateTo} setDateTo={setDateTo}
/> />
</div> </div>
<div style={{ display: "flex", gap: "10px", alignItems: "center", flexWrap: "wrap" }}> <div
style={{
display: "flex",
gap: "10px",
alignItems: "center",
flexWrap: "wrap",
}}
>
<ButtonMaterial <ButtonMaterial
variant="contained" variant="contained"
startIcon={<PayIcon />} startIcon={<PayIcon />}
@@ -1009,7 +1135,7 @@ export default function OrganizationNotPayedAppointments() {
fontWeight: "bold", fontWeight: "bold",
borderRadius: "20px", borderRadius: "20px",
padding: "8px 20px", padding: "8px 20px",
textTransform: "none" textTransform: "none",
}} }}
> >
Pagar Todos Pagar Todos
@@ -1023,10 +1149,13 @@ export default function OrganizationNotPayedAppointments() {
fontWeight: "bold", fontWeight: "bold",
borderRadius: "20px", borderRadius: "20px",
padding: "8px 20px", padding: "8px 20px",
textTransform: "none" textTransform: "none",
}} }}
> >
<PictureAsPdfIcon style={{ fontSize: "18px", marginRight: "5px" }} /> PDF <PictureAsPdfIcon
style={{ fontSize: "18px", marginRight: "5px" }}
/>{" "}
PDF
</ButtonMaterial> </ButtonMaterial>
</div> </div>
</motion.div> </motion.div>
@@ -1035,10 +1164,22 @@ export default function OrganizationNotPayedAppointments() {
</div> </div>
{/* Dynamic Spacer for layout shift */} {/* Dynamic Spacer for layout shift */}
<div style={{ height: isMinimized ? "150px" : "0px", transition: "height 0.3s ease" }}></div> <div
style={{
height: isMinimized ? "150px" : "0px",
transition: "height 0.3s ease",
}}
></div>
{/* Payments List */} {/* Payments List */}
<div style={{ display: "flex", flexDirection: "column", gap: "15px", padding: "15px 10px 0 10px" }}> <div
style={{
display: "flex",
flexDirection: "column",
gap: "15px",
padding: "15px 10px 0 10px",
}}
>
<AnimatePresence> <AnimatePresence>
{payments?.data.map((payment, index) => ( {payments?.data.map((payment, index) => (
<motion.div <motion.div
@@ -1054,31 +1195,72 @@ export default function OrganizationNotPayedAppointments() {
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
boxShadow: "0 4px 15px rgba(0,0,0,0.05)", boxShadow: "0 4px 15px rgba(0,0,0,0.05)",
borderLeft: `4px solid var(--red)` borderLeft: `4px solid var(--red)`,
}} }}
> >
<div style={{ display: "flex", alignItems: "center", gap: "15px" }}> <div
<div style={{ style={{
display: "flex",
alignItems: "center",
gap: "15px",
}}
>
<div
style={{
background: "rgba(255, 75, 43, 0.1)", background: "rgba(255, 75, 43, 0.1)",
padding: "10px", padding: "10px",
borderRadius: "12px", borderRadius: "12px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
color: "var(--red)" color: "var(--red)",
}}> }}
>
<CreditCardIcon /> <CreditCardIcon />
</div> </div>
<div> <div>
<div style={{ fontWeight: "800", fontSize: "15px", color: "#333", marginBottom: "4px" }}>{payment.clientName}</div> <div
<div style={{ fontWeight: "700", fontSize: "13px", color: "var(--purple)", marginBottom: "4px" }}>{payment.serviceName}</div> style={{
fontWeight: "800",
fontSize: "15px",
color: "#333",
marginBottom: "4px",
}}
>
{payment.clientName}
</div>
<div
style={{
fontWeight: "700",
fontSize: "13px",
color: "var(--purple)",
marginBottom: "4px",
}}
>
{payment.serviceName}
</div>
<div style={{ fontSize: "12px", color: "#888" }}> <div style={{ fontSize: "12px", color: "#888" }}>
{dayjs(payment.createdAt).format("DD MMMM YYYY • HH:mm")} • {payment.employeeName} {dayjs(payment.createdAt).format(
"DD MMMM YYYY • HH:mm",
)}{" "}
• {payment.employeeName}
</div> </div>
</div> </div>
</div> </div>
<div style={{ display: "flex", alignItems: "center", gap: "15px" }}> <div
<div style={{ fontWeight: "900", fontSize: "16px", color: "var(--red)" }}> style={{
display: "flex",
alignItems: "center",
gap: "15px",
}}
>
<div
style={{
fontWeight: "900",
fontSize: "16px",
color: "var(--red)",
}}
>
{formatPrice(payment.price)} {formatPrice(payment.price)}
</div> </div>
<IconButton <IconButton
@@ -1096,10 +1278,22 @@ export default function OrganizationNotPayedAppointments() {
))} ))}
{(!payments?.data || payments.data.length === 0) && ( {(!payments?.data || payments.data.length === 0) && (
<div style={{ textAlign: "center", padding: "50px", color: "#aaa" }}> <div
<CreditCardIcon sx={{ fontSize: "60px", opacity: 0.2, marginBottom: "15px" }} /> style={{ textAlign: "center", padding: "50px", color: "#aaa" }}
<div style={{ fontSize: "18px", fontWeight: "bold" }}>No hay pagos adeudados</div> >
<div style={{ fontSize: "14px", marginTop: "5px" }}>Intenta ajustar los filtros de fecha</div> <CreditCardIcon
sx={{
fontSize: "60px",
opacity: 0.2,
marginBottom: "15px",
}}
/>
<div style={{ fontSize: "18px", fontWeight: "bold" }}>
No hay pagos adeudados
</div>
<div style={{ fontSize: "14px", marginTop: "5px" }}>
Intenta ajustar los filtros de fecha
</div>
</div> </div>
)} )}
</AnimatePresence> </AnimatePresence>
@@ -1107,7 +1301,9 @@ export default function OrganizationNotPayedAppointments() {
{/* Pagination */} {/* Pagination */}
{payments && payments.pages > 1 && ( {payments && payments.pages > 1 && (
<div style={{ display: "flex", justifyContent: "center", marginTop: "40px" }}> <div
style={{ display: "flex", justifyContent: "center", marginTop: "40px" }}
>
<Pagination <Pagination
count={payments.pages} count={payments.pages}
page={payments.page} page={payments.page}
@@ -1134,7 +1330,7 @@ export default function OrganizationNotPayedAppointments() {
data={paymentMethhodFilterData} data={paymentMethhodFilterData}
onChange={(id) => { onChange={(id) => {
setPaymentMethodId( setPaymentMethodId(
id as CLIENT_ACCOUNT_PAYMENT_METHODS id as CLIENT_ACCOUNT_PAYMENT_METHODS,
); );
}} }}
/> />
@@ -1185,7 +1381,7 @@ export default function OrganizationNotPayedAppointments() {
data={paymentMethhodFilterData} data={paymentMethhodFilterData}
onChange={(id) => { onChange={(id) => {
setPaymentMethodId( setPaymentMethodId(
id as CLIENT_ACCOUNT_PAYMENT_METHODS id as CLIENT_ACCOUNT_PAYMENT_METHODS,
); );
}} }}
/> />
@@ -1219,7 +1415,6 @@ export default function OrganizationNotPayedAppointments() {
</DialogActions> </DialogActions>
</> </>
</DialogWindow> </DialogWindow>
</div> </div>
</SecureByPlan> </SecureByPlan>
</> </>
@@ -47,6 +47,7 @@ import SearchOffIcon from "@mui/icons-material/SearchOff";
import UnfoldLessIcon from "@mui/icons-material/UnfoldLess"; import UnfoldLessIcon from "@mui/icons-material/UnfoldLess";
import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore"; import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore";
import FilterAltOutlinedIcon from "@mui/icons-material/FilterAltOutlined"; import FilterAltOutlinedIcon from "@mui/icons-material/FilterAltOutlined";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
id: string; id: string;
@@ -68,8 +69,12 @@ const FilterContent = (props: FilterContentProps) => {
const [clientFilterContent] = useState<TextObjectModalContent>({ const [clientFilterContent] = useState<TextObjectModalContent>({
header: ( header: (
<div> <div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>Seleccionar Cliente:</div> <div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>Ingrese el nombre del cliente para buscarlo.</p> Seleccionar Cliente:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del cliente para buscarlo.
</p>
</div> </div>
), ),
bottom: <div>&nbsp;</div>, bottom: <div>&nbsp;</div>,
@@ -81,8 +86,12 @@ const FilterContent = (props: FilterContentProps) => {
const [collaboratorFilterContent] = useState<TextObjectModalContent>({ const [collaboratorFilterContent] = useState<TextObjectModalContent>({
header: ( header: (
<div> <div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>Seleccionar Colaborador:</div> <div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>Ingrese el nombre del colaborador para buscarlo.</p> Seleccionar Colaborador:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del colaborador para buscarlo.
</p>
</div> </div>
), ),
bottom: <div>&nbsp;</div>, bottom: <div>&nbsp;</div>,
@@ -94,8 +103,12 @@ const FilterContent = (props: FilterContentProps) => {
const [serviceFilterContent] = useState<TextObjectModalContent>({ const [serviceFilterContent] = useState<TextObjectModalContent>({
header: ( header: (
<div> <div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>Seleccionar Servicio:</div> <div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>Ingrese el nombre del servicio para buscarlo.</p> Seleccionar Servicio:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del servicio para buscarlo.
</p>
</div> </div>
), ),
bottom: <div>&nbsp;</div>, bottom: <div>&nbsp;</div>,
@@ -107,8 +120,12 @@ const FilterContent = (props: FilterContentProps) => {
const [methodFilterContent] = useState<TextObjectModalContent>({ const [methodFilterContent] = useState<TextObjectModalContent>({
header: ( header: (
<div> <div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>Seleccionar Método de Pago:</div> <div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>Ingrese el nombre del método de pago para buscarlo.</p> Seleccionar Método de Pago:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del método de pago para buscarlo.
</p>
</div> </div>
), ),
bottom: <div>&nbsp;</div>, bottom: <div>&nbsp;</div>,
@@ -152,6 +169,7 @@ export default function OrganizationEdit() {
const data = useParams<UrlData>(); const data = useParams<UrlData>();
const id = data.id; const id = data.id;
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore(); const SessionInfo = useSessionStore();
const filter = useFilterStore(); const filter = useFilterStore();
@@ -168,7 +186,9 @@ export default function OrganizationEdit() {
const [clientFilterData, setClientFilterData] = useState<TextObjectFilterResult[]>([]); const [clientFilterData, setClientFilterData] = useState<TextObjectFilterResult[]>([]);
const [clientIds, setClientIds] = useState<string[]>([]); const [clientIds, setClientIds] = useState<string[]>([]);
const [collaboratorFilterData, setCollaboratorFilterData] = useState<TextObjectFilterResult[]>([]); const [collaboratorFilterData, setCollaboratorFilterData] = useState<TextObjectFilterResult[]>(
[],
);
const [collaboratorId, setCollaboratorId] = useState<string>(""); const [collaboratorId, setCollaboratorId] = useState<string>("");
const [serviceFilterData, setServiceFilterData] = useState<TextObjectFilterResult[]>([]); const [serviceFilterData, setServiceFilterData] = useState<TextObjectFilterResult[]>([]);
@@ -188,7 +208,6 @@ export default function OrganizationEdit() {
const generarReporte = async () => { const generarReporte = async () => {
try { try {
const pdfMake = await import("pdfmake/build/pdfmake"); const pdfMake = await import("pdfmake/build/pdfmake");
const pdfFonts = (await import("pdfmake/build/vfs_fonts")) as any; const pdfFonts = (await import("pdfmake/build/vfs_fonts")) as any;
pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs; pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs;
@@ -201,7 +220,7 @@ export default function OrganizationEdit() {
serviceId: serviceId ? serviceId : undefined, serviceId: serviceId ? serviceId : undefined,
employeeId: collaboratorId ? collaboratorId : undefined, employeeId: collaboratorId ? collaboratorId : undefined,
method: methodId ? (methodId as any) : undefined, method: methodId ? (methodId as any) : undefined,
sessionUser: SessionInfo.userId sessionUser: SessionInfo.userId,
}); });
if (!report.items || report.items.length === 0) { if (!report.items || report.items.length === 0) {
@@ -218,85 +237,113 @@ export default function OrganizationEdit() {
reader.onloadend = () => resolve(reader.result as string); reader.onloadend = () => resolve(reader.result as string);
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
}); });
} catch(e) {} } catch (e) {}
const tableBody: any[] = [ const tableBody: any[] = [
[ [
{ text: "Cliente", bold: true, color: "#333333", fillColor: "#f5f5f5" }, { text: "Cliente", bold: true, color: "#333333", fillColor: "#f5f5f5" },
{ text: "Datos del Turno", bold: true, color: "#333333", fillColor: "#f5f5f5" }, { text: "Datos del Turno", bold: true, color: "#333333", fillColor: "#f5f5f5" },
{ text: "Monto", bold: true, color: "#333333", fillColor: "#f5f5f5" } { text: "Monto", bold: true, color: "#333333", fillColor: "#f5f5f5" },
] ],
]; ];
report.items.forEach(item => { report.items.forEach((item) => {
tableBody.push([ tableBody.push([
item.clientName || "-", item.clientName || "-",
{ {
text: [ text: [
{ text: `${item.serviceName || "-"}\n`, bold: true, fontSize: 11 }, { text: `${item.serviceName || "-"}\n`, bold: true, fontSize: 11 },
`Prof: ${item.employeeName || "-"}\nFecha: ${dayjs(item.appointmentDate).format("DD/MM/YYYY HH:mm")} hs\nMétodo: ${item.method || "-"}` `Prof: ${item.employeeName || "-"}\nFecha: ${dayjs(item.appointmentDate).format("DD/MM/YYYY HH:mm")} hs\nMétodo: ${item.method || "-"}`,
], ],
fontSize: 10 fontSize: 10,
}, },
{ text: "$" + item.price.toLocaleString("es-AR"), alignment: "right" } { text: "$" + item.price.toLocaleString("es-AR"), alignment: "right" },
]); ]);
}); });
tableBody.push([ tableBody.push([
{ text: "TOTAL PROCESADO", bold: true, colSpan: 2, alignment: "right" }, { text: "TOTAL PROCESADO", bold: true, colSpan: 2, alignment: "right" },
{}, {},
{ text: "$" + report.totalAmount.toLocaleString("es-AR"), bold: true, alignment: "right" } {
text: "$" + report.totalAmount.toLocaleString("es-AR"),
bold: true,
alignment: "right",
},
]); ]);
const docDefinition: unknown = { const docDefinition: unknown = {
content: [ content: [
{ {
columns: [ columns: [
{ text: "Reporte de Cobros Procesados", fontSize: 18, bold: true, margin: [0, 0, 0, 10], width: '*' }, {
...(logoDataUrl ? [{ image: logoDataUrl, width: 45, margin: [0, 0, 0, 10], alignment: 'right' }] : []) text: "Reporte de Cobros Procesados",
fontSize: 18,
bold: true,
margin: [0, 0, 0, 10],
width: "*",
},
...(logoDataUrl
? [
{
image: logoDataUrl,
width: 45,
margin: [0, 0, 0, 10],
alignment: "right",
},
] ]
: []),
],
}, },
{ text: `Filtros aplicados:`, bold: true, margin: [0, 0, 0, 5] }, { text: `Filtros aplicados:`, bold: true, margin: [0, 0, 0, 5] },
{ {
text: [ text: [
{ text: "Clientes Seleccionados: ", bold: true }, { text: "Clientes Seleccionados: ", bold: true },
`${clientIds.length > 0 ? clientIds.length : "Todos"}` `${clientIds.length > 0 ? clientIds.length : "Todos"}`,
], ],
margin: [0, 0, 0, 2] margin: [0, 0, 0, 2],
}, },
{ {
text: [ text: [
{ text: "Período: ", bold: true }, { text: "Período: ", bold: true },
`${dateFrom ? dateFrom.format("DD/MM/YYYY") : "Inicio"} al ${dateTo ? dateTo.format("DD/MM/YYYY") : "Fin"}` `${dateFrom ? dateFrom.format("DD/MM/YYYY") : "Inicio"} al ${dateTo ? dateTo.format("DD/MM/YYYY") : "Fin"}`,
], ],
margin: [0, 0, 0, 20] margin: [0, 0, 0, 20],
}, },
{ {
table: { table: {
headerRows: 1, headerRows: 1,
widths: [120, "*", "auto"], widths: [120, "*", "auto"],
body: tableBody body: tableBody,
}, },
layout: "lightHorizontalLines" layout: "lightHorizontalLines",
} },
] ],
}; };
if ((window as unknown as { Android: any }).Android && (window as unknown as { Android: any }).Android.savePdf) { if (
(window as unknown as { Android: any }).Android &&
(window as unknown as { Android: any }).Android.savePdf
) {
pdfMake.createPdf(docDefinition as any).getBase64((data: any) => { pdfMake.createPdf(docDefinition as any).getBase64((data: any) => {
(window as unknown as { Android: any }).Android?.savePdf(data, `reporte-procesados.pdf`); (window as unknown as { Android: any }).Android?.savePdf(
data,
`reporte-procesados.pdf`,
);
}); });
} else { } else {
pdfMake.createPdf(docDefinition as any).download(`reporte-procesados.pdf`); pdfMake.createPdf(docDefinition as any).download(`reporte-procesados.pdf`);
} }
} catch (error: unknown) { } catch (error: unknown) {
console.error(error); console.error(error);
alert.showError((error as { format?: () => string }).format ? (error as { format: () => string }).format() : "Error generando PDF"); alert.showError(
(error as { format?: () => string }).format
? (error as { format: () => string }).format()
: "Error generando PDF",
);
} }
}; };
useEffect(() => { useEffect(() => {
const handleScroll = (e: Event) => { const handleScroll = (e: Event) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
// Check if it's a DOM element scrolling // Check if it's a DOM element scrolling
@@ -389,15 +436,23 @@ export default function OrganizationEdit() {
useEffect(() => { useEffect(() => {
const cFilter: FindClientsParams = { companyId: id }; const cFilter: FindClientsParams = { companyId: id };
filterObjects<FindClientsParams>("clients/filter-all", cFilter).then((res) => setClientFilterData(res)); filterObjects<FindClientsParams>("clients/filter-all", cFilter).then((res) =>
setClientFilterData(res),
);
const eFilter: FindEmployeesParams = { companyId: id }; const eFilter: FindEmployeesParams = { companyId: id };
filterObjects<FindEmployeesParams>("employees/filter-all", eFilter).then((res) => setCollaboratorFilterData(res)); filterObjects<FindEmployeesParams>("employees/filter-all", eFilter).then((res) =>
setCollaboratorFilterData(res),
);
const sFilter: FindServicesParams = { companyId: id }; const sFilter: FindServicesParams = { companyId: id };
filterObjects<FindServicesParams>("services/filter-all", sFilter).then((res) => setServiceFilterData(res)); filterObjects<FindServicesParams>("services/filter-all", sFilter).then((res) =>
setServiceFilterData(res),
);
setMethodFilterData(paymentMethods.map((m) => ({ id: m.id, text: m.name, name: m.name, image: "" }))); setMethodFilterData(
paymentMethods.map((m) => ({ id: m.id, text: m.name, name: m.name, image: "" })),
);
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -414,17 +469,30 @@ export default function OrganizationEdit() {
clientData={clientFilterData} clientData={clientFilterData}
setClientIds={setClientIds} setClientIds={setClientIds}
clientIds={clientIds} clientIds={clientIds}
/> />,
); );
filter.setOnFilter(() => { }); filter.setOnFilter(() => {});
}, [collaboratorFilterData, serviceFilterData, methodFilterData, clientFilterData]); }, [collaboratorFilterData, serviceFilterData, methodFilterData, clientFilterData]);
useEffect(() => { useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP); eventHandler.setEventType(EVENT_TYPES.SLEEP);
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ url: "/admin/org", title: "Organizaciones", icon: null }); location.add({ url: "/admin/org", title: "Organizaciones", icon: null });
location.add({ url: "/admin/org/profile/" + id, title: "Perfil", icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" /> }); location.add({
location.add({ url: "/admin/org/profile/" + id, title: "Pagos", icon: <PaymentsIcon sx={{ fontSize: "32px", mr: "10px" }} /> }); url: "/admin/org/profile/" + id,
title: "Perfil",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Pagos",
icon: <PaymentsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -444,35 +512,75 @@ export default function OrganizationEdit() {
<Location /> <Location />
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}> <SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}>
<div style={{ maxWidth: "800px", margin: "0 auto", paddingBottom: "100px", marginTop: "0px" }}> <div
style={{
maxWidth: "800px",
margin: "0 auto",
paddingBottom: "100px",
marginTop: "0px",
}}
>
{/* Sentinel element to detect scroll */} {/* Sentinel element to detect scroll */}
<div ref={sentinelRef} style={{ height: "1px", width: "100%" }}></div> <div ref={sentinelRef} style={{ height: "1px", width: "100%" }}></div>
{/* Top Status Card */} {/* Top Status Card */}
<div style={{ position: "sticky", top: "70px", zIndex: 100, marginBottom: "50px", display: "flex", flexDirection: "column", background: "white", paddingBottom: "10px", paddingTop: "5px", margin: "0 -10px", paddingLeft: "10px", paddingRight: "10px" }}> <div
style={{
position: "sticky",
top: "70px",
zIndex: 100,
marginBottom: "50px",
display: "flex",
flexDirection: "column",
background: "white",
paddingBottom: "10px",
paddingTop: "5px",
margin: "0 -10px",
paddingLeft: "10px",
paddingRight: "10px",
}}
>
{/* Tabs Selector */} {/* Tabs Selector */}
<div style={{ display: "flex", justifyContent: "center", marginBottom: "15px" }}> <div
style={{
display: "flex",
justifyContent: "center",
marginBottom: "15px",
}}
>
<ThemeProvider theme={turnosXpressTheme}> <ThemeProvider theme={turnosXpressTheme}>
<div style={{ <div
style={{
display: "flex", display: "flex",
background: "rgba(255,255,255,0.9)", background: "rgba(255,255,255,0.9)",
backdropFilter: "blur(10px)", backdropFilter: "blur(10px)",
borderRadius: "30px", borderRadius: "30px",
padding: "5px", padding: "5px",
boxShadow: "0 4px 15px rgba(0,0,0,0.05)" boxShadow: "0 4px 15px rgba(0,0,0,0.05)",
}}> }}
>
<ButtonMaterial <ButtonMaterial
variant="contained" variant="contained"
style={{ borderRadius: "25px", boxShadow: "0 2px 8px rgba(0,0,0,0.1)", fontWeight: "bold", padding: "8px 25px" }} style={{
borderRadius: "25px",
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
fontWeight: "bold",
padding: "8px 25px",
}}
> >
Pagados Pagados
</ButtonMaterial> </ButtonMaterial>
<ButtonMaterial <ButtonMaterial
variant="text" variant="text"
onClick={() => goTo(`/admin/org/profile/${id}/payments/not-payed`)} onClick={() =>
style={{ borderRadius: "25px", color: "#666", fontWeight: "bold", padding: "8px 25px" }} goTo(`/admin/org/profile/${id}/payments/not-payed`)
}
style={{
borderRadius: "25px",
color: "#666",
fontWeight: "bold",
padding: "8px 25px",
}}
> >
Adeudados Adeudados
</ButtonMaterial> </ButtonMaterial>
@@ -487,25 +595,33 @@ export default function OrganizationEdit() {
opacity: 1, opacity: 1,
y: 0, y: 0,
padding: isMinimized ? "25px 50px" : "40px 20px", padding: isMinimized ? "25px 50px" : "40px 20px",
borderRadius: isMinimized ? "15px" : "20px" borderRadius: isMinimized ? "15px" : "20px",
}} }}
style={{ style={{
background: "linear-gradient(135deg, #4b6cb7 0%, #182848 100%)", background: "linear-gradient(135deg, #4b6cb7 0%, #182848 100%)",
color: "white", color: "white",
textAlign: "center", textAlign: "center",
boxShadow: isMinimized ? "0 4px 15px rgba(0,0,0,0.3)" : "0 10px 30px rgba(0,0,0,0.15)", boxShadow: isMinimized
? "0 4px 15px rgba(0,0,0,0.3)"
: "0 10px 30px rgba(0,0,0,0.15)",
position: "relative", position: "relative",
display: "flex", display: "flex",
flexDirection: isMinimized ? "row" : "column", flexDirection: isMinimized ? "row" : "column",
alignItems: "center", alignItems: "center",
justifyContent: isMinimized ? "space-between" : "center", justifyContent: isMinimized ? "space-between" : "center",
transition: "box-shadow 0.3s ease" transition: "box-shadow 0.3s ease",
}} }}
> >
<IconButton <IconButton
onClick={() => setForceMinimized(!forceMinimized)} onClick={() => setForceMinimized(!forceMinimized)}
style={{ position: "absolute", top: "10px", left: "10px", color: "rgba(255,255,255,0.7)", zIndex: 10 }} style={{
position: "absolute",
top: "10px",
left: "10px",
color: "rgba(255,255,255,0.7)",
zIndex: 10,
}}
size="small" size="small"
title={forceMinimized ? "Maximizar" : "Minimizar"} title={forceMinimized ? "Maximizar" : "Minimizar"}
> >
@@ -513,28 +629,59 @@ export default function OrganizationEdit() {
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => filter.toggle()} onClick={() => filter.toggle()}
style={{ position: "absolute", top: "10px", right: "10px", color: "rgba(255,255,255,0.7)", zIndex: 10 }} style={{
position: "absolute",
top: "10px",
right: "10px",
color: "rgba(255,255,255,0.7)",
zIndex: 10,
}}
size="small" size="small"
title="Filtrar" title="Filtrar"
> >
<FilterAltOutlinedIcon /> <FilterAltOutlinedIcon />
</IconButton> </IconButton>
<div style={{ position: "absolute", top: "-50px", right: "-50px", width: "150px", height: "150px", borderRadius: "50%", background: "rgba(255,255,255,0.05)" }}></div> <div
<div style={{ position: "absolute", bottom: "-30px", left: "-30px", width: "100px", height: "100px", borderRadius: "50%", background: "rgba(255,255,255,0.05)" }}></div> style={{
position: "absolute",
top: "-50px",
right: "-50px",
width: "150px",
height: "150px",
borderRadius: "50%",
background: "rgba(255,255,255,0.05)",
}}
></div>
<div
style={{
position: "absolute",
bottom: "-30px",
left: "-30px",
width: "100px",
height: "100px",
borderRadius: "50%",
background: "rgba(255,255,255,0.05)",
}}
></div>
<motion.div <motion.div
animate={{ animate={{
fontSize: isMinimized ? "14px" : "16px", fontSize: isMinimized ? "14px" : "16px",
marginBottom: isMinimized ? "0px" : "10px" marginBottom: isMinimized ? "0px" : "10px",
}}
style={{
fontWeight: "bold",
textTransform: "uppercase",
letterSpacing: "2px",
opacity: 0.9,
}} }}
style={{ fontWeight: "bold", textTransform: "uppercase", letterSpacing: "2px", opacity: 0.9 }}
> >
Total Recaudado Total Recaudado
</motion.div> </motion.div>
<motion.div <motion.div
animate={{ animate={{
fontSize: isMinimized ? "24px" : "56px", fontSize: isMinimized ? "24px" : "56px",
margin: isMinimized ? "0px" : "10px 0" margin: isMinimized ? "0px" : "10px 0",
}} }}
style={{ fontWeight: "900" }} style={{ fontWeight: "900" }}
> >
@@ -547,7 +694,16 @@ export default function OrganizationEdit() {
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
style={{ fontSize: "14px", opacity: 0.8 }} style={{ fontSize: "14px", opacity: 0.8 }}
> >
<div style={{ display: "flex", gap: "10px", alignItems: "center", flexWrap: "wrap", justifyContent: "center", marginTop: "10px" }}> <div
style={{
display: "flex",
gap: "10px",
alignItems: "center",
flexWrap: "wrap",
justifyContent: "center",
marginTop: "10px",
}}
>
<div style={{ width: "250px", maxWidth: "100%" }}> <div style={{ width: "250px", maxWidth: "100%" }}>
<TextRangeDateModern <TextRangeDateModern
label="Período" label="Período"
@@ -567,10 +723,13 @@ export default function OrganizationEdit() {
borderRadius: "20px", borderRadius: "20px",
padding: "8px 20px", padding: "8px 20px",
textTransform: "none", textTransform: "none",
height: "56px" height: "56px",
}} }}
> >
<PictureAsPdfIcon style={{ fontSize: "18px", marginRight: "5px" }} /> PDF <PictureAsPdfIcon
style={{ fontSize: "18px", marginRight: "5px" }}
/>{" "}
PDF
</ButtonMaterial> </ButtonMaterial>
</div> </div>
</motion.div> </motion.div>
@@ -579,10 +738,22 @@ export default function OrganizationEdit() {
</div> </div>
{/* Dynamic Spacer for layout shift */} {/* Dynamic Spacer for layout shift */}
<div style={{ height: isMinimized ? "100px" : "0px", transition: "height 0.3s ease" }}></div> <div
style={{
height: isMinimized ? "100px" : "0px",
transition: "height 0.3s ease",
}}
></div>
{/* Payments List */} {/* Payments List */}
<div style={{ display: "flex", flexDirection: "column", gap: "12px", paddingTop: "15px" }}> <div
style={{
display: "flex",
flexDirection: "column",
gap: "12px",
paddingTop: "15px",
}}
>
{dataList.length > 0 ? ( {dataList.length > 0 ? (
<AnimatePresence> <AnimatePresence>
{dataList.map((payment, index) => ( {dataList.map((payment, index) => (
@@ -602,11 +773,19 @@ export default function OrganizationEdit() {
padding: "16px 20px", padding: "16px 20px",
boxShadow: "0 4px 12px rgba(0,0,0,0.05)", boxShadow: "0 4px 12px rgba(0,0,0,0.05)",
borderLeft: "5px solid #28a745", borderLeft: "5px solid #28a745",
position: "relative" position: "relative",
}} }}
> >
<div style={{ display: "flex", alignItems: "center", gap: "15px", flex: 1 }}> <div
<div style={{ style={{
display: "flex",
alignItems: "center",
gap: "15px",
flex: 1,
}}
>
<div
style={{
width: "50px", width: "50px",
height: "50px", height: "50px",
borderRadius: "12px", borderRadius: "12px",
@@ -614,31 +793,65 @@ export default function OrganizationEdit() {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
color: "#28a745" color: "#28a745",
}}> }}
>
{getPaymentIcon(payment.method)} {getPaymentIcon(payment.method)}
</div> </div>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div style={{ fontSize: "16px", fontWeight: "bold", color: "#333", marginBottom: "2px" }}> <div
style={{
fontSize: "16px",
fontWeight: "bold",
color: "#333",
marginBottom: "2px",
}}
>
{payment.clientName} {payment.clientName}
</div> </div>
<div style={{ fontSize: "14px", color: "var(--wine-dark)", fontWeight: "bold", marginBottom: "4px" }}> <div
style={{
fontSize: "14px",
color: "var(--wine-dark)",
fontWeight: "bold",
marginBottom: "4px",
}}
>
{payment.serviceName} {payment.serviceName}
</div> </div>
<div style={{ display: "flex", gap: "10px", fontSize: "12px", color: "#888" }}> <div
<span>{dayjs(payment.createdAt).format("DD MMMM YYYY • HH:mm")}</span> style={{
display: "flex",
gap: "10px",
fontSize: "12px",
color: "#888",
}}
>
<span>
{dayjs(payment.createdAt).format(
"DD MMMM YYYY • HH:mm",
)}
</span>
<span>•</span> <span>•</span>
<span>{payment.employeeName}</span> <span>{payment.employeeName}</span>
</div> </div>
</div> </div>
</div> </div>
<div style={{ display: "flex", alignItems: "center", gap: "15px" }}> <div
<div style={{ style={{
display: "flex",
alignItems: "center",
gap: "15px",
}}
>
<div
style={{
fontSize: "20px", fontSize: "20px",
fontWeight: "900", fontWeight: "900",
color: "#28a745", color: "#28a745",
textAlign: "right" textAlign: "right",
}}> }}
>
+{formatPrice(payment.price)} +{formatPrice(payment.price)}
</div> </div>
<IconButton <IconButton
@@ -646,14 +859,18 @@ export default function OrganizationEdit() {
size="small" size="small"
onClick={() => { onClick={() => {
confirm.show( confirm.show(
"¿Estás seguro que quieres eliminar el pago de " + payment.clientName + "?", "¿Estás seguro que quieres eliminar el pago de " +
payment.clientName +
"?",
() => deletePaymentHandler(payment.id), () => deletePaymentHandler(payment.id),
() => { } () => {},
); );
}} }}
sx={{ sx={{
backgroundColor: "rgba(220, 53, 69, 0.05)", backgroundColor: "rgba(220, 53, 69, 0.05)",
"&:hover": { backgroundColor: "rgba(220, 53, 69, 0.15)" } "&:hover": {
backgroundColor: "rgba(220, 53, 69, 0.15)",
},
}} }}
> >
<DeleteIcon fontSize="small" /> <DeleteIcon fontSize="small" />
@@ -666,17 +883,29 @@ export default function OrganizationEdit() {
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
style={{ textAlign: "center", padding: "60px 20px", backgroundColor: "#fff", borderRadius: "15px", border: "1px dashed #ccc" }} style={{
textAlign: "center",
padding: "60px 20px",
backgroundColor: "#fff",
borderRadius: "15px",
border: "1px dashed #ccc",
}}
> >
<SearchOffIcon sx={{ fontSize: "64px", color: "#ccc", marginBottom: "15px" }} /> <SearchOffIcon
sx={{ fontSize: "64px", color: "#ccc", marginBottom: "15px" }}
/>
<h2 style={{ margin: 0, color: "#999" }}>Sin Pagos</h2> <h2 style={{ margin: 0, color: "#999" }}>Sin Pagos</h2>
<p style={{ color: "#aaa" }}>No se encontraron pagos registrados para este período.</p> <p style={{ color: "#aaa" }}>
No se encontraron pagos registrados para este período.
</p>
</motion.div> </motion.div>
)} )}
</div> </div>
{payments && payments.pages > 1 && ( {payments && payments.pages > 1 && (
<div style={{ display: "flex", justifyContent: "center", marginTop: "40px" }}> <div
style={{ display: "flex", justifyContent: "center", marginTop: "40px" }}
>
<Pagination <Pagination
count={payments.pages} count={payments.pages}
page={payments.page} page={payments.page}
@@ -17,6 +17,7 @@ import Avatar from "@components/Avatar/Avatar";
import Button from "@core/app/components/Button/Button"; import Button from "@core/app/components/Button/Button";
import { useConfirmStore } from "@core/Store/Confirm.Store"; import { useConfirmStore } from "@core/Store/Confirm.Store";
import { getMondayWeekStart } from "@core/helpers/week"; import { getMondayWeekStart } from "@core/helpers/week";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
dayjs.locale("es"); dayjs.locale("es");
@@ -28,6 +29,7 @@ export default function Schedules() {
const data = useParams<UrlData>(); const data = useParams<UrlData>();
const id = data.id; const id = data.id;
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore(); const SessionInfo = useSessionStore();
const alert = useAlert(); const alert = useAlert();
@@ -38,6 +40,8 @@ export default function Schedules() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org", url: "/admin/org",
title: "Organizaciones", title: "Organizaciones",
@@ -55,6 +59,10 @@ export default function Schedules() {
title: "Horarios Habilitados", title: "Horarios Habilitados",
icon: null, icon: null,
}); });
return () => {
bottomToolbar.clear();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -118,7 +126,7 @@ export default function Schedules() {
}, },
() => { () => {
return; return;
} },
); );
}; };
@@ -80,7 +80,7 @@ export default function OrganizationService() {
const [serviceDiscountName, setServiceDiscountName] = useState<string | undefined>(undefined); const [serviceDiscountName, setServiceDiscountName] = useState<string | undefined>(undefined);
const [servicePublished, setServicePublished] = useState<SERVICE_PUBLISHED_STATUS | undefined>( const [servicePublished, setServicePublished] = useState<SERVICE_PUBLISHED_STATUS | undefined>(
undefined undefined,
); );
const [savePublishedStatusAvailable, setSavePublishedStatusAvailable] = useState(false); const [savePublishedStatusAvailable, setSavePublishedStatusAvailable] = useState(false);
@@ -149,7 +149,7 @@ export default function OrganizationService() {
> >
Aplicar Descuento Aplicar Descuento
</ButtonMaterial> </ButtonMaterial>
</div> </div>,
); );
}, []); }, []);
@@ -271,8 +271,12 @@ export default function OrganizationService() {
setServiceDiscountType(response.discountType); setServiceDiscountType(response.discountType);
setServiceDiscountValue(response.discountValue); setServiceDiscountValue(response.discountValue);
setServiceDiscountName(response.discountName); setServiceDiscountName(response.discountName);
setShowPublicScores(response.showPublicScores !== undefined ? response.showPublicScores : true); setShowPublicScores(
setShowPublicOpinions(response.showPublicOpinions !== undefined ? response.showPublicOpinions : true); response.showPublicScores !== undefined ? response.showPublicScores : true,
);
setShowPublicOpinions(
response.showPublicOpinions !== undefined ? response.showPublicOpinions : true,
);
}) })
.catch(() => {}) .catch(() => {})
.finally(() => { .finally(() => {
@@ -283,7 +287,7 @@ export default function OrganizationService() {
const formatDiscount = ( const formatDiscount = (
value: number | undefined, value: number | undefined,
type: string | undefined, type: string | undefined,
name: string | undefined name: string | undefined,
): string => { ): string => {
if (value === undefined || value === null || type === undefined || type === null) { if (value === undefined || value === null || type === undefined || type === null) {
return ""; return "";
@@ -464,7 +468,7 @@ export default function OrganizationService() {
setServicePublished( setServicePublished(
checked checked
? SERVICE_PUBLISHED_STATUS.PUBLISHED ? SERVICE_PUBLISHED_STATUS.PUBLISHED
: SERVICE_PUBLISHED_STATUS.PRIVATE : SERVICE_PUBLISHED_STATUS.PRIVATE,
); );
}} }}
/> />
@@ -506,10 +510,15 @@ export default function OrganizationService() {
/> />
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}> <MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
<b>Visibilidad pública:</b> Elegí si querés mostrar puntuaciones y opiniones de este servicio en páginas públicas. Disponible para planes pagos. <b>Visibilidad pública:</b> Elegí si querés mostrar puntuaciones y opiniones de este
servicio en páginas públicas. Disponible para planes pagos.
</MaterialAlert> </MaterialAlert>
<div style={{ display: "flex", flexDirection: "column", gap: "10px", marginTop: "10px" }}> <div
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}> style={{ display: "flex", flexDirection: "column", gap: "10px", marginTop: "10px" }}
>
<label
style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}
>
<Switch <Switch
checked={showPublicScores} checked={showPublicScores}
disabled={!canEditPublicRatingFlags} disabled={!canEditPublicRatingFlags}
@@ -520,7 +529,9 @@ export default function OrganizationService() {
/> />
Mostrar puntuaciones públicas Mostrar puntuaciones públicas
</label> </label>
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}> <label
style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}
>
<Switch <Switch
checked={showPublicOpinions} checked={showPublicOpinions}
disabled={!canEditPublicRatingFlags} disabled={!canEditPublicRatingFlags}
@@ -567,7 +578,7 @@ export default function OrganizationService() {
() => { () => {
deleteDiscountHandler(); deleteDiscountHandler();
}, },
() => {} () => {},
); );
}} }}
> >
@@ -32,8 +32,9 @@ import {
WhatsappIcon as WhatsAppShareIcon, WhatsappIcon as WhatsAppShareIcon,
TwitterIcon, TwitterIcon,
EmailIcon, EmailIcon,
TelegramIcon TelegramIcon,
} from "react-share"; } from "react-share";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
id: string; id: string;
@@ -43,6 +44,7 @@ export default function Org() {
const urlData = useParams<UrlData>(); const urlData = useParams<UrlData>();
const id = urlData.id; const id = urlData.id;
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const alert = useAlert(); const alert = useAlert();
@@ -58,6 +60,8 @@ export default function Org() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org", url: "/admin/org",
title: "Mis organizaciones", title: "Mis organizaciones",
@@ -75,6 +79,10 @@ export default function Org() {
title: "Mis servicios", title: "Mis servicios",
icon: <StorefrontOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />, icon: <StorefrontOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
return () => {
bottomToolbar.clear();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -105,7 +113,7 @@ export default function Org() {
<div <div
className={style.serviceCard} className={style.serviceCard}
style={{ style={{
borderLeft: `6px solid ${data.color}` borderLeft: `6px solid ${data.color}`,
}} }}
onClick={() => handleServiceClick(data.id)} onClick={() => handleServiceClick(data.id)}
> >
@@ -115,7 +123,11 @@ export default function Org() {
<div className={style.serviceInfo}> <div className={style.serviceInfo}>
<h1>{data.name}</h1> <h1>{data.name}</h1>
<p>{data.description}</p> <p>{data.description}</p>
<ServiceRatingSummary serviceId={data.id} variant="row" showOpinionCount={false} /> <ServiceRatingSummary
serviceId={data.id}
variant="row"
showOpinionCount={false}
/>
</div> </div>
<div className={style.serviceActions}> <div className={style.serviceActions}>
<span className={style.servicePrice}>{formatPrice(data.price)}</span> <span className={style.servicePrice}>{formatPrice(data.price)}</span>
@@ -159,8 +171,21 @@ export default function Org() {
<div style={{ marginBottom: "20px" }}>&nbsp;</div> <div style={{ marginBottom: "20px" }}>&nbsp;</div>
{shareService && ( {shareService && (
<Dialog open={!!shareService} onClose={() => setShareService(null)} maxWidth="sm" fullWidth> <Dialog
<DialogTitle sx={{ m: 0, p: 2, display: "flex", justifyContent: "space-between", alignItems: "center" }}> open={!!shareService}
onClose={() => setShareService(null)}
maxWidth="sm"
fullWidth
>
<DialogTitle
sx={{
m: 0,
p: 2,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span style={{ fontWeight: "bold" }}>Compartir Servicio</span> <span style={{ fontWeight: "bold" }}>Compartir Servicio</span>
<IconButton <IconButton
aria-label="close" aria-label="close"
@@ -171,23 +196,57 @@ export default function Org() {
</IconButton> </IconButton>
</DialogTitle> </DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<div style={{ textAlign: "center", marginBottom: "30px", fontSize: "16px", color: "var(--black-light)" }}> <div
Compartí tu servicio <strong>{shareService.name}</strong> para que tus clientes puedan reservarlo directamente. style={{
textAlign: "center",
marginBottom: "30px",
fontSize: "16px",
color: "var(--black-light)",
}}
>
Compartí tu servicio <strong>{shareService.name}</strong> para que tus
clientes puedan reservarlo directamente.
</div> </div>
<div style={{ display: "flex", justifyContent: "center", flexDirection: "row", alignItems: "center", gap: "20px", paddingBottom: "20px" }}> <div
<WhatsappShareButton title={"Reserva este servicio: " + shareService.name} separator=":: " url={window.location.origin + "/landing/service/" + shareService.id}> style={{
display: "flex",
justifyContent: "center",
flexDirection: "row",
alignItems: "center",
gap: "20px",
paddingBottom: "20px",
}}
>
<WhatsappShareButton
title={"Reserva este servicio: " + shareService.name}
separator=":: "
url={window.location.origin + "/landing/service/" + shareService.id}
>
<WhatsAppShareIcon size={54} round /> <WhatsAppShareIcon size={54} round />
</WhatsappShareButton> </WhatsappShareButton>
<FacebookShareButton title={"Reserva este servicio: " + shareService.name} url={window.location.origin + "/landing/service/" + shareService.id}> <FacebookShareButton
title={"Reserva este servicio: " + shareService.name}
url={window.location.origin + "/landing/service/" + shareService.id}
>
<FacebookIcon size={54} round /> <FacebookIcon size={54} round />
</FacebookShareButton> </FacebookShareButton>
<EmailShareButton title={"Reserva este servicio: " + shareService.name} separator=":: " url={window.location.origin + "/landing/service/" + shareService.id}> <EmailShareButton
title={"Reserva este servicio: " + shareService.name}
separator=":: "
url={window.location.origin + "/landing/service/" + shareService.id}
>
<EmailIcon size={54} round /> <EmailIcon size={54} round />
</EmailShareButton> </EmailShareButton>
<TelegramShareButton title={"Reserva este servicio: " + shareService.name} url={window.location.origin + "/landing/service/" + shareService.id}> <TelegramShareButton
title={"Reserva este servicio: " + shareService.name}
url={window.location.origin + "/landing/service/" + shareService.id}
>
<TelegramIcon size={54} round /> <TelegramIcon size={54} round />
</TelegramShareButton> </TelegramShareButton>
<TwitterShareButton title={"Reserva este servicio: " + shareService.name} url={window.location.origin + "/landing/service/" + shareService.id}> <TwitterShareButton
title={"Reserva este servicio: " + shareService.name}
url={window.location.origin + "/landing/service/" + shareService.id}
>
<TwitterIcon size={54} round /> <TwitterIcon size={54} round />
</TwitterShareButton> </TwitterShareButton>
</div> </div>
@@ -54,7 +54,7 @@ export default function TemplateCreate() {
const insertAtCursor = ( const insertAtCursor = (
ref: React.RefObject<HTMLInputElement | HTMLTextAreaElement>, ref: React.RefObject<HTMLInputElement | HTMLTextAreaElement>,
insertText: string insertText: string,
) => { ) => {
const el = ref.current; const el = ref.current;
if (!el) return; // todavía no está montado if (!el) return; // todavía no está montado
@@ -126,14 +126,14 @@ export default function TemplateCreate() {
}, },
() => { () => {
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE); eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
} },
); );
}} }}
> >
Eliminar Eliminar
</ButtonMaterial> </ButtonMaterial>
)} )}
</div> </div>,
); );
loadTemplate(); loadTemplate();
@@ -17,6 +17,7 @@ import {
} from "@models/Templates.model"; } from "@models/Templates.model";
import { findTemplatesByOrganization } from "./Templates.Service"; import { findTemplatesByOrganization } from "./Templates.Service";
import { useNavigation } from "@core/app/hooks/goto"; import { useNavigation } from "@core/app/hooks/goto";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
id: string; id: string;
@@ -29,6 +30,7 @@ export default function TemplatesGrid() {
const oid = data.id; const oid = data.id;
const { goTo } = useNavigation(); const { goTo } = useNavigation();
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const formatType = (type: string) => { const formatType = (type: string) => {
@@ -91,8 +93,8 @@ export default function TemplatesGrid() {
parseInt( parseInt(
process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE ? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
: "10" : "10",
) ),
); );
const [templates, setTemplates] = useState<PaginateTemplatesResults>(); const [templates, setTemplates] = useState<PaginateTemplatesResults>();
@@ -103,6 +105,8 @@ export default function TemplatesGrid() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org", url: "/admin/org",
title: "Mis organizaciones", title: "Mis organizaciones",
@@ -122,6 +126,10 @@ export default function TemplatesGrid() {
}); });
loadTemplates(1, pageSize); loadTemplates(1, pageSize);
return () => {
bottomToolbar.clear();
};
}, []); }, []);
const loadTemplates = (page: number = 1, pageSize: number = 30) => { const loadTemplates = (page: number = 1, pageSize: number = 30) => {
@@ -26,6 +26,7 @@ import Image from "next/image";
import Switch from "@core/app/components/Switch/Switch"; import Switch from "@core/app/components/Switch/Switch";
import { useConfirmStore } from "@core/Store/Confirm.Store"; import { useConfirmStore } from "@core/Store/Confirm.Store";
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan"; import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = { type UrlData = {
id: string; id: string;
@@ -35,6 +36,7 @@ export default function OrganizationEdit() {
const data = useParams<UrlData>(); const data = useParams<UrlData>();
const id = data.id; const id = data.id;
const location = useLocationStore(); const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore(); const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore(); const SessionInfo = useSessionStore();
const alert = useAlert(); const alert = useAlert();
@@ -46,6 +48,8 @@ export default function OrganizationEdit() {
useEffect(() => { useEffect(() => {
location.clear(); location.clear();
bottomToolbar.set(<></>);
location.add({ location.add({
url: "/admin/org", url: "/admin/org",
title: "Organizaciones", title: "Organizaciones",
@@ -63,6 +67,10 @@ export default function OrganizationEdit() {
title: "Whatsapp", title: "Whatsapp",
icon: <OrgConfigIcon sx={{ fontSize: "32px", mr: "10px" }} />, icon: <OrgConfigIcon sx={{ fontSize: "32px", mr: "10px" }} />,
}); });
return () => {
bottomToolbar.clear();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -420,7 +428,7 @@ export default function OrganizationEdit() {
eventHandler.setEventType(EVENT_TYPES.SLEEP); eventHandler.setEventType(EVENT_TYPES.SLEEP);
}); });
}, },
() => { } () => {},
); );
}} }}
/> />