Update patient search functionality in AddDialog to include patient ID. Enhance GetRecentPatientRequest to support ID-based searches and adjust ServerHandler to process ID queries. Revise CheckUpPage to automatically mark checkups as finished based on settings. Update configuration properties for application behavior.

This commit is contained in:
Dat Le committed 2026-07-07 23:22:30 -04:00
1 parent 2b53dda415
commit 5bd321b28a
8 files changed
+554 -126

No files matched your search

+2 -2
View File
@@ -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
@@ -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)<br\\s*/?>", "\n")
.replaceAll("(?i)</p>", "\n")
.replaceAll("(?i)</div>", "\n")
.replaceAll("<[^>]+>", "");
return text
.replace("&nbsp;", " ")
.replace("&amp;", "&")
.replace("&lt;", "<")
.replace("&gt;", ">")
.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);
}
}
/**
@@ -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
@@ -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);
}
}
@@ -380,6 +380,7 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
boolean hasNameSearch = getRecentPatientRequest.getSearchName() != null && !getRecentPatientRequest.getSearchName().trim().isEmpty();
boolean hasPhoneSearch = getRecentPatientRequest.getSearchPhone() != null && !getRecentPatientRequest.getSearchPhone().trim().isEmpty();
boolean hasIdSearch = getRecentPatientRequest.getSearchId() != null && !getRecentPatientRequest.getSearchId().trim().isEmpty();
// Prepare wildcard search pattern and accent-stripped term for precise filtering
String wildcardSearchPattern = null;
@@ -393,18 +394,24 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
wildcardSearchPattern = createVietnameseWildcardPattern(accentStrippedSearchName);
}
if (hasNameSearch || hasPhoneSearch) {
if (hasNameSearch || hasPhoneSearch || hasIdSearch) {
queryBuilder.append(" WHERE ");
boolean needsAnd = false;
if (hasNameSearch) {
// Use wildcard pattern that matches accented variants
queryBuilder.append("(LOWER(customer_first_name) LIKE ? OR LOWER(customer_last_name) LIKE ? OR ")
.append("LOWER(customer_last_name || ' ' || customer_first_name) LIKE ?)");
}
if (hasNameSearch && hasPhoneSearch) {
queryBuilder.append(" AND ");
needsAnd = true;
}
if (hasPhoneSearch) {
if (needsAnd) queryBuilder.append(" AND ");
queryBuilder.append("customer_number LIKE ?");
needsAnd = true;
}
if (hasIdSearch) {
if (needsAnd) queryBuilder.append(" AND ");
queryBuilder.append("CAST(customer_id AS TEXT) LIKE ?");
needsAnd = true;
}
}
@@ -429,6 +436,10 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
String searchPhone = "%" + getRecentPatientRequest.getSearchPhone().trim() + "%";
countStmt.setString(paramIndex++, searchPhone);
}
if (hasIdSearch) {
String searchId = "%" + getRecentPatientRequest.getSearchId().trim() + "%";
countStmt.setString(paramIndex++, searchId);
}
try (ResultSet countRs = countStmt.executeQuery()) {
if (countRs.next()) {
@@ -458,6 +469,10 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
String searchPhone = "%" + getRecentPatientRequest.getSearchPhone().trim() + "%";
stmt.setString(paramIndex++, searchPhone);
}
if (hasIdSearch) {
String searchId = "%" + getRecentPatientRequest.getSearchId().trim() + "%";
stmt.setString(paramIndex++, searchId);
}
stmt.setInt(paramIndex++, pageSize);
stmt.setInt(paramIndex, offset);
+10 -15
View File
@@ -16,7 +16,8 @@
<!-- ===== SERVER ===== -->
<!-- Full log: everything INFO and above -->
<RollingFile name="ServerFile" fileName="${logDir}/bsk_server.log"
filePattern="${logDir}/bsk_server-%d{yyyy-MM-dd}-%i.log.gz">
filePattern="${logDir}/bsk_server-%d{yyyy-MM-dd}-%i.log.gz"
createOnDemand="true">
<PatternLayout pattern="${pattern}"/>
<Policies>
<TimeBasedTriggeringPolicy/>
@@ -32,7 +33,8 @@
<!-- Errors only: WARN and above (easy to scan when something breaks) -->
<RollingFile name="ServerErrorFile" fileName="${logDir}/bsk_server_error.log"
filePattern="${logDir}/bsk_server_error-%d{yyyy-MM-dd}-%i.log.gz">
filePattern="${logDir}/bsk_server_error-%d{yyyy-MM-dd}-%i.log.gz"
createOnDemand="true">
<PatternLayout pattern="${pattern}"/>
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
<Policies>
@@ -50,7 +52,8 @@
<!-- ===== CLIENT ===== -->
<!-- Full log: everything INFO and above -->
<RollingFile name="ClientFile" fileName="${logDir}/bsk_client.log"
filePattern="${logDir}/bsk_client-%d{yyyy-MM-dd}-%i.log.gz">
filePattern="${logDir}/bsk_client-%d{yyyy-MM-dd}-%i.log.gz"
createOnDemand="true">
<PatternLayout pattern="${pattern}"/>
<Policies>
<TimeBasedTriggeringPolicy/>
@@ -66,7 +69,8 @@
<!-- Errors only: WARN and above -->
<RollingFile name="ClientErrorFile" fileName="${logDir}/bsk_client_error.log"
filePattern="${logDir}/bsk_client_error-%d{yyyy-MM-dd}-%i.log.gz">
filePattern="${logDir}/bsk_client_error-%d{yyyy-MM-dd}-%i.log.gz"
createOnDemand="true">
<PatternLayout pattern="${pattern}"/>
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
<Policies>
@@ -81,29 +85,20 @@
</DefaultRolloverStrategy>
</RollingFile>
<!-- Async wrappers for the high-volume full logs (better performance).
The error files stay synchronous so crash/disconnect errors are
always flushed to disk even if the app exits abruptly. -->
<Async name="ServerFileAsync">
<AppenderRef ref="ServerFile"/>
</Async>
<Async name="ClientFileAsync">
<AppenderRef ref="ClientFile"/>
</Async>
</Appenders>
<Loggers>
<!-- Server-side classes -->
<Logger name="BsK.server" level="INFO" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="ServerFileAsync"/>
<AppenderRef ref="ServerFile"/>
<AppenderRef ref="ServerErrorFile"/>
</Logger>
<!-- Client-side classes -->
<Logger name="BsK.client" level="INFO" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="ClientFileAsync"/>
<AppenderRef ref="ClientFile"/>
<AppenderRef ref="ClientErrorFile"/>
</Logger>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 B

After

Width:  |  Height:  |  Size: 98 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 B

After

Width:  |  Height:  |  Size: 512 B