Option Compare Database
Option Explicit
' ===========================================================
' MÓDULO PERRÓN PARA CRUCE LLAMADAS ↔ CLASIFICACIONES
' - No crea tablas ni columnas; tú las defines una vez:
' * tblCruce_Final (campos de L + campos de C)
' * tblSinLlamada_Final (campos de C)
' * tblSinClasif_Final (campos de L)
' - Limpia salidas al arrancar; luego inserta resultados.
' - Lee nombres de campos automáticamente (DAO.TableDefs).
' - Regla: No debe haber nombres de campo iguales entre L y C.
' - Procesa por día calendario (DateValue), por rango o todo.
' ===========================================================
'========================
' Utilidades de esquema
'========================
' Devuelve los nombres de campos de una tabla como Collection.
Private Function GetFieldNames(ByVal tableName As String) As Collection
Dim c As New Collection, f As DAO.Field
For Each f In CurrentDb.TableDefs(tableName).Fields
c.Add f.Name
Next f
Set GetFieldNames = c
End Function
' ¿Existe un nombre en la Collection (case-insensitive)?
Private Function InCollection(ByVal col As Collection, ByVal nameText As String) As Boolean
Dim v As Variant
For Each v In col
If StrComp(CStr(v), nameText, vbTextCompare) = 0 Then
InCollection = True
Exit Function
End If
Next v
InCollection = False
End Function
' Une columnas con comas, con o sin alias y con corchetes.
' - cols: Collection de nombres de columna
' - prefix: ej. "L." o "C." para armar SELECT L.[col]
' - bracket: True → usa [col]; False → sin corchete
Private Function JoinCols(ByVal cols As Collection, Optional ByVal prefix As String = "", Optional ByVal bracket As Boolean = True) As String
Dim parts() As String, i As Long, v As Variant, token As String
ReDim parts(0 To cols.Count - 1)
i = 0
For Each v In cols
token = IIf(bracket, "[" & CStr(v) & "]", CStr(v))
If Len(prefix) > 0 Then token = prefix & token
parts(i) = token
i = i + 1
Next v
JoinCols = Join(parts, ", ")
End Function
'========================
' Lectura de configuración
'========================
' Lee nombres de tablas/campos desde tblConfig.
' Requiere:
' - TablaLlamadas, CampoIdUsuario1, CampoInicio, CampoFin
' - TablaClasificador, CampoIdUsuario2, CampoFechaRegistro
Private Sub LeerConfig( _
ByRef tL As String, ByRef idL As String, ByRef iniL As String, ByRef finL As String, _
ByRef tC As String, ByRef idC As String, ByRef tsC As String)
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset("SELECT * FROM tblConfig")
If rs.EOF Then Err.Raise vbObjectError + 9001, "LeerConfig", "tblConfig no tiene filas."
tL = Nz(rs!TablaLlamadas, "")
idL = Nz(rs!CampoIdUsuario1, "")
iniL = Nz(rs!CampoInicio, "")
finL = Nz(rs!CampoFin, "")
tC = Nz(rs!TablaClasificador, "")
idC = Nz(rs!CampoIdUsuario2, "")
tsC = Nz(rs!CampoFechaRegistro, "")
rs.Close
If tL = "" Or idL = "" Or iniL = "" Or finL = "" Or tC = "" Or idC = "" Or tsC = "" Then
Err.Raise vbObjectError + 9002, "LeerConfig", "Faltan campos en tblConfig."
End If
End Sub
'========================
' Mapeo dinámico de columnas
'========================
' Arma el mapeo de columnas desde Origen → Destino:
' - Toma columnas de la tabla DESTINO
' - Conserva aquellas que también existan en la tabla ORIGEN
' Devuelve:
' destCols: columnas destino a insertar (orden destino)
' selCols : columnas origen a seleccionar (mismo orden)
Private Sub MapearDestino( _
ByVal tablaDestino As String, _
ByVal tablaOrigen As String, _
ByRef destCols As Collection, _
ByRef selCols As Collection)
Dim destAll As Collection, srcAll As Collection, v As Variant
Set destCols = New Collection
Set selCols = New Collection
Set destAll = GetFieldNames(tablaDestino)
Set srcAll = GetFieldNames(tablaOrigen)
For Each v In destAll
If InCollection(srcAll, CStr(v)) Then
destCols.Add CStr(v) ' la columna existe en destino
selCols.Add CStr(v) ' y existe en origen (mismo nombre)
End If
Next v
End Sub
'========================
' Limpieza idempotente
'========================
' Limpia TODAS las salidas al inicio de la corrida.
' (Como ya no usamos FechaProceso, esto evita duplicados)
Private Sub LimpiarTablasSalida()
CurrentDb.Execute "DELETE FROM tblCruce_Final"
CurrentDb.Execute "DELETE FROM tblSinLlamada_Final"
CurrentDb.Execute "DELETE FROM tblSinClasif_Final"
End Sub
'========================
' Días disponibles (por DateValue)
'========================
' Devuelve días calendario con datos en Clasificador, ordenados.
Private Function DiasDisponibles(ByVal tablaClasif As String, ByVal campoTS As String) As Collection
Dim c As New Collection, rs As DAO.Recordset
Dim sqlDias As String
sqlDias = "SELECT DISTINCT DateValue([" & campoTS & "]) AS Dia " & _
"FROM [" & tablaClasif & "] ORDER BY Dia"
Set rs = CurrentDb.OpenRecordset(sqlDias)
Do While Not rs.EOF
c.Add rs!Dia
rs.MoveNext
Loop
rs.Close
Set DiasDisponibles = c
End Function
'========================
' Construcción de match dinámico
'========================
' Construye expresión multi-columna para NOT EXISTS:
' X.[col] = C.[col] AND X.[col2] = C.[col2] ...
' Se usa para comparar filas de C contra lo ya cruzado en tblCruce_Final.
' Si no hay columnas comunes (nunca debería pasar), retorna 1=0 (bloquea inserción).
Private Function JoinMatchExpr(ByVal cols As Collection, ByVal aliasX As String, ByVal aliasC As String) As String
Dim parts() As String, i As Long, v As Variant
If cols.Count = 0 Then
JoinMatchExpr = "1=0"
Exit Function
End If
ReDim parts(0 To cols.Count - 1)
i = 0
For Each v In cols
parts(i) = "[" & aliasX & "].[" & CStr(v) & "] = [" & aliasC & "].[" & CStr(v) & "]"
i = i + 1
Next v
JoinMatchExpr = Join(parts, " AND ")
End Function
'========================
' INSERTS (4 pasos)
'========================
' 1) Cruce en rango (FechaRegistro ∈ [Inicio, Fin]) para mismo ejecutivo
Private Function InsertCruceEnRango( _
ByVal tL As String, ByVal idL As String, ByVal iniL As String, ByVal finL As String, _
ByVal tC As String, ByVal idC As String, ByVal tsC As String, _
ByVal dia As Date, _
ByRef affected As Long) As Boolean
Dim destCols As New Collection, selL As New Collection, selC As New Collection
Dim destCols2 As New Collection, selCOnly As New Collection
Dim destColsStr As String, selStr As String, sqlText As String
Dim f As String: f = Format(dia, "yyyy-mm-dd")
' Mapear columnas que existen en tblCruce_Final desde L y luego desde C
Call MapearDestino("tblCruce_Final", tL, destCols, selL)
Call MapearDestino("tblCruce_Final", tC, destCols2, selCOnly)
' Unir: primero columnas de L, luego columnas de C (orden destino)
Dim v As Variant
For Each v In destCols2: destCols.Add v: Next v
For Each v In selCOnly: selC.Add v: Next v
destColsStr = JoinCols(destCols) ' [colA], [colB], ...
selStr = JoinCols(selL, "L.") & IIf(selL.Count > 0 And selC.Count > 0, ", ", "") & JoinCols(selC, "C.")
sqlText = "INSERT INTO tblCruce_Final (" & destColsStr & ") " & _
"SELECT " & selStr & " " & _
"FROM [" & tL & "] AS L INNER JOIN [" & tC & "] AS C " & _
"ON L.[" & idL & "] = C.[" & idC & "] " & _
"AND C.[" & tsC & "] BETWEEN L.[" & iniL & "] AND L.[" & finL & "] " & _
"WHERE DateValue(C.[" & tsC & "]) = #" & f & "#"
CurrentDb.Execute sqlText
affected = CurrentDb.RecordsAffected
InsertCruceEnRango = True
End Function
' 2) Cruce por última previa (misma persona, MAX(Fin) < FechaRegistro)
Private Function InsertCruceUltimaPrevia( _
ByVal tL As String, ByVal idL As String, ByVal iniL As String, ByVal finL As String, _
ByVal tC As String, ByVal idC As String, ByVal tsC As String, _
ByVal dia As Date, _
ByRef affected As Long) As Boolean
Dim destCols As New Collection, selL As New Collection, selC As New Collection
Dim destCols2 As New Collection, selCOnly As New Collection
Dim destColsStr As String, selStr As String, sqlText As String
Dim f As String: f = Format(dia, "yyyy-mm-dd")
Call MapearDestino("tblCruce_Final", tL, destCols, selL)
Call MapearDestino("tblCruce_Final", tC, destCols2, selCOnly)
Dim v As Variant
For Each v In destCols2: destCols.Add v: Next v
For Each v In selCOnly: selC.Add v: Next v
destColsStr = JoinCols(destCols)
selStr = JoinCols(selL, "L.") & IIf(selL.Count > 0 And selC.Count > 0, ", ", "") & JoinCols(selC, "C.")
sqlText = "INSERT INTO tblCruce_Final (" & destColsStr & ") " & _
"SELECT " & selStr & " " & _
"FROM [" & tL & "] AS L INNER JOIN [" & tC & "] AS C " & _
"ON L.[" & idL & "] = C.[" & idC & "] " & _
"WHERE L.[" & finL & "] = (" & _
" SELECT MAX(L2.[" & finL & "]) FROM [" & tL & "] AS L2 " & _
" WHERE L2.[" & idL & "] = C.[" & idC & "] AND L2.[" & finL & "] < C.[" & tsC & "]" & _
") AND DateValue(C.[" & tsC & "]) = #" & f & "#"
CurrentDb.Execute sqlText
affected = CurrentDb.RecordsAffected
InsertCruceUltimaPrevia = True
End Function
' 3) Clasificaciones sin llamada (no presentes en tblCruce_Final ese día)
' Compara contra tblCruce_Final usando columnas comunes entre C y tblSinLlamada_Final.
Private Function InsertSinLlamada( _
ByVal tC As String, ByVal tsC As String, _
ByVal dia As Date, _
ByRef affected As Long) As Boolean
Dim destCols As New Collection, selC As New Collection
Dim destColsStr As String, selStr As String, sqlText As String
Dim f As String: f = Format(dia, "yyyy-mm-dd")
' Columnas de tblSinLlamada_Final que también existan en C
Call MapearDestino("tblSinLlamada_Final", tC, destCols, selC)
destColsStr = JoinCols(destCols)
selStr = JoinCols(selC, "C.")
sqlText = "INSERT INTO tblSinLlamada_Final (" & destColsStr & ") " & _
"SELECT " & selStr & " FROM [" & tC & "] AS C " & _
"WHERE DateValue(C.[" & tsC & "]) = #" & f & "# " & _
"AND NOT EXISTS (" & _
" SELECT 1 FROM tblCruce_Final AS X " & _
" WHERE " & JoinMatchExpr(selC, "X", "C") & _
")"
CurrentDb.Execute sqlText
affected = CurrentDb.RecordsAffected
InsertSinLlamada = True
End Function
' 4) Llamadas sin clasificaciones (no presentes en tblCruce_Final ese día)
' Compara contra tblCruce_Final por Inicio, Fin, IdUsuario (lo mínimo para identidad de llamada).
Private Function InsertSinClasif( _
ByVal tL As String, ByVal idL As String, ByVal iniL As String, ByVal finL As String, _
ByVal dia As Date, _
ByRef affected As Long) As Boolean
Dim destCols As New Collection, selL As New Collection
Dim destColsStr As String, selStr As String, sqlText As String
Dim f As String: f = Format(dia, "yyyy-mm-dd")
' Columnas de tblSinClasif_Final que existan en L
Call MapearDestino("tblSinClasif_Final", tL, destCols, selL)
destColsStr = JoinCols(destCols)
selStr = JoinCols(selL, "L.")
sqlText = "INSERT INTO tblSinClasif_Final (" & destColsStr & ") " & _
"SELECT " & selStr & " FROM [" & tL & "] AS L " & _
"WHERE DateValue(L.[" & iniL & "]) = #" & f & "# " & _
"AND NOT EXISTS (" & _
" SELECT 1 FROM tblCruce_Final AS X " & _
" WHERE X.[" & iniL & "] = L.[" & iniL & "] " & _
" AND X.[" & finL & "] = L.[" & finL & "] " & _
" AND X.[" & idL & "] = L.[" & idL & "]" & _
")"
CurrentDb.Execute sqlText
affected = CurrentDb.RecordsAffected
InsertSinClasif = True
End Function
'========================
' Orquestador (híbrido)
'========================
' Llamada principal:
' - Sin parámetros → procesa TODOS los días disponibles en el clasificador.
' - Con fechas (desde formulario) → procesa únicamente el rango.
' Requisitos:
' * tblCruce_Final contiene campos de L y de C (nombres distintos entre sí)
' * tblSinLlamada_Final contiene campos de C
' * tblSinClasif_Final contiene campos de L
Public Sub ProcesarRango(Optional ByVal fIni As Variant, Optional ByVal fFin As Variant)
On Error GoTo EH
Dim tL As String, idL As String, iniL As String, finL As String
Dim tC As String, idC As String, tsC As String
Dim dias As Collection, d As Variant
Dim minDia As Date, maxDia As Date
Dim totalCruce As Long, totalSinLlamada As Long, totalSinClasif As Long
Dim a As Long
' 1) Config
LeerConfig tL, idL, iniL, finL, tC, idC, tsC
' 2) Días disponibles en el insumo (clasificador)
Set dias = DiasDisponibles(tC, tsC)
If dias.Count = 0 Then
MsgBox "No hay registros en la tabla de clasificador.", vbInformation
Exit Sub
End If
' 3) Determinar rango efectivo
minDia = CDate(dias(1))
maxDia = CDate(dias(dias.Count))
If IsMissing(fIni) Or IsNull(fIni) Then fIni = minDia
If IsMissing(fFin) Or IsNull(fFin) Then fFin = maxDia
' 4) Limpiar salidas al inicio de la corrida
LimpiarTablasSalida
' 5) Procesar día por día dentro del rango
For Each d In dias
If CDate(d) >= CDate(fIni) And CDate(d) <= CDate(fFin) Then
' (1) En rango
InsertCruceEnRango tL, idL, iniL, finL, tC, idC, tsC, CDate(d), a
totalCruce = totalCruce + a
' (2) Última previa
InsertCruceUltimaPrevia tL, idL, iniL, finL, tC, idC, tsC, CDate(d), a
totalCruce = totalCruce + a
' (3) Clasificaciones sin llamada
InsertSinLlamada tC, tsC, CDate(d), a
totalSinLlamada = totalSinLlamada + a
' (4) Llamadas sin clasificaciones
InsertSinClasif tL, idL, iniL, finL, CDate(d), a
totalSinClasif = totalSinClasif + a
End If
Next d
' 6) Resumen final
MsgBox "Procesamiento completado: " & Format(CDate(fIni), "dd/mm/yyyy") & " a " & Format(CDate(fFin), "dd/mm/yyyy") & vbCrLf & vbCrLf & _
"Registros insertados:" & vbCrLf & _
" tblCruce_Final: " & totalCruce & vbCrLf & _
" tblSinLlamada_Final: " & totalSinLlamada & vbCrLf & _
" tblSinClasif_Final: " & totalSinClasif, vbInformation
Exit Sub
EH:
MsgBox "Error " & Err.Number & ": " & Err.Description, vbCritical, "ProcesarRango"
End Sub