https://foro.gambas-es.org/viewtopic.php?f=3&t=7603&p=45324#p45324
-----------------------------------
shordi
Viernes, 26 Julio 2019, 10:15

Re: Sobre Llevar Un Historial De Los Movimientos De Usuarios De Un Sistema
-----------------------------------
Bueno... lo que pides no es difícil, pero tiene implicación en muchas partes de tu sistema.
El sistema tiene dos partes: Por un lado la grabación y firmado de cada registro/consulta que se haga a la/s BBDD por otro la creación de un histórico de acciones de usuario y por último un sistema de consultas de todo eso.
La primera parte  más o menos es así:
A.- FIRMAR TODAS LAS OPERACIONES Y MOSTRAR LA FIRMA
A.1.- Añade a TODAS las tablas de tu sistema dos campos al final 

     usr varchar(50)
     fmod datetime

Estos campos sirven de firma para saber quién ha hecho la última modificación de cada registro. usr contendrá el "nick" de cada usuario (mejor no poner en él su código para mejorar la legibilidad). fmod contendrá la fecha y hora de la actualización.

A.2.- Para hacer sencillo el "firmado de los registros", en la función que utilizes para modificar la BBDD (yo uso un módulo para altas bajas y modificaciones), añade el firmado con el nick de usuario y la fecha y hora. Algo así (extraído de cómo lo hago yo en mi módulo):
[code linenumbers=false syntax=gambas] 
...
...
rs = laConexion.edit(tabla, campoClave & "= &1", clave)
    rs = Control2Result(oContenedor, rs) 'esta función rellena el result a partir del form o panel que contenga los controles 
    Try rs["usr"] = mComun.usuario.nick  'el nick del usuario. Le pongo el try por si la tabla no tuviese dicho campo 
    Try rs["fmod"] = Now 'la fecha y la hora. El try se pone por lo mismo de arriba
    Try rs.Update
...
...
[/code]

Con esto ya tienes seguimiento de quién ha hecho la última acción sobre cada registro.

A.3.- A TODOS los formularios de datos, añade un textbox llamado firma en la parte de abajo. A la hora de rellenar cada formulario con los datos existentes, añade ésto a la función que recupere los datos de la bbdd
[code linenumbers=false syntax=gambas] 
...
...
'Este es parte del bucle con el que yo relleno los controles del form desde los campos del result (que se llaman igual que los controles).
'Tú sustituye ésto por la función que utilices para rellenar el result por eso te los pongo comentados
'For Each f In rs.Fields
'            campo =  f.Name  'con ésto le quitamos los alias de a., b., etc. a los campos.
'            Try o = oContenedor.controls[campo]
'            If Not Error And Not IsNull(o) Then
'                If o Is Textedit Then
'                    o.richText = rs[o.name]
'                Else
'                    Try o.Value = rs[f.Name]
'                    If Error Then o.Text = rs[f.Name]
'                Endif
'            Endif
'        Next
        Try oContenedor.controls["firma"].text = "Modificado por " & rs!usr & " el " & Format(rs!fmod, "dd-mm-yyyy") 'aquí muestras la firma en pantalla. Le pongo el Try por si el form no contuviese el campo firma
....
....
[/code]

B) CREAR UN REGISTRO HISTÓRICO DE OPERACIONES SOBRE LA BBDD

B.1.- Creación de la tabla del histódiro.

