adding ability to submit support tickets in-app.

added logos to the patient profile menu
added link to fundraising doc in-app
update fontawesome version.

fixes #272
This commit is contained in:
Jason Kulatunga 2023-10-09 16:38:17 -07:00
parent bbf5169a44
commit cae3afce72
No known key found for this signature in database
10 changed files with 289 additions and 9 deletions

View File

@ -0,0 +1,14 @@
package models
type SupportRequest struct {
FullName string `json:"full_name"`
Email string `json:"email"`
RequestContent string `json:"request_content"`
CurrentPage string `json:"current_page"`
DistType string `json:"dist_type"`
Version string `json:"version"`
Flavor string `json:"flavor"`
Os string `json:"os"`
Arch string `json:"arch"`
}

View File

@ -0,0 +1,58 @@
package handler
import (
"fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/gin-gonic/gin"
"io/ioutil"
"net/http"
"net/url"
)
func SupportRequest(c *gin.Context) {
var supportRequest models.SupportRequest
if err := c.ShouldBindJSON(&supportRequest); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
//submit support request to Google Form
//https://medium.com/front-end-augustus-study-notes/custom-google-form-en-f7be4c27a98b
//source: https://docs.google.com/forms/d/e/1FAIpQLSfFxttuzE4mYNtQxa2XxsHw3uyNsxUzE-BeYF4JXxoKku3R5A/viewform
formUrl := "https://docs.google.com/forms/u/0/d/e/1FAIpQLSfFxttuzE4mYNtQxa2XxsHw3uyNsxUzE-BeYF4JXxoKku3R5A/formResponse"
supportRequestResponse, err := http.PostForm(formUrl, url.Values{
"entry.1688458216": {supportRequest.FullName},
"entry.153181769": {supportRequest.Email},
"entry.1194157548": {supportRequest.RequestContent},
"entry.108410483": {supportRequest.CurrentPage},
"entry.1640090028": {supportRequest.DistType},
"entry.882116507": {supportRequest.Flavor},
"entry.1331679697": {supportRequest.Version},
"entry.164864077": {supportRequest.Arch},
"entry.1469583108": {supportRequest.Os},
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
defer supportRequestResponse.Body.Close()
body, err := ioutil.ReadAll(supportRequestResponse.Body)
if err != nil {
//handle read response error
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
} else if supportRequestResponse.StatusCode != 200 {
//handle non 200 response
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": fmt.Sprintf("status code error: %d %s", supportRequestResponse.StatusCode, supportRequestResponse.Status)})
return
}
fmt.Printf("%s\n", string(body))
c.JSON(http.StatusOK, gin.H{"success": true})
}

View File

@ -58,6 +58,7 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {
//r.GET("/cors/*proxyPath", handler.CORSProxy) //r.GET("/cors/*proxyPath", handler.CORSProxy)
//r.OPTIONS("/cors/*proxyPath", handler.CORSProxy) //r.OPTIONS("/cors/*proxyPath", handler.CORSProxy)
api.GET("/glossary/code", handler.GlossarySearchByCode) api.GET("/glossary/code", handler.GlossarySearchByCode)
api.POST("/support/request", handler.SupportRequest)
secure := api.Group("/secure").Use(middleware.RequireAuth()) secure := api.Group("/secure").Use(middleware.RequireAuth())
{ {

View File

@ -67,10 +67,121 @@
<span>Adminstrator</span> <span>Adminstrator</span>
</div><!-- az-header-profile --> </div><!-- az-header-profile -->
<a ngbTooltip="not yet implemented" class="dropdown-item"><i class="typcn typcn-time"></i> Activity Logs</a> <a (click)="openSupportForm(content)" class="dropdown-item cursor-pointer"><i style="font-size: medium;" class="fas fa-question-circle"></i> Get Support</a>
<a (click)="signOut($event)" class="dropdown-item"><i class="typcn typcn-power-outline"></i> Sign Out</a> <a class="dropdown-item cursor-pointer" href="https://docs.fastenhealth.com/FUNDRAISING.html" externalLink><i style="font-size: medium;" class="fas fa-hand-holding-medical"></i> Donate</a>
<a (click)="signOut($event)" class="dropdown-item cursor-pointer"><i style="font-size: medium;" class="fas fa-power-off"></i> Sign Out</a>
</div><!-- dropdown-menu --> </div><!-- dropdown-menu -->
</div> </div>
</div><!-- az-header-right --> </div><!-- az-header-right -->
</div><!-- container --> </div><!-- container -->
</div><!-- az-header --> </div><!-- az-header -->
<ng-template #content let-modal>
<div class="modal-header">
<h4 class="modal-title">Get Support</h4>
<button type="button" class="btn close" aria-label="Close" (click)="modal.dismiss('Cross click')"><span aria-hidden="true">×</span></button>
</div>
<div class="modal-body">
<ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-0">
<ngb-panel title="File an Issue">
<ng-template ngbPanelContent>
<p>If you're having issues with Fasten Health, you can use this form to file a ticket.</p>
<div class="alert alert-warning" role="alert">
<strong>Your Privacy is Important</strong> None of the information provided in this form will be made public,
however we will anonymize & summarize the content before creating a <a href="https://github.com/fastenhealth/fasten-onprem/issues" externalLink><i class="fab fa-github"></i> Github Issue</a> for tracking purposes.
</div>
<form *ngIf="!submitSuccess; else supportRequestSuccess" (ngSubmit)="submitSupportForm()" #supportRequestForm="ngForm">
<div class="form-group">
<div class="row">
<div class="col-6">
<label class="az-content-label tx-11 tx-medium tx-gray-600">Name</label>
<input [(ngModel)]="newSupportRequest.full_name" name="full_name" #full_name="ngModel" required minlength="2" type="text" class="form-control" placeholder="Enter your name">
<div *ngIf="full_name.invalid && (full_name.dirty || full_name.touched)" class="alert alert-danger">
<div *ngIf="full_name.errors?.['required']">
Name is required.
</div>
<div *ngIf="full_name.errors?.['minlength']">
Name must be at least 2 characters long.
</div>
</div>
</div>
<div class="col-6">
<label class="az-content-label tx-11 tx-medium tx-gray-600">Email Address</label>
<input [(ngModel)]="newSupportRequest.email" email name="emailAddr" #emailAddr="ngModel" required minlength="4" type="text" class="form-control" autocapitalize="none" placeholder="Enter your email address">
<div *ngIf="emailAddr.invalid && (emailAddr.dirty || emailAddr.touched)" class="alert alert-danger">
<div *ngIf="emailAddr.errors?.['required']">
Email Address is required.
</div>
<div *ngIf="emailAddr.errors?.['minlength']">
Email Address must be at least 4 characters long.
</div>
<div *ngIf="emailAddr.errors?.['email']">
Email Address must be a valid email address.
</div>
</div>
</div>
</div>
</div><!-- form-group -->
<div class="form-group">
<label class="az-content-label tx-11 tx-medium tx-gray-600">Issue Description</label>
<textarea [(ngModel)]="newSupportRequest.request_content" name="requestContent" #requestContent="ngModel" rows="3" class="form-control" placeholder="Please provide a detailed description of the issue that you're encountering" required minlength="4"></textarea>
<div *ngIf="requestContent.invalid && (requestContent.dirty || requestContent.touched)" class="alert alert-danger">
<div *ngIf="requestContent.errors?.['required']">
Issue Description is required.
</div>
<div *ngIf="requestContent.errors?.['minlength']">
Issue Description must be at least 4 characters long.
</div>
</div>
</div><!-- form-group -->
<button [disabled]="!supportRequestForm.form.valid || loading" type="submit" class="btn btn-az-primary btn-block">Submit Issue</button>
<div *ngIf="errorMsg" class="alert alert-danger mt-3" role="alert">
<strong>Error</strong> {{errorMsg}}
</div>
</form>
<ng-template #supportRequestSuccess>
<div class="tx-center pd-y-20 pd-x-20">
<i class="far fa-check-square tx-100 tx-success lh-1 mg-t-20 d-inline-block"></i>
<h4 class="tx-success mg-b-20">Success!</h4>
<p class="mg-b-20 mg-x-20">Someone from the dev team will contact <br/>you shortly regarding your feedback. Thanks!</p>
<button type="button" (click)="resetSupportRequestForm()" class="btn btn-success pd-x-25">Continue</button>
</div>
</ng-template>
</ng-template>
</ngb-panel>
<ngb-panel title="Community Discussions & Support">
<ng-template ngbPanelContent>
<p>Whether you have a question or a suggestion, we are always happy to listen to you.</p>
<ul>
<li>
<a href="https://discord.gg/Bykz6BAN8p" externalLink><i class="fab fa-discord"></i> Discord Channel</a>
</li>
<li>
<a href="https://www.reddit.com/r/FastenHealth/" externalLink><i class="fab fa-reddit"></i> Reddit</a>
</li>
<li>
<a href="https://docs.fastenhealth.com/" externalLink><i class="fas fa-book"></i> Documentation</a>
</li>
</ul>
</ng-template>
</ngb-panel>
</ngb-accordion>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" (click)="modal.close('Close click')">Close</button>
</div>
</ng-template>

View File

@ -1,18 +1,37 @@
import { Component, OnInit } from '@angular/core'; import {Component, OnDestroy, OnInit} from '@angular/core';
import { Router } from '@angular/router'; import {ActivatedRoute, NavigationStart, Router} from '@angular/router';
import {AuthService} from '../../services/auth.service'; import {AuthService} from '../../services/auth.service';
import {UserRegisteredClaims} from '../../models/fasten/user-registered-claims'; import {UserRegisteredClaims} from '../../models/fasten/user-registered-claims';
import {FastenApiService} from '../../services/fasten-api.service'; import {FastenApiService} from '../../services/fasten-api.service';
import {BackgroundJob} from '../../models/fasten/background-job'; import {BackgroundJob} from '../../models/fasten/background-job';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {SupportRequest} from '../../models/fasten/support-request';
import {environment} from '../../../environments/environment';
import {versionInfo} from '../../../environments/versions';
import {Subscription} from 'rxjs';
import {ToastNotification, ToastType} from '../../models/fasten/toast';
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
templateUrl: './header.component.html', templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'] styleUrls: ['./header.component.scss']
}) })
export class HeaderComponent implements OnInit { export class HeaderComponent implements OnInit, OnDestroy {
current_user_claims: UserRegisteredClaims current_user_claims: UserRegisteredClaims
backgroundJobs: BackgroundJob[] = [] backgroundJobs: BackgroundJob[] = []
constructor(private authService: AuthService, private router: Router, private fastenApi: FastenApiService) { }
newSupportRequest: SupportRequest = null
loading: boolean = false
errorMsg: string = ""
submitSuccess: boolean = false
routerSubscription: Subscription = null
constructor(
private authService: AuthService,
private router: Router,
private fastenApi: FastenApiService,
private modalService: NgbModal) { }
ngOnInit() { ngOnInit() {
try { try {
@ -21,11 +40,25 @@ export class HeaderComponent implements OnInit {
this.current_user_claims = new UserRegisteredClaims() this.current_user_claims = new UserRegisteredClaims()
} }
this.fastenApi.getBackgroundJobs().subscribe((data) => { this.fastenApi.getBackgroundJobs().subscribe((data) => {
this.backgroundJobs = data.filter((job) => { this.backgroundJobs = data.filter((job) => {
return job.data?.checkpoint_data?.summary?.UpdatedResources?.length > 0 return job.data?.checkpoint_data?.summary?.UpdatedResources?.length > 0
}) })
}) })
this.resetSupportRequestForm()
this.routerSubscription = this.router.events.subscribe((event) => {
if (event instanceof NavigationStart) {
this.newSupportRequest.current_page = event.url.toString()
}
})
}
ngOnDestroy() {
if(this.routerSubscription){
this.routerSubscription.unsubscribe()
}
} }
closeMenu(e) { closeMenu(e) {
@ -42,4 +75,39 @@ export class HeaderComponent implements OnInit {
this.authService.Logout() this.authService.Logout()
.then(() => this.router.navigate(['auth/signin'])) .then(() => this.router.navigate(['auth/signin']))
} }
//support Form
openSupportForm(content) {
this.modalService.open(content, { size: 'lg' });
}
resetSupportRequestForm() {
this.submitSuccess = false
let newSupportRequest = new SupportRequest()
newSupportRequest.dist_type = environment.environment_desktop ? 'desktop' : 'docker'
newSupportRequest.flavor = environment.environment_name
newSupportRequest.version = versionInfo.version
newSupportRequest.current_page = this.router.url.toString()
this.newSupportRequest = newSupportRequest
}
submitSupportForm() {
console.log("submitting support form", this.newSupportRequest)
this.loading = false
this.fastenApi.supportRequest(this.newSupportRequest).subscribe((resp: any) => {
this.loading = false
console.log(resp);
this.submitSuccess = true
//show success toast? close modal?
},
(err)=>{
this.loading = false
console.error("an error occurred during support request submission",err)
this.errorMsg = err || "An error occurred while submitting your support request. Please try again later."
})
}
} }

View File

@ -78,7 +78,7 @@ import {ListOrganizationComponent} from './list-generic-resource/list-organizati
import {ListPractitionerComponent} from './list-generic-resource/list-practitioner.component' import {ListPractitionerComponent} from './list-generic-resource/list-practitioner.component'
import {ListProcedureComponent} from './list-generic-resource/list-procedure.component' import {ListProcedureComponent} from './list-generic-resource/list-procedure.component'
import {ListServiceRequestComponent} from './list-generic-resource/list-service-request.component'; import {ListServiceRequestComponent} from './list-generic-resource/list-service-request.component';
import {NgbCollapseModule, NgbModule, NgbDropdownModule} from '@ng-bootstrap/ng-bootstrap'; import {NgbCollapseModule, NgbModule, NgbDropdownModule, NgbAccordionModule} from '@ng-bootstrap/ng-bootstrap';
import {PipesModule} from '../pipes/pipes.module'; import {PipesModule} from '../pipes/pipes.module';
import {ResourceListOutletDirective} from './resource-list/resource-list-outlet.directive'; import {ResourceListOutletDirective} from './resource-list/resource-list-outlet.directive';
import {DirectivesModule} from '../directives/directives.module'; import {DirectivesModule} from '../directives/directives.module';
@ -91,6 +91,7 @@ import {DirectivesModule} from '../directives/directives.module';
NgbModule, NgbModule,
NgbDropdownModule, NgbDropdownModule,
NgbCollapseModule, NgbCollapseModule,
NgbAccordionModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
MomentModule, MomentModule,

View File

@ -0,0 +1,13 @@
export class SupportRequest {
full_name: string
email: string
request_content: string
current_page: string
dist_type: 'desktop' | 'docker' | 'cloud'
flavor: string
version: string
arch: string
os: string
}

View File

@ -25,6 +25,7 @@ import {DashboardWidgetQuery} from '../models/widget/dashboard-widget-query';
import {ResourceGraphResponse} from '../models/fasten/resource-graph-response'; import {ResourceGraphResponse} from '../models/fasten/resource-graph-response';
import { fetchEventSource } from '@microsoft/fetch-event-source'; import { fetchEventSource } from '@microsoft/fetch-event-source';
import {BackgroundJob} from '../models/fasten/background-job'; import {BackgroundJob} from '../models/fasten/background-job';
import {SupportRequest} from '../models/fasten/support-request';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -292,4 +293,17 @@ export class FastenApiService {
}) })
); );
} }
supportRequest(request: SupportRequest): Observable<any> {
return this._httpClient.post<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/support/request`, request)
.pipe(
map((response: ResponseWrapper) => {
console.log("Support request response", response)
// @ts-ignore
return {}
})
);
}
} }

View File

@ -11,7 +11,7 @@
<title>fastenhealth</title> <title>fastenhealth</title>
<!-- Fonts and icons --> <!-- Fonts and icons -->
<link href="https://fonts.googleapis.com/css?family=Poppins:200,300,400,600,700,800" rel="stylesheet"/> <link href="https://fonts.googleapis.com/css?family=Poppins:200,300,400,600,700,800" rel="stylesheet"/>
<link href="https://use.fontawesome.com/releases/v5.0.6/css/all.css" rel="stylesheet"/> <link href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" rel="stylesheet"/>
<link href="https://cdn.hello.coop/css/hello-btn.css" rel="stylesheet"/> <link href="https://cdn.hello.coop/css/hello-btn.css" rel="stylesheet"/>
<script src="https://cdn.hello.coop/js/hello-btn.js"></script> <script src="https://cdn.hello.coop/js/hello-btn.js"></script>

View File

@ -11,7 +11,7 @@
<title>fastenhealth</title> <title>fastenhealth</title>
<!-- Fonts and icons --> <!-- Fonts and icons -->
<link href="https://fonts.googleapis.com/css?family=Poppins:200,300,400,600,700,800" rel="stylesheet"/> <link href="https://fonts.googleapis.com/css?family=Poppins:200,300,400,600,700,800" rel="stylesheet"/>
<link href="https://use.fontawesome.com/releases/v5.0.6/css/all.css" rel="stylesheet"/> <link href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" rel="stylesheet"/>
<script src="./assets/js/asmcrypto-2.3.2.all.es5.min.js"></script> <script src="./assets/js/asmcrypto-2.3.2.all.es5.min.js"></script>
<script> <script>
var baseHref = "/" var baseHref = "/"