Загрузка данных


Option Explicit

' =========================
' НАСТРОЙКИ
' =========================
Private Const REPORT_SHEET_DASH As String = "Дашборд"
Private Const REPORT_SHEET_SUMMARY As String = "Сводка"
Private Const REPORT_SHEET_REPAIRS As String = "Ремонты"
Private Const REPORT_SHEET_REASONS As String = "Причины"
Private Const REPORT_SHEET_FILES As String = "Файлы"
Private Const REPORT_SHEET_DES As String = "ДЭС_НП"
Private Const REPORT_SHEET_BOIL As String = "Котельные_НП"

Private Const SRC_SHEET_DES As String = "ДЭС"
Private Const SRC_SHEET_BOILERS As String = "Котельные"
Private Const SRC_SHEET_REPAIRS As String = "Ремонт оборудования"

' Лист ДЭС
Private Const DES_COL_STATE_WORK As Long = 8      ' H
Private Const DES_COL_STATE_RESERVE As Long = 9   ' I
Private Const DES_COL_STATE_REPAIR As Long = 10   ' J
Private Const DES_COL_NAME As Long = 11           ' K
Private Const DES_COL_LOAD_MAXDAY As Long = 20    ' T
Private Const DES_COL_OIL As Long = 23            ' W
Private Const DES_COL_FUEL As Long = 25           ' Y
Private Const DES_COL_OUTPUT As Long = 42         ' AP

Private gDES As Object
Private gBoilers As Object

' =========================
' ГЛАВНЫЙ ЗАПУСК
' =========================
Public Sub ОбновитьДанные()
    Dim folderPath As String
    Dim fileName As String
    Dim fullPath As String
    Dim wb As Workbook
    Dim reportName As String
    Dim dt As Variant

    Dim wsDash As Worksheet, wsSummary As Worksheet, wsRepairs As Worksheet
    Dim wsReasons As Worksheet, wsFiles As Worksheet, wsDES As Worksheet, wsBoil As Worksheet

    Dim dictSummary As Object
    Dim dictReasons As Object

    Dim repairsRow As Long
    Dim filesRow As Long

    On Error GoTo EH

    Application.ScreenUpdating = False
    Application.DisplayAlerts = False
    Application.EnableEvents = False
    Application.AskToUpdateLinks = False
    Application.Calculation = xlCalculationManual
    Application.StatusBar = "Обновление отчета..."

    folderPath = ThisWorkbook.Path
    If Len(folderPath) = 0 Then
        MsgBox "Сначала сохраните файл отчета в папку с исходными файлами.", vbExclamation
        GoTo SafeExit
    End If

    reportName = ThisWorkbook.Name

    Set dictSummary = CreateObject("Scripting.Dictionary")
    Set dictReasons = CreateObject("Scripting.Dictionary")
    Set gDES = CreateObject("Scripting.Dictionary")
    Set gBoilers = CreateObject("Scripting.Dictionary")

    PrepareSheets wsDash, wsSummary, wsRepairs, wsReasons, wsFiles, wsDES, wsBoil

    repairsRow = 2
    filesRow = 2

    fileName = Dir(folderPath & "\*.xls*")
    Do While fileName <> ""
        If LCase$(fileName) <> LCase$(reportName) And Left$(fileName, 2) <> "~$" Then
            fullPath = folderPath & "\" & fileName
            dt = ExtractDateFromFileName(fileName)

            If IsDate(dt) Then
                Set wb = Nothing
                On Error Resume Next
                Set wb = Workbooks.Open(Filename:=fullPath, ReadOnly:=True, UpdateLinks:=False, IgnoreReadOnlyRecommended:=True)
                On Error GoTo EH

                If Not wb Is Nothing Then
                    ProcessWorkbook wb, CDate(dt), fileName, dictSummary, dictReasons, wsRepairs, repairsRow, wsFiles, filesRow
                    wb.Close SaveChanges:=False
                    Set wb = Nothing
                Else
                    LogFile wsFiles, filesRow, fileName, CDate(dt), "Ошибка", "Не удалось открыть файл"
                    filesRow = filesRow + 1
                End If
            Else
                LogFile wsFiles, filesRow, fileName, Empty, "Пропущен", "Не найдена дата в имени файла"
                filesRow = filesRow + 1
            End If
        End If
        fileName = Dir
    Loop

    ExportSummary wsSummary, dictSummary
    ExportReasons wsReasons, dictReasons
    ExportDESBlocks wsDES, gDES
    ExportBoilerBlocks wsBoil, gBoilers

    BuildDashboard wsDash, wsSummary, wsReasons, wsDES, wsBoil
    FormatSheets wsSummary, wsRepairs, wsReasons, wsFiles, wsDash, wsDES, wsBoil

    MsgBox "Обновление завершено.", vbInformation

SafeExit:
    Application.ScreenUpdating = True
    Application.DisplayAlerts = True
    Application.EnableEvents = True
    Application.AskToUpdateLinks = True
    Application.Calculation = xlCalculationAutomatic
    Application.StatusBar = False
    Exit Sub

EH:
    MsgBox "Ошибка при обновлении: " & Err.Description, vbCritical
    Resume SafeExit
End Sub

Public Sub СоздатьКнопкуОбновления()
    Dim ws As Worksheet
    Dim shp As Shape

    Set ws = GetOrCreateSheet(REPORT_SHEET_DASH)

    On Error Resume Next
    ws.Shapes("btnRefresh").Delete
    On Error GoTo 0

    Set shp = ws.Shapes.AddShape(msoShapeRoundedRectangle, 700, 20, 180, 35)
    shp.Name = "btnRefresh"
    shp.TextFrame.Characters.Text = "Обновить данные"
    shp.OnAction = "ОбновитьДанные"
End Sub

