UNPKG

@team_yumi/dynamic-form

Version:

A dynamic form library with bottom sheet modal for Ionic React applications

384 lines 19.3 kB
"use strict"; 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