From 3259a7468eba7373590f47e7752a0b4c89d8e884 Mon Sep 17 00:00:00 2001 From: Mohammad Amin Date: Thu, 2 Apr 2026 18:29:19 +0330 Subject: [PATCH] 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