5656import java .io .IOException ;
5757import java .util .Comparator ;
5858import java .util .ArrayList ;
59+ import java .util .Collections ;
5960import java .util .EnumMap ;
6061import java .util .Map ;
6162import java .util .Optional ;
6869import java .util .Arrays ;
6970import javafx .beans .value .ChangeListener ;
7071import org .kordamp .ikonli .javafx .FontIcon ;
72+ import org .kordamp .ikonli .materialdesign2 .MaterialDesignB ;
7173import org .kordamp .ikonli .materialdesign2 .MaterialDesignA ;
7274import org .kordamp .ikonli .materialdesign2 .MaterialDesignC ;
7375import org .kordamp .ikonli .materialdesign2 .MaterialDesignD ;
7476import org .kordamp .ikonli .materialdesign2 .MaterialDesignF ;
7577import org .kordamp .ikonli .materialdesign2 .MaterialDesignS ;
78+ import org .kordamp .ikonli .materialdesign2 .MaterialDesignL ;
7679
7780public class MainViewController {
7881
@@ -549,6 +552,66 @@ private HBox buildCardDetails(Note note) {
549552 Tooltip .install (assigneeLabel , new Tooltip ("Assigned to: " + assigneeNames ));
550553 detailsBox .getChildren ().add (assigneeLabel );
551554 }
555+
556+ // Dependencies
557+ if (note .getDependencies () != null && !note .getDependencies ().isEmpty ()) {
558+ // --- Active Blockers ---
559+ List <String > activeBlockerTitles = note .getDependencies ().stream ()
560+ .filter (dep -> dep .type () == Note .DependencyType .BLOCKED_BY )
561+ .filter (dep -> noteManager .findNoteAcrossAllBoards (dep .otherNoteId ())
562+ .map (blocker -> blocker .getStatus () != Note .Status .DONE && blocker .getStatus () != Note .Status .ARCHIVED )
563+ .orElse (false )) // If note not found, it's not an active blocker.
564+ .map (Note .Dependency ::otherNoteTitle )
565+ .collect (Collectors .toList ());
566+
567+ if (!activeBlockerTitles .isEmpty ()) {
568+ FontIcon depIcon = new FontIcon (MaterialDesignL .LOCK_OUTLINE );
569+ depIcon .setIconSize (12 );
570+ depIcon .getStyleClass ().addAll ("detail-icon" , "danger-icon" );
571+ Tooltip .install (depIcon , new Tooltip ("Blocked by: " + String .join (", " , activeBlockerTitles )));
572+ detailsBox .getChildren ().add (depIcon );
573+ }
574+
575+ // --- Notes this one Blocks ---
576+ List <String > blockedByThisNoteTitles = note .getDependencies ().stream ()
577+ .filter (dep -> dep .type () == Note .DependencyType .BLOCKS )
578+ .map (Note .Dependency ::otherNoteTitle )
579+ .collect (Collectors .toList ());
580+
581+ if (!blockedByThisNoteTitles .isEmpty ()) {
582+ FontIcon depIcon = new FontIcon (MaterialDesignB .BLOCK_HELPER );
583+ depIcon .setIconSize (12 );
584+ depIcon .getStyleClass ().addAll ("detail-icon" , "warning-icon" );
585+ Tooltip .install (depIcon , new Tooltip ("Blocks: " + String .join (", " , blockedByThisNoteTitles )));
586+ detailsBox .getChildren ().add (depIcon );
587+ }
588+ }
589+
590+ // --- Related & Linked Goals Icon ---
591+ List <String > relatedNoteTitles = note .getDependencies ().stream ()
592+ .filter (dep -> dep .type () == Note .DependencyType .RELATED_TO )
593+ .map (Note .Dependency ::otherNoteTitle )
594+ .collect (Collectors .toList ());
595+
596+ boolean hasLinkedGoals = note .getGoals ().stream ().anyMatch (this ::goalHasLinks );
597+
598+ if (!relatedNoteTitles .isEmpty () || hasLinkedGoals ) {
599+ FontIcon linkIcon = new FontIcon (MaterialDesignL .LINK_VARIANT );
600+ linkIcon .setIconSize (12 );
601+ linkIcon .getStyleClass ().add ("detail-icon" );
602+
603+ List <String > tooltipParts = new ArrayList <>();
604+ if (!relatedNoteTitles .isEmpty ()) {
605+ tooltipParts .add ("Related to: " + String .join (", " , relatedNoteTitles ));
606+ }
607+ if (hasLinkedGoals ) {
608+ tooltipParts .add ("Contains links to other notes in its goals." );
609+ }
610+
611+ Tooltip .install (linkIcon , new Tooltip (String .join ("\n " , tooltipParts )));
612+ detailsBox .getChildren ().add (linkIcon );
613+ }
614+
552615 return detailsBox ;
553616 }
554617
@@ -1008,7 +1071,22 @@ private void handleSortColumn(Note.Status status, Comparator<Note> comparator) {
10081071 }
10091072
10101073 private void handleReorderNote (UUID draggedNoteId , Note .Status targetStatus , int newIndexInUI ) {
1074+ // Find the note first to perform checks before any UI manipulation
10111075 currentBoard .findNoteById (draggedNoteId ).ifPresent (noteToMove -> {
1076+ // --- NEW: Check for blockers before moving to DONE ---
1077+ if (targetStatus == Note .Status .DONE || targetStatus == Note .Status .ARCHIVED ) {
1078+ List <String > openBlockers = getOpenBlockers (noteToMove );
1079+ if (!openBlockers .isEmpty ()) {
1080+ String blockersList = openBlockers .stream ()
1081+ .map (title -> "- " + title )
1082+ .collect (Collectors .joining ("\n " ));
1083+ showError ("Move Blocked" , "This note cannot be completed because it is still blocked by:\n \n " +
1084+ blockersList );
1085+ return ; // Abort the move
1086+ }
1087+ }
1088+
1089+ // If checks pass, proceed with the move
10121090 VBox oldContainer = noteContainersMap .get (noteToMove .getStatus ());
10131091 VBox newContainer = noteContainersMap .get (targetStatus );
10141092
@@ -1180,6 +1258,7 @@ private void showNoteDetailView(Note note, VBox noteCard) {
11801258 controller .setNote (note );
11811259 controller .setUsers (currentBoard .getMembers ());
11821260 controller .setAllTags (noteManager .getAllTags ());
1261+ controller .setNoteManager (noteManager ); // Pass the NoteManager for dependency searching
11831262 if (noteManager .getCurrentUser () != null ) {
11841263 controller .setCurrentUser (noteManager .getCurrentUser ()); // Set the current user for commenting
11851264 }
@@ -1205,6 +1284,25 @@ private void showNoteDetailView(Note note, VBox noteCard) {
12051284 // Refresh recent notes list after closing
12061285 refreshRecentNotesList ();
12071286
1287+ // Check if the user clicked a link to open another note
1288+ Optional <UUID > noteToOpenId = controller .getNoteToOpen ();
1289+ if (noteToOpenId .isPresent ()) {
1290+ noteManager .findNoteAndBoard (noteToOpenId .get ()).ifPresent (pair -> {
1291+ // Use Platform.runLater to avoid issues with opening a new dialog
1292+ // while the old one is still in its closing phase.
1293+ Platform .runLater (() -> {
1294+ // Switch to the board if it's not the current one
1295+ if (currentBoard == null || !currentBoard .getName ().equals (pair .board .getName ())) {
1296+ boardListView .getSelectionModel ().select (pair .board .getName ());
1297+ }
1298+ // Open the detail view for the linked note. We don't have a card context here.
1299+ showNoteDetailView (pair .note , null );
1300+ });
1301+ });
1302+ // Stop further processing since we are opening a new note and any other result is irrelevant.
1303+ return ;
1304+ }
1305+
12081306 // Check if the user saved the changes by getting the returned note copy
12091307 controller .getResult ().ifPresent (result -> {
12101308 Note savedNoteCopy = result .savedNote ();
@@ -1242,4 +1340,37 @@ private void showNoteDetailView(Note note, VBox noteCard) {
12421340 showError ("Failed to open editor" , "Could not load the note detail view. Error: " + e .getMessage ());
12431341 }
12441342 }
1343+
1344+ /**
1345+ * Checks a note for any "BLOCKED_BY" dependencies that are not yet completed.
1346+ *
1347+ * @param note The note to check.
1348+ * @return A list of titles of the notes that are actively blocking the given note.
1349+ */
1350+ private List <String > getOpenBlockers (Note note ) {
1351+ if (note == null || note .getDependencies () == null ) {
1352+ return Collections .emptyList ();
1353+ }
1354+ return note .getDependencies ().stream ()
1355+ .filter (dep -> dep .type () == Note .DependencyType .BLOCKED_BY )
1356+ .filter (dep -> noteManager .findNoteAcrossAllBoards (dep .otherNoteId ())
1357+ .map (blocker -> blocker .getStatus () != Note .Status .DONE && blocker .getStatus () != Note .Status .ARCHIVED )
1358+ .orElse (false )) // Treat a non-existent note as a non-blocker
1359+ .map (Note .Dependency ::otherNoteTitle )
1360+ .collect (Collectors .toList ());
1361+ }
1362+
1363+ /**
1364+ * Recursively checks if a goal or any of its sub-goals are links to other notes.
1365+ *
1366+ * @param goal The goal to check.
1367+ * @return true if a link is found, false otherwise.
1368+ */
1369+ private boolean goalHasLinks (Note .Goal goal ) {
1370+ if (goal .isLink ()) {
1371+ return true ;
1372+ }
1373+ // The getSubGoals() method on the Goal class returns an unmodifiable list, which is never null.
1374+ return goal .getSubGoals ().stream ().anyMatch (this ::goalHasLinks );
1375+ }
12451376}
0 commit comments