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


Option Explicit

Private Const FIRST_DATA_ROW As Long = 5

Private Const ID_COLUMN As Long = 1          ' A
Private Const STATUS_COLUMN As Long = 9      ' I

Private Const US_DAYS_COLUMN As Long = 14    ' N
Private Const OKB_DAYS_COLUMN As Long = 15   ' O

Private Const HEADER_ROW As Long = 4

Private Const HISTORY_SHEET_NAME As String = "ИсторияСтатусов"


Private Sub Worksheet_Change(ByVal Target As Range)

    Dim wsHistory As Worksheet

    Dim oldValue As String
    Dim newValue As String

    Dim objectID As Variant
    Dim historyRow As Long

    ' Обрабатываем только изменение одной ячейки
    If Target.CountLarge <> 1 Then Exit Sub

    ' Только столбец I
    If Target.Column <> STATUS_COLUMN Then Exit Sub

    ' Только начиная с I5
    If Target.Row < FIRST_DATA_ROW Then Exit Sub

    On Error GoTo ErrorHandler

    ' Сначала сохраняем новое значение.
    ' До Application.Undo нельзя изменять книгу.
    newValue = Trim$(CStr(Target.Value))

    ' Получаем старое значение
    Application.Undo

    oldValue = Trim$(CStr(Target.Value))

    ' Теперь отключаем события перед возвратом нового значения
    Application.EnableEvents = False

    ' Возвращаем новое значение
    Target.Value = newValue

    ' Если фактически ничего не изменилось
    If StrComp(oldValue, newValue, vbTextCompare) = 0 Then
        GoTo SafeExit
    End If

    ' Получаем уникальный ID из столбца A
    objectID = Me.Cells(Target.Row, ID_COLUMN).Value

    If Len(Trim$(CStr(objectID))) = 0 Then

        MsgBox _
            "Статус изменен, но история не записана." & vbCrLf & vbCrLf & _
            "В строке " & Target.Row & _
            " отсутствует уникальный ID в столбце A.", _
            vbExclamation, _
            "Не указан ID"

        GoTo SafeExit

    End If

    ' Проверяем / создаем служебную структуру
    Set wsHistory = EnsureStructure()

    ' Следующая свободная строка журнала
    historyRow = wsHistory.Cells( _
        wsHistory.Rows.Count, "A").End(xlUp).Row + 1

    If historyRow < 2 Then historyRow = 2

    ' Записываем переход
    With wsHistory

        .Cells(historyRow, "A").Value = objectID
        .Cells(historyRow, "B").Value = oldValue
        .Cells(historyRow, "C").Value = newValue
        .Cells(historyRow, "D").Value = Now

    End With

    ' Пересчитываем количество рабочих дней
    UpdateStatusDays objectID, Target.Row, wsHistory

SafeExit:

    Application.EnableEvents = True
    Exit Sub


ErrorHandler:

    Application.EnableEvents = True

    MsgBox _
        "Ошибка при обработке изменения статуса:" & vbCrLf & vbCrLf & _
        Err.Description, _
        vbExclamation, _
        "Ошибка"

End Sub


Private Function EnsureStructure() As Worksheet

    Dim wsHistory As Worksheet

    ' ---------------------------------------------------------
    ' Ищем лист истории
    ' ---------------------------------------------------------

    On Error Resume Next

    Set wsHistory = _
        ThisWorkbook.Worksheets(HISTORY_SHEET_NAME)

    On Error GoTo 0

    ' ---------------------------------------------------------
    ' Если листа нет — создаем
    ' ---------------------------------------------------------

    If wsHistory Is Nothing Then

        Set wsHistory = ThisWorkbook.Worksheets.Add( _
            After:=ThisWorkbook.Worksheets( _
                ThisWorkbook.Worksheets.Count))

        wsHistory.Name = HISTORY_SHEET_NAME

    End If

    ' ---------------------------------------------------------
    ' Заголовки листа истории
    ' ---------------------------------------------------------

    With wsHistory

        .Cells(1, "A").Value = "ID"
        .Cells(1, "B").Value = "Старый статус"
        .Cells(1, "C").Value = "Новый статус"
        .Cells(1, "D").Value = "Дата изменения"

        .Columns("D").NumberFormat = "dd.mm.yyyy hh:mm:ss"

    End With

    ' ---------------------------------------------------------
    ' Заголовки основной таблицы
    ' ---------------------------------------------------------

    Me.Cells(HEADER_ROW, US_DAYS_COLUMN).Value = _
        "Рабочих дней УС"

    Me.Cells(HEADER_ROW, OKB_DAYS_COLUMN).Value = _
        "Рабочих дней ОКБ"

    Me.Columns(US_DAYS_COLUMN).NumberFormat = "0.00"
    Me.Columns(OKB_DAYS_COLUMN).NumberFormat = "0.00"

    Set EnsureStructure = wsHistory

