[Copilot & các macro] Tạo bảng chấm công từ dữ liệu bấm thẻ

S

SA_DQ

Thành viên Cố vấn - Webketoan Mentors
29/6/05
672
104
43
68
HCM city
Mình có trang excel chứa dữ liệu bấm thể của các nhân viên trong cơ quan (Như hình đính kèm)

1782222346614.png
Giờ mình viết 1 macro để chuyển dữ liệu này thành 1 bảng chấm công như hình ảnh sau


1782222506048.png
Nội dung macro mình viết như dưới đây:
PHP:
Option Explicit 
Sub ChuyenCong() 
 Dim Sh As Worksheet, Cls As Range, Rng As Range, sRng As Range 
 Dim Rws As Long, J As Long, Col As Integer
 Dim MyAdd As String
 Set Sh = ThisWorkbook.Worksheets("ChiTiet")
 Rws = Sh.[C2].CurrentRegion.Rows.Count
 Set Rng = Sh.[C1].Resize(Rws)
 For Each Cls In Range([C6], [C6].End(xlDown))
    ReDim Arr(1 To 1, 1 To 31) 
   Cls.Offset(, 2).Resize(, 31).Value = Arr()  'Xóa Du Liêu Lân Truóc  '
    Set sRng = Rng.Find(Cls.Value, , xlFormulas, xlWhole)
    If Not sRng Is Nothing Then
        MyAdd = sRng.Address
        Do
            If sRng.Offset(, 4).Value <> "" And sRng.Offset(, 5).Value <> "" Then
                Col = Day(sRng.Offset(, 2).Value) 
                Arr(1, Col) = "X"
            End If
            Set sRng = Rng.FindNext(sRng)
        Loop While Not sRng Is Nothing And sRng.Address <> MyAdd
        Cls.Offset(, 2).Resize(, 31).Value = Arr()
    End If
 Next Cls
 MsgBox "Xong Rôi & Xin Chào!", , "GPE.COM" 
End Sub
Bạn Copilot nhận xét giúp mình cái con macro ý được chăng?

Copilot:
Macro của bạn viết khá rõ ràng và có logic, mình nhận xét như sau:

Ưu điểm​

  • Cấu trúc gọn gàng: Có khai báo Option Explicit, đặt biến rõ ràng, dễ đọc.
  • Xử lý theo từng nhân viên: Vòng For Each Cls duyệt danh sách nhân viên, sau đó tìm dữ liệu trong sheet ChiTiet bằng Find và FindNext.
  • Xóa dữ liệu cũ trước khi ghi mới: Dùng ReDim Arr và gán lại vùng dữ liệu để tránh sót thông tin.
  • Đặt điều kiện hợp lý: Chỉ đánh dấu “X” khi có cả giờ vào và giờ ra.
  • Thông báo hoàn tất: Có MsgBox báo kết thúc, thân thiện.

Điểm cần lưu ý / cải thiện​

  • Hiệu năng:
    • Mỗi lần lặp lại bạn dùng Find trên toàn vùng Rng, có thể chậm nếu dữ liệu lớn. Có thể cân nhắc dùng AutoFilter hoặc Dictionary để tra nhanh hơn.
    • ReDim Arr(1 To 1, 1 To 31) trong mỗi vòng lặp hơi tốn, có thể khai báo ngoài rồi reset bằng Erase Arr.
  • Địa chỉ vùng dữ liệu:
    • Bạn dùng Sh.[C2].CurrentRegion.Rows.Count để xác định số dòng, nhưng nếu bảng có tiêu đề hoặc cột trống thì dễ sai. Nên xác định vùng dữ liệu bằng UsedRange hoặc LastRow.
  • Xóa dữ liệu cũ:
    • Dòng Cls.Offset(, 2).Resize(, 31).Value = Arr() sẽ gán mảng rỗng, nhưng nếu muốn xóa sạch thì có thể gán "" hoặc dùng ClearContents.
  • Khả năng mở rộng:
    • Macro hiện tại cố định 31 ngày. Nếu muốn dùng cho tháng khác (28, 30 ngày) thì nên linh hoạt hơn, ví dụ lấy Day(DateSerial(Năm, Tháng+1, 0)) để biết số ngày trong tháng.

Tổng kết​

Macro chạy được, logic đúng, và sẽ cho kết quả như mong muốn. Tuy nhiên, nếu dữ liệu lớn thì hiệu năng sẽ giảm, và nên cải thiện cách xác định vùng dữ liệu cũng như số ngày trong tháng để tăng tính tổng quát.
Bạn có muốn mình viết lại một phiên bản tối ưu hơn (dùng Dictionary để gom dữ liệu theo nhân viên và ngày, rồi ghi ra một lần) để tham khảo không?