' =========================
' ПОДГОТОВКА ЛИСТОВ
' =========================
Private Sub PrepareSheets(ByRef wsDash As Worksheet, ByRef wsSummary As Worksheet, ByRef wsRepairs As Worksheet, ByRef wsReasons As Worksheet, ByRef wsFiles As Worksheet, ByRef wsDES As Worksheet, ByRef wsBoil As Worksheet)
    Set wsDash = GetOrCreateSheet(REPORT_SHEET_DASH)
    Set wsSummary = GetOrCreateSheet(REPORT_SHEET_SUMMARY)
    Set wsRepairs = GetOrCreateSheet(REPORT_SHEET_REPAIRS)
    Set wsReasons = GetOrCreateSheet(REPORT_SHEET_REASONS)
    Set wsFiles = GetOrCreateSheet(REPORT_SHEET_FILES)
    Set wsDES = GetOrCreateSheet(REPORT_SHEET_DES)
    Set wsBoil = GetOrCreateSheet(REPORT_SHEET_BOIL)

    wsDash.Cells.Clear
    wsSummary.Cells.Clear
    wsRepairs.Cells.Clear
    wsReasons.Cells.Clear
    wsFiles.Cells.Clear
    wsDES.Cells.Clear
    wsBoil.Cells.Clear

    wsSummary.Range("A1:M1").Value = Array( _
        "Дата", "Файлов", "Ремонтов", "Нагрузка ДЭС, кВт", "Выработка ДЭС, кВт", _
        "Топливо ДЭС, л", "Топливо котельных, м3", "Общее топливо", _
        "ДГУ в работе", "ДГУ в резерве", "ДГУ в ремонте", "Кол-во причин", "Расход масла ДЭС")

    wsRepairs.Range("A1:F1").Value = Array("Дата", "Источник", "Объект/оборудование", "Причина", "Примечание", "Исходный файл")
    wsReasons.Range("A1:B1").Value = Array("Причина", "Количество")
    wsFiles.Range("A1:D1").Value = Array("Файл", "Дата", "Статус", "Комментарий")

    wsDES.Range("A1:M1").Value = Array( _
        "Дата", "Блок ДЭС", "Нагрузка мин", "Нагрузка макс", "Нагрузка средняя", "Нагрузка сумма", _
        "Выработка", "Топливо ДЭС", "Масло ДЭС", "Кол-во установок", "В работе", "В резерве", "В ремонте")

    wsBoil.Range("A1:F1").Value = Array( _
        "Дата", "Блок котельной", "Топливо мин", "Топливо макс", "Топливо среднее", "Топливо сумма")
End Sub

' =========================
' ОБРАБОТКА КНИГИ
' =========================
Private Sub ProcessWorkbook(ByVal wb As Workbook, ByVal dt As Date, ByVal fileName As String, ByVal dictSummary As Object, ByVal dictReasons As Object, ByVal wsRepairs As Worksheet, ByRef repairsRow As Long, ByVal wsFiles As Worksheet, ByRef filesRow As Long)
    Dim desLoad As Double
    Dim desOutput As Double
    Dim desFuel As Double
    Dim boilersFuel As Double
    Dim totalFuel As Double
    Dim desOil As Double
    Dim cntRepairs As Long
    Dim cntWork As Long, cntReserve As Long, cntRepairState As Long
    Dim okAny As Boolean

    okAny = False

    desLoad = ReadDESLoadStatus(wb, dt, cntWork, cntReserve, cntRepairState)
    desOutput = ReadDESOutput(wb, dt)
    desFuel = ReadDESFuel(wb, dt)
    desOil = ReadDESOil(wb, dt)
    boilersFuel = ReadBoilerFuel(wb, dt)
    totalFuel = desFuel + boilersFuel
    cntRepairs = ReadRepairs(wb, dt, fileName, dictReasons, wsRepairs, repairsRow)

    If desLoad <> 0 Or desOutput <> 0 Or desFuel <> 0 Or boilersFuel <> 0 Or desOil <> 0 Then okAny = True
    If cntWork <> 0 Or cntReserve <> 0 Or cntRepairState <> 0 Then okAny = True
    If cntRepairs <> 0 Then okAny = True

    AddToSummary dictSummary, dt, 1, cntRepairs, desLoad, desOutput, desFuel, boilersFuel, totalFuel, cntWork, cntReserve, cntRepairState, desOil

    If okAny Then
        LogFile wsFiles, filesRow, fileName, dt, "ОК", "Обработан"
    Else
        LogFile wsFiles, filesRow, fileName, dt, "ОК", "Файл открыт, но данные не распознаны"
    End If

    filesRow = filesRow + 1
End Sub

' =========================
' ДЭС
' =========================
Private Function ReadDESLoadStatus(ByVal wb As Workbook, ByVal dt As Date, ByRef cntWork As Long, ByRef cntReserve As Long, ByRef cntRepair As Long) As Double
    Dim ws As Worksheet
    Dim r As Long, lastRow As Long
    Dim blockName As String
    Dim loadVal As Double
    Dim vRun As Variant, vReserve As Variant, vRepair As Variant
    Dim stWork As Long, stReserve As Long, stRepair As Long
    Dim totalLoad As Double

    On Error Resume Next
    Set ws = wb.Worksheets(SRC_SHEET_DES)
    On Error GoTo 0
    If ws Is Nothing Then Exit Function

    lastRow = LastRow(ws)
    blockName = ""
    totalLoad = 0

    For r = 1 To lastRow
        If IsDESBlockHeader(ws, r) Then
            blockName = GetDESBlockName(ws, r)
            EnsureDESBlock dt, blockName
        ElseIf IsDESEquipmentRow(ws, r) Then
            If blockName = "" Then blockName = "Не определено"

            loadVal = ToNumber(ws.Cells(r, DES_COL_LOAD_MAXDAY).Value)
            vRun = ws.Cells(r, DES_COL_STATE_WORK).Value
            vReserve = ws.Cells(r, DES_COL_STATE_RESERVE).Value
            vRepair = ws.Cells(r, DES_COL_STATE_REPAIR).Value

            stWork = 0
            stReserve = 0
            stRepair = 0

            If Trim$(CStr(vRun)) = "+" Then
                cntWork = cntWork + 1
                stWork = 1
            End If
            If Trim$(CStr(vReserve)) = "+" Then
                cntReserve = cntReserve + 1
                stReserve = 1
            End If
            If LCase$(Trim$(CStr(vRepair))) = "ремонт" Or Trim$(CStr(vRepair)) = "+" Then
                cntRepair = cntRepair + 1
                stRepair = 1
            End If

            totalLoad = totalLoad + loadVal
            UpdateDESBlock dt, blockName, loadVal, 0, 0, 0, stWork, stReserve, stRepair
        End If
    Next r

    ReadDESLoadStatus = totalLoad
End Function

