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


'== Модуль: UserForm1 (форма пустая, элементы строит код) — вставьте как ВЕСЬ код модуля (Ctrl+A, затем вставка). ==
Option Explicit

' --- Win32 API: держать форму поверх всех окон при скрытом Excel (Excel 2007, 32-бит) ---
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function SetWindowPos Lib "user32" (ByVal hWnd As Long, ByVal hWndInsertAfter As Long, ByVal X As Long, ByVal Y As Long, ByVal cx As Long, ByVal cy As Long, ByVal wFlags As Long) As Long
Private Const HWND_TOPMOST As Long = -1
Private Const HWND_NOTOPMOST As Long = -2
Private Const SWP_NOMOVE As Long = &H2
Private Const SWP_NOSIZE As Long = &H1

Private mH As Collection
Private mHooked As Object          ' п.4 — имена элементов, уже подписанных на события
Private mBusy As Boolean
Private mOrder() As String
Private mNArea As Long
Private mNCommon As Long
Private mRowsC As Long
Private mFlacons() As String
Private mPays() As String

Private Const RARE_LIST As String = "легкие/средостение|сердце|молочная железа|голова-шея|прочее"
Private Const YLOW_HIDDEN As Single = 270
Private Const YLOW_SHOWN As Single = 354      ' контраст стал чуть выше (заголовок флаконов)
Private Const MAX_FORM_H As Single = 620       ' выше окно не растёт (редкие области — с прокруткой)

Private txtNumber As MSForms.TextBox
Private txtStudyDate As MSForms.TextBox
Private PatientName As MSForms.TextBox
Private DoB As MSForms.TextBox
Private cboDept As MSForms.ComboBox
Private History_number As MSForms.TextBox
Private txtArea As MSForms.TextBox
Private optAnestYes As MSForms.OptionButton
Private optAnestNo As MSForms.OptionButton
Private optContrastYes As MSForms.OptionButton
Private optContrastNo As MSForms.OptionButton
Private cboContrast As MSForms.ComboBox
Private txtDose As MSForms.TextBox
Private cboPerf As MSForms.ComboBox
Private chkRare As MSForms.CheckBox
Private frContrast As MSForms.Frame
Private frStudies As MSForms.Frame
Private frPay As MSForms.Frame
Private lblWarn As MSForms.Label
Private btnMain As MSForms.CommandButton
Private btnDelete As MSForms.CommandButton
Private btnCancelPat As MSForms.CommandButton
Private btnSettings As MSForms.CommandButton
Private btnExport As MSForms.CommandButton
Private btnDiscBurn As MSForms.CommandButton   ' крупная кнопка «Записать диск» (в обоих режимах окна)
Private btnImport As MSForms.CommandButton     ' «Выгрузить пациента» из другого журнала (включается в Настройках)
Private lblPerfHead As MSForms.Label           ' надпись «Исполнитель» (сдвигается, когда есть btnImport)
Private mEdit As Boolean                       ' окно в режиме редактирования записи

Private Const fmDDL As Integer = 2
Private Const FS As Single = 9                ' базовый размер шрифта (п.6 — крупнее)

Private Sub UserForm_Initialize()
    Set mH = New Collection
    Set mHooked = CreateObject("Scripting.Dictionary")
    ReadArrays
    BuildForm
    If CurrentRow > 0 And Not gEditSheet Is Nothing Then FillData Else ResetNew
End Sub

Private Sub ReadArrays()
    Dim a As Variant, i As Long, common() As String, rare() As String, nc As Long, nr As Long
    a = GetList("области")
    mFlacons = ToStrArray(GetList("флаконы"))
    mPays = ToStrArray(GetList("оплата"))
    ReDim common(0 To 200): ReDim rare(0 To 200): nc = 0: nr = 0
    If IsArray(a) Then
        If UBound(a) >= 0 Then
            For i = 0 To UBound(a)
                If IsRareArea(CStr(a(i))) Then
                    rare(nr) = CStr(a(i)): nr = nr + 1
                Else
                    common(nc) = CStr(a(i)): nc = nc + 1
                End If
            Next i
        End If
    End If
    mNCommon = nc: mNArea = nc + nr
    ReDim mOrder(0 To IIf(mNArea = 0, 0, mNArea - 1))
    For i = 0 To nc - 1: mOrder(i) = common(i): Next i
    For i = 0 To nr - 1: mOrder(nc + i) = rare(i): Next i
    mRowsC = (mNCommon + 1) \ 2: If mRowsC < 1 Then mRowsC = 1
End Sub
Private Function IsRareArea(ByVal nm As String) As Boolean
    IsRareArea = (InStr("|" & RARE_LIST & "|", "|" & nm & "|") > 0)
End Function
Private Function ToStrArray(ByVal v As Variant) As String()
    Dim r() As String, i As Long
    If IsArray(v) Then
        If UBound(v) >= 0 Then
            ReDim r(0 To UBound(v))
            For i = 0 To UBound(v): r(i) = CStr(v(i)): Next i
            ToStrArray = r: Exit Function
        End If
    End If
    ReDim r(0 To -1): ToStrArray = r
End Function