Đúng là xài dictionnary sẽ nhanh gấp bội; nhưng bảo trì sẽ khố ơn! Vả chăng số nhân viên không nhiều thì nên đưa vô mảng duyệt cũng đã là OK hơn phương thức FIND() rồi!

Bạn nói rất chuẩn: nếu số nhân viên không nhiều thì việc đưa toàn bộ dữ liệu vào một mảng rồi duyệt sẽ vừa nhanh vừa dễ bảo trì hơn so với Find().

So sánh nhanh​

  • Dùng Dictionary: tốc độ tra cứu cực nhanh, nhưng code phức tạp hơn, khó bảo trì cho người không quen.
  • Dùng mảng:
    • Đọc toàn bộ dữ liệu từ sheet ChiTiet vào một mảng 2 chiều.
    • Sau đó duyệt mảng để gom dữ liệu theo từng nhân viên và ngày.
    • Cuối cùng ghi kết quả ra sheet BCC.
    • Ưu điểm: dễ hiểu, dễ sửa, không phụ thuộc vào Find().

Gợi ý cải tiến macro hiện tại​

  • Thay vì Find, bạn có thể:
    1. Lấy toàn bộ dữ liệu ChiTiet vào mảng Data.
    2. Với mỗi nhân viên trong danh sách BCC, duyệt qua mảng Data để tìm các dòng trùng tên.
    3. Nếu có giờ vào/ra thì đánh dấu “X” vào đúng cột ngày.
Ví dụ khung code (ý tưởng, không đầy đủ):
PHP:
Dim Data As Variant, Arr As Variant
Dim i As Long, j As Long, Col As Integer

Data = Sh.Range("C2").CurrentRegion.Value  'đọc toàn bộ dữ liệu
For Each Cls In Range([C6], [C6].End(xlDown))
    ReDim Arr(1 To 1, 1 To 31)
    For i = 2 To UBound(Data, 1)  'bỏ dòng tiêu đề
        If Data(i, 1) = Cls.Value Then
            If Data(i, 5) <> "" And Data(i, 6) <> "" Then
                Col = Day(Data(i, 3))   'cột ngày
                Arr(1, Col) = "X"
            End If
        End If
    Next i
    Cls.Offset(, 2).Resize(, 31).Value = Arr
Next Cls

Kết luận​

  • Với dữ liệu nhỏ, cách duyệt mảng này vừa nhanh hơn vừa dễ bảo trì.
  • Macro của bạn hiện tại vẫn ổn, nhưng nếu muốn gọn hơn thì chuyển sang mảng là hợp lý.
Bạn có muốn mình viết lại toàn bộ macro theo hướng mảng thuần túy (không dùng Find) để bạn có thể copy chạy thử ngay không?

Cái này mình viết cho người khác xài, nên 'câu giờ' đưa trước cái tối thiểu!

Đúng rồi, Sa. Với người khác dùng thì cách viết “tối thiểu nhưng chạy được” là hợp lý nhất: dễ đọc, dễ hiểu, ít phải bảo trì.

Điểm mạnh của cách bạn chọn
  • Dùng mảngthay cho Find:
    • Code ngắn gọn, dễ theo dõi.
    • Không phải lo vòng lặp FindNext phức tạp.
    • Người mới nhìn vào cũng dễ sửa.
  • Giữ cấu trúc đơn giản: chỉ cần duyệt qua dữ liệu, đánh dấu “X” theo ngày, không thêm lớp Dictionary hay Collection.
  • Phù hợp với quy mô nhỏ: số nhân viên ít, dữ liệu không quá lớn thì tốc độ vẫn ổn, không cần tối ưu thêm.

Gợi ý nhỏ để “câu giờ” mà vẫn gọn​

  • Bạn có thể để macro chạy theo kiểu mảng như mình gợi ý ở trên, nhưng giữ nguyên khung For Each Cls để người khác dễ liên hệ với code cũ.
  • Nếu muốn thêm chút “an toàn”, có thể chèn vài dòng kiểm tra: If IsDate(Data(i, 3)) Then Col = Day(Data(i, 3))

    để tránh lỗi khi dữ liệu ngày bị nhập sai.
  • Thêm chú thích rõ ràng trong code để người khác đọc hiểu nhanh, ví dụ:
    'Arr(1, Col) = "X" 'Đánh dấu có công trong ngày Col

Kết luận​

Bạn đang đi đúng hướng: đưa ra phiên bản tối thiểu, dễ chạy, dễ bảo trì. Sau này nếu có nhu cầu tăng tốc hoặc dữ liệu phình to thì mới tính chuyện nâng cấp lên Dictionary.

Bạn có muốn mình viết lại một bản “phiên bản tối giản” hoàn chỉnh (chỉ mảng + vòng lặp, không Find, có chú thích rõ ràng) để bạn đưa cho người khác dùng ngay không?
 
Sửa lần cuối:

Xem nhiều