This commit is contained in:
Lê Thành Đạt committed 2025-08-01 17:19:12 +07:00
1 parent 9b498ac9e6
commit a583b133bc
14 files changed
+403 -269

No files matched your search

@@ -49,7 +49,6 @@ public class ClientHandler extends SimpleChannelInboundHandler<TextWebSocketFram
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
log.debug("Received message: {}", frame.text());
var packet = PacketSerializer.GSON.fromJson(frame.text(), Packet.class);
if (packet != null) {
@@ -124,26 +124,11 @@ public class DashboardPage extends JPanel {
loadDashboardData();
startRealTimeClock();
addAncestorListener(new AncestorListener() {
@Override
public void ancestorAdded(AncestorEvent event) {
ClientHandler.addResponseListener(GetCheckUpQueueUpdateResponse.class, checkUpQueueUpdateListener);
ClientHandler.addResponseListener(TodayPatientCountResponse.class, todayPatientCountListener);
ClientHandler.addResponseListener(RecheckCountResponse.class, recheckCountListener);
log.info("DashboardPage listeners registered. Requesting initial data.");
NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new GetCheckUpQueueUpdateRequest());
}
@Override
public void ancestorRemoved(AncestorEvent event) {
ClientHandler.deleteListener(GetCheckUpQueueUpdateResponse.class, checkUpQueueUpdateListener);
ClientHandler.deleteListener(TodayPatientCountResponse.class, todayPatientCountListener);
log.info("DashboardPage listeners removed.");
}
@Override
public void ancestorMoved(AncestorEvent event) { /* Do nothing */ }
});
ClientHandler.addResponseListener(GetCheckUpQueueUpdateResponse.class, checkUpQueueUpdateListener);
ClientHandler.addResponseListener(TodayPatientCountResponse.class, todayPatientCountListener);
ClientHandler.addResponseListener(RecheckCountResponse.class, recheckCountListener);
log.info("DashboardPage listeners registered. Requesting initial data.");
NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new GetCheckUpQueueUpdateRequest());
}
private void startRealTimeClock() {
@@ -9,6 +9,7 @@ import BsK.common.packet.req.GetMedInfoRequest;
import BsK.common.packet.res.GetMedInfoResponse;
import BsK.common.util.network.NetworkUtil;
import BsK.common.util.text.TextUtils;
import lombok.extern.slf4j.Slf4j;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
@@ -24,6 +25,7 @@ import java.util.List;
* A JPanel dedicated to managing medicine inventory (CRUD operations).
* It handles its own UI, data fetching, and event handling.
*/
@Slf4j
public class MedicineManagementPanel extends JPanel {
// --- UI Components ---
@@ -348,15 +350,15 @@ public class MedicineManagementPanel extends JPanel {
return;
}
// 4. Construct the special 'preferedNote' string
String preferedNote = morningSpinner.getValue().toString() + ","
// 4. Construct the special 'preferredNote' string
String preferredNote = morningSpinner.getValue().toString() + ","
+ noonSpinner.getValue().toString() + ","
+ eveningSpinner.getValue().toString() + ","
+ noteField.getText().trim();
// 5. Create the request packet and send it to the server
// IMPORTANT: Assumes your AddMedicineRequest packet is updated to include 'supplement'
AddMedicineRequest request = new AddMedicineRequest(name, company, description, unit, price, preferedNote, isSupplement, isDeleted);
AddMedicineRequest request = new AddMedicineRequest(name, company, description, unit, price, preferredNote, isSupplement, isDeleted);
NetworkUtil.sendPacket(ClientHandler.ctx.channel(), request);
// 6. Provide feedback and reset the form
@@ -410,15 +412,15 @@ public class MedicineManagementPanel extends JPanel {
return;
}
// 5. Construct the 'preferedNote' string
String preferedNote = morningSpinner.getValue().toString() + ","
// 5. Construct the 'preferredNote' string
String preferredNote = morningSpinner.getValue().toString() + ","
+ noonSpinner.getValue().toString() + ","
+ eveningSpinner.getValue().toString() + ","
+ noteField.getText().trim();
// 6. Create the request packet and send it
// IMPORTANT: Assumes your EditMedicineRequest packet is updated to include 'supplement'
EditMedicineRequest request = new EditMedicineRequest(id, name, company, description, unit, price, preferedNote, isSupplement, isDeleted);
EditMedicineRequest request = new EditMedicineRequest(id, name, company, description, unit, price, preferredNote, isSupplement, isDeleted);
NetworkUtil.sendPacket(ClientHandler.ctx.channel(), request);
// 7. Provide user feedback and clear the form
@@ -441,7 +443,8 @@ public class MedicineManagementPanel extends JPanel {
TitledBorder.LEFT, TitledBorder.TOP, new Font("Arial", Font.BOLD, 14)));
// --- Table Setup ---
String[] medicineColumns = {"ID", "Tên thuốc", "Đơn vị", "Giá", "Bổ sung","Chú thích", "Trạng thái"};
// --- BUG FIX: Corrected column names to match the data being added ---
String[] medicineColumns = {"ID", "Tên thuốc", "Bổ sung", "Trạng thái"};
medicineTableModel = new DefaultTableModel(medicineColumns, 0) {
@Override
public boolean isCellEditable(int row, int column) { return false; }
@@ -511,6 +514,32 @@ public class MedicineManagementPanel extends JPanel {
chkIsSupplement.setSelected("1".equals(med.getSupplement()));
chkIsDeleted.setSelected("1".equals(med.getDeleted()));
// --- PARSE PREFERRED NOTE ---
String preferredNote = med.getPreferredNote();
if (preferredNote != null && !preferredNote.trim().isEmpty()) {
String[] parts = preferredNote.split(",", 4);
try {
morningSpinner.setValue(parts.length > 0 ? Integer.parseInt(parts[0].trim()) : 0);
noonSpinner.setValue(parts.length > 1 ? Integer.parseInt(parts[1].trim()) : 0);
eveningSpinner.setValue(parts.length > 2 ? Integer.parseInt(parts[2].trim()) : 0);
noteField.setText(parts.length > 3 ? parts[3].trim() : "");
} catch (NumberFormatException e) {
log.error("Could not parse preferred note: {}", preferredNote, e);
// Reset to default values in case of parsing error
morningSpinner.setValue(0);
noonSpinner.setValue(0);
eveningSpinner.setValue(0);
noteField.setText("");
}
} else {
// No preferred note, set default values
morningSpinner.setValue(0);
noonSpinner.setValue(0);
eveningSpinner.setValue(0);
noteField.setText("");
}
// --- END PARSING ---
// Update button states
btnAdd.setEnabled(false);
btnEdit.setEnabled(true);
@@ -525,6 +554,14 @@ public class MedicineManagementPanel extends JPanel {
medicineDescriptionField.setText("");
medicineUnitField.setText("");
medicinePriceField.setText("");
// --- ADDED RESET LOGIC ---
morningSpinner.setValue(0);
noonSpinner.setValue(0);
eveningSpinner.setValue(0);
noteField.setText("");
// --- END ADD ---
chkIsSupplement.setSelected(false);
chkIsDeleted.setSelected(false);
medicineTable.clearSelection();
@@ -555,10 +592,7 @@ public class MedicineManagementPanel extends JPanel {
medicineTableModel.addRow(new Object[]{
med.getId(),
med.getName(),
med.getUnit(),
med.getSellingPrice(),
supplementStatus,
med.getPreferredNote(),
deletedStatus
});
}
@@ -1,11 +1,5 @@
package BsK.client.ui.component.common.MedicineDialog;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumnModel;
import BsK.client.network.handler.ClientHandler;
import BsK.client.network.handler.ResponseListener;
import BsK.common.entity.Medicine;
@@ -17,25 +11,25 @@ import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumnModel;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.*;
import java.text.NumberFormat; // --- ADD THIS IMPORT ---
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.KeyStroke;
import java.util.Locale; // --- ADD THIS IMPORT ---
@Slf4j
public class MedicineDialog extends JDialog {
private JTextField medicineNameField;
private JTextField medicineCompanyField;
private JTextArea medicineDescriptionField;
private JCheckBox chkIsSupplement; // The new checkbox
private JSpinner quantitySpinner;
private JComboBox<String> UnitComboBox;
private JTextField priceField;
@@ -69,10 +63,10 @@ public class MedicineDialog extends JDialog {
void getMedInfoHandler(GetMedInfoResponse response) {
logger.info("Received medicine data");
medicineData = response.getMedInfo();
// Get Medicine objects from the response
medicines = response.getMedicines();
// We don't need to populate a main table anymore, just have the data ready.
}
@@ -87,7 +81,7 @@ public class MedicineDialog extends JDialog {
this.originalPrescription = new String[0][0];
init(parent);
}
public MedicineDialog(final Frame parent, String[][] existingPrescription) {
super(parent, "Đơn thuốc", true);
this.medicinePrescription = existingPrescription != null ? existingPrescription : new String[0][0];
@@ -105,17 +99,17 @@ public class MedicineDialog extends JDialog {
setSize(1200, 700);
setLocationRelativeTo(parent);
setResizable(true);
// UI Fonts and Dimensions
Font labelFont = new Font("Arial", Font.BOLD, 15);
Font textFont = new Font("Arial", Font.PLAIN, 15);
Font titleFont = new Font("Arial", Font.BOLD, 16);
Dimension textFieldSize = new Dimension(100, 30);
// Ensure modal behavior and proper parent relationship
setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
setAlwaysOnTop(false); // Don't force always on top, let modal behavior handle this
// Add window listener to handle minimize/restore events with parent
addWindowListener(new WindowAdapter() {
@Override
@@ -124,7 +118,7 @@ public class MedicineDialog extends JDialog {
parent.setState(Frame.ICONIFIED);
}
}
@Override
public void windowDeiconified(WindowEvent e) {
if (parent != null && parent.getState() == Frame.ICONIFIED) {
@@ -263,6 +257,16 @@ public class MedicineDialog extends JDialog {
descriptionScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
medicineInfoPanel.add(descriptionScrollPane, gbc);
gbc.weightx = 0.0;
gbc.gridy++;
gbc.gridx = 0;
gbc.gridwidth = 2;
chkIsSupplement = new JCheckBox("Thực phẩm bổ sung");
chkIsSupplement.setFont(labelFont);
chkIsSupplement.setEnabled(false);
chkIsSupplement.setOpaque(false);
medicineInfoPanel.add(chkIsSupplement, gbc);
gbc.gridwidth = 1;
gbc.gridy = 0;
gbc.gridx = 0;
@@ -723,18 +727,21 @@ public class MedicineDialog extends JDialog {
UnitComboBox.setSelectedItem(medicine.getUnit());
priceField.setText(medicine.getSellingPrice());
// Update the new checkbox
chkIsSupplement.setSelected("1".equals(medicine.getSupplement()));
quantitySpinner.setValue(1);
String preferenceNote = medicine.getPreferredNote();
if (preferenceNote != null && !preferenceNote.trim().isEmpty()) {
String[] parts = preferenceNote.split(",", 4);
String preferredNote = medicine.getPreferredNote();
if (preferredNote != null && !preferredNote.trim().isEmpty()) {
String[] parts = preferredNote.split(",", 4);
try {
morningSpinner.setValue(parts.length > 0 ? Integer.parseInt(parts[0].trim()) : 0);
noonSpinner.setValue(parts.length > 1 ? Integer.parseInt(parts[1].trim()) : 0);
eveningSpinner.setValue(parts.length > 2 ? Integer.parseInt(parts[2].trim()) : 0);
noteField.setText(parts.length > 3 ? parts[3].trim() : "");
} catch (NumberFormatException e) {
logger.error("Could not parse preference note: {}", preferenceNote, e);
logger.error("Could not parse preferred note: {}", preferredNote, e);
// Reset to default values in case of parsing error
morningSpinner.setValue(0);
noonSpinner.setValue(0);
@@ -742,7 +749,7 @@ public class MedicineDialog extends JDialog {
noteField.setText("");
}
} else {
// No preference note, set default values
// No preferred note, set default values
morningSpinner.setValue(0);
noonSpinner.setValue(0);
eveningSpinner.setValue(0);
@@ -760,6 +767,7 @@ public class MedicineDialog extends JDialog {
UnitComboBox.setSelectedIndex(0);
priceField.setText("");
totalField.setText("");
chkIsSupplement.setSelected(false); // Reset the checkbox
quantitySpinner.setValue(1);
morningSpinner.setValue(0);
noonSpinner.setValue(0);
@@ -770,9 +778,19 @@ public class MedicineDialog extends JDialog {
private void updateTotalField() {
try {
int quantity = (Integer) quantitySpinner.getValue();
if (quantity < 0) { quantitySpinner.setValue(0); quantity = 0; }
if (quantity < 0) {
quantitySpinner.setValue(0);
quantity = 0;
}
double price = priceField.getText().isEmpty() ? 0 : Double.parseDouble(priceField.getText());
totalField.setText(String.format("%.0f", quantity * price));
double totalValue = quantity * price;
// Create formatter for Vietnamese locale
NumberFormat formatter = NumberFormat.getInstance(new Locale("vi", "VN"));
// Format the total and set the text
totalField.setText(formatter.format(totalValue));
} catch (NumberFormatException e) {
totalField.setText("Giá trị không hợp lệ");
logger.error("Error parsing price for total calculation: " + priceField.getText(), e);
@@ -12,7 +12,7 @@ public class AddMedicineRequest implements Packet{
private String description;
private String unit;
private Double price;
private String preferedNote;
private String preferredNote;
private Boolean supplement;
private Boolean deleted;
}
@@ -13,7 +13,7 @@ public class EditMedicineRequest implements Packet {
private String description;
private String unit;
private Double price;
private String preferedNote;
private String preferredNote;
private Boolean supplement;
private Boolean deleted;
}
+27 -1
View File
@@ -330,5 +330,31 @@ public class Server {
parentGroup.shutdownGracefully();
childGroup.shutdownGracefully();
}
}
}
/**
* Initiates the backup of the main database file to Google Drive.
* This is intended to be called from the ServerDashboard.
* @throws IOException if the file cannot be found or the upload fails.
*/
public static void backupDatabaseToDrive() throws IOException {
ServerDashboard dashboard = ServerDashboard.getInstance();
if (!isGoogleDriveConnected() || googleDriveService == null) {
dashboard.addLog("❌ Cannot backup database: Google Drive is not connected.");
throw new IOException("Google Drive service is not available.");
}
// Define the path to the database
String dbPath = "src/main/resources/database/BSK.db";
java.io.File dbFile = new java.io.File(dbPath);
if (!dbFile.exists()) {
dashboard.addLog("❌ Cannot backup database: File not found at " + dbPath);
throw new IOException("Database file not found: " + dbPath);
}
// Call the service to perform the backup
googleDriveService.backupDatabaseFile(dbFile);
}
}
+180 -185
View File
@@ -1,16 +1,16 @@
package BsK.server;
import javax.swing.*;
import javax.swing.border.TitledBorder;
import javax.swing.table.DefaultTableModel;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Vector;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.OperatingSystemMXBean;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Vector;
import BsK.server.network.entity.ClientConnection;
import BsK.server.network.manager.SessionManager;
import lombok.extern.slf4j.Slf4j;
@@ -36,6 +36,9 @@ public class ServerDashboard extends JFrame {
private JTable networkTable;
private DefaultTableModel networkTableModel;
// --- NEW ---
private JButton backupDbButton;
public static ServerDashboard getInstance() {
if (instance == null) {
instance = new ServerDashboard();
@@ -49,97 +52,158 @@ public class ServerDashboard extends JFrame {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
// Create main panel with border layout
JPanel mainPanel = new JPanel(new BorderLayout());
// Main container panel
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// Status Panel (North)
JPanel topPanel = new JPanel(new GridLayout(3, 1, 0, 5));
// Server Status Panel
JPanel statusPanel = new JPanel(new GridLayout(1, 4, 10, 0));
statusPanel.setBorder(BorderFactory.createTitledBorder("Server Status"));
// Add main components
mainPanel.add(createHeaderPanel(), BorderLayout.NORTH);
mainPanel.add(createMainContentPanel(), BorderLayout.CENTER);
add(mainPanel);
// Start timers
startSystemStatsTimer();
startNetworkStatsTimer();
}
private JPanel createHeaderPanel() {
JPanel headerPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.BOTH;
gbc.insets = new Insets(0, 5, 0, 5);
gbc.weighty = 1.0;
// Server Status Panel
JPanel statusPanel = new JPanel(new GridLayout(1, 3, 10, 0));
statusPanel.setBorder(BorderFactory.createTitledBorder("Server Status"));
statusLabel = new JLabel("Status: Starting...", SwingConstants.CENTER);
portLabel = new JLabel("Port: --", SwingConstants.CENTER);
clientsLabel = new JLabel("Connected Clients: 0", SwingConstants.CENTER);
googleDriveLabel = new JLabel("Google Drive: Checking...", SwingConstants.CENTER);
statusPanel.add(statusLabel);
statusPanel.add(portLabel);
statusPanel.add(clientsLabel);
statusPanel.add(googleDriveLabel);
gbc.gridx = 0;
gbc.weightx = 0.35; // MODIFIED: Adjusted weight
headerPanel.add(statusPanel, gbc);
// System Info Panel
JPanel systemPanel = new JPanel(new GridLayout(1, 4, 10, 0));
JPanel systemPanel = new JPanel(new GridLayout(1, 3, 10, 0));
systemPanel.setBorder(BorderFactory.createTitledBorder("System Information"));
memoryLabel = new JLabel("Memory Usage: --", SwingConstants.CENTER);
cpuLabel = new JLabel("CPU Usage: --", SwingConstants.CENTER);
JLabel osLabel = new JLabel("OS: " + System.getProperty("os.name"), SwingConstants.CENTER);
// Google Drive settings button
googleDriveButton = new JButton("Retry Drive");
googleDriveButton.addActionListener(e -> retryGoogleDriveConnection());
googleDriveButton.setToolTipText("Retry Google Drive connection");
systemPanel.add(memoryLabel);
systemPanel.add(cpuLabel);
systemPanel.add(osLabel);
systemPanel.add(googleDriveButton);
gbc.gridx = 1;
gbc.weightx = 0.35; // MODIFIED: Adjusted weight
headerPanel.add(systemPanel, gbc);
// Google Drive Settings Panel
JPanel driveSettingsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
driveSettingsPanel.setBorder(BorderFactory.createTitledBorder("Google Drive Settings"));
// Google Drive Panel
JPanel drivePanel = new JPanel(new BorderLayout(10, 0));
drivePanel.setBorder(BorderFactory.createTitledBorder("Google Drive"));
googleDriveLabel = new JLabel("Status: Checking...", SwingConstants.CENTER);
googleDriveButton = new JButton("Retry");
googleDriveButton.addActionListener(e -> retryGoogleDriveConnection());
drivePanel.add(googleDriveLabel, BorderLayout.CENTER);
drivePanel.add(googleDriveButton, BorderLayout.EAST);
gbc.gridx = 2;
gbc.weightx = 0.2; // MODIFIED: Adjusted weight
headerPanel.add(drivePanel, gbc);
JLabel folderNameLabel = new JLabel("Root Folder Name:");
JTextField driveRootFolderField = new JTextField(getCurrentDriveRootFolder(), 20);
JButton applyDriveSettingsButton = new JButton("Apply");
applyDriveSettingsButton.addActionListener(e -> applyDriveSettings(driveRootFolderField.getText().trim()));
driveSettingsPanel.add(folderNameLabel);
driveSettingsPanel.add(driveRootFolderField);
driveSettingsPanel.add(applyDriveSettingsButton);
// --- NEW: Actions Panel ---
JPanel actionsPanel = new JPanel(new BorderLayout());
actionsPanel.setBorder(BorderFactory.createTitledBorder("Actions"));
backupDbButton = new JButton("Backup DB to Drive");
backupDbButton.setToolTipText("Upload a timestamped copy of the database to Google Drive.");
backupDbButton.addActionListener(e -> performDatabaseBackup());
actionsPanel.add(backupDbButton, BorderLayout.CENTER);
gbc.gridx = 3;
gbc.weightx = 0.1; // MODIFIED: Adjusted weight
headerPanel.add(actionsPanel, gbc);
topPanel.add(statusPanel);
topPanel.add(systemPanel);
topPanel.add(driveSettingsPanel);
// Create split pane for logs and network info
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
splitPane.setResizeWeight(0.1); // Give more space to the top component
return headerPanel;
}
// --- NEW: Method to handle the backup action ---
private void performDatabaseBackup() {
// Disable button to prevent multiple clicks
backupDbButton.setEnabled(false);
addLog("▶️ Starting database backup to Google Drive...");
// Network Information Panel
// Run the backup in a background thread to not freeze the UI
new Thread(() -> {
try {
// Call the static method in the Server class
BsK.server.Server.backupDatabaseToDrive();
// Update UI on success
SwingUtilities.invokeLater(() -> {
addLog("✅ Database backup completed successfully.");
JOptionPane.showMessageDialog(this,
"Database backup was successfully uploaded to Google Drive.",
"Backup Successful",
JOptionPane.INFORMATION_MESSAGE);
});
} catch (Exception e) {
log.error("Database backup failed", e);
// Update UI on failure
SwingUtilities.invokeLater(() -> {
addLog("❌ Database backup failed: " + e.getMessage());
JOptionPane.showMessageDialog(this,
"Failed to backup the database.\nError: " + e.getMessage() + "\n\nCheck logs for more details.",
"Backup Failed",
JOptionPane.ERROR_MESSAGE);
});
} finally {
// Always re-enable the button on the UI thread
SwingUtilities.invokeLater(() -> backupDbButton.setEnabled(true));
}
}).start();
}
private JSplitPane createMainContentPanel() {
// ... (This method remains unchanged)
// --- Log Panel ---
JPanel logPanel = new JPanel(new BorderLayout());
logPanel.setBorder(BorderFactory.createTitledBorder("Server Logs"));
logArea = new JTextPane();
logArea.setEditable(false);
logArea.setFont(new Font("Monospaced", Font.PLAIN, 12));
JScrollPane logScrollPane = new JScrollPane(logArea);
logPanel.add(createLogControlsPanel(), BorderLayout.NORTH);
logPanel.add(logScrollPane, BorderLayout.CENTER);
// --- Network Panel ---
JPanel networkPanel = new JPanel(new BorderLayout());
networkPanel.setBorder(BorderFactory.createTitledBorder("Network Information"));
// Create table model with columns
String[] columnNames = {"Session ID", "IP Address", "Port", "Role", "Connected Time", "Last Activity", "Bytes Sent", "Bytes Received"};
String[] columnNames = {"Session ID", "IP Address", "Port", "Role", "Connected Time", "Last Activity"};
networkTableModel = new DefaultTableModel(columnNames, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
public boolean isCellEditable(int row, int column) { return false; }
};
networkTable = new JTable(networkTableModel);
networkTable.setFillsViewportHeight(true);
JScrollPane networkScrollPane = new JScrollPane(networkTable);
networkPanel.add(networkScrollPane, BorderLayout.CENTER);
networkPanel.add(new JScrollPane(networkTable), BorderLayout.CENTER);
// Log Controls Panel
JPanel logControlsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
// --- Split Pane ---
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, logPanel, networkPanel);
splitPane.setResizeWeight(0.80); // Give 80% of space to the logs
// Search field
return splitPane;
}
private JPanel createLogControlsPanel() {
// ... (This method remains unchanged)
JPanel logControlsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
searchField = new JTextField(20);
JButton searchButton = new JButton("Search");
searchButton.addActionListener(e -> searchInLogs());
// Clear logs button
JButton clearButton = new JButton("Clear Logs");
clearButton.addActionListener(e -> clearLogs());
// Auto-scroll toggle
autoScrollToggle = new JToggleButton("Auto-scroll", true);
autoScrollToggle.addActionListener(e -> isAutoScrollEnabled = autoScrollToggle.isSelected());
@@ -150,52 +214,25 @@ public class ServerDashboard extends JFrame {
logControlsPanel.add(clearButton);
logControlsPanel.add(Box.createHorizontalStrut(20));
logControlsPanel.add(autoScrollToggle);
// Log Area Panel
JPanel logPanel = new JPanel(new BorderLayout());
logPanel.setBorder(BorderFactory.createTitledBorder("Server Logs"));
logArea = new JTextPane();
logArea.setEditable(false);
logArea.setFont(new Font("Monospaced", Font.PLAIN, 12));
JScrollPane logScrollPane = new JScrollPane(logArea);
logPanel.add(logControlsPanel, BorderLayout.NORTH);
logPanel.add(logScrollPane, BorderLayout.CENTER);
// Add components to split pane
splitPane.setTopComponent(logPanel);
splitPane.setBottomComponent(networkPanel);
// Add components to main panel
mainPanel.add(topPanel, BorderLayout.NORTH);
mainPanel.add(splitPane, BorderLayout.CENTER);
add(mainPanel);
// Start system stats update timer
startSystemStatsTimer();
// Start network stats update timer
startNetworkStatsTimer();
return logControlsPanel;
}
private void startSystemStatsTimer() {
// ... (This method remains unchanged)
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
statsTimer = new Timer(2000, e -> {
// Update memory usage
long usedMemory = memoryBean.getHeapMemoryUsage().getUsed() / (1024 * 1024);
long maxMemory = memoryBean.getHeapMemoryUsage().getMax() / (1024 * 1024);
SwingUtilities.invokeLater(() ->
memoryLabel.setText(String.format("Memory Usage: %d MB / %d MB", usedMemory, maxMemory))
SwingUtilities.invokeLater(() ->
memoryLabel.setText(String.format("Memory: %d MB / %d MB", usedMemory, maxMemory))
);
// Update CPU usage if available
if (osBean instanceof com.sun.management.OperatingSystemMXBean) {
double cpuLoad = ((com.sun.management.OperatingSystemMXBean) osBean).getSystemCpuLoad() * 100;
SwingUtilities.invokeLater(() ->
cpuLabel.setText(String.format("CPU Usage: %.1f%%", cpuLoad))
SwingUtilities.invokeLater(() ->
cpuLabel.setText(String.format("CPU: %.1f%%", cpuLoad))
);
}
});
@@ -203,16 +240,15 @@ public class ServerDashboard extends JFrame {
}
private void startNetworkStatsTimer() {
// ... (This method remains unchanged)
networkStatsTimer = new Timer(1000, e -> refreshNetworkTable());
networkStatsTimer.start();
}
public void refreshNetworkTable() {
// ... (This method remains unchanged)
SwingUtilities.invokeLater(() -> {
// Clear existing rows
networkTableModel.setRowCount(0);
// Add current connections
for (ClientConnection conn : SessionManager.getAllConnections()) {
Vector<Object> row = new Vector<>();
row.add(conn.getSessionId());
@@ -221,21 +257,13 @@ public class ServerDashboard extends JFrame {
row.add(conn.getUserRole());
row.add(conn.getConnectionDuration());
row.add(conn.getLastActivityDuration());
row.add(formatBytes(conn.getBytesSent()));
row.add(formatBytes(conn.getBytesReceived()));
networkTableModel.addRow(row);
}
});
}
private String formatBytes(long bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024));
return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024));
}
private void searchInLogs() {
// ... (This method remains unchanged)
String searchText = searchField.getText().toLowerCase();
if (searchText.isEmpty()) {
return;
@@ -248,9 +276,9 @@ public class ServerDashboard extends JFrame {
int index = content.toLowerCase().indexOf(searchText);
while (index >= 0) {
logArea.getHighlighter().addHighlight(
index,
index + searchText.length(),
new DefaultHighlighter.DefaultHighlightPainter(Color.YELLOW)
index,
index + searchText.length(),
new DefaultHighlighter.DefaultHighlightPainter(Color.YELLOW)
);
index = content.toLowerCase().indexOf(searchText, index + 1);
}
@@ -260,6 +288,7 @@ public class ServerDashboard extends JFrame {
}
private void clearLogs() {
// ... (This method remains unchanged)
SwingUtilities.invokeLater(() -> {
try {
logArea.getDocument().remove(0, logArea.getDocument().getLength());
@@ -271,6 +300,7 @@ public class ServerDashboard extends JFrame {
}
public void updateStatus(String status, Color color) {
// ... (This method remains unchanged)
SwingUtilities.invokeLater(() -> {
statusLabel.setText("Status: " + status);
statusLabel.setForeground(color);
@@ -278,47 +308,48 @@ public class ServerDashboard extends JFrame {
}
public void updatePort(int port) {
SwingUtilities.invokeLater(() ->
// ... (This method remains unchanged)
SwingUtilities.invokeLater(() ->
portLabel.setText("Port: " + port)
);
}
public static void incrementClients() {
// ... (This method remains unchanged)
connectedClients++;
updateClientCount();
}
public static void decrementClients() {
connectedClients = Math.max(0, connectedClients - 1); // Ensure we don't go below 0
// ... (This method remains unchanged)
connectedClients = Math.max(0, connectedClients - 1);
updateClientCount();
}
private static void updateClientCount() {
// ... (This method remains unchanged)
if (instance != null) {
SwingUtilities.invokeLater(() -> {
instance.clientsLabel.setText("Connected Clients: " + connectedClients);
// Also update the network table since client count changed
instance.refreshNetworkTable();
});
}
}
public void addLog(String message) {
// ... (This method remains unchanged)
SwingUtilities.invokeLater(() -> {
try {
Document doc = logArea.getDocument();
String timestamp = timeFormat.format(new Date());
String formattedMessage = String.format("[%s] %s%n", timestamp, message);
// Create style for new text
Style style = logArea.addStyle("Log Style", null);
StyleConstants.setFontFamily(style, "Monospace");
StyleConstants.setFontSize(style, 12);
// Insert the text at the end
doc.insertString(doc.getLength(), formattedMessage, style);
// Scroll to bottom if auto-scroll is enabled
if (isAutoScrollEnabled) {
logArea.setCaretPosition(doc.getLength());
}
@@ -329,96 +360,60 @@ public class ServerDashboard extends JFrame {
}
public void updateGoogleDriveStatus(boolean connected, String statusMessage) {
// ... (This method remains unchanged)
SwingUtilities.invokeLater(() -> {
if (connected) {
googleDriveLabel.setText("Google Drive: ✅ " + statusMessage);
googleDriveLabel.setForeground(Color.GREEN);
googleDriveButton.setText("Test Drive");
googleDriveLabel.setText("✅ " + statusMessage);
googleDriveLabel.setForeground(new Color(34, 139, 34)); // Forest Green
googleDriveButton.setText("Test");
googleDriveButton.setToolTipText("Test Google Drive connection");
} else {
googleDriveLabel.setText("Google Drive: ❌ " + statusMessage);
googleDriveLabel.setText("❌ " + statusMessage);
googleDriveLabel.setForeground(Color.RED);
googleDriveButton.setText("Retry Drive");
googleDriveButton.setText("Retry");
googleDriveButton.setToolTipText("Retry Google Drive connection");
}
});
}
private void retryGoogleDriveConnection() {
// ... (This method remains unchanged)
SwingUtilities.invokeLater(() -> {
googleDriveLabel.setText("Google Drive: 🔄 Connecting...");
googleDriveLabel.setText("🔄 Connecting...");
googleDriveLabel.setForeground(Color.ORANGE);
googleDriveButton.setEnabled(false);
addLog("Retrying Google Drive connection...");
addLog("Testing Google Drive connection...");
});
// Run connection attempt in background thread
// Run connection attempt in a background thread
new Thread(() -> {
try {
// This method should attempt the connection and update the server's internal state.
BsK.server.Server.retryGoogleDriveConnection();
SwingUtilities.invokeLater(() -> {
googleDriveButton.setEnabled(true);
if (BsK.server.Server.isGoogleDriveConnected()) {
addLog("Google Drive connection successful ✅");
} else {
addLog("Google Drive connection failed ❌");
}
});
// After the attempt, get the result from the server's state
boolean isConnected = BsK.server.Server.isGoogleDriveConnected();
String message = isConnected ? "Connection test successful" : "Connection test failed";
// *** THIS IS THE FIX ***
// Call the standard UI update method with the result.
updateGoogleDriveStatus(isConnected, message);
} catch (Exception e) {
SwingUtilities.invokeLater(() -> {
googleDriveButton.setEnabled(true);
addLog("Google Drive connection error: " + e.getMessage());
});
}
}).start();
}
log.error("Google Drive connection test failed with an exception", e);
// Also update the UI in case of an exception
updateGoogleDriveStatus(false, "Connection error");
addLog("Google Drive connection error: " + e.getMessage());
} finally {
// ALWAYS re-enable the button on the UI thread
SwingUtilities.invokeLater(() -> googleDriveButton.setEnabled(true));
}
}).start();
}
private String getCurrentDriveRootFolder() {
// Get current root folder name from Server or return default
try {
if (BsK.server.Server.getGoogleDriveService() != null) {
return BsK.server.Server.getGoogleDriveRootFolderName();
}
} catch (Exception e) {
log.warn("Could not get current drive root folder name", e);
}
return "BSK_Clinic_Patient_Files"; // Default value
}
private void applyDriveSettings(String newRootFolderName) {
if (newRootFolderName == null || newRootFolderName.trim().isEmpty()) {
addLog("❌ Root folder name cannot be empty");
return;
}
// Sanitize folder name
String sanitizedName = newRootFolderName.replaceAll("[^a-zA-Z0-9._-]", "_");
if (!sanitizedName.equals(newRootFolderName)) {
addLog("⚠️ Folder name sanitized to: " + sanitizedName);
}
addLog("🔄 Updating Google Drive root folder to: " + sanitizedName);
new Thread(() -> {
try {
// Update the root folder name in server
BsK.server.Server.updateGoogleDriveRootFolder(sanitizedName);
SwingUtilities.invokeLater(() -> {
addLog("✅ Google Drive root folder updated successfully");
addLog("💡 New patient folders will be created under: " + sanitizedName);
});
} catch (Exception e) {
SwingUtilities.invokeLater(() -> {
addLog("❌ Failed to update Google Drive root folder: " + e.getMessage());
});
log.error("Error updating Google Drive root folder", e);
}
}).start();
}
@Override
public void dispose() {
@Override
public void dispose() {
// ... (This method remains unchanged)
if (statsTimer != null) {
statsTimer.stop();
}
@@ -427,4 +422,4 @@ public class ServerDashboard extends JFrame {
}
super.dispose();
}
}
}
@@ -51,4 +51,11 @@ public class User {
public boolean isAuthenticated() {
return role != Role.GUEST;
}
public void resetAuthentication() {
this.role = Role.GUEST;
this.userId = -1; // Or some other indicator of not being logged in
SessionManager.updateUserRole(channel.id().asLongText(), this.role.toString(), this.userId);
log.info("User session {} has been de-authenticated.", sessionId);
}
}
@@ -58,7 +58,6 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
log.debug("Received message: {}", frame.text());
// Update last activity time for the client
var connectedUser = SessionManager.getUserByChannel(ctx.channel().id().asLongText());
if (connectedUser != null) {
@@ -91,19 +90,17 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
// Tạo user trong database hoặc check exist
boolean isUserExist = false;
} else if (packet instanceof LogoutRequest) {
var logoutUser = SessionManager.getUserByChannel(ctx.channel().id().asLongText());
var logoutUser = SessionManager.getUserByChannel(ctx.channel().id().asLongText());
if (logoutUser != null) {
log.info("User {} (Session {}) requested logout.", logoutUser.getUserId(), logoutUser.getSessionId());
SessionManager.onUserDisconnect(ctx.channel());
// Optionally, you could send a LogoutResponse back to the client if needed,
// but since the client navigates away, it might not be processed.
logoutUser.resetAuthentication();
} else {
log.warn("Received LogoutRequest from a channel with no active user session: {}", ctx.channel().id().asLongText());
}
} else {
// Check if user is authenticated
var currentUser = SessionManager.getUserByChannel(ctx.channel().id().asLongText());
if (!currentUser.isAuthenticated()) {
if (currentUser == null || !currentUser.isAuthenticated()) {
log.warn("Received packet from unauthenticated user: {}", packet);
return;
}
@@ -1461,14 +1458,14 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
}
log.info("Received AddMedRequest");
try {
String sql = "INSERT INTO Medicine (med_name, med_company, med_description, med_unit, med_selling_price, preference_note, supplement, deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
String sql = "INSERT INTO Medicine (med_name, med_company, med_description, med_unit, med_selling_price, preferred_note, supplement, deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
PreparedStatement stmt = Server.connection.prepareStatement(sql);
stmt.setString(1, addMedicineRequest.getName());
stmt.setString(2, addMedicineRequest.getCompany());
stmt.setString(3, addMedicineRequest.getDescription());
stmt.setString(4, addMedicineRequest.getUnit());
stmt.setDouble(5, addMedicineRequest.getPrice());
stmt.setString(6, addMedicineRequest.getPreferedNote());
stmt.setString(6, addMedicineRequest.getPreferredNote());
stmt.setBoolean(7, addMedicineRequest.getSupplement());
stmt.setBoolean(8, addMedicineRequest.getDeleted());
stmt.executeUpdate();
@@ -1486,14 +1483,14 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
}
log.info("Received EditMedRequest");
try {
String sql = "UPDATE Medicine SET med_name = ?, med_company = ?, med_description = ?, med_unit = ?, med_selling_price = ?, preference_note = ?, supplement = ?, deleted = ? WHERE med_id = ?";
String sql = "UPDATE Medicine SET med_name = ?, med_company = ?, med_description = ?, med_unit = ?, med_selling_price = ?, preferred_note = ?, supplement = ?, deleted = ? WHERE med_id = ?";
PreparedStatement stmt = Server.connection.prepareStatement(sql);
stmt.setString(1, editMedicineRequest.getName());
stmt.setString(2, editMedicineRequest.getCompany());
stmt.setString(3, editMedicineRequest.getDescription());
stmt.setString(4, editMedicineRequest.getUnit());
stmt.setDouble(5, editMedicineRequest.getPrice());
stmt.setString(6, editMedicineRequest.getPreferedNote());
stmt.setString(6, editMedicineRequest.getPreferredNote());
stmt.setBoolean(7, editMedicineRequest.getSupplement());
stmt.setBoolean(8, editMedicineRequest.getDeleted());
stmt.setString(9, editMedicineRequest.getId());
@@ -1669,7 +1666,7 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
private void getMedInfo(int sessionId) {
try {
ResultSet rs = statement.executeQuery(
"select med_id, med_name, med_company, med_description, med_unit, med_selling_price, preference_note, supplement, deleted\n" +
"select med_id, med_name, med_company, med_description, med_unit, med_selling_price, preferred_note, supplement, deleted\n" +
" from Medicine"
);
@@ -1684,12 +1681,12 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
String medDescription = rs.getString("med_description");
String medUnit = rs.getString("med_unit");
String medSellingPrice = rs.getString("med_selling_price");
String preferenceNote = rs.getString("preference_note");
String preferredNote = rs.getString("preferred_note");
String supplement = rs.getString("supplement");
String deleted = rs.getString("deleted");
String result = String.join("|",medId, medName, medCompany, medDescription, medUnit,
medSellingPrice, preferenceNote != null ? preferenceNote : "", supplement != null ? supplement : "0", deleted);
medSellingPrice, preferredNote != null ? preferredNote : "", supplement != null ? supplement : "0", deleted);
resultList.add(result);
}
@@ -1768,6 +1765,9 @@ public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFram
ServerDashboard.getInstance().addLog("Client timed out: Session " + user.getSessionId());
}
SessionManager.onUserDisconnect(ctx.channel());
if (user != null) {
user.resetAuthentication();
}
ctx.channel().close();
} catch (Exception e) {
ServerDashboard.getInstance().addLog("Error during client timeout: " + e.getMessage());
@@ -46,13 +46,19 @@ public class SessionManager {
public static int onUserLogin(Channel channel) {
int sessionId = SESSION_ID.incrementAndGet();
var user = new User(channel, sessionId);
String channelId = channel.id().asLongText();
var user = CHANNEL_TO_USER_MAP.get(channelId);
if(user == null) {
user = new User(channel, sessionId);
CHANNEL_TO_USER_MAP.put(channelId, user);
} else {
user.setSessionId(sessionId);
user.setChannel(channel);
}
var connection = new ClientConnection(channel, sessionId);
String channelId = channel.id().asLongText();
SESSION_ID_MAP.put(sessionId, channelId);
CHANNEL_SESSION_ID_MAP.put(channelId, sessionId);
CHANNEL_TO_USER_MAP.put(channelId, user);
CHANNEL_TO_CONNECTION_MAP.put(channelId, connection);
return sessionId;
@@ -60,10 +66,12 @@ public class SessionManager {
public static void onUserDisconnect(Channel channel) {
String channelId = channel.id().asLongText();
int sessionId = CHANNEL_SESSION_ID_MAP.remove(channelId);
SESSION_ID_MAP.remove(sessionId);
CHANNEL_TO_USER_MAP.remove(channelId);
CHANNEL_TO_CONNECTION_MAP.remove(channelId);
var user = CHANNEL_TO_USER_MAP.get(channelId);
if(user != null && user.getUserId() != -1) {
int oldSessionId = CHANNEL_SESSION_ID_MAP.remove(channelId);
SESSION_ID_MAP.remove(oldSessionId);
CHANNEL_TO_CONNECTION_MAP.remove(channelId);
}
}
public static void updateUserRole(String channelId, String role, int userId) {
@@ -464,4 +464,63 @@ public class GoogleDriveServiceOAuth {
return result.getFiles();
}
/**
* Finds a folder by name within a specific parent folder, or creates it if it doesn't exist.
* @param parentFolderId The ID of the parent folder.
* @param folderName The name of the folder to find or create.
* @return The ID of the found or newly created folder.
* @throws IOException if a Google Drive API error occurs.
*/
private String findOrCreateFolder(String parentFolderId, String folderName) throws IOException {
// Search for existing folder
String query = String.format("name = '%s' and mimeType='application/vnd.google-apps.folder' and '%s' in parents and trashed=false",
folderName.replace("'", "\\'"),
parentFolderId);
FileList result = driveService.files().list()
.setQ(query)
.setSpaces("drive")
.setFields("files(id)")
.execute();
if (!result.getFiles().isEmpty()) {
String existingFolderId = result.getFiles().get(0).getId();
log.info("Found existing folder '{}' (ID: {}) inside parent {}", folderName, existingFolderId, parentFolderId);
return existingFolderId;
}
// Create folder if it doesn't exist
log.info("Creating new folder '{}' under parent {}", folderName, parentFolderId);
return createFolderUnderParent(parentFolderId, folderName);
}
// Add this primary backup method inside the GoogleDriveServiceOAuth class
/**
* Backs up the specified database file to a dedicated backup folder in Google Drive.
* The backup filename will be timestamped.
* @param dbFile The local database file to upload.
* @return The ID of the uploaded file in Google Drive.
* @throws IOException if the upload fails.
*/
public String backupDatabaseFile(java.io.File dbFile) throws IOException {
// 1. Define the dedicated, private backup folder name
String backupFolderName = "_Database_Backups"; // Using underscore to sort it to the top
// 2. Get or create the backup folder under the main application root folder
String backupFolderId = findOrCreateFolder(this.rootFolderId, backupFolderName);
log.info("Using database backup folder: {} (ID: {})", backupFolderName, backupFolderId);
// 3. Create a unique, timestamped filename for the backup
String timestamp = new java.text.SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(new java.util.Date());
String backupFileName = "BSK_Backup_" + timestamp + ".db";
// 4. Upload the file
log.info("Uploading database backup as '{}' to Google Drive...", backupFileName);
String fileId = uploadFileToFolder(backupFolderId, dbFile, backupFileName);
log.info("✅ Successfully uploaded database backup. File ID: {}", fileId);
return fileId;
}
}
+4 -1
View File
@@ -28,4 +28,7 @@ google.drive.root.folder=BSK_Clinic_Patient_Files
storage.image_db_path=img_db
# The base directory for checkup media resources (currently unused but defined for future use).
storage.checkup_media_base_dir=src/main/resources/image/checkup_media
storage.checkup_media_base_dir=src/main/resources/image/checkup_media
app.auto_change_status_to_finished=false
Binary file not shown.