Private Function ReadDESOutput(ByVal wb As Workbook, ByVal dt As Date) As Double
    Dim ws As Worksheet
    Dim r As Long, lastRow As Long
    Dim blockName As String
    Dim v As Double, totalV As Double

    On Error Resume Next
    Set ws = wb.Worksheets(SRC_SHEET_DES)
    On Error GoTo 0
    If ws Is Nothing Then Exit Function

    lastRow = LastRow(ws)
    blockName = ""

    For r = 1 To lastRow
        If IsDESBlockHeader(ws, r) Then
            blockName = GetDESBlockName(ws, r)
            EnsureDESBlock dt, blockName
        ElseIf IsDESEquipmentRow(ws, r) Then
            If blockName = "" Then blockName = "Не определено"
            v = ToNumber(ws.Cells(r, DES_COL_OUTPUT).Value)
            totalV = totalV + v
            UpdateDESBlock dt, blockName, 0, v, 0, 0, 0, 0, 0
        End If
    Next r

    ReadDESOutput = totalV
End Function

Private Function ReadDESFuel(ByVal wb As Workbook, ByVal dt As Date) As Double
    Dim ws As Worksheet
    Dim r As Long, lastRow As Long
    Dim blockName As String
    Dim v As Double, totalV As Double

    On Error Resume Next
    Set ws = wb.Worksheets(SRC_SHEET_DES)
    On Error GoTo 0
    If ws Is Nothing Then Exit Function

    lastRow = LastRow(ws)
    blockName = ""

    For r = 1 To lastRow
        If IsDESBlockHeader(ws, r) Then
            blockName = GetDESBlockName(ws, r)
            EnsureDESBlock dt, blockName
        ElseIf IsDESEquipmentRow(ws, r) Then
            If blockName = "" Then blockName = "Не определено"
            v = ToNumber(ws.Cells(r, DES_COL_FUEL).Value)
            totalV = totalV + v
            UpdateDESBlock dt, blockName, 0, 0, v, 0, 0, 0, 0
        End If
    Next r

    ReadDESFuel = totalV
End Function

Private Function ReadDESOil(ByVal wb As Workbook, ByVal dt As Date) As Double
    Dim ws As Worksheet
    Dim r As Long, lastRow As Long
    Dim blockName As String
    Dim v As Double, totalV As Double

    On Error Resume Next
    Set ws = wb.Worksheets(SRC_SHEET_DES)
    On Error GoTo 0
    If ws Is Nothing Then Exit Function

    lastRow = LastRow(ws)
    blockName = ""

    For r = 1 To lastRow
        If IsDESBlockHeader(ws, r) Then
            blockName = GetDESBlockName(ws, r)
            EnsureDESBlock dt, blockName
        ElseIf IsDESEquipmentRow(ws, r) Then
            If blockName = "" Then blockName = "Не определено"
            v = ToNumber(ws.Cells(r, DES_COL_OIL).Value)
            totalV = totalV + v
            UpdateDESBlock dt, blockName, 0, 0, 0, v, 0, 0, 0
        End If
    Next r

    ReadDESOil = totalV
End Function

Private Function IsDESBlockHeader(ByVal ws As Worksheet, ByVal r As Long) As Boolean
    Dim txt As String
    txt = Trim$(RowText(ws, r))
    If txt = "" Then Exit Function

    If InStr(1, txt, "АО ", vbTextCompare) > 0 Then IsDESBlockHeader = True: Exit Function
    If InStr(1, txt, "ООО ", vbTextCompare) > 0 Then IsDESBlockHeader = True: Exit Function
    If InStr(1, txt, "ДЭС-", vbTextCompare) > 0 Then IsDESBlockHeader = True: Exit Function
    If InStr(1, txt, "ГПТЭС", vbTextCompare) > 0 Then IsDESBlockHeader = True: Exit Function
    If InStr(1, txt, "ГПЭС", vbTextCompare) > 0 Then IsDESBlockHeader = True: Exit Function
    If InStr(1, txt, "Харсаим ДЭС", vbTextCompare) > 0 Then IsDESBlockHeader = True: Exit Function
End Function

Private Function GetDESBlockName(ByVal ws As Worksheet, ByVal r As Long) As String
    Dim s As String
    s = Trim$(RowText(ws, r))
    If s = "" Then s = "Не определено"

    If InStr(1, s, "АО ""Харп-Энерго Газ""", vbTextCompare) > 0 Then
        GetDESBlockName = "АО ""Харп-Энерго Газ"""
        Exit Function
    End If

    If InStr(1, s, "ООО ""ВДМ-Сервис"" ГПЭС Харсаим", vbTextCompare) > 0 Then
        GetDESBlockName = "ООО ""ВДМ-Сервис"" ГПЭС Харсаим ул. Объездная"
        Exit Function
    End If

    If InStr(1, s, "Харсаим ДЭС-34", vbTextCompare) > 0 Then
        GetDESBlockName = "Харсаим ДЭС-34"
        Exit Function
    End If

    GetDESBlockName = s
End Function

Private Function IsDESEquipmentRow(ByVal ws As Worksheet, ByVal r As Long) As Boolean
    Dim nm As String
    Dim cntNums As Long

    nm = Trim$(CStr(ws.Cells(r, DES_COL_NAME).Value))

    If LCase$(nm) Like "*итого*" Then Exit Function
    If LCase$(nm) Like "*фидер*" Then Exit Function
    If LCase$(nm) Like "*номер*" Then Exit Function
    If LCase$(nm) Like "*собствен*" Then Exit Function

    If InStr(1, nm, "Ф-", vbTextCompare) > 0 _
       Or InStr(1, nm, "ДГУ-", vbTextCompare) > 0 _
       Or InStr(1, nm, "ГТУ-", vbTextCompare) > 0 _
       Or InStr(1, nm, "РДГУ-", vbTextCompare) > 0 _
       Or InStr(1, nm, "ГПА-", vbTextCompare) > 0 _
       Or InStr(1, nm, "Ввод 1", vbTextCompare) > 0 _
       Or InStr(1, nm, "Ввод 2", vbTextCompare) > 0 _
       Or InStr(1, nm, "Ввод 3", vbTextCompare) > 0 _
       Or InStr(1, nm, "Ввод 4", vbTextCompare) > 0 Then
        IsDESEquipmentRow = True
        Exit Function
    End If

    cntNums = CountNumericInRange(ws, r, 12, 20)
    If cntNums >= 3 Then
        IsDESEquipmentRow = True
    End If