'--------------------- ПОСТРОЕНИЕ ОКНА ---------------------
Private Sub BuildForm()
    Dim clrHead As Long, clrPrimary As Long, clrDel As Long, clrCancel As Long, clrExp As Long
    clrHead = RGB(28, 62, 110): clrPrimary = RGB(46, 120, 90)
    clrDel = RGB(150, 55, 55): clrCancel = RGB(196, 138, 46): clrExp = RGB(60, 90, 150)

    Me.Caption = "Журнал МРТ — регистрация"
    Me.BackColor = RGB(244, 247, 250)
    Me.Width = 340: Me.Height = 400

    Set btnSettings = AddButton("btnSettings", 318, 4, 16, 14, ChrW(9881), "SETTINGS")
    btnSettings.Font.Size = 8: btnSettings.BackColor = RGB(238, 241, 244)
    btnSettings.ForeColor = RGB(150, 160, 170): btnSettings.ControlTipText = "Настройки (пароль)"

    Dim be As MSForms.CommandButton
    Set be = AddButton("btnEye", 318, 20, 16, 14, "N", "EYE")   ' Webdings "N" = глаз, под шестерёнкой
    be.Font.Name = "Webdings": be.Font.Size = 9
    be.BackColor = RGB(238, 241, 244): be.ForeColor = RGB(150, 160, 170)
    be.ControlTipText = "Скрыть/показать окно Excel"

    AddHead "Дата", 10, 8, 34, clrHead
    Set txtStudyDate = AddText("txtStudyDate", 44, 6, 72): HookText txtStudyDate, "DATE"
    AddHead "№", 10, 30, 18, clrHead
    Set txtNumber = AddText("txtNumber", 44, 28, 58)
    Set lblPerfHead = AddLabel("Исполнитель", 172, 8, 90, 13)
    lblPerfHead.ForeColor = clrHead
    Set cboPerf = AddCombo("cboPerf", 172, 22, 120, "исполнители")
    ' «Выгрузить пациента» — вверху справа; видна, если включено в Настройках (ShowImportBtn)
    Set btnImport = AddButton("btnImport", 172, 2, 142, 18, "Выгрузить пациента", "IMPORT")
    btnImport.Font.Size = 8: btnImport.BackColor = RGB(60, 90, 150): btnImport.ForeColor = vbWhite
    btnImport.ControlTipText = "Взять данные пациента из другого журнала (путь — в Настройках)"
    btnImport.Visible = False

    AddHead "ФИО", 10, 48, 40, clrHead
    Set PatientName = AddText("PatientName", 10, 62, 238)

    AddHead "Дата рождения", 10, 82, 90, clrHead
    Set DoB = AddText("DoB", 10, 96, 80): HookText DoB, "DOB"
    AddHead "№ ист. болезни", 152, 82, 90, clrHead
    Set History_number = AddText("History_number", 152, 96, 78)

    AddHead "Отделение", 10, 116, 66, clrHead
    Set cboDept = AddCombo("cboDept", 10, 130, 76, "отделения")
    AddHead "Область исследования", 152, 116, 140, clrHead
    Set txtArea = AddText("txtArea", 152, 130, 178)

    ' Анестезия и Оплата — в отдельных рамках, варианты в столбец
    Dim frAnest As MSForms.Frame
    Set frAnest = AddFrame("frAnest", 10, 150, 96, 72, "Анестезия")
    frAnest.ForeColor = clrHead: frAnest.BackColor = RGB(244, 247, 250)
    AddOptionIn frAnest, "optAnestYes", 8, 20, 78, "да", "ANEST"
    AddOptionIn frAnest, "optAnestNo", 8, 44, 78, "нет", "ANEST"
    Set optAnestYes = frAnest.Controls("optAnestYes")
    Set optAnestNo = frAnest.Controls("optAnestNo")

    Set frPay = AddFrame("frPay", 176, 150, 152, 72, "Оплата")
    frPay.ForeColor = clrHead: frPay.BackColor = RGB(244, 247, 250)
    Dim i As Long, py As Single: py = 15
    For i = 0 To UBoundS(mPays)
        AddOptionIn frPay, "optPay_" & i, 8, py, 132, mPays(i), "PAY"
        py = py + 16
    Next i

    ' п.2 — выбор «да/нет» вместо галочки; «да» раскрывает поля контраста
    Dim frCT As MSForms.Frame
    Set frCT = AddFrame("frContrToggle", 10, 222, 320, 42, "Введение контрастного препарата")
    frCT.ForeColor = clrHead
    AddOptionIn frCT, "optContrastYes", 8, 16, 60, "да", "grpContrast"
    AddOptionIn frCT, "optContrastNo", 74, 16, 60, "нет", "grpContrast"
    Set optContrastYes = frCT.Controls("optContrastYes")
    Set optContrastNo = frCT.Controls("optContrastNo")
    HookOpt optContrastYes, "CONTRYES"
    HookOpt optContrastNo, "CONTRNO"
    Set frContrast = AddFrame("frContrast", 10, YLOW_HIDDEN, 320, 80, "")
    frContrast.BackColor = RGB(238, 244, 249)
    AddLabelIn frContrast, "Препарат", 6, 6, 54
    Set cboContrast = AddComboIn(frContrast, "cboContrast", 62, 4, 90, "контрасты")
    AddLabelIn frContrast, "Доза, мл", 162, 6, 50
    Set txtDose = AddTextIn(frContrast, "txtDose", 214, 4, 50)
    AddLabelIn frContrast, "Списываемые флаконы контраста:", 6, 26, 300   ' п.4 — подпись
    Dim fy As Single: fy = 44
    For i = 0 To UBoundS(mFlacons)
        AddCheckIn frContrast, "chkFlac_" & i, 8, fy, 58, mFlacons(i)      ' п.4 — счётчик ближе
        Dim sp As MSForms.SpinButton
        Set sp = AddSpinIn(frContrast, "spnFlac_" & i, 66, fy, 14, 16)
        AddLabelIn frContrast, "1", 84, fy + 1, 22, "lblFlac_" & i
        HookSpin sp, "lblFlac_" & i
        fy = fy + 18
    Next i
    frContrast.Visible = False

    Set frStudies = AddFrame("frStudies", 10, YLOW_HIDDEN, 320, 84, "Для формы №30")
    frStudies.ForeColor = clrHead: frStudies.BackColor = RGB(242, 246, 248)
    frStudies.ScrollBars = fmScrollBarsNone         ' высота рамки — по числу строк (PlaceLower)
    Dim colW As Single: colW = 152
    Dim gx As Single, gy As Single, col As Long, row As Long
    For i = 0 To mNArea - 1
        If i < mNCommon Then
            col = i \ mRowsC: row = i Mod mRowsC
        Else
            Dim j As Long: j = i - mNCommon
            col = j \ 3: row = mRowsC + (j Mod 3)
        End If
        gx = 6 + col * colW: gy = 6 + row * 19
        AddCheckIn frStudies, "chkStudy_" & i, gx, gy, 100, mOrder(i)
        Dim sp2 As MSForms.SpinButton
        Set sp2 = AddSpinIn(frStudies, "spnStudy_" & i, gx + 102, gy, 14, 16)
        AddLabelIn frStudies, "1", gx + 118, gy + 1, 16, "lblStudy_" & i
        HookSpin sp2, "lblStudy_" & i
        If i >= mNCommon Then
            frStudies.Controls("chkStudy_" & i).Visible = False
            frStudies.Controls("spnStudy_" & i).Visible = False
            frStudies.Controls("lblStudy_" & i).Visible = False
        End If
    Next i

    Set chkRare = AddCheck("chkRare", 10, 0, 220, "Показать редкие области")
    HookChk chkRare, "RARE"

    Set lblWarn = AddLabel("", 10, 0, 320, 18)
    lblWarn.ForeColor = RGB(170, 0, 0): lblWarn.Font.Bold = True: lblWarn.Font.Size = 10
    lblWarn.BackStyle = 1: lblWarn.BackColor = RGB(255, 225, 225)      ' заметнее: розовый фон
    lblWarn.WordWrap = True: lblWarn.Visible = False

    Set btnMain = AddButton("btnMain", 10, 0, 120, 24, "Зарегистрировать", "REGISTER")
    btnMain.BackColor = clrPrimary: btnMain.ForeColor = vbWhite: btnMain.Font.Bold = True
    Set btnDelete = AddButton("btnDelete", 136, 0, 92, 24, "Удалить запись", "DELETE")
    btnDelete.BackColor = clrDel: btnDelete.ForeColor = vbWhite
    Set btnCancelPat = AddButton("btnCancelPat", 232, 0, 98, 24, "Отменить пациента", "CANCELPAT")
    btnCancelPat.BackColor = clrCancel: btnCancelPat.ForeColor = vbWhite
    Set btnExport = AddButton("btnExport", 150, 0, 180, 22, "Выгрузить данные в журналы", "EXPORT")
    btnExport.BackColor = clrExp: btnExport.ForeColor = vbWhite
    ' крупная кнопка записи диска — и в окне регистрации, и в окне редактирования
    Set btnDiscBurn = AddButton("btnDiscBurn", 175, 0, 155, 28, "Записать диск", "DISC")
    btnDiscBurn.BackColor = RGB(96, 72, 140): btnDiscBurn.ForeColor = vbWhite
    btnDiscBurn.Font.Bold = True: btnDiscBurn.Font.Size = 10

    PlaceLower YLOW_HIDDEN
    HookAllForF9 Me.Controls          ' п.4 — F9 закрывает окно из любого поля
End Sub

' Нижняя часть окна: рамка «Для формы №30» (высота — по числу строк областей, без
' полосы прокрутки), «редкие области», предупреждение, кнопки
Private Sub PlaceLower(ByVal topY As Single)
    Dim fh As Single, need As Single, y As Single, by As Single
    need = StudiesFrameHeight()
    fh = need
    If topY + fh + 38 + 56 + 36 > MAX_FORM_H Then fh = MAX_FORM_H - (topY + 38 + 56 + 36)   ' очень высокое окно
    If fh < 60 Then fh = 60
    frStudies.Top = topY: frStudies.Height = fh
    If fh < need Then
        frStudies.ScrollBars = fmScrollBarsVertical
        frStudies.ScrollHeight = need - 18
    Else
        frStudies.ScrollBars = fmScrollBarsNone
        frStudies.ScrollTop = 0
    End If
    y = topY + fh + 4
    chkRare.Top = y
    lblWarn.Top = y + 16
    by = y + 38
    btnMain.Top = by: btnDelete.Top = by: btnCancelPat.Top = by
    If mEdit Then
        btnDiscBurn.Top = by + 28: btnExport.Top = by + 28      ' ряд 2: диск + выгрузка
    Else
        btnDiscBurn.Top = by: btnExport.Top = by + 32           ' ряд 1: регистрация + диск
    End If
    Me.Height = by + 56 + 36
