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 0000000..f8b251e Binary files /dev/null and b/image/checkup_media/2201/IMG_2201_20260705_092258_356.jpg differ diff --git a/image/checkup_media/2201/IMG_2201_20260705_092259_806.jpg b/image/checkup_media/2201/IMG_2201_20260705_092259_806.jpg new file mode 100644 index 0000000..403069e Binary files /dev/null and b/image/checkup_media/2201/IMG_2201_20260705_092259_806.jpg differ 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