End Function

' =========================
' КОТЕЛЬНЫЕ
' =========================
Private Function ReadBoilerFuel(ByVal wb As Workbook, ByVal dt As Date) As Double
    Dim ws As Worksheet
    Dim fuelCol As Long
    Dim r As Long, lastRow As Long
    Dim blockName As String
    Dim v As Double, totalV As Double

    On Error Resume Next
    Set ws = wb.Worksheets(SRC_SHEET_BOILERS)
    On Error GoTo 0
    If ws Is Nothing Then Exit Function

    fuelCol = FindBoilerFuelCol(ws)
    lastRow = LastRow(ws)
    blockName = ""

    For r = 1 To lastRow
        If IsBoilerBlockHeader(ws, r) Then
            blockName = GetBoilerBlockName(ws, r)
            EnsureBoilerBlock dt, blockName
        ElseIf IsBoilerDataRow(ws, r) Then
            If blockName = "" Then blockName = "Не определено"
            v = ToNumber(ws.Cells(r, fuelCol).Value)
            totalV = totalV + v
            UpdateBoilerBlock dt, blockName, v
        End If
    Next r

    ReadBoilerFuel = totalV
End Function

Private Function IsBoilerBlockHeader(ByVal ws As Worksheet, ByVal r As Long) As Boolean
    Dim txt As String
    Dim nonEmpty As Long

    txt = Trim$(RowText(ws, r))
    If txt = "" Then Exit Function

    nonEmpty = Application.WorksheetFunction.CountA(ws.Rows(r))

    If nonEmpty <= 3 Then
        If InStr(1, txt, "№", vbTextCompare) > 0 Then
            IsBoilerBlockHeader = True
            Exit Function
        End If
        If InStr(1, txt, "АО ", vbTextCompare) > 0 Then
            IsBoilerBlockHeader = True
            Exit Function
        End If
        If InStr(1, txt, "ООО ", vbTextCompare) > 0 Then
            IsBoilerBlockHeader = True
            Exit Function
        End If
    End If
End Function

Private Function GetBoilerBlockName(ByVal ws As Worksheet, ByVal r As Long) As String
    Dim s As String
    s = Trim$(RowText(ws, r))
    If s = "" Then s = "Не определено"
    GetBoilerBlockName = s
End Function

Private Function IsBoilerDataRow(ByVal ws As Worksheet, ByVal r As Long) As Boolean
    Dim txt As String
    txt = Trim$(RowText(ws, r))
    If txt = "" Then Exit Function
    If IsBoilerBlockHeader(ws, r) Then Exit Function
    If InStr(1, LCase$(txt), "итого", vbTextCompare) > 0 Then Exit Function
    If Application.WorksheetFunction.CountA(ws.Rows(r)) > 3 Then IsBoilerDataRow = True
End Function

Private Function FindBoilerFuelCol(ByVal ws As Worksheet) As Long
    Dim r As Long, c As Long, lastCol As Long
    Dim txt As String

    For r = 1 To 12
        lastCol = ws.Cells(r, ws.Columns.Count).End(xlToLeft).Column
        For c = 1 To lastCol
            txt = LCase$(Trim$(CStr(ws.Cells(r, c).Value)))
            If txt <> "" Then
                If InStr(txt, "расход") > 0 And InStr(txt, "топлив") > 0 And InStr(txt, "сутк") > 0 Then
                    FindBoilerFuelCol = c
                    Exit Function
                End If
            End If
        Next c
    Next r

    FindBoilerFuelCol = 20
End Function

' =========================
' РЕМОНТЫ
' =========================
Private Function ReadRepairs(ByVal wb As Workbook, ByVal dt As Date, ByVal fileName As String, ByVal dictReasons As Object, ByVal wsRepairs As Worksheet, ByRef repairsRow As Long) As Long
    Dim ws As Worksheet
    Dim lastRow As Long, r As Long
    Dim unitNo As Variant
    Dim typeVal As String, engineVal As String, reasonVal As String, noteVal As String
    Dim cnt As Long

    On Error Resume Next
    Set ws = wb.Worksheets(SRC_SHEET_REPAIRS)
    On Error GoTo 0
    If ws Is Nothing Then Exit Function

    lastRow = LastRow(ws)

    For r = 3 To lastRow
        unitNo = ws.Cells(r, 2).Value
        typeVal = Trim$(CStr(ws.Cells(r, 3).Value))
        engineVal = Trim$(CStr(ws.Cells(r, 4).Value))
        reasonVal = Trim$(CStr(ws.Cells(r, 6).Value))
        noteVal = Trim$(CStr(ws.Cells(r, 8).Value))

        If reasonVal <> "" And IsNumeric(unitNo) Then
            wsRepairs.Cells(repairsRow, 1).Value = dt
            wsRepairs.Cells(repairsRow, 2).Value = "Ремонт оборудования"
            wsRepairs.Cells(repairsRow, 3).Value = Trim$(typeVal & " " & engineVal)
            wsRepairs.Cells(repairsRow, 4).Value = reasonVal
            wsRepairs.Cells(repairsRow, 5).Value = noteVal
            wsRepairs.Cells(repairsRow, 6).Value = fileName

            If dictReasons.Exists(reasonVal) Then
                dictReasons(reasonVal) = CLng(dictReasons(reasonVal)) + 1
            Else
                dictReasons.Add reasonVal, 1
            End If

            repairsRow = repairsRow + 1
            cnt = cnt + 1
        End If
    Next r

    ReadRepairs = cnt
End Function

' =========================
' СЛОВАРИ БЛОКОВ
' =========================
Private Sub EnsureDESBlock(ByVal dt As Date, ByVal blockName As String)
    Dim key As String
    Dim arr As Variant

    If blockName = "" Then blockName = "Не определено"
    key = Format$(dt, "yyyy-mm-dd") & "|" & blockName

    If Not gDES.Exists(key) Then
        ReDim arr(1 To 10)
        arr(1) = 0
        arr(2) = 0
        arr(3) = 0
        arr(4) = 0
        arr(5) = 0
        arr(6) = 0
        arr(7) = 0
        arr(8) = 0
        arr(9) = 0
        arr(10) = 0
        gDES.Add key, arr
    End If
End Sub

