Avatar of mlagrange
mlagrange
Flag for United States of America asked on

Looking for methods to log user time while they have a given record open in a form

Hello - I'm looking for examples of easy, unobtrusive ways for users to track the time the spend as they pull up records in an Access application, record by record.

Take for instance, in a job tracking app, where for a given job, steps are assigned to various users, and those users open job step records assigned to them in a detail form.

I was thinking to provide Start/Pause/Stop buttons on the job step detail form, that would log the start, stop, and elapsed times to a log file, including the user ID and the primary key of the Job step .

And then I thought, "I bet somebody has already done something like this..."   :-)

Anybody done this, or heard of something similar?

Thanks
Microsoft Access

Avatar of undefined
Last Comment
Nick67

8/22/2022 - Mon
ASKER CERTIFIED SOLUTION
aikimark

THIS SOLUTION ONLY AVAILABLE TO MEMBERS.
View this solution by signing up for a free trial.
Members can start a 7-Day free trial and enjoy unlimited access to the platform.
See Pricing Options
Start Free Trial
GET A PERSONALIZED SOLUTION
Ask your own question & get feedback from real experts
Find out why thousands trust the EE community with their toughest problems.
mlagrange

ASKER
ok, I've slept on it, and realized I don't have to have Start/Pause/Stop buttons on forms, I just need to log Now() on the OnOpen, OnCurrent, and OnClose events (or aikimark's better idea of defaulting a DateTime field), with whatever info I need on the given form. It carries the client ID; purpose can be inferred from the form name, and get the user ID from Environ().

Thanks, aikimark
Nick67

I am looking to do something similar here
https://www.experts-exchange.com/questions/28918627/Fix-the-dog-screwers-wagons-but-good.html
I have the wrinkle that most of the data entry stuff is done by entries in unbound controls and pounded into tables with VBA, so things aren't quite so simple.
mlagrange

ASKER
Thanks, Nick67 - good discussion, great title :-D
All of life is about relationships, and EE has made a viirtual community a real community. It lifts everyone's boat
William Peck
Nick67

:) Thanks

I am working on it.
It's complex for me.
There are a LOT of forms -- 172 at last count, including forms and subforms.
There's one linchpin ID to each 'job'
What I am looking to accomplish is to be able to sum all the time expended entering each 'job'
The unbound elements make things more difficult, since there's no current event to hang a hat on
I have the ReturnUserName and ReturnComputerName code from the AccessWeb site going

Option Compare Database
Option Explicit

Private Type USER_INFO_2
    usri2_name As Long
    usri2_password  As Long  ' Null, only settable
    usri2_password_age  As Long
    usri2_priv  As Long
    usri2_home_dir  As Long
    usri2_comment  As Long
    usri2_flags  As Long
    usri2_script_path  As Long
    usri2_auth_flags  As Long
    usri2_full_name As Long
    usri2_usr_comment  As Long
    usri2_parms  As Long
    usri2_workstations  As Long
    usri2_last_logon  As Long
    usri2_last_logoff  As Long
    usri2_acct_expires  As Long
    usri2_max_storage  As Long
    usri2_units_per_week  As Long
    usri2_logon_hours  As Long
    usri2_bad_pw_count  As Long
    usri2_num_logons  As Long
    usri2_logon_server  As Long
    usri2_country_code  As Long
    usri2_code_page  As Long
End Type

Public Declare Function GetUserName Lib "advapi32.dll" _
Alias "GetUserNameA" (ByVal lpBuffer As String, nSize As Long) As Long


Private Declare Function GetComputerName Lib "kernel32" _
Alias "GetComputerNameA" _
(ByVal lpBuffer As String, nSize As Long) As Long

 
Private Declare Function apiNetGetDCName _
    Lib "netapi32.dll" Alias "NetGetDCName" _
    (ByVal servername As Long, _
    ByVal DomainName As Long, _
    bufptr As Long) As Long
 
' function frees the memory that the NetApiBufferAllocate
' function allocates.
Private Declare Function apiNetAPIBufferFree _
    Lib "netapi32.dll" Alias "NetApiBufferFree" _
    (ByVal buffer As Long) _
    As Long
 
' Retrieves the length of the specified wide string.
Private Declare Function apilstrlenW _
    Lib "kernel32" Alias "lstrlenW" _
    (ByVal lpString As Long) _
    As Long
 
Private Declare Function apiNetUserGetInfo _
    Lib "netapi32.dll" Alias "NetUserGetInfo" _
    (servername As Any, _
    username As Any, _
    ByVal Level As Long, _
    bufptr As Long) As Long
 
' moves memory either forward or backward, aligned or unaligned,
' in 4-byte blocks, followed by any remaining bytes
Private Declare Sub sapiCopyMem _
    Lib "kernel32" Alias "RtlMoveMemory" _
    (Destination As Any, _
    Source As Any, _
    ByVal Length As Long)
 
Private Declare Function apiGetUserName Lib _
    "advapi32.dll" Alias "GetUserNameA" _
    (ByVal lpBuffer As String, _
    nSize As Long) _
    As Long
 
Private Const MAXCOMMENTSZ = 256
Private Const NERR_SUCCESS = 0
Private Const ERROR_MORE_DATA = 234&
Private Const MAX_CHUNK = 25
Private Const ERROR_SUCCESS = 0&


