@team_yumi/dynamic-form
Version:
A dynamic form library with bottom sheet modal for Ionic React applications
384 lines • 19.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.FormBottomSheet = void 0;
const tslib_1 = require("tslib");
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const axios_1 = tslib_1.__importDefault(require("axios"));
const ramen_1 = tslib_1.__importDefault(require("@team_yumi/ramen"));
const instrument_container_1 = require("../instrument-container");
require("./index.sass");
require("@team_yumi/ramen/index.css");
const FormBottomSheet = ({ isOpen, onDidDismiss, formData: propFormData,
// Nuevos parámetros
hostUrl, sheetEndpoint, skipEndpoint, sendEndpoint, apiKey,
// Parámetros legacy
title, subtitle, confirmButtonText = 'Confirmar', skipButtonText = 'Saltear', skipDelayMinutes = 30, onFormComplete, onFormChange, onFormSkipped, getUserData, onShouldNotShow, size = 'l', }) => {
const [answers, setAnswers] = (0, react_1.useState)({});
const [stats, setStats] = (0, react_1.useState)();
const [formData, setFormData] = (0, react_1.useState)(propFormData || null);
const [internalIsOpen, setInternalIsOpen] = (0, react_1.useState)(false);
const [loading, setLoading] = (0, react_1.useState)(false);
const [error, setError] = (0, react_1.useState)(null);
const [userData, setUserData] = (0, react_1.useState)(null);
const [shouldShowForm, setShouldShowForm] = (0, react_1.useState)(true); // Controla si el formulario debe mostrarse
const [currentSheetId, setCurrentSheetId] = (0, react_1.useState)(null); // ID del sheet actual para skip
const [isSending, setIsSending] = (0, react_1.useState)(false); // Estado para el envío de respuestas
const [dynamicTitle, setDynamicTitle] = (0, react_1.useState)(null); // Title dinámico del API
const [dynamicSubtitle, setDynamicSubtitle] = (0, react_1.useState)(null); // Subtitle dinámico del API
const [isSkippable, setIsSkippable] = (0, react_1.useState)(true); // Si el formulario puede ser saltado
const [isCompleted, setIsCompleted] = (0, react_1.useState)(false); // Estado para controlar si el formulario se ha completado
// Función para verificar si el skipDate aún es vigente
const isSkipDateValid = (skipDate) => {
if (!skipDate)
return false;
try {
const skipDateTime = new Date(skipDate);
const now = new Date();
// Si la fecha actual es menor o igual a skipDate, el skip aún es vigente
return now <= skipDateTime;
}
catch (error) {
console.error('Error al parsear skipDate:', error);
return false;
}
};
// Función para cargar datos del usuario
const loadUserData = () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
if (!getUserData)
return;
try {
const result = yield getUserData();
setUserData(result);
}
catch (err) {
console.error('Error al cargar datos del usuario:', err);
}
});
// Función para construir la URL del sheet endpoint con query parameters
const buildSheetUrl = () => {
if (!hostUrl || !sheetEndpoint || !userData)
return null;
const baseUrl = `${hostUrl}${sheetEndpoint}`;
const params = new URLSearchParams(Object.assign(Object.assign({ businessUnit: userData.businessUnit, role: userData.role, userEmail: userData.email }, (userData.moduleCode ? { moduleCode: userData.moduleCode } : {})), (userData.country ? { country: userData.country } : {})));
return `${baseUrl}?${params.toString()}`;
};
// Función para cargar datos del formulario desde el endpoint
const loadFormData = () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
// Usar nuevos parámetros si están disponibles, sino usar endpoint legacy
const urlToUse = buildSheetUrl();
if (!urlToUse || !apiKey)
return;
setLoading(true);
setError(null);
try {
const response = yield axios_1.default.get(urlToUse, {
headers: {
apiKey: apiKey,
},
});
// Tomar el primer elemento de la lista y extraer el schema
if (response.data && response.data.length > 0) {
const firstItem = response.data[0];
// Verificar userEngagement para determinar si mostrar el formulario
if (firstItem.userEngagement) {
const { hasResponse, status, isSkipped, skipDate } = firstItem.userEngagement;
// Si ya completó la encuesta (hasResponse: true, status: 'sent'), no mostrar
if (hasResponse && status === 'sent') {
setShouldShowForm(false);
console.log('El usuario ya completó esta encuesta');
onShouldNotShow === null || onShouldNotShow === void 0 ? void 0 : onShouldNotShow();
return;
}
// Verificar si el formulario fue saltado y si el skip aún es vigente
if (isSkipped) {
if (skipDate && isSkipDateValid(skipDate)) {
// El skip es vigente, no mostrar el formulario
setShouldShowForm(false);
console.log('El usuario saltó esta encuesta y el período de skip aún es vigente');
onShouldNotShow === null || onShouldNotShow === void 0 ? void 0 : onShouldNotShow();
return;
}
// Si skipDate no tiene valor o ya no es vigente, permitir mostrar el formulario
if (!skipDate) {
console.log('El usuario saltó esta encuesta pero skipDate es null, permitir mostrar');
}
else {
console.log('El usuario saltó esta encuesta pero el período de skip ha expirado, permitir mostrar');
}
}
// Si está en borrador (hasResponse: true, status: 'draft'), sí puede mostrarse
// Si hasResponse: false o no hay status, también se puede mostrar
}
if (firstItem.schema && firstItem.schema.questions) {
const formData = {
questions: firstItem.schema.questions,
overrideAlternatives: [],
};
setAnswers({});
setStats(undefined);
setFormData(formData);
setShouldShowForm(true);
// Guardar el sheetId para operaciones de skip
if (firstItem.id || firstItem._id || firstItem.sheetId) {
setCurrentSheetId(firstItem.id || firstItem._id || firstItem.sheetId || null);
}
// Extraer title y subtitle dinámicos del API si están disponibles
if (firstItem.title) {
setDynamicTitle(firstItem.title);
}
if (firstItem.subtitle) {
setDynamicSubtitle(firstItem.subtitle);
}
// Extraer configuración de skippable del API
if (typeof firstItem.skippable === 'boolean') {
setIsSkippable(firstItem.skippable);
}
else {
setIsSkippable(true); // Por defecto es skippable
}
}
else {
setError('El formato del formulario no es válido');
setShouldShowForm(false);
onShouldNotShow === null || onShouldNotShow === void 0 ? void 0 : onShouldNotShow();
}
}
else {
setError('No se encontraron formularios activos');
setShouldShowForm(false);
onShouldNotShow === null || onShouldNotShow === void 0 ? void 0 : onShouldNotShow();
}
}
catch (err) {
console.error('Error al cargar datos del formulario:', err);
setError('Error al cargar la configuración del formulario');
setShouldShowForm(false); // No mostrar el modal si hay error
onShouldNotShow === null || onShouldNotShow === void 0 ? void 0 : onShouldNotShow();
}
finally {
setLoading(false);
}
});
const handleFormChange = (newAnswers, newStats, question) => {
setAnswers(newAnswers);
setStats(newStats);
onFormChange === null || onFormChange === void 0 ? void 0 : onFormChange(newAnswers, newStats, question);
};
const handleConfirm = () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
if (!(stats === null || stats === void 0 ? void 0 : stats.finished))
return;
// Si hay sendEndpoint configurado, enviar las respuestas automáticamente
if (hostUrl && sendEndpoint && userData && currentSheetId) {
setIsSending(true);
try {
yield sendResponses();
console.log('Respuestas enviadas exitosamente');
}
catch (err) {
console.error('Error al enviar respuestas:', err);
// Continúa con el flujo normal aunque haya error
}
finally {
setIsSending(false);
}
}
// Ejecutar callback original
if (onFormComplete) {
onFormComplete(answers, stats, userData || undefined);
setIsCompleted(true);
setFormData(null);
}
// 🆕 Cerrar modal automáticamente si se usa estado interno
// if (isOpen === undefined) {
// setInternalIsOpen(false);
// }
// onDidDismiss();
});
const sendResponses = () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
if (!hostUrl || !sendEndpoint || !userData || !currentSheetId) {
throw new Error('Faltan parámetros necesarios para enviar respuestas');
}
const responsePayload = {
sheetId: currentSheetId,
country: userData.country || undefined,
businessUnit: userData.businessUnit,
moduleCode: userData.moduleCode || undefined,
userName: userData.name,
userEmail: userData.email,
userStore: userData.store,
userRole: userData.role,
response: answers,
skip: false,
};
const sendUrl = `${hostUrl}${sendEndpoint}`;
yield axios_1.default.post(sendUrl, responsePayload, {
headers: {
apiKey: apiKey,
'Content-Type': 'application/json',
},
});
});
const handleCancel = () => {
// 🆕 Cerrar modal automáticamente si se usa estado interno
if (isOpen === undefined) {
setInternalIsOpen(false);
}
onDidDismiss();
};
const handleSkip = () => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
if (!hostUrl || !skipEndpoint || !userData || !currentSheetId) {
console.error('Faltan parámetros necesarios para hacer skip');
return;
}
try {
const skipPayload = {
sheetId: currentSheetId,
userEmail: userData.email,
userStore: userData.store,
userRole: userData.role,
userName: userData.name,
country: userData.country || undefined,
businessUnit: userData.businessUnit,
moduleCode: userData.moduleCode || undefined,
skipDelayMinutes: skipDelayMinutes,
};
const skipUrl = `${hostUrl}${skipEndpoint}`;
yield axios_1.default.post(skipUrl, skipPayload, {
headers: {
apiKey: apiKey,
'Content-Type': 'application/json',
},
});
console.log('Skip realizado exitosamente');
onFormSkipped === null || onFormSkipped === void 0 ? void 0 : onFormSkipped(userData);
// 🆕 Cerrar modal automáticamente si se usa estado interno
if (isOpen === undefined) {
setInternalIsOpen(false);
}
onDidDismiss();
}
catch (err) {
console.error('Error al realizar skip:', err);
// En caso de error, seguimos permitiendo cerrar el modal
onDidDismiss();
}
});
// 🆕 Inicialización - cargar datos del usuario al montar el componente
(0, react_1.useEffect)(() => {
// Reset del estado de visualización del formulario
setShouldShowForm(true);
setCurrentSheetId(null);
setDynamicTitle(null);
setDynamicSubtitle(null);
setIsSkippable(true);
// Cargar datos del usuario si hay callback
if (getUserData) {
loadUserData();
}
}, []);
// Cargar datos del formulario cuando se tengan los datos del usuario
(0, react_1.useEffect)(() => {
if (!propFormData && apiKey) {
// Para nuevos parámetros, necesitamos userData antes de hacer la llamada
if (hostUrl && sheetEndpoint && userData) {
loadFormData();
}
}
}, [userData, hostUrl, sheetEndpoint, apiKey, propFormData]);
// 🆕 Abrir modal automáticamente cuando hay formData válido (solo si isOpen no está definido)
(0, react_1.useEffect)(() => {
if (isOpen === undefined && formData && formData.questions.length > 0 && shouldShowForm) {
console.log('->', isOpen, formData.questions.length);
setInternalIsOpen(true);
}
}, [formData, shouldShowForm, isOpen]);
// Funciones para obtener title y subtitle final (props tienen prioridad sobre API)
const getFinalTitle = (0, react_1.useCallback)(() => {
return title || dynamicTitle || 'Formulario';
}, [title, dynamicTitle]);
const getFinalSubtitle = (0, react_1.useCallback)(() => {
return subtitle || dynamicSubtitle || undefined;
}, [subtitle, dynamicSubtitle]);
// Determinar si mostrar el botón de skip
const shouldShowSkipButton = hostUrl && skipEndpoint && userData && currentSheetId && isSkippable;
const actions = [
...(isCompleted ? [{
key: 'close',
text: 'Cerrar',
type: 'outline',
disabled: false,
}] : [
{
key: 'confirm',
text: confirmButtonText,
type: 'solid',
disabled: !(stats === null || stats === void 0 ? void 0 : stats.finished) || loading || isSending,
},
]),
...(!isCompleted && shouldShowSkipButton
? [
{
key: 'skip',
text: skipButtonText,
type: 'clear',
disabled: !isSkippable || loading || isSending,
},
]
: []),
];
const handleActionClick = (key) => {
if (key === 'confirm') {
if (stats === null || stats === void 0 ? void 0 : stats.finished) {
handleConfirm();
}
}
else if (key === 'skip') {
handleSkip();
}
else if (key === 'cancel') {
handleCancel();
}
else if (key === 'close') {
onDidDismiss();
setInternalIsOpen(false);
}
};
// Contenido del modal basado en el estado
const renderContent = (0, react_1.useCallback)(() => {
var _a;
if (loading) {
return ((0, jsx_runtime_1.jsx)(ramen_1.default.XBox, { orientation: "vertical", gap: "m", horizontalAlign: "center", padding: "xl", children: (0, jsx_runtime_1.jsx)(ramen_1.default.XText, { children: "Cargando formulario..." }) }));
}
if (error) {
return ((0, jsx_runtime_1.jsx)(ramen_1.default.XBox, { orientation: "vertical", gap: "m", horizontalAlign: "center", padding: "xl", children: (0, jsx_runtime_1.jsx)(ramen_1.default.XText, { children: error }) }));
}
if ((!formData || !formData.questions || formData.questions.length === 0) && !isCompleted) {
return ((0, jsx_runtime_1.jsx)(ramen_1.default.XBox, { orientation: "vertical", gap: "m", horizontalAlign: "center", padding: "xl", children: (0, jsx_runtime_1.jsx)(ramen_1.default.XText, { children: "No hay preguntas disponibles" }) }));
}
if (!formData && isCompleted) {
return ((0, jsx_runtime_1.jsx)(ramen_1.default.XEmptyState, { title: 'Gracias por tu respuesta', subtitle: '!Seguimos trabajando para ofrecerte una mejor experiencia!', isCenter: true, type: 'success' }));
}
return ((0, jsx_runtime_1.jsx)(instrument_container_1.InstrumentContainer, { questions: (_a = formData === null || formData === void 0 ? void 0 : formData.questions) !== null && _a !== void 0 ? _a : [], answers: answers, onChangeHandler: handleFormChange }));
}, [loading, error, formData, answers, handleFormChange]);
// 🆕 Determinar la visibilidad del modal
const getModalVisibility = (0, react_1.useCallback)(() => {
if (isOpen !== undefined) {
// Si isOpen está definido, usar ese valor
return isOpen && shouldShowForm;
}
else {
// Si isOpen no está definido, usar estado interno
return internalIsOpen && shouldShowForm;
}
}, [isOpen, shouldShowForm, internalIsOpen]);
// 🆕 Early return si el modal no debe mostrarse
if (!shouldShowForm) {
return null;
}
return ((0, jsx_runtime_1.jsxs)(ramen_1.default.XModal, { visible: getModalVisibility(), title: getFinalTitle(), subtitle: getFinalSubtitle(), size: size, actions: [], closable: !isCompleted, onActionClick: handleActionClick, onClose: () => {
handleSkip();
}, children: [(0, jsx_runtime_1.jsx)(ramen_1.default.XBox, { orientation: "vertical", gap: "m", children: renderContent() }), (0, jsx_runtime_1.jsx)(ramen_1.default.XBox, { orientation: "horizontal", gap: "m", horizontalAlign: "center", children: (0, jsx_runtime_1.jsx)(ramen_1.default.XDivider, { size: 'l', orientation: 'horizontal', backgroundThone: 'light' }) }), (0, jsx_runtime_1.jsx)(ramen_1.default.XBox, { orientation: "vertical", verticalAlign: 'center', width: 'full', gap: "m", horizontalAlign: "center", padding: 'xl', children: actions.map((action) => ((0, jsx_runtime_1.jsx)(ramen_1.default.XButton, { disabled: action.disabled, text: action.text, type: action.type, size: 'xl', onClick: () => handleActionClick(action.key) }, action.key))) })] }));
};
exports.FormBottomSheet = FormBottomSheet;
//# sourceMappingURL=index.js.map