Private Sub UpdateDESBlock(ByVal dt As Date, ByVal blockName As String, ByVal loadVal As Double, ByVal outputVal As Double, ByVal fuelVal As Double, ByVal oilVal As Double, ByVal stWork As Long, ByVal stReserve As Long, ByVal stRepair As Long)
    Dim key As String
    Dim arr As Variant

    If blockName = "" Then blockName = "Не определено"
    key = Format$(dt, "yyyy-mm-dd") & "|" & blockName
    If Not gDES.Exists(key) Then EnsureDESBlock dt, blockName

    arr = gDES(key)

    If loadVal > 0 Then
        If arr(1) = 0 Or loadVal < arr(1) Then arr(1) = loadVal
        If loadVal > arr(2) Then arr(2) = loadVal
        arr(3) = arr(3) + loadVal
        arr(7) = arr(7) + 1
    End If

    arr(4) = arr(4) + outputVal
    arr(5) = arr(5) + fuelVal
    arr(6) = arr(6) + oilVal
    arr(8) = arr(8) + stWork
    arr(9) = arr(9) + stReserve
    arr(10) = arr(10) + stRepair

    gDES(key) = arr
End Sub

Private Sub EnsureBoilerBlock(ByVal dt As Date, ByVal blockName As String)
    Dim key As String
    Dim arr As Variant

    If blockName = "" Then blockName = "Не определено"
    key = Format$(dt, "yyyy-mm-dd") & "|" & blockName

    If Not gBoilers.Exists(key) Then
        ReDim arr(1 To 4)
        arr(1) = 0
        arr(2) = 0
        arr(3) = 0
        arr(4) = 0
        gBoilers.Add key, arr
    End If
End Sub

Private Sub UpdateBoilerBlock(ByVal dt As Date, ByVal blockName As String, ByVal fuelVal As Double)
    Dim key As String
    Dim arr As Variant

    If blockName = "" Then blockName = "Не определено"
    key = Format$(dt, "yyyy-mm-dd") & "|" & blockName
    If Not gBoilers.Exists(key) Then EnsureBoilerBlock dt, blockName

    arr = gBoilers(key)

    If fuelVal > 0 Then
        If arr(1) = 0 Or fuelVal < arr(1) Then arr(1) = fuelVal
        If fuelVal > arr(2) Then arr(2) = fuelVal
        arr(3) = arr(3) + fuelVal
        arr(4) = arr(4) + 1
    End If

    gBoilers(key) = arr
End Sub

' =========================
' ВЫГРУЗКА
' =========================
Private Sub ExportSummary(ByVal ws As Worksheet, ByVal dictSummary As Object)
    Dim keys As Variant, i As Long, r As Long, arr As Variant

    If dictSummary.Count = 0 Then Exit Sub
    keys = dictSummary.Keys
    SortDateKeys keys

    r = 2
    For i = LBound(keys) To UBound(keys)
        arr = dictSummary(keys(i))
        ws.Cells(r, 1).Value = CDate(keys(i))
        ws.Cells(r, 2).Value = arr(1)
        ws.Cells(r, 3).Value = arr(2)
        ws.Cells(r, 4).Value = arr(3)
        ws.Cells(r, 5).Value = arr(4)
        ws.Cells(r, 6).Value = arr(5)
        ws.Cells(r, 7).Value = arr(6)
        ws.Cells(r, 8).Value = arr(7)
        ws.Cells(r, 9).Value = arr(8)
        ws.Cells(r, 10).Value = arr(9)
        ws.Cells(r, 11).Value = arr(10)
        ws.Cells(r, 12).Formula = "=COUNTIF(Ремонты!A:A,A" & r & ")"
        ws.Cells(r, 13).Value = arr(12)
        r = r + 1
    Next i
End Sub

Private Sub ExportReasons(ByVal ws As Worksheet, ByVal dictReasons As Object)
    Dim keys As Variant, i As Long, r As Long, lastRowVal As Long

    If dictReasons.Count = 0 Then Exit Sub

    keys = dictReasons.Keys
    r = 2
    For i = LBound(keys) To UBound(keys)
        ws.Cells(r, 1).Value = keys(i)
        ws.Cells(r, 2).Value = dictReasons(keys(i))
        r = r + 1
    Next i

    lastRowVal = LastRow(ws)
    If lastRowVal >= 2 Then
        ws.Range("A1:B" & lastRowVal).Sort Key1:=ws.Range("B2"), Order1:=xlDescending, Header:=xlYes
    End If
End Sub

Private Sub ExportDESBlocks(ByVal ws As Worksheet, ByVal dictData As Object)
    Dim keys As Variant, i As Long, r As Long, arr As Variant, parts() As String
    Dim avgLoad As Double

    If dictData Is Nothing Or dictData.Count = 0 Then Exit Sub

    keys = dictData.Keys
    SortStringKeys keys

    r = 2
    For i = LBound(keys) To UBound(keys)
        arr = dictData(keys(i))
        parts = Split(CStr(keys(i)), "|")

        avgLoad = 0
        If arr(7) > 0 Then avgLoad = arr(3) / arr(7)

        ws.Cells(r, 1).Value = CDate(parts(0))
        ws.Cells(r, 2).Value = parts(1)
        ws.Cells(r, 3).Value = arr(1)
        ws.Cells(r, 4).Value = arr(2)
        ws.Cells(r, 5).Value = avgLoad
        ws.Cells(r, 6).Value = arr(3)
        ws.Cells(r, 7).Value = arr(4)
        ws.Cells(r, 8).Value = arr(5)
        ws.Cells(r, 9).Value = arr(6)
        ws.Cells(r, 10).Value = arr(7)
        ws.Cells(r, 11).Value = arr(8)
        ws.Cells(r, 12).Value = arr(9)
        ws.Cells(r, 13).Value = arr(10)
        r = r + 1
    Next i
End Sub

Private Sub ExportBoilerBlocks(ByVal ws As Worksheet, ByVal dictData As Object)
    Dim keys As Variant, i As Long, r As Long, arr As Variant, parts() As String
    Dim avgFuel As Double

    If dictData Is Nothing Or dictData.Count = 0 Then Exit Sub

    keys = dictData.Keys
    SortStringKeys keys

    r = 2
    For i = LBound(keys) To UBound(keys)
        arr = dictData(keys(i))
        parts = Split(CStr(keys(i)), "|")

        avgFuel = 0
        If arr(4) > 0 Then avgFuel = arr(3) / arr(4)

        ws.Cells(r, 1).Value = CDate(parts(0))
        ws.Cells(r, 2).Value = parts(1)
        ws.Cells(r, 3).Value = arr(1)
        ws.Cells(r, 4).Value = arr(2)
        ws.Cells(r, 5).Value = avgFuel
        ws.Cells(r, 6).Value = arr(3)
        r = r + 1
    Next i
