package BsK.client.ui.component.common; import BsK.client.LocalStorage; import BsK.client.ui.component.MainFrame; import BsK.client.ui.component.InfoPage.InfoDialog; import BsK.client.ui.component.SettingsPage.SettingsDialog; import BsK.client.ui.component.DataDialog.DataDialog; import BsK.client.ui.component.CheckUpPage.CheckUpPage; import BsK.common.Error; import BsK.common.packet.req.EmergencyRequest; import BsK.common.packet.req.LogoutRequest; import BsK.common.util.network.NetworkUtil; 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; import javax.swing.*; 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 { private JLabel activeNavItem = null; private final MainFrame mainFrame; private JLabel unreadCountLabel; // NEW: Label to display unread count private RoundedPanel messageButtonPanel; // NEW: Reference to message button panel for repainting // 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; // Register the response listener for emergency alerts. 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)); // Left section with navigation items JPanel navItemsPanel = new JPanel(); navItemsPanel.setBackground(this.getBackground()); navItemsPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 8, 0)); navItemsPanel.setAlignmentY(Component.CENTER_ALIGNMENT); // Center section with current page title JPanel titlePanel = new JPanel(new GridBagLayout()); titlePanel.setBackground(this.getBackground()); titlePanel.setAlignmentY(Component.CENTER_ALIGNMENT); JLabel titleLabel = new JLabel(activePageTitle); titleLabel.setFont(new Font("Arial", Font.BOLD, 24)); titleLabel.setForeground(new Color(60, 60, 60)); titlePanel.add(titleLabel, new GridBagConstraints()); // Right section with user info and emergency button JPanel userPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0)); userPanel.setBackground(this.getBackground()); userPanel.setAlignmentY(Component.CENTER_ALIGNMENT); Component messageButton = createMessageButton(); userPanel.add(messageButton); // --- END: NEW MESSAGE BUTTON --- // Add the Emergency button Component emergencyButton = createEmergencyButton(); userPanel.add(emergencyButton); // Add welcome label with user info JPanel welcomePanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); welcomePanel.setBackground(this.getBackground()); JLabel welcomeLabel = new JLabel("Chào, " + LocalStorage.username); welcomeLabel.setFont(new Font("Arial", Font.BOLD, 14)); welcomeLabel.setForeground(new Color(60, 60, 60)); welcomeLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); welcomeLabel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { showUserMenu(welcomeLabel); } @Override public void mouseEntered(MouseEvent e) { welcomeLabel.setForeground(new Color(30, 30, 30)); } @Override public void mouseExited(MouseEvent e) { welcomeLabel.setForeground(new Color(60, 60, 60)); } }); welcomePanel.add(welcomeLabel); userPanel.add(welcomePanel); String[] navBarItems = {"Thống kê", "Thăm khám", "Dữ liệu", "Cài đặt", "Thông tin"}; String[] destination = {"DashboardPage", "CheckUpPage", "CheckUpPage", "SettingsPage", "InfoPage"}; String[] iconFiles = {"dashboard.png", "health-check.png", "database.png","settings.png", "info.png"}; Color[] navItemColors = { new Color(51, 135, 204), // Darker Blue new Color(66, 157, 21), // Darker Green new Color(200, 138, 16), // Darker Orange new Color(91, 37, 167), // Darker Purple new Color(196, 27, 36) // Darker Red-Orange }; final Color defaultTextColor = new Color(50, 50, 50); final Color activeTextColor = Color.WHITE; for (int i = 0; i < navBarItems.length; i++) { final String itemText = navBarItems[i]; final String dest = destination[i]; String iconFileName = iconFiles[i]; final Color itemColor = navItemColors[i]; RoundedPanel itemPanel = new RoundedPanel(15, itemColor.brighter(), true); itemPanel.setLayout(new BorderLayout(2, 2)); itemPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(2, 2, 2, 2), BorderFactory.createEmptyBorder(5, 10, 5, 10) )); itemPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); itemPanel.setPreferredSize(new Dimension(100, 55)); final JLabel label = new JLabel(itemText); label.setForeground(defaultTextColor); label.setHorizontalAlignment(SwingConstants.CENTER); label.setFont(new Font("Arial", Font.BOLD, 12)); label.setOpaque(false); try { String iconPath = "src/main/java/BsK/client/ui/assets/icon/" + iconFileName; ImageIcon originalIcon = new ImageIcon(iconPath); Image scaledImage = originalIcon.getImage().getScaledInstance(20, 20, Image.SCALE_SMOOTH); ImageIcon scaledIcon = new ImageIcon(scaledImage); label.setIcon(scaledIcon); } catch (Exception e) { System.err.println("Error loading icon: " + iconFileName + " for nav item: " + itemText); e.printStackTrace(); } label.setVerticalTextPosition(SwingConstants.BOTTOM); label.setHorizontalTextPosition(SwingConstants.CENTER); itemPanel.add(label, BorderLayout.CENTER); if (itemText.equals(activePageTitle)) { itemPanel.setBackground(itemColor); label.setForeground(activeTextColor); activeNavItem = label; } itemPanel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if ("InfoPage".equals(dest)) { InfoDialog infoDialog = new InfoDialog(); infoDialog.setVisible(true); return; } if ("SettingsPage".equals(dest)) { SettingsDialog settingsDialog = new SettingsDialog(mainFrame); settingsDialog.setVisible(true); return; } if ("Dữ liệu".equals(itemText)) { if (!(mainFrame.getCurrentPage() instanceof CheckUpPage)) { mainFrame.showPage("CheckUpPage"); if (activeNavItem != null) { JPanel prevPanel = (JPanel) activeNavItem.getParent(); prevPanel.setBackground(navItemColors[findNavItemIndex(activeNavItem.getText(), navBarItems)].brighter()); activeNavItem.setForeground(defaultTextColor); } for (int j = 0; j < navBarItems.length; j++) { if ("Thăm khám".equals(navBarItems[j])) { Component[] components = navItemsPanel.getComponents(); for(Component comp : components) { if (comp instanceof JPanel && ((JPanel) comp).getComponentCount() > 0 && ((JPanel) comp).getComponent(0) instanceof JLabel) { JLabel potentialLabel = (JLabel) ((JPanel) comp).getComponent(0); if ("Thăm khám".equals(potentialLabel.getText())) { activeNavItem = potentialLabel; ((JPanel) comp).setBackground(navItemColors[j]); potentialLabel.setForeground(activeTextColor); break; } } } break; } } } SwingUtilities.invokeLater(() -> { // Get the current CheckUpPage instance from the main frame CheckUpPage checkUpPage = (CheckUpPage) mainFrame.getCurrentPage(); // Pass this instance to the DataDialog DataDialog dataDialog = new DataDialog(mainFrame, checkUpPage); // <-- MODIFIED CONSTRUCTOR dataDialog.setVisible(true); }); return; } if (activeNavItem != null && activeNavItem != label) { JPanel prevPanel = (JPanel) activeNavItem.getParent(); prevPanel.setBackground(navItemColors[findNavItemIndex(activeNavItem.getText(), navBarItems)].brighter()); activeNavItem.setForeground(defaultTextColor); } activeNavItem = label; itemPanel.setBackground(itemColor); label.setForeground(activeTextColor); mainFrame.showPage(dest); } @Override public void mouseEntered(MouseEvent e) { if (label != activeNavItem) { itemPanel.setBackground(itemColor); label.setForeground(activeTextColor); } } @Override public void mouseExited(MouseEvent e) { if (label != activeNavItem) { itemPanel.setBackground(itemColor.brighter()); label.setForeground(defaultTextColor); } } }); navItemsPanel.add(itemPanel); // --- MODIFIED SEPARATOR --- if (i == 1) { // Create the separator with a darker color JSeparator separator = new JSeparator(SwingConstants.VERTICAL); separator.setForeground(new Color(150, 150, 150)); // Set a fixed size to make it thicker and taller separator.setPreferredSize(new Dimension(2, 45)); // Create a panel to hold the separator and add some horizontal padding for spacing JPanel separatorPanel = new JPanel(); separatorPanel.setBackground(this.getBackground()); // Match the navbar background separatorPanel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5)); // Add 5px padding left and right separatorPanel.add(separator); navItemsPanel.add(separatorPanel); } // --- END MODIFIED SEPARATOR --- } this.add(navItemsPanel); this.add(Box.createHorizontalGlue()); this.add(titlePanel); this.add(Box.createHorizontalGlue()); this.add(userPanel); } private int findNavItemIndex(String text, String[] items) { for (int i = 0; i < items.length; i++) { if (items[i].equals(text)) return i; } return 0; } private void showNotificationForMessage(SimpleMessageResponse response) { SwingUtilities.invokeLater(() -> { // Increment unread message count LocalStorage.unreadMessageCnt++; updateUnreadCountDisplay(); // 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."); } // NEW: Method to reset unread count when chat dialog opens public void resetUnreadMessageCount() { LocalStorage.unreadMessageCnt = 0; updateUnreadCountDisplay(); log.info("Unread message count reset to 0."); } // NEW: Method to update the unread count display private void updateUnreadCountDisplay() { SwingUtilities.invokeLater(() -> { if (LocalStorage.unreadMessageCnt > 0) { unreadCountLabel.setText(String.valueOf(LocalStorage.unreadMessageCnt)); unreadCountLabel.setVisible(true); } else { unreadCountLabel.setVisible(false); } messageButtonPanel.repaint(); }); } private Component createEmergencyButton() { Color emergencyColor = new Color(220, 38, 38); RoundedPanel buttonPanel = new RoundedPanel(15, emergencyColor, true); buttonPanel.setLayout(new BorderLayout(2, 2)); buttonPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); buttonPanel.setPreferredSize(new Dimension(100, 55)); buttonPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(2, 2, 2, 2), BorderFactory.createEmptyBorder(5, 10, 5, 10) )); JLabel buttonLabel = new JLabel("Khẩn cấp"); buttonLabel.setForeground(Color.WHITE); buttonLabel.setFont(new Font("Arial", Font.BOLD, 12)); buttonLabel.setHorizontalAlignment(SwingConstants.CENTER); buttonLabel.setOpaque(false); try { String iconPath = "src/main/java/BsK/client/ui/assets/icon/emergency.png"; ImageIcon originalIcon = new ImageIcon(iconPath); Image scaledImage = originalIcon.getImage().getScaledInstance(20, 20, Image.SCALE_SMOOTH); buttonLabel.setIcon(new ImageIcon(scaledImage)); } catch (Exception e) { System.err.println("Error loading icon: emergency.png. Using text fallback."); buttonLabel.setText("Khẩn cấp"); } buttonLabel.setVerticalTextPosition(SwingConstants.BOTTOM); buttonLabel.setHorizontalTextPosition(SwingConstants.CENTER); buttonPanel.add(buttonLabel, BorderLayout.CENTER); buttonPanel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { int response = JOptionPane.showConfirmDialog( mainFrame, "Bạn có chắc chắn muốn kích hoạt chế độ khẩn cấp không?", "XÁC NHẬN KHẨN CẤP", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE ); if (response == JOptionPane.YES_OPTION) { NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new EmergencyRequest()); } } @Override public void mouseEntered(MouseEvent e) { buttonPanel.setBackground(emergencyColor.darker()); } @Override public void mouseExited(MouseEvent e) { buttonPanel.setBackground(emergencyColor); } }); return buttonPanel; } private void showUserMenu(JLabel welcomeLabel) { JPopupMenu popupMenu = new JPopupMenu(); popupMenu.setBorder(BorderFactory.createEmptyBorder()); JMenuItem profileItem = createMenuItem("Hồ sơ cá nhân", e -> JOptionPane.showMessageDialog(mainFrame, "Tính năng Hồ sơ cá nhân sắp ra mắt!", "Thông báo", JOptionPane.INFORMATION_MESSAGE)); JMenuItem settingsItem = createMenuItem("Cài đặt", e -> { SettingsDialog settingsDialog = new SettingsDialog(mainFrame); settingsDialog.setVisible(true); }); JMenuItem logoutItem = createMenuItem("Đăng xuất", e -> { NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new LogoutRequest()); LocalStorage.username = null; LocalStorage.userId = -1; mainFrame.showPage("LandingPage"); }); popupMenu.add(profileItem); popupMenu.add(settingsItem); popupMenu.addSeparator(); popupMenu.add(logoutItem); popupMenu.setPreferredSize(new Dimension(180, popupMenu.getPreferredSize().height)); popupMenu.show(welcomeLabel, 0, welcomeLabel.getHeight()); } private JMenuItem createMenuItem(String text, ActionListener action) { JMenuItem item = new JMenuItem(text); item.setFont(new Font("Arial", Font.PLAIN, 12)); item.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10)); item.addActionListener(action); return item; } private Component createMessageButton() { Color messageColor = new Color(150, 150, 150); // Muted gray color messageButtonPanel = new RoundedPanel(15, messageColor, true); // Store reference messageButtonPanel.setLayout(null); // Use null layout for absolute positioning messageButtonPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); messageButtonPanel.setToolTipText("Tin nhắn"); // Add a tooltip // Make the button smaller and square-like messageButtonPanel.setPreferredSize(new Dimension(55, 55)); messageButtonPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(2, 2, 2, 2), BorderFactory.createEmptyBorder(5, 10, 5, 10) )); // Create the main icon label JLabel buttonLabel = new JLabel(); buttonLabel.setHorizontalAlignment(SwingConstants.CENTER); buttonLabel.setOpaque(false); buttonLabel.setBounds(0, 0, 55, 55); // Position the main icon 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 } // Create the unread count badge unreadCountLabel = new JLabel(); unreadCountLabel.setOpaque(true); unreadCountLabel.setBackground(Color.RED); unreadCountLabel.setForeground(Color.WHITE); unreadCountLabel.setFont(new Font("Arial", Font.BOLD, 10)); unreadCountLabel.setHorizontalAlignment(SwingConstants.CENTER); unreadCountLabel.setBounds(35, 5, 18, 18); // Position in top-right corner unreadCountLabel.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); // Make the badge circular unreadCountLabel.setPreferredSize(new Dimension(18, 18)); unreadCountLabel.setMinimumSize(new Dimension(18, 18)); unreadCountLabel.setMaximumSize(new Dimension(18, 18)); // Initially hide the badge unreadCountLabel.setVisible(false); messageButtonPanel.add(buttonLabel); messageButtonPanel.add(unreadCountLabel); messageButtonPanel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { // Reset unread count when opening chat resetUnreadMessageCount(); // Open the SimpleChatDialog SimpleChatDialog chatDialog = new SimpleChatDialog(mainFrame, NavBar.this); chatDialog.setVisible(true); } @Override public void mouseEntered(MouseEvent e) { messageButtonPanel.setBackground(messageColor.darker()); } @Override public void mouseExited(MouseEvent e) { messageButtonPanel.setBackground(messageColor); } }); return messageButtonPanel; } /** * Handles the incoming EmergencyResponse. This version is silent and uses a manual * dialog creation to ensure the custom close button works correctly. * @param response The emergency response packet from the server. */ private void handleEmergencyResponse(EmergencyResponse response) { SwingUtilities.invokeLater(() -> { log.warn("Received EMERGENCY alert from: {}", response.getSenderName()); // Create enhanced visual components for the dialog String htmlMessage = "" + "

CẢNH BÁO KHẨN CẤP!

" + "

Nhận được tín hiệu khẩn cấp từ người dùng:

" + "

" + response.getSenderName() + "

" + "

Vui lòng kiểm tra và hỗ trợ ngay lập tức!

" + ""; JLabel messageLabel = new JLabel(htmlMessage); JCheckBox acknowledgementCheck = new JCheckBox("Tôi xác nhận đã đọc và hiểu rõ cảnh báo này."); acknowledgementCheck.setFont(new Font("Arial", Font.ITALIC, 12)); acknowledgementCheck.setOpaque(false); JPanel contentPanel = new JPanel(new BorderLayout(0, 15)); contentPanel.setOpaque(false); contentPanel.add(messageLabel, BorderLayout.CENTER); contentPanel.add(acknowledgementCheck, BorderLayout.SOUTH); JPanel dialogPanel = new JPanel(new BorderLayout()); dialogPanel.setBackground(new Color(255, 220, 220)); dialogPanel.setBorder(BorderFactory.createLineBorder(Color.RED, 3)); dialogPanel.add(contentPanel, BorderLayout.CENTER); JButton confirmButton = new JButton("ĐÃ XỬ LÝ"); confirmButton.setFont(new Font("Arial", Font.BOLD, 14)); confirmButton.setEnabled(false); acknowledgementCheck.addActionListener(e -> confirmButton.setEnabled(acknowledgementCheck.isSelected())); // --- CORRECTED DIALOG CREATION --- // 1. Create a JOptionPane instance with our custom panel and button. JOptionPane optionPane = new JOptionPane( dialogPanel, JOptionPane.ERROR_MESSAGE, JOptionPane.DEFAULT_OPTION, null, new Object[]{confirmButton}, // Array of options confirmButton); // 2. Create a JDialog from the JOptionPane. final JDialog dialog = optionPane.createDialog(mainFrame, "🚨🚨 TÍN HIỆU KHẨN CẤP 🚨🚨"); // 3. Add an ActionListener to our custom button to close the dialog. confirmButton.addActionListener(e -> dialog.dispose()); // 4. Show the dialog. dialog.setVisible(true); }); } private void handleErrorResponse(ErrorResponse response) { log.error("Error response: {}", response.getError()); SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(mainFrame, response.getError().toString(), "Lỗi", JOptionPane.ERROR_MESSAGE)); } /** * Cleans up resources, specifically unregistering network listeners. * This should be called when the NavBar is no longer needed (e.g., on logout). */ public void cleanup() { ClientHandler.deleteListener(EmergencyResponse.class, emergencyResponseListener); ClientHandler.deleteListener(ErrorResponse.class, errorResponseListener); ClientHandler.deleteListener(SimpleMessageResponse.class, navBarMessageListener); log.info("All NavBar listeners removed."); } }