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 SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
import { motion } from "motion/react";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
oid: string;
@@ -36,13 +37,14 @@ export default function ClientAccount() {
const alert = useAlert();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
const [view, setView] = useState<ClientAccountView | null>(null);
const [isScrolled, setIsScrolled] = useState(false);
const [forceMinimized, setForceMinimized] = useState(false);
const isMinimized = forceMinimized || isScrolled;
useEffect(() => {
@@ -68,6 +70,8 @@ export default function ClientAccount() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org/profile/" + id + "/clients",
title: "Clientes",
@@ -85,6 +89,10 @@ export default function ClientAccount() {
title: "Cuenta Corriente",
icon: <AccountBalanceOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, []);
useEffect(() => {
@@ -127,149 +135,273 @@ export default function ClientAccount() {
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}>
<div style={{ maxWidth: "800px", margin: "0 auto", paddingBottom: "100px" }}>
{/* 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" }}>
{view && (
<motion.div
layout
initial={{ opacity: 0, y: -20 }}
animate={{
opacity: 1,
y: 0,
padding: isMinimized ? "25px 50px" : "40px 20px",
borderRadius: isMinimized ? "15px" : "20px"
}}
<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 && (
<motion.div
layout
initial={{ opacity: 0, y: -20 }}
animate={{
opacity: 1,
y: 0,
padding: isMinimized ? "25px 50px" : "40px 20px",
borderRadius: isMinimized ? "15px" : "20px",
}}
style={{
display: "flex",
flexDirection: isMinimized ? "row" : "column",
alignItems: "center",
justifyContent: isMinimized ? "space-between" : "center",
background:
view.amount >= 0
? "linear-gradient(135deg, #1d976c 0%, #93f9b9 100%)"
: "linear-gradient(135deg, #cb2d3e 0%, #ef473a 100%)",
color: "white",
boxShadow: isMinimized
? "0 4px 15px rgba(0,0,0,0.3)"
: "0 10px 30px rgba(0,0,0,0.15)",
position: "relative",
overflow: "hidden",
transition: "box-shadow 0.3s ease",
}}
>
<IconButton
onClick={() => setForceMinimized(!forceMinimized)}
style={{
position: "absolute",
top: "10px",
left: "10px",
color: "rgba(255,255,255,0.7)",
zIndex: 10,
}}
size="small"
title={forceMinimized ? "Maximizar" : "Minimizar"}
>
{forceMinimized ? <UnfoldMoreIcon /> : <UnfoldLessIcon />}
</IconButton>
{/* 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
style={{
position: "absolute",
bottom: "-30px",
left: "-30px",
width: "100px",
height: "100px",
borderRadius: "50%",
background: "rgba(255,255,255,0.1)",
}}
></div>
<motion.div
animate={{
fontSize: isMinimized ? "14px" : "16px",
marginBottom: isMinimized ? "0px" : "0px",
marginLeft: isMinimized ? "30px" : "0px",
}}
style={{
fontWeight: "bold",
opacity: 0.9,
textTransform: "uppercase",
letterSpacing: "1px",
}}
>
Saldo Actual
</motion.div>
<motion.div
animate={{
fontSize: isMinimized ? "24px" : "56px",
margin: isMinimized ? "0px" : "10px 0 30px 0",
}}
style={{
fontWeight: "900",
textShadow: "0 2px 10px rgba(0,0,0,0.1)",
}}
>
{formatPrice(view.amount)}
</motion.div>
{!isMinimized && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
style={{
display: "flex",
gap: "15px",
flexDirection: "row",
zIndex: 1,
}}
>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<IconButton
sx={{
backgroundColor: "rgba(255,255,255,0.2)",
color: "white",
display: "flex",
alignItems: "center",
flexDirection: "column",
width: "110px",
height: "90px",
borderRadius: "15px",
"&:hover": {
backgroundColor: "rgba(255,255,255,0.3)",
},
}}
onClick={() =>
add(CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT)
}
>
<DepositarIcon
sx={{ fontSize: "32px", marginBottom: "5px" }}
/>
<span
style={{ fontSize: "14px", fontWeight: "bold" }}
>
Ingresar
</span>
</IconButton>
</motion.div>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<IconButton
sx={{
backgroundColor: "rgba(255,255,255,0.2)",
color: "white",
display: "flex",
alignItems: "center",
flexDirection: "column",
width: "110px",
height: "90px",
borderRadius: "15px",
"&:hover": {
backgroundColor: "rgba(255,255,255,0.3)",
},
}}
onClick={() =>
add(CLIENT_ACCOUNT_MOVEMENT_TYPES.WITHDRAWAL)
}
>
<RetirarIcon
sx={{ fontSize: "32px", marginBottom: "5px" }}
/>
<span
style={{ fontSize: "14px", fontWeight: "bold" }}
>
Retirar
</span>
</IconButton>
</motion.div>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<IconButton
sx={{
backgroundColor: "white",
color: view.amount >= 0 ? "#1d976c" : "#cb2d3e",
display: "flex",
alignItems: "center",
flexDirection: "column",
width: "110px",
height: "90px",
borderRadius: "15px",
boxShadow: "0 4px 15px rgba(0,0,0,0.1)",
"&:hover": { backgroundColor: "#f8f9fa" },
}}
onClick={() =>
goTo(
"/admin/org/" +
id +
"/client/" +
clientId +
"/account/movements",
)
}
>
<MovimientosIcon
sx={{ fontSize: "32px", marginBottom: "5px" }}
/>
<span
style={{ fontSize: "14px", fontWeight: "bold" }}
>
Historial
</span>
</IconButton>
</motion.div>
</motion.div>
)}
</motion.div>
)}
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
<div
style={{
display: "flex",
flexDirection: isMinimized ? "row" : "column",
justifyContent: "space-between",
alignItems: "center",
justifyContent: isMinimized ? "space-between" : "center",
background: view.amount >= 0
? "linear-gradient(135deg, #1d976c 0%, #93f9b9 100%)"
: "linear-gradient(135deg, #cb2d3e 0%, #ef473a 100%)",
color: "white",
boxShadow: isMinimized ? "0 4px 15px rgba(0,0,0,0.3)" : "0 10px 30px rgba(0,0,0,0.15)",
position: "relative",
overflow: "hidden",
transition: "box-shadow 0.3s ease"
marginBottom: "15px",
padding: "0 10px",
}}
>
<IconButton
onClick={() => setForceMinimized(!forceMinimized)}
style={{ position: "absolute", top: "10px", left: "10px", color: "rgba(255,255,255,0.7)", zIndex: 10 }}
size="small"
title={forceMinimized ? "Maximizar" : "Minimizar"}
>
{forceMinimized ? <UnfoldMoreIcon /> : <UnfoldLessIcon />}
</IconButton>
{/* 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 style={{ position: "absolute", bottom: "-30px", left: "-30px", width: "100px", height: "100px", borderRadius: "50%", background: "rgba(255,255,255,0.1)" }}></div>
<motion.div
animate={{
fontSize: isMinimized ? "14px" : "16px",
marginBottom: isMinimized ? "0px" : "0px",
marginLeft: isMinimized ? "30px" : "0px"
<div
style={{
fontSize: "20px",
fontWeight: "bold",
color: "var(--black-light)",
}}
style={{ fontWeight: "bold", opacity: 0.9, textTransform: "uppercase", letterSpacing: "1px" }}
>
Saldo Actual
</motion.div>
<motion.div
animate={{
fontSize: isMinimized ? "24px" : "56px",
margin: isMinimized ? "0px" : "10px 0 30px 0"
}}
style={{ fontWeight: "900", textShadow: "0 2px 10px rgba(0,0,0,0.1)" }}
>
{formatPrice(view.amount)}
</motion.div>
{!isMinimized && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
style={{ display: "flex", gap: "15px", flexDirection: "row", zIndex: 1 }}
>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<IconButton
sx={{
backgroundColor: "rgba(255,255,255,0.2)",
color: "white",
display: "flex",
alignItems: "center",
flexDirection: "column",
width: "110px",
height: "90px",
borderRadius: "15px",
"&:hover": { backgroundColor: "rgba(255,255,255,0.3)" }
}}
onClick={() => add(CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT)}
>
<DepositarIcon sx={{ fontSize: "32px", marginBottom: "5px" }} />
<span style={{ fontSize: "14px", fontWeight: "bold" }}>Ingresar</span>
</IconButton>
</motion.div>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<IconButton
sx={{
backgroundColor: "rgba(255,255,255,0.2)",
color: "white",
display: "flex",
alignItems: "center",
flexDirection: "column",
width: "110px",
height: "90px",
borderRadius: "15px",
"&:hover": { backgroundColor: "rgba(255,255,255,0.3)" }
}}
onClick={() => add(CLIENT_ACCOUNT_MOVEMENT_TYPES.WITHDRAWAL)}
>
<RetirarIcon sx={{ fontSize: "32px", marginBottom: "5px" }} />
<span style={{ fontSize: "14px", fontWeight: "bold" }}>Retirar</span>
</IconButton>
</motion.div>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<IconButton
sx={{
backgroundColor: "white",
color: view.amount >= 0 ? "#1d976c" : "#cb2d3e",
display: "flex",
alignItems: "center",
flexDirection: "column",
width: "110px",
height: "90px",
borderRadius: "15px",
boxShadow: "0 4px 15px rgba(0,0,0,0.1)",
"&:hover": { backgroundColor: "#f8f9fa" }
}}
onClick={() => goTo("/admin/org/" + id + "/client/" + clientId + "/account/movements")}
>
<MovimientosIcon sx={{ fontSize: "32px", marginBottom: "5px" }} />
<span style={{ fontSize: "14px", fontWeight: "bold" }}>Historial</span>
</IconButton>
</motion.div>
</motion.div>
)}
</motion.div>
)}
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
<div 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
</div>
<div
style={{ fontSize: "14px", color: "var(--wine-dark)", cursor: "pointer", fontWeight: "bold" }}
onClick={() => goTo("/admin/org/" + id + "/client/" + clientId + "/account/movements")}
<div
style={{
fontSize: "14px",
color: "var(--wine-dark)",
cursor: "pointer",
fontWeight: "bold",
}}
onClick={() =>
goTo(
"/admin/org/" +
id +
"/client/" +
clientId +
"/account/movements",
)
}
>
Ver todos
</div>
@@ -294,42 +426,79 @@ export default function ClientAccount() {
boxShadow: "0 2px 8px rgba(0,0,0,0.05)",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "15px" }}>
<div style={{
width: "45px",
height: "45px",
borderRadius: "12px",
backgroundColor: movement.amount > 0 ? "rgba(40, 167, 69, 0.1)" : "rgba(220, 53, 69, 0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: movement.amount > 0 ? "#28a745" : "#dc3545"
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "15px",
}}
>
<div
style={{
width: "45px",
height: "45px",
borderRadius: "12px",
backgroundColor:
movement.amount > 0
? "rgba(40, 167, 69, 0.1)"
: "rgba(220, 53, 69, 0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
color:
movement.amount > 0 ? "#28a745" : "#dc3545",
}}
>
{getPaymentIcon(movement.paymentMethod)}
</div>
<div>
<div style={{ fontSize: "16px", fontWeight: "bold", color: "#333", marginBottom: "3px" }}>
<div
style={{
fontSize: "16px",
fontWeight: "bold",
color: "#333",
marginBottom: "3px",
}}
>
{movement.description}
</div>
<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 style={{
fontSize: "18px",
fontWeight: "900",
color: movement.amount > 0 ? "#28a745" : "#dc3545",
}}>
{movement.amount > 0 ? "+" : ""}{formatPrice(movement.amount)}
<div
style={{
fontSize: "18px",
fontWeight: "900",
color: movement.amount > 0 ? "#28a745" : "#dc3545",
}}
>
{movement.amount > 0 ? "+" : ""}
{formatPrice(movement.amount)}
</div>
</motion.div>
))}
</div>
) : (
<div style={{ 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" }}>
<div
style={{
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
</div>
<div style={{ fontSize: "14px" }}>
@@ -1,9 +1,6 @@
"use client";
import { useAlert } from "@core/Store/Alert.Store";
import {
useEventHandlerStore,
EVENT_TYPES,
} from "@core/Store/EventHandler.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@core/Store/EventHandler.Store";
import { useLocationStore } from "@core/Store/Location.Store";
import { useParams } from "next/navigation";
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 { useNavigation } from "@core/app/hooks/goto";
import { useMenuStore } from "@core/Store/Menu.Store";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
oid: string;
@@ -26,14 +24,14 @@ export default function ClientAppointments() {
const clientId = data.clientId;
const alert = useAlert();
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const menu = useMenuStore();
const { goTo } = useNavigation();
const [appintments, setAppointments] =
useState<AppointmentAdminByClientView>({
events: [],
});
const [appintments, setAppointments] = useState<AppointmentAdminByClientView>({
events: [],
});
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
@@ -44,6 +42,8 @@ export default function ClientAppointments() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org/profile/" + id + "/clients",
title: "Clientes",
@@ -61,6 +61,10 @@ export default function ClientAppointments() {
title: "Turnos reservados",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, []);
useEffect(() => {
@@ -89,15 +93,17 @@ export default function ClientAppointments() {
}}
>
{appintments.events.length === 0 ? (
<div style={{
gridColumn: "1 / -1",
textAlign: "center",
padding: "40px 20px",
background: "#fff",
borderRadius: "12px",
boxShadow: "0 4px 15px rgba(0,0,0,0.02)",
color: "#7f8c8d"
}}>
<div
style={{
gridColumn: "1 / -1",
textAlign: "center",
padding: "40px 20px",
background: "#fff",
borderRadius: "12px",
boxShadow: "0 4px 15px rgba(0,0,0,0.02)",
color: "#7f8c8d",
}}
>
No hay turnos registrados para este cliente.
</div>
) : (
@@ -113,7 +119,7 @@ export default function ClientAppointments() {
"/client/" +
clientId +
"/appointments/" +
event.id
event.id,
);
}}
/>
@@ -15,6 +15,7 @@ import { validatePhone } from "@core/helpers/validatePhone";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import Switch from "@core/app/components/Switch/Switch";
import { Box, Typography } from "@mui/material";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
oid: string;
@@ -29,6 +30,7 @@ export default function ClientCreate() {
const confirm = useConfirmStore();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
@@ -46,6 +48,8 @@ export default function ClientCreate() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org/profile/" + id + "/clients",
title: "Clientes",
@@ -63,6 +67,10 @@ export default function ClientCreate() {
title: "Editar",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, []);
useEffect(() => {
@@ -92,7 +100,7 @@ export default function ClientCreate() {
confirm.show(
validationPhoneResult.message,
() => {},
() => {}
() => {},
);
eventHandler.setEventType(EVENT_TYPES.SLEEP);
return;
@@ -112,7 +120,7 @@ export default function ClientCreate() {
})
.then(() => {
alert.showSuccess(
`Información del cliente ${firstName} ${lastName} actualizada`
`Información del cliente ${firstName} ${lastName} actualizada`,
);
setTimeout(() => {
goTo("/admin/org/" + id + "/client/" + clientId + "/profile");
@@ -180,7 +188,9 @@ export default function ClientCreate() {
/>
<div style={{ marginTop: "15px" }}></div>
<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
checked={banned}
onChange={(checked) => {
@@ -18,6 +18,7 @@ import { ClientMergeParams, OrganizationClientView } from "@core/Models/Clients.
import Button from "@core/app/components/Button/Button";
import MaterialAlert from "@mui/material/Alert";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
oid: string;
@@ -32,6 +33,7 @@ export default function ClientCreate() {
const confirm = useConfirmStore();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
@@ -53,7 +55,7 @@ export default function ClientCreate() {
});
const [clientMergeFilterData, setClientMergeFilterData] = useState<TextObjectFilterResult[]>(
[]
[],
);
const [clientMergeId, setClientMergeId] = useState<string>("");
@@ -94,7 +96,7 @@ export default function ClientCreate() {
() => {
mergeClients();
},
() => {}
() => {},
);
};
@@ -105,6 +107,8 @@ export default function ClientCreate() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org/profile/" + id + "/clients",
title: "Clientes",
@@ -122,6 +126,10 @@ export default function ClientCreate() {
title: "Fusionar clientes",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, []);
useEffect(() => {
@@ -11,6 +11,7 @@ import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import NotificationsIcon from "@mui/icons-material/NotificationsOutlined";
import MaterialAlert from "@mui/material/Alert";
import styles from "./NotificationsPage.module.css";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
oid: string;
@@ -20,7 +21,7 @@ type UrlData = {
const CHANNELS: { key: NotificationChannel; label: string }[] = [
{ key: "whatsapp", label: "WhatsApp" },
{ key: "email", label: "Email" },
{ key: "system", label: "Sistema" }
{ key: "system", label: "Sistema" },
];
type ChannelPreference = "default" | "preferred" | "muted";
@@ -30,34 +31,41 @@ export default function NotificationsPage() {
const oid = data.oid;
const clientId = data.clientId;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const store = useNotificationPreferencesStore();
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org",
title: "Organizaciones",
icon: null
icon: null,
});
location.add({
url: "/admin/org/profile/" + oid,
title: "Organización",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/" + oid + "/client/" + clientId + "/profile",
title: "Cliente",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
location.add({
url: "",
title: "Notificaciones",
icon: <NotificationsIcon sx={{ fontSize: "32px", mr: "10px" }} />
icon: <NotificationsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, [oid, clientId]);
const getChannelPreference = (channel: NotificationChannel): ChannelPreference => {
@@ -81,7 +89,7 @@ export default function NotificationsPage() {
mutedChannels:
preference === "muted"
? [...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}>
<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>
<div className={styles.channels}>
@@ -102,24 +112,32 @@ export default function NotificationsPage() {
<div>
<div className={styles.channelTitle}>{channel.label}</div>
<div className={styles.channelDescription}>
{selectedPreference === "preferred" && "Priorizar este canal para el cliente."}
{selectedPreference === "muted" && "No enviar notificaciones por este canal."}
{selectedPreference === "default" && "Usar la configuración general de la organización."}
{selectedPreference === "preferred" &&
"Priorizar este canal para el cliente."}
{selectedPreference === "muted" &&
"No enviar notificaciones por este canal."}
{selectedPreference === "default" &&
"Usar la configuración general de la organización."}
</div>
</div>
<div className={styles.options}>
{[
{ 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) => {
const isSelected = selectedPreference === option.key;
return (
<button
key={option.key}
type="button"
onClick={() => selectPreference(channel.key, option.key)}
onClick={() =>
selectPreference(channel.key, option.key)
}
className={`${styles.optionButton} ${isSelected ? styles.optionButtonActive : ""}`}
>
{option.label}
@@ -8,6 +8,7 @@ import { useLocationStore } from "@store/Location.Store";
import { useNavigation } from "@hooks/goto";
import { useParams } from "next/navigation";
import { useEffect } from "react";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
oid: string;
@@ -19,11 +20,14 @@ export default function ClientProfile() {
const clientId = data.clientId;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const { goTo } = useNavigation();
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org",
title: "Mis organizaciones",
@@ -33,9 +37,7 @@ export default function ClientProfile() {
location.add({
url: "/admin/org/profile/" + oid,
title: "Organizacion",
icon: (
<StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />
),
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
@@ -43,6 +45,10 @@ export default function ClientProfile() {
title: "Perfil del cliente",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, []);
return (
@@ -59,79 +65,35 @@ export default function ClientProfile() {
<ButtonOption
text="Editar"
icon="/profile-icon.svg"
onClick={() =>
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/edit"
)
}
onClick={() => goTo("/admin/org/" + oid + "/client/" + clientId + "/edit")}
/>
<ButtonOption
text="Turnos"
icon="/profile-icon.svg"
onClick={() =>
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/appointments"
)
goTo("/admin/org/" + oid + "/client/" + clientId + "/appointments")
}
/>
<ButtonOption
text="Fusionar Duplicados"
icon="/client-merge-icon.svg"
onClick={() =>
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/merge"
)
}
onClick={() => goTo("/admin/org/" + oid + "/client/" + clientId + "/merge")}
/>
<ButtonOption
text="Repeticiones"
icon="/repeat-icon.svg"
onClick={() =>
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/repeats"
)
}
onClick={() => goTo("/admin/org/" + oid + "/client/" + clientId + "/repeats")}
/>
<ButtonOption
text="Cuenta"
icon="/account-icon.svg"
onClick={() =>
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/account"
)
}
onClick={() => goTo("/admin/org/" + oid + "/client/" + clientId + "/account")}
/>
<ButtonOption
text="Notificaciones"
icon="/notifications-icon.svg"
onClick={() =>
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/notifications"
)
goTo("/admin/org/" + oid + "/client/" + clientId + "/notifications")
}
/>
</div>
@@ -17,6 +17,7 @@ import { deleteRepeat, getRepeatsByClient } from "../Client.Service";
import { DeleteRepeatsParams, FindRepeatsParams, RepeatView } from "@core/Models/Repeats.model";
import style from "./repeats.module.css";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
oid: string;
@@ -31,6 +32,7 @@ export default function ClientCreate() {
const confirm = useConfirmStore();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const [repeats, setRepeats] = useState<RepeatView[]>([]);
@@ -52,6 +54,8 @@ export default function ClientCreate() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org/profile/" + id + "/clients",
title: "Clientes",
@@ -69,6 +73,10 @@ export default function ClientCreate() {
title: "Repeticiones",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, []);
useEffect(() => {
@@ -162,7 +170,7 @@ export default function ClientCreate() {
() => {
handleDelete(repeat.id);
},
() => {}
() => {},
);
}}
>
@@ -27,6 +27,7 @@ import TextObjectFilter from "@components/TextObjectFilter/TextObjectFilter";
import SelectLocationMap, {
LocationCoordinates,
} from "@core/app/components/SelectLocationMap/SelectLocationMap";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
id: string;
@@ -36,6 +37,7 @@ export default function OrganizationEdit() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const alert = useAlert();
@@ -140,7 +142,7 @@ export default function OrganizationEdit() {
});
const [localidadesFilterData, setLocalidadesFilterData] = useState<TextObjectFilterResult[]>(
[]
[],
);
const [selectedLocalidad, setSelectedLocalidad] = useState<string>("");
@@ -188,6 +190,8 @@ export default function OrganizationEdit() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org",
title: "Organizaciones",
@@ -205,6 +209,10 @@ export default function OrganizationEdit() {
title: "Ubicación",
icon: <OrgConfigIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, []);
useEffect(() => {
@@ -14,6 +14,7 @@ import Button from "@core/app/components/Button/Button";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import { deleteOrganization } from "./advanced.Service";
import { useNavigation } from "@core/app/hooks/goto";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
id: string;
@@ -23,6 +24,7 @@ export default function OrganizationAdvancedEdit() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const alert = useAlert();
@@ -33,6 +35,8 @@ export default function OrganizationAdvancedEdit() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org",
title: "Organizaciones",
@@ -50,6 +54,10 @@ export default function OrganizationAdvancedEdit() {
title: "Avanzado",
icon: <OrgConfigIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, []);
const eliminarHandler = () => {
@@ -73,7 +81,7 @@ export default function OrganizationAdvancedEdit() {
},
() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}
},
);
} else {
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 { getAssistanceUserInfo } from "./Assistance.Service";
import AssistanceIcon from "@mui/icons-material/HeadsetMicOutlined";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
id: string;
@@ -23,6 +24,7 @@ export default function CollaboratorsAdd() {
const alert = useAlert();
const eventHandler = useEventHandlerStore();
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const SessionInfo = useSessionStore();
const [user, setUser] = useState<UserAssistanceInfo | null>(null);
const [findEmail, setFindEmail] = useState<string>("");
@@ -34,6 +36,8 @@ export default function CollaboratorsAdd() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org/profile/" + id,
title: "Organizacion",
@@ -45,6 +49,10 @@ export default function CollaboratorsAdd() {
title: "Asistencia",
icon: <AssistanceIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, []);
const findUser = async () => {
@@ -10,6 +10,7 @@ import { CompanyEmployeesView } from "@models/Collaborators.model";
import { loadCollaboratorsByCompanyId } from "./Collaborators.Service";
import { useAlert } from "@store/Alert.Store";
import CollaboratorItem from "@core/app/components/CollaboratorItem/CollaboratorItem";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
id: string;
@@ -19,6 +20,7 @@ export default function OrganizationProfile() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const alert = useAlert();
const { goTo } = useNavigation();
@@ -31,6 +33,8 @@ export default function OrganizationProfile() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org",
title: "Mis organizaciones",
@@ -60,6 +64,10 @@ export default function OrganizationProfile() {
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
});
return () => {
bottomToolbar.clear();
};
}, []);
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 { useFilterStore } from "@core/Store/Filter.Store";
import filterObjects from "@components/TextObjectFilter/Filter.Service";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
id: string;
@@ -112,6 +113,7 @@ export default function DiscountsEmailGrid() {
const data = useParams<UrlData>();
const oid = data.id;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const alert = useAlert();
@@ -151,7 +153,9 @@ export default function DiscountsEmailGrid() {
render: (value: DiscountEmailRow) => (
<div style={{ paddingBottom: "10px" }}>
<div>
<div style={{ paddingLeft: "10px", paddingTop: "10px", fontWeight: "bold" }}>
<div
style={{ paddingLeft: "10px", paddingTop: "10px", fontWeight: "bold" }}
>
{value.discountName + " (" + value.discountCode + ")"}
</div>
<div style={{ paddingLeft: "10px", paddingTop: "0px", fontWeight: "bold" }}>
@@ -206,8 +210,10 @@ export default function DiscountsEmailGrid() {
const [pageSize] = useState<number>(
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 discountsRef = useRef(discounts);
@@ -219,6 +225,8 @@ export default function DiscountsEmailGrid() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org",
title: "Mis organizaciones",
@@ -238,6 +246,10 @@ export default function DiscountsEmailGrid() {
});
loadDiscountsEmail(1, pageSize);
return () => {
bottomToolbar.clear();
};
}, []);
useEffect(() => {
@@ -267,7 +279,7 @@ export default function DiscountsEmailGrid() {
setDiscountId={setDiscountId}
filterEmail={filterEmail}
setFilterEmail={setFilterEmail}
/>
/>,
);
filter.setOnFilter(() => {
setSearchByFilters(true);
@@ -327,7 +339,7 @@ export default function DiscountsEmailGrid() {
alert.showError(err.format());
});
},
() => {}
() => {},
);
};
@@ -22,6 +22,7 @@ import turnosXpressTheme from "@core/app/theme/turnosXpress";
import { ButtonGroup } from "@mui/material";
import ButtonMaterial from "@mui/material/Button";
import { useNavigation } from "@core/app/hooks/goto";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
id: string;
@@ -33,6 +34,7 @@ export default function DiscountsGrid() {
const data = useParams<UrlData>();
const oid = data.id;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
@@ -67,7 +69,9 @@ export default function DiscountsGrid() {
color: "var(--green-darkestX2)",
}}
>
{value.type === DiscountType.FIXED ? formatPrice(value.value) : value.value + "%"}
{value.type === DiscountType.FIXED
? formatPrice(value.value)
: value.value + "%"}
</div>
</div>
),
@@ -76,8 +80,10 @@ export default function DiscountsGrid() {
const [pageSize] = useState<number>(
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>();
@@ -88,6 +94,8 @@ export default function DiscountsGrid() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org",
title: "Mis organizaciones",
@@ -107,6 +115,10 @@ export default function DiscountsGrid() {
});
loadDiscounts(1, pageSize);
return () => {
bottomToolbar.clear();
};
}, []);
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 filterObjects from "@core/app/components/TextObjectFilter/Filter.Service";
import { CompanyServiceView } from "@models/Service.model";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
id: string;
@@ -47,6 +48,7 @@ export default function OrganizationEdit() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const alert = useAlert();
@@ -66,11 +68,12 @@ export default function OrganizationEdit() {
const [servicesShowPublicScores, setServicesShowPublicScores] = useState(true);
const [servicesShowPublicOpinions, setServicesShowPublicOpinions] = useState(true);
const [originalServicesShowPublicScores, setOriginalServicesShowPublicScores] = useState(true);
const [originalServicesShowPublicOpinions, setOriginalServicesShowPublicOpinions] = useState(true);
const [originalServicesShowPublicOpinions, setOriginalServicesShowPublicOpinions] =
useState(true);
const [services, setServices] = useState<CompanyServiceView[]>([]);
const [orgPublished, setOrgPublished] = useState<COMPANY_PUBLISHED_STATUS | undefined>(
undefined
undefined,
);
const [savePublishedStatusAvailable, setSavePublishedStatusAvailable] = useState(false);
@@ -199,6 +202,8 @@ export default function OrganizationEdit() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org",
title: "Organizaciones",
@@ -218,6 +223,10 @@ export default function OrganizationEdit() {
});
loadCategories();
return () => {
bottomToolbar.clear();
};
}, []);
useEffect(() => {
@@ -241,25 +250,33 @@ export default function OrganizationEdit() {
setCategoryId(organization.categoryId.toString());
setOrgSlug(getSlug(organization.name));
setOrgPublished(organization.published);
setShowPublicScores(organization.showPublicScores !== undefined ? organization.showPublicScores : true);
setShowPublicOpinions(organization.showPublicOpinions !== undefined ? organization.showPublicOpinions : true);
setShowPublicScores(
organization.showPublicScores !== undefined
? organization.showPublicScores
: true,
);
setShowPublicOpinions(
organization.showPublicOpinions !== undefined
? organization.showPublicOpinions
: true,
);
setShowPublicProfessionals(
organization.showPublicProfessionals !== undefined
? organization.showPublicProfessionals
: true
: true,
);
setServices(loadedServices);
setServicesShowPublicScores(
loadedServices.every((service) => service.showPublicScores !== false)
loadedServices.every((service) => service.showPublicScores !== false),
);
setServicesShowPublicOpinions(
loadedServices.every((service) => service.showPublicOpinions !== false)
loadedServices.every((service) => service.showPublicOpinions !== false),
);
setOriginalServicesShowPublicScores(
loadedServices.every((service) => service.showPublicScores !== false)
loadedServices.every((service) => service.showPublicScores !== false),
);
setOriginalServicesShowPublicOpinions(
loadedServices.every((service) => service.showPublicOpinions !== false)
loadedServices.every((service) => service.showPublicOpinions !== false),
);
requestAnimationFrame(() => {
@@ -287,7 +304,7 @@ export default function OrganizationEdit() {
confirm.show(
validationPhoneResult.message,
() => {},
() => {}
() => {},
);
eventHandler.setEventType(EVENT_TYPES.SLEEP);
return;
@@ -307,7 +324,8 @@ export default function OrganizationEdit() {
phoneNumber: phone,
templateWapNotifId: templateWapNotifId === "" ? undefined : templateWapNotifId,
templateWapAltaId: templateWapAltaId === "" ? undefined : templateWapAltaId,
templateWapCancellationId: templateWapCancellationId === "" ? undefined : templateWapCancellationId,
templateWapCancellationId:
templateWapCancellationId === "" ? undefined : templateWapCancellationId,
...(canEditPublicVisibilityFlags
? {
showPublicScores,
@@ -321,31 +339,31 @@ export default function OrganizationEdit() {
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."),
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(
"Debe seleccionar una categoría para la organización."
"Debe seleccionar una categoría para la organización.",
),
phoneAreaCode: Yup.number()
.typeError("Código de área debe ser un número.")
.transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value
originalValue.toString().trim() === "" ? null : value,
)
.nullable(),
phoneNumber: Yup.number()
.typeError("El teléfono debe ser un número.")
.transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value
originalValue.toString().trim() === "" ? null : value,
)
.nullable(),
appointmentAlert: Yup.string().optional().default(""),
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()
.typeError("Fracción de tiempo debe ser un número.")
.transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value
originalValue.toString().trim() === "" ? null : value,
)
.nullable()
.integer("Fracción de tiempo debe ser un número entero en minutos.")
@@ -354,7 +372,7 @@ export default function OrganizationEdit() {
appointmentTime: Yup.number()
.typeError("El Plazo Mínimo de Reserva debe ser un número.")
.transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value
originalValue.toString().trim() === "" ? null : value,
)
.nullable()
.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()
.typeError("El Plazo Mínimo de Cancelación debe ser un número.")
.transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value
originalValue.toString().trim() === "" ? null : value,
)
.nullable()
.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;
const changedServices =
canEditPublicVisibilityFlags &&
(servicesShowPublicScoresChanged || servicesShowPublicOpinionsChanged)
(servicesShowPublicScoresChanged ||
servicesShowPublicOpinionsChanged)
? services
: [];
@@ -414,8 +433,8 @@ export default function OrganizationEdit() {
: service.showPublicOpinions !== undefined
? service.showPublicOpinions
: true,
})
)
}),
),
).then(() => {
const updatedServices = services.map((service) => ({
...service,
@@ -434,7 +453,9 @@ export default function OrganizationEdit() {
});
})
.catch((error: ApiError | Error) => {
alert.showError(error instanceof ApiError ? error.format() : error.message);
alert.showError(
error instanceof ApiError ? error.format() : error.message,
);
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
@@ -453,7 +474,7 @@ export default function OrganizationEdit() {
id: category.id.toString(),
name: category.name,
description: category.description,
}))
})),
);
};
@@ -533,7 +554,7 @@ export default function OrganizationEdit() {
setOrgPublished(
checked
? 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" }}>
<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>
{!canEditPublicVisibilityFlags && (
<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>
)}
<div style={{ display: "flex", flexDirection: "column", gap: "10px", marginTop: "10px" }}>
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
<div
style={{ display: "flex", flexDirection: "column", gap: "10px", marginTop: "10px" }}
>
<label
style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}
>
<Checkbox
checked={showPublicScores}
disabled={!canEditPublicVisibilityFlags}
@@ -600,7 +629,9 @@ export default function OrganizationEdit() {
/>
Mostrar puntuaciones públicas
</label>
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
<label
style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}
>
<Checkbox
checked={showPublicOpinions}
disabled={!canEditPublicVisibilityFlags}
@@ -611,7 +642,9 @@ export default function OrganizationEdit() {
/>
Mostrar opiniones públicas
</label>
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
<label
style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}
>
<Checkbox
checked={showPublicProfessionals}
disabled={!canEditPublicVisibilityFlags}
@@ -632,7 +665,9 @@ export default function OrganizationEdit() {
{services.length > 0 && (
<>
<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>
<div
style={{
@@ -642,7 +677,14 @@ export default function OrganizationEdit() {
marginTop: "10px",
}}
>
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
<label
style={{
display: "flex",
alignItems: "center",
gap: "10px",
fontWeight: 600,
}}
>
<Checkbox
checked={servicesShowPublicScores}
disabled={!canEditPublicVisibilityFlags}
@@ -653,7 +695,14 @@ export default function OrganizationEdit() {
/>
Mostrar puntuaciones públicas de los servicios
</label>
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
<label
style={{
display: "flex",
alignItems: "center",
gap: "10px",
fontWeight: 600,
}}
>
<Checkbox
checked={servicesShowPublicOpinions}
disabled={!canEditPublicVisibilityFlags}
@@ -666,7 +715,8 @@ export default function OrganizationEdit() {
</label>
{!canEditPublicVisibilityFlags && (
<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>
)}
</div>
@@ -24,6 +24,7 @@ import {
OrganizationFiscalProfileTaxCondition,
upsertOrganizationFiscalProfile,
} from "./Fiscal.Service";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
id: string;
@@ -33,13 +34,15 @@ export default function OrganizationFiscalProfileEdit() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const alert = useAlert();
const [cuit, setCuit] = useState("");
const [legalName, setLegalName] = useState("");
const [taxCondition, setTaxCondition] = useState<OrganizationFiscalProfileTaxCondition>("MONOTRIBUTO");
const [taxCondition, setTaxCondition] =
useState<OrganizationFiscalProfileTaxCondition>("MONOTRIBUTO");
const [pointOfSale, setPointOfSale] = useState("1");
const [activityCode, setActivityCode] = useState("");
const [activityDescription, setActivityDescription] = useState("");
@@ -80,6 +83,8 @@ export default function OrganizationFiscalProfileEdit() {
useEffect(() => {
location.clear();
bottomToolbar.clear();
location.add({
url: "/admin/org",
title: "Organizaciones",
@@ -97,6 +102,10 @@ export default function OrganizationFiscalProfileEdit() {
title: "Datos fiscales",
icon: <ReceiptLongOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, []);
useEffect(() => {
@@ -118,7 +127,9 @@ export default function OrganizationFiscalProfileEdit() {
setLegalName(profile.legalName || "");
setTaxCondition(profile.taxCondition || "MONOTRIBUTO");
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 || "");
setActivityDescription(profile.activityDescription || profileActivity?.name || "");
})
@@ -152,7 +163,10 @@ export default function OrganizationFiscalProfileEdit() {
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.");
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
return;
@@ -160,7 +174,9 @@ export default function OrganizationFiscalProfileEdit() {
eventHandler.setEventType(EVENT_TYPES.LOADING);
const selectedActivity = arcaActivities.find((activity) => activity.id === activityCode.trim());
const selectedActivity = arcaActivities.find(
(activity) => activity.id === activityCode.trim(),
);
upsertOrganizationFiscalProfile({
companyId: id,
@@ -173,7 +189,9 @@ export default function OrganizationFiscalProfileEdit() {
sessionUser: SessionInfo.userId,
})
.then((profile) => {
const savedActivity = arcaActivities.find((activity) => activity.id === profile.activityCode);
const savedActivity = arcaActivities.find(
(activity) => activity.id === profile.activityCode,
);
setActivityCode(profile.activityCode || "");
setActivityDescription(profile.activityDescription || savedActivity?.name || "");
alert.showSuccess("Datos fiscales actualizados.");
@@ -198,7 +216,8 @@ export default function OrganizationFiscalProfileEdit() {
<div>
<h2>Configuración fiscal</h2>
<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>
</div>
</section>
@@ -206,32 +225,32 @@ export default function OrganizationFiscalProfileEdit() {
<section className={style.formSection}>
<h3>Datos del emisor</h3>
<Grid2 container spacing={2}>
<Grid2 size={{ xs: 12, sm: 6 }}>
<Textbox
name="cuit"
placeholder="CUIT"
type="text"
value={cuit}
width="100%"
onChange={(event) => {
setCuit(event.target.value.replace(/\D/g, ""));
markNeedsSave();
}}
/>
</Grid2>
<Grid2 size={{ xs: 12, sm: 6 }}>
<Textbox
name="legalName"
placeholder="Razón social"
type="text"
value={legalName}
width="100%"
onChange={(event) => {
setLegalName(event.target.value);
markNeedsSave();
}}
/>
</Grid2>
<Grid2 size={{ xs: 12, sm: 6 }}>
<Textbox
name="cuit"
placeholder="CUIT"
type="text"
value={cuit}
width="100%"
onChange={(event) => {
setCuit(event.target.value.replace(/\D/g, ""));
markNeedsSave();
}}
/>
</Grid2>
<Grid2 size={{ xs: 12, sm: 6 }}>
<Textbox
name="legalName"
placeholder="Razón social"
type="text"
value={legalName}
width="100%"
onChange={(event) => {
setLegalName(event.target.value);
markNeedsSave();
}}
/>
</Grid2>
</Grid2>
</section>
@@ -240,7 +259,11 @@ export default function OrganizationFiscalProfileEdit() {
<h3>Condición fiscal</h3>
<p>Seleccioná la condición declarada para esta organización.</p>
</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) => {
const selected = taxCondition === option.id;
@@ -256,7 +279,9 @@ export default function OrganizationFiscalProfileEdit() {
aria-pressed={selected}
>
<span className={style.optionTitle}>{option.title}</span>
<span className={style.optionDescription}>{option.description}</span>
<span className={style.optionDescription}>
{option.description}
</span>
</button>
);
})}
@@ -266,19 +291,19 @@ export default function OrganizationFiscalProfileEdit() {
<section className={style.formSection}>
<h3>Facturación ARCA</h3>
<Grid2 container spacing={2}>
<Grid2 size={{ xs: 12, sm: 6 }}>
<Textbox
name="pointOfSale"
placeholder="Punto de venta"
type="text"
value={pointOfSale}
width="100%"
onChange={(event) => {
setPointOfSale(event.target.value.replace(/\D/g, ""));
markNeedsSave();
}}
/>
</Grid2>
<Grid2 size={{ xs: 12, sm: 6 }}>
<Textbox
name="pointOfSale"
placeholder="Punto de venta"
type="text"
value={pointOfSale}
width="100%"
onChange={(event) => {
setPointOfSale(event.target.value.replace(/\D/g, ""));
markNeedsSave();
}}
/>
</Grid2>
</Grid2>
{fiscalDataLoaded && (
@@ -290,7 +315,7 @@ export default function OrganizationFiscalProfileEdit() {
selectedId={activityCode}
onChange={(selectedActivityCode) => {
const selectedActivity = arcaActivities.find(
(activity) => activity.id === selectedActivityCode
(activity) => activity.id === selectedActivityCode,
);
setActivityCode(selectedActivity?.id || "");
@@ -13,6 +13,7 @@ import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import NotificationsIcon from "@mui/icons-material/NotificationsOutlined";
import MaterialAlert from "@mui/material/Alert";
import dayjs from "dayjs";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
id: string;
@@ -21,7 +22,7 @@ type UrlData = {
const CHANNELS: { key: NotificationChannel; label: string }[] = [
{ key: "whatsapp", label: "WhatsApp" },
{ key: "email", label: "Email" },
{ key: "system", label: "Sistema" }
{ key: "system", label: "Sistema" },
];
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: 720, label: "12 horas 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() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const store = useNotificationPreferencesStore();
const eventHandler = useEventHandlerStore();
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org",
title: "Organizaciones",
icon: null
icon: null,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Perfil",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id + "/notifications",
title: "Notificaciones",
icon: <NotificationsIcon sx={{ fontSize: "32px", mr: "10px" }} />
icon: <NotificationsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, [id]);
const toggleChannel = (channel: NotificationChannel) => {
@@ -73,18 +81,22 @@ export default function NotificationsPage() {
const updateQuietHours = (field: "from" | "to", value: string) => {
const current = store.formData.quietHours || { from: "00:00", to: "00:00" };
store.updateFormData({
quietHours: { ...current, [field]: value }
quietHours: { ...current, [field]: value },
});
};
const quietHourValue = (value: string | undefined, fallback: string) => {
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 = () => {
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 });
} else {
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" }}>
{/* Channel Toggles Section */}
<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>
<div style={{ display: "flex", flexDirection: "column", gap: "8px", marginBottom: "30px" }}>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "8px",
marginBottom: "30px",
}}
>
{CHANNELS.map((channel) => (
<div
key={channel.key}
@@ -121,10 +144,12 @@ export default function NotificationsPage() {
padding: "12px 16px",
borderRadius: "4px",
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
checked={store.formData.defaultChannels.includes(channel.key)}
onChange={() => toggleChannel(channel.key)}
@@ -135,7 +160,8 @@ export default function NotificationsPage() {
{/* Quiet Hours Section */}
<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>
<div
@@ -147,7 +173,7 @@ export default function NotificationsPage() {
borderRadius: "8px",
border: "1px solid var(--border-color)",
backgroundColor: "var(--bg-primary)",
marginBottom: "16px"
marginBottom: "16px",
}}
>
<span style={{ color: "var(--text-primary)", fontWeight: "500" }}>
@@ -160,13 +186,22 @@ export default function NotificationsPage() {
</div>
{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 }}>
<TextTime
placeholder="Desde"
width="100%"
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 style={{ flex: 1 }}>
@@ -182,12 +217,15 @@ export default function NotificationsPage() {
{/* Reminder Rules Section */}
<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>
<div style={{ display: "flex", flexWrap: "wrap", gap: "10px" }}>
{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 (
<button
key={preset.minutes}
@@ -198,11 +236,15 @@ export default function NotificationsPage() {
padding: "10px 16px",
borderRadius: "999px",
border: `1px solid ${isActive ? "var(--primary-color)" : "var(--border-color)"}`,
backgroundColor: isActive ? "rgba(25, 118, 210, 0.12)" : "var(--bg-primary)",
color: isActive ? "var(--primary-color)" : "var(--text-secondary)",
backgroundColor: isActive
? "rgba(25, 118, 210, 0.12)"
: "var(--bg-primary)",
color: isActive
? "var(--primary-color)"
: "var(--text-secondary)",
fontWeight: isActive ? "600" : "500",
fontSize: "14px",
transition: "all 0.2s ease"
transition: "all 0.2s ease",
}}
>
{preset.label}
@@ -248,7 +248,7 @@ export default function OrganizationNotPayedAppointments() {
const [dialogPayVisible, setDialogPayVisible] = useState<boolean>(false);
const [dialogPayOneVisible, setDialogPayOneVisible] = useState<boolean>(false);
const [createPaymentData, setCreatePaymentData] = useState<PaginatePaymentsView | undefined>(
undefined
undefined,
);
const [paymentMethodFilterContent] = useState<TextObjectModalContent>({
@@ -284,7 +284,7 @@ export default function OrganizationNotPayedAppointments() {
TextObjectFilterResult[]
>([]);
const [paymentMethodId, setPaymentMethodId] = useState<CLIENT_ACCOUNT_PAYMENT_METHODS>(
CLIENT_ACCOUNT_PAYMENT_METHODS.CASH
CLIENT_ACCOUNT_PAYMENT_METHODS.CASH,
);
useEffect(() => {
@@ -326,7 +326,7 @@ export default function OrganizationNotPayedAppointments() {
TextObjectFilterResult[]
>([]);
const [paymentProcessDateId, setPaymentProcessDateId] = useState<PAYMENT_PROCCESS_DATE>(
PAYMENT_PROCCESS_DATE.NOW
PAYMENT_PROCCESS_DATE.NOW,
);
useEffect(() => {
@@ -356,7 +356,7 @@ export default function OrganizationNotPayedAppointments() {
id: category.id.toString(),
name: category.name,
description: category.description,
}))
})),
);
};
@@ -370,7 +370,7 @@ export default function OrganizationNotPayedAppointments() {
const [clientIds, setClientIds] = useState<string[]>([]);
const [collaboratorFilterData, setCollaboratorFilterData] = useState<TextObjectFilterResult[]>(
[]
[],
);
const [collaboratorId, setCollaboratorId] = useState<string>("");
@@ -484,8 +484,8 @@ export default function OrganizationNotPayedAppointments() {
parseInt(
process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
: "10"
)
: "10",
),
);
const [payments, setPayments] = useState<PaginateNotPayedAppointmentsResult>();
const paymentsRef = useRef(payments);
@@ -508,7 +508,7 @@ export default function OrganizationNotPayedAppointments() {
};
window.addEventListener("scroll", handleScroll, { capture: true, passive: true });
return () => {
window.removeEventListener("scroll", handleScroll, { capture: true });
};
@@ -595,7 +595,6 @@ export default function OrganizationNotPayedAppointments() {
});
};
const generarReporte = async () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
try {
@@ -611,11 +610,11 @@ export default function OrganizationNotPayedAppointments() {
if (methodId) filterData.method = methodId as any;
const report = await GetNotPayedReport(filterData);
const pdfMake = await import("pdfmake/build/pdfmake");
const pdfFonts = (await import("pdfmake/build/vfs_fonts")) as any;
pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs;
let logoDataUrl = "";
try {
const response = await fetch("/favicon512.png");
@@ -625,27 +624,27 @@ export default function OrganizationNotPayedAppointments() {
reader.onloadend = () => resolve(reader.result as string);
reader.readAsDataURL(blob);
});
} catch(e) {}
} catch (e) {}
const tableBody: any[] = [
[
{ text: "Cliente", 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([
item.clientName || "-",
{
{
text: [
{ 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([
{ 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 = {
content: [
{
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: [
{ 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: "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: {
headerRows: 1,
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) => {
(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 {
pdfMake.createPdf(docDefinition as any).download(`reporte-adeudados.pdf`);
}
} 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 {
eventHandler.setEventType(EVENT_TYPES.CLEAR);
}
@@ -832,14 +860,24 @@ export default function OrganizationNotPayedAppointments() {
setMethodId={setMethodId}
clientData={clientFilterData}
setClientIds={setClientIds}
/>
/>,
);
filter.setOnFilter(() => { });
filter.setOnFilter(() => {});
return () => {
bottomToolbar.clear();
};
}, [paymentsTotal, collaboratorFilterData, serviceFilterData, methodFilterData, clientFilterData, clientIds, collaboratorId, serviceId, methodId]);
}, [
paymentsTotal,
collaboratorFilterData,
serviceFilterData,
methodFilterData,
clientFilterData,
clientIds,
collaboratorId,
serviceId,
methodId,
]);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
@@ -881,72 +919,116 @@ export default function OrganizationNotPayedAppointments() {
loadNotPayedAppointmentsTotal();
}, [clientIds, collaboratorId, serviceId, methodId, dateFrom, dateTo, reloadPayments]);
return (
<>
<Location />
<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 */}
<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 */}
<div style={{ display: "flex", justifyContent: "center", marginBottom: "15px" }}>
<ThemeProvider theme={turnosXpressTheme}>
<div style={{
display: "flex",
background: "rgba(255,255,255,0.9)",
backdropFilter: "blur(10px)",
borderRadius: "30px",
padding: "5px",
boxShadow: "0 4px 15px rgba(0,0,0,0.05)"
}}>
<ButtonMaterial
variant="text"
onClick={() => goTo(`/admin/org/profile/${id}/payments`)}
style={{ borderRadius: "25px", color: "#666", fontWeight: "bold", padding: "8px 25px" }}
<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 */}
<div
style={{
display: "flex",
justifyContent: "center",
marginBottom: "15px",
}}
>
<ThemeProvider theme={turnosXpressTheme}>
<div
style={{
display: "flex",
background: "rgba(255,255,255,0.9)",
backdropFilter: "blur(10px)",
borderRadius: "30px",
padding: "5px",
boxShadow: "0 4px 15px rgba(0,0,0,0.05)",
}}
>
Pagados
</ButtonMaterial>
<ButtonMaterial
variant="contained"
style={{ borderRadius: "25px", boxShadow: "0 2px 8px rgba(0,0,0,0.1)", fontWeight: "bold", padding: "8px 25px" }}
>
Adeudados
</ButtonMaterial>
</div>
</ThemeProvider>
</div>
<ButtonMaterial
variant="text"
onClick={() => goTo(`/admin/org/profile/${id}/payments`)}
style={{
borderRadius: "25px",
color: "#666",
fontWeight: "bold",
padding: "8px 25px",
}}
>
Pagados
</ButtonMaterial>
<ButtonMaterial
variant="contained"
style={{
borderRadius: "25px",
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
fontWeight: "bold",
padding: "8px 25px",
}}
>
Adeudados
</ButtonMaterial>
</div>
</ThemeProvider>
</div>
<motion.div
layout
initial={{ opacity: 0, y: -20 }}
animate={{
opacity: 1,
animate={{
opacity: 1,
y: 0,
padding: isMinimized ? "25px 50px" : "40px 20px",
borderRadius: isMinimized ? "15px" : "20px"
borderRadius: isMinimized ? "15px" : "20px",
}}
style={{
background: "linear-gradient(135deg, #FF416C 0%, #FF4B2B 100%)", // Red gradient for debts
color: "white",
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",
display: "flex",
flexDirection: isMinimized ? "row" : "column",
alignItems: "center",
justifyContent: isMinimized ? "space-between" : "center",
transition: "box-shadow 0.3s ease"
transition: "box-shadow 0.3s ease",
}}
>
<IconButton
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"
title={forceMinimized ? "Maximizar" : "Minimizar"}
>
@@ -954,51 +1036,95 @@ export default function OrganizationNotPayedAppointments() {
</IconButton>
<IconButton
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"
title="Filtrar"
>
<FilterAltOutlinedIcon />
</IconButton>
<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
animate={{
fontSize: isMinimized ? "14px" : "16px",
marginBottom: isMinimized ? "0px" : "10px"
<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
animate={{
fontSize: isMinimized ? "14px" : "16px",
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
</motion.div>
<motion.div
animate={{
<motion.div
animate={{
fontSize: isMinimized ? "24px" : "56px",
margin: isMinimized ? "0px" : "10px 0"
margin: isMinimized ? "0px" : "10px 0",
}}
style={{ fontWeight: "900" }}
>
{formatPrice(paymentsTotal)}
</motion.div>
{!isMinimized && (
<motion.div
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
style={{ marginTop: "15px" }}
>
<div style={{ marginBottom: "15px", width: "250px", maxWidth: "100%" }}>
<div
style={{
marginBottom: "15px",
width: "250px",
maxWidth: "100%",
}}
>
<TextRangeDateModern
label="Período"
dateFrom={dateFrom}
dateTo={dateTo}
setDateFrom={setDateFrom}
setDateTo={setDateTo}
/>
label="Período"
dateFrom={dateFrom}
dateTo={dateTo}
setDateFrom={setDateFrom}
setDateTo={setDateTo}
/>
</div>
<div style={{ display: "flex", gap: "10px", alignItems: "center", flexWrap: "wrap" }}>
<div
style={{
display: "flex",
gap: "10px",
alignItems: "center",
flexWrap: "wrap",
}}
>
<ButtonMaterial
variant="contained"
startIcon={<PayIcon />}
@@ -1009,7 +1135,7 @@ export default function OrganizationNotPayedAppointments() {
fontWeight: "bold",
borderRadius: "20px",
padding: "8px 20px",
textTransform: "none"
textTransform: "none",
}}
>
Pagar Todos
@@ -1023,10 +1149,13 @@ export default function OrganizationNotPayedAppointments() {
fontWeight: "bold",
borderRadius: "20px",
padding: "8px 20px",
textTransform: "none"
textTransform: "none",
}}
>
<PictureAsPdfIcon style={{ fontSize: "18px", marginRight: "5px" }} /> PDF
<PictureAsPdfIcon
style={{ fontSize: "18px", marginRight: "5px" }}
/>{" "}
PDF
</ButtonMaterial>
</div>
</motion.div>
@@ -1035,10 +1164,22 @@ export default function OrganizationNotPayedAppointments() {
</div>
{/* 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 */}
<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>
{payments?.data.map((payment, index) => (
<motion.div
@@ -1054,35 +1195,76 @@ export default function OrganizationNotPayedAppointments() {
alignItems: "center",
justifyContent: "space-between",
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 style={{
background: "rgba(255, 75, 43, 0.1)",
padding: "10px",
borderRadius: "12px",
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "var(--red)"
}}>
gap: "15px",
}}
>
<div
style={{
background: "rgba(255, 75, 43, 0.1)",
padding: "10px",
borderRadius: "12px",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "var(--red)",
}}
>
<CreditCardIcon />
</div>
<div>
<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={{
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" }}>
{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 style={{ display: "flex", alignItems: "center", gap: "15px" }}>
<div style={{ fontWeight: "900", fontSize: "16px", color: "var(--red)" }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "15px",
}}
>
<div
style={{
fontWeight: "900",
fontSize: "16px",
color: "var(--red)",
}}
>
{formatPrice(payment.price)}
</div>
<IconButton
color="success"
<IconButton
color="success"
onClick={() => {
setCreatePaymentData(payment);
setDialogPayOneVisible(true);
@@ -1094,12 +1276,24 @@ export default function OrganizationNotPayedAppointments() {
</div>
</motion.div>
))}
{(!payments?.data || payments.data.length === 0) && (
<div style={{ textAlign: "center", padding: "50px", color: "#aaa" }}>
<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
style={{ textAlign: "center", padding: "50px", color: "#aaa" }}
>
<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>
)}
</AnimatePresence>
@@ -1107,7 +1301,9 @@ export default function OrganizationNotPayedAppointments() {
{/* Pagination */}
{payments && payments.pages > 1 && (
<div style={{ display: "flex", justifyContent: "center", marginTop: "40px" }}>
<div
style={{ display: "flex", justifyContent: "center", marginTop: "40px" }}
>
<Pagination
count={payments.pages}
page={payments.page}
@@ -1134,7 +1330,7 @@ export default function OrganizationNotPayedAppointments() {
data={paymentMethhodFilterData}
onChange={(id) => {
setPaymentMethodId(
id as CLIENT_ACCOUNT_PAYMENT_METHODS
id as CLIENT_ACCOUNT_PAYMENT_METHODS,
);
}}
/>
@@ -1185,7 +1381,7 @@ export default function OrganizationNotPayedAppointments() {
data={paymentMethhodFilterData}
onChange={(id) => {
setPaymentMethodId(
id as CLIENT_ACCOUNT_PAYMENT_METHODS
id as CLIENT_ACCOUNT_PAYMENT_METHODS,
);
}}
/>
@@ -1219,7 +1415,6 @@ export default function OrganizationNotPayedAppointments() {
</DialogActions>
</>
</DialogWindow>
</div>
</SecureByPlan>
</>
@@ -47,6 +47,7 @@ import SearchOffIcon from "@mui/icons-material/SearchOff";
import UnfoldLessIcon from "@mui/icons-material/UnfoldLess";
import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore";
import FilterAltOutlinedIcon from "@mui/icons-material/FilterAltOutlined";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
id: string;
@@ -68,8 +69,12 @@ const FilterContent = (props: FilterContentProps) => {
const [clientFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>Seleccionar Cliente:</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>Ingrese el nombre del cliente para buscarlo.</p>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Cliente:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del cliente para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
@@ -81,8 +86,12 @@ const FilterContent = (props: FilterContentProps) => {
const [collaboratorFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>Seleccionar Colaborador:</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>Ingrese el nombre del colaborador para buscarlo.</p>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Colaborador:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del colaborador para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
@@ -94,8 +103,12 @@ const FilterContent = (props: FilterContentProps) => {
const [serviceFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>Seleccionar Servicio:</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>Ingrese el nombre del servicio para buscarlo.</p>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Servicio:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del servicio para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
@@ -107,8 +120,12 @@ const FilterContent = (props: FilterContentProps) => {
const [methodFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>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 style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
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>
),
bottom: <div>&nbsp;</div>,
@@ -152,6 +169,7 @@ export default function OrganizationEdit() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const filter = useFilterStore();
@@ -168,7 +186,9 @@ export default function OrganizationEdit() {
const [clientFilterData, setClientFilterData] = useState<TextObjectFilterResult[]>([]);
const [clientIds, setClientIds] = useState<string[]>([]);
const [collaboratorFilterData, setCollaboratorFilterData] = useState<TextObjectFilterResult[]>([]);
const [collaboratorFilterData, setCollaboratorFilterData] = useState<TextObjectFilterResult[]>(
[],
);
const [collaboratorId, setCollaboratorId] = useState<string>("");
const [serviceFilterData, setServiceFilterData] = useState<TextObjectFilterResult[]>([]);
@@ -188,7 +208,6 @@ export default function OrganizationEdit() {
const generarReporte = async () => {
try {
const pdfMake = await import("pdfmake/build/pdfmake");
const pdfFonts = (await import("pdfmake/build/vfs_fonts")) as any;
pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs;
@@ -201,7 +220,7 @@ export default function OrganizationEdit() {
serviceId: serviceId ? serviceId : undefined,
employeeId: collaboratorId ? collaboratorId : undefined,
method: methodId ? (methodId as any) : undefined,
sessionUser: SessionInfo.userId
sessionUser: SessionInfo.userId,
});
if (!report.items || report.items.length === 0) {
@@ -218,86 +237,114 @@ export default function OrganizationEdit() {
reader.onloadend = () => resolve(reader.result as string);
reader.readAsDataURL(blob);
});
} catch(e) {}
} catch (e) {}
const tableBody: any[] = [
[
{ text: "Cliente", 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([
item.clientName || "-",
{
{
text: [
{ 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([
{ 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 = {
content: [
{
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: [
{ 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: "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: {
headerRows: 1,
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) => {
(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 {
pdfMake.createPdf(docDefinition as any).download(`reporte-procesados.pdf`);
}
} catch (error: unknown) {
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(() => {
const handleScroll = (e: Event) => {
const handleScroll = (e: Event) => {
const target = e.target as HTMLElement;
// Check if it's a DOM element scrolling
if (target && target.scrollTop !== undefined) {
@@ -313,7 +360,7 @@ export default function OrganizationEdit() {
// Use capture: true to catch all scroll events in the app regardless of where they happen
window.addEventListener("scroll", handleScroll, { capture: true, passive: true });
return () => {
window.removeEventListener("scroll", handleScroll, { capture: true });
};
@@ -389,15 +436,23 @@ export default function OrganizationEdit() {
useEffect(() => {
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 };
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 };
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(() => {
@@ -413,18 +468,31 @@ export default function OrganizationEdit() {
setMethodId={setMethodId}
clientData={clientFilterData}
setClientIds={setClientIds}
clientIds={clientIds}
/>
clientIds={clientIds}
/>,
);
filter.setOnFilter(() => { });
filter.setOnFilter(() => {});
}, [collaboratorFilterData, serviceFilterData, methodFilterData, clientFilterData]);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
location.clear();
bottomToolbar.set(<></>);
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({ url: "/admin/org/profile/" + id, title: "Pagos", icon: <PaymentsIcon sx={{ fontSize: "32px", mr: "10px" }} /> });
location.add({
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(() => {
@@ -444,68 +512,116 @@ export default function OrganizationEdit() {
<Location />
<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 */}
<div ref={sentinelRef} style={{ height: "1px", width: "100%" }}></div>
{/* 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" }}>
{/* Tabs Selector */}
<div style={{ display: "flex", justifyContent: "center", marginBottom: "15px" }}>
<ThemeProvider theme={turnosXpressTheme}>
<div style={{
display: "flex",
background: "rgba(255,255,255,0.9)",
backdropFilter: "blur(10px)",
borderRadius: "30px",
padding: "5px",
boxShadow: "0 4px 15px rgba(0,0,0,0.05)"
}}>
<ButtonMaterial
variant="contained"
style={{ borderRadius: "25px", boxShadow: "0 2px 8px rgba(0,0,0,0.1)", fontWeight: "bold", padding: "8px 25px" }}
<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 */}
<div
style={{
display: "flex",
justifyContent: "center",
marginBottom: "15px",
}}
>
<ThemeProvider theme={turnosXpressTheme}>
<div
style={{
display: "flex",
background: "rgba(255,255,255,0.9)",
backdropFilter: "blur(10px)",
borderRadius: "30px",
padding: "5px",
boxShadow: "0 4px 15px rgba(0,0,0,0.05)",
}}
>
Pagados
</ButtonMaterial>
<ButtonMaterial
variant="text"
onClick={() => goTo(`/admin/org/profile/${id}/payments/not-payed`)}
style={{ borderRadius: "25px", color: "#666", fontWeight: "bold", padding: "8px 25px" }}
>
Adeudados
</ButtonMaterial>
</div>
</ThemeProvider>
</div>
<ButtonMaterial
variant="contained"
style={{
borderRadius: "25px",
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
fontWeight: "bold",
padding: "8px 25px",
}}
>
Pagados
</ButtonMaterial>
<ButtonMaterial
variant="text"
onClick={() =>
goTo(`/admin/org/profile/${id}/payments/not-payed`)
}
style={{
borderRadius: "25px",
color: "#666",
fontWeight: "bold",
padding: "8px 25px",
}}
>
Adeudados
</ButtonMaterial>
</div>
</ThemeProvider>
</div>
<motion.div
layout
initial={{ opacity: 0, y: -20 }}
animate={{
opacity: 1,
animate={{
opacity: 1,
y: 0,
padding: isMinimized ? "25px 50px" : "40px 20px",
borderRadius: isMinimized ? "15px" : "20px"
borderRadius: isMinimized ? "15px" : "20px",
}}
style={{
background: "linear-gradient(135deg, #4b6cb7 0%, #182848 100%)",
color: "white",
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",
display: "flex",
flexDirection: isMinimized ? "row" : "column",
alignItems: "center",
justifyContent: isMinimized ? "space-between" : "center",
transition: "box-shadow 0.3s ease"
transition: "box-shadow 0.3s ease",
}}
>
<IconButton
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"
title={forceMinimized ? "Maximizar" : "Minimizar"}
>
@@ -513,41 +629,81 @@ export default function OrganizationEdit() {
</IconButton>
<IconButton
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"
title="Filtrar"
>
<FilterAltOutlinedIcon />
</IconButton>
<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
animate={{
fontSize: isMinimized ? "14px" : "16px",
marginBottom: isMinimized ? "0px" : "10px"
<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
animate={{
fontSize: isMinimized ? "14px" : "16px",
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
</motion.div>
<motion.div
animate={{
<motion.div
animate={{
fontSize: isMinimized ? "24px" : "56px",
margin: isMinimized ? "0px" : "10px 0"
margin: isMinimized ? "0px" : "10px 0",
}}
style={{ fontWeight: "900" }}
>
{formatPrice(paymentsTotal)}
</motion.div>
{!isMinimized && (
<motion.div
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
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%" }}>
<TextRangeDateModern
label="Período"
@@ -567,10 +723,13 @@ export default function OrganizationEdit() {
borderRadius: "20px",
padding: "8px 20px",
textTransform: "none",
height: "56px"
height: "56px",
}}
>
<PictureAsPdfIcon style={{ fontSize: "18px", marginRight: "5px" }} /> PDF
<PictureAsPdfIcon
style={{ fontSize: "18px", marginRight: "5px" }}
/>{" "}
PDF
</ButtonMaterial>
</div>
</motion.div>
@@ -579,10 +738,22 @@ export default function OrganizationEdit() {
</div>
{/* 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 */}
<div style={{ display: "flex", flexDirection: "column", gap: "12px", paddingTop: "15px" }}>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "12px",
paddingTop: "15px",
}}
>
{dataList.length > 0 ? (
<AnimatePresence>
{dataList.map((payment, index) => (
@@ -602,43 +773,85 @@ export default function OrganizationEdit() {
padding: "16px 20px",
boxShadow: "0 4px 12px rgba(0,0,0,0.05)",
borderLeft: "5px solid #28a745",
position: "relative"
position: "relative",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "15px", flex: 1 }}>
<div style={{
width: "50px",
height: "50px",
borderRadius: "12px",
backgroundColor: "rgba(40, 167, 69, 0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#28a745"
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "15px",
flex: 1,
}}
>
<div
style={{
width: "50px",
height: "50px",
borderRadius: "12px",
backgroundColor: "rgba(40, 167, 69, 0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#28a745",
}}
>
{getPaymentIcon(payment.method)}
</div>
<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}
</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}
</div>
<div style={{ display: "flex", gap: "10px", fontSize: "12px", color: "#888" }}>
<span>{dayjs(payment.createdAt).format("DD MMMM YYYY • HH:mm")}</span>
<div
style={{
display: "flex",
gap: "10px",
fontSize: "12px",
color: "#888",
}}
>
<span>
{dayjs(payment.createdAt).format(
"DD MMMM YYYY • HH:mm",
)}
</span>
<span>•</span>
<span>{payment.employeeName}</span>
</div>
</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "15px" }}>
<div style={{
fontSize: "20px",
fontWeight: "900",
color: "#28a745",
textAlign: "right"
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "15px",
}}
>
<div
style={{
fontSize: "20px",
fontWeight: "900",
color: "#28a745",
textAlign: "right",
}}
>
+{formatPrice(payment.price)}
</div>
<IconButton
@@ -646,14 +859,18 @@ export default function OrganizationEdit() {
size="small"
onClick={() => {
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),
() => { }
() => {},
);
}}
sx={{
sx={{
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" />
@@ -663,23 +880,35 @@ export default function OrganizationEdit() {
))}
</AnimatePresence>
) : (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
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>
<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>
)}
</div>
{payments && payments.pages > 1 && (
<div style={{ display: "flex", justifyContent: "center", marginTop: "40px" }}>
<Pagination
count={payments.pages}
page={payments.page}
<div
style={{ display: "flex", justifyContent: "center", marginTop: "40px" }}
>
<Pagination
count={payments.pages}
page={payments.page}
onChange={(e, value) => loadPayments(value, pageSize)}
color="primary"
/>
@@ -17,6 +17,7 @@ import Avatar from "@components/Avatar/Avatar";
import Button from "@core/app/components/Button/Button";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import { getMondayWeekStart } from "@core/helpers/week";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
dayjs.locale("es");
@@ -28,6 +29,7 @@ export default function Schedules() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const alert = useAlert();
@@ -38,6 +40,8 @@ export default function Schedules() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org",
title: "Organizaciones",
@@ -55,6 +59,10 @@ export default function Schedules() {
title: "Horarios Habilitados",
icon: null,
});
return () => {
bottomToolbar.clear();
};
}, []);
useEffect(() => {
@@ -118,7 +126,7 @@ export default function Schedules() {
},
() => {
return;
}
},
);
};
@@ -80,7 +80,7 @@ export default function OrganizationService() {
const [serviceDiscountName, setServiceDiscountName] = useState<string | undefined>(undefined);
const [servicePublished, setServicePublished] = useState<SERVICE_PUBLISHED_STATUS | undefined>(
undefined
undefined,
);
const [savePublishedStatusAvailable, setSavePublishedStatusAvailable] = useState(false);
@@ -149,7 +149,7 @@ export default function OrganizationService() {
>
Aplicar Descuento
</ButtonMaterial>
</div>
</div>,
);
}, []);
@@ -271,8 +271,12 @@ export default function OrganizationService() {
setServiceDiscountType(response.discountType);
setServiceDiscountValue(response.discountValue);
setServiceDiscountName(response.discountName);
setShowPublicScores(response.showPublicScores !== undefined ? response.showPublicScores : true);
setShowPublicOpinions(response.showPublicOpinions !== undefined ? response.showPublicOpinions : true);
setShowPublicScores(
response.showPublicScores !== undefined ? response.showPublicScores : true,
);
setShowPublicOpinions(
response.showPublicOpinions !== undefined ? response.showPublicOpinions : true,
);
})
.catch(() => {})
.finally(() => {
@@ -283,7 +287,7 @@ export default function OrganizationService() {
const formatDiscount = (
value: number | undefined,
type: string | undefined,
name: string | undefined
name: string | undefined,
): string => {
if (value === undefined || value === null || type === undefined || type === null) {
return "";
@@ -464,7 +468,7 @@ export default function OrganizationService() {
setServicePublished(
checked
? 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" }}>
<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>
<div style={{ display: "flex", flexDirection: "column", gap: "10px", marginTop: "10px" }}>
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
<div
style={{ display: "flex", flexDirection: "column", gap: "10px", marginTop: "10px" }}
>
<label
style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}
>
<Switch
checked={showPublicScores}
disabled={!canEditPublicRatingFlags}
@@ -520,7 +529,9 @@ export default function OrganizationService() {
/>
Mostrar puntuaciones públicas
</label>
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
<label
style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}
>
<Switch
checked={showPublicOpinions}
disabled={!canEditPublicRatingFlags}
@@ -567,7 +578,7 @@ export default function OrganizationService() {
() => {
deleteDiscountHandler();
},
() => {}
() => {},
);
}}
>
@@ -32,8 +32,9 @@ import {
WhatsappIcon as WhatsAppShareIcon,
TwitterIcon,
EmailIcon,
TelegramIcon
TelegramIcon,
} from "react-share";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
id: string;
@@ -43,6 +44,7 @@ export default function Org() {
const urlData = useParams<UrlData>();
const id = urlData.id;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const alert = useAlert();
@@ -58,6 +60,8 @@ export default function Org() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org",
title: "Mis organizaciones",
@@ -75,6 +79,10 @@ export default function Org() {
title: "Mis servicios",
icon: <StorefrontOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, []);
useEffect(() => {
@@ -105,7 +113,7 @@ export default function Org() {
<div
className={style.serviceCard}
style={{
borderLeft: `6px solid ${data.color}`
borderLeft: `6px solid ${data.color}`,
}}
onClick={() => handleServiceClick(data.id)}
>
@@ -115,12 +123,16 @@ export default function Org() {
<div className={style.serviceInfo}>
<h1>{data.name}</h1>
<p>{data.description}</p>
<ServiceRatingSummary serviceId={data.id} variant="row" showOpinionCount={false} />
<ServiceRatingSummary
serviceId={data.id}
variant="row"
showOpinionCount={false}
/>
</div>
<div className={style.serviceActions}>
<span className={style.servicePrice}>{formatPrice(data.price)}</span>
<div className={style.iconsRow}>
<IconButton
<IconButton
onClick={(e) => {
e.stopPropagation();
goTo(`/landing/service/${data.id}`);
@@ -131,7 +143,7 @@ export default function Org() {
>
<OpenInNewIcon fontSize="small" />
</IconButton>
<IconButton
<IconButton
onClick={(e) => {
e.stopPropagation();
setShareService(data);
@@ -159,8 +171,21 @@ export default function Org() {
<div style={{ marginBottom: "20px" }}>&nbsp;</div>
{shareService && (
<Dialog open={!!shareService} onClose={() => setShareService(null)} maxWidth="sm" fullWidth>
<DialogTitle sx={{ m: 0, p: 2, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Dialog
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>
<IconButton
aria-label="close"
@@ -171,23 +196,57 @@ export default function Org() {
</IconButton>
</DialogTitle>
<DialogContent dividers>
<div 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
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 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}>
<div
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 />
</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 />
</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 />
</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 />
</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 />
</TwitterShareButton>
</div>
@@ -54,7 +54,7 @@ export default function TemplateCreate() {
const insertAtCursor = (
ref: React.RefObject<HTMLInputElement | HTMLTextAreaElement>,
insertText: string
insertText: string,
) => {
const el = ref.current;
if (!el) return; // todavía no está montado
@@ -126,14 +126,14 @@ export default function TemplateCreate() {
},
() => {
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}
},
);
}}
>
Eliminar
</ButtonMaterial>
)}
</div>
</div>,
);
loadTemplate();
@@ -17,6 +17,7 @@ import {
} from "@models/Templates.model";
import { findTemplatesByOrganization } from "./Templates.Service";
import { useNavigation } from "@core/app/hooks/goto";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
id: string;
@@ -29,6 +30,7 @@ export default function TemplatesGrid() {
const oid = data.id;
const { goTo } = useNavigation();
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const formatType = (type: string) => {
@@ -91,8 +93,8 @@ export default function TemplatesGrid() {
parseInt(
process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
: "10"
)
: "10",
),
);
const [templates, setTemplates] = useState<PaginateTemplatesResults>();
@@ -103,6 +105,8 @@ export default function TemplatesGrid() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org",
title: "Mis organizaciones",
@@ -122,6 +126,10 @@ export default function TemplatesGrid() {
});
loadTemplates(1, pageSize);
return () => {
bottomToolbar.clear();
};
}, []);
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 { useConfirmStore } from "@core/Store/Confirm.Store";
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
id: string;
@@ -35,6 +36,7 @@ export default function OrganizationEdit() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const alert = useAlert();
@@ -46,6 +48,8 @@ export default function OrganizationEdit() {
useEffect(() => {
location.clear();
bottomToolbar.set(<></>);
location.add({
url: "/admin/org",
title: "Organizaciones",
@@ -63,6 +67,10 @@ export default function OrganizationEdit() {
title: "Whatsapp",
icon: <OrgConfigIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
return () => {
bottomToolbar.clear();
};
}, []);
useEffect(() => {
@@ -420,7 +428,7 @@ export default function OrganizationEdit() {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
},
() => { }
() => {},
);
}}
/>