End Sub

' =========================
' ДАШБОРД
' =========================
Private Sub BuildDashboard(ByVal wsDash As Worksheet, ByVal wsSummary As Worksheet, ByVal wsReasons As Worksheet, ByVal wsDES As Worksheet, ByVal wsBoil As Worksheet)
    Dim lastSummaryRow As Long, lastReasonsRow As Long, lastDESRow As Long, lastBoilRow As Long
    Dim ch As ChartObject

    wsDash.Cells.Clear

    wsDash.Range("A1").Value = "Месячный дашборд"
    wsDash.Range("A1").Font.Size = 18
    wsDash.Range("A1").Font.Bold = True

    wsDash.Range("A3").Value = "Файлов обработано"
    wsDash.Range("B3").Formula = "=MAX(0,COUNTA(Файлы!A:A)-1)"
    wsDash.Range("D3").Value = "Дней с данными"
    wsDash.Range("E3").Formula = "=MAX(0,COUNTA(Сводка!A:A)-1)"

    wsDash.Range("A4").Value = "Всего ремонтов"
    wsDash.Range("B4").Formula = "=SUM(Сводка!C:C)"
    wsDash.Range("D4").Value = "Выработка ДЭС"
    wsDash.Range("E4").Formula = "=SUM(Сводка!E:E)"

    wsDash.Range("A5").Value = "Топливо ДЭС"
    wsDash.Range("B5").Formula = "=SUM(Сводка!F:F)"
    wsDash.Range("D5").Value = "Топливо котельных"
    wsDash.Range("E5").Formula = "=SUM(Сводка!G:G)"

    wsDash.Range("A6").Value = "Общее топливо"
    wsDash.Range("B6").Formula = "=SUM(Сводка!H:H)"
    wsDash.Range("D6").Value = "Нагрузка ДЭС"
    wsDash.Range("E6").Formula = "=SUM(Сводка!D:D)"

    wsDash.Range("A7").Value = "Расход масла ДЭС"
    wsDash.Range("B7").Formula = "=SUM(Сводка!M:M)"
    wsDash.Range("D7").Value = "Последняя дата"
    wsDash.Range("E7").Formula = "=MAX(Сводка!A:A)"
    wsDash.Range("E7").NumberFormat = "dd.mm.yyyy"

    lastSummaryRow = LastRow(wsSummary)
    lastReasonsRow = LastRow(wsReasons)
    lastDESRow = LastRow(wsDES)
    lastBoilRow = LastRow(wsBoil)

    DeleteAllCharts wsDash
    wsDash.Range("J:O").Clear

    If lastSummaryRow >= 2 Then
        Set ch = wsDash.ChartObjects.Add(Left:=20, Top:=130, Width:=420, Height:=220)
        With ch.Chart
            .ChartType = xlColumnClustered
            ClearSeries ch.Chart
            .SeriesCollection.NewSeries
            .SeriesCollection(1).Name = "=""Ремонты"""
            .SeriesCollection(1).XValues = wsSummary.Range("A2:A" & lastSummaryRow)
            .SeriesCollection(1).Values = wsSummary.Range("C2:C" & lastSummaryRow)
            .HasTitle = True
            .ChartTitle.Text = "Ремонты по дням"
        End With

        Set ch = wsDash.ChartObjects.Add(Left:=460, Top:=130, Width:=420, Height:=220)
        With ch.Chart
            .ChartType = xlLine
            ClearSeries ch.Chart
            .SeriesCollection.NewSeries
            .SeriesCollection(1).Name = "=""Общее топливо"""
            .SeriesCollection(1).XValues = wsSummary.Range("A2:A" & lastSummaryRow)
            .SeriesCollection(1).Values = wsSummary.Range("H2:H" & lastSummaryRow)
            .HasTitle = True
            .ChartTitle.Text = "Общее топливо по дням"
        End With
    End If

    If lastDESRow >= 2 Then
        AggregateDESForDashboard wsDES, wsDash

        Set ch = wsDash.ChartObjects.Add(Left:=20, Top:=380, Width:=860, Height:=260)
        With ch.Chart
            .ChartType = xlColumnClustered
            ClearSeries ch.Chart
            .SeriesCollection.NewSeries
            .SeriesCollection(1).Name = "=""Мин"""
            .SeriesCollection(1).XValues = wsDash.Range("J3:J" & LastRow(wsDash))
            .SeriesCollection(1).Values = wsDash.Range("K3:K" & LastRow(wsDash))
            .SeriesCollection.NewSeries
            .SeriesCollection(2).Name = "=""Сред"""
            .SeriesCollection(2).XValues = wsDash.Range("J3:J" & LastRow(wsDash))
            .SeriesCollection(2).Values = wsDash.Range("L3:L" & LastRow(wsDash))
            .SeriesCollection.NewSeries
            .SeriesCollection(3).Name = "=""Макс"""
            .SeriesCollection(3).XValues = wsDash.Range("J3:J" & LastRow(wsDash))
            .SeriesCollection(3).Values = wsDash.Range("M3:M" & LastRow(wsDash))
            .HasTitle = True
            .ChartTitle.Text = "Мин / сред / макс нагрузки ДЭС по блокам"
        End With
    End If

    If lastBoilRow >= 2 Then
        AggregateBoilersForDashboard wsBoil, wsDash

        Set ch = wsDash.ChartObjects.Add(Left:=20, Top:=960, Width:=860, Height:=260)
        With ch.Chart
            .ChartType = xlColumnClustered
            ClearSeries ch.Chart
            .SeriesCollection.NewSeries
            .SeriesCollection(1).Name = "=""Мин"""
            .SeriesCollection(1).XValues = wsDash.Range("J41:J" & LastRow(wsDash))
            .SeriesCollection(1).Values = wsDash.Range("K41:K" & LastRow(wsDash))
            .SeriesCollection.NewSeries
            .SeriesCollection(2).Name = "=""Сред"""
            .SeriesCollection(2).XValues = wsDash.Range("J41:J" & LastRow(wsDash))
            .SeriesCollection(2).Values = wsDash.Range("L41:L" & LastRow(wsDash))
            .SeriesCollection.NewSeries
            .SeriesCollection(3).Name = "=""Макс"""
            .SeriesCollection(3).XValues = wsDash.Range("J41:J" & LastRow(wsDash))
            .SeriesCollection(3).Values = wsDash.Range("M41:M" & LastRow(wsDash))
            .HasTitle = True
            .ChartTitle.Text = "Мин / сред / макс топлива котельных по блокам"
        End With
    End If

    If lastReasonsRow >= 2 Then
        Set ch = wsDash.ChartObjects.Add(Left:=20, Top:=1540, Width:=860, Height:=260)
        With ch.Chart
            .ChartType = xlBarClustered
            ClearSeries ch.Chart
            .SeriesCollection.NewSeries
            .SeriesCollection(1).Name = "=""Причины"""
            .SeriesCollection(1).XValues = wsReasons.Range("A2:A" & WorksheetFunction.Min(lastReasonsRow, 11))
            .SeriesCollection(1).Values = wsReasons.Range("B2:B" & WorksheetFunction.Min(lastReasonsRow, 11))
            .HasTitle = True
            .ChartTitle.Text = "Топ причин ремонтов"
        End With
    End If
