2022-10-27 12:59:19 +00:00
< script setup >
2023-01-26 13:29:01 +00:00
import axios from 'axios' ;
2024-02-02 15:45:29 +00:00
import { onMounted , onUnmounted , computed , ref , watch , nextTick } from 'vue' ;
2024-03-06 15:23:19 +00:00
import { onBeforeRouteLeave , useRouter } from 'vue-router' ;
2022-10-27 12:59:19 +00:00
import { useI18n } from 'vue-i18n' ;
import { useQuasar } from 'quasar' ;
import { useState } from 'src/composables/useState' ;
2023-08-16 13:04:16 +00:00
import { useStateStore } from 'stores/useStateStore' ;
2022-10-31 08:34:01 +00:00
import { useValidator } from 'src/composables/useValidator' ;
2023-11-30 20:46:07 +00:00
import useNotify from 'src/composables/useNotify.js' ;
2022-11-17 07:04:12 +00:00
import SkeletonForm from 'components/ui/SkeletonForm.vue' ;
2024-02-27 14:41:09 +00:00
import VnConfirm from './ui/VnConfirm.vue' ;
2024-03-12 14:31:32 +00:00
import { tMobile } from 'src/composables/tMobile' ;
2024-05-22 09:22:46 +00:00
import { useArrayData } from 'src/composables/useArrayData' ;
2024-06-11 12:09:42 +00:00
import { useRoute } from 'vue-router' ;
2024-11-21 19:14:54 +00:00
import { getDifferences } from 'src/filters' ;
2022-10-27 12:59:19 +00:00
2024-03-06 15:23:19 +00:00
const { push } = useRouter ( ) ;
2022-10-27 12:59:19 +00:00
const quasar = useQuasar ( ) ;
const state = useState ( ) ;
2023-08-16 13:04:16 +00:00
const stateStore = useStateStore ( ) ;
const { t } = useI18n ( ) ;
2022-10-31 08:34:01 +00:00
const { validate } = useValidator ( ) ;
2023-11-30 20:46:07 +00:00
const { notify } = useNotify ( ) ;
2024-06-11 12:09:42 +00:00
const route = useRoute ( ) ;
2024-06-21 11:09:34 +00:00
const myForm = ref ( null ) ;
2022-10-27 12:59:19 +00:00
const $props = defineProps ( {
url : {
type : String ,
default : '' ,
} ,
model : {
type : String ,
2024-06-11 12:09:42 +00:00
default : null ,
2022-10-27 12:59:19 +00:00
} ,
filter : {
type : Object ,
default : null ,
} ,
2023-07-10 04:04:05 +00:00
urlUpdate : {
type : String ,
default : null ,
} ,
2023-11-30 20:46:07 +00:00
urlCreate : {
type : String ,
default : null ,
} ,
2023-08-07 10:00:42 +00:00
defaultActions : {
type : Boolean ,
default : true ,
} ,
2024-03-12 14:31:32 +00:00
defaultButtons : {
type : Object ,
default : ( ) => { } ,
} ,
2023-11-30 11:47:40 +00:00
autoLoad : {
type : Boolean ,
default : false ,
} ,
2023-11-30 20:46:07 +00:00
formInitialData : {
type : Object ,
default : ( ) => { } ,
} ,
observeFormChanges : {
type : Boolean ,
default : true ,
description :
'Esto se usa principalmente para permitir guardar sin hacer cambios (Útil para la feature de clonar ya que en este caso queremos poder guardar de primeras)' ,
} ,
2023-12-11 14:47:16 +00:00
mapper : {
type : Function ,
default : null ,
2023-12-26 19:15:46 +00:00
} ,
2024-02-26 16:22:25 +00:00
clearStoreOnUnmount : {
type : Boolean ,
default : true ,
} ,
2024-02-12 14:06:20 +00:00
saveFn : {
type : Function ,
default : null ,
2024-02-05 14:04:13 +00:00
} ,
2024-03-06 15:23:19 +00:00
goTo : {
type : String ,
default : '' ,
description : 'It is used for redirect on click "save and continue"' ,
} ,
2024-06-20 08:26:22 +00:00
reload : {
type : Boolean ,
default : false ,
} ,
2024-08-16 10:29:35 +00:00
defaultTrim : {
type : Boolean ,
default : true ,
} ,
2022-10-27 12:59:19 +00:00
} ) ;
2023-12-18 15:06:07 +00:00
const emit = defineEmits ( [ 'onFetch' , 'onDataSaved' ] ) ;
2024-06-11 12:09:42 +00:00
const modelValue = computed (
2024-07-12 11:34:31 +00:00
( ) => $props . model ? ? ` formModel_ ${ route ? . meta ? . title ? ? route . name } `
2024-06-18 07:44:50 +00:00
) . value ;
2024-02-02 15:45:29 +00:00
const componentIsRendered = ref ( false ) ;
2024-06-11 12:09:42 +00:00
const arrayData = useArrayData ( modelValue ) ;
2024-05-22 09:22:46 +00:00
const isLoading = ref ( false ) ;
// Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas
const isResetting = ref ( false ) ;
const hasChanges = ref ( ! $props . observeFormChanges ) ;
2024-11-22 09:48:03 +00:00
const changes = ref ( { } ) ;
2024-05-22 09:22:46 +00:00
const originalData = ref ( { } ) ;
2024-06-11 12:09:42 +00:00
const formData = computed ( ( ) => state . get ( modelValue ) ) ;
2024-05-22 09:22:46 +00:00
const defaultButtons = computed ( ( ) => ( {
save : {
color : 'primary' ,
icon : 'save' ,
label : 'globals.save' ,
2024-06-21 11:09:34 +00:00
click : ( ) => myForm . value . submit ( ) ,
type : 'submit' ,
2024-05-22 09:22:46 +00:00
} ,
reset : {
color : 'primary' ,
icon : 'restart_alt' ,
label : 'globals.reset' ,
2024-06-21 11:09:34 +00:00
click : ( ) => reset ( ) ,
2024-05-22 09:22:46 +00:00
} ,
... $props . defaultButtons ,
} ) ) ;
2024-02-02 15:45:29 +00:00
2023-11-30 11:47:40 +00:00
onMounted ( async ( ) => {
2024-05-23 07:29:50 +00:00
originalData . value = JSON . parse ( JSON . stringify ( $props . formInitialData ? ? { } ) ) ;
2024-05-22 09:22:46 +00:00
nextTick ( ( ) => ( componentIsRendered . value = true ) ) ;
2024-02-02 15:45:29 +00:00
2023-11-30 20:46:07 +00:00
// Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla
2024-06-11 12:09:42 +00:00
state . set ( modelValue , $props . formInitialData ) ;
2023-11-30 20:46:07 +00:00
2024-06-11 07:06:09 +00:00
if ( ! $props . formInitialData ) {
if ( $props . autoLoad && $props . url ) await fetch ( ) ;
2024-06-18 07:43:44 +00:00
else if ( arrayData . store . data ) updateAndEmit ( 'onFetch' , arrayData . store . data ) ;
2024-06-11 07:06:09 +00:00
}
2023-11-30 20:46:07 +00:00
if ( $props . observeFormChanges ) {
2024-05-22 09:22:46 +00:00
watch (
( ) => formData . value ,
2024-05-22 11:57:12 +00:00
( newVal , oldVal ) => {
if ( ! oldVal ) return ;
2024-05-23 07:29:50 +00:00
hasChanges . value =
! isResetting . value &&
JSON . stringify ( newVal ) !== JSON . stringify ( originalData . value ) ;
2024-11-22 09:48:03 +00:00
console . error ( getDifferences ( originalData . value , newVal ) ) ;
console . error ( getDifferences ( newVal , originalData . value ) ) ;
console . error ( getDiff ( newVal , originalData . value ) ) ;
console . error ( getDiff ( originalData . value , newVal ) ) ;
console . error ( getDifferencess ( originalData . value , newVal ) ) ;
changes . value = {
... changes . value ,
... getDifferences ( originalData . value , oldVal ) ,
} ;
2024-05-22 09:22:46 +00:00
isResetting . value = false ;
} ,
2024-11-22 09:48:03 +00:00
{ deep : true , flush : 'sync' }
2024-05-22 09:22:46 +00:00
) ;
2023-11-30 20:46:07 +00:00
}
2023-11-30 11:47:40 +00:00
} ) ;
2024-11-22 09:48:03 +00:00
function getDifferencess ( oldVal , newVal ) {
return Object . keys ( newVal ) . reduce ( ( diff , key ) => {
if ( oldVal [ key ] !== newVal [ key ] ) {
diff [ key ] = newVal [ key ] ;
}
return diff ;
} , { } ) ;
}
function getDiff ( o2 , o1 ) {
let diff = Object . keys ( o2 ) . reduce ( ( diff , key ) => {
if ( o1 [ key ] === o2 [ key ] ) return diff ;
return {
... diff ,
[ key ] : o2 [ key ] ,
} ;
} , { } ) ;
2022-10-27 12:59:19 +00:00
2024-11-22 09:48:03 +00:00
return diff ;
}
2024-05-22 09:22:46 +00:00
if ( ! $props . url )
watch (
( ) => arrayData . store . data ,
2024-07-12 11:34:31 +00:00
( val ) => updateAndEmit ( 'onFetch' , val )
2024-05-22 09:22:46 +00:00
) ;
2024-08-05 11:36:27 +00:00
watch (
( ) => [ $props . url , $props . filter ] ,
async ( ) => {
originalData . value = null ;
reset ( ) ;
await fetch ( ) ;
}
) ;
2024-05-22 09:22:46 +00:00
2024-02-27 14:41:09 +00:00
onBeforeRouteLeave ( ( to , from , next ) => {
2024-04-19 07:39:17 +00:00
if ( hasChanges . value && $props . observeFormChanges )
2024-04-09 13:12:54 +00:00
quasar . dialog ( {
component : VnConfirm ,
componentProps : {
2024-08-01 08:51:13 +00:00
title : t ( 'globals.unsavedPopup.title' ) ,
message : t ( 'globals.unsavedPopup.subtitle' ) ,
2024-04-09 13:12:54 +00:00
promise : ( ) => next ( ) ,
} ,
} ) ;
else next ( ) ;
2024-02-27 14:41:09 +00:00
} ) ;
2022-10-27 12:59:19 +00:00
onUnmounted ( ( ) => {
2024-02-26 16:22:25 +00:00
// Restauramos los datos originales en el store si se realizaron cambios en el formulario pero no se guardaron, evitando modificaciones erróneas.
2024-06-11 12:09:42 +00:00
if ( hasChanges . value ) return state . set ( modelValue , originalData . value ) ;
if ( $props . clearStoreOnUnmount ) state . unset ( modelValue ) ;
2022-10-27 12:59:19 +00:00
} ) ;
async function fetch ( ) {
2024-02-05 20:51:09 +00:00
try {
2024-05-08 09:02:40 +00:00
let { data } = await axios . get ( $props . url , {
2024-02-05 20:51:09 +00:00
params : { filter : JSON . stringify ( $props . filter ) } ,
} ) ;
2024-05-10 11:07:58 +00:00
if ( Array . isArray ( data ) ) data = data [ 0 ] ? ? { } ;
2024-05-08 09:02:40 +00:00
2024-06-06 07:53:48 +00:00
updateAndEmit ( 'onFetch' , data ) ;
} catch ( e ) {
2024-06-11 12:09:42 +00:00
state . set ( modelValue , { } ) ;
2024-02-05 20:51:09 +00:00
originalData . value = { } ;
}
2022-10-27 12:59:19 +00:00
}
2024-11-22 09:04:17 +00:00
function handleRequestArgs ( ) {
2024-11-21 19:14:54 +00:00
const isUrlCreate = $props . urlCreate ;
const differences = getDifferences ( formData . value , originalData . value ) ;
return {
method : isUrlCreate ? 'post' : 'patch' ,
url : isUrlCreate || $props . urlUpdate || $props . url || arrayData . store . url ,
body : $props . mapper
? $props . mapper ( formData . value )
: isUrlCreate
? formData . value
: differences ,
} ;
}
2024-11-22 09:48:03 +00:00
function getUpdatedValues ( keys , formData ) {
return keys . reduce ( ( acc , key ) => {
acc [ key ] = formData [ key ] ;
return acc ;
} , { } ) ;
}
2022-10-27 12:59:19 +00:00
async function save ( ) {
2024-05-22 09:22:46 +00:00
if ( $props . observeFormChanges && ! hasChanges . value )
return notify ( 'globals.noChanges' , 'negative' ) ;
2023-11-30 20:46:07 +00:00
2024-05-22 09:22:46 +00:00
isLoading . value = true ;
2023-11-30 20:46:07 +00:00
try {
2024-08-16 10:29:35 +00:00
formData . value = trimData ( formData . value ) ;
2024-11-22 09:04:17 +00:00
const { method , body , url } = handleRequestArgs ( ) ;
2024-11-22 09:48:03 +00:00
// Obtener las claves del objeto original
const originalKeys = Object . keys ( body ) ;
// Construir el objeto con valores actualizados
const updatedValues = getUpdatedValues ( originalKeys , formData . value ) ;
2024-11-21 19:14:54 +00:00
2024-01-22 16:15:51 +00:00
let response ;
2024-05-22 09:22:46 +00:00
2024-11-22 09:48:03 +00:00
if ( $props . saveFn ) response = await $props . saveFn ( updatedValues ) ;
else response = await axios [ method ] ( url , updatedValues ) ;
2024-05-22 09:22:46 +00:00
2024-02-12 14:06:20 +00:00
if ( $props . urlCreate ) notify ( 'globals.dataCreated' , 'positive' ) ;
2024-06-06 07:53:48 +00:00
updateAndEmit ( 'onDataSaved' , formData . value , response ? . data ) ;
2024-06-20 08:26:22 +00:00
if ( $props . reload ) await arrayData . fetch ( { } ) ;
2024-06-27 11:56:20 +00:00
hasChanges . value = false ;
2023-11-30 20:46:07 +00:00
} catch ( err ) {
2024-02-12 14:06:20 +00:00
console . error ( err ) ;
2024-02-27 07:22:38 +00:00
notify ( 'errors.writeRequest' , 'negative' ) ;
2024-06-06 07:33:01 +00:00
} finally {
isLoading . value = false ;
2023-11-30 20:46:07 +00:00
}
2022-10-27 12:59:19 +00:00
}
2024-03-06 15:23:19 +00:00
async function saveAndGo ( ) {
2024-05-23 14:48:50 +00:00
await save ( ) ;
2024-03-06 15:23:19 +00:00
push ( { path : $props . goTo } ) ;
}
2023-08-24 13:05:31 +00:00
2022-10-27 12:59:19 +00:00
function reset ( ) {
2024-06-06 10:14:00 +00:00
updateAndEmit ( 'onFetch' , originalData . value ) ;
2023-12-07 17:33:11 +00:00
if ( $props . observeFormChanges ) {
hasChanges . value = false ;
2023-12-11 14:47:16 +00:00
isResetting . value = true ;
2023-12-07 17:33:11 +00:00
}
2022-10-27 12:59:19 +00:00
}
2023-11-30 11:47:40 +00:00
2023-06-01 07:09:54 +00:00
// eslint-disable-next-line vue/no-dupe-keys
2022-10-31 08:34:01 +00:00
function filter ( value , update , filterOptions ) {
update (
( ) => {
const { options , filterFn } = filterOptions ;
options . value = filterFn ( options , value ) ;
} ,
( ref ) => {
ref . setOptionIndex ( - 1 ) ;
ref . moveOptionSelection ( 1 , true ) ;
2024-07-12 11:34:31 +00:00
}
2022-10-31 08:34:01 +00:00
) ;
}
2023-01-26 13:29:01 +00:00
2024-06-06 07:53:48 +00:00
function updateAndEmit ( evt , val , res ) {
2024-06-11 12:09:42 +00:00
state . set ( modelValue , val ) ;
2024-05-22 09:22:46 +00:00
originalData . value = val && JSON . parse ( JSON . stringify ( val ) ) ;
if ( ! $props . url ) arrayData . store . data = val ;
2024-03-08 18:05:11 +00:00
2024-06-11 12:09:42 +00:00
emit ( evt , state . get ( modelValue ) , res ) ;
2024-05-22 09:22:46 +00:00
}
2024-03-08 18:05:11 +00:00
2024-08-16 10:29:35 +00:00
function trimData ( data ) {
if ( ! $props . defaultTrim ) return data ;
for ( const key in data ) {
if ( typeof data [ key ] == 'string' ) data [ key ] = data [ key ] . trim ( ) ;
}
return data ;
}
2024-03-08 18:05:11 +00:00
defineExpose ( {
save ,
isLoading ,
2024-03-28 16:45:35 +00:00
hasChanges ,
2024-06-03 11:47:32 +00:00
reset ,
2024-06-04 12:25:03 +00:00
fetch ,
2024-03-08 18:05:11 +00:00
} ) ;
2022-10-27 12:59:19 +00:00
< / script >
< template >
2023-12-26 19:15:46 +00:00
< div class = "column items-center full-width" >
2024-11-22 09:48:03 +00:00
{ { changes } }
2023-10-25 16:55:20 +00:00
< QForm
2024-06-21 11:09:34 +00:00
ref = "myForm"
2023-10-25 16:55:20 +00:00
v - if = "formData"
@ submit = "save"
@ reset = "reset"
class = "q-pa-md"
id = "formModel"
>
< QCard >
< slot
2024-06-27 11:16:42 +00:00
v - if = "formData"
2023-10-25 16:55:20 +00:00
name = "form"
: data = "formData"
: validate = "validate"
: filter = "filter"
/ >
2024-07-12 11:34:31 +00:00
< SkeletonForm v -else / >
2023-10-25 16:55:20 +00:00
< / QCard >
< / QForm >
< / div >
2024-02-02 15:45:29 +00:00
< Teleport
to = "#st-actions"
2024-07-12 11:34:31 +00:00
v - if = "
$props . defaultActions &&
stateStore ? . isSubToolbarShown ( ) &&
componentIsRendered
"
2024-02-02 15:45:29 +00:00
>
2024-07-12 11:34:31 +00:00
< QBtnGroup push class = "q-gutter-x-sm" >
< slot name = "moreActions" / >
< QBtn
: label = "tMobile(defaultButtons.reset.label)"
: color = "defaultButtons.reset.color"
: icon = "defaultButtons.reset.icon"
flat
2024-07-16 07:26:22 +00:00
@ click = "defaultButtons.reset.click"
2024-07-12 11:34:31 +00:00
: disable = "!hasChanges"
: title = "t(defaultButtons.reset.label)"
/ >
< QBtnDropdown
v - if = "$props.goTo"
@ click = "saveAndGo"
: label = "tMobile('globals.saveAndContinue')"
: title = "t('globals.saveAndContinue')"
: disable = "!hasChanges"
color = "primary"
icon = "save"
split
>
< QList >
< QItem
clickable
v - close - popup
@ click = "save"
: title = "t('globals.save')"
>
< QItemSection >
< QItemLabel >
< QIcon
name = "save"
color = "white"
class = "q-mr-sm"
size = "sm"
/ >
{ { t ( 'globals.save' ) . toUpperCase ( ) } }
< / QItemLabel >
< / QItemSection >
< / QItem >
< / QList >
< / QBtnDropdown >
< QBtn
v - else
: label = "tMobile('globals.save')"
color = "primary"
icon = "save"
2024-07-16 07:26:22 +00:00
@ click = "defaultButtons.save.click"
2024-07-12 11:34:31 +00:00
: disable = "!hasChanges"
: title = "t(defaultButtons.save.label)"
/ >
< / QBtnGroup >
2023-08-07 10:00:42 +00:00
< / Teleport >
2024-07-12 11:34:31 +00:00
2023-04-11 11:31:03 +00:00
< QInnerLoading
: showing = "isLoading"
: label = "t('globals.pleaseWait')"
color = "primary"
2023-12-19 15:10:58 +00:00
style = "min-width: 100%"
2023-04-11 11:31:03 +00:00
/ >
2022-10-27 12:59:19 +00:00
< / template >
2023-10-25 16:45:29 +00:00
< style lang = "scss" scoped >
2024-04-02 08:46:27 +00:00
. q - notifications {
color : black ;
}
2023-10-25 16:45:29 +00:00
# formModel {
2023-09-19 07:42:47 +00:00
max - width : 800 px ;
width : 100 % ;
}
2023-11-30 11:47:40 +00:00
2023-10-25 16:45:29 +00:00
. q - card {
padding : 32 px ;
}
2023-09-19 07:42:47 +00:00
< / style >