diff --git a/config/config.properties b/config/config.properties index 714f407..f323dda 100644 --- a/config/config.properties +++ b/config/config.properties @@ -1,6 +1,6 @@ #BSK Clinic Management System Configuration - Updated via Settings Dialog -#Tue Aug 05 09:52:46 ICT 2025 -app.auto_change_status_to_finished=false +#Tue Jul 07 20:38:31 EDT 2026 +app.auto_change_status_to_finished=true clinic.address=112B/1 Khu ph\u1ED1 2, x\u00E3 \u0110\u1EE9c H\u00F2a, t\u1EC9nh T\u00E2y Ninh clinic.name=BS. NGUY\u1EC4N TH\u1ECA KH\u1EAEP clinic.phone=0908410007 diff --git a/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java b/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java index b57f0c8..956fab7 100644 --- a/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java +++ b/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java @@ -107,9 +107,11 @@ import javax.swing.text.MutableAttributeSet; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import javax.swing.text.Document; +import javax.swing.text.BadLocationException; import java.io.ByteArrayOutputStream; import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.io.Reader; import java.io.StringReader; // --- DateUtils Import --- @@ -1133,8 +1135,6 @@ public class CheckUpPage extends JPanel { add(centerContentPanel, BorderLayout.CENTER); - // In the constructor, after creating notesField: - setupNotesPasteHandler(); setupShortcuts(); // NOTE: CallPatientResponse is already handled by callPatientResponseListener (handleCallPatientResponse method) @@ -1375,29 +1375,10 @@ public class CheckUpPage extends JPanel { // Set preferred size for notes field notesField.setPreferredSize(new Dimension(0, 400)); // Make it tall - // Add UndoManager + // Undo/redo + copy/cut/paste key bindings notesUndoManager = new UndoManager(); notesField.getDocument().addUndoableEditListener(notesUndoManager); - - notesField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "Undo"); - notesField.getActionMap().put("Undo", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - if (notesUndoManager.canUndo()) { - notesUndoManager.undo(); - } - } - }); - - notesField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "Redo"); - notesField.getActionMap().put("Redo", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - if (notesUndoManager.canRedo()) { - notesUndoManager.redo(); - } - } - }); + setupNotesEditorKeyBindings(); // Enhanced Toolbar for notes with more formatting options @@ -1528,6 +1509,7 @@ public class CheckUpPage extends JPanel { suggestionField.setFont(fieldFont); suggestionField.setLineWrap(true); suggestionField.setWrapStyleWord(true); + setupTextAreaEditorKeyBindings(suggestionField); // addSelectAllOnFocus(suggestionField); JScrollPane suggestionScrollPane = new JScrollPane(suggestionField); suggestionScrollPane.setPreferredSize(new Dimension(0, 50)); // Reduced height @@ -1614,6 +1596,7 @@ public class CheckUpPage extends JPanel { diagnosisField.setFont(new Font("Arial", Font.BOLD, 16)); // Slightly smaller font diagnosisField.setLineWrap(true); diagnosisField.setWrapStyleWord(true); + setupTextAreaEditorKeyBindings(diagnosisField); // addSelectAllOnFocus(diagnosisField); diagnosisPanel.add(new JScrollPane(diagnosisField), BorderLayout.CENTER); @@ -1630,6 +1613,7 @@ public class CheckUpPage extends JPanel { conclusionField.setFont(new Font("Arial", Font.BOLD, 16)); // Slightly smaller font conclusionField.setLineWrap(true); conclusionField.setWrapStyleWord(true); + setupTextAreaEditorKeyBindings(conclusionField); // addSelectAllOnFocus(conclusionField); conclusionPanel.add(new JScrollPane(conclusionField), BorderLayout.CENTER); @@ -2013,10 +1997,10 @@ public class CheckUpPage extends JPanel { } // Step 3: If report creation is successful, commit by saving and printing. - // This will be turn on later - // if ("ĐANG KHÁM".equals(statusComboBox.getSelectedItem())) { - // statusComboBox.setSelectedItem("ĐÃ KHÁM"); - // } + // When enabled in Settings, automatically mark the checkup as done ("ĐÃ KHÁM") on print. + if (LocalStorage.autoChangeStatusToFinished) { + statusComboBox.setSelectedItem("ĐÃ KHÁM"); + } String statusToSavePrint = (String) statusComboBox.getSelectedItem(); handleSave(); @@ -4820,63 +4804,455 @@ public class CheckUpPage extends JPanel { } - private void handleRtfPaste() { - try { - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - Transferable content = clipboard.getContents(null); - - if (content != null) { - // Try to get RTF data first - if (content.isDataFlavorSupported(new DataFlavor("text/rtf"))) { - Object rtfData = content.getTransferData(new DataFlavor("text/rtf")); - if (rtfData instanceof InputStream) { - RTFEditorKit kit = (RTFEditorKit) notesField.getEditorKit(); - kit.read((InputStream) rtfData, notesField.getDocument(), notesField.getCaretPosition()); - return; - } - } - - // Fall back to plain text if RTF is not available - if (content.isDataFlavorSupported(DataFlavor.stringFlavor)) { - String text = (String) content.getTransferData(DataFlavor.stringFlavor); - - // Convert plain text to RTF with proper Vietnamese encoding - StringBuilder rtfContent = new StringBuilder(); - rtfContent.append("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033{\\fonttbl{\\f0\\fnil\\fcharset163 Arial;}}\n"); - rtfContent.append("{\\colortbl ;\\red0\\green0\\blue0;}\n"); - rtfContent.append("\\viewkind4\\uc1\\pard\\cf1\\f0\\fs32 "); - - // Convert Vietnamese characters to RTF Unicode escape sequences - for (char c : text.toCharArray()) { - if (c < 128) { - rtfContent.append(c); - } else { - rtfContent.append("\\u").append((int) c).append("?"); - } - } - - rtfContent.append("\\par}"); - - // Insert the RTF content - ByteArrayInputStream in = new ByteArrayInputStream(rtfContent.toString().getBytes("ISO-8859-1")); - RTFEditorKit kit = (RTFEditorKit) notesField.getEditorKit(); - kit.read(in, notesField.getDocument(), notesField.getCaretPosition()); - } + private void setupTextAreaEditorKeyBindings(JTextArea textArea) { + UndoManager undoManager = new UndoManager(); + textArea.getDocument().addUndoableEditListener(undoManager); + + int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); + InputMap inputMap = textArea.getInputMap(JComponent.WHEN_FOCUSED); + ActionMap actionMap = textArea.getActionMap(); + + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, shortcutMask), "textArea-undo"); + actionMap.put("textArea-undo", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + performUndo(undoManager); } + }); + + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, shortcutMask), "textArea-redo"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, shortcutMask | KeyEvent.SHIFT_DOWN_MASK), "textArea-redo"); + actionMap.put("textArea-redo", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + performRedo(undoManager); + } + }); + + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "textArea-copy"); + actionMap.put("textArea-copy", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + handleTextAreaCopy(textArea); + } + }); + + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, shortcutMask), "textArea-cut"); + actionMap.put("textArea-cut", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + handleTextAreaCut(textArea); + } + }); + + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "textArea-paste"); + actionMap.put("textArea-paste", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + handleTextAreaPaste(textArea); + } + }); + } + + private void handleTextAreaCopy(JTextArea textArea) { + int start = textArea.getSelectionStart(); + int end = textArea.getSelectionEnd(); + if (start == end) { + return; + } + + try { + String selected = textArea.getSelectedText(); + if (selected == null) { + selected = textArea.getDocument().getText(start, end - start); + } + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(new StringSelection(selected), null); } catch (Exception e) { - log.error("Error handling RTF paste", e); + log.warn("Custom text area copy failed, falling back to default", e); + textArea.copy(); } } - // Add paste key binding to the notes field - private void setupNotesPasteHandler() { - notesField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_V, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "customPaste"); - notesField.getActionMap().put("customPaste", new AbstractAction() { + private void handleTextAreaCut(JTextArea textArea) { + if (textArea.getSelectionStart() == textArea.getSelectionEnd()) { + return; + } + handleTextAreaCopy(textArea); + textArea.replaceSelection(""); + } + + private void handleTextAreaPaste(JTextArea textArea) { + try { + int insertPos = preparePasteInsertionPoint(textArea); + + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable content = clipboard.getContents(null); + if (content == null) { + return; + } + + if (content.isDataFlavorSupported(DataFlavor.stringFlavor)) { + String text = (String) content.getTransferData(DataFlavor.stringFlavor); + if (text != null) { + insertPlainTextAtPosition(textArea, insertPos, text); + return; + } + } + + if (content.isDataFlavorSupported(DataFlavor.allHtmlFlavor)) { + String html = (String) content.getTransferData(DataFlavor.allHtmlFlavor); + insertPlainTextAtPosition(textArea, insertPos, htmlToPlainText(html)); + return; + } + } catch (Exception e) { + log.warn("Custom text area paste failed, falling back to default", e); + } + + try { + int insertPos = preparePasteInsertionPoint(textArea); + textArea.setCaretPosition(insertPos); + textArea.paste(); + } catch (Exception e) { + log.warn("Default text area paste failed", e); + } + } + + /** + * Returns the document offset where pasted content should be inserted. + * Removes any current selection first and moves the caret to that point. + */ + private int preparePasteInsertionPoint(JTextComponent component) throws BadLocationException { + if (!component.hasFocus()) { + component.requestFocusInWindow(); + } + + int start = component.getSelectionStart(); + int end = component.getSelectionEnd(); + if (start > end) { + int temp = start; + start = end; + end = temp; + } + + Document doc = component.getDocument(); + if (start != end) { + doc.remove(start, end - start); + } else { + start = component.getCaretPosition(); + } + + component.setCaretPosition(start); + return start; + } + + private void insertPlainTextAtPosition(JTextComponent component, int position, String text) + throws BadLocationException { + if (text == null || text.isEmpty()) { + return; + } + component.getDocument().insertString(position, text, null); + component.setCaretPosition(position + text.length()); + } + + private void performUndo(UndoManager undoManager) { + if (undoManager == null || !undoManager.canUndo()) { + return; + } + try { + undoManager.undo(); + } catch (Exception e) { + log.warn("Undo failed", e); + } + } + + private void performRedo(UndoManager undoManager) { + if (undoManager == null || !undoManager.canRedo()) { + return; + } + try { + undoManager.redo(); + } catch (Exception e) { + log.warn("Redo failed", e); + } + } + + private void setupNotesEditorKeyBindings() { + int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); + InputMap inputMap = notesField.getInputMap(JComponent.WHEN_FOCUSED); + ActionMap actionMap = notesField.getActionMap(); + + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, shortcutMask), "notes-undo"); + actionMap.put("notes-undo", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { - handleRtfPaste(); + performNotesUndo(); } }); + + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, shortcutMask), "notes-redo"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, shortcutMask | KeyEvent.SHIFT_DOWN_MASK), "notes-redo"); + actionMap.put("notes-redo", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + performNotesRedo(); + } + }); + + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "notes-copy"); + actionMap.put("notes-copy", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + handleNotesCopy(); + } + }); + + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, shortcutMask), "notes-cut"); + actionMap.put("notes-cut", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + handleNotesCut(); + } + }); + + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "notes-paste"); + actionMap.put("notes-paste", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + handleNotesPaste(); + } + }); + } + + private void performNotesUndo() { + performUndo(notesUndoManager); + } + + private void performNotesRedo() { + performRedo(notesUndoManager); + } + + private void handleNotesCopy() { + int start = notesField.getSelectionStart(); + int end = notesField.getSelectionEnd(); + if (start == end) { + return; + } + + try { + ByteArrayOutputStream rtfOut = new ByteArrayOutputStream(); + notesField.getEditorKit().write(rtfOut, notesField.getDocument(), start, end - start); + byte[] rtfBytes = rtfOut.toByteArray(); + String plainText = notesField.getSelectedText(); + if (plainText == null) { + plainText = notesField.getDocument().getText(start, end - start); + } + + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(new RtfTransferable(rtfBytes, plainText), null); + } catch (Exception e) { + log.warn("Custom notes copy failed, falling back to default", e); + notesField.copy(); + } + } + + private void handleNotesCut() { + if (notesField.getSelectionStart() == notesField.getSelectionEnd()) { + return; + } + handleNotesCopy(); + notesField.replaceSelection(""); + } + + private void handleNotesPaste() { + try { + int insertPos = preparePasteInsertionPoint(notesField); + + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable content = clipboard.getContents(null); + if (content == null) { + return; + } + + if (tryPasteRtfAtPosition(content, insertPos)) { + return; + } + + if (content.isDataFlavorSupported(DataFlavor.stringFlavor)) { + String text = (String) content.getTransferData(DataFlavor.stringFlavor); + if (text != null) { + insertPlainTextAtPosition(notesField, insertPos, text); + return; + } + } + + if (content.isDataFlavorSupported(DataFlavor.allHtmlFlavor)) { + String html = (String) content.getTransferData(DataFlavor.allHtmlFlavor); + insertPlainTextAtPosition(notesField, insertPos, htmlToPlainText(html)); + return; + } + } catch (Exception e) { + log.warn("Custom notes paste failed, falling back to default", e); + } + + try { + int insertPos = preparePasteInsertionPoint(notesField); + notesField.setCaretPosition(insertPos); + notesField.paste(); + } catch (Exception e) { + log.warn("Default notes paste failed", e); + } + } + + private boolean tryPasteRtfAtPosition(Transferable content, int insertPos) throws Exception { + byte[] rtfBytes = extractRtfBytes(content); + if (rtfBytes == null || rtfBytes.length == 0) { + return false; + } + + // Read RTF into a temporary pane first, then insert the fragment at the caret. + // Reading a full RTF document directly into the main pane often appends at the end. + JEditorPane tempPane = new JEditorPane(); + tempPane.setContentType("text/rtf"); + tempPane.setEditorKit(new RTFEditorKit()); + tempPane.read(new ByteArrayInputStream(rtfBytes), null); + + Document tempDoc = tempPane.getDocument(); + int tempLen = tempDoc.getLength(); + if (tempLen <= 0) { + return false; + } + + // DefaultStyledDocument always ends with a newline; exclude it from pasted fragment. + int fragmentLen = tempLen; + if (fragmentLen > 0 && "\n".equals(tempDoc.getText(fragmentLen - 1, 1))) { + fragmentLen--; + } + if (fragmentLen <= 0) { + return false; + } + + ByteArrayOutputStream fragmentOut = new ByteArrayOutputStream(); + tempPane.getEditorKit().write(fragmentOut, tempDoc, 0, fragmentLen); + + notesField.setCaretPosition(insertPos); + RTFEditorKit kit = (RTFEditorKit) notesField.getEditorKit(); + int lengthBefore = notesField.getDocument().getLength(); + kit.read(new ByteArrayInputStream(fragmentOut.toByteArray()), notesField.getDocument(), insertPos); + int insertedLength = notesField.getDocument().getLength() - lengthBefore; + notesField.setCaretPosition(insertPos + insertedLength); + return true; + } + + private byte[] extractRtfBytes(Transferable content) throws Exception { + DataFlavor[] flavors = content.getTransferDataFlavors(); + for (DataFlavor flavor : flavors) { + if (flavor == null || !flavor.isMimeTypeEqual("text/rtf")) { + continue; + } + + Object data = content.getTransferData(flavor); + if (data instanceof byte[]) { + return (byte[]) data; + } + if (data instanceof InputStream) { + return readAllBytes((InputStream) data); + } + if (data instanceof String) { + return ((String) data).getBytes("ISO-8859-1"); + } + if (data instanceof Reader) { + char[] buffer = new char[4096]; + StringBuilder sb = new StringBuilder(); + Reader reader = (Reader) data; + int read; + while ((read = reader.read(buffer)) != -1) { + sb.append(buffer, 0, read); + } + return sb.toString().getBytes("ISO-8859-1"); + } + } + return null; + } + + private byte[] readAllBytes(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + return out.toByteArray(); + } + + private String htmlToPlainText(String html) { + if (html == null || html.isEmpty()) { + return ""; + } + String text = html + .replaceAll("(?i)", "\n") + .replaceAll("(?i)