End Sub

Private Sub AggregateDESForDashboard(ByVal wsSrc As Worksheet, ByVal wsDash As Worksheet)
    Dim dict As Object, r As Long, lastRowVal As Long, blockName As String
    Dim arr As Variant, outRow As Long, keys As Variant, i As Long
    Dim mn As Double, mx As Double, avgV As Double, outputV As Double, fuelV As Double

    Set dict = CreateObject("Scripting.Dictionary")
    lastRowVal = LastRow(wsSrc)

    wsDash.Range("J2:O200").Clear
    wsDash.Range("J2:O2").Value = Array("Блок", "Мин", "Сред", "Макс", "Выработка", "Топливо ДЭС")

    For r = 2 To lastRowVal
        blockName = Trim$(CStr(wsSrc.Cells(r, 2).Value))
        If blockName <> "" Then
            mn = ToNumber(wsSrc.Cells(r, 3).Value)
            mx = ToNumber(wsSrc.Cells(r, 4).Value)
            avgV = ToNumber(wsSrc.Cells(r, 5).Value)
            outputV = ToNumber(wsSrc.Cells(r, 7).Value)
            fuelV = ToNumber(wsSrc.Cells(r, 8).Value)

            If dict.Exists(blockName) Then
                arr = dict(blockName)
                If arr(1) = 0 Or (mn > 0 And mn < arr(1)) Then arr(1) = mn
                If mx > arr(3) Then arr(3) = mx
                arr(2) = arr(2) + avgV
                arr(4) = arr(4) + outputV
                arr(5) = arr(5) + fuelV
                arr(6) = arr(6) + 1
            Else
                ReDim arr(1 To 6)
                arr(1) = mn
                arr(2) = avgV
                arr(3) = mx
                arr(4) = outputV
                arr(5) = fuelV
                arr(6) = 1
            End If
            dict(blockName) = arr
        End If
    Next r

    keys = dict.Keys
    SortStringKeys keys

    outRow = 3
    For i = LBound(keys) To UBound(keys)
        arr = dict(keys(i))
        wsDash.Cells(outRow, 10).Value = keys(i)
        wsDash.Cells(outRow, 11).Value = arr(1)
        wsDash.Cells(outRow, 12).Value = arr(2) / arr(6)
        wsDash.Cells(outRow, 13).Value = arr(3)
        wsDash.Cells(outRow, 14).Value = arr(4)
        wsDash.Cells(outRow, 15).Value = arr(5)
        outRow = outRow + 1
    Next i
End Sub

Private Sub AggregateBoilersForDashboard(ByVal wsSrc As Worksheet, ByVal wsDash As Worksheet)
    Dim dict As Object, r As Long, lastRowVal As Long, blockName As String
    Dim arr As Variant, outRow As Long, keys As Variant, i As Long
    Dim mn As Double, mx As Double, avgV As Double, sumV As Double

    Set dict = CreateObject("Scripting.Dictionary")
    lastRowVal = LastRow(wsSrc)

    wsDash.Range("J40:N250").Clear
    wsDash.Range("J40:N40").Value = Array("Блок", "Мин", "Сред", "Макс", "Сумма")

    For r = 2 To lastRowVal
        blockName = Trim$(CStr(wsSrc.Cells(r, 2).Value))
        If blockName <> "" Then
            mn = ToNumber(wsSrc.Cells(r, 3).Value)
            mx = ToNumber(wsSrc.Cells(r, 4).Value)
            avgV = ToNumber(wsSrc.Cells(r, 5).Value)
            sumV = ToNumber(wsSrc.Cells(r, 6).Value)

            If dict.Exists(blockName) Then
                arr = dict(blockName)
                If arr(1) = 0 Or (mn > 0 And mn < arr(1)) Then arr(1) = mn
                If mx > arr(3) Then arr(3) = mx
                arr(2) = arr(2) + avgV
                arr(4) = arr(4) + sumV
                arr(5) = arr(5) + 1
            Else
                ReDim arr(1 To 5)
                arr(1) = mn
                arr(2) = avgV
                arr(3) = mx
                arr(4) = sumV
                arr(5) = 1
            End If
            dict(blockName) = arr
        End If
    Next r

    keys = dict.Keys
    SortStringKeys keys

    outRow = 41
    For i = LBound(keys) To UBound(keys)
        arr = dict(keys(i))
        wsDash.Cells(outRow, 10).Value = keys(i)
        wsDash.Cells(outRow, 11).Value = arr(1)
        wsDash.Cells(outRow, 12).Value = arr(2) / arr(5)
        wsDash.Cells(outRow, 13).Value = arr(3)
        wsDash.Cells(outRow, 14).Value = arr(4)
        outRow = outRow + 1
    Next i
End Sub

