From 5d23a16a1d35a92f154edf80305ed0c623e55dbb Mon Sep 17 00:00:00 2001
From: Florent Poittevin <florent.poittevin@unige.ch>
Date: Wed, 12 Feb 2020 15:02:39 +0100
Subject: [PATCH] feat: 1084 display nice page when aip is not present on
 storagion

---
 .../aip/aip-detail-guard.service.ts           | 69 ++++++++++++++++
 .../preservation/aip/aip-routing.module.ts    |  7 ++
 .../features/preservation/aip/aip.module.ts   | 11 +--
 .../routables/aip-home/aip-home.routable.ts   |  6 +-
 .../aip-not-found/aip-not-found.routable.html | 23 ++++++
 .../aip-not-found/aip-not-found.routable.scss | 36 ++++++++
 .../aip-not-found/aip-not-found.routable.ts   | 77 +++++++++++++++++
 .../preservation/aip/stores/aip.action.ts     |  3 +
 .../preservation/aip/stores/aip.state.ts      | 82 +++++++++++++------
 ...red-info-excluded-ignored-file.dialog.html |  6 +-
 ...hared-info-excluded-ignored-file.dialog.ts |  5 ++
 src/app/shared/enums/routes.enum.ts           |  1 +
 src/assets/i18n/de.json                       | 27 ++++--
 src/assets/i18n/en.json                       | 27 ++++--
 src/assets/i18n/fr.json                       | 25 ++++--
 15 files changed, 346 insertions(+), 59 deletions(-)
 create mode 100644 src/app/features/preservation/aip/aip-detail-guard.service.ts
 create mode 100644 src/app/features/preservation/aip/components/routables/aip-not-found/aip-not-found.routable.html
 create mode 100644 src/app/features/preservation/aip/components/routables/aip-not-found/aip-not-found.routable.scss
 create mode 100644 src/app/features/preservation/aip/components/routables/aip-not-found/aip-not-found.routable.ts

diff --git a/src/app/features/preservation/aip/aip-detail-guard.service.ts b/src/app/features/preservation/aip/aip-detail-guard.service.ts
new file mode 100644
index 000000000..721fe7708
--- /dev/null
+++ b/src/app/features/preservation/aip/aip-detail-guard.service.ts
@@ -0,0 +1,69 @@
+import {Injectable} from "@angular/core";
+import {
+  ActivatedRouteSnapshot,
+  CanActivate,
+  Router,
+} from "@angular/router";
+import {environment} from "@environments/environment";
+import {
+  Actions,
+  ofActionCompleted,
+  Store,
+} from "@ngxs/store";
+import {
+  AppRoutesEnum,
+  PreservationPlanningRoutesEnum,
+  RoutesEnum,
+} from "@shared/enums/routes.enum";
+import {
+  Observable,
+  of,
+} from "rxjs";
+import {
+  map,
+  take,
+} from "rxjs/operators";
+import {
+  isNullOrUndefined,
+  isTrue,
+} from "solidify-frontend";
+import {PreservationAipAction} from "./stores/aip.action";
+
+@Injectable({
+  providedIn: "root",
+})
+export class AipDetailGuardService implements CanActivate {
+  constructor(private readonly _router: Router,
+              private readonly _store: Store,
+              private readonly _actions$: Actions) {
+  }
+
+  canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
+    return this.isAuthorized(route);
+  }
+
+  private isAuthorized(route: ActivatedRouteSnapshot): Observable<boolean> {
+    const idAip = route.params[AppRoutesEnum.paramIdWithoutPrefixParam];
+    const storagionNodeId = +route.parent.params[PreservationPlanningRoutesEnum.storagionNumberWithoutPrefixParam];
+
+    const storage = environment.storagionUrls.find(storagion => storagion.index === storagionNodeId);
+    if (isNullOrUndefined(storage)) {
+      this._router.navigate([RoutesEnum.preservationAip]);
+      return of(false);
+    }
+
+    this._store.dispatch(new PreservationAipAction.GetById(idAip, false, false, storagionNodeId));
+    return this._actions$.pipe(
+      ofActionCompleted(PreservationAipAction.GetById),
+      take(1),
+      map((result) => {
+        if (isTrue(result.result.successful)) {
+          return true;
+        } else {
+          this._router.navigate([RoutesEnum.preservationAip, storagionNodeId, PreservationPlanningRoutesEnum.aipNotFound, idAip]);
+          return false;
+        }
+      }),
+    );
+  }
+}
diff --git a/src/app/features/preservation/aip/aip-routing.module.ts b/src/app/features/preservation/aip/aip-routing.module.ts
index dc5229dad..174a498e0 100644
--- a/src/app/features/preservation/aip/aip-routing.module.ts
+++ b/src/app/features/preservation/aip/aip-routing.module.ts
@@ -5,9 +5,11 @@ import {
 } from "@angular/router";
 import {AipDetailEditRoutable} from "@app/features/preservation/aip/components/routables/aip-detail-edit/aip-detail-edit.routable";
 import {AipHomeRoutable} from "@app/features/preservation/aip/components/routables/aip-home/aip-home.routable";
