mirror of
https://github.com/tiennm99/BSK-All-in-One-Clinic-Management-System.git
synced 2026-10-16 16:15:08 +00:00
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:
1 parent
2b53dda415
commit
5bd321b28a
8 files changed
+554
-126
No files matched your search
@@ -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(" ", " ")
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 |
Reference in new issue
Block a user