End Sub

' Высота рамки, чтобы все видимые строки областей помещались без прокрутки
Private Function StudiesFrameHeight() As Single
    Dim nRows As Long, nRare As Long
    nRows = mRowsC
    If chkRare.Value Then
        nRare = mNArea - mNCommon
        If nRare > 3 Then nRare = 3
        nRows = nRows + nRare
    End If
    StudiesFrameHeight = nRows * 19 + 26
End Function

'--------------------- Хелперы (шрифт FS=9) ---------------------
Private Sub AddHead(cap As String, x As Single, y As Single, w As Single, clr As Long)
    Dim c As MSForms.Label
    Set c = Me.Controls.Add("Forms.Label.1", "lb" & Me.Controls.Count, True)
    c.Caption = cap: c.Left = x: c.Top = y: c.Width = w: c.Height = 13: c.Font.Size = FS: c.ForeColor = clr
End Sub
Private Function AddLabel(cap As String, x As Single, y As Single, w As Single, h As Single) As MSForms.Label
    Dim c As MSForms.Label
    Set c = Me.Controls.Add("Forms.Label.1", "lb" & Me.Controls.Count, True)
    c.Caption = cap: c.Left = x: c.Top = y: c.Width = w: c.Height = h: c.Font.Size = FS
    Set AddLabel = c
End Function
Private Function AddText(nm As String, x As Single, y As Single, w As Single) As MSForms.TextBox
    Dim c As MSForms.TextBox
    Set c = Me.Controls.Add("Forms.TextBox.1", nm, True)
    c.Left = x: c.Top = y: c.Width = w: c.Height = 18: c.Font.Size = FS
    Set AddText = c
End Function
Private Function AddCombo(nm As String, x As Single, y As Single, w As Single, listHdr As String) As MSForms.ComboBox
    Dim c As MSForms.ComboBox
    Set c = Me.Controls.Add("Forms.ComboBox.1", nm, True)
    c.Left = x: c.Top = y: c.Width = w: c.Height = 18: c.Font.Size = FS
    c.Style = fmDDL: c.MatchRequired = True
    FillCtl c, listHdr
    Set AddCombo = c
End Function
Private Function AddCheck(nm As String, x As Single, y As Single, w As Single, cap As String) As MSForms.CheckBox
    Dim c As MSForms.CheckBox
    Set c = Me.Controls.Add("Forms.CheckBox.1", nm, True)
    c.Caption = cap: c.Left = x: c.Top = y: c.Width = w: c.Height = 16: c.Font.Size = FS
    Set AddCheck = c
End Function
Private Function AddOption(nm As String, x As Single, y As Single, w As Single, cap As String, grp As String) As MSForms.OptionButton
    Dim c As MSForms.OptionButton
    Set c = Me.Controls.Add("Forms.OptionButton.1", nm, True)
    c.Caption = cap: c.Left = x: c.Top = y: c.Width = w: c.Height = 16: c.Font.Size = FS: c.GroupName = grp
    Set AddOption = c
End Function
Private Function AddFrame(nm As String, x As Single, y As Single, w As Single, h As Single, cap As String) As MSForms.Frame
    Dim c As MSForms.Frame
    Set c = Me.Controls.Add("Forms.Frame.1", nm, True)
    c.Left = x: c.Top = y: c.Width = w: c.Height = h: c.Caption = cap: c.Font.Size = FS
    Set AddFrame = c
End Function
Private Function AddButton(nm As String, x As Single, y As Single, w As Single, h As Single, cap As String, key As String) As MSForms.CommandButton
    Dim c As MSForms.CommandButton
    Set c = Me.Controls.Add("Forms.CommandButton.1", nm, True)
    c.Caption = cap: c.Left = x: c.Top = y: c.Width = w: c.Height = h: c.Font.Size = FS
    HookBtn c, key
    Set AddButton = c
End Function
Private Sub AddLabelIn(fr As MSForms.Frame, cap As String, x As Single, y As Single, w As Single, Optional nm As String = "")
    Dim c As MSForms.Label
    If nm = "" Then nm = "l_" & fr.Name & "_" & fr.Controls.Count
    Set c = fr.Controls.Add("Forms.Label.1", nm, True)
    c.Caption = cap: c.Left = x: c.Top = y: c.Width = w: c.Height = 14: c.Font.Size = FS
End Sub
Private Function AddTextIn(fr As MSForms.Frame, nm As String, x As Single, y As Single, w As Single) As MSForms.TextBox
    Dim c As MSForms.TextBox
    Set c = fr.Controls.Add("Forms.TextBox.1", nm, True)
    c.Left = x: c.Top = y: c.Width = w: c.Height = 18: c.Font.Size = FS
    Set AddTextIn = c
End Function
Private Function AddComboIn(fr As MSForms.Frame, nm As String, x As Single, y As Single, w As Single, listHdr As String) As MSForms.ComboBox
    Dim c As MSForms.ComboBox
    Set c = fr.Controls.Add("Forms.ComboBox.1", nm, True)
    c.Left = x: c.Top = y: c.Width = w: c.Height = 18: c.Font.Size = FS
    c.Style = fmDDL: c.MatchRequired = True
    FillCtl c, listHdr
    Set AddComboIn = c
End Function
Private Sub AddCheckIn(fr As MSForms.Frame, nm As String, x As Single, y As Single, w As Single, cap As String)
    Dim c As MSForms.CheckBox
    Set c = fr.Controls.Add("Forms.CheckBox.1", nm, True)
    c.Caption = cap: c.Left = x: c.Top = y: c.Width = w: c.Height = 16: c.Font.Size = FS
End Sub
Private Function AddSpinIn(fr As MSForms.Frame, nm As String, x As Single, y As Single, w As Single, h As Single) As MSForms.SpinButton
    Dim c As MSForms.SpinButton
    Set c = fr.Controls.Add("Forms.SpinButton.1", nm, True)
    c.Left = x: c.Top = y: c.Width = w: c.Height = h: c.Min = 1: c.Max = 50: c.Value = 1
    Set AddSpinIn = c
End Function
Private Sub AddOptionIn(fr As MSForms.Frame, nm As String, x As Single, y As Single, w As Single, cap As String, grp As String)
    Dim c As MSForms.OptionButton
    Set c = fr.Controls.Add("Forms.OptionButton.1", nm, True)
    c.Caption = cap: c.Left = x: c.Top = y: c.Width = w: c.Height = 16: c.Font.Size = FS: c.GroupName = grp
End Sub
Private Sub FillCtl(ByVal cbo As MSForms.ComboBox, ByVal header As String)
    Dim arr As Variant, i As Long
    cbo.Clear: arr = GetList(header)
    If IsArray(arr) Then
        If UBound(arr) >= 0 Then
            For i = 0 To UBound(arr): cbo.AddItem arr(i): Next i
        End If
    End If
End Sub
Private Function UBoundS(a() As String) As Long
    On Error Resume Next
    UBoundS = UBound(a): If Err.Number <> 0 Then UBoundS = -1
    On Error GoTo 0
End Function

Private Sub HookBtn(b As MSForms.CommandButton, k As String)
    Dim h As ctlEvents: Set h = New ctlEvents: Set h.Frm = Me: h.Key = k: Set h.Btn = b: mH.Add h: mHooked(b.Name) = True
End Sub
Private Sub HookSpin(s As MSForms.SpinButton, k As String)
    Dim h As ctlEvents: Set h = New ctlEvents: Set h.Frm = Me: h.Key = k: Set h.Spn = s: mH.Add h: mHooked(s.Name) = True
End Sub
Private Sub HookText(t As MSForms.TextBox, k As String)
    Dim h As ctlEvents: Set h = New ctlEvents: Set h.Frm = Me: h.Key = k: Set h.Txt = t: mH.Add h: mHooked(t.Name) = True
