From 564fee9e906ad88b895b821d7a0b0e9d9f615cf6 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Fri, 23 Sep 2022 22:42:01 -0700 Subject: [PATCH] added athena health client. fixed header. added a logout/signout function. --- backend/pkg/constants.go | 1 + backend/pkg/hub/factory.go | 3 + .../pkg/hub/internal/fhir/athena/client.go | 69 +++ backend/pkg/web/handler/metadata.go | 1 + backend/pkg/web/handler/source.go | 64 ++- backend/pkg/web/server.go | 1 + .../components/header/header.component.html | 13 +- .../app/components/header/header.component.ts | 9 +- .../resource-list/resource-list.component.ts | 5 +- .../medical-sources.component.html | 32 +- .../medical-sources.component.ts | 82 +++- .../src/app/services/fasten-api.service.ts | 10 + frontend/src/assets/logo/logo-text.png | Bin 0 -> 7083 bytes frontend/src/assets/logo/logo2-text.png | Bin 0 -> 9585 bytes frontend/src/assets/logo/logo3-text.png | Bin 0 -> 3329 bytes frontend/src/assets/scss/custom/_modal.scss | 437 +++++++++++++++++- frontend/src/assets/sources/athena.png | Bin 0 -> 4063 bytes frontend/src/styles.scss | 3 + 18 files changed, 672 insertions(+), 58 deletions(-) create mode 100644 backend/pkg/hub/internal/fhir/athena/client.go create mode 100644 frontend/src/assets/logo/logo-text.png create mode 100644 frontend/src/assets/logo/logo2-text.png create mode 100644 frontend/src/assets/logo/logo3-text.png create mode 100644 frontend/src/assets/sources/athena.png diff --git a/backend/pkg/constants.go b/backend/pkg/constants.go index 38a4011e..4a1f26e4 100644 --- a/backend/pkg/constants.go +++ b/backend/pkg/constants.go @@ -7,6 +7,7 @@ const ( SourceTypeManual SourceType = "manual" SourceTypeAetna SourceType = "aetna" + SourceTypeAthena SourceType = "athena" SourceTypeAnthem SourceType = "anthem" SourceTypeCedarSinai SourceType = "cedarssinai" SourceTypeCerner SourceType = "cerner" diff --git a/backend/pkg/hub/factory.go b/backend/pkg/hub/factory.go index 681a6ea3..b3630c41 100644 --- a/backend/pkg/hub/factory.go +++ b/backend/pkg/hub/factory.go @@ -7,6 +7,7 @@ import ( "github.com/fastenhealth/fastenhealth-onprem/backend/pkg" "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config" "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/aetna" + "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/athena" "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/base" "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/cerner" "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/cigna" @@ -25,6 +26,8 @@ func NewClient(sourceType pkg.SourceType, ctx context.Context, appConfig config. switch sourceType { case pkg.SourceTypeAetna: sourceClient, updatedSource, err = aetna.NewClient(ctx, appConfig, globalLogger, credentials, testHttpClient...) + case pkg.SourceTypeAthena: + sourceClient, updatedSource, err = athena.NewClient(ctx, appConfig, globalLogger, credentials, testHttpClient...) case pkg.SourceTypeAnthem: sourceClient, updatedSource, err = cigna.NewClient(ctx, appConfig, globalLogger, credentials, testHttpClient...) case pkg.SourceTypeCerner: diff --git a/backend/pkg/hub/internal/fhir/athena/client.go b/backend/pkg/hub/internal/fhir/athena/client.go new file mode 100644 index 00000000..cf332aab --- /dev/null +++ b/backend/pkg/hub/internal/fhir/athena/client.go @@ -0,0 +1,69 @@ +package athena + +import ( + "context" + "fmt" + "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config" + "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/database" + "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/base" + "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models" + "github.com/sirupsen/logrus" + "net/http" +) + +type AthenaClient struct { + *base.FHIR401Client +} + +func NewClient(ctx context.Context, appConfig config.Interface, globalLogger logrus.FieldLogger, source models.Source, testHttpClient ...*http.Client) (base.Client, *models.Source, error) { + baseClient, updatedSource, err := base.NewFHIR401Client(ctx, appConfig, globalLogger, source, testHttpClient...) + return AthenaClient{ + baseClient, + }, updatedSource, err +} + +func (c AthenaClient) SyncAll(db database.DatabaseRepository) error { + + supportedResources := []string{ + "AllergyIntolerance", + "CarePlan", + "CareTeam", + "Condition", + "Device", + "DiagnosticReport", + "DocumentReference", + "Encounter", + "Goal", + "Immunization", + "Location", + "Medication", + "MedicationRequest", + "Observation", + "Organization", + "Patient", + "Practitioner", + "Procedure", + "Provenance", + } + for _, resourceType := range supportedResources { + bundle, err := c.GetResourceBundle(fmt.Sprintf("%s?patient=%s", resourceType, c.Source.PatientId)) + if err != nil { + return err + } + wrappedResourceModels, err := c.ProcessBundle(bundle) + if err != nil { + c.Logger.Infof("An error occurred while processing %s bundle %s", resourceType, c.Source.PatientId) + return err + } + //todo, create the resources in dependency order + + for _, apiModel := range wrappedResourceModels { + err = db.UpsertResource(context.Background(), apiModel) + if err != nil { + return err + } + } + } + return nil + +} diff --git a/backend/pkg/web/handler/metadata.go b/backend/pkg/web/handler/metadata.go index 1e7e88bc..952a7e99 100644 --- a/backend/pkg/web/handler/metadata.go +++ b/backend/pkg/web/handler/metadata.go @@ -13,6 +13,7 @@ func GetMetadataSource(c *gin.Context) { string(pkg.SourceTypeLogica): {Display: "Logica (Sandbox)", SourceType: pkg.SourceTypeLogica, Category: []string{"Sandbox"}, Supported: true}, string(pkg.SourceTypeEpic): {Display: "Epic (Sandbox)", SourceType: pkg.SourceTypeEpic, Category: []string{"Sandbox"}, Supported: true}, string(pkg.SourceTypeCerner): {Display: "Cerner (Sandbox)", SourceType: pkg.SourceTypeCerner, Category: []string{"Sandbox"}, Supported: true}, + string(pkg.SourceTypeAthena): {Display: "Athena (Sandbox)", SourceType: pkg.SourceTypeAthena, Category: []string{"Sandbox"}, Supported: true}, // enabled string(pkg.SourceTypeAetna): {Display: "Aetna", SourceType: pkg.SourceTypeAetna, Category: []string{"Insurance"}, Supported: true}, diff --git a/backend/pkg/web/handler/source.go b/backend/pkg/web/handler/source.go index 9574da55..eb6eead7 100644 --- a/backend/pkg/web/handler/source.go +++ b/backend/pkg/web/handler/source.go @@ -35,24 +35,8 @@ func CreateSource(c *gin.Context) { } // after creating the source, we should do a bulk import - sourceClient, updatedSource, err := hub.NewClient(sourceCred.SourceType, c, nil, logger, sourceCred) + err = syncSourceResources(c, logger, databaseRepo, sourceCred) if err != nil { - logger.Errorln("An error occurred while initializing hub client using source credential", err) - c.JSON(http.StatusInternalServerError, gin.H{"success": false}) - return - } - if updatedSource != nil { - err := databaseRepo.CreateSource(c, updatedSource) - if err != nil { - logger.Errorln("An error occurred while updating source credential", err) - c.JSON(http.StatusInternalServerError, gin.H{"success": false}) - return - } - } - - err = sourceClient.SyncAll(databaseRepo) - if err != nil { - logger.Errorln("An error occurred while bulk import of resources from source", err) c.JSON(http.StatusInternalServerError, gin.H{"success": false}) return } @@ -60,6 +44,28 @@ func CreateSource(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true, "data": sourceCred}) } +func SourceSync(c *gin.Context) { + logger := c.MustGet("LOGGER").(*logrus.Entry) + databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository) + + logger.Infof("Get Source Credentials: %v", c.Param("sourceId")) + + sourceCred, err := databaseRepo.GetSource(c, c.Param("sourceId")) + if err != nil { + logger.Errorln("An error occurred while retrieving source credential", err) + c.JSON(http.StatusInternalServerError, gin.H{"success": false}) + return + } + + // after creating the source, we should do a bulk import + err = syncSourceResources(c, logger, databaseRepo, *sourceCred) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": sourceCred}) +} + func CreateManualSource(c *gin.Context) { logger := c.MustGet("LOGGER").(*logrus.Entry) databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository) @@ -200,3 +206,27 @@ func RawRequestSource(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"success": true, "data": resp}) } + +////// private functions +func syncSourceResources(c *gin.Context, logger *logrus.Entry, databaseRepo database.DatabaseRepository, sourceCred models.Source) error { + // after creating the source, we should do a bulk import + sourceClient, updatedSource, err := hub.NewClient(sourceCred.SourceType, c, nil, logger, sourceCred) + if err != nil { + logger.Errorln("An error occurred while initializing hub client using source credential", err) + return err + } + if updatedSource != nil { + err := databaseRepo.CreateSource(c, updatedSource) + if err != nil { + logger.Errorln("An error occurred while updating source credential", err) + return err + } + } + + err = sourceClient.SyncAll(databaseRepo) + if err != nil { + logger.Errorln("An error occurred while bulk import of resources from source", err) + return err + } + return nil +} diff --git a/backend/pkg/web/server.go b/backend/pkg/web/server.go index bd030159..ebc4cc4f 100644 --- a/backend/pkg/web/server.go +++ b/backend/pkg/web/server.go @@ -51,6 +51,7 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine { secure.POST("/source/manual", handler.CreateManualSource) secure.GET("/source", handler.ListSource) secure.GET("/source/:sourceId", handler.GetSource) + secure.POST("/source/:sourceId/sync", handler.SourceSync) secure.GET("/source/:sourceId/summary", handler.GetSourceSummary) secure.GET("/resource/fhir", handler.ListResourceFhir) // secure.GET("/resource/fhir/:resourceId", handler.GetResourceFhir) diff --git a/frontend/src/app/components/header/header.component.html b/frontend/src/app/components/header/header.component.html index 3c97965f..4fe54e6e 100644 --- a/frontend/src/app/components/header/header.component.html +++ b/frontend/src/app/components/header/header.component.html @@ -65,24 +65,21 @@ diff --git a/frontend/src/app/components/header/header.component.ts b/frontend/src/app/components/header/header.component.ts index b531ca97..5aaedbd9 100644 --- a/frontend/src/app/components/header/header.component.ts +++ b/frontend/src/app/components/header/header.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit } from '@angular/core'; - +import {FastenApiService} from '../../services/fasten-api.service'; +import { Router } from '@angular/router'; @Component({ selector: 'app-header', templateUrl: './header.component.html', @@ -7,7 +8,7 @@ import { Component, OnInit } from '@angular/core'; }) export class HeaderComponent implements OnInit { - constructor() { } + constructor(private fastenApi: FastenApiService, private router: Router) { } ngOnInit() { } @@ -22,4 +23,8 @@ export class HeaderComponent implements OnInit { document.querySelector('body').classList.toggle('az-header-menu-show'); } + signOut(e) { + this.fastenApi.logout() + this.router.navigate(['auth/signin']); + } } diff --git a/frontend/src/app/components/resource-list/resource-list.component.ts b/frontend/src/app/components/resource-list/resource-list.component.ts index 506c6a0b..bb3a3ad6 100644 --- a/frontend/src/app/components/resource-list/resource-list.component.ts +++ b/frontend/src/app/components/resource-list/resource-list.component.ts @@ -76,9 +76,8 @@ export class ResourceListComponent implements OnInit, OnChanges { getResources(): Observable{ - if(!this.resourceListCache[this.resourceListType]){ + if(this.resourceListType && !this.resourceListCache[this.resourceListType]){ // this resource type list has not been downloaded yet, do so now - return this.fastenApi.getResources(this.resourceListType, this.source.id) .pipe(map((resourceList: ResourceFhir[]) => { //cache this response so we can skip the request next time @@ -86,7 +85,7 @@ export class ResourceListComponent implements OnInit, OnChanges { return resourceList })) } else { - return of(this.resourceListCache[this.resourceListType]) + return of(this.resourceListCache[this.resourceListType] || []) } } diff --git a/frontend/src/app/pages/medical-sources/medical-sources.component.html b/frontend/src/app/pages/medical-sources/medical-sources.component.html index 2dc638b3..def5a993 100644 --- a/frontend/src/app/pages/medical-sources/medical-sources.component.html +++ b/frontend/src/app/pages/medical-sources/medical-sources.component.html @@ -9,10 +9,10 @@

Connected Sources

-
+
-
- client +
+ client
@@ -62,3 +62,29 @@
+ + + + + + + + diff --git a/frontend/src/app/pages/medical-sources/medical-sources.component.ts b/frontend/src/app/pages/medical-sources/medical-sources.component.ts index 4e974cf7..f80ed1a1 100644 --- a/frontend/src/app/pages/medical-sources/medical-sources.component.ts +++ b/frontend/src/app/pages/medical-sources/medical-sources.component.ts @@ -11,6 +11,7 @@ import {Observable, of, throwError} from 'rxjs'; import {concatMap, delay, retryWhen} from 'rxjs/operators'; import * as FHIR from "fhirclient" import {MetadataSource} from '../../models/fasten/metadata-source'; +import {NgbModal, ModalDismissReasons} from '@ng-bootstrap/ng-bootstrap'; export const retryCount = 24; //wait 2 minutes (5 * 24 = 120) export const retryWaitMilliSeconds = 5000; //wait 5 seconds @@ -25,16 +26,20 @@ export class MedicalSourcesComponent implements OnInit { constructor( private lighthouseApi: LighthouseService, private fastenApi: FastenApiService, + private modalService: NgbModal, ) { } status: { [name: string]: string } = {} metadataSources: {[name:string]: MetadataSource} = {} - connectedSourceList = [] - availableSourceList = [] + connectedSourceList:any[] = [] + availableSourceList:MetadataSource[] = [] uploadedFile: File[] = [] + closeResult = ''; + modalSourceInfo:any = null; + ngOnInit(): void { this.fastenApi.getMetadataSources().subscribe((metadataSources: {[name:string]: MetadataSource}) => { this.metadataSources = metadataSources @@ -46,7 +51,7 @@ export class MedicalSourcesComponent implements OnInit { let isConnected = false for(const connectedSource of sourceList){ if(connectedSource.source_type == sourceType){ - this.connectedSourceList.push({"source_type": sourceType, "display": this.metadataSources[sourceType]["display"], "enabled": this.metadataSources[sourceType]["enabled"]}) + this.connectedSourceList.push({source: connectedSource, metadata: this.metadataSources[sourceType]}) isConnected = true break } @@ -54,7 +59,7 @@ export class MedicalSourcesComponent implements OnInit { if(!isConnected){ //this source has not been found in the connected list, lets add it to the available list. - this.availableSourceList.push({"source_type": sourceType, "display": this.metadataSources[sourceType]["display"], "enabled": this.metadataSources[sourceType]["enabled"]}) + this.availableSourceList.push(this.metadataSources[sourceType]) } } @@ -141,6 +146,7 @@ export class MedicalSourcesComponent implements OnInit { } //Create FHIR Client + const sourceCredential: Source = { source_type: sourceType, oauth_authorization_endpoint: connectData.oauth_authorization_endpoint, @@ -156,9 +162,12 @@ export class MedicalSourcesComponent implements OnInit { access_token: payload.access_token, refresh_token: payload.refresh_token, id_token: payload.id_token, - expires_at: getAccessTokenExpiration(payload, new BrowserAdapter()), code_challenge: codeChallenge, code_verifier: codeVerifier, + + // @ts-ignore - in some cases the getAccessTokenExpiration is a string, which cases failures to store Source in db. + expires_at: parseInt(getAccessTokenExpiration(payload, new BrowserAdapter())), + } await this.fastenApi.createSource(sourceCredential).subscribe( @@ -189,13 +198,54 @@ export class MedicalSourcesComponent implements OnInit { // @ts-ignore event.origin); }, 5000); - - } + uploadSourceBundle(event) { + this.uploadedFile = [event.addedFiles[0]] + this.fastenApi.createManualSource(event.addedFiles[0]).subscribe( + (respData) => { + console.log("source manual source create response:", respData) + }, + (err) => {console.log(err)}, + () => { + this.uploadedFile = [] + } + ) + } + openModal(contentModalRef, sourceInfo: any) { + this.modalSourceInfo = sourceInfo + let modalSourceInfo = this.modalSourceInfo + this.modalService.open(contentModalRef, {ariaLabelledBy: 'modal-basic-title'}).result.then((result) => { + modalSourceInfo = null + this.closeResult = `Closed with: ${result}`; + }, (reason) => { + modalSourceInfo = null + this.closeResult = `Dismissed ${this.getDismissReason(reason)}`; + }); + } - waitForClaimOrTimeout(sourceType: string, state: string): Observable { + syncSource(source: Source){ + this.modalService.dismissAll() + this.fastenApi.syncSource(source.id).subscribe( + (respData) => { + console.log("source sync response:", respData) + }, + (err) => {console.log(err)}, + ) + } + + private getDismissReason(reason: any): string { + if (reason === ModalDismissReasons.ESC) { + return 'by pressing ESC'; + } else if (reason === ModalDismissReasons.BACKDROP_CLICK) { + return 'by clicking on a backdrop'; + } else { + return `with: ${reason}`; + } + } + + private waitForClaimOrTimeout(sourceType: string, state: string): Observable { return this.lighthouseApi.getSourceAuthorizeClaim(sourceType, state).pipe( retryWhen(error => error.pipe( @@ -211,21 +261,7 @@ export class MedicalSourcesComponent implements OnInit { ) } - - uploadSourceBundle(event) { - this.uploadedFile = [event.addedFiles[0]] - this.fastenApi.createManualSource(event.addedFiles[0]).subscribe( - (respData) => { - console.log("source manual source create response:", respData) - }, - (err) => {console.log(err)}, - () => { - this.uploadedFile = [] - } - ) - } - - uuidV4(){ + private uuidV4(){ // @ts-ignore return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) diff --git a/frontend/src/app/services/fasten-api.service.ts b/frontend/src/app/services/fasten-api.service.ts index 73e0fdcd..6e5b88ee 100644 --- a/frontend/src/app/services/fasten-api.service.ts +++ b/frontend/src/app/services/fasten-api.service.ts @@ -132,6 +132,16 @@ export class FastenApiService { ); } + syncSource(sourceId: string): Observable { + return this._httpClient.post(`${this.getBasePath()}/api/secure/source/${sourceId}/sync`, {}) + .pipe( + map((response: ResponseWrapper) => { + console.log("SOURCE RESPONSE", response) + return response.data + }) + ); + } + getResources(sourceResourceType?: string, sourceID?: string): Observable { let queryParams = {} if(sourceResourceType){ diff --git a/frontend/src/assets/logo/logo-text.png b/frontend/src/assets/logo/logo-text.png new file mode 100644 index 0000000000000000000000000000000000000000..e0b23668997676e8ca78bd0a94e184a359634504 GIT binary patch literal 7083 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 I{_nH@2i*au&j0`b literal 0 HcmV?d00001 diff --git a/frontend/src/assets/logo/logo3-text.png b/frontend/src/assets/logo/logo3-text.png new file mode 100644 index 0000000000000000000000000000000000000000..c0514ee8e8ee1c48fe06c0b81958ac0011827a42 GIT binary patch literal 3329 zcmeHJX;hO}8pc{z;M9sEA}CJBg&Efx1sp)?njo7Cn=GNA1U?|_2!>6HWrwgR0)b#9 zU?3Qf1`ydSsGu0Y078Nwjvx>S7J&fY_ucO{{GZdF)1NcF=e+0Lp69*qdCvWD6V6+Z zRo8D_ucV};YC<`EQAz0w$+E3osi^25`f`QBtq8nGKDk2tVqcUZS?5Q&9H^wEthQ_` zlydX7DBx=6&8*Hq2xmXT(f`2yZsJ7?QTH<_F9O_rP?!x0GC@u%o|S;7-^7!n@Psfd zCJ4LXi-&n(*JxOvD@J$1JZ&+zi>QkQ>OjW4E@PA9aIl}~d_g>UL{xIY?RY#c1PgV? z0Dxm7u%nHrDg$|GcuWxPX@^Zop|F+Ub3ke|J~9lS))Sl@5F3KgtWXq%eIg<`5}%!c z;okVfN7($Bc=epfPQnpeR6Y-%H4d+bFs5gmwE z+y``f?EMgwj={{ExC0seB!hWO+{pwj$_375D2Bqy5^(hbN)YgEKM{&x88`7rU%a#c zcvz#YPY9=TXxTl0<1jN8pPz$4uGsKfDDEXBZwOaQR5l4cY|xosp`SChAcr|AcuhIL zF?jC|=;zTse;xv%vHe{4flsCLr}3+Ww4)yN=z@13jHYJOfLO z!n2ccaSsuE6)PzKcW(oRC+6#f<)?$zUkHC^Y*0+3MdNjq09}Ghi_npRii8A-wzSwmo+_duFZjs_Wfa zUF#2X+jKT|rP&J2+IE-Sq?qpL7Hzz=x2jg9@)a$2%zH6@SLLRW2seGq5ViovzCHR2 zmz%2ksO|*~3wg&BuklZv9+rVG@xzZ}d3EFU&rGRRbGU62O}t9|fV2PWmnG^A)u>u? zN!?;bpA$oTuf5!^%2mqY2rm!6@<~j>AG$ zO(PxTSna3pelt*BJGIT7I{&o=CHsix=6#GY4{K9a!Yn@}sdN0Py~M0C#JLW+TUIdU z$V#ZV-X6Tyl07-$*Hb(+|LIs%e_4_6%YH^n2E8S2KRwa>U1+!d<1F1Ge*IFz6{)>s zQ*Ky0_FTPMP!cei;eX9&wAI+N9#+e)92mI3`oiG2ANHsvOIIcY!*S2Gs|ARxMSIC3 zsiMf-!JuZJubQGFO=VWCcaR(reE1-1mVeL|PC53E@5FS-bV>F?La~b}>+H0*R<+v3 z+-z!Zy*HV@)lNUl_^K((HL%rBIYm&S7iDWA7SUc^i>fU4Mo84K#O5sBEAu6FFJCHr z(83nq2*ZCid69Gtjyu^^7wTRi8NI1v6q(=dOkUjJf{=2iH5R{)6wwY((F0(i0HF^y zN-K+m`cu$5xL-umng))+eV0TumFc~UA@T&rN*51@axNcTOQy$Oiwf>Jn5MfD3lHul zo3LC456Q_D>~9bfH~4QgCpst+l?i2aD|Mi#aMqN@L?#F{x?*~bv`{#r5VOs}}A9_|qv(jv|cbZA&2)WXsf`vqm zF#eiPX=*4$Vy39JQER1_KQ_6BE)0L8I=%msCfi+l^B`)^f(39Z0E|zP65cN0d{b_b)6_`qSJ; z^%PO#BZf|XZTSUpr_EPIdQIM!du%@5YjO?xF&o!>x!4#;$GX zsU$)91FA_J(f^Ii*t(o^%Cd?QMx8(RZ-L;1c#=mJ6&mYNu!Ft-n0W21$pPQZQy7Ah z6B6a+4qa6gS;5%3O(MQKRRRxjZwyS zBk>Mdl#Qa(d6W`nshBobz8Zmb3XZ2g9~4N_=f>HqkuZ|lD!GDMyPP+viW-X*fXAE@ zlA$(>=oVGckb`1W6$MLG^topwaixlFHY3{H?Iuinj;v&6Q=0Bi9CIB+?VRLh#_xrJ zo5gi#&wj5od1d#a;m_jppy=?r+nr-TkOpxE*=}u@=24wS7=H31G)ZC&%Xe__uBqa=l?~WKlQ)+@1zYAQ9JzA<9j2v RA6f>OoUu5~Kl%L+zX4?@nB)Ke literal 0 HcmV?d00001 diff --git a/frontend/src/assets/scss/custom/_modal.scss b/frontend/src/assets/scss/custom/_modal.scss index a4f612fa..c8ea6e59 100755 --- a/frontend/src/assets/scss/custom/_modal.scss +++ b/frontend/src/assets/scss/custom/_modal.scss @@ -1,6 +1,439 @@ /* ###### 4.7 Modal ###### */ // MODAL EFFECTS -.modal { -} +.modal { + position: fixed; + top: 0; + left: 0; + z-index: 1055; + display: none; + width: 100%; + height: 100%; + overflow-x: hidden; + overflow-y: auto; + outline: 0; } + +.modal-dialog { + position: relative; + width: auto; + margin: 0.5rem; + pointer-events: none; } +.modal.fade .modal-dialog { + transition: transform 0.3s ease-out; + transform: translate(0, -50px); } +@media (prefers-reduced-motion: reduce) { + .modal.fade .modal-dialog { + transition: none; } } +.modal.show .modal-dialog { + transform: none; } +.modal.modal-static .modal-dialog { + transform: scale(1.02); } + +.modal-dialog-scrollable { + height: calc(100% - 1rem); } +.modal-dialog-scrollable .modal-content { + max-height: 100%; + overflow: hidden; } +.modal-dialog-scrollable .modal-body { + overflow-y: auto; } + +.modal-dialog-centered { + display: flex; + align-items: center; + min-height: calc(100% - 1rem); } + +.modal-content { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + pointer-events: auto; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; + outline: 0; } + +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + z-index: 1050; + width: 100vw; + height: 100vh; + background-color: #000; } +.modal-backdrop.fade { + opacity: 0; } +.modal-backdrop.show { + opacity: 0.5; } + +.modal-header { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: space-between; + padding: 1rem 1rem; + border-bottom: 1px solid #cdd4e0; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); } +.modal-header .btn-close { + padding: 0.5rem 0.5rem; + margin: -0.5rem -0.5rem -0.5rem auto; } + +.modal-title { + margin-bottom: 0; + line-height: 1.5; } + +.modal-body { + position: relative; + flex: 1 1 auto; + padding: 1rem; } + +.modal-footer { + display: flex; + flex-wrap: wrap; + flex-shrink: 0; + align-items: center; + justify-content: flex-end; + padding: 0.75rem; + border-top: 1px solid #cdd4e0; + border-bottom-right-radius: calc(0.3rem - 1px); + border-bottom-left-radius: calc(0.3rem - 1px); } +.modal-footer > * { + margin: 0.25rem; } + +@media (min-width: 576px) { + .modal-dialog { + max-width: 500px; + margin: 1.75rem auto; } + .modal-dialog-scrollable { + height: calc(100% - 3.5rem); } + .modal-dialog-centered { + min-height: calc(100% - 3.5rem); } + .modal-sm { + max-width: 300px; } } + +@media (min-width: 992px) { + .modal-lg, + .modal-xl { + max-width: 800px; } } + +@media (min-width: 1200px) { + .modal-xl { + max-width: 1140px; } } + +.modal-fullscreen { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; } +.modal-fullscreen .modal-content { + height: 100%; + border: 0; + border-radius: 0; } +.modal-fullscreen .modal-header { + border-radius: 0; } +.modal-fullscreen .modal-body { + overflow-y: auto; } +.modal-fullscreen .modal-footer { + border-radius: 0; } + +@media (max-width: 575.98px) { + .modal-fullscreen-sm-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; } + .modal-fullscreen-sm-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; } + .modal-fullscreen-sm-down .modal-header { + border-radius: 0; } + .modal-fullscreen-sm-down .modal-body { + overflow-y: auto; } + .modal-fullscreen-sm-down .modal-footer { + border-radius: 0; } } + +@media (max-width: 767.98px) { + .modal-fullscreen-md-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; } + .modal-fullscreen-md-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; } + .modal-fullscreen-md-down .modal-header { + border-radius: 0; } + .modal-fullscreen-md-down .modal-body { + overflow-y: auto; } + .modal-fullscreen-md-down .modal-footer { + border-radius: 0; } } + +@media (max-width: 991.98px) { + .modal-fullscreen-lg-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; } + .modal-fullscreen-lg-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; } + .modal-fullscreen-lg-down .modal-header { + border-radius: 0; } + .modal-fullscreen-lg-down .modal-body { + overflow-y: auto; } + .modal-fullscreen-lg-down .modal-footer { + border-radius: 0; } } + +@media (max-width: 1199.98px) { + .modal-fullscreen-xl-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; } + .modal-fullscreen-xl-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; } + .modal-fullscreen-xl-down .modal-header { + border-radius: 0; } + .modal-fullscreen-xl-down .modal-body { + overflow-y: auto; } + .modal-fullscreen-xl-down .modal-footer { + border-radius: 0; } } + +@media (max-width: 1399.98px) { + .modal-fullscreen-xxl-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; } + .modal-fullscreen-xxl-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; } + .modal-fullscreen-xxl-down .modal-header { + border-radius: 0; } + .modal-fullscreen-xxl-down .modal-body { + overflow-y: auto; } + .modal-fullscreen-xxl-down .modal-footer { + border-radius: 0; } } + +/* ###### 3.12 Modal ###### */ +.modal-backdrop { + background-color: #0c1019; } +.modal-backdrop.show { + opacity: .8; } + +.modal-content { + border-radius: 0; + border-width: 0; } +.modal-content .close { + font-size: 28px; + padding: 0; + margin: 0; + line-height: .5; + border: none; + box-shadow: none; + background: transparent; + float: right; } + +.modal-header { + align-items: center; + padding: 15px; } +@media (min-width: 576px) { + .modal-header { + padding: 15px 20px; } } +@media (min-width: 992px) { + .modal-header { + padding: 20px; } } +@media (min-width: 1200px) { + .modal-header { + padding: 20px 25px; } } +.modal-header .modal-title { + margin-bottom: 0; } + +.modal-title { + font-size: 18px; + font-weight: 700; + color: #1c273c; + line-height: 1; } + +.modal-body { + padding: 25px; } + + +/* ###### 4.7 Modal ###### */ +.modal.animated .modal-dialog { + transform: translate(0, 0); } + +.modal.effect-scale .modal-dialog { + transform: scale(0.7); + opacity: 0; + transition: all 0.3s; } + +.modal.effect-scale.show .modal-dialog { + transform: scale(1); + opacity: 1; } + +.modal.effect-slide-in-right .modal-dialog { + transform: translateX(20%); + opacity: 0; + transition: all 0.3s cubic-bezier(0.25, 0.5, 0.5, 0.9); } + +.modal.effect-slide-in-right.show .modal-dialog { + transform: translateX(0); + opacity: 1; } + +.modal.effect-slide-in-bottom .modal-dialog { + transform: translateY(20%); + opacity: 0; + transition: all 0.3s; } + +.modal.effect-slide-in-bottom.show .modal-dialog { + transform: translateY(0); + opacity: 1; } + +.modal.effect-newspaper .modal-dialog { + transform: scale(0) rotate(720deg); + opacity: 0; } + +.modal.effect-newspaper.show ~ .modal-backdrop, +.modal.effect-newspaper .modal-dialog { + transition: all 0.5s; } + +.modal.effect-newspaper.show .modal-dialog { + transform: scale(1) rotate(0deg); + opacity: 1; } + +.modal.effect-fall { + -webkit-perspective: 1300px; + -moz-perspective: 1300px; + perspective: 1300px; } +.modal.effect-fall .modal-dialog { + -moz-transform-style: preserve-3d; + transform-style: preserve-3d; + transform: translateZ(600px) rotateX(20deg); + opacity: 0; } +.modal.effect-fall.show .modal-dialog { + transition: all 0.3s ease-in; + transform: translateZ(0px) rotateX(0deg); + opacity: 1; } + +.modal.effect-flip-horizontal { + perspective: 1300px; } +.modal.effect-flip-horizontal .modal-dialog { + -moz-transform-style: preserve-3d; + transform-style: preserve-3d; + transform: rotateY(-70deg); + transition: all 0.3s; + opacity: 0; } +.modal.effect-flip-horizontal.show .modal-dialog { + transform: rotateY(0deg); + opacity: 1; } + +.modal.effect-flip-vertical { + perspective: 1300px; } +.modal.effect-flip-vertical .modal-dialog { + -moz-transform-style: preserve-3d; + transform-style: preserve-3d; + transform: rotateX(-70deg); + transition: all 0.3s; + opacity: 0; } +.modal.effect-flip-vertical.show .modal-dialog { + transform: rotateX(0deg); + opacity: 1; } + +.modal.effect-super-scaled .modal-dialog { + transform: scale(2); + opacity: 0; + transition: all 0.3s; } + +.modal.effect-super-scaled.show .modal-dialog { + transform: scale(1); + opacity: 1; } + +.modal.effect-sign { + perspective: 1300px; } +.modal.effect-sign .modal-dialog { + -moz-transform-style: preserve-3d; + transform-style: preserve-3d; + transform: rotateX(-60deg); + transform-origin: 50% 0; + opacity: 0; + transition: all 0.3s; } +.modal.effect-sign.show .modal-dialog { + transform: rotateX(0deg); + opacity: 1; } + +.modal.effect-rotate-bottom { + perspective: 1300px; } +.modal.effect-rotate-bottom .modal-dialog { + -moz-transform-style: preserve-3d; + transform-style: preserve-3d; + transform: translateY(100%) rotateX(90deg); + transform-origin: 0 100%; + opacity: 0; + transition: all 0.3s ease-out; } +.modal.effect-rotate-bottom.show .modal-dialog { + transform: translateY(0%) rotateX(0deg); + opacity: 1; } + +.modal.effect-rotate-left { + perspective: 1300px; } +.modal.effect-rotate-left .modal-dialog { + -moz-transform-style: preserve-3d; + transform-style: preserve-3d; + transform: translateZ(100px) translateX(-30%) rotateY(90deg); + transform-origin: 0 100%; + opacity: 0; + transition: all 0.3s; } +.modal.effect-rotate-left.show .modal-dialog { + transform: translateZ(0px) translateX(0%) rotateY(0deg); + opacity: 1; } + +.modal.effect-just-me .modal-dialog { + transform: scale(0.8); + opacity: 0; + transition: all 0.3s; } + +.modal.effect-just-me .modal-content { + background-color: transparent; } + +.modal.effect-just-me .close { + text-shadow: none; + color: #fff; } + +.modal.effect-just-me .modal-header { + background-color: transparent; + border-bottom-color: rgba(255, 255, 255, 0.1); + padding-left: 0; + padding-right: 0; } +.modal.effect-just-me .modal-header h6, .modal.effect-just-me .modal-header .h6 { + color: #fff; + font-weight: 500; } + +.modal.effect-just-me .modal-body { + color: rgba(255, 255, 255, 0.8); + padding-left: 0; + padding-right: 0; } +.modal.effect-just-me .modal-body h6, .modal.effect-just-me .modal-body .h6 { + color: #fff; } + +.modal.effect-just-me .modal-footer { + background-color: transparent; + padding-left: 0; + padding-right: 0; + border-top-color: rgba(255, 255, 255, 0.1); } + +.modal.effect-just-me.show ~ .modal-backdrop { + opacity: .96; } + +.modal.effect-just-me.show .modal-dialog { + transform: scale(1); + opacity: 1; } diff --git a/frontend/src/assets/sources/athena.png b/frontend/src/assets/sources/athena.png new file mode 100644 index 0000000000000000000000000000000000000000..cc5be2ae0063d637f91f3e16ea483e8aa4dc32bb GIT binary patch literal 4063 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 e`MJ1m5KU literal 0 HcmV?d00001 diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 4dc7769d..95c5f7c9 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -143,6 +143,7 @@ @import "./assets/scss/custom/variables"; @import "./assets/scss/custom/mixins"; + /* ########## BOOTSTRAP OVERRIDES ########## */ @import "./assets/scss/bootstrap/accordion"; @import "./assets/scss/bootstrap/alerts"; @@ -167,6 +168,7 @@ @import "./assets/scss/custom/image"; @import "./assets/scss/custom/list"; @import "./assets/scss/custom/nav"; +@import "./assets/scss/custom/modal"; /* ############### CUSTOM VENDOR STYLES ############### */ @import "./assets/scss/lib/select2"; @@ -228,3 +230,4 @@ @import '~@swimlane/ngx-datatable/index.css'; @import '~@swimlane/ngx-datatable/themes/bootstrap.css'; @import '~@swimlane/ngx-datatable/assets/icons.css'; +