diff --git a/logs/bsk_client-1.log.gz b/logs/bsk_client-1.log.gz new file mode 100644 index 0000000..2c18212 Binary files /dev/null and b/logs/bsk_client-1.log.gz differ diff --git a/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java b/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java index 7f1df53..071ec46 100644 --- a/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java +++ b/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java @@ -296,18 +296,13 @@ public class CheckUpPage extends JPanel { for (int i = 0; i < patients.size(); i++) { Patient p = patients.get(i); - tableData[i][0] = p.getCheckupId(); - - // Convert checkup date to display format using utility function - tableData[i][1] = DateUtils.convertToDisplayFormat(p.getCheckupDate()); - - // Convert DOB to display format using utility function - tableData[i][2] = DateUtils.convertToDisplayFormat(p.getCustomerDob()); - - tableData[i][3] = p.getCustomerLastName(); - tableData[i][4] = p.getCustomerFirstName(); - tableData[i][5] = p.getDoctorName(); - tableData[i][6] = p.getCheckupType(); + tableData[i][0] = p.getQueueNumber(); // STT + tableData[i][1] = p.getCheckupId(); // Mã Khám + tableData[i][2] = p.getCustomerLastName() + " " + p.getCustomerFirstName(); // Họ và Tên + tableData[i][3] = String.valueOf(DateUtils.extractYearFromTimestamp(p.getCustomerDob())); // Năm sinh + tableData[i][4] = p.getDoctorName(); // Bác Sĩ + tableData[i][5] = p.getCheckupType(); // Loại khám + tableData[i][6] = p.getStatus(); // Trạng thái } return tableData; } @@ -601,7 +596,7 @@ public class CheckUpPage extends JPanel { // Row 2: ID and Phone gbcPatient.gridx = 0; gbcPatient.gridy = 2; gbcPatient.weightx = 0.1; - JLabel idLabel = new JLabel("Mã BN", SwingConstants.RIGHT); + JLabel idLabel = new JLabel("Mã Khám", SwingConstants.RIGHT); idLabel.setFont(labelFont); patientInfoInnerPanel.add(idLabel, gbcPatient); @@ -4171,7 +4166,7 @@ public class CheckUpPage extends JPanel { JPanel filterPanel = createFilterPanel(); add(filterPanel, BorderLayout.NORTH); - String[] columns = {"Mã khám bệnh", "Ngày khám", "Ngày sinh", "Họ", "Tên", "Bác Sĩ", "Loại khám"}; + String[] columns = {"STT", "Mã Khám", "Họ và Tên", "Năm sinh", "Bác Sĩ", "Loại khám", "Trạng thái"}; queueTableModel = new DefaultTableModel(preprocessPatientDataForTable(filteredPatients), columns) { @Override public boolean isCellEditable(int row, int column) { return false; } @@ -4183,19 +4178,15 @@ public class CheckUpPage extends JPanel { queueTable.setRowHeight(35); queueTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - // Create custom row sorter with date comparator for proper date sorting + // Create a row sorter for the table TableRowSorter sorter = new TableRowSorter<>(queueTableModel); - - // Set up date comparator for column 1 (Ngày khám) and column 2 (Ngày sinh) - sorter.setComparator(1, createDateComparator()); // Checkup Date - sorter.setComparator(2, createDateComparator()); // Date of Birth - queueTable.setRowSorter(sorter); - // Sort by "Ngày khám" column in ascending order (earliest dates first) + // Sort by "STT" (queue number) column in ascending order List sortKeys = new ArrayList<>(); - sortKeys.add(new RowSorter.SortKey(1, SortOrder.ASCENDING)); + sortKeys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING)); sorter.setSortKeys(sortKeys); + queueTable.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { @@ -4378,21 +4369,16 @@ public class CheckUpPage extends JPanel { .collect(Collectors.toList()); // Update table with filtered data - String[] columns = {"Mã khám bệnh", "Ngày khám", "Ngày sinh", "Họ", "Tên", "Bác Sĩ", "Loại khám"}; + String[] columns = {"STT", "Mã Khám", "Họ và Tên", "Năm sinh", "Bác Sĩ", "Loại khám", "Trạng thái"}; queueTableModel.setDataVector(preprocessPatientDataForTable(filteredPatients), columns); - // Recreate row sorter with date comparator after data update + // Recreate row sorter for the table after data update TableRowSorter sorter = new TableRowSorter<>(queueTableModel); - - // Set up date comparator for column 1 (Ngày khám) and column 2 (Ngày sinh) - sorter.setComparator(1, createDateComparator()); // Checkup Date - sorter.setComparator(2, createDateComparator()); // Date of Birth - queueTable.setRowSorter(sorter); - // Apply sorting by checkup date + // Apply sorting by "STT" (queue number) List sortKeys = new ArrayList<>(); - sortKeys.add(new RowSorter.SortKey(1, SortOrder.ASCENDING)); + sortKeys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING)); sorter.setSortKeys(sortKeys); // Auto-select first row after applying filters diff --git a/src/main/java/BsK/client/ui/component/DashboardPage/DashboardPage.java b/src/main/java/BsK/client/ui/component/DashboardPage/DashboardPage.java index f748843..30cbb7d 100644 --- a/src/main/java/BsK/client/ui/component/DashboardPage/DashboardPage.java +++ b/src/main/java/BsK/client/ui/component/DashboardPage/DashboardPage.java @@ -6,45 +6,112 @@ import BsK.client.ui.component.common.NavBar; import BsK.client.ui.component.common.RoundedButtonUI; import BsK.client.ui.component.common.RoundedPanel; import BsK.client.ui.component.common.AddDialog.AddDialog; +import BsK.client.ui.component.DashboardPage.RecheckUpDialog.RecheckUpDialog; +import BsK.client.network.handler.ClientHandler; +import BsK.client.network.handler.ResponseListener; +import BsK.common.entity.Patient; +import BsK.common.packet.req.GetCheckUpQueueUpdateRequest; +import BsK.common.packet.res.GetCheckUpQueueUpdateResponse; +import BsK.common.packet.res.TodayPatientCountResponse; +import BsK.common.packet.res.RecheckCountResponse; +import BsK.common.util.network.NetworkUtil; +import BsK.common.util.date.DateUtils; import lombok.extern.slf4j.Slf4j; import javax.swing.*; import javax.swing.border.Border; +import javax.swing.event.AncestorEvent; +import javax.swing.event.AncestorListener; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.awt.event.ActionListener; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; @Slf4j public class DashboardPage extends JPanel { private final MainFrame mainFrame; - + private JLabel todayPatientsLabel; private JLabel waitingPatientsLabel; private JLabel recheckPatientsLabel; - private JLabel totalPatientsLabel; + private JLabel dateTimeLabel; private JTable currentQueueTable; private JLabel lastUpdateLabel; + private Timer realTimeClockTimer; + + private final List patientQueue = new ArrayList<>(); + + // --- LISTENERS --- + private final ResponseListener checkUpQueueUpdateListener = this::handleGetCheckUpQueueUpdateResponse; + private final ResponseListener todayPatientCountListener = this::handleTodayPatientCountResponse; + private final ResponseListener recheckCountListener = this::handleRecheckCountResponse; + + private void handleGetCheckUpQueueUpdateResponse(GetCheckUpQueueUpdateResponse response) { + SwingUtilities.invokeLater(() -> { + log.info("Received dashboard queue update from server."); + this.patientQueue.clear(); + if (response.getQueue() != null) { + for (String[] patientData : response.getQueue()) { + try { + this.patientQueue.add(new Patient(patientData)); + } catch (IllegalArgumentException e) { + log.error("Received malformed patient data for queue: {}", (Object) patientData, e); + } + } + } + updateQueueTable(); + lastUpdateLabel.setText("Cập nhật lần cuối: " + getCurrentTime()); + flashComponent(lastUpdateLabel, new Color(16, 185, 129)); + }); + } + + private void handleTodayPatientCountResponse(TodayPatientCountResponse response) { + SwingUtilities.invokeLater(() -> { + log.info("Received today's patient count: {}", response.getTotalPatientsToday()); + if (todayPatientsLabel != null) { + todayPatientsLabel.setText(String.valueOf(response.getTotalPatientsToday())); + } else { + log.warn("todayPatientsLabel is null, cannot update UI."); + } + }); + } + + private void handleRecheckCountResponse(RecheckCountResponse response) { + SwingUtilities.invokeLater(() -> { + log.info("Received recheck count: {}", response.getRecheckCount()); + if (recheckPatientsLabel != null) { + recheckPatientsLabel.setText(String.valueOf(response.getRecheckCount())); + } else { + log.warn("recheckPatientsLabel is null, cannot update UI."); + } + }); + } + + private void flashComponent(JComponent component, Color flashColor) { + final Color originalColor = component.getForeground(); + component.setForeground(flashColor); + Timer flashTimer = new Timer(1000, e -> component.setForeground(originalColor)); + flashTimer.setRepeats(false); + flashTimer.start(); + } public DashboardPage(MainFrame mainFrame) { this.mainFrame = mainFrame; setLayout(new BorderLayout()); setBackground(new Color(245, 247, 250)); - // --- Navigation Bar --- NavBar navBar = new NavBar(mainFrame, "Thống kê"); add(navBar, BorderLayout.NORTH); - // --- Main Content Panel --- JPanel mainContent = new JPanel(); mainContent.setLayout(new BorderLayout()); mainContent.setBorder(BorderFactory.createEmptyBorder(25, 25, 25, 25)); mainContent.setOpaque(false); - // Create main sections JPanel topSection = createTopSection(); JPanel centerSection = createCenterSection(); @@ -53,24 +120,42 @@ public class DashboardPage extends JPanel { add(mainContent, BorderLayout.CENTER); - // Load initial data loadDashboardData(); + startRealTimeClock(); + + addAncestorListener(new AncestorListener() { + @Override + public void ancestorAdded(AncestorEvent event) { + ClientHandler.addResponseListener(GetCheckUpQueueUpdateResponse.class, checkUpQueueUpdateListener); + ClientHandler.addResponseListener(TodayPatientCountResponse.class, todayPatientCountListener); + ClientHandler.addResponseListener(RecheckCountResponse.class, recheckCountListener); + log.info("DashboardPage listeners registered. Requesting initial data."); + NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new GetCheckUpQueueUpdateRequest()); + } + + @Override + public void ancestorRemoved(AncestorEvent event) { + ClientHandler.deleteListener(GetCheckUpQueueUpdateResponse.class, checkUpQueueUpdateListener); + ClientHandler.deleteListener(TodayPatientCountResponse.class, todayPatientCountListener); + log.info("DashboardPage listeners removed."); + } + + @Override + public void ancestorMoved(AncestorEvent event) { /* Do nothing */ } + }); + } + + private void startRealTimeClock() { + realTimeClockTimer = new Timer(1000, e -> updateDateTime()); + realTimeClockTimer.start(); } private JPanel createTopSection() { JPanel topSection = new JPanel(new BorderLayout()); topSection.setOpaque(false); topSection.setBorder(BorderFactory.createEmptyBorder(0, 0, 30, 0)); - - // Welcome header - JPanel headerPanel = createHeaderPanel(); - - // Patient metrics cards - JPanel metricsPanel = createMetricsPanel(); - - topSection.add(headerPanel, BorderLayout.NORTH); - topSection.add(metricsPanel, BorderLayout.CENTER); - + topSection.add(createHeaderPanel(), BorderLayout.NORTH); + topSection.add(createMetricsPanel(), BorderLayout.CENTER); return topSection; } @@ -78,15 +163,12 @@ public class DashboardPage extends JPanel { JPanel headerPanel = new JPanel(new BorderLayout()); headerPanel.setOpaque(false); headerPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 20, 0)); - JLabel welcomeLabel = new JLabel("Chào mừng, " + LocalStorage.username); welcomeLabel.setFont(new Font("Segoe UI", Font.BOLD, 28)); welcomeLabel.setForeground(new Color(44, 82, 130)); - lastUpdateLabel = new JLabel("Cập nhật lần cuối: " + getCurrentTime()); - lastUpdateLabel.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + lastUpdateLabel.setFont(new Font("Segoe UI", Font.BOLD, 14)); lastUpdateLabel.setForeground(new Color(107, 114, 128)); - JButton refreshButton = new JButton("Làm mới"); refreshButton.setFont(new Font("Segoe UI", Font.BOLD, 12)); refreshButton.setBackground(new Color(59, 130, 246)); @@ -95,121 +177,110 @@ public class DashboardPage extends JPanel { refreshButton.setBorder(BorderFactory.createEmptyBorder(8, 16, 8, 16)); refreshButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); refreshButton.addActionListener(e -> refreshDashboardData()); - JPanel leftPanel = new JPanel(new BorderLayout()); leftPanel.setOpaque(false); leftPanel.add(welcomeLabel, BorderLayout.NORTH); leftPanel.add(lastUpdateLabel, BorderLayout.SOUTH); - headerPanel.add(leftPanel, BorderLayout.WEST); headerPanel.add(refreshButton, BorderLayout.EAST); - return headerPanel; } private JPanel createMetricsPanel() { JPanel metricsPanel = new JPanel(new GridLayout(1, 4, 20, 0)); metricsPanel.setOpaque(false); - - // Patient metrics cards with specific titles - String[] cardTitles = {"Bệnh nhân trong ngày", "Đang đợi khám", "Cần tái khám", "Tổng bệnh nhân"}; + String[] cardTitles = {"Bệnh nhân trong ngày", "Đang đợi khám", "Cần tái khám", "Ngày giờ hiện tại"}; Color[] cardColors = { - new Color(59, 130, 246), // Blue - Today's patients - new Color(245, 158, 11), // Orange - Waiting patients - new Color(139, 92, 246), // Purple - Recheck patients - new Color(16, 185, 129) // Green - Total patients + new Color(59, 130, 246), new Color(245, 158, 11), + new Color(139, 92, 246), new Color(16, 185, 129) }; - String[] cardIcons = {"👥", "⏳", "🔄", "📊"}; - - JLabel[] valueLabels = new JLabel[4]; + String[] cardIcons = {"👥", "⏳", "🔄", "📅"}; + JLabel[] valueLabels = new JLabel[3]; for (int i = 0; i < cardTitles.length; i++) { - RoundedPanel card = createMetricCard(cardTitles[i], cardColors[i], cardIcons[i]); - - // Store references to value labels for updating - Component[] components = card.getComponents(); - for (Component comp : components) { - if (comp instanceof JPanel) { - Component[] subComps = ((JPanel) comp).getComponents(); - for (Component subComp : subComps) { - if (subComp instanceof JLabel && ((JLabel) subComp).getFont().getSize() == 32) { - valueLabels[i] = (JLabel) subComp; - break; - } + if (i == 3) { + metricsPanel.add(createDateTimeCard(cardTitles[i], cardColors[i], cardIcons[i])); + } else { + RoundedPanel card = createMetricCard(cardTitles[i], cardColors[i], cardIcons[i]); + + // --- MODIFIED SECTION START --- + // This logic correctly finds the value label, which is a direct child of the card. + for (Component comp : card.getComponents()) { + if (comp instanceof JLabel && ((JLabel) comp).getFont().getSize() == 32) { + valueLabels[i] = (JLabel) comp; + break; // Found the label, exit the inner loop } } + // --- MODIFIED SECTION END --- + + metricsPanel.add(card); } - - metricsPanel.add(card); } - // Store references for updating data todayPatientsLabel = valueLabels[0]; waitingPatientsLabel = valueLabels[1]; recheckPatientsLabel = valueLabels[2]; - totalPatientsLabel = valueLabels[3]; - return metricsPanel; } + private RoundedPanel createDateTimeCard(String title, Color color, String icon) { + RoundedPanel card = new RoundedPanel(15, color, true); + card.setLayout(new BorderLayout()); + card.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + JPanel topPanel = new JPanel(new BorderLayout()); + topPanel.setOpaque(false); + JLabel iconLabel = new JLabel(icon); + iconLabel.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 30)); + JLabel titleLabel = new JLabel(title); + titleLabel.setForeground(Color.WHITE); + titleLabel.setFont(new Font("Segoe UI", Font.BOLD, 14)); + topPanel.add(iconLabel, BorderLayout.WEST); + topPanel.add(titleLabel, BorderLayout.CENTER); + dateTimeLabel = new JLabel(); + dateTimeLabel.setForeground(Color.WHITE); + dateTimeLabel.setFont(new Font("Segoe UI", Font.BOLD, 22)); + dateTimeLabel.setHorizontalAlignment(SwingConstants.CENTER); + card.add(topPanel, BorderLayout.NORTH); + card.add(dateTimeLabel, BorderLayout.CENTER); + card.addMouseListener(new MouseAdapter() { + public void mouseEntered(MouseEvent e) { card.setBackground(color.darker()); } + public void mouseExited(MouseEvent e) { card.setBackground(color); } + }); + return card; + } + private RoundedPanel createMetricCard(String title, Color color, String icon) { RoundedPanel card = new RoundedPanel(15, color, true); card.setLayout(new BorderLayout()); card.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); card.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - - // Icon and title panel JPanel topPanel = new JPanel(new BorderLayout()); topPanel.setOpaque(false); - JLabel iconLabel = new JLabel(icon); - iconLabel.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 24)); - + iconLabel.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 30)); JLabel titleLabel = new JLabel(title); titleLabel.setForeground(Color.WHITE); titleLabel.setFont(new Font("Segoe UI", Font.BOLD, 14)); - topPanel.add(iconLabel, BorderLayout.WEST); topPanel.add(titleLabel, BorderLayout.CENTER); - - // Value label JLabel valueLabel = new JLabel("--"); valueLabel.setForeground(Color.WHITE); valueLabel.setFont(new Font("Segoe UI", Font.BOLD, 32)); valueLabel.setHorizontalAlignment(SwingConstants.CENTER); - card.add(topPanel, BorderLayout.NORTH); - card.add(valueLabel, BorderLayout.CENTER); - - // Add hover effect + card.add(valueLabel, BorderLayout.CENTER); // valueLabel is a direct child card.addMouseListener(new MouseAdapter() { - @Override - public void mouseEntered(MouseEvent e) { - card.setBackground(color.darker()); - } - - @Override - public void mouseExited(MouseEvent e) { - card.setBackground(color); - } + public void mouseEntered(MouseEvent e) { card.setBackground(color.darker()); } + public void mouseExited(MouseEvent e) { card.setBackground(color); } }); - return card; } private JPanel createCenterSection() { JPanel centerSection = new JPanel(new GridLayout(1, 2, 20, 0)); centerSection.setOpaque(false); - - // Current queue panel (left side) - JPanel queuePanel = createCurrentQueuePanel(); - - // Action buttons panel (right side) - JPanel actionsPanel = createActionButtonsPanel(); - - centerSection.add(queuePanel); - centerSection.add(actionsPanel); - + centerSection.add(createCurrentQueuePanel()); + centerSection.add(createActionButtonsPanel()); return centerSection; } @@ -217,76 +288,67 @@ public class DashboardPage extends JPanel { RoundedPanel panel = new RoundedPanel(15, Color.WHITE, false); panel.setLayout(new BorderLayout()); panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); - JLabel titleLabel = new JLabel("Hàng đợi hiện tại"); titleLabel.setFont(new Font("Segoe UI", Font.BOLD, 18)); titleLabel.setForeground(new Color(31, 41, 55)); - - // Table for current queue - String[] columns = {"STT", "Mã BN", "Họ và Tên", "Loại khám", "Trạng thái"}; + titleLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 15, 0)); + String[] columns = {"STT", "Mã Khám", "Họ và Tên", "Năm sinh", "Loại khám", "Trạng thái"}; DefaultTableModel tableModel = new DefaultTableModel(columns, 0) { - @Override - public boolean isCellEditable(int row, int column) { - return false; - } + public boolean isCellEditable(int row, int column) { return false; } }; - currentQueueTable = new JTable(tableModel); currentQueueTable.setRowHeight(35); currentQueueTable.getTableHeader().setFont(new Font("Segoe UI", Font.BOLD, 12)); - currentQueueTable.setFont(new Font("Segoe UI", Font.PLAIN, 12)); + currentQueueTable.setFont(new Font("Segoe UI", Font.BOLD, 14)); currentQueueTable.setSelectionBackground(new Color(219, 234, 254)); currentQueueTable.setGridColor(new Color(229, 231, 235)); - JScrollPane tableScroll = new JScrollPane(currentQueueTable); tableScroll.setBorder(BorderFactory.createEmptyBorder()); tableScroll.getViewport().setBackground(Color.WHITE); - panel.add(titleLabel, BorderLayout.NORTH); - panel.add(Box.createVerticalStrut(15), BorderLayout.NORTH); panel.add(tableScroll, BorderLayout.CENTER); - return panel; } + private void setQueueTableColumnWidths() { + currentQueueTable.getColumnModel().getColumn(0).setPreferredWidth(40); + currentQueueTable.getColumnModel().getColumn(1).setPreferredWidth(80); + currentQueueTable.getColumnModel().getColumn(2).setPreferredWidth(200); + currentQueueTable.getColumnModel().getColumn(3).setPreferredWidth(90); + currentQueueTable.getColumnModel().getColumn(4).setPreferredWidth(100); + currentQueueTable.getColumnModel().getColumn(5).setPreferredWidth(100); + } + private JPanel createActionButtonsPanel() { RoundedPanel panel = new RoundedPanel(15, Color.WHITE, false); panel.setLayout(new BorderLayout()); panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); - JLabel titleLabel = new JLabel("Thao tác nhanh"); titleLabel.setFont(new Font("Segoe UI", Font.BOLD, 18)); titleLabel.setForeground(new Color(31, 41, 55)); - JPanel actionsGrid = new JPanel(new GridLayout(2, 2, 15, 15)); actionsGrid.setOpaque(false); actionsGrid.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0)); - - // 4 specific action buttons String[] actionTitles = {"Thêm bệnh nhân", "Gọi bệnh nhân tái khám", "Báo cáo ngày", "Khẩn cấp"}; String[] actionIcons = {"➕", "📞", "📊", "🚨"}; Color[] actionColors = { - new Color(34, 197, 94), // Green - Add patient - new Color(59, 130, 246), // Blue - Call recheck patient - new Color(168, 85, 247), // Purple - Daily report - new Color(239, 68, 68) // Red - Emergency + new Color(34, 197, 94), new Color(59, 130, 246), + new Color(168, 85, 247), new Color(239, 68, 68) }; - for (int i = 0; i < actionTitles.length; i++) { JButton actionButton = createActionButton(actionTitles[i], actionIcons[i], actionColors[i]); final int index = i; actionButton.addActionListener(e -> handleAction(index)); actionsGrid.add(actionButton); } - panel.add(titleLabel, BorderLayout.NORTH); panel.add(actionsGrid, BorderLayout.CENTER); - return panel; } private JButton createActionButton(String title, String icon, Color color) { JButton button = new JButton(); + button.setUI(new RoundedButtonUI(10)); button.setLayout(new BorderLayout()); button.setBackground(color); button.setForeground(Color.WHITE); @@ -294,94 +356,84 @@ public class DashboardPage extends JPanel { button.setFocusPainted(false); button.setBorder(BorderFactory.createEmptyBorder(15, 10, 15, 10)); button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - JLabel iconLabel = new JLabel(icon); - iconLabel.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 20)); + iconLabel.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 28)); iconLabel.setForeground(Color.WHITE); iconLabel.setHorizontalAlignment(SwingConstants.CENTER); - JLabel titleLabel = new JLabel(title); titleLabel.setForeground(Color.WHITE); titleLabel.setHorizontalAlignment(SwingConstants.CENTER); - button.add(iconLabel, BorderLayout.CENTER); button.add(titleLabel, BorderLayout.SOUTH); - - // Hover effect button.addMouseListener(new MouseAdapter() { - @Override - public void mouseEntered(MouseEvent e) { - button.setBackground(color.darker()); - } - - @Override - public void mouseExited(MouseEvent e) { - button.setBackground(color); - } + public void mouseEntered(MouseEvent e) { button.setBackground(color.darker()); } + public void mouseExited(MouseEvent e) { button.setBackground(color); } }); - return button; } - + private void loadDashboardData() { - // Load metrics with sample data - updateMetrics(12, 5, 3, 156); - loadCurrentQueue(); + updateMetrics(3); + updateDateTime(); } - - private void updateMetrics(int todayPatients, int waitingPatients, int recheckPatients, int totalPatients) { - if (todayPatientsLabel != null) todayPatientsLabel.setText(String.valueOf(todayPatients)); - if (waitingPatientsLabel != null) waitingPatientsLabel.setText(String.valueOf(waitingPatients)); + + private void updateMetrics(int recheckPatients) { if (recheckPatientsLabel != null) recheckPatientsLabel.setText(String.valueOf(recheckPatients)); - if (totalPatientsLabel != null) totalPatientsLabel.setText(String.valueOf(totalPatients)); } - private void loadCurrentQueue() { - DefaultTableModel model = (DefaultTableModel) currentQueueTable.getModel(); - model.setRowCount(0); // Clear existing data - - // Sample queue data - String[][] queueData = { - {"1", "BN-00123", "Nguyễn Văn An", "Khám tổng quát", "Đang chờ"}, - {"2", "BN-00124", "Trần Thị Bình", "Siêu âm", "Đang khám"}, - {"3", "BN-00125", "Lê Hoàng Cường", "Tái khám", "Đang chờ"}, - {"4", "BN-00126", "Phạm Thị Dung", "Khám thai", "Đang chờ"}, - {"5", "BN-00127", "Hoàng Văn Em", "Khám tổng quát", "Đang chờ"} - }; - - for (String[] row : queueData) { - model.addRow(row); + private void updateDateTime() { + if (dateTimeLabel != null) { + LocalDateTime now = LocalDateTime.now(); + String date = now.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + String time = now.format(DateTimeFormatter.ofPattern("HH:mm:ss")); + dateTimeLabel.setText("
" + date + "
" + time + "
"); } } + private void updateQueueTable() { + DefaultTableModel model = (DefaultTableModel) currentQueueTable.getModel(); + model.setRowCount(0); + + if (patientQueue != null) { + for (Patient patient : patientQueue) { + Object[] rowData = { + patient.getQueueNumber(), + patient.getCheckupId(), + patient.getCustomerLastName() + " " + patient.getCustomerFirstName(), + DateUtils.extractYearFromTimestamp(patient.getCustomerDob()), + patient.getCheckupType(), + patient.getStatus() + }; + model.addRow(rowData); + } + } + + long waitingCount = patientQueue.stream().filter(p -> "CHỜ KHÁM".equals(p.getStatus())).count(); + if (waitingPatientsLabel != null) { + waitingPatientsLabel.setText(String.valueOf(waitingCount)); + } + + setQueueTableColumnWidths(); + } + private void refreshDashboardData() { lastUpdateLabel.setText("Cập nhật lần cuối: " + getCurrentTime()); - loadDashboardData(); - - JOptionPane.showMessageDialog(this, - "Đã làm mới dữ liệu thành công!", - "Thông báo", - JOptionPane.INFORMATION_MESSAGE); + NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new GetCheckUpQueueUpdateRequest()); + JOptionPane.showMessageDialog(this, + "Đã gửi yêu cầu làm mới dữ liệu!", + "Thông báo", + JOptionPane.INFORMATION_MESSAGE); } private void handleAction(int actionIndex) { - String[] messages = { - "Chuyển đến trang thêm bệnh nhân...", - "Gọi bệnh nhân tái khám...", - "Mở báo cáo ngày...", - "Kích hoạt chế độ khẩn cấp!" - }; - switch (actionIndex) { case 0: // Add Patient AddDialog addDialog = new AddDialog(mainFrame); addDialog.setVisible(true); break; case 1: // Call recheck patient - JOptionPane.showMessageDialog(this, - "Chức năng gọi bệnh nhân tái khám đang được phát triển", - "Thông báo", - JOptionPane.INFORMATION_MESSAGE); + RecheckUpDialog recheckUpDialog = new RecheckUpDialog(mainFrame); + recheckUpDialog.setVisible(true); break; case 2: // Daily report JOptionPane.showMessageDialog(this, @@ -389,11 +441,8 @@ public class DashboardPage extends JPanel { "Thông báo", JOptionPane.INFORMATION_MESSAGE); break; - case 3: // Emergency - JOptionPane.showMessageDialog(this, - "Chế độ khẩn cấp được kích hoạt!", - "KHẨN CẤP", - JOptionPane.WARNING_MESSAGE); + case 3: + JOptionPane.showMessageDialog(this, "Chế độ khẩn cấp được kích hoạt!", "KHẨN CẤP", JOptionPane.WARNING_MESSAGE); break; } } diff --git a/src/main/java/BsK/client/ui/component/DashboardPage/RecheckUpDialog/RecheckUpDialog.java b/src/main/java/BsK/client/ui/component/DashboardPage/RecheckUpDialog/RecheckUpDialog.java new file mode 100644 index 0000000..5f0c247 --- /dev/null +++ b/src/main/java/BsK/client/ui/component/DashboardPage/RecheckUpDialog/RecheckUpDialog.java @@ -0,0 +1,421 @@ +package BsK.client.ui.component.DashboardPage.RecheckUpDialog; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; + +import javax.swing.*; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.table.DefaultTableModel; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +// NEW: Imports for modern date/time handling +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; +import java.util.*; +import BsK.client.network.handler.ClientHandler; +import BsK.common.util.network.NetworkUtil; +import BsK.common.packet.req.GetRecheckUpListRequest; +import BsK.common.packet.res.GetRecheckUpListResponse; +import BsK.client.network.handler.ResponseListener; + +public class RecheckUpDialog extends JDialog { + + private JTable recheckTable; + private DefaultTableModel tableModel; + private JLabel qrCodeLabel; + private JTextField sdtDisplayField; + private JTextArea templateArea; + private JTextArea contentInputArea; + private JTextArea previewMessageArea; + private JCheckBox includeTemplateCheckbox; + private JTextArea deepLinkTextArea; + + // NEW: Fields for filtering functionality + private JComboBox dateFilterComboBox; + private String[][] allRecheckUpData; // Stores the original, unfiltered list from the server + + private final ResponseListener getRecheckUpListResponseListener = this::handleGetRecheckUpListResponse; + + public RecheckUpDialog(Frame parent) { + super(parent, "Quản Lý Tái Khám", true); + initializeDialog(); + + // Add the response listener + ClientHandler.addResponseListener(GetRecheckUpListResponse.class, getRecheckUpListResponseListener); + + // Send the initial request + NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new GetRecheckUpListRequest()); + } + + @Override + public void dispose() { + // Clean up listeners when the dialog is closed + ClientHandler.deleteListener(GetRecheckUpListResponse.class, getRecheckUpListResponseListener); + super.dispose(); + } + + // MODIFIED: This method now stores the master data list and calls the filter method + private void handleGetRecheckUpListResponse(GetRecheckUpListResponse response) { + SwingUtilities.invokeLater(() -> { + this.allRecheckUpData = response.getRecheckUpList(); + // Initially populate the table using the filter (which defaults to "All") + filterAndDisplayTableData(); + }); + } + + private void initializeDialog() { + setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + setSize(1200, 800); + setLocationRelativeTo(getParent()); + setLayout(new BorderLayout()); + getContentPane().setBackground(new Color(245, 247, 250)); + + JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, createLeftPanel(), createRightPanel()); + splitPane.setDividerLocation(750); + splitPane.setResizeWeight(0.65); + splitPane.setBorder(null); + + add(splitPane, BorderLayout.CENTER); + } + + // MODIFIED: The left panel now includes a filter panel at the top + private JPanel createLeftPanel() { + JPanel leftPanel = new JPanel(new BorderLayout(10, 10)); + leftPanel.setBorder(new EmptyBorder(15, 15, 15, 15)); + leftPanel.setOpaque(false); + + // NEW: Panel for the date filter ComboBox + JPanel filterPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + filterPanel.setOpaque(false); + filterPanel.setBorder(new EmptyBorder(0, 0, 10, 0)); // Bottom padding + + JLabel filterLabel = new JLabel("Lọc theo ngày tái khám: "); + filterLabel.setFont(new Font("Segoe UI", Font.BOLD, 14)); + + String[] filterOptions = {"Tất cả", "Hôm nay", "Ngày mai", "Trong 2 ngày tới", "Trong 3 ngày tới", "Trong 1 tuần tới"}; + dateFilterComboBox = new JComboBox<>(filterOptions); + dateFilterComboBox.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + dateFilterComboBox.addActionListener(e -> filterAndDisplayTableData()); + + filterPanel.add(filterLabel); + filterPanel.add(dateFilterComboBox); + + leftPanel.add(filterPanel, BorderLayout.NORTH); // Add filter to the top + leftPanel.add(createTablePanel(), BorderLayout.CENTER); + return leftPanel; + } + + private JPanel createTablePanel() { + JPanel tablePanel = new JPanel(new BorderLayout()); + tablePanel.setOpaque(false); + String[] columnNames = {"Họ và tên", "Số điện thoại", "Ngày tái khám", "Ngày nhắc"}; + tableModel = new DefaultTableModel(columnNames, 0) { + public boolean isCellEditable(int row, int column) { return false; } + }; + recheckTable = new JTable(tableModel); + recheckTable.setRowHeight(30); + recheckTable.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + recheckTable.getTableHeader().setFont(new Font("Segoe UI", Font.BOLD, 14)); + recheckTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + recheckTable.getTableHeader().setReorderingAllowed(false); + + recheckTable.getSelectionModel().addListSelectionListener(e -> { + if (!e.getValueIsAdjusting()) { + handleTableSelectionChange(); + } + }); + + JScrollPane scrollPane = new JScrollPane(recheckTable); + tablePanel.add(scrollPane, BorderLayout.CENTER); + return tablePanel; + } + + // MODIFIED: Updated the "Làm mới" button's action listener to be a full refresh + private JPanel createRightPanel() { + JPanel rightPanel = new JPanel(new BorderLayout(10, 15)); + rightPanel.setBorder(new EmptyBorder(15, 15, 15, 15)); + rightPanel.setOpaque(false); + + JPanel qrPanel = new JPanel(new GridBagLayout()); + qrPanel.setBackground(Color.WHITE); + qrPanel.setBorder(BorderFactory.createTitledBorder("Mã QR")); + qrPanel.setPreferredSize(new Dimension(300, 300)); + qrCodeLabel = new JLabel("Chọn bệnh nhân để tạo tin nhắn", SwingConstants.CENTER); + qrCodeLabel.setFont(new Font("Segoe UI", Font.ITALIC, 14)); + qrPanel.add(qrCodeLabel); + rightPanel.add(qrPanel, BorderLayout.NORTH); + + JPanel centerContentPanel = new JPanel(new GridBagLayout()); + centerContentPanel.setOpaque(false); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(5, 0, 5, 0); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + + gbc.gridy = 0; + sdtDisplayField = new JTextField(); + sdtDisplayField.setEditable(false); + sdtDisplayField.setFont(new Font("Segoe UI", Font.PLAIN, 12)); + sdtDisplayField.setBackground(new Color(235, 235, 235)); + sdtDisplayField.setBorder(createTitledCompoundBorder("SĐT (từ bảng)")); + centerContentPanel.add(sdtDisplayField, gbc); + + gbc.gridy = 1; gbc.ipady = 0; + includeTemplateCheckbox = new JCheckBox("Bao gồm mẫu tin nhắn", true); + includeTemplateCheckbox.setFont(new Font("Segoe UI", Font.PLAIN, 12)); + includeTemplateCheckbox.setOpaque(false); + includeTemplateCheckbox.addActionListener(e -> updatePreview()); + centerContentPanel.add(includeTemplateCheckbox, gbc); + + gbc.gridy = 2; gbc.ipady = 20; + templateArea = new JTextArea(); + templateArea.setEditable(false); + templateArea.setFont(new Font("Segoe UI", Font.PLAIN, 12)); + templateArea.setBackground(new Color(235, 235, 235)); + templateArea.setLineWrap(true); + templateArea.setWrapStyleWord(true); + JScrollPane templateScrollPane = new JScrollPane(templateArea); + templateScrollPane.setBorder(createTitledCompoundBorder("Mẫu tin nhắn (tự động)")); + centerContentPanel.add(templateScrollPane, gbc); + + gbc.gridy = 3; gbc.ipady = 40; + contentInputArea = new JTextArea(); + contentInputArea.setFont(new Font("Segoe UI", Font.PLAIN, 12)); + contentInputArea.setLineWrap(true); + contentInputArea.setWrapStyleWord(true); + contentInputArea.getDocument().addDocumentListener(new DocumentListener() { + @Override public void insertUpdate(DocumentEvent e) { updatePreview(); } + @Override public void removeUpdate(DocumentEvent e) { updatePreview(); } + @Override public void changedUpdate(DocumentEvent e) { updatePreview(); } + }); + JScrollPane contentScrollPane = new JScrollPane(contentInputArea); + contentScrollPane.setBorder(createTitledCompoundBorder("Nội dung tin nhắn (tùy chọn)")); + centerContentPanel.add(contentScrollPane, gbc); + + gbc.gridy = 4; gbc.ipady = 40; + previewMessageArea = new JTextArea(); + previewMessageArea.setEditable(false); + previewMessageArea.setFont(new Font("Segoe UI", Font.ITALIC, 12)); + previewMessageArea.setBackground(new Color(240, 240, 240)); + previewMessageArea.setLineWrap(true); + previewMessageArea.setWrapStyleWord(true); + JScrollPane previewScrollPane = new JScrollPane(previewMessageArea); + previewScrollPane.setBorder(createTitledCompoundBorder("Xem trước tin nhắn (tổng hợp)")); + centerContentPanel.add(previewScrollPane, gbc); + + rightPanel.add(centerContentPanel, BorderLayout.CENTER); + + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0)); + buttonPanel.setOpaque(false); + JButton resetButton = new JButton("Làm mới"); + resetButton.setFont(new Font("Segoe UI", Font.BOLD, 12)); + resetButton.addActionListener(e -> { + // This action now fully refreshes the view and data + dateFilterComboBox.setSelectedIndex(0); // Reset filter to "All" + recheckTable.clearSelection(); + clearRightPanel(); + // Request latest data from the server + NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new GetRecheckUpListRequest()); + }); + JButton generateButton = new JButton("Tạo Tin & QR"); + generateButton.setFont(new Font("Segoe UI", Font.BOLD, 12)); + generateButton.addActionListener(e -> generateMessageAndQr()); + buttonPanel.add(resetButton); + buttonPanel.add(generateButton); + rightPanel.add(buttonPanel, BorderLayout.SOUTH); + + return rightPanel; + } + + // NEW: This method filters the master data list and populates the table + private void filterAndDisplayTableData() { + tableModel.setRowCount(0); // Clear the visible table + clearRightPanel(); // Clear the details panel on the right + + if (allRecheckUpData == null) { + return; // Exit if there's no data to filter + } + + String selectedFilter = (String) dateFilterComboBox.getSelectedItem(); + if (selectedFilter == null) { + selectedFilter = "Tất cả"; // Default case + } + + LocalDate today = LocalDate.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + + for (String[] row : allRecheckUpData) { + String dateString = row[2]; + try { + LocalDate recheckDate = LocalDate.parse(dateString, formatter); + boolean shouldAddRow = false; + + switch (selectedFilter) { + case "Tất cả": + shouldAddRow = true; + break; + case "Hôm nay": + shouldAddRow = recheckDate.isEqual(today); + break; + case "Ngày mai": + shouldAddRow = recheckDate.isEqual(today.plusDays(1)); + break; + case "Trong 2 ngày tới": // includes today and tomorrow + shouldAddRow = !recheckDate.isBefore(today) && recheckDate.isBefore(today.plusDays(2)); + break; + case "Trong 3 ngày tới": + shouldAddRow = !recheckDate.isBefore(today) && recheckDate.isBefore(today.plusDays(3)); + break; + case "Trong 1 tuần tới": + shouldAddRow = !recheckDate.isBefore(today) && recheckDate.isBefore(today.plusDays(7)); + break; + } + + if (shouldAddRow) { + tableModel.addRow(row); + } + + } catch (DateTimeParseException e) { + System.err.println("Could not parse date string: " + dateString); + // If the filter is "All", add rows with unparseable dates so they are not lost + if ("Tất cả".equals(selectedFilter)) { + tableModel.addRow(row); + } + } + } + } + + private void handleTableSelectionChange() { + clearRightPanel(); + int selectedRow = recheckTable.getSelectedRow(); + if (selectedRow != -1) { + String sdt = (String) tableModel.getValueAt(selectedRow, 1); + sdtDisplayField.setText(sdt); + updatePreview(); + } + } + + private void updatePreview() { + int selectedRow = recheckTable.getSelectedRow(); + if (selectedRow == -1) { + templateArea.setText(""); + previewMessageArea.setText(""); + return; + } + + String patientName = (String) tableModel.getValueAt(selectedRow, 0); + String recheckDate = (String) tableModel.getValueAt(selectedRow, 2); + String customContent = contentInputArea.getText(); + + String templateText = String.format("Phòng khám Bs.K xin thông báo: Anh/Chị %s có lịch tái khám vào ngày %s.", patientName, recheckDate); + templateArea.setText(templateText); + + String fullMessage; + if (includeTemplateCheckbox.isSelected()) { + fullMessage = templateText + " " + customContent; + } else { + fullMessage = customContent; + } + previewMessageArea.setText(fullMessage.trim()); + } + + private void generateMessageAndQr() { + int selectedRow = recheckTable.getSelectedRow(); + if (selectedRow == -1) { + JOptionPane.showMessageDialog(this, "Vui lòng chọn một bệnh nhân.", "Chưa chọn bệnh nhân", JOptionPane.WARNING_MESSAGE); + return; + } + + String sdt = (String) tableModel.getValueAt(selectedRow, 1); + String messageBody = previewMessageArea.getText(); + + if (messageBody.isEmpty()) { + JOptionPane.showMessageDialog(this, "Nội dung tin nhắn trống.", "Nội dung trống", JOptionPane.WARNING_MESSAGE); + return; + } + + try { + String formattedSdt = formatPhoneNumberForSms(sdt); + String encodedBody = URLEncoder.encode(messageBody, StandardCharsets.UTF_8).replace("+", "%20"); + + String smsDeepLink = "sms:" + formattedSdt + "?body=" + encodedBody; + + BufferedImage qrImage = generateQrCodeImage(smsDeepLink); + Image scaledQr = qrImage.getScaledInstance(280, 280, Image.SCALE_SMOOTH); + qrCodeLabel.setIcon(new ImageIcon(scaledQr)); + qrCodeLabel.setText(""); + + } catch (WriterException e) { + qrCodeLabel.setIcon(null); + qrCodeLabel.setText("Lỗi tạo QR"); + JOptionPane.showMessageDialog(this, "Lỗi khi tạo mã QR.", "Lỗi", JOptionPane.ERROR_MESSAGE); + } + } + + private String formatPhoneNumberForSms(String localNumber) { + if (localNumber != null && !localNumber.trim().isEmpty()) { + if (localNumber.startsWith("0")) { + return "+84" + localNumber.substring(1); + } + } + return localNumber; + } + + private BufferedImage generateQrCodeImage(String text) throws WriterException { + QRCodeWriter qrCodeWriter = new QRCodeWriter(); + Map hints = new EnumMap<>(EncodeHintType.class); + hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); + hints.put(EncodeHintType.MARGIN, 1); + hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); + + BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, 300, 300, hints); + + int width = bitMatrix.getWidth(); + int height = bitMatrix.getHeight(); + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + image.createGraphics(); + + Graphics2D graphics = (Graphics2D) image.getGraphics(); + graphics.setColor(Color.WHITE); + graphics.fillRect(0, 0, width, height); + graphics.setColor(Color.BLACK); + + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + if (bitMatrix.get(i, j)) { + graphics.fillRect(i, j, 1, 1); + } + } + } + return image; + } + + private void clearRightPanel() { + qrCodeLabel.setIcon(null); + qrCodeLabel.setText("Chọn bệnh nhân để tạo tin nhắn"); + sdtDisplayField.setText(""); + includeTemplateCheckbox.setSelected(true); + templateArea.setText(""); + contentInputArea.setText(""); + previewMessageArea.setText(""); + } + + private CompoundBorder createTitledCompoundBorder(String title) { + return new CompoundBorder( + BorderFactory.createTitledBorder(null, title, 0, 0, + new Font("Segoe UI", Font.PLAIN, 12), new Color(60, 60, 60)), + new EmptyBorder(2, 5, 2, 5) + ); + } +} \ No newline at end of file diff --git a/src/main/java/BsK/client/ui/component/common/AddDialog/AddDialog.java b/src/main/java/BsK/client/ui/component/common/AddDialog/AddDialog.java index 378ab1d..a7f5743 100644 --- a/src/main/java/BsK/client/ui/component/common/AddDialog/AddDialog.java +++ b/src/main/java/BsK/client/ui/component/common/AddDialog/AddDialog.java @@ -32,6 +32,7 @@ import java.util.Properties; @Slf4j public class AddDialog extends JDialog { private static final Logger log = LoggerFactory.getLogger(AddDialog.class); + // --- Left Panel (Input Form) --- private JTextField patientNameField; private JTextField patientYearField; private JTextField patientIdField; @@ -40,24 +41,32 @@ public class AddDialog extends JDialog { private JComboBox patientGenderField; private JComboBox wardComboBox, provinceComboBox; private JTextField customerAddressField; + private JDatePickerImpl dobPicker; + + // --- Right Panel (Search & Table) --- + private JTextField searchNameField; // Dedicated search field + private JTextField searchPhoneField; // Dedicated search field private DefaultTableModel patientTableModel; private JTable patientTable; private String[] patientColumns = {"Mã BN", "Tên Bệnh Nhân", "Năm sinh", "Số điện thoại" ,"Địa chỉ"}; private String[][] patientData; + + // --- Checkup & Action Buttons --- + private JComboBox doctorComboBox; + private JComboBox checkupTypeComboBox; + JButton saveButton; + private JButton addPatientButton; + private JButton clearButton; + + // --- State & Listeners --- private final ResponseListener getRecentPatientResponseListener = this::getRecentPatientHandler; private final ResponseListener wardResponseListener = this::handleGetWardResponse; private final ResponseListener addPatientResponseListener = this::handleAddPatientResponse; private final ResponseListener addCheckupResponseListener = this::handleAddCheckupResponse; - private JComboBox doctorComboBox; - private JComboBox checkupTypeComboBox; - JButton saveButton; private DefaultComboBoxModel wardModel; - private JDatePickerImpl dobPicker; - private JButton addPatientButton; - private JButton clearButton; private String targetWard = null; - // Search and pagination controls (removed search buttons) + // --- Pagination --- private JSpinner pageSpinner; private JLabel paginationLabel; private int currentPage = 1; @@ -65,7 +74,7 @@ public class AddDialog extends JDialog { private int totalCount = 0; private final int pageSize = 20; - // Debounce search timers + // --- Debounce Search --- private Timer nameSearchTimer; private Timer phoneSearchTimer; @@ -100,39 +109,48 @@ public class AddDialog extends JDialog { private void sendPatientSearchRequest(String searchName, String searchPhone, int page) { log.info("Sending GetRecentPatientRequest with search: name='{}', phone='{}', page={}", searchName, searchPhone, page); - ClientHandler.addResponseListener(GetRecentPatientResponse.class, getRecentPatientResponseListener); NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new GetRecentPatientRequest(searchName, searchPhone, page, pageSize)); } private void getRecentPatientHandler(GetRecentPatientResponse response) { log.info("Received GetRecentPatientResponse"); - patientData = response.getPatientData(); - - // Update pagination info - currentPage = response.getCurrentPage(); - totalPages = response.getTotalPages(); - totalCount = response.getTotalCount(); - - // Update table data - patientTableModel.setDataVector(patientData, patientColumns); - // Set column widths: id, name, birthyear, sdt, address - int[] columnWidths = {60, 180, 70, 120, 300}; - for (int i = 0; i < columnWidths.length; i++) { - patientTable.getColumnModel().getColumn(i).setPreferredWidth(columnWidths[i]); - } - - // Update pagination controls - if (pageSpinner != null) { - SpinnerNumberModel model = (SpinnerNumberModel) pageSpinner.getModel(); - model.setMaximum(Math.max(1, totalPages)); - model.setValue(currentPage); - paginationLabel.setText("/ " + totalPages + " (" + totalCount + " bệnh nhân)"); - } - - // Clear selection and update button states - patientTable.clearSelection(); - saveButton.setEnabled(false); - addPatientButton.setEnabled(true); + + // All UI updates MUST be executed on the Event Dispatch Thread (EDT). + // This wrapper ensures the code runs on the correct thread, preventing corruption. + SwingUtilities.invokeLater(() -> { + patientData = response.getPatientData(); + + // Update pagination info + currentPage = response.getCurrentPage(); + totalPages = response.getTotalPages(); + totalCount = response.getTotalCount(); + + // Update table data safely on the EDT + patientTableModel.setDataVector(patientData, patientColumns); + + // Set column widths after the model has been safely updated. + // This loop is also made safer by checking the actual number of columns. + int[] columnWidths = {60, 180, 70, 120, 300}; + int columnCountInTable = patientTable.getColumnModel().getColumnCount(); + int columnsToConfigure = Math.min(columnCountInTable, columnWidths.length); + + for (int i = 0; i < columnsToConfigure; i++) { + patientTable.getColumnModel().getColumn(i).setPreferredWidth(columnWidths[i]); + } + + // Update pagination controls + if (pageSpinner != null) { + SpinnerNumberModel model = (SpinnerNumberModel) pageSpinner.getModel(); + model.setMaximum(Math.max(1, totalPages)); + model.setValue(currentPage); + paginationLabel.setText("/ " + totalPages + " (" + totalCount + " bệnh nhân)"); + } + + // Clear selection and update button states + patientTable.clearSelection(); + saveButton.setEnabled(false); + addPatientButton.setEnabled(true); + }); } private int findProvinceIndex(String province) { @@ -162,7 +180,7 @@ public class AddDialog extends JDialog { ClientHandler.deleteListener(AddPatientResponse.class, addPatientResponseListener); ClientHandler.deleteListener(GetRecentPatientResponse.class, getRecentPatientResponseListener); ClientHandler.deleteListener(GetWardResponse.class, wardResponseListener); - + // Clean up timers if (nameSearchTimer != null && nameSearchTimer.isRunning()) { nameSearchTimer.stop(); @@ -215,11 +233,12 @@ public class AddDialog extends JDialog { }); // Send request to get the latest 20 patients in the database - sendGetRecentPatientRequest(); ClientHandler.addResponseListener(GetWardResponse.class, wardResponseListener); ClientHandler.addResponseListener(AddPatientResponse.class, addPatientResponseListener); ClientHandler.addResponseListener(AddCheckupResponse.class, addCheckupResponseListener); + ClientHandler.addResponseListener(GetRecentPatientResponse.class, getRecentPatientResponseListener); + sendGetRecentPatientRequest(); // Add patent table on the right side // Add a scroll pane to the table patientTableModel = new DefaultTableModel(patientData, patientColumns) { @@ -518,10 +537,6 @@ public class AddDialog extends JDialog { gbc.weightx = 0.0; // Reset weightx for any subsequent components in this panel gbc.gridwidth = 1; // Reset gridwidth - - // Setup debounce search functionality - setupDebounceSearch(); - // Province ComboBox Listener provinceComboBox.addActionListener(new ActionListener() { @Override @@ -538,9 +553,6 @@ public class AddDialog extends JDialog { } }); - - // Ward ComboBox Listener - inputPanel.setMinimumSize(new Dimension(400, 0)); // Wrap inputPanel in a JScrollPane @@ -551,6 +563,7 @@ public class AddDialog extends JDialog { // Create right panel for table and pagination JPanel rightPanel = new JPanel(new BorderLayout()); + rightPanel.add(createSearchPanel(), BorderLayout.NORTH); // Add new search panel rightPanel.add(scrollPane, BorderLayout.CENTER); // Create pagination panel @@ -598,7 +611,7 @@ public class AddDialog extends JDialog { } int doctorId = Integer.parseInt(selectedDoctor.getId()); String selectedCheckupType = (String) checkupTypeComboBox.getSelectedItem(); - NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new AddCheckupRequest(patientId, doctorId, LocalStorage.userId, selectedCheckupType, "ĐANG KHÁM")); + NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new AddCheckupRequest(patientId, doctorId, LocalStorage.userId, selectedCheckupType, "CHỜ KHÁM")); }); saveButton.setEnabled(false); @@ -684,24 +697,18 @@ public class AddDialog extends JDialog { JOptionPane.showMessageDialog(null, "Định dạng ngày không hợp lệ: " + dateText); } }); - + + setupDebounceSearch(); + clearButton.addActionListener(e -> { - // Stop any ongoing search timers - if (nameSearchTimer != null && nameSearchTimer.isRunning()) { - nameSearchTimer.stop(); - } - if (phoneSearchTimer != null && phoneSearchTimer.isRunning()) { - phoneSearchTimer.stop(); - } - - // Clear all fields + // Clear all fields on the left patientNameField.setText(""); patientPhoneField.setText(""); patientIdField.setText(""); cccdField.setText(""); customerAddressField.setText(""); - // Reset combo boxes + // Reset combo boxes on the left patientGenderField.setSelectedIndex(0); provinceComboBox.setSelectedIndex(0); wardComboBox.setSelectedIndex(0); @@ -711,13 +718,14 @@ public class AddDialog extends JDialog { } checkupTypeComboBox.setSelectedIndex(0); - // Reset targetWard - targetWard = null; - // Clear date picker dobPicker.getModel().setValue(null); dobPicker.getJFormattedTextField().setText(""); + // Clear right-side search fields + searchNameField.setText(""); + searchPhoneField.setText(""); + // Clear table selection patientTable.clearSelection(); @@ -729,15 +737,13 @@ public class AddDialog extends JDialog { sendGetRecentPatientRequest(); }); - - // Add pagination spinner listener pageSpinner.addChangeListener(e -> { int selectedPage = (Integer) pageSpinner.getValue(); if (selectedPage != currentPage && selectedPage >= 1 && selectedPage <= totalPages) { - // Determine current search parameters - String searchName = patientNameField.getText().trim(); - String searchPhone = patientPhoneField.getText().trim(); + // Determine current search parameters from the dedicated search fields + String searchName = searchNameField.getText().trim(); + String searchPhone = searchPhoneField.getText().trim(); searchName = searchName.isEmpty() ? null : searchName; searchPhone = searchPhone.isEmpty() ? null : searchPhone; @@ -745,29 +751,25 @@ public class AddDialog extends JDialog { } }); } - + private void setupDebounceSearch() { // Initialize timers for debounce search (1 second delay) nameSearchTimer = new Timer(1000, e -> { - String searchName = patientNameField.getText().trim(); - String searchPhone = patientPhoneField.getText().trim(); - searchName = searchName.isEmpty() ? null : searchName; - searchPhone = searchPhone.isEmpty() ? null : searchPhone; - sendPatientSearchRequest(searchName, searchPhone, 1); + String searchName = searchNameField.getText().trim(); + String searchPhone = searchPhoneField.getText().trim(); + sendPatientSearchRequest(searchName.isEmpty() ? null : searchName, searchPhone.isEmpty() ? null : searchPhone, 1); }); nameSearchTimer.setRepeats(false); phoneSearchTimer = new Timer(1000, e -> { - String searchName = patientNameField.getText().trim(); - String searchPhone = patientPhoneField.getText().trim(); - searchName = searchName.isEmpty() ? null : searchName; - searchPhone = searchPhone.isEmpty() ? null : searchPhone; - sendPatientSearchRequest(searchName, searchPhone, 1); + String searchName = searchNameField.getText().trim(); + String searchPhone = searchPhoneField.getText().trim(); + sendPatientSearchRequest(searchName.isEmpty() ? null : searchName, searchPhone.isEmpty() ? null : searchPhone, 1); }); phoneSearchTimer.setRepeats(false); - // Add DocumentListener to patientNameField for debounced search - patientNameField.getDocument().addDocumentListener(new DocumentListener() { + // Add DocumentListener to searchNameField for debounced search + searchNameField.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { resetAndStartTimer(nameSearchTimer); @@ -784,8 +786,8 @@ public class AddDialog extends JDialog { } }); - // Add DocumentListener to patientPhoneField for debounced search - patientPhoneField.getDocument().addDocumentListener(new DocumentListener() { + // Add DocumentListener to searchPhoneField for debounced search + searchPhoneField.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { resetAndStartTimer(phoneSearchTimer); @@ -810,6 +812,29 @@ public class AddDialog extends JDialog { timer.start(); } + private JPanel createSearchPanel() { + JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + searchPanel.setBorder(BorderFactory.createTitledBorder("Tìm kiếm bệnh nhân")); + + searchPanel.add(new JLabel("Tên BN:")); + searchNameField = new JTextField(15); + searchPanel.add(searchNameField); + + searchPanel.add(new JLabel("SĐT:")); + searchPhoneField = new JTextField(10); + searchPanel.add(searchPhoneField); + + JButton clearSearchButton = new JButton("Xóa tìm kiếm"); + clearSearchButton.addActionListener(e -> { + searchNameField.setText(""); + searchPhoneField.setText(""); + sendGetRecentPatientRequest(); // Fetch initial list + }); + searchPanel.add(clearSearchButton); + + return searchPanel; + } + private void handleRowSelection(int selectedRow) { if (selectedRow < 0) { return; @@ -890,15 +915,27 @@ public class AddDialog extends JDialog { private void handleAddPatientResponse(AddPatientResponse response) { log.info("Received AddPatientResponse"); - if (response.isSuccess()) { - JOptionPane.showMessageDialog(this, "Thêm bệnh nhân thành công"); - patientIdField.setText(String.valueOf(response.getPatientId())); - addPatientButton.setEnabled(false); // Disable add patient button since a patient is now selected - saveButton.setEnabled(true); // Enable save button since we now have a patient - sendGetRecentPatientRequest(); - } else { - JOptionPane.showMessageDialog(this, response.getMessage()); - } + + // This entire block of logic will now be executed on the Swing EDT + SwingUtilities.invokeLater(() -> { + if (response.isSuccess()) { + JOptionPane.showMessageDialog(this, "Thêm bệnh nhân thành công"); + + // Now that the JOptionPane is closed, the connection is still alive. + // This packet will be sent successfully. + NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new GetCheckUpQueueUpdateRequest()); + + patientIdField.setText(String.valueOf(response.getPatientId())); + addPatientButton.setEnabled(false); // Disable add patient button + saveButton.setEnabled(true); // Enable save button + + // Refresh the patient list in the dialog + sendGetRecentPatientRequest(); + + } else { + JOptionPane.showMessageDialog(this, response.getMessage()); + } + }); } diff --git a/src/main/java/BsK/client/ui/component/common/NavBar.java b/src/main/java/BsK/client/ui/component/common/NavBar.java index d09382e..1438736 100644 --- a/src/main/java/BsK/client/ui/component/common/NavBar.java +++ b/src/main/java/BsK/client/ui/component/common/NavBar.java @@ -79,11 +79,11 @@ public class NavBar extends JPanel { String[] destination = {"DashboardPage", "CheckUpPage", "CheckUpPage", "SettingsPage", "InfoPage"}; String[] iconFiles = {"dashboard.png", "health-check.png", "database.png","settings.png", "info.png"}; Color[] navItemColors = { - new Color(51, 135, 204), // Darker Blue - new Color(66, 157, 21), // Darker Green - new Color(200, 138, 16), // Darker Orange - new Color(91, 37, 167), // Darker Purple - new Color(196, 27, 36) // Darker Red-Orange + new Color(51, 135, 204), // Darker Blue + new Color(66, 157, 21), // Darker Green + new Color(200, 138, 16), // Darker Orange + new Color(91, 37, 167), // Darker Purple + new Color(196, 27, 36) // Darker Red-Orange }; final Color defaultTextColor = new Color(50, 50, 50); @@ -98,8 +98,8 @@ public class NavBar extends JPanel { RoundedPanel itemPanel = new RoundedPanel(15, itemColor.brighter(), true); itemPanel.setLayout(new BorderLayout(2, 2)); itemPanel.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createEmptyBorder(2, 2, 2, 2), - BorderFactory.createEmptyBorder(5, 10, 5, 10) + BorderFactory.createEmptyBorder(2, 2, 2, 2), + BorderFactory.createEmptyBorder(5, 10, 5, 10) )); itemPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); itemPanel.setPreferredSize(new Dimension(100, 55)); @@ -117,7 +117,6 @@ public class NavBar extends JPanel { ImageIcon scaledIcon = new ImageIcon(scaledImage); label.setIcon(scaledIcon); } catch (Exception e) { - // log is not available here, print to stderr System.err.println("Error loading icon: " + iconFileName + " for nav item: " + itemText); e.printStackTrace(); } @@ -142,40 +141,38 @@ public class NavBar extends JPanel { } if ("SettingsPage".equals(dest)) { - // Open settings dialog instead of navigating to a page SettingsDialog settingsDialog = new SettingsDialog(mainFrame); settingsDialog.setVisible(true); return; } if ("Dữ liệu".equals(itemText)) { - // If not currently on CheckUpPage, navigate there first if (!(mainFrame.getCurrentPage() instanceof CheckUpPage)) { mainFrame.showPage("CheckUpPage"); - // Update active nav item to CheckUpPage if (activeNavItem != null) { JPanel prevPanel = (JPanel) activeNavItem.getParent(); prevPanel.setBackground(navItemColors[findNavItemIndex(activeNavItem.getText(), navBarItems)].brighter()); activeNavItem.setForeground(defaultTextColor); } - // Find and set the "Thăm khám" nav item as active for (int j = 0; j < navBarItems.length; j++) { if ("Thăm khám".equals(navBarItems[j])) { Component[] components = navItemsPanel.getComponents(); - if (j < components.length && components[j] instanceof JPanel) { - JPanel checkupPanel = (JPanel) components[j]; - if (checkupPanel.getComponentCount() > 0 && checkupPanel.getComponent(0) instanceof JLabel) { - JLabel checkupLabel = (JLabel) checkupPanel.getComponent(0); - activeNavItem = checkupLabel; - checkupPanel.setBackground(navItemColors[j]); - checkupLabel.setForeground(activeTextColor); + // Search through components to find the correct panel, as separators change the index + for(Component comp : components) { + if (comp instanceof JPanel && ((JPanel) comp).getComponentCount() > 0 && ((JPanel) comp).getComponent(0) instanceof JLabel) { + JLabel potentialLabel = (JLabel) ((JPanel) comp).getComponent(0); + if ("Thăm khám".equals(potentialLabel.getText())) { + activeNavItem = potentialLabel; + ((JPanel) comp).setBackground(navItemColors[j]); + potentialLabel.setForeground(activeTextColor); + break; + } } } break; } } } - // Open data dialog SwingUtilities.invokeLater(() -> { DataDialog dataDialog = new DataDialog(mainFrame); dataDialog.setVisible(true); @@ -197,20 +194,28 @@ public class NavBar extends JPanel { @Override public void mouseEntered(MouseEvent e) { if (label != activeNavItem) { - itemPanel.setBackground(itemColor); // Darken background on hover - label.setForeground(activeTextColor); // Set text to active color + itemPanel.setBackground(itemColor); + label.setForeground(activeTextColor); } } @Override public void mouseExited(MouseEvent e) { if (label != activeNavItem) { - itemPanel.setBackground(itemColor.brighter()); // Revert to default inactive background - label.setForeground(defaultTextColor); // Revert text color + itemPanel.setBackground(itemColor.brighter()); + label.setForeground(defaultTextColor); } } }); navItemsPanel.add(itemPanel); + + // ADDED: Add a visual separator after the second item ("Thăm khám") + if (i == 1) { + JSeparator separator = new JSeparator(SwingConstants.VERTICAL); + separator.setPreferredSize(new Dimension(separator.getPreferredSize().width, 40)); + separator.setForeground(new Color(180, 180, 180)); // Subtle gray color + navItemsPanel.add(separator); + } } this.add(navItemsPanel); @@ -263,4 +268,4 @@ public class NavBar extends JPanel { item.addActionListener(action); return item; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/java/BsK/common/packet/req/GetRecheckUpListRequest.java b/src/main/java/BsK/common/packet/req/GetRecheckUpListRequest.java new file mode 100644 index 0000000..66fb457 --- /dev/null +++ b/src/main/java/BsK/common/packet/req/GetRecheckUpListRequest.java @@ -0,0 +1,8 @@ +package BsK.common.packet.req; + +import BsK.common.packet.Packet; + +public class GetRecheckUpListRequest implements Packet { + public GetRecheckUpListRequest() { + } +} \ No newline at end of file diff --git a/src/main/java/BsK/common/packet/res/GetRecheckUpListResponse.java b/src/main/java/BsK/common/packet/res/GetRecheckUpListResponse.java new file mode 100644 index 0000000..182497a --- /dev/null +++ b/src/main/java/BsK/common/packet/res/GetRecheckUpListResponse.java @@ -0,0 +1,11 @@ +package BsK.common.packet.res; + +import BsK.common.packet.Packet; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class GetRecheckUpListResponse implements Packet { + private String[][] recheckUpList; +} \ No newline at end of file diff --git a/src/main/java/BsK/common/packet/res/RecheckCountResponse.java b/src/main/java/BsK/common/packet/res/RecheckCountResponse.java new file mode 100644 index 0000000..7800c83 --- /dev/null +++ b/src/main/java/BsK/common/packet/res/RecheckCountResponse.java @@ -0,0 +1,11 @@ +package BsK.common.packet.res; + +import BsK.common.packet.Packet; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class RecheckCountResponse implements Packet { + private int recheckCount; +} \ No newline at end of file diff --git a/src/main/java/BsK/common/packet/res/TodayPatientCountResponse.java b/src/main/java/BsK/common/packet/res/TodayPatientCountResponse.java new file mode 100644 index 0000000..c9a754d --- /dev/null +++ b/src/main/java/BsK/common/packet/res/TodayPatientCountResponse.java @@ -0,0 +1,11 @@ +package BsK.common.packet.res; + +import BsK.common.packet.Packet; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class TodayPatientCountResponse implements Packet { + private int totalPatientsToday; +} \ No newline at end of file diff --git a/src/main/java/BsK/server/network/handler/ServerHandler.java b/src/main/java/BsK/server/network/handler/ServerHandler.java index 796d290..e75f847 100644 --- a/src/main/java/BsK/server/network/handler/ServerHandler.java +++ b/src/main/java/BsK/server/network/handler/ServerHandler.java @@ -37,6 +37,8 @@ import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; +import java.time.ZoneId; +import java.time.LocalDate; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -116,7 +118,7 @@ public class ServerHandler extends SimpleChannelInboundHandler resultList = new ArrayList<>(); - while (rs.next()) { - String checkupId = rs.getString("checkup_id"); - String checkupDate = rs.getString("checkup_date"); - long checkupDateLong = Long.parseLong(checkupDate); - Timestamp timestamp = new Timestamp(checkupDateLong); - Date date = new Date(timestamp.getTime()); // Needed to recode - SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); - checkupDate = sdf.format(date); - String customerLastName = rs.getString("customer_last_name"); - String customerFirstName = rs.getString("customer_first_name"); - String doctorFirstName = rs.getString("doctor_first_name"); - String doctorLastName = rs.getString("doctor_last_name"); - String suggestion = rs.getString("suggestion"); - String diagnosis = rs.getString("diagnosis"); - String notes = rs.getString("notes"); - String status = rs.getString("status"); - String customerId = rs.getString("customer_id"); - String customerNumber = rs.getString("customer_number"); - String customerAddress = rs.getString("customer_address"); - String customerWeight = rs.getString("customer_weight"); - String customerHeight = rs.getString("customer_height"); - String customerGender = rs.getString("customer_gender"); - String customerDob = rs.getString("customer_dob"); - String checkupType = rs.getString("checkup_type"); - String conclusion = rs.getString("conclusion"); - String reCheckupDate = rs.getString("reCheckupDate"); - String cccdDdcn = rs.getString("cccd_ddcn"); - String heartBeat = rs.getString("heart_beat"); - String bloodPressure = rs.getString("blood_pressure"); - String driveUrl = rs.getString("drive_url"); - String doctorUltrasoundId = rs.getString("doctor_ultrasound_id"); - String queueNumber = String.format("%02d", rs.getInt("queue_number")); - if (driveUrl == null) { - driveUrl = ""; - } - String result = String.join("|", checkupId, - checkupDate, customerLastName, customerFirstName, - doctorLastName + " " + doctorFirstName, suggestion, - diagnosis, notes, status, customerId, customerNumber, customerAddress, customerWeight, customerHeight, - customerGender, customerDob, checkupType, conclusion, reCheckupDate, cccdDdcn, heartBeat, bloodPressure, - driveUrl, doctorUltrasoundId, queueNumber - ); - - resultList.add(result); - - } - - String[] resultString = resultList.toArray(new String[0]); - String[][] resultArray = new String[resultString.length][]; - for (int i = 0; i < resultString.length; i++) { - resultArray[i] = resultString[i].split("\\|"); - } - //send to all - int maxCurId = SessionManager.getMaxSessionId(); - for (int sessionId = 1; sessionId <= maxCurId; sessionId++) { - UserUtil.sendPacket(sessionId, new GetCheckUpQueueUpdateResponse(resultArray)); - } - } - } catch (SQLException e) { - throw new RuntimeException(e); - } + broadcastQueueUpdate(); } - // Get general doctor info if (packet instanceof GetDoctorGeneralInfo) { log.debug("Received GetDoctorGeneralInfo"); @@ -627,36 +547,30 @@ public class ServerHandler extends SimpleChannelInboundHandler resultList = new ArrayList<>(); - resultList.add("Phường/Xã"); - while (rs.next()) { - String wardName = rs.getString("name"); - resultList.add(wardName); + // Use try-with-resources to automatically close resources + try (PreparedStatement preparedStatement = Server.connection.prepareStatement(sql)) { + preparedStatement.setString(1, getWardRequest.getProvinceId()); + try (ResultSet rs = preparedStatement.executeQuery()) { + if (!rs.isBeforeFirst()) { + System.out.println("No data found in the ward table."); + // Send empty response + UserUtil.sendPacket(currentUser.getSessionId(), new GetWardResponse(new String[]{"Phường/Xã"})); + } else { + ArrayList resultList = new ArrayList<>(); + resultList.add("Phường/Xã"); + while (rs.next()) { + resultList.add(rs.getString("name")); + } + String[] resultString = resultList.toArray(new String[0]); + UserUtil.sendPacket(currentUser.getSessionId(), new GetWardResponse(resultString)); } - - String[] resultString = resultList.toArray(new String[0]); - - UserUtil.sendPacket(currentUser.getSessionId(), new GetWardResponse(resultString)); } - } catch (SQLException e) { - throw new RuntimeException(e); + } catch (SQLException e) { + log.error("Error fetching wards for province {}", getWardRequest.getProvinceId(), e); + throw new RuntimeException(e); } } if (packet instanceof AddPatientRequest addPatientRequest) { @@ -754,8 +668,6 @@ public class ServerHandler extends SimpleChannelInboundHandler results = new ArrayList<>(); + while (rs.next()) { + String[] row = new String[5]; + row[0] = rs.getString("customer_last_name") + " " + rs.getString("customer_first_name"); + row[1] = rs.getString("customer_number"); + + long recheckTimestamp = rs.getLong("reCheckupDate"); + long remindTimestamp = rs.getLong("remind_date"); + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + row[2] = sdf.format(new Date(recheckTimestamp)); + row[3] = sdf.format(new Date(remindTimestamp)); + row[4] = rs.getString("checkup_id"); + results.add(row); + } + + UserUtil.sendPacket(currentUser.getSessionId(), new GetRecheckUpListResponse(results.toArray(new String[0][]))); + + } catch (SQLException e) { + log.error("Error fetching recheck-up list", e); + UserUtil.sendPacket(currentUser.getSessionId(), new ErrorResponse(Error.SQL_EXCEPTION)); + } + } } } @@ -1889,4 +1842,120 @@ public class ServerHandler extends SimpleChannelInboundHandler resultList = new ArrayList<>(); + while (rs.next()) { + String checkupId = rs.getString("checkup_id"); + String checkupDate = rs.getString("checkup_date"); + long checkupDateLong = Long.parseLong(checkupDate); + Timestamp timestamp = new Timestamp(checkupDateLong); + Date date = new Date(timestamp.getTime()); + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + checkupDate = sdf.format(date); + String customerLastName = rs.getString("customer_last_name"); + String customerFirstName = rs.getString("customer_first_name"); + String doctorFirstName = rs.getString("doctor_first_name"); + String doctorLastName = rs.getString("doctor_last_name"); + String suggestion = rs.getString("suggestion"); + String diagnosis = rs.getString("diagnosis"); + String notes = rs.getString("notes"); + String status = rs.getString("status"); + String customerId = rs.getString("customer_id"); + String customerNumber = rs.getString("customer_number"); + String customerAddress = rs.getString("customer_address"); + String customerWeight = rs.getString("customer_weight"); + String customerHeight = rs.getString("customer_height"); + String customerGender = rs.getString("customer_gender"); + String customerDob = rs.getString("customer_dob"); + String checkupType = rs.getString("checkup_type"); + String conclusion = rs.getString("conclusion"); + String reCheckupDate = rs.getString("reCheckupDate"); + String cccdDdcn = rs.getString("cccd_ddcn"); + String heartBeat = rs.getString("heart_beat"); + String bloodPressure = rs.getString("blood_pressure"); + String driveUrl = rs.getString("drive_url"); + String doctorUltrasoundId = rs.getString("doctor_ultrasound_id"); + String queueNumber = String.format("%02d", rs.getInt("queue_number")); + if (driveUrl == null) { driveUrl = ""; } + String result = String.join("|", checkupId, checkupDate, customerLastName, customerFirstName, + doctorLastName + " " + doctorFirstName, suggestion, diagnosis, notes, status, customerId, customerNumber, customerAddress, customerWeight, customerHeight, + customerGender, customerDob, checkupType, conclusion, reCheckupDate, cccdDdcn, heartBeat, bloodPressure, + driveUrl, doctorUltrasoundId, queueNumber + ); + resultList.add(result); + } + String[] resultString = resultList.toArray(new String[0]); + resultArray = new String[resultString.length][]; + for (int i = 0; i < resultString.length; i++) { + resultArray[i] = resultString[i].split("\\|"); + } + } + + // --- EXISTING LOGIC FOR TODAY'S PATIENT COUNT --- + ZoneId zoneId = ZoneId.of("Asia/Ho_Chi_Minh"); + LocalDate today = LocalDate.now(zoneId); + long startOfTodayMillis = today.atStartOfDay(zoneId).toInstant().toEpochMilli(); + long endOfTodayMillis = today.plusDays(1).atStartOfDay(zoneId).toInstant().toEpochMilli() - 1; + String countQuery = String.format( + "SELECT COUNT(DISTINCT a.customer_id) as patient_count FROM checkup as a WHERE a.checkup_date >= %d AND a.checkup_date <= %d", + startOfTodayMillis, endOfTodayMillis + ); + int totalPatientsToday = 0; + ResultSet countRs = statement.executeQuery(countQuery); + if (countRs.next()) { + totalPatientsToday = countRs.getInt("patient_count"); + } + + // --- ADDED LOGIC: QUERY FOR TODAY'S RE-CHECKUP COUNT --- + String recheckQuery = String.format( + "SELECT COUNT(*) as recheck_count FROM checkup WHERE reCheckupDate >= %d AND reCheckupDate <= %d", + startOfTodayMillis, endOfTodayMillis + ); + int totalRecheckToday = 0; + ResultSet recheckRs = statement.executeQuery(recheckQuery); + if (recheckRs.next()) { + totalRecheckToday = recheckRs.getInt("recheck_count"); + } + + // --- SEND ALL RESPONSES TO ALL CLIENTS --- + int maxCurId = SessionManager.getMaxSessionId(); + for (int sessionId = 1; sessionId <= maxCurId; sessionId++) { + // Send the queue update response + UserUtil.sendPacket(sessionId, new GetCheckUpQueueUpdateResponse(resultArray)); + + // Send the patient count response + UserUtil.sendPacket(sessionId, new TodayPatientCountResponse(totalPatientsToday)); + + // Send the new re-checkup count response + UserUtil.sendPacket(sessionId, new RecheckCountResponse(totalRecheckToday)); + } + log.info("send checkup queue update response to all clients"); + log.info("send today patient count response to all clients"); + log.info("send recheck count response to all clients"); + + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + } diff --git a/src/main/resources/database/BSK.db b/src/main/resources/database/BSK.db index 752064e..adf7f07 100644 Binary files a/src/main/resources/database/BSK.db and b/src/main/resources/database/BSK.db differ