Refactor Client and Server components to remove unused IdleStateHandler and related ping-pong logic, streamlining connection management. Enhance LocalStorage and DataDialog with additional search fields for improved patient data filtering. Update CheckUpPage to register response listeners more efficiently and improve UI control panel structure. Introduce new search parameters in GetCheckupDataRequest for better data retrieval capabilities.

This commit is contained in:
Lê Thành Đạt committed 2026-07-05 00:37:13 -04:00
1 parent d23262ce1e
commit 30fde158dc
11 files changed
+670 -636

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

-4
View File
@@ -13,7 +13,6 @@ import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import io.netty.handler.timeout.IdleStateHandler;
import java.net.URI;
import java.net.URISyntaxException;
import lombok.extern.slf4j.Slf4j;
@@ -189,9 +188,6 @@ public class Client {
true,
new DefaultHttpHeaders(),
50 * 1024 * 1024))
// Read timeout: 5 minutes (300s) - disconnect if no data from server
// Write timeout: 2 seconds - send ping every 2 seconds (CLINIC MODE - CRITICAL)
.addLast(new IdleStateHandler(40, 2, 0))
.addLast("ws", ClientHandler.INSTANCE);
}
});
@@ -30,6 +30,10 @@ public class LocalStorage {
public static Date dataDialogToDate = new Date();
public static String dataDialogDoctorName = "Tất cả";
public static String dataDialogIdSearchTerm = "";
public static String dataDialogNameSearch = "";
public static String dataDialogPatientIdSearch = "";
public static String dataDialogPhoneSearch = "";
public static boolean dataDialogDateFilterEnabled = false;
public static int unreadMessageCnt = 0;
public static List<DoctorItem> doctorsName = new ArrayList<>();
@@ -6,14 +6,10 @@ import BsK.client.ui.handler.UIHandler;
import BsK.common.entity.DoctorItem;
import BsK.common.packet.Packet;
import BsK.common.packet.PacketSerializer;
import BsK.common.packet.req.PingRequest;
import BsK.common.packet.res.*;
import BsK.common.util.network.NetworkUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
@@ -37,12 +33,6 @@ public class ClientHandler extends SimpleChannelInboundHandler<TextWebSocketFram
private static final long NETWORK_ISSUE_DIALOG_COOLDOWN = 10000; // 10 seconds
private static volatile boolean dialogCurrentlyShowing = false;
// Ping-pong tracking
private static volatile int missedPongCount = 0;
private static final int MAX_MISSED_PONGS = 3;
private static volatile long lastPingRequestTime = 0;
private static volatile boolean awaitingPongResponse = false;
// Disconnection type tracking
public enum DisconnectionType {
TIMEOUT, // Server not responding (READER_IDLE)
@@ -53,10 +43,6 @@ public class ClientHandler extends SimpleChannelInboundHandler<TextWebSocketFram
}
private static volatile DisconnectionType lastDisconnectionType = DisconnectionType.UNKNOWN;
private static volatile boolean wasTimeoutDetected = false;
private static volatile long lastPingTime = 0;
private static volatile long lastPongTime = 0;
private static volatile boolean awaitingPong = false;
/**
* Get a human-readable description of the disconnection type
@@ -144,19 +130,6 @@ public class ClientHandler extends SimpleChannelInboundHandler<TextWebSocketFram
return;
}
// Handle PongResponse
if (packet instanceof PongResponse pongResponse) {
long roundTripTime = System.currentTimeMillis() - pongResponse.getTimestamp();
// log.debug("Received PongResponse (round trip: {}ms)", roundTripTime);
// Reset missed pong counter on successful pong
missedPongCount = 0;
awaitingPongResponse = false;
wasTimeoutDetected = false;
return;
}
// Dispatch to registered listeners
List<ResponseListener<?>> responseListeners = listeners.get(packet.getClass());
if (responseListeners != null && !responseListeners.isEmpty()) {
@@ -172,47 +145,6 @@ public class ClientHandler extends SimpleChannelInboundHandler<TextWebSocketFram
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.WRITER_IDLE) {
// Send PingRequest packet to keep connection alive
if (awaitingPongResponse) {
// Previous ping not answered - increment missed counter
missedPongCount++;
log.warn("Missed pong #{} - no response to previous ping", missedPongCount);
if (missedPongCount >= MAX_MISSED_PONGS) {
log.error("Missed {} consecutive pongs - disconnecting", missedPongCount);
wasTimeoutDetected = true;
lastDisconnectionType = DisconnectionType.TIMEOUT;
ctx.close(); // Force disconnect
return;
}
}
// Send new ping packet
// log.debug("Sending PingRequest to server (missed count: {})", missedPongCount);
lastPingRequestTime = System.currentTimeMillis();
awaitingPongResponse = true;
PingRequest pingRequest = new PingRequest();
NetworkUtil.sendPacket(ctx.channel(), pingRequest);
} else if (event.state() == IdleState.READER_IDLE) {
// Server hasn't sent any data (including pong) for 5 minutes
log.warn("Server not responding - no data received for 5 minutes (READER_IDLE)");
wasTimeoutDetected = true;
lastDisconnectionType = DisconnectionType.TIMEOUT;
// Let channelInactive handle the disconnect
}
}
// IMPORTANT: Call super to ensure event propagation continues
super.userEventTriggered(ctx, evt);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("Connection exception: {}", cause.getMessage());
@@ -251,16 +183,8 @@ public class ClientHandler extends SimpleChannelInboundHandler<TextWebSocketFram
// Determine disconnection type if not already set
if (lastDisconnectionType == DisconnectionType.UNKNOWN) {
if (wasTimeoutDetected || missedPongCount >= MAX_MISSED_PONGS) {
lastDisconnectionType = DisconnectionType.TIMEOUT;
log.info("Channel inactive due to timeout (missed {} pongs)", missedPongCount);
} else if (awaitingPongResponse && (System.currentTimeMillis() - lastPingRequestTime) > 30000) {
lastDisconnectionType = DisconnectionType.TIMEOUT;
log.info("Channel inactive - server stopped responding to pings");
} else {
lastDisconnectionType = DisconnectionType.CONNECTION_LOST;
log.info("Channel inactive - abrupt connection loss");
}
lastDisconnectionType = DisconnectionType.CONNECTION_LOST;
log.info("Channel inactive - connection lost");
}
// Show appropriate dialog based on disconnection type - this forces restart
@@ -286,13 +210,6 @@ public class ClientHandler extends SimpleChannelInboundHandler<TextWebSocketFram
private void resetConnectionState() {
lastDisconnectionType = DisconnectionType.UNKNOWN;
wasTimeoutDetected = false;
awaitingPong = false;
lastPingTime = 0;
lastPongTime = 0;
missedPongCount = 0;
awaitingPongResponse = false;
lastPingRequestTime = 0;
}
/**
@@ -361,19 +361,7 @@ public class CheckUpPage extends JPanel {
JOptionPane.ERROR_MESSAGE);
}
ClientHandler.addResponseListener(GetCheckUpQueueResponse.class, checkUpQueueListener);
ClientHandler.addResponseListener(GetCheckUpQueueUpdateResponse.class, checkUpQueueUpdateListener);
ClientHandler.addResponseListener(GetPatientHistoryResponse.class, patientHistoryListener);
ClientHandler.addResponseListener(GetOrderInfoByCheckupRes.class, orderInfoByCheckupListener);
ClientHandler.addResponseListener(GetWardResponse.class, wardResponseListener);
ClientHandler.addResponseListener(CallPatientResponse.class, callPatientResponseListener);
ClientHandler.addResponseListener(GetAllTemplatesRes.class, getAllTemplatesListener);
ClientHandler.addResponseListener(UploadCheckupImageResponse.class, uploadImageResponseListener);
ClientHandler.addResponseListener(UploadCheckupPdfResponse.class, uploadPdfResponseListener);
ClientHandler.addResponseListener(SyncCheckupImagesResponse.class, syncImagesResponseListener);
ClientHandler.addResponseListener(GetCheckupImageResponse.class, getImageResponseListener);
ClientHandler.addResponseListener(DeleteCheckupImageResponse.class, deleteImageResponseListener);
ClientHandler.addResponseListener(SaveCheckupRes.class, saveCheckupResponseListener);
registerResponseListeners();
// Instantiate the new queue page but don't show it yet
queueManagementPage = new QueueManagementPage();
@@ -397,82 +385,7 @@ public class CheckUpPage extends JPanel {
add(navBar, BorderLayout.NORTH);
// --- Central Control Panel (New) ---
JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 5)); // Reduced vertical gap from 10 to 5
controlPanel.setBorder(BorderFactory.createEmptyBorder(3, 5, 3, 5)); // Reduced top/bottom from 5 to 3
controlPanel.setBackground(Color.WHITE);
openQueueButton = new JButton("<html>Danh sách chờ <font color='red'><b>(F1)</b></font></html>");
openQueueButton.setBackground(new Color(255, 152, 0)); // Amber
openQueueButton.setForeground(Color.WHITE);
openQueueButton.setFocusPainted(false);
openQueueButton.setBorder(BorderFactory.createEmptyBorder(8, 15, 8, 15)); // Reduced vertical padding from 10 to 8
openQueueButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
openQueueButton.addActionListener(e -> {
// Always fetch the latest queue to avoid cross-client mismatch
updateQueue();
// Reset filters to "Tất cả" to avoid index/filter/sort mismatch on open
if (queueManagementPage != null) {
queueManagementPage.checkupTypeFilter.setSelectedItem("Tất cả");
queueManagementPage.applyFilters();
}
queueManagementPage.setVisible(true);
queueManagementPage.toFront();
});
JButton tvQueueButton = new JButton("Màn hình chờ TV");
tvQueueButton.setBackground(new Color(0, 150, 136));
tvQueueButton.setForeground(Color.WHITE);
tvQueueButton.setFocusPainted(false);
tvQueueButton.setBorder(BorderFactory.createEmptyBorder(8, 15, 8, 15)); // Reduced vertical padding from 10 to 8
tvQueueButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
tvQueueButton.addActionListener(e -> {
if (tvQueueFrame == null || !tvQueueFrame.isDisplayable()) {
tvQueueFrame = new QueueViewPage();
} else {
tvQueueFrame.toFront();
}
tvQueueFrame.updateQueueData(this.rawQueueForTv);
tvQueueFrame.setVisible(true);
});
addPatientButton = new JButton("<html>THÊM BN <font color='red'><b>(F2)</b></font></html>");
addPatientButton.setBackground(new Color(63, 81, 181));
addPatientButton.setForeground(Color.WHITE);
addPatientButton.setFocusPainted(false);
addPatientButton.setBorder(BorderFactory.createEmptyBorder(8, 20, 8, 20)); // Reduced vertical padding from 10 to 8
addPatientButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
addPatientButton.addActionListener(e -> {
// Unregister the listener from the parent page to avoid conflicts
ClientHandler.deleteListener(GetWardResponse.class, wardResponseListener);
// Dispose any existing dialog to ensure clean listener removal
if (addDialog != null) {
addDialog.dispose();
}
addDialog = new AddDialog(mainFrame) {
@Override
public void dispose() {
super.dispose();
addDialog = null; // Clear the reference when dialog is disposed
// Re-register the listener after the dialog is actually disposed
ClientHandler.addResponseListener(GetWardResponse.class, wardResponseListener);
log.info("AddDialog disposed, re-registered ward listener for CheckUpPage.");
}
};
// Show the dialog without blocking the main thread
SwingUtilities.invokeLater(() -> {
addDialog.setVisible(true);
// The re-registration will now happen inside the dispose() override
});
updateUpdateQueue();
});
controlPanel.add(openQueueButton);
controlPanel.add(tvQueueButton);
controlPanel.add(addPatientButton);
JPanel controlPanel = buildTopControlPanel(mainFrame);
// --- Status Label ---
callingStatusLabel = new JLabel(" ", SwingConstants.CENTER);
@@ -856,82 +769,7 @@ public class CheckUpPage extends JPanel {
patientInfoInnerPanel.add(wardComboBox, gbcPatient);
// Add Room Selection Panel
JPanel roomControlPanel = new JPanel(new GridBagLayout());
roomControlPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(),
"Điều khiển phòng khám",
TitledBorder.LEADING, TitledBorder.TOP,
new Font("Arial", Font.BOLD, 14),
new Color(50, 50, 50)
));
GridBagConstraints gbcRoom = new GridBagConstraints();
gbcRoom.insets = new Insets(5, 8, 5, 8);
// --- Left Side: Controls ---
// Room Selection Label
gbcRoom.fill = GridBagConstraints.HORIZONTAL;
gbcRoom.gridx = 0; gbcRoom.gridy = 0;
JLabel roomLabel = new JLabel("Phòng khám:", SwingConstants.RIGHT);
roomLabel.setFont(labelFont);
roomControlPanel.add(roomLabel, gbcRoom);
// Room Selection ComboBox
gbcRoom.gridx = 1;
String[] roomOptions = {"Phòng 1", "Phòng 2"};
callRoomComboBox = new JComboBox<>(roomOptions);
callRoomComboBox.setFont(fieldFont);
roomControlPanel.add(callRoomComboBox, gbcRoom);
// Call Patient Button
gbcRoom.gridx = 2;
callPatientButton = new JButton("Gọi bệnh nhân");
callPatientButton.setFont(fieldFont);
callPatientButton.setBackground(new Color(33, 150, 243));
callPatientButton.setForeground(Color.WHITE);
callPatientButton.setFocusPainted(false);
callPatientButton.addActionListener(e -> handleCallPatient()); // Ensure this method updates the panels
roomControlPanel.add(callPatientButton, gbcRoom);
// Free Room Button (spans the width of the controls)
gbcRoom.gridx = 0; gbcRoom.gridy = 1; gbcRoom.gridwidth = 3;
JButton freeRoomButton = new JButton("Đánh dấu phòng trống");
freeRoomButton.setFont(fieldFont);
freeRoomButton.setBackground(new Color(46, 204, 113));
freeRoomButton.setForeground(Color.WHITE);
freeRoomButton.setFocusPainted(false);
freeRoomButton.addActionListener(e -> handleFreeRoom()); // Ensure this method updates the panels
roomControlPanel.add(freeRoomButton, gbcRoom);
// --- Right Side: Status Panels ---
// Reset constraints for status panels
gbcRoom.gridwidth = 1;
gbcRoom.gridheight = 2; // Make panels span 2 rows vertically
gbcRoom.fill = GridBagConstraints.BOTH; // Fill available space
// Room 1 Status Panel
gbcRoom.gridx = 3; gbcRoom.gridy = 0;
room1StatusPanel = new JPanel(new BorderLayout());
room1StatusPanel.setPreferredSize(new Dimension(110, 0)); // Set width, height is controlled by layout
room1StatusLabel = new JLabel("", SwingConstants.CENTER);
room1StatusLabel.putClientProperty("baseText", "PHÒNG 1"); // Store base text for reuse
styleRoomStatusPanel(room1StatusPanel, room1StatusLabel, Color.GREEN.darker(), "TRỐNG"); // Initial state
room1StatusPanel.add(room1StatusLabel, BorderLayout.CENTER);
roomControlPanel.add(room1StatusPanel, gbcRoom);
// Room 2 Status Panel
gbcRoom.gridx = 4; gbcRoom.gridy = 0;
room2StatusPanel = new JPanel(new BorderLayout());
room2StatusPanel.setPreferredSize(new Dimension(110, 0)); // Set width, height is controlled by layout
room2StatusLabel = new JLabel("", SwingConstants.CENTER);
room2StatusLabel.putClientProperty("baseText", "PHÒNG 2"); // Store base text for reuse
styleRoomStatusPanel(room2StatusPanel, room2StatusLabel, Color.GREEN.darker(), "TRỐNG"); // Initial state
room2StatusPanel.add(room2StatusLabel, BorderLayout.CENTER);
roomControlPanel.add(room2StatusPanel, gbcRoom);
JPanel roomControlPanel = buildRoomControlPanel(labelFont, fieldFont);
// --- Add the completed panel to your main layout ---
// This part remains the same
@@ -1128,6 +966,361 @@ public class CheckUpPage extends JPanel {
mainContentPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 3, 5)); // Reduced top from 10 to 5, bottom from 5 to 3
// Create left panel for Nội dung (larger)
JPanel leftPanel = buildNotesEditorPanel();
// Create right panel for Triệu chứng and Chẩn đoán
JPanel rightPanel = buildSymptomsPanel(fieldFont);
// Create split pane for left and right panels
JSplitPane contentSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
contentSplitPane.setResizeWeight(0.85); // Give maximum weight to the left panel
contentSplitPane.setDividerSize(3); // Make divider even smaller
contentSplitPane.setBorder(null);
contentSplitPane.setOneTouchExpandable(true); // Add one-touch expand/collapse buttons
// Add to main content panel
mainContentPanel.add(templatePanel, BorderLayout.NORTH);
mainContentPanel.add(contentSplitPane, BorderLayout.CENTER);
// Create a container panel to hold the top rows with vertical layout
JPanel topContainer = new JPanel();
topContainer.setLayout(new BoxLayout(topContainer, BoxLayout.Y_AXIS));
topContainer.add(topRowPanel);
topContainer.add(doctorPanel);
// Assemble checkup info panel
checkupInfoPanel.add(topContainer, BorderLayout.NORTH);
checkupInfoPanel.add(mainContentPanel, BorderLayout.CENTER);
// Create tabbed pane for patient info and checkup info
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.setFont(new Font("Arial", Font.BOLD, 14));
tabbedPane.setBackground(Color.WHITE);
tabbedPane.setForeground(new Color(63, 81, 181));
// Add tabs with icons
ImageIcon patientIcon = new ImageIcon("src/main/java/BsK/client/ui/assets/icon/user.png");
ImageIcon checkupIcon = new ImageIcon("src/main/java/BsK/client/ui/assets/icon/health-check.png");
// Scale icons if needed
if (patientIcon.getIconWidth() > 20) {
Image scaledImage = patientIcon.getImage().getScaledInstance(20, 20, Image.SCALE_SMOOTH);
patientIcon = new ImageIcon(scaledImage);
}
if (checkupIcon.getIconWidth() > 20) {
Image scaledImage = checkupIcon.getImage().getScaledInstance(20, 20, Image.SCALE_SMOOTH);
checkupIcon = new ImageIcon(scaledImage);
}
// Create a container for patient info and history
JPanel patientInfoContainer = new JPanel(new BorderLayout(0, 10));
patientInfoContainer.add(patientInfoInnerPanel, BorderLayout.CENTER);
// Add history panel to the bottom of patient info
JPanel historyPanel = buildHistoryPanel();
patientInfoContainer.add(historyPanel, BorderLayout.SOUTH);
// Create scroll panes for each panel
JScrollPane patientScrollPane = new JScrollPane(patientInfoContainer);
patientScrollPane.setBorder(BorderFactory.createEmptyBorder());
patientScrollPane.getVerticalScrollBar().setUnitIncrement(16);
JScrollPane checkupScrollPane = new JScrollPane(checkupInfoPanel);
checkupScrollPane.setBorder(BorderFactory.createEmptyBorder());
checkupScrollPane.getVerticalScrollBar().setUnitIncrement(16);
// Add tabs with icons
tabbedPane.addTab("Thông tin bệnh nhân", patientIcon, patientScrollPane);
tabbedPane.addTab("Thông tin khám bệnh", checkupIcon, checkupScrollPane);
// Style the tabs
for (int i = 0; i < tabbedPane.getTabCount(); i++) {
tabbedPane.setBackgroundAt(i, new Color(245, 245, 245));
}
// Add some padding around the tabbed pane
JPanel tabbedPaneContainer = new JPanel(new BorderLayout());
tabbedPaneContainer.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
tabbedPaneContainer.add(tabbedPane, BorderLayout.CENTER);
// New panel to hold controls at the top of the details panel
JPanel topActionPanel = new JPanel(new BorderLayout(20, 0)); // Horizontal gap
topActionPanel.add(controlPanel, BorderLayout.WEST);
topActionPanel.add(callingStatusLabel, BorderLayout.CENTER);
rightBottomPanel.add(topActionPanel, BorderLayout.NORTH);
rightBottomPanel.add(tabbedPaneContainer, BorderLayout.CENTER);
// --- Assemble New Layout ---
UIManager.getDefaults().put("SplitPane.border", BorderFactory.createEmptyBorder());
// Main Split Pane (Horizontal): Details on left, Right Panel on right
JSplitPane mainSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, rightBottomPanel, rightContainer);
mainSplitPane.setResizeWeight(0.7); // Left side gets 70% of space
mainSplitPane.setDividerSize(5); // Reduced from 8 to 5
mainSplitPane.setContinuousLayout(true);
mainSplitPane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
// Configure webcam container
webcamControlContainer.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(5, 10, 3, 5), // Reduced top from 10 to 5, bottom from 5 to 3
BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(new Color(63, 81, 181), 1, true),
"Webcam",
TitledBorder.LEADING, TitledBorder.TOP,
new Font("Arial", Font.BOLD, 16), new Color(63, 81, 181)
)
));
webcamControlContainer.setPreferredSize(new Dimension(0, 280)); // Reduced height from 300 to 280
// New Right Split Pane (Vertical): Webcam on top, Gallery in middle, Prescription on bottom
JSplitPane topRightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, webcamControlContainer, rightTopPanel);
topRightSplitPane.setResizeWeight(0.4); // Webcam gets 40% of space
topRightSplitPane.setDividerSize(3); // Reduced from 5 to 3
JSplitPane newRightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, topRightSplitPane, prescriptionDisplayPanel);
newRightSplitPane.setResizeWeight(0.7); // Top part gets 70% of space
newRightSplitPane.setDividerSize(3); // Reduced from 5 to 3
// Create a container for the right side that includes the split pane and action buttons
rightContainer = new JPanel(new BorderLayout());
rightContainer.add(newRightSplitPane, BorderLayout.CENTER);
// Create a more modern action panel
JPanel iconPanel = new JPanel(new GridLayout(2, 3, 10, 10)); // 2x3 grid with 10px gaps
iconPanel.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10)); // Reduced top/bottom from 10 to 5
iconPanel.setBackground(Color.WHITE);
// Define button properties
Object[][] buttonData = {
{"medicine", "Thêm thuốc", new Color(0, 150, 136)},
{"service", "Thêm DV", new Color(255, 152, 0)},
{"printer", "<html><center>In toa thuốc<br><font color='red'>(F7)</font></center></html>", new Color(156, 39, 176)},
{"save", "<html><center>Lưu<br><font color='red'>(F8)</font></center></html>", new Color(63, 81, 181)},
{"loupe", "<html><center>Lưu & Xem KQ<br><font color='red'>(F9)</font></center></html>", new Color(0, 172, 193)},
{"ultrasound", "<html><center>Lưu & In KQ<br><font color='red'>(F10)</font></center></html>", new Color(21, 101, 192)}
};
// Create array to store action buttons for later access
actionButtons = new JButton[buttonData.length];
for (int i = 0; i < buttonData.length; i++) {
String name = (String) buttonData[i][0];
String text = (String) buttonData[i][1];
Color color = (Color) buttonData[i][2];
JButton button = createActionButton(name, text, color);
button.setEnabled(false); // Disabled by default
button.addActionListener(e -> handleActionPanelClick(name));
iconPanel.add(button);
actionButtons[i] = button;
}
rightContainer.add(iconPanel, BorderLayout.SOUTH);
// Main Split Pane (Horizontal)
mainSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, rightBottomPanel, rightContainer);
mainSplitPane.setResizeWeight(0.7); // Left side gets 70% of space
mainSplitPane.setDividerSize(5); // Reduced from 8 to 5
mainSplitPane.setContinuousLayout(true);
mainSplitPane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
// --- Add components to the main panel ---
JPanel centerContentPanel = new JPanel(new BorderLayout(0, 3)); // Reduced vertical gap from 5 to 3
centerContentPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 5, 10)); // Reduced bottom from 10 to 5
centerContentPanel.add(mainSplitPane, BorderLayout.CENTER);
add(centerContentPanel, BorderLayout.CENTER);
// In the constructor, after creating notesField:
setupNotesPasteHandler();
setupShortcuts();
// NOTE: CallPatientResponse is already handled by callPatientResponseListener (handleCallPatientResponse method)
// which updates both tvQueueFrame and room status panels. No duplicate listener needed here.
}
/** Builds the "Điều khiển phòng khám" panel with room selection, call/free buttons and status panels. */
private JPanel buildRoomControlPanel(Font labelFont, Font fieldFont) {
JPanel roomControlPanel = new JPanel(new GridBagLayout());
roomControlPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(),
"Điều khiển phòng khám",
TitledBorder.LEADING, TitledBorder.TOP,
new Font("Arial", Font.BOLD, 14),
new Color(50, 50, 50)
));
GridBagConstraints gbcRoom = new GridBagConstraints();
gbcRoom.insets = new Insets(5, 8, 5, 8);
// --- Left Side: Controls ---
// Room Selection Label
gbcRoom.fill = GridBagConstraints.HORIZONTAL;
gbcRoom.gridx = 0; gbcRoom.gridy = 0;
JLabel roomLabel = new JLabel("Phòng khám:", SwingConstants.RIGHT);
roomLabel.setFont(labelFont);
roomControlPanel.add(roomLabel, gbcRoom);
// Room Selection ComboBox
gbcRoom.gridx = 1;
String[] roomOptions = {"Phòng 1", "Phòng 2"};
callRoomComboBox = new JComboBox<>(roomOptions);
callRoomComboBox.setFont(fieldFont);
roomControlPanel.add(callRoomComboBox, gbcRoom);
// Call Patient Button
gbcRoom.gridx = 2;
callPatientButton = new JButton("Gọi bệnh nhân");
callPatientButton.setFont(fieldFont);
callPatientButton.setBackground(new Color(33, 150, 243));
callPatientButton.setForeground(Color.WHITE);
callPatientButton.setFocusPainted(false);
callPatientButton.addActionListener(e -> handleCallPatient()); // Ensure this method updates the panels
roomControlPanel.add(callPatientButton, gbcRoom);
// Free Room Button (spans the width of the controls)
gbcRoom.gridx = 0; gbcRoom.gridy = 1; gbcRoom.gridwidth = 3;
JButton freeRoomButton = new JButton("Đánh dấu phòng trống");
freeRoomButton.setFont(fieldFont);
freeRoomButton.setBackground(new Color(46, 204, 113));
freeRoomButton.setForeground(Color.WHITE);
freeRoomButton.setFocusPainted(false);
freeRoomButton.addActionListener(e -> handleFreeRoom()); // Ensure this method updates the panels
roomControlPanel.add(freeRoomButton, gbcRoom);
// --- Right Side: Status Panels ---
// Reset constraints for status panels
gbcRoom.gridwidth = 1;
gbcRoom.gridheight = 2; // Make panels span 2 rows vertically
gbcRoom.fill = GridBagConstraints.BOTH; // Fill available space
// Room 1 Status Panel
gbcRoom.gridx = 3; gbcRoom.gridy = 0;
room1StatusPanel = new JPanel(new BorderLayout());
room1StatusPanel.setPreferredSize(new Dimension(110, 0)); // Set width, height is controlled by layout
room1StatusLabel = new JLabel("", SwingConstants.CENTER);
room1StatusLabel.putClientProperty("baseText", "PHÒNG 1"); // Store base text for reuse
styleRoomStatusPanel(room1StatusPanel, room1StatusLabel, Color.GREEN.darker(), "TRỐNG"); // Initial state
room1StatusPanel.add(room1StatusLabel, BorderLayout.CENTER);
roomControlPanel.add(room1StatusPanel, gbcRoom);
// Room 2 Status Panel
gbcRoom.gridx = 4; gbcRoom.gridy = 0;
room2StatusPanel = new JPanel(new BorderLayout());
room2StatusPanel.setPreferredSize(new Dimension(110, 0)); // Set width, height is controlled by layout
room2StatusLabel = new JLabel("", SwingConstants.CENTER);
room2StatusLabel.putClientProperty("baseText", "PHÒNG 2"); // Store base text for reuse
styleRoomStatusPanel(room2StatusPanel, room2StatusLabel, Color.GREEN.darker(), "TRỐNG"); // Initial state
room2StatusPanel.add(room2StatusLabel, BorderLayout.CENTER);
roomControlPanel.add(room2StatusPanel, gbcRoom);
return roomControlPanel;
}
/** Builds the top control bar (queue list / TV screen / add patient buttons). */
private JPanel buildTopControlPanel(MainFrame mainFrame) {
JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 5)); // Reduced vertical gap from 10 to 5
controlPanel.setBorder(BorderFactory.createEmptyBorder(3, 5, 3, 5)); // Reduced top/bottom from 5 to 3
controlPanel.setBackground(Color.WHITE);
openQueueButton = new JButton("<html>Danh sách chờ <font color='red'><b>(F1)</b></font></html>");
openQueueButton.setBackground(new Color(255, 152, 0)); // Amber
openQueueButton.setForeground(Color.WHITE);
openQueueButton.setFocusPainted(false);
openQueueButton.setBorder(BorderFactory.createEmptyBorder(8, 15, 8, 15)); // Reduced vertical padding from 10 to 8
openQueueButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
openQueueButton.addActionListener(e -> {
// Always fetch the latest queue to avoid cross-client mismatch
updateQueue();
// Reset filters to "Tất cả" to avoid index/filter/sort mismatch on open
if (queueManagementPage != null) {
queueManagementPage.checkupTypeFilter.setSelectedItem("Tất cả");
queueManagementPage.applyFilters();
}
queueManagementPage.setVisible(true);
queueManagementPage.toFront();
});
JButton tvQueueButton = new JButton("Màn hình chờ TV");
tvQueueButton.setBackground(new Color(0, 150, 136));
tvQueueButton.setForeground(Color.WHITE);
tvQueueButton.setFocusPainted(false);
tvQueueButton.setBorder(BorderFactory.createEmptyBorder(8, 15, 8, 15)); // Reduced vertical padding from 10 to 8
tvQueueButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
tvQueueButton.addActionListener(e -> {
if (tvQueueFrame == null || !tvQueueFrame.isDisplayable()) {
tvQueueFrame = new QueueViewPage();
} else {
tvQueueFrame.toFront();
}
tvQueueFrame.updateQueueData(this.rawQueueForTv);
tvQueueFrame.setVisible(true);
});
addPatientButton = new JButton("<html>THÊM BN <font color='red'><b>(F2)</b></font></html>");
addPatientButton.setBackground(new Color(63, 81, 181));
addPatientButton.setForeground(Color.WHITE);
addPatientButton.setFocusPainted(false);
addPatientButton.setBorder(BorderFactory.createEmptyBorder(8, 20, 8, 20)); // Reduced vertical padding from 10 to 8
addPatientButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
addPatientButton.addActionListener(e -> {
// Unregister the listener from the parent page to avoid conflicts
ClientHandler.deleteListener(GetWardResponse.class, wardResponseListener);
// Dispose any existing dialog to ensure clean listener removal
if (addDialog != null) {
addDialog.dispose();
}
addDialog = new AddDialog(mainFrame) {
@Override
public void dispose() {
super.dispose();
addDialog = null; // Clear the reference when dialog is disposed
// Re-register the listener after the dialog is actually disposed
ClientHandler.addResponseListener(GetWardResponse.class, wardResponseListener);
log.info("AddDialog disposed, re-registered ward listener for CheckUpPage.");
}
};
// Show the dialog without blocking the main thread
SwingUtilities.invokeLater(() -> {
addDialog.setVisible(true);
// The re-registration will now happen inside the dispose() override
});
updateUpdateQueue();
});
controlPanel.add(openQueueButton);
controlPanel.add(tvQueueButton);
controlPanel.add(addPatientButton);
return controlPanel;
}
/** Registers all server response listeners used by this page. */
private void registerResponseListeners() {
ClientHandler.addResponseListener(GetCheckUpQueueResponse.class, checkUpQueueListener);
ClientHandler.addResponseListener(GetCheckUpQueueUpdateResponse.class, checkUpQueueUpdateListener);
ClientHandler.addResponseListener(GetPatientHistoryResponse.class, patientHistoryListener);
ClientHandler.addResponseListener(GetOrderInfoByCheckupRes.class, orderInfoByCheckupListener);
ClientHandler.addResponseListener(GetWardResponse.class, wardResponseListener);
ClientHandler.addResponseListener(CallPatientResponse.class, callPatientResponseListener);
ClientHandler.addResponseListener(GetAllTemplatesRes.class, getAllTemplatesListener);
ClientHandler.addResponseListener(UploadCheckupImageResponse.class, uploadImageResponseListener);
ClientHandler.addResponseListener(UploadCheckupPdfResponse.class, uploadPdfResponseListener);
ClientHandler.addResponseListener(SyncCheckupImagesResponse.class, syncImagesResponseListener);
ClientHandler.addResponseListener(GetCheckupImageResponse.class, getImageResponseListener);
ClientHandler.addResponseListener(DeleteCheckupImageResponse.class, deleteImageResponseListener);
ClientHandler.addResponseListener(SaveCheckupRes.class, saveCheckupResponseListener);
}
/** Builds the RTF notes editor ("Nội dung") panel including its formatting toolbar. */
private JPanel buildNotesEditorPanel() {
JPanel leftPanel = new JPanel(new BorderLayout(5, 5));
leftPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(),
@@ -1311,8 +1504,11 @@ public class CheckUpPage extends JPanel {
JScrollPane notesScrollPane = new JScrollPane(notesField);
notesScrollPane.setPreferredSize(new Dimension(0, 400)); // Set scroll pane size too
leftPanel.add(notesScrollPane, BorderLayout.CENTER);
return leftPanel;
}
// Create right panel for Triệu chứng and Chẩn đoán
/** Builds the right-hand panel containing Đề nghị / Chẩn đoán / Kết luận and the re-checkup picker. */
private JPanel buildSymptomsPanel(Font fieldFont) {
JPanel rightPanel = new JPanel();
rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.Y_AXIS));
rightPanel.setPreferredSize(new Dimension(250, 0)); // Set a smaller fixed width for right panel
@@ -1442,53 +1638,11 @@ public class CheckUpPage extends JPanel {
rightPanel.add(diagnosisPanel);
rightPanel.add(Box.createRigidArea(new Dimension(0, 5))); // Reduced from 10 to 5
rightPanel.add(conclusionPanel);
return rightPanel;
}
// Create split pane for left and right panels
JSplitPane contentSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
contentSplitPane.setResizeWeight(0.85); // Give maximum weight to the left panel
contentSplitPane.setDividerSize(3); // Make divider even smaller
contentSplitPane.setBorder(null);
contentSplitPane.setOneTouchExpandable(true); // Add one-touch expand/collapse buttons
// Add to main content panel
mainContentPanel.add(templatePanel, BorderLayout.NORTH);
mainContentPanel.add(contentSplitPane, BorderLayout.CENTER);
// Create a container panel to hold the top rows with vertical layout
JPanel topContainer = new JPanel();
topContainer.setLayout(new BoxLayout(topContainer, BoxLayout.Y_AXIS));
topContainer.add(topRowPanel);
topContainer.add(doctorPanel);
// Assemble checkup info panel
checkupInfoPanel.add(topContainer, BorderLayout.NORTH);
checkupInfoPanel.add(mainContentPanel, BorderLayout.CENTER);
// Create tabbed pane for patient info and checkup info
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.setFont(new Font("Arial", Font.BOLD, 14));
tabbedPane.setBackground(Color.WHITE);
tabbedPane.setForeground(new Color(63, 81, 181));
// Add tabs with icons
ImageIcon patientIcon = new ImageIcon("src/main/java/BsK/client/ui/assets/icon/user.png");
ImageIcon checkupIcon = new ImageIcon("src/main/java/BsK/client/ui/assets/icon/health-check.png");
// Scale icons if needed
if (patientIcon.getIconWidth() > 20) {
Image scaledImage = patientIcon.getImage().getScaledInstance(20, 20, Image.SCALE_SMOOTH);
patientIcon = new ImageIcon(scaledImage);
}
if (checkupIcon.getIconWidth() > 20) {
Image scaledImage = checkupIcon.getImage().getScaledInstance(20, 20, Image.SCALE_SMOOTH);
checkupIcon = new ImageIcon(scaledImage);
}
// Create a container for patient info and history
JPanel patientInfoContainer = new JPanel(new BorderLayout(0, 10));
patientInfoContainer.add(patientInfoInnerPanel, BorderLayout.CENTER);
// Add history panel to the bottom of patient info
/** Builds the "Lịch sử khám bệnh" (patient history) table panel. */
private JPanel buildHistoryPanel() {
JPanel historyPanel = new JPanel(new BorderLayout());
historyPanel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(5, 5, 5, 5),
@@ -1543,127 +1697,7 @@ public class CheckUpPage extends JPanel {
tableScroll2.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
historyPanel.add(tableScroll2, BorderLayout.CENTER);
historyPanel.setPreferredSize(new Dimension(0, 200)); // Set fixed height for history panel
patientInfoContainer.add(historyPanel, BorderLayout.SOUTH);
// Create scroll panes for each panel
JScrollPane patientScrollPane = new JScrollPane(patientInfoContainer);
patientScrollPane.setBorder(BorderFactory.createEmptyBorder());
patientScrollPane.getVerticalScrollBar().setUnitIncrement(16);
JScrollPane checkupScrollPane = new JScrollPane(checkupInfoPanel);
checkupScrollPane.setBorder(BorderFactory.createEmptyBorder());
checkupScrollPane.getVerticalScrollBar().setUnitIncrement(16);
// Add tabs with icons
tabbedPane.addTab("Thông tin bệnh nhân", patientIcon, patientScrollPane);
tabbedPane.addTab("Thông tin khám bệnh", checkupIcon, checkupScrollPane);
// Style the tabs
for (int i = 0; i < tabbedPane.getTabCount(); i++) {
tabbedPane.setBackgroundAt(i, new Color(245, 245, 245));
}
// Add some padding around the tabbed pane
JPanel tabbedPaneContainer = new JPanel(new BorderLayout());
tabbedPaneContainer.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
tabbedPaneContainer.add(tabbedPane, BorderLayout.CENTER);
// New panel to hold controls at the top of the details panel
JPanel topActionPanel = new JPanel(new BorderLayout(20, 0)); // Horizontal gap
topActionPanel.add(controlPanel, BorderLayout.WEST);
topActionPanel.add(callingStatusLabel, BorderLayout.CENTER);
rightBottomPanel.add(topActionPanel, BorderLayout.NORTH);
rightBottomPanel.add(tabbedPaneContainer, BorderLayout.CENTER);
// --- Assemble New Layout ---
UIManager.getDefaults().put("SplitPane.border", BorderFactory.createEmptyBorder());
// Main Split Pane (Horizontal): Details on left, Right Panel on right
JSplitPane mainSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, rightBottomPanel, rightContainer);
mainSplitPane.setResizeWeight(0.7); // Left side gets 70% of space
mainSplitPane.setDividerSize(5); // Reduced from 8 to 5
mainSplitPane.setContinuousLayout(true);
mainSplitPane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
// Configure webcam container
webcamControlContainer.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(5, 10, 3, 5), // Reduced top from 10 to 5, bottom from 5 to 3
BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(new Color(63, 81, 181), 1, true),
"Webcam",
TitledBorder.LEADING, TitledBorder.TOP,
new Font("Arial", Font.BOLD, 16), new Color(63, 81, 181)
)
));
webcamControlContainer.setPreferredSize(new Dimension(0, 280)); // Reduced height from 300 to 280
// New Right Split Pane (Vertical): Webcam on top, Gallery in middle, Prescription on bottom
JSplitPane topRightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, webcamControlContainer, rightTopPanel);
topRightSplitPane.setResizeWeight(0.4); // Webcam gets 40% of space
topRightSplitPane.setDividerSize(3); // Reduced from 5 to 3
JSplitPane newRightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, topRightSplitPane, prescriptionDisplayPanel);
newRightSplitPane.setResizeWeight(0.7); // Top part gets 70% of space
newRightSplitPane.setDividerSize(3); // Reduced from 5 to 3
// Create a container for the right side that includes the split pane and action buttons
rightContainer = new JPanel(new BorderLayout());
rightContainer.add(newRightSplitPane, BorderLayout.CENTER);
// Create a more modern action panel
JPanel iconPanel = new JPanel(new GridLayout(2, 3, 10, 10)); // 2x3 grid with 10px gaps
iconPanel.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10)); // Reduced top/bottom from 10 to 5
iconPanel.setBackground(Color.WHITE);
// Define button properties
Object[][] buttonData = {
{"medicine", "Thêm thuốc", new Color(0, 150, 136)},
{"service", "Thêm DV", new Color(255, 152, 0)},
{"printer", "<html><center>In toa thuốc<br><font color='red'>(F7)</font></center></html>", new Color(156, 39, 176)},
{"save", "<html><center>Lưu<br><font color='red'>(F8)</font></center></html>", new Color(63, 81, 181)},
{"loupe", "<html><center>Lưu & Xem KQ<br><font color='red'>(F9)</font></center></html>", new Color(0, 172, 193)},
{"ultrasound", "<html><center>Lưu & In KQ<br><font color='red'>(F10)</font></center></html>", new Color(21, 101, 192)}
};
// Create array to store action buttons for later access
actionButtons = new JButton[buttonData.length];
for (int i = 0; i < buttonData.length; i++) {
String name = (String) buttonData[i][0];
String text = (String) buttonData[i][1];
Color color = (Color) buttonData[i][2];
JButton button = createActionButton(name, text, color);
button.setEnabled(false); // Disabled by default
button.addActionListener(e -> handleActionPanelClick(name));
iconPanel.add(button);
actionButtons[i] = button;
}
rightContainer.add(iconPanel, BorderLayout.SOUTH);
// Main Split Pane (Horizontal)
mainSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, rightBottomPanel, rightContainer);
mainSplitPane.setResizeWeight(0.7); // Left side gets 70% of space
mainSplitPane.setDividerSize(5); // Reduced from 8 to 5
mainSplitPane.setContinuousLayout(true);
mainSplitPane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
// --- Add components to the main panel ---
JPanel centerContentPanel = new JPanel(new BorderLayout(0, 3)); // Reduced vertical gap from 5 to 3
centerContentPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 5, 10)); // Reduced bottom from 10 to 5
centerContentPanel.add(mainSplitPane, BorderLayout.CENTER);
add(centerContentPanel, BorderLayout.CENTER);
// In the constructor, after creating notesField:
setupNotesPasteHandler();
setupShortcuts();
// NOTE: CallPatientResponse is already handled by callPatientResponseListener (handleCallPatientResponse method)
// which updates both tvQueueFrame and room status panels. No duplicate listener needed here.
return historyPanel;
}
private void addSelectAllOnFocus(JTextComponent textComponent) {
@@ -2809,27 +2843,14 @@ public class CheckUpPage extends JPanel {
initializeWebcam(selectedDevice);
} else {
cleanupWebcam();
if (webcamContainer != null) {
webcamContainer.removeAll();
JLabel noWebcamLabel = new JLabel("Chọn thiết bị webcam", SwingConstants.CENTER);
noWebcamLabel.setPreferredSize(new Dimension(180, 140));
webcamContainer.add(noWebcamLabel);
webcamContainer.revalidate();
webcamContainer.repaint();
}
showWebcamPlaceholder("Chọn thiết bị webcam");
}
});
devicePanel.add(webcamDeviceComboBox);
// Add refresh button with reload icon
ImageIcon reloadIcon = getReloadIcon();
if (reloadIcon != null) {
webcamRefreshButton = new JButton(reloadIcon);
} else {
// Fallback to emoji if icon fails to load
webcamRefreshButton = new JButton("🔄");
log.warn("Failed to load reload icon, using emoji fallback");
}
webcamRefreshButton = new JButton();
applyRefreshButtonIcon();
webcamRefreshButton.setPreferredSize(new Dimension(30, 25));
webcamRefreshButton.setToolTipText("Tìm kiếm thiết bị webcam (thay vì tự động tìm)");
webcamRefreshButton.setFocusPainted(false);
@@ -2917,6 +2938,30 @@ public class CheckUpPage extends JPanel {
return null;
}
}
// Applies the reload icon to the webcam refresh button, falling back to an emoji.
private void applyRefreshButtonIcon() {
ImageIcon reloadIcon = getReloadIcon();
if (reloadIcon != null) {
webcamRefreshButton.setText("");
webcamRefreshButton.setIcon(reloadIcon);
} else {
webcamRefreshButton.setText("🔄"); // Fallback to emoji
webcamRefreshButton.setIcon(null);
log.warn("Failed to load reload icon, using emoji fallback");
}
}
// Shows a centered placeholder message in the webcam preview area.
private void showWebcamPlaceholder(String text) {
if (webcamContainer == null) return;
webcamContainer.removeAll();
JLabel label = new JLabel(text, SwingConstants.CENTER);
label.setPreferredSize(new Dimension(180, 140));
webcamContainer.add(label);
webcamContainer.revalidate();
webcamContainer.repaint();
}
// Manual webcam device refresh
private void refreshWebcamDevices() {
@@ -2967,15 +3012,7 @@ public class CheckUpPage extends JPanel {
// Restore button with icon
webcamRefreshButton.setEnabled(true);
ImageIcon reloadIcon = getReloadIcon();
if (reloadIcon != null) {
webcamRefreshButton.setText("");
webcamRefreshButton.setIcon(reloadIcon);
} else {
webcamRefreshButton.setText("🔄"); // Fallback to emoji
webcamRefreshButton.setIcon(null);
log.warn("Failed to restore reload icon, using emoji");
}
applyRefreshButtonIcon();
if (webcams.isEmpty()) {
log.warn("No webcam devices found");
@@ -2992,15 +3029,7 @@ public class CheckUpPage extends JPanel {
webcamDeviceComboBox.addItem("Lỗi tìm thiết bị");
webcamRefreshButton.setEnabled(true);
// Restore reload icon on error
ImageIcon reloadIcon = getReloadIcon();
if (reloadIcon != null) {
webcamRefreshButton.setText("");
webcamRefreshButton.setIcon(reloadIcon);
} else {
webcamRefreshButton.setText("🔄"); // Fallback to emoji
webcamRefreshButton.setIcon(null);
log.warn("Failed to restore reload icon after error, using emoji");
}
applyRefreshButtonIcon();
});
}
});
@@ -3030,16 +3059,7 @@ public class CheckUpPage extends JPanel {
try {
// Quick cleanup without waiting
if (webcamPanel != null) {
SwingUtilities.invokeLater(() -> {
if (webcamContainer != null) {
webcamContainer.removeAll();
JLabel placeholderLabel = new JLabel("Webcam đã tắt", SwingConstants.CENTER);
placeholderLabel.setPreferredSize(new Dimension(180, 140));
webcamContainer.add(placeholderLabel);
webcamContainer.revalidate();
webcamContainer.repaint();
}
});
SwingUtilities.invokeLater(() -> showWebcamPlaceholder("Webcam đã tắt"));
// Stop webcam panel in background
try {
@@ -35,10 +35,18 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DataDialog extends JDialog {
private static final String NAME_PLACEHOLDER = "Họ và tên...";
private static final String PATIENT_ID_PLACEHOLDER = "Mã BN...";
private static final String PHONE_PLACEHOLDER = "Số điện thoại...";
private static final String CHECKUP_ID_PLACEHOLDER = "Mã phiếu khám...";
//<editor-fold desc="Properties for Checkup Data Tab">
private JTextField searchField;
private JTextField idSearchField;
private JTextField nameField;
private JTextField patientIdField;
private JTextField phoneField;
private JTextField checkupIdField;
private JComboBox<String> doctorComboBox;
private JCheckBox dateFilterCheckBox;
private JSpinner fromDateSpinner;
private JSpinner toDateSpinner;
private JTable dataTable;
@@ -148,13 +156,10 @@ public class DataDialog extends JDialog {
//<editor-fold desc="Checkup Data Tab Methods">
private void loadFiltersFromLocalStorage() {
if (LocalStorage.dataDialogSearchTerm == null || LocalStorage.dataDialogSearchTerm.isEmpty()) {
searchField.setText("Tìm theo tên bệnh nhân, mã bệnh nhân...");
searchField.setForeground(Color.GRAY);
} else {
searchField.setText(LocalStorage.dataDialogSearchTerm);
searchField.setForeground(Color.BLACK);
}
setFieldValue(nameField, NAME_PLACEHOLDER, LocalStorage.dataDialogNameSearch);
setFieldValue(patientIdField, PATIENT_ID_PLACEHOLDER, LocalStorage.dataDialogPatientIdSearch);
setFieldValue(phoneField, PHONE_PLACEHOLDER, LocalStorage.dataDialogPhoneSearch);
setFieldValue(checkupIdField, CHECKUP_ID_PLACEHOLDER, LocalStorage.dataDialogIdSearchTerm);
if (LocalStorage.dataDialogFromDate != null) {
fromDateSpinner.setValue(LocalStorage.dataDialogFromDate);
@@ -168,18 +173,69 @@ public class DataDialog extends JDialog {
toDateSpinner.setValue(new Date());
}
dateFilterCheckBox.setSelected(LocalStorage.dataDialogDateFilterEnabled);
setDateSpinnersEnabled(LocalStorage.dataDialogDateFilterEnabled);
if (LocalStorage.dataDialogDoctorName != null) {
doctorComboBox.setSelectedItem(LocalStorage.dataDialogDoctorName);
} else {
doctorComboBox.setSelectedIndex(0);
}
}
if (LocalStorage.dataDialogIdSearchTerm != null && !LocalStorage.dataDialogIdSearchTerm.isEmpty()) {
idSearchField.setText(LocalStorage.dataDialogIdSearchTerm);
idSearchField.setForeground(Color.BLACK);
/**
* Creates a text field showing gray placeholder text that clears on focus.
*/
private JTextField createPlaceholderField(String placeholder, int columns) {
JTextField field = new JTextField(placeholder, columns);
field.setForeground(Color.GRAY);
field.addFocusListener(new java.awt.event.FocusAdapter() {
public void focusGained(java.awt.event.FocusEvent evt) {
if (field.getText().equals(placeholder)) {
field.setText("");
field.setForeground(Color.BLACK);
}
}
public void focusLost(java.awt.event.FocusEvent evt) {
if (field.getText().isEmpty()) {
field.setText(placeholder);
field.setForeground(Color.GRAY);
}
}
});
return field;
}
/**
* Returns the real value of a placeholder field, or null if it is empty/showing the placeholder.
*/
private String getFieldValue(JTextField field, String placeholder) {
String text = field.getText().trim();
if (text.isEmpty() || text.equals(placeholder)) {
return null;
}
return text;
}
/**
* Sets a placeholder field to the given value, restoring gray placeholder text when empty.
*/
private void setFieldValue(JTextField field, String placeholder, String value) {
if (value == null || value.isEmpty()) {
field.setText(placeholder);
field.setForeground(Color.GRAY);
} else {
idSearchField.setText("Tìm theo mã phiếu khám...");
idSearchField.setForeground(Color.GRAY);
field.setText(value);
field.setForeground(Color.BLACK);
}
}
private void setDateSpinnersEnabled(boolean enabled) {
if (fromDateSpinner != null) {
fromDateSpinner.setEnabled(enabled);
}
if (toDateSpinner != null) {
toDateSpinner.setEnabled(enabled);
}
}
@@ -187,23 +243,17 @@ public class DataDialog extends JDialog {
JPanel controlPanel = new JPanel(new BorderLayout());
controlPanel.setBorder(BorderFactory.createTitledBorder("Tìm kiếm và Lọc dữ liệu"));
JPanel topRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10));
searchField = new JTextField("Tìm theo tên bệnh nhân, mã bệnh nhân...", 25);
searchField.setForeground(Color.GRAY);
searchField.addFocusListener(new java.awt.event.FocusAdapter() {
public void focusGained(java.awt.event.FocusEvent evt) {
if (searchField.getText().equals("Tìm theo tên bệnh nhân, mã bệnh nhân...")) {
searchField.setText("");
searchField.setForeground(Color.BLACK);
}
}
public void focusLost(java.awt.event.FocusEvent evt) {
if (searchField.getText().isEmpty()) {
searchField.setText("Tìm theo tên bệnh nhân, mã bệnh nhân...");
searchField.setForeground(Color.GRAY);
}
}
});
// --- Row 1: individual search fields (combined together with AND) ---
JPanel searchRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10));
nameField = createPlaceholderField(NAME_PLACEHOLDER, 15);
nameField.setPreferredSize(new Dimension(170, 25));
patientIdField = createPlaceholderField(PATIENT_ID_PLACEHOLDER, 8);
patientIdField.setPreferredSize(new Dimension(110, 25));
phoneField = createPlaceholderField(PHONE_PLACEHOLDER, 10);
phoneField.setPreferredSize(new Dimension(130, 25));
checkupIdField = createPlaceholderField(CHECKUP_ID_PLACEHOLDER, 10);
checkupIdField.setPreferredSize(new Dimension(130, 25));
JButton filterButton = new JButton("Lọc");
filterButton.setBackground(new Color(51, 135, 204));
@@ -213,29 +263,25 @@ public class DataDialog extends JDialog {
JButton clearFilterButton = new JButton("Xóa bộ lọc");
clearFilterButton.setPreferredSize(new Dimension(100, 30));
JButton getAllButton = new JButton("Lấy tất cả");
getAllButton.setPreferredSize(new Dimension(100, 30));
JButton exportExcelButton = new JButton("Xuất Excel");
exportExcelButton.setBackground(new Color(66, 157, 21));
exportExcelButton.setForeground(Color.WHITE);
exportExcelButton.setPreferredSize(new Dimension(100, 30));
JButton addNewButton = new JButton("+ Thêm mới");
addNewButton.setBackground(new Color(200, 138, 16));
addNewButton.setForeground(Color.WHITE);
addNewButton.setPreferredSize(new Dimension(120, 30));
topRow.add(new JLabel("🔍"));
topRow.add(searchField);
topRow.add(filterButton);
topRow.add(clearFilterButton);
topRow.add(getAllButton);
topRow.add(Box.createHorizontalStrut(20));
topRow.add(exportExcelButton);
topRow.add(addNewButton);
searchRow.add(new JLabel("🔍"));
searchRow.add(new JLabel("Họ tên:"));
searchRow.add(nameField);
searchRow.add(new JLabel("Mã BN:"));
searchRow.add(patientIdField);
searchRow.add(new JLabel("SĐT:"));
searchRow.add(phoneField);
searchRow.add(new JLabel("Mã phiếu:"));
searchRow.add(checkupIdField);
searchRow.add(filterButton);
searchRow.add(clearFilterButton);
// --- Row 2: date range, doctor and action buttons ---
JPanel bottomRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5));
// Date filtering is optional: off by default, only applied when checked
dateFilterCheckBox = new JCheckBox("Lọc theo ngày");
bottomRow.add(dateFilterCheckBox);
bottomRow.add(new JLabel("Từ ngày:"));
fromDateSpinner = new JSpinner(new SpinnerDateModel());
fromDateSpinner.setEditor(new JSpinner.DateEditor(fromDateSpinner, "dd/MM/yyyy"));
@@ -248,6 +294,10 @@ public class DataDialog extends JDialog {
toDateSpinner.setPreferredSize(new Dimension(100, 25));
bottomRow.add(toDateSpinner);
// Enable/disable date spinners with the checkbox
dateFilterCheckBox.addActionListener(e -> setDateSpinnersEnabled(dateFilterCheckBox.isSelected()));
setDateSpinnersEnabled(false);
bottomRow.add(Box.createHorizontalStrut(20));
bottomRow.add(new JLabel("Bác sĩ:"));
@@ -262,28 +312,26 @@ public class DataDialog extends JDialog {
doctorComboBox.setPreferredSize(new Dimension(150, 25));
bottomRow.add(doctorComboBox);
bottomRow.add(Box.createHorizontalStrut(15));
bottomRow.add(new JLabel("Mã Phiếu:"));
idSearchField = new JTextField("Tìm theo mã phiếu khám...", 12);
idSearchField.setForeground(Color.GRAY);
idSearchField.setPreferredSize(new Dimension(150, 25));
idSearchField.addFocusListener(new java.awt.event.FocusAdapter() {
public void focusGained(java.awt.event.FocusEvent evt) {
if (idSearchField.getText().equals("Tìm theo mã phiếu khám...")) {
idSearchField.setText("");
idSearchField.setForeground(Color.BLACK);
}
}
public void focusLost(java.awt.event.FocusEvent evt) {
if (idSearchField.getText().isEmpty()) {
idSearchField.setText("Tìm theo mã phiếu khám...");
idSearchField.setForeground(Color.GRAY);
}
}
});
bottomRow.add(idSearchField);
bottomRow.add(Box.createHorizontalStrut(20));
controlPanel.add(topRow, BorderLayout.NORTH);
JButton getAllButton = new JButton("Lấy tất cả");
getAllButton.setPreferredSize(new Dimension(100, 30));
JButton exportExcelButton = new JButton("Xuất Excel");
exportExcelButton.setBackground(new Color(66, 157, 21));
exportExcelButton.setForeground(Color.WHITE);
exportExcelButton.setPreferredSize(new Dimension(100, 30));
JButton addNewButton = new JButton("+ Thêm mới");
addNewButton.setBackground(new Color(200, 138, 16));
addNewButton.setForeground(Color.WHITE);
addNewButton.setPreferredSize(new Dimension(120, 30));
bottomRow.add(getAllButton);
bottomRow.add(exportExcelButton);
bottomRow.add(addNewButton);
controlPanel.add(searchRow, BorderLayout.NORTH);
controlPanel.add(bottomRow, BorderLayout.SOUTH);
addNewButton.addActionListener(e -> {
@@ -302,7 +350,10 @@ public class DataDialog extends JDialog {
});
getAllButton.addActionListener(e -> {
GetCheckupDataRequest request = new GetCheckupDataRequest(null, null,null, null, null, 1, recordsPerPage, LocalStorage.currentShift);
GetCheckupDataRequest request = new GetCheckupDataRequest();
request.setPage(1);
request.setPageSize(recordsPerPage);
request.setShift(LocalStorage.currentShift);
NetworkUtil.sendPacket(ClientHandler.ctx.channel(), request);
resultCountLabel.setText("Đang tải tất cả dữ liệu...");
});
@@ -317,11 +368,11 @@ public class DataDialog extends JDialog {
resultCountLabel.setFont(new Font("Arial", Font.PLAIN, 12));
gridPanel.add(resultCountLabel, BorderLayout.NORTH);
String[] columnNames = {"STT", "Mã khám", "Họ và Tên", "Năm sinh", "Giới tính", "Ngày khám", "Bác sĩ khám", "Chẩn đoán", "Kết luận", "Hành động"};
String[] columnNames = {"STT", "Mã khám", "Mã BN", "Họ và Tên", "Năm sinh", "Giới tính", "Ngày khám", "Bác sĩ khám", "Chẩn đoán", "Kết luận", "Hành động"};
tableModel = new DefaultTableModel(columnNames, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return column == 9; // Only the "Hành động" column is editable
return column == 10; // Only the "Hành động" column is editable
}
};
dataTable = new JTable(tableModel);
@@ -332,7 +383,7 @@ public class DataDialog extends JDialog {
dataTable.setSelectionBackground(new Color(230, 240, 255));
TableColumn column;
int[] columnWidths = {40, 80, 150, 80, 60, 100, 130, 150, 150, 120}; // Adjusted widths
int[] columnWidths = {40, 80, 70, 150, 80, 60, 100, 130, 150, 150, 120}; // Adjusted widths
for (int i = 0; i < columnWidths.length; i++) {
column = dataTable.getColumnModel().getColumn(i);
column.setPreferredWidth(columnWidths[i]);
@@ -347,38 +398,39 @@ public class DataDialog extends JDialog {
}
private void fetchData(int page) {
String searchTerm = searchField.getText();
if (searchTerm.equals("Tìm theo tên bệnh nhân, mã bệnh nhân...")) {
searchTerm = null;
String nameSearch = getFieldValue(nameField, NAME_PLACEHOLDER);
String patientIdSearch = getFieldValue(patientIdField, PATIENT_ID_PLACEHOLDER);
String phoneSearch = getFieldValue(phoneField, PHONE_PLACEHOLDER);
String checkupIdSearch = getFieldValue(checkupIdField, CHECKUP_ID_PLACEHOLDER);
// Date range is only applied when the user enables it
Long fromTimestamp = null;
Long toTimestamp = null;
if (dateFilterCheckBox.isSelected()) {
Date fromDate = (Date) fromDateSpinner.getValue();
Date toDate = (Date) toDateSpinner.getValue();
Calendar cal = Calendar.getInstance(DateUtils.VIETNAM_TIMEZONE); // Use Vietnam Timezone
cal.setTime(fromDate);
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
fromTimestamp = cal.getTimeInMillis();
cal.setTime(toDate);
cal.set(Calendar.HOUR_OF_DAY, 23);
cal.set(Calendar.MINUTE, 59);
cal.set(Calendar.SECOND, 59);
toTimestamp = cal.getTimeInMillis();
if (fromTimestamp > toTimestamp) {
JOptionPane.showMessageDialog(this,
"Ngày bắt đầu phải trước hoặc bằng ngày kết thúc.",
"Khoảng ngày không hợp lệ", JOptionPane.WARNING_MESSAGE);
return;
}
}
String checkupIdSearch = idSearchField.getText();
if (checkupIdSearch == null || checkupIdSearch.equals("Tìm theo mã phiếu khám...") || checkupIdSearch.trim().isEmpty()) {
checkupIdSearch = null;
}
if (checkupIdSearch != null) {
searchTerm = null;
searchField.setText("Tìm theo tên bệnh nhân, mã bệnh nhân...");
searchField.setForeground(Color.GRAY);
}
Date fromDate = (Date) fromDateSpinner.getValue();
Date toDate = (Date) toDateSpinner.getValue();
Calendar cal = Calendar.getInstance(DateUtils.VIETNAM_TIMEZONE); // Use Vietnam Timezone
cal.setTime(fromDate);
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
long fromTimestamp = cal.getTimeInMillis();
cal.setTime(toDate);
cal.set(Calendar.HOUR_OF_DAY, 23);
cal.set(Calendar.MINUTE, 59);
cal.set(Calendar.SECOND, 59);
long toTimestamp = cal.getTimeInMillis();
String selectedDoctor = (String) doctorComboBox.getSelectedItem();
Integer doctorId = null;
@@ -397,7 +449,17 @@ public class DataDialog extends JDialog {
}
}
GetCheckupDataRequest request = new GetCheckupDataRequest(searchTerm, checkupIdSearch, fromTimestamp, toTimestamp, doctorId, page, recordsPerPage, LocalStorage.currentShift);
GetCheckupDataRequest request = new GetCheckupDataRequest();
request.setCheckupIdSearch(checkupIdSearch);
request.setNameSearch(nameSearch);
request.setPatientIdSearch(patientIdSearch);
request.setPhoneSearch(phoneSearch);
request.setFromDate(fromTimestamp);
request.setToDate(toTimestamp);
request.setDoctorId(doctorId);
request.setPage(page);
request.setPageSize(recordsPerPage);
request.setShift(LocalStorage.currentShift);
NetworkUtil.sendPacket(ClientHandler.ctx.channel(), request);
resultCountLabel.setText("Đang tải dữ liệu cho trang " + page + "...");
@@ -440,6 +502,7 @@ public class DataDialog extends JDialog {
String[] tableRow = {
String.valueOf(stt++),
patient.getCheckupId(), // Changed from getCustomerId() to getCheckupId()
patient.getCustomerId(),
patient.getCustomerLastName() + " " + patient.getCustomerFirstName(),
String.valueOf(DateUtils.extractYearFromTimestamp(patient.getCustomerDob())),
patient.getCustomerGender(),
@@ -610,32 +673,42 @@ public class DataDialog extends JDialog {
}
private void saveFiltersToLocalStorage() {
String searchTerm = searchField.getText();
LocalStorage.dataDialogSearchTerm = searchTerm.equals("Tìm theo tên bệnh nhân, mã bệnh nhân...") ? "" : searchTerm;
String nameSearch = getFieldValue(nameField, NAME_PLACEHOLDER);
String patientIdSearch = getFieldValue(patientIdField, PATIENT_ID_PLACEHOLDER);
String phoneSearch = getFieldValue(phoneField, PHONE_PLACEHOLDER);
String checkupIdSearch = getFieldValue(checkupIdField, CHECKUP_ID_PLACEHOLDER);
String idSearchTerm = idSearchField.getText();
LocalStorage.dataDialogIdSearchTerm = idSearchTerm.equals("Tìm theo mã phiếu khám...") ? "" : idSearchTerm;
LocalStorage.dataDialogNameSearch = nameSearch == null ? "" : nameSearch;
LocalStorage.dataDialogPatientIdSearch = patientIdSearch == null ? "" : patientIdSearch;
LocalStorage.dataDialogPhoneSearch = phoneSearch == null ? "" : phoneSearch;
LocalStorage.dataDialogIdSearchTerm = checkupIdSearch == null ? "" : checkupIdSearch;
LocalStorage.dataDialogFromDate = (Date) fromDateSpinner.getValue();
LocalStorage.dataDialogToDate = (Date) toDateSpinner.getValue();
LocalStorage.dataDialogDateFilterEnabled = dateFilterCheckBox.isSelected();
LocalStorage.dataDialogDoctorName = (String) doctorComboBox.getSelectedItem();
}
private void clearFilters() {
searchField.setText("Tìm theo tên bệnh nhân, mã bệnh nhân...");
searchField.setForeground(Color.GRAY);
idSearchField.setText("Tìm theo mã phiếu khám...");
idSearchField.setForeground(Color.GRAY);
setFieldValue(nameField, NAME_PLACEHOLDER, null);
setFieldValue(patientIdField, PATIENT_ID_PLACEHOLDER, null);
setFieldValue(phoneField, PHONE_PLACEHOLDER, null);
setFieldValue(checkupIdField, CHECKUP_ID_PLACEHOLDER, null);
doctorComboBox.setSelectedIndex(0);
Date today = new Date();
fromDateSpinner.setValue(today);
toDateSpinner.setValue(today);
dateFilterCheckBox.setSelected(false);
setDateSpinnersEnabled(false);
LocalStorage.dataDialogSearchTerm = "";
LocalStorage.dataDialogNameSearch = "";
LocalStorage.dataDialogPatientIdSearch = "";
LocalStorage.dataDialogPhoneSearch = "";
LocalStorage.dataDialogIdSearchTerm = "";
LocalStorage.dataDialogFromDate = today;
LocalStorage.dataDialogToDate = today;
LocalStorage.dataDialogDateFilterEnabled = false;
LocalStorage.dataDialogDoctorName = "Tất cả";
}
@@ -735,7 +808,7 @@ public class DataDialog extends JDialog {
int modelRow = dataTable.convertRowIndexToModel(selectedViewRow);
String checkupIdStr = (String) tableModel.getValueAt(modelRow, 1);
String patientName = (String) tableModel.getValueAt(modelRow, 2);
String patientName = (String) tableModel.getValueAt(modelRow, 3);
int confirmation = JOptionPane.showConfirmDialog(
parentDialog,
@@ -1119,7 +1119,7 @@ private void setupDebounceSearch() {
sendGetRecentPatientRequest();
} else {
JOptionPane.showMessageDialog(this, response.getMessage());
JOptionPane.showMessageDialog(this, response.getMessage(), "Không thể thêm bệnh nhân", JOptionPane.WARNING_MESSAGE);
}
});
}
@@ -11,6 +11,9 @@ import lombok.NoArgsConstructor;
public class GetCheckupDataRequest implements Packet {
private String searchTerm;
private String checkupIdSearch;
private String nameSearch;
private String patientIdSearch;
private String phoneSearch;
private Long fromDate; // Use Long for timestamp
private Long toDate; // Use Long for timestamp
private Integer doctorId;
-5
View File
@@ -17,7 +17,6 @@ import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
import java.awt.Color;
import java.io.File;
import java.io.IOException;
@@ -294,10 +293,6 @@ public class Server {
.addLast(new ChunkedWriteHandler())
.addLast(new HttpObjectAggregator(50 * 1024 * 1024))
.addLast(new WebSocketServerProtocolHandler("/", null, true, 50 * 1024 * 1024))
// Add IdleStateHandler to detect inactive clients
// Reader timeout: 40 seconds - disconnect if no data from client (ping should arrive every 10s)
// Allow up to 4 missed pings (10s * 4 = 40s) before disconnecting
.addLast(new IdleStateHandler(40, 0, 0))
.addLast(new ServerHandler());
}
});
@@ -20,8 +20,6 @@ import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.Utf8FrameValidator;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler.HandshakeComplete;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
@@ -110,11 +108,6 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
// Check if user is authenticated
var currentUser = SessionManager.getUserByChannel(ctx.channel().id().asLongText());
if (packet instanceof PingRequest pingRequest) {
// Respond immediately to ping with pong
// log.debug("Received PingRequest from session {}, responding with PongResponse", currentUser.getSessionId());
UserUtil.sendPacket(currentUser.getSessionId(), new PongResponse(pingRequest.getTimestamp()));
}
if (currentUser == null || !currentUser.isAuthenticated()) {
log.warn("Received packet from unauthenticated user: {}", packet);
return;
@@ -587,7 +580,23 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
// SỬA ĐỔI: Bắt đầu bằng try-with-resources để lấy Connection từ pool
try (Connection conn = DatabaseManager.getConnection()) {
// Kiểm tra bệnh nhân đã tồn tại theo số điện thoại
String checkPhoneSql = "SELECT customer_last_name, customer_first_name FROM Customer WHERE customer_number = ?";
try (PreparedStatement checkStatement = conn.prepareStatement(checkPhoneSql)) {
checkStatement.setString(1, addPatientRequest.getPatientPhone());
try (ResultSet existingPatient = checkStatement.executeQuery()) {
if (existingPatient.next()) {
String existingName = (existingPatient.getString(1) + " " + existingPatient.getString(2)).trim();
log.info("Duplicate phone number detected: {}", addPatientRequest.getPatientPhone());
UserUtil.sendPacket(currentUser.getSessionId(), new AddPatientResponse(false, -1,
"Bệnh nhân đã tồn tại! Số điện thoại " + addPatientRequest.getPatientPhone()
+ " đã được đăng ký cho bệnh nhân: " + existingName));
return;
}
}
}
// 1. Bắt đầu transaction trên kết nối vừa mượn
conn.setAutoCommit(false);
@@ -1701,41 +1710,74 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
StringBuilder whereClause = new StringBuilder();
java.util.List<Object> params = new java.util.ArrayList<>();
boolean hasWhere = false;
// Mỗi bộ lọc được thêm độc lập và kết hợp với nhau bằng AND
java.util.List<String> conditions = new java.util.ArrayList<>();
// Mã phiếu khám (khớp chính xác)
String checkupIdSearch = getCheckupDataRequest.getCheckupIdSearch();
if (checkupIdSearch != null && !checkupIdSearch.trim().isEmpty()) {
whereClause.append("WHERE a.checkup_id = ?");
conditions.add("a.checkup_id = ?");
try {
params.add(Integer.parseInt(checkupIdSearch.trim()));
} catch (NumberFormatException e) {
params.add(-1);
params.add(-1);
}
hasWhere = true;
}
else if (getCheckupDataRequest.getSearchTerm() != null && !getCheckupDataRequest.getSearchTerm().trim().isEmpty()) {
// SỬA LỖI: SQLite dùng || để nối chuỗi, không phải CONCAT()
whereClause.append("WHERE (LOWER(c.customer_first_name) LIKE ? OR LOWER(c.customer_last_name) LIKE ? OR " +
"LOWER(c.customer_last_name || ' ' || c.customer_first_name) LIKE ? OR c.customer_number LIKE ?)");
String searchTerm = "%" + getCheckupDataRequest.getSearchTerm().toLowerCase().trim() + "%";
params.add(searchTerm);
params.add(searchTerm);
params.add(searchTerm);
params.add(searchTerm);
hasWhere = true;
}
// Họ tên (khớp một phần: tên, họ, họ tên đầy đủ)
String nameSearch = getCheckupDataRequest.getNameSearch();
if (nameSearch != null && !nameSearch.trim().isEmpty()) {
// SỬA LỖI: SQLite dùng || để nối chuỗi, không phải CONCAT()
conditions.add("(LOWER(c.customer_first_name) LIKE ? OR LOWER(c.customer_last_name) LIKE ? OR " +
"LOWER(c.customer_last_name || ' ' || c.customer_first_name) LIKE ?)");
String t = "%" + nameSearch.toLowerCase().trim() + "%";
params.add(t);
params.add(t);
params.add(t);
}
// Mã bệnh nhân (khớp một phần)
String patientIdSearch = getCheckupDataRequest.getPatientIdSearch();
if (patientIdSearch != null && !patientIdSearch.trim().isEmpty()) {
conditions.add("CAST(c.customer_id AS TEXT) LIKE ?");
params.add("%" + patientIdSearch.trim() + "%");
}
// Số điện thoại (khớp một phần)
String phoneSearch = getCheckupDataRequest.getPhoneSearch();
if (phoneSearch != null && !phoneSearch.trim().isEmpty()) {
conditions.add("c.customer_number LIKE ?");
params.add("%" + phoneSearch.trim() + "%");
}
// Tìm kiếm chung (tương thích ngược): tên, SĐT, mã bệnh nhân
String searchTerm = getCheckupDataRequest.getSearchTerm();
if (searchTerm != null && !searchTerm.trim().isEmpty()) {
conditions.add("(LOWER(c.customer_first_name) LIKE ? OR LOWER(c.customer_last_name) LIKE ? OR " +
"LOWER(c.customer_last_name || ' ' || c.customer_first_name) LIKE ? OR c.customer_number LIKE ? OR " +
"CAST(c.customer_id AS TEXT) LIKE ?)");
String t = "%" + searchTerm.toLowerCase().trim() + "%";
params.add(t);
params.add(t);
params.add(t);
params.add(t);
params.add(t);
}
if (getCheckupDataRequest.getFromDate() != null && getCheckupDataRequest.getToDate() != null) {
whereClause.append(hasWhere ? " AND " : "WHERE ").append("a.checkup_date BETWEEN ? AND ?");
conditions.add("a.checkup_date BETWEEN ? AND ?");
params.add(getCheckupDataRequest.getFromDate());
params.add(getCheckupDataRequest.getToDate());
hasWhere = true;
}
if (getCheckupDataRequest.getDoctorId() != null && getCheckupDataRequest.getDoctorId() > 0) {
whereClause.append(hasWhere ? " AND " : "WHERE ").append("a.doctor_id = ?");
conditions.add("a.doctor_id = ?");
params.add(getCheckupDataRequest.getDoctorId());
}
if (!conditions.isEmpty()) {
whereClause.append("WHERE ").append(String.join(" AND ", conditions));
}
int totalRecords = 0;
String countQuery = "SELECT COUNT(*) " + fromAndJoin + whereClause;
@@ -2708,23 +2750,7 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent event) {
if (event.state() == IdleState.READER_IDLE) {
try {
var user = SessionManager.getUserByChannel(ctx.channel().id().asLongText());
if (user != null) {
ServerDashboard.getInstance().addLog("Client timed out: Session " + user.getSessionId());
}
SessionManager.onUserDisconnect(ctx.channel());
if (user != null) {
user.resetAuthentication();
}
ctx.channel().close();
} catch (Exception e) {
ServerDashboard.getInstance().addLog("Error during client timeout: " + e.getMessage());
}
}
} else if (evt instanceof HandshakeComplete) {
if (evt instanceof HandshakeComplete) {
int SessionId = SessionManager.onUserLogin(ctx.channel());
log.info("Session {} logged in", SessionId);
ServerDashboard.getInstance().addLog("New client connected: Session " + SessionId);