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