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() }