feat: add appointment notification sharing functionality via WhatsApp, email, and system clipboard

This commit is contained in:
2026-07-29 14:38:13 -03:00
parent 3d8911c093
commit 2a33f636a0
19 changed files with 679 additions and 5 deletions
@@ -1,6 +1,7 @@
import ApiRequest from "@services/Api.Service";
import {
ApplyAppointmentDiscountParams,
AppointmentNotificationPreviewResult,
AppointmentAdminByClientView,
AppointmentEventByClient,
DeleteAppointmentDiscountParams,
@@ -108,6 +109,23 @@ export const createImmediateNotificationJobs = async (
});
};
export const getAppointmentNotificationPreview = async (
data: SendAppointmentNotificationParams
): Promise<AppointmentNotificationPreviewResult> => {
return new Promise<AppointmentNotificationPreviewResult>((resolve, reject) => {
schemaSendWapNotification
.validate(data, { abortEarly: true })
.then(() => {
resolve(
ApiRequest.post<AppointmentNotificationPreviewResult>("appointments/notification-preview", data)
);
})
.catch((error) => {
reject(new ApiError(500, error.message));
});
});
};
const schemaApplyDiscount = Yup.object().shape({
appointmentId: Yup.string().required("Debe seleccionar un turno."),
companyId: Yup.string().required("Organización no seleccionada."),
@@ -13,12 +13,14 @@ import {
deleteAppointment,
deleteAppointmentDiscount,
findAppointmentsAdminByClient,
getAppointmentNotificationPreview,
updateAppointment,
} from "../Appointments.Service";
import {
ApplyAppointmentDiscountParams,
APPOINTMENT_NOTIFICATION_TYPE,
AppointmentEventByClient,
AppointmentNotificationPreviewResult,
DeleteAppointmentDiscountParams,
UpdateAppointmentParams,
} from "@core/Models/Appointments.model";
@@ -27,6 +29,10 @@ import Textbox from "@core/app/components/Textbox/Textbox";
import { useMenuStore } from "@core/Store/Menu.Store";
import NotificationsActiveOutlinedIcon from "@mui/icons-material/NotificationsActiveOutlined";
import DeleteIcon from "@mui/icons-material/Delete";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import EmailIcon from "@mui/icons-material/Email";
import ShareIcon from "@mui/icons-material/Share";
import WhatsAppIcon from "@mui/icons-material/WhatsApp";
import Switch from "@mui/material/Switch";
import { ThemeProvider } from "@emotion/react";
@@ -91,6 +97,8 @@ export default function ClientAppointment() {
const [comments, setComments] = useState<string>("");
const [dialogDiscountVisible, setDialogDiscountVisible] = useState(false);
const [dialogShareVisible, setDialogShareVisible] = useState(false);
const [sharePreview, setSharePreview] = useState<AppointmentNotificationPreviewResult | undefined>(undefined);
const [discountsFilterContent] = useState<TextObjectModalContent>({
header: (
@@ -361,6 +369,14 @@ export default function ClientAppointment() {
icon: <NotificationsActiveOutlinedIcon />,
});
menu.add({
text: "Compartir",
onClick: () => {
openShareDialog();
},
icon: <ShareIcon />,
});
if (showQuitDiscount) {
menu.divider();
@@ -408,6 +424,66 @@ export default function ClientAppointment() {
});
};
const openShareDialog = () => {
if (!appointmentId) {
return;
}
setDialogShareVisible(true);
eventHandler.setEventType(EVENT_TYPES.LOADING);
getAppointmentNotificationPreview({
appointmentId: appointmentId.toString(),
type: APPOINTMENT_NOTIFICATION_TYPE.REMINDER,
sessionUser: SessionUser.userId,
})
.then((preview) => {
setSharePreview(preview);
})
.catch((error) => {
alert.showError(error.format());
setDialogShareVisible(false);
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const copyShareText = async () => {
if (!sharePreview) {
return;
}
await navigator.clipboard.writeText(sharePreview.wapMessage);
alert.showSuccess("Texto copiado al portapapeles");
};
const shareByWhatsApp = () => {
if (!sharePreview) {
return;
}
window.open(`https://wa.me/?text=${encodeURIComponent(sharePreview.wapMessage)}`, "_blank");
};
const shareByEmail = () => {
if (!sharePreview) {
return;
}
window.location.href = `mailto:?subject=${encodeURIComponent(sharePreview.subject)}&body=${encodeURIComponent(sharePreview.emailMessage)}`;
};
const nativeShare = async () => {
if (!sharePreview || !navigator.share) {
return;
}
await navigator.share({
title: sharePreview.subject,
text: sharePreview.wapMessage,
});
};
const applyDiscountHandler = () => {
if (!SessionUser.userId) {
return;
@@ -728,6 +804,88 @@ export default function ClientAppointment() {
</>
</DialogWindow>
<DialogWindow
visible={dialogShareVisible}
setVisible={setDialogShareVisible}
>
<>
<DialogContent
style={{
maxWidth: "520px",
minWidth: "300px",
margin: "auto",
}}
>
<h3 style={{ marginBottom: "10px" }}>Compartir recordatorio</h3>
<p style={{ fontSize: "14px", fontWeight: 500, marginBottom: "10px" }}>
Este texto usa el mismo contenido del recordatorio de notificaciones.
</p>
<textarea
readOnly
value={sharePreview?.wapMessage || ""}
style={{
width: "100%",
minHeight: "180px",
border: "1px solid var(--gray)",
borderRadius: "8px",
color: "var(--black)",
fontSize: "14px",
padding: "10px",
}}
/>
</DialogContent>
<DialogActions style={{ flexWrap: "wrap", gap: "8px" }}>
<ButtonMaterial
variant="contained"
color="primary"
startIcon={<WhatsAppIcon />}
disabled={!sharePreview}
onClick={shareByWhatsApp}
>
WhatsApp
</ButtonMaterial>
<ButtonMaterial
variant="contained"
color="primary"
startIcon={<EmailIcon />}
disabled={!sharePreview}
onClick={shareByEmail}
>
Email
</ButtonMaterial>
<ButtonMaterial
variant="contained"
color="primary"
startIcon={<ContentCopyIcon />}
disabled={!sharePreview}
onClick={copyShareText}
>
Copiar
</ButtonMaterial>
{typeof navigator !== "undefined" && !!navigator.share && (
<ButtonMaterial
variant="contained"
color="primary"
startIcon={<ShareIcon />}
disabled={!sharePreview}
onClick={nativeShare}
>
Compartir
</ButtonMaterial>
)}
<ButtonMaterial
variant="contained"
color="secondary"
onClick={() => {
setDialogShareVisible(false);
}}
>
Cerrar
</ButtonMaterial>
</DialogActions>
</>
</DialogWindow>
<DialogWindow
visible={dialogChangeServiceVisible}
setVisible={setDialogChangeServiceVisible}