From 8e44c2a49c4adc76a222e942b65990ae13d0c4d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Th=C3=A0nh=20=C4=90=E1=BA=A1t?= <91755733+lds217@users.noreply.github.com> Date: Thu, 31 Jul 2025 01:07:22 +0700 Subject: [PATCH] [update] Implement chat message handling in NavBar and enhance LocalStorage. Add chat history storage and notification for incoming messages. Introduce a new drive button in CheckUpPage for patient document access, improving user interaction and data management capabilities. --- src/main/java/BsK/client/LocalStorage.java | 1 + .../java/BsK/client/ui/assets/icon/chat.png | Bin 0 -> 6815 bytes .../client/ui/assets/icon/google-drive.png | Bin 0 -> 18328 bytes .../ui/component/CheckUpPage/CheckUpPage.java | 52 +- .../ui/component/DataDialog/DataDialog.java | 215 ++++---- .../DataDialog/MedicineManagementPanel.java | 498 ++++++++++++++++++ .../DataDialog/ServiceManagementPanel.java | 368 +++++++++++++ .../client/ui/component/common/NavBar.java | 131 ++++- .../ui/component/common/SimpleChatDialog.java | 130 +++++ .../packet/req/SimpleMessageRequest.java | 12 + .../packet/res/SimpleMessageResponse.java | 12 + .../server/network/handler/ServerHandler.java | 9 + 12 files changed, 1316 insertions(+), 112 deletions(-) create mode 100644 src/main/java/BsK/client/ui/assets/icon/chat.png create mode 100644 src/main/java/BsK/client/ui/assets/icon/google-drive.png create mode 100644 src/main/java/BsK/client/ui/component/DataDialog/MedicineManagementPanel.java create mode 100644 src/main/java/BsK/client/ui/component/DataDialog/ServiceManagementPanel.java create mode 100644 src/main/java/BsK/client/ui/component/common/SimpleChatDialog.java create mode 100644 src/main/java/BsK/common/packet/req/SimpleMessageRequest.java create mode 100644 src/main/java/BsK/common/packet/res/SimpleMessageResponse.java diff --git a/src/main/java/BsK/client/LocalStorage.java b/src/main/java/BsK/client/LocalStorage.java index d5536fc..014eb84 100644 --- a/src/main/java/BsK/client/LocalStorage.java +++ b/src/main/java/BsK/client/LocalStorage.java @@ -22,6 +22,7 @@ public class LocalStorage { public static String ClinicName = "Phòng khám BSK"; public static String ClinicAddress = "Số ABC, Đường XYZ, Quận 1, TP.HCM"; public static String ClinicPhone = "0123456789"; + public static List chatHistory = new ArrayList<>(); public static String dataDialogSearchTerm = ""; public static Date dataDialogFromDate = new Date(); // Default to today diff --git a/src/main/java/BsK/client/ui/assets/icon/chat.png b/src/main/java/BsK/client/ui/assets/icon/chat.png new file mode 100644 index 0000000000000000000000000000000000000000..7263d8e43ab967a3c4465eb03d231a888f4b9ecd GIT binary patch literal 6815 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 kuTt@xL{*f { + Patient currentPatient = getCurrentSelectedPatient(); + if (currentPatient != null && currentPatient.getDriveUrl() != null && !currentPatient.getDriveUrl().isEmpty()) { + try { + Desktop.getDesktop().browse(new java.net.URI(currentPatient.getDriveUrl())); + } catch (Exception ex) { + log.error("Could not open patient drive URL: {}", currentPatient.getDriveUrl(), ex); + JOptionPane.showMessageDialog(this, "Không thể mở liên kết Drive:\n" + ex.getMessage(), "Lỗi", JOptionPane.ERROR_MESSAGE); + } + } else { + JOptionPane.showMessageDialog(this, "Bệnh nhân này không có liên kết Google Drive.", "Thông báo", JOptionPane.INFORMATION_MESSAGE); + } + }); + + patientInfoInnerPanel.add(driveButton, gbcPatient); + // --- END: NEW DRIVE BUTTON CODE --- + // Row 1: Name gbcPatient.gridx = 0; gbcPatient.gridy = 1; gbcPatient.weightx = 0.1; JLabel hoLabel = new JLabel("Họ", SwingConstants.RIGHT); @@ -2097,6 +2139,7 @@ public class CheckUpPage extends JPanel { comp.setEnabled(true); } } + driveButton.setEnabled(true); // <-- ADD THIS LINE // Reset saved flag for new patient saved = true; @@ -4809,10 +4852,11 @@ public class CheckUpPage extends JPanel { // Disable all action buttons for (Component comp : ((JPanel)((JPanel)rightContainer.getComponent(1))).getComponents()) { - if (comp instanceof JButton) { - comp.setEnabled(false); - } + if (comp instanceof JButton) { + comp.setEnabled(false); } + } + driveButton.setEnabled(false); // <-- ADD THIS LINE // Clear media view if (imageGalleryPanel != null) { diff --git a/src/main/java/BsK/client/ui/component/DataDialog/DataDialog.java b/src/main/java/BsK/client/ui/component/DataDialog/DataDialog.java index 3f8847a..cbfe27a 100644 --- a/src/main/java/BsK/client/ui/component/DataDialog/DataDialog.java +++ b/src/main/java/BsK/client/ui/component/DataDialog/DataDialog.java @@ -27,10 +27,10 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; public class DataDialog extends JDialog { + // private JTextField searchField; private JComboBox doctorComboBox; private JSpinner fromDateSpinner; @@ -38,27 +38,30 @@ public class DataDialog extends JDialog { private JTable dataTable; private DefaultTableModel tableModel; private JLabel resultCountLabel; - private final ResponseListener dataResponseListener = this::handleGetCheckupDataResponse; - private int currentPage = 1; private int totalPages = 1; private int recordsPerPage = 20; private int totalRecords = 0; private boolean isExporting = false; private File fileForExport = null; - private JPanel mainPanel; private JPanel paginationPanel; - private CheckUpPage checkUpPageInstance; // <-- ADD THIS FIELD + private CheckUpPage checkUpPageInstance; + // + + // --- Panels for the other tabs + private MedicineManagementPanel medicinePanel; + private ServiceManagementPanel servicePanel; - // --- MODIFIED CONSTRUCTOR --- public DataDialog(JFrame parent, CheckUpPage checkUpPage) { - super(parent, "Quản Lý Dữ Liệu Khám Bệnh", true); - this.checkUpPageInstance = checkUpPage; // Store the instance + super(parent, "Quản Lý Dữ Liệu", true); + this.checkUpPageInstance = checkUpPage; initializeDialog(); setupNetworking(); + + // Initial data fetch for checkup data tab fetchData(1); } @@ -68,22 +71,54 @@ public class DataDialog extends JDialog { setLocationRelativeTo(getParent()); setLayout(new BorderLayout()); - mainPanel = new JPanel(new BorderLayout()); - mainPanel.setBorder(new EmptyBorder(20, 20, 20, 20)); + JTabbedPane tabbedPane = new JTabbedPane(); + tabbedPane.setFont(new Font("Arial", Font.BOLD, 14)); + // --- Tab 1: Patient Checkup Data (Existing UI) --- + mainPanel = new JPanel(new BorderLayout()); + mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); mainPanel.add(createControlPanel(), BorderLayout.NORTH); mainPanel.add(createDataGridPanel(), BorderLayout.CENTER); - paginationPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10)); - mainPanel.add(paginationPanel, BorderLayout.SOUTH); - - add(mainPanel); - loadFiltersFromLocalStorage(); // <-- CALL THE NEW METHOD HERE + mainPanel.add(paginationPanel, BorderLayout.SOUTH); + tabbedPane.addTab("Dữ Liệu Khám Bệnh", mainPanel); + + // --- Tab 2: Medicine Management (Instantiate new panel) --- + medicinePanel = new MedicineManagementPanel(); + tabbedPane.addTab("Quản Lý Thuốc", medicinePanel); + + // --- Tab 3: Service Management (Instantiate new panel) --- + // *** THIS IS THE CORRECTED PART *** + servicePanel = new ServiceManagementPanel(); + tabbedPane.addTab("Quản Lý Dịch Vụ", servicePanel); + + add(tabbedPane, BorderLayout.CENTER); + + loadFiltersFromLocalStorage(); } - - // --- ADD THIS NEW METHOD --- + + private void setupNetworking() { + ClientHandler.addResponseListener(GetCheckupDataResponse.class, dataResponseListener); + + this.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + // Clean up all listeners + ClientHandler.deleteListener(GetCheckupDataResponse.class, dataResponseListener); + if (medicinePanel != null) { + medicinePanel.cleanup(); + } + // *** THIS NOW WORKS CORRECTLY *** + if (servicePanel != null) { + servicePanel.cleanup(); + } + super.windowClosing(e); + } + }); + } + + // private void loadFiltersFromLocalStorage() { - // Load search term, handling placeholder text if (LocalStorage.dataDialogSearchTerm == null || LocalStorage.dataDialogSearchTerm.isEmpty()) { searchField.setText("Tìm theo tên bệnh nhân, mã bệnh nhân..."); searchField.setForeground(Color.GRAY); @@ -92,7 +127,6 @@ public class DataDialog extends JDialog { searchField.setForeground(Color.BLACK); } - // Load dates, providing a default if null if (LocalStorage.dataDialogFromDate != null) { fromDateSpinner.setValue(LocalStorage.dataDialogFromDate); } else { @@ -104,29 +138,14 @@ public class DataDialog extends JDialog { } else { toDateSpinner.setValue(new Date()); } - - // Load doctor selection + if (LocalStorage.dataDialogDoctorName != null) { doctorComboBox.setSelectedItem(LocalStorage.dataDialogDoctorName); } else { - doctorComboBox.setSelectedIndex(0); // Default to "Tất cả" + doctorComboBox.setSelectedIndex(0); } } - private void setupNetworking() { - ClientHandler.addResponseListener(GetCheckupDataResponse.class, dataResponseListener); - this.addWindowListener(new WindowAdapter() { - @Override - public void windowClosing(WindowEvent e) { - // --- MODIFIED: Changed to deleteListener as requested --- - ClientHandler.deleteListener(GetCheckupDataResponse.class, dataResponseListener); - super.windowClosing(e); - } - }); - } - - - private JPanel createControlPanel() { JPanel controlPanel = new JPanel(new BorderLayout()); controlPanel.setBorder(BorderFactory.createTitledBorder("Tìm kiếm và Lọc dữ liệu")); @@ -190,16 +209,15 @@ public class DataDialog extends JDialog { bottomRow.add(Box.createHorizontalStrut(20)); bottomRow.add(new JLabel("Bác sĩ:")); - - // --- MODIFIED: Use DoctorItem from LocalStorage --- + doctorComboBox = new JComboBox<>(); - // Add "Tất cả" option first doctorComboBox.addItem("Tất cả"); - // Add doctors from LocalStorage - for (DoctorItem doctor : LocalStorage.doctorsName) { - doctorComboBox.addItem(doctor.getName()); + if (LocalStorage.doctorsName != null) { + for (DoctorItem doctor : LocalStorage.doctorsName) { + doctorComboBox.addItem(doctor.getName()); + } } - doctorComboBox.setSelectedIndex(0); // Default to "Tất cả" + doctorComboBox.setSelectedIndex(0); doctorComboBox.setPreferredSize(new Dimension(150, 25)); bottomRow.add(doctorComboBox); @@ -211,9 +229,9 @@ public class DataDialog extends JDialog { addDialog.setVisible(true); }); exportExcelButton.addActionListener(e -> handleExportToExcel()); - + filterButton.addActionListener(e -> { - saveFiltersToLocalStorage(); // <-- SAVE STATE BEFORE FETCHING + saveFiltersToLocalStorage(); fetchData(1); }); clearFilterButton.addActionListener(e -> { @@ -231,12 +249,11 @@ public class DataDialog extends JDialog { resultCountLabel.setFont(new Font("Arial", Font.PLAIN, 12)); gridPanel.add(resultCountLabel, BorderLayout.NORTH); - // --- MODIFIED: Added more columns for better data visibility --- String[] columnNames = {"STT", "Mã BN", "Họ và Tên", "Năm sinh", "Giới tính", "Ngày khám", "Bác sĩ khám", "Chẩn đoán", "Kết luận", "Hành động"}; tableModel = new DefaultTableModel(columnNames, 0) { @Override public boolean isCellEditable(int row, int column) { - return column == 9; // Action column is now at index 9 + return column == 9; } }; dataTable = new JTable(tableModel); @@ -245,8 +262,7 @@ public class DataDialog extends JDialog { dataTable.getTableHeader().setFont(new Font("Arial", Font.BOLD, 12)); dataTable.getTableHeader().setBackground(new Color(240, 240, 240)); dataTable.setSelectionBackground(new Color(230, 240, 255)); - - // --- MODIFIED: Adjusted column widths for the new layout --- + TableColumn column; int[] columnWidths = {40, 80, 150, 80, 60, 100, 130, 150, 200, 120}; for (int i = 0; i < columnWidths.length; i++) { @@ -256,7 +272,7 @@ public class DataDialog extends JDialog { dataTable.getColumn("Hành động").setCellRenderer(new ActionButtonRenderer()); dataTable.getColumn("Hành động").setCellEditor(new ActionButtonEditor(this)); - + JScrollPane scrollPane = new JScrollPane(dataTable); gridPanel.add(scrollPane, BorderLayout.CENTER); return gridPanel; @@ -286,31 +302,29 @@ public class DataDialog extends JDialog { String selectedDoctor = (String) doctorComboBox.getSelectedItem(); Integer doctorId = null; - - // --- MODIFIED: Get correct doctor ID from LocalStorage --- + if (selectedDoctor != null && !selectedDoctor.equals("Tất cả")) { - for (DoctorItem doctor : LocalStorage.doctorsName) { - if (doctor.getName().equals(selectedDoctor)) { - try { - doctorId = Integer.parseInt(doctor.getId()); - } catch (NumberFormatException e) { - System.err.println("Invalid doctor ID format: " + doctor.getId()); + if (LocalStorage.doctorsName != null) { + for (DoctorItem doctor : LocalStorage.doctorsName) { + if (doctor.getName().equals(selectedDoctor)) { + try { + doctorId = Integer.parseInt(doctor.getId()); + } catch (NumberFormatException e) { + System.err.println("Invalid doctor ID format: " + doctor.getId()); + } + break; } - break; } } } GetCheckupDataRequest request = new GetCheckupDataRequest(searchTerm, fromTimestamp, toTimestamp, doctorId, page, recordsPerPage); NetworkUtil.sendPacket(ClientHandler.ctx.channel(), request); - + resultCountLabel.setText("Đang tải dữ liệu cho trang " + page + "..."); } - // --- MODIFIED: Process raw data into Patient DTOs and populate table correctly --- private void handleGetCheckupDataResponse(GetCheckupDataResponse response) { - // --- START OF MODIFICATION --- - // Check if we are in export mode if (isExporting) { List patientsToExport = new ArrayList<>(); if (response.getCheckupData() != null) { @@ -325,15 +339,12 @@ public class DataDialog extends JDialog { } catch (IOException ex) { JOptionPane.showMessageDialog(this, "Lỗi khi ghi file Excel: " + ex.getMessage(), "Lỗi", JOptionPane.ERROR_MESSAGE); } finally { - // Reset the flag and file variable after exporting isExporting = false; fileForExport = null; } - return; // IMPORTANT: Stop further execution to avoid updating the UI table + return; } - // --- END OF MODIFICATION --- - // This is your existing code for updating the table for pagination SwingUtilities.invokeLater(() -> { this.currentPage = response.getCurrentPage(); this.totalPages = response.getTotalPages(); @@ -347,16 +358,16 @@ public class DataDialog extends JDialog { try { Patient patient = new Patient(rowData); String[] tableRow = { - String.valueOf(stt++), - patient.getCustomerId(), // Correctly use customerId for "Mã BN" - patient.getCustomerLastName() + " " + patient.getCustomerFirstName(), - patient.getCustomerDob(), - patient.getCustomerGender(), - patient.getCheckupDate(), - patient.getDoctorName(), - patient.getDiagnosis(), - patient.getConclusion(), - "" // Action column placeholder + String.valueOf(stt++), + patient.getCustomerId(), + patient.getCustomerLastName() + " " + patient.getCustomerFirstName(), + patient.getCustomerDob(), + patient.getCustomerGender(), + patient.getCheckupDate(), + patient.getDoctorName(), + patient.getDiagnosis(), + patient.getConclusion(), + "" }; tableModel.addRow(tableRow); } catch (IllegalArgumentException e) { @@ -364,7 +375,7 @@ public class DataDialog extends JDialog { } } } - + updateResultCountLabel(); updatePaginationControls(); }); @@ -388,24 +399,21 @@ public class DataDialog extends JDialog { if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { this.fileForExport = fileChooser.getSelectedFile(); - // Ensure the file has a .xlsx extension if (!this.fileForExport.getName().toLowerCase().endsWith(".xlsx")) { this.fileForExport = new File(this.fileForExport.getParentFile(), this.fileForExport.getName() + ".xlsx"); } - - // Set the flag to true + this.isExporting = true; - + JOptionPane.showMessageDialog(this, "Đang chuẩn bị dữ liệu để xuất file...", "Thông báo", JOptionPane.INFORMATION_MESSAGE); - - // Call fetchData with a special page number (-1) to signal "get all" - fetchData(-1); + + fetchData(-1); } } private void updatePaginationControls() { paginationPanel.removeAll(); - + JButton firstPageButton = new JButton("<<"); firstPageButton.addActionListener(e -> fetchData(1)); firstPageButton.setEnabled(currentPage > 1); @@ -413,7 +421,7 @@ public class DataDialog extends JDialog { JButton prevPageButton = new JButton("<"); prevPageButton.addActionListener(e -> fetchData(currentPage - 1)); prevPageButton.setEnabled(currentPage > 1); - + paginationPanel.add(firstPageButton); paginationPanel.add(prevPageButton); @@ -423,7 +431,7 @@ public class DataDialog extends JDialog { if (startPage > 1) { paginationPanel.add(new JLabel("...")); } - + for (int i = startPage; i <= endPage; i++) { JButton pageButton = new JButton(String.valueOf(i)); if (i == currentPage) { @@ -436,50 +444,46 @@ public class DataDialog extends JDialog { } if (endPage < totalPages) { - paginationPanel.add(new JLabel("...")); + paginationPanel.add(new JLabel("...")); } - + JButton nextPageButton = new JButton(">"); nextPageButton.addActionListener(e -> fetchData(currentPage + 1)); nextPageButton.setEnabled(currentPage < totalPages); - + JButton lastPageButton = new JButton(">>"); lastPageButton.addActionListener(e -> fetchData(totalPages)); lastPageButton.setEnabled(currentPage < totalPages); - + paginationPanel.add(nextPageButton); paginationPanel.add(lastPageButton); paginationPanel.revalidate(); paginationPanel.repaint(); } - - // --- ADD THIS NEW METHOD TO THE DataDialog CLASS --- + private void saveFiltersToLocalStorage() { String searchTerm = searchField.getText(); - // Don't save the placeholder text if (searchTerm.equals("Tìm theo tên bệnh nhân, mã bệnh nhân...")) { LocalStorage.dataDialogSearchTerm = ""; } else { LocalStorage.dataDialogSearchTerm = searchTerm; } - + LocalStorage.dataDialogFromDate = (Date) fromDateSpinner.getValue(); LocalStorage.dataDialogToDate = (Date) toDateSpinner.getValue(); LocalStorage.dataDialogDoctorName = (String) doctorComboBox.getSelectedItem(); } private void clearFilters() { - // Reset UI components searchField.setText("Tìm theo tên bệnh nhân, mã bệnh nhân..."); searchField.setForeground(Color.GRAY); - doctorComboBox.setSelectedIndex(0); // Defaults to "Tất cả" - + doctorComboBox.setSelectedIndex(0); + Date today = new Date(); fromDateSpinner.setValue(today); toDateSpinner.setValue(today); - // --- ADD THIS PART TO RESET LOCALSTORAGE --- LocalStorage.dataDialogSearchTerm = ""; LocalStorage.dataDialogFromDate = today; LocalStorage.dataDialogToDate = today; @@ -494,14 +498,13 @@ public class DataDialog extends JDialog { System.err.println("Couldn't find file: " + path); return null; } - + class ActionButtonRenderer extends JPanel implements TableCellRenderer { private JButton editButton, deleteButton; public ActionButtonRenderer() { setLayout(new FlowLayout(FlowLayout.CENTER, 2, 2)); - ImageIcon editIcon = createIcon("/BsK/client/ui/assets/icon/edit.png", 16, 16); editButton = new JButton(editIcon); ImageIcon deleteIcon = createIcon("/BsK/client/ui/assets/icon/delete.png", 16, 16); @@ -527,7 +530,7 @@ public class DataDialog extends JDialog { return this; } } - + class ActionButtonEditor extends DefaultCellEditor { private JPanel panel; private JButton editButton, deleteButton; @@ -536,7 +539,6 @@ public class DataDialog extends JDialog { super(new JCheckBox()); panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 2, 2)); - ImageIcon editIcon = createIcon("/BsK/client/ui/assets/icon/edit.png", 16, 16); editButton = new JButton(editIcon); ImageIcon deleteIcon = createIcon("/BsK/client/ui/assets/icon/delete.png", 16, 16); @@ -552,7 +554,7 @@ public class DataDialog extends JDialog { if (checkUpPageInstance != null) { checkUpPageInstance.loadPatientByCheckupId(checkupId); } - + parentDialog.dispose(); fireEditingStopped(); }); @@ -586,4 +588,5 @@ public class DataDialog extends JDialog { return ""; } } -} + // +} \ No newline at end of file diff --git a/src/main/java/BsK/client/ui/component/DataDialog/MedicineManagementPanel.java b/src/main/java/BsK/client/ui/component/DataDialog/MedicineManagementPanel.java new file mode 100644 index 0000000..ea0a53b --- /dev/null +++ b/src/main/java/BsK/client/ui/component/DataDialog/MedicineManagementPanel.java @@ -0,0 +1,498 @@ +package BsK.client.ui.component.DataDialog; + +import BsK.client.network.handler.ClientHandler; +import BsK.client.network.handler.ResponseListener; +import BsK.common.entity.Medicine; +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 javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.border.TitledBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableRowSorter; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +/** + * A JPanel dedicated to managing medicine inventory (CRUD operations). + * It handles its own UI, data fetching, and event handling. + */ +public class MedicineManagementPanel extends JPanel { + + // --- UI Components + private JTable medicineTable; + private DefaultTableModel medicineTableModel; + private JTextField medicineSearchField; + + // --- Input Fields + private JTextField medicineNameField; + private JTextField medicineCompanyField; + private JTextArea medicineDescriptionField; + private JTextField medicineQuantityField; + private JComboBox medicineUnitComboBox; + private JTextField medicinePriceField; + private JTextField totalField; + private JSpinner morningSpinner, noonSpinner, eveningSpinner; + private JTextArea noteField; + + // --- Data & State + private List allMedicines = new ArrayList<>(); + private String selectedMedicineId = null; + + // --- Networking + private final ResponseListener getMedInfoResponseListener = this::handleGetMedInfoResponse; + + public MedicineManagementPanel() { + super(new BorderLayout(10, 10)); + initComponents(); + setupNetworking(); + } + + private void initComponents() { + this.setBorder(new EmptyBorder(15, 15, 15, 15)); + this.add(createMedicineInputPanel(), BorderLayout.WEST); // Input form + this.add(createMedicineListPanel(), BorderLayout.CENTER); // Searchable table + } + + private void setupNetworking() { + ClientHandler.addResponseListener(GetMedInfoResponse.class, getMedInfoResponseListener); + NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new GetMedInfoRequest()); + } + + /** + * Creates the left panel with input fields, structured into sub-panels. + */ + private JPanel createMedicineInputPanel() { + // --- Main container for the left side --- + JPanel mainInputPanel = new JPanel(new GridBagLayout()); + GridBagConstraints mainGbc = new GridBagConstraints(); + mainGbc.fill = GridBagConstraints.HORIZONTAL; + mainGbc.anchor = GridBagConstraints.NORTHWEST; + mainGbc.insets = new Insets(0, 0, 10, 0); // Padding between sub-panels + mainGbc.weightx = 1.0; + + // --- Define fonts --- + Font titleFont = new Font("Arial", Font.BOLD, 14); + Font labelFont = new Font("Arial", Font.BOLD, 13); + Font textFont = new Font("Arial", Font.PLAIN, 13); + Dimension textFieldSize = new Dimension(100, 30); + + // --- Sub-Panel 1: Medicine Info --- + JPanel medicineInfoPanel = new JPanel(new GridBagLayout()); + medicineInfoPanel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), "Thông tin thuốc", + TitledBorder.LEADING, TitledBorder.TOP, + titleFont, new Color(50, 50, 50) + )); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(5, 5, 5, 5); + gbc.weightx = 0.0; + gbc.weighty = 0.0; + + gbc.gridy = 0; + gbc.gridx = 0; + JLabel nameLabel = new JLabel("Tên thuốc:"); + nameLabel.setFont(labelFont); + medicineInfoPanel.add(nameLabel, gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + medicineNameField = new JTextField(20); + medicineNameField.setFont(textFont); + medicineNameField.setPreferredSize(textFieldSize); + medicineInfoPanel.add(medicineNameField, gbc); + gbc.weightx = 0.0; + + gbc.gridy++; + gbc.gridx = 0; + JLabel companyLabel = new JLabel("Công ty:"); + companyLabel.setFont(labelFont); + medicineInfoPanel.add(companyLabel, gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + medicineCompanyField = new JTextField(20); + medicineCompanyField.setFont(textFont); + medicineCompanyField.setPreferredSize(textFieldSize); + medicineCompanyField.setEditable(false); + medicineInfoPanel.add(medicineCompanyField, gbc); + gbc.weightx = 0.0; + + gbc.gridy++; + gbc.gridx = 0; + gbc.anchor = GridBagConstraints.NORTHWEST; + JLabel descriptionLabel = new JLabel("Mô tả:"); + descriptionLabel.setFont(labelFont); + medicineInfoPanel.add(descriptionLabel, gbc); + gbc.anchor = GridBagConstraints.WEST; + + gbc.gridx = 1; + gbc.weightx = 1.0; + medicineDescriptionField = new JTextArea(3, 20); + medicineDescriptionField.setFont(textFont); + medicineDescriptionField.setLineWrap(true); + medicineDescriptionField.setWrapStyleWord(true); + medicineDescriptionField.setEditable(false); + JScrollPane descriptionScrollPane = new JScrollPane(medicineDescriptionField); + descriptionScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + medicineInfoPanel.add(descriptionScrollPane, gbc); + gbc.weightx = 0.0; + + mainGbc.gridx = 0; mainGbc.gridy = 0; + mainInputPanel.add(medicineInfoPanel, mainGbc); + + // --- Sub-Panel 2: Quantity & Price --- + JPanel quantityPricePanel = new JPanel(new GridBagLayout()); + quantityPricePanel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), "Số lượng & Giá", + TitledBorder.LEADING, TitledBorder.TOP, + titleFont, new Color(50, 50, 50) + )); + + gbc.gridy = 0; + gbc.gridx = 0; + JLabel quantityLabel = new JLabel("Số lượng:"); + quantityLabel.setFont(labelFont); + quantityPricePanel.add(quantityLabel, gbc); + + gbc.gridx = 1; + gbc.weightx = 0.5; + JSpinner quantitySpinner = new JSpinner(new SpinnerNumberModel(1, 1, 100, 1)); + quantitySpinner.setFont(textFont); + quantitySpinner.setPreferredSize(new Dimension(80, 30)); + quantityPricePanel.add(quantitySpinner, gbc); + + gbc.gridx = 2; + gbc.weightx = 0.0; + JLabel quantityLeftLabel = new JLabel("Còn lại:"); + quantityLeftLabel.setFont(labelFont); + quantityPricePanel.add(quantityLeftLabel, gbc); + + gbc.gridx = 3; + gbc.weightx = 0.5; + medicineQuantityField = new JTextField(5); + medicineQuantityField.setFont(textFont); + medicineQuantityField.setPreferredSize(textFieldSize); + medicineQuantityField.setEditable(false); + quantityPricePanel.add(medicineQuantityField, gbc); + gbc.weightx = 0.0; + + gbc.gridy++; + gbc.gridx = 0; + JLabel unitLabel = new JLabel("ĐVT:"); + unitLabel.setFont(labelFont); + quantityPricePanel.add(unitLabel, gbc); + + gbc.gridx = 1; + gbc.weightx = 0.5; + String[] units = {"Viên", "Vỉ", "Hộp", "Chai", "Tuýp", "Gói"}; + medicineUnitComboBox = new JComboBox<>(units); + medicineUnitComboBox.setFont(textFont); + medicineUnitComboBox.setPreferredSize(textFieldSize); + medicineUnitComboBox.setEnabled(false); + quantityPricePanel.add(medicineUnitComboBox, gbc); + + gbc.gridx = 2; + gbc.weightx = 0.0; + JLabel priceLabel = new JLabel("Giá (VNĐ):"); + priceLabel.setFont(labelFont); + quantityPricePanel.add(priceLabel, gbc); + + gbc.gridx = 3; + gbc.weightx = 0.5; + medicinePriceField = new JTextField(10); + medicinePriceField.setFont(textFont); + medicinePriceField.setPreferredSize(textFieldSize); + medicinePriceField.setEditable(false); + quantityPricePanel.add(medicinePriceField, gbc); + gbc.weightx = 0.0; + + gbc.gridy++; + gbc.gridx = 0; + JLabel totalLabel = new JLabel("Thành tiền (VNĐ):"); + totalLabel.setFont(labelFont); + quantityPricePanel.add(totalLabel, gbc); + + gbc.gridx = 1; + gbc.gridwidth = 3; + gbc.weightx = 1.0; + totalField = new JTextField(10); + totalField.setFont(textFont); + totalField.setPreferredSize(textFieldSize); + totalField.setEditable(false); + quantityPricePanel.add(totalField, gbc); + gbc.gridwidth = 1; + gbc.weightx = 0.0; + + mainGbc.gridx = 0; mainGbc.gridy = 1; + mainInputPanel.add(quantityPricePanel, mainGbc); + + // --- Sub-Panel 3: Dosage --- + JPanel dosagePanel = new JPanel(new GridBagLayout()); + dosagePanel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), "Liều dùng", + TitledBorder.LEADING, TitledBorder.TOP, + titleFont, new Color(50, 50, 50) + )); + + gbc.gridy = 0; + gbc.gridx = 0; + gbc.weightx = 0.0; + JLabel morningLabel = new JLabel("Sáng:"); + morningLabel.setFont(labelFont); + dosagePanel.add(morningLabel, gbc); + gbc.gridx = 1; + gbc.weightx = 0.3; + morningSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 10, 1)); + morningSpinner.setFont(textFont); + morningSpinner.setPreferredSize(new Dimension(60,30)); + dosagePanel.add(morningSpinner, gbc); + gbc.gridx = 2; + gbc.weightx = 0.0; + JLabel noonLabel = new JLabel("Trưa:"); + noonLabel.setFont(labelFont); + dosagePanel.add(noonLabel, gbc); + gbc.gridx = 3; + gbc.weightx = 0.3; + noonSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 10, 1)); + noonSpinner.setFont(textFont); + noonSpinner.setPreferredSize(new Dimension(60,30)); + dosagePanel.add(noonSpinner, gbc); + gbc.gridx = 4; + gbc.weightx = 0.0; + JLabel eveningLabel = new JLabel("Chiều:"); + eveningLabel.setFont(labelFont); + dosagePanel.add(eveningLabel, gbc); + gbc.gridx = 5; + gbc.weightx = 0.3; + eveningSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 10, 1)); + eveningSpinner.setFont(textFont); + eveningSpinner.setPreferredSize(new Dimension(60,30)); + dosagePanel.add(eveningSpinner, gbc); + gbc.weightx = 0.0; + + mainGbc.gridx = 0; mainGbc.gridy = 2; + mainInputPanel.add(dosagePanel, mainGbc); + + // --- Sub-Panel 4: Note --- + JPanel notePanel = new JPanel(new GridBagLayout()); + notePanel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), "Ghi chú", + TitledBorder.LEADING, TitledBorder.TOP, + titleFont, new Color(50, 50, 50) + )); + + gbc.gridy = 0; + gbc.gridx = 0; + JLabel noteLabel = new JLabel("Ghi chú:"); + noteLabel.setFont(labelFont); + notePanel.add(noteLabel, gbc); + gbc.anchor = GridBagConstraints.WEST; + + gbc.gridx = 1; + gbc.weightx = 1.0; + gbc.fill = GridBagConstraints.BOTH; + gbc.weighty = 1.0; + noteField = new JTextArea(3, 20); + noteField.setFont(textFont); + noteField.setLineWrap(true); + noteField.setWrapStyleWord(true); + JScrollPane noteScrollPane = new JScrollPane(noteField); + noteScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + notePanel.add(noteScrollPane, gbc); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 0.0; + gbc.weighty = 0.0; + + mainGbc.gridx = 0; mainGbc.gridy = 3; + mainInputPanel.add(notePanel, mainGbc); + + // --- Sub-Panel 5: Action Buttons --- + mainGbc.gridx = 0; mainGbc.gridy = 4; mainGbc.fill = GridBagConstraints.HORIZONTAL; + mainInputPanel.add(createMedicineButtonPanel(), mainGbc); + + // --- Filler Panel to push everything up --- + mainGbc.gridx = 0; mainGbc.gridy = 5; mainGbc.weighty = 1.0; + mainInputPanel.add(new JPanel(), mainGbc); + + return mainInputPanel; + } + + + private JPanel createMedicineButtonPanel() { + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 5)); + JButton btnAdd = new JButton("Thêm mới"); + JButton btnEdit = new JButton("Chỉnh sửa"); + JButton btnDelete = new JButton("Xoá"); + JButton btnClear = new JButton("Làm mới"); + + Dimension btnSize = new Dimension(100, 35); + btnAdd.setPreferredSize(btnSize); + btnEdit.setPreferredSize(btnSize); + btnDelete.setPreferredSize(btnSize); + btnClear.setPreferredSize(btnSize); + + buttonPanel.add(btnAdd); + buttonPanel.add(btnEdit); + buttonPanel.add(btnDelete); + buttonPanel.add(btnClear); + + // --- Button Actions --- + btnClear.addActionListener(e -> clearMedicineFields()); + + btnAdd.addActionListener(e -> { + String name = medicineNameField.getText(); + if (name.trim().isEmpty()) { + JOptionPane.showMessageDialog(this, "Tên thuốc không được để trống.", "Lỗi", JOptionPane.ERROR_MESSAGE); + return; + } + // TODO: Implement Add Medicine network request + JOptionPane.showMessageDialog(this, "Chức năng 'Thêm mới' cho thuốc '" + name + "' sẽ được thực hiện tại đây.", "Thông báo", JOptionPane.INFORMATION_MESSAGE); + }); + + btnEdit.addActionListener(e -> { + if (selectedMedicineId == null) { + JOptionPane.showMessageDialog(this, "Vui lòng chọn một loại thuốc để chỉnh sửa.", "Chưa chọn thuốc", JOptionPane.WARNING_MESSAGE); + return; + } + // TODO: Implement Edit Medicine network request + String name = medicineNameField.getText(); + JOptionPane.showMessageDialog(this, "Chức năng 'Chỉnh sửa' cho thuốc ID: " + selectedMedicineId + " (" + name + ") sẽ được thực hiện tại đây.", "Thông báo", JOptionPane.INFORMATION_MESSAGE); + }); + + btnDelete.addActionListener(e -> { + if (selectedMedicineId == null) { + JOptionPane.showMessageDialog(this, "Vui lòng chọn một loại thuốc để xóa.", "Chưa chọn thuốc", JOptionPane.WARNING_MESSAGE); + return; + } + int choice = JOptionPane.showConfirmDialog(this, + "Bạn có chắc chắn muốn xóa thuốc '" + medicineNameField.getText() + "' không?", + "Xác nhận xóa", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + if (choice == JOptionPane.YES_OPTION) { + // TODO: Implement Delete Medicine network request + JOptionPane.showMessageDialog(this, "Chức năng 'Xoá' cho thuốc ID: " + selectedMedicineId + " sẽ được thực hiện tại đây.", "Thông báo", JOptionPane.INFORMATION_MESSAGE); + } + }); + + return buttonPanel; + } + + private JPanel createMedicineListPanel() { + JPanel listPanel = new JPanel(new BorderLayout(10, 10)); + listPanel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), "Danh Sách Thuốc", + TitledBorder.LEFT, TitledBorder.TOP, new Font("Arial", Font.BOLD, 14))); + + // --- Table Setup --- + String[] medicineColumns = {"ID", "Tên thuốc", "Công ty", "Tồn kho", "ĐVT", "Đơn giá"}; + medicineTableModel = new DefaultTableModel(medicineColumns, 0) { + @Override + public boolean isCellEditable(int row, int column) { return false; } + }; + medicineTable = new JTable(medicineTableModel); + medicineTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + medicineTable.setFont(new Font("Arial", Font.PLAIN, 12)); + medicineTable.setRowHeight(28); + + medicineTable.setRowSorter(null); + + medicineTable.getSelectionModel().addListSelectionListener(e -> { + if (!e.getValueIsAdjusting() && medicineTable.getSelectedRow() != -1) { + int viewRow = medicineTable.getSelectedRow(); + String medicineId = (String) medicineTableModel.getValueAt(viewRow, 0); + + allMedicines.stream() + .filter(med -> med.getId().equals(medicineId)) + .findFirst() + .ifPresent(this::populateMedicineFields); + } + }); + + // --- Search Panel --- + JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + searchPanel.add(new JLabel("Tìm kiếm thuốc:")); + medicineSearchField = new JTextField(25); + searchPanel.add(medicineSearchField); + + medicineSearchField.getDocument().addDocumentListener(new DocumentListener() { + @Override public void insertUpdate(DocumentEvent e) { filterMedicineTable(); } + @Override public void removeUpdate(DocumentEvent e) { filterMedicineTable(); } + @Override public void changedUpdate(DocumentEvent e) { filterMedicineTable(); } + }); + + listPanel.add(searchPanel, BorderLayout.NORTH); + listPanel.add(new JScrollPane(medicineTable), BorderLayout.CENTER); + + return listPanel; + } + + private void handleGetMedInfoResponse(GetMedInfoResponse response) { + SwingUtilities.invokeLater(() -> { + if (response != null && response.getMedicines() != null) { + allMedicines = response.getMedicines(); + populateMedicineTable(); + } + }); + } + + private void populateMedicineTable() { + // Just call the filter method, which will show all items if search is empty + filterMedicineTable(); + } + + private void populateMedicineFields(Medicine med) { + if (med == null) return; + selectedMedicineId = med.getId(); + medicineNameField.setText(med.getName()); + medicineCompanyField.setText(med.getCompany()); + medicineDescriptionField.setText(med.getDescription()); + medicineQuantityField.setText(med.getQuantity()); + medicineUnitComboBox.setSelectedItem(med.getUnit()); + medicinePriceField.setText(med.getSellingPrice()); + } + + private void clearMedicineFields() { + selectedMedicineId = null; + medicineNameField.setText(""); + medicineCompanyField.setText(""); + medicineDescriptionField.setText(""); + medicineQuantityField.setText(""); + medicineUnitComboBox.setSelectedIndex(0); + medicinePriceField.setText(""); + medicineTable.clearSelection(); + medicineNameField.requestFocusInWindow(); + } + + private void filterMedicineTable() { + String filterText = medicineSearchField.getText().trim(); + String lowerCaseFilterText = TextUtils.removeAccents(filterText.toLowerCase()); + + medicineTableModel.setRowCount(0); // Clear the table before adding filtered results + + for (Medicine med : allMedicines) { + if (filterText.isEmpty() || TextUtils.removeAccents(med.getName().toLowerCase()).contains(lowerCaseFilterText)) { + medicineTableModel.addRow(new Object[]{ + med.getId(), + med.getName(), + med.getCompany(), + med.getQuantity(), + med.getUnit(), + med.getSellingPrice() + }); + } + } + } + + public void cleanup() { + ClientHandler.deleteListener(GetMedInfoResponse.class, getMedInfoResponseListener); + } +} \ No newline at end of file diff --git a/src/main/java/BsK/client/ui/component/DataDialog/ServiceManagementPanel.java b/src/main/java/BsK/client/ui/component/DataDialog/ServiceManagementPanel.java new file mode 100644 index 0000000..d38822d --- /dev/null +++ b/src/main/java/BsK/client/ui/component/DataDialog/ServiceManagementPanel.java @@ -0,0 +1,368 @@ +package BsK.client.ui.component.DataDialog; + +import BsK.client.network.handler.ClientHandler; +import BsK.client.network.handler.ResponseListener; +import BsK.common.entity.Service; +import BsK.common.packet.req.GetSerInfoRequest; +import BsK.common.packet.res.GetSerInfoResponse; +import BsK.common.util.network.NetworkUtil; +import BsK.common.util.text.TextUtils; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.border.TitledBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableRowSorter; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +/** + * A JPanel dedicated to managing services (CRUD operations). + * It handles its own UI, data fetching, and event handling. + */ +public class ServiceManagementPanel extends JPanel { + + // --- UI Components --- + private JTable serviceTable; + private DefaultTableModel serviceTableModel; + private JTextField serviceSearchField; + + // --- Input Fields (Left Side) --- + private JTextField serviceNameField; + private JSpinner quantitySpinner; + private JTextField priceField; + private JTextField totalField; + private JTextArea noteField; + + // --- Data & State --- + private List allServices = new ArrayList<>(); + private String selectedServiceId = null; + + // --- Networking --- + private final ResponseListener getSerInfoResponseListener = this::handleGetSerInfoResponse; + + public ServiceManagementPanel() { + super(new BorderLayout(10, 10)); + initComponents(); + setupNetworking(); + } + + private void initComponents() { + this.setBorder(new EmptyBorder(15, 15, 15, 15)); + this.add(createServiceInputPanel(), BorderLayout.WEST); + this.add(createServiceListPanel(), BorderLayout.CENTER); + } + + private void setupNetworking() { + ClientHandler.addResponseListener(GetSerInfoResponse.class, getSerInfoResponseListener); + NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new GetSerInfoRequest()); + } + + /** + * Creates the left panel with input fields for service details and action buttons. + */ + private JPanel createServiceInputPanel() { + JPanel mainInputPanel = new JPanel(new GridBagLayout()); + mainInputPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + GridBagConstraints mainGbc = new GridBagConstraints(); + mainGbc.fill = GridBagConstraints.HORIZONTAL; + mainGbc.anchor = GridBagConstraints.NORTHWEST; + mainGbc.insets = new Insets(5, 5, 5, 5); + mainGbc.weightx = 1.0; + + Font titleFont = new Font("Arial", Font.BOLD, 16); + Font labelFont = new Font("Arial", Font.BOLD, 15); + Font textFont = new Font("Arial", Font.PLAIN, 15); + Dimension textFieldSize = new Dimension(100, 30); + + JPanel serviceInfoPanel = new JPanel(new GridBagLayout()); + serviceInfoPanel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), "Thông tin dịch vụ", + javax.swing.border.TitledBorder.LEADING, javax.swing.border.TitledBorder.TOP, + titleFont, new Color(50, 50, 50) + )); + mainGbc.gridx = 0; + mainGbc.gridy = 0; + mainInputPanel.add(serviceInfoPanel, mainGbc); + + JPanel notePanel = new JPanel(new GridBagLayout()); + notePanel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), "Ghi chú", + javax.swing.border.TitledBorder.LEADING, javax.swing.border.TitledBorder.TOP, + titleFont, new Color(50, 50, 50) + )); + mainGbc.gridy = 1; + mainInputPanel.add(notePanel, mainGbc); + + mainGbc.gridy = 2; + mainInputPanel.add(createServiceButtonPanel(), mainGbc); + + mainGbc.gridy = 3; + mainGbc.weighty = 1.0; + mainInputPanel.add(new JPanel(), mainGbc); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(5, 5, 5, 5); + gbc.weightx = 0.0; + gbc.weighty = 0.0; + + gbc.gridy = 0; + gbc.gridx = 0; + JLabel nameLabel = new JLabel("Tên dịch vụ:"); + nameLabel.setFont(labelFont); + serviceInfoPanel.add(nameLabel, gbc); + + gbc.gridx = 1; + gbc.gridwidth = 3; + gbc.weightx = 1.0; + serviceNameField = new JTextField(20); + serviceNameField.setFont(textFont); + serviceNameField.setPreferredSize(textFieldSize); + serviceInfoPanel.add(serviceNameField, gbc); + gbc.gridwidth = 1; + gbc.weightx = 0.0; + + gbc.gridy++; + gbc.gridx = 0; + JLabel quantityLabel = new JLabel("Số lượng:"); + quantityLabel.setFont(labelFont); + serviceInfoPanel.add(quantityLabel, gbc); + + gbc.gridx = 1; + gbc.weightx = 0.5; + quantitySpinner = new JSpinner(new SpinnerNumberModel(1, 1, 100, 1)); + quantitySpinner.setFont(textFont); + quantitySpinner.setPreferredSize(new Dimension(80, 30)); + serviceInfoPanel.add(quantitySpinner, gbc); + gbc.weightx = 0.0; + + gbc.gridx = 2; + JLabel priceLabel = new JLabel("Đơn giá (VNĐ):"); + priceLabel.setFont(labelFont); + serviceInfoPanel.add(priceLabel, gbc); + + gbc.gridx = 3; + gbc.weightx = 0.5; + priceField = new JTextField(10); + priceField.setFont(textFont); + priceField.setPreferredSize(textFieldSize); + serviceInfoPanel.add(priceField, gbc); + gbc.weightx = 0.0; + + gbc.gridy++; + gbc.gridx = 0; + JLabel totalLabel = new JLabel("Thành tiền (VNĐ):"); + totalLabel.setFont(labelFont); + serviceInfoPanel.add(totalLabel, gbc); + + gbc.gridx = 1; + gbc.gridwidth = 3; + gbc.weightx = 1.0; + totalField = new JTextField(10); + totalField.setFont(textFont); + totalField.setPreferredSize(textFieldSize); + totalField.setEditable(false); + serviceInfoPanel.add(totalField, gbc); + gbc.gridwidth = 1; + gbc.weightx = 0.0; + + gbc.gridy = 0; + gbc.gridx = 0; + gbc.anchor = GridBagConstraints.NORTHWEST; + JLabel noteLabel = new JLabel("Ghi chú:"); + noteLabel.setFont(labelFont); + notePanel.add(noteLabel, gbc); + gbc.anchor = GridBagConstraints.WEST; + + gbc.gridx = 1; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + noteField = new JTextArea(3, 20); + noteField.setFont(textFont); + noteField.setLineWrap(true); + noteField.setWrapStyleWord(true); + JScrollPane noteScrollPane = new JScrollPane(noteField); + noteScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + notePanel.add(noteScrollPane, gbc); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 0.0; + gbc.weighty = 0.0; + + return mainInputPanel; + } + + /** + * Creates the panel containing the "Thêm", "Sửa", "Xóa", "Làm mới" buttons. + */ + private JPanel createServiceButtonPanel() { + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 5)); + JButton btnAdd = new JButton("Thêm mới"); + JButton btnEdit = new JButton("Chỉnh sửa"); + JButton btnDelete = new JButton("Xoá"); + JButton btnClear = new JButton("Làm mới"); + + Dimension btnSize = new Dimension(100, 35); + btnAdd.setPreferredSize(btnSize); + btnEdit.setPreferredSize(btnSize); + btnDelete.setPreferredSize(btnSize); + btnClear.setPreferredSize(btnSize); + + buttonPanel.add(btnAdd); + buttonPanel.add(btnEdit); + buttonPanel.add(btnDelete); + buttonPanel.add(btnClear); + + // Button Actions + btnClear.addActionListener(e -> clearServiceFields()); + + btnAdd.addActionListener(e -> { + String name = serviceNameField.getText(); + if (name.trim().isEmpty()) { + JOptionPane.showMessageDialog(this, "Tên dịch vụ không được để trống.", "Lỗi", JOptionPane.ERROR_MESSAGE); + return; + } + // TODO: Implement Add Service network request + JOptionPane.showMessageDialog(this, "Chức năng 'Thêm mới' cho dịch vụ '" + name + "' sẽ được thực hiện tại đây.", "Thông báo", JOptionPane.INFORMATION_MESSAGE); + }); + + btnEdit.addActionListener(e -> { + if (selectedServiceId == null) { + JOptionPane.showMessageDialog(this, "Vui lòng chọn một dịch vụ để chỉnh sửa.", "Chưa chọn dịch vụ", JOptionPane.WARNING_MESSAGE); + return; + } + // TODO: Implement Edit Service network request + String name = serviceNameField.getText(); + JOptionPane.showMessageDialog(this, "Chức năng 'Chỉnh sửa' cho dịch vụ ID: " + selectedServiceId + " (" + name + ") sẽ được thực hiện tại đây.", "Thông báo", JOptionPane.INFORMATION_MESSAGE); + }); + + btnDelete.addActionListener(e -> { + if (selectedServiceId == null) { + JOptionPane.showMessageDialog(this, "Vui lòng chọn một dịch vụ để xóa.", "Chưa chọn dịch vụ", JOptionPane.WARNING_MESSAGE); + return; + } + int choice = JOptionPane.showConfirmDialog(this, + "Bạn có chắc chắn muốn xóa dịch vụ '" + serviceNameField.getText() + "' không?", + "Xác nhận xóa", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + if (choice == JOptionPane.YES_OPTION) { + // TODO: Implement Delete Service network request + JOptionPane.showMessageDialog(this, "Chức năng 'Xoá' cho dịch vụ ID: " + selectedServiceId + " sẽ được thực hiện tại đây.", "Thông báo", JOptionPane.INFORMATION_MESSAGE); + } + }); + + return buttonPanel; + } + + /** + * Creates the right panel with the search field and the table of all services. + */ + private JPanel createServiceListPanel() { + JPanel listPanel = new JPanel(new BorderLayout(10, 10)); + listPanel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), "Danh Sách Dịch Vụ", + TitledBorder.LEFT, TitledBorder.TOP, new Font("Arial", Font.BOLD, 14))); + + // Table + String[] serviceColumns = {"ID", "Tên dịch vụ", "Đơn giá (VNĐ)"}; + serviceTableModel = new DefaultTableModel(serviceColumns, 0) { + @Override + public boolean isCellEditable(int row, int column) { return false; } + }; + serviceTable = new JTable(serviceTableModel); + serviceTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + serviceTable.setFont(new Font("Arial", Font.PLAIN, 12)); + serviceTable.setRowHeight(28); + + serviceTable.setRowSorter(null); + + serviceTable.getSelectionModel().addListSelectionListener(e -> { + if (!e.getValueIsAdjusting() && serviceTable.getSelectedRow() != -1) { + // IMPORTANT: We need to find the original index from the 'allServices' list + int viewRow = serviceTable.getSelectedRow(); + String serviceId = (String) serviceTableModel.getValueAt(viewRow, 0); + + allServices.stream() + .filter(service -> service.getId().equals(serviceId)) + .findFirst() + .ifPresent(this::populateServiceFields); + } + }); + + // Search + JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + searchPanel.add(new JLabel("Tìm kiếm dịch vụ:")); + serviceSearchField = new JTextField(25); + searchPanel.add(serviceSearchField); + + serviceSearchField.getDocument().addDocumentListener(new DocumentListener() { + @Override public void insertUpdate(DocumentEvent e) { filterServiceTable(); } + @Override public void removeUpdate(DocumentEvent e) { filterServiceTable(); } + @Override public void changedUpdate(DocumentEvent e) { filterServiceTable(); } + }); + + listPanel.add(searchPanel, BorderLayout.NORTH); + listPanel.add(new JScrollPane(serviceTable), BorderLayout.CENTER); + + return listPanel; + } + + private void handleGetSerInfoResponse(GetSerInfoResponse response) { + SwingUtilities.invokeLater(() -> { + if (response != null && response.getServices() != null) { + allServices = response.getServices(); + populateServiceTable(); + } + }); + } + + private void populateServiceTable() { + // Just call the filter method, which will show all items if search is empty + filterServiceTable(); + } + + private void populateServiceFields(Service service) { + if (service == null) return; + selectedServiceId = service.getId(); + serviceNameField.setText(service.getName()); + priceField.setText(service.getCost()); + noteField.setText(""); // Assuming notes are not stored in the main service entity + quantitySpinner.setValue(1); + } + + private void clearServiceFields() { + selectedServiceId = null; + serviceNameField.setText(""); + priceField.setText(""); + totalField.setText(""); + noteField.setText(""); + quantitySpinner.setValue(1); + serviceTable.clearSelection(); + serviceNameField.requestFocusInWindow(); + } + + private void filterServiceTable() { + String filterText = serviceSearchField.getText().trim(); + String lowerCaseFilterText = TextUtils.removeAccents(filterText.toLowerCase()); + + serviceTableModel.setRowCount(0); // Clear the table before adding filtered results + + for (Service service : allServices) { + if (filterText.isEmpty() || TextUtils.removeAccents(service.getName().toLowerCase()).contains(lowerCaseFilterText)) { + serviceTableModel.addRow(new Object[]{ + service.getId(), + service.getName(), + service.getCost() + }); + } + } + } + + public void cleanup() { + ClientHandler.deleteListener(GetSerInfoResponse.class, getSerInfoResponseListener); + } +} \ No newline at end of file diff --git a/src/main/java/BsK/client/ui/component/common/NavBar.java b/src/main/java/BsK/client/ui/component/common/NavBar.java index 8ddea3a..7105dc5 100644 --- a/src/main/java/BsK/client/ui/component/common/NavBar.java +++ b/src/main/java/BsK/client/ui/component/common/NavBar.java @@ -14,6 +14,7 @@ import BsK.client.network.handler.ClientHandler; import BsK.client.network.handler.ResponseListener; import BsK.common.packet.res.EmergencyResponse; import BsK.common.packet.res.ErrorResponse; +import BsK.common.packet.res.SimpleMessageResponse; import lombok.extern.slf4j.Slf4j; import javax.swing.Icon; @@ -22,6 +23,8 @@ import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.ActionListener; +import java.text.SimpleDateFormat; +import java.util.Date; @Slf4j public class NavBar extends JPanel { @@ -32,6 +35,7 @@ public class NavBar extends JPanel { // Listener for incoming emergency alerts private final ResponseListener emergencyResponseListener = this::handleEmergencyResponse; private final ResponseListener errorResponseListener = this::handleErrorResponse; + private final ResponseListener navBarMessageListener = this::showNotificationForMessage; public NavBar(MainFrame mainFrame, String activePageTitle) { this.mainFrame = mainFrame; @@ -40,6 +44,7 @@ public class NavBar extends JPanel { ClientHandler.addResponseListener(EmergencyResponse.class, emergencyResponseListener); // add error response listener ClientHandler.addResponseListener(ErrorResponse.class, errorResponseListener); + ClientHandler.addResponseListener(SimpleMessageResponse.class, navBarMessageListener); this.setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); this.setBackground(new Color(240, 240, 240)); this.setBorder(BorderFactory.createEmptyBorder(5, 25, 5, 25)); @@ -65,7 +70,11 @@ public class NavBar extends JPanel { userPanel.setBackground(this.getBackground()); userPanel.setAlignmentY(Component.CENTER_ALIGNMENT); - // Add the Emergency button first + Component messageButton = createMessageButton(); + userPanel.add(messageButton); + // --- END: NEW MESSAGE BUTTON --- + + // Add the Emergency button Component emergencyButton = createEmergencyButton(); userPanel.add(emergencyButton); @@ -264,6 +273,69 @@ public class NavBar extends JPanel { return 0; } + private void showNotificationForMessage(SimpleMessageResponse response) { + SwingUtilities.invokeLater(() -> { + // Create a borderless window for the notification + JWindow notificationWindow = new JWindow(mainFrame); + + // Create a styled panel for the content + JPanel panel = new JPanel(new BorderLayout(10, 5)); + panel.setBackground(new Color(45, 45, 45)); + panel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(0, 132, 255), 2), + BorderFactory.createEmptyBorder(10, 30, 10, 30) + )); + + String timestamp = new SimpleDateFormat("HH:mm").format(new Date()); + String formattedMessage = String.format("[%s] %s: %s\n", + timestamp, + response.getSenderName(), + response.getMessage() + ); + // add message to local storage + LocalStorage.chatHistory.add(formattedMessage); + + // Create the message content + JLabel titleLabel = new JLabel("Tin nhắn mới từ: " + response.getSenderName()); + titleLabel.setFont(new Font("Arial", Font.BOLD, 14)); + titleLabel.setForeground(Color.WHITE); + + JLabel messageLabel = new JLabel(response.getMessage()); + messageLabel.setFont(new Font("Arial", Font.PLAIN, 12)); + messageLabel.setForeground(Color.LIGHT_GRAY); + + panel.add(titleLabel, BorderLayout.NORTH); + panel.add(messageLabel, BorderLayout.CENTER); + + notificationWindow.add(panel); + notificationWindow.pack(); + + // Position the notification at the bottom-right of the screen + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + Rectangle screenRect = ge.getMaximumWindowBounds(); + int x = screenRect.width - notificationWindow.getWidth() - 30; + int y = screenRect.height - notificationWindow.getHeight() - 30; + notificationWindow.setLocation(x, y); + + notificationWindow.setVisible(true); + + // Use a Timer to automatically close the notification after 5 seconds + Timer closeTimer = new Timer(5000, e -> notificationWindow.dispose()); + closeTimer.setRepeats(false); // Only run once + closeTimer.start(); + }); + } + + public void disableMessageListener() { + ClientHandler.deleteListener(SimpleMessageResponse.class, navBarMessageListener); + log.info("NavBar message listener disabled."); + } + + public void enableMessageListener() { + ClientHandler.addResponseListener(SimpleMessageResponse.class, navBarMessageListener); + log.info("NavBar message listener re-enabled."); + } + private Component createEmergencyButton() { Color emergencyColor = new Color(220, 38, 38); RoundedPanel buttonPanel = new RoundedPanel(15, emergencyColor, true); @@ -361,6 +433,60 @@ public class NavBar extends JPanel { return item; } + private Component createMessageButton() { + Color messageColor = new Color(150, 150, 150); // Muted gray color + RoundedPanel buttonPanel = new RoundedPanel(15, messageColor, true); + buttonPanel.setLayout(new BorderLayout(2, 2)); + buttonPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + buttonPanel.setToolTipText("Tin nhắn"); // Add a tooltip + + // Make the button smaller and square-like + buttonPanel.setPreferredSize(new Dimension(55, 55)); + buttonPanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(2, 2, 2, 2), + BorderFactory.createEmptyBorder(5, 10, 5, 10) + )); + + // Create the label without text + JLabel buttonLabel = new JLabel(); + buttonLabel.setHorizontalAlignment(SwingConstants.CENTER); + buttonLabel.setOpaque(false); + + try { + String iconPath = "src/main/java/BsK/client/ui/assets/icon/chat.png"; + ImageIcon originalIcon = new ImageIcon(iconPath); + // Use a slightly smaller icon + Image scaledImage = originalIcon.getImage().getScaledInstance(24, 24, Image.SCALE_SMOOTH); + buttonLabel.setIcon(new ImageIcon(scaledImage)); + } catch (Exception e) { + log.error("Error loading icon: chat.png. Using text fallback."); + buttonLabel.setText("Msg"); // Fallback text if icon fails + } + + buttonPanel.add(buttonLabel, BorderLayout.CENTER); + + buttonPanel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + // Open the SimpleChatDialog + SimpleChatDialog chatDialog = new SimpleChatDialog(mainFrame, NavBar.this); + chatDialog.setVisible(true); + } + + @Override + public void mouseEntered(MouseEvent e) { + buttonPanel.setBackground(messageColor.darker()); + } + + @Override + public void mouseExited(MouseEvent e) { + buttonPanel.setBackground(messageColor); + } + }); + + return buttonPanel; + } + /** * Handles the incoming EmergencyResponse. This version is silent and uses a manual * dialog creation to ensure the custom close button works correctly. @@ -433,6 +559,7 @@ public class NavBar extends JPanel { public void cleanup() { ClientHandler.deleteListener(EmergencyResponse.class, emergencyResponseListener); ClientHandler.deleteListener(ErrorResponse.class, errorResponseListener); - log.info("EmergencyResponse listener removed from NavBar."); + ClientHandler.deleteListener(SimpleMessageResponse.class, navBarMessageListener); + log.info("All NavBar listeners removed."); } } \ No newline at end of file diff --git a/src/main/java/BsK/client/ui/component/common/SimpleChatDialog.java b/src/main/java/BsK/client/ui/component/common/SimpleChatDialog.java new file mode 100644 index 0000000..07b235c --- /dev/null +++ b/src/main/java/BsK/client/ui/component/common/SimpleChatDialog.java @@ -0,0 +1,130 @@ +package BsK.client.ui.component.common; + +import BsK.client.LocalStorage; +import BsK.client.network.handler.ClientHandler; +import BsK.client.network.handler.ResponseListener; +import BsK.common.packet.req.SimpleMessageRequest; +import BsK.common.packet.res.SimpleMessageResponse; +import BsK.common.util.network.NetworkUtil; + +import javax.swing.*; +import java.awt.*; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +public class SimpleChatDialog extends JDialog { + + private JTextArea chatArea; + private JTextField messageField; + private JButton sendButton; + private final NavBar navBar; // <-- MODIFIED: This field is now used + + // Listener for incoming chat messages + private final ResponseListener messageListener = this::handleNewMessage; + + // --- MODIFIED CONSTRUCTOR --- + public SimpleChatDialog(Frame parent, NavBar navBar) { + super(parent, "Tin nhắn Nội bộ", true); + this.navBar = navBar; // Store the NavBar instance + + setSize(500, 600); + setLocationRelativeTo(parent); + setLayout(new BorderLayout(10, 10)); + + // --- Disclaimer --- + JLabel disclaimerLabel = new JLabel( + "Lưu ý: Tin nhắn chỉ lưu cục bộ, không được lưu ở máy chủ và sẽ bị xoá khi tắt app.", + SwingConstants.CENTER + ); + disclaimerLabel.setFont(new Font("Arial", Font.ITALIC, 11)); + disclaimerLabel.setForeground(Color.GRAY); + disclaimerLabel.setBorder(BorderFactory.createEmptyBorder(5, 10, 0, 10)); // Top, Left, Bottom, Right + add(disclaimerLabel, BorderLayout.NORTH); + + // --- UI Components --- + chatArea = new JTextArea(); + chatArea.setEditable(false); + chatArea.setLineWrap(true); + chatArea.setWrapStyleWord(true); + chatArea.setFont(new Font("Arial", Font.PLAIN, 14)); + chatArea.setMargin(new Insets(10, 10, 10, 10)); + + messageField = new JTextField(); + messageField.setFont(new Font("Arial", Font.PLAIN, 14)); + messageField.addActionListener(e -> sendMessage()); + + sendButton = new JButton("Gửi"); + sendButton.setFont(new Font("Arial", Font.BOLD, 14)); + sendButton.addActionListener(e -> sendMessage()); + + // --- Layout --- + JPanel bottomPanel = new JPanel(new BorderLayout(10, 0)); + bottomPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); + bottomPanel.add(messageField, BorderLayout.CENTER); + bottomPanel.add(sendButton, BorderLayout.EAST); + + add(new JScrollPane(chatArea), BorderLayout.CENTER); + add(bottomPanel, BorderLayout.SOUTH); + + // --- Networking and History --- + setupNetworking(); + loadHistory(); + } + + // --- MODIFIED setupNetworking METHOD --- + private void setupNetworking() { + // Disable the NavBar's notification listener when this dialog opens + if (this.navBar != null) { + this.navBar.disableMessageListener(); + } + + // Register this dialog's listener to update the chat area + ClientHandler.addResponseListener(SimpleMessageResponse.class, messageListener); + + // Re-enable the NavBar's listener when this dialog closes + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + ClientHandler.deleteListener(SimpleMessageResponse.class, messageListener); + if (navBar != null) { + navBar.enableMessageListener(); + } + } + }); + } + + private void loadHistory() { + chatArea.setText(""); // Clear the area first + for (String message : LocalStorage.chatHistory) { + chatArea.append(message); // Append each saved message + } + chatArea.setCaretPosition(chatArea.getDocument().getLength()); + } + + private void sendMessage() { + String messageText = messageField.getText().trim(); + if (!messageText.isEmpty()) { + SimpleMessageRequest request = new SimpleMessageRequest(LocalStorage.username, messageText); + NetworkUtil.sendPacket(ClientHandler.ctx.channel(), request); + messageField.setText(""); + } + } + + // --- MODIFIED handleNewMessage METHOD --- + private void handleNewMessage(SimpleMessageResponse response) { + SwingUtilities.invokeLater(() -> { + String timestamp = new SimpleDateFormat("HH:mm").format(new Date()); + String formattedMessage = String.format("[%s] %s: %s\n", + timestamp, + response.getSenderName(), + response.getMessage() + ); + + LocalStorage.chatHistory.add(formattedMessage); + chatArea.append(formattedMessage); + chatArea.setCaretPosition(chatArea.getDocument().getLength()); + }); + } +} \ No newline at end of file diff --git a/src/main/java/BsK/common/packet/req/SimpleMessageRequest.java b/src/main/java/BsK/common/packet/req/SimpleMessageRequest.java new file mode 100644 index 0000000..b8826da --- /dev/null +++ b/src/main/java/BsK/common/packet/req/SimpleMessageRequest.java @@ -0,0 +1,12 @@ +package BsK.common.packet.req; + +import BsK.common.packet.Packet; +import lombok.AllArgsConstructor; +import lombok.Data; +@Data +@AllArgsConstructor +public class SimpleMessageRequest implements Packet { + private String senderName; + private String message; + +} diff --git a/src/main/java/BsK/common/packet/res/SimpleMessageResponse.java b/src/main/java/BsK/common/packet/res/SimpleMessageResponse.java new file mode 100644 index 0000000..6b136c8 --- /dev/null +++ b/src/main/java/BsK/common/packet/res/SimpleMessageResponse.java @@ -0,0 +1,12 @@ +package BsK.common.packet.res; + +import BsK.common.packet.Packet; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class SimpleMessageResponse implements Packet { + private String senderName; + private String message; +} diff --git a/src/main/java/BsK/server/network/handler/ServerHandler.java b/src/main/java/BsK/server/network/handler/ServerHandler.java index 293a2e6..31ebcfc 100644 --- a/src/main/java/BsK/server/network/handler/ServerHandler.java +++ b/src/main/java/BsK/server/network/handler/ServerHandler.java @@ -1511,6 +1511,15 @@ public class ServerHandler extends SimpleChannelInboundHandler