End Sub
Private Sub HookChk(ch As MSForms.CheckBox, k As String)
    Dim h As ctlEvents: Set h = New ctlEvents: Set h.Frm = Me: h.Key = k: Set h.Chk = ch: mH.Add h: mHooked(ch.Name) = True
End Sub
Private Sub HookOpt(o As MSForms.OptionButton, k As String)
    Dim h As ctlEvents: Set h = New ctlEvents: Set h.Frm = Me: h.Key = k: Set h.Opt = o: mH.Add h: mHooked(o.Name) = True
End Sub

' п.4 — подписать на F9 все элементы, у которых ещё нет обработчика
' (ФИО, № истории, отделение, оплата, анестезия, флаконы, области и т.д.)
Private Sub HookAllForF9(ByVal ctls As Object)
    Dim c As Object, h As ctlEvents, tn As String
    For Each c In ctls
        tn = TypeName(c)
        If tn = "Frame" Then
            HookAllForF9 c.Controls
        ElseIf Not mHooked.Exists(c.Name) Then
            Set h = Nothing
            Select Case tn
                Case "TextBox": Set h = New ctlEvents: Set h.Txt = c
                Case "ComboBox": Set h = New ctlEvents: Set h.cbo = c
                Case "CheckBox": Set h = New ctlEvents: Set h.Chk = c
                Case "OptionButton": Set h = New ctlEvents: Set h.Opt = c
                Case "CommandButton": Set h = New ctlEvents: Set h.Btn = c
                Case "SpinButton": Set h = New ctlEvents: Set h.Spn = c
            End Select
            If Not h Is Nothing Then
                Set h.Frm = Me: h.F9Only = True
                mH.Add h: mHooked(c.Name) = True
            End If
        End If
    Next c
End Sub

'--------------------- Диспетчеры ---------------------
Public Sub OnButton(ByVal k As String)
    Select Case k
        Case "REGISTER": DoRegister
        Case "DELETE": DoDelete
        Case "CANCELPAT": DoCancelPatient
        Case "SETTINGS": OpenSettings
        Case "IMPORT": ImportPatient
        Case "EYE": ToggleExcel
        Case "EXPORT"
            ' показать Excel, чтобы окна подтверждения и итога выгрузки были доступны
            On Error Resume Next
            Application.Visible = True
            SetTopMost False
            On Error GoTo 0
            ExportEndOfShift
        Case "DISC"
            ' окно записи диска (UserForm4, модуль modDisc); без записи журнала —
            ' пациента выбирают в самом окне (угадывается по последнему исследованию)
            On Error Resume Next
            Application.Visible = True
            SetTopMost False
            On Error GoTo 0
            If CurrentRow > 0 And Not gEditSheet Is Nothing Then
                OpenDiscWindow gEditSheet, CurrentRow, "burn"
            Else
                OpenDiscWindow Nothing, 0, "burn"
            End If
    End Select
End Sub
Public Sub OnSpin(ByVal k As String, ByVal v As Long)
    On Error Resume Next
    FindCtl(k).Caption = CStr(v)
    On Error GoTo 0
End Sub
Public Sub OnText(ByVal k As String)
    If mBusy Then Exit Sub
    If k = "DOB" Then ReformatDate DoB Else ReformatDate txtStudyDate   ' п.7
End Sub
Public Sub OnCheck(ByVal k As String, ByVal v As Boolean)
    If k = "CONTR" Then
        frContrast.Visible = v
        If v Then PlaceLower YLOW_SHOWN Else PlaceLower YLOW_HIDDEN
    ElseIf k = "RARE" Then
        ShowRare v
    End If
End Sub
Public Sub OnCombo(ByVal k As String)
End Sub
Public Sub OnOption(ByVal k As String)
    Select Case k
        Case "CONTRYES": If optContrastYes.Value Then ShowContrast True
        Case "CONTRNO": If optContrastNo.Value Then ShowContrast False
    End Select
End Sub
Private Sub ShowContrast(ByVal v As Boolean)
    frContrast.Visible = v
    If v Then PlaceLower YLOW_SHOWN Else PlaceLower YLOW_HIDDEN
End Sub

' п.7 — устойчивое проставление точек: работает из цифр, backspace корректен,
' двойные точки невозможны (все нецифры отбрасываются)
Private Sub ReformatDate(tb As MSForms.TextBox)
    Dim raw As String, d As String, i As Long, ch As String, s As String
    raw = tb.Text: d = ""
    For i = 1 To Len(raw)
        ch = Mid$(raw, i, 1)
        If ch >= "0" And ch <= "9" Then d = d & ch
    Next i
    If Len(d) > 8 Then d = Left$(d, 8)
    If Len(d) <= 2 Then
        s = d
    ElseIf Len(d) <= 4 Then
        s = Left$(d, 2) & "." & Mid$(d, 3)
    Else
        s = Left$(d, 2) & "." & Mid$(d, 3, 2) & "." & Mid$(d, 5)
    End If
    If s = raw Then Exit Sub
    mBusy = True
    tb.Text = s: tb.SelStart = Len(s)
    mBusy = False
End Sub

Private Sub ShowRare(ByVal show As Boolean)
    Dim i As Long
    For i = mNCommon To mNArea - 1
        frStudies.Controls("chkStudy_" & i).Visible = show
        frStudies.Controls("spnStudy_" & i).Visible = show
        frStudies.Controls("lblStudy_" & i).Visible = show
    Next i
    PlaceLower frStudies.Top                     ' рамка растёт/сжимается под строки
End Sub
Private Function FindCtl(ByVal nm As String) As Object
    On Error Resume Next
    Set FindCtl = frStudies.Controls(nm)
    If FindCtl Is Nothing Then Set FindCtl = frContrast.Controls(nm)
    If FindCtl Is Nothing Then Set FindCtl = Me.Controls(nm)
    On Error GoTo 0
End Function

'--------------------- Новая регистрация ---------------------
Private Sub ResetNew()
    mEdit = False
    mBusy = True: txtStudyDate.Value = Format$(Date, "dd.mm.yyyy"): mBusy = False
    txtNumber.Value = NextPatientNumber()
    PatientName.Value = "": DoB.Value = "": cboDept.Value = ""
    History_number.Value = "": txtArea.Value = ""
    optAnestYes.Value = False: optAnestNo.Value = False
    optContrastYes.Value = False: optContrastNo.Value = False: frContrast.Visible = False: PlaceLower YLOW_HIDDEN   ' п.2 — ответ не выбран заранее
    If cboContrast.ListCount > 0 Then cboContrast.Value = cboContrast.List(0)   ' п.1 — Гадобутрол по умолч.
    txtDose.Value = ""
    ClearFlacons: ClearStudies: ClearPay
    If gShiftPerformer <> "" Then cboPerf.Value = gShiftPerformer Else cboPerf.Value = ""
    lblWarn.Visible = False
    btnMain.Caption = "Зарегистрировать"
    btnDelete.Visible = False: btnCancelPat.Visible = False
    btnExport.Visible = True: btnDiscBurn.Visible = True
    ' ряд 1: «Зарегистрировать» и «Записать диск»; ряд 2: «Выгрузить данные в журналы»
    btnMain.Left = 10: btnMain.Width = 155: btnMain.Height = 28
    btnDiscBurn.Left = 175: btnDiscBurn.Width = 155
    btnDiscBurn.ControlTipText = "Записать диск пациенту (пациента можно выбрать в окне записи)"
    btnExport.Left = 10: btnExport.Width = 320: btnExport.Height = 22
    PlaceLower frStudies.Top
    ShowImportBtn
End Sub
Private Sub ClearFlacons()
    Dim i As Long
    For i = 0 To UBoundS(mFlacons)
        frContrast.Controls("chkFlac_" & i).Value = False
        frContrast.Controls("spnFlac_" & i).Value = 1
        frContrast.Controls("lblFlac_" & i).Caption = "1"
    Next i
