class WebService{ static base_url = '/api'; static async call( route, data, method, token, success, error ){ let headers = { "Host": App.getHost() }; if( token ){ headers[ 'Authorization' ] = 'Bearer '+token } let url = WebService.base_url + route; if( route.indexOf('http')==0 ){ url = route; } let errorFunc = WebService.onError( error ); let successFunc = WebService.onSuccess( success, errorFunc ); method = method.toUpperCase(); let config = { "method": method, "headers": headers }; let formData = false; if( !(data instanceof FormData) ){ formData = new FormData(); for( let key in data ){ formData.append( key, data[key] ); } } else { formData = data; } if( method!='GET' ){ config.body = formData; } else { let params = ''; for( const entry of formData.entries() ){ params += ( params!='' ? '&' : '' ); params += entry[0]+'='+entry[1]; } if( params!='' ){ if( url.indexOf('?')==-1 ){ url += '?'+params; } else { url += '&'+params; } } } fetch( url, config ) .then( (response) => response.json() ) .then( successFunc ) .catch( errorFunc ); } static onSuccess( successFunc, errorFunc ){ return function( response ){ if( Tools.exists(response.success) ){ if( response.success ){ if( Tools.isFunction(successFunc) ){ successFunc( response ); } } else { errorFunc( response ); } } else { errorFunc( {"succes":false, "message":"Server error, retry later.", "error":"server-error"} ); } } } static onError( errorFunc ){ return function( error ){ if( Tools.isFunction(errorFunc) ){ errorFunc( error ); } } } }class FormHelper{ static parse( template ){ return FormHelper.processTemplate( template ); } static processTemplate( template, excludeTag ){ let types = FormHelper.getTypes(); for( let type of types ){ if( type.tag != excludeTag ){ template = FormHelper.process( template, type.tag, type.tpl, type.multiple ); } } return template; } static process( template, tag, tpl, multiple ){ const genericParameters = ['required', 'autocomplete', 'id', 'class', 'type', 'name', 'placeholder', 'source']; let regexp = new RegExp( '\{'+tag+'(((.)*)?\})', 'gm'); let results = template.match( regexp ); if( results && results.length>0 ){ let paramsRegExp = /[\w-]+=".+?"/gm; for( let i in results ){ let input = results[i].replace(/\n/g, ''); input = input.replaceAll(/\s\s/g, ''); let parameters = input.match( paramsRegExp ); let node = tpl; if( parameters ){ for( let parameter of parameters ){ let prop = parameter.split('=')[0]; let value= parameter.match( /".+?"/gm )[0].replaceAll('"', ''); if( $.inArray(prop, genericParameters)>=0 ){ value = prop + '="'+value+'"'; } node = node.replaceAll( '{'+prop+'}', value ); } } for( let genericParameter of genericParameters ){ node = node.replaceAll( '{'+genericParameter+'}', '' ); } template = template.replace( results[i], node ); } } return template; } static getTypes(){ const inputTemplate = `
`; const inputImageTemplate = `
`; const inputPDFTemplate = `
`; const hiddenTemplate = ` `; const inputPasswordTemplate = `
`+Lang.t('Forgot password')+`
`; const inputPasswordConfirmTemplate = `
`; const inputOTPTemplate = `
`+Lang.t('Resend')+`
`; const submitTemplate = `
`; const optionTemplate = ` `; const selectStartTemplate = `
`; const toggleTemplate = `
`; const types = [ {'tag':'Hidden', 'tpl':hiddenTemplate}, {'tag':'InputPasswordConfirm', 'tpl':inputPasswordConfirmTemplate}, {'tag':'InputPassword', 'tpl':inputPasswordTemplate}, {'tag':'InputOTP', 'tpl':inputOTPTemplate}, {'tag':'Input', 'tpl':inputTemplate}, {'tag':'Image', 'tpl':inputImageTemplate}, {'tag':'PDF', 'tpl':inputPDFTemplate}, {'tag':'Select', 'tpl':selectStartTemplate}, {'tag':'/Select', 'tpl':selectEndTemplate}, {'tag':'Option', 'tpl':optionTemplate}, {'tag':'Toggle', 'tpl':toggleTemplate}, {'tag':'Submit', 'tpl':submitTemplate} ]; return types; } }class ImageInputHelper{ static initField( inst, object ){ ImageInputHelper.setFormEvents( inst, object ); if( object.files ){ for( let fileProp in object.files ){ let file = object.files[ fileProp ]; if( parseInt(file.id) ){ let propName = 'file_' + fileProp; let isImage = $( inst.node ).find('form [name="'+propName+'"]').hasClass('image'); if( isImage ){ let imageSelectorContainer = $( inst.node ).find('form [name="'+propName+'"]').closest('.image-selector'); if( file && file.url ){ ImageInputHelper.setPreviewImage( inst, object, imageSelectorContainer, file ); } } } } } } static setFormEvents( inst, object ){ $( inst.node ).find('input.image') .change( ImageInputHelper.handleChangeFile( inst, object ) ) } static handleChangeFile( inst, object ){ return function( event ){ let imageSelectorContainer = $( event.currentTarget ).closest('.image-selector'); let [file] = $( event.currentTarget ).get(0).files; if( file ){ let imageUrl = URL.createObjectURL( file ); ImageInputHelper.setPreviewImage( inst, object, imageSelectorContainer, {'url':imageUrl} ); } } } static setPreviewImage( inst, object, imageSelectorContainer, file ){ let image = $('',{'src':file.url, 'class':'object-cover rounded-md'}); let selectImageBtn = $( imageSelectorContainer ).find('.btn-select-image'); let deleteImageBtn = $( imageSelectorContainer ).find('.btn-delete-image'); $( selectImageBtn ).find('span') .html( Lang.t('Change image') ); if( file && parseInt(file.id) ){ $( imageSelectorContainer ).attr('data-id-file', file.id); } $( imageSelectorContainer ).find('.image-preview') .removeClass( 'hidden' ) .html( image ); $( deleteImageBtn ) .removeClass( 'hidden') .click( ImageInputHelper.handleRemoveImage( inst, object ) ); $( imageSelectorContainer ).find('.input-delete-file').remove(); } static handleRemoveImage( inst, object ){ return function( event ){ let imageSelectorContainer = $( event.currentTarget ).closest( '.image-selector' ); let selectImageBtn = $( imageSelectorContainer ).find('.btn-select-image'); let deleteImageBtn = $( imageSelectorContainer ).find('.btn-delete-image'); $( imageSelectorContainer ).find('.image-preview') .addClass( 'hidden' ); $( selectImageBtn ).find('span') .html( Lang.t('Select Image') ); $( deleteImageBtn ) .addClass('hidden'); $( imageSelectorContainer ).find('input.image') .val(''); if( object && parseInt(object.id) ){ let idObject = parseInt($( imageSelectorContainer ).attr('data-id-file')); if( idObject && parseInt(idObject) ){ $( imageSelectorContainer ).removeAttr('data-id-file'); let deleteFieldName = 'id_' + $( imageSelectorContainer ).find('input.image').attr('name'); let deleteInput = $('',{'type':'hidden', 'name':deleteFieldName, 'value':'delete_'+idObject, 'class':'input-delete-file'}); $( imageSelectorContainer ).append( deleteInput ); } } } } } class ObjectModel{ static routes = {}; static models = []; static getModelClass( model ){ let modelClass = false; if( ObjectModel.models[ model ] && Tools.isFunction(ObjectModel.models[ model ]) ){ modelClass = ObjectModel.models[ model ]; } else if( ObjectModel.models[ model + 'Model' ] && Tools.isFunction(ObjectModel.models[ model + 'Model' ]) ){ modelClass = ObjectModel.models[ model + 'Model' ]; } return modelClass; } static getRoutes(){ return this.routes; } static getEndpoint( method ){ let endpoint = false; let routes = this.getRoutes(); if( routes && routes[ method ] && routes[ method ].endpoint ){ endpoint = routes[ method ].endpoint; } return endpoint; } static getRoute( method ){ let route = false; let routes = this.getRoutes(); if( routes && routes[ method ] && routes[ method ].route ){ route = routes[ method ].route; } return route; } static get( id, onSuccess, onError ){ if( parseInt(id) ){ let method = 'GET'; let endpoint = this.getEndpoint( 'update' ); if( endpoint ){ endpoint = endpoint.replace( '{id}', parseInt(id) ); WebService.call( endpoint, false, method, Context.getToken(), onSuccess, onError ); } } } static save( object, onSuccess, onError ){ let method = 'POST'; let endpoint = this.getEndpoint( 'add' ); if( object ){ let id = typeof(object.get)=='function' ? object.get('id') : parseInt(object.id); if( id ){ method = 'PUT'; endpoint = this.getEndpoint( 'update' ); endpoint = endpoint.replace( '{id}', id ); } } if( endpoint ){ WebService.call( endpoint, object, method, Context.getToken(), onSuccess, onError ); } } static list( params, onSuccess, onError ){ let endpoint = this.getEndpoint( 'list' ); if( typeof(params)=='string' ){ endpoint = params; params = ''; } if( endpoint ){ WebService.call( endpoint, params, 'GET', Context.getToken(), onSuccess, onError ); } } static delete( id, onSuccess, onError ){ if( parseInt(id) ){ let endpoint = this.getEndpoint( 'delete' ); if( endpoint ){ endpoint = endpoint.replace( '{id}', id ); WebService.call( endpoint, false, 'DELETE', Context.getToken(), onSuccess, onError ); } } } }class Auth{ static profile = false; static tokens = {}; static model = false; static setProfile( profile ){ Auth.profile = profile; } static getProfile(){ return Auth.profile; } static getModel(){ let model = false; let profile = Auth.getProfile(); model = AuthObjectModel.profiles[profile]; return model; } static loadProfiles(){ let tokens = Auth.getLocalSession(); if( tokens && !Tools.empty(tokens) ){ Auth.tokens = tokens; let token = Auth.getCurrentToken(); if( token ){ Auth.validateToken( token ); } else { EventDispatcher.trigger( 'auth_error' ); } } else { EventDispatcher.trigger( 'auth_error' ); } } static validateToken( token ){ WebService.call( '/me', {}, 'POST', token, Auth.handleTokenSuccess, Auth.handleTokenError ); } static handleTokenSuccess( response ){ if( response.success && response.result ){ Context.setProfile( Auth.getProfile() ); Context.setToken( response.token ); Context.setMe( response.result ); let user_type = response.result.object_type.toLowerCase(); if( user_type ){ $('body').addClass( 'is-'+user_type ); } EventDispatcher.trigger( 'auth_success' ); } else { EventDispatcher.trigger( 'auth_error' ); } } static handleTokenError( error ){ EventDispatcher.trigger( 'auth_error' ); } static setToken( token, type ){ if( token && type ){ Auth.tokens[ type ] = token; Auth.updateLocalSession(); EventDispatcher.trigger( 'auth_success' ); } } static getCurrentToken(){ let token = false; if( Tools.exists(Auth.tokens[ Auth.getProfile() ]) ){ token = Auth.tokens[ Auth.getProfile() ]; } return token; } static getToken( type ){ let token = false; if( Auth.tokens[ type ] ){ token = Auth.tokens[ type ]; } return token; } static async logout(){ if( Context.getToken() ){ let type = Auth.getProfile(); WebService.call( '/logout', {}, 'POST', Context.getToken() ); delete(Auth.tokens[ type ]); Context.setMe( false ); Context.setToken( false ); Auth.updateLocalSession(); EventDispatcher.trigger( 'auth_logout' ); } } static getLocalSession(){ let tokens = localStorage.getItem("tokens"); try{ tokens = JSON.parse(tokens); } catch( error ){ tokens = false; } return tokens; } static async updateLocalSession(){ localStorage.setItem("tokens", JSON.stringify(Auth.tokens)); } static removeToken( token ){ for( let userType in Auth.tokens ){ if( Auth.tokens[userType].token == token ){ delete( Auth.tokens[userType] ); localStorage.setItem("tokens", Auth.tokens); } } } static isGranted( acl ){ let isGranted = false; let me = Context.getMe(); if( me && me.object_type.toLowerCase()==acl ){ if( Tools.exists(Auth.tokens[ acl ]) && Auth.tokens[ acl ] ){ isGranted = true; } } return isGranted; } static checkACL(){ let profile = Auth.getProfile(); let token = Auth.getTokenFromACL( profile ); return token; } static getTokenFromACL(){ let token = false; if( Context.getMe() ){ let profile = Auth.getProfile(); if( Tools.exists(Auth.tokens[ profile ]) && Auth.tokens[ profile ] ){ token = Auth.tokens[ profile ]; } } return token; } }class Colors{ static getTextColor( type ){ let textColor = 'text-gray-900'; switch( type ){ case 'info': textColor = 'text-cyan-500'; break; case 'success': textColor = 'text-emerald-500'; break; case 'warning': textColor = 'text-amber-500'; break; case 'error': textColor = 'text-rose-500'; break; } return textColor; } static getBgColor( type ){ let bgColor = 'bg-gray-100'; switch( type ){ case 'info': bgColor = 'bg-sky-50'; break; case 'success': bgColor = 'bg-emerald-50'; break; case 'warning': bgColor = 'bg-amber-50'; break; case 'error': bgColor = 'bg-rose-50'; break; } return bgColor; } }class Context{ static me = false; static token = false; static profile = false; static setMe( me ){ Context.me = me; } static getMe(){ return Context.me; } static setProfile( profile ){ Context.profile = profile; } static getProfile(){ return Context.profile; } static setToken( token ){ Context.token = token; } static getToken(){ return Context.token; } }class DynamicLoader{ static loaded = []; static loadScript( src, success, error ){ if( !Tools.inArray( src, DynamicLoader.loaded ) ){ var script = document.createElement('script'); script.onload = DynamicLoader.onLoadSuccess( src, success ); script.onerror = DynamicLoader.onLoadError( src, error ); script.src = src; document.head.appendChild( script ); } else if( Tools.isFunction(success) ){ success(); } } static loadStyle( src, success, error ){ if( !Tools.inArray( src, DynamicLoader.loaded ) ){ var style = document.createElement('link'); style.onload = DynamicLoader.onLoadSuccess( src, success ); style.onerror = DynamicLoader.onLoadError( src, error ); style.rel = "stylesheet"; style.type = "text/css"; style.href = src; document.head.appendChild( style ); } else if( Tools.isFunction(success) ){ success(); } } static onLoadSuccess( src, successFunc ){ return function(){ DynamicLoader.loaded.push( src ); if( Tools.isFunction(successFunc) ){ successFunc(); } } } static onLoadError( src, errorFunc ){ return function(){ if( Tools.isFunction(errorFunc) ){ errorFunc(); } } } }class EventDispatcher{ static events = {}; static on( eventName, func ){ if( !Tools.isset( EventDispatcher.events[eventName] ) ){ EventDispatcher.events[eventName] = []; } if( Tools.isFunction(func) ){ EventDispatcher.events[eventName].push( func ); } } static trigger( eventName, params ){ if( Tools.isset( EventDispatcher.events[eventName] ) ){ for( let func of EventDispatcher.events[eventName] ){ func( params ); } } } }class Lang{ static current = false; static dictionaries = {}; static loadDictionary( lang, dictionary ){ if( !Tools.empty( dictionary ) ){ Lang.dictionaries[lang] = dictionary; } } static setDictionary( lang ){ if( !Tools.empty(Lang.dictionaries[lang]) ){ Lang.current = lang; } } static t( str ){ if( Lang.current && Tools.exists(Lang.dictionaries[Lang.current]) && Tools.exists( Lang.dictionaries[Lang.current][str] ) ){ str = Lang.dictionaries[Lang.current][str]; } return str; } }class Tools{ static inArray( value, mixed ){ return mixed.indexOf( value ) !== -1; } static empty( mixed ){ return !!mixed && mixed!='' && (typeof( mixed )!='object' || Object.entries(mixed).lenght>0); } static isFunction( mixed ){ return Tools.exists(mixed) && typeof( mixed ) == 'function'; } static isObject( mixed ){ return typeof(mixed) == 'object'; } static isset( mixed ){ return typeof( mixed )!='undefined' && !!mixed; } static exists( mixed ){ let exists = false; try{ exists = typeof( mixed )!='undefined'; } catch(error) {}; return exists; } }class Alert{ static tpl = ` `; static render( target, message, type ){ if( !type ){ type = 'brand'; } let textColor = Colors.getTextColor( type ); let bgColor = Colors.getBgColor( type ); let node = $( Alert.tpl ).html( message ); $( node ).addClass( textColor + ' ' + bgColor ); $( target ).append( node ); return node; } }class Animate{ static start( target, animation ){ if( !animation ){ animation = 'slide-in-fwd-center'; } $( target ).addClass( animation ); setTimeout( ((target, animation) => { return () => { $(target).removeClass(animation); } })(target, animation), 750 ); } }class Confirm{ static render( content, options ){ let default_options = { 'confirm':false, 'cancel':true, 'size':'sm' }; if( options ){ options = Object.assign( default_options, options ); } else { options = default_options; } let footer = $('
',{'class':'grid grid-cols-2 gap-7'}); if( options.cancel ){ let iconCancel = options.cancel.icon ? $('',{'class':'fa fa-'+options.cancel.icon+' mr-2 text-xs'}) : ''; let labelCancel = options.cancel.label ? options.cancel.label : Lang.t('Cancel'); let onClickCancel = typeof(options.cancel.func)=='function' ? Confirm.handleClickFunc( options.cancel.func ) : Confirm.handleClose; let cancelButton = $('
`; static render( icon, message, type, duration, target ){ if( !type ){ type = 'brand'; } if( !duration ){ duration = 3750; } if( !target ){ target = 'body'; } let textColor = Colors.getTextColor( type ); let bgColor = Colors.getBgColor( type ); let node = $( Toaster.tpl ); $( node ).find('.toast-icon') .html( '' ) .addClass( textColor + ' ' + bgColor ); $( node ).find('.toast-message') .html( message ); $( target ).append( node ); Animate.start( node, 'slide-in-elliptic-bottom-fwd' ); $( node ).find( 'button' ).click( Toaster.handleClose ); setTimeout( ( (node) => { return() => { $(node).remove(); }; } )( node ), duration ); return node; } static handleClose( event ){ $( event.currentTarget ).closest( '.toast' ).remove(); } }class Component{ tpl = ''; styles = ''; view = false; node = false; parameters = {}; static render( target, parameters ){ let me = new this(); if( parameters ){ me.parameters = Object.assign( me.parameters, parameters ); } if( target instanceof View ){ me.view = target; target = target.node; if( me.view.parameters ){ me.parameters = Object.assign( me.parameters, me.view.parameters ); } } me.beforeParseTemplate(); let template = me.parseTemplate(); template = me.addStyles( template ); let componentSlug = me.constructor.name.replace('component','').toLocaleLowerCase(); me.node = $('
',{'class':'component '+componentSlug}).html( template ); me.beforeRender(); $( target ).append( me.node ); me.afterRender(); me.setEvents(); return me; } beforeParseTemplate(){ } beforeRender(){ } afterRender(){ } setEvents(){ } getParameter( name ){ let value = false; if( Tools.exists(this.parameters[name]) ){ value = this.parameters[name]; } return value; } parseTemplate(){ let template = this.tpl; return template; } addStyles( template ){ if( this.styles && this.styles!='' ){ template += ''; } return template; } }class FormComponent extends Component{ parseTemplate(){ let template = super.parseTemplate(); template = FormHelper.parse( template ); return template; } async afterRender(){ this.loadSelectsAjaxOptions(); super.afterRender(); this.setRoutes(); this.loadObject(); } setRoutes(){ } loadObject(){ let object = this.getParameter('object'); if( object && parseInt(object.id) ){ let id = parseInt(object.id); let model = this.getParameter('model'); let successFunc = this.handleObjectLoaded(this); let errorFunc = this.handleObjectLoadError(this); model.get( id, successFunc, errorFunc ); } } handleObjectLoaded( inst ){ return function( response ){ if( response && response.success && response.result ){ inst.parameters.object = response.result; inst.setObject(); } else { inst.renderLoadError( Lang.t('An error occurs') ); } } } handleObjectLoadError( inst ){ return function( error ){ inst.renderLoadError( error.message ); } } renderLoadError( message ){ Toaster.render( 'times', message, 'error' ); } setObject(){ let object = this.getParameter('object'); if( object ){ for( let prop in object ){ let value = object[ prop ]; let type = $( this.node ).find('form [name="'+prop+'"]').prop("type"); if( type=='checkbox' || type=='radio' ){ if( parseInt(value) ){ $( this.node ).find('form [name="'+prop+'"]').prop('checked', 'checked'); } } else { $( this.node ).find('form [name="'+prop+'"]').val( value ); } } } ImageInputHelper.initField( this, object ); } setEvents(){ super.setEvents(); $( this.node ).find('form') .submit( this.handleSubmit( this ) ) .data( 'onSuccess', this.handleSubmitSuccess( this ) ) .data( 'onError', this.handleSubmitError( this ) ); $( this.node ).find('.cancel') .click( this.handleCancel(this) ); } handleCancel( inst ){ return (event) => { inst.cancel(); } } cancel(){ let model = this.getParameter('model'); let routeName = model.getRoute('list'); let profile = Auth.getProfile(); if( profile ){ let aclRoute = '/'+profile+routeName; if( Tools.exists(Viewport.routes[ aclRoute ]) ){ routeName = aclRoute; } } Viewport.navigate( routeName ); } handleSubmit( inst ){ return (event) => { event.preventDefault(); event.stopImmediatePropagation(); inst.submit(); return false; }; } submit(){ let form = $( this.node ).find('form').get(0); let formData = new FormData( form ); $( form ).find('input[type="checkbox"]').each(function(i, input){ let name = $(input).attr('name'); let value = $(input).prop('checked') ? 1 : 0; formData.delete( name ); formData.append( name, value ); }); let object = this.getParameter( 'object' ); if( object && object.id ){ formData.append( 'id', object.id ); } let onSuccess = $( form ).data('onSuccess'); let onError = $( form ).data('onError'); this.submitData( formData, onSuccess, onError ); } submitData( formData, onSuccess, onError ){ let model = this.getParameter('model'); if( model ){ model.save( formData, onSuccess, onError ); } } handleSubmitSuccess( inst ){ return (response) => { inst.submitSuccess( response ); }; } submitSuccess( response ){ if( response.success && response.result ){ let model = this.getParameter( 'model' ); let routeName = model.getRoute( 'list' ); let profile = Auth.getProfile(); if( profile ){ let aclRoute = '/'+profile+routeName; if( Tools.exists(Viewport.routes[ aclRoute ]) ){ routeName = aclRoute; } } Viewport.navigate( routeName ); Toaster.render( 'check', Lang.t('Informations saved'), 'success' ); } else { this.submitError( response ); } } handleSubmitError( inst ){ return (response) => { inst.submitError( response ); }; } submitError( response ){ let errorMessage = Lang.t( response.error ); switch( response.error ){ case 'email-missing': $( this.node ).find( '[name="email"]' ).addClass( 'border-red-700' ); break; } if( errorMessage && errorMessage!='' ){ $( this.node ).find( '.error-message' ).html( '' + errorMessage ); } } //Ajax selects loadSelectsAjaxOptions(){ let selects = $( this.node ).find('select[source]'); for( let select of selects ){ this.loadSelectAjaxOptions( select ); }; } loadSelectAjaxOptions( select ){ let ajaxOptions = this.getSelectAjaxOptions( select ); if( ajaxOptions ){ $( select ).select2({ 'ajax': ajaxOptions }); ajaxOptions.async = false; $.get( ajaxOptions, (( select ) => { return (data)=>{ let items = false; if( data.success && data.result ){ items = $.map( data.result, (item) => { return { 'text': item.name ?? item.title ?? item.label, 'id': item.id }; } ); for( let item of items ){ let option = $('