", "\n") + .replaceAll("(?i)", "\n") + .replaceAll("<[^>]+>", ""); + return text + .replace(" ", " ") + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\r\n", "\n") + .trim(); + } + + /** Clipboard payload that exposes both RTF and plain text for copy/paste interoperability. */ + private static final class RtfTransferable implements Transferable { + private static final DataFlavor RTF_BYTES_FLAVOR; + + static { + DataFlavor flavor = null; + try { + flavor = new DataFlavor("text/rtf;class=\"[B\""); + } catch (Exception ignored) { + // Fallback handled at runtime via supported flavors list. + } + RTF_BYTES_FLAVOR = flavor; + } + + private final byte[] rtfBytes; + private final String plainText; + + private RtfTransferable(byte[] rtfBytes, String plainText) { + this.rtfBytes = rtfBytes; + this.plainText = plainText != null ? plainText : ""; + } + + @Override + public DataFlavor[] getTransferDataFlavors() { + if (RTF_BYTES_FLAVOR != null) { + return new DataFlavor[]{RTF_BYTES_FLAVOR, DataFlavor.stringFlavor}; + } + return new DataFlavor[]{DataFlavor.stringFlavor}; + } + + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + for (DataFlavor supported : getTransferDataFlavors()) { + if (supported.equals(flavor)) { + return true; + } + } + return false; + } + + @Override + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { + if (DataFlavor.stringFlavor.equals(flavor)) { + return plainText; + } + if (RTF_BYTES_FLAVOR != null && RTF_BYTES_FLAVOR.equals(flavor)) { + return rtfBytes; + } + throw new UnsupportedFlavorException(flavor); + } } /** 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 b02dfd6..ed4229c 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 @@ -46,6 +46,7 @@ public class AddDialog extends JDialog { // --- Right Panel (Search & Table) --- private JTextField searchNameField; // Dedicated search field private JTextField searchPhoneField; // Dedicated search field + private JTextField searchIdField; // Dedicated search field for Mã BN (patient ID) private DefaultTableModel patientTableModel; private JTable patientTable; private String[] patientColumns = {"Mã BN", "Tên Bệnh Nhân", "Năm sinh", "Số điện thoại" ,"Địa chỉ"}; @@ -79,6 +80,7 @@ public class AddDialog extends JDialog { // --- Debounce Search --- private Timer nameSearchTimer; private Timer phoneSearchTimer; + private Timer idSearchTimer; private void handleAddCheckupResponse(AddCheckupResponse response) { log.info("Received AddCheckupResponse"); @@ -116,12 +118,24 @@ public class AddDialog extends JDialog { } private void sendGetRecentPatientRequest() { - sendPatientSearchRequest(null, null, 1); + sendPatientSearchRequest(null, null, null, 1); } - private void sendPatientSearchRequest(String searchName, String searchPhone, int page) { - log.info("Sending GetRecentPatientRequest with search: name='{}', phone='{}', page={}", searchName, searchPhone, page); - NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new GetRecentPatientRequest(searchName, searchPhone, page, pageSize)); + private void sendPatientSearchRequest(String searchName, String searchPhone, String searchId, int page) { + log.info("Sending GetRecentPatientRequest with search: name='{}', phone='{}', id='{}', page={}", searchName, searchPhone, searchId, page); + NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new GetRecentPatientRequest(searchName, searchPhone, searchId, page, pageSize)); + } + + // Reads the current search fields and issues a search request for the given page. + private void triggerSearch(int page) { + String searchName = searchNameField.getText().trim(); + String searchPhone = searchPhoneField.getText().trim(); + String searchId = searchIdField.getText().trim(); + sendPatientSearchRequest( + searchName.isEmpty() ? null : searchName, + searchPhone.isEmpty() ? null : searchPhone, + searchId.isEmpty() ? null : searchId, + page); } private void getRecentPatientHandler(GetRecentPatientResponse response) { @@ -190,7 +204,7 @@ public class AddDialog extends JDialog { // Search on page 1 if we're not already there if (currentPage != 1) { log.info("Patient not found on current page, searching on page 1"); - sendPatientSearchRequest(null, null, 1); + sendPatientSearchRequest(null, null, null, 1); return; // Don't reset autoSelectPatientId yet, let the next response handle it } else { log.warn("Could not find newly added patient with ID: {}", autoSelectPatientId); @@ -244,6 +258,9 @@ public class AddDialog extends JDialog { if (phoneSearchTimer != null && phoneSearchTimer.isRunning()) { phoneSearchTimer.stop(); } + if (idSearchTimer != null && idSearchTimer.isRunning()) { + idSearchTimer.stop(); + } super.dispose(); } @@ -255,7 +272,7 @@ public class AddDialog extends JDialog { setDefaultCloseOperation(DISPOSE_ON_CLOSE); // Set size of the dialog - setSize(1200, 800); + setSize(1450, 820); // Put in the middle of parent window setLocationRelativeTo(parent); setResizable(true); @@ -739,6 +756,9 @@ public class AddDialog extends JDialog { inputScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); inputScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); // Usually not needed for forms inputScrollPane.setBorder(BorderFactory.createEmptyBorder()); // Remove scroll pane's own border + // Enforce a usable width for the form so the split pane can't squeeze it when the + // right side (search bar) needs more room, and so the bold labels aren't clipped. + inputScrollPane.setMinimumSize(new Dimension(560, 0)); // Create right panel for table and pagination JPanel rightPanel = new JPanel(new BorderLayout()); @@ -767,9 +787,10 @@ public class AddDialog extends JDialog { rightPanel.add(paginationPanel, BorderLayout.SOUTH); JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, inputScrollPane, rightPanel); - // Favor the right panel; set an initial divider position and resize weight - splitPane.setDividerLocation(0.25); - splitPane.setResizeWeight(0.2); + // Keep the form at a fixed, readable width (wide enough for the bold labels) + // and give all extra space to the table/search side. + splitPane.setDividerLocation(680); + splitPane.setResizeWeight(0.0); add(splitPane, BorderLayout.CENTER); @@ -880,6 +901,7 @@ public class AddDialog extends JDialog { dobPicker.getJFormattedTextField().setText(""); // Clear right-side search fields + searchIdField.setText(""); searchNameField.setText(""); searchPhoneField.setText(""); @@ -902,12 +924,7 @@ public class AddDialog extends JDialog { int selectedPage = (Integer) pageSpinner.getValue(); if (selectedPage != currentPage && selectedPage >= 1 && selectedPage <= totalPages) { // Determine current search parameters from the dedicated search fields - String searchName = searchNameField.getText().trim(); - String searchPhone = searchPhoneField.getText().trim(); - searchName = searchName.isEmpty() ? null : searchName; - searchPhone = searchPhone.isEmpty() ? null : searchPhone; - - sendPatientSearchRequest(searchName, searchPhone, selectedPage); + triggerSearch(selectedPage); } }); setDefaultLocation(); @@ -915,20 +932,15 @@ public class AddDialog extends JDialog { private void setupDebounceSearch() { // Initialize timers for debounce search (1 second delay) - nameSearchTimer = new Timer(1000, e -> { - String searchName = searchNameField.getText().trim(); - String searchPhone = searchPhoneField.getText().trim(); - sendPatientSearchRequest(searchName.isEmpty() ? null : searchName, searchPhone.isEmpty() ? null : searchPhone, 1); - }); + nameSearchTimer = new Timer(1000, e -> triggerSearch(1)); nameSearchTimer.setRepeats(false); - phoneSearchTimer = new Timer(1000, e -> { - String searchName = searchNameField.getText().trim(); - String searchPhone = searchPhoneField.getText().trim(); - sendPatientSearchRequest(searchName.isEmpty() ? null : searchName, searchPhone.isEmpty() ? null : searchPhone, 1); - }); + phoneSearchTimer = new Timer(1000, e -> triggerSearch(1)); phoneSearchTimer.setRepeats(false); + idSearchTimer = new Timer(1000, e -> triggerSearch(1)); + idSearchTimer.setRepeats(false); + // Add DocumentListener to searchNameField for debounced search searchNameField.getDocument().addDocumentListener(new DocumentListener() { @Override @@ -965,6 +977,24 @@ private void setupDebounceSearch() { } }); + // Add DocumentListener to searchIdField for debounced search + searchIdField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + resetAndStartTimer(idSearchTimer); + } + + @Override + public void removeUpdate(DocumentEvent e) { + resetAndStartTimer(idSearchTimer); + } + + @Override + public void changedUpdate(DocumentEvent e) { + resetAndStartTimer(idSearchTimer); + } + }); + patientPhoneField.getDocument().addDocumentListener(new DocumentListener() { private void syncPhoneFields() { @@ -996,20 +1026,26 @@ private void setupDebounceSearch() { } private JPanel createSearchPanel() { - JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 4)); searchPanel.setBorder(BorderFactory.createTitledBorder("Tìm kiếm bệnh nhân")); + searchPanel.add(new JLabel("Mã BN:")); + searchIdField = new JTextField(5); + searchIdField.setToolTipText("Tìm kiếm theo mã bệnh nhân"); + searchPanel.add(searchIdField); + searchPanel.add(new JLabel("Họ tên:")); - searchNameField = new JTextField(15); + searchNameField = new JTextField(12); searchNameField.setToolTipText("Tìm kiếm theo họ tên (không cần dấu)"); searchPanel.add(searchNameField); searchPanel.add(new JLabel("SĐT:")); - searchPhoneField = new JTextField(10); + searchPhoneField = new JTextField(9); searchPanel.add(searchPhoneField); JButton clearSearchButton = new JButton("Xóa tìm kiếm"); clearSearchButton.addActionListener(e -> { + searchIdField.setText(""); searchNameField.setText(""); searchPhoneField.setText(""); sendGetRecentPatientRequest(); // Fetch initial list diff --git a/src/main/java/BsK/common/packet/req/GetRecentPatientRequest.java b/src/main/java/BsK/common/packet/req/GetRecentPatientRequest.java index dc6d55a..a25107c 100644 --- a/src/main/java/BsK/common/packet/req/GetRecentPatientRequest.java +++ b/src/main/java/BsK/common/packet/req/GetRecentPatientRequest.java @@ -13,16 +13,22 @@ import lombok.extern.slf4j.Slf4j; public class GetRecentPatientRequest implements Packet { private String searchName; private String searchPhone; + private String searchId; private int page = 1; private int pageSize = 20; // Constructor for backward compatibility (gets first page with default page size) public GetRecentPatientRequest(int page, int pageSize) { - this(null, null, page, pageSize); + this(null, null, null, page, pageSize); } // Constructor for search without pagination public GetRecentPatientRequest(String searchName, String searchPhone) { - this(searchName, searchPhone, 1, 20); + this(searchName, searchPhone, null, 1, 20); + } + + // Constructor for name/phone search with pagination (no ID search) + public GetRecentPatientRequest(String searchName, String searchPhone, int page, int pageSize) { + this(searchName, searchPhone, null, page, pageSize); } } diff --git a/src/main/java/BsK/server/network/handler/ServerHandler.java b/src/main/java/BsK/server/network/handler/ServerHandler.java index d0f4369..b34b7a4 100644 --- a/src/main/java/BsK/server/network/handler/ServerHandler.java +++ b/src/main/java/BsK/server/network/handler/ServerHandler.java @@ -380,6 +380,7 @@ public class ServerHandler extends SimpleChannelInboundHandler + filePattern="${logDir}/bsk_server-%d{yyyy-MM-dd}-%i.log.gz" + createOnDemand="true"> @@ -32,7 +33,8 @@ + filePattern="${logDir}/bsk_server_error-%d{yyyy-MM-dd}-%i.log.gz" + createOnDemand="true"> @@ -50,7 +52,8 @@ + filePattern="${logDir}/bsk_client-%d{yyyy-MM-dd}-%i.log.gz" + createOnDemand="true"> @@ -66,7 +69,8 @@ + filePattern="${logDir}/bsk_client_error-%d{yyyy-MM-dd}-%i.log.gz" + createOnDemand="true"> @@ -81,29 +85,20 @@ - - - - - - - - + - + diff --git a/temp/barcodes/invoice_barcode.png b/temp/barcodes/invoice_barcode.png index 8897071..9de72aa 100644 Binary files a/temp/barcodes/invoice_barcode.png and b/temp/barcodes/invoice_barcode.png differ diff --git a/temp/qrcodes/invoice_qrcode.png b/temp/qrcodes/invoice_qrcode.png index 2f5aa8f..718e2e8 100644 Binary files a/temp/qrcodes/invoice_qrcode.png and b/temp/qrcodes/invoice_qrcode.png differ