End Sub
Private Sub ClearStudies()
    Dim i As Long
    For i = 0 To mNArea - 1
        frStudies.Controls("chkStudy_" & i).Value = False
        frStudies.Controls("spnStudy_" & i).Value = 1
        frStudies.Controls("lblStudy_" & i).Caption = "1"
    Next i
End Sub
Private Sub ClearPay()
    Dim i As Long
    For i = 0 To UBoundS(mPays): frPay.Controls("optPay_" & i).Value = False: Next i
End Sub

'--------------------- Редактирование ---------------------
Public Sub FillData()
    Dim ws As Worksheet, r As Long
    mEdit = True
    Set ws = gEditSheet: r = CurrentRow
    mBusy = True
    txtNumber.Value = ws.Cells(r, COL_NUM).Value
    PatientName.Value = ws.Cells(r, COL_FIO).Value
    If IsDate(ws.Cells(r, COL_DOB).Value) Then _
        DoB.Value = Format$(ws.Cells(r, COL_DOB).Value, "dd.mm.yyyy") _
    Else DoB.Value = CStr(ws.Cells(r, COL_DOB).Value)
    cboDept.Value = ws.Cells(r, COL_DEPT).Value
    History_number.Value = ws.Cells(r, COL_CARD).Value
    txtArea.Value = ws.Cells(r, COL_AREA).Value
    Dim an As String: an = LCase$(Trim$(CStr(ws.Cells(r, COL_ANEST).Value)))
    optAnestYes.Value = (an = "да"): optAnestNo.Value = (an = "нет")
    SetPay CStr(ws.Cells(r, COL_PAY).Value)
    Dim hc As Boolean: hc = (Trim$(CStr(ws.Cells(r, COL_CONTR_NAME).Value)) <> "")
    optContrastYes.Value = hc: optContrastNo.Value = Not hc: frContrast.Visible = hc
    If hc Then PlaceLower YLOW_SHOWN Else PlaceLower YLOW_HIDDEN
    cboContrast.Value = ws.Cells(r, COL_CONTR_NAME).Value
    If IsNumeric(ws.Cells(r, COL_CONTR_DOSE).Value) Then _
        txtDose.Value = Format$(ws.Cells(r, COL_CONTR_DOSE).Value, "0.0")
    LoadFlacons CStr(ws.Cells(r, COL_CONTR_FLAC).Value)
    cboPerf.Value = ws.Cells(r, COL_PERF).Value
    LoadStudies CStr(ws.Cells(r, COL_STUDY).Value)
    Dim dRow As Long: dRow = r
    Do While Trim$(CStr(ws.Cells(dRow, COL_DATE).Value)) = "" And dRow > 1
        dRow = dRow - 1
    Loop
    If IsDate(ws.Cells(dRow, COL_DATE).Value) Then _
        txtStudyDate.Value = Format$(ws.Cells(dRow, COL_DATE).Value, "dd.mm.yyyy")
    mBusy = False
    lblWarn.Visible = False
    btnMain.Caption = "Редактировать"          ' п.9
    btnDelete.Visible = True: btnCancelPat.Visible = True
    btnExport.Visible = True                    ' п.8 — выгрузка доступна и в окне редактирования
    btnDiscBurn.Visible = True
    ' раскладка режима редактирования: три кнопки в ряд; ниже — «Записать диск» и «Выгрузить …»
    btnMain.Width = 106: btnMain.Left = 10: btnMain.Height = 24
    btnDelete.Width = 96: btnDelete.Left = 122
    btnCancelPat.Width = 106: btnCancelPat.Left = 224
    btnDiscBurn.Left = 10: btnDiscBurn.Width = 155
    btnDiscBurn.ControlTipText = "Записать диск этому пациенту"
    btnExport.Left = 175: btnExport.Width = 155: btnExport.Height = 28
    PlaceLower frStudies.Top
    ShowImportBtn
End Sub
Private Sub SetPay(ByVal v As String)
    Dim i As Long: v = Trim$(v)
    For i = 0 To UBoundS(mPays): frPay.Controls("optPay_" & i).Value = (mPays(i) = v): Next i
End Sub
Private Sub LoadFlacons(ByVal s As String)
    Dim parts() As String, i As Long, j As Long, nm As String, cnt As Long
    ClearFlacons: s = Trim$(s): If s = "" Then Exit Sub
    parts = Split(s, ";")
    For j = LBound(parts) To UBound(parts)
        If Trim$(parts(j)) <> "" Then
            ParseToken parts(j), "flac", nm, cnt
            For i = 0 To UBoundS(mFlacons)
                If mFlacons(i) = nm Then
                    frContrast.Controls("chkFlac_" & i).Value = True
                    frContrast.Controls("spnFlac_" & i).Value = cnt
                    frContrast.Controls("lblFlac_" & i).Caption = CStr(cnt)
                End If
            Next i
        End If
    Next j
End Sub
Private Sub LoadStudies(ByVal s As String)
    Dim parts() As String, i As Long, j As Long, nm As String, cnt As Long, anyRare As Boolean
    ClearStudies: s = Trim$(s): If s = "" Then Exit Sub
    parts = Split(s, ",")
    For j = LBound(parts) To UBound(parts)
        If Trim$(parts(j)) <> "" Then
            ParseToken parts(j), "study", nm, cnt
            For i = 0 To mNArea - 1
                If mOrder(i) = nm Then
                    frStudies.Controls("chkStudy_" & i).Value = True
                    frStudies.Controls("spnStudy_" & i).Value = cnt
                    frStudies.Controls("lblStudy_" & i).Caption = CStr(cnt)
                    If i >= mNCommon Then anyRare = True
                End If
            Next i
        End If
    Next j
    If anyRare Then chkRare.Value = True
End Sub

'--------------------- Сбор данных ---------------------
Private Function SelectedPay() As String
    Dim i As Long
    For i = 0 To UBoundS(mPays)
        If frPay.Controls("optPay_" & i).Value Then SelectedPay = mPays(i): Exit Function
    Next i
End Function
Private Function AnestValue() As String
    If optAnestYes.Value Then
        AnestValue = "да"
    ElseIf optAnestNo.Value Then
        AnestValue = "нет"
    End If
End Function
Private Function CollectFlacons() As String
    Dim i As Long, s As String, cnt As Long
    For i = 0 To UBoundS(mFlacons)
        If frContrast.Controls("chkFlac_" & i).Value Then
            cnt = frContrast.Controls("spnFlac_" & i).Value: If cnt < 1 Then cnt = 1
            s = s & IIf(s = "", "", "; ") & FormatToken(mFlacons(i), cnt, "flac")
        End If
    Next i
    CollectFlacons = s
End Function
Private Function AnyFlacon() As Boolean
    Dim i As Long
    For i = 0 To UBoundS(mFlacons)
        If frContrast.Controls("chkFlac_" & i).Value Then AnyFlacon = True: Exit Function
    Next i
End Function
Private Function CollectStudies(ByRef outCount As Long, ByVal hasContrast As Boolean) As String
    Dim i As Long, s As String, cnt As Long
    outCount = 0
    For i = 0 To mNArea - 1
        If frStudies.Controls("chkStudy_" & i).Value Then
            cnt = frStudies.Controls("spnStudy_" & i).Value: If cnt < 1 Then cnt = 1
            s = s & IIf(s = "", "", ", ") & FormatToken(mOrder(i), cnt, "study")
            If hasContrast And mOrder(i) <> "сосуды" Then outCount = outCount + cnt * 2 _
            Else outCount = outCount + cnt
        End If
    Next i
    CollectStudies = s
End Function

' п.3 — корректный разбор дозы с одним знаком после запятой, независимо от локали
Private Function IsDecimal(ByVal s As String) As Boolean
    Dim i As Long, ch As String, sep As Long
    s = Trim$(s): If s = "" Then Exit Function
    sep = 0
    For i = 1 To Len(s)
        ch = Mid$(s, i, 1)
        If ch = "," Or ch = "." Then
            sep = sep + 1: If sep > 1 Then Exit Function
        ElseIf ch < "0" Or ch > "9" Then
            Exit Function
        End If
    Next i
    IsDecimal = True
