From 06b2b3478d29ae1543f4500b7cdf2705f7e2be99 Mon Sep 17 00:00:00 2001 From: Mohammad Amin Date: Tue, 31 Mar 2026 22:07:49 +0330 Subject: [PATCH 1/5] Implement patient list end point --- patientapp/app.go | 33 ++ patientapp/config.go | 4 + patientapp/delivery/http/analytic/.gitkeep | 0 .../delivery/http/analytic/dto/param.go | 70 ++++ patientapp/delivery/http/analytic/handler.go | 155 +++++++++ patientapp/delivery/http/analytic/helper.go | 70 ++++ patientapp/delivery/http/analytic/router.go | 17 + patientapp/delivery/http/analytic/server.go | 23 ++ patientapp/repository/database/.gitkeep | 0 .../repository/database/analytic_repo.go | 303 ++++++++++++++++++ patientapp/repository/database/dbconfig.yml | 10 + patientapp/repository/database/helper.go | 33 ++ patientapp/service/analytic/.gitkeep | 0 patientapp/service/analytic/helper.go | 32 ++ patientapp/service/analytic/patient_filter.go | 29 ++ patientapp/service/analytic/service.go | 128 ++++++++ patientapp/service/analytic/validator.go | 4 + patientapp/service/entity/address.go | 28 ++ patientapp/service/entity/map_summary.go | 17 + patientapp/service/entity/patient.go | 64 ++++ patientapp/tree.txt | Bin 0 -> 10 bytes 21 files changed, 1020 insertions(+) create mode 100644 patientapp/config.go delete mode 100644 patientapp/delivery/http/analytic/.gitkeep create mode 100644 patientapp/delivery/http/analytic/dto/param.go create mode 100644 patientapp/delivery/http/analytic/handler.go create mode 100644 patientapp/delivery/http/analytic/helper.go create mode 100644 patientapp/delivery/http/analytic/router.go create mode 100644 patientapp/delivery/http/analytic/server.go delete mode 100644 patientapp/repository/database/.gitkeep create mode 100644 patientapp/repository/database/analytic_repo.go create mode 100644 patientapp/repository/database/dbconfig.yml create mode 100644 patientapp/repository/database/helper.go delete mode 100644 patientapp/service/analytic/.gitkeep create mode 100644 patientapp/service/analytic/helper.go create mode 100644 patientapp/service/analytic/patient_filter.go create mode 100644 patientapp/service/analytic/service.go create mode 100644 patientapp/service/analytic/validator.go create mode 100644 patientapp/service/entity/address.go create mode 100644 patientapp/service/entity/map_summary.go create mode 100644 patientapp/service/entity/patient.go create mode 100644 patientapp/tree.txt diff --git a/patientapp/app.go b/patientapp/app.go index 29d637af..4400d98d 100644 --- a/patientapp/app.go +++ b/patientapp/app.go @@ -1 +1,34 @@ package patientapp + +import ( + "net/http" + + "git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic" + "git.gocasts.ir/ebhomengo/niki/patientapp/repository/database" +) + +type Application struct { + Config Config + HTTPServer *http.Server + DB *database.DataBase + Router http.Handler +} + +func Setup(config Config, server *http.Server, conn *database.DataBase) Application { + handler := analytic.NewHandler() + router := analytic.NewRouter(handler) + + return Application{ + Config: config, + HTTPServer: server, + DB: conn, + Router: router, + } +} + +func (a *Application) Start() { + srv := &http.Server{Addr: a.HTTPServer.Addr} + server := analytic.NewServer(srv, a.Router) + + _ = server.Serve() +} diff --git a/patientapp/config.go b/patientapp/config.go new file mode 100644 index 00000000..9bddc704 --- /dev/null +++ b/patientapp/config.go @@ -0,0 +1,4 @@ +package patientapp + +type Config struct { +} diff --git a/patientapp/delivery/http/analytic/.gitkeep b/patientapp/delivery/http/analytic/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/patientapp/delivery/http/analytic/dto/param.go b/patientapp/delivery/http/analytic/dto/param.go new file mode 100644 index 00000000..efd3bac6 --- /dev/null +++ b/patientapp/delivery/http/analytic/dto/param.go @@ -0,0 +1,70 @@ +package dto + +import ( + "time" + + "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" +) + +type ListPatientAnalyticRequest struct { + // All fields are optional + MinAge *int `json:"minAge,omitempty"` + MaxAge *int `json:"maxAge,omitempty"` + Sex *entity.Sex `json:"sex,omitempty"` + + City *int64 `json:"city,omitempty"` + Province *int64 `json:"province,omitempty"` + + Search *string `json:"search,omitempty"` + + Limit int `json:"limit,omitempty"` + Offset int `json:"offset,omitempty"` +} + +type PatientAnalyticItem struct { + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"Last_name"` + DateOfBirth *time.Time `json:"dob,omitempty"` + Sex entity.Sex `json:"sex"` + Phone string `json:"phone"` + Address entity.Address `json:"address"` +} +type PatientAnalyticResponse struct { + Items []PatientAnalyticItem `json:"items"` + Limit int `json:"limit"` + Offset int `json:"offset"` + Total int `json:"total"` +} + +func ToPatientResponse(patient entity.Patient) PatientAnalyticItem { + return PatientAnalyticItem{ + ID: patient.ID, + FirstName: patient.FirstName, + LastName: patient.LastName, + DateOfBirth: patient.DateOfBirth, + Sex: patient.Sex, + Phone: patient.Phone, + Address: entity.Address{ + ProvinceID: patient.Address.ProvinceID, + CityID: patient.Address.CityID, + }, + } +} + +// =========================== Map ================================== + +type GetPatientMapSummaryRequest struct { + Level entity.MapLevel `json:"level"` + ParentID *int `json:"parentID"` + + MinAge *int `json:"minAge,omitempty"` + MaxAge *int `json:"maxAge,omitempty"` + Sex *entity.Sex `json:"sex,omitempty"` + Search *string `json:"search,omitempty"` +} + +type GetPatientMapSummaryResponse struct { + Level entity.MapLevel `json:"level"` + Items []entity.MapSummaryItem `json:"items"` +} diff --git a/patientapp/delivery/http/analytic/handler.go b/patientapp/delivery/http/analytic/handler.go new file mode 100644 index 00000000..827db2c1 --- /dev/null +++ b/patientapp/delivery/http/analytic/handler.go @@ -0,0 +1,155 @@ +package analytic + +import ( + "fmt" + "log" + "net/http" + "strings" + + "git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic/dto" + svc "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic" +) + +type Handler struct { + service svc.Service +} + +func NewHandler(service svc.Service) *Handler { + return &Handler{ + service: service, + } +} + +func (h *Handler) Health(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) +} + +func (h *Handler) PatientsAnalytic(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + + minAge, err := parseIntPtr(q, "minAge") + if err != nil { + writeError(w, http.StatusBadRequest, "invalid minAge") + return + } + maxAge, err := parseIntPtr(q, "maxAge") + if err != nil { + writeError(w, http.StatusBadRequest, "invalid maxAge") + return + } + + sex, err := parseSexPtr(q, "sex") + if err != nil { + writeError(w, http.StatusBadRequest, "invalid sex") + return + } + + provinceID, err := parseIntPtr(q, "provinceId") + if err != nil { + writeError(w, http.StatusBadRequest, "invalid provinceId") + return + } + cityID, err := parseIntPtr(q, "cityId") + if err != nil { + writeError(w, http.StatusBadRequest, "invalid cityId") + return + } + + search := strings.TrimSpace(q.Get("search")) + var searchPtr *string + if search != "" { + searchPtr = &search + } + + limit, err := parseInt(q, "limit") + if err != nil { + writeError(w, http.StatusBadRequest, "invalid limit") + return + } + offset, err := parseInt(q, "offset") + if err != nil { + writeError(w, http.StatusBadRequest, "invalid offset") + return + } + + pId := int64(*provinceID) + cId := int64(*cityID) + + req := dto.ListPatientAnalyticRequest{ + MinAge: minAge, + MaxAge: maxAge, + Sex: sex, + Province: &pId, + City: &cId, + Search: searchPtr, + Limit: limit, + Offset: offset, + } + + response, err := h.service.List(r.Context(), req) + if err != nil { + log.Println("List error:", err) + writeError(w, http.StatusInternalServerError, fmt.Errorf("List error: %w", err).Error()) + return + } + + writeJSON(w, http.StatusOK, response) +} + +func (h *Handler) PatientsMapSummary(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + + level, err := parseMapLevel(q, "level") + if err != nil { + writeError(w, http.StatusBadRequest, "invalid level (city|province|country)") + return + } + + parentID, err := parseIntPtr(q, "parentId") + if err != nil { + writeError(w, http.StatusBadRequest, "invalid parentId") + return + } + + minAge, err := parseIntPtr(q, "minAge") + if err != nil { + writeError(w, http.StatusBadRequest, "invalid minAge") + return + } + maxAge, err := parseIntPtr(q, "maxAge") + if err != nil { + writeError(w, http.StatusBadRequest, "invalid maxAge") + return + } + + sex, err := parseSexPtr(q, "sex") + if err != nil { + writeError(w, http.StatusBadRequest, "invalid sex") + return + } + + search := strings.TrimSpace(q.Get("search")) + var searchPtr *string + if search != "" { + searchPtr = &search + } + + req := dto.GetPatientMapSummaryRequest{ + Level: level, + ParentID: parentID, + MinAge: minAge, + MaxAge: maxAge, + Sex: sex, + Search: searchPtr, + } + + resp, svcErr := h.service.GetMapSummary(r.Context(), req) + if svcErr != nil { + log.Println("GetMapSummary error:", svcErr) + writeError(w, http.StatusInternalServerError, svcErr.Error()) + return + } + + writeJSON(w, http.StatusOK, resp) +} diff --git a/patientapp/delivery/http/analytic/helper.go b/patientapp/delivery/http/analytic/helper.go new file mode 100644 index 00000000..9058409a --- /dev/null +++ b/patientapp/delivery/http/analytic/helper.go @@ -0,0 +1,70 @@ +package analytic + +import ( + "encoding/json" + "net/http" + "strconv" + "strings" + + "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" +) + +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(v) +} + +func writeError(w http.ResponseWriter, status int, msg string) { + writeJSON(w, status, map[string]any{"error": msg}) +} + +func parseInt(q urlValues, key string) (int, error) { + val := strings.TrimSpace(q.Get(key)) + if val == "" { + return 0, nil + } + return strconv.Atoi(val) +} + +type urlValues interface { + Get(key string) string +} + +func parseIntPtr(q urlValues, key string) (*int, error) { + val := strings.TrimSpace(q.Get(key)) + if val == "" { + return nil, nil + } + i, err := strconv.Atoi(val) + if err != nil { + return nil, err + } + return &i, nil +} + +func parseSexPtr(q urlValues, key string) (*entity.Sex, error) { + val := strings.TrimSpace(q.Get(key)) + if val == "" { + return nil, nil + } + s := entity.Sex(val) + if !s.SexValidation() { + return nil, strconv.ErrSyntax + } + return &s, nil +} + +func parseMapLevel(q urlValues, key string) (entity.MapLevel, error) { + val := strings.TrimSpace(q.Get(key)) + if val == "" { + return "", strconv.ErrSyntax + } + l := entity.MapLevel(val) + switch l { + case entity.MapLevelCity, entity.MapLevelProvince, entity.MapLevelCountry: + return l, nil + default: + return "", strconv.ErrSyntax + } +} diff --git a/patientapp/delivery/http/analytic/router.go b/patientapp/delivery/http/analytic/router.go new file mode 100644 index 00000000..6bc5b325 --- /dev/null +++ b/patientapp/delivery/http/analytic/router.go @@ -0,0 +1,17 @@ +package analytic + +import "net/http" + +func NewRouter(h *Handler) http.Handler { + mux := http.NewServeMux() + + // Define Routes + v1 := http.NewServeMux() + v1.HandleFunc("GET /health", h.Health) + v1.HandleFunc("GET /patients", h.PatientsAnalytic) + v1.HandleFunc("GET /patients-summary", h.PatientsMapSummary) + + mux.Handle("/v1/", http.StripPrefix("/v1", v1)) + + return mux +} diff --git a/patientapp/delivery/http/analytic/server.go b/patientapp/delivery/http/analytic/server.go new file mode 100644 index 00000000..4903eeb4 --- /dev/null +++ b/patientapp/delivery/http/analytic/server.go @@ -0,0 +1,23 @@ +package analytic + +import ( + "fmt" + "net/http" +) + +type Server struct { + HTTPServer *http.Server +} + +func NewServer(server *http.Server, router http.Handler) *Server { + server.Handler = router + return &Server{ + HTTPServer: server, + } +} + +func (s Server) Serve() error { + // Start server + fmt.Printf("start sever on %s \n", s.HTTPServer.Addr) + return s.HTTPServer.ListenAndServe() +} diff --git a/patientapp/repository/database/.gitkeep b/patientapp/repository/database/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/patientapp/repository/database/analytic_repo.go b/patientapp/repository/database/analytic_repo.go new file mode 100644 index 00000000..9c72ae0d --- /dev/null +++ b/patientapp/repository/database/analytic_repo.go @@ -0,0 +1,303 @@ +package database + +import ( + "context" + "database/sql" + "strings" + + "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic" + "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" +) + +type DataBase struct { + conn *mysql.DB +} + +func NewPatientRepo(conn *mysql.DB) *DataBase { + return &DataBase{ + conn: conn, + } +} + +func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) { + const op = "mysqlpatient.GetPatients" + limit, offset := clampLimitOffset(f.Limit, f.Offset) + + q := ` +SELECT + p.id, + p.first_name, + p.last_name, + p.date_of_birth, + p.sex, + p.phone, + + p.country_id, + p.province_id, + p.city_id, + p.postal_code, + p.address_line1, + p.address_line2, + + p.case_status, + p.referral_source, + p.assigned_staff_id, + p.start_date, + p.end_date +FROM patients p +WHERE p.deleted_at IS NULL +` + + stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) + if err != nil { + return nil, richerror.New(op).WithErr(err). + WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) + } + args := make([]any, 0) + + // filters + if f.Sex != nil { + q += " AND p.sex = ?" + args = append(args, string(*f.Sex)) + } + if f.DOBFrom != nil { + q += " AND p.date_of_birth >= ?" + args = append(args, f.DOBFrom.Format("2006-01-02")) + } + if f.DOBTo != nil { + q += " AND p.date_of_birth <= ?" + args = append(args, f.DOBTo.Format("2006-01-02")) + } + if f.Country != nil { + q += " AND p.country_id = ?" + args = append(args, *f.Country) + } + if f.Province != nil { + q += " AND p.province_id = ?" + args = append(args, *f.Province) + } + if f.City != nil { + q += " AND p.city_id = ?" + args = append(args, *f.City) + } + + if f.Search != nil && strings.TrimSpace(*f.Search) != "" { + term := "%" + strings.TrimSpace(*f.Search) + "%" + q += " AND (p.first_name LIKE ? OR p.last_name LIKE ? OR p.phone LIKE ?)" + args = append(args, term, term, term) + } + + q += " ORDER BY p.id DESC LIMIT ? OFFSET ?" + args = append(args, limit, offset) + + rows, err := stmt.QueryContext(ctx, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + out := make([]entity.Patient, 0, limit) + + for rows.Next() { + var p entity.Patient + var dob sql.NullTime + var provinceID, cityID uint + + if err := rows.Scan( + &p.ID, + &p.FirstName, + &p.LastName, + &dob, + &p.Sex, + &p.Phone, + + &provinceID, + &cityID, + &p.Address.PostalCode, + + &p.CaseStatus, + &p.ReferralSource, + &p.AssignedStaffId, + &p.StartDate, + &p.EndDate, + ); err != nil { + return nil, err + } + + if dob.Valid { + t := dob.Time + p.DateOfBirth = &t + } + + p.Address.ProvinceID = provinceID + p.Address.CityID = cityID + + out = append(out, p) + } + + return out, rows.Err() +} + +func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) { + const op = "mysqlpatient.CountPatients" + q := `SELECT COUNT(*) FROM patients p WHERE p.deleted_at IS NULL` + args := make([]any, 0) + + stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) + if err != nil { + return 0, richerror.New(op).WithErr(err). + WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) + } + + if f.Sex != nil { + q += " AND p.sex = ?" + args = append(args, string(*f.Sex)) + } + if f.DOBFrom != nil { + q += " AND p.date_of_birth >= ?" + args = append(args, f.DOBFrom.Format("2006-01-02")) + } + if f.DOBTo != nil { + q += " AND p.date_of_birth <= ?" + args = append(args, f.DOBTo.Format("2006-01-02")) + } + if f.Country != nil { + q += " AND p.country_id = ?" + args = append(args, *f.Country) + } + if f.Province != nil { + q += " AND p.province_id = ?" + args = append(args, *f.Province) + } + if f.City != nil { + q += " AND p.city_id = ?" + args = append(args, *f.City) + } + + var total int + + err = stmt.QueryRowContext(ctx, args...).Scan(&total) + return total, err +} + +func (db *DataBase) SummaryByCity(ctx context.Context, provinceID int, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) { + const op = "mysqlpatient.SummaryByCity" + q := ` +SELECT + c.id AS location_id, + c.name, + c.centroid_lat, + c.centroid_lng, + COUNT(p.id) AS count +FROM locations c +JOIN patients p ON p.city_id = c.id +WHERE c.level = 'city' + AND c.parent_id = ? + AND p.deleted_at IS NULL +` + + stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) + if err != nil { + return nil, richerror.New(op).WithErr(err). + WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) + } + args := []any{provinceID} + + if f.Sex != nil { + q += " AND p.sex = ?" + args = append(args, string(*f.Sex)) + } + if f.MinDOB != nil { + q += " AND p.date_of_birth >= ?" + args = append(args, f.MinDOB.Format("2006-01-02")) + } + if f.MaxDOB != nil { + q += " AND p.date_of_birth <= ?" + args = append(args, f.MaxDOB.Format("2006-01-02")) + } + if f.Search != nil && strings.TrimSpace(*f.Search) != "" { + term := "%" + strings.TrimSpace(*f.Search) + "%" + q += " AND (p.first_name LIKE ? OR p.last_name LIKE ? OR p.phone LIKE ?)" + args = append(args, term, term, term) + } + + q += ` GROUP BY c.id, c.name, c.centroid_lat, c.centroid_lng + ORDER BY count DESC` + + rows, err := stmt.QueryContext(ctx, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []entity.MapSummaryItem + for rows.Next() { + var it entity.MapSummaryItem + if err := rows.Scan(&it.LocationID, &it.Name, &it.CentroidLat, &it.CentroidLng, &it.Count); err != nil { + return nil, err + } + out = append(out, it) + } + return out, rows.Err() +} + +func (db *DataBase) SummaryByProvince(ctx context.Context, countryID int, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) { + const op = "mysqlpatient.SummaryByProvince" + + q := ` +SELECT + prov.id AS location_id, + prov.name, + prov.centroid_lat, + prov.centroid_lng, + COUNT(p.id) AS count +FROM patients p +JOIN locations city ON city.id = p.city_id AND city.level = 'city' +JOIN locations prov ON prov.id = city.parent_id AND prov.level = 'province' +WHERE prov.parent_id = ? + AND p.deleted_at IS NULL +` + + stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) + if err != nil { + return nil, richerror.New(op).WithErr(err). + WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) + } + + args := []any{countryID} + + if f.Sex != nil { + q += " AND p.sex = ?" + args = append(args, string(*f.Sex)) + } + if f.MinDOB != nil { + q += " AND p.date_of_birth >= ?" + args = append(args, f.MinDOB.Format("2006-01-02")) + } + if f.MaxDOB != nil { + q += " AND p.date_of_birth <= ?" + args = append(args, f.MaxDOB.Format("2006-01-02")) + } + + q += ` GROUP BY prov.id, prov.name, prov.centroid_lat, prov.centroid_lng + ORDER BY count DESC` + + rows, err := stmt.QueryContext(ctx, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []entity.MapSummaryItem + for rows.Next() { + var it entity.MapSummaryItem + if err := rows.Scan(&it.LocationID, &it.Name, &it.CentroidLat, &it.CentroidLng, &it.Count); err != nil { + return nil, err + } + out = append(out, it) + } + return out, rows.Err() +} diff --git a/patientapp/repository/database/dbconfig.yml b/patientapp/repository/database/dbconfig.yml new file mode 100644 index 00000000..1e5a8a42 --- /dev/null +++ b/patientapp/repository/database/dbconfig.yml @@ -0,0 +1,10 @@ +postgres_db: + driver: postgres + host: patient-db + port: 5432 + user: patient_admin + password: password1234 + db_name: patient_db + ssl_mode: disable + +path_of_migration: ./patientapp/repository/database/migrations \ No newline at end of file diff --git a/patientapp/repository/database/helper.go b/patientapp/repository/database/helper.go new file mode 100644 index 00000000..b0ea1e0a --- /dev/null +++ b/patientapp/repository/database/helper.go @@ -0,0 +1,33 @@ +package database + +import "strings" + +type whereClause struct { + sql strings.Builder + args []any +} + +func (w *whereClause) add(cond string, args ...any) { + if w.sql.Len() == 0 { + w.sql.WriteString(" WHERE ") + } else { + w.sql.WriteString(" AND ") + } + w.sql.WriteString(cond) + w.args = append(w.args, args...) +} + +func (w *whereClause) String() string { return w.sql.String() } + +func clampLimitOffset(limit, offset int) (int, int) { + if limit <= 0 { + limit = 50 + } + if limit > 100 { + limit = 100 + } + if offset < 0 { + offset = 0 + } + return limit, offset +} diff --git a/patientapp/service/analytic/.gitkeep b/patientapp/service/analytic/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/patientapp/service/analytic/helper.go b/patientapp/service/analytic/helper.go new file mode 100644 index 00000000..1e0b3df8 --- /dev/null +++ b/patientapp/service/analytic/helper.go @@ -0,0 +1,32 @@ +package analytic + +import "time" + +func normalizeLimitOffset(limit, offset int) (int, int) { + if limit <= 0 { + limit = 50 + } + if limit > 100 { + limit = 100 + } + if offset < 0 { + offset = 0 + } + return limit, offset +} + +// convert age range -> DOB range +func ageRangeToDOB(minAge, maxAge *int, now time.Time) (dobFrom, dobTo *time.Time) { + // maxAge => DOB >= now-(maxAge+1y)+1day + // eg: now is : 3/31/2026; maxAge=30 => (now - 31)= 3/31/1994 => +=1 + if maxAge != nil { + t := now.AddDate(-*maxAge-1, 0, 0).Add(24 * time.Hour) + dobFrom = &t + } + // minAge => DOB <= now-minAge + if minAge != nil { + t := now.AddDate(-*minAge, 0, 0) + dobTo = &t + } + return +} diff --git a/patientapp/service/analytic/patient_filter.go b/patientapp/service/analytic/patient_filter.go new file mode 100644 index 00000000..6ecaf1c0 --- /dev/null +++ b/patientapp/service/analytic/patient_filter.go @@ -0,0 +1,29 @@ +package analytic + +import ( + "time" + + "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" +) + +type PatientFilter struct { + DOBFrom *time.Time // born after + DOBTo *time.Time // born before + Sex *entity.Sex + + City *int64 + Province *int64 + Country *int64 + + Search *string + + Limit int + Offset int +} + +type PatientMapFilter struct { + MinDOB *time.Time + MaxDOB *time.Time + Sex *entity.Sex + Search *string +} diff --git a/patientapp/service/analytic/service.go b/patientapp/service/analytic/service.go new file mode 100644 index 00000000..2547fb82 --- /dev/null +++ b/patientapp/service/analytic/service.go @@ -0,0 +1,128 @@ +package analytic + +import ( + "context" + "errors" + "fmt" + "time" + + analytic2 "git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic/dto" + "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" +) + +var ( + ErrInvalidProvinceID = errors.New("invalid province id") + ErrInvalidCountryID = errors.New("invalid country id") + ErrInvalidMapLevel = errors.New("invalid map level") +) + +type Repository interface { + GetPatients(ctx context.Context, f PatientFilter) ([]entity.Patient, error) + CountPatients(ctx context.Context, f PatientFilter) (int, error) + + SummaryByCity(ctx context.Context, provinceID int, f PatientMapFilter) ([]entity.MapSummaryItem, error) + SummaryByProvince(ctx context.Context, countryID int, f PatientMapFilter) ([]entity.MapSummaryItem, error) + SummaryByCountry(ctx context.Context, f PatientMapFilter) ([]entity.MapSummaryItem, error) +} + +type Service struct { + repository Repository + validator Validator +} + +func NewPatientAnalyticService(repo Repository, validator Validator) Service { + return Service{ + repository: repo, + validator: validator, + } +} + +func (s Service) List(ctx context.Context, req analytic2.ListPatientAnalyticRequest) (analytic2.PatientAnalyticResponse, error) { + + limit, offset := normalizeLimitOffset(req.Limit, req.Offset) + + // convert age range + dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now()) + + filter := PatientFilter{ + DOBFrom: dobFrom, + DOBTo: dobTo, + Sex: req.Sex, + City: req.City, + Province: req.Province, + Search: req.Search, + Limit: limit, + Offset: offset, + } + + items, err := s.repository.GetPatients(ctx, filter) + if err != nil { + return analytic2.PatientAnalyticResponse{}, fmt.Errorf("GetPatients: %w", err) + } + + total, err := s.repository.CountPatients(ctx, filter) + if err != nil { + return analytic2.PatientAnalyticResponse{}, fmt.Errorf("CountPatients: %w", err) + } + + // mapping response + out := make([]analytic2.PatientAnalyticItem, 0, len(items)) + for _, value := range items { + out = append(out, analytic2.ToPatientResponse(value)) + } + + return analytic2.PatientAnalyticResponse{ + Items: out, + Limit: limit, + Offset: offset, + Total: total, + }, nil + +} + +func (s Service) GetMapSummary(ctx context.Context, req analytic2.GetPatientMapSummaryRequest) (analytic2.GetPatientMapSummaryResponse, error) { + + dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now()) + + filter := PatientMapFilter{ + MinDOB: dobFrom, + MaxDOB: dobTo, + Sex: req.Sex, + Search: req.Search, + } + + switch req.Level { + case entity.MapLevelCity: + if req.ParentID == nil || *req.ParentID <= 0 { + return analytic2.GetPatientMapSummaryResponse{}, ErrInvalidProvinceID + } + + items, err := s.repository.SummaryByCity(ctx, *req.ParentID, filter) + if err != nil { + return analytic2.GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByCity: %w", err) + } + return analytic2.GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil + + case entity.MapLevelProvince: + if req.ParentID == nil || *req.ParentID <= 0 { + return analytic2.GetPatientMapSummaryResponse{}, ErrInvalidCountryID + } + + items, err := s.repository.SummaryByProvince(ctx, *req.ParentID, filter) + if err != nil { + return analytic2.GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByProvince: %w", err) + } + return analytic2.GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil + + case entity.MapLevelCountry: + items, err := s.repository.SummaryByCountry(ctx, filter) + if err != nil { + return analytic2.GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByCountry: %w", err) + } + return analytic2.GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil + + default: + return analytic2.GetPatientMapSummaryResponse{}, ErrInvalidMapLevel + } + +} diff --git a/patientapp/service/analytic/validator.go b/patientapp/service/analytic/validator.go new file mode 100644 index 00000000..91079f66 --- /dev/null +++ b/patientapp/service/analytic/validator.go @@ -0,0 +1,4 @@ +package analytic + +type Validator struct { +} diff --git a/patientapp/service/entity/address.go b/patientapp/service/entity/address.go new file mode 100644 index 00000000..193531a9 --- /dev/null +++ b/patientapp/service/entity/address.go @@ -0,0 +1,28 @@ +package entity + +type Address struct { + ID uint + PostalCode string + Address string + Name string + Lat float64 + Lon float64 + CityID uint + ProvinceID uint +} + +type AddressAggregated struct { + Address Address + Province Province + City City +} + +type Province struct { + ID uint + Name string +} +type City struct { + ID uint + Name string + ProvinceID uint +} diff --git a/patientapp/service/entity/map_summary.go b/patientapp/service/entity/map_summary.go new file mode 100644 index 00000000..fcfd1284 --- /dev/null +++ b/patientapp/service/entity/map_summary.go @@ -0,0 +1,17 @@ +package entity + +type MapLevel string + +const ( + MapLevelCity MapLevel = "city" + MapLevelProvince MapLevel = "province" + MapLevelCountry MapLevel = "country" +) + +type MapSummaryItem struct { + LocationID int64 + Name string + Count int + CentroidLat float64 + CentroidLng float64 +} diff --git a/patientapp/service/entity/patient.go b/patientapp/service/entity/patient.go new file mode 100644 index 00000000..e508a8d4 --- /dev/null +++ b/patientapp/service/entity/patient.go @@ -0,0 +1,64 @@ +package entity + +import "time" + +type Patient struct { + ID int64 + FirstName string + LastName string + DateOfBirth *time.Time + Sex Sex + Phone string + Address Address + CaseStatus CaseStatus + ReferralSource ReferralSource + AssignedStaffId int64 + StartDate string + EndDate string +} + +// Sex ================================== Sex type ========================================== +type Sex string + +const ( + SexUnknown Sex = "unknown" + SexMale Sex = "male" + SexFemale Sex = "female" + SexOther Sex = "other" +) + +func (s Sex) SexValidation() bool { + switch s { + case SexUnknown, SexMale, SexFemale, SexOther: + return true + default: + return false + } +} + +// CaseStatus =================================== Case Status ======================================= +type CaseStatus string + +const ( + Open CaseStatus = "open" + Close CaseStatus = "close" + InProgress CaseStatus = "inProgress" +) + +func (s CaseStatus) CaseStatusValidation() bool { + switch s { + case Open, Close, InProgress: + return true + default: + return false + } +} + +// ReferralSource =================================== Referral source ======================================= +type ReferralSource string + +const ( + Hospital ReferralSource = "hospital" + Community ReferralSource = "community" + Other ReferralSource = "other" +) diff --git a/patientapp/tree.txt b/patientapp/tree.txt new file mode 100644 index 0000000000000000000000000000000000000000..d7d81fdbccf6e24c96c4bda0df0727e6050cc390 GIT binary patch literal 10 OcmezWkC%aq0fYe)+5#*9 literal 0 HcmV?d00001 From 3259a7468eba7373590f47e7752a0b4c89d8e884 Mon Sep 17 00:00:00 2001 From: Mohammad Amin Date: Thu, 2 Apr 2026 18:29:19 +0330 Subject: [PATCH 2/5] add echo web framework to patientapp domain --- delivery/http_server/end2end/setup/docker.go | 6 +- main.go | 2 +- patientapp/app.go | 35 +- patientapp/config.go | 4 - patientapp/config/config.go | 22 + patientapp/delivery/http/analytic/handler.go | 154 +- patientapp/delivery/http/analytic/helper.go | 70 - patientapp/delivery/http/analytic/router.go | 24 +- patientapp/delivery/http/analytic/server.go | 30 +- .../repository/database/analytic_repo.go | 303 --- patientapp/repository/database/helper.go | 33 - patientapp/repository/mysql/analytic_repo.go | 349 +++ .../{database => mysql}/dbconfig.yml | 2 +- patientapp/repository/mysql/helper.go | 56 + patientapp/repository/mysql/rawdata.json | 2202 +++++++++++++++++ patientapp/service/analytic/helper.go | 27 +- .../dto => service/analytic}/param.go | 6 +- patientapp/service/analytic/patient_filter.go | 10 +- patientapp/service/analytic/service.go | 37 +- patientapp/service/analytic/validator.go | 4 + patientapp/service/entity/patient.go | 4 +- patientapp/tree.txt | Bin 10 -> 0 bytes pkg/date_parser/date_parser.go | 19 + vendor/github.com/jalaali/go-jalaali/LICENSE | 21 + .../github.com/jalaali/go-jalaali/README.md | 23 + .../jalaali/go-jalaali/convertion.go | 147 ++ .../github.com/jalaali/go-jalaali/errors.go | 19 + .../github.com/jalaali/go-jalaali/format.go | 318 +++ vendor/github.com/jalaali/go-jalaali/go.mod | 3 + .../github.com/jalaali/go-jalaali/jalaali.go | 79 + .../jalaali/go-jalaali/jalaali_test.go | 188 ++ vendor/github.com/jalaali/go-jalaali/utils.go | 53 + 32 files changed, 3637 insertions(+), 613 deletions(-) delete mode 100644 patientapp/config.go create mode 100644 patientapp/config/config.go delete mode 100644 patientapp/delivery/http/analytic/helper.go delete mode 100644 patientapp/repository/database/analytic_repo.go delete mode 100644 patientapp/repository/database/helper.go create mode 100644 patientapp/repository/mysql/analytic_repo.go rename patientapp/repository/{database => mysql}/dbconfig.yml (71%) create mode 100644 patientapp/repository/mysql/helper.go create mode 100644 patientapp/repository/mysql/rawdata.json rename patientapp/{delivery/http/analytic/dto => service/analytic}/param.go (96%) delete mode 100644 patientapp/tree.txt create mode 100644 pkg/date_parser/date_parser.go create mode 100644 vendor/github.com/jalaali/go-jalaali/LICENSE create mode 100644 vendor/github.com/jalaali/go-jalaali/README.md create mode 100644 vendor/github.com/jalaali/go-jalaali/convertion.go create mode 100644 vendor/github.com/jalaali/go-jalaali/errors.go create mode 100644 vendor/github.com/jalaali/go-jalaali/format.go create mode 100644 vendor/github.com/jalaali/go-jalaali/go.mod create mode 100644 vendor/github.com/jalaali/go-jalaali/jalaali.go create mode 100644 vendor/github.com/jalaali/go-jalaali/jalaali_test.go create mode 100644 vendor/github.com/jalaali/go-jalaali/utils.go diff --git a/delivery/http_server/end2end/setup/docker.go b/delivery/http_server/end2end/setup/docker.go index 2eddecb0..fd3c718e 100644 --- a/delivery/http_server/end2end/setup/docker.go +++ b/delivery/http_server/end2end/setup/docker.go @@ -18,8 +18,8 @@ type TestContainer struct { dockerPool *dockertest.Pool // the connection pool to Docker. mariaResource *dockertest.Resource // MariaDB Docker container resource. redisResource *dockertest.Resource // Redis Docker container resource. - mariaDBConn *mysql.DB // Connection to the MariaDB database. - redisDBConn *redisadapter.Adapter // Connection to the Redis database. + mariaDBConn *mysql.DB // Connection to the MariaDB mysql. + redisDBConn *redisadapter.Adapter // Connection to the Redis mysql. containerExpiryInSeconds uint } @@ -158,7 +158,7 @@ func (t *TestContainer) Start() { return nil }); err != nil { - log.Fatalf("Could not connect to database: %s", err) + log.Fatalf("Could not connect to mysql: %s", err) } } diff --git a/main.go b/main.go index 0fccecb6..dba18f22 100644 --- a/main.go +++ b/main.go @@ -43,7 +43,7 @@ func Config() config.Config { } func MariaDB(cfg config.Config) *mysql.DB { - migrate := flag.Bool("migrate", false, "perform database migration") + migrate := flag.Bool("migrate", false, "perform mysql migration") flag.Parse() if *migrate { migrator.New(migrator.Config{ diff --git a/patientapp/app.go b/patientapp/app.go index 4400d98d..dce0558e 100644 --- a/patientapp/app.go +++ b/patientapp/app.go @@ -1,34 +1,37 @@ package patientapp import ( - "net/http" - + "git.gocasts.ir/ebhomengo/niki/patientapp/config" "git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic" - "git.gocasts.ir/ebhomengo/niki/patientapp/repository/database" + "git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql" + "github.com/labstack/echo/v4" ) type Application struct { - Config Config - HTTPServer *http.Server - DB *database.DataBase - Router http.Handler + //Config Config + HTTPServer *config.EchoServer + DB *mysql.DataBase } -func Setup(config Config, server *http.Server, conn *database.DataBase) Application { - handler := analytic.NewHandler() - router := analytic.NewRouter(handler) +func Setup(cfg config.Config, conn *mysql.DataBase) Application { + + e := echo.New() + + server := config.EchoServer{ + Router: e, + Config: cfg, + } return Application{ - Config: config, - HTTPServer: server, + //Config: config, + HTTPServer: &server, DB: conn, - Router: router, } } -func (a *Application) Start() { - srv := &http.Server{Addr: a.HTTPServer.Addr} - server := analytic.NewServer(srv, a.Router) +func (a Application) Start() { + + server := analytic.NewServer(a.HTTPServer) _ = server.Serve() } diff --git a/patientapp/config.go b/patientapp/config.go deleted file mode 100644 index 9bddc704..00000000 --- a/patientapp/config.go +++ /dev/null @@ -1,4 +0,0 @@ -package patientapp - -type Config struct { -} diff --git a/patientapp/config/config.go b/patientapp/config/config.go new file mode 100644 index 00000000..de2c385c --- /dev/null +++ b/patientapp/config/config.go @@ -0,0 +1,22 @@ +package config + +import ( + "time" + + "github.com/labstack/echo/v4" +) + +type Config struct { + Port int `koanf:"port"` + Cors Cors `koanf:"cors"` + ShutDownCtxTimeout time.Duration `koanf:"shutdown_context_timeout"` +} + +type Cors struct { + AllowOrigins []string `koanf:"allow_origins"` +} + +type EchoServer struct { + Router *echo.Echo + Config Config +} diff --git a/patientapp/delivery/http/analytic/handler.go b/patientapp/delivery/http/analytic/handler.go index 827db2c1..fbe7d3c4 100644 --- a/patientapp/delivery/http/analytic/handler.go +++ b/patientapp/delivery/http/analytic/handler.go @@ -1,13 +1,11 @@ package analytic import ( - "fmt" - "log" "net/http" - "strings" - "git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic/dto" svc "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + "github.com/labstack/echo/v4" ) type Handler struct { @@ -20,136 +18,48 @@ func NewHandler(service svc.Service) *Handler { } } -func (h *Handler) Health(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("ok")) +func (h *Handler) Health(e echo.Context) error { + return e.JSON(http.StatusOK, map[string]interface{}{"status": "ok"}) } -func (h *Handler) PatientsAnalytic(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query() +func (h *Handler) PatientsAnalytic(e echo.Context) error { + var req svc.ListPatientAnalyticRequest - minAge, err := parseIntPtr(q, "minAge") + richErr := richerror.New(richerror.Op("fetchingPatientList.PatientsAnalytic")) + + if err := e.Bind(&req); err != nil { + richErr = richErr.WithErr(err) + richErr = richErr.WithKind(1) + return echo.NewHTTPError(http.StatusBadRequest, richErr) + } + + response, err := h.service.List(e.Request().Context(), req) if err != nil { - writeError(w, http.StatusBadRequest, "invalid minAge") - return - } - maxAge, err := parseIntPtr(q, "maxAge") - if err != nil { - writeError(w, http.StatusBadRequest, "invalid maxAge") - return + richErr = richErr.WithErr(err) + richErr = richErr.WithKind(4) + return echo.NewHTTPError(http.StatusBadRequest, richErr) } - sex, err := parseSexPtr(q, "sex") - if err != nil { - writeError(w, http.StatusBadRequest, "invalid sex") - return - } - - provinceID, err := parseIntPtr(q, "provinceId") - if err != nil { - writeError(w, http.StatusBadRequest, "invalid provinceId") - return - } - cityID, err := parseIntPtr(q, "cityId") - if err != nil { - writeError(w, http.StatusBadRequest, "invalid cityId") - return - } - - search := strings.TrimSpace(q.Get("search")) - var searchPtr *string - if search != "" { - searchPtr = &search - } - - limit, err := parseInt(q, "limit") - if err != nil { - writeError(w, http.StatusBadRequest, "invalid limit") - return - } - offset, err := parseInt(q, "offset") - if err != nil { - writeError(w, http.StatusBadRequest, "invalid offset") - return - } - - pId := int64(*provinceID) - cId := int64(*cityID) - - req := dto.ListPatientAnalyticRequest{ - MinAge: minAge, - MaxAge: maxAge, - Sex: sex, - Province: &pId, - City: &cId, - Search: searchPtr, - Limit: limit, - Offset: offset, - } - - response, err := h.service.List(r.Context(), req) - if err != nil { - log.Println("List error:", err) - writeError(w, http.StatusInternalServerError, fmt.Errorf("List error: %w", err).Error()) - return - } - - writeJSON(w, http.StatusOK, response) + return e.JSON(http.StatusOK, response) } -func (h *Handler) PatientsMapSummary(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query() +func (h *Handler) PatientsMapSummary(e echo.Context) error { + richErr := richerror.New(richerror.Op("fetchingPatientMapSummary.PatientsMapSummary")) - level, err := parseMapLevel(q, "level") - if err != nil { - writeError(w, http.StatusBadRequest, "invalid level (city|province|country)") - return + var req svc.GetPatientMapSummaryRequest + + if err := e.Bind(&req); err != nil { + richErr = richErr.WithErr(err) + richErr = richErr.WithKind(1) + return echo.NewHTTPError(http.StatusBadRequest, richErr) } - parentID, err := parseIntPtr(q, "parentId") - if err != nil { - writeError(w, http.StatusBadRequest, "invalid parentId") - return - } - - minAge, err := parseIntPtr(q, "minAge") - if err != nil { - writeError(w, http.StatusBadRequest, "invalid minAge") - return - } - maxAge, err := parseIntPtr(q, "maxAge") - if err != nil { - writeError(w, http.StatusBadRequest, "invalid maxAge") - return - } - - sex, err := parseSexPtr(q, "sex") - if err != nil { - writeError(w, http.StatusBadRequest, "invalid sex") - return - } - - search := strings.TrimSpace(q.Get("search")) - var searchPtr *string - if search != "" { - searchPtr = &search - } - - req := dto.GetPatientMapSummaryRequest{ - Level: level, - ParentID: parentID, - MinAge: minAge, - MaxAge: maxAge, - Sex: sex, - Search: searchPtr, - } - - resp, svcErr := h.service.GetMapSummary(r.Context(), req) + resp, svcErr := h.service.GetMapSummary(e.Request().Context(), req) if svcErr != nil { - log.Println("GetMapSummary error:", svcErr) - writeError(w, http.StatusInternalServerError, svcErr.Error()) - return + richErr = richErr.WithErr(svcErr) + richErr = richErr.WithKind(4) + return echo.NewHTTPError(http.StatusBadRequest, richErr) } - writeJSON(w, http.StatusOK, resp) + return e.JSON(http.StatusOK, resp) } diff --git a/patientapp/delivery/http/analytic/helper.go b/patientapp/delivery/http/analytic/helper.go deleted file mode 100644 index 9058409a..00000000 --- a/patientapp/delivery/http/analytic/helper.go +++ /dev/null @@ -1,70 +0,0 @@ -package analytic - -import ( - "encoding/json" - "net/http" - "strconv" - "strings" - - "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" -) - -func writeJSON(w http.ResponseWriter, status int, v any) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(v) -} - -func writeError(w http.ResponseWriter, status int, msg string) { - writeJSON(w, status, map[string]any{"error": msg}) -} - -func parseInt(q urlValues, key string) (int, error) { - val := strings.TrimSpace(q.Get(key)) - if val == "" { - return 0, nil - } - return strconv.Atoi(val) -} - -type urlValues interface { - Get(key string) string -} - -func parseIntPtr(q urlValues, key string) (*int, error) { - val := strings.TrimSpace(q.Get(key)) - if val == "" { - return nil, nil - } - i, err := strconv.Atoi(val) - if err != nil { - return nil, err - } - return &i, nil -} - -func parseSexPtr(q urlValues, key string) (*entity.Sex, error) { - val := strings.TrimSpace(q.Get(key)) - if val == "" { - return nil, nil - } - s := entity.Sex(val) - if !s.SexValidation() { - return nil, strconv.ErrSyntax - } - return &s, nil -} - -func parseMapLevel(q urlValues, key string) (entity.MapLevel, error) { - val := strings.TrimSpace(q.Get(key)) - if val == "" { - return "", strconv.ErrSyntax - } - l := entity.MapLevel(val) - switch l { - case entity.MapLevelCity, entity.MapLevelProvince, entity.MapLevelCountry: - return l, nil - default: - return "", strconv.ErrSyntax - } -} diff --git a/patientapp/delivery/http/analytic/router.go b/patientapp/delivery/http/analytic/router.go index 6bc5b325..eddd18c4 100644 --- a/patientapp/delivery/http/analytic/router.go +++ b/patientapp/delivery/http/analytic/router.go @@ -1,17 +1,21 @@ package analytic -import "net/http" +import ( + "git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql" + analytic2 "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic" + "github.com/labstack/echo/v4" +) -func NewRouter(h *Handler) http.Handler { - mux := http.NewServeMux() +func NewPatientAnalyticRouter(s *echo.Group) { - // Define Routes - v1 := http.NewServeMux() - v1.HandleFunc("GET /health", h.Health) - v1.HandleFunc("GET /patients", h.PatientsAnalytic) - v1.HandleFunc("GET /patients-summary", h.PatientsMapSummary) + repo := mysql.NewPatientRepo() + validator := analytic2.NewValidator() + analyticService := analytic2.NewPatientAnalyticService(repo, *validator) - mux.Handle("/v1/", http.StripPrefix("/v1", v1)) + h := NewHandler(analyticService) + + s.GET("/patients", h.PatientsAnalytic) + s.GET("/patients-summary", h.PatientsMapSummary) + s.GET("/health", h.Health) - return mux } diff --git a/patientapp/delivery/http/analytic/server.go b/patientapp/delivery/http/analytic/server.go index 4903eeb4..91ec8ba7 100644 --- a/patientapp/delivery/http/analytic/server.go +++ b/patientapp/delivery/http/analytic/server.go @@ -1,23 +1,41 @@ package analytic import ( + "context" "fmt" - "net/http" + + "git.gocasts.ir/ebhomengo/niki/patientapp/config" ) type Server struct { - HTTPServer *http.Server + HTTPServer *config.EchoServer } -func NewServer(server *http.Server, router http.Handler) *Server { - server.Handler = router +func NewServer(server *config.EchoServer) *Server { + return &Server{ HTTPServer: server, } } func (s Server) Serve() error { + s.RegisterRoutes() // Start server - fmt.Printf("start sever on %s \n", s.HTTPServer.Addr) - return s.HTTPServer.ListenAndServe() + return s.HTTPServer.Router.Start(fmt.Sprintf(":%d", s.HTTPServer.Config.Port)) +} + +func (s Server) Stop(ctx context.Context) error { + return s.HTTPServer.Router.Shutdown(ctx) + +} + +func (s Server) RegisterRoutes() { + + v1 := s.HTTPServer.Router.Group("/v1") + { + // Analytic Group + analyticGroup := v1.Group("/analytic") + NewPatientAnalyticRouter(analyticGroup) + } + } diff --git a/patientapp/repository/database/analytic_repo.go b/patientapp/repository/database/analytic_repo.go deleted file mode 100644 index 9c72ae0d..00000000 --- a/patientapp/repository/database/analytic_repo.go +++ /dev/null @@ -1,303 +0,0 @@ -package database - -import ( - "context" - "database/sql" - "strings" - - "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic" - "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" - errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" - richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" - "git.gocasts.ir/ebhomengo/niki/repository/mysql" -) - -type DataBase struct { - conn *mysql.DB -} - -func NewPatientRepo(conn *mysql.DB) *DataBase { - return &DataBase{ - conn: conn, - } -} - -func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) { - const op = "mysqlpatient.GetPatients" - limit, offset := clampLimitOffset(f.Limit, f.Offset) - - q := ` -SELECT - p.id, - p.first_name, - p.last_name, - p.date_of_birth, - p.sex, - p.phone, - - p.country_id, - p.province_id, - p.city_id, - p.postal_code, - p.address_line1, - p.address_line2, - - p.case_status, - p.referral_source, - p.assigned_staff_id, - p.start_date, - p.end_date -FROM patients p -WHERE p.deleted_at IS NULL -` - - stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) - if err != nil { - return nil, richerror.New(op).WithErr(err). - WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) - } - args := make([]any, 0) - - // filters - if f.Sex != nil { - q += " AND p.sex = ?" - args = append(args, string(*f.Sex)) - } - if f.DOBFrom != nil { - q += " AND p.date_of_birth >= ?" - args = append(args, f.DOBFrom.Format("2006-01-02")) - } - if f.DOBTo != nil { - q += " AND p.date_of_birth <= ?" - args = append(args, f.DOBTo.Format("2006-01-02")) - } - if f.Country != nil { - q += " AND p.country_id = ?" - args = append(args, *f.Country) - } - if f.Province != nil { - q += " AND p.province_id = ?" - args = append(args, *f.Province) - } - if f.City != nil { - q += " AND p.city_id = ?" - args = append(args, *f.City) - } - - if f.Search != nil && strings.TrimSpace(*f.Search) != "" { - term := "%" + strings.TrimSpace(*f.Search) + "%" - q += " AND (p.first_name LIKE ? OR p.last_name LIKE ? OR p.phone LIKE ?)" - args = append(args, term, term, term) - } - - q += " ORDER BY p.id DESC LIMIT ? OFFSET ?" - args = append(args, limit, offset) - - rows, err := stmt.QueryContext(ctx, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - out := make([]entity.Patient, 0, limit) - - for rows.Next() { - var p entity.Patient - var dob sql.NullTime - var provinceID, cityID uint - - if err := rows.Scan( - &p.ID, - &p.FirstName, - &p.LastName, - &dob, - &p.Sex, - &p.Phone, - - &provinceID, - &cityID, - &p.Address.PostalCode, - - &p.CaseStatus, - &p.ReferralSource, - &p.AssignedStaffId, - &p.StartDate, - &p.EndDate, - ); err != nil { - return nil, err - } - - if dob.Valid { - t := dob.Time - p.DateOfBirth = &t - } - - p.Address.ProvinceID = provinceID - p.Address.CityID = cityID - - out = append(out, p) - } - - return out, rows.Err() -} - -func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) { - const op = "mysqlpatient.CountPatients" - q := `SELECT COUNT(*) FROM patients p WHERE p.deleted_at IS NULL` - args := make([]any, 0) - - stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) - if err != nil { - return 0, richerror.New(op).WithErr(err). - WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) - } - - if f.Sex != nil { - q += " AND p.sex = ?" - args = append(args, string(*f.Sex)) - } - if f.DOBFrom != nil { - q += " AND p.date_of_birth >= ?" - args = append(args, f.DOBFrom.Format("2006-01-02")) - } - if f.DOBTo != nil { - q += " AND p.date_of_birth <= ?" - args = append(args, f.DOBTo.Format("2006-01-02")) - } - if f.Country != nil { - q += " AND p.country_id = ?" - args = append(args, *f.Country) - } - if f.Province != nil { - q += " AND p.province_id = ?" - args = append(args, *f.Province) - } - if f.City != nil { - q += " AND p.city_id = ?" - args = append(args, *f.City) - } - - var total int - - err = stmt.QueryRowContext(ctx, args...).Scan(&total) - return total, err -} - -func (db *DataBase) SummaryByCity(ctx context.Context, provinceID int, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) { - const op = "mysqlpatient.SummaryByCity" - q := ` -SELECT - c.id AS location_id, - c.name, - c.centroid_lat, - c.centroid_lng, - COUNT(p.id) AS count -FROM locations c -JOIN patients p ON p.city_id = c.id -WHERE c.level = 'city' - AND c.parent_id = ? - AND p.deleted_at IS NULL -` - - stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) - if err != nil { - return nil, richerror.New(op).WithErr(err). - WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) - } - args := []any{provinceID} - - if f.Sex != nil { - q += " AND p.sex = ?" - args = append(args, string(*f.Sex)) - } - if f.MinDOB != nil { - q += " AND p.date_of_birth >= ?" - args = append(args, f.MinDOB.Format("2006-01-02")) - } - if f.MaxDOB != nil { - q += " AND p.date_of_birth <= ?" - args = append(args, f.MaxDOB.Format("2006-01-02")) - } - if f.Search != nil && strings.TrimSpace(*f.Search) != "" { - term := "%" + strings.TrimSpace(*f.Search) + "%" - q += " AND (p.first_name LIKE ? OR p.last_name LIKE ? OR p.phone LIKE ?)" - args = append(args, term, term, term) - } - - q += ` GROUP BY c.id, c.name, c.centroid_lat, c.centroid_lng - ORDER BY count DESC` - - rows, err := stmt.QueryContext(ctx, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var out []entity.MapSummaryItem - for rows.Next() { - var it entity.MapSummaryItem - if err := rows.Scan(&it.LocationID, &it.Name, &it.CentroidLat, &it.CentroidLng, &it.Count); err != nil { - return nil, err - } - out = append(out, it) - } - return out, rows.Err() -} - -func (db *DataBase) SummaryByProvince(ctx context.Context, countryID int, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) { - const op = "mysqlpatient.SummaryByProvince" - - q := ` -SELECT - prov.id AS location_id, - prov.name, - prov.centroid_lat, - prov.centroid_lng, - COUNT(p.id) AS count -FROM patients p -JOIN locations city ON city.id = p.city_id AND city.level = 'city' -JOIN locations prov ON prov.id = city.parent_id AND prov.level = 'province' -WHERE prov.parent_id = ? - AND p.deleted_at IS NULL -` - - stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) - if err != nil { - return nil, richerror.New(op).WithErr(err). - WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) - } - - args := []any{countryID} - - if f.Sex != nil { - q += " AND p.sex = ?" - args = append(args, string(*f.Sex)) - } - if f.MinDOB != nil { - q += " AND p.date_of_birth >= ?" - args = append(args, f.MinDOB.Format("2006-01-02")) - } - if f.MaxDOB != nil { - q += " AND p.date_of_birth <= ?" - args = append(args, f.MaxDOB.Format("2006-01-02")) - } - - q += ` GROUP BY prov.id, prov.name, prov.centroid_lat, prov.centroid_lng - ORDER BY count DESC` - - rows, err := stmt.QueryContext(ctx, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var out []entity.MapSummaryItem - for rows.Next() { - var it entity.MapSummaryItem - if err := rows.Scan(&it.LocationID, &it.Name, &it.CentroidLat, &it.CentroidLng, &it.Count); err != nil { - return nil, err - } - out = append(out, it) - } - return out, rows.Err() -} diff --git a/patientapp/repository/database/helper.go b/patientapp/repository/database/helper.go deleted file mode 100644 index b0ea1e0a..00000000 --- a/patientapp/repository/database/helper.go +++ /dev/null @@ -1,33 +0,0 @@ -package database - -import "strings" - -type whereClause struct { - sql strings.Builder - args []any -} - -func (w *whereClause) add(cond string, args ...any) { - if w.sql.Len() == 0 { - w.sql.WriteString(" WHERE ") - } else { - w.sql.WriteString(" AND ") - } - w.sql.WriteString(cond) - w.args = append(w.args, args...) -} - -func (w *whereClause) String() string { return w.sql.String() } - -func clampLimitOffset(limit, offset int) (int, int) { - if limit <= 0 { - limit = 50 - } - if limit > 100 { - limit = 100 - } - if offset < 0 { - offset = 0 - } - return limit, offset -} diff --git a/patientapp/repository/mysql/analytic_repo.go b/patientapp/repository/mysql/analytic_repo.go new file mode 100644 index 00000000..8a29427f --- /dev/null +++ b/patientapp/repository/mysql/analytic_repo.go @@ -0,0 +1,349 @@ +package mysql + +import ( + "context" + "log" + + "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic" + "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" +) + +type DataBase struct { + //conn *mysql.DB + patients []entity.Patient +} + +func NewPatientRepo( /*conn *mysql.DB*/) *DataBase { + patients, err := LoadPatientsFromJSON("C:\\Users\\cafel\\Documents\\gocast\\eb\\patientapp\\repository\\mysql\\rawdata.json") + if err != nil { + log.Fatal(err) + return nil + } + return &DataBase{ + //conn: conn, + patients: patients, + } +} + +func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) { + + return db.patients, nil + +} + +func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) { + + return len(db.patients), nil +} + +func (db *DataBase) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) { + + var out []entity.MapSummaryItem + + for _, patient := range db.patients { + if patient.Address.ProvinceID == provinceID { + out = append(out, entity.MapSummaryItem{ + LocationID: int64(patient.Address.ID), + Name: patient.Address.Name, + CentroidLat: patient.Address.Lat, + CentroidLng: patient.Address.Lon, + }) + } + } + return out, nil +} + +func (db *DataBase) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) { + result := make(map[uint][]entity.MapSummaryItem, 0) + for _, patient := range db.patients { + out := entity.MapSummaryItem{ + LocationID: int64(patient.Address.ID), + Name: patient.Address.Name, + CentroidLat: patient.Address.Lat, + CentroidLng: patient.Address.Lon, + } + result[patient.Address.ProvinceID] = append(result[patient.Address.ProvinceID], out) + + } + return result, nil +} + +//func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) { +// const op = "mysqlpatient.GetPatients" +// limit, offset := clampLimitOffset(f.Limit, f.Offset) +// +// q := ` +//SELECT +// p.id, +// p.first_name, +// p.last_name, +// p.date_of_birth, +// p.sex, +// p.phone, +// +// p.country_id, +// p.province_id, +// p.city_id, +// p.postal_code, +// p.address_line1, +// p.address_line2, +// +// p.case_status, +// p.referral_source, +// p.assigned_staff_id, +// p.start_date, +// p.end_date +//FROM patients p +//WHERE p.deleted_at IS NULL +//` +// +// stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) +// if err != nil { +// return nil, richerror.New(op).WithErr(err). +// WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) +// } +// args := make([]any, 0) +// +// // filters +// if f.Sex != nil { +// q += " AND p.sex = ?" +// args = append(args, string(*f.Sex)) +// } +// if f.DOBFrom != nil { +// q += " AND p.date_of_birth >= ?" +// args = append(args, f.DOBFrom.Format("2006-01-02")) +// } +// if f.DOBTo != nil { +// q += " AND p.date_of_birth <= ?" +// args = append(args, f.DOBTo.Format("2006-01-02")) +// } +// if f.Country != nil { +// q += " AND p.country_id = ?" +// args = append(args, *f.Country) +// } +// if f.Province != nil { +// q += " AND p.province_id = ?" +// args = append(args, *f.Province) +// } +// if f.City != nil { +// q += " AND p.city_id = ?" +// args = append(args, *f.City) +// } +// +// if f.Search != nil && strings.TrimSpace(*f.Search) != "" { +// term := "%" + strings.TrimSpace(*f.Search) + "%" +// q += " AND (p.first_name LIKE ? OR p.last_name LIKE ? OR p.phone LIKE ?)" +// args = append(args, term, term, term) +// } +// +// q += " ORDER BY p.id DESC LIMIT ? OFFSET ?" +// args = append(args, limit, offset) +// +// rows, err := stmt.QueryContext(ctx, args...) +// if err != nil { +// return nil, err +// } +// defer rows.Close() +// +// out := make([]entity.Patient, 0, limit) +// +// for rows.Next() { +// var p entity.Patient +// var dob sql.NullTime +// var provinceID, cityID uint +// +// if err := rows.Scan( +// &p.ID, +// &p.FirstName, +// &p.LastName, +// &dob, +// &p.Sex, +// &p.Phone, +// +// &provinceID, +// &cityID, +// &p.Address.PostalCode, +// +// &p.CaseStatus, +// &p.ReferralSource, +// &p.AssignedStaffId, +// &p.StartDate, +// &p.EndDate, +// ); err != nil { +// return nil, err +// } +// +// if dob.Valid { +// t := dob.Time +// p.DateOfBirth = &t +// } +// +// p.Address.ProvinceID = provinceID +// p.Address.CityID = cityID +// +// out = append(out, p) +// } +// +// return out, rows.Err() +//} +// +//func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) { +// const op = "mysqlpatient.CountPatients" +// q := `SELECT COUNT(*) FROM patients p WHERE p.deleted_at IS NULL` +// args := make([]any, 0) +// +// stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) +// if err != nil { +// return 0, richerror.New(op).WithErr(err). +// WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) +// } +// +// if f.Sex != nil { +// q += " AND p.sex = ?" +// args = append(args, string(*f.Sex)) +// } +// if f.DOBFrom != nil { +// q += " AND p.date_of_birth >= ?" +// args = append(args, f.DOBFrom.Format("2006-01-02")) +// } +// if f.DOBTo != nil { +// q += " AND p.date_of_birth <= ?" +// args = append(args, f.DOBTo.Format("2006-01-02")) +// } +// if f.Country != nil { +// q += " AND p.country_id = ?" +// args = append(args, *f.Country) +// } +// if f.Province != nil { +// q += " AND p.province_id = ?" +// args = append(args, *f.Province) +// } +// if f.City != nil { +// q += " AND p.city_id = ?" +// args = append(args, *f.City) +// } +// +// var total int +// +// err = stmt.QueryRowContext(ctx, args...).Scan(&total) +// return total, err +//} +// +//func (db *DataBase) SummaryByCity(ctx context.Context, provinceID int, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) { +// const op = "mysqlpatient.SummaryByCity" +// q := ` +//SELECT +// c.id AS location_id, +// c.name, +// c.centroid_lat, +// c.centroid_lng, +// COUNT(p.id) AS count +//FROM locations c +//JOIN patients p ON p.city_id = c.id +//WHERE c.level = 'city' +// AND c.parent_id = ? +// AND p.deleted_at IS NULL +//` +// +// stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) +// if err != nil { +// return nil, richerror.New(op).WithErr(err). +// WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) +// } +// args := []any{provinceID} +// +// if f.Sex != nil { +// q += " AND p.sex = ?" +// args = append(args, string(*f.Sex)) +// } +// if f.MinDOB != nil { +// q += " AND p.date_of_birth >= ?" +// args = append(args, f.MinDOB.Format("2006-01-02")) +// } +// if f.MaxDOB != nil { +// q += " AND p.date_of_birth <= ?" +// args = append(args, f.MaxDOB.Format("2006-01-02")) +// } +// if f.Search != nil && strings.TrimSpace(*f.Search) != "" { +// term := "%" + strings.TrimSpace(*f.Search) + "%" +// q += " AND (p.first_name LIKE ? OR p.last_name LIKE ? OR p.phone LIKE ?)" +// args = append(args, term, term, term) +// } +// +// q += ` GROUP BY c.id, c.name, c.centroid_lat, c.centroid_lng +// ORDER BY count DESC` +// +// rows, err := stmt.QueryContext(ctx, args...) +// if err != nil { +// return nil, err +// } +// defer rows.Close() +// +// var out []entity.MapSummaryItem +// for rows.Next() { +// var it entity.MapSummaryItem +// if err := rows.Scan(&it.LocationID, &it.Name, &it.CentroidLat, &it.CentroidLng, &it.Count); err != nil { +// return nil, err +// } +// out = append(out, it) +// } +// return out, rows.Err() +//} +// +//func (db *DataBase) SummaryByProvince(ctx context.Context, countryID int, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) { +// const op = "mysqlpatient.SummaryByProvince" +// +// q := ` +//SELECT +// prov.id AS location_id, +// prov.name, +// prov.centroid_lat, +// prov.centroid_lng, +// COUNT(p.id) AS count +//FROM patients p +//JOIN locations city ON city.id = p.city_id AND city.level = 'city' +//JOIN locations prov ON prov.id = city.parent_id AND prov.level = 'province' +//WHERE prov.parent_id = ? +// AND p.deleted_at IS NULL +//` +// +// stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) +// if err != nil { +// return nil, richerror.New(op).WithErr(err). +// WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) +// } +// +// args := []any{countryID} +// +// if f.Sex != nil { +// q += " AND p.sex = ?" +// args = append(args, string(*f.Sex)) +// } +// if f.MinDOB != nil { +// q += " AND p.date_of_birth >= ?" +// args = append(args, f.MinDOB.Format("2006-01-02")) +// } +// if f.MaxDOB != nil { +// q += " AND p.date_of_birth <= ?" +// args = append(args, f.MaxDOB.Format("2006-01-02")) +// } +// +// q += ` GROUP BY prov.id, prov.name, prov.centroid_lat, prov.centroid_lng +// ORDER BY count DESC` +// +// rows, err := stmt.QueryContext(ctx, args...) +// if err != nil { +// return nil, err +// } +// defer rows.Close() +// +// var out []entity.MapSummaryItem +// for rows.Next() { +// var it entity.MapSummaryItem +// if err := rows.Scan(&it.LocationID, &it.Name, &it.CentroidLat, &it.CentroidLng, &it.Count); err != nil { +// return nil, err +// } +// out = append(out, it) +// } +// return out, rows.Err() +//} diff --git a/patientapp/repository/database/dbconfig.yml b/patientapp/repository/mysql/dbconfig.yml similarity index 71% rename from patientapp/repository/database/dbconfig.yml rename to patientapp/repository/mysql/dbconfig.yml index 1e5a8a42..8c7e965d 100644 --- a/patientapp/repository/database/dbconfig.yml +++ b/patientapp/repository/mysql/dbconfig.yml @@ -7,4 +7,4 @@ postgres_db: db_name: patient_db ssl_mode: disable -path_of_migration: ./patientapp/repository/database/migrations \ No newline at end of file +path_of_migration: ./patientapp/repository/mysql/migrations \ No newline at end of file diff --git a/patientapp/repository/mysql/helper.go b/patientapp/repository/mysql/helper.go new file mode 100644 index 00000000..181ec5df --- /dev/null +++ b/patientapp/repository/mysql/helper.go @@ -0,0 +1,56 @@ +package mysql + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" +) + +type whereClause struct { + sql strings.Builder + args []any +} + +func (w *whereClause) add(cond string, args ...any) { + if w.sql.Len() == 0 { + w.sql.WriteString(" WHERE ") + } else { + w.sql.WriteString(" AND ") + } + w.sql.WriteString(cond) + w.args = append(w.args, args...) +} + +func (w *whereClause) String() string { return w.sql.String() } + +func clampLimitOffset(limit, offset int) (int, int) { + if limit <= 0 { + limit = 50 + } + if limit > 100 { + limit = 100 + } + if offset < 0 { + offset = 0 + } + return limit, offset +} + +func LoadPatientsFromJSON(filePath string) ([]entity.Patient, error) { + // Read the file + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + // Unmarshal into slice of Patient + var patients []entity.Patient + if err := json.Unmarshal(data, &patients); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) + } + + return patients, nil +} diff --git a/patientapp/repository/mysql/rawdata.json b/patientapp/repository/mysql/rawdata.json new file mode 100644 index 00000000..73134bec --- /dev/null +++ b/patientapp/repository/mysql/rawdata.json @@ -0,0 +1,2202 @@ +[ + { + "ID": 1, + "FirstName": "علی", + "LastName": "احمدی", + "DateOfBirth": "1370/05/12", + "Sex": "male", + "Phone": "09121234567", + "Address": { + "ID": 1, + "PostalCode": "1234567890", + "Name": "home", + "Lat": 35.6892, + "Lon": 51.3890, + "CityID": 14, + "ProvinceID": 8 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 3, + "StartDate": "1402/01/15", + "EndDate": "" + }, + { + "ID": 2, + "FirstName": "فاطمه", + "LastName": "محمدی", + "DateOfBirth": "1365/08/20", + "Sex": "female", + "Phone": "09351234567", + "Address": { + "ID": 2, + "PostalCode": "9876543210", + "Name": "home", + "Lat": 36.2972, + "Lon": 59.6067, + "CityID": 27, + "ProvinceID": 15 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 5, + "StartDate": "1402/02/10", + "EndDate": "" + }, + { + "ID": 3, + "FirstName": "محمد", + "LastName": "حسینی", + "DateOfBirth": "1358/11/03", + "Sex": "male", + "Phone": "09181234567", + "Address": { + "ID": 3, + "PostalCode": "4561237890", + "Name": "home", + "Lat": 32.6546, + "Lon": 51.6680, + "CityID": 43, + "ProvinceID": 4 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 2, + "StartDate": "1402/03/05", + "EndDate": "" + }, + { + "ID": 4, + "FirstName": "زهرا", + "LastName": "رضایی", + "DateOfBirth": "1375/02/28", + "Sex": "female", + "Phone": "09301234567", + "Address": { + "ID": 4, + "PostalCode": "7894561230", + "Name": "home", + "Lat": 37.2808, + "Lon": 49.5832, + "CityID": 67, + "ProvinceID": 22 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 7, + "StartDate": "1402/04/18", + "EndDate": "" + }, + { + "ID": 5, + "FirstName": "حسن", + "LastName": "کریمی", + "DateOfBirth": "1360/07/15", + "Sex": "male", + "Phone": "09141234567", + "Address": { + "ID": 5, + "PostalCode": "3216549870", + "Name": "home", + "Lat": 34.3277, + "Lon": 47.0785, + "CityID": 88, + "ProvinceID": 19 + }, + "CaseStatus": "closed", + "ReferralSource": "hospital", + "AssignedStaffId": 4, + "StartDate": "1401/06/20", + "EndDate": "1402/01/10" + }, + { + "ID": 6, + "FirstName": "مریم", + "LastName": "صادقی", + "DateOfBirth": "1372/09/14", + "Sex": "female", + "Phone": "09361234567", + "Address": { + "ID": 6, + "PostalCode": "6541239870", + "Name": "home", + "Lat": 38.0799, + "Lon": 46.2919, + "CityID": 5, + "ProvinceID": 2 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 6, + "StartDate": "1402/05/22", + "EndDate": "" + }, + { + "ID": 7, + "FirstName": "رضا", + "LastName": "علیزاده", + "DateOfBirth": "1355/04/10", + "Sex": "male", + "Phone": "09111234567", + "Address": { + "ID": 7, + "PostalCode": "9871234560", + "Name": "home", + "Lat": 35.1833, + "Lon": 47.0000, + "CityID": 32, + "ProvinceID": 11 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 1, + "StartDate": "1402/06/01", + "EndDate": "" + }, + { + "ID": 8, + "FirstName": "سارا", + "LastName": "موسوی", + "DateOfBirth": "1380/12/25", + "Sex": "female", + "Phone": "09211234567", + "Address": { + "ID": 8, + "PostalCode": "1597534680", + "Name": "home", + "Lat": 33.9900, + "Lon": 50.8736, + "CityID": 51, + "ProvinceID": 17 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 9, + "StartDate": "1402/07/11", + "EndDate": "" + }, + { + "ID": 9, + "FirstName": "امیر", + "LastName": "جعفری", + "DateOfBirth": "1368/06/19", + "Sex": "male", + "Phone": "09151234567", + "Address": { + "ID": 9, + "PostalCode": "7531594680", + "Name": "home", + "Lat": 30.2839, + "Lon": 57.0834, + "CityID": 73, + "ProvinceID": 28 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 3, + "StartDate": "1402/08/03", + "EndDate": "" + }, + { + "ID": 10, + "FirstName": "نرگس", + "LastName": "تهرانی", + "DateOfBirth": "1363/03/07", + "Sex": "female", + "Phone": "09391234567", + "Address": { + "ID": 10, + "PostalCode": "3571594680", + "Name": "home", + "Lat": 36.5683, + "Lon": 53.0601, + "CityID": 19, + "ProvinceID": 6 + }, + "CaseStatus": "closed", + "ReferralSource": "hospital", + "AssignedStaffId": 8, + "StartDate": "1401/09/15", + "EndDate": "1402/03/20" + }, + { + "ID": 11, + "FirstName": "سجاد", + "LastName": "نجفی", + "DateOfBirth": "1371/01/01", + "Sex": "male", + "Phone": "09131234567", + "Address": { + "ID": 11, + "PostalCode": "4682057391", + "Name": "home", + "Lat": 31.3203, + "Lon": 48.6692, + "CityID": 62, + "ProvinceID": 24 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 2, + "StartDate": "1402/09/14", + "EndDate": "" + }, + { + "ID": 12, + "FirstName": "الهام", + "LastName": "اکبری", + "DateOfBirth": "1376/10/22", + "Sex": "female", + "Phone": "09331234567", + "Address": { + "ID": 12, + "PostalCode": "8207461935", + "Name": "home", + "Lat": 32.9034, + "Lon": 59.2139, + "CityID": 37, + "ProvinceID": 13 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 5, + "StartDate": "1402/10/05", + "EndDate": "" + }, + { + "ID": 13, + "FirstName": "محسن", + "LastName": "شریفی", + "DateOfBirth": "1357/05/30", + "Sex": "male", + "Phone": "09161234567", + "Address": { + "ID": 13, + "PostalCode": "6194820735", + "Name": "home", + "Lat": 29.6060, + "Lon": 52.5311, + "CityID": 91, + "ProvinceID": 30 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 7, + "StartDate": "1402/11/18", + "EndDate": "" + }, + { + "ID": 14, + "FirstName": "پریسا", + "LastName": "قاسمی", + "DateOfBirth": "1382/07/08", + "Sex": "female", + "Phone": "09341234567", + "Address": { + "ID": 14, + "PostalCode": "0573948261", + "Name": "home", + "Lat": 37.5527, + "Lon": 45.0760, + "CityID": 8, + "ProvinceID": 3 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 4, + "StartDate": "1402/12/01", + "EndDate": "" + }, + { + "ID": 15, + "FirstName": "کاوه", + "LastName": "حیدری", + "DateOfBirth": "1362/08/17", + "Sex": "male", + "Phone": "09171234567", + "Address": { + "ID": 15, + "PostalCode": "3948261057", + "Name": "home", + "Lat": 34.7960, + "Lon": 48.5146, + "CityID": 55, + "ProvinceID": 20 + }, + "CaseStatus": "closed", + "ReferralSource": "hospital", + "AssignedStaffId": 6, + "StartDate": "1401/11/05", + "EndDate": "1402/05/15" + }, + { + "ID": 16, + "FirstName": "شیرین", + "LastName": "یوسفی", + "DateOfBirth": "1373/04/11", + "Sex": "female", + "Phone": "09221234567", + "Address": { + "ID": 16, + "PostalCode": "2948163057", + "Name": "home", + "Lat": 35.6836, + "Lon": 51.4211, + "CityID": 23, + "ProvinceID": 9 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 1, + "StartDate": "1403/01/08", + "EndDate": "" + }, + { + "ID": 17, + "FirstName": "داریوش", + "LastName": "مرادی", + "DateOfBirth": "1356/11/24", + "Sex": "male", + "Phone": "09191234567", + "Address": { + "ID": 17, + "PostalCode": "7461392058", + "Name": "home", + "Lat": 36.8962, + "Lon": 54.4334, + "CityID": 78, + "ProvinceID": 25 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 9, + "StartDate": "1403/01/22", + "EndDate": "" + }, + { + "ID": 18, + "FirstName": "آزاده", + "LastName": "ابراهیمی", + "DateOfBirth": "1378/09/03", + "Sex": "female", + "Phone": "09381234567", + "Address": { + "ID": 18, + "PostalCode": "5839204716", + "Name": "home", + "Lat": 30.2672, + "Lon": 50.0079, + "CityID": 46, + "ProvinceID": 16 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 3, + "StartDate": "1403/02/10", + "EndDate": "" + }, + { + "ID": 19, + "FirstName": "بهزاد", + "LastName": "نوری", + "DateOfBirth": "1367/02/14", + "Sex": "male", + "Phone": "09101234567", + "Address": { + "ID": 19, + "PostalCode": "1234098765", + "Name": "home", + "Lat": 31.8974, + "Lon": 54.3678, + "CityID": 69, + "ProvinceID": 27 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 8, + "StartDate": "1403/02/28", + "EndDate": "" + }, + { + "ID": 20, + "FirstName": "ناهید", + "LastName": "صالحی", + "DateOfBirth": "1361/06/29", + "Sex": "female", + "Phone": "09371234567", + "Address": { + "ID": 20, + "PostalCode": "0987612345", + "Name": "home", + "Lat": 38.4192, + "Lon": 44.9671, + "CityID": 12, + "ProvinceID": 1 + }, + "CaseStatus": "closed", + "ReferralSource": "hospital", + "AssignedStaffId": 2, + "StartDate": "1401/07/01", + "EndDate": "1402/07/01" + }, + { + "ID": 21, + "FirstName": "سیامک", + "LastName": "غفاری", + "DateOfBirth": "1364/10/05", + "Sex": "male", + "Phone": "09121357900", + "Address": { + "ID": 21, + "PostalCode": "5678901234", + "Name": "home", + "Lat": 33.5731, + "Lon": 48.3956, + "CityID": 34, + "ProvinceID": 12 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 5, + "StartDate": "1403/03/05", + "EndDate": "" + }, + { + "ID": 22, + "FirstName": "لیلا", + "LastName": "منصوری", + "DateOfBirth": "1377/03/18", + "Sex": "female", + "Phone": "09351357900", + "Address": { + "ID": 22, + "PostalCode": "4321098765", + "Name": "home", + "Lat": 35.3392, + "Lon": 46.9964, + "CityID": 57, + "ProvinceID": 21 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 7, + "StartDate": "1403/03/20", + "EndDate": "" + }, + { + "ID": 23, + "FirstName": "وحید", + "LastName": "طاهری", + "DateOfBirth": "1359/12/10", + "Sex": "male", + "Phone": "09181357900", + "Address": { + "ID": 23, + "PostalCode": "8765432109", + "Name": "home", + "Lat": 29.9553, + "Lon": 55.4368, + "CityID": 83, + "ProvinceID": 29 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 4, + "StartDate": "1403/04/01", + "EndDate": "" + }, + { + "ID": 24, + "FirstName": "گلناز", + "LastName": "پارسا", + "DateOfBirth": "1384/05/07", + "Sex": "female", + "Phone": "09301357900", + "Address": { + "ID": 24, + "PostalCode": "2109876543", + "Name": "home", + "Lat": 36.6689, + "Lon": 48.5136, + "CityID": 26, + "ProvinceID": 10 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 6, + "StartDate": "1403/04/15", + "EndDate": "" + }, + { + "ID": 25, + "FirstName": "نیما", + "LastName": "باقری", + "DateOfBirth": "1369/08/22", + "Sex": "male", + "Phone": "09141357900", + "Address": { + "ID": 25, + "PostalCode": "6543210987", + "Name": "home", + "Lat": 31.2667, + "Lon": 56.9667, + "CityID": 100, + "ProvinceID": 32 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 1, + "StartDate": "1403/05/02", + "EndDate": "" + }, + { + "ID": 26, + "FirstName": "مینا", + "LastName": "سلیمانی", + "DateOfBirth": "1374/01/15", + "Sex": "female", + "Phone": "09361357900", + "Address": { + "ID": 26, + "PostalCode": "0987654321", + "Name": "home", + "Lat": 32.4667, + "Lon": 53.6667, + "CityID": 47, + "ProvinceID": 18 + }, + "CaseStatus": "closed", + "ReferralSource": "hospital", + "AssignedStaffId": 9, + "StartDate": "1401/05/10", + "EndDate": "1402/09/25" + }, + { + "ID": 27, + "FirstName": "فرهاد", + "LastName": "اسدی", + "DateOfBirth": "1356/07/20", + "Sex": "male", + "Phone": "09111357900", + "Address": { + "ID": 27, + "PostalCode": "3456789012", + "Name": "home", + "Lat": 37.9380, + "Lon": 58.3660, + "CityID": 71, + "ProvinceID": 26 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 3, + "StartDate": "1403/05/18", + "EndDate": "" + }, + { + "ID": 28, + "FirstName": "سپیده", + "LastName": "کمالی", + "DateOfBirth": "1381/11/11", + "Sex": "female", + "Phone": "09211357900", + "Address": { + "ID": 28, + "PostalCode": "7890123456", + "Name": "home", + "Lat": 35.7447, + "Lon": 50.0890, + "CityID": 16, + "ProvinceID": 7 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 8, + "StartDate": "1403/06/01", + "EndDate": "" + }, + { + "ID": 29, + "FirstName": "شاهین", + "LastName": "زارعی", + "DateOfBirth": "1366/04/27", + "Sex": "male", + "Phone": "09151357900", + "Address": { + "ID": 29, + "PostalCode": "2345678901", + "Name": "home", + "Lat": 33.0953, + "Lon": 51.5051, + "CityID": 59, + "ProvinceID": 23 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 2, + "StartDate": "1403/06/15", + "EndDate": "" + }, + { + "ID": 30, + "FirstName": "رویا", + "LastName": "حسنی", + "DateOfBirth": "1379/09/03", + "Sex": "female", + "Phone": "09391357900", + "Address": { + "ID": 30, + "PostalCode": "6789012345", + "Name": "home", + "Lat": 30.8161, + "Lon": 57.0985, + "CityID": 85, + "ProvinceID": 31 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 5, + "StartDate": "1403/07/01", + "EndDate": "" + }, + { + "ID": 31, + "FirstName": "مهدی", + "LastName": "عباسی", + "DateOfBirth": "1361/02/16", + "Sex": "male", + "Phone": "09131357900", + "Address": { + "ID": 31, + "PostalCode": "1230984567", + "Name": "home", + "Lat": 36.2605, + "Lon": 50.0041, + "CityID": 31, + "ProvinceID": 14 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 7, + "StartDate": "1403/07/20", + "EndDate": "" + }, + { + "ID": 32, + "FirstName": "بهاره", + "LastName": "رستمی", + "DateOfBirth": "1375/06/08", + "Sex": "female", + "Phone": "09331357900", + "Address": { + "ID": 32, + "PostalCode": "9870123456", + "Name": "home", + "Lat": 34.9081, + "Lon": 50.8756, + "CityID": 53, + "ProvinceID": 17 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 4, + "StartDate": "1403/08/05", + "EndDate": "" + }, + { + "ID": 33, + "FirstName": "علیرضا", + "LastName": "شاهی", + "DateOfBirth": "1353/10/25", + "Sex": "male", + "Phone": "09161357900", + "Address": { + "ID": 33, + "PostalCode": "4560123789", + "Name": "home", + "Lat": 31.8955, + "Lon": 54.3701, + "CityID": 77, + "ProvinceID": 27 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 6, + "StartDate": "1403/08/18", + "EndDate": "" + }, + { + "ID": 34, + "FirstName": "نسرین", + "LastName": "جلیلی", + "DateOfBirth": "1383/03/12", + "Sex": "female", + "Phone": "09341357900", + "Address": { + "ID": 34, + "PostalCode": "7893216540", + "Name": "home", + "Lat": 37.4713, + "Lon": 49.7166, + "CityID": 22, + "ProvinceID": 9 + }, + "CaseStatus": "closed", + "ReferralSource": "hospital", + "AssignedStaffId": 1, + "StartDate": "1401/03/07", + "EndDate": "1402/11/20" + }, + { + "ID": 35, + "FirstName": "آرش", + "LastName": "فرهادی", + "DateOfBirth": "1368/07/21", + "Sex": "male", + "Phone": "09171357900", + "Address": { + "ID": 35, + "PostalCode": "3210987654", + "Name": "home", + "Lat": 28.9234, + "Lon": 50.8203, + "CityID": 94, + "ProvinceID": 32 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 9, + "StartDate": "1403/09/01", + "EndDate": "" + }, + { + "ID": 36, + "FirstName": "مژده", + "LastName": "کوهی", + "DateOfBirth": "1377/12/14", + "Sex": "female", + "Phone": "09221357900", + "Address": { + "ID": 36, + "PostalCode": "6547893210", + "Name": "home", + "Lat": 35.3219, + "Lon": 46.9979, + "CityID": 41, + "ProvinceID": 15 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 3, + "StartDate": "1403/09/15", + "EndDate": "" + }, + { + "ID": 37, + "FirstName": "حمید", + "LastName": "شکری", + "DateOfBirth": "1360/05/03", + "Sex": "male", + "Phone": "09191357900", + "Address": { + "ID": 37, + "PostalCode": "9213456780", + "Name": "home", + "Lat": 34.0840, + "Lon": 49.6967, + "CityID": 64, + "ProvinceID": 22 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 8, + "StartDate": "1403/10/02", + "EndDate": "" + }, + { + "ID": 38, + "FirstName": "ندا", + "LastName": "پورمحمد", + "DateOfBirth": "1385/08/28", + "Sex": "female", + "Phone": "09381357900", + "Address": { + "ID": 38, + "PostalCode": "0132456789", + "Name": "home", + "Lat": 31.5497, + "Lon": 48.7547, + "CityID": 88, + "ProvinceID": 24 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 2, + "StartDate": "1403/10/18", + "EndDate": "" + }, + { + "ID": 39, + "FirstName": "صادق", + "LastName": "محسنی", + "DateOfBirth": "1354/01/30", + "Sex": "male", + "Phone": "09101357900", + "Address": { + "ID": 39, + "PostalCode": "5678123490", + "Name": "home", + "Lat": 30.4271, + "Lon": 48.2121, + "CityID": 97, + "ProvinceID": 30 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 5, + "StartDate": "1403/11/05", + "EndDate": "" + }, + { + "ID": 40, + "FirstName": "فروغ", + "LastName": "ملکی", + "DateOfBirth": "1370/04/17", + "Sex": "female", + "Phone": "09371357900", + "Address": { + "ID": 40, + "PostalCode": "3214567890", + "Name": "home", + "Lat": 37.0539, + "Lon": 54.8340, + "CityID": 7, + "ProvinceID": 3 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 7, + "StartDate": "1403/11/20", + "EndDate": "" + }, + { + "ID": 41, + "FirstName": "پدرام", + "LastName": "ولی", + "DateOfBirth": "1365/11/09", + "Sex": "male", + "Phone": "09129876543", + "Address": { + "ID": 41, + "PostalCode": "8901234567", + "Name": "home", + "Lat": 36.5544, + "Lon": 52.6611, + "CityID": 28, + "ProvinceID": 6 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 4, + "StartDate": "1403/12/01", + "EndDate": "" + }, + { + "ID": 42, + "FirstName": "زینب", + "LastName": "احمدپور", + "DateOfBirth": "1380/02/20", + "Sex": "female", + "Phone": "09359876543", + "Address": { + "ID": 42, + "PostalCode": "1234509876", + "Name": "home", + "Lat": 35.6777, + "Lon": 51.3888, + "CityID": 14, + "ProvinceID": 8 + }, + "CaseStatus": "closed", + "ReferralSource": "hospital", + "AssignedStaffId": 6, + "StartDate": "1401/08/12", + "EndDate": "1402/06/30" + }, + { + "ID": 43, + "FirstName": "کامران", + "LastName": "درویشی", + "DateOfBirth": "1358/09/14", + "Sex": "male", + "Phone": "09189876543", + "Address": { + "ID": 43, + "PostalCode": "6789012340", + "Name": "home", + "Lat": 32.6719, + "Lon": 51.6618, + "CityID": 43, + "ProvinceID": 4 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 1, + "StartDate": "1404/01/10", + "EndDate": "" + }, + { + "ID": 44, + "FirstName": "آرزو", + "LastName": "حاتمی", + "DateOfBirth": "1376/07/05", + "Sex": "female", + "Phone": "09309876543", + "Address": { + "ID": 44, + "PostalCode": "2345016789", + "Name": "home", + "Lat": 34.3277, + "Lon": 47.0785, + "CityID": 75, + "ProvinceID": 19 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 9, + "StartDate": "1404/01/25", + "EndDate": "" + }, + { + "ID": 45, + "FirstName": "بابک", + "LastName": "معینی", + "DateOfBirth": "1363/04/19", + "Sex": "male", + "Phone": "09149876543", + "Address": { + "ID": 45, + "PostalCode": "9078563412", + "Name": "home", + "Lat": 38.0792, + "Lon": 46.2895, + "CityID": 5, + "ProvinceID": 2 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 3, + "StartDate": "1404/02/08", + "EndDate": "" + }, + { + "ID": 46, + "FirstName": "مهناز", + "LastName": "قربانی", + "DateOfBirth": "1372/12/01", + "Sex": "female", + "Phone": "09369876543", + "Address": { + "ID": 46, + "PostalCode": "4563218790", + "Name": "home", + "Lat": 37.2710, + "Lon": 49.5887, + "CityID": 67, + "ProvinceID": 22 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 8, + "StartDate": "1404/02/22", + "EndDate": "" + }, + { + "ID": 47, + "FirstName": "ایمان", + "LastName": "رحمانی", + "DateOfBirth": "1367/06/24", + "Sex": "male", + "Phone": "09119876543", + "Address": { + "ID": 47, + "PostalCode": "1087923456", + "Name": "home", + "Lat": 36.2972, + "Lon": 59.6067, + "CityID": 35, + "ProvinceID": 13 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 2, + "StartDate": "1404/03/05", + "EndDate": "" + }, + { + "ID": 48, + "FirstName": "شادی", + "LastName": "امیری", + "DateOfBirth": "1381/03/17", + "Sex": "female", + "Phone": "09219876543", + "Address": { + "ID": 48, + "PostalCode": "6541237890", + "Name": "home", + "Lat": 33.9900, + "Lon": 50.8736, + "CityID": 53, + "ProvinceID": 17 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 5, + "StartDate": "1404/03/20", + "EndDate": "" + }, + { + "ID": 49, + "FirstName": "بهروز", + "LastName": "سبزواری", + "DateOfBirth": "1355/10/08", + "Sex": "male", + "Phone": "09159876543", + "Address": { + "ID": 49, + "PostalCode": "7893045612", + "Name": "home", + "Lat": 29.6060, + "Lon": 52.5311, + "CityID": 91, + "ProvinceID": 30 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 7, + "StartDate": "1404/04/01", + "EndDate": "" + }, + { + "ID": 50, + "FirstName": "ترانه", + "LastName": "محمودی", + "DateOfBirth": "1384/08/11", + "Sex": "female", + "Phone": "09399876543", + "Address": { + "ID": 50, + "PostalCode": "2340897615", + "Name": "home", + "Lat": 31.3203, + "Lon": 48.6692, + "CityID": 62, + "ProvinceID": 24 + }, + "CaseStatus": "closed", + "ReferralSource": "hospital", + "AssignedStaffId": 4, + "StartDate": "1401/10/05", + "EndDate": "1402/08/15" + }, + { + "ID": 51, + "FirstName": "فریدون", + "LastName": "نعمتی", + "DateOfBirth": "1357/01/27", + "Sex": "male", + "Phone": "09139876543", + "Address": { + "ID": 51, + "PostalCode": "5671289034", + "Name": "home", + "Lat": 30.2839, + "Lon": 57.0834, + "CityID": 73, + "ProvinceID": 28 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 6, + "StartDate": "1404/04/18", + "EndDate": "" + }, + { + "ID": 52, + "FirstName": "مهسا", + "LastName": "خلیلی", + "DateOfBirth": "1378/05/23", + "Sex": "female", + "Phone": "09339876543", + "Address": { + "ID": 52, + "PostalCode": "9034567812", + "Name": "home", + "Lat": 37.5527, + "Lon": 45.0760, + "CityID": 8, + "ProvinceID": 3 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 1, + "StartDate": "1404/05/02", + "EndDate": "" + }, + { + "ID": 53, + "FirstName": "جلال", + "LastName": "حبیبی", + "DateOfBirth": "1362/11/15", + "Sex": "male", + "Phone": "09169876543", + "Address": { + "ID": 53, + "PostalCode": "1289034567", + "Name": "home", + "Lat": 34.7960, + "Lon": 48.5146, + "CityID": 55, + "ProvinceID": 20 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 9, + "StartDate": "1404/05/18", + "EndDate": "" + }, + { + "ID": 54, + "FirstName": "صبا", + "LastName": "خوش‌نژاد", + "DateOfBirth": "1387/02/07", + "Sex": "female", + "Phone": "09349876543", + "Address": { + "ID": 54, + "PostalCode": "3450678912", + "Name": "home", + "Lat": 35.6836, + "Lon": 51.4211, + "CityID": 14, + "ProvinceID": 8 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 3, + "StartDate": "1404/06/01", + "EndDate": "" + }, + { + "ID": 55, + "FirstName": "هادی", + "LastName": "خدابخش", + "DateOfBirth": "1366/09/29", + "Sex": "male", + "Phone": "09179876543", + "Address": { + "ID": 55, + "PostalCode": "7812034560", + "Name": "home", + "Lat": 36.8962, + "Lon": 54.4334, + "CityID": 78, + "ProvinceID": 25 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 8, + "StartDate": "1404/06/15", + "EndDate": "" + }, + { + "ID": 56, + "FirstName": "افسانه", + "LastName": "دهقانی", + "DateOfBirth": "1373/07/12", + "Sex": "female", + "Phone": "09229876543", + "Address": { + "ID": 56, + "PostalCode": "6780123459", + "Name": "home", + "Lat": 30.2672, + "Lon": 50.0079, + "CityID": 46, + "ProvinceID": 16 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 2, + "StartDate": "1404/07/01", + "EndDate": "" + }, + { + "ID": 57, + "FirstName": "سروش", + "LastName": "پاک‌نهاد", + "DateOfBirth": "1360/03/04", + "Sex": "male", + "Phone": "09199876543", + "Address": { + "ID": 57, + "PostalCode": "4509123678", + "Name": "home", + "Lat": 31.8974, + "Lon": 54.3678, + "CityID": 69, + "ProvinceID": 27 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 5, + "StartDate": "1404/07/18", + "EndDate": "" + }, + { + "ID": 58, + "FirstName": "فرناز", + "LastName": "بهرامی", + "DateOfBirth": "1382/10/20", + "Sex": "female", + "Phone": "09389876543", + "Address": { + "ID": 58, + "PostalCode": "2356789014", + "Name": "home", + "Lat": 38.4192, + "Lon": 44.9671, + "CityID": 12, + "ProvinceID": 1 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 7, + "StartDate": "1404/08/05", + "EndDate": "" + }, + { + "ID": 59, + "FirstName": "ابراهیم", + "LastName": "ستاری", + "DateOfBirth": "1354/07/16", + "Sex": "male", + "Phone": "09109876543", + "Address": { + "ID": 59, + "PostalCode": "7890234561", + "Name": "home", + "Lat": 33.5731, + "Lon": 48.3956, + "CityID": 34, + "ProvinceID": 12 + }, + "CaseStatus": "closed", + "ReferralSource": "hospital", + "AssignedStaffId": 4, + "StartDate": "1401/04/01", + "EndDate": "1402/10/10" + }, + { + "ID": 60, + "FirstName": "گیتا", + "LastName": "نادری", + "DateOfBirth": "1376/04/28", + "Sex": "female", + "Phone": "09379876543", + "Address": { + "ID": 60, + "PostalCode": "1345678092", + "Name": "home", + "Lat": 35.3392, + "Lon": 46.9964, + "CityID": 57, + "ProvinceID": 21 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 6, + "StartDate": "1404/08/20", + "EndDate": "" + }, + { + "ID": 61, + "FirstName": "مصطفی", + "LastName": "میرزایی", + "DateOfBirth": "1363/01/11", + "Sex": "male", + "Phone": "09125551234", + "Address": { + "ID": 61, + "PostalCode": "9034512678", + "Name": "home", + "Lat": 29.9553, + "Lon": 55.4368, + "CityID": 83, + "ProvinceID": 29 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 1, + "StartDate": "1404/09/01", + "EndDate": "" + }, + { + "ID": 62, + "FirstName": "هنگامه", + "LastName": "کاظمی", + "DateOfBirth": "1379/11/08", + "Sex": "female", + "Phone": "09355551234", + "Address": { + "ID": 62, + "PostalCode": "6701234589", + "Name": "home", + "Lat": 36.6689, + "Lon": 48.5136, + "CityID": 26, + "ProvinceID": 10 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 9, + "StartDate": "1404/09/15", + "EndDate": "" + }, + { + "ID": 63, + "FirstName": "فواد", + "LastName": "شریعتی", + "DateOfBirth": "1359/06/22", + "Sex": "male", + "Phone": "09185551234", + "Address": { + "ID": 63, + "PostalCode": "3456012789", + "Name": "home", + "Lat": 31.2667, + "Lon": 56.9667, + "CityID": 100, + "ProvinceID": 32 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 3, + "StartDate": "1404/10/02", + "EndDate": "" + }, + { + "ID": 64, + "FirstName": "سوسن", + "LastName": "دوستی", + "DateOfBirth": "1374/09/14", + "Sex": "female", + "Phone": "09305551234", + "Address": { + "ID": 64, + "PostalCode": "8912034567", + "Name": "home", + "Lat": 32.4667, + "Lon": 53.6667, + "CityID": 47, + "ProvinceID": 18 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 8, + "StartDate": "1404/10/18", + "EndDate": "" + }, + { + "ID": 65, + "FirstName": "تورج", + "LastName": "ایمانی", + "DateOfBirth": "1356/04/30", + "Sex": "male", + "Phone": "09145551234", + "Address": { + "ID": 65, + "PostalCode": "2134567890", + "Name": "home", + "Lat": 37.9380, + "Lon": 58.3660, + "CityID": 71, + "ProvinceID": 26 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 2, + "StartDate": "1404/11/05", + "EndDate": "" + }, + { + "ID": 66, + "FirstName": "شبنم", + "LastName": "احمدزاده", + "DateOfBirth": "1385/01/17", + "Sex": "female", + "Phone": "09365551234", + "Address": { + "ID": 66, + "PostalCode": "5678094321", + "Name": "home", + "Lat": 35.7447, + "Lon": 50.0890, + "CityID": 16, + "ProvinceID": 7 + }, + "CaseStatus": "closed", + "ReferralSource": "hospital", + "AssignedStaffId": 5, + "StartDate": "1401/02/15", + "EndDate": "1402/04/05" + }, + { + "ID": 67, + "FirstName": "عباس", + "LastName": "طباطبایی", + "DateOfBirth": "1358/08/09", + "Sex": "male", + "Phone": "09115551234", + "Address": { + "ID": 67, + "PostalCode": "0912347856", + "Name": "home", + "Lat": 33.0953, + "Lon": 51.5051, + "CityID": 59, + "ProvinceID": 23 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 7, + "StartDate": "1404/11/20", + "EndDate": "" + }, + { + "ID": 68, + "FirstName": "مهتاب", + "LastName": "اکبرزاده", + "DateOfBirth": "1383/06/26", + "Sex": "female", + "Phone": "09215551234", + "Address": { + "ID": 68, + "PostalCode": "7456238901", + "Name": "home", + "Lat": 30.8161, + "Lon": 57.0985, + "CityID": 85, + "ProvinceID": 31 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 4, + "StartDate": "1404/12/01", + "EndDate": "" + }, + { + "ID": 69, + "FirstName": "قاسم", + "LastName": "ذاکری", + "DateOfBirth": "1362/02/15", + "Sex": "male", + "Phone": "09155551234", + "Address": { + "ID": 69, + "PostalCode": "3890127456", + "Name": "home", + "Lat": 36.2605, + "Lon": 50.0041, + "CityID": 31, + "ProvinceID": 14 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 6, + "StartDate": "1404/12/15", + "EndDate": "" + }, + { + "ID": 70, + "FirstName": "نیلوفر", + "LastName": "وطن‌دوست", + "DateOfBirth": "1375/10/03", + "Sex": "female", + "Phone": "09395551234", + "Address": { + "ID": 70, + "PostalCode": "1267890345", + "Name": "home", + "Lat": 34.9081, + "Lon": 50.8756, + "CityID": 53, + "ProvinceID": 17 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 1, + "StartDate": "1405/01/05", + "EndDate": "" + }, + { + "ID": 71, + "FirstName": "سهراب", + "LastName": "قلی‌زاده", + "DateOfBirth": "1361/11/21", + "Sex": "male", + "Phone": "09135551234", + "Address": { + "ID": 71, + "PostalCode": "4512367890", + "Name": "home", + "Lat": 31.8955, + "Lon": 54.3701, + "CityID": 77, + "ProvinceID": 27 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 9, + "StartDate": "1405/01/10", + "EndDate": "" + }, + { + "ID": 72, + "FirstName": "آناهیتا", + "LastName": "رضوانی", + "DateOfBirth": "1386/07/14", + "Sex": "female", + "Phone": "09335551234", + "Address": { + "ID": 72, + "PostalCode": "8034512679", + "Name": "home", + "Lat": 37.4713, + "Lon": 49.7166, + "CityID": 22, + "ProvinceID": 9 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 3, + "StartDate": "1405/01/12", + "EndDate": "" + }, + { + "ID": 73, + "FirstName": "منوچهر", + "LastName": "صمدی", + "DateOfBirth": "1353/05/18", + "Sex": "male", + "Phone": "09165551234", + "Address": { + "ID": 73, + "PostalCode": "6789345012", + "Name": "home", + "Lat": 28.9234, + "Lon": 50.8203, + "CityID": 94, + "ProvinceID": 32 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 8, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 74, + "FirstName": "فرحناز", + "LastName": "ضیایی", + "DateOfBirth": "1377/01/25", + "Sex": "female", + "Phone": "09345551234", + "Address": { + "ID": 74, + "PostalCode": "2301456789", + "Name": "home", + "Lat": 35.3219, + "Lon": 46.9979, + "CityID": 41, + "ProvinceID": 15 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 2, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 75, + "FirstName": "کیوان", + "LastName": "پیری", + "DateOfBirth": "1364/09/07", + "Sex": "male", + "Phone": "09175551234", + "Address": { + "ID": 75, + "PostalCode": "5678901432", + "Name": "home", + "Lat": 34.0840, + "Lon": 49.6967, + "CityID": 64, + "ProvinceID": 22 + }, + "CaseStatus": "closed", + "ReferralSource": "hospital", + "AssignedStaffId": 5, + "StartDate": "1401/12/01", + "EndDate": "1402/12/15" + }, + { + "ID": 76, + "FirstName": "سمیه", + "LastName": "مختاری", + "DateOfBirth": "1371/04/11", + "Sex": "female", + "Phone": "09225551234", + "Address": { + "ID": 76, + "PostalCode": "9012345678", + "Name": "home", + "Lat": 31.5497, + "Lon": 48.7547, + "CityID": 88, + "ProvinceID": 24 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 7, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 77, + "FirstName": "رامین", + "LastName": "بیگدلی", + "DateOfBirth": "1360/12/28", + "Sex": "male", + "Phone": "09195551234", + "Address": { + "ID": 77, + "PostalCode": "3456789102", + "Name": "home", + "Lat": 30.4271, + "Lon": 48.2121, + "CityID": 97, + "ProvinceID": 30 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 4, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 78, + "FirstName": "ثریا", + "LastName": "عارف", + "DateOfBirth": "1380/08/17", + "Sex": "female", + "Phone": "09385551234", + "Address": { + "ID": 78, + "PostalCode": "7891234056", + "Name": "home", + "Lat": 37.0539, + "Lon": 54.8340, + "CityID": 7, + "ProvinceID": 3 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 6, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 79, + "FirstName": "ژاله", + "LastName": "حکیمی", + "DateOfBirth": "1368/03/22", + "Sex": "female", + "Phone": "09105551234", + "Address": { + "ID": 79, + "PostalCode": "2345601789", + "Name": "home", + "Lat": 36.5544, + "Lon": 52.6611, + "CityID": 28, + "ProvinceID": 6 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 1, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 80, + "FirstName": "ارسلان", + "LastName": "فلاح", + "DateOfBirth": "1355/06/14", + "Sex": "male", + "Phone": "09375551234", + "Address": { + "ID": 80, + "PostalCode": "6012345789", + "Name": "home", + "Lat": 35.6777, + "Lon": 51.3888, + "CityID": 14, + "ProvinceID": 8 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 9, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 81, + "FirstName": "پونه", + "LastName": "طلایی", + "DateOfBirth": "1382/12/05", + "Sex": "female", + "Phone": "09127778901", + "Address": { + "ID": 81, + "PostalCode": "4567890213", + "Name": "home", + "Lat": 32.6719, + "Lon": 51.6618, + "CityID": 43, + "ProvinceID": 4 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 3, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 82, + "FirstName": "کریم", + "LastName": "معظمی", + "DateOfBirth": "1357/09/01", + "Sex": "male", + "Phone": "09357778901", + "Address": { + "ID": 82, + "PostalCode": "1908234567", + "Name": "home", + "Lat": 34.3277, + "Lon": 47.0785, + "CityID": 88, + "ProvinceID": 19 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 8, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 83, + "FirstName": "فریبا", + "LastName": "خاکپور", + "DateOfBirth": "1374/06/20", + "Sex": "female", + "Phone": "09187778901", + "Address": { + "ID": 83, + "PostalCode": "8745123609", + "Name": "home", + "Lat": 38.0799, + "Lon": 46.2919, + "CityID": 5, + "ProvinceID": 2 + }, + "CaseStatus": "closed", + "ReferralSource": "hospital", + "AssignedStaffId": 2, + "StartDate": "1401/01/10", + "EndDate": "1403/01/10" + }, + { + "ID": 84, + "FirstName": "غلامرضا", + "LastName": "توکلی", + "DateOfBirth": "1352/03/15", + "Sex": "male", + "Phone": "09307778901", + "Address": { + "ID": 84, + "PostalCode": "0123456897", + "Name": "home", + "Lat": 37.2808, + "Lon": 49.5832, + "CityID": 67, + "ProvinceID": 22 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 5, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 85, + "FirstName": "آرمین", + "LastName": "حق‌شناس", + "DateOfBirth": "1369/12/10", + "Sex": "male", + "Phone": "09147778901", + "Address": { + "ID": 85, + "PostalCode": "5601234789", + "Name": "home", + "Lat": 35.1833, + "Lon": 47.0000, + "CityID": 32, + "ProvinceID": 11 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 7, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 86, + "FirstName": "تهمینه", + "LastName": "فروزنده", + "DateOfBirth": "1378/10/27", + "Sex": "female", + "Phone": "09367778901", + "Address": { + "ID": 86, + "PostalCode": "3789012456", + "Name": "home", + "Lat": 33.9900, + "Lon": 50.8736, + "CityID": 51, + "ProvinceID": 17 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 4, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 87, + "FirstName": "نادر", + "LastName": "مشهدی", + "DateOfBirth": "1361/08/18", + "Sex": "male", + "Phone": "09117778901", + "Address": { + "ID": 87, + "PostalCode": "9123450678", + "Name": "home", + "Lat": 30.2839, + "Lon": 57.0834, + "CityID": 73, + "ProvinceID": 28 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 6, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 88, + "FirstName": "ماندانا", + "LastName": "صبوری", + "DateOfBirth": "1376/05/05", + "Sex": "female", + "Phone": "09217778901", + "Address": { + "ID": 88, + "PostalCode": "6034512789", + "Name": "home", + "Lat": 36.5683, + "Lon": 53.0601, + "CityID": 19, + "ProvinceID": 6 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 1, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 89, + "FirstName": "بیژن", + "LastName": "مهدوی", + "DateOfBirth": "1363/11/23", + "Sex": "male", + "Phone": "09157778901", + "Address": { + "ID": 89, + "PostalCode": "1234896057", + "Name": "home", + "Lat": 31.3203, + "Lon": 48.6692, + "CityID": 62, + "ProvinceID": 24 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 9, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 90, + "FirstName": "حمیده", + "LastName": "میری", + "DateOfBirth": "1381/07/10", + "Sex": "female", + "Phone": "09397778901", + "Address": { + "ID": 90, + "PostalCode": "7893456012", + "Name": "home", + "Lat": 29.6060, + "Lon": 52.5311, + "CityID": 91, + "ProvinceID": 30 + }, + "CaseStatus": "closed", + "ReferralSource": "hospital", + "AssignedStaffId": 3, + "StartDate": "1401/06/01", + "EndDate": "1403/06/01" + }, + { + "ID": 91, + "FirstName": "اردشیر", + "LastName": "نوروزی", + "DateOfBirth": "1358/04/06", + "Sex": "male", + "Phone": "09137778901", + "Address": { + "ID": 91, + "PostalCode": "0456789123", + "Name": "home", + "Lat": 37.5527, + "Lon": 45.0760, + "CityID": 8, + "ProvinceID": 3 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 8, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 92, + "FirstName": "حمیرا", + "LastName": "تقوی", + "DateOfBirth": "1373/02/18", + "Sex": "female", + "Phone": "09337778901", + "Address": { + "ID": 92, + "PostalCode": "5012345679", + "Name": "home", + "Lat": 34.7960, + "Lon": 48.5146, + "CityID": 55, + "ProvinceID": 20 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 2, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 93, + "FirstName": "فریدون", + "LastName": "آهنگر", + "DateOfBirth": "1355/08/30", + "Sex": "male", + "Phone": "09167778901", + "Address": { + "ID": 93, + "PostalCode": "2890134567", + "Name": "home", + "Lat": 35.6836, + "Lon": 51.4211, + "CityID": 23, + "ProvinceID": 9 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 5, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 94, + "FirstName": "رزا", + "LastName": "یاری", + "DateOfBirth": "1384/11/15", + "Sex": "female", + "Phone": "09347778901", + "Address": { + "ID": 94, + "PostalCode": "7456890123", + "Name": "home", + "Lat": 36.8962, + "Lon": 54.4334, + "CityID": 78, + "ProvinceID": 25 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 7, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 95, + "FirstName": "میثم", + "LastName": "کرمی", + "DateOfBirth": "1367/01/07", + "Sex": "male", + "Phone": "09177778901", + "Address": { + "ID": 95, + "PostalCode": "3123456789", + "Name": "home", + "Lat": 30.2672, + "Lon": 50.0079, + "CityID": 46, + "ProvinceID": 16 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 4, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 96, + "FirstName": "سلما", + "LastName": "ایزدی", + "DateOfBirth": "1379/04/24", + "Sex": "female", + "Phone": "09227778901", + "Address": { + "ID": 96, + "PostalCode": "8901234765", + "Name": "home", + "Lat": 31.8974, + "Lon": 54.3678, + "CityID": 69, + "ProvinceID": 27 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 6, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 97, + "FirstName": "شهریار", + "LastName": "بختیاری", + "DateOfBirth": "1357/07/11", + "Sex": "male", + "Phone": "09197778901", + "Address": { + "ID": 97, + "PostalCode": "4567012398", + "Name": "home", + "Lat": 38.4192, + "Lon": 44.9671, + "CityID": 12, + "ProvinceID": 1 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 1, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 98, + "FirstName": "اعظم", + "LastName": "شمس", + "DateOfBirth": "1372/10/28", + "Sex": "female", + "Phone": "09387778901", + "Address": { + "ID": 98, + "PostalCode": "1234567809", + "Name": "home", + "Lat": 33.5731, + "Lon": 48.3956, + "CityID": 34, + "ProvinceID": 12 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 9, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 99, + "FirstName": "احمد", + "LastName": "فیروزی", + "DateOfBirth": "1362/06/19", + "Sex": "male", + "Phone": "09107778901", + "Address": { + "ID": 99, + "PostalCode": "6789123450", + "Name": "home", + "Lat": 35.3392, + "Lon": 46.9964, + "CityID": 57, + "ProvinceID": 21 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 3, + "StartDate": "1405/01/13", + "EndDate": "" + }, + { + "ID": 100, + "FirstName": "ملیکا", + "LastName": "صفوی", + "DateOfBirth": "1386/03/08", + "Sex": "female", + "Phone": "09377778901", + "Address": { + "ID": 100, + "PostalCode": "2345678190", + "Name": "home", + "Lat": 29.9553, + "Lon": 55.4368, + "CityID": 83, + "ProvinceID": 29 + }, + "CaseStatus": "open", + "ReferralSource": "hospital", + "AssignedStaffId": 8, + "StartDate": "1405/01/13", + "EndDate": "" + } +] diff --git a/patientapp/service/analytic/helper.go b/patientapp/service/analytic/helper.go index 1e0b3df8..94b87fa5 100644 --- a/patientapp/service/analytic/helper.go +++ b/patientapp/service/analytic/helper.go @@ -1,6 +1,10 @@ package analytic -import "time" +import ( + "fmt" + "github.com/jalaali/go-jalaali" + "time" +) func normalizeLimitOffset(limit, offset int) (int, int) { if limit <= 0 { @@ -16,17 +20,24 @@ func normalizeLimitOffset(limit, offset int) (int, int) { } // convert age range -> DOB range -func ageRangeToDOB(minAge, maxAge *int, now time.Time) (dobFrom, dobTo *time.Time) { - // maxAge => DOB >= now-(maxAge+1y)+1day - // eg: now is : 3/31/2026; maxAge=30 => (now - 31)= 3/31/1994 => +=1 +func ageRangeToDOB(minAge, maxAge *int, now time.Time) (dobFrom, dobTo *string) { if maxAge != nil { - t := now.AddDate(-*maxAge-1, 0, 0).Add(24 * time.Hour) - dobFrom = &t + t := now.AddDate(-(*maxAge + 1), 0, 1) + jy, jm, jd, err := jalaali.ToJalaali(t.Year(), t.Month(), t.Day()) + if err != nil { + } + s := fmt.Sprintf("%04d/%02d/%02d", jy, jm, jd) + dobFrom = &s } - // minAge => DOB <= now-minAge + if minAge != nil { t := now.AddDate(-*minAge, 0, 0) - dobTo = &t + jy, jm, jd, err := jalaali.ToJalaali(t.Year(), t.Month(), t.Day()) + if err != nil { + } + s := fmt.Sprintf("%04d/%02d/%02d", jy, jm, jd) + dobTo = &s } + return } diff --git a/patientapp/delivery/http/analytic/dto/param.go b/patientapp/service/analytic/param.go similarity index 96% rename from patientapp/delivery/http/analytic/dto/param.go rename to patientapp/service/analytic/param.go index efd3bac6..946ecaef 100644 --- a/patientapp/delivery/http/analytic/dto/param.go +++ b/patientapp/service/analytic/param.go @@ -1,8 +1,6 @@ -package dto +package analytic import ( - "time" - "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" ) @@ -25,7 +23,7 @@ type PatientAnalyticItem struct { ID int64 `json:"id"` FirstName string `json:"first_name"` LastName string `json:"Last_name"` - DateOfBirth *time.Time `json:"dob,omitempty"` + DateOfBirth string `json:"dob,omitempty"` Sex entity.Sex `json:"sex"` Phone string `json:"phone"` Address entity.Address `json:"address"` diff --git a/patientapp/service/analytic/patient_filter.go b/patientapp/service/analytic/patient_filter.go index 6ecaf1c0..c7dd7695 100644 --- a/patientapp/service/analytic/patient_filter.go +++ b/patientapp/service/analytic/patient_filter.go @@ -1,14 +1,12 @@ package analytic import ( - "time" - "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" ) type PatientFilter struct { - DOBFrom *time.Time // born after - DOBTo *time.Time // born before + DOBFrom *string // born after + DOBTo *string // born before Sex *entity.Sex City *int64 @@ -22,8 +20,8 @@ type PatientFilter struct { } type PatientMapFilter struct { - MinDOB *time.Time - MaxDOB *time.Time + MinDOB *string + MaxDOB *string Sex *entity.Sex Search *string } diff --git a/patientapp/service/analytic/service.go b/patientapp/service/analytic/service.go index 2547fb82..a92cfbcf 100644 --- a/patientapp/service/analytic/service.go +++ b/patientapp/service/analytic/service.go @@ -6,7 +6,6 @@ import ( "fmt" "time" - analytic2 "git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic/dto" "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" ) @@ -22,7 +21,6 @@ type Repository interface { SummaryByCity(ctx context.Context, provinceID int, f PatientMapFilter) ([]entity.MapSummaryItem, error) SummaryByProvince(ctx context.Context, countryID int, f PatientMapFilter) ([]entity.MapSummaryItem, error) - SummaryByCountry(ctx context.Context, f PatientMapFilter) ([]entity.MapSummaryItem, error) } type Service struct { @@ -37,7 +35,7 @@ func NewPatientAnalyticService(repo Repository, validator Validator) Service { } } -func (s Service) List(ctx context.Context, req analytic2.ListPatientAnalyticRequest) (analytic2.PatientAnalyticResponse, error) { +func (s Service) List(ctx context.Context, req ListPatientAnalyticRequest) (PatientAnalyticResponse, error) { limit, offset := normalizeLimitOffset(req.Limit, req.Offset) @@ -57,21 +55,21 @@ func (s Service) List(ctx context.Context, req analytic2.ListPatientAnalyticRequ items, err := s.repository.GetPatients(ctx, filter) if err != nil { - return analytic2.PatientAnalyticResponse{}, fmt.Errorf("GetPatients: %w", err) + return PatientAnalyticResponse{}, fmt.Errorf("GetPatients: %w", err) } total, err := s.repository.CountPatients(ctx, filter) if err != nil { - return analytic2.PatientAnalyticResponse{}, fmt.Errorf("CountPatients: %w", err) + return PatientAnalyticResponse{}, fmt.Errorf("CountPatients: %w", err) } // mapping response - out := make([]analytic2.PatientAnalyticItem, 0, len(items)) + out := make([]PatientAnalyticItem, 0, len(items)) for _, value := range items { - out = append(out, analytic2.ToPatientResponse(value)) + out = append(out, ToPatientResponse(value)) } - return analytic2.PatientAnalyticResponse{ + return PatientAnalyticResponse{ Items: out, Limit: limit, Offset: offset, @@ -80,7 +78,7 @@ func (s Service) List(ctx context.Context, req analytic2.ListPatientAnalyticRequ } -func (s Service) GetMapSummary(ctx context.Context, req analytic2.GetPatientMapSummaryRequest) (analytic2.GetPatientMapSummaryResponse, error) { +func (s Service) GetMapSummary(ctx context.Context, req GetPatientMapSummaryRequest) (GetPatientMapSummaryResponse, error) { dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now()) @@ -94,35 +92,28 @@ func (s Service) GetMapSummary(ctx context.Context, req analytic2.GetPatientMapS switch req.Level { case entity.MapLevelCity: if req.ParentID == nil || *req.ParentID <= 0 { - return analytic2.GetPatientMapSummaryResponse{}, ErrInvalidProvinceID + return GetPatientMapSummaryResponse{}, ErrInvalidProvinceID } items, err := s.repository.SummaryByCity(ctx, *req.ParentID, filter) if err != nil { - return analytic2.GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByCity: %w", err) + return GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByCity: %w", err) } - return analytic2.GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil + return GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil case entity.MapLevelProvince: if req.ParentID == nil || *req.ParentID <= 0 { - return analytic2.GetPatientMapSummaryResponse{}, ErrInvalidCountryID + return GetPatientMapSummaryResponse{}, ErrInvalidCountryID } items, err := s.repository.SummaryByProvince(ctx, *req.ParentID, filter) if err != nil { - return analytic2.GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByProvince: %w", err) + return GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByProvince: %w", err) } - return analytic2.GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil - - case entity.MapLevelCountry: - items, err := s.repository.SummaryByCountry(ctx, filter) - if err != nil { - return analytic2.GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByCountry: %w", err) - } - return analytic2.GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil + return GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil default: - return analytic2.GetPatientMapSummaryResponse{}, ErrInvalidMapLevel + return GetPatientMapSummaryResponse{}, ErrInvalidMapLevel } } diff --git a/patientapp/service/analytic/validator.go b/patientapp/service/analytic/validator.go index 91079f66..7e65ad93 100644 --- a/patientapp/service/analytic/validator.go +++ b/patientapp/service/analytic/validator.go @@ -2,3 +2,7 @@ package analytic type Validator struct { } + +func NewValidator() *Validator { + return &Validator{} +} diff --git a/patientapp/service/entity/patient.go b/patientapp/service/entity/patient.go index e508a8d4..33857e26 100644 --- a/patientapp/service/entity/patient.go +++ b/patientapp/service/entity/patient.go @@ -1,12 +1,10 @@ package entity -import "time" - type Patient struct { ID int64 FirstName string LastName string - DateOfBirth *time.Time + DateOfBirth string Sex Sex Phone string Address Address diff --git a/patientapp/tree.txt b/patientapp/tree.txt deleted file mode 100644 index d7d81fdbccf6e24c96c4bda0df0727e6050cc390..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10 OcmezWkC%aq0fYe)+5#*9 diff --git a/pkg/date_parser/date_parser.go b/pkg/date_parser/date_parser.go new file mode 100644 index 00000000..5b89e6ff --- /dev/null +++ b/pkg/date_parser/date_parser.go @@ -0,0 +1,19 @@ +package date_parser + +import ( + "fmt" + "time" +) + +// ParseDate parses a date string in "YYYY-MM-DD" format and returns a time.Time object +func ParseDate(input string) (time.Time, error) { + const layout = "2006-01-02" + + // Parse the input string + convertedDate, err := time.Parse(layout, input) + if err != nil { + return time.Time{}, fmt.Errorf("invalid date format: %v", err) + } + + return convertedDate, nil +} diff --git a/vendor/github.com/jalaali/go-jalaali/LICENSE b/vendor/github.com/jalaali/go-jalaali/LICENSE new file mode 100644 index 00000000..1a6e1367 --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Amir Khazaie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/jalaali/go-jalaali/README.md b/vendor/github.com/jalaali/go-jalaali/README.md new file mode 100644 index 00000000..e5f2b4ec --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/README.md @@ -0,0 +1,23 @@ +# Jalaali + +Golang implementation of [Jalaali JS](https://github.com/jalaali/jalaali-js) and [Jalaali Python](https://github.com/jalaali/jalaali-python) implementations of Jalaali (Jalali, Persian, Khayyami, Khorshidi, Shamsi) convertion to Gregorian calendar system and vice-versa. + +This implementation is based on an [algorithm by Kazimierz M. Borkowski](http://www.astro.uni.torun.pl/~kb/Papers/EMP/PersianC-EMP.htm). Borkowski claims that this algorithm works correctly for 3000 years! + +Documentation on API is available [here](https://pkg.go.dev/github.com/jalaali/go-jalaali) at Go official documentation site. + +## Installation + +Use `go get` on this repository: + +```sh +$ go get -u github.com/jalaali/go-jalaali +``` + +## Usage + +* Wrapper around Golang [time package](https://golang.org/pkg/time): + * Call `Jalaali.Now()` to get instance of current time. You can use all function from `time` package with this wrapper. + * Call `Jalaali.From(t)` and pass a `time` instance to it. The you can work with it the same way you work with `time` package. +* Jalaali Formatting: + * Call `JFormat` method of a Jalaali instance and pass it the same formatting options that is used for Golang `time` package. The output will be in Jalaali date and use persian digits and words. diff --git a/vendor/github.com/jalaali/go-jalaali/convertion.go b/vendor/github.com/jalaali/go-jalaali/convertion.go new file mode 100644 index 00000000..3163786c --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/convertion.go @@ -0,0 +1,147 @@ +package jalaali + +import ( + "fmt" + "time" +) + +var ( + breaks = [...]int{-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, + 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178} +) + +// ToJalaali converts Gregorian to Jalaali date. Error is not nil if Jalaali +// year passed to function is not valid. +func ToJalaali(gregorianYear int, gregorianMonth time.Month, gregorianDay int) (int, Month, int, error) { + jy, jm, jd, err := d2j(g2d(gregorianYear, int(gregorianMonth), gregorianDay)) + return jy, Month(jm), jd, err +} + +// ToGregorian converts Jalaali to Gregorian date. Error is not nil if Jalaali +// year passed to function is not valid. +func ToGregorian(jalaaliYear int, jalaaliMonth Month, jalaaliDay int) (int, time.Month, int, error) { + // validate the jalaali date using the utility function + if !IsValidDate(jalaaliYear, int(jalaaliMonth), jalaaliDay) { + return 0, 0, 0, fmt.Errorf("invalid jalaali date: year=%d, month=%d, day=%d", jalaaliYear, jalaaliMonth, jalaaliDay) + } + jdn, err := j2d(jalaaliYear, int(jalaaliMonth), jalaaliDay) + if err != nil { + return 0, 0, 0, err + } + + gy, gm, gd := d2g(jdn) + return gy, time.Month(gm), gd, nil +} + +func j2d(jy, jm, jd int) (jdn int, err error) { + _, gy, march, err := jalCal(jy) + if err != nil { + return 0, err + } + return g2d(gy, 3, march) + (jm-1)*31 - div(jm, 7)*(jm-7) + jd - 1, nil +} + +func d2j(jdn int) (int, int, int, error) { + gy, _, _ := d2g(jdn) // Calculate Gregorian year (gy). + jy := gy - 621 + leap, _, march, err := jalCal(jy) + jdn1f := g2d(gy, 3, march) + + if err != nil { + return 0, 0, 0, err + } + + // Find number of days that passed since 1 Farvardin. + k := jdn - jdn1f + if k >= 0 { + if k <= 185 { + // The first 6 months. + jm := 1 + div(k, 31) + jd := mod(k, 31) + 1 + return jy, jm, jd, nil + } + // The remaining months. + k -= 186 + } else { + // Previous Jalaali year. + jy-- + k += 179 + if leap == 1 { + k++ + } + } + jm := 7 + div(k, 30) + jd := mod(k, 30) + 1 + return jy, jm, jd, nil +} + +func jalCal(jy int) (int, int, int, error) { + bl, gy, leapJ, jp := len(breaks), jy+621, -14, breaks[0] + jump := 0 + + if jy < jp || jy >= breaks[bl-1] { + return 0, 0, 0, &ErrorInvalidYear{jy} + } + + // Find the limiting years for the Jalaali year jy. + for i := 1; i < bl; i++ { + jm := breaks[i] + jump = jm - jp + if jy < jm { + break + } + leapJ += div(jump, 33)*8 + div(mod(jump, 33), 4) + jp = jm + } + n := jy - jp + + // Find the number of leap years from AD 621 to the beginning + // of the current Jalaali year in the Persian calendar. + leapJ += div(n, 33)*8 + div(mod(n, 33)+3, 4) + if mod(jump, 33) == 4 && jump-n == 4 { + leapJ++ + } + + // And the same in the Gregorian calendar (until the year gy). + leapG := div(gy, 4) - div((div(gy, 100)+1)*3, 4) - 150 + + // Determine the Gregorian date of Farvardin the 1st. + march := 20 + leapJ - leapG + + // Find how many years have passed since the last leap year. + if jump-n < 6 { + n -= jump + div(jump+4, 33)*33 + } + leap := mod(mod(n+1, 33)-1, 4) + if leap == -1 { + leap = 4 + } + + return leap, gy, march, nil +} + +func g2d(gy, gm, gd int) int { + d := div((gy+div(gm-8, 6)+100100)*1461, 4) + + div(153*mod(gm+9, 12)+2, 5) + + gd - 34840408 + d = d - div(div(gy+100100+div(gm-8, 6), 100)*3, 4) + 752 + return d +} + +func d2g(jdn int) (int, int, int) { + j := 4*jdn + 139361631 + j = j + div(div(4*jdn+183187720, 146097)*3, 4)*4 - 3908 + i := div(mod(j, 1461), 4)*5 + 308 + gd := div(mod(i, 153), 5) + 1 + gm := mod(div(i, 153), 12) + 1 + gy := div(j, 1461) - 100100 + div(8-gm, 6) + return gy, gm, gd +} + +func div(a, b int) int { + return a / b +} + +func mod(a, b int) int { + return a % b +} diff --git a/vendor/github.com/jalaali/go-jalaali/errors.go b/vendor/github.com/jalaali/go-jalaali/errors.go new file mode 100644 index 00000000..59d33d01 --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/errors.go @@ -0,0 +1,19 @@ +package jalaali + +import "fmt" + +// ErrorNilReference is happening when a pointer is nil. +type ErrorNilReference struct{} + +// ErrorInvalidYear is happening when year passed is is in proper range. +type ErrorInvalidYear struct { + year int +} + +func (e *ErrorNilReference) Error() string { + return "jalaali: reference is nil" +} + +func (e *ErrorInvalidYear) Error() string { + return fmt.Sprintf("jalaali: %v is invalid year", e.year) +} diff --git a/vendor/github.com/jalaali/go-jalaali/format.go b/vendor/github.com/jalaali/go-jalaali/format.go new file mode 100644 index 00000000..459df940 --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/format.go @@ -0,0 +1,318 @@ +package jalaali + +const ( + _ = iota + stdLongMonth = iota + stdNeedDate // "January" + stdMonth // "Jan" + stdNumMonth // "1" + stdZeroMonth // "01" + stdLongWeekDay // "Monday" + stdWeekDay // "Mon" + stdDay // "2" + stdUnderDay // "_2" + stdZeroDay // "02" + stdHour = iota + stdNeedClock // "15" + stdHour12 // "3" + stdZeroHour12 // "03" + stdMinute // "4" + stdZeroMinute // "04" + stdSecond // "5" + stdZeroSecond // "05" + stdLongYear = iota + stdNeedDate // "2006" + stdYear // "06" + stdPM = iota + stdNeedClock // "PM" + stdpm // "pm" + stdFracSecond0 // ".0", ".00", ... , trailing zeros included + stdFracSecond9 // ".9", ".99", ..., trailing zeros omitted + + stdNeedDate = 1 << 8 // need month, day, year + stdNeedClock = 2 << 8 // need hour, minute, second + stdArgShift = 16 // extra argument in high bits, above low stdArgShift + stdMask = 1<= 12 { + b = append(b, "بعدازظهر"...) + } else { + b = append(b, "قبل‌ازظهر"...) + } + case stdFracSecond0, stdFracSecond9: + b = formatNano(b, uint(j.Nanosecond()), std>>stdArgShift, std&stdMask == stdFracSecond9) + } + } + return b, nil +} + +// nextStdChunk finds the first occurrence of a std string in +// layout and returns the text before, the std string, and the text after. +func nextStdChunk(layout string) (prefix string, std int, suffix string) { + for i := 0; i < len(layout); i++ { + switch c := int(layout[i]); c { + case 'J': // January, Jan + if len(layout) >= i+3 && layout[i:i+3] == "Jan" { + if len(layout) >= i+7 && layout[i:i+7] == "January" { + return layout[0:i], stdLongMonth, layout[i+7:] + } + if !startsWithLowerCase(layout[i+3:]) { + return layout[0:i], stdMonth, layout[i+3:] + } + } + + case 'M': // Monday, Mon + if layout[i:i+3] == "Mon" { + if len(layout) >= i+6 && layout[i:i+6] == "Monday" { + return layout[0:i], stdLongWeekDay, layout[i+6:] + } + if !startsWithLowerCase(layout[i+3:]) { + return layout[0:i], stdWeekDay, layout[i+3:] + } + } + + case '0': // 01, 02, 03, 04, 05, 06 + if len(layout) >= i+2 && '1' <= layout[i+1] && layout[i+1] <= '6' { + return layout[0:i], std0x[layout[i+1]-'1'], layout[i+2:] + } + + case '1': // 15, 1 + if len(layout) >= i+2 && layout[i+1] == '5' { + return layout[0:i], stdHour, layout[i+2:] + } + return layout[0:i], stdNumMonth, layout[i+1:] + + case '2': // 2006, 2 + if len(layout) >= i+4 && layout[i:i+4] == "2006" { + return layout[0:i], stdLongYear, layout[i+4:] + } + return layout[0:i], stdDay, layout[i+1:] + + case '_': // _2, _2006 + if len(layout) >= i+2 && layout[i+1] == '2' { + //_2006 is really a literal _, followed by stdLongYear + if len(layout) >= i+5 && layout[i+1:i+5] == "2006" { + return layout[0 : i+1], stdLongYear, layout[i+5:] + } + return layout[0:i], stdUnderDay, layout[i+2:] + } + + case '3': + return layout[0:i], stdHour12, layout[i+1:] + + case '4': + return layout[0:i], stdMinute, layout[i+1:] + + case '5': + return layout[0:i], stdSecond, layout[i+1:] + + case 'P': // PM + if len(layout) >= i+2 && layout[i+1] == 'M' { + return layout[0:i], stdPM, layout[i+2:] + } + + case 'p': // pm + if len(layout) >= i+2 && layout[i+1] == 'm' { + return layout[0:i], stdpm, layout[i+2:] + } + + case '.': // .000 or .999 - repeated digits for fractional seconds. + if i+1 < len(layout) && (layout[i+1] == '0' || layout[i+1] == '9') { + ch := layout[i+1] + j := i + 1 + for j < len(layout) && layout[j] == ch { + j++ + } + // String of digits must end here - only fractional second is all digits. + if !isDigit(layout, j) { + std := stdFracSecond0 + if layout[i+1] == '9' { + std = stdFracSecond9 + } + std |= (j - (i + 1)) << stdArgShift + return layout[0:i], std, layout[j:] + } + } + } + } + return layout, 0, "" +} + +// startsWithLowerCase reports whether the string has a lower-case letter at the beginning. +// Its purpose is to prevent matching strings like "Month" when looking for "Mon". +func startsWithLowerCase(str string) bool { + if len(str) == 0 { + return false + } + c := str[0] + return 'a' <= c && c <= 'z' +} + +// isDigit reports whether s[i] is in range and is a decimal digit. +func isDigit(s string, i int) bool { + if len(s) <= i { + return false + } + c := s[i] + return '0' <= c && c <= '9' +} + +// appendInt appends the decimal form of x to b and returns the result. +// If the decimal form (excluding sign) is shorter than width, the result is padded with leading 0's. +// Duplicates functionality in strconv, but avoids dependency. +func appendInt(b []byte, x int, width int) []byte { + u := uint(x) + if x < 0 { + b = append(b, '-') + u = uint(-x) + } + + // Assemble decimal in reverse order. + var buf [20]rune + i := len(buf) + for u >= 10 { + i-- + q := u / 10 + buf[i] = rune('۰' + u - q*10) + u = q + } + i-- + buf[i] = rune('۰' + u) + + // Add 0-padding. + for w := len(buf) - i; w < width; w++ { + b = append(b, []byte("۰")...) + } + + return append(b, []byte(string(buf[i:]))...) +} + +// formatNano appends a fractional second, as nanoseconds, to b +// and returns the result. +func formatNano(b []byte, nanosec uint, n int, trim bool) []byte { + u := nanosec + var buf [9]rune + for start := len(buf); start > 0; { + start-- + buf[start] = rune(u%10 + '۰') + u /= 10 + } + + if n > 9 { + n = 9 + } + if trim { + for n > 0 && buf[n-1] == '۰' { + n-- + } + if n == 0 { + return b + } + } + b = append(b, '.') + return append(b, []byte(string(buf[:n]))...) +} diff --git a/vendor/github.com/jalaali/go-jalaali/go.mod b/vendor/github.com/jalaali/go-jalaali/go.mod new file mode 100644 index 00000000..b4d320f2 --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/go.mod @@ -0,0 +1,3 @@ +module github.com/jalaali/go-jalaali + +go 1.13 diff --git a/vendor/github.com/jalaali/go-jalaali/jalaali.go b/vendor/github.com/jalaali/go-jalaali/jalaali.go new file mode 100644 index 00000000..622a642b --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/jalaali.go @@ -0,0 +1,79 @@ +package jalaali + +import ( + "strconv" + "time" +) + +// A simple wrapper around Golang default time package. You have all the functionality of +// default time package and functionalities needed for Jalaali calender. +type Jalaali struct { + time.Time +} + +// From initialize new instance of Jalaali from a time instance. +func From(t time.Time) Jalaali { + return Jalaali{t} +} + +// Now with return Jalaali instance of current time. +func Now() Jalaali { + return From(time.Now()) +} + +// A Month specifies a month of the year (Farvardin = 1, ...). +type Month int + +const ( + Farvardin Month = 1 + iota + Ordibehesht + Khordad + Tir + Mordad + Shahrivar + Mehr + Aban + Azar + Dey + Bahman + Esfand +) + +var months = []string{ + "فروردین", "اردیبهشت", "خرداد", + "تیر", "مرداد", "شهریور", + "مهر", "آبان", "آذر", + "دی", "بهمن", "اسفند", +} + +func (m Month) String() string { + if Farvardin <= m && m <= Esfand { + return months[m-1] + } + return "%!Month(" + strconv.Itoa(int(m)) + ")" +} + +// A Weekday specifies a day of the week (Shanbe = 0, ...). +type Weekday int + +const ( + Shanbe Weekday = iota + IekShanbe + DoShanbe + SeShanbe + ChaharShanbe + PanjShanbe + Jome +) + +var days = []string{ + "شنبه", "یک‌شنبه", "دوشنبه", "سه‌شنبه", "چهارشنبه", "پنج‌شنبه", "جمعه", +} + +func (d Weekday) String() string { + if Shanbe <= d && d <= Jome { + return days[d] + } + return "%!Weekday(" + strconv.Itoa(int(d)) + ")" +} + diff --git a/vendor/github.com/jalaali/go-jalaali/jalaali_test.go b/vendor/github.com/jalaali/go-jalaali/jalaali_test.go new file mode 100644 index 00000000..6db3d615 --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/jalaali_test.go @@ -0,0 +1,188 @@ +package jalaali + +import ( + "testing" + "time" +) + +func TestFromYMD(t *testing.T) { + tests := []struct { + gy, gm, gd, jy, jm, jd int + }{ + {1981, 8, 17, 1360, 5, 26}, + {2013, 1, 10, 1391, 10, 21}, + {2014, 8, 4, 1393, 5, 13}, + } + + for _, test := range tests { + y, m, d, err := ToJalaali(test.gy, time.Month(test.gm), test.gd) + if err != nil { + t.Errorf("%v", err) + } else if y != test.jy || m != Month(test.jm) || d != test.jd { + t.Errorf("Expected %v/%v/%v got %v/%v%v.", test.jy, test.jm, test.jd, y, m, d) + } + } + +} + +func TestToGregorian(t *testing.T) { + tests := []struct { + jy, jm, jd, gy, gm, gd int + }{ + {1360, 5, 26, 1981, 8, 17}, + {1391, 10, 21, 2013, 1, 10}, + {1393, 5, 13, 2014, 8, 4}, + } + + for _, test := range tests { + y, m, d, err := ToGregorian(test.jy, Month(test.jm), test.jd) + if err != nil { + t.Errorf("%v", err) + } else if y != test.gy || m != time.Month(test.gm) || d != test.gd { + t.Errorf("Expected %v/%v/%v got %v/%v%v.", test.gy, test.gm, test.gd, y, m, d) + } + } + +} + +func TestIsValidDate(t *testing.T) { + tests := []struct { + y, m, d int + ok bool + }{ + {-62, 12, 29, false}, + {-61, 1, 1, true}, + {3178, 1, 1, false}, + {3177, 12, 29, true}, + {1393, 0, 1, false}, + {1393, 13, 1, false}, + {1393, 1, 0, false}, + {1393, 1, 32, false}, + {1393, 1, 31, true}, + {1393, 11, 31, false}, + {1393, 11, 30, true}, + {1393, 12, 30, false}, + {1393, 12, 29, true}, + {1395, 12, 30, true}, + } + + for _, test := range tests { + valid := IsValidDate(test.y, test.m, test.d) + if valid != test.ok { + calculated, actual := "", " not" + if test.ok { + calculated, actual = " not", "" + } + t.Errorf("%v/%v/%v is%v valid date but considered%v valid.", + test.y, test.m, test.d, actual, calculated) + } + } +} + +func TestIsLeapYear(t *testing.T) { + tests := []struct { + year int + leap bool + }{ + {1393, false}, + {1394, false}, + {1395, true}, + {1396, false}, + } + + for _, test := range tests { + leap, err := IsLeapYear(test.year) + if err != nil { + t.Errorf("%v", err) + } else if leap != test.leap { + calculated, actual := "", " not" + if leap { + calculated, actual = " not", "" + } + t.Errorf("%v is%v leap but considered%v leap.", test.year, actual, calculated) + } + } +} + +func TestMonthLength(t *testing.T) { + tests := []struct { + y, m, ml int + }{ + {1393, 1, 31}, + {1393, 4, 31}, + {1393, 6, 31}, + {1393, 7, 30}, + {1393, 10, 30}, + {1393, 12, 29}, + {1394, 12, 29}, + {1395, 12, 30}, + } + + for _, test := range tests { + calculated, err := MonthLength(test.y, test.m) + if err != nil { + t.Errorf("%v", err) + } else if calculated != test.ml { + t.Errorf("Length of %v/%v month is %v but considered %v.", + test.y, test.m, test.ml, calculated) + } + } +} + +func TestJFormat(t *testing.T) { + iran, _ := time.LoadLocation("Asia/Tehran") + + tests := []struct { + time time.Time + format []string + result []string + }{ + { + time.Date(2001, 1, 1, 1, 1, 1, 1, iran), + []string{ + "2006 06", // Year formatting + "January Jan 1 01", // Month formatting + "Monday Mon 2 _2 02", // Day formatting + "15 3 03 4 04 5 05 PM pm", // Hour, Minute, Second formatting + ".0 .00 .000 .000000 .000000000 .9 .99 .999 .999999 .999999999", // Nanosecond formatting + }, + []string{ + "۱۳۷۹ ۷۹", // Year formatting + "دی دی ۱۰ ۱۰", // Month formatting + "دوشنبه دوشنبه ۱۲ ۱۲ ۱۲", // Day formatting + "۰۱ ۱ ۰۱ ۱ ۰۱ ۱ ۰۱ قبل‌ازظهر قبل‌ازظهر", // Hour, Minute, Second formatting + ".۰ .۰۰ .۰۰۰ .۰۰۰۰۰۰ .۰۰۰۰۰۰۰۰۱ .۰۰۰۰۰۰۰۰۱", // Nanosecond formatting + }, + }, { + time.Date(2001, 2, 3, 15, 17, 1, 999999999, iran), + []string{ + "2006 06", // Year formatting + "January Jan 1 01", // Month formatting + "Monday Mon 2 _2 02", // Day formatting + "15 3 03 4 04 5 05 PM pm", // Hour, Minute, Second formatting + ".0 .00 .000 .000000 .000000000 .9 .99 .999 .999999 .999999999", // Nanosecond formatting + }, + []string{ + "۱۳۷۹ ۷۹", // Year formatting + "بهمن بهمن ۱۱ ۱۱", // Month formatting + "شنبه شنبه ۱۵ ۱۵ ۱۵", // Day formatting + "۱۵ ۳ ۰۳ ۱۷ ۱۷ ۱ ۰۱ بعدازظهر بعدازظهر", // Hour, Minute, Second formatting + ".۹ .۹۹ .۹۹۹ .۹۹۹۹۹۹ .۹۹۹۹۹۹۹۹۹ .۹ .۹۹ .۹۹۹ .۹۹۹۹۹۹ .۹۹۹۹۹۹۹۹۹", // Nanosecond formatting + }, + }, + } + + for i, test := range tests { + j := From(test.time) + + for f := range test.format { + result, err := j.JFormat(test.format[f]) + if err != nil { + t.Error(err) + } + if result != test.result[f] { + t.Error("Bad formatting for test as index: ", i, "\nWanted: ", test.result[f], "\nGot: ", result) + } + } + } +} diff --git a/vendor/github.com/jalaali/go-jalaali/utils.go b/vendor/github.com/jalaali/go-jalaali/utils.go new file mode 100644 index 00000000..c2279763 --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/utils.go @@ -0,0 +1,53 @@ +package jalaali + +import "strings" + +var enToFa = strings.NewReplacer( + "0", "۰", + "1", "۱", + "2", "۲", + "3", "۳", + "4", "۴", + "5", "۵", + "6", "۶", + "7", "۷", + "8", "۸", + "9", "۹", +) + +// IsValidDate take Jalaali date and return true if it is valid, +// otherwise false. +func IsValidDate(jy, jm, jd int) bool { + d, err := MonthLength(jy, jm) + if err != nil { + return false + } + return -61 <= jy && jy <= 3177 && + 1 <= jm && jm <= 12 && + 1 <= jd && jd <= d +} + +// MonthLength take Jalaali date and return length of that specific +// month. Error is not nil if Jalaali year passed to function is not valid. +func MonthLength(jy, jm int) (int, error) { + if jm <= 6 { + return 31, nil + } else if jm <= 11 { + return 30, nil + } + + leap, err := IsLeapYear(jy) + if err != nil { + return 0, err + } else if leap { + return 30, nil + } + return 29, nil +} + +// IsLeapYear take a Jalaali year and return true if it is leap year. Error +// is not nil if Jalaali year passed to function is not valid. +func IsLeapYear(jy int) (bool, error) { + leap, _, _, err := jalCal(jy) + return leap == 0, err +} \ No newline at end of file From 787f338202f7422b73393267f3ca52281cf81e26 Mon Sep 17 00:00:00 2001 From: Mohammad Amin Date: Fri, 3 Apr 2026 16:46:02 +0330 Subject: [PATCH 3/5] add cmd and bug fixed --- patientapp/cmd/main.go | 26 ++++++++++++++++ patientapp/repository/mysql/analytic_repo.go | 16 ++++++---- patientapp/service/analytic/param.go | 32 ++++++++++---------- patientapp/service/analytic/service.go | 6 ++-- 4 files changed, 55 insertions(+), 25 deletions(-) create mode 100644 patientapp/cmd/main.go diff --git a/patientapp/cmd/main.go b/patientapp/cmd/main.go new file mode 100644 index 00000000..0c28ac82 --- /dev/null +++ b/patientapp/cmd/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "time" + + "git.gocasts.ir/ebhomengo/niki/patientapp" + "git.gocasts.ir/ebhomengo/niki/patientapp/config" + "git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql" +) + +func main() { + db := mysql.DataBase{} + + cfg := config.Config{ + Port: 8080, + Cors: config.Cors{ + AllowOrigins: []string{"*"}, + }, + ShutDownCtxTimeout: 5 * time.Second, + } + + app := patientapp.Setup(cfg, &db) + + app.Start() + +} diff --git a/patientapp/repository/mysql/analytic_repo.go b/patientapp/repository/mysql/analytic_repo.go index 8a29427f..bb5aa9c3 100644 --- a/patientapp/repository/mysql/analytic_repo.go +++ b/patientapp/repository/mysql/analytic_repo.go @@ -13,7 +13,7 @@ type DataBase struct { patients []entity.Patient } -func NewPatientRepo( /*conn *mysql.DB*/) *DataBase { +func NewPatientRepo( /*conn *mysql.DB*/ ) *DataBase { patients, err := LoadPatientsFromJSON("C:\\Users\\cafel\\Documents\\gocast\\eb\\patientapp\\repository\\mysql\\rawdata.json") if err != nil { log.Fatal(err) @@ -36,21 +36,25 @@ func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) return len(db.patients), nil } -func (db *DataBase) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) { +func (db *DataBase) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) { - var out []entity.MapSummaryItem + //var out []entity.MapSummaryItem + result := make(map[uint][]entity.MapSummaryItem, 0) for _, patient := range db.patients { if patient.Address.ProvinceID == provinceID { - out = append(out, entity.MapSummaryItem{ + out := entity.MapSummaryItem{ LocationID: int64(patient.Address.ID), Name: patient.Address.Name, CentroidLat: patient.Address.Lat, CentroidLng: patient.Address.Lon, - }) + } + result[patient.Address.ProvinceID] = append(result[patient.Address.ProvinceID], out) + } + } - return out, nil + return result, nil } func (db *DataBase) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) { diff --git a/patientapp/service/analytic/param.go b/patientapp/service/analytic/param.go index 946ecaef..bd7d4400 100644 --- a/patientapp/service/analytic/param.go +++ b/patientapp/service/analytic/param.go @@ -6,17 +6,17 @@ import ( type ListPatientAnalyticRequest struct { // All fields are optional - MinAge *int `json:"minAge,omitempty"` - MaxAge *int `json:"maxAge,omitempty"` - Sex *entity.Sex `json:"sex,omitempty"` + MinAge *int `query:"minAge,omitempty"` + MaxAge *int `query:"maxAge,omitempty"` + Sex *entity.Sex `query:"sex,omitempty"` - City *int64 `json:"city,omitempty"` - Province *int64 `json:"province,omitempty"` + City *int64 `query:"city,omitempty"` + Province *int64 `query:"province,omitempty"` - Search *string `json:"search,omitempty"` + Search *string `query:"search,omitempty"` - Limit int `json:"limit,omitempty"` - Offset int `json:"offset,omitempty"` + Limit int `query:"limit,omitempty"` + Offset int `query:"offset,omitempty"` } type PatientAnalyticItem struct { @@ -53,16 +53,16 @@ func ToPatientResponse(patient entity.Patient) PatientAnalyticItem { // =========================== Map ================================== type GetPatientMapSummaryRequest struct { - Level entity.MapLevel `json:"level"` - ParentID *int `json:"parentID"` + Level entity.MapLevel `query:"level"` + ParentID *int `query:"parentID"` - MinAge *int `json:"minAge,omitempty"` - MaxAge *int `json:"maxAge,omitempty"` - Sex *entity.Sex `json:"sex,omitempty"` - Search *string `json:"search,omitempty"` + MinAge *int `query:"minAge,omitempty"` + MaxAge *int `query:"maxAge,omitempty"` + Sex *entity.Sex `query:"sex,omitempty"` + Search *string `query:"search,omitempty"` } type GetPatientMapSummaryResponse struct { - Level entity.MapLevel `json:"level"` - Items []entity.MapSummaryItem `json:"items"` + Level entity.MapLevel `json:"level"` + Items map[uint][]entity.MapSummaryItem `json:"items"` } diff --git a/patientapp/service/analytic/service.go b/patientapp/service/analytic/service.go index a92cfbcf..683f9a74 100644 --- a/patientapp/service/analytic/service.go +++ b/patientapp/service/analytic/service.go @@ -19,8 +19,8 @@ type Repository interface { GetPatients(ctx context.Context, f PatientFilter) ([]entity.Patient, error) CountPatients(ctx context.Context, f PatientFilter) (int, error) - SummaryByCity(ctx context.Context, provinceID int, f PatientMapFilter) ([]entity.MapSummaryItem, error) - SummaryByProvince(ctx context.Context, countryID int, f PatientMapFilter) ([]entity.MapSummaryItem, error) + SummaryByCity(ctx context.Context, provinceID int, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) + SummaryByProvince(ctx context.Context, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) } type Service struct { @@ -106,7 +106,7 @@ func (s Service) GetMapSummary(ctx context.Context, req GetPatientMapSummaryRequ return GetPatientMapSummaryResponse{}, ErrInvalidCountryID } - items, err := s.repository.SummaryByProvince(ctx, *req.ParentID, filter) + items, err := s.repository.SummaryByProvince(ctx, filter) if err != nil { return GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByProvince: %w", err) } From 564b021a7cc18dd758ceda4834569ee21e33dc3c Mon Sep 17 00:00:00 2001 From: Mohammad Amin Date: Fri, 3 Apr 2026 20:02:43 +0330 Subject: [PATCH 4/5] bug fixed --- patientapp/delivery/http/analytic/router.go | 4 +- patientapp/repository/mysql/analytic_repo.go | 315 +-- patientapp/repository/mysql/dbconfig.yml | 10 - patientapp/repository/mysql/helper.go | 56 - patientapp/repository/mysql/rawdata.json | 2202 ------------------ patientapp/service/analytic/param.go | 19 +- patientapp/service/analytic/service.go | 20 +- patientapp/service/analytic/validator.go | 8 - 8 files changed, 27 insertions(+), 2607 deletions(-) delete mode 100644 patientapp/repository/mysql/dbconfig.yml delete mode 100644 patientapp/repository/mysql/helper.go delete mode 100644 patientapp/repository/mysql/rawdata.json delete mode 100644 patientapp/service/analytic/validator.go diff --git a/patientapp/delivery/http/analytic/router.go b/patientapp/delivery/http/analytic/router.go index eddd18c4..1ef8c004 100644 --- a/patientapp/delivery/http/analytic/router.go +++ b/patientapp/delivery/http/analytic/router.go @@ -9,8 +9,8 @@ import ( func NewPatientAnalyticRouter(s *echo.Group) { repo := mysql.NewPatientRepo() - validator := analytic2.NewValidator() - analyticService := analytic2.NewPatientAnalyticService(repo, *validator) + + analyticService := analytic2.NewPatientAnalyticService(repo) h := NewHandler(analyticService) diff --git a/patientapp/repository/mysql/analytic_repo.go b/patientapp/repository/mysql/analytic_repo.go index bb5aa9c3..743d8e51 100644 --- a/patientapp/repository/mysql/analytic_repo.go +++ b/patientapp/repository/mysql/analytic_repo.go @@ -2,7 +2,6 @@ package mysql import ( "context" - "log" "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic" "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" @@ -14,11 +13,8 @@ type DataBase struct { } func NewPatientRepo( /*conn *mysql.DB*/ ) *DataBase { - patients, err := LoadPatientsFromJSON("C:\\Users\\cafel\\Documents\\gocast\\eb\\patientapp\\repository\\mysql\\rawdata.json") - if err != nil { - log.Fatal(err) - return nil - } + patients := make([]entity.Patient, 0) + return &DataBase{ //conn: conn, patients: patients, @@ -38,316 +34,13 @@ func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) func (db *DataBase) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) { - //var out []entity.MapSummaryItem - result := make(map[uint][]entity.MapSummaryItem, 0) + var out map[uint][]entity.MapSummaryItem - for _, patient := range db.patients { - if patient.Address.ProvinceID == provinceID { - out := entity.MapSummaryItem{ - LocationID: int64(patient.Address.ID), - Name: patient.Address.Name, - CentroidLat: patient.Address.Lat, - CentroidLng: patient.Address.Lon, - } - result[patient.Address.ProvinceID] = append(result[patient.Address.ProvinceID], out) - - } - - } - return result, nil + return out, nil } func (db *DataBase) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) { result := make(map[uint][]entity.MapSummaryItem, 0) - for _, patient := range db.patients { - out := entity.MapSummaryItem{ - LocationID: int64(patient.Address.ID), - Name: patient.Address.Name, - CentroidLat: patient.Address.Lat, - CentroidLng: patient.Address.Lon, - } - result[patient.Address.ProvinceID] = append(result[patient.Address.ProvinceID], out) - } return result, nil } - -//func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) { -// const op = "mysqlpatient.GetPatients" -// limit, offset := clampLimitOffset(f.Limit, f.Offset) -// -// q := ` -//SELECT -// p.id, -// p.first_name, -// p.last_name, -// p.date_of_birth, -// p.sex, -// p.phone, -// -// p.country_id, -// p.province_id, -// p.city_id, -// p.postal_code, -// p.address_line1, -// p.address_line2, -// -// p.case_status, -// p.referral_source, -// p.assigned_staff_id, -// p.start_date, -// p.end_date -//FROM patients p -//WHERE p.deleted_at IS NULL -//` -// -// stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) -// if err != nil { -// return nil, richerror.New(op).WithErr(err). -// WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) -// } -// args := make([]any, 0) -// -// // filters -// if f.Sex != nil { -// q += " AND p.sex = ?" -// args = append(args, string(*f.Sex)) -// } -// if f.DOBFrom != nil { -// q += " AND p.date_of_birth >= ?" -// args = append(args, f.DOBFrom.Format("2006-01-02")) -// } -// if f.DOBTo != nil { -// q += " AND p.date_of_birth <= ?" -// args = append(args, f.DOBTo.Format("2006-01-02")) -// } -// if f.Country != nil { -// q += " AND p.country_id = ?" -// args = append(args, *f.Country) -// } -// if f.Province != nil { -// q += " AND p.province_id = ?" -// args = append(args, *f.Province) -// } -// if f.City != nil { -// q += " AND p.city_id = ?" -// args = append(args, *f.City) -// } -// -// if f.Search != nil && strings.TrimSpace(*f.Search) != "" { -// term := "%" + strings.TrimSpace(*f.Search) + "%" -// q += " AND (p.first_name LIKE ? OR p.last_name LIKE ? OR p.phone LIKE ?)" -// args = append(args, term, term, term) -// } -// -// q += " ORDER BY p.id DESC LIMIT ? OFFSET ?" -// args = append(args, limit, offset) -// -// rows, err := stmt.QueryContext(ctx, args...) -// if err != nil { -// return nil, err -// } -// defer rows.Close() -// -// out := make([]entity.Patient, 0, limit) -// -// for rows.Next() { -// var p entity.Patient -// var dob sql.NullTime -// var provinceID, cityID uint -// -// if err := rows.Scan( -// &p.ID, -// &p.FirstName, -// &p.LastName, -// &dob, -// &p.Sex, -// &p.Phone, -// -// &provinceID, -// &cityID, -// &p.Address.PostalCode, -// -// &p.CaseStatus, -// &p.ReferralSource, -// &p.AssignedStaffId, -// &p.StartDate, -// &p.EndDate, -// ); err != nil { -// return nil, err -// } -// -// if dob.Valid { -// t := dob.Time -// p.DateOfBirth = &t -// } -// -// p.Address.ProvinceID = provinceID -// p.Address.CityID = cityID -// -// out = append(out, p) -// } -// -// return out, rows.Err() -//} -// -//func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) { -// const op = "mysqlpatient.CountPatients" -// q := `SELECT COUNT(*) FROM patients p WHERE p.deleted_at IS NULL` -// args := make([]any, 0) -// -// stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) -// if err != nil { -// return 0, richerror.New(op).WithErr(err). -// WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) -// } -// -// if f.Sex != nil { -// q += " AND p.sex = ?" -// args = append(args, string(*f.Sex)) -// } -// if f.DOBFrom != nil { -// q += " AND p.date_of_birth >= ?" -// args = append(args, f.DOBFrom.Format("2006-01-02")) -// } -// if f.DOBTo != nil { -// q += " AND p.date_of_birth <= ?" -// args = append(args, f.DOBTo.Format("2006-01-02")) -// } -// if f.Country != nil { -// q += " AND p.country_id = ?" -// args = append(args, *f.Country) -// } -// if f.Province != nil { -// q += " AND p.province_id = ?" -// args = append(args, *f.Province) -// } -// if f.City != nil { -// q += " AND p.city_id = ?" -// args = append(args, *f.City) -// } -// -// var total int -// -// err = stmt.QueryRowContext(ctx, args...).Scan(&total) -// return total, err -//} -// -//func (db *DataBase) SummaryByCity(ctx context.Context, provinceID int, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) { -// const op = "mysqlpatient.SummaryByCity" -// q := ` -//SELECT -// c.id AS location_id, -// c.name, -// c.centroid_lat, -// c.centroid_lng, -// COUNT(p.id) AS count -//FROM locations c -//JOIN patients p ON p.city_id = c.id -//WHERE c.level = 'city' -// AND c.parent_id = ? -// AND p.deleted_at IS NULL -//` -// -// stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) -// if err != nil { -// return nil, richerror.New(op).WithErr(err). -// WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) -// } -// args := []any{provinceID} -// -// if f.Sex != nil { -// q += " AND p.sex = ?" -// args = append(args, string(*f.Sex)) -// } -// if f.MinDOB != nil { -// q += " AND p.date_of_birth >= ?" -// args = append(args, f.MinDOB.Format("2006-01-02")) -// } -// if f.MaxDOB != nil { -// q += " AND p.date_of_birth <= ?" -// args = append(args, f.MaxDOB.Format("2006-01-02")) -// } -// if f.Search != nil && strings.TrimSpace(*f.Search) != "" { -// term := "%" + strings.TrimSpace(*f.Search) + "%" -// q += " AND (p.first_name LIKE ? OR p.last_name LIKE ? OR p.phone LIKE ?)" -// args = append(args, term, term, term) -// } -// -// q += ` GROUP BY c.id, c.name, c.centroid_lat, c.centroid_lng -// ORDER BY count DESC` -// -// rows, err := stmt.QueryContext(ctx, args...) -// if err != nil { -// return nil, err -// } -// defer rows.Close() -// -// var out []entity.MapSummaryItem -// for rows.Next() { -// var it entity.MapSummaryItem -// if err := rows.Scan(&it.LocationID, &it.Name, &it.CentroidLat, &it.CentroidLng, &it.Count); err != nil { -// return nil, err -// } -// out = append(out, it) -// } -// return out, rows.Err() -//} -// -//func (db *DataBase) SummaryByProvince(ctx context.Context, countryID int, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) { -// const op = "mysqlpatient.SummaryByProvince" -// -// q := ` -//SELECT -// prov.id AS location_id, -// prov.name, -// prov.centroid_lat, -// prov.centroid_lng, -// COUNT(p.id) AS count -//FROM patients p -//JOIN locations city ON city.id = p.city_id AND city.level = 'city' -//JOIN locations prov ON prov.id = city.parent_id AND prov.level = 'province' -//WHERE prov.parent_id = ? -// AND p.deleted_at IS NULL -//` -// -// stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q) -// if err != nil { -// return nil, richerror.New(op).WithErr(err). -// WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected) -// } -// -// args := []any{countryID} -// -// if f.Sex != nil { -// q += " AND p.sex = ?" -// args = append(args, string(*f.Sex)) -// } -// if f.MinDOB != nil { -// q += " AND p.date_of_birth >= ?" -// args = append(args, f.MinDOB.Format("2006-01-02")) -// } -// if f.MaxDOB != nil { -// q += " AND p.date_of_birth <= ?" -// args = append(args, f.MaxDOB.Format("2006-01-02")) -// } -// -// q += ` GROUP BY prov.id, prov.name, prov.centroid_lat, prov.centroid_lng -// ORDER BY count DESC` -// -// rows, err := stmt.QueryContext(ctx, args...) -// if err != nil { -// return nil, err -// } -// defer rows.Close() -// -// var out []entity.MapSummaryItem -// for rows.Next() { -// var it entity.MapSummaryItem -// if err := rows.Scan(&it.LocationID, &it.Name, &it.CentroidLat, &it.CentroidLng, &it.Count); err != nil { -// return nil, err -// } -// out = append(out, it) -// } -// return out, rows.Err() -//} diff --git a/patientapp/repository/mysql/dbconfig.yml b/patientapp/repository/mysql/dbconfig.yml deleted file mode 100644 index 8c7e965d..00000000 --- a/patientapp/repository/mysql/dbconfig.yml +++ /dev/null @@ -1,10 +0,0 @@ -postgres_db: - driver: postgres - host: patient-db - port: 5432 - user: patient_admin - password: password1234 - db_name: patient_db - ssl_mode: disable - -path_of_migration: ./patientapp/repository/mysql/migrations \ No newline at end of file diff --git a/patientapp/repository/mysql/helper.go b/patientapp/repository/mysql/helper.go deleted file mode 100644 index 181ec5df..00000000 --- a/patientapp/repository/mysql/helper.go +++ /dev/null @@ -1,56 +0,0 @@ -package mysql - -import ( - "encoding/json" - "fmt" - "os" - "strings" - - "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" -) - -type whereClause struct { - sql strings.Builder - args []any -} - -func (w *whereClause) add(cond string, args ...any) { - if w.sql.Len() == 0 { - w.sql.WriteString(" WHERE ") - } else { - w.sql.WriteString(" AND ") - } - w.sql.WriteString(cond) - w.args = append(w.args, args...) -} - -func (w *whereClause) String() string { return w.sql.String() } - -func clampLimitOffset(limit, offset int) (int, int) { - if limit <= 0 { - limit = 50 - } - if limit > 100 { - limit = 100 - } - if offset < 0 { - offset = 0 - } - return limit, offset -} - -func LoadPatientsFromJSON(filePath string) ([]entity.Patient, error) { - // Read the file - data, err := os.ReadFile(filePath) - if err != nil { - return nil, fmt.Errorf("failed to read file: %w", err) - } - - // Unmarshal into slice of Patient - var patients []entity.Patient - if err := json.Unmarshal(data, &patients); err != nil { - return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) - } - - return patients, nil -} diff --git a/patientapp/repository/mysql/rawdata.json b/patientapp/repository/mysql/rawdata.json deleted file mode 100644 index 73134bec..00000000 --- a/patientapp/repository/mysql/rawdata.json +++ /dev/null @@ -1,2202 +0,0 @@ -[ - { - "ID": 1, - "FirstName": "علی", - "LastName": "احمدی", - "DateOfBirth": "1370/05/12", - "Sex": "male", - "Phone": "09121234567", - "Address": { - "ID": 1, - "PostalCode": "1234567890", - "Name": "home", - "Lat": 35.6892, - "Lon": 51.3890, - "CityID": 14, - "ProvinceID": 8 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 3, - "StartDate": "1402/01/15", - "EndDate": "" - }, - { - "ID": 2, - "FirstName": "فاطمه", - "LastName": "محمدی", - "DateOfBirth": "1365/08/20", - "Sex": "female", - "Phone": "09351234567", - "Address": { - "ID": 2, - "PostalCode": "9876543210", - "Name": "home", - "Lat": 36.2972, - "Lon": 59.6067, - "CityID": 27, - "ProvinceID": 15 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 5, - "StartDate": "1402/02/10", - "EndDate": "" - }, - { - "ID": 3, - "FirstName": "محمد", - "LastName": "حسینی", - "DateOfBirth": "1358/11/03", - "Sex": "male", - "Phone": "09181234567", - "Address": { - "ID": 3, - "PostalCode": "4561237890", - "Name": "home", - "Lat": 32.6546, - "Lon": 51.6680, - "CityID": 43, - "ProvinceID": 4 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 2, - "StartDate": "1402/03/05", - "EndDate": "" - }, - { - "ID": 4, - "FirstName": "زهرا", - "LastName": "رضایی", - "DateOfBirth": "1375/02/28", - "Sex": "female", - "Phone": "09301234567", - "Address": { - "ID": 4, - "PostalCode": "7894561230", - "Name": "home", - "Lat": 37.2808, - "Lon": 49.5832, - "CityID": 67, - "ProvinceID": 22 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 7, - "StartDate": "1402/04/18", - "EndDate": "" - }, - { - "ID": 5, - "FirstName": "حسن", - "LastName": "کریمی", - "DateOfBirth": "1360/07/15", - "Sex": "male", - "Phone": "09141234567", - "Address": { - "ID": 5, - "PostalCode": "3216549870", - "Name": "home", - "Lat": 34.3277, - "Lon": 47.0785, - "CityID": 88, - "ProvinceID": 19 - }, - "CaseStatus": "closed", - "ReferralSource": "hospital", - "AssignedStaffId": 4, - "StartDate": "1401/06/20", - "EndDate": "1402/01/10" - }, - { - "ID": 6, - "FirstName": "مریم", - "LastName": "صادقی", - "DateOfBirth": "1372/09/14", - "Sex": "female", - "Phone": "09361234567", - "Address": { - "ID": 6, - "PostalCode": "6541239870", - "Name": "home", - "Lat": 38.0799, - "Lon": 46.2919, - "CityID": 5, - "ProvinceID": 2 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 6, - "StartDate": "1402/05/22", - "EndDate": "" - }, - { - "ID": 7, - "FirstName": "رضا", - "LastName": "علیزاده", - "DateOfBirth": "1355/04/10", - "Sex": "male", - "Phone": "09111234567", - "Address": { - "ID": 7, - "PostalCode": "9871234560", - "Name": "home", - "Lat": 35.1833, - "Lon": 47.0000, - "CityID": 32, - "ProvinceID": 11 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 1, - "StartDate": "1402/06/01", - "EndDate": "" - }, - { - "ID": 8, - "FirstName": "سارا", - "LastName": "موسوی", - "DateOfBirth": "1380/12/25", - "Sex": "female", - "Phone": "09211234567", - "Address": { - "ID": 8, - "PostalCode": "1597534680", - "Name": "home", - "Lat": 33.9900, - "Lon": 50.8736, - "CityID": 51, - "ProvinceID": 17 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 9, - "StartDate": "1402/07/11", - "EndDate": "" - }, - { - "ID": 9, - "FirstName": "امیر", - "LastName": "جعفری", - "DateOfBirth": "1368/06/19", - "Sex": "male", - "Phone": "09151234567", - "Address": { - "ID": 9, - "PostalCode": "7531594680", - "Name": "home", - "Lat": 30.2839, - "Lon": 57.0834, - "CityID": 73, - "ProvinceID": 28 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 3, - "StartDate": "1402/08/03", - "EndDate": "" - }, - { - "ID": 10, - "FirstName": "نرگس", - "LastName": "تهرانی", - "DateOfBirth": "1363/03/07", - "Sex": "female", - "Phone": "09391234567", - "Address": { - "ID": 10, - "PostalCode": "3571594680", - "Name": "home", - "Lat": 36.5683, - "Lon": 53.0601, - "CityID": 19, - "ProvinceID": 6 - }, - "CaseStatus": "closed", - "ReferralSource": "hospital", - "AssignedStaffId": 8, - "StartDate": "1401/09/15", - "EndDate": "1402/03/20" - }, - { - "ID": 11, - "FirstName": "سجاد", - "LastName": "نجفی", - "DateOfBirth": "1371/01/01", - "Sex": "male", - "Phone": "09131234567", - "Address": { - "ID": 11, - "PostalCode": "4682057391", - "Name": "home", - "Lat": 31.3203, - "Lon": 48.6692, - "CityID": 62, - "ProvinceID": 24 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 2, - "StartDate": "1402/09/14", - "EndDate": "" - }, - { - "ID": 12, - "FirstName": "الهام", - "LastName": "اکبری", - "DateOfBirth": "1376/10/22", - "Sex": "female", - "Phone": "09331234567", - "Address": { - "ID": 12, - "PostalCode": "8207461935", - "Name": "home", - "Lat": 32.9034, - "Lon": 59.2139, - "CityID": 37, - "ProvinceID": 13 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 5, - "StartDate": "1402/10/05", - "EndDate": "" - }, - { - "ID": 13, - "FirstName": "محسن", - "LastName": "شریفی", - "DateOfBirth": "1357/05/30", - "Sex": "male", - "Phone": "09161234567", - "Address": { - "ID": 13, - "PostalCode": "6194820735", - "Name": "home", - "Lat": 29.6060, - "Lon": 52.5311, - "CityID": 91, - "ProvinceID": 30 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 7, - "StartDate": "1402/11/18", - "EndDate": "" - }, - { - "ID": 14, - "FirstName": "پریسا", - "LastName": "قاسمی", - "DateOfBirth": "1382/07/08", - "Sex": "female", - "Phone": "09341234567", - "Address": { - "ID": 14, - "PostalCode": "0573948261", - "Name": "home", - "Lat": 37.5527, - "Lon": 45.0760, - "CityID": 8, - "ProvinceID": 3 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 4, - "StartDate": "1402/12/01", - "EndDate": "" - }, - { - "ID": 15, - "FirstName": "کاوه", - "LastName": "حیدری", - "DateOfBirth": "1362/08/17", - "Sex": "male", - "Phone": "09171234567", - "Address": { - "ID": 15, - "PostalCode": "3948261057", - "Name": "home", - "Lat": 34.7960, - "Lon": 48.5146, - "CityID": 55, - "ProvinceID": 20 - }, - "CaseStatus": "closed", - "ReferralSource": "hospital", - "AssignedStaffId": 6, - "StartDate": "1401/11/05", - "EndDate": "1402/05/15" - }, - { - "ID": 16, - "FirstName": "شیرین", - "LastName": "یوسفی", - "DateOfBirth": "1373/04/11", - "Sex": "female", - "Phone": "09221234567", - "Address": { - "ID": 16, - "PostalCode": "2948163057", - "Name": "home", - "Lat": 35.6836, - "Lon": 51.4211, - "CityID": 23, - "ProvinceID": 9 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 1, - "StartDate": "1403/01/08", - "EndDate": "" - }, - { - "ID": 17, - "FirstName": "داریوش", - "LastName": "مرادی", - "DateOfBirth": "1356/11/24", - "Sex": "male", - "Phone": "09191234567", - "Address": { - "ID": 17, - "PostalCode": "7461392058", - "Name": "home", - "Lat": 36.8962, - "Lon": 54.4334, - "CityID": 78, - "ProvinceID": 25 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 9, - "StartDate": "1403/01/22", - "EndDate": "" - }, - { - "ID": 18, - "FirstName": "آزاده", - "LastName": "ابراهیمی", - "DateOfBirth": "1378/09/03", - "Sex": "female", - "Phone": "09381234567", - "Address": { - "ID": 18, - "PostalCode": "5839204716", - "Name": "home", - "Lat": 30.2672, - "Lon": 50.0079, - "CityID": 46, - "ProvinceID": 16 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 3, - "StartDate": "1403/02/10", - "EndDate": "" - }, - { - "ID": 19, - "FirstName": "بهزاد", - "LastName": "نوری", - "DateOfBirth": "1367/02/14", - "Sex": "male", - "Phone": "09101234567", - "Address": { - "ID": 19, - "PostalCode": "1234098765", - "Name": "home", - "Lat": 31.8974, - "Lon": 54.3678, - "CityID": 69, - "ProvinceID": 27 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 8, - "StartDate": "1403/02/28", - "EndDate": "" - }, - { - "ID": 20, - "FirstName": "ناهید", - "LastName": "صالحی", - "DateOfBirth": "1361/06/29", - "Sex": "female", - "Phone": "09371234567", - "Address": { - "ID": 20, - "PostalCode": "0987612345", - "Name": "home", - "Lat": 38.4192, - "Lon": 44.9671, - "CityID": 12, - "ProvinceID": 1 - }, - "CaseStatus": "closed", - "ReferralSource": "hospital", - "AssignedStaffId": 2, - "StartDate": "1401/07/01", - "EndDate": "1402/07/01" - }, - { - "ID": 21, - "FirstName": "سیامک", - "LastName": "غفاری", - "DateOfBirth": "1364/10/05", - "Sex": "male", - "Phone": "09121357900", - "Address": { - "ID": 21, - "PostalCode": "5678901234", - "Name": "home", - "Lat": 33.5731, - "Lon": 48.3956, - "CityID": 34, - "ProvinceID": 12 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 5, - "StartDate": "1403/03/05", - "EndDate": "" - }, - { - "ID": 22, - "FirstName": "لیلا", - "LastName": "منصوری", - "DateOfBirth": "1377/03/18", - "Sex": "female", - "Phone": "09351357900", - "Address": { - "ID": 22, - "PostalCode": "4321098765", - "Name": "home", - "Lat": 35.3392, - "Lon": 46.9964, - "CityID": 57, - "ProvinceID": 21 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 7, - "StartDate": "1403/03/20", - "EndDate": "" - }, - { - "ID": 23, - "FirstName": "وحید", - "LastName": "طاهری", - "DateOfBirth": "1359/12/10", - "Sex": "male", - "Phone": "09181357900", - "Address": { - "ID": 23, - "PostalCode": "8765432109", - "Name": "home", - "Lat": 29.9553, - "Lon": 55.4368, - "CityID": 83, - "ProvinceID": 29 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 4, - "StartDate": "1403/04/01", - "EndDate": "" - }, - { - "ID": 24, - "FirstName": "گلناز", - "LastName": "پارسا", - "DateOfBirth": "1384/05/07", - "Sex": "female", - "Phone": "09301357900", - "Address": { - "ID": 24, - "PostalCode": "2109876543", - "Name": "home", - "Lat": 36.6689, - "Lon": 48.5136, - "CityID": 26, - "ProvinceID": 10 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 6, - "StartDate": "1403/04/15", - "EndDate": "" - }, - { - "ID": 25, - "FirstName": "نیما", - "LastName": "باقری", - "DateOfBirth": "1369/08/22", - "Sex": "male", - "Phone": "09141357900", - "Address": { - "ID": 25, - "PostalCode": "6543210987", - "Name": "home", - "Lat": 31.2667, - "Lon": 56.9667, - "CityID": 100, - "ProvinceID": 32 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 1, - "StartDate": "1403/05/02", - "EndDate": "" - }, - { - "ID": 26, - "FirstName": "مینا", - "LastName": "سلیمانی", - "DateOfBirth": "1374/01/15", - "Sex": "female", - "Phone": "09361357900", - "Address": { - "ID": 26, - "PostalCode": "0987654321", - "Name": "home", - "Lat": 32.4667, - "Lon": 53.6667, - "CityID": 47, - "ProvinceID": 18 - }, - "CaseStatus": "closed", - "ReferralSource": "hospital", - "AssignedStaffId": 9, - "StartDate": "1401/05/10", - "EndDate": "1402/09/25" - }, - { - "ID": 27, - "FirstName": "فرهاد", - "LastName": "اسدی", - "DateOfBirth": "1356/07/20", - "Sex": "male", - "Phone": "09111357900", - "Address": { - "ID": 27, - "PostalCode": "3456789012", - "Name": "home", - "Lat": 37.9380, - "Lon": 58.3660, - "CityID": 71, - "ProvinceID": 26 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 3, - "StartDate": "1403/05/18", - "EndDate": "" - }, - { - "ID": 28, - "FirstName": "سپیده", - "LastName": "کمالی", - "DateOfBirth": "1381/11/11", - "Sex": "female", - "Phone": "09211357900", - "Address": { - "ID": 28, - "PostalCode": "7890123456", - "Name": "home", - "Lat": 35.7447, - "Lon": 50.0890, - "CityID": 16, - "ProvinceID": 7 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 8, - "StartDate": "1403/06/01", - "EndDate": "" - }, - { - "ID": 29, - "FirstName": "شاهین", - "LastName": "زارعی", - "DateOfBirth": "1366/04/27", - "Sex": "male", - "Phone": "09151357900", - "Address": { - "ID": 29, - "PostalCode": "2345678901", - "Name": "home", - "Lat": 33.0953, - "Lon": 51.5051, - "CityID": 59, - "ProvinceID": 23 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 2, - "StartDate": "1403/06/15", - "EndDate": "" - }, - { - "ID": 30, - "FirstName": "رویا", - "LastName": "حسنی", - "DateOfBirth": "1379/09/03", - "Sex": "female", - "Phone": "09391357900", - "Address": { - "ID": 30, - "PostalCode": "6789012345", - "Name": "home", - "Lat": 30.8161, - "Lon": 57.0985, - "CityID": 85, - "ProvinceID": 31 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 5, - "StartDate": "1403/07/01", - "EndDate": "" - }, - { - "ID": 31, - "FirstName": "مهدی", - "LastName": "عباسی", - "DateOfBirth": "1361/02/16", - "Sex": "male", - "Phone": "09131357900", - "Address": { - "ID": 31, - "PostalCode": "1230984567", - "Name": "home", - "Lat": 36.2605, - "Lon": 50.0041, - "CityID": 31, - "ProvinceID": 14 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 7, - "StartDate": "1403/07/20", - "EndDate": "" - }, - { - "ID": 32, - "FirstName": "بهاره", - "LastName": "رستمی", - "DateOfBirth": "1375/06/08", - "Sex": "female", - "Phone": "09331357900", - "Address": { - "ID": 32, - "PostalCode": "9870123456", - "Name": "home", - "Lat": 34.9081, - "Lon": 50.8756, - "CityID": 53, - "ProvinceID": 17 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 4, - "StartDate": "1403/08/05", - "EndDate": "" - }, - { - "ID": 33, - "FirstName": "علیرضا", - "LastName": "شاهی", - "DateOfBirth": "1353/10/25", - "Sex": "male", - "Phone": "09161357900", - "Address": { - "ID": 33, - "PostalCode": "4560123789", - "Name": "home", - "Lat": 31.8955, - "Lon": 54.3701, - "CityID": 77, - "ProvinceID": 27 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 6, - "StartDate": "1403/08/18", - "EndDate": "" - }, - { - "ID": 34, - "FirstName": "نسرین", - "LastName": "جلیلی", - "DateOfBirth": "1383/03/12", - "Sex": "female", - "Phone": "09341357900", - "Address": { - "ID": 34, - "PostalCode": "7893216540", - "Name": "home", - "Lat": 37.4713, - "Lon": 49.7166, - "CityID": 22, - "ProvinceID": 9 - }, - "CaseStatus": "closed", - "ReferralSource": "hospital", - "AssignedStaffId": 1, - "StartDate": "1401/03/07", - "EndDate": "1402/11/20" - }, - { - "ID": 35, - "FirstName": "آرش", - "LastName": "فرهادی", - "DateOfBirth": "1368/07/21", - "Sex": "male", - "Phone": "09171357900", - "Address": { - "ID": 35, - "PostalCode": "3210987654", - "Name": "home", - "Lat": 28.9234, - "Lon": 50.8203, - "CityID": 94, - "ProvinceID": 32 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 9, - "StartDate": "1403/09/01", - "EndDate": "" - }, - { - "ID": 36, - "FirstName": "مژده", - "LastName": "کوهی", - "DateOfBirth": "1377/12/14", - "Sex": "female", - "Phone": "09221357900", - "Address": { - "ID": 36, - "PostalCode": "6547893210", - "Name": "home", - "Lat": 35.3219, - "Lon": 46.9979, - "CityID": 41, - "ProvinceID": 15 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 3, - "StartDate": "1403/09/15", - "EndDate": "" - }, - { - "ID": 37, - "FirstName": "حمید", - "LastName": "شکری", - "DateOfBirth": "1360/05/03", - "Sex": "male", - "Phone": "09191357900", - "Address": { - "ID": 37, - "PostalCode": "9213456780", - "Name": "home", - "Lat": 34.0840, - "Lon": 49.6967, - "CityID": 64, - "ProvinceID": 22 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 8, - "StartDate": "1403/10/02", - "EndDate": "" - }, - { - "ID": 38, - "FirstName": "ندا", - "LastName": "پورمحمد", - "DateOfBirth": "1385/08/28", - "Sex": "female", - "Phone": "09381357900", - "Address": { - "ID": 38, - "PostalCode": "0132456789", - "Name": "home", - "Lat": 31.5497, - "Lon": 48.7547, - "CityID": 88, - "ProvinceID": 24 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 2, - "StartDate": "1403/10/18", - "EndDate": "" - }, - { - "ID": 39, - "FirstName": "صادق", - "LastName": "محسنی", - "DateOfBirth": "1354/01/30", - "Sex": "male", - "Phone": "09101357900", - "Address": { - "ID": 39, - "PostalCode": "5678123490", - "Name": "home", - "Lat": 30.4271, - "Lon": 48.2121, - "CityID": 97, - "ProvinceID": 30 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 5, - "StartDate": "1403/11/05", - "EndDate": "" - }, - { - "ID": 40, - "FirstName": "فروغ", - "LastName": "ملکی", - "DateOfBirth": "1370/04/17", - "Sex": "female", - "Phone": "09371357900", - "Address": { - "ID": 40, - "PostalCode": "3214567890", - "Name": "home", - "Lat": 37.0539, - "Lon": 54.8340, - "CityID": 7, - "ProvinceID": 3 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 7, - "StartDate": "1403/11/20", - "EndDate": "" - }, - { - "ID": 41, - "FirstName": "پدرام", - "LastName": "ولی", - "DateOfBirth": "1365/11/09", - "Sex": "male", - "Phone": "09129876543", - "Address": { - "ID": 41, - "PostalCode": "8901234567", - "Name": "home", - "Lat": 36.5544, - "Lon": 52.6611, - "CityID": 28, - "ProvinceID": 6 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 4, - "StartDate": "1403/12/01", - "EndDate": "" - }, - { - "ID": 42, - "FirstName": "زینب", - "LastName": "احمدپور", - "DateOfBirth": "1380/02/20", - "Sex": "female", - "Phone": "09359876543", - "Address": { - "ID": 42, - "PostalCode": "1234509876", - "Name": "home", - "Lat": 35.6777, - "Lon": 51.3888, - "CityID": 14, - "ProvinceID": 8 - }, - "CaseStatus": "closed", - "ReferralSource": "hospital", - "AssignedStaffId": 6, - "StartDate": "1401/08/12", - "EndDate": "1402/06/30" - }, - { - "ID": 43, - "FirstName": "کامران", - "LastName": "درویشی", - "DateOfBirth": "1358/09/14", - "Sex": "male", - "Phone": "09189876543", - "Address": { - "ID": 43, - "PostalCode": "6789012340", - "Name": "home", - "Lat": 32.6719, - "Lon": 51.6618, - "CityID": 43, - "ProvinceID": 4 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 1, - "StartDate": "1404/01/10", - "EndDate": "" - }, - { - "ID": 44, - "FirstName": "آرزو", - "LastName": "حاتمی", - "DateOfBirth": "1376/07/05", - "Sex": "female", - "Phone": "09309876543", - "Address": { - "ID": 44, - "PostalCode": "2345016789", - "Name": "home", - "Lat": 34.3277, - "Lon": 47.0785, - "CityID": 75, - "ProvinceID": 19 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 9, - "StartDate": "1404/01/25", - "EndDate": "" - }, - { - "ID": 45, - "FirstName": "بابک", - "LastName": "معینی", - "DateOfBirth": "1363/04/19", - "Sex": "male", - "Phone": "09149876543", - "Address": { - "ID": 45, - "PostalCode": "9078563412", - "Name": "home", - "Lat": 38.0792, - "Lon": 46.2895, - "CityID": 5, - "ProvinceID": 2 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 3, - "StartDate": "1404/02/08", - "EndDate": "" - }, - { - "ID": 46, - "FirstName": "مهناز", - "LastName": "قربانی", - "DateOfBirth": "1372/12/01", - "Sex": "female", - "Phone": "09369876543", - "Address": { - "ID": 46, - "PostalCode": "4563218790", - "Name": "home", - "Lat": 37.2710, - "Lon": 49.5887, - "CityID": 67, - "ProvinceID": 22 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 8, - "StartDate": "1404/02/22", - "EndDate": "" - }, - { - "ID": 47, - "FirstName": "ایمان", - "LastName": "رحمانی", - "DateOfBirth": "1367/06/24", - "Sex": "male", - "Phone": "09119876543", - "Address": { - "ID": 47, - "PostalCode": "1087923456", - "Name": "home", - "Lat": 36.2972, - "Lon": 59.6067, - "CityID": 35, - "ProvinceID": 13 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 2, - "StartDate": "1404/03/05", - "EndDate": "" - }, - { - "ID": 48, - "FirstName": "شادی", - "LastName": "امیری", - "DateOfBirth": "1381/03/17", - "Sex": "female", - "Phone": "09219876543", - "Address": { - "ID": 48, - "PostalCode": "6541237890", - "Name": "home", - "Lat": 33.9900, - "Lon": 50.8736, - "CityID": 53, - "ProvinceID": 17 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 5, - "StartDate": "1404/03/20", - "EndDate": "" - }, - { - "ID": 49, - "FirstName": "بهروز", - "LastName": "سبزواری", - "DateOfBirth": "1355/10/08", - "Sex": "male", - "Phone": "09159876543", - "Address": { - "ID": 49, - "PostalCode": "7893045612", - "Name": "home", - "Lat": 29.6060, - "Lon": 52.5311, - "CityID": 91, - "ProvinceID": 30 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 7, - "StartDate": "1404/04/01", - "EndDate": "" - }, - { - "ID": 50, - "FirstName": "ترانه", - "LastName": "محمودی", - "DateOfBirth": "1384/08/11", - "Sex": "female", - "Phone": "09399876543", - "Address": { - "ID": 50, - "PostalCode": "2340897615", - "Name": "home", - "Lat": 31.3203, - "Lon": 48.6692, - "CityID": 62, - "ProvinceID": 24 - }, - "CaseStatus": "closed", - "ReferralSource": "hospital", - "AssignedStaffId": 4, - "StartDate": "1401/10/05", - "EndDate": "1402/08/15" - }, - { - "ID": 51, - "FirstName": "فریدون", - "LastName": "نعمتی", - "DateOfBirth": "1357/01/27", - "Sex": "male", - "Phone": "09139876543", - "Address": { - "ID": 51, - "PostalCode": "5671289034", - "Name": "home", - "Lat": 30.2839, - "Lon": 57.0834, - "CityID": 73, - "ProvinceID": 28 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 6, - "StartDate": "1404/04/18", - "EndDate": "" - }, - { - "ID": 52, - "FirstName": "مهسا", - "LastName": "خلیلی", - "DateOfBirth": "1378/05/23", - "Sex": "female", - "Phone": "09339876543", - "Address": { - "ID": 52, - "PostalCode": "9034567812", - "Name": "home", - "Lat": 37.5527, - "Lon": 45.0760, - "CityID": 8, - "ProvinceID": 3 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 1, - "StartDate": "1404/05/02", - "EndDate": "" - }, - { - "ID": 53, - "FirstName": "جلال", - "LastName": "حبیبی", - "DateOfBirth": "1362/11/15", - "Sex": "male", - "Phone": "09169876543", - "Address": { - "ID": 53, - "PostalCode": "1289034567", - "Name": "home", - "Lat": 34.7960, - "Lon": 48.5146, - "CityID": 55, - "ProvinceID": 20 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 9, - "StartDate": "1404/05/18", - "EndDate": "" - }, - { - "ID": 54, - "FirstName": "صبا", - "LastName": "خوش‌نژاد", - "DateOfBirth": "1387/02/07", - "Sex": "female", - "Phone": "09349876543", - "Address": { - "ID": 54, - "PostalCode": "3450678912", - "Name": "home", - "Lat": 35.6836, - "Lon": 51.4211, - "CityID": 14, - "ProvinceID": 8 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 3, - "StartDate": "1404/06/01", - "EndDate": "" - }, - { - "ID": 55, - "FirstName": "هادی", - "LastName": "خدابخش", - "DateOfBirth": "1366/09/29", - "Sex": "male", - "Phone": "09179876543", - "Address": { - "ID": 55, - "PostalCode": "7812034560", - "Name": "home", - "Lat": 36.8962, - "Lon": 54.4334, - "CityID": 78, - "ProvinceID": 25 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 8, - "StartDate": "1404/06/15", - "EndDate": "" - }, - { - "ID": 56, - "FirstName": "افسانه", - "LastName": "دهقانی", - "DateOfBirth": "1373/07/12", - "Sex": "female", - "Phone": "09229876543", - "Address": { - "ID": 56, - "PostalCode": "6780123459", - "Name": "home", - "Lat": 30.2672, - "Lon": 50.0079, - "CityID": 46, - "ProvinceID": 16 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 2, - "StartDate": "1404/07/01", - "EndDate": "" - }, - { - "ID": 57, - "FirstName": "سروش", - "LastName": "پاک‌نهاد", - "DateOfBirth": "1360/03/04", - "Sex": "male", - "Phone": "09199876543", - "Address": { - "ID": 57, - "PostalCode": "4509123678", - "Name": "home", - "Lat": 31.8974, - "Lon": 54.3678, - "CityID": 69, - "ProvinceID": 27 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 5, - "StartDate": "1404/07/18", - "EndDate": "" - }, - { - "ID": 58, - "FirstName": "فرناز", - "LastName": "بهرامی", - "DateOfBirth": "1382/10/20", - "Sex": "female", - "Phone": "09389876543", - "Address": { - "ID": 58, - "PostalCode": "2356789014", - "Name": "home", - "Lat": 38.4192, - "Lon": 44.9671, - "CityID": 12, - "ProvinceID": 1 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 7, - "StartDate": "1404/08/05", - "EndDate": "" - }, - { - "ID": 59, - "FirstName": "ابراهیم", - "LastName": "ستاری", - "DateOfBirth": "1354/07/16", - "Sex": "male", - "Phone": "09109876543", - "Address": { - "ID": 59, - "PostalCode": "7890234561", - "Name": "home", - "Lat": 33.5731, - "Lon": 48.3956, - "CityID": 34, - "ProvinceID": 12 - }, - "CaseStatus": "closed", - "ReferralSource": "hospital", - "AssignedStaffId": 4, - "StartDate": "1401/04/01", - "EndDate": "1402/10/10" - }, - { - "ID": 60, - "FirstName": "گیتا", - "LastName": "نادری", - "DateOfBirth": "1376/04/28", - "Sex": "female", - "Phone": "09379876543", - "Address": { - "ID": 60, - "PostalCode": "1345678092", - "Name": "home", - "Lat": 35.3392, - "Lon": 46.9964, - "CityID": 57, - "ProvinceID": 21 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 6, - "StartDate": "1404/08/20", - "EndDate": "" - }, - { - "ID": 61, - "FirstName": "مصطفی", - "LastName": "میرزایی", - "DateOfBirth": "1363/01/11", - "Sex": "male", - "Phone": "09125551234", - "Address": { - "ID": 61, - "PostalCode": "9034512678", - "Name": "home", - "Lat": 29.9553, - "Lon": 55.4368, - "CityID": 83, - "ProvinceID": 29 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 1, - "StartDate": "1404/09/01", - "EndDate": "" - }, - { - "ID": 62, - "FirstName": "هنگامه", - "LastName": "کاظمی", - "DateOfBirth": "1379/11/08", - "Sex": "female", - "Phone": "09355551234", - "Address": { - "ID": 62, - "PostalCode": "6701234589", - "Name": "home", - "Lat": 36.6689, - "Lon": 48.5136, - "CityID": 26, - "ProvinceID": 10 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 9, - "StartDate": "1404/09/15", - "EndDate": "" - }, - { - "ID": 63, - "FirstName": "فواد", - "LastName": "شریعتی", - "DateOfBirth": "1359/06/22", - "Sex": "male", - "Phone": "09185551234", - "Address": { - "ID": 63, - "PostalCode": "3456012789", - "Name": "home", - "Lat": 31.2667, - "Lon": 56.9667, - "CityID": 100, - "ProvinceID": 32 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 3, - "StartDate": "1404/10/02", - "EndDate": "" - }, - { - "ID": 64, - "FirstName": "سوسن", - "LastName": "دوستی", - "DateOfBirth": "1374/09/14", - "Sex": "female", - "Phone": "09305551234", - "Address": { - "ID": 64, - "PostalCode": "8912034567", - "Name": "home", - "Lat": 32.4667, - "Lon": 53.6667, - "CityID": 47, - "ProvinceID": 18 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 8, - "StartDate": "1404/10/18", - "EndDate": "" - }, - { - "ID": 65, - "FirstName": "تورج", - "LastName": "ایمانی", - "DateOfBirth": "1356/04/30", - "Sex": "male", - "Phone": "09145551234", - "Address": { - "ID": 65, - "PostalCode": "2134567890", - "Name": "home", - "Lat": 37.9380, - "Lon": 58.3660, - "CityID": 71, - "ProvinceID": 26 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 2, - "StartDate": "1404/11/05", - "EndDate": "" - }, - { - "ID": 66, - "FirstName": "شبنم", - "LastName": "احمدزاده", - "DateOfBirth": "1385/01/17", - "Sex": "female", - "Phone": "09365551234", - "Address": { - "ID": 66, - "PostalCode": "5678094321", - "Name": "home", - "Lat": 35.7447, - "Lon": 50.0890, - "CityID": 16, - "ProvinceID": 7 - }, - "CaseStatus": "closed", - "ReferralSource": "hospital", - "AssignedStaffId": 5, - "StartDate": "1401/02/15", - "EndDate": "1402/04/05" - }, - { - "ID": 67, - "FirstName": "عباس", - "LastName": "طباطبایی", - "DateOfBirth": "1358/08/09", - "Sex": "male", - "Phone": "09115551234", - "Address": { - "ID": 67, - "PostalCode": "0912347856", - "Name": "home", - "Lat": 33.0953, - "Lon": 51.5051, - "CityID": 59, - "ProvinceID": 23 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 7, - "StartDate": "1404/11/20", - "EndDate": "" - }, - { - "ID": 68, - "FirstName": "مهتاب", - "LastName": "اکبرزاده", - "DateOfBirth": "1383/06/26", - "Sex": "female", - "Phone": "09215551234", - "Address": { - "ID": 68, - "PostalCode": "7456238901", - "Name": "home", - "Lat": 30.8161, - "Lon": 57.0985, - "CityID": 85, - "ProvinceID": 31 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 4, - "StartDate": "1404/12/01", - "EndDate": "" - }, - { - "ID": 69, - "FirstName": "قاسم", - "LastName": "ذاکری", - "DateOfBirth": "1362/02/15", - "Sex": "male", - "Phone": "09155551234", - "Address": { - "ID": 69, - "PostalCode": "3890127456", - "Name": "home", - "Lat": 36.2605, - "Lon": 50.0041, - "CityID": 31, - "ProvinceID": 14 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 6, - "StartDate": "1404/12/15", - "EndDate": "" - }, - { - "ID": 70, - "FirstName": "نیلوفر", - "LastName": "وطن‌دوست", - "DateOfBirth": "1375/10/03", - "Sex": "female", - "Phone": "09395551234", - "Address": { - "ID": 70, - "PostalCode": "1267890345", - "Name": "home", - "Lat": 34.9081, - "Lon": 50.8756, - "CityID": 53, - "ProvinceID": 17 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 1, - "StartDate": "1405/01/05", - "EndDate": "" - }, - { - "ID": 71, - "FirstName": "سهراب", - "LastName": "قلی‌زاده", - "DateOfBirth": "1361/11/21", - "Sex": "male", - "Phone": "09135551234", - "Address": { - "ID": 71, - "PostalCode": "4512367890", - "Name": "home", - "Lat": 31.8955, - "Lon": 54.3701, - "CityID": 77, - "ProvinceID": 27 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 9, - "StartDate": "1405/01/10", - "EndDate": "" - }, - { - "ID": 72, - "FirstName": "آناهیتا", - "LastName": "رضوانی", - "DateOfBirth": "1386/07/14", - "Sex": "female", - "Phone": "09335551234", - "Address": { - "ID": 72, - "PostalCode": "8034512679", - "Name": "home", - "Lat": 37.4713, - "Lon": 49.7166, - "CityID": 22, - "ProvinceID": 9 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 3, - "StartDate": "1405/01/12", - "EndDate": "" - }, - { - "ID": 73, - "FirstName": "منوچهر", - "LastName": "صمدی", - "DateOfBirth": "1353/05/18", - "Sex": "male", - "Phone": "09165551234", - "Address": { - "ID": 73, - "PostalCode": "6789345012", - "Name": "home", - "Lat": 28.9234, - "Lon": 50.8203, - "CityID": 94, - "ProvinceID": 32 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 8, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 74, - "FirstName": "فرحناز", - "LastName": "ضیایی", - "DateOfBirth": "1377/01/25", - "Sex": "female", - "Phone": "09345551234", - "Address": { - "ID": 74, - "PostalCode": "2301456789", - "Name": "home", - "Lat": 35.3219, - "Lon": 46.9979, - "CityID": 41, - "ProvinceID": 15 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 2, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 75, - "FirstName": "کیوان", - "LastName": "پیری", - "DateOfBirth": "1364/09/07", - "Sex": "male", - "Phone": "09175551234", - "Address": { - "ID": 75, - "PostalCode": "5678901432", - "Name": "home", - "Lat": 34.0840, - "Lon": 49.6967, - "CityID": 64, - "ProvinceID": 22 - }, - "CaseStatus": "closed", - "ReferralSource": "hospital", - "AssignedStaffId": 5, - "StartDate": "1401/12/01", - "EndDate": "1402/12/15" - }, - { - "ID": 76, - "FirstName": "سمیه", - "LastName": "مختاری", - "DateOfBirth": "1371/04/11", - "Sex": "female", - "Phone": "09225551234", - "Address": { - "ID": 76, - "PostalCode": "9012345678", - "Name": "home", - "Lat": 31.5497, - "Lon": 48.7547, - "CityID": 88, - "ProvinceID": 24 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 7, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 77, - "FirstName": "رامین", - "LastName": "بیگدلی", - "DateOfBirth": "1360/12/28", - "Sex": "male", - "Phone": "09195551234", - "Address": { - "ID": 77, - "PostalCode": "3456789102", - "Name": "home", - "Lat": 30.4271, - "Lon": 48.2121, - "CityID": 97, - "ProvinceID": 30 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 4, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 78, - "FirstName": "ثریا", - "LastName": "عارف", - "DateOfBirth": "1380/08/17", - "Sex": "female", - "Phone": "09385551234", - "Address": { - "ID": 78, - "PostalCode": "7891234056", - "Name": "home", - "Lat": 37.0539, - "Lon": 54.8340, - "CityID": 7, - "ProvinceID": 3 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 6, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 79, - "FirstName": "ژاله", - "LastName": "حکیمی", - "DateOfBirth": "1368/03/22", - "Sex": "female", - "Phone": "09105551234", - "Address": { - "ID": 79, - "PostalCode": "2345601789", - "Name": "home", - "Lat": 36.5544, - "Lon": 52.6611, - "CityID": 28, - "ProvinceID": 6 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 1, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 80, - "FirstName": "ارسلان", - "LastName": "فلاح", - "DateOfBirth": "1355/06/14", - "Sex": "male", - "Phone": "09375551234", - "Address": { - "ID": 80, - "PostalCode": "6012345789", - "Name": "home", - "Lat": 35.6777, - "Lon": 51.3888, - "CityID": 14, - "ProvinceID": 8 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 9, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 81, - "FirstName": "پونه", - "LastName": "طلایی", - "DateOfBirth": "1382/12/05", - "Sex": "female", - "Phone": "09127778901", - "Address": { - "ID": 81, - "PostalCode": "4567890213", - "Name": "home", - "Lat": 32.6719, - "Lon": 51.6618, - "CityID": 43, - "ProvinceID": 4 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 3, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 82, - "FirstName": "کریم", - "LastName": "معظمی", - "DateOfBirth": "1357/09/01", - "Sex": "male", - "Phone": "09357778901", - "Address": { - "ID": 82, - "PostalCode": "1908234567", - "Name": "home", - "Lat": 34.3277, - "Lon": 47.0785, - "CityID": 88, - "ProvinceID": 19 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 8, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 83, - "FirstName": "فریبا", - "LastName": "خاکپور", - "DateOfBirth": "1374/06/20", - "Sex": "female", - "Phone": "09187778901", - "Address": { - "ID": 83, - "PostalCode": "8745123609", - "Name": "home", - "Lat": 38.0799, - "Lon": 46.2919, - "CityID": 5, - "ProvinceID": 2 - }, - "CaseStatus": "closed", - "ReferralSource": "hospital", - "AssignedStaffId": 2, - "StartDate": "1401/01/10", - "EndDate": "1403/01/10" - }, - { - "ID": 84, - "FirstName": "غلامرضا", - "LastName": "توکلی", - "DateOfBirth": "1352/03/15", - "Sex": "male", - "Phone": "09307778901", - "Address": { - "ID": 84, - "PostalCode": "0123456897", - "Name": "home", - "Lat": 37.2808, - "Lon": 49.5832, - "CityID": 67, - "ProvinceID": 22 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 5, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 85, - "FirstName": "آرمین", - "LastName": "حق‌شناس", - "DateOfBirth": "1369/12/10", - "Sex": "male", - "Phone": "09147778901", - "Address": { - "ID": 85, - "PostalCode": "5601234789", - "Name": "home", - "Lat": 35.1833, - "Lon": 47.0000, - "CityID": 32, - "ProvinceID": 11 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 7, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 86, - "FirstName": "تهمینه", - "LastName": "فروزنده", - "DateOfBirth": "1378/10/27", - "Sex": "female", - "Phone": "09367778901", - "Address": { - "ID": 86, - "PostalCode": "3789012456", - "Name": "home", - "Lat": 33.9900, - "Lon": 50.8736, - "CityID": 51, - "ProvinceID": 17 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 4, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 87, - "FirstName": "نادر", - "LastName": "مشهدی", - "DateOfBirth": "1361/08/18", - "Sex": "male", - "Phone": "09117778901", - "Address": { - "ID": 87, - "PostalCode": "9123450678", - "Name": "home", - "Lat": 30.2839, - "Lon": 57.0834, - "CityID": 73, - "ProvinceID": 28 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 6, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 88, - "FirstName": "ماندانا", - "LastName": "صبوری", - "DateOfBirth": "1376/05/05", - "Sex": "female", - "Phone": "09217778901", - "Address": { - "ID": 88, - "PostalCode": "6034512789", - "Name": "home", - "Lat": 36.5683, - "Lon": 53.0601, - "CityID": 19, - "ProvinceID": 6 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 1, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 89, - "FirstName": "بیژن", - "LastName": "مهدوی", - "DateOfBirth": "1363/11/23", - "Sex": "male", - "Phone": "09157778901", - "Address": { - "ID": 89, - "PostalCode": "1234896057", - "Name": "home", - "Lat": 31.3203, - "Lon": 48.6692, - "CityID": 62, - "ProvinceID": 24 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 9, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 90, - "FirstName": "حمیده", - "LastName": "میری", - "DateOfBirth": "1381/07/10", - "Sex": "female", - "Phone": "09397778901", - "Address": { - "ID": 90, - "PostalCode": "7893456012", - "Name": "home", - "Lat": 29.6060, - "Lon": 52.5311, - "CityID": 91, - "ProvinceID": 30 - }, - "CaseStatus": "closed", - "ReferralSource": "hospital", - "AssignedStaffId": 3, - "StartDate": "1401/06/01", - "EndDate": "1403/06/01" - }, - { - "ID": 91, - "FirstName": "اردشیر", - "LastName": "نوروزی", - "DateOfBirth": "1358/04/06", - "Sex": "male", - "Phone": "09137778901", - "Address": { - "ID": 91, - "PostalCode": "0456789123", - "Name": "home", - "Lat": 37.5527, - "Lon": 45.0760, - "CityID": 8, - "ProvinceID": 3 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 8, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 92, - "FirstName": "حمیرا", - "LastName": "تقوی", - "DateOfBirth": "1373/02/18", - "Sex": "female", - "Phone": "09337778901", - "Address": { - "ID": 92, - "PostalCode": "5012345679", - "Name": "home", - "Lat": 34.7960, - "Lon": 48.5146, - "CityID": 55, - "ProvinceID": 20 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 2, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 93, - "FirstName": "فریدون", - "LastName": "آهنگر", - "DateOfBirth": "1355/08/30", - "Sex": "male", - "Phone": "09167778901", - "Address": { - "ID": 93, - "PostalCode": "2890134567", - "Name": "home", - "Lat": 35.6836, - "Lon": 51.4211, - "CityID": 23, - "ProvinceID": 9 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 5, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 94, - "FirstName": "رزا", - "LastName": "یاری", - "DateOfBirth": "1384/11/15", - "Sex": "female", - "Phone": "09347778901", - "Address": { - "ID": 94, - "PostalCode": "7456890123", - "Name": "home", - "Lat": 36.8962, - "Lon": 54.4334, - "CityID": 78, - "ProvinceID": 25 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 7, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 95, - "FirstName": "میثم", - "LastName": "کرمی", - "DateOfBirth": "1367/01/07", - "Sex": "male", - "Phone": "09177778901", - "Address": { - "ID": 95, - "PostalCode": "3123456789", - "Name": "home", - "Lat": 30.2672, - "Lon": 50.0079, - "CityID": 46, - "ProvinceID": 16 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 4, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 96, - "FirstName": "سلما", - "LastName": "ایزدی", - "DateOfBirth": "1379/04/24", - "Sex": "female", - "Phone": "09227778901", - "Address": { - "ID": 96, - "PostalCode": "8901234765", - "Name": "home", - "Lat": 31.8974, - "Lon": 54.3678, - "CityID": 69, - "ProvinceID": 27 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 6, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 97, - "FirstName": "شهریار", - "LastName": "بختیاری", - "DateOfBirth": "1357/07/11", - "Sex": "male", - "Phone": "09197778901", - "Address": { - "ID": 97, - "PostalCode": "4567012398", - "Name": "home", - "Lat": 38.4192, - "Lon": 44.9671, - "CityID": 12, - "ProvinceID": 1 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 1, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 98, - "FirstName": "اعظم", - "LastName": "شمس", - "DateOfBirth": "1372/10/28", - "Sex": "female", - "Phone": "09387778901", - "Address": { - "ID": 98, - "PostalCode": "1234567809", - "Name": "home", - "Lat": 33.5731, - "Lon": 48.3956, - "CityID": 34, - "ProvinceID": 12 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 9, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 99, - "FirstName": "احمد", - "LastName": "فیروزی", - "DateOfBirth": "1362/06/19", - "Sex": "male", - "Phone": "09107778901", - "Address": { - "ID": 99, - "PostalCode": "6789123450", - "Name": "home", - "Lat": 35.3392, - "Lon": 46.9964, - "CityID": 57, - "ProvinceID": 21 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 3, - "StartDate": "1405/01/13", - "EndDate": "" - }, - { - "ID": 100, - "FirstName": "ملیکا", - "LastName": "صفوی", - "DateOfBirth": "1386/03/08", - "Sex": "female", - "Phone": "09377778901", - "Address": { - "ID": 100, - "PostalCode": "2345678190", - "Name": "home", - "Lat": 29.9553, - "Lon": 55.4368, - "CityID": 83, - "ProvinceID": 29 - }, - "CaseStatus": "open", - "ReferralSource": "hospital", - "AssignedStaffId": 8, - "StartDate": "1405/01/13", - "EndDate": "" - } -] diff --git a/patientapp/service/analytic/param.go b/patientapp/service/analytic/param.go index bd7d4400..da14af37 100644 --- a/patientapp/service/analytic/param.go +++ b/patientapp/service/analytic/param.go @@ -15,8 +15,7 @@ type ListPatientAnalyticRequest struct { Search *string `query:"search,omitempty"` - Limit int `query:"limit,omitempty"` - Offset int `query:"offset,omitempty"` + Pagination *Pagination `query:"pagination,omitempty"` } type PatientAnalyticItem struct { @@ -29,10 +28,9 @@ type PatientAnalyticItem struct { Address entity.Address `json:"address"` } type PatientAnalyticResponse struct { - Items []PatientAnalyticItem `json:"items"` - Limit int `json:"limit"` - Offset int `json:"offset"` - Total int `json:"total"` + Items []PatientAnalyticItem `json:"items"` + Pagination *Pagination `json:"pagination"` + Total int `json:"total"` } func ToPatientResponse(patient entity.Patient) PatientAnalyticItem { @@ -50,8 +48,7 @@ func ToPatientResponse(patient entity.Patient) PatientAnalyticItem { } } -// =========================== Map ================================== - +// GetPatientMapSummaryRequest =========================== Map ================================== type GetPatientMapSummaryRequest struct { Level entity.MapLevel `query:"level"` ParentID *int `query:"parentID"` @@ -66,3 +63,9 @@ type GetPatientMapSummaryResponse struct { Level entity.MapLevel `json:"level"` Items map[uint][]entity.MapSummaryItem `json:"items"` } + +// Pagination ================================ Pagination ============================= +type Pagination struct { + Limit int `query:"limit,omitempty"` + Offset int `query:"offset,omitempty"` +} diff --git a/patientapp/service/analytic/service.go b/patientapp/service/analytic/service.go index 683f9a74..518d8756 100644 --- a/patientapp/service/analytic/service.go +++ b/patientapp/service/analytic/service.go @@ -19,25 +19,23 @@ type Repository interface { GetPatients(ctx context.Context, f PatientFilter) ([]entity.Patient, error) CountPatients(ctx context.Context, f PatientFilter) (int, error) - SummaryByCity(ctx context.Context, provinceID int, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) + SummaryByCity(ctx context.Context, provinceID uint, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) SummaryByProvince(ctx context.Context, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) } type Service struct { repository Repository - validator Validator } -func NewPatientAnalyticService(repo Repository, validator Validator) Service { +func NewPatientAnalyticService(repo Repository) Service { return Service{ repository: repo, - validator: validator, } } func (s Service) List(ctx context.Context, req ListPatientAnalyticRequest) (PatientAnalyticResponse, error) { - limit, offset := normalizeLimitOffset(req.Limit, req.Offset) + limit, offset := normalizeLimitOffset(req.Pagination.Limit, req.Pagination.Offset) // convert age range dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now()) @@ -70,10 +68,12 @@ func (s Service) List(ctx context.Context, req ListPatientAnalyticRequest) (Pati } return PatientAnalyticResponse{ - Items: out, - Limit: limit, - Offset: offset, - Total: total, + Items: out, + Pagination: &Pagination{ + Limit: limit, + Offset: offset, + }, + Total: total, }, nil } @@ -95,7 +95,7 @@ func (s Service) GetMapSummary(ctx context.Context, req GetPatientMapSummaryRequ return GetPatientMapSummaryResponse{}, ErrInvalidProvinceID } - items, err := s.repository.SummaryByCity(ctx, *req.ParentID, filter) + items, err := s.repository.SummaryByCity(ctx, uint(*req.ParentID), filter) if err != nil { return GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByCity: %w", err) } diff --git a/patientapp/service/analytic/validator.go b/patientapp/service/analytic/validator.go deleted file mode 100644 index 7e65ad93..00000000 --- a/patientapp/service/analytic/validator.go +++ /dev/null @@ -1,8 +0,0 @@ -package analytic - -type Validator struct { -} - -func NewValidator() *Validator { - return &Validator{} -} From 01909b366ad0113971249bccbe6c874f99ccea1c Mon Sep 17 00:00:00 2001 From: Mohammad Amin Date: Tue, 7 Apr 2026 17:19:14 +0330 Subject: [PATCH 5/5] add grpc repo --- patientapp/delivery/http/analytic/router.go | 5 +-- patientapp/repository/grpc/analytic_repo.go | 35 ++++++++++++++++++++ patientapp/repository/mysql/analytic_repo.go | 24 ++++---------- 3 files changed, 45 insertions(+), 19 deletions(-) create mode 100644 patientapp/repository/grpc/analytic_repo.go diff --git a/patientapp/delivery/http/analytic/router.go b/patientapp/delivery/http/analytic/router.go index 1ef8c004..c6499428 100644 --- a/patientapp/delivery/http/analytic/router.go +++ b/patientapp/delivery/http/analytic/router.go @@ -8,9 +8,10 @@ import ( func NewPatientAnalyticRouter(s *echo.Group) { - repo := mysql.NewPatientRepo() + mysqlRepo := mysql.NewPatientRepo() + //rpcRepo := grpc.NewPatientRepo() - analyticService := analytic2.NewPatientAnalyticService(repo) + analyticService := analytic2.NewPatientAnalyticService(mysqlRepo) h := NewHandler(analyticService) diff --git a/patientapp/repository/grpc/analytic_repo.go b/patientapp/repository/grpc/analytic_repo.go new file mode 100644 index 00000000..fe598750 --- /dev/null +++ b/patientapp/repository/grpc/analytic_repo.go @@ -0,0 +1,35 @@ +package grpc + +import ( + "context" + + "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic" + "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" +) + +type AnalyticRepository struct{} + +func NewPatientRepo() *AnalyticRepository { + + return &AnalyticRepository{} +} + +func (db *AnalyticRepository) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) { + + return nil, nil +} + +func (db *AnalyticRepository) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) { + + return 0, nil +} + +func (db *AnalyticRepository) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) { + + return nil, nil +} + +func (db *AnalyticRepository) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) { + + return nil, nil +} diff --git a/patientapp/repository/mysql/analytic_repo.go b/patientapp/repository/mysql/analytic_repo.go index 743d8e51..223080cf 100644 --- a/patientapp/repository/mysql/analytic_repo.go +++ b/patientapp/repository/mysql/analytic_repo.go @@ -7,40 +7,30 @@ import ( "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" ) -type DataBase struct { - //conn *mysql.DB - patients []entity.Patient -} +type DataBase struct{} -func NewPatientRepo( /*conn *mysql.DB*/ ) *DataBase { - patients := make([]entity.Patient, 0) +func NewPatientRepo() *DataBase { - return &DataBase{ - //conn: conn, - patients: patients, - } + return &DataBase{} } func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) { - return db.patients, nil + return nil, nil } func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) { - return len(db.patients), nil + return 0, nil } func (db *DataBase) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) { - var out map[uint][]entity.MapSummaryItem - - return out, nil + return nil, nil } func (db *DataBase) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) { - result := make(map[uint][]entity.MapSummaryItem, 0) - return result, nil + return nil, nil }