From 9bb0a4adef7848847ecaffa296131474878710b7 Mon Sep 17 00:00:00 2001 From: Mann Patel <130435633+Patel-Mann@users.noreply.github.com> Date: Sat, 30 Aug 2025 12:59:30 -0600 Subject: [PATCH] reports kind of working? --- README.MD | 39 +- app/internal/handlers/admin_reports.go | 824 +++++++++++++++++ app/internal/handlers/admin_voluteers.go | 11 +- app/internal/handlers/login.go | 2 +- .../templates/dashboard/dashboard.html | 40 + app/internal/templates/reports.html | 831 ++++++++++++++++++ app/main.go | 2 + 7 files changed, 1702 insertions(+), 47 deletions(-) create mode 100644 app/internal/handlers/admin_reports.go create mode 100644 app/internal/templates/reports.html diff --git a/README.MD b/README.MD index f8204c9..09fd3e3 100644 --- a/README.MD +++ b/README.MD @@ -1,38 +1,5 @@ # Poll-system -# ADDRESSES: - -- A giant dataset of all the addresses and their log,lat location (not interactive) -- A user able to see his ward addresses -- Assing the address to a user whose role is leader or volunteer - - mass assign addresses to the user, multiple houses can be assined ith tiem left blank - - we can assing only after checking id the volunter is free on that day and time -- volunteer schedualing their time and date -- view the volunteers schedualling preference - -# TODO - -## VOLUNTEER - -- Volunteer Schdual -- Appointment - -## APPOINTMENT - -```sql -create table user_addresses -( - user_id integer not null - references users - on delete cascade, - address_line1 varchar(200) not null, - address_line2 varchar(200), - city varchar(100), - province varchar(100), - country varchar(100), - postal_code varchar(20) not null, - created_at timestamp default now(), - updated_at timestamp default now(), - primary key (user_id, address_line1, postal_code) -); -``` +- TODO: Volunteer scehdual database +- TODO: Map View +- TODO: CSV smart address upload diff --git a/app/internal/handlers/admin_reports.go b/app/internal/handlers/admin_reports.go new file mode 100644 index 0000000..82a660b --- /dev/null +++ b/app/internal/handlers/admin_reports.go @@ -0,0 +1,824 @@ +package handlers + +import ( + "database/sql" + "fmt" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/patel-mann/poll-system/app/internal/models" + "github.com/patel-mann/poll-system/app/internal/utils" +) + +// ReportData represents the combined data for reports +type ReportData struct { + Users []models.User + Polls []PollWithDetails + Appointments []AppointmentWithDetails + Addresses []models.AddressDatabase + Teams []TeamWithDetails + TotalUsers int + TotalPolls int + TotalAddresses int +} + +type PollWithDetails struct { + PollID int `json:"poll_id"` + UserID int `json:"user_id"` + AuthorName string `json:"author_name"` + AddressID int `json:"address_id"` + Address string `json:"address"` + PollTitle string `json:"poll_title"` + PollDescription string `json:"poll_description"` + IsActive bool `json:"is_active"` + AmountDonated float64 `json:"amount_donated"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type AppointmentWithDetails struct { + SchedID int `json:"sched_id"` + UserID int `json:"user_id"` + UserName string `json:"user_name"` + AddressID int `json:"address_id"` + Address string `json:"address"` + AppointmentDate time.Time `json:"appointment_date"` + AppointmentTime time.Time `json:"appointment_time"` + CreatedAt time.Time `json:"created_at"` +} + +type TeamWithDetails struct { + TeamID int `json:"team_id"` + TeamLeadID int `json:"team_lead_id"` + TeamLeadName string `json:"team_lead_name"` + VolunteerID int `json:"volunteer_id"` + VolunteerName string `json:"volunteer_name"` + CreatedAt time.Time `json:"created_at"` +} + +// ReportHandler handles the report page with search and filter functionality +func ReportHandler(w http.ResponseWriter, r *http.Request) { + // currentUserID := r.Context().Value("user_id").(int) + username, _ := models.GetCurrentUserName(r) + role := r.Context().Value("user_role").(int) + + // Check if user has permission to view reports + if role != 1 { // Assuming role 1 is admin + http.Error(w, "Unauthorized", http.StatusForbidden) + return + } + + // Parse query parameters for filtering + searchType := r.URL.Query().Get("search_type") // users, polls, appointments, addresses, teams + searchQuery := r.URL.Query().Get("search_query") // general search term + dateFrom := r.URL.Query().Get("date_from") + dateTo := r.URL.Query().Get("date_to") + roleFilter := r.URL.Query().Get("role_filter") + statusFilter := r.URL.Query().Get("status_filter") // active, inactive for polls + sortBy := r.URL.Query().Get("sort_by") // created_at, name, email, etc. + sortOrder := r.URL.Query().Get("sort_order") // asc, desc + page := r.URL.Query().Get("page") + limit := r.URL.Query().Get("limit") + + // Set defaults + if sortBy == "" { + sortBy = "created_at" + } + if sortOrder == "" { + sortOrder = "desc" + } + if page == "" { + page = "1" + } + if limit == "" { + limit = "50" + } + + pageInt, _ := strconv.Atoi(page) + limitInt, _ := strconv.Atoi(limit) + offset := (pageInt - 1) * limitInt + + reportData := ReportData{} + + // Build queries based on search type and filters + switch searchType { + case "users": + reportData.Users = searchUsers(searchQuery, roleFilter, dateFrom, dateTo, sortBy, sortOrder, limitInt, offset) + reportData.TotalUsers = countUsers(searchQuery, roleFilter, dateFrom, dateTo) + case "polls": + reportData.Polls = searchPolls(searchQuery, statusFilter, dateFrom, dateTo, sortBy, sortOrder, limitInt, offset) + reportData.TotalPolls = countPolls(searchQuery, statusFilter, dateFrom, dateTo) + case "appointments": + reportData.Appointments = searchAppointments(searchQuery, dateFrom, dateTo, sortBy, sortOrder, limitInt, offset) + case "addresses": + reportData.Addresses = searchAddresses(searchQuery, dateFrom, dateTo, sortBy, sortOrder, limitInt, offset) + reportData.TotalAddresses = countAddresses(searchQuery, dateFrom, dateTo) + case "teams": + reportData.Teams = searchTeams(searchQuery, dateFrom, dateTo, sortBy, sortOrder, limitInt, offset) + default: + // Load summary data for all types + reportData.Users = searchUsers("", "", "", "", "created_at", "desc", 10, 0) + reportData.Polls = searchPolls("", "", "", "", "created_at", "desc", 10, 0) + reportData.Appointments = searchAppointments("", "", "", "created_at", "desc", 10, 0) + reportData.Addresses = searchAddresses("", "", "", "created_at", "desc", 10, 0) + reportData.Teams = searchTeams("", "", "", "created_at", "desc", 10, 0) + reportData.TotalUsers = countUsers("", "", "", "") + reportData.TotalPolls = countPolls("", "", "", "") + reportData.TotalAddresses = countAddresses("", "", "") + } + + adminnav := role == 1 + volunteernav := role != 1 + + utils.Render(w, "reports.html", map[string]interface{}{ + "Title": "Reports & Analytics", + "IsAuthenticated": true, + "ShowAdminNav": adminnav, + "ShowVolunteerNav": volunteernav, + "UserName": username, + "ActiveSection": "reports", + "ReportData": reportData, + "SearchType": searchType, + "SearchQuery": searchQuery, + "DateFrom": dateFrom, + "DateTo": dateTo, + "RoleFilter": roleFilter, + "StatusFilter": statusFilter, + "SortBy": sortBy, + "SortOrder": sortOrder, + "CurrentPage": pageInt, + "Limit": limitInt, + }) +} + +// searchUsers searches users with filters +func searchUsers(searchQuery, roleFilter, dateFrom, dateTo, sortBy, sortOrder string, limit, offset int) []models.User { + var users []models.User + + query := ` + SELECT u.user_id, u.first_name, u.last_name, u.email, u.phone, u.role_id, u.created_at, u.updated_at, u.admin_code + FROM users u + LEFT JOIN role r ON u.role_id = r.role_id + WHERE 1=1` + + var args []interface{} + argCount := 0 + + // Add search conditions + if searchQuery != "" { + argCount++ + query += fmt.Sprintf(` AND (LOWER(u.first_name) LIKE LOWER($%d) OR LOWER(u.last_name) LIKE LOWER($%d) OR LOWER(u.email) LIKE LOWER($%d))`, argCount, argCount, argCount) + args = append(args, "%"+searchQuery+"%") + } + + if roleFilter != "" { + argCount++ + query += fmt.Sprintf(` AND u.role_id = $%d`, argCount) + roleID, _ := strconv.Atoi(roleFilter) + args = append(args, roleID) + } + + if dateFrom != "" { + argCount++ + query += fmt.Sprintf(` AND u.created_at >= $%d`, argCount) + args = append(args, dateFrom) + } + + if dateTo != "" { + argCount++ + query += fmt.Sprintf(` AND u.created_at <= $%d`, argCount) + args = append(args, dateTo+" 23:59:59") + } + + // Add sorting + validSortColumns := map[string]bool{"created_at": true, "first_name": true, "last_name": true, "email": true} + if !validSortColumns[sortBy] { + sortBy = "created_at" + } + if sortOrder != "asc" && sortOrder != "desc" { + sortOrder = "desc" + } + query += fmt.Sprintf(` ORDER BY u.%s %s`, sortBy, strings.ToUpper(sortOrder)) + + // Add pagination + argCount++ + query += fmt.Sprintf(` LIMIT $%d`, argCount) + args = append(args, limit) + argCount++ + query += fmt.Sprintf(` OFFSET $%d`, argCount) + args = append(args, offset) + + rows, err := models.DB.Query(query, args...) + if err != nil { + log.Println("Error searching users:", err) + return users + } + defer rows.Close() + + for rows.Next() { + var user models.User + err := rows.Scan(&user.UserID, &user.FirstName, &user.LastName, &user.Email, &user.Phone, &user.RoleID, &user.CreatedAt, &user.UpdatedAt, &user.AdminCode) + if err != nil { + log.Println("Error scanning user:", err) + continue + } + users = append(users, user) + } + + return users +} + +// searchPolls searches polls with filters +func searchPolls(searchQuery, statusFilter, dateFrom, dateTo, sortBy, sortOrder string, limit, offset int) []PollWithDetails { + var polls []PollWithDetails + + query := ` + SELECT p.poll_id, p.user_id, COALESCE(u.first_name || ' ' || u.last_name, 'Unknown') as author_name, + p.address_id, COALESCE(a.address, 'No Address') as address, + p.poll_title, p.poll_description, p.is_active, p.amount_donated, p.created_at, p.updated_at + FROM poll p + LEFT JOIN users u ON p.user_id = u.user_id + LEFT JOIN address_database a ON p.address_id = a.address_id + WHERE 1=1` + + var args []interface{} + argCount := 0 + + if searchQuery != "" { + argCount++ + query += fmt.Sprintf(` AND (LOWER(p.poll_title) LIKE LOWER($%d) OR LOWER(p.poll_description) LIKE LOWER($%d))`, argCount, argCount) + args = append(args, "%"+searchQuery+"%") + } + + if statusFilter == "active" { + query += ` AND p.is_active = true` + } else if statusFilter == "inactive" { + query += ` AND p.is_active = false` + } + + if dateFrom != "" { + argCount++ + query += fmt.Sprintf(` AND p.created_at >= $%d`, argCount) + args = append(args, dateFrom) + } + + if dateTo != "" { + argCount++ + query += fmt.Sprintf(` AND p.created_at <= $%d`, argCount) + args = append(args, dateTo+" 23:59:59") + } + + validSortColumns := map[string]bool{"created_at": true, "poll_title": true, "amount_donated": true, "is_active": true} + if !validSortColumns[sortBy] { + sortBy = "created_at" + } + if sortOrder != "asc" && sortOrder != "desc" { + sortOrder = "desc" + } + query += fmt.Sprintf(` ORDER BY p.%s %s`, sortBy, strings.ToUpper(sortOrder)) + + argCount++ + query += fmt.Sprintf(` LIMIT $%d`, argCount) + args = append(args, limit) + argCount++ + query += fmt.Sprintf(` OFFSET $%d`, argCount) + args = append(args, offset) + + rows, err := models.DB.Query(query, args...) + if err != nil { + log.Println("Error searching polls:", err) + return polls + } + defer rows.Close() + + for rows.Next() { + var poll PollWithDetails + err := rows.Scan(&poll.PollID, &poll.UserID, &poll.AuthorName, &poll.AddressID, &poll.Address, + &poll.PollTitle, &poll.PollDescription, &poll.IsActive, &poll.AmountDonated, &poll.CreatedAt, &poll.UpdatedAt) + if err != nil { + log.Println("Error scanning poll:", err) + continue + } + polls = append(polls, poll) + } + + return polls +} + +// searchAppointments searches appointments with filters +func searchAppointments(searchQuery, dateFrom, dateTo, sortBy, sortOrder string, limit, offset int) []AppointmentWithDetails { + var appointments []AppointmentWithDetails + + query := ` + SELECT a.sched_id, a.user_id, COALESCE(u.first_name || ' ' || u.last_name, 'Unknown') as user_name, + a.address_id, COALESCE(ad.address, 'No Address') as address, + a.appointment_date, a.appointment_time, a.created_at + FROM appointment a + LEFT JOIN users u ON a.user_id = u.user_id + LEFT JOIN address_database ad ON a.address_id = ad.address_id + WHERE 1=1` + + var args []interface{} + argCount := 0 + + if searchQuery != "" { + argCount++ + query += fmt.Sprintf(` AND (LOWER(u.first_name) LIKE LOWER($%d) OR LOWER(u.last_name) LIKE LOWER($%d) OR LOWER(ad.address) LIKE LOWER($%d))`, argCount, argCount, argCount) + args = append(args, "%"+searchQuery+"%") + } + + if dateFrom != "" { + argCount++ + query += fmt.Sprintf(` AND a.appointment_date >= $%d`, argCount) + args = append(args, dateFrom) + } + + if dateTo != "" { + argCount++ + query += fmt.Sprintf(` AND a.appointment_date <= $%d`, argCount) + args = append(args, dateTo) + } + + validSortColumns := map[string]bool{"created_at": true, "appointment_date": true, "appointment_time": true} + if !validSortColumns[sortBy] { + sortBy = "appointment_date" + } + if sortOrder != "asc" && sortOrder != "desc" { + sortOrder = "desc" + } + query += fmt.Sprintf(` ORDER BY a.%s %s`, sortBy, strings.ToUpper(sortOrder)) + + argCount++ + query += fmt.Sprintf(` LIMIT $%d`, argCount) + args = append(args, limit) + argCount++ + query += fmt.Sprintf(` OFFSET $%d`, argCount) + args = append(args, offset) + + rows, err := models.DB.Query(query, args...) + if err != nil { + log.Println("Error searching appointments:", err) + return appointments + } + defer rows.Close() + + for rows.Next() { + var apt AppointmentWithDetails + var appointmentTime sql.NullTime + err := rows.Scan(&apt.SchedID, &apt.UserID, &apt.UserName, &apt.AddressID, &apt.Address, + &apt.AppointmentDate, &appointmentTime, &apt.CreatedAt) + if err != nil { + log.Println("Error scanning appointment:", err) + continue + } + if appointmentTime.Valid { + apt.AppointmentTime = appointmentTime.Time + } + appointments = append(appointments, apt) + } + + return appointments +} + +// searchAddresses searches addresses with filters +func searchAddresses(searchQuery, dateFrom, dateTo, sortBy, sortOrder string, limit, offset int) []models.AddressDatabase { + var addresses []models.AddressDatabase + + query := ` + SELECT address_id, address, street_name, street_type, street_quadrant, + house_number, house_alpha, longitude, latitude, visited_validated, created_at, updated_at + FROM address_database + WHERE 1=1` + + var args []interface{} + argCount := 0 + + if searchQuery != "" { + argCount++ + query += fmt.Sprintf(` AND (LOWER(address) LIKE LOWER($%d) OR LOWER(street_name) LIKE LOWER($%d) OR house_number LIKE $%d)`, argCount, argCount, argCount) + args = append(args, "%"+searchQuery+"%") + } + + if dateFrom != "" { + argCount++ + query += fmt.Sprintf(` AND created_at >= $%d`, argCount) + args = append(args, dateFrom) + } + + if dateTo != "" { + argCount++ + query += fmt.Sprintf(` AND created_at <= $%d`, argCount) + args = append(args, dateTo+" 23:59:59") + } + + validSortColumns := map[string]bool{"created_at": true, "address": true, "street_name": true, "visited_validated": true} + if !validSortColumns[sortBy] { + sortBy = "created_at" + } + if sortOrder != "asc" && sortOrder != "desc" { + sortOrder = "desc" + } + query += fmt.Sprintf(` ORDER BY %s %s`, sortBy, strings.ToUpper(sortOrder)) + + argCount++ + query += fmt.Sprintf(` LIMIT $%d`, argCount) + args = append(args, limit) + argCount++ + query += fmt.Sprintf(` OFFSET $%d`, argCount) + args = append(args, offset) + + rows, err := models.DB.Query(query, args...) + if err != nil { + log.Println("Error searching addresses:", err) + return addresses + } + defer rows.Close() + + for rows.Next() { + var addr models.AddressDatabase + err := rows.Scan(&addr.AddressID, &addr.Address, &addr.StreetName, &addr.StreetType, &addr.StreetQuadrant, + &addr.HouseNumber, &addr.HouseAlpha, &addr.Longitude, &addr.Latitude, &addr.VisitedValidated, &addr.CreatedAt, &addr.UpdatedAt) + if err != nil { + log.Println("Error scanning address:", err) + continue + } + addresses = append(addresses, addr) + } + + return addresses +} + +// searchTeams searches teams with filters +func searchTeams(searchQuery, dateFrom, dateTo, sortBy, sortOrder string, limit, offset int) []TeamWithDetails { + var teams []TeamWithDetails + + query := ` + SELECT t.team_id, t.team_lead_id, + COALESCE(lead.first_name || ' ' || lead.last_name, 'No Lead') as team_lead_name, + t.volunteer_id, + COALESCE(vol.first_name || ' ' || vol.last_name, 'No Volunteer') as volunteer_name, + t.created_at + FROM team t + LEFT JOIN users lead ON t.team_lead_id = lead.user_id + LEFT JOIN users vol ON t.volunteer_id = vol.user_id + WHERE 1=1` + + var args []interface{} + argCount := 0 + + if searchQuery != "" { + argCount++ + query += fmt.Sprintf(` AND (LOWER(lead.first_name) LIKE LOWER($%d) OR LOWER(lead.last_name) LIKE LOWER($%d) OR LOWER(vol.first_name) LIKE LOWER($%d) OR LOWER(vol.last_name) LIKE LOWER($%d))`, argCount, argCount, argCount, argCount) + args = append(args, "%"+searchQuery+"%") + } + + if dateFrom != "" { + argCount++ + query += fmt.Sprintf(` AND t.created_at >= $%d`, argCount) + args = append(args, dateFrom) + } + + if dateTo != "" { + argCount++ + query += fmt.Sprintf(` AND t.created_at <= $%d`, argCount) + args = append(args, dateTo+" 23:59:59") + } + + validSortColumns := map[string]bool{"created_at": true, "team_id": true} + if !validSortColumns[sortBy] { + sortBy = "created_at" + } + if sortOrder != "asc" && sortOrder != "desc" { + sortOrder = "desc" + } + query += fmt.Sprintf(` ORDER BY t.%s %s`, sortBy, strings.ToUpper(sortOrder)) + + argCount++ + query += fmt.Sprintf(` LIMIT $%d`, argCount) + args = append(args, limit) + argCount++ + query += fmt.Sprintf(` OFFSET $%d`, argCount) + args = append(args, offset) + + rows, err := models.DB.Query(query, args...) + if err != nil { + log.Println("Error searching teams:", err) + return teams + } + defer rows.Close() + + for rows.Next() { + var team TeamWithDetails + err := rows.Scan(&team.TeamID, &team.TeamLeadID, &team.TeamLeadName, &team.VolunteerID, &team.VolunteerName, &team.CreatedAt) + if err != nil { + log.Println("Error scanning team:", err) + continue + } + teams = append(teams, team) + } + + return teams +} + +// Helper functions for counting total records +func countUsers(searchQuery, roleFilter, dateFrom, dateTo string) int { + query := `SELECT COUNT(*) FROM users u WHERE 1=1` + var args []interface{} + argCount := 0 + + if searchQuery != "" { + argCount++ + query += fmt.Sprintf(` AND (LOWER(u.first_name) LIKE LOWER($%d) OR LOWER(u.last_name) LIKE LOWER($%d) OR LOWER(u.email) LIKE LOWER($%d))`, argCount, argCount, argCount) + args = append(args, "%"+searchQuery+"%") + } + + if roleFilter != "" { + argCount++ + query += fmt.Sprintf(` AND u.role_id = $%d`, argCount) + roleID, _ := strconv.Atoi(roleFilter) + args = append(args, roleID) + } + + if dateFrom != "" { + argCount++ + query += fmt.Sprintf(` AND u.created_at >= $%d`, argCount) + args = append(args, dateFrom) + } + + if dateTo != "" { + argCount++ + query += fmt.Sprintf(` AND u.created_at <= $%d`, argCount) + args = append(args, dateTo+" 23:59:59") + } + + var count int + err := models.DB.QueryRow(query, args...).Scan(&count) + if err != nil { + log.Println("Error counting users:", err) + return 0 + } + return count +} + +func countPolls(searchQuery, statusFilter, dateFrom, dateTo string) int { + query := `SELECT COUNT(*) FROM poll p WHERE 1=1` + var args []interface{} + argCount := 0 + + if searchQuery != "" { + argCount++ + query += fmt.Sprintf(` AND (LOWER(p.poll_title) LIKE LOWER($%d) OR LOWER(p.poll_description) LIKE LOWER($%d))`, argCount, argCount) + args = append(args, "%"+searchQuery+"%") + } + + if statusFilter == "active" { + query += ` AND p.is_active = true` + } else if statusFilter == "inactive" { + query += ` AND p.is_active = false` + } + + if dateFrom != "" { + argCount++ + query += fmt.Sprintf(` AND p.created_at >= $%d`, argCount) + args = append(args, dateFrom) + } + + if dateTo != "" { + argCount++ + query += fmt.Sprintf(` AND p.created_at <= $%d`, argCount) + args = append(args, dateTo+" 23:59:59") + } + + var count int + err := models.DB.QueryRow(query, args...).Scan(&count) + if err != nil { + log.Println("Error counting polls:", err) + return 0 + } + return count +} + +func countAddresses(searchQuery, dateFrom, dateTo string) int { + query := `SELECT COUNT(*) FROM address_database WHERE 1=1` + var args []interface{} + argCount := 0 + + if searchQuery != "" { + argCount++ + query += fmt.Sprintf(` AND (LOWER(address) LIKE LOWER($%d) OR LOWER(street_name) LIKE LOWER($%d) OR house_number LIKE $%d)`, argCount, argCount, argCount) + args = append(args, "%"+searchQuery+"%") + } + + if dateFrom != "" { + argCount++ + query += fmt.Sprintf(` AND created_at >= $%d`, argCount) + args = append(args, dateFrom) + } + + if dateTo != "" { + argCount++ + query += fmt.Sprintf(` AND created_at <= $%d`, argCount) + args = append(args, dateTo+" 23:59:59") + } + + var count int + err := models.DB.QueryRow(query, args...).Scan(&count) + if err != nil { + log.Println("Error counting addresses:", err) + return 0 + } + return count +} + +// ExportReportHandler handles CSV export of filtered data +func ExportReportHandler(w http.ResponseWriter, r *http.Request) { + role := r.Context().Value("user_role").(int) + if role != 1 { // Admin only + http.Error(w, "Unauthorized", http.StatusForbidden) + return + } + + searchType := r.URL.Query().Get("search_type") + // Get all the same filter parameters + searchQuery := r.URL.Query().Get("search_query") + dateFrom := r.URL.Query().Get("date_from") + dateTo := r.URL.Query().Get("date_to") + roleFilter := r.URL.Query().Get("role_filter") + statusFilter := r.URL.Query().Get("status_filter") + + w.Header().Set("Content-Type", "text/csv") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s_report_%s.csv\"", searchType, time.Now().Format("2006-01-02"))) + + switch searchType { + case "users": + users := searchUsers(searchQuery, roleFilter, dateFrom, dateTo, "created_at", "desc", 10000, 0) // Get all for export + w.Write([]byte("User ID,First Name,Last Name,Email,Phone,Role ID,Admin Code,Created At\n")) + for _, user := range users { + line := fmt.Sprintf("%d,%s,%s,%s,%s,%d,%s,%s\n", + user.UserID, user.FirstName, user.LastName, user.Email, user.Phone, user.RoleID, user.AdminCode, user.CreatedAt.Format("2006-01-02 15:04:05")) + w.Write([]byte(line)) + } + case "polls": + polls := searchPolls(searchQuery, statusFilter, dateFrom, dateTo, "created_at", "desc", 10000, 0) + w.Write([]byte("Poll ID,Author,Address,Title,Description,Active,Amount Donated,Created At\n")) + for _, poll := range polls { + line := fmt.Sprintf("%d,%s,%s,%s,%s,%t,%.2f,%s\n", + poll.PollID, poll.AuthorName, poll.Address, poll.PollTitle, poll.PollDescription, poll.IsActive, poll.AmountDonated, poll.CreatedAt.Format("2006-01-02 15:04:05")) + w.Write([]byte(line)) + } + case "appointments": + appointments := searchAppointments(searchQuery, dateFrom, dateTo, "appointment_date", "desc", 10000, 0) + w.Write([]byte("Schedule ID,User,Address,Date,Time,Created At\n")) + for _, apt := range appointments { + timeStr := "" + if !apt.AppointmentTime.IsZero() { + timeStr = apt.AppointmentTime.Format("15:04:05") + } + line := fmt.Sprintf("%d,%s,%s,%s,%s,%s\n", + apt.SchedID, apt.UserName, apt.Address, apt.AppointmentDate.Format("2006-01-02"), timeStr, apt.CreatedAt.Format("2006-01-02 15:04:05")) + w.Write([]byte(line)) + } + case "addresses": + addresses := searchAddresses(searchQuery, dateFrom, dateTo, "created_at", "desc", 10000, 0) + w.Write([]byte("Address ID,Address,Street Name,Street Type,Quadrant,House Number,Alpha,Longitude,Latitude,Visited,Created At\n")) + for _, addr := range addresses { + line := fmt.Sprintf("%d,%s,%s,%s,%s,%s,%s,%.6f,%.6f,%t,%s\n", + addr.AddressID, addr.Address, addr.StreetName, addr.StreetType, addr.StreetQuadrant, addr.HouseNumber, addr.HouseAlpha, + addr.Longitude, addr.Latitude, addr.VisitedValidated, addr.CreatedAt.Format("2006-01-02 15:04:05")) + w.Write([]byte(line)) + } + case "teams": + teams := searchTeams(searchQuery, dateFrom, dateTo, "created_at", "desc", 10000, 0) + w.Write([]byte("Team ID,Team Lead,Volunteer,Created At\n")) + for _, team := range teams { + line := fmt.Sprintf("%d,%s,%s,%s\n", + team.TeamID, team.TeamLeadName, team.VolunteerName, team.CreatedAt.Format("2006-01-02 15:04:05")) + w.Write([]byte(line)) + } + default: + http.Error(w, "Invalid export type", http.StatusBadRequest) + return + } +} + +// ReportStatsHandler provides JSON API for dashboard statistics +func ReportStatsHandler(w http.ResponseWriter, r *http.Request) { + role := r.Context().Value("user_role").(int) + if role != 1 { // Admin only + http.Error(w, "Unauthorized", http.StatusForbidden) + return + } + + stats := make(map[string]interface{}) + + // Get total counts + var totalUsers, totalPolls, totalAddresses, totalAppointments, totalTeams int + var activePolls, inactivePolls int + var visitedAddresses, unvisitedAddresses int + + // Total users + models.DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&totalUsers) + + // Total and active/inactive polls + models.DB.QueryRow("SELECT COUNT(*) FROM poll").Scan(&totalPolls) + models.DB.QueryRow("SELECT COUNT(*) FROM poll WHERE is_active = true").Scan(&activePolls) + models.DB.QueryRow("SELECT COUNT(*) FROM poll WHERE is_active = false").Scan(&inactivePolls) + + // Total and visited/unvisited addresses + models.DB.QueryRow("SELECT COUNT(*) FROM address_database").Scan(&totalAddresses) + models.DB.QueryRow("SELECT COUNT(*) FROM address_database WHERE visited_validated = true").Scan(&visitedAddresses) + models.DB.QueryRow("SELECT COUNT(*) FROM address_database WHERE visited_validated = false").Scan(&unvisitedAddresses) + + // Total appointments and teams + models.DB.QueryRow("SELECT COUNT(*) FROM appointment").Scan(&totalAppointments) + models.DB.QueryRow("SELECT COUNT(*) FROM team").Scan(&totalTeams) + + // Recent activity (last 30 days) + var recentUsers, recentPolls, recentAppointments int + models.DB.QueryRow("SELECT COUNT(*) FROM users WHERE created_at >= NOW() - INTERVAL '30 days'").Scan(&recentUsers) + models.DB.QueryRow("SELECT COUNT(*) FROM poll WHERE created_at >= NOW() - INTERVAL '30 days'").Scan(&recentPolls) + models.DB.QueryRow("SELECT COUNT(*) FROM appointment WHERE created_at >= NOW() - INTERVAL '30 days'").Scan(&recentAppointments) + + // Total donations + var totalDonations float64 + models.DB.QueryRow("SELECT COALESCE(SUM(amount_donated), 0) FROM poll").Scan(&totalDonations) + + stats["totals"] = map[string]int{ + "users": totalUsers, + "polls": totalPolls, + "addresses": totalAddresses, + "appointments": totalAppointments, + "teams": totalTeams, + } + + stats["poll_breakdown"] = map[string]int{ + "active": activePolls, + "inactive": inactivePolls, + } + + stats["address_breakdown"] = map[string]int{ + "visited": visitedAddresses, + "unvisited": unvisitedAddresses, + } + + stats["recent_activity"] = map[string]int{ + "users": recentUsers, + "polls": recentPolls, + "appointments": recentAppointments, + } + + stats["total_donations"] = totalDonations + + w.Header().Set("Content-Type", "application/json") + // utils.WriteJSON(w, stats) +} + +// Advanced search handler for complex queries +func AdvancedSearchHandler(w http.ResponseWriter, r *http.Request) { + role := r.Context().Value("user_role").(int) + if role != 1 { // Admin only + http.Error(w, "Unauthorized", http.StatusForbidden) + return + } + + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + err := r.ParseForm() + if err != nil { + http.Error(w, "Invalid form data", http.StatusBadRequest) + return + } + + // Build complex query based on multiple criteria + searchCriteria := map[string]string{ + "entity": r.FormValue("entity"), // users, polls, appointments, etc. + "text_search": r.FormValue("text_search"), + "date_from": r.FormValue("date_from"), + "date_to": r.FormValue("date_to"), + "role_filter": r.FormValue("role_filter"), + "status_filter": r.FormValue("status_filter"), + "location_filter": r.FormValue("location_filter"), // address-based filtering + "amount_min": r.FormValue("amount_min"), // for polls with donations + "amount_max": r.FormValue("amount_max"), + "sort_by": r.FormValue("sort_by"), + "sort_order": r.FormValue("sort_order"), + } + + // Redirect with query parameters + redirectURL := "/reports?" + params := []string{} + for key, value := range searchCriteria { + if value != "" { + params = append(params, fmt.Sprintf("%s=%s", key, value)) + } + } + redirectURL += strings.Join(params, "&") + + http.Redirect(w, r, redirectURL, http.StatusSeeOther) +} \ No newline at end of file diff --git a/app/internal/handlers/admin_voluteers.go b/app/internal/handlers/admin_voluteers.go index 66930c2..830beb0 100644 --- a/app/internal/handlers/admin_voluteers.go +++ b/app/internal/handlers/admin_voluteers.go @@ -13,7 +13,6 @@ import ( func VolunteerHandler(w http.ResponseWriter, r *http.Request) { - // TODO: Replace this with actual session/jwt extraction currentAdminID := r.Context().Value("user_id").(int) username,_ := models.GetCurrentUserName(r) @@ -134,12 +133,4 @@ func EditVolunteerHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/volunteers", http.StatusSeeOther) } -} - - - - -//assign volunterr the title of team_leader -//Team View -//edit volnteer data -// +} \ No newline at end of file diff --git a/app/internal/handlers/login.go b/app/internal/handlers/login.go index ab460e2..55f5489 100644 --- a/app/internal/handlers/login.go +++ b/app/internal/handlers/login.go @@ -58,7 +58,7 @@ func createJWTToken(userID, role int) (string, time.Time, error) { // Get individual components from environment variables jwtSecret := os.Getenv("JWT_SECRET") - var jwtKey = []byte(jwtSecret) //TODO: Move to env/config + var jwtKey = []byte(jwtSecret) expirationTime := time.Now().Add(12 * time.Hour) diff --git a/app/internal/templates/dashboard/dashboard.html b/app/internal/templates/dashboard/dashboard.html index 82756fb..1112bfd 100644 --- a/app/internal/templates/dashboard/dashboard.html +++ b/app/internal/templates/dashboard/dashboard.html @@ -20,6 +20,46 @@
Total Users
+{{.ReportData.TotalUsers}}
+Total Polls
+{{.ReportData.TotalPolls}}
+Total Addresses
+{{.ReportData.TotalAddresses}}
+Appointments
+{{len .ReportData.Appointments}}
+| User | +Phone | +Role | +Admin Code | +Created | +|
|---|---|---|---|---|---|
|
+ {{.FirstName}} {{.LastName}}
+ ID: {{.UserID}}
+ |
+ {{.Email}} | +{{.Phone}} | ++ + {{if eq .RoleID 1}}Admin{{else}}Volunteer{{end}} + + | +{{.AdminCode}} | +{{.CreatedAt.Format "2006-01-02 15:04"}} | +
| Poll | +Author | +Address | +Status | +Donated | +Created | +
|---|---|---|---|---|---|
|
+ {{.PollTitle}}
+ {{.PollDescription}}
+ |
+ {{.AuthorName}} | +{{.Address}} | ++ + {{if .IsActive}}Active{{else}}Inactive{{end}} + + | +${{printf "%.2f" .AmountDonated}} | +{{.CreatedAt.Format "2006-01-02 15:04"}} | +
| User | +Address | +Date | +Time | +Created | +
|---|---|---|---|---|
| {{.UserName}} | +{{.Address}} | +{{.AppointmentDate.Format "2006-01-02"}} | +{{.AppointmentTime.Format "15:04"}} | +{{.CreatedAt.Format "2006-01-02 15:04"}} | +
| Address | +Street Details | +Coordinates | +Visited | +Created | +
|---|---|---|---|---|
|
+ {{.Address}}
+ {{.HouseNumber}}{{.HouseAlpha}}
+ |
+
+ {{.StreetName}} {{.StreetType}}
+ {{.StreetQuadrant}}
+ |
+ {{printf "%.6f" .Latitude}}, {{printf "%.6f" .Longitude}} | ++ + {{if .VisitedValidated}}Visited{{else}}Not Visited{{end}} + + | +{{.CreatedAt.Format "2006-01-02 15:04"}} | +
| Team ID | +Team Lead | +Volunteer | +Created | +Actions | +
|---|---|---|---|---|
| {{.TeamID}} | +{{.TeamLeadName}} | +{{.VolunteerName}} | +{{.CreatedAt.Format "2006-01-02 15:04"}} | ++ View Details + | +
No users found
+ {{end}} +No polls found
+ {{end}} +No appointments found
+ {{end}} +No addresses found
+ {{end}} +