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 0000000..7263d8e Binary files /dev/null and b/src/main/java/BsK/client/ui/assets/icon/chat.png differ diff --git a/src/main/java/BsK/client/ui/assets/icon/google-drive.png b/src/main/java/BsK/client/ui/assets/icon/google-drive.png new file mode 100644 index 0000000..e1e4f64 Binary files /dev/null and b/src/main/java/BsK/client/ui/assets/icon/google-drive.png differ diff --git a/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java b/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java index ee00705..3fcedf6 100644 --- a/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java +++ b/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java @@ -1,5 +1,8 @@ package BsK.client.ui.component.CheckUpPage; +import java.awt.Desktop; +import java.net.URI; + import BsK.client.Client; import BsK.client.LocalStorage; import BsK.client.network.handler.ClientHandler; @@ -201,6 +204,7 @@ public class CheckUpPage extends JPanel { private JButton openQueueButton; private JButton addPatientButton; private JButton[] actionButtons; + private JButton driveButton; // Template info labels private JLabel imageCountValueLabel; @@ -564,13 +568,51 @@ public class CheckUpPage extends JPanel { cccdLabel.setFont(labelFont); patientInfoInnerPanel.add(cccdLabel, gbcPatient); - gbcPatient.gridx = 1; gbcPatient.weightx = 0.4; gbcPatient.gridwidth = 3; + gbcPatient.gridx = 1; gbcPatient.weightx = 0.4; gbcPatient.gridwidth = 2; // <-- MODIFIED from 3 to 2 customerCccdDdcnField = new JTextField(15); customerCccdDdcnField.setFont(fieldFont); addSelectAllOnFocus(customerCccdDdcnField); patientInfoInnerPanel.add(customerCccdDdcnField, gbcPatient); gbcPatient.gridwidth = 1; // Reset gridwidth + // --- START: ADD THE NEW DRIVE BUTTON HERE --- + gbcPatient.gridx = 3; gbcPatient.weightx = 0.2; // Add to the right + driveButton = new JButton("Drive Bệnh nhân"); + driveButton.setFont(new Font("Arial", Font.BOLD, 14)); + driveButton.setBackground(new Color(26, 115, 232)); // Google Blue color + driveButton.setForeground(Color.WHITE); + driveButton.setFocusPainted(false); + driveButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); + driveButton.setEnabled(false); // Disabled by default + driveButton.setBorder(BorderFactory.createEmptyBorder(8, 15, 8, 15)); + + // Load and set the icon + try { + String iconPath = "src/main/java/BsK/client/ui/assets/icon/google-drive.png"; + ImageIcon driveIcon = new ImageIcon(new ImageIcon(iconPath).getImage().getScaledInstance(20, 20, Image.SCALE_SMOOTH)); + driveButton.setIcon(driveIcon); + } catch (Exception e) { + log.error("Failed to load Google Drive icon", e); + } + + // Add action listener to open the URL + driveButton.addActionListener(e -> { + 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