End Function
Private Function ToDouble(ByVal s As String) As Double
    ToDouble = Val(Replace(Trim$(s), ",", "."))     ' Val всегда трактует точку как разделитель
End Function

'--------------------- РЕГИСТРАЦИЯ ---------------------
Private Sub DoRegister()
    ' при регистрации возвращаем окно Excel — иначе окна подтверждения не видны/некликабельны
    On Error Resume Next
    Application.Visible = True
    SetTopMost False
    On Error GoTo 0

    Dim bDate As Date, dReg As Date, num As Long
    Dim cName As String, cDose As String, flacText As String
    Dim hasContrast As Boolean, writeContrast As Boolean
    Dim studiesText As String, studiesN As Long
    Dim soft As String, hard As String

    hasContrast = optContrastYes.Value
    cName = Trim$(cboContrast.Value): cDose = Trim$(txtDose.Value)

    If Trim$(PatientName.Value) = "" Then soft = AddProblem(soft, "ФИО")
    If Trim$(cboDept.Value) = "" Then soft = AddProblem(soft, "отделение")
    If Trim$(History_number.Value) = "" Then soft = AddProblem(soft, "№ истории болезни")
    If Trim$(txtArea.Value) = "" Then soft = AddProblem(soft, "область")
    If AnestValue() = "" Then soft = AddProblem(soft, "анестезия")
    If SelectedPay() = "" Then soft = AddProblem(soft, "оплата")
    If Trim$(cboPerf.Value) = "" Then soft = AddProblem(soft, "исполнитель")

    studiesText = CollectStudies(studiesN, hasContrast)
    If studiesText = "" Then soft = AddProblem(soft, "исследования")

    If hasContrast Then
        If cName = "" Then soft = AddProblem(soft, "контраст: препарат")
        If cDose = "" Then soft = AddProblem(soft, "контраст: доза")
        If Not AnyFlacon() Then soft = AddProblem(soft, "контраст: флакон")
        If cDose <> "" Then If Not IsDecimal(cDose) Then soft = AddProblem(soft, "доза (например 3,5)")
    End If

    If Trim$(History_number.Value) <> "" Then If Not ValidCardNumber(CStr(History_number.Value)) Then _
        soft = AddProblem(soft, "укажите номер истории болезни в формате «номер-год»")

    If Not TryDoB(bDate) Then soft = AddProblem(soft, "дата рождения")

    If Not ParseDate(txtStudyDate.Text, dReg) Then hard = AddProblem(hard, "дата исследования (дд.мм.гггг)")
    If Not IsNumeric(txtNumber.Value) Then hard = AddProblem(hard, "№ (число)")
    If Not optContrastYes.Value And Not optContrastNo.Value Then _
        hard = AddProblem(hard, "введение контраста — выберите «да» или «нет»")      ' п.2

    If hard <> "" Then
        lblWarn.Caption = "Исправьте: " & hard: lblWarn.Visible = True: Exit Sub
    End If
    If soft <> "" Then
        Dim resp As VbMsgBoxResult
        resp = WarnAsk("Заполнены не все данные:" & vbCrLf & soft & vbCrLf & vbCrLf & _
                      "Всё равно зарегистрировать пациента?" & vbCrLf & _
                      "(Да — продолжить, Нет — назад)", vbYesNo + vbExclamation, "Предупреждение")
        If resp = vbNo Then
            lblWarn.Caption = "Заполнены не все данные: " & soft: lblWarn.Visible = True: Exit Sub
        End If
    End If
    lblWarn.Visible = False
    If Not EnsureDiscColumn(False) Then Exit Sub        ' столбец O «Запись диска» (разово, при первом запуске)

    num = CLng(txtNumber.Value)
    writeContrast = hasContrast And cName <> "" And AnyFlacon() And cDose <> "" And IsDecimal(cDose)
    studiesText = CollectStudies(studiesN, writeContrast)
    flacText = CollectFlacons()

    If CurrentRow > 0 And Not gEditSheet Is Nothing Then
        WritePatient gEditSheet, CurrentRow, num, bDate, writeContrast, flacText, studiesText, studiesN, cDose
        gShiftPerformer = CStr(cboPerf.Value)
        MsgBox "Изменения сохранены.", vbInformation
        CurrentRow = 0: Set gEditSheet = Nothing: ResetNew: Exit Sub   ' п.8 — снова окно регистрации
    End If

    Dim ws As Worksheet: Set ws = MonthSheet(dReg)
    If ws Is Nothing Then WarnAsk "Лист «" & MonthName_Nom(dReg) & "» не найден.", vbCritical: Exit Sub

    ' п.3б — убрать «висячую» неправильную дату (дата без пациента) на других листах-месяцах
    CleanDanglingDates ws

    Dim lastA As Long, lastC As Long, lastRow As Long, patRow As Long, sameDate As Boolean
    Dim dateRow As Long
    lastA = ws.Cells(ws.Rows.Count, COL_DATE).End(xlUp).Row
    lastC = ws.Cells(ws.Rows.Count, COL_FIO).End(xlUp).Row
    If lastA >= FIRST_DATA_ROW And lastA > lastC Then
        ' п.3а — последняя строка = дата без пациента: перезаписываем её правильной датой
        dateRow = lastA: patRow = dateRow + 1
        UnprotectSheet ws
        ws.Cells(dateRow, COL_DATE).Value = dReg
        ws.Cells(dateRow, COL_DATE).NumberFormat = "dd.mm.yyyy"
        ProtectSheet ws
    Else
        lastRow = Application.WorksheetFunction.Max(lastA, lastC, FIRST_DATA_ROW - 1)
        If lastA >= FIRST_DATA_ROW Then If IsDate(ws.Cells(lastA, COL_DATE).Value) Then _
            sameDate = (Int(ws.Cells(lastA, COL_DATE).Value) = Int(dReg))
        If sameDate Then
            patRow = lastRow + 1
        Else
            dateRow = lastRow + 1: patRow = dateRow + 1
            UnprotectSheet ws
            ws.Cells(dateRow, COL_DATE).Value = dReg
            ws.Cells(dateRow, COL_DATE).NumberFormat = "dd.mm.yyyy"
            ProtectSheet ws
        End If
    End If
    WritePatient ws, patRow, num, bDate, writeContrast, flacText, studiesText, studiesN, cDose
    If gShiftPerformer = "" Then gShiftPerformer = CStr(cboPerf.Value)
    Dim pName As String: pName = Trim$(CStr(PatientName.Value))
    If pName = "" Then pName = "№" & num
    MsgBox "Пациент «" & pName & "» добавлен на лист «" & ws.Name & "».", vbInformation

    txtNumber.Value = NextPatientNumber()
    PatientName.Value = "": DoB.Value = "": cboDept.Value = "": History_number.Value = "": txtArea.Value = ""
    optAnestYes.Value = False: optAnestNo.Value = False
    optContrastYes.Value = False: optContrastNo.Value = False: frContrast.Visible = False: PlaceLower YLOW_HIDDEN   ' п.2 — ответ не выбран заранее
    If cboContrast.ListCount > 0 Then cboContrast.Value = cboContrast.List(0)
    txtDose.Value = "": ClearFlacons: ClearStudies
    PatientName.SetFocus
End Sub

Private Function AddProblem(ByVal cur As String, ByVal item As String) As String
    AddProblem = cur & IIf(cur = "", "", ", ") & item
End Function
Private Function TryDoB(ByRef bDate As Date) As Boolean
    Dim d As Integer, m As Integer, y As Integer
    bDate = 0
    If Len(DoB.Text) <> 10 Then Exit Function
    d = Val(Left$(DoB.Text, 2)): m = Val(Mid$(DoB.Text, 4, 2)): y = Val(Right$(DoB.Text, 4))
    If d < 1 Or d > 31 Or m < 1 Or m > 12 Or y < 1900 Then Exit Function
    On Error Resume Next
    bDate = DateSerial(y, m, d)
    On Error GoTo 0
    If Month(bDate) = m And bDate <= Date Then TryDoB = True Else bDate = 0