' =========================
' ОФОРМЛЕНИЕ
' =========================
Private Sub FormatSheets(ByVal wsSummary As Worksheet, ByVal wsRepairs As Worksheet, ByVal wsReasons As Worksheet, ByVal wsFiles As Worksheet, ByVal wsDash As Worksheet, ByVal wsDES As Worksheet, ByVal wsBoil As Worksheet)
    FormatOneSheet wsSummary
    FormatOneSheet wsRepairs
    FormatOneSheet wsReasons
    FormatOneSheet wsFiles
    FormatOneSheet wsDES
    FormatOneSheet wsBoil

    wsSummary.Columns("A").NumberFormat = "dd.mm.yyyy"
    wsRepairs.Columns("A").NumberFormat = "dd.mm.yyyy"
    wsFiles.Columns("B").NumberFormat = "dd.mm.yyyy"
    wsDES.Columns("A").NumberFormat = "dd.mm.yyyy"
    wsBoil.Columns("A").NumberFormat = "dd.mm.yyyy"

    wsDash.Columns("A:F").ColumnWidth = 16
    wsDash.Columns("J:O").EntireColumn.AutoFit
    wsDash.Range("A3:E7").Font.Bold = True
End Sub

Private Sub FormatOneSheet(ByVal ws As Worksheet)
    With ws.Rows(1)
        .Font.Bold = True
        .Interior.Color = RGB(217, 225, 242)
    End With
    ws.Cells.EntireColumn.AutoFit
    On Error Resume Next
    ws.Rows(1).AutoFilter
    On Error GoTo 0
End Sub

' =========================
' УТИЛИТЫ
' =========================
Private Function GetOrCreateSheet(ByVal sheetName As String) As Worksheet
    Dim ws As Worksheet
    For Each ws In ThisWorkbook.Worksheets
        If LCase$(ws.Name) = LCase$(sheetName) Then
            Set GetOrCreateSheet = ws
            Exit Function
        End If
    Next ws

    Set ws = ThisWorkbook.Worksheets.Add(After:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count))
    ws.Name = Left$(sheetName, 31)
    Set GetOrCreateSheet = ws
End Function

Private Function ExtractDateFromFileName(ByVal fileName As String) As Variant
    Dim re As Object, m As Object
    Dim d As Integer, mn As Integer, y As Integer

    Set re = CreateObject("VBScript.RegExp")
    re.Global = False
    re.IgnoreCase = True
    re.Pattern = "(\d{1,2})[.\-_](\d{1,2})[.\-_](\d{4})"

    If re.Test(fileName) Then
        Set m = re.Execute(fileName)(0)
        d = CInt(m.SubMatches(0))
        mn = CInt(m.SubMatches(1))
        y = CInt(m.SubMatches(2))
        ExtractDateFromFileName = DateSerial(y, mn, d)
    Else
        ExtractDateFromFileName = Empty
    End If
End Function

Private Function LastRow(ByVal ws As Worksheet) As Long
    Dim f As Range
    On Error Resume Next
    Set f = ws.Cells.Find(What:="*", After:=ws.Range("A1"), LookAt:=xlPart, LookIn:=xlFormulas, SearchOrder:=xlByRows, SearchDirection:=xlPrevious)
    On Error GoTo 0

    If f Is Nothing Then
        LastRow = 1
    Else
        LastRow = f.Row
    End If
End Function

Private Function ToNumber(ByVal v As Variant) As Double
    Dim s As String

    If IsNumeric(v) Then
        ToNumber = CDbl(v)
        Exit Function
    End If

    s = Trim$(CStr(v))
    If s = "" Or s = "-" Then Exit Function

    s = Replace(s, Chr(160), "")
    s = Replace(s, " ", "")
    s = Replace(s, "кВт", "", , , vbTextCompare)
    s = Replace(s, "квт", "", , , vbTextCompare)
    s = Replace(s, "л.", "", , , vbTextCompare)
    s = Replace(s, "л", "", , , vbTextCompare)
    s = Replace(s, "м³", "", , , vbTextCompare)
    s = Replace(s, "м3", "", , , vbTextCompare)
    s = Replace(s, ",", ".")

    If IsNumeric(s) Then ToNumber = CDbl(s)
End Function

Private Function RowText(ByVal ws As Worksheet, ByVal r As Long) As String
    Dim lastCol As Long, c As Long
    Dim s As String, v As String

    lastCol = ws.Cells(r, ws.Columns.Count).End(xlToLeft).Column
    For c = 1 To lastCol
        v = Trim$(CStr(ws.Cells(r, c).Text))
        If v <> "" Then s = s & " " & v
    Next c
    RowText = Trim$(s)
End Function

Private Function CountNumericInRange(ByVal ws As Worksheet, ByVal r As Long, ByVal c1 As Long, ByVal c2 As Long) As Long
    Dim c As Long
    For c = c1 To c2
        If ToNumber(ws.Cells(r, c).Value) <> 0 Then CountNumericInRange = CountNumericInRange + 1
    Next c
End Function

Private Sub LogFile(ByVal ws As Worksheet, ByVal rowNum As Long, ByVal fileName As String, ByVal dt As Variant, ByVal statusText As String, ByVal commentText As String)
    ws.Cells(rowNum, 1).Value = fileName
    If IsDate(dt) Then ws.Cells(rowNum, 2).Value = CDate(dt)
    ws.Cells(rowNum, 3).Value = statusText
    ws.Cells(rowNum, 4).Value = commentText
End Sub

Private Sub ClearSeries(ByVal ch As Chart)
    On Error Resume Next
    Do While ch.SeriesCollection.Count > 0
        ch.SeriesCollection(1).Delete
    Loop
    On Error GoTo 0
End Sub

Private Sub DeleteAllCharts(ByVal ws As Worksheet)
    Dim i As Long
    For i = ws.ChartObjects.Count To 1 Step -1
        ws.ChartObjects(i).Delete
    Next i
End Sub

Private Sub SortDateKeys(ByRef arr As Variant)
    Dim i As Long, j As Long, tmp As Variant
    For i = LBound(arr) To UBound(arr) - 1
        For j = i + 1 To UBound(arr)
            If CDate(arr(j)) < CDate(arr(i)) Then
                tmp = arr(i)
                arr(i) = arr(j)
                arr(j) = tmp
            End If
        Next j
    Next i
End Sub

Private Sub SortStringKeys(ByRef arr As Variant)
    Dim i As Long, j As Long, tmp As Variant
    For i = LBound(arr) To UBound(arr) - 1
        For j = i + 1 To UBound(arr)
            If CStr(arr(j)) < CStr(arr(i)) Then
                tmp = arr(i)
                arr(i) = arr(j)
                arr(j) = tmp
            End If
        Next j
    Next i
End Sub