Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show correlated cases in case details #428

Merged
merged 23 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Indice.Features.Cases.App/cases-api.nswag
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"runtime": "Net70",
"runtime": "Net80",
"defaultVariables": "",
"documentGenerator": {
"fromDocument": {
Expand Down
3,220 changes: 2,709 additions & 511 deletions src/Indice.Features.Cases.App/src/app/core/services/cases-api.service.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
</app-case-discard-draft>
</div>
</div>

<app-case-related class="bg-white shadow rounded-sm mt-2 px-4 py-1 flow-root" *ngIf="relatedCases$ | async as relatedCases" [relatedCases]="relatedCases"></app-case-related>

<div class="bg-white shadow rounded-sm">
<ng-container *ngIf="timelineEntries$ | async as timelineEntries">
<div class="mt-2 px-4 py-1 flow-root">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ToasterService, ToastType } from '@indice/ng-components';
import { iif, Observable, ReplaySubject, of } from 'rxjs';
import { map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { filter, map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { CaseDetailsService } from 'src/app/core/services/case-details.service';
import { CaseActions, Case, CasesApiService, ActionRequest, TimelineEntry, CaseStatus, SuccessMessage } from 'src/app/core/services/cases-api.service';
import { CaseActions, Case, CasesApiService, ActionRequest, TimelineEntry, CaseStatus, SuccessMessage, CasePartial } from 'src/app/core/services/cases-api.service';

@Component({
selector: 'app-case-detail-page',
Expand All @@ -19,6 +19,8 @@ export class CaseDetailPageComponent implements OnInit, OnDestroy {

public timelineEntries$: Observable<TimelineEntry[]> | undefined;

public relatedCases$: Observable<CasePartial[]> | undefined;

public formValid: boolean = false;
public formUnSavedChanges: boolean = false;

Expand Down Expand Up @@ -49,6 +51,7 @@ export class CaseDetailPageComponent implements OnInit, OnDestroy {
this.requestModel();
this.getCaseActions();
this.getTimeline();
this.getRelatedCases()
});
}

Expand All @@ -71,25 +74,24 @@ export class CaseDetailPageComponent implements OnInit, OnDestroy {
}

public requestModel(): void {
this.api
this.model$ = this.api
.getCaseById(this.caseId)
.pipe(
switchMap(caseDetails => {
return iif(
switchMap(caseDetails =>
iif(
() => caseDetails.draft === true,
this.getCustomerData$(caseDetails), // draft mode, we need to prefill the form with customer data (if any)
of(caseDetails))
}),
this.getCustomerData$(caseDetails), // In draft mode we must prefill the form data
of(caseDetails)
)
),
tap((response: Case) => {
this.caseTypeConfig = response.caseType?.config ? JSON.parse(response.caseType?.config) : {};
this.caseDetailsService.setCaseDetails(response);
// ensure that we have the correct "latest" caseDetails!
this.model$ = this.caseDetailsService.caseDetails$;
}),
takeUntil(this.componentDestroy$)
).subscribe();

);
}

private getCustomerData$(caseDetails: Case): Observable<Case> {
return this.api
.getCustomerData(caseDetails.customerId ?? "", caseDetails.caseType?.code ?? "")
Expand All @@ -112,10 +114,10 @@ export class CaseDetailPageComponent implements OnInit, OnDestroy {
}

/**
* Event for PDF print action,
* Event for PDF print action,
* registers the state of the PDF print action
* @param printed
* @returns
* @param printed
* @returns
*/
onPdfButtonClicked(printed: boolean | undefined) {
if (printed === undefined) {
Expand Down Expand Up @@ -170,4 +172,11 @@ export class CaseDetailPageComponent implements OnInit, OnDestroy {
this.timelineEntries$ = this.api.getCaseTimeline(this.caseId!);
}

}
private getRelatedCases(): void {
this.relatedCases$ = this.model$?.pipe(
filter(model => !!model.metadata && !!model.metadata['ExternalCorrelationKey']), // Check if ExternalCorrelationKey has value
switchMap(() => this.api.getRelatedCases(this.caseId)),
takeUntil(this.componentDestroy$)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<div class="mt-auto pt-1 pl-1">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4"> Σχετικές Υποθέσεις </h3>
<ul>
<li *ngFor="let relatedCase of relatedCases" class="flex flex-row items-center p-1">
<div class="text-xs font-semibold px-2.5 py-0.5 bg-blue-100 text-blue-800 dark:bg-blue-200 dark:text-blue-800 w-32 text-center">
{{ relatedCase.caseType?.code }}
</div>
<div class="ml-2 text-xs font-semibold px-2.5 py-0.5 bg-blue-100 text-blue-800 dark:bg-blue-200 dark:text-blue-800 w-32 text-center">
{{ relatedCase.checkpointType?.code }}
</div>
<div class="flex-1">
<ng-container *ngIf="relatedCase.id === currentCaseId; else clickableLink">
<div class="ml-4">{{ relatedCase.createdByWhen | toReadableDate }}</div>
</ng-container>
<ng-template #clickableLink>
<a [routerLink]="['/cases', relatedCase.id, 'details']"
class="ml-4 text-blue-600 hover:text-blue-800 font-semibold hover:underline">
{{ relatedCase.createdByWhen | toReadableDate }}
</a>
</ng-template>
</div>
</li>
</ul>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CasePartial } from 'src/app/core/services/cases-api.service';

@Component({
selector: 'app-case-related',
templateUrl: './related-cases.component.html'
})
export class RelatedCasesComponent implements OnInit {

@Input() relatedCases: CasePartial[] = [];
currentCaseId: string = "";

constructor(private route: ActivatedRoute) { }

ngOnInit(): void {
// From the related cases, remove the currently opened case from the list
this.route.paramMap.subscribe(params => {
this.currentCaseId = params.get('caseId') ?? "";
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import { QueriesModalComponent } from "src/app/shared/components/query-modal/que
import { QueriesPageComponent } from "./queries-page/queries-page.component";
import { GeneralCasesComponent } from "./general-cases/general-cases.component";
import { CaseTypeSpecificCasesComponent } from "./case-type-specific-cases/case-type-specific-cases.component";
import { ValueFromPathPipe } from "src/app/shared/pipes.services";
import { RelatedCasesComponent } from "./case-detail-page/related-cases/related-cases.component";
import { ToReadableDatePipe, ValueFromPathPipe } from "src/app/shared/pipes.services";

@NgModule({
declarations: [
Expand All @@ -36,7 +37,8 @@ import { ValueFromPathPipe } from "src/app/shared/pipes.services";
CaseWarningModalComponent,
QueriesModalComponent,
GeneralCasesComponent,
CaseTypeSpecificCasesComponent
CaseTypeSpecificCasesComponent,
RelatedCasesComponent
],
imports: [
BrowserModule,
Expand All @@ -55,7 +57,8 @@ import { ValueFromPathPipe } from "src/app/shared/pipes.services";
CaseDetailPageComponent
],
providers: [
ValueFromPathPipe
ValueFromPathPipe,
ToReadableDatePipe
]
})
export class CasesModule { }
11 changes: 11 additions & 0 deletions src/Indice.Features.Cases.App/src/app/shared/pipes.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,14 @@ export class ValueFromPathPipe implements PipeTransform {
return givenObject;
}
}

@Pipe({
name: 'toReadableDate',
pure: true
})
export class ToReadableDatePipe implements PipeTransform {
transform(value: any): string {
return new Date(value).toLocaleString('en-GB');
}
}

9 changes: 5 additions & 4 deletions src/Indice.Features.Cases.App/src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { HttpClientModule } from '@angular/common/http';
import { ApprovalButtonsComponent } from './components/approval-buttons/approval-buttons.component';
import { PageIllustrationComponent } from './components/page-illustration/page-illustration.component';
import { RadioButtonsListComponent } from './components/radio-buttons-list/radio-buttons-list.component';
import { BeautifyBooleanPipe } from './pipes.services';
import { TailwindFrameworkComponent } from './ajsf/json-schema-frameworks/tailwind-framework/tailwind-framework.component';
import { JsonSchemaFormModule } from '@ajsf-extended/core';
import { SubmitWidgetComponent } from './ajsf/json-schema-frameworks/tailwind-framework/submit-widget/submit-widget.component';
Expand Down Expand Up @@ -31,7 +30,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { NgModule } from '@angular/core';
import { QuillModule } from 'ngx-quill';
import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask';
import { ValueFromPathPipe } from './pipes.services';
import { BeautifyBooleanPipe, ValueFromPathPipe, ToReadableDatePipe } from './pipes.services';

@NgModule({
declarations: [
Expand Down Expand Up @@ -62,7 +61,8 @@ import { ValueFromPathPipe } from './pipes.services';
LabelOnlyWidgetComponent,
// pipes
BeautifyBooleanPipe,
ValueFromPathPipe
ValueFromPathPipe,
ToReadableDatePipe
],
imports: [
CommonModule,
Expand Down Expand Up @@ -101,7 +101,8 @@ import { ValueFromPathPipe } from './pipes.services';
// pipes
BeautifyBooleanPipe,
TranslateModule,
ValueFromPathPipe
ValueFromPathPipe,
ToReadableDatePipe
],
providers: [
provideNgxMask()
Expand Down
5 changes: 5 additions & 0 deletions src/Indice.Features.Cases.AspNetCore/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [WIP]
### Added
- Cases can now be related to other cases, the relation is based on the assigned `metadata.ExternalRelationKey`
- Now showing table with the related cases in the case details page

## [7.39.1] - 2024-11-08
### Fixed
- Calling `GetAttachmentByField` while a property is filled with an empty string will no longer be problematic.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,22 @@ public async Task<IActionResult> GetCaseTimeline([FromRoute] Guid caseId) {
return Ok(timeline);
}

/// <summary>
/// Gets the cases that are related to the given id.
/// Set a value to the case's metadata with the key ExternalCorrelationKey to correlate cases.
/// </summary>
/// <param name="caseId">The id of the case.</param>
/// <response code="200">OK</response>
/// <response code="404">Not Found</response>
[HttpGet("{caseId:guid}/related-cases")]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<Guid>))]
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ProblemDetails))]
public async Task<IActionResult> GetRelatedCases([FromRoute] Guid caseId) {
var relatedCases = await _adminCaseService.GetRelatedCases(User, caseId);
return Ok(relatedCases);
}

/// <summary>Gets the cases actions (Approval, edit, assignments, etc) for a case Id. Actions differ based on user role.</summary>
/// <param name="caseId">The id of the case.</param>
/// <response code="200">OK</response>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ public interface IAdminCaseService
/// <returns></returns>
Task<IEnumerable<TimelineEntry>> GetTimeline(ClaimsPrincipal user, Guid caseId);

/// <summary>
/// Gets the cases that are related to the given id.
/// Set a value to the case's metadata with the key ExternalCorrelationKey to correlate cases.
/// </summary>
/// <param name="user">The user that creates the request.</param>
/// <param name="caseId">The Id of the case.</param>
/// <returns></returns>
Task<List<CasePartial>> GetRelatedCases(ClaimsPrincipal user, Guid caseId);

/// <summary>Get a list of attachments by CaseId</summary>
/// <param name="caseId"></param>
/// <returns></returns>
Expand Down
12 changes: 12 additions & 0 deletions src/Indice.Features.Cases.AspNetCore/Services/AdminCaseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,18 @@ public async Task<IEnumerable<TimelineEntry>> GetTimeline(ClaimsPrincipal user,
return timeline;
}

public async Task<List<CasePartial>> GetRelatedCases(ClaimsPrincipal user, Guid caseId) {
// Check that user role can view this case
var @case = await GetCaseById(user, caseId, false);
var result = await GetCases(user, new ListOptions<GetCasesListFilter>() {
Filter = new GetCasesListFilter {
Metadata = [new FilterClause("metadata.ExternalCorrelationKey", @case.Metadata["ExternalCorrelationKey"], FilterOperator.Eq, JsonDataType.String)]
}
});

return result.Items.OrderByDescending(x=> x.CreatedByWhen).ToList();
}

private async Task<List<FilterClause>> MapCheckpointTypeCodeToId(List<FilterClause> checkpointTypeCodeFilterClauses) {
var checkpointTypeCodes = checkpointTypeCodeFilterClauses.Select(f => f.Value).ToList();
var checkpointTypeIds = new List<FilterClause>();
Expand Down