End Function

Private Sub WritePatient(ByVal ws As Worksheet, ByVal rowN As Long, ByVal num As Long, _
        ByVal bDate As Date, ByVal writeContrast As Boolean, ByVal flacText As String, _
        ByVal studiesText As String, ByVal studiesN As Long, ByVal cDose As String)
    UnprotectSheet ws
    ws.Cells(rowN, COL_NUM).Value = num
    ws.Cells(rowN, COL_FIO).Value = PatientName.Value
    If bDate > 0 Then
        ws.Cells(rowN, COL_DOB).Value = bDate
        ws.Cells(rowN, COL_DOB).NumberFormat = "dd.mm.yyyy"
    Else
        ws.Cells(rowN, COL_DOB).ClearContents
    End If
    ws.Cells(rowN, COL_DEPT).Value = cboDept.Value
    ws.Cells(rowN, COL_CARD).Value = History_number.Value
    ws.Cells(rowN, COL_AREA).Value = txtArea.Value
    ws.Cells(rowN, COL_ANEST).Value = AnestValue()
    If writeContrast Then
        ws.Cells(rowN, COL_CONTR_NAME).Value = cboContrast.Value
        ws.Cells(rowN, COL_CONTR_DOSE).Value = ToDouble(cDose)      ' п.3
        ws.Cells(rowN, COL_CONTR_DOSE).NumberFormat = "0.0"
        ws.Cells(rowN, COL_CONTR_FLAC).Value = flacText
    Else
        ws.Cells(rowN, COL_CONTR_NAME).ClearContents
        ws.Cells(rowN, COL_CONTR_DOSE).ClearContents
        ws.Cells(rowN, COL_CONTR_FLAC).ClearContents
    End If
    ws.Cells(rowN, COL_PAY).Value = SelectedPay()
    ws.Cells(rowN, COL_PERF).Value = cboPerf.Value
    ws.Cells(rowN, COL_STUDY).Value = studiesText
    ws.Cells(rowN, COL_COUNT).Value = studiesN
    ProtectSheet ws
End Sub

'--------------------- Удаление / отмена / настройки ---------------------
' п.3б — удалить одинокую дату (без пациента) в конце других листов-месяцев
Private Sub CleanDanglingDates(ByVal keepWs As Worksheet)
    Dim i As Long, s As Worksheet, la As Long, lc As Long
    For i = 1 To 12
        Set s = MonthSheet(DateSerial(2000, i, 1))
        If Not s Is Nothing Then
            If Not (s Is keepWs) Then
                la = s.Cells(s.Rows.Count, COL_DATE).End(xlUp).Row
                lc = s.Cells(s.Rows.Count, COL_FIO).End(xlUp).Row
                If la >= FIRST_DATA_ROW And la > lc Then
                    UnprotectSheet s
                    s.Rows(la).Delete Shift:=xlUp
                    ProtectSheet s
                End If
            End If
        End If
    Next i
End Sub

Private Sub DoDelete()
    If gEditSheet Is Nothing Or CurrentRow = 0 Then Exit Sub
    If MsgBox("Вы действительно хотите удалить данные о пациенте?", vbYesNo + vbQuestion, "Удаление") = vbYes Then
        Dim ws As Worksheet: Set ws = gEditSheet
        UnprotectSheet ws
        ws.Rows(CurrentRow).Delete Shift:=xlUp
        ProtectSheet ws
        RenumberSheet ws
        CurrentRow = 0: Set gEditSheet = Nothing
        ResetNew                       ' п.8 — снова окно регистрации
    End If
End Sub
Private Sub DoCancelPatient()
    If gEditSheet Is Nothing Or CurrentRow = 0 Then Exit Sub
    Dim reason As String
    reason = InputBox("Причина отмены исследования:" & vbCrLf & "(ОК — отменить, Отмена — назад)", "Отмена пациента")
    If Trim$(reason) = "" Then Exit Sub
    Dim ws As Worksheet, r As Long: Set ws = gEditSheet: r = CurrentRow
    UnprotectSheet ws
    ws.Cells(r, COL_CONTR_NAME).ClearContents
    ws.Cells(r, COL_CONTR_DOSE).ClearContents
    ws.Cells(r, COL_CONTR_FLAC).ClearContents
    ws.Cells(r, COL_STUDY).ClearContents
    ws.Cells(r, COL_COUNT).ClearContents
    ws.Cells(r, COL_AREA).Value = "отмена – " & reason
    ProtectSheet ws
    MsgBox "Исследование отменено.", vbInformation
    CurrentRow = 0: Set gEditSheet = Nothing: ResetNew   ' п.8 — снова окно регистрации
End Sub
Private Sub OpenSettings()
    Dim p As String
    p = InputBox("Введите пароль настроек:", "Настройки")
    If p = "" Then Exit Sub
    If Not SettingsPWOk(p) Then WarnAsk "Неверный пароль.", vbExclamation: Exit Sub
    UserForm2.Show vbModal
    UnloadForm "UserForm2"                      ' если окно закрыли клавишей F9 (Hide)
    FillCtl cboDept, "отделения": FillCtl cboPerf, "исполнители": FillCtl cboContrast, "контрасты"
    ShowImportBtn                               ' опцию «Выгрузка пациентов…» могли включить/выключить
End Sub

'--------------------- Пациент из другого журнала ---------------------
' Кнопка «Выгрузить пациента» (Настройки -> «Обмен пациентами со старым журналом»):
' видна в окне новой регистрации, если опция включена; тогда «Исполнитель» чуть ниже.
' Вызывается из Настроек сразу после «Сохранить»
Public Sub RefreshImportBtn()
    ShowImportBtn
End Sub

Private Sub ShowImportBtn()
    Dim isOn As Boolean
    On Error Resume Next
    isOn = ImportEnabled()
    On Error GoTo 0
    btnImport.Visible = (isOn And Not mEdit)
    If isOn Then
        lblPerfHead.Top = 22: cboPerf.Top = 35
    Else
        lblPerfHead.Top = 8: cboPerf.Top = 22
    End If
End Sub

' Окно выбора пациента другого журнала (UserForm6) -> данные в поля окна регистрации
Private Sub ImportPatient()
    Dim f As UserForm6, rec As Variant, fl As String, got As Boolean
    If mEdit Then Exit Sub
    Set f = New UserForm6
    If f.Prepare() Then
        f.Show vbModal
        got = f.Chosen
        If got Then rec = f.Record: fl = f.Fields
    End If
    Unload f
    Set f = Nothing
    If got Then ApplyImported rec, fl
End Sub

