[update] Revamp CheckUpPage and DashboardPage for enhanced patient management. Update table structure to include queue number and status, improving data clarity. Implement real-time updates for patient counts and queue status, optimizing user experience. Refactor UI components for better organization and responsiveness, ensuring a cohesive interface across the application.

This commit is contained in:
Lê Thành Đạt committed 2025-07-29 21:37:08 +07:00
1 parent 0ac1516e1b
commit 1d66cb16af
12 files changed
+1036 -428

No files matched your search

Binary file not shown.
@@ -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<DefaultTableModel> 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<RowSorter.SortKey> 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<DefaultTableModel> 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<RowSorter.SortKey> 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
@@ -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<Patient> patientQueue = new ArrayList<>();
// --- LISTENERS ---
private final ResponseListener<GetCheckUpQueueUpdateResponse> checkUpQueueUpdateListener = this::handleGetCheckUpQueueUpdateResponse;
private final ResponseListener<TodayPatientCountResponse> todayPatientCountListener = this::handleTodayPatientCountResponse;
private final ResponseListener<RecheckCountResponse> 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("<html><div style='text-align: center;'>" + date + "<br>" + time + "</div></html>");
}
}
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;
}
}
@@ -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<String> dateFilterComboBox;
private String[][] allRecheckUpData; // Stores the original, unfiltered list from the server
private final ResponseListener<GetRecheckUpListResponse> 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<EncodeHintType, Object> 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)
);
}
}
@@ -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<DoctorItem> doctorComboBox;
private JComboBox<String> checkupTypeComboBox;
JButton saveButton;
private JButton addPatientButton;
private JButton clearButton;
// --- State & Listeners ---
private final ResponseListener<GetRecentPatientResponse> getRecentPatientResponseListener = this::getRecentPatientHandler;
private final ResponseListener<GetWardResponse> wardResponseListener = this::handleGetWardResponse;
private final ResponseListener<AddPatientResponse> addPatientResponseListener = this::handleAddPatientResponse;
private final ResponseListener<AddCheckupResponse> addCheckupResponseListener = this::handleAddCheckupResponse;
private JComboBox<DoctorItem> doctorComboBox;
private JComboBox<String> checkupTypeComboBox;
JButton saveButton;
private DefaultComboBoxModel<String> 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());
}
});
}
@@ -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;
}
}
}
@@ -0,0 +1,8 @@
package BsK.common.packet.req;
import BsK.common.packet.Packet;
public class GetRecheckUpListRequest implements Packet {
public GetRecheckUpListRequest() {
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<TextWebSocketFram
"from checkup as a\n" +
"join customer as c on a.customer_id = c.customer_id\n" +
"join Doctor D on a.doctor_id = D.doctor_id\n" +
"where a.status = 'ĐANG KHÁM'"
"where a.status = 'ĐANG KHÁM' or a.status = 'CHỜ KHÁM'"
);
if (!rs.isBeforeFirst()) {
@@ -185,91 +187,9 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
if (packet instanceof GetCheckUpQueueUpdateRequest) {
log.debug("Received GetCheckUpQueueUpdateRequest");
try {
ResultSet rs = statement.executeQuery(
"select a.checkup_id, a.checkup_date, c.customer_last_name, c.customer_first_name,\n" +
"d.doctor_first_name, d.doctor_last_name, a.suggestion, a.diagnosis, a.notes, a.status, a.customer_id, \n" +
"c.customer_number, c.customer_address, a.customer_weight, a.customer_height, c.customer_gender, c.customer_dob, \n" +
"a.checkup_type, a.conclusion, a.reCheckupDate, c.cccd_ddcn, a.heart_beat, a.blood_pressure, c.drive_url, a.doctor_ultrasound_id, a.queue_number\n" +
"from checkup as a\n" +
"join customer as c on a.customer_id = c.customer_id\n" +
"join Doctor D on a.doctor_id = D.doctor_id\n" +
"where a.status = 'ĐANG KHÁM'"
);
if (!rs.isBeforeFirst()) {
System.out.println("No data found in the checkup table.");
// Also send an empty response to clients so they clear their queues
int maxCurId = SessionManager.getMaxSessionId();
for (int sessionId = 1; sessionId <= maxCurId; sessionId++) {
UserUtil.sendPacket(sessionId, new GetCheckUpQueueUpdateResponse(new String[0][0]));
}
} else {
ArrayList<String> 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<TextWebSocketFram
}
if (packet instanceof GetWardRequest getWardRequest) {
log.debug("Received GetWardRequest");
try{
log.debug("Received GetWardRequest for province {}", getWardRequest.getProvinceId());
String sql = "SELECT wards.name FROM wards WHERE province_code = ? ORDER BY wards.name";
PreparedStatement preparedStatement = Server.connection.prepareStatement(
"SELECT wards.code, wards.name " +
" FROM wards " +
" WHERE province_code = ? " +
" ORDER BY wards.name"
);
preparedStatement.setString(1, getWardRequest.getProvinceId());
ResultSet rs = preparedStatement.executeQuery();
if (!rs.isBeforeFirst()) {
System.out.println("No data found in the ward table.");
} else {
ArrayList<String> 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<String> 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<TextWebSocketFram
log.error("Error updating queue counter", e);
throw new RuntimeException(e);
}
// To make it looks better when small number we mod it to 100 so the queue is only 1 to 100
queueNumber = (queueNumber % 100) + 1;
// First query: Insert Checkup
PreparedStatement checkupStmt = Server.connection.prepareStatement(
"INSERT INTO Checkup (customer_id, doctor_id, checkup_type, status, queue_number) VALUES (?, ?, ?, ?, ?)");
@@ -798,6 +710,8 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
// Send immediate response to client
UserUtil.sendPacket(currentUser.getSessionId(), new AddCheckupResponse(true, "Thêm bệnh nhân thành công", queueNumber));
broadcastQueueUpdate();
// Create Google Drive folder asynchronously (don't block the response)
createCheckupGoogleDriveFolderAsync(generatedCheckupId, addCheckupRequest.getCustomerId());
}
@@ -1486,6 +1400,45 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
log.info("Sent sync response with {} images for checkupId: {}", imageDatas.size(), checkupId);
}
if (packet instanceof GetRecheckUpListRequest getRecheckUpListRequest) {
log.debug("Received GetRecheckUpListRequest");
try {
String sql = "SELECT c.customer_last_name, c.customer_first_name, c.customer_number, ch.reCheckupDate, ch.remind_date, ch.checkup_id " +
"FROM Checkup ch " +
"JOIN Customer c ON ch.customer_id = c.customer_id " +
"WHERE ch.reCheckupDate BETWEEN ? AND ?";
long fromDate = System.currentTimeMillis();
long toDate = fromDate + (7L * 24 * 60 * 60 * 1000);
PreparedStatement stmt = Server.connection.prepareStatement(sql);
stmt.setLong(1, fromDate);
stmt.setLong(2, toDate);
ResultSet rs = stmt.executeQuery();
List<String[]> 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<TextWebSocketFram
}
}).start();
}
private void broadcastQueueUpdate() {
try {
// --- EXISTING LOGIC FOR THE QUEUE (UNCHANGED) ---
ResultSet rs = statement.executeQuery(
"select a.checkup_id, a.checkup_date, c.customer_last_name, c.customer_first_name,\n" +
"d.doctor_first_name, d.doctor_last_name, a.suggestion, a.diagnosis, a.notes, a.status, a.customer_id, \n" +
"c.customer_number, c.customer_address, a.customer_weight, a.customer_height, c.customer_gender, c.customer_dob, \n" +
"a.checkup_type, a.conclusion, a.reCheckupDate, c.cccd_ddcn, a.heart_beat, a.blood_pressure, c.drive_url, a.doctor_ultrasound_id, a.queue_number\n" +
"from checkup as a\n" +
"join customer as c on a.customer_id = c.customer_id\n" +
"join Doctor D on a.doctor_id = D.doctor_id\n" +
"where a.status = 'ĐANG KHÁM' or a.status = 'CHỜ KHÁM'"
);
String[][] resultArray;
if (!rs.isBeforeFirst()) {
System.out.println("No data found in the checkup table.");
resultArray = new String[0][0];
} else {
// (Your existing code to process the queue results remains here)
ArrayList<String> 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);
}
}
}
Binary file not shown.