From 30fde158dc9fd4cde2a5cab6d8558fc6ef6c2ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Th=C3=A0nh=20=C4=90=E1=BA=A1t?= Date: Sun, 5 Jul 2026 00:37:13 -0400 Subject: [PATCH] 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. --- .../2201/IMG_2201_20260705_092258_356.jpg | Bin 0 -> 22147 bytes .../2201/IMG_2201_20260705_092259_806.jpg | Bin 0 -> 22050 bytes src/main/java/BsK/client/Client.java | 4 - src/main/java/BsK/client/LocalStorage.java | 4 + .../client/network/handler/ClientHandler.java | 87 +- .../ui/component/CheckUpPage/CheckUpPage.java | 774 +++++++++--------- .../ui/component/DataDialog/DataDialog.java | 309 ++++--- .../component/common/AddDialog/AddDialog.java | 2 +- .../packet/req/GetCheckupDataRequest.java | 3 + src/main/java/BsK/server/Server.java | 5 - .../server/network/handler/ServerHandler.java | 118 +-- 11 files changed, 670 insertions(+), 636 deletions(-) create mode 100644 image/checkup_media/2201/IMG_2201_20260705_092258_356.jpg create mode 100644 image/checkup_media/2201/IMG_2201_20260705_092259_806.jpg diff --git a/image/checkup_media/2201/IMG_2201_20260705_092258_356.jpg b/image/checkup_media/2201/IMG_2201_20260705_092258_356.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f8b251ef7fbd5e0cbcefd2d0bea529ccc6842343 GIT binary patch literal 22147 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 literal 0 HcmV?d00001 diff --git a/src/main/java/BsK/client/Client.java b/src/main/java/BsK/client/Client.java index f881fa4..6a836ca 100644 --- a/src/main/java/BsK/client/Client.java +++ b/src/main/java/BsK/client/Client.java @@ -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); } }); diff --git a/src/main/java/BsK/client/LocalStorage.java b/src/main/java/BsK/client/LocalStorage.java index 2e9e102..85e16e2 100644 --- a/src/main/java/BsK/client/LocalStorage.java +++ b/src/main/java/BsK/client/LocalStorage.java @@ -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 doctorsName = new ArrayList<>(); diff --git a/src/main/java/BsK/client/network/handler/ClientHandler.java b/src/main/java/BsK/client/network/handler/ClientHandler.java index 1296bc3..88d601d 100644 --- a/src/main/java/BsK/client/network/handler/ClientHandler.java +++ b/src/main/java/BsK/client/network/handler/ClientHandler.java @@ -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> responseListeners = listeners.get(packet.getClass()); if (responseListeners != null && !responseListeners.isEmpty()) { @@ -172,47 +145,6 @@ public class ClientHandler extends SimpleChannelInboundHandler= 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= 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 SimpleChannelInboundHandlerDanh sách chờ (F1)"); - 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("THÊM BN (F2)"); - 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", "
In toa thuốc
(F7)
", new Color(156, 39, 176)}, + {"save", "
Lưu
(F8)
", new Color(63, 81, 181)}, + {"loupe", "
Lưu & Xem KQ
(F9)
", new Color(0, 172, 193)}, + {"ultrasound", "
Lưu & In KQ
(F10)
", 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("Danh sách chờ (F1)"); + 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("THÊM BN (F2)"); + 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", "
In toa thuốc
(F7)
", new Color(156, 39, 176)}, - {"save", "
Lưu
(F8)
", new Color(63, 81, 181)}, - {"loupe", "
Lưu & Xem KQ
(F9)
", new Color(0, 172, 193)}, - {"ultrasound", "
Lưu & In KQ
(F10)
", 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 { diff --git a/src/main/java/BsK/client/ui/component/DataDialog/DataDialog.java b/src/main/java/BsK/client/ui/component/DataDialog/DataDialog.java index e499ab9..d07093a 100644 --- a/src/main/java/BsK/client/ui/component/DataDialog/DataDialog.java +++ b/src/main/java/BsK/client/ui/component/DataDialog/DataDialog.java @@ -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..."; + // - private JTextField searchField; - private JTextField idSearchField; + private JTextField nameField; + private JTextField patientIdField; + private JTextField phoneField; + private JTextField checkupIdField; private JComboBox doctorComboBox; + private JCheckBox dateFilterCheckBox; private JSpinner fromDateSpinner; private JSpinner toDateSpinner; private JTable dataTable; @@ -148,13 +156,10 @@ public class DataDialog extends JDialog { // 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, diff --git a/src/main/java/BsK/client/ui/component/common/AddDialog/AddDialog.java b/src/main/java/BsK/client/ui/component/common/AddDialog/AddDialog.java index 125f679..b02dfd6 100644 --- a/src/main/java/BsK/client/ui/component/common/AddDialog/AddDialog.java +++ b/src/main/java/BsK/client/ui/component/common/AddDialog/AddDialog.java @@ -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); } }); } diff --git a/src/main/java/BsK/common/packet/req/GetCheckupDataRequest.java b/src/main/java/BsK/common/packet/req/GetCheckupDataRequest.java index f0aa474..39f3b6b 100644 --- a/src/main/java/BsK/common/packet/req/GetCheckupDataRequest.java +++ b/src/main/java/BsK/common/packet/req/GetCheckupDataRequest.java @@ -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; diff --git a/src/main/java/BsK/server/Server.java b/src/main/java/BsK/server/Server.java index 917ba8a..9a9aadf 100644 --- a/src/main/java/BsK/server/Server.java +++ b/src/main/java/BsK/server/Server.java @@ -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()); } }); diff --git a/src/main/java/BsK/server/network/handler/ServerHandler.java b/src/main/java/BsK/server/network/handler/ServerHandler.java index 11d6948..d0f4369 100644 --- a/src/main/java/BsK/server/network/handler/ServerHandler.java +++ b/src/main/java/BsK/server/network/handler/ServerHandler.java @@ -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 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 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