' rec — запись пациента другого журнала (см. IMP_NF в Module2): 1 ФИО, 2 дата рождения,
' 3 отделение, 4 № мед. карты, 5 область, 6 оплата, 7 исполнитель, 8 анестезия, 9 дата
' исследования, 11 препарат, 12 доза, 13 «Для формы 30», 14 какие столбцы есть в том журнале.
' fl — что вносить (10 знаков «1»/«0»): ФИО, ДР, отделение, № карты, область, оплата,
' исполнитель, анестезия, контраст, «Для формы №30». Дата исследования берётся всегда.
Private Sub ApplyImported(ByVal rec As Variant, ByVal fl As String)
    Dim miss As String, s As String, sd As String, an As String, dayNote As String, av As String
    av = CStr(rec(14))
    mBusy = True
    s = Trim$(CStr(rec(9)))
    If s <> "" And s <> Trim$(CStr(txtStudyDate.Value)) Then
        txtStudyDate.Value = s
        dayNote = vbCrLf & vbCrLf & "Дата исследования в окне — " & s & ", как в другом журнале."
    End If
    If Want(fl, av, 1, 1) Then PatientName.Value = CStr(rec(1))
    If Want(fl, av, 2, 2) Then DoB.Value = CStr(rec(2))
    If Want(fl, av, 3, 3) Then
        s = Trim$(CStr(rec(3)))
        If Not PickCombo(cboDept, s) Then miss = miss & vbCrLf & "  • отделение «" & s & "» — нет в списке"
    End If
    If Want(fl, av, 4, 4) Then History_number.Value = CStr(rec(4))
    If Want(fl, av, 5, 5) Then txtArea.Value = CStr(rec(5))
    s = Trim$(CStr(rec(6)))
    If Want(fl, av, 6, 6) And s <> "" Then
        If Not PickPay(s) Then miss = miss & vbCrLf & "  • оплата «" & s & "» — нет такого варианта"
    End If
    s = Trim$(CStr(rec(7)))
    If Want(fl, av, 7, 7) And s <> "" Then
        If Not PickCombo(cboPerf, s) Then miss = miss & vbCrLf & "  • исполнитель «" & s & "» — нет в списке"
    End If
    an = NormTxt(CStr(rec(8)))
    If Want(fl, av, 8, 8) And an <> "" Then
        If Left$(an, 2) = "да" Or an = "+" Then
            optAnestYes.Value = True
        ElseIf Left$(an, 3) = "нет" Or an = "-" Then
            optAnestNo.Value = True
        Else
            miss = miss & vbCrLf & "  • анестезия «" & Trim$(CStr(rec(8))) & "» — выберите «да» или «нет»"
        End If
    End If
    ' контраст: в том журнале есть препарат/доза — «да» + препарат + доза (флаконы — вручную); пусто — «нет»
    If Want(fl, av, 9, 9) Then
        s = Trim$(CStr(rec(11))): sd = Trim$(CStr(rec(12)))
        If s <> "" Or sd <> "" Then
            optContrastYes.Value = True: ShowContrast True
            If s <> "" Then
                If Not PickCombo(cboContrast, s) Then miss = miss & vbCrLf & "  • контраст «" & s & "» — нет в списке препаратов"
            End If
            txtDose.Value = sd
            miss = miss & vbCrLf & "  • списанные флаконы контраста"
        Else
            optContrastNo.Value = True: ShowContrast False
        End If
    End If
    ' «Для формы №30» — галочки и количества
    If Want(fl, av, 10, 11) Then miss = miss & ApplyStudies(CStr(rec(13)))
    mBusy = False
    lblWarn.Visible = False
    If miss <> "" Then
        MsgBox "Данные пациента внесены. Выберите вручную:" & miss & dayNote, vbInformation, "Выгрузить пациента"
    ElseIf dayNote <> "" Then
        MsgBox "Данные пациента внесены." & dayNote, vbInformation, "Выгрузить пациента"
    End If
End Sub

' Вносить ли поле: отмечено галочкой (позиция fPos в fl) и столбец есть в том журнале
' (поле col записи, позиция col+1 в av; пустое av — считать, что есть)
Private Function Want(ByVal fl As String, ByVal av As String, ByVal fPos As Long, ByVal col As Long) As Boolean
    If Mid$(fl, fPos, 1) <> "1" Then Exit Function
    If Len(av) >= col + 1 Then
        If Mid$(av, col + 1, 1) = "0" Then Exit Function
    End If
    Want = True
End Function

' «Для формы 30» другого журнала («гм, шоп, суставы/мягкиеткани, суставы/мягкиеткани»,
' повтор = ещё одно исследование; «гм-2» — тоже понимается) -> галочки и количества рамки
' «Для формы №30». Возвращает строки «нет в списке» для сообщения.
Private Function ApplyStudies(ByVal s As String) As String
    Dim parts() As String, j As Long, i As Long, nm As String, cnt As Long, cnts() As Long
    Dim anyRare As Boolean, hit As Boolean, bad As String
    ClearStudies
    s = Trim$(s)
    If s = "" Or mNArea < 1 Then Exit Function
    ReDim cnts(0 To mNArea - 1)
    parts = Split(Replace(s, ";", ","), ",")
    For j = LBound(parts) To UBound(parts)
        If Trim$(parts(j)) <> "" Then
            ParseToken parts(j), "study", nm, cnt
            If cnt < 1 Then cnt = 1
            hit = False
            For i = 0 To mNArea - 1
                If NormTxt(mOrder(i)) = NormTxt(nm) Then
                    cnts(i) = cnts(i) + cnt: hit = True
                    Exit For
                End If
            Next i
            If Not hit Then bad = bad & vbCrLf & "  • для формы №30: «" & Trim$(parts(j)) & "» — нет в списке"
        End If
    Next j
    For i = 0 To mNArea - 1
        If cnts(i) > 0 Then
            If cnts(i) > 50 Then cnts(i) = 50
            frStudies.Controls("chkStudy_" & i).Value = True
            frStudies.Controls("spnStudy_" & i).Value = cnts(i)
            frStudies.Controls("lblStudy_" & i).Caption = CStr(cnts(i))
            If i >= mNCommon Then anyRare = True
        End If
    Next i
    If anyRare Then chkRare.Value = True
    ApplyStudies = bad
End Function

' Выбрать в списке значение, равное тексту (без учёта регистра, пробелов, точек, «ё»);
' нет равного — единственное, начинающееся так же. "" — очистить. False — не нашлось.
Private Function PickCombo(ByVal cb As MSForms.ComboBox, ByVal txt As String) As Boolean
    Dim i As Long, t As String, it As String, hit As Long, nHit As Long
    t = NormTxt(txt)
    If t = "" Then cb.ListIndex = -1: PickCombo = True: Exit Function
    For i = 0 To cb.ListCount - 1
        If NormTxt(CStr(cb.List(i))) = t Then cb.ListIndex = i: PickCombo = True: Exit Function
    Next i
    hit = -1
    For i = 0 To cb.ListCount - 1
        it = NormTxt(CStr(cb.List(i)))
        If it <> "" Then
            If Left$(it, Len(t)) = t Or Left$(t, Len(it)) = it Then hit = i: nHit = nHit + 1
        End If
    Next i
    If nHit = 1 Then cb.ListIndex = hit: PickCombo = True
End Function

Private Function PickPay(ByVal txt As String) As Boolean
    Dim i As Long, t As String
    t = NormTxt(txt)
    For i = 0 To UBoundS(mPays)
        If NormTxt(mPays(i)) = t Then
            ClearPay
            frPay.Controls("optPay_" & i).Value = True
            PickPay = True: Exit Function
        End If
    Next i
End Function

Private Function NormTxt(ByVal s As String) As String
    s = LCase$(Trim$(s))
    s = Replace(s, "ё", "е"): s = Replace(s, Chr(160), "")
    s = Replace(s, " ", ""): s = Replace(s, ".", "")
    NormTxt = s
End Function

' Выгрузить форму, только если она загружена (не создавая её заново)
Private Sub UnloadForm(ByVal nm As String)
    Dim f As Object
    On Error Resume Next
    For Each f In VBA.UserForms
        If TypeName(f) = nm Then Unload f: Exit For
    Next f
End Sub

Private Sub ToggleExcel()
    ' скрыть/показать окно Excel — чтобы форму можно было поставить рядом с эл. историей
    On Error Resume Next
    Application.Visible = Not Application.Visible
    SetTopMost (Application.Visible = False)   ' при скрытом Excel держим форму поверх всех окон
End Sub

Private Sub SetTopMost(ByVal onTop As Boolean)
    On Error Resume Next
    Dim h As Long
    h = FindWindow("ThunderDFrame", Me.Caption)          ' немодальная форма
    If h = 0 Then h = FindWindow("ThunderXFrame", Me.Caption)   ' на случай модальной
    If h <> 0 Then SetWindowPos h, IIf(onTop, HWND_TOPMOST, HWND_NOTOPMOST), 0, 0, 0, 0, SWP_NOMOVE Or SWP_NOSIZE
End Sub

Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
    On Error Resume Next
    Application.Visible = True        ' на всякий случай вернуть окно Excel
    CurrentRow = 0: Set gEditSheet = Nothing
End Sub