Merge main
Picked up on most recent change with DeleteSource func
|
@ -1,17 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
|
||||||
<data-source source="LOCAL" name="fasten" uuid="d164a8cf-0ebf-4a73-b7a0-59097243fbce">
|
|
||||||
<driver-ref>sqlite.xerial</driver-ref>
|
|
||||||
<synchronize>true</synchronize>
|
|
||||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
|
||||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/fasten.db</jdbc-url>
|
|
||||||
<working-dir>$ProjectFileDir$</working-dir>
|
|
||||||
<libraries>
|
|
||||||
<library>
|
|
||||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.40.1/org/xerial/sqlite-jdbc/3.40.1.0/sqlite-jdbc-3.40.1.0.jar</url>
|
|
||||||
</library>
|
|
||||||
</libraries>
|
|
||||||
</data-source>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -21,7 +21,8 @@ var goos string
|
||||||
var goarch string
|
var goarch string
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
log.Print("Starting fasten-onprem")
|
||||||
|
defer log.Print("Finished fasten-onprem")
|
||||||
appconfig, err := config.Create()
|
appconfig, err := config.Create()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FATAL: %+v\n", err)
|
fmt.Printf("FATAL: %+v\n", err)
|
||||||
|
|
|
@ -884,6 +884,62 @@ func (gr *GormRepository) GetSources(ctx context.Context) ([]models.SourceCreden
|
||||||
return sourceCreds, results.Error
|
return sourceCreds, results.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gr *GormRepository) DeleteSource(ctx context.Context, sourceId string) (int64, error) {
|
||||||
|
currentUser, currentUserErr := gr.GetCurrentUser(ctx)
|
||||||
|
if currentUserErr != nil {
|
||||||
|
return 0, currentUserErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(sourceId) == "" {
|
||||||
|
return 0, fmt.Errorf("sourceId cannot be blank")
|
||||||
|
}
|
||||||
|
//delete all resources for this source
|
||||||
|
sourceUUID, err := uuid.Parse(sourceId)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsEffected := int64(0)
|
||||||
|
resourceTypes := databaseModel.GetAllowedResourceTypes()
|
||||||
|
for _, resourceType := range resourceTypes {
|
||||||
|
tableName, err := databaseModel.GetTableNameByResourceType(resourceType)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
results := gr.GormClient.WithContext(ctx).
|
||||||
|
Where(models.OriginBase{
|
||||||
|
UserID: currentUser.ID,
|
||||||
|
SourceID: sourceUUID,
|
||||||
|
}).
|
||||||
|
Table(tableName).
|
||||||
|
Delete(&models.ResourceBase{})
|
||||||
|
rowsEffected += results.RowsAffected
|
||||||
|
if results.Error != nil {
|
||||||
|
return rowsEffected, results.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//delete relatedResources entries
|
||||||
|
results := gr.GormClient.WithContext(ctx).
|
||||||
|
Where(models.RelatedResource{ResourceBaseUserID: currentUser.ID, ResourceBaseSourceID: sourceUUID}).
|
||||||
|
Delete(&models.RelatedResource{})
|
||||||
|
if results.Error != nil {
|
||||||
|
return rowsEffected, results.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
//soft delete the source credential
|
||||||
|
results = gr.GormClient.WithContext(ctx).
|
||||||
|
Where(models.SourceCredential{
|
||||||
|
ModelBase: models.ModelBase{
|
||||||
|
ID: sourceUUID,
|
||||||
|
},
|
||||||
|
UserID: currentUser.ID,
|
||||||
|
}).
|
||||||
|
Delete(&models.SourceCredential{})
|
||||||
|
rowsEffected += results.RowsAffected
|
||||||
|
return rowsEffected, results.Error
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Background Job
|
// Background Job
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -38,6 +38,7 @@ type DatabaseRepository interface {
|
||||||
GetSourceSummary(context.Context, string) (*models.SourceSummary, error)
|
GetSourceSummary(context.Context, string) (*models.SourceSummary, error)
|
||||||
GetSources(context.Context) ([]models.SourceCredential, error)
|
GetSources(context.Context) ([]models.SourceCredential, error)
|
||||||
UpdateSource(ctx context.Context, sourceCreds *models.SourceCredential) error
|
UpdateSource(ctx context.Context, sourceCreds *models.SourceCredential) error
|
||||||
|
DeleteSource(ctx context.Context, sourceId string) (int64, error)
|
||||||
|
|
||||||
CreateGlossaryEntry(ctx context.Context, glossaryEntry *models.Glossary) error
|
CreateGlossaryEntry(ctx context.Context, glossaryEntry *models.Glossary) error
|
||||||
GetGlossaryEntry(ctx context.Context, code string, codeSystem string) (*models.Glossary, error)
|
GetGlossaryEntry(ctx context.Context, code string, codeSystem string) (*models.Glossary, error)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/jwk"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/jwk"
|
||||||
|
@ -134,7 +135,90 @@ func (s *SourceCredential) IsDynamicClient() bool {
|
||||||
return len(s.DynamicClientRegistrationMode) > 0
|
return len(s.DynamicClientRegistrationMode) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This method will generate a new keypair, register a new dynamic client with the provider
|
||||||
|
// it will set the following fields:
|
||||||
|
// - DynamicClientJWKS
|
||||||
|
// - DynamicClientId
|
||||||
|
func (s *SourceCredential) RegisterDynamicClient() error {
|
||||||
|
|
||||||
|
//this source requires dynamic client registration
|
||||||
|
// see https://fhir.epic.com/Documentation?docId=Oauth2§ion=Standalone-Oauth2-OfflineAccess-0
|
||||||
|
|
||||||
|
// Generate a public-private key pair
|
||||||
|
// Must be 2048 bits (larger keys will silently fail when used with Epic, untested on other providers)
|
||||||
|
sourceSpecificClientKeyPair, err := jwk.JWKGenerate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("an error occurred while generating device-specific keypair for dynamic client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//store in sourceCredential
|
||||||
|
serializedKeypair, err := jwk.JWKSerialize(sourceSpecificClientKeyPair)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("an error occurred while serializing keypair for dynamic client: %w", err)
|
||||||
|
}
|
||||||
|
s.DynamicClientJWKS = []map[string]string{
|
||||||
|
serializedKeypair,
|
||||||
|
}
|
||||||
|
|
||||||
|
//generate dynamic client registration request
|
||||||
|
payload := ClientRegistrationRequest{
|
||||||
|
SoftwareId: s.ClientId,
|
||||||
|
Jwks: ClientRegistrationRequestJwks{
|
||||||
|
Keys: []ClientRegistrationRequestJwksKey{
|
||||||
|
{
|
||||||
|
KeyType: "RSA",
|
||||||
|
KeyId: serializedKeypair["kid"],
|
||||||
|
Modulus: serializedKeypair["n"],
|
||||||
|
PublicExponent: serializedKeypair["e"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
payloadBytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("an error occurred while marshalling dynamic client registration request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//http.Post("https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token", "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(fmt.Sprintf("grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=%s&scope=system/Patient.read", sourceSpecificClientKeyPair))))
|
||||||
|
req, err := http.NewRequest(http.MethodPost, s.RegistrationEndpoint, bytes.NewBuffer(payloadBytes))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("an error occurred while generating dynamic client registration request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.AccessToken))
|
||||||
|
|
||||||
|
registrationResponse, err := http.DefaultClient.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("an error occurred while sending dynamic client registration request: %w", err)
|
||||||
|
}
|
||||||
|
defer registrationResponse.Body.Close()
|
||||||
|
if registrationResponse.StatusCode >= 300 || registrationResponse.StatusCode < 200 {
|
||||||
|
b, err := io.ReadAll(registrationResponse.Body)
|
||||||
|
if err == nil {
|
||||||
|
log.Printf("Error Response body: %s", string(b))
|
||||||
|
}
|
||||||
|
return fmt.Errorf("an error occurred while reading dynamic client registration response, status code was not 200: %d", registrationResponse.StatusCode)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//read response
|
||||||
|
var registrationResponseBytes ClientRegistrationResponse
|
||||||
|
err = json.NewDecoder(registrationResponse.Body).Decode(®istrationResponseBytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("an error occurred while parsing dynamic client registration response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//store the dynamic client id
|
||||||
|
s.DynamicClientId = registrationResponseBytes.ClientId
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// this will set/update the AccessToken and Expiry using the dynamic client credentials
|
// this will set/update the AccessToken and Expiry using the dynamic client credentials
|
||||||
|
// it will set the following fields:
|
||||||
|
// - AccessToken
|
||||||
|
// - ExpiresAt
|
||||||
func (s *SourceCredential) RefreshDynamicClientAccessToken() error {
|
func (s *SourceCredential) RefreshDynamicClientAccessToken() error {
|
||||||
if len(s.DynamicClientRegistrationMode) == 0 {
|
if len(s.DynamicClientRegistrationMode) == 0 {
|
||||||
return fmt.Errorf("dynamic client registration mode not set")
|
return fmt.Errorf("dynamic client registration mode not set")
|
||||||
|
|
|
@ -5,124 +5,29 @@
|
||||||
"description": "An example dashboard to show-off the power of Fasten widgets",
|
"description": "An example dashboard to show-off the power of Fasten widgets",
|
||||||
"widgets": [
|
"widgets": [
|
||||||
{
|
{
|
||||||
"title_text": "Diabetes Tracking",
|
"title_text": "Records Summary",
|
||||||
"description_text": "Track key metrics for your chronic disease (eg. Diabetes). The data within this widget is not reflective of your health record, and is only present for demonstrational purposes.",
|
"description_text": "Track key metrics for your chronic disease (eg. Diabetes). The data within this widget is not reflective of your health record, and is only present for demonstrational purposes.",
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
"width": 8,
|
"width": 8,
|
||||||
"height": 5,
|
"height": 6,
|
||||||
"item_type": "complex-line-widget"
|
"item_type": "records-summary-widget"
|
||||||
},
|
|
||||||
{
|
|
||||||
"title_text": "Weight",
|
|
||||||
"description_text": "",
|
|
||||||
"x": 8,
|
|
||||||
"y": 0,
|
|
||||||
"width": 2,
|
|
||||||
"height": 2,
|
|
||||||
"item_type": "simple-line-chart-widget",
|
|
||||||
"queries": [{
|
|
||||||
"q": {
|
|
||||||
"select": [
|
|
||||||
"valueQuantity.value as data",
|
|
||||||
"valueQuantity.unit as unit",
|
|
||||||
"(effectiveDateTime | issued).first() as label"
|
|
||||||
],
|
|
||||||
"from": "Observation",
|
|
||||||
"where": {
|
|
||||||
"code": "http://loinc.org|29463-7,http://loinc.org|3141-9,http://snomed.info/sct|27113001"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
"parsing": {
|
|
||||||
"xAxisKey": "label",
|
|
||||||
"yAxisKey": "data"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title_text": "Height",
|
|
||||||
"description_text": "",
|
|
||||||
"x": 10,
|
|
||||||
"y": 0,
|
|
||||||
"width": 2,
|
|
||||||
"height": 2,
|
|
||||||
"item_type": "simple-line-chart-widget",
|
|
||||||
"queries": [{
|
|
||||||
"q": {
|
|
||||||
"select": [
|
|
||||||
"valueQuantity.value as data",
|
|
||||||
"valueQuantity.unit as unit",
|
|
||||||
"(effectiveDateTime | issued).first() as label"
|
|
||||||
],
|
|
||||||
"from": "Observation",
|
|
||||||
"where": {
|
|
||||||
"code": "http://loinc.org|8302-2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
"parsing": {
|
|
||||||
"xAxisKey": "label",
|
|
||||||
"yAxisKey": "data"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title_text": "Blood Pressure",
|
|
||||||
"description_text": "How much pressure your blood is exerting against your artery walls when the heart beats",
|
|
||||||
"x": 8,
|
|
||||||
"y": 2,
|
|
||||||
"width": 4,
|
|
||||||
"height": 3,
|
|
||||||
"item_type": "grouped-bar-chart-widget",
|
|
||||||
"queries": [
|
|
||||||
{
|
|
||||||
"q": {
|
|
||||||
"select": [
|
|
||||||
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8462-4').valueQuantity.value as data",
|
|
||||||
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8462-4').valueQuantity.unit as unit"
|
|
||||||
],
|
|
||||||
"from": "Observation",
|
|
||||||
"where": {
|
|
||||||
"componentCode": "http://loinc.org|8462-4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dataset_options": {
|
|
||||||
"label": "Diastolic"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"q": {
|
|
||||||
"select": [
|
|
||||||
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8480-6').valueQuantity.value as data",
|
|
||||||
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8480-6').valueQuantity.unit as unit"
|
|
||||||
],
|
|
||||||
"from": "Observation",
|
|
||||||
"where": {
|
|
||||||
"componentCode": "http://loinc.org|8480-6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dataset_options": {
|
|
||||||
"label": "Systolic"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
"parsing": {
|
|
||||||
"xAxisKey": "id",
|
|
||||||
"yAxisKey": "data"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title_text": "Patient Vitals",
|
"title_text": "Patient Vitals",
|
||||||
"description_text": "",
|
"description_text": "",
|
||||||
"x": 0,
|
"x": 8,
|
||||||
"y": 5,
|
"y": 0,
|
||||||
"width": 4,
|
"width": 4,
|
||||||
"height": 5,
|
"height": 6,
|
||||||
"item_type": "patient-vitals-widget"
|
"item_type": "patient-vitals-widget"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"title_text": "Observations by Type",
|
"title_text": "Observations by Type",
|
||||||
"description_text": "",
|
"description_text": "",
|
||||||
"x": 4,
|
"x": 0,
|
||||||
"y": 5,
|
"y": 6,
|
||||||
"width": 8,
|
"width": 8,
|
||||||
"height": 5,
|
"height": 5,
|
||||||
"item_type": "donut-chart-widget",
|
"item_type": "donut-chart-widget",
|
||||||
|
@ -141,11 +46,113 @@
|
||||||
"key": "value"
|
"key": "value"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"title_text": "Weight",
|
||||||
|
"description_text": "",
|
||||||
|
"x": 8,
|
||||||
|
"y": 6,
|
||||||
|
"width": 2,
|
||||||
|
"height": 2,
|
||||||
|
"item_type": "simple-line-chart-widget",
|
||||||
|
"queries": [{
|
||||||
|
"q": {
|
||||||
|
"select": [
|
||||||
|
"valueQuantity.value as data",
|
||||||
|
"valueQuantity.unit as unit",
|
||||||
|
"(effectiveDateTime | issued).first() as label"
|
||||||
|
],
|
||||||
|
"from": "Observation",
|
||||||
|
"where": {
|
||||||
|
"code": "http://loinc.org|29463-7,http://loinc.org|3141-9,http://snomed.info/sct|27113001"
|
||||||
|
},
|
||||||
|
"limit": 50
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"parsing": {
|
||||||
|
"xAxisKey": "label",
|
||||||
|
"yAxisKey": "data"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title_text": "Height",
|
||||||
|
"description_text": "",
|
||||||
|
"x": 10,
|
||||||
|
"y": 6,
|
||||||
|
"width": 2,
|
||||||
|
"height": 2,
|
||||||
|
"item_type": "simple-line-chart-widget",
|
||||||
|
"queries": [{
|
||||||
|
"q": {
|
||||||
|
"select": [
|
||||||
|
"valueQuantity.value as data",
|
||||||
|
"valueQuantity.unit as unit",
|
||||||
|
"(effectiveDateTime | issued).first() as label"
|
||||||
|
],
|
||||||
|
"from": "Observation",
|
||||||
|
"where": {
|
||||||
|
"code": "http://loinc.org|8302-2"
|
||||||
|
},
|
||||||
|
"limit": 50
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"parsing": {
|
||||||
|
"xAxisKey": "label",
|
||||||
|
"yAxisKey": "data"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title_text": "Blood Pressure",
|
||||||
|
"description_text": "How much pressure your blood is exerting against your artery walls when the heart beats",
|
||||||
|
"x": 8,
|
||||||
|
"y": 8,
|
||||||
|
"width": 4,
|
||||||
|
"height": 3,
|
||||||
|
"item_type": "grouped-bar-chart-widget",
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"q": {
|
||||||
|
"select": [
|
||||||
|
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8462-4').valueQuantity.value as data",
|
||||||
|
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8462-4').valueQuantity.unit as unit"
|
||||||
|
],
|
||||||
|
"from": "Observation",
|
||||||
|
"where": {
|
||||||
|
"componentCode": "http://loinc.org|8462-4"
|
||||||
|
},
|
||||||
|
"limit": 50
|
||||||
|
},
|
||||||
|
"dataset_options": {
|
||||||
|
"label": "Diastolic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": {
|
||||||
|
"select": [
|
||||||
|
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8480-6').valueQuantity.value as data",
|
||||||
|
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8480-6').valueQuantity.unit as unit"
|
||||||
|
],
|
||||||
|
"from": "Observation",
|
||||||
|
"where": {
|
||||||
|
"componentCode": "http://loinc.org|8480-6"
|
||||||
|
},
|
||||||
|
"limit": 50
|
||||||
|
},
|
||||||
|
"dataset_options": {
|
||||||
|
"label": "Systolic"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"parsing": {
|
||||||
|
"xAxisKey": "id",
|
||||||
|
"yAxisKey": "data"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"title_text": "Compliance",
|
"title_text": "Compliance",
|
||||||
"description_text": "Use to track important healthcare and medical tasks.",
|
"description_text": "Use to track important healthcare and medical tasks.",
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 10,
|
"y": 11,
|
||||||
"width": 4,
|
"width": 4,
|
||||||
"height": 2,
|
"height": 2,
|
||||||
"item_type": "dual-gauges-widget",
|
"item_type": "dual-gauges-widget",
|
||||||
|
@ -184,7 +191,7 @@
|
||||||
"title_text": "Recent Encounters",
|
"title_text": "Recent Encounters",
|
||||||
"description_text": "Recent interactions with healthcare providers",
|
"description_text": "Recent interactions with healthcare providers",
|
||||||
"x": 4,
|
"x": 4,
|
||||||
"y": 10,
|
"y": 11,
|
||||||
"width": 8,
|
"width": 8,
|
||||||
"height": 4,
|
"height": 4,
|
||||||
"item_type": "table-widget",
|
"item_type": "table-widget",
|
||||||
|
@ -197,7 +204,8 @@
|
||||||
"participant.individual.display as provider"
|
"participant.individual.display as provider"
|
||||||
],
|
],
|
||||||
"from": "Encounter",
|
"from": "Encounter",
|
||||||
"where": {}
|
"where": {},
|
||||||
|
"limit": 50
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
"parsing": {
|
"parsing": {
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"id": "secondary",
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"title": "Secondary Dashboard",
|
||||||
|
"description": "An second dashboard to show-off the flexibility of the dashboard system.",
|
||||||
|
"widgets": [
|
||||||
|
{
|
||||||
|
"title_text": "Records Summary",
|
||||||
|
"description_text": "Track key metrics for your chronic disease (eg. Diabetes). The data within this widget is not reflective of your health record, and is only present for demonstrational purposes.",
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"width": 8,
|
||||||
|
"height": 6,
|
||||||
|
"item_type": "records-summary-widget"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title_text": "Care Team",
|
||||||
|
"description_text": "Track key metrics for your chronic disease (eg. Diabetes). The data within this widget is not reflective of your health record, and is only present for demonstrational purposes.",
|
||||||
|
"x": 8,
|
||||||
|
"y": 0,
|
||||||
|
"width": 4,
|
||||||
|
"height": 6,
|
||||||
|
"item_type": "image-list-group-widget"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,24 +1,21 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/jwk"
|
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
||||||
"github.com/fastenhealth/fasten-sources/clients/factory"
|
"github.com/fastenhealth/fasten-sources/clients/factory"
|
||||||
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
|
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateSource(c *gin.Context) {
|
func CreateReconnectSource(c *gin.Context) {
|
||||||
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
|
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
|
||||||
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
|
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
|
||||||
|
|
||||||
|
@ -39,91 +36,13 @@ func CreateSource(c *gin.Context) {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//this source requires dynamic client registration
|
|
||||||
// see https://fhir.epic.com/Documentation?docId=Oauth2§ion=Standalone-Oauth2-OfflineAccess-0
|
|
||||||
|
|
||||||
// Generate a public-private key pair
|
err := sourceCred.RegisterDynamicClient()
|
||||||
// Must be 2048 bits (larger keys will silently fail when used with Epic, untested on other providers)
|
|
||||||
sourceSpecificClientKeyPair, err := jwk.JWKGenerate()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorln("An error occurred while generating device-specific keypair for dynamic client", err)
|
logger.Errorln("An error occurred while registering dynamic client", err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//store in sourceCredential
|
|
||||||
serializedKeypair, err := jwk.JWKSerialize(sourceSpecificClientKeyPair)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorln("An error occurred while serializing keypair for dynamic client", err)
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sourceCred.DynamicClientJWKS = []map[string]string{
|
|
||||||
serializedKeypair,
|
|
||||||
}
|
|
||||||
|
|
||||||
//generate dynamic client registration request
|
|
||||||
payload := models.ClientRegistrationRequest{
|
|
||||||
SoftwareId: sourceCred.ClientId,
|
|
||||||
Jwks: models.ClientRegistrationRequestJwks{
|
|
||||||
Keys: []models.ClientRegistrationRequestJwksKey{
|
|
||||||
{
|
|
||||||
KeyType: "RSA",
|
|
||||||
KeyId: serializedKeypair["kid"],
|
|
||||||
Modulus: serializedKeypair["n"],
|
|
||||||
PublicExponent: serializedKeypair["e"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
payloadBytes, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorln("An error occurred while marshalling dynamic client registration request", err)
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//http.Post("https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token", "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(fmt.Sprintf("grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=%s&scope=system/Patient.read", sourceSpecificClientKeyPair))))
|
|
||||||
req, err := http.NewRequest(http.MethodPost, sourceCred.RegistrationEndpoint, bytes.NewBuffer(payloadBytes))
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorln("An error occurred while generating dynamic client registration request", err)
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", sourceCred.AccessToken))
|
|
||||||
|
|
||||||
registrationResponse, err := http.DefaultClient.Do(req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorln("An error occurred while sending dynamic client registration request", err)
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer registrationResponse.Body.Close()
|
|
||||||
if registrationResponse.StatusCode >= 300 || registrationResponse.StatusCode < 200 {
|
|
||||||
logger.Errorln("An error occurred while reading dynamic client registration response, status code was not 200", registrationResponse.StatusCode)
|
|
||||||
b, err := io.ReadAll(registrationResponse.Body)
|
|
||||||
if err == nil {
|
|
||||||
logger.Printf("Error Response body: %s", string(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//read response
|
|
||||||
var registrationResponseBytes models.ClientRegistrationResponse
|
|
||||||
err = json.NewDecoder(registrationResponse.Body).Decode(®istrationResponseBytes)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorln("An error occurred while parsing dynamic client registration response", err)
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"success": false})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//store the dynamic client id
|
|
||||||
sourceCred.DynamicClientId = registrationResponseBytes.ClientId
|
|
||||||
|
|
||||||
//generate a JWT token and then use it to get an access token for the dynamic client
|
//generate a JWT token and then use it to get an access token for the dynamic client
|
||||||
err = sourceCred.RefreshDynamicClientAccessToken()
|
err = sourceCred.RefreshDynamicClientAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -133,11 +52,22 @@ func CreateSource(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := databaseRepo.CreateSource(c, &sourceCred)
|
if sourceCred.ID != uuid.Nil {
|
||||||
if err != nil {
|
//reconnect
|
||||||
logger.Errorln("An error occurred while storing source credential", err)
|
err := databaseRepo.UpdateSource(c, &sourceCred)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
if err != nil {
|
||||||
return
|
logger.Errorln("An error occurred while reconnecting source credential", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//create source for the first time
|
||||||
|
err := databaseRepo.CreateSource(c, &sourceCred)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorln("An error occurred while storing source credential", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// after creating the source, we should do a bulk import (in the background)
|
// after creating the source, we should do a bulk import (in the background)
|
||||||
|
@ -312,3 +242,16 @@ func ListSource(c *gin.Context) {
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": sourceCreds})
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": sourceCreds})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteSource(c *gin.Context) {
|
||||||
|
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
|
||||||
|
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
|
||||||
|
|
||||||
|
rowsEffected, err := databaseRepo.DeleteSource(c, c.Param("sourceId"))
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorln("An error occurred while deleting source credential", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": rowsEffected})
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/config"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/config"
|
||||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
|
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
|
||||||
|
@ -53,16 +52,6 @@ func UnsafeRequestSource(c *gin.Context) {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//TODO: if source has been updated, we should save the access/refresh token.
|
|
||||||
//if updatedSource != nil {
|
|
||||||
// logger.Warnf("TODO: source credential has been updated, we should store it in the database: %v", updatedSource)
|
|
||||||
// // err := databaseRepo.CreateSource(c, updatedSource)
|
|
||||||
// // if err != nil {
|
|
||||||
// // logger.Errorf("An error occurred while updating source credential %v", err)
|
|
||||||
// // c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
|
|
||||||
// // return
|
|
||||||
// // }
|
|
||||||
//}
|
|
||||||
|
|
||||||
var resp map[string]interface{}
|
var resp map[string]interface{}
|
||||||
|
|
||||||
|
@ -75,6 +64,23 @@ func UnsafeRequestSource(c *gin.Context) {
|
||||||
//make sure we include all query string parameters with the raw request.
|
//make sure we include all query string parameters with the raw request.
|
||||||
parsedUrl.RawQuery = c.Request.URL.Query().Encode()
|
parsedUrl.RawQuery = c.Request.URL.Query().Encode()
|
||||||
|
|
||||||
|
//make sure we store the source credential information in the database, even if the request fails
|
||||||
|
defer func() {
|
||||||
|
//update source incase the access token/refresh token has been updated
|
||||||
|
sourceCredential := client.GetSourceCredential()
|
||||||
|
sourceCredentialConcrete, ok := sourceCredential.(*models.SourceCredential)
|
||||||
|
if !ok {
|
||||||
|
logger.Errorln("An error occurred while updating source credential, source credential is not of type *models.SourceCredential")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = databaseRepo.UpdateSource(c, sourceCredentialConcrete)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("An error occurred while updating source credential: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Successfully updated source credential")
|
||||||
|
}()
|
||||||
|
|
||||||
_, err = client.GetRequest(parsedUrl.String(), &resp)
|
_, err = client.GetRequest(parsedUrl.String(), &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Error making raw request, %v", err)
|
logger.Errorf("Error making raw request, %v", err)
|
||||||
|
@ -82,21 +88,6 @@ func UnsafeRequestSource(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//update source incase the access token/refresh token has been updated
|
|
||||||
sourceCredential := client.GetSourceCredential()
|
|
||||||
sourceCredentialConcrete, ok := sourceCredential.(*models.SourceCredential)
|
|
||||||
if !ok {
|
|
||||||
logger.Errorln("An error occurred while updating source credential, source credential is not of type *models.SourceCredential")
|
|
||||||
c.JSON(http.StatusOK, gin.H{"success": false, "data": resp, "error": fmt.Errorf("An error occurred while updating source credential, source credential is not of type *models.SourceCredential")})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = databaseRepo.UpdateSource(c, sourceCredentialConcrete)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("An error occurred while updating source credential: %v", err)
|
|
||||||
c.JSON(http.StatusOK, gin.H{"success": false, "data": resp, "error": fmt.Errorf("An error occurred while updating source credential: %v", err)})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": resp})
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": resp})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,10 +64,11 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {
|
||||||
{
|
{
|
||||||
secure.GET("/summary", handler.GetSummary)
|
secure.GET("/summary", handler.GetSummary)
|
||||||
|
|
||||||
secure.POST("/source", handler.CreateSource)
|
secure.POST("/source", handler.CreateReconnectSource)
|
||||||
secure.POST("/source/manual", handler.CreateManualSource)
|
secure.POST("/source/manual", handler.CreateManualSource)
|
||||||
secure.GET("/source", handler.ListSource)
|
secure.GET("/source", handler.ListSource)
|
||||||
secure.GET("/source/:sourceId", handler.GetSource)
|
secure.GET("/source/:sourceId", handler.GetSource)
|
||||||
|
secure.DELETE("/source/:sourceId", handler.DeleteSource)
|
||||||
secure.POST("/source/:sourceId/sync", handler.SourceSync)
|
secure.POST("/source/:sourceId/sync", handler.SourceSync)
|
||||||
secure.GET("/source/:sourceId/summary", handler.GetSourceSummary)
|
secure.GET("/source/:sourceId/summary", handler.GetSourceSummary)
|
||||||
secure.GET("/resource/fhir", handler.ListResourceFhir)
|
secure.GET("/resource/fhir", handler.ListResourceFhir)
|
||||||
|
|
|
@ -11,15 +11,7 @@ import { GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode, GridSta
|
||||||
|
|
||||||
import { GridItemCompHTMLElement, GridstackItemComponent } from './gridstack-item.component';
|
import { GridItemCompHTMLElement, GridstackItemComponent } from './gridstack-item.component';
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {WidgetsModule} from '../../widgets/widgets.module';
|
import {WidgetsModule, WidgetComponents} from '../../widgets/widgets.module';
|
||||||
import {ComplexLineWidgetComponent} from '../../widgets/complex-line-widget/complex-line-widget.component';
|
|
||||||
import {DashboardWidgetComponent} from '../../widgets/dashboard-widget/dashboard-widget.component';
|
|
||||||
import {DonutChartWidgetComponent} from '../../widgets/donut-chart-widget/donut-chart-widget.component';
|
|
||||||
import {DualGaugesWidgetComponent} from '../../widgets/dual-gauges-widget/dual-gauges-widget.component';
|
|
||||||
import {GroupedBarChartWidgetComponent} from '../../widgets/grouped-bar-chart-widget/grouped-bar-chart-widget.component';
|
|
||||||
import {PatientVitalsWidgetComponent} from '../../widgets/patient-vitals-widget/patient-vitals-widget.component';
|
|
||||||
import {SimpleLineChartWidgetComponent} from '../../widgets/simple-line-chart-widget/simple-line-chart-widget.component';
|
|
||||||
import {TableWidgetComponent} from '../../widgets/table-widget/table-widget.component';
|
|
||||||
import {DashboardWidgetComponentInterface} from '../../widgets/dashboard-widget-component-interface';
|
import {DashboardWidgetComponentInterface} from '../../widgets/dashboard-widget-component-interface';
|
||||||
|
|
||||||
/** events handlers emitters signature for different events */
|
/** events handlers emitters signature for different events */
|
||||||
|
@ -137,16 +129,7 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// register all our dynamic components created in the grid
|
// register all our dynamic components created in the grid
|
||||||
GridstackComponent.addComponentToSelectorType([
|
GridstackComponent.addComponentToSelectorType(WidgetComponents());
|
||||||
ComplexLineWidgetComponent,
|
|
||||||
DashboardWidgetComponent,
|
|
||||||
DonutChartWidgetComponent,
|
|
||||||
DualGaugesWidgetComponent,
|
|
||||||
GroupedBarChartWidgetComponent,
|
|
||||||
PatientVitalsWidgetComponent,
|
|
||||||
SimpleLineChartWidgetComponent,
|
|
||||||
TableWidgetComponent,
|
|
||||||
]);
|
|
||||||
// set globally our method to create the right widget type
|
// set globally our method to create the right widget type
|
||||||
GridStack.addRemoveCB = gsCreateNgComponents;
|
GridStack.addRemoveCB = gsCreateNgComponents;
|
||||||
GridStack.saveCB = gsSaveAdditionalNgInfo;
|
GridStack.saveCB = gsSaveAdditionalNgInfo;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-center p-1" style="width:100%">
|
<div class="card-footer text-center p-1" style="width:100%">
|
||||||
<small class="tx-gray-700">
|
<small class="tx-gray-700">
|
||||||
{{sourceInfo?.metadata.display}}
|
{{sourceInfo?.metadata?.display || getSourceDisplayName(sourceInfo)}}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.img-fluid {
|
.img-fluid {
|
||||||
min-height:50px;
|
width:100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-left-danger {
|
.border-left-danger {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||||
import {SourceListItem} from '../../pages/medical-sources/medical-sources.component';
|
import {SourceListItem} from '../../pages/medical-sources/medical-sources.component';
|
||||||
|
import moment from 'moment/moment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-medical-sources-card',
|
selector: 'app-medical-sources-card',
|
||||||
|
@ -22,4 +23,14 @@ export class MedicalSourcesCardComponent implements OnInit {
|
||||||
this.onClick.emit(this.sourceInfo)
|
this.onClick.emit(this.sourceInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSourceDisplayName(sourceItem: SourceListItem): string {
|
||||||
|
if(sourceItem.metadata?.display) {
|
||||||
|
return sourceItem.metadata?.display
|
||||||
|
}
|
||||||
|
if(sourceItem.source?.source_type == 'manual') {
|
||||||
|
return 'Uploaded ' + moment(sourceItem.source?.created_at).format('MMM DD, YYYY')
|
||||||
|
}
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,10 +42,9 @@
|
||||||
Actions
|
Actions
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownManual">
|
<div ngbDropdownMenu aria-labelledby="dropdownManual">
|
||||||
<button ngbDropdownItem (click)="sourceSyncHandler(modalSelectedSourceListItem.source)" type="button" class="btn btn-indigo">Sync</button>
|
<button *ngIf="modalSelectedSourceListItem.source?.source_type != 'manual'" ngbDropdownItem (click)="sourceSyncHandler(modalSelectedSourceListItem.source)" type="button" class="btn btn-indigo">Sync</button>
|
||||||
<!-- <button ngbDropdownItem (click)="connectHandler($event, modalSelectedSourceListItem.source['source_type'])" type="button" class="btn btn-outline-light">Reconnect</button>-->
|
<button *ngIf="modalSelectedSourceListItem.source?.source_type != 'manual'" ngbDropdownItem (click)="sourceReconnectHandler(modalSelectedSourceListItem)" type="button" class="btn btn-danger">Reconnect</button>
|
||||||
<button ngbDropdownItem type="button" class="btn disabled btn-outline-danger">Reconnect</button>
|
<button ngbDropdownItem (click)="sourceDeleteHandler()" type="button" class="btn btn-danger">Delete</button>
|
||||||
<button ngbDropdownItem type="button" class="btn disabled btn-outline-danger">Delete</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button (click)="modal.dismiss('Close click')" type="button" class="btn btn-outline-light">Close</button>
|
<button (click)="modal.dismiss('Close click')" type="button" class="btn btn-outline-light">Close</button>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {ToastService} from '../../services/toast.service';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
import {Location} from '@angular/common';
|
import {Location} from '@angular/common';
|
||||||
import {EventBusService} from '../../services/event-bus.service';
|
import {EventBusService} from '../../services/event-bus.service';
|
||||||
|
import {SourceState} from '../../models/fasten/source-state';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-medical-sources-connected',
|
selector: 'app-medical-sources-connected',
|
||||||
|
@ -102,7 +103,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
|
||||||
});
|
});
|
||||||
this.location.replaceState(urlTree.toString());
|
this.location.replaceState(urlTree.toString());
|
||||||
|
|
||||||
const expectedSourceStateInfo = JSON.parse(localStorage.getItem(callbackState))
|
const expectedSourceStateInfo = JSON.parse(localStorage.getItem(callbackState)) as SourceState
|
||||||
localStorage.removeItem(callbackState)
|
localStorage.removeItem(callbackState)
|
||||||
|
|
||||||
if(callbackError && !callbackCode){
|
if(callbackError && !callbackCode){
|
||||||
|
@ -147,6 +148,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
|
||||||
//Create FHIR Client
|
//Create FHIR Client
|
||||||
|
|
||||||
const dbSourceCredential = new Source({
|
const dbSourceCredential = new Source({
|
||||||
|
id: expectedSourceStateInfo.reconnect_source_id,
|
||||||
source_type: sourceType,
|
source_type: sourceType,
|
||||||
|
|
||||||
authorization_endpoint: sourceMetadata.authorization_endpoint,
|
authorization_endpoint: sourceMetadata.authorization_endpoint,
|
||||||
|
@ -323,6 +325,70 @@ export class MedicalSourcesConnectedComponent implements OnInit {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sourceDeleteHandler(){
|
||||||
|
let source = this.modalSelectedSourceListItem.source
|
||||||
|
let sourceDisplayName = this.modalSelectedSourceListItem?.metadata?.display || this.modalSelectedSourceListItem?.source?.source_type || 'unknown'
|
||||||
|
|
||||||
|
this.status[source.id] = "authorize"
|
||||||
|
this.modalService.dismissAll()
|
||||||
|
|
||||||
|
this.fastenApi.deleteSource(source.id).subscribe(
|
||||||
|
(respData) => {
|
||||||
|
delete this.status[source.id]
|
||||||
|
delete this.status[source.source_type]
|
||||||
|
|
||||||
|
//delete this source from the connnected list
|
||||||
|
let foundIndex = this.connectedSourceList.findIndex((connectedSource) => {
|
||||||
|
return connectedSource?.source?.id == source.id
|
||||||
|
}, this)
|
||||||
|
if(foundIndex > -1){
|
||||||
|
this.connectedSourceList.splice(foundIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("source delete response:", respData)
|
||||||
|
|
||||||
|
|
||||||
|
const toastNotification = new ToastNotification()
|
||||||
|
toastNotification.type = ToastType.Success
|
||||||
|
toastNotification.message = `Successfully deleted source: ${sourceDisplayName}, ${respData} row(s) effected`
|
||||||
|
this.toastService.show(toastNotification)
|
||||||
|
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
delete this.status[source.id]
|
||||||
|
delete this.status[source.source_type]
|
||||||
|
|
||||||
|
const toastNotification = new ToastNotification()
|
||||||
|
toastNotification.type = ToastType.Error
|
||||||
|
toastNotification.message = `An error occurred while deleting source: ${sourceDisplayName}`
|
||||||
|
this.toastService.show(toastNotification)
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//this is similar to the connectHandler in the MedicalSourcesComponent
|
||||||
|
public sourceReconnectHandler(selectedSourceListItem: SourceListItem){
|
||||||
|
|
||||||
|
let sourceType = selectedSourceListItem.metadata.source_type
|
||||||
|
this.lighthouseApi.getLighthouseSource(sourceType)
|
||||||
|
.then(async (sourceMetadata: LighthouseSourceMetadata) => {
|
||||||
|
console.log(sourceMetadata);
|
||||||
|
let authorizationUrl = await this.lighthouseApi.generateSourceAuthorizeUrl(sourceType, sourceMetadata, selectedSourceListItem.source.id)
|
||||||
|
|
||||||
|
console.log('authorize url:', authorizationUrl.toString());
|
||||||
|
// redirect to lighthouse with uri's (or open a new window in desktop mode)
|
||||||
|
this.lighthouseApi.redirectWithOriginAndDestination(authorizationUrl.toString(), sourceType, sourceMetadata.redirect_uri).subscribe((codeData) => {
|
||||||
|
//Note: this code will only run in Desktop mode (with popups)
|
||||||
|
//in non-desktop environments, the user is redirected in the same window, and this code is never executed.
|
||||||
|
|
||||||
|
//always close the modal
|
||||||
|
this.modalService.dismissAll()
|
||||||
|
|
||||||
|
//redirect the browser back to this page with the code in the query string parameters
|
||||||
|
this.lighthouseApi.redirectWithDesktopCode(sourceType, codeData)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private getDismissReason(reason: any): string {
|
private getDismissReason(reason: any): string {
|
||||||
if (reason === ModalDismissReasons.ESC) {
|
if (reason === ModalDismissReasons.ESC) {
|
||||||
|
|
|
@ -2,6 +2,8 @@ export class SourceState {
|
||||||
state: string
|
state: string
|
||||||
|
|
||||||
source_type: string //used to override the source_type for sources which have a single redirect url (eg. Epic)
|
source_type: string //used to override the source_type for sources which have a single redirect url (eg. Epic)
|
||||||
|
reconnect_source_id?: string //used to reconnect a source
|
||||||
|
|
||||||
code_verifier?: string
|
code_verifier?: string
|
||||||
code_challenge_method?: string
|
code_challenge_method?: string
|
||||||
code_challenge?: string
|
code_challenge?: string
|
||||||
|
|
|
@ -3,6 +3,8 @@ import {BackgroundJob} from './background-job';
|
||||||
|
|
||||||
export class Source extends LighthouseSourceMetadata{
|
export class Source extends LighthouseSourceMetadata{
|
||||||
id?: string
|
id?: string
|
||||||
|
created_at?: string
|
||||||
|
updated_at?: string
|
||||||
user_id?: number
|
user_id?: number
|
||||||
source_type: string
|
source_type: string
|
||||||
latest_background_job?: BackgroundJob
|
latest_background_job?: BackgroundJob
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as _ from 'lodash';
|
||||||
|
|
||||||
export class DashboardWidgetConfig {
|
export class DashboardWidgetConfig {
|
||||||
id?: string
|
id?: string
|
||||||
item_type: "complex-line-widget" | "donut-chart-widget" | "dual-gauges-widget" | "grouped-bar-chart-widget" | "patient-vitals-widget" | "simple-line-chart-widget" | "table-widget"
|
item_type: "image-list-group-widget" | "complex-line-widget" | "donut-chart-widget" | "dual-gauges-widget" | "grouped-bar-chart-widget" | "patient-vitals-widget" | "simple-line-chart-widget" | "table-widget" | "records-summary-widget"
|
||||||
|
|
||||||
title_text: string
|
title_text: string
|
||||||
description_text: string
|
description_text: string
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {LighthouseSourceMetadata} from '../../models/lighthouse/lighthouse-sourc
|
||||||
import {Source} from '../../models/fasten/source';
|
import {Source} from '../../models/fasten/source';
|
||||||
import {MetadataSource} from '../../models/fasten/metadata-source';
|
import {MetadataSource} from '../../models/fasten/metadata-source';
|
||||||
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {ActivatedRoute, Router, UrlSerializer} from '@angular/router';
|
import {ActivatedRoute} from '@angular/router';
|
||||||
import {environment} from '../../../environments/environment';
|
import {environment} from '../../../environments/environment';
|
||||||
import {BehaviorSubject, forkJoin, Observable, of, Subject} from 'rxjs';
|
import {BehaviorSubject, forkJoin, Observable, of, Subject} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
|
@ -17,7 +17,6 @@ import {debounceTime, distinctUntilChanged, pairwise, startWith} from 'rxjs/oper
|
||||||
import {MedicalSourcesFilter, MedicalSourcesFilterService} from '../../services/medical-sources-filter.service';
|
import {MedicalSourcesFilter, MedicalSourcesFilterService} from '../../services/medical-sources-filter.service';
|
||||||
import {FormControl, FormGroup} from '@angular/forms';
|
import {FormControl, FormGroup} from '@angular/forms';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import {Location} from '@angular/common';
|
|
||||||
|
|
||||||
export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
|
export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
|
||||||
|
|
||||||
|
@ -79,9 +78,7 @@ export class MedicalSourcesComponent implements OnInit {
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private filterService: MedicalSourcesFilterService,
|
private filterService: MedicalSourcesFilterService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private router: Router,
|
|
||||||
private urlSerializer: UrlSerializer,
|
|
||||||
private location: Location,
|
|
||||||
) {
|
) {
|
||||||
this.filterService.filterChanges.subscribe((filterInfo) => {
|
this.filterService.filterChanges.subscribe((filterInfo) => {
|
||||||
|
|
||||||
|
@ -295,22 +292,8 @@ export class MedicalSourcesComponent implements OnInit {
|
||||||
//always close the modal
|
//always close the modal
|
||||||
this.modalService.dismissAll()
|
this.modalService.dismissAll()
|
||||||
|
|
||||||
if(!codeData){
|
//redirect the browser back to this page with the code in the query string parameters
|
||||||
//if we redirected completely, no callback data will be present.
|
this.lighthouseApi.redirectWithDesktopCode(sourceType, codeData)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//User was shown a popup, which was closed, and data was returned using events
|
|
||||||
//redirect to callback page with code
|
|
||||||
|
|
||||||
let urlTree = this.router.createUrlTree(
|
|
||||||
['/sources/callback/' + sourceType],
|
|
||||||
{ queryParams: codeData, }
|
|
||||||
);
|
|
||||||
|
|
||||||
let absUrl = this.location.prepareExternalUrl(this.urlSerializer.serialize(urlTree))
|
|
||||||
console.log(absUrl);
|
|
||||||
window.location.replace(absUrl)
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="az-content-label mg-b-5">Condition</div>
|
<div class="az-content-label mg-b-5">Condition</div>
|
||||||
<p class="mg-b-20">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna </p>
|
<p class="mg-b-20">
|
||||||
|
A condition is a disease, illness, or injury that needs to be managed over time. A condition may be a comorbidity (a co-occurring condition), or it may be a main diagnosis.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<form (ngSubmit)="onSubmit()" [formGroup]="form">
|
<form (ngSubmit)="onSubmit()" [formGroup]="form">
|
||||||
|
@ -75,7 +77,9 @@
|
||||||
<div class="card-header" (click)="collapsePanel['medication'] = !collapsePanel['medication']">
|
<div class="card-header" (click)="collapsePanel['medication'] = !collapsePanel['medication']">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="card-title">Medications</h6>
|
<h6 class="card-title">Medications</h6>
|
||||||
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p>
|
<p class="card-text">
|
||||||
|
Medications are substances that are taken to treat or prevent disease or illness. Medications are generally characterized by their chemical composition and the way they are administered.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['medication']" class="card-body">
|
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['medication']" class="card-body">
|
||||||
|
@ -173,7 +177,9 @@
|
||||||
<div class="card-header" (click)="collapsePanel['procedure'] = !collapsePanel['procedure']">
|
<div class="card-header" (click)="collapsePanel['procedure'] = !collapsePanel['procedure']">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="card-title">Major Surgeries and Implants</h6>
|
<h6 class="card-title">Major Surgeries and Implants</h6>
|
||||||
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p>
|
<p class="card-text">
|
||||||
|
Implants, devices, major surgeries and other procedures that are part of a patient's history.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['procedure']" class="card-body">
|
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['procedure']" class="card-body">
|
||||||
|
@ -262,7 +268,7 @@
|
||||||
<div class="card-header" (click)="collapsePanel['practitioner'] = !collapsePanel['practitioner']">
|
<div class="card-header" (click)="collapsePanel['practitioner'] = !collapsePanel['practitioner']">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="card-title">Medical Practitioners</h6>
|
<h6 class="card-title">Medical Practitioners</h6>
|
||||||
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p>
|
<p class="card-text">Practitioners involved in the care of the patient.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['practitioner']" class="card-body">
|
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['practitioner']" class="card-body">
|
||||||
|
@ -332,7 +338,8 @@
|
||||||
<div class="card-header" (click)="collapsePanel['organization'] = !collapsePanel['organization']">
|
<div class="card-header" (click)="collapsePanel['organization'] = !collapsePanel['organization']">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="card-title">Medical Location/Organizations</h6>
|
<h6 class="card-title">Medical Location/Organizations</h6>
|
||||||
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p>
|
<p class="card-text">Locations and Organizations involved in the care of the patient.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['organization']" class="card-body">
|
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['organization']" class="card-body">
|
||||||
|
@ -397,7 +404,9 @@
|
||||||
<div class="card-header" (click)="collapsePanel['attachments'] = !collapsePanel['attachments']">
|
<div class="card-header" (click)="collapsePanel['attachments'] = !collapsePanel['attachments']">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="card-title">Notes & Attachments</h6>
|
<h6 class="card-title">Notes & Attachments</h6>
|
||||||
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p>
|
<p class="card-text">
|
||||||
|
Files and notes related to medications, procedures, or the current condition.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['attachments']" class="card-body">
|
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['attachments']" class="card-body">
|
||||||
|
|
|
@ -137,6 +137,15 @@ export class FastenApiService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteSource(sourceId: string): Observable<number> {
|
||||||
|
return this._httpClient.delete<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/source/${sourceId}`)
|
||||||
|
.pipe(
|
||||||
|
map((response: ResponseWrapper) => {
|
||||||
|
return response.data as number
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
syncSource(sourceId: string): Observable<any> {
|
syncSource(sourceId: string): Observable<any> {
|
||||||
return this._httpClient.post<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/source/${sourceId}/sync`, {})
|
return this._httpClient.post<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/source/${sourceId}/sync`, {})
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {LighthouseSourceSearch} from '../models/lighthouse/lighthouse-source-sea
|
||||||
import {HTTP_CLIENT_TOKEN} from "../dependency-injection";
|
import {HTTP_CLIENT_TOKEN} from "../dependency-injection";
|
||||||
import {MedicalSourcesFilter} from './medical-sources-filter.service';
|
import {MedicalSourcesFilter} from './medical-sources-filter.service';
|
||||||
import {OpenExternalLink} from '../../lib/utils/external_link';
|
import {OpenExternalLink} from '../../lib/utils/external_link';
|
||||||
|
import {Router, UrlSerializer} from '@angular/router';
|
||||||
|
import {Location} from '@angular/common';
|
||||||
|
|
||||||
export const sourceConnectDesktopTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
|
export const sourceConnectDesktopTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
|
||||||
|
|
||||||
|
@ -21,8 +23,12 @@ export const sourceConnectDesktopTimeout = 24*5000 //wait 2 minutes (5 * 24 = 12
|
||||||
})
|
})
|
||||||
export class LighthouseService {
|
export class LighthouseService {
|
||||||
|
|
||||||
constructor(@Inject(HTTP_CLIENT_TOKEN) private _httpClient: HttpClient) {
|
constructor(
|
||||||
}
|
@Inject(HTTP_CLIENT_TOKEN) private _httpClient: HttpClient,
|
||||||
|
private router: Router,
|
||||||
|
private urlSerializer: UrlSerializer,
|
||||||
|
private location: Location,
|
||||||
|
) {}
|
||||||
|
|
||||||
public searchLighthouseSources(filter: MedicalSourcesFilter): Observable<LighthouseSourceSearch> {
|
public searchLighthouseSources(filter: MedicalSourcesFilter): Observable<LighthouseSourceSearch> {
|
||||||
if(filter.searchAfter){
|
if(filter.searchAfter){
|
||||||
|
@ -87,11 +93,15 @@ export class LighthouseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async generateSourceAuthorizeUrl(sourceType: string, lighthouseSource: LighthouseSourceMetadata): Promise<URL> {
|
async generateSourceAuthorizeUrl(sourceType: string, lighthouseSource: LighthouseSourceMetadata, reconnectSourceId?: string): Promise<URL> {
|
||||||
const state = uuidV4()
|
const state = uuidV4()
|
||||||
let sourceStateInfo = new SourceState()
|
let sourceStateInfo = new SourceState()
|
||||||
sourceStateInfo.state = state
|
sourceStateInfo.state = state
|
||||||
sourceStateInfo.source_type = sourceType
|
sourceStateInfo.source_type = sourceType
|
||||||
|
if(reconnectSourceId){
|
||||||
|
//if the source already exists, and we want to re-connect it (because of an expiration), we need to pass the existing source id
|
||||||
|
sourceStateInfo.reconnect_source_id = reconnectSourceId
|
||||||
|
}
|
||||||
|
|
||||||
// generate the authorization url
|
// generate the authorization url
|
||||||
const authorizationUrl = new URL(lighthouseSource.authorization_endpoint);
|
const authorizationUrl = new URL(lighthouseSource.authorization_endpoint);
|
||||||
|
@ -283,6 +293,28 @@ export class LighthouseService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//after waiting for the desktop code, we need to redirect to the callback page with the code in the query params
|
||||||
|
// (which is what would have happened if we were in a browser and we were redirected as usual)
|
||||||
|
redirectWithDesktopCode(sourceType: string, codeData: any){
|
||||||
|
|
||||||
|
if(!codeData){
|
||||||
|
//if we redirected completely, no callback data will be present.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//User was shown a popup, which was closed, and data was returned using events
|
||||||
|
//redirect to callback page with code
|
||||||
|
|
||||||
|
let urlTree = this.router.createUrlTree(
|
||||||
|
['/sources/callback/' + sourceType],
|
||||||
|
{ queryParams: codeData, }
|
||||||
|
);
|
||||||
|
|
||||||
|
let absUrl = this.location.prepareExternalUrl(this.urlSerializer.serialize(urlTree))
|
||||||
|
console.log(absUrl);
|
||||||
|
window.location.replace(absUrl)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<ng-container [ngTemplateOutlet]="loading ? showLoading : (!loading && isEmpty) ? showEmpty : showChart"></ng-container>
|
||||||
|
|
||||||
|
<ng-template #showLoading>
|
||||||
|
<loading-widget></loading-widget>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #showEmpty>
|
||||||
|
<empty-widget></empty-widget>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #showChart>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item d-flex align-items-center">
|
||||||
|
<img src="../img/faces/face6.jpg" class="wd-30 rounded-circle mg-r-15" alt="">
|
||||||
|
<div>
|
||||||
|
<h6 class="tx-13 tx-inverse tx-semibold mg-b-0">Dr. Ester Howard</h6>
|
||||||
|
<span class="d-block tx-11 text-muted">Neurologist, UCSF</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex align-items-center">
|
||||||
|
<img src="../img/faces/face7.jpg" class="wd-30 rounded-circle mg-r-15" alt="">
|
||||||
|
<div>
|
||||||
|
<h6 class="tx-13 tx-inverse tx-semibold mg-b-0">Dr. Joel Mendez</h6>
|
||||||
|
<span class="d-block tx-11 text-muted">General Medicine, Kaiser</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex align-items-center">
|
||||||
|
<img src="../img/faces/face8.jpg" class="wd-30 rounded-circle mg-r-15" alt="">
|
||||||
|
<div>
|
||||||
|
<h6 class="tx-13 tx-inverse tx-semibold mg-b-0">Dr. Marianne Audrey</h6>
|
||||||
|
<span class="d-block tx-11 text-muted">orthopedic surgeon, UCSF</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ng-template><!-- card -->
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ImageListGroupWidgetComponent } from './image-list-group-widget.component';
|
||||||
|
|
||||||
|
describe('ImageListGroupWidgetComponent', () => {
|
||||||
|
let component: ImageListGroupWidgetComponent;
|
||||||
|
let fixture: ComponentFixture<ImageListGroupWidgetComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ ImageListGroupWidgetComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ImageListGroupWidgetComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import {NgChartsModule} from 'ng2-charts';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {LoadingWidgetComponent} from '../loading-widget/loading-widget.component';
|
||||||
|
import {EmptyWidgetComponent} from '../empty-widget/empty-widget.component';
|
||||||
|
import {DashboardWidgetComponent} from '../dashboard-widget/dashboard-widget.component';
|
||||||
|
import {DashboardWidgetConfig} from '../../models/widget/dashboard-widget-config';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [NgChartsModule, CommonModule, LoadingWidgetComponent, EmptyWidgetComponent],
|
||||||
|
selector: 'image-list-group-widget',
|
||||||
|
templateUrl: './image-list-group-widget.component.html',
|
||||||
|
styleUrls: ['./image-list-group-widget.component.scss']
|
||||||
|
})
|
||||||
|
export class ImageListGroupWidgetComponent extends DashboardWidgetComponent implements OnInit {
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
//manually define the widget config, rather than pull from the configuration file
|
||||||
|
this.widgetConfig = {
|
||||||
|
id: 'image-list-group-widget',
|
||||||
|
item_type: 'image-list-group-widget',
|
||||||
|
description_text: 'Displays a summary of patient records',
|
||||||
|
width: 4,
|
||||||
|
height: 5,
|
||||||
|
title_text: 'Medical Records',
|
||||||
|
queries: []
|
||||||
|
|
||||||
|
} as DashboardWidgetConfig
|
||||||
|
super.ngOnInit();
|
||||||
|
this.loading = false
|
||||||
|
this.isEmpty = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
<ng-container [ngTemplateOutlet]="loading ? showLoading : (!loading && isEmpty) ? showEmpty : showChart"></ng-container>
|
||||||
|
|
||||||
|
<ng-template #showLoading>
|
||||||
|
<loading-widget></loading-widget>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #showEmpty>
|
||||||
|
<empty-widget></empty-widget>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #showChart>
|
||||||
|
<div class="card card-dashboard-sixteen">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="card-title">Medical Records</h6>
|
||||||
|
</div><!-- card-header -->
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table mg-b-0">
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let groupInfo of groupLookup">
|
||||||
|
<td>
|
||||||
|
<div class="az-img-user"><img src="assets/icons/{{groupInfo.imageName}}.svg" alt=""></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<h6 class="mg-b-0 tx-inverse">{{groupInfo.displayName}}</h6>
|
||||||
|
<small class="tx-11 tx-gray-500" *ngIf="groupInfo.includedResourceTypes.length > 1">
|
||||||
|
<span *ngFor="let resourceTypes of groupInfo.includedResourceTypes">
|
||||||
|
{{resourceTypes}}<br/>
|
||||||
|
</span>
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<h6 class="mg-b-0 tx-inverse">{{groupInfo.count}}</h6>
|
||||||
|
<small class="tx-11 tx-gray-500">Records</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- card-body -->
|
||||||
|
</div>
|
||||||
|
</ng-template><!-- card -->
|
|
@ -0,0 +1,14 @@
|
||||||
|
tbody {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Borders */
|
||||||
|
/* The margin declarations are used to simulate border-collapse:collapse */
|
||||||
|
tr {
|
||||||
|
margin: 0 0 -1px -1px;
|
||||||
|
//border: 1px solid black;
|
||||||
|
}
|
||||||
|
tbody:not(:empty) {
|
||||||
|
margin: 0 0 1px 1px;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { RecordsSummaryWidgetComponent } from './records-summary-widget.component';
|
||||||
|
|
||||||
|
describe('RecordsSummaryWidgetComponent', () => {
|
||||||
|
let component: RecordsSummaryWidgetComponent;
|
||||||
|
let fixture: ComponentFixture<RecordsSummaryWidgetComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ RecordsSummaryWidgetComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(RecordsSummaryWidgetComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,214 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import {NgChartsModule} from 'ng2-charts';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {MomentModule} from 'ngx-moment';
|
||||||
|
import {LoadingWidgetComponent} from '../loading-widget/loading-widget.component';
|
||||||
|
import {EmptyWidgetComponent} from '../empty-widget/empty-widget.component';
|
||||||
|
import {DashboardWidgetComponent} from '../dashboard-widget/dashboard-widget.component';
|
||||||
|
import {DashboardWidgetConfig} from '../../models/widget/dashboard-widget-config';
|
||||||
|
import {Summary} from '../../models/fasten/summary';
|
||||||
|
|
||||||
|
class GroupedSummary {
|
||||||
|
displayName: string
|
||||||
|
imageName: string
|
||||||
|
resourceTypes: string[]
|
||||||
|
includedResourceTypes: string[] = []
|
||||||
|
count: number = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, LoadingWidgetComponent, EmptyWidgetComponent],
|
||||||
|
selector: 'records-summary-widget',
|
||||||
|
templateUrl: './records-summary-widget.component.html',
|
||||||
|
styleUrls: ['./records-summary-widget.component.scss']
|
||||||
|
})
|
||||||
|
export class RecordsSummaryWidgetComponent extends DashboardWidgetComponent implements OnInit {
|
||||||
|
|
||||||
|
// constructor() { }
|
||||||
|
|
||||||
|
summary: Summary
|
||||||
|
|
||||||
|
groupLookup: GroupedSummary[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Allergies',
|
||||||
|
imageName: 'allergies',
|
||||||
|
resourceTypes: ['AllergyIntolerance', 'AdverseEvent'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Care Team',
|
||||||
|
imageName: 'care-team',
|
||||||
|
resourceTypes: ['CareTeam', 'Practitioner', 'Patient', 'RelatedPerson', 'PractitionerRole'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Clinical Notes',
|
||||||
|
imageName: 'clinical-notes',
|
||||||
|
resourceTypes: ['DocumentReference', 'DiagnosticReport'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Files',
|
||||||
|
imageName: 'files',
|
||||||
|
resourceTypes: ['Binary', 'Media'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Lab Results',
|
||||||
|
imageName: 'lab-results',
|
||||||
|
resourceTypes: ['Observation', 'Specimen'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Health Issues',
|
||||||
|
imageName: 'health-issues',
|
||||||
|
resourceTypes: ['Conditions', 'Encounters'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Facilities',
|
||||||
|
imageName: 'facilities',
|
||||||
|
resourceTypes: ['Organization', 'Location'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Health Goals',
|
||||||
|
imageName: 'health-goals',
|
||||||
|
resourceTypes: ['Goal'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Health Insurance',
|
||||||
|
imageName: 'health-insurance',
|
||||||
|
resourceTypes: ['Coverage', 'ExplanationOfBenefit', 'Claim'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Health Assessments',
|
||||||
|
imageName: 'health-assessments',
|
||||||
|
resourceTypes: ['QuestionnaireResponse','Questionnaire', 'CarePlan', 'FamilyMemberHistory'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Immunizations',
|
||||||
|
imageName: 'immunizations',
|
||||||
|
resourceTypes: ['Immunization'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Implants',
|
||||||
|
imageName: 'implants',
|
||||||
|
resourceTypes: ['Device'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Medications',
|
||||||
|
imageName: 'medications',
|
||||||
|
resourceTypes: ['Medication', 'MedicationRequest', 'MedicationStatement', 'MedicationAdministration', 'MedicationDispense'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Demographics',
|
||||||
|
imageName: 'demographics',
|
||||||
|
resourceTypes: ['Patient'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Procedures',
|
||||||
|
imageName: 'procedures',
|
||||||
|
resourceTypes: ['Procedure','ServiceRequest'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Provenance',
|
||||||
|
imageName: 'provenance',
|
||||||
|
resourceTypes: ['Provenance'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Appointments',
|
||||||
|
imageName: 'appointments',
|
||||||
|
resourceTypes: ['Appointment', 'Schedule', 'Slot'],
|
||||||
|
includedResourceTypes:[],
|
||||||
|
count: 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
//manually define the widget config, rather than pull from the configuration file
|
||||||
|
this.widgetConfig = {
|
||||||
|
id: 'records-summary-widget',
|
||||||
|
item_type: 'records-summary-widget',
|
||||||
|
description_text: 'Displays a summary of patient records',
|
||||||
|
width: 4,
|
||||||
|
height: 5,
|
||||||
|
title_text: 'Medical Records',
|
||||||
|
queries: [
|
||||||
|
]
|
||||||
|
|
||||||
|
} as DashboardWidgetConfig
|
||||||
|
super.ngOnInit();
|
||||||
|
this.chartProcessQueryResults(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
chartProcessQueryResults(queryResults: any[]) {
|
||||||
|
|
||||||
|
this.fastenApi.getSummary().subscribe((summary: Summary) => {
|
||||||
|
this.summary = summary
|
||||||
|
for (let resourceTypeCount of summary.resource_type_counts) {
|
||||||
|
let foundGroup = false
|
||||||
|
for (let groupKey in this.groupLookup) {
|
||||||
|
let group = this.groupLookup[groupKey]
|
||||||
|
if (group.resourceTypes.indexOf(resourceTypeCount.resource_type) > -1) {
|
||||||
|
foundGroup = true
|
||||||
|
this.groupLookup[groupKey].count += resourceTypeCount.count
|
||||||
|
this.groupLookup[groupKey].includedResourceTypes.push(resourceTypeCount.resource_type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundGroup) {
|
||||||
|
this.groupLookup[resourceTypeCount.resource_type] = {
|
||||||
|
displayName: resourceTypeCount.resource_type,
|
||||||
|
|
||||||
|
resourceTypes: [resourceTypeCount.resource_type],
|
||||||
|
count: resourceTypeCount.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//filter any groups with 0 counts
|
||||||
|
this.groupLookup = this.groupLookup.filter((group) => {
|
||||||
|
return group.count > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
if(this.summary.resource_type_counts.length > 0){
|
||||||
|
this.isEmpty = false
|
||||||
|
}
|
||||||
|
this.loading = false
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
this.loading = false
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
console.log('completed getting summary')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/angular';
|
||||||
|
import {RecordsSummaryWidgetComponent} from './records-summary-widget.component';
|
||||||
|
import {applicationConfig, moduleMetadata} from '@storybook/angular';
|
||||||
|
import {HttpClient, HttpClientModule} from '@angular/common/http';
|
||||||
|
import {HTTP_CLIENT_TOKEN} from '../../dependency-injection';
|
||||||
|
import {importProvidersFrom} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
|
||||||
|
const meta: Meta<RecordsSummaryWidgetComponent> = {
|
||||||
|
title: 'Widget/RecordsSummaryWidget',
|
||||||
|
component: RecordsSummaryWidgetComponent,
|
||||||
|
decorators: [
|
||||||
|
applicationConfig({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: HttpClient,
|
||||||
|
useClass: HttpClient
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: HTTP_CLIENT_TOKEN,
|
||||||
|
useClass: HttpClient,
|
||||||
|
},
|
||||||
|
importProvidersFrom(HttpClientModule)
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
moduleMetadata({
|
||||||
|
imports: [CommonModule, HttpClientModule],
|
||||||
|
})
|
||||||
|
],
|
||||||
|
tags: ['autodocs'],
|
||||||
|
render: (args: RecordsSummaryWidgetComponent) => ({
|
||||||
|
props: {
|
||||||
|
backgroundColor: null,
|
||||||
|
...args,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
argTypes: {
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<RecordsSummaryWidgetComponent>;
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { NgModule } from '@angular/core';
|
import {Component, NgModule, Type} from '@angular/core';
|
||||||
import {ComplexLineWidgetComponent} from './complex-line-widget/complex-line-widget.component';
|
import {ComplexLineWidgetComponent} from './complex-line-widget/complex-line-widget.component';
|
||||||
import {DonutChartWidgetComponent} from './donut-chart-widget/donut-chart-widget.component';
|
import {DonutChartWidgetComponent} from './donut-chart-widget/donut-chart-widget.component';
|
||||||
import {DualGaugesWidgetComponent} from './dual-gauges-widget/dual-gauges-widget.component';
|
import {DualGaugesWidgetComponent} from './dual-gauges-widget/dual-gauges-widget.component';
|
||||||
|
@ -9,6 +9,8 @@ import {TableWidgetComponent} from './table-widget/table-widget.component';
|
||||||
import { LoadingWidgetComponent } from './loading-widget/loading-widget.component';
|
import { LoadingWidgetComponent } from './loading-widget/loading-widget.component';
|
||||||
import { EmptyWidgetComponent } from './empty-widget/empty-widget.component';
|
import { EmptyWidgetComponent } from './empty-widget/empty-widget.component';
|
||||||
import {DashboardWidgetComponent} from './dashboard-widget/dashboard-widget.component';
|
import {DashboardWidgetComponent} from './dashboard-widget/dashboard-widget.component';
|
||||||
|
import { RecordsSummaryWidgetComponent } from './records-summary-widget/records-summary-widget.component';
|
||||||
|
import { ImageListGroupWidgetComponent } from './image-list-group-widget/image-list-group-widget.component';
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
||||||
|
@ -18,6 +20,8 @@ import {DashboardWidgetComponent} from './dashboard-widget/dashboard-widget.comp
|
||||||
DualGaugesWidgetComponent,
|
DualGaugesWidgetComponent,
|
||||||
GroupedBarChartWidgetComponent,
|
GroupedBarChartWidgetComponent,
|
||||||
PatientVitalsWidgetComponent,
|
PatientVitalsWidgetComponent,
|
||||||
|
RecordsSummaryWidgetComponent,
|
||||||
|
ImageListGroupWidgetComponent,
|
||||||
SimpleLineChartWidgetComponent,
|
SimpleLineChartWidgetComponent,
|
||||||
TableWidgetComponent,
|
TableWidgetComponent,
|
||||||
LoadingWidgetComponent,
|
LoadingWidgetComponent,
|
||||||
|
@ -32,6 +36,8 @@ import {DashboardWidgetComponent} from './dashboard-widget/dashboard-widget.comp
|
||||||
DualGaugesWidgetComponent,
|
DualGaugesWidgetComponent,
|
||||||
GroupedBarChartWidgetComponent,
|
GroupedBarChartWidgetComponent,
|
||||||
PatientVitalsWidgetComponent,
|
PatientVitalsWidgetComponent,
|
||||||
|
RecordsSummaryWidgetComponent,
|
||||||
|
ImageListGroupWidgetComponent,
|
||||||
SimpleLineChartWidgetComponent,
|
SimpleLineChartWidgetComponent,
|
||||||
TableWidgetComponent,
|
TableWidgetComponent,
|
||||||
LoadingWidgetComponent,
|
LoadingWidgetComponent,
|
||||||
|
@ -41,3 +47,21 @@ import {DashboardWidgetComponent} from './dashboard-widget/dashboard-widget.comp
|
||||||
})
|
})
|
||||||
|
|
||||||
export class WidgetsModule { }
|
export class WidgetsModule { }
|
||||||
|
|
||||||
|
//when adding widgets to this list, you must also register the widget id in
|
||||||
|
// frontend/src/app/models/widget/dashboard-widget-config.ts
|
||||||
|
export function WidgetComponents(): Type<Object>[] {
|
||||||
|
return [
|
||||||
|
ComplexLineWidgetComponent,
|
||||||
|
DonutChartWidgetComponent,
|
||||||
|
DualGaugesWidgetComponent,
|
||||||
|
GroupedBarChartWidgetComponent,
|
||||||
|
PatientVitalsWidgetComponent,
|
||||||
|
RecordsSummaryWidgetComponent,
|
||||||
|
ImageListGroupWidgetComponent,
|
||||||
|
SimpleLineChartWidgetComponent,
|
||||||
|
TableWidgetComponent,
|
||||||
|
LoadingWidgetComponent,
|
||||||
|
EmptyWidgetComponent
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #ffd67b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #bde9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #0098e3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #e89b00;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M29.29,29.05s-14-22-2.81-24.74S34.91,17.24,29.29,29.05Z"/>
|
||||||
|
<path class="cls-1" d="M29.24,28.83S3.73,23.18,9.76,13.32,24.87,16.51,29.24,28.83Z"/>
|
||||||
|
<path class="cls-1" d="M29.05,28.71s-22,14-24.74,2.81S17.24,23.09,29.05,28.71Z"/>
|
||||||
|
<path class="cls-1" d="M28.83,28.76S23.18,54.27,13.32,48.24,16.51,33.13,28.83,28.76Z"/>
|
||||||
|
<path class="cls-1" d="M28.71,29s14,22,2.81,24.74S23.09,40.76,28.71,29Z"/>
|
||||||
|
<path class="cls-1" d="M28.76,29.17s25.51,5.65,19.48,15.51S33.13,41.49,28.76,29.17Z"/>
|
||||||
|
<path class="cls-1" d="M29,29.29s22-14,24.74-2.81S40.76,34.91,29,29.29Z"/>
|
||||||
|
<path class="cls-1" d="M29.17,29.24S34.82,3.73,44.68,9.76,41.49,24.87,29.17,29.24Z"/>
|
||||||
|
</g>
|
||||||
|
<path class="cls-2" d="M58,32.66a6.68,6.68,0,0,0-9.44.06L39.81,41.6h0l-6.88,7A6.68,6.68,0,1,0,42.43,58L50.25,50h0l7.82-7.92A6.67,6.67,0,0,0,58,32.66Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M49.63,51.24l8.5-8.61a6.48,6.48,0,0,0-.06-9.17h0a6.49,6.49,0,0,0-9.17.06l-8.5,8.62Z"/>
|
||||||
|
<path class="cls-4" d="M41.31,41.21,33.72,48.9a6.49,6.49,0,0,0,.06,9.17h0A6.48,6.48,0,0,0,43,58l7.59-7.69Z"/>
|
||||||
|
</g>
|
||||||
|
<circle class="cls-5" cx="29" cy="29" r="8"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,26 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #1eb37a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #a5f3ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #0e6b5a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<circle class="cls-1" cx="32" cy="32" r="28"/>
|
||||||
|
<circle class="cls-2" cx="32" cy="32" r="22"/>
|
||||||
|
<circle class="cls-1" cx="32" cy="32" r="17"/>
|
||||||
|
<circle class="cls-2" cx="32" cy="32" r="12"/>
|
||||||
|
<circle class="cls-1" cx="32" cy="32" r="7"/>
|
||||||
|
<path class="cls-3" d="M32.67,28.82h0a3.83,3.83,0,0,1,.62-1.4L43,14l4.17,4L36.63,29.32a5,5,0,0,1-1.46,1.09l-.26.14A1.57,1.57,0,0,1,32.67,28.82Z"/>
|
||||||
|
<polygon class="cls-3" points="43 14 43 11 48.17 4 50 11 45 17 43 14"/>
|
||||||
|
<polygon class="cls-3" points="47 18 50 18 57 11.83 50 11 44 17 47 18"/>
|
||||||
|
<polygon class="cls-3" points="33.5 29 32 32 35 30 33.5 29"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 891 B |
|
@ -0,0 +1,40 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #34b4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #0098e3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #ffd67b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #818cb0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #eff1f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
fill: #dfe3e8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M48.36,54.78,34.19,59.64a6.84,6.84,0,0,1-4.38,0L15.64,54.78a7.52,7.52,0,0,1-5.21-7.05h0A10.82,10.82,0,0,1,21.49,37.18h21A10.82,10.82,0,0,1,53.57,47.73h0A7.52,7.52,0,0,1,48.36,54.78Z"/>
|
||||||
|
<path class="cls-2" d="M35.06,37.18H28.94A1.36,1.36,0,0,0,27.65,39l2.19,6.32a2.29,2.29,0,0,0,4.32,0L36.35,39A1.36,1.36,0,0,0,35.06,37.18Z"/>
|
||||||
|
<path class="cls-3" d="M32,4a14.93,14.93,0,0,0-13.63,8.85H45.63A14.93,14.93,0,0,0,32,4Z"/>
|
||||||
|
<path class="cls-3" d="M17.07,18.93v6.64a14.93,14.93,0,0,0,29.86,0V18.93c0-.19,0-.37,0-.55H17.1C17.09,18.56,17.07,18.74,17.07,18.93Z"/>
|
||||||
|
<path class="cls-4" d="M18.37,12.85a14.78,14.78,0,0,0-1.27,5.53H46.9a14.78,14.78,0,0,0-1.27-5.53Z"/>
|
||||||
|
<g>
|
||||||
|
<circle class="cls-5" cx="32" cy="15.61" r="6.69"/>
|
||||||
|
<path class="cls-6" d="M32,9.42a6.19,6.19,0,1,1-6.19,6.19A6.2,6.2,0,0,1,32,9.42m0-1a7.19,7.19,0,1,0,7.19,7.19A7.19,7.19,0,0,0,32,8.42Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,48 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #ffd67b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #fdba19;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #fffbd5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #243147;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #1eb37a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
fill: #14866d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-7 {
|
||||||
|
fill: #c0c7d2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M11,10H41a7,7,0,0,1,7,7V49a7,7,0,0,1-7,7H16L4,44V17A7,7,0,0,1,11,10Z"/>
|
||||||
|
<path class="cls-2" d="M10.82,44H4L16,56V49.18A5.18,5.18,0,0,0,10.82,44Z"/>
|
||||||
|
<rect class="cls-3" x="9" y="17" width="27" height="3" transform="translate(45 37) rotate(-180)"/>
|
||||||
|
<rect class="cls-3" x="9" y="22" width="17" height="3" transform="translate(35 47) rotate(-180)"/>
|
||||||
|
<rect class="cls-3" x="9" y="28" width="11" height="3" transform="translate(29 59) rotate(-180)"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-4" d="M24.35,40.66h2.24a0,0,0,0,1,0,0v5.6a1.12,1.12,0,0,1-1.12,1.12h0a1.12,1.12,0,0,1-1.12-1.12v-5.6a0,0,0,0,1,0,0Z" transform="translate(38.59 -5.12) rotate(45)"/>
|
||||||
|
<path class="cls-5" d="M57.17,18.67,32.6,43.23a4.47,4.47,0,0,1-6.33,0h0a4.48,4.48,0,0,1,0-6.34L50.83,12.33Z"/>
|
||||||
|
<path class="cls-6" d="M58,11.54a4.48,4.48,0,0,0-6.34,0l-2.18,2.18,6.34,6.34L58,17.88A4.48,4.48,0,0,0,58,11.54Z"/>
|
||||||
|
<path class="cls-7" d="M59.27,21.42l-8.75-8.76-1.75,1.76,8.75,8.75L47.89,32.8a1.24,1.24,0,0,0,1.75,1.75l9.63-9.63h0a2.47,2.47,0,0,0,0-3.5Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,30 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #34b4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #0098e3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #bde9ff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-1" x="4" y="10" width="56" height="44" rx="6.94"/>
|
||||||
|
<path class="cls-2" d="M60,16.87A6.94,6.94,0,0,0,53,10H11a6.94,6.94,0,0,0-7,6.87V20H60Z"/>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-3" x="33" y="34" width="21" height="3"/>
|
||||||
|
<rect class="cls-3" x="33" y="41" width="21" height="3"/>
|
||||||
|
<rect class="cls-3" x="33" y="27" width="12" height="3"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<circle class="cls-3" cx="18" cy="30" r="6"/>
|
||||||
|
<path class="cls-3" d="M16.27,38h3.47A6.27,6.27,0,0,1,26,44.27V46a0,0,0,0,1,0,0H10a0,0,0,0,1,0,0V44.27A6.27,6.27,0,0,1,16.27,38Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 881 B |
|
@ -0,0 +1,38 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #706e85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #c0c7d2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<rect class="cls-1" x="4" y="57" width="56" height="3" rx="1.38"/>
|
||||||
|
<path class="cls-2" d="M16.65,8.73H47.35A4.65,4.65,0,0,1,52,13.38V57a0,0,0,0,1,0,0H12a0,0,0,0,1,0,0V13.38A4.65,4.65,0,0,1,16.65,8.73Z"/>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-1" x="16.55" y="22.66" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
<rect class="cls-1" x="16.55" y="31.31" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
<rect class="cls-1" x="16.55" y="39.97" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
<rect class="cls-1" x="25.57" y="22.66" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
<rect class="cls-1" x="25.57" y="31.31" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
<rect class="cls-1" x="25.57" y="39.97" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-1" x="34.55" y="22.66" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
<rect class="cls-1" x="34.55" y="31.31" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
<rect class="cls-1" x="34.55" y="39.97" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
<rect class="cls-1" x="43.57" y="22.66" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
<rect class="cls-1" x="43.57" y="31.31" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
<rect class="cls-1" x="43.57" y="39.97" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
</g>
|
||||||
|
<rect class="cls-1" x="34.55" y="14.14" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
<rect class="cls-1" x="26" y="49" width="13" height="7" rx="1.17"/>
|
||||||
|
<rect class="cls-1" x="43.57" y="14.14" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
<rect class="cls-1" x="16.55" y="14.14" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
<rect class="cls-1" x="25.57" y="14.14" width="4.88" height="5.77" rx="0.65"/>
|
||||||
|
<path class="cls-2" d="M21.87,5.89H44.13a.87.87,0,0,1,.87.87v2a0,0,0,0,1,0,0H21a0,0,0,0,1,0,0v-2A.87.87,0,0,1,21.87,5.89Z"/>
|
||||||
|
<path class="cls-2" d="M25.58,4H40.42a.58.58,0,0,1,.58.58V5.89a0,0,0,0,1,0,0H25a0,0,0,0,1,0,0V4.58A.58.58,0,0,1,25.58,4Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1,48 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #ffd67b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #fdba19;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #fffbd5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #243147;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #1eb37a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
fill: #14866d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-7 {
|
||||||
|
fill: #c0c7d2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M11,10H41a7,7,0,0,1,7,7V49a7,7,0,0,1-7,7H16L4,44V17A7,7,0,0,1,11,10Z"/>
|
||||||
|
<path class="cls-2" d="M10.82,44H4L16,56V49.18A5.18,5.18,0,0,0,10.82,44Z"/>
|
||||||
|
<rect class="cls-3" x="9" y="17" width="27" height="3" transform="translate(45 37) rotate(-180)"/>
|
||||||
|
<rect class="cls-3" x="9" y="22" width="17" height="3" transform="translate(35 47) rotate(-180)"/>
|
||||||
|
<rect class="cls-3" x="9" y="28" width="11" height="3" transform="translate(29 59) rotate(-180)"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-4" d="M24.35,40.66h2.24a0,0,0,0,1,0,0v5.6a1.12,1.12,0,0,1-1.12,1.12h0a1.12,1.12,0,0,1-1.12-1.12v-5.6a0,0,0,0,1,0,0Z" transform="translate(38.59 -5.12) rotate(45)"/>
|
||||||
|
<path class="cls-5" d="M57.17,18.67,32.6,43.23a4.47,4.47,0,0,1-6.33,0h0a4.48,4.48,0,0,1,0-6.34L50.83,12.33Z"/>
|
||||||
|
<path class="cls-6" d="M58,11.54a4.48,4.48,0,0,0-6.34,0l-2.18,2.18,6.34,6.34L58,17.88A4.48,4.48,0,0,0,58,11.54Z"/>
|
||||||
|
<path class="cls-7" d="M59.27,21.42l-8.75-8.76-1.75,1.76,8.75,8.75L47.89,32.8a1.24,1.24,0,0,0,1.75,1.75l9.63-9.63h0a2.47,2.47,0,0,0,0-3.5Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,31 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #de9c76;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #f8f9fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #9eacc3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #c0c7d2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<rect class="cls-1" x="10" y="6" width="44" height="52" rx="3.44"/>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-1" x="10" y="6" width="44" height="52" rx="7"/>
|
||||||
|
<rect class="cls-2" x="15" y="9" width="34" height="45" rx="2.07"/>
|
||||||
|
<path class="cls-3" d="M21.75,6h20.5a0,0,0,0,1,0,0V9.2a1.92,1.92,0,0,1-1.92,1.92H23.67A1.92,1.92,0,0,1,21.75,9.2V6A0,0,0,0,1,21.75,6Z"/>
|
||||||
|
</g>
|
||||||
|
<rect class="cls-4" x="20" y="18" width="25" height="3"/>
|
||||||
|
<rect class="cls-4" x="20" y="24" width="25" height="3"/>
|
||||||
|
<rect class="cls-4" x="20" y="30" width="25" height="3"/>
|
||||||
|
<rect class="cls-4" x="20" y="36" width="25" height="3"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 937 B |
|
@ -0,0 +1,26 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #1eb37a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #a5f3ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #0e6b5a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<circle class="cls-1" cx="32" cy="32" r="28"/>
|
||||||
|
<circle class="cls-2" cx="32" cy="32" r="22"/>
|
||||||
|
<circle class="cls-1" cx="32" cy="32" r="17"/>
|
||||||
|
<circle class="cls-2" cx="32" cy="32" r="12"/>
|
||||||
|
<circle class="cls-1" cx="32" cy="32" r="7"/>
|
||||||
|
<path class="cls-3" d="M32.67,28.82h0a3.83,3.83,0,0,1,.62-1.4L43,14l4.17,4L36.63,29.32a5,5,0,0,1-1.46,1.09l-.26.14A1.57,1.57,0,0,1,32.67,28.82Z"/>
|
||||||
|
<polygon class="cls-3" points="43 14 43 11 48.17 4 50 11 45 17 43 14"/>
|
||||||
|
<polygon class="cls-3" points="47 18 50 18 57 11.83 50 11 44 17 47 18"/>
|
||||||
|
<polygon class="cls-3" points="33.5 29 32 32 35 30 33.5 29"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 891 B |
|
@ -0,0 +1,45 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #c0c7d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #dfe3e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #f8f9fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #fb7373;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #bde9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
fill: #d1d6dd;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M51,58H13a7,7,0,0,1-7-7V21H58V51A7,7,0,0,1,51,58Z"/>
|
||||||
|
<path class="cls-2" d="M51,57H13a7,7,0,0,1-7-7V20H58V50A7,7,0,0,1,51,57Z"/>
|
||||||
|
<path class="cls-3" d="M51,55H13a7,7,0,0,1-7-7V19H58V48A7,7,0,0,1,51,55Z"/>
|
||||||
|
<path class="cls-4" d="M13,8H51a7,7,0,0,1,7,7v7a0,0,0,0,1,0,0H6a0,0,0,0,1,0,0V15A7,7,0,0,1,13,8Z"/>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-4" x="40" y="39" width="10" height="10" rx="2.14"/>
|
||||||
|
<rect class="cls-5" x="26.5" y="39" width="10" height="10" rx="2.14"/>
|
||||||
|
<rect class="cls-5" x="13.5" y="39" width="10" height="10" rx="2.14"/>
|
||||||
|
<rect class="cls-5" x="40" y="26" width="10" height="10" rx="2.14"/>
|
||||||
|
<rect class="cls-5" x="26.5" y="26" width="10" height="10" rx="2.14"/>
|
||||||
|
<rect class="cls-5" x="13.5" y="26" width="10" height="10" rx="2.14"/>
|
||||||
|
</g>
|
||||||
|
<rect class="cls-6" x="16.82" y="4" width="5.36" height="8.93" rx="2.61"/>
|
||||||
|
<rect class="cls-6" x="41.82" y="4" width="5.36" height="8.93" rx="2.61"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,29 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #976cea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #d6e3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #b387ff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<rect class="cls-1" x="38.35" y="5.06" width="4.53" height="36.66" rx="1.91" transform="translate(28.43 -21.87) rotate(45)"/>
|
||||||
|
<rect class="cls-1" x="45.19" y="8.92" width="15.84" height="3.93" rx="1.17" transform="translate(23.26 -34.37) rotate(45)"/>
|
||||||
|
<rect class="cls-2" x="20.91" y="9.81" width="18.1" height="48.45" rx="4.39" transform="translate(32.84 -11.22) rotate(45)"/>
|
||||||
|
<rect class="cls-1" x="34.39" y="16.33" width="22.63" height="3.93" rx="1.4" transform="translate(26.32 -26.96) rotate(45)"/>
|
||||||
|
<polygon class="cls-1" points="10.3 48.63 8.19 54.23 9.69 55.74 15.23 53.57 10.3 48.63"/>
|
||||||
|
<path class="cls-1" d="M12.12,50.46,4.59,57.83a1.15,1.15,0,0,0-.13,1.63h0a1.16,1.16,0,0,0,1.62-.12l7.46-7.46Z"/>
|
||||||
|
<path class="cls-3" d="M12.78,33.48H28.62a0,0,0,0,1,0,0V49.21a3.9,3.9,0,0,1-3.9,3.9h-8a3.9,3.9,0,0,1-3.9-3.9V33.48a0,0,0,0,1,0,0Z" transform="translate(36.68 -1.96) rotate(45)"/>
|
||||||
|
<rect class="cls-1" x="36.32" y="16.61" width="2.07" height="5.66" transform="translate(-2.81 32.11) rotate(-45)"/>
|
||||||
|
<rect class="cls-1" x="30.77" y="22.17" width="2.07" height="5.66" transform="translate(-8.36 29.81) rotate(-45)"/>
|
||||||
|
<rect class="cls-1" x="25.21" y="27.72" width="2.07" height="5.66" transform="translate(-13.92 27.51) rotate(-45)"/>
|
||||||
|
<rect class="cls-1" x="19.66" y="33.28" width="2.07" height="5.66" transform="translate(-19.47 25.21) rotate(-45)"/>
|
||||||
|
<rect class="cls-1" x="14.1" y="38.84" width="2.07" height="5.66" transform="translate(-25.03 22.91) rotate(-45)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1,25 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #c0c7d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #9eacc3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #d1d6dd;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M20.23,32.69l-3.55,3.79-3.9,4.15-3.72,4-3.77,4a2.2,2.2,0,0,0,.1,3.12l7.66,7.18a2.21,2.21,0,0,0,3.11-.1l7.92-8.38,3.76-4L31.92,42l3.72-4,4-4.28L24.06,28.6Z"/>
|
||||||
|
<polygon class="cls-1" points="33.35 18.7 27.51 24.93 43.05 30.15 44.22 28.9 33.35 18.7"/>
|
||||||
|
<polygon class="cls-2" points="24.06 28.6 39.65 33.77 43.05 30.15 27.51 24.93 24.06 28.6"/>
|
||||||
|
</g>
|
||||||
|
<path class="cls-3" d="M49.51,17.55,46,14l2.85-4.75a18,18,0,0,0-25.29.94L54.16,38.93a18,18,0,0,0,.22-24.36Z"/>
|
||||||
|
<polygon class="cls-2" points="27.84 46.36 24.08 50.38 9.06 44.6 12.78 40.63 27.84 46.36"/>
|
||||||
|
<polygon class="cls-2" points="35.64 38.05 31.92 42.02 16.68 36.48 20.23 32.69 35.64 38.05"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 977 B |
|
@ -0,0 +1,29 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #caeaf1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #d6e3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #b387ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #ebdcff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M23,26S10,44,10,51a7,7,0,0,0,7,7H47a7,7,0,0,0,7-7c0-7-13-25-13-25"/>
|
||||||
|
<path class="cls-2" d="M41,26V11h.5a2.5,2.5,0,0,0,0-5h-19a2.5,2.5,0,0,0,0,5H23V26S10,44,10,51a7,7,0,0,0,7,7H47a7,7,0,0,0,7-7C54,44,41,26,41,26Z"/>
|
||||||
|
<path class="cls-3" d="M43.81,34c3.4,5.08,8.07,12.7,8.07,16.31A5.69,5.69,0,0,1,46.19,56H17.81a5.68,5.68,0,0,1-5.68-5.69C12.13,46.7,20,34,20.19,34,29,34,29,37,36,37,40,37,43.81,34,43.81,34Z"/>
|
||||||
|
<circle class="cls-4" cx="24" cy="50" r="2"/>
|
||||||
|
<circle class="cls-4" cx="28.5" cy="45.5" r="1.5"/>
|
||||||
|
<circle class="cls-4" cx="23.5" cy="38.5" r="1.5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 950 B |
|
@ -0,0 +1,24 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #fdc7c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #fb7373;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M34.82,38.32,49.57,23.37a11.26,11.26,0,0,0-.1-15.92h0a11.26,11.26,0,0,0-15.92.1l-14.76,15Z"/>
|
||||||
|
<path class="cls-2" d="M20.37,20.91,7.2,34.26a11.26,11.26,0,0,0,.1,15.92h0a11.26,11.26,0,0,0,15.92-.11L36.4,36.72Z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M59.88,48.86a11,11,0,0,1-22,.14Z"/>
|
||||||
|
<path class="cls-2" d="M37.88,48.14a11,11,0,0,1,22-.14Z"/>
|
||||||
|
<polygon class="cls-1" points="60 48.82 38.01 49 38 47.18 59.99 47 60 48.82"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 747 B |
|
@ -0,0 +1,17 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #fb7373;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="cls-1" d="M52.23,60H11.77a6.9,6.9,0,0,1-6.59-8.07l4-27A6.78,6.78,0,0,1,15.72,19H48.28a6.78,6.78,0,0,1,6.59,5.94l4,27A6.9,6.9,0,0,1,52.23,60Z"/>
|
||||||
|
<rect class="cls-2" x="28" y="28" width="9" height="24"/>
|
||||||
|
<rect class="cls-2" x="28" y="27.5" width="9" height="25" transform="translate(72.5 7.5) rotate(90)"/>
|
||||||
|
<path class="cls-1" d="M46.22,22.22H18.78V13a8.68,8.68,0,0,1,8.67-8.67h10.1A8.68,8.68,0,0,1,46.22,13Zm-24-3.44H42.78V13a5.24,5.24,0,0,0-5.23-5.23H27.45A5.24,5.24,0,0,0,22.22,13Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 742 B |
|
@ -0,0 +1,27 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #1eb37a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #34b4ff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<circle class="cls-1" cx="18.5" cy="19.5" r="14.5"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M28.4,27a3,3,0,0,1-.62,1.8A3.33,3.33,0,0,1,26.17,30l-6.08,2a3,3,0,0,1-1.88,0l-6.08-2A3.16,3.16,0,0,1,9.9,27a4.24,4.24,0,0,1,1.39-3.12,4.9,4.9,0,0,1,3.35-1.29h.49a6.34,6.34,0,0,0,2.29,1.15,6.57,6.57,0,0,0,1.73.23,6.54,6.54,0,0,0,4-1.38h.48A4.59,4.59,0,0,1,28.4,27Z"/>
|
||||||
|
<path class="cls-2" d="M25.55,14.24V17a6.17,6.17,0,0,1-2.38,4.86A6.38,6.38,0,0,1,20.87,23a6.06,6.06,0,0,1-1.72.23A6.18,6.18,0,0,1,17.42,23a6.34,6.34,0,0,1-2.29-1.15A6.18,6.18,0,0,1,12.74,17V14.24a1.64,1.64,0,0,1,0-.22,6.16,6.16,0,0,1,.54-2.32A6.47,6.47,0,0,1,25,11.7,6,6,0,0,1,25.54,14C25.54,14.09,25.55,14.17,25.55,14.24Z"/>
|
||||||
|
</g>
|
||||||
|
<circle class="cls-3" cx="39.25" cy="39.25" r="20.25"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M52.2,50.77a4.22,4.22,0,0,1-.87,2.54,4.73,4.73,0,0,1-2.27,1.62l-8.57,2.86a4.25,4.25,0,0,1-2.64,0l-8.57-2.86a4.45,4.45,0,0,1-3.14-4.16,6,6,0,0,1,1.95-4.39,6.9,6.9,0,0,1,4.73-1.82h.69a9.2,9.2,0,0,0,11.32,0h.69A6.46,6.46,0,0,1,52.2,50.77Z"/>
|
||||||
|
<path class="cls-2" d="M48.19,32.8v3.91a8.69,8.69,0,0,1-3.36,6.85,9,9,0,0,1-3.23,1.62,9.2,9.2,0,0,1-8.09-1.62,8.7,8.7,0,0,1-3.37-6.85V32.8c0-.11,0-.22,0-.32a8.57,8.57,0,0,1,.77-3.26,9.11,9.11,0,0,1,16.47,0,8.39,8.39,0,0,1,.77,3.26C48.18,32.58,48.19,32.69,48.19,32.8Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,24 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #d1d6dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #00a6f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #8e9fba;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M51.55,6.21a8.61,8.61,0,0,0-1.93-.52c-2.73-.35-1.59.2-2.24-.06A.32.32,0,0,0,47,5.8a.24.24,0,0,0,0,.15c.07,1.15.17,2.07.38,2,.48-.26,1.41,0,1.91-.06,2.42-.1,3.2,1.15,2.83,3.54a37.75,37.75,0,0,1-4.8,13.35,32.45,32.45,0,0,1-3.89,5.56c-1.85,2-3.17,1.93-5.1.11l-.15-.15c-4.88-5-7.61-11.86-8.79-18.64-.43-2.45.39-3.4,2.88-3.62a2.34,2.34,0,0,1,.95.14c.06-.78.08-1.57.07-2.36-.68.28-1.38,0-2.13.2-3.42.76-4.54,2.61-4,6.06.91,5.85,3.77,11,6.13,16.24a10.92,10.92,0,0,0,5,5.15,2.12,2.12,0,0,1,.43.28,5.33,5.33,0,0,1,4.12.45,2.83,2.83,0,0,1,1.09-1,12.79,12.79,0,0,0,4.88-5.52,71.52,71.52,0,0,0,2.93-6.95A54,54,0,0,0,53.51,15a24.19,24.19,0,0,0,.76-4.54A4.27,4.27,0,0,0,51.55,6.21Z"/>
|
||||||
|
<path class="cls-2" d="M53,18.25a1.66,1.66,0,0,0-3.21-.69c-2.48,6.76-6.74,14.06-9.3,14.06-2.23,0-6.61-7.29-9.3-14.1A1.71,1.71,0,0,0,28,18.78c1.35,3.41,5.78,13.74,10.73,15.86-1.05,4.55.64,7.32,2.14,9.77C42,46.27,43,47.87,43,50.28c-.13,5.23-5.43,6.3-13.35,6.3-4.14,0-9.26-1.36-11.34-4.4C17.11,50.51,17,48.52,18,46.1A12.59,12.59,0,0,1,21.06,42C23.5,39.48,26.53,36.41,23,30.5A1.71,1.71,0,0,0,20,32.27c2.21,3.64,1,4.85-1.39,7.27a15.62,15.62,0,0,0-3.87,5.32c-1.68,4.37-.56,7.44.68,9.25,3,4.35,9.49,5.89,14.16,5.89,4.95,0,16.53,0,16.77-9.63C46.45,47,45,44.65,43.8,42.63c-1.43-2.33-2.56-4.16-1.65-7.59a2,2,0,0,0,.06-.34c5.46-2.1,9.89-13.49,10.79-16Z"/>
|
||||||
|
<path class="cls-3" d="M26.49,25.64a6.42,6.42,0,1,1-6.09-4.35h0A6.34,6.34,0,0,1,26.49,25.64Z"/>
|
||||||
|
<path class="cls-3" d="M36.21,7a2.6,2.6,0,0,1-2.6,2.6h0c-1,0-.91-1.16-.91-2.6s-.24-2.6.91-2.6A2.61,2.61,0,0,1,36.21,7Z"/>
|
||||||
|
<path class="cls-3" d="M45,6.56A2.56,2.56,0,0,1,47.6,4h0c1,0,.89,1.18.87,2.61,0,1.67.29,3-1.62,2.48A2.54,2.54,0,0,1,45,6.57Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
|
@ -0,0 +1,33 @@
|
||||||
|
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #fb7373;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #ffb1c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #fff;
|
||||||
|
stroke: #ffb1c0;
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
stroke-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #fedada;
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-1" x="6" y="6" width="52" height="52" rx="7.58"/>
|
||||||
|
<path class="cls-2" d="M53.41,19.59a2,2,0,0,0-2.82,0L32,38.17,19.41,25.59a2,2,0,0,0-2.82,0L6,36.17v5.66l12-12L30.59,42.41a2,2,0,0,0,2.82,0l20-20A2,2,0,0,0,53.41,19.59Z"/>
|
||||||
|
<circle class="cls-3" cx="18" cy="28" r="4"/>
|
||||||
|
<circle class="cls-3" cx="32" cy="41" r="4"/>
|
||||||
|
<circle class="cls-3" cx="52.5" cy="20.5" r="4"/>
|
||||||
|
<rect class="cls-4" x="6" y="24" width="52" height="18"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 910 B |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 7.2 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,102 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="172.66693mm"
|
||||||
|
height="19.869011mm"
|
||||||
|
viewBox="0 0 172.66693 19.869011"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e4, 2022-07-14)"
|
||||||
|
sodipodi:docname="PRN_Banner_Logo.svg"
|
||||||
|
inkscape:dataloss="true"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.14"
|
||||||
|
inkscape:cx="407.01754"
|
||||||
|
inkscape:cy="173.24561"
|
||||||
|
inkscape:window-width="1390"
|
||||||
|
inkscape:window-height="1205"
|
||||||
|
inkscape:window-x="584"
|
||||||
|
inkscape:window-y="108"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" /><defs
|
||||||
|
id="defs2" /><g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-17.665347,-25.597495)"><g
|
||||||
|
id="g6958"><rect
|
||||||
|
style="fill:#1953a4;fill-opacity:1;stroke:#1953a4;stroke-width:0.001;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
id="rect1907"
|
||||||
|
width="19.385"
|
||||||
|
height="19.379999"
|
||||||
|
x="17.68"
|
||||||
|
y="25.613001"
|
||||||
|
inkscape:export-filename="rect1907.svg"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96" /><g
|
||||||
|
aria-label="P"
|
||||||
|
transform="matrix(0.36808457,0,0,0.41364273,43.541475,28.391929)"
|
||||||
|
id="text2073"
|
||||||
|
style="font-size:42.8327px;font-family:'Trajan Pro';-inkscape-font-specification:'Trajan Pro, Normal';fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.862431"><path
|
||||||
|
d="m -68.456551,13.220795 c 0.01834,2.268624 -0.01764,6.967438 -0.07824,9.083709 -0.03582,1.251065 -0.91553,1.59871 -1.764011,2.054275 -1.05e-4,0.529631 -4.82e-4,0.728918 -6.09e-4,1.17631 1.28498,0 4.25801,-0.0049 4.386508,-0.0049 0.214163,0 2.948819,-3.4e-4 5.304618,-3.4e-4 -0.09285,-0.521346 -0.890534,-0.765322 -2.090824,-1.413227 -0.562953,-0.303877 -1.281534,-1.991633 -1.309246,-3.070346 -0.07583,-2.95172 -0.06938,-5.470042 -0.06938,-7.937909 V -4.4958538 c 0,-0.428327 0.0079,-0.762466 0.0079,-0.762466 0,0 2.174423,-0.279373 3.040358,-0.308351 1.031625,-0.03452 3.913481,-0.151492 6.472604,2.559415 2.559122,2.71090699 2.27823,6.4146497 1.884078,7.725814 -0.394152,1.3111643 -1.881538,3.2625519 -2.844972,3.9171479 -0.963435,0.654596 -1.862411,1.0359956 -3.359177,1.24289 -1.496767,0.2068949 -3.412001,-0.023882 -3.403676,0.2261479 0.02155,0.647266 2.356849,0.531491 3.027364,0.507821 0.670516,-0.02367 1.976921,0.0075 3.144765,-0.333939 1.167844,-0.3413988 3.332494,-1.4925958 4.397591,-2.6268058 1.895335,-2.018322 3.112969,-3.864086 3.148861,-6.520499 0.04107,-3.039569 -0.444482,-4.44153 -1.702238,-5.822209 -1.093796,-1.0281485 -2.048493,-1.7565988 -4.744156,-2.0531377 -0.867648,-0.00591 -1.955473,-0.00815 -3.274611,-0.00815 -1.970304,0 -6.201979,0.00343 -7.546706,-2.512e-4 -1.344726,-0.00368 -0.01779,-0.00575 -4.467438,0.00233 0.0012,0.3537653 -6.49e-4,0.2735701 -4.73e-4,0.6246381 0.427158,0.2619053 0.989472,0.586534 1.287609,0.9117073 0.46051,0.502271 0.541875,1.648131 0.539481,2.570302 -0.0057,2.20793799 -0.09501,2.37774099 -0.04952,8.004236 z"
|
||||||
|
id="path386"
|
||||||
|
sodipodi:nodetypes="ssccscsssscszzzzszzssccszccssss"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:none" /></g><g
|
||||||
|
aria-label="R"
|
||||||
|
transform="matrix(0.39563776,0,0,0.39064801,44.41624,28.391929)"
|
||||||
|
id="text1004"
|
||||||
|
style="font-size:45.3539px;font-family:'Trajan Pro';-inkscape-font-specification:'Trajan Pro, Normal';fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.913197"><path
|
||||||
|
d="m -46.381766,13.353976 c -0.01223,2.243212 -0.08492,8.782844 -0.177789,9.742586 -0.06447,0.666209 -1.38779,2.412261 -2.147927,2.345646 -1.929922,1.199555 -0.250199,1.056622 1.010404,1.114005 1.56572,0.102316 3.092726,0.0318 3.822444,0.01896 0.602543,-0.0106 5.812522,0.493212 5.872411,0.229065 0.112418,-0.495834 -1.00092,-0.366093 -2.034728,-0.813263 -0.519795,-0.367442 -1.521008,-1.47994 -1.85299,-2.447962 -0.22677,-1.904864 -0.3585,-3.752424 -0.27575,-8.105592 l 0.04898,-2.576669 c 0.0043,-0.226728 1.119295,-0.114803 1.346062,-0.114803 1.615474,0.03695 3.414671,-0.0098 4.669131,0.784495 1.726031,1.092925 3.362185,3.349799 5.267049,5.844264 2.340272,3.599965 4.079105,5.448553 5.926856,6.667023 1.117869,0.737161 2.975123,1.601086 4.974806,1.602729 0.538764,4.43e-4 1.015339,-0.02394 1.448669,-0.06337 0.0068,-1.750116 0.0061,-0.338605 0.006,-1.745326 -0.536763,-0.0819 -2.186195,-0.433235 -4.635305,-3.018408 -2.630527,-2.811942 -5.714592,-6.848439 -9.433612,-11.610598 4.081851,-3.7643743 5.487822,-7.1205633 5.487822,-10.47675125 0,-3.12942005 -1.995571,-5.35176105 -3.129419,-6.16813105 -1.359478,-0.9747199 -2.088979,-1.4686689 -2.983134,-1.7083006 -1.042167,-0.00414 -15.502616,0.00407 -17.289252,0.00165 0.421026,0.179659 1.753492,0.3306459 2.544266,1.0370949 0.547976,0.489542 1.23546,1.215048 1.391933,2.04428 0.06419,1.523168 0.184564,9.800946 0.183043,10.079972 z m 4.265055,-18.5199823 c 0,-0.272123 0.01401,-0.468116 0.01401,-0.468116 0,0 2.689404,-0.615043 4.024277,-0.348254 3.971223,0.793694 6.376818,4.542477 6.621719,8.22748 0.22856,3.439116 -1.346638,6.936502 -2.829341,7.530166 -1.540856,0.6169493 -5.307973,0.7932893 -7.513187,0.4336973 -0.255776,-0.06767 -0.317477,-0.3174773 -0.317477,-0.5442473 z"
|
||||||
|
id="path182"
|
||||||
|
sodipodi:nodetypes="ssccssccsscscssccccscccscssscssscss"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:none" /></g><g
|
||||||
|
aria-label="N"
|
||||||
|
transform="matrix(0.38001185,0,0,0.4006599,13.218138,21.430173)"
|
||||||
|
id="text1110"
|
||||||
|
style="font-size:38.0513px;font-family:'Trajan Pro';-inkscape-font-specification:'Trajan Pro, Normal';fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.940287"><path
|
||||||
|
d="m 22.414401,37.274291 c 1.243139,1.514945 5.81269,6.05318 10.379623,10.833775 4.364789,4.318207 6.961266,6.91021 8.199807,7.991161 1.238541,1.080951 2.397343,1.975644 3.24444,2.57881 0.07572,0.05392 0.15735,0.114427 0.242633,0.1764 0.56385,-0.0034 0.950635,-0.0019 1.504297,-0.0026 0.261974,-1.121488 0.281445,-3.783724 0.358758,-5.638495 0.07731,-1.854772 0.01971,-15.78434 0.01971,-17.515146 0,-1.730806 -0.02084,-2.516081 0.363787,-2.784029 0.384623,-0.267948 1.336578,-0.485128 0.505465,-1.36189 -1.647956,0.263864 -2.010206,0.262537 -2.844798,0.275326 -0.48706,0.0075 -1.48186,-0.155409 -2.774993,-0.25383 -0.755765,-0.05752 -1.62804,-0.10802 -1.623384,0.09869 0.0024,0.1058 0.285853,0.330921 1.047234,0.45307 0.838447,0.134514 1.707331,0.500405 1.949097,0.662222 0.16803,0.112465 0.437192,0.240074 0.5582,1.233763 0.151509,1.244165 0.137256,3.585901 0.162047,5.979779 0.04459,4.305819 0.06233,13.759575 0.06233,13.759575 -0.4581,-0.603937 -5.718922,-6.1178 -9.196404,-9.396197 C 31.094764,41.086289 24.086012,34.19445 23.515242,33.547578 c -0.380513,-0.380513 -2.632825,-2.64806 -2.937235,-2.64806 -0.304411,0 -0.368406,1.366359 -0.438525,3.182891 l -0.02043,18.736025 c -0.0015,1.355312 0.0063,3.496141 -0.2831,4.102219 -0.24455,0.512228 -0.663553,0.840682 -1.010746,1.09283 -0.600027,0.435769 -0.84232,0.511275 -1.605676,0.849836 3.039255,-0.0011 7.276282,0.0021 8.555603,0.002 0.100344,-0.378259 -0.624145,-0.30993 -1.057851,-0.414193 -0.576724,-0.138643 -1.089876,-0.175886 -1.403964,-0.600481 -0.393603,-0.532085 -0.518224,-1.562119 -0.561263,-2.903563 0,0 -0.104046,-4.500998 -0.132072,-7.159344 -0.02803,-2.658346 -0.214145,-6.315221 -0.249008,-7.786997 -0.03486,-1.471775 0.04343,-2.726411 0.04343,-2.726411 z"
|
||||||
|
id="path383"
|
||||||
|
sodipodi:nodetypes="cczscczzzcsssssssczcscsssccssszzcc"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:none" /></g><g
|
||||||
|
aria-label="&"
|
||||||
|
id="text442"
|
||||||
|
style="font-weight:bold;font-size:5.64444px;font-family:'Trajan Pro';-inkscape-font-specification:'Trajan Pro, Bold';fill:#ffffff;stroke-width:0.79375"
|
||||||
|
transform="matrix(0.39019932,0,0,0.39019932,13.911529,21.165112)"><path
|
||||||
|
d="m 25.991961,31.656869 c 0,0.491066 0.361244,1.247421 1.569154,1.247421 0.773288,0 1.151466,-0.344311 1.337732,-0.513644 0.485422,0.372533 0.688622,0.434622 1.332088,0.434622 h 0.434622 c 0.124178,0 0.169333,-0.01693 0.169333,-0.0508 0,-0.04516 -0.02822,-0.0508 -0.07902,-0.0508 -0.112889,0 -0.423333,-0.09031 -0.603955,-0.197556 -0.1524,-0.09031 -0.4064,-0.242711 -0.784577,-0.553155 0.321733,-0.378177 0.553155,-0.90311 0.553155,-1.337732 0,-0.06773 -0.08467,-0.1016 -0.191911,-0.112889 -0.316089,-0.03387 -0.541866,-0.03951 -0.677333,-0.03951 -0.04516,0 -0.09031,0.01129 -0.09031,0.05644 0,0.03387 0.03387,0.04516 0.06773,0.04516 0.08467,0 0.203199,0.01693 0.276577,0.08467 0.08467,0.07338 0.107244,0.174977 0.107244,0.259644 0,0.344311 -0.158044,0.711199 -0.270933,0.869244 -0.107244,-0.09596 -0.80151,-0.722489 -0.993421,-0.942622 -0.451555,-0.502355 -0.936977,-0.95391 -0.936977,-1.484488 0,-0.479777 0.366888,-0.643466 0.603955,-0.643466 0.316089,0 0.513644,0.118533 0.626533,0.254 0.124177,0.146755 0.163689,0.327377 0.163689,0.474133 0,0.08467 0.0056,0.107244 0.03951,0.107244 0.03387,0 0.05644,-0.01129 0.06773,-0.09596 0.01693,-0.08467 0.06209,-0.536222 0.06209,-0.722489 0,-0.06773 0,-0.112888 -0.09596,-0.146755 -0.1524,-0.06209 -0.406399,-0.118533 -0.682977,-0.118533 -0.982133,0 -1.377243,0.592666 -1.371599,1.038577 0.0056,0.254 0.0508,0.485422 0.287866,0.80151 -0.338666,0.163689 -0.920043,0.677333 -0.920043,1.337733 z m 1.761065,0.891821 c -0.587022,0 -1.083732,-0.451555 -1.083732,-1.15711 0,-0.451555 0.197555,-0.716844 0.389466,-0.857955 0.09596,0.141111 0.4572,0.547511 0.603955,0.705555 0.146755,0.158044 0.688622,0.688622 1.015999,0.970844 -0.118533,0.112888 -0.491066,0.338666 -0.925688,0.338666 z"
|
||||||
|
id="path444" /></g><text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:8.80984px;font-family:'Minion Pro';-inkscape-font-specification:'Minion Pro, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:-1.0324px;fill:#1953a4;fill-opacity:1;stroke:#1953a4;stroke-width:0.02;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
x="38.754604"
|
||||||
|
y="37.825531"
|
||||||
|
id="text1960"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan1958"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:14.1783px;font-family:'Minion Pro';-inkscape-font-specification:'Minion Pro, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1953a4;fill-opacity:1;stroke:#1953a4;stroke-width:0.02;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
x="38.754604"
|
||||||
|
y="37.825531"
|
||||||
|
dx="0">Princeton & Rutgers Neurology</tspan></text><text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:14.1783px;font-family:'Minion Pro';-inkscape-font-specification:'Minion Pro, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:-0.407797px;fill:#1953a4;fill-opacity:1;stroke:#1953a4;stroke-width:0.02;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
x="121.98303"
|
||||||
|
y="44.08754"
|
||||||
|
id="text5617"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan5615"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:5.36848px;font-family:'Minion Pro';-inkscape-font-specification:'Minion Pro, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke:#1953a4;stroke-width:0.02;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
|
||||||
|
x="121.98303"
|
||||||
|
y="44.08754">A New Jersey Center Of Excellence</tspan></text></g></g></svg>
|
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 86 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 8.8 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 129 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 65 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 84 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 12 KiB |