From c4bec9e52cf0dcf65a4f592071938d0a4805af95 Mon Sep 17 00:00:00 2001 From: Orkhan Kazymov <rayskarken@gmail.com> Date: Sat, 11 May 2024 03:07:20 +0300 Subject: [PATCH 1/4] + patient creation --- go.mod | 1 + internal/app/app.go | 12 +- internal/app/http/handlers/handlers.go | 14 +- internal/app/http/handlers/middlewares.go | 7 +- internal/app/http/handlers/patient.go | 39 ++++ internal/app/http/routs.go | 14 ++ internal/app/patient/patientrepo/repo.go | 42 ++++ .../app/patient/patientservice/service.go | 40 ++++ internal/domain/customerrors/access.go | 10 + internal/domain/patient.go | 25 ++ internal/views/base_layout.templ | 5 +- internal/views/base_layout_templ.go | 12 +- internal/views/home.templ | 87 +++++++ internal/views/home_templ.go | 221 ++++++++++++++++++ 14 files changed, 518 insertions(+), 11 deletions(-) create mode 100644 internal/app/http/handlers/patient.go create mode 100644 internal/app/patient/patientrepo/repo.go create mode 100644 internal/app/patient/patientservice/service.go create mode 100644 internal/domain/customerrors/access.go create mode 100644 internal/domain/patient.go create mode 100644 internal/views/home.templ create mode 100644 internal/views/home_templ.go diff --git a/go.mod b/go.mod index 4b295fc..7df4020 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22 require ( github.com/a-h/templ v0.2.663 github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/uuid v1.6.0 github.com/jmoiron/sqlx v1.3.5 github.com/lib/pq v1.2.0 github.com/pressly/goose/v3 v3.20.0 diff --git a/internal/app/app.go b/internal/app/app.go index d59b88c..72c31a3 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -10,6 +10,8 @@ import ( "github.com/pressly/goose/v3" http2 "github.com/radiologist-ai/web-app/internal/app/http" "github.com/radiologist-ai/web-app/internal/app/http/handlers" + "github.com/radiologist-ai/web-app/internal/app/patient/patientrepo" + "github.com/radiologist-ai/web-app/internal/app/patient/patientservice" "github.com/radiologist-ai/web-app/internal/app/users/usersrepo" "github.com/radiologist-ai/web-app/internal/app/users/usersservice" "github.com/radiologist-ai/web-app/internal/config" @@ -41,15 +43,23 @@ func Run(backgroundCtx context.Context, wg *sync.WaitGroup) error { if err != nil { return err } + patientRepo, err := patientrepo.New(db, logger) + if err != nil { + return err + } // service usersService, err := usersservice.New(logger, usersRepo) if err != nil { return err } + patientService, err := patientservice.New(logger, patientRepo) + if err != nil { + return err + } // handlers - handle, err := handlers.NewHandlers(logger, usersService, cfg.Server.Secret) + handle, err := handlers.NewHandlers(logger, usersService, patientService, cfg.Server.Secret) if err != nil { return err } diff --git a/internal/app/http/handlers/handlers.go b/internal/app/http/handlers/handlers.go index 6ae9dcd..7f77aaf 100644 --- a/internal/app/http/handlers/handlers.go +++ b/internal/app/http/handlers/handlers.go @@ -7,20 +7,24 @@ import ( ) type Handlers struct { - logger *zerolog.Logger - users domain.UsersService - secret []byte + logger *zerolog.Logger + users domain.UsersService + patients domain.PatientService + secret []byte } -func NewHandlers(logger *zerolog.Logger, users domain.UsersService, secret string) (*Handlers, error) { +func NewHandlers(logger *zerolog.Logger, users domain.UsersService, patients domain.PatientService, secret string) (*Handlers, error) { if logger == nil { return nil, errors.New("logger is required") } if users == nil { return nil, errors.New("users is required") } + if patients == nil { + return nil, errors.New("patients is required") + } if secret == "" { return nil, errors.New("secret is required") } - return &Handlers{logger: logger, users: users, secret: []byte(secret)}, nil + return &Handlers{logger: logger, users: users, patients: patients, secret: []byte(secret)}, nil } diff --git a/internal/app/http/handlers/middlewares.go b/internal/app/http/handlers/middlewares.go index 988ddcc..0efbd48 100644 --- a/internal/app/http/handlers/middlewares.go +++ b/internal/app/http/handlers/middlewares.go @@ -34,8 +34,11 @@ func (h *Handlers) WithCurrentUser(handler func(http.ResponseWriter, *http.Reque if err != nil { goto handle } - if user, ok, err = h.users.GetByEmail(r.Context(), email); err != nil || !ok { - h.logger.Error().Err(err).Bool("userExists", ok).Str("email", email).Str("token", token).Msg("error or user not found") + if user, ok, err = h.users.GetByEmail(r.Context(), email); err != nil { + h.logger.Error().Err(err).Str("email", email).Str("token", token).Msg("error") + goto handle + } else if !ok { + h.logger.Warn().Bool("userExists", ok).Str("email", email).Str("token", token).Msg("user not found") goto handle } r = r.WithContext(context.WithValue(r.Context(), domain.CurrentUserCtxKey, user)) diff --git a/internal/app/http/handlers/patient.go b/internal/app/http/handlers/patient.go new file mode 100644 index 0000000..fd79a50 --- /dev/null +++ b/internal/app/http/handlers/patient.go @@ -0,0 +1,39 @@ +package handlers + +import ( + "errors" + "github.com/radiologist-ai/web-app/internal/domain" + "github.com/radiologist-ai/web-app/internal/domain/customerrors" + "net/http" +) + +func (h *Handlers) PostPatientHandler(w http.ResponseWriter, r *http.Request) { + currentUser, ok := GetCurrentUser(r.Context()) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + var form domain.PatientRepoModel + form.Name = r.FormValue("name") + if form.Name == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + form.PatientIdentifier = r.FormValue("identifier") + + patient, err := h.patients.CreatePatient(r.Context(), *currentUser, form) + if err != nil { + h.logger.Error().Err(err).Msg("error creating patient") + if errors.Is(err, customerrors.AccessError) { + w.WriteHeader(http.StatusForbidden) + return + } + w.WriteHeader(http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/patients/"+patient.ID.String()+"/reports", http.StatusFound) + return +} + +// TODO get my patients list diff --git a/internal/app/http/routs.go b/internal/app/http/routs.go index f335de4..32cabdb 100644 --- a/internal/app/http/routs.go +++ b/internal/app/http/routs.go @@ -59,6 +59,20 @@ func NewRouter(handlers *handlers.Handlers) (*http.ServeMux, error) { handlers.AuthRequired( handlers.PostLogout))) + mux.HandleFunc("GET /home", + handlers.WithHTMLResponse( + handlers.WithCurrentUser( + templ.Handler( + views.Layout( + views.Home(), + "Home")). + ServeHTTP))) + + mux.HandleFunc("POST /my-patients", + handlers.WithCurrentUser( + handlers.AuthRequired( + handlers.PostPatientHandler))) + // technical mux.HandleFunc("GET /internal_server_error", handlers.WithHTMLResponse( diff --git a/internal/app/patient/patientrepo/repo.go b/internal/app/patient/patientrepo/repo.go new file mode 100644 index 0000000..2d88c6a --- /dev/null +++ b/internal/app/patient/patientrepo/repo.go @@ -0,0 +1,42 @@ +package patientrepo + +import ( + "context" + "errors" + "fmt" + "github.com/jmoiron/sqlx" + "github.com/radiologist-ai/web-app/internal/domain" + "github.com/radiologist-ai/web-app/internal/domain/customerrors" + "github.com/rs/zerolog" +) + +type PatientRepo struct { + db *sqlx.DB + logger *zerolog.Logger +} + +func (pr *PatientRepo) InsertPatient(ctx context.Context, patient domain.PatientRepoModel) (domain.PatientRepoModel, error) { + var res domain.PatientRepoModel + query := ` + INSERT INTO patients (creator_id, name, patient_identifier) + VALUES ($1, $2, $3) + RETURNING id, user_id, creator_id, name, patient_identifier, created_at, updated_at +` + if err := pr.db.QueryRowxContext(ctx, query, patient.CreatorID, patient.Name, patient.PatientIdentifier).StructScan(&res); err != nil { + return domain.PatientRepoModel{}, fmt.Errorf("%w%w", customerrors.InternalErrorSQL, err) + } + return res, nil +} + +func New(db *sqlx.DB, logger *zerolog.Logger) (*PatientRepo, error) { + if logger == nil { + return nil, errors.New("logger required") + } + if db == nil { + return nil, errors.New("db required") + } + return &PatientRepo{ + db: db, + logger: logger, + }, nil +} diff --git a/internal/app/patient/patientservice/service.go b/internal/app/patient/patientservice/service.go new file mode 100644 index 0000000..27698cc --- /dev/null +++ b/internal/app/patient/patientservice/service.go @@ -0,0 +1,40 @@ +package patientservice + +import ( + "context" + "fmt" + "github.com/radiologist-ai/web-app/internal/domain" + "github.com/radiologist-ai/web-app/internal/domain/customerrors" + "github.com/rs/zerolog" +) + +type PatientService struct { + repo domain.PatientRepository + logger *zerolog.Logger +} + +func (ps *PatientService) CreatePatient(ctx context.Context, creator domain.UserRepoModel, form domain.PatientRepoModel) (domain.PatientRepoModel, error) { + if !creator.IsDoctor { + return domain.PatientRepoModel{}, fmt.Errorf("%wonly doctor can create patient. ", customerrors.NeedToBeDoctor) + } + form.CreatorID = creator.ID + patient, err := ps.repo.InsertPatient(ctx, form) + if err != nil { + ps.logger.Error().Err(err).Msg("patient creation failed") + return domain.PatientRepoModel{}, err + } + return patient, nil +} + +func New(logger *zerolog.Logger, repo domain.PatientRepository) (*PatientService, error) { + if logger == nil { + return nil, fmt.Errorf("logger can not be nil") + } + if repo == nil { + return nil, fmt.Errorf("repo can not be nil") + } + return &PatientService{ + repo: repo, + logger: logger, + }, nil +} diff --git a/internal/domain/customerrors/access.go b/internal/domain/customerrors/access.go new file mode 100644 index 0000000..1215f8f --- /dev/null +++ b/internal/domain/customerrors/access.go @@ -0,0 +1,10 @@ +package customerrors + +import "fmt" + +var ( + AccessError = fmt.Errorf("access error. ") + AccessErrorNotEnoughRights = fmt.Errorf("%wnot enough rights for operation. ", AccessError) + NeedToBeDoctor = fmt.Errorf("%wonly doctor can perform this operation. ", AccessErrorNotEnoughRights) + NeedToBePatient = fmt.Errorf("%wonly patient can perform this operation. ", AccessErrorNotEnoughRights) +) diff --git a/internal/domain/patient.go b/internal/domain/patient.go new file mode 100644 index 0000000..d8d6fe8 --- /dev/null +++ b/internal/domain/patient.go @@ -0,0 +1,25 @@ +package domain + +import ( + "context" + "github.com/google/uuid" + "time" +) + +type PatientRepoModel struct { + ID uuid.UUID `db:"id"` + UserID *int `db:"user_id"` + CreatorID int `db:"creator_id"` + Name string `db:"name"` + PatientIdentifier string `db:"patient_identifier"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type PatientService interface { + CreatePatient(ctx context.Context, creator UserRepoModel, form PatientRepoModel) (PatientRepoModel, error) +} + +type PatientRepository interface { + InsertPatient(ctx context.Context, patient PatientRepoModel) (PatientRepoModel, error) +} diff --git a/internal/views/base_layout.templ b/internal/views/base_layout.templ index 33ed9cb..051fb12 100644 --- a/internal/views/base_layout.templ +++ b/internal/views/base_layout.templ @@ -52,8 +52,9 @@ templ Nav(user *domain.UserRepoModel){ { user.FirstName } </a> <ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> - <li><a class="dropdown-item" href="/users/{user.ID}/reports">Library</a></li> - <li><a class="dropdown-item" href="/settings">Settings</a></li> + if !user.IsDoctor { + <li><a class="dropdown-item" href="/patient/me/reports">Library</a></li> + } <li><a class="dropdown-item btn-outline-danger" href="#" hx-post="/logout" hx-trigger="click" diff --git a/internal/views/base_layout_templ.go b/internal/views/base_layout_templ.go index 87c27e3..82736b7 100644 --- a/internal/views/base_layout_templ.go +++ b/internal/views/base_layout_templ.go @@ -117,7 +117,17 @@ func Nav(user *domain.UserRepoModel) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><ul class=\"dropdown-menu\" aria-labelledby=\"navbarDropdownMenuLink\"><li><a class=\"dropdown-item\" href=\"/users/{user.ID}/reports\">Library</a></li><li><a class=\"dropdown-item\" href=\"/settings\">Settings</a></li><li><a class=\"dropdown-item btn-outline-danger\" href=\"#\" hx-post=\"/logout\" hx-trigger=\"click\" hx-swap=\"outerHTML\" hx-target=\"#mainNav\">Log out</a></li></ul></li>") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><ul class=\"dropdown-menu\" aria-labelledby=\"navbarDropdownMenuLink\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !user.IsDoctor { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a class=\"dropdown-item\" href=\"/patient/me/reports\">Library</a></li>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a class=\"dropdown-item btn-outline-danger\" href=\"#\" hx-post=\"/logout\" hx-trigger=\"click\" hx-swap=\"outerHTML\" hx-target=\"#mainNav\">Log out</a></li></ul></li>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/views/home.templ b/internal/views/home.templ new file mode 100644 index 0000000..130575c --- /dev/null +++ b/internal/views/home.templ @@ -0,0 +1,87 @@ +package views + +import "github.com/radiologist-ai/web-app/internal/domain" +import "github.com/google/uuid" +import templ2 "github.com/a-h/templ" + +templ PatientCard(patient domain.PatientRepoModel) { + <div class="card" style="width: 18rem;"> + if patient.PatientIdentifier != "" { + <div class="card-header"> + { patient.PatientIdentifier } + </div> + } + <div class="card-body"> + <h5 class="card-title"> + { patient.Name } + </h5> + <a href={ templ2.SafeURL("/patients/" + patient.ID.String() + "/reports") } class="btn btn-primary">Open</a> + </div> + </div> +} + +templ PatientsList(patients []domain.PatientRepoModel) { + <div class="container"> + <div class="row"> + <h3>Patients</h3> + <div class="d-grid gap-2 d-md-flex"> + <button class="btn btn-primary" type="button" + data-bs-toggle="modal" data-bs-target="#createPatientModal">Add New</button> + </div> + </div> + for _, patient := range patients { + <div class="row"> + @PatientCard(patient) + </div> + } + </div> +} + +templ CreatePatientModal(currentUser *domain.UserRepoModel) { + <div class="modal fade" id="createPatientModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="exampleModalLabel">New Patient</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <form id="createPatientForm" class="row g-3 needs-validation" action="/my-patients" method="post"> + <div class="modal-body"> + <div class="col-12"> + <label for="inputPatientName" class="form-label">Name</label> + <input type="text" class="form-control" name="name" id="inputPatientName" required/> + </div> + <div class="col-12"> + <label for="inputPatientIdentifier" class="form-label">Identifier</label> + <input type="text" class="form-control" name="identifier" id="inputPatientIdentifier"/> + </div> + + </div> + <div class="modal-footer"> + <button type="submit" class="btn btn-primary">Submit</button> + </div> + </form> + </div> + </div> + </div> +} + +templ InnerHome(currentUser *domain.UserRepoModel) { + if currentUser != nil && currentUser.IsDoctor { + @CreatePatientModal(currentUser) + @PatientsList([]domain.PatientRepoModel{{Name: "asd"}, {Name: "qwe", PatientIdentifier: "ET798E72", ID: uuid.New()}}) + + } else if currentUser != nil && !currentUser.IsDoctor { + <h2> Placeholder TODO </h2> + /* TODO */ + } else { + + } +} + + +templ Home() { + <div class="container"> + @InnerHome(GetCurrentUser(ctx)) + </div> +} diff --git a/internal/views/home_templ.go b/internal/views/home_templ.go new file mode 100644 index 0000000..1ec258d --- /dev/null +++ b/internal/views/home_templ.go @@ -0,0 +1,221 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.663 +package views + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +import "github.com/radiologist-ai/web-app/internal/domain" +import "github.com/google/uuid" +import templ2 "github.com/a-h/templ" + +func PatientCard(patient domain.PatientRepoModel) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"card\" style=\"width: 18rem;\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if patient.PatientIdentifier != "" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"card-header\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(patient.PatientIdentifier) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\home.templ`, Line: 11, Col: 37} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"card-body\"><h5 class=\"card-title\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(patient.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\home.templ`, Line: 16, Col: 24} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h5><a href=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 templ.SafeURL = templ2.SafeURL("/patients/" + patient.ID.String() + "/reports") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4))) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"btn btn-primary\">Open</a></div></div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func PatientsList(patients []domain.PatientRepoModel) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var5 := templ.GetChildren(ctx) + if templ_7745c5c3_Var5 == nil { + templ_7745c5c3_Var5 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container\"><div class=\"row\"><h3>Patients</h3><div class=\"d-grid gap-2 d-md-flex\"><button class=\"btn btn-primary\" type=\"button\" data-bs-toggle=\"modal\" data-bs-target=\"#createPatientModal\">Add New</button></div></div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, patient := range patients { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"row\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = PatientCard(patient).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func CreatePatientModal(currentUser *domain.UserRepoModel) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var6 := templ.GetChildren(ctx) + if templ_7745c5c3_Var6 == nil { + templ_7745c5c3_Var6 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"modal fade\" id=\"createPatientModal\" tabindex=\"-1\" aria-labelledby=\"exampleModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"exampleModalLabel\">New Patient</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><form id=\"createPatientForm\" class=\"row g-3 needs-validation\" action=\"/my-patients\" method=\"post\"><div class=\"modal-body\"><div class=\"col-12\"><label for=\"inputPatientName\" class=\"form-label\">Name</label> <input type=\"text\" class=\"form-control\" name=\"name\" id=\"inputPatientName\" required></div><div class=\"col-12\"><label for=\"inputPatientIdentifier\" class=\"form-label\">Identifier</label> <input type=\"text\" class=\"form-control\" name=\"identifier\" id=\"inputPatientIdentifier\"></div></div><div class=\"modal-footer\"><button type=\"submit\" class=\"btn btn-primary\">Submit</button></div></form></div></div></div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func InnerHome(currentUser *domain.UserRepoModel) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var7 := templ.GetChildren(ctx) + if templ_7745c5c3_Var7 == nil { + templ_7745c5c3_Var7 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if currentUser != nil && currentUser.IsDoctor { + templ_7745c5c3_Err = CreatePatientModal(currentUser).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = PatientsList([]domain.PatientRepoModel{{Name: "asd"}, {Name: "qwe", PatientIdentifier: "ET798E72", ID: uuid.New()}}).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if currentUser != nil && !currentUser.IsDoctor { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h2>Placeholder TODO </h2>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func Home() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var8 := templ.GetChildren(ctx) + if templ_7745c5c3_Var8 == nil { + templ_7745c5c3_Var8 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = InnerHome(GetCurrentUser(ctx)).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} -- GitLab From e5f03ad014c3578529fc61f276ab06ec2f49e484 Mon Sep 17 00:00:00 2001 From: Orkhan Kazymov <rayskarken@gmail.com> Date: Sat, 11 May 2024 14:42:02 +0300 Subject: [PATCH 2/4] + patient listing --- internal/app/http/handlers/patient.go | 34 +++++++++++++++++-- internal/app/http/routs.go | 6 +--- internal/app/patient/patientrepo/repo.go | 14 ++++++++ .../app/patient/patientservice/service.go | 12 +++++++ internal/domain/patient.go | 2 ++ internal/views/home.templ | 18 +++++----- internal/views/home_templ.go | 23 +++++++------ 7 files changed, 81 insertions(+), 28 deletions(-) diff --git a/internal/app/http/handlers/patient.go b/internal/app/http/handlers/patient.go index fd79a50..759cfc8 100644 --- a/internal/app/http/handlers/patient.go +++ b/internal/app/http/handlers/patient.go @@ -4,6 +4,7 @@ import ( "errors" "github.com/radiologist-ai/web-app/internal/domain" "github.com/radiologist-ai/web-app/internal/domain/customerrors" + "github.com/radiologist-ai/web-app/internal/views" "net/http" ) @@ -17,7 +18,7 @@ func (h *Handlers) PostPatientHandler(w http.ResponseWriter, r *http.Request) { form.Name = r.FormValue("name") if form.Name == "" { w.WriteHeader(http.StatusBadRequest) - return + return // TODO render smth } form.PatientIdentifier = r.FormValue("identifier") @@ -28,7 +29,7 @@ func (h *Handlers) PostPatientHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) return } - w.WriteHeader(http.StatusInternalServerError) + http.Redirect(w, r, "/internal_server_error", http.StatusFound) return } @@ -36,4 +37,31 @@ func (h *Handlers) PostPatientHandler(w http.ResponseWriter, r *http.Request) { return } -// TODO get my patients list +func (h *Handlers) GetHomeHandler(w http.ResponseWriter, r *http.Request) { + currentUser, ok := GetCurrentUser(r.Context()) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + if currentUser.IsDoctor { + patients, err := h.patients.GetAll(r.Context(), *currentUser) + if err != nil { + h.logger.Error().Err(err).Msg("error getting patients") + http.Redirect(w, r, "/internal_server_error", http.StatusFound) + return + } + err = views.Layout(views.Home(patients), "My Patients").Render(r.Context(), w) + if err != nil { + h.logger.Error().Err(err).Msg("error rendering layout") + http.Redirect(w, r, "/internal_server_error", http.StatusFound) + } + return + } else { + err := views.Layout(views.Home(nil), "Home").Render(r.Context(), w) + if err != nil { + h.logger.Error().Err(err).Msg("error rendering layout") + http.Redirect(w, r, "/internal_server_error", http.StatusFound) + } + return + } +} diff --git a/internal/app/http/routs.go b/internal/app/http/routs.go index 32cabdb..ec86951 100644 --- a/internal/app/http/routs.go +++ b/internal/app/http/routs.go @@ -62,11 +62,7 @@ func NewRouter(handlers *handlers.Handlers) (*http.ServeMux, error) { mux.HandleFunc("GET /home", handlers.WithHTMLResponse( handlers.WithCurrentUser( - templ.Handler( - views.Layout( - views.Home(), - "Home")). - ServeHTTP))) + handlers.GetHomeHandler))) mux.HandleFunc("POST /my-patients", handlers.WithCurrentUser( diff --git a/internal/app/patient/patientrepo/repo.go b/internal/app/patient/patientrepo/repo.go index 2d88c6a..38175d6 100644 --- a/internal/app/patient/patientrepo/repo.go +++ b/internal/app/patient/patientrepo/repo.go @@ -28,6 +28,20 @@ func (pr *PatientRepo) InsertPatient(ctx context.Context, patient domain.Patient return res, nil } +func (pr *PatientRepo) SelectAll(ctx context.Context, userID int) ([]domain.PatientRepoModel, error) { + res := make([]domain.PatientRepoModel, 0) + query := ` + SELECT id, user_id, creator_id, name, patient_identifier, created_at, updated_at + FROM patients WHERE creator_id=$1 + ORDER BY updated_at DESC +` + err := pr.db.SelectContext(ctx, &res, query, userID) + if err != nil { + return nil, fmt.Errorf("%w%w", customerrors.InternalErrorSQL, err) + } + return res, nil +} + func New(db *sqlx.DB, logger *zerolog.Logger) (*PatientRepo, error) { if logger == nil { return nil, errors.New("logger required") diff --git a/internal/app/patient/patientservice/service.go b/internal/app/patient/patientservice/service.go index 27698cc..182962e 100644 --- a/internal/app/patient/patientservice/service.go +++ b/internal/app/patient/patientservice/service.go @@ -26,6 +26,18 @@ func (ps *PatientService) CreatePatient(ctx context.Context, creator domain.User return patient, nil } +func (ps *PatientService) GetAll(ctx context.Context, currentUser domain.UserRepoModel) ([]domain.PatientRepoModel, error) { + if !currentUser.IsDoctor { + return nil, fmt.Errorf("%wonly doctor can create patient. ", customerrors.NeedToBeDoctor) + } + res, err := ps.repo.SelectAll(ctx, currentUser.ID) + if err != nil { + ps.logger.Error().Err(err).Int("user_id", currentUser.ID).Msg("patient query failed") + return nil, err + } + return res, nil +} + func New(logger *zerolog.Logger, repo domain.PatientRepository) (*PatientService, error) { if logger == nil { return nil, fmt.Errorf("logger can not be nil") diff --git a/internal/domain/patient.go b/internal/domain/patient.go index d8d6fe8..a9c87d8 100644 --- a/internal/domain/patient.go +++ b/internal/domain/patient.go @@ -18,8 +18,10 @@ type PatientRepoModel struct { type PatientService interface { CreatePatient(ctx context.Context, creator UserRepoModel, form PatientRepoModel) (PatientRepoModel, error) + GetAll(ctx context.Context, currentUser UserRepoModel) ([]PatientRepoModel, error) } type PatientRepository interface { InsertPatient(ctx context.Context, patient PatientRepoModel) (PatientRepoModel, error) + SelectAll(ctx context.Context, userID int) ([]PatientRepoModel, error) } diff --git a/internal/views/home.templ b/internal/views/home.templ index 130575c..369f36f 100644 --- a/internal/views/home.templ +++ b/internal/views/home.templ @@ -1,7 +1,6 @@ package views import "github.com/radiologist-ai/web-app/internal/domain" -import "github.com/google/uuid" import templ2 "github.com/a-h/templ" templ PatientCard(patient domain.PatientRepoModel) { @@ -23,14 +22,14 @@ templ PatientCard(patient domain.PatientRepoModel) { templ PatientsList(patients []domain.PatientRepoModel) { <div class="container"> <div class="row"> - <h3>Patients</h3> - <div class="d-grid gap-2 d-md-flex"> + <h3 class="mb-3">Patients</h3> + <div class="d-grid gap-2 d-md-flex mb-5"> <button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#createPatientModal">Add New</button> </div> </div> for _, patient := range patients { - <div class="row"> + <div class="row mb-3"> @PatientCard(patient) </div> } @@ -66,11 +65,12 @@ templ CreatePatientModal(currentUser *domain.UserRepoModel) { </div> } -templ InnerHome(currentUser *domain.UserRepoModel) { +templ InnerHome(currentUser *domain.UserRepoModel, patients []domain.PatientRepoModel) { if currentUser != nil && currentUser.IsDoctor { @CreatePatientModal(currentUser) - @PatientsList([]domain.PatientRepoModel{{Name: "asd"}, {Name: "qwe", PatientIdentifier: "ET798E72", ID: uuid.New()}}) - + if patients != nil { + @PatientsList(patients) + } } else if currentUser != nil && !currentUser.IsDoctor { <h2> Placeholder TODO </h2> /* TODO */ @@ -80,8 +80,8 @@ templ InnerHome(currentUser *domain.UserRepoModel) { } -templ Home() { +templ Home(patients []domain.PatientRepoModel) { <div class="container"> - @InnerHome(GetCurrentUser(ctx)) + @InnerHome(GetCurrentUser(ctx), patients) </div> } diff --git a/internal/views/home_templ.go b/internal/views/home_templ.go index 1ec258d..d51dd27 100644 --- a/internal/views/home_templ.go +++ b/internal/views/home_templ.go @@ -11,7 +11,6 @@ import "io" import "bytes" import "github.com/radiologist-ai/web-app/internal/domain" -import "github.com/google/uuid" import templ2 "github.com/a-h/templ" func PatientCard(patient domain.PatientRepoModel) templ.Component { @@ -39,7 +38,7 @@ func PatientCard(patient domain.PatientRepoModel) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(patient.PatientIdentifier) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\home.templ`, Line: 11, Col: 37} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\home.templ`, Line: 10, Col: 37} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -57,7 +56,7 @@ func PatientCard(patient domain.PatientRepoModel) templ.Component { var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(patient.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\home.templ`, Line: 16, Col: 24} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\home.templ`, Line: 15, Col: 24} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -96,12 +95,12 @@ func PatientsList(patients []domain.PatientRepoModel) templ.Component { templ_7745c5c3_Var5 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container\"><div class=\"row\"><h3>Patients</h3><div class=\"d-grid gap-2 d-md-flex\"><button class=\"btn btn-primary\" type=\"button\" data-bs-toggle=\"modal\" data-bs-target=\"#createPatientModal\">Add New</button></div></div>") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container\"><div class=\"row\"><h3 class=\"mb-3\">Patients</h3><div class=\"d-grid gap-2 d-md-flex mb-5\"><button class=\"btn btn-primary\" type=\"button\" data-bs-toggle=\"modal\" data-bs-target=\"#createPatientModal\">Add New</button></div></div>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, patient := range patients { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"row\">") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"row mb-3\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -149,7 +148,7 @@ func CreatePatientModal(currentUser *domain.UserRepoModel) templ.Component { }) } -func InnerHome(currentUser *domain.UserRepoModel) templ.Component { +func InnerHome(currentUser *domain.UserRepoModel, patients []domain.PatientRepoModel) templ.Component { return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) if !templ_7745c5c3_IsBuffer { @@ -171,9 +170,11 @@ func InnerHome(currentUser *domain.UserRepoModel) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = PatientsList([]domain.PatientRepoModel{{Name: "asd"}, {Name: "qwe", PatientIdentifier: "ET798E72", ID: uuid.New()}}).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err + if patients != nil { + templ_7745c5c3_Err = PatientsList(patients).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } } else if currentUser != nil && !currentUser.IsDoctor { _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h2>Placeholder TODO </h2>") @@ -188,7 +189,7 @@ func InnerHome(currentUser *domain.UserRepoModel) templ.Component { }) } -func Home() templ.Component { +func Home(patients []domain.PatientRepoModel) templ.Component { return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) if !templ_7745c5c3_IsBuffer { @@ -205,7 +206,7 @@ func Home() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = InnerHome(GetCurrentUser(ctx)).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = InnerHome(GetCurrentUser(ctx), patients).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } -- GitLab From 5b82bbd7dd86dc48f466653f804abbc4bb0c591b Mon Sep 17 00:00:00 2001 From: Orkhan Kazymov <rayskarken@gmail.com> Date: Sat, 11 May 2024 14:43:26 +0300 Subject: [PATCH 3/4] = todo done --- internal/app/users/usersservice/validation.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/app/users/usersservice/validation.go b/internal/app/users/usersservice/validation.go index 066f69d..44427be 100644 --- a/internal/app/users/usersservice/validation.go +++ b/internal/app/users/usersservice/validation.go @@ -37,7 +37,6 @@ func (s *Service) ValidatePassword(pwd string) error { return nil } -// TODO check if email already in use func (s *Service) validateRegisterForm(form domain.UserForm) error { if form.LastName == "" { return customerrors.ValidationErrorLastNameEmpty -- GitLab From 9b7df770bd6681a12b0404dd95ecbcd8bdea8c7a Mon Sep 17 00:00:00 2001 From: Orkhan Kazymov <rayskarken@gmail.com> Date: Sun, 12 May 2024 13:51:41 +0300 Subject: [PATCH 4/4] + patient account linking --- internal/app/http/handlers/patient.go | 90 ++++++++++++ internal/app/http/routs.go | 28 ++++ internal/app/patient/patientrepo/repo.go | 52 +++++++ .../app/patient/patientservice/service.go | 37 +++++ internal/domain/customerrors/common.go | 7 + internal/domain/customerrors/validation.go | 1 + internal/domain/patient.go | 51 ++++--- internal/views/base_layout.templ | 8 +- internal/views/base_layout_templ.go | 10 +- internal/views/link_account_page.templ | 77 +++++++++++ internal/views/patient_page.templ | 28 ++++ internal/views/patient_page_templ.go | 130 ++++++++++++++++++ 12 files changed, 494 insertions(+), 25 deletions(-) create mode 100644 internal/domain/customerrors/common.go create mode 100644 internal/views/link_account_page.templ create mode 100644 internal/views/patient_page.templ create mode 100644 internal/views/patient_page_templ.go diff --git a/internal/app/http/handlers/patient.go b/internal/app/http/handlers/patient.go index 759cfc8..9d9cb18 100644 --- a/internal/app/http/handlers/patient.go +++ b/internal/app/http/handlers/patient.go @@ -2,12 +2,102 @@ package handlers import ( "errors" + "github.com/google/uuid" "github.com/radiologist-ai/web-app/internal/domain" "github.com/radiologist-ai/web-app/internal/domain/customerrors" "github.com/radiologist-ai/web-app/internal/views" "net/http" ) +func (h *Handlers) GetMyAccountsHandler(w http.ResponseWriter, r *http.Request) { + h.logger.Info().Any("path", r.URL).Msg("request received GetMyAccountsHandler") + currentUser, ok := GetCurrentUser(r.Context()) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + accounts, err := h.patients.GetAllByUser(r.Context(), *currentUser) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if err := views.ListOfAccounts(accounts).Render(r.Context(), w); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + return +} + +func (h *Handlers) PostLinkAccountHandler(w http.ResponseWriter, r *http.Request) { + currentUser, ok := GetCurrentUser(r.Context()) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + var ( + comment string + success bool + ) + patientID := r.FormValue("patientID") + if patientID == "" { + comment = "Patient ID required" + goto renderHTML + } + + if err := h.patients.LinkPatient(r.Context(), *currentUser, patientID); err != nil { + if errors.Is(err, customerrors.NotFoundError) { + comment = "Patient account for this code not found" + goto renderHTML + } + if errors.Is(err, customerrors.ValidationErrorUUID) { + comment = "Invalid code" + goto renderHTML + } + comment = "Internal Error" + goto renderHTML + } + comment = "Patient account created" + success = true + +renderHTML: + if err := views.LinkAccountForm(comment, success).Render(r.Context(), w); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } +} + +func (h *Handlers) GetPatientHandler(w http.ResponseWriter, r *http.Request) { + currentUser, ok := GetCurrentUser(r.Context()) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + patientID := r.PathValue("patientID") + if patientID == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + patientUUID, err := uuid.Parse(patientID) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + patient, err := h.patients.GetOne(r.Context(), *currentUser, patientUUID) + if err != nil { + if errors.Is(err, customerrors.NotFoundError) { + w.WriteHeader(http.StatusNotFound) + } + http.Redirect(w, r, "/internal_server_error", http.StatusFound) + return + } + if err := views.Layout(views.PatientInfo(patient), patient.Name).Render(r.Context(), w); err != nil { + http.Redirect(w, r, "/internal_server_error", http.StatusFound) + return + } + +} + func (h *Handlers) PostPatientHandler(w http.ResponseWriter, r *http.Request) { currentUser, ok := GetCurrentUser(r.Context()) if !ok { diff --git a/internal/app/http/routs.go b/internal/app/http/routs.go index ec86951..ce8293e 100644 --- a/internal/app/http/routs.go +++ b/internal/app/http/routs.go @@ -69,6 +69,34 @@ func NewRouter(handlers *handlers.Handlers) (*http.ServeMux, error) { handlers.AuthRequired( handlers.PostPatientHandler))) + mux.HandleFunc("GET /patients/{patientID}/reports", + handlers.WithHTMLResponse( + handlers.WithCurrentUser( + handlers.AuthRequired( + handlers.GetPatientHandler)))) + + mux.HandleFunc("GET /link-account", + handlers.WithHTMLResponse( + handlers.WithCurrentUser( + handlers.AuthRequired( + templ.Handler( + views.Layout( + views.LinkAccountPage(), + "Link account")). + ServeHTTP)))) + + mux.HandleFunc("POST /link-account", + handlers.WithHTMLResponse( + handlers.WithCurrentUser( + handlers.AuthRequired( + handlers.PostLinkAccountHandler)))) + + mux.HandleFunc("GET /my-accounts", + handlers.WithHTMLResponse( + handlers.WithCurrentUser( + handlers.AuthRequired( + handlers.GetMyAccountsHandler)))) + // technical mux.HandleFunc("GET /internal_server_error", handlers.WithHTMLResponse( diff --git a/internal/app/patient/patientrepo/repo.go b/internal/app/patient/patientrepo/repo.go index 38175d6..cfb9b93 100644 --- a/internal/app/patient/patientrepo/repo.go +++ b/internal/app/patient/patientrepo/repo.go @@ -2,8 +2,10 @@ package patientrepo import ( "context" + "database/sql" "errors" "fmt" + "github.com/google/uuid" "github.com/jmoiron/sqlx" "github.com/radiologist-ai/web-app/internal/domain" "github.com/radiologist-ai/web-app/internal/domain/customerrors" @@ -15,6 +17,56 @@ type PatientRepo struct { logger *zerolog.Logger } +func (pr *PatientRepo) LinkPatient(ctx context.Context, user domain.UserRepoModel, patientID uuid.UUID) error { + query := `UPDATE patients SET user_id = $1 WHERE id = $2 AND user_id IS NULL` + res, err := pr.db.ExecContext(ctx, query, user.ID, patientID) + if err != nil { + return fmt.Errorf("%w%w", customerrors.InternalErrorSQL, err) + } + affected, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("%w%w", customerrors.InternalError, err) + } + if affected == 0 { + return customerrors.NotFoundError + } + return nil +} + +func (pr *PatientRepo) SelectAllByUser(ctx context.Context, userID int) ([]domain.PatientAccountInfo, error) { + res := make([]domain.PatientAccountInfo, 0) + query := ` + SELECT + p.id as id, + p.name as name, + p.patient_identifier as patient_identifier, + u.first_name || ' ' || u.last_name as doctor_full_name + FROM patients AS p INNER JOIN + users AS u ON u.id = p.creator_id + WHERE p.user_id = $1 AND p.creator_id != $1 +` + err := pr.db.SelectContext(ctx, &res, query, userID) + if err != nil { + return nil, fmt.Errorf("%w%w", customerrors.InternalErrorSQL, err) + } + return res, nil +} + +func (pr *PatientRepo) SelectOne(ctx context.Context, userID int, patientID uuid.UUID) (domain.PatientRepoModel, error) { + var res domain.PatientRepoModel + query := `SELECT id, user_id, creator_id, name, patient_identifier, created_at, updated_at + FROM patients WHERE creator_id = $1 AND id = $2` + err := pr.db.QueryRowxContext(ctx, query, userID, patientID).StructScan(&res) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return domain.PatientRepoModel{}, fmt.Errorf("%w%w", customerrors.NotFoundError, err) + } + pr.logger.Error().Err(err).Any("patientID", patientID).Int("doctorID", userID).Msg("error selecting patient") + return domain.PatientRepoModel{}, fmt.Errorf("%w%w", customerrors.InternalErrorSQL, err) + } + return res, nil +} + func (pr *PatientRepo) InsertPatient(ctx context.Context, patient domain.PatientRepoModel) (domain.PatientRepoModel, error) { var res domain.PatientRepoModel query := ` diff --git a/internal/app/patient/patientservice/service.go b/internal/app/patient/patientservice/service.go index 182962e..9297f22 100644 --- a/internal/app/patient/patientservice/service.go +++ b/internal/app/patient/patientservice/service.go @@ -3,6 +3,7 @@ package patientservice import ( "context" "fmt" + "github.com/google/uuid" "github.com/radiologist-ai/web-app/internal/domain" "github.com/radiologist-ai/web-app/internal/domain/customerrors" "github.com/rs/zerolog" @@ -13,6 +14,30 @@ type PatientService struct { logger *zerolog.Logger } +func (ps *PatientService) GetAllByUser(ctx context.Context, user domain.UserRepoModel) ([]domain.PatientAccountInfo, error) { + if user.IsDoctor { + return make([]domain.PatientAccountInfo, 0), customerrors.NeedToBePatient + } + result, err := ps.repo.SelectAllByUser(ctx, user.ID) + if err != nil { + ps.logger.Error().Err(err).Any("user", user).Ctx(ctx).Msg("patientService.GetAllByUser") + return nil, err + } + return result, nil +} + +func (ps *PatientService) LinkPatient(ctx context.Context, user domain.UserRepoModel, patientID string) error { + patientUUID, err := uuid.Parse(patientID) + if err != nil { + return customerrors.ValidationErrorUUID + } + if err := ps.repo.LinkPatient(ctx, user, patientUUID); err != nil { + ps.logger.Error().Err(err).Any("user", user).Str("patientID", patientID).Msg("failed to link patient") + return err + } + return nil +} + func (ps *PatientService) CreatePatient(ctx context.Context, creator domain.UserRepoModel, form domain.PatientRepoModel) (domain.PatientRepoModel, error) { if !creator.IsDoctor { return domain.PatientRepoModel{}, fmt.Errorf("%wonly doctor can create patient. ", customerrors.NeedToBeDoctor) @@ -38,6 +63,18 @@ func (ps *PatientService) GetAll(ctx context.Context, currentUser domain.UserRep return res, nil } +func (ps *PatientService) GetOne(ctx context.Context, currentUser domain.UserRepoModel, patientID uuid.UUID) (domain.PatientRepoModel, error) { + if !currentUser.IsDoctor { + return domain.PatientRepoModel{}, fmt.Errorf("%wonly doctor can create patient. ", customerrors.NeedToBeDoctor) + } + patient, err := ps.repo.SelectOne(ctx, currentUser.ID, patientID) + if err != nil { + ps.logger.Error().Err(err).Msg("patient query failed") + return domain.PatientRepoModel{}, err + } + return patient, nil +} + func New(logger *zerolog.Logger, repo domain.PatientRepository) (*PatientService, error) { if logger == nil { return nil, fmt.Errorf("logger can not be nil") diff --git a/internal/domain/customerrors/common.go b/internal/domain/customerrors/common.go new file mode 100644 index 0000000..b6ef111 --- /dev/null +++ b/internal/domain/customerrors/common.go @@ -0,0 +1,7 @@ +package customerrors + +import "errors" + +var ( + NotFoundError = errors.New("not found. ") +) diff --git a/internal/domain/customerrors/validation.go b/internal/domain/customerrors/validation.go index 29f6794..8e4b26b 100644 --- a/internal/domain/customerrors/validation.go +++ b/internal/domain/customerrors/validation.go @@ -20,4 +20,5 @@ var ( ValidationErrorFirstNameEmpty = fmt.Errorf("%wempty first name. ", ValidationErrorFirstName) ValidationErrorLastNameEmpty = fmt.Errorf("%wempty last name. ", ValidationErrorLastName) ValidationErrorJWT = fmt.Errorf("%winvalid JWT. ", ValidationError) + ValidationErrorUUID = fmt.Errorf("%winvalid UUID. ", ValidationError) ) diff --git a/internal/domain/patient.go b/internal/domain/patient.go index a9c87d8..ef0d8fb 100644 --- a/internal/domain/patient.go +++ b/internal/domain/patient.go @@ -6,22 +6,39 @@ import ( "time" ) -type PatientRepoModel struct { - ID uuid.UUID `db:"id"` - UserID *int `db:"user_id"` - CreatorID int `db:"creator_id"` - Name string `db:"name"` - PatientIdentifier string `db:"patient_identifier"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` -} +type ( + PatientRepoModel struct { + ID uuid.UUID `db:"id"` + UserID *int `db:"user_id"` + CreatorID int `db:"creator_id"` + Name string `db:"name"` + PatientIdentifier string `db:"patient_identifier"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + } -type PatientService interface { - CreatePatient(ctx context.Context, creator UserRepoModel, form PatientRepoModel) (PatientRepoModel, error) - GetAll(ctx context.Context, currentUser UserRepoModel) ([]PatientRepoModel, error) -} + PatientAccountInfo struct { + ID uuid.UUID `db:"id"` + Name string `db:"name"` + PatientIdentifier string `db:"patient_identifier"` + DoctorFullName string `db:"doctor_full_name"` + } +) + +type ( + PatientService interface { + CreatePatient(ctx context.Context, creator UserRepoModel, form PatientRepoModel) (PatientRepoModel, error) + GetAll(ctx context.Context, currentUser UserRepoModel) ([]PatientRepoModel, error) + GetOne(ctx context.Context, currentUser UserRepoModel, patientID uuid.UUID) (PatientRepoModel, error) + LinkPatient(ctx context.Context, user UserRepoModel, patientID string) error + GetAllByUser(ctx context.Context, user UserRepoModel) ([]PatientAccountInfo, error) + } -type PatientRepository interface { - InsertPatient(ctx context.Context, patient PatientRepoModel) (PatientRepoModel, error) - SelectAll(ctx context.Context, userID int) ([]PatientRepoModel, error) -} + PatientRepository interface { + InsertPatient(ctx context.Context, patient PatientRepoModel) (PatientRepoModel, error) + SelectAll(ctx context.Context, userID int) ([]PatientRepoModel, error) + SelectOne(ctx context.Context, userID int, patientID uuid.UUID) (PatientRepoModel, error) + SelectAllByUser(ctx context.Context, userID int) ([]PatientAccountInfo, error) + LinkPatient(ctx context.Context, user UserRepoModel, patientID uuid.UUID) error + } +) diff --git a/internal/views/base_layout.templ b/internal/views/base_layout.templ index 051fb12..fad2b44 100644 --- a/internal/views/base_layout.templ +++ b/internal/views/base_layout.templ @@ -48,20 +48,22 @@ templ Nav(user *domain.UserRepoModel){ </li> } else { <li class="nav-item dropdown"> + <div class="btn-group dropstart"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false"> { user.FirstName } </a> - <ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> + <ul class="dropdown-menu dropdown-menu-start" aria-labelledby="navbarDropdownMenuLink"> if !user.IsDoctor { - <li><a class="dropdown-item" href="/patient/me/reports">Library</a></li> + <li><a class="dropdown-item" href="/link-account">Link Patient Account</a></li> } - <li><a class="dropdown-item btn-outline-danger" href="#" + <li><a class="dropdown-item btn-outline-danger m-1" href="#" hx-post="/logout" hx-trigger="click" hx-swap="outerHTML" hx-target="#mainNav" >Log out</a></li> </ul> + </div> </li> } </ul> diff --git a/internal/views/base_layout_templ.go b/internal/views/base_layout_templ.go index 82736b7..91d138b 100644 --- a/internal/views/base_layout_templ.go +++ b/internal/views/base_layout_templ.go @@ -104,30 +104,30 @@ func Nav(user *domain.UserRepoModel) templ.Component { return templ_7745c5c3_Err } } else { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"nav-item dropdown\"><a class=\"nav-link dropdown-toggle\" href=\"#\" id=\"navbarDropdownMenuLink\" role=\"button\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\">") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"nav-item dropdown\"><div class=\"btn-group dropstart\"><a class=\"nav-link dropdown-toggle\" href=\"#\" id=\"navbarDropdownMenuLink\" role=\"button\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(user.FirstName) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\base_layout.templ`, Line: 52, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\base_layout.templ`, Line: 53, Col: 40} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><ul class=\"dropdown-menu\" aria-labelledby=\"navbarDropdownMenuLink\">") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><ul class=\"dropdown-menu dropdown-menu-start\" aria-labelledby=\"navbarDropdownMenuLink\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if !user.IsDoctor { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a class=\"dropdown-item\" href=\"/patient/me/reports\">Library</a></li>") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a class=\"dropdown-item\" href=\"/link-account\">Link Patient Account</a></li>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a class=\"dropdown-item btn-outline-danger\" href=\"#\" hx-post=\"/logout\" hx-trigger=\"click\" hx-swap=\"outerHTML\" hx-target=\"#mainNav\">Log out</a></li></ul></li>") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a class=\"dropdown-item btn-outline-danger m-1\" href=\"#\" hx-post=\"/logout\" hx-trigger=\"click\" hx-swap=\"outerHTML\" hx-target=\"#mainNav\">Log out</a></li></ul></div></li>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/views/link_account_page.templ b/internal/views/link_account_page.templ new file mode 100644 index 0000000..663832d --- /dev/null +++ b/internal/views/link_account_page.templ @@ -0,0 +1,77 @@ +package views + +import "github.com/google/uuid" +import "github.com/radiologist-ai/web-app/internal/domain" + +templ LinkAccountForm(comment string, success bool) { + <form id="linkAccountForm" class="form-control needs-validation" + hx-trigger="submit" + hx-target="#linkAccountForm" + hx-swap="outerHTML" + hx-post="/link-account" + > + <div class="mb-3"> + <label for="inputPatientID" class="form-label">Linking code (ID)</label> + <input name="patientID" placeholder={ uuid.UUID{}.String() } type="text" class="form-control" id="inputPatientID" aria-describedby="inputPatientIDHelp" required/> + <div id="inputPatientIDHelp" class="form-text">Put there code that your doctor gave you.</div> + </div> + + if comment != "" { + if success { + <div class="alert alert-success" role="alert"> + { comment } + </div> + } else { + <div class="alert alert-danger" role="alert"> + { comment } + </div> + } + } + + <button type="submit" class="btn btn-primary">Link account</button> + </form> +} + +templ ListOfAccounts(accounts []domain.PatientAccountInfo) { + <div id="listOfMyAccounts" + hx-trigger="load, submit from:form" + hx-get="/my-accounts" + hx-target="#innerAccountsList" + hx-swap="outerHTML" + hx-select="#innerAccountsList" + > + <div id="innerAccountsList"> + if accounts != nil { + <table class="table"> + <thead> + <tr> + <th scope="col">ID</th> + <th scope="col">Name</th> + <th scope="col">Patient Identifier</th> + <th scope="col">Doctor Name</th> + </tr> + </thead> + <tbody> + for _, acc := range accounts { + <tr> + <th scope="row"><code>{ acc.ID.String() }</code></th> + <td>{ acc.Name }</td> + <td>{ acc.PatientIdentifier }</td> + <td>{ acc.DoctorFullName }</td> + </tr> + } + </tbody> + </table> } + </div> + </div> +} + +templ LinkAccountPage() { + <div class="container mb-3"> + @LinkAccountForm("", false) + </div> + <div class="container mb-3"> + @ListOfAccounts(nil) + </div> + +} diff --git a/internal/views/patient_page.templ b/internal/views/patient_page.templ new file mode 100644 index 0000000..4500434 --- /dev/null +++ b/internal/views/patient_page.templ @@ -0,0 +1,28 @@ +package views + +import "github.com/radiologist-ai/web-app/internal/domain" + +templ PatientInfo(patient domain.PatientRepoModel) { + <div class="container m-5"> + <h1>Patient Profile</h1> + <div class="row"> + <div class="col"> + <p><strong>ID:</strong> <code>{ patient.ID.String() }</code> + if patient.UserID == nil { + <span class="badge bg-info text-dark ml-2"> + { "Copy this code and give it to your patient for account linking." } + </span> + } else { + <span class="badge bg-success ml-2"> Activated</span> + } + </p> + <p><strong>Name:</strong> { patient.Name }</p> + <p><strong>Patient Identifier:</strong> { patient.PatientIdentifier }</p> + </div> + <div class="col"> + <p><strong>Created At:</strong> { patient.CreatedAt.Format("2006-01-02 15:04") }</p> + <p><strong>Updated At:</strong> { patient.UpdatedAt.Format("2006-01-02 15:04") }</p> + </div> + </div> + </div> +} diff --git a/internal/views/patient_page_templ.go b/internal/views/patient_page_templ.go new file mode 100644 index 0000000..f8111b1 --- /dev/null +++ b/internal/views/patient_page_templ.go @@ -0,0 +1,130 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.663 +package views + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +import "github.com/radiologist-ai/web-app/internal/domain" + +func PatientInfo(patient domain.PatientRepoModel) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container m-5\"><h1>Patient Profile</h1><div class=\"row\"><div class=\"col\"><p><strong>ID:</strong> <code>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(patient.ID.String()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\patient_page.templ`, Line: 10, Col: 67} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</code> ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if patient.UserID == nil { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span class=\"badge bg-info text-dark ml-2\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs("Copy this code and give it to your patient for account linking.") + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\patient_page.templ`, Line: 13, Col: 87} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span class=\"badge bg-success ml-2\">Activated</span>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p><strong>Name:</strong> ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(patient.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\patient_page.templ`, Line: 19, Col: 56} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p><strong>Patient Identifier:</strong> ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(patient.PatientIdentifier) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\patient_page.templ`, Line: 20, Col: 83} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div><div class=\"col\"><p><strong>Created At:</strong> ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(patient.CreatedAt.Format("2006-01-02 15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\patient_page.templ`, Line: 23, Col: 94} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p><strong>Updated At:</strong> ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(patient.UpdatedAt.Format("2006-01-02 15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\patient_page.templ`, Line: 24, Col: 94} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div></div></div>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} -- GitLab