End Function


Private Sub UpdateStatusDays( _
    ByVal objectID As Variant, _
    ByVal sourceRow As Long, _
    ByVal wsHistory As Worksheet)

    Dim lastRow As Long
    Dim i As Long

    Dim eventID As Variant
    Dim newStatus As String
    Dim nextStatus As String

    Dim startDate As Date
    Dim endDate As Date

    Dim usDays As Double
    Dim okbDays As Double

    lastRow = wsHistory.Cells( _
        wsHistory.Rows.Count, "A").End(xlUp).Row

    ' Идем по истории конкретного ID
    For i = 2 To lastRow

        eventID = wsHistory.Cells(i, "A").Value

        If CStr(eventID) = CStr(objectID) Then

            newStatus = _
                Trim$(CStr(wsHistory.Cells(i, "C").Value))

            ' Нас интересуют только УС и ОКБ
            If StrComp(newStatus, "УС", vbTextCompare) = 0 _
            Or StrComp(newStatus, "ОКБ", vbTextCompare) = 0 Then

                startDate = wsHistory.Cells(i, "D").Value

                ' Ищем следующее изменение этого же ID
                endDate = GetNextChangeDate( _
                    wsHistory, _
                    objectID, _
                    i + 1, _
                    lastRow)

                ' Если следующего изменения нет,
                ' статус считается текущим до настоящего момента
                If endDate = 0 Then
                    endDate = Now
                End If

                If StrComp( _
                    newStatus, _
                    "УС", _
                    vbTextCompare) = 0 Then

                    usDays = usDays + _
                        BusinessDaysBetween(startDate, endDate)

                ElseIf StrComp( _
                    newStatus, _
                    "ОКБ", _
                    vbTextCompare) = 0 Then

                    okbDays = okbDays + _
                        BusinessDaysBetween(startDate, endDate)

                End If

            End If

        End If

    Next i

    Me.Cells(sourceRow, US_DAYS_COLUMN).Value = usDays
    Me.Cells(sourceRow, OKB_DAYS_COLUMN).Value = okbDays

End Sub


Private Function GetNextChangeDate( _
    ByVal wsHistory As Worksheet, _
    ByVal objectID As Variant, _
    ByVal startRow As Long, _
    ByVal lastRow As Long) As Date

    Dim i As Long

    For i = startRow To lastRow

        If CStr(wsHistory.Cells(i, "A").Value) = _
           CStr(objectID) Then

            GetNextChangeDate = _
                wsHistory.Cells(i, "D").Value

            Exit Function

        End If

    Next i

    GetNextChangeDate = 0

End Function


Private Function BusinessDaysBetween( _
    ByVal startDate As Date, _
    ByVal endDate As Date) As Double

    Dim currentDate As Date

    Dim dayStart As Date
    Dim dayEnd As Date

    Dim intervalStart As Date
    Dim intervalEnd As Date

    Dim totalDays As Double

    ' Защита от некорректного интервала
    If endDate <= startDate Then
        BusinessDaysBetween = 0
        Exit Function
    End If

    currentDate = Int(startDate)

    Do While currentDate <= Int(endDate)

        ' Понедельник = 1 ... Пятница = 5
        If Weekday(currentDate, vbMonday) <= 5 Then

            dayStart = currentDate
            dayEnd = currentDate + 1

            If startDate > dayStart Then
                intervalStart = startDate
            Else
                intervalStart = dayStart
            End If

            If endDate < dayEnd Then
                intervalEnd = endDate
            Else
                intervalEnd = dayEnd
            End If

            If intervalEnd > intervalStart Then

                totalDays = totalDays + _
                    (intervalEnd - intervalStart)

            End If

        End If

        currentDate = currentDate + 1

    Loop

    BusinessDaysBetween = totalDays

End Function