'******** Code Start ********
'This code was originally written by Dev Ashish.
'It is not to be altered or distributed,
'except as part of an application.
'You are free to use it in any application,
'provided the copyright notice is left unchanged.
'
'Code Courtesy of
'Dev Ashish
'
 
Function fGetFullNameOfLoggedUser(Optional strUserName As String) As String
'
' Returns the full name for a given UserID
'   NT/2000 only
' Omitting the strUserName argument will try and
' retrieve the full name for the currently logged on user
'
On Error GoTo ErrHandler
Dim pBuf As Long
Dim dwRec As Long
Dim pTmp As USER_INFO_2
Dim abytPDCName() As Byte
Dim abytUserName() As Byte
Dim lngRet As Long
Dim i As Long
 
    ' Unicode
    abytPDCName = fGetDCName() & vbNullChar
    If (Len(strUserName) = 0) Then strUserName = fGetUserName()
    abytUserName = strUserName & vbNullChar
 
    ' Level 2
    lngRet = apiNetUserGetInfo( _
                            abytPDCName(0), _
                            abytUserName(0), _
                            2, _
                            pBuf)
    If (lngRet = ERROR_SUCCESS) Then
        Call sapiCopyMem(pTmp, ByVal pBuf, Len(pTmp))
        fGetFullNameOfLoggedUser = fStrFromPtrW(pTmp.usri2_full_name)
    End If
 
    Call apiNetAPIBufferFree(pBuf)
ExitHere:
    Exit Function
ErrHandler:
    fGetFullNameOfLoggedUser = vbNullString
    Resume ExitHere
End Function
 
Private Function fGetUserName() As String
' Returns the network login name
Dim lngLen As Long, lngRet As Long
Dim strUserName As String
    strUserName = String$(254, 0)
    lngLen = 255
    lngRet = apiGetUserName(strUserName, lngLen)
    If lngRet Then
        fGetUserName = Left$(strUserName, lngLen - 1)
    End If
End Function
 
Function fGetDCName() As String
Dim pTmp As Long
Dim lngRet As Long
Dim abytBuf() As Byte
 
    lngRet = apiNetGetDCName(0, 0, pTmp)
    If lngRet = NERR_SUCCESS Then
        fGetDCName = fStrFromPtrW(pTmp)
    End If
    Call apiNetAPIBufferFree(pTmp)
End Function
 
Private Function fStrFromPtrW(pBuf As Long) As String
Dim lngLen As Long
Dim abytBuf() As Byte
 
    ' Get the length of the string at the memory location
    lngLen = apilstrlenW(pBuf) * 2
    ' if it's not a ZLS
    If lngLen Then
        ReDim abytBuf(lngLen)
        ' then copy the memory contents
        ' into a temp buffer
        Call sapiCopyMem( _
                abytBuf(0), _
                ByVal pBuf, _
                lngLen)
        ' return the buffer
        fStrFromPtrW = abytBuf
    End If
End Function


Function ReturnUserName() As String
' returns the NT Domain User Name
Dim rString As String * 255, sLen As Long, tString As String
    tString = ""
    On Error Resume Next
    sLen = GetUserName(rString, 255)
    sLen = InStr(1, rString, Chr(0))
    If sLen > 0 Then
        tString = Left(rString, sLen - 1)
    Else
        tString = rString
    End If
    On Error GoTo 0
    ReturnUserName = UCase(Trim(tString))
End Function

 
Function ReturnComputerName() As String
    Dim rString As String * 255, sLen As Long, tString As String
    tString = ""
    On Error Resume Next
    sLen = GetComputerName(rString, 255)
    sLen = InStr(1, rString, Chr(0))
    If sLen > 0 Then
        tString = Left(rString, sLen - 1)
    Else
        tString = rString
    End If
    On Error GoTo 0
    ReturnComputerName = UCase(Trim(tString))
End Function

Open in new window


And a table, tblUsage, with the stuff I want to track
DurationID Autonumber
ReturnUserName Text
ReturnComputerName Text
JobID  Long
TheFormName Text
StartTime Date
CompletedTime Date

And a public function in a code module to pound data into the table
Public Function RecordUsage(ReturnUserName As String, ReturnComputerName As String, _
JobID As Long, TheFormName As Long, StartTime As Date, CompletedTime As Date) As Boolean
Dim db As Database
Dim rs As Recordset

Set db = CurrentDb
Set rs = db.OpenRecordset("Select * from tblUsage where 1=2", dbOpenDynaset, dbSeeChanges)
With rs
    .AddNew
    !ReturnUserName = ReturnUserName
    !ReturnComputerName = ReturnComputerName
    !JobID = JobID
    !formname = TheFormName
    !StartTime = StartTime
    !CompletedTime = CompletedTime
    .Update
End With
rs.Close
Set rs = Nothing
End Function

Open in new window


Next comes the work of deciding how and when to call the code on all the forms to capture the time spent.
Then comes what will be arguably harder -- building the reporting.
JobID is a foreign key in most of the 162 tables (SQL server backend -- Access tops out at 32 foreign key relationships).
And anyone of 30-some users on 30-some machines COULD be involved in creating/editing a job
So how do you get the right conciseness?
And a report that doesn't bring the system to it's knees when someone at the top of the food chain wants to ask "How much time was actually spent entering stuff in the computer this year?"

That's a bit further down the road yet!