Crea una tabla con éste diseño (Este es para mysql, puedes adaptarlo a la BBDD que uses:
[code linenumbers=false syntax=gambas]CREATE TABLE `ad_hlog` (
 `idhlog` int(8) unsigned NOT NULL AUTO_INCREMENT,
 `tipo` varchar(100) DEFAULT NULL,
 `detalle` mediumtext,
 `usr` varchar(20) DEFAULT NULL,
 `fmod` datetime DEFAULT NULL,
 KEY `id` (`idhlog`)
) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=utf8

[/code]

[b]tipo [/b]es una cadena que identifica la operación, por ejemplo: "Actualiza Personas", "Añade Registro en Personas", "Elimina Personas", donde Personas es, claro, una tabla existente.
[b]detalle[/b] es el contenido de la operación, ahora hablamos de ello.
[b]usr y fmod[/b] ya sabemos lo que contienen

B.2.- Función de actualización del histórico. 

En el módulo de actualización de la BBDD, añade ésta función:
[code linenumbers=false syntax=gambas]Public Sub hlog(tipo As String, detalle As String)

    detalle = Replace(detalle, "'", "\\'") 'escapamos toda comilla que pueda haber 
    detalle = Replace(detalle, "\"", "\\\"")
    detalle = "insert into ad_hlog (tipo, detalle, usr,fmod) values ('" & String.Mid(tipo, 1) & "', '" & String.Mid(detalle, 1) & "', '" & String.Mid(mComun.usuario.nick, 1) & "', " & "now()" & ")"
    Connections[Application.name].exec(detalle)

Catch
    'Enviamos un email a los administradores del programa indicando el error producido
     mEMail.sendmail(mComun.entorno["mailprograma"], [mComun.entorno["mailerrores"]], "Error en " & Application.Name & ", "Usuario: " & mComun.usuario.nick & "\nFecha: " & Format(Now(), "dd-mm-yyyy hh:mm:ss") & "\n" & Error.Text & " - " & Error.where, mComun.servidores["mailserver"])

End[/code]
Fíjate que sólo grabamos cuatro campos, el tipo, el detalle y la firma del propio histórico usr y fmod

B.3.- Rellenado de la tabla del histórico:

En tu función de actualización de la BBDD (la misma en la que grababas la firma en el punto A.2) añade el siguiente código antes del update:
[code linenumbers=false syntax=gambas]
 '  dim cad as string 
 '  Try rs["usr"] = mComun.usuario.nick  'el nick del usuario. Le pongo el try por si la tabla no tuviese dicho campo 
 '   Try rs["fmod"] = Now 'la fecha y la hora. El try se pone por lo mismo de arriba
     For Each f In rs.Fields
        Try cad &= f.name & ": " & rs[f.name] & "\n" 'try porque si el campo es mediumtext y está vacío, casca
    Next
 '  Try rs.Update
[/code]
Y luego llama a la función de actualización del histórico con ésta línea:
[code linenumbers=false syntax=gambas]mComun.hlog("Añade en " & tabla, cad)[/code]
donde tabla contiene el nombre de la tabla y cad, como ves más arriba, el nombre de los campos y el contenido de cada uno que se graban en la BBDD
Con ésto ya tienes todo lo que quieres saber grabado en el histórico de movimientos en la bbdd.
Te adjunto aquí mismo el módulo entero de actualización de la BBDD incluyendo todo lo que arriba te pongo (casi) tal y como lo usamos en la empresa.

[code linenumbers=false syntax=gambas]
' Gambas module file

Public Function grabaRegistro(tabla As String, campoClave As String, clave As Variant, oContenedor As Object, Optional laConexion As Connection) As Boolean
    'NOTE: la funcion grabaRegistro se utiliza para grabar un determinado valor de la base de datos por ejemplo a la hora de actualizar la clave de un usuario
    ' devuelve -1 si ha fallado la actualización y 0 si ha sido exitosa

    Dim rs As Result
    Dim lret As Integer
    Dim cad As String
    Dim f As ResultField

    If Not laConexion Then
        laConexion = Connections[Application.name]
    Endif
    If Not laConexion.Opened Then laConexion.open
    rs = laConexion.edit(tabla, campoClave & "= &1", clave) ' utiliza la funcion edit de gambas para grabar los datos en vez de utilizar sentencias select
    rs = Control2Result(oContenedor, rs)
    Try rs["usr"] = mComun.usuario.nick
    Try rs["fmod"] = Now
    Try rs.Update
    If Error Then
        mEMail.sendmail(mComun.entorno["mailprograma"], [mComun.entorno["mailerrores"]], "Error en " & Application.Name & ", "Usuario: " & mComun.usuario.nick & "\nFecha: " & Format(Now(), "dd-mm-yyyy hh:mm:ss") & "\n" & Error.Text & " - " & Error.where, mComun.servidores["mailserver"])
        lret = -1
    Else
        lret = 0
        For Each f In rs.Fields
            Try cad &= f.Name & ": " & rs[f.Name] & "\n" 'try porque si el campo es mediumtext y está vacío, casca
        Next
        mComun.hlog("Modifica " & tabla, cad)
    End If
    Return lret

End

Public Function creaRegistro(tabla As String, oContenedor As Object, Optional conec As Connection, Optional verClave As Boolean) As Variant

    ' devuelve -1 si ha fallado la adición. Si verClave=true se devuelve el id del registro añadido. Si verClave=false, se devuelve 0
    Dim rs, r As Result
    Dim lret As Integer
    Dim f As ResultField
    Dim cad, clave As String

    If Not conec Then conec = Connections[Application.name]
    If Not mComun.abreConexion(conec.name) Then Return False
    rs = conec.create(tabla)
    rs = Control2Result(oContenedor, rs)
    Try rs["usr"] = mComun.usuario.nick
    Try rs["fmod"] = Now
    For Each f In rs.Fields
        Try cad &= f.name & ": " & rs[f.name] & "\n" 'try porque si el campo es mediumtext y está vacío, casca
    Next
    Try rs.Update
    If Error Then
        mEMail.sendmail(mComun.entorno["mailprograma"], [mComun.entorno["mailerrores"]], "Error en " & Application.Name & ", "Usuario: " & mComun.usuario.nick & "\nFecha: " & Format(Now(), "dd-mm-yyyy hh:mm:ss") & "\n" & Error.Text & " - " & Error.where, mComun.servidores["mailserver"])
        lret = -1
    Else
        If verClave Then
            clave = conec.Tables[tabla].primaryKey[0]
            r = conec.Exec("select Last_Insert_id() as '" & clave & "' from `" & tabla & "`")
            lret = r[clave]
        Else
            lret = 0
        Endif
        mComun.hlog("Añade en " & tabla, cad)
    Endif
    Return lret

End

Public Function borraRegistro(tabla As String, campoClave As String, clave As Variant, Optional hcon As Connection) As Boolean

    Dim r As Result
    Dim f As ResultField
    Dim cad As String

    If Not hcon Then hcon = Connections[Application.name]
    If Not mComun.abreConexion() Then Return False
    r = hcon.exec(Subst("SELECT * FROM " & tabla & " where `" & campoClave & "` = &1", clave))
    If r.Available Then
        For Each f In r.Fields
            Try cad &= f.Name & ": " & f[f.Name] & "\n" 'try porque si el campo es mediumtext y está vacío, casca
        Next
    Endif
    Try hcon.delete(tabla, campoclave & "=&1", clave)
    If Error Then
        mEMail.sendmail(mComun.entorno["mailprograma"], [mComun.entorno["mailerrores"]], "Error en " & Application.Name & ", "Usuario: " & mComun.usuario.nick & "\nFecha: " & Format(Now(), "dd-mm-yyyy hh:mm:ss") & "\n" & Error.Text & " - " & Error.where, mComun.servidores["mailserver"])
        Return False
    Else
        mComun.hlog("Borra de " & tabla, cad)
        Return True
    Endif

End

Public Sub limpiaCampos(oContenedor As Object)

    Dim o As Object
    Dim n As Integer

    For Each o In oContenedor.Children
        If o Is TabStrip Or o Is IconPanel Or o Is TabPanel Or o Is ToolPanel Then
            For n = 0 To o.Count - 1
                o.index = n
                limpiaCampos(o) 'Recursividad
            Next
            o.index = 0
        Endif
        If o Is Container Then
            limpiaCampos(o) 'Recursividad
        Endif
        'Especificamos los tipos para que no limpie también los labels y demás.
        If o Is ValueBox Or If o Is CheckBox Or If o Is RadioButton Or If o Is Color Or If o Is Button Or If o Is DirBox Or If o Is SwitchButton Or If o Is SpinBox Then
            Try o.Value = 0
        Else If o Is TextArea Or If o Is TextBox Or If o Is ListBox Or If o Is ComboBox Or If o Is DbListBox Or If o Is DbComboBox Or If o Is TextEditor Or If o Is TextEdit Then
            Try o.Text = ""
        Else If o Is HtEdit Then
            Try o.html = ""
        Endif
    Next

End

Public Function Control2Result(oContenedor As Object, rs As Result) As Result ' este procedimiento se utiliza desde el formulario perfil para cargar los controles o listados en tiempo de ejecucion.
    ', Optional campoclave As String) As Result

    Dim o As Object
    Dim f As ResultField
    Dim n As Integer
    Dim campo As String

    If oContenedor Is Form Then 'Esta debería ser la opción preferida, por velocidad.
        For Each f In rs.Fields
            campo = IIf(Mid(f.name, 2, 1) = ".", Mid(f.name, 3), f.Name)
            Try o = oContenedor.controls[campo]
            If Not Error And Not IsNull(o) Then
                If o Is Textedit Then
                    rs[o.name] = o.richText
                Else
                    Try rs[o.Name] = o.Value
                    If Error Then rs[o.name] = o.Text
                Endif
            Endif
        Next
    Else
        For Each o In oContenedor.Children
            If o Is TabStrip Or o Is IconPanel Or o Is TabPanel Or o Is ToolPanel Then
                For n = 0 To o.Count - 1
                    o.index = n
                    Control2Result(o, rs) 'Recursividad
                Next
                o.index = 0
            Endif
            If o Is Container Then
                Control2Result(o, rs) 'Recursividad
            Endif
            For n = 0 To rs.Fields.count - 1
                campo = IIf(Mid(rs.Fields[n].name, 2, 1) = ".", Mid(rs.Fields[n].name, 3), rs.Fields[n].Name)
                If o.name = campo Then
                    If o Is Textedit Then
                        rs[o.name] = o.richText
                    Else
                        Try rs[o.Name] = o.Value
                        If Error Then rs[o.name] = o.Text
                        Break
                    Endif
                Endif
            Next
        Next
    Endif

Finally

    Return rs

Catch

    mEMail.sendmail(mComun.entorno["mailprograma"], [mComun.entorno["mailerrores"]], "Error en " & Application.Name & ", "Usuario: " & mComun.usuario.nick & "\nFecha: " & Format(Now(), "dd-mm-yyyy hh:mm:ss") & "\n" & Error.Text & " - " & Error.where, mComun.servidores["mailserver"])

End

Public Sub Result2Control(rs As Result, oContenedor As Object)

    Dim o As Object
    Dim f As ResultField
    Dim campo As String
    Dim n As Integer

    If oContenedor Is Form Then 'Esta debería ser la opción preferida por velocidad
        For Each f In rs.Fields
            campo = IIf(Mid(f.name, 2, 1) = ".", Mid(f.name, 3), f.Name) 'con ésto le quitamos los alias de a., b., etc. a los campos.
            Try o = oContenedor.controls[campo]
            If Not Error And Not IsNull(o) Then
                If o Is Textedit Then
                    o.richText = rs[o.name]
                Else
                    Try o.Value = rs[f.Name]
                    If Error Then o.Text = rs[f.Name]
                Endif
            Endif
        Next
        Try oContenedor.controls["firma"].text = "Modificado por " & rs!usr & " el " & Format(rs!fmod, "dd-mm-yyyy")
    Else
        For Each o In oContenedor.Children
            If Left(o.name, 5) = "firma" Then o.text = "Modificado por " & rs!usr & " el " & Format(rs!fmod, "dd-mm-yyyy")
            If o Is TabStrip Or o Is IconPanel Or o Is TabPanel Or o Is ToolPanel Then
                For n = 0 To o.Count - 1
                    o.index = n
                    Result2Control(rs, o) 'Recursividad
                Next
                o.index = 0
            Endif
            If o Is Container Then
                Result2Control(rs, o) 'Recursividad
            Endif
            For Each f In rs.Fields
                campo = IIf(Mid(f.name, 2, 1) = ".", Mid(f.name, 3), f.Name)
                For n = 0 To oContenedor.children.count - 1
                    If oContenedor.children[n].name = campo Then
                        o = oContenedor.children[n]
                        If o Is TextEdit Then
                            o.richtext = rs[f.name]
                        Else
                            Try o.Value = rs[f.Name]
                            If Error Then o.Text = rs[f.Name]
                            Break
                        Endif
                    Endif
                Next
            Next
        Next

    Endif

Catch

    mEMail.sendmail(mComun.entorno["mailprograma"], [mComun.entorno["mailerrores"]], "Error en " & Application.Name & ", "Usuario: " & mComun.usuario.nick & "\nFecha: " & Format(Now(), "dd-mm-yyyy hh:mm:ss") & "\n" & Error.Text & " - " & Error.where, mComun.servidores["mailserver"])

End

Public Sub hlog(tipo As String, detalle As String)

    detalle = Replace(detalle, "'", "\\'")
    detalle = Replace(detalle, "\"", "\\\"")
    detalle = "insert into ad_hlog (tipo, detalle, usr,fmod) values ('" & String.Mid(tipo, 1) & "', '" & String.Mid(detalle, 1) & "', '" & String.Mid(mComun.usuario.nick, 1) & "', " & "now()" & ")"
    Connections[Application.name].exec(detalle)

Catch
    mEMail.sendmail(mComun.entorno["mailprograma"], [mComun.entorno["mailerrores"]], "Error en " & Application.Name & ", "Usuario: " & mComun.usuario.nick & "\nFecha: " & Format(Now(), "dd-mm-yyyy hh:mm:ss") & "\n" & Error.Text & " - " & Error.where, mComun.servidores["mailserver"])

End
[/code]

Sólo te falta la manera de consultar y manejar toda esa información... pero ahora que veo todo lo que te he puesto, me parece todo un poco confuso. 

Dame un poco de tiempo y te hago un ejemplo funcional en Sqlite.

Saludos.


