diff --git a/database/BSK.db-shm b/database/BSK.db-shm index 224e88b..7e895a6 100644 Binary files a/database/BSK.db-shm and b/database/BSK.db-shm differ diff --git a/database/BSK.db-wal b/database/BSK.db-wal index 1146c84..189fb73 100644 Binary files a/database/BSK.db-wal and b/database/BSK.db-wal differ diff --git a/image/checkup_media/48/IMG_48_20251013_000936_631.jpg b/image/checkup_media/48/IMG_48_20251013_000936_631.jpg new file mode 100644 index 0000000..aa26899 Binary files /dev/null and b/image/checkup_media/48/IMG_48_20251013_000936_631.jpg differ diff --git a/image/checkup_media/48/IMG_48_20251013_000937_904.jpg b/image/checkup_media/48/IMG_48_20251013_000937_904.jpg new file mode 100644 index 0000000..9da5e45 Binary files /dev/null and b/image/checkup_media/48/IMG_48_20251013_000937_904.jpg differ diff --git a/image/checkup_media/48/IMG_48_20251013_000939_184.jpg b/image/checkup_media/48/IMG_48_20251013_000939_184.jpg new file mode 100644 index 0000000..64babb7 Binary files /dev/null and b/image/checkup_media/48/IMG_48_20251013_000939_184.jpg differ diff --git a/image/checkup_media/48/IMG_48_20251013_000940_386.jpg b/image/checkup_media/48/IMG_48_20251013_000940_386.jpg new file mode 100644 index 0000000..1ee173a Binary files /dev/null and b/image/checkup_media/48/IMG_48_20251013_000940_386.jpg differ diff --git a/src/main/DebugFileWalk.java b/src/main/DebugFileWalk.java deleted file mode 100644 index b0624f1..0000000 --- a/src/main/DebugFileWalk.java +++ /dev/null @@ -1,51 +0,0 @@ -package BsK.client.ui.component.CheckUpPage; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.stream.Stream; - -public class DebugFileWalk { - - public static void main(String[] args) { - // ▼▼▼ IMPORTANT ▼▼▼ - // Change this path to the EXACT folder you are trying to scan. - // Use double backslashes "\\" for Windows paths. - // Example: "C:\\Users\\YourName\\Pictures\\MyEvent" - Path mediaPath = Paths.get("ANH SIEU AM"); - // ▲▲▲ IMPORTANT ▲▲▲ - - - System.out.println("--- Starting Scan ---"); - System.out.println("Scanning Path: " + mediaPath.toAbsolutePath()); - System.out.println("Does path exist? " + Files.exists(mediaPath)); - System.out.println("--------------------------------\n"); - - System.out.println(">>> ALL files and folders found by Files.walk():"); - try (Stream pathStream = Files.walk(mediaPath)) { - pathStream.forEach(path -> { - System.out.println(" -> Found: " + path); - }); - } catch (IOException e) { - System.err.println("An ERROR occurred during the scan:"); - e.printStackTrace(); - } - - System.out.println("\n--------------------------------"); - System.out.println(">>> Files that match the '.jpg' filter:"); - try (Stream pathStream = Files.walk(mediaPath)) { - pathStream - .filter(path -> !Files.isDirectory(path)) - .filter(path -> path.toString().toLowerCase().endsWith(".jpg")) - .forEach(path -> { - System.out.println(" -> Matched: " + path); - }); - } catch (IOException e) { - System.err.println("An ERROR occurred during the filtered scan:"); - e.printStackTrace(); - } - - System.out.println("\n--- Scan Finished ---"); - } -} \ No newline at end of file diff --git a/src/main/java/BsK/client/Client.java b/src/main/java/BsK/client/Client.java index 2d25915..76e696c 100644 --- a/src/main/java/BsK/client/Client.java +++ b/src/main/java/BsK/client/Client.java @@ -189,9 +189,10 @@ public class Client { true, new DefaultHttpHeaders(), 50 * 1024 * 1024)) - // Add IdleStateHandler with longer timeouts for network resilience - // Read timeout: 60 seconds, Write timeout: 30 seconds - .addLast(new IdleStateHandler(60, 30, 0)) + // Add IdleStateHandler for keep-alive and timeout detection + // Read timeout: 5 minutes (300s) - disconnect if no data from server + // Write timeout: 10 seconds - send ping if no data sent to server + .addLast(new IdleStateHandler(300, 10, 0)) .addLast("ws", ClientHandler.INSTANCE); } }); diff --git a/src/main/java/BsK/client/network/handler/ClientHandler.java b/src/main/java/BsK/client/network/handler/ClientHandler.java index 8c0b5da..1714c45 100644 --- a/src/main/java/BsK/client/network/handler/ClientHandler.java +++ b/src/main/java/BsK/client/network/handler/ClientHandler.java @@ -6,7 +6,9 @@ import BsK.client.ui.handler.UIHandler; import BsK.common.entity.DoctorItem; import BsK.common.packet.Packet; import BsK.common.packet.PacketSerializer; +import BsK.common.packet.req.PingRequest; import BsK.common.packet.res.*; +import BsK.common.util.network.NetworkUtil; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.websocketx.*; @@ -35,6 +37,12 @@ public class ClientHandler extends SimpleChannelInboundHandler> responseListeners = listeners.get(packet.getClass()); @@ -156,33 +177,40 @@ public class ClientHandler extends SimpleChannelInboundHandler= MAX_MISSED_PONGS) { + log.error("Missed {} consecutive pongs - disconnecting", missedPongCount); + wasTimeoutDetected = true; + lastDisconnectionType = DisconnectionType.TIMEOUT; + ctx.close(); // Force disconnect + return; + } + } + + // Send new ping packet + log.debug("Sending PingRequest to server (missed count: {})", missedPongCount); + lastPingRequestTime = System.currentTimeMillis(); + awaitingPongResponse = true; + + PingRequest pingRequest = new PingRequest(); + NetworkUtil.sendPacket(ctx.channel(), pingRequest); + } else if (event.state() == IdleState.READER_IDLE) { - // Server hasn't responded in a while - this is a timeout scenario - log.warn("Server not responding - timeout detected"); + // Server hasn't sent any data (including pong) for 5 minutes + log.warn("Server not responding - no data received for 5 minutes (READER_IDLE)"); wasTimeoutDetected = true; lastDisconnectionType = DisconnectionType.TIMEOUT; - // Just set the flag, don't show dialog yet - let channelInactive handle it - // Try to send a ping to test connection - lastPingTime = System.currentTimeMillis(); - awaitingPong = true; - ctx.writeAndFlush(new PingWebSocketFrame()); - } - } else if (evt instanceof WebSocketFrame) { - // Handle pong responses - if (evt instanceof PongWebSocketFrame) { - lastPongTime = System.currentTimeMillis(); - long pingRoundTrip = lastPongTime - lastPingTime; - log.debug("Received pong from server (round trip: {}ms)", pingRoundTrip); - awaitingPong = false; - // Reset timeout flag since we got a response - wasTimeoutDetected = false; + // Let channelInactive handle the disconnect } } + + // IMPORTANT: Call super to ensure event propagation continues + super.userEventTriggered(ctx, evt); } @Override @@ -223,10 +251,10 @@ public class ClientHandler extends SimpleChannelInboundHandler= MAX_MISSED_PONGS) { lastDisconnectionType = DisconnectionType.TIMEOUT; - log.info("Channel inactive due to timeout"); - } else if (awaitingPong && (System.currentTimeMillis() - lastPingTime) > 60000) { + log.info("Channel inactive due to timeout (missed {} pongs)", missedPongCount); + } else if (awaitingPongResponse && (System.currentTimeMillis() - lastPingRequestTime) > 30000) { lastDisconnectionType = DisconnectionType.TIMEOUT; log.info("Channel inactive - server stopped responding to pings"); } else { @@ -262,6 +290,9 @@ public class ClientHandler extends SimpleChannelInboundHandler uploadPdfResponseListener = this::handleUploadPdfResponse; private final ResponseListener syncImagesResponseListener = this::handleSyncImagesResponse; private final ResponseListener getImageResponseListener = this::handleGetCheckupImageResponse; + private final ResponseListener deleteImageResponseListener = this::handleDeleteImageResponse; private JTextField checkupIdField, customerLastNameField, customerFirstNameField,customerAddressField, customerPhoneField, customerIdField, customerCccdDdcnField; private JTextArea suggestionField, diagnosisField, conclusionField; // Changed symptomsField to suggestionField private JTextPane notesField; @@ -365,6 +366,7 @@ public class CheckUpPage extends JPanel { ClientHandler.addResponseListener(UploadCheckupPdfResponse.class, uploadPdfResponseListener); ClientHandler.addResponseListener(SyncCheckupImagesResponse.class, syncImagesResponseListener); ClientHandler.addResponseListener(GetCheckupImageResponse.class, getImageResponseListener); + ClientHandler.addResponseListener(DeleteCheckupImageResponse.class, deleteImageResponseListener); // Instantiate the new queue page but don't show it yet queueManagementPage = new QueueManagementPage(); @@ -3652,6 +3654,45 @@ public class CheckUpPage extends JPanel { }); } + private void deleteImage(File imageFile) { + if (imageFile == null || !imageFile.exists()) { + log.warn("Cannot delete image - file does not exist or is null"); + return; + } + + String fileName = imageFile.getName(); + String checkupId = currentCheckupIdForMedia; + + if (checkupId == null || checkupId.trim().isEmpty()) { + log.error("Cannot delete image - no checkup ID available"); + JOptionPane.showMessageDialog(this, + "Không thể xóa ảnh - không có mã khám.", + "Lỗi", + JOptionPane.ERROR_MESSAGE); + return; + } + + // Send delete request to server + DeleteCheckupImageRequest request = new DeleteCheckupImageRequest(checkupId, fileName); + NetworkUtil.sendPacket(ClientHandler.ctx.channel(), request); + log.info("Sent DeleteCheckupImageRequest for file: {} in checkup: {}", fileName, checkupId); + + // Delete local file immediately + try { + if (imageFile.delete()) { + log.info("Successfully deleted local image file: {}", fileName); + // Refresh the gallery + if (currentCheckupMediaPath != null && Files.exists(currentCheckupMediaPath)) { + loadAndDisplayImages(currentCheckupMediaPath); + } + } else { + log.warn("Failed to delete local image file: {}", fileName); + } + } catch (Exception e) { + log.error("Error deleting local image file: {}", fileName, e); + } + } + private void handleUploadImageResponse(UploadCheckupImageResponse response) { String fileName = response.getFileName(); @@ -3672,6 +3713,29 @@ public class CheckUpPage extends JPanel { } } + private void handleDeleteImageResponse(DeleteCheckupImageResponse response) { + String fileName = response.getFileName(); + + if (response.isSuccess()) { + log.info("Successfully deleted image from server: {} - {}", fileName, response.getMessage()); + SwingUtilities.invokeLater(() -> { + // Image already deleted locally, just log success + if (callingStatusLabel != null) { + callingStatusLabel.setText("Đã xóa ảnh: " + fileName); + callingStatusLabel.setForeground(new Color(0, 150, 0)); // Green color + } + }); + } else { + log.error("Failed to delete image from server: {} - {}", fileName, response.getMessage()); + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(this, + "Lỗi khi xóa ảnh trên server: " + response.getMessage(), + "Lỗi Xóa Ảnh", + JOptionPane.ERROR_MESSAGE); + }); + } + } + private void handleUploadPdfResponse(UploadCheckupPdfResponse response) { if (response.isSuccess()) { log.info("Successfully uploaded PDF: {} - {}", response.getPdfType(), response.getMessage()); @@ -4091,11 +4155,33 @@ public class CheckUpPage extends JPanel { layeredPane.setBorder(BorderFactory.createLineBorder(Color.GRAY)); layeredPane.setCursor(new Cursor(Cursor.HAND_CURSOR)); // Set the hand cursor + // Create popup menu for right-click + JPopupMenu imagePopupMenu = new JPopupMenu(); + JMenuItem deleteMenuItem = new JMenuItem("Xoá"); + deleteMenuItem.setIcon(UIManager.getIcon("FileView.fileIcon")); // Optional icon + deleteMenuItem.addActionListener(ev -> { + int confirm = JOptionPane.showConfirmDialog( + CheckUpPage.this, + "Bạn có chắc chắn muốn xoá ảnh này không?\n" + imageFile.getName(), + "Xác nhận xoá", + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE); + + if (confirm == JOptionPane.YES_OPTION) { + deleteImage(imageFile); + } + }); + imagePopupMenu.add(deleteMenuItem); + // Add the click listener directly to the image label imageLabel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - showFullImageDialog(imageFile); + if (SwingUtilities.isRightMouseButton(e)) { + imagePopupMenu.show(e.getComponent(), e.getX(), e.getY()); + } else if (SwingUtilities.isLeftMouseButton(e)) { + showFullImageDialog(imageFile); + } } }); @@ -4300,6 +4386,20 @@ public class CheckUpPage extends JPanel { TableRowSorter sorter = new TableRowSorter<>(queueTableModel); queueTable.setRowSorter(sorter); + // Set custom comparator for STT column to sort as integers + sorter.setComparator(0, new Comparator() { + @Override + public int compare(String s1, String s2) { + try { + Integer i1 = Integer.parseInt(s1); + Integer i2 = Integer.parseInt(s2); + return i1.compareTo(i2); + } catch (NumberFormatException e) { + return s1.compareTo(s2); // Fallback to string comparison + } + } + }); + // Sort by "STT" (queue number) column in ascending order List sortKeys = new ArrayList<>(); sortKeys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING)); diff --git a/src/main/java/BsK/common/packet/req/DeleteCheckupImageRequest.java b/src/main/java/BsK/common/packet/req/DeleteCheckupImageRequest.java new file mode 100644 index 0000000..421408a --- /dev/null +++ b/src/main/java/BsK/common/packet/req/DeleteCheckupImageRequest.java @@ -0,0 +1,16 @@ +package BsK.common.packet.req; + +import BsK.common.packet.Packet; +import lombok.Data; + +@Data +public class DeleteCheckupImageRequest implements Packet { + private String checkupId; + private String fileName; + + public DeleteCheckupImageRequest(String checkupId, String fileName) { + this.checkupId = checkupId; + this.fileName = fileName; + } +} + diff --git a/src/main/java/BsK/common/packet/req/PingRequest.java b/src/main/java/BsK/common/packet/req/PingRequest.java index c2c0ae4..972adbb 100644 --- a/src/main/java/BsK/common/packet/req/PingRequest.java +++ b/src/main/java/BsK/common/packet/req/PingRequest.java @@ -1,9 +1,13 @@ package BsK.common.packet.req; import BsK.common.packet.Packet; -import lombok.AllArgsConstructor; import lombok.Data; -@AllArgsConstructor @Data -public class PingRequest implements Packet {} +public class PingRequest implements Packet { + private long timestamp; + + public PingRequest() { + this.timestamp = System.currentTimeMillis(); + } +} diff --git a/src/main/java/BsK/common/packet/res/DeleteCheckupImageResponse.java b/src/main/java/BsK/common/packet/res/DeleteCheckupImageResponse.java new file mode 100644 index 0000000..c615d95 --- /dev/null +++ b/src/main/java/BsK/common/packet/res/DeleteCheckupImageResponse.java @@ -0,0 +1,18 @@ +package BsK.common.packet.res; + +import BsK.common.packet.Packet; +import lombok.Data; + +@Data +public class DeleteCheckupImageResponse implements Packet { + private boolean success; + private String message; + private String fileName; + + public DeleteCheckupImageResponse(boolean success, String message, String fileName) { + this.success = success; + this.message = message; + this.fileName = fileName; + } +} + diff --git a/src/main/java/BsK/common/packet/res/PongResponse.java b/src/main/java/BsK/common/packet/res/PongResponse.java new file mode 100644 index 0000000..8a32576 --- /dev/null +++ b/src/main/java/BsK/common/packet/res/PongResponse.java @@ -0,0 +1,18 @@ +package BsK.common.packet.res; + +import BsK.common.packet.Packet; +import lombok.Data; + +@Data +public class PongResponse implements Packet { + private long timestamp; + + public PongResponse() { + this.timestamp = System.currentTimeMillis(); + } + + public PongResponse(long clientTimestamp) { + this.timestamp = clientTimestamp; + } +} + diff --git a/src/main/java/BsK/server/Server.java b/src/main/java/BsK/server/Server.java index 4ff0779..30578fb 100644 --- a/src/main/java/BsK/server/Server.java +++ b/src/main/java/BsK/server/Server.java @@ -294,9 +294,10 @@ public class Server { .addLast(new ChunkedWriteHandler()) .addLast(new HttpObjectAggregator(50 * 1024 * 1024)) .addLast(new WebSocketServerProtocolHandler("/", null, true, 50 * 1024 * 1024)) - // Add IdleStateHandler with longer timeout to be more tolerant of network issues - // Reader timeout: 90 seconds (longer than client's 60s to allow for network delays) - .addLast(new IdleStateHandler(90, 0, 0)) + // Add IdleStateHandler to detect inactive clients + // Reader timeout: 40 seconds - disconnect if no data from client (ping should arrive every 10s) + // Allow up to 4 missed pings (10s * 4 = 40s) before disconnecting + .addLast(new IdleStateHandler(40, 0, 0)) .addLast(new ServerHandler()); } }); diff --git a/src/main/java/BsK/server/network/handler/ServerHandler.java b/src/main/java/BsK/server/network/handler/ServerHandler.java index 1a8cf72..6ceeb5d 100644 --- a/src/main/java/BsK/server/network/handler/ServerHandler.java +++ b/src/main/java/BsK/server/network/handler/ServerHandler.java @@ -1217,6 +1217,59 @@ public class ServerHandler extends SimpleChannelInboundHandler { + try { + if (!Server.isGoogleDriveConnected()) { + log.info("Google Drive not connected - skipping image deletion for checkup {}", checkupId); + return; + } + + // Get the checkup's Google Drive folder ID from database + String checkupDriveFolderId = null; + String sql = "SELECT drive_folder_id FROM Checkup WHERE checkup_id = ?"; + + try (Connection conn = DatabaseManager.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setInt(1, Integer.parseInt(checkupId)); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + checkupDriveFolderId = rs.getString("drive_folder_id"); + } + } + } + + if (checkupDriveFolderId == null || checkupDriveFolderId.trim().isEmpty()) { + log.warn("Checkup {} has no Google Drive folder ID - cannot delete image", checkupId); + return; + } + + log.info("Deleting image {} from Google Drive for checkup {}", fileName, checkupId); + + // Find and delete the file from Google Drive + List files = + Server.getGoogleDriveService().findFilesByName(checkupDriveFolderId, fileName); + + if (files.isEmpty()) { + log.warn("File {} not found in Google Drive folder for checkup {}", fileName, checkupId); + return; + } + + // Delete all matching files (there should typically be only one) + for (com.google.api.services.drive.model.File file : files) { + Server.getGoogleDriveService().deleteFile(file.getId()); + log.info("Successfully deleted file {} (ID: {}) from Google Drive for checkup {}", + fileName, file.getId(), checkupId); + + if (ServerDashboard.getInstance() != null) { + ServerDashboard.getInstance().addLog( + String.format("Deleted image %s from Google Drive for checkup %s", fileName, checkupId) + ); + } + } + + } catch (NumberFormatException e) { + log.error("Invalid checkupId format '{}' for Google Drive deletion.", checkupId); + } catch (Exception e) { + log.error("Failed to delete image from Google Drive for checkup {}: {}", checkupId, e.getMessage(), e); + if (ServerDashboard.getInstance() != null) { + ServerDashboard.getInstance().addLog( + String.format("Failed to delete image %s from Google Drive for checkup %s: %s", + fileName, checkupId, e.getMessage()) + ); + } + } + }); + } + private void broadcastQueueUpdate() { // SỬA ĐỔI: Sử dụng một kết nối duy nhất, an toàn cho tất cả các truy vấn bên trong diff --git a/src/main/java/BsK/server/service/GoogleDriveServiceOAuth.java b/src/main/java/BsK/server/service/GoogleDriveServiceOAuth.java index 5788751..9816120 100644 --- a/src/main/java/BsK/server/service/GoogleDriveServiceOAuth.java +++ b/src/main/java/BsK/server/service/GoogleDriveServiceOAuth.java @@ -451,7 +451,7 @@ public class GoogleDriveServiceOAuth { * @return A list of File objects matching the name. * @throws IOException if the Google Drive API call fails. */ - private List findFilesByName(String parentFolderId, String fileName) throws IOException { + public List findFilesByName(String parentFolderId, String fileName) throws IOException { String query = String.format("name = '%s' and '%s' in parents and trashed = false", fileName.replace("'", "\\'"), // Escape single quotes in filename parentFolderId); @@ -523,4 +523,14 @@ public class GoogleDriveServiceOAuth { return fileId; } + + /** + * Deletes a file from Google Drive by its file ID. + * @param fileId The ID of the file to delete. + * @throws IOException if the deletion fails. + */ + public void deleteFile(String fileId) throws IOException { + driveService.files().delete(fileId).execute(); + log.info("Successfully deleted file with ID: {}", fileId); + } } \ No newline at end of file