+import {AipDetailGuardService} from "@preservation/aip/aip-detail-guard.service";
 import {AipAipRoutable} from "@preservation/aip/components/routables/aip-aip/aip-aip.routable";
 import {AipListRoutable} from "@preservation/aip/components/routables/aip-list/aip-list.routable";
 import {AipMetadataRoutable} from "@preservation/aip/components/routables/aip-metadata/aip-metadata.routable";
+import {AipNotFoundRoutable} from "@preservation/aip/components/routables/aip-not-found/aip-not-found.routable";
 import {AipStoragionRootRoutable} from "@preservation/aip/components/routables/aip-storagion-root/aip-storagion-root.routable";
 import {AipTabsRoutable} from "@preservation/aip/components/routables/aip-tabs/aip-tabs.routable";
 import {PreservationAipState} from "@preservation/aip/stores/aip.state";
@@ -59,6 +61,10 @@ const routes: DlcmRoutes = [
       breadcrumb: callbackAipStoragionBreadcrumb,
     },
     children: [
+      {
+        path: PreservationPlanningRoutesEnum.aipNotFound + AppRoutesEnum.separator + AppRoutesEnum.paramId,
+        component: AipNotFoundRoutable,
+      },
       {
         path: PreservationPlanningRoutesEnum.aipList,
         component: AipTabsRoutable,
@@ -80,6 +86,7 @@ const routes: DlcmRoutes = [
         data: {
           breadcrumbMemoizedSelector: PreservationAipState.currentAipName,
         },
+        canActivate: [AipDetailGuardService],
         children: [
           {
             path: PreservationPlanningRoutesEnum.aipMetadata,
diff --git a/src/app/features/preservation/aip/aip.module.ts b/src/app/features/preservation/aip/aip.module.ts
index 0b5407077..f69746889 100644
--- a/src/app/features/preservation/aip/aip.module.ts
+++ b/src/app/features/preservation/aip/aip.module.ts
@@ -10,6 +10,7 @@ import {TranslateModule} from "@ngx-translate/core";
 import {NgxsModule} from "@ngxs/store";
 import {AipAipRoutable} from "@preservation/aip/components/routables/aip-aip/aip-aip.routable";
 import {AipMetadataRoutable} from "@preservation/aip/components/routables/aip-metadata/aip-metadata.routable";
+import {AipNotFoundRoutable} from "@preservation/aip/components/routables/aip-not-found/aip-not-found.routable";
 import {AipStoragionRootRoutable} from "@preservation/aip/components/routables/aip-storagion-root/aip-storagion-root.routable";
 import {AipTabsRoutable} from "@preservation/aip/components/routables/aip-tabs/aip-tabs.routable";
 import {PreservationAipAipState} from "@preservation/aip/stores/aip-aip/aip-aip.state";
@@ -24,13 +25,13 @@ const routables = [
   AipDetailEditRoutable,
   AipStoragionRootRoutable,
   AipMetadataRoutable,
-  AipAipRoutable
+  AipAipRoutable,
+  AipNotFoundRoutable,
 ];
 const containers = [];
-const dialogs = [
-];
+const dialogs = [];
 const presentationals = [
-  AipFormPresentational
+  AipFormPresentational,
 ];
 
 @NgModule({
@@ -49,7 +50,7 @@ const presentationals = [
       PreservationAipOrganizationalUnitState,
       PreservationAipAipState,
       PreservationAipStatusHistoryState,
-      PreservationAipAipStatusHistoryState
+      PreservationAipAipStatusHistoryState,
     ]),
   ],
   entryComponents: [
diff --git a/src/app/features/preservation/aip/components/routables/aip-home/aip-home.routable.ts b/src/app/features/preservation/aip/components/routables/aip-home/aip-home.routable.ts
index abaf24cc7..a34b20e9e 100644
--- a/src/app/features/preservation/aip/components/routables/aip-home/aip-home.routable.ts
+++ b/src/app/features/preservation/aip/components/routables/aip-home/aip-home.routable.ts
@@ -4,7 +4,7 @@ import {
 } from "@angular/core";
 import {Navigate} from "@ngxs/router-plugin";
 import {Store} from "@ngxs/store";
-import {SharedAbstractPresentational} from "@shared/components/presentationals/shared-abstract/shared-abstract.presentational";
+import {SharedAbstractRoutable} from "@shared/components/routables/shared-abstract/shared-abstract.routable";
 import {ApplicationRoleEnum} from "@shared/enums/application-role.enum";
 import {
   AppRoutesEnum,
@@ -21,7 +21,7 @@ import {environment} from "../../../../../../../environments/environment";
   styleUrls: ["./aip-home.routable.scss"],
   changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class AipHomeRoutable extends SharedAbstractPresentational {
+export class AipHomeRoutable extends SharedAbstractRoutable {
 
   userRolesObs: ApplicationRoleEnum[];
 
@@ -48,7 +48,7 @@ export class AipHomeRoutable extends SharedAbstractPresentational {
           path: RoutesEnum.preservationAip + AppRoutesEnum.separator + url.index,
           isVisible: () => true,
           url: url.url,
-      });
+        });
       i++;
     });
     return this.aipResources;
diff --git a/src/app/features/preservation/aip/components/routables/aip-not-found/aip-not-found.routable.html b/src/app/features/preservation/aip/components/routables/aip-not-found/aip-not-found.routable.html
new file mode 100644
index 000000000..58d1b12e3
--- /dev/null
+++ b/src/app/features/preservation/aip/components/routables/aip-not-found/aip-not-found.routable.html
@@ -0,0 +1,23 @@
+<div class="container">
+  <h1 class="title">{{'app.aip.notFound.title' | translate }}</h1>
+
+  <p class="not-present">{{'app.aip.notFound.notPresentOnNode' | translate:{storagionNumber: storagion_number} }}</p>
+  <p class="see-other-nodes">{{'app.aip.notFound.seeOnOtherNodes' | translate}}</p>
+
+  <ul class="list">
+    <li
+        *ngFor="let storage of listStorage"
+    ><a
+        (click)="navigateToStorageAipDetail(storage)"
+    >{{'app.aip.notFound.storagionItem' | translate:{storagionNumber: storage.index} }}</a></li>
+  </ul>
+
+  <div class="button-wrapper">
+    <button mat-flat-button
+            color="primary"
+            class="back-button"
+            (click)="back()"
+    >{{'app.aip.notFound.button.backToStoragionList' | translate}}
+    </button>
+  </div>
+</div>
diff --git a/src/app/features/preservation/aip/components/routables/aip-not-found/aip-not-found.routable.scss b/src/app/features/preservation/aip/components/routables/aip-not-found/aip-not-found.routable.scss
new file mode 100644
index 000000000..01103613a
--- /dev/null
+++ b/src/app/features/preservation/aip/components/routables/aip-not-found/aip-not-found.routable.scss
@@ -0,0 +1,36 @@
+@import "../../../../../../../sass/abstracts/variables";
+@import "../../../../../../../sass/abstracts/mixins";
+
+:host {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 20px;
+  min-height: 300px;
+
+  .container {
+    .title {
+
+    }
+
+    .not-present {
+      margin: 0;
+    }
+
+    .see-other-nodes {
+
+    }
+
+    .list {
+
+    }
+
+    .button-wrapper {
+      text-align: center;
+      margin-top: 20px;
+
+      .back-button {
+      }
+    }
+  }
+}
diff --git a/src/app/features/preservation/aip/components/routables/aip-not-found/aip-not-found.routable.ts b/src/app/features/preservation/aip/components/routables/aip-not-found/aip-not-found.routable.ts
new file mode 100644
index 000000000..98546b797
--- /dev/null
+++ b/src/app/features/preservation/aip/components/routables/aip-not-found/aip-not-found.routable.ts
@@ -0,0 +1,77 @@
+import {
+  ChangeDetectionStrategy,
+  Component,
+  OnInit,
+} from "@angular/core";
+import {
+  ActivatedRoute,
+  NavigationEnd,
+  Router,
+} from "@angular/router";
+import {Storage} from "@app/shared/models/storage.model";
+import {environment} from "@environments/environment";
+import {Navigate} from "@ngxs/router-plugin";
+import {Store} from "@ngxs/store";
+import {SharedAbstractRoutable} from "@shared/components/routables/shared-abstract/shared-abstract.routable";
+import {
+  AppRoutesEnum,
+  PreservationPlanningRoutesEnum,
+  RoutesEnum,
+} from "@shared/enums/routes.enum";
+import {tap} from "rxjs/internal/operators/tap";
+import {
+  distinctUntilChanged,
+  filter,
+} from "rxjs/operators";
+
+@Component({
+  selector: "dlcm-aip-not-found-routable",
+  templateUrl: "./aip-not-found.routable.html",
+  styleUrls: ["./aip-not-found.routable.scss"],
+  changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AipNotFoundRoutable extends SharedAbstractRoutable implements OnInit {
+  aipId: string;
+  storagion_number: number | undefined;
+
+  listStorage: Storage[];
+
+  constructor(private readonly _store: Store,
+              private readonly _route: ActivatedRoute,
+              private readonly _router: Router) {
+    super();
+  }
+
+  ngOnInit(): void {
+    super.ngOnInit();
+    this.retrieveAttribute();
+
+    this.subscribe(this._router.events
+      .pipe(
+        filter(event => event instanceof NavigationEnd),
+        distinctUntilChanged(),
+        tap(event => {
+          this.retrieveAttribute();
+        }),
+      ),
+    );
+  }
+
+  retrieveAttribute(): void {
+    this.storagion_number = +this._route.snapshot.parent.paramMap.get(PreservationPlanningRoutesEnum.storagionNumberWithoutPrefixParam);
+    this.aipId = this._route.snapshot.paramMap.get(AppRoutesEnum.paramIdWithoutPrefixParam);
+    this.listStorage = environment.storagionUrls.filter(storage => storage.index !== this.storagion_number);
+  }
+
+  back(): void {
+    this.navigate([RoutesEnum.preservationAip]);
+  }
+
+  navigateToStorageAipDetail(storage: Storage): void {
+    this.navigate([RoutesEnum.preservationAip, storage.index, PreservationPlanningRoutesEnum.aipDetail, this.aipId]);
+  }
+
+  private navigate(path: (string | number)[]): void {
+    this._store.dispatch(new Navigate(path));
+  }
+}
diff --git a/src/app/features/preservation/aip/stores/aip.action.ts b/src/app/features/preservation/aip/stores/aip.action.ts
index 572a0d13a..2515be3f8 100644
--- a/src/app/features/preservation/aip/stores/aip.action.ts
+++ b/src/app/features/preservation/aip/stores/aip.action.ts
@@ -54,6 +54,9 @@ export namespace PreservationAipAction {
 
   @TypeDefaultAction(state)
   export class GetById extends ResourceAction.GetById {
+    constructor(public id: string, public keepCurrentContext: boolean = false, public addInListTemp: boolean = false, public storagionNumber: number | undefined = undefined) {
+      super(id, keepCurrentContext, addInListTemp);
+    }
   }
 
   @TypeDefaultAction(state)
diff --git a/src/app/features/preservation/aip/stores/aip.state.ts b/src/app/features/preservation/aip/stores/aip.state.ts
index 4b7fb0920..89fe98173 100644
--- a/src/app/features/preservation/aip/stores/aip.state.ts
+++ b/src/app/features/preservation/aip/stores/aip.state.ts
@@ -1,8 +1,8 @@
+import {HttpParams} from "@angular/common/http";
 import {
-  HttpClient,
-  HttpHeaders,
-  HttpParams,
-} from "@angular/common/http";
+  ActivatedRoute,
+  Router,
+} from "@angular/router";
 import {AipExtended} from "@app/features/preservation/aip/models/aip-extended.model";
 import {
   defaultAipOrgUnitValue,
@@ -26,8 +26,8 @@ import {
   PreservationAipAipStateModel,
 } from "@preservation/aip/stores/aip-aip/aip-aip.state";
 import {
-  preservationAipActionNameSpace,
   PreservationAipAction,
+  preservationAipActionNameSpace,
 } from "@preservation/aip/stores/aip.action";
 import {
   PreservationAipStatusHistoryState,
@@ -43,7 +43,6 @@ import {
 } from "@shared/enums/routes.enum";
 import {DownloadService} from "@shared/services/download.service";
 import {defaultStatusHistoryInitValue} from "@shared/stores/status-history/status-history.state";
-import {saveAs} from "file-saver";
 import {Observable} from "rxjs";
 import {
   catchError,
@@ -54,8 +53,10 @@ import {
   defaultResourceStateInitValue,
   isNullOrUndefined,
   NotificationService,
+  OverrideDefaultAction,
   ResourceState,
   ResourceStateModel,
+  SolidifyStateError,
   StoreUtil,
   StringUtil,
   TRANSLATE,
@@ -78,7 +79,7 @@ export interface PreservationAipStateModel extends ResourceStateModel<AipExtende
   children: [
     PreservationAipOrganizationalUnitState,
     PreservationAipAipState,
-    PreservationAipStatusHistoryState
+    PreservationAipStatusHistoryState,
   ],
 })
 export class PreservationAipState extends ResourceState<PreservationAipStateModel, AipExtended> {
@@ -87,6 +88,8 @@ export class PreservationAipState extends ResourceState<PreservationAipStateMode
               protected store: Store,
               protected notificationService: NotificationService,
               protected actions$: Actions,
+              private readonly _router: Router,
+              private readonly _route: ActivatedRoute,
               private downloadService: DownloadService) {
     super(apiService, store, notificationService, actions$, {
       nameSpace: preservationAipActionNameSpace,
@@ -94,20 +97,25 @@ export class PreservationAipState extends ResourceState<PreservationAipStateMode
   }
 
   protected get _urlResource(): string {
-    let uid = StringUtil.stringEmpty;
-    if (isNullOrUndefined(this.store)) {
-      return StringUtil.stringEmpty;
-    }
-    const url = this.store.selectSnapshot(state => state.router.state.url);
-    if (url.includes(PreservationPlanningRoutesEnum.aipDetail)) {
-      const urlParts = url.split(AppRoutesEnum.separator);
-      uid = urlParts[urlParts.indexOf(PreservationPlanningRoutesEnum.aipDetail) - 1];
-    } else {
-      uid = this.store.selectSnapshot(state => state.router.state.root.children[0].children[0].children[0].params[PreservationPlanningRoutesEnum.storagionNumberWithoutPrefixParam]);
+    return this._generateUrlResource();
+  }
+
+  private _generateUrlResource(storagionNumber: number | undefined = undefined): string {
+    if (isNullOrUndefined(storagionNumber)) {
+      if (isNullOrUndefined(this.store)) {
+        return StringUtil.stringEmpty;
+      }
+      const url = this.store.selectSnapshot(state => state.router.state.url);
+      if (url.includes(PreservationPlanningRoutesEnum.aipDetail)) {
+        const urlParts = url.split(AppRoutesEnum.separator);
+        storagionNumber = urlParts[urlParts.indexOf(PreservationPlanningRoutesEnum.aipDetail) - 1];
+      } else {
+        storagionNumber = this.store.selectSnapshot(state => state.router.state.root.children[0].children[0].children[0].params[PreservationPlanningRoutesEnum.storagionNumberWithoutPrefixParam]);
+      }
     }
-    const storagion = ArchivalStorageResourceApiEnum.aipStorages.find(aip => aip.index === +uid);
+    const storagion = ArchivalStorageResourceApiEnum.aipStorages.find(aip => aip.index === +storagionNumber);
     if (isNullOrUndefined(storagion)) {
-      throw new Error(`The storagion index '${uid}' is not find in setting`);
+      throw new Error(`The storagion index '${storagionNumber}' is not find in setting`);
     }
     return storagion.url;
   }
@@ -141,6 +149,32 @@ export class PreservationAipState extends ResourceState<PreservationAipStateMode
     });
   }
 
+  @OverrideDefaultAction()
+  @Action(PreservationAipAction.GetById)
+  getById(ctx: StateContext<PreservationAipStateModel>, action: PreservationAipAction.GetById): Observable<AipExtended> {
+    let reset = {};
+    if (!action.keepCurrentContext) {
+      reset = {
+        current: undefined,
+      };
+    }
+    ctx.patchState({
+      isLoadingCounter: ctx.getState().isLoadingCounter + 1,
+      ...reset,
+    });
+
+    return this.apiService.getById<AipExtended>(this._generateUrlResource(action.storagionNumber), action.id)
+      .pipe(
+        tap((model: AipExtended) => {
+          ctx.dispatch(new PreservationAipAction.GetByIdSuccess(action, model));
+        }),
+        catchError(error => {
+          ctx.dispatch(new PreservationAipAction.GetByIdFail(action));
+          throw new SolidifyStateError(this, error);
+        }),
+      );
+  }
+
   @Action(PreservationAipAction.Reindex)
   reindex(ctx: StateContext<PreservationAipStateModel>, action: PreservationAipAction.Reindex): Observable<string> {
     ctx.patchState({
@@ -155,7 +189,7 @@ export class PreservationAipState extends ResourceState<PreservationAipStateMode
         catchError(error => {
           ctx.dispatch(new PreservationAipAction.ReindexFail(action));
           throw error;
-        })
+        }),
       );
   }
 
@@ -189,7 +223,7 @@ export class PreservationAipState extends ResourceState<PreservationAipStateMode
         catchError(error => {
           ctx.dispatch(new PreservationAipAction.DeepChecksumFail(action));
           throw error;
-        })
+        }),
       );
   }
 
@@ -223,7 +257,7 @@ export class PreservationAipState extends ResourceState<PreservationAipStateMode
         catchError(error => {
           ctx.dispatch(new PreservationAipAction.SimpleChecksumFail(action));
           throw error;
-        })
+        }),
       );
   }
 
@@ -279,9 +313,9 @@ export class PreservationAipState extends ResourceState<PreservationAipStateMode
   @Action(PreservationAipAction.GoToAip)
   goToAip(ctx: StateContext<PreservationAipStateModel>, action: PreservationAipAction.GoToAip): void {
     ctx.patchState({
-      current: action.aip
+      current: action.aip,
     });
-    const pathAipDetail = RoutesEnum.preservationAip + urlSeparator + action.storagion_number + urlSeparator + PreservationPlanningRoutesEnum.aipDetail + AppRoutesEnum.separator ;
+    const pathAipDetail = RoutesEnum.preservationAip + urlSeparator + action.storagion_number + urlSeparator + PreservationPlanningRoutesEnum.aipDetail + AppRoutesEnum.separator;
     const path = [pathAipDetail, action.aip.resId];
     ctx.dispatch(new Navigate(path));
   }
diff --git a/src/app/shared/components/dialogs/shared-info-excluded-ignored-file/shared-info-excluded-ignored-file.dialog.html b/src/app/shared/components/dialogs/shared-info-excluded-ignored-file/shared-info-excluded-ignored-file.dialog.html
index 46e2110b3..c0c342edb 100644
--- a/src/app/shared/components/dialogs/shared-info-excluded-ignored-file/shared-info-excluded-ignored-file.dialog.html
+++ b/src/app/shared/components/dialogs/shared-info-excluded-ignored-file/shared-info-excluded-ignored-file.dialog.html
@@ -7,7 +7,11 @@
     </ul>
     <h3>{{'deposit.file.dialog.excludedIgnoredFile.categeroy.puids' | translate}}</h3>
     <ul class="list">
-      <li *ngFor="let puid of fileList.puids">{{puid}}</li>
+      <li *ngFor="let puid of fileList.puids">
+        <a [href]="getPuidLink(puid)"
+           target="_blank"
+        >{{puid}}</a>
+      </li>
     </ul>
   </ng-container>
 </dlcm-shared-base-info-dialog>
diff --git a/src/app/shared/components/dialogs/shared-info-excluded-ignored-file/shared-info-excluded-ignored-file.dialog.ts b/src/app/shared/components/dialogs/shared-info-excluded-ignored-file/shared-info-excluded-ignored-file.dialog.ts
index 4c34238df..ea855dd5b 100644
--- a/src/app/shared/components/dialogs/shared-info-excluded-ignored-file/shared-info-excluded-ignored-file.dialog.ts
+++ b/src/app/shared/components/dialogs/shared-info-excluded-ignored-file/shared-info-excluded-ignored-file.dialog.ts
@@ -9,6 +9,7 @@ import {
   MatDialogRef,
 } from "@angular/material/dialog";
 import {DepositState} from "@deposit/stores/deposit.state";
+import {environment} from "@environments/environment";
 import {Store} from "@ngxs/store";
 import {SharedAbstractContainer} from "@shared/components/containers/shared-abstract/shared-abstract.container";
 import {DataFileStatusEnum} from "@shared/enums/business/data-file-status.enum";
@@ -52,6 +53,10 @@ export class SharedInfoExcludedIgnoredFileDialog extends SharedAbstractContainer
       this.dialogRef.close();
     }
   }
+
+  getPuidLink(puid: string): string {
+    return environment.urlNationalArchivePronom + puid;
+  }
 }
 
 export interface SharedInfoExcludedIgnoredFileDialogData {
diff --git a/src/app/shared/enums/routes.enum.ts b/src/app/shared/enums/routes.enum.ts
index dd2bc26d2..8a980288e 100644
--- a/src/app/shared/enums/routes.enum.ts
+++ b/src/app/shared/enums/routes.enum.ts
@@ -141,6 +141,7 @@ export enum PreservationPlanningRoutesEnum {
   aipTabCollections = "collection",
   aipMetadata = "metadata",
   aipFiles = "files",
+  aipNotFound = "not-found",
 
   storagionNumber = ":storagion",
   storagionNumberWithoutPrefixParam = "storagion",
diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json
index 054f4386a..b13e671ca 100644
--- a/src/assets/i18n/de.json
+++ b/src/assets/i18n/de.json
@@ -682,6 +682,15 @@
   },
   "app": {
     "aip": {
+      "notFound": {
+        "button": {
+          "backToStoragionList": "app.aip.notFound.button.backToStoragionList"
+        },
+        "notPresentOnNode": "app.aip.notFound.notPresentOnNode",
+        "seeOnOtherNodes": "app.aip.notFound.seeOnOtherNodes",
+        "storagionItem": "app.aip.notFound.storagionItem",
+        "title": "app.aip.notFound.title"
+      },
       "storage": "Storagion {{number}}",
       "tabs": {
         "all": "All",
@@ -971,9 +980,9 @@
   },
   "dataFileQuickStatus": {
     "cleaned": "File cleaned",
-    "excludedFile": "File to remove",
-    "ignoredFile": "File to not ignore or remove. Click to see the reason",
-    "inError": "In error. File to resume or remove. Click to see the reason",
+    "excludedFile": "File to remove. Click to see the list of excluded files",
+    "ignoredFile": "File to not ignore or remove. Click to see the list of ignored files",
+    "inError": "In error. File to resume or remove",
     "pending": "Processing",
     "ready": "No action to do"
   },
@@ -1077,12 +1086,12 @@
           "fileName": "File name",
           "location": "Location",
           "mimeType": "MimeType",
-          "puid": "Puid",
+          "puid": "PRONOM ID",
           "smartSize": "Size",
           "tool": {
             "description": "Tool description",
             "name": "Tool name",
-            "puid": "Tool Puid",
+            "puid": "PRONOM ID Tool",
             "version": "Tool version"
           },
           "type": "Type",
@@ -1104,16 +1113,16 @@
       "dialog": {
         "excludedIgnoredFile": {
           "categeroy": {
-            "files": "Files name",
-            "puids": "Puids"
+            "files": "Filepath pattern",
+            "puids": "PRONOM IDs"
           },
           "message": {
             "excluded": "This file is excluded because it falls within one of the criteria defined below :",
             "ignored": "This file is ignored because it falls within one of the criteria defined below :"
           },
           "title": {
-            "excluded": "Reason for file exclusion",
-            "ignored": "Reason for ignoring the file"
+            "excluded": "List of excluded files",
+            "ignored": "List of ignored files"
           }
         }
       },
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index 054f4386a..9771988bd 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -682,6 +682,15 @@
   },
   "app": {
     "aip": {
+      "notFound": {
+        "button": {
+          "backToStoragionList": "Back to the list of storagion nodes"
+        },
+        "notPresentOnNode": "The AIP is not present on the storage node {{storagionNumber}}.",
+        "seeOnOtherNodes": "You can view the details on another storagion node:",
+        "storagionItem": "Storagion node {{storagionNumber}}",
+        "title": "AIP absent from storagion node"
+      },
       "storage": "Storagion {{number}}",
       "tabs": {
         "all": "All",
@@ -971,9 +980,9 @@
   },
   "dataFileQuickStatus": {
     "cleaned": "File cleaned",
-    "excludedFile": "File to remove",
-    "ignoredFile": "File to not ignore or remove. Click to see the reason",
-    "inError": "In error. File to resume or remove. Click to see the reason",
+    "excludedFile": "File to remove. Click to see the list of excluded files",
+    "ignoredFile": "File to not ignore or remove. Click to see the list of ignored files",
+    "inError": "In error. File to resume or remove",
     "pending": "Processing",
     "ready": "No action to do"
   },
@@ -1077,12 +1086,12 @@
           "fileName": "File name",
           "location": "Location",
           "mimeType": "MimeType",
-          "puid": "Puid",
+          "puid": "PRONOM ID",
           "smartSize": "Size",
           "tool": {
             "description": "Tool description",
             "name": "Tool name",
-            "puid": "Tool Puid",
+            "puid": "PRONOM ID Tool",
             "version": "Tool version"
           },
           "type": "Type",
@@ -1104,16 +1113,16 @@
       "dialog": {
         "excludedIgnoredFile": {
           "categeroy": {
-            "files": "Files name",
-            "puids": "Puids"
+            "files": "Filepath pattern",
+            "puids": "PRONOM IDs"
           },
           "message": {
             "excluded": "This file is excluded because it falls within one of the criteria defined below :",
             "ignored": "This file is ignored because it falls within one of the criteria defined below :"
           },
           "title": {
-            "excluded": "Reason for file exclusion",
-            "ignored": "Reason for ignoring the file"
+            "excluded": "List of excluded files",
+            "ignored": "List of ignored files"
           }
         }
       },
diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json
index b8a64afa4..bf005cd84 100644
--- a/src/assets/i18n/fr.json
+++ b/src/assets/i18n/fr.json
@@ -682,6 +682,15 @@
   },
   "app": {
     "aip": {
+      "notFound": {
+        "button": {
+          "backToStoragionList": "Retour à la liste des noeuds de stockage"
+        },
+        "notPresentOnNode": "L'AIP est non présent sur le noeud de stockage {{storagionNumber}}.",
+        "seeOnOtherNodes": "Vous pouvez consulter le détail sur un autre noeud de stockage :",
+        "storagionItem": "Noeud de stockage {{storagionNumber}}",
+        "title": "AIP absent du noeud de stockage"
+      },
       "storage": "Storagion {{number}}",
       "tabs": {
         "all": "Tous",
@@ -971,8 +980,8 @@
   },
   "dataFileQuickStatus": {
     "cleaned": "Fichier nettoyé",
-    "excludedFile": "Fichier à supprimer. Cliquer pour voir la raison",
-    "ignoredFile": "Fichier à ne pas ignorer ou supprimer. Cliquer pour voir la raison",
+    "excludedFile": "Fichier à supprimer. Cliquer pour voir la liste de fichiers exclus",
+    "ignoredFile": "Fichier à ne pas ignorer ou supprimer. Cliquer pour voir la liste de fichiers ignorés",
     "inError": "En erreur de traitement. Fichier à relancer ou supprimer",
     "pending": "En cours de traitement",
     "ready": "Aucune action à effectuer"
@@ -1077,12 +1086,12 @@
           "fileName": "Nom du fichier",
           "location": "Location",
           "mimeType": "MimeType",
-          "puid": "Puid",
+          "puid": "PRONOM ID",
           "smartSize": "Taille",
           "tool": {
             "description": "Description",
             "name": "Nom d'outil",
-            "puid": "Puid d'outil",
+            "puid": "PRONOM ID de l'outil",
             "version": "Version"
           },
           "type": "Type",
@@ -1104,16 +1113,16 @@
       "dialog": {
         "excludedIgnoredFile": {
           "categeroy": {
-            "files": "Nom de fichiers",
-            "puids": "Puids"
+            "files": "Modèle de chemin de fichier",
+            "puids": "PRONOM IDs"
           },
           "message": {
             "excluded": "Ce fichier est exclu car il entre dans un des critères définis ci dessous :",
             "ignored": "Ce fichier est ignoré car il entre dans un des critères définis ci dessous :"
           },
           "title": {
-            "excluded": "Motif de l'exclusion du fichier",
-            "ignored": "Motif de l'ignorance du fichier"
+            "excluded": "Liste des fichiers exclus",
+            "ignored": "Liste des fichiers ignorés"
           }
         }
       },
-- 
GitLab