Delete misc-code directory

This commit is contained in:
Mann Patel
2025-09-01 17:35:01 -06:00
committed by GitHub
parent 7f2b7e481a
commit d4ac56e73d
7 changed files with 0 additions and 410991 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,402 +0,0 @@
// package handlers
// import (
// "database/sql"
// "errors"
// "log"
// "net/http"
// "strconv"
// "time"
// "github.com/patel-mann/poll-system/app/internal/models"
// "github.com/patel-mann/poll-system/app/internal/utils"
// )
// // View model for listing/assigning schedules
// type AssignmentVM struct {
// ID int
// VolunteerID int
// VolunteerName string
// AddressID int
// Address string
// Date string // YYYY-MM-DD (for input[type=date])
// AppointmentTime string // HH:MM
// VisitedValidated bool
// }
// // GET + POST in one handler:
// // - GET: show assignments + form to assign
// // - POST: create a new assignment
// func AdminAssignmentsHandler(w http.ResponseWriter, r *http.Request) {
// switch r.Method {
// case http.MethodPost:
// if err := createAssignmentFromForm(r); err != nil {
// log.Println("create assignment error:", err)
// volunteers, _ := fetchVolunteers()
// addresses, _ := fetchAddresses()
// assignments, _ := fetchAssignments()
// utils.Render(w, "schedual/assignments.html", map[string]interface{}{
// "Title": "Admin — Assign Addresses",
// "IsAuthenticated": true,
// "ActiveSection": "admin_assignments",
// "Volunteers": volunteers,
// "Addresses": addresses,
// "Assignments": assignments,
// "Error": err.Error(),
// })
// return
// }
// http.Redirect(w, r, "/admin/assignments", http.StatusSeeOther)
// return
// }
// // GET: fetch volunteers, addresses, and existing assignments
// volunteers, err := fetchVolunteers()
// if err != nil {
// log.Println("fetch volunteers error:", err)
// http.Error(w, "Failed to load volunteers", http.StatusInternalServerError)
// return
// }
// addresses, err := fetchAddresses()
// if err != nil {
// log.Println("fetch addresses error:", err)
// http.Error(w, "Failed to load addresses", http.StatusInternalServerError)
// return
// }
// assignments, err := fetchAssignments()
// if err != nil {
// log.Println("fetch assignments error:", err)
// http.Error(w, "Failed to load assignments", http.StatusInternalServerError)
// return
// }
// utils.Render(w, "assignments.html", map[string]interface{}{
// "Title": "Admin — Assign Addresses",
// "IsAuthenticated": true,
// "ActiveSection": "admin_assignments",
// "Volunteers": volunteers,
// "Addresses": addresses,
// "Assignments": assignments,
// })
// }
// // GET (edit form) + POST (update/delete)
// func AdminAssignmentEditHandler(w http.ResponseWriter, r *http.Request) {
// idStr := r.URL.Query().Get("id")
// id, _ := strconv.Atoi(idStr)
// if id <= 0 {
// http.NotFound(w, r)
// return
// }
// if r.Method == http.MethodPost {
// action := r.FormValue("action")
// switch action {
// case "delete":
// if err := deleteAssignment(id); err != nil {
// log.Println("delete assignment error:", err)
// http.Error(w, "Failed to delete assignment", http.StatusInternalServerError)
// return
// }
// http.Redirect(w, r, "/admin/assignments", http.StatusSeeOther)
// return
// case "update":
// if err := updateAssignmentFromForm(id, r); err != nil {
// log.Println("update assignment error:", err)
// vm, _ := fetchAssignmentByID(id)
// volunteers, _ := fetchVolunteers()
// addresses, _ := fetchAddresses()
// utils.Render(w, "assignment_edit.html", map[string]interface{}{
// "Title": "Edit Assignment",
// "Assignment": vm,
// "Volunteers": volunteers,
// "Addresses": addresses,
// "Error": err.Error(),
// })
// return
// }
// http.Redirect(w, r, "/admin/assignments", http.StatusSeeOther)
// return
// default:
// http.Error(w, "Unknown action", http.StatusBadRequest)
// return
// }
// }
// // GET edit
// vm, err := fetchAssignmentByID(id)
// if err != nil {
// if err == sql.ErrNoRows {
// http.NotFound(w, r)
// return
// }
// log.Println("fetch assignment by ID error:", err)
// http.Error(w, "Failed to load assignment", http.StatusInternalServerError)
// return
// }
// volunteers, err := fetchVolunteers()
// if err != nil {
// log.Println("fetch volunteers error:", err)
// http.Error(w, "Failed to load volunteers", http.StatusInternalServerError)
// return
// }
// addresses, err := fetchAddresses()
// if err != nil {
// log.Println("fetch addresses error:", err)
// http.Error(w, "Failed to load addresses", http.StatusInternalServerError)
// return
// }
// utils.Render(w, "assignment_edit.html", map[string]interface{}{
// "Title": "Edit Assignment",
// "Assignment": vm,
// "Volunteers": volunteers,
// "Addresses": addresses,
// })
// }
// // ----- Helpers -----
// func createAssignmentFromForm(r *http.Request) error {
// volID, _ := strconv.Atoi(r.FormValue("volunteer_id"))
// addrID, _ := strconv.Atoi(r.FormValue("address_id"))
// dateStr := r.FormValue("date")
// timeStr := r.FormValue("appointment_time")
// if volID <= 0 || addrID <= 0 || dateStr == "" || timeStr == "" {
// return errors.New("please fill all required fields")
// }
// if _, err := time.Parse("2006-01-02", dateStr); err != nil {
// return errors.New("invalid date format")
// }
// if _, err := time.Parse("15:04", timeStr); err != nil {
// return errors.New("invalid time format")
// }
// _, err := models.DB.Exec(`
// INSERT INTO schedual (user_id, address_id, appointment_date, appointment_time, created_at, updated_at)
// VALUES ($1,$2,$3,$4,NOW(),NOW())
// `, volID, addrID, dateStr, timeStr)
// if err != nil {
// log.Println("database insert error:", err)
// return errors.New("failed to create assignment")
// }
// return nil
// }
// func updateAssignmentFromForm(id int, r *http.Request) error {
// volID, _ := strconv.Atoi(r.FormValue("volunteer_id"))
// addrID, _ := strconv.Atoi(r.FormValue("address_id"))
// dateStr := r.FormValue("date")
// timeStr := r.FormValue("appointment_time")
// if volID <= 0 || addrID <= 0 || dateStr == "" || timeStr == "" {
// return errors.New("please fill all required fields")
// }
// if _, err := time.Parse("2006-01-02", dateStr); err != nil {
// return errors.New("invalid date format")
// }
// if _, err := time.Parse("15:04", timeStr); err != nil {
// return errors.New("invalid time format")
// }
// result, err := models.DB.Exec(`
// UPDATE schedual
// SET user_id=$1, address_id=$2, appointment_date=$3, appointment_time=$4, updated_at=NOW()
// WHERE schedual_id=$5
// `, volID, addrID, dateStr, timeStr, id)
// if err != nil {
// log.Println("database update error:", err)
// return errors.New("failed to update assignment")
// }
// rowsAffected, _ := result.RowsAffected()
// if rowsAffected == 0 {
// return errors.New("assignment not found")
// }
// return nil
// }
// func deleteAssignment(id int) error {
// result, err := models.DB.Exec(`DELETE FROM schedual WHERE schedual_id=$1`, id)
// if err != nil {
// log.Println("database delete error:", err)
// return errors.New("failed to delete assignment")
// }
// rowsAffected, _ := result.RowsAffected()
// if rowsAffected == 0 {
// return errors.New("assignment not found")
// }
// return nil
// }
// // Fetch volunteers
// type VolunteerPick struct {
// ID int
// FirstName string
// LastName string
// Email string
// }
// func fetchVolunteers() ([]VolunteerPick, error) {
// rows, err := models.DB.Query(`
// SELECT users_id, first_name, last_name, email
// FROM "user"
// WHERE role='volunteer'
// ORDER BY first_name, last_name
// `)
// if err != nil {
// return nil, err
// }
// defer rows.Close()
// var out []VolunteerPick
// for rows.Next() {
// var v VolunteerPick
// if err := rows.Scan(&v.ID, &v.FirstName, &v.LastName, &v.Email); err != nil {
// log.Println("fetchVolunteers scan:", err)
// continue
// }
// out = append(out, v)
// }
// return out, rows.Err()
// }
// // Fetch addresses
// type AddressPick struct {
// ID int
// Label string
// VisitedValidated bool
// }
// func fetchAddresses() ([]AddressPick, error) {
// rows, err := models.DB.Query(`
// SELECT
// address_id,
// address,
// street_name,
// street_type,
// street_quadrant,
// house_number,
// house_alpha,
// longitude,
// latitude,
// visited_validated
// FROM address_database
// ORDER BY address_id DESC
// `)
// if err != nil {
// return nil, err
// }
// defer rows.Close()
// var out []AddressPick
// for rows.Next() {
// var addr models.AddressDatabase
// if err := rows.Scan(
// &addr.AddressID,
// &addr.Address,
// &addr.StreetName,
// &addr.StreetType,
// &addr.StreetQuadrant,
// &addr.HouseNumber,
// &addr.HouseAlpha,
// &addr.Longitude,
// &addr.Latitude,
// &addr.VisitedValidated,
// ); err != nil {
// log.Println("fetchAddresses scan:", err)
// continue
// }
// label := addr.Address
// if label == "" {
// label = addr.HouseNumber
// if addr.StreetName != "" {
// if label != "" {
// label += " "
// }
// label += addr.StreetName
// }
// if addr.StreetType != "" {
// label += " " + addr.StreetType
// }
// if addr.StreetQuadrant != "" {
// label += " " + addr.StreetQuadrant
// }
// if addr.HouseAlpha != nil {
// label += " " + *addr.HouseAlpha
// }
// }
// out = append(out, AddressPick{
// ID: addr.AddressID,
// Label: label,
// VisitedValidated: addr.VisitedValidated,
// })
// }
// return out, rows.Err()
// }
// // Add this missing function
// func fetchAssignments() ([]AssignmentVM, error) {
// rows, err := models.DB.Query(`
// SELECT
// s.schedual_id,
// u.users_id,
// COALESCE(u.first_name,'') || ' ' || COALESCE(u.last_name,'') AS volunteer_name,
// a.address_id,
// COALESCE(a.address,'') AS address,
// s.appointment_date,
// s.appointment_time
// FROM schedual s
// JOIN "user" u ON u.users_id = s.user_id
// JOIN address_database a ON a.address_id = s.address_id
// ORDER BY s.appointment_date DESC, s.appointment_time DESC
// `)
// if err != nil {
// return nil, err
// }
// defer rows.Close()
// var assignments []AssignmentVM
// for rows.Next() {
// var vm AssignmentVM
// if err := rows.Scan(&vm.ID, &vm.VolunteerID, &vm.VolunteerName, &vm.AddressID, &vm.Address,
// &vm.Date, &vm.AppointmentTime); err != nil {
// log.Println("fetchAssignments scan:", err)
// continue
// }
// assignments = append(assignments, vm)
// }
// return assignments, rows.Err()
// }
// func fetchAssignmentByID(id int) (AssignmentVM, error) {
// var vm AssignmentVM
// err := models.DB.QueryRow(`
// SELECT
// s.schedual_id,
// u.users_id,
// COALESCE(u.first_name,'') || ' ' || COALESCE(u.last_name,'') AS volunteer_name,
// a.address_id,
// COALESCE(a.address,'') AS address,
// s.appointment_date,
// s.appointment_time
// FROM schedual s
// JOIN "user" u ON u.users_id = s.user_id
// JOIN address_database a ON a.address_id = s.address_id
// WHERE s.schedual_id = $1
// `, id).Scan(&vm.ID, &vm.VolunteerID, &vm.VolunteerName, &vm.AddressID, &vm.Address,
// &vm.Date, &vm.AppointmentTime)
// return vm, err
// }

View File

@@ -1,83 +0,0 @@
package handlers
import (
"log"
"net/http"
"github.com/patel-mann/poll-system/app/internal/models"
"github.com/patel-mann/poll-system/app/internal/utils"
)
type AssignedAddress struct {
AddressID int
Address string
StreetName string
StreetType string
StreetQuadrant string
HouseNumber string
HouseAlpha *string
Longitude float64
Latitude float64
VisitedValidated bool
CreatedAt string
UpdatedAt string
Assigned bool
UserName string
UserEmail string
UserPhone string
AppointmentDate *string
AppointmentTime *string
}
func AssignedAddressesHandler(w http.ResponseWriter, r *http.Request) {
username,_ := models.GetCurrentUserName(r)
rows, err := models.DB.Query(`
SELECT
a.address_id, a.address, a.street_name, a.street_type, a.street_quadrant,
a.house_number, a.house_alpha, a.longitude, a.latitude, a.visited_validated,
a.created_at, a.updated_at,
CASE WHEN ap.user_id IS NOT NULL THEN true ELSE false END as assigned,
COALESCE(u.first_name || ' ' || u.last_name, '') as user_name,
COALESCE(u.email, '') as user_email,
COALESCE(u.phone, '') as user_phone,
TO_CHAR(ap.appointment_date, 'YYYY-MM-DD') as appointment_date,
TO_CHAR(ap.appointment_time, 'HH24:MI') as appointment_time
FROM address_database a
LEFT JOIN appointment ap ON a.address_id = ap.address_id
LEFT JOIN users u ON ap.user_id = u.user_id
ORDER BY a.address_id;
`)
if err != nil {
log.Printf("query error: %v", err)
http.Error(w, "query error", http.StatusInternalServerError)
return
}
defer rows.Close()
var assignedAddresses []AssignedAddress
for rows.Next() {
var addr AssignedAddress
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, &addr.Assigned, &addr.UserName, &addr.UserEmail,
&addr.UserPhone, &addr.AppointmentDate, &addr.AppointmentTime,
)
if err != nil {
log.Printf("scan error: %v", err)
continue
}
assignedAddresses = append(assignedAddresses, addr)
}
utils.Render(w, "address_assigned.html", map[string]interface{}{
"Title": "Assigned Addresses",
"IsAuthenticated": true,
"AssignedList": assignedAddresses,
"ShowAdminNav": true,
"Role": "admin",
"UserName": username,
"ActiveSection": "assigned",
})
}

View File

@@ -1,824 +0,0 @@
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)
}

View File

@@ -1,2 +0,0 @@
Name,Address,Email,Phone,Phone
Bob Johnson,1782 cornerstone bv ne,bob@email.com,555-0125,555-0125
1 Name Address Email Phone Phone
2 Bob Johnson 1782 cornerstone bv ne bob@email.com 555-0125 555-0125

View File

@@ -1,365 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Moraine lake sunset - Google Search</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap");
body {
font-family: "Roboto", sans-serif;
}
.mountain-bg {
background: linear-gradient(
135deg,
#4fc3f7 0%,
#29b6f6 25%,
#0277bd 50%,
#01579b 75%,
#263238 100%
);
}
</style>
</head>
<body class="bg-white">
<!-- Header with mountain background -->
<div class="mountain-bg relative h-48 overflow-hidden">
<!-- Mountain silhouettes -->
<div class="absolute inset-0">
<svg viewBox="0 0 1200 200" class="w-full h-full">
<!-- Background mountains -->
<polygon
points="0,200 0,100 100,80 200,120 300,90 400,110 500,85 600,105 700,95 800,115 900,100 1000,120 1100,110 1200,130 1200,200"
fill="rgba(69,90,100,0.4)"
/>
<!-- Mid mountains -->
<polygon
points="0,200 0,120 80,100 180,140 280,110 380,130 480,105 580,125 680,115 780,135 880,125 980,145 1080,135 1200,155 1200,200"
fill="rgba(38,50,56,0.6)"
/>
<!-- Front mountains -->
<polygon
points="0,200 0,140 60,120 160,160 260,130 360,150 460,125 560,145 660,135 760,155 860,145 960,165 1060,155 1200,175 1200,200"
fill="rgba(38,50,56,0.8)"
/>
<!-- Snow caps -->
<polygon points="50,130 80,100 110,130" fill="white" opacity="0.9" />
<polygon
points="150,150 180,140 210,150"
fill="white"
opacity="0.9"
/>
<polygon
points="250,140 280,110 310,140"
fill="white"
opacity="0.9"
/>
<polygon
points="450,135 480,105 510,135"
fill="white"
opacity="0.9"
/>
<polygon
points="650,145 680,115 710,145"
fill="white"
opacity="0.9"
/>
<polygon
points="850,155 880,125 910,155"
fill="white"
opacity="0.9"
/>
</svg>
</div>
<!-- Google logo and search bar -->
<div class="relative z-10 pt-8 px-4">
<div class="max-w-2xl mx-auto">
<!-- Google logo -->
<div class="mb-8">
<h1 class="text-white text-6xl font-normal tracking-tight">
Google
</h1>
</div>
<!-- Search bar -->
<div class="relative">
<div
class="bg-white rounded-full shadow-lg flex items-center px-4 py-3"
>
<input
type="text"
value="Moraine lake sunset"
class="flex-1 text-gray-900 text-lg outline-none px-2"
/>
<button class="p-2 hover:bg-gray-100 rounded-full">
<svg
class="w-6 h-6 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
></path>
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- User info -->
<div class="absolute top-4 right-4 flex items-center space-x-4">
<svg
class="w-6 h-6 text-white opacity-70"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
clip-rule="evenodd"
></path>
</svg>
<div
class="w-6 h-6 bg-white bg-opacity-20 rounded grid grid-cols-3 gap-0.5 p-1"
>
<div class="bg-white bg-opacity-60 rounded-sm"></div>
<div class="bg-white bg-opacity-60 rounded-sm"></div>
<div class="bg-white bg-opacity-60 rounded-sm"></div>
<div class="bg-white bg-opacity-60 rounded-sm"></div>
<div class="bg-white bg-opacity-60 rounded-sm"></div>
<div class="bg-white bg-opacity-60 rounded-sm"></div>
<div class="bg-white bg-opacity-60 rounded-sm"></div>
<div class="bg-white bg-opacity-60 rounded-sm"></div>
<div class="bg-white bg-opacity-60 rounded-sm"></div>
</div>
<span class="text-white text-sm">aurelien.salomon@gmail.com</span>
<svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
></path>
</svg>
</div>
</div>
<!-- Navigation -->
<div class="bg-gray-700 border-b border-gray-600">
<div class="max-w-6xl mx-auto px-4">
<div class="flex items-center justify-between">
<div class="flex space-x-8">
<a
href="#"
class="text-blue-300 border-b-2 border-blue-300 py-3 px-1 text-sm font-medium"
>WEB</a
>
<a href="#" class="text-gray-300 hover:text-white py-3 px-1 text-sm"
>MAPS</a
>
<a href="#" class="text-gray-300 hover:text-white py-3 px-1 text-sm"
>IMAGES</a
>
<a href="#" class="text-gray-300 hover:text-white py-3 px-1 text-sm"
>VIDEOS</a
>
<div class="relative">
<a
href="#"
class="text-gray-300 hover:text-white py-3 px-1 text-sm flex items-center"
>
MORE
<svg
class="w-4 h-4 ml-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
></path>
</svg>
</a>
</div>
<a href="#" class="text-gray-300 hover:text-white py-3 px-1 text-sm"
>SEARCH TOOLS</a
>
</div>
<div class="text-gray-400 text-sm">
About 7,920,200 Results&nbsp;&nbsp;0.18 seconds
</div>
</div>
</div>
</div>
<!-- Main content -->
<div class="max-w-6xl mx-auto px-4 py-4 flex">
<!-- Left column - Search results -->
<div class="flex-1 max-w-2xl">
<!-- Result 1 -->
<div class="mb-6">
<div class="text-sm text-green-600 mb-1">
wikipedia.com/morainelake
</div>
<h3 class="text-xl text-blue-600 hover:underline cursor-pointer mb-2">
Moraine lake - Wikipedia
</h3>
<p class="text-sm text-gray-600 leading-relaxed">
Moraine Lake is a glacially-fed lake in Banff National Park, 14
kilometres outside the Village of Lake Louise, Alberta, Canada. It
is situated in the Valley of the Ten Peaks, at an elevation of
approximately 6,183 feet. The lake has a surface area of 5 square
kilometres. The lake, being glacially fed, does not reach its crest
until mid to ...
</p>
</div>
<!-- Result 2 -->
<div class="mb-6">
<div class="text-sm text-green-600 mb-1">rockies.com</div>
<h3 class="text-xl text-blue-600 hover:underline cursor-pointer mb-2">
Best places to photograph rockies
</h3>
<p class="text-sm text-gray-600 leading-relaxed">
The light is usually best around sunrise and sunset, but interesting
photographs. Moraine Lake is about a 20 minute drive from Lake
Louise which is something ...
</p>
</div>
<!-- Images section -->
<div class="mb-8">
<h3 class="text-xl text-gray-900 mb-4">
Images for Moraine lake sunset
</h3>
<div class="grid grid-cols-4 gap-2">
<div
class="aspect-square bg-gradient-to-br from-blue-400 to-orange-400 rounded overflow-hidden"
>
<div
class="w-full h-full bg-gradient-to-t from-blue-900 via-blue-400 to-orange-300 relative"
>
<div
class="absolute bottom-0 w-full h-1/3 bg-gradient-to-r from-gray-800 to-gray-600"
></div>
</div>
</div>
<div
class="aspect-square bg-gradient-to-br from-orange-500 to-red-600 rounded overflow-hidden"
>
<div
class="w-full h-full bg-gradient-to-t from-gray-900 via-orange-500 to-yellow-400 relative"
>
<div
class="absolute bottom-0 w-full h-1/3 bg-gradient-to-r from-gray-900 to-gray-700"
></div>
</div>
</div>
<div
class="aspect-square bg-gradient-to-br from-yellow-400 to-orange-500 rounded overflow-hidden"
>
<div
class="w-full h-full bg-gradient-to-t from-blue-800 via-yellow-400 to-orange-300 relative"
>
<div
class="absolute bottom-0 w-full h-1/3 bg-gradient-to-r from-gray-800 to-gray-600"
></div>
</div>
</div>
<div
class="aspect-square bg-gradient-to-br from-green-400 to-blue-500 rounded overflow-hidden"
>
<div
class="w-full h-full bg-gradient-to-t from-green-800 via-blue-400 to-orange-200 relative"
>
<div
class="absolute bottom-0 w-full h-1/3 bg-gradient-to-r from-gray-900 to-gray-700"
></div>
</div>
</div>
</div>
</div>
<!-- Result 3 -->
<div class="mb-6">
<div class="text-sm text-green-600 mb-1">estravel.com</div>
<h3 class="text-xl text-blue-600 hover:underline cursor-pointer mb-2">
Best point for sunset in Alberta
</h3>
<p class="text-sm text-gray-600 leading-relaxed">
Moraine Lake is only half the size of its nearby neighbour Lake
Louise, but perhaps even more scenic. It's a glacier-fed lake
situated in the beautiful valley of the ten ...
</p>
</div>
<!-- Result 4 -->
<div class="mb-6">
<div class="text-sm text-green-600 mb-1">sunset.com</div>
<h3 class="text-xl text-blue-600 hover:underline cursor-pointer mb-2">
Sunset in the canada rocks
</h3>
<p class="text-sm text-gray-600 leading-relaxed">
Moraine Lake is only half the size of its nearby neighbour Lake
Louise, but perhaps even more scenic. It's a glacier-fed lake
situated in the beautiful valley of the ten ...
</p>
</div>
</div>
<!-- Right column - Weather widget -->
<div class="ml-8 w-80">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900">Moraine lake</h3>
<p class="text-sm text-gray-500">Sunshine</p>
</div>
<!-- Weather chart -->
<div class="mb-4">
<div
class="relative h-32 bg-gradient-to-t from-yellow-300 via-yellow-400 to-blue-400 rounded-lg overflow-hidden"
>
<!-- Sun -->
<div
class="absolute top-4 right-8 w-8 h-8 bg-yellow-400 rounded-full border-4 border-yellow-300"
></div>
<!-- Time indicator -->
<div
class="absolute top-2 right-2 text-xs font-medium text-gray-700"
>
6:35 PM<br />
<span class="text-blue-600">Sunset</span>
</div>
<!-- Time markers -->
<div
class="absolute bottom-2 left-0 right-0 flex justify-between text-xs text-gray-600 px-2"
>
<span>6:00 AM</span>
<span>1:00 PM</span>
<span>8:00 PM</span>
</div>
</div>
</div>
<!-- Days -->
<div class="grid grid-cols-4 gap-2 text-center text-sm">
<div class="text-gray-900 font-medium">TODAY</div>
<div class="text-gray-500">THURSDAY</div>
<div class="text-gray-500">FRIDAY</div>
<div class="text-gray-500">SATURDAY</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,319 +0,0 @@
import pandas as pd
import requests
import time
import json
from typing import Optional, Dict, Any
import logging
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class PostalCodeFetcher:
"""Fetch postal codes for Canadian addresses using various geocoding services"""
def __init__(self, api_key: Optional[str] = None, service: str = 'nominatim'):
"""
Initialize the postal code fetcher
Args:
api_key: API key for paid services (Google, MapBox, etc.)
service: Geocoding service to use ('nominatim', 'google', 'mapbox')
"""
self.api_key = api_key
self.service = service.lower()
self.session = requests.Session()
self.rate_limit_delay = 1.0 # seconds between requests
def format_address(self, row: Dict[str, Any]) -> str:
"""
Format address from CSV row data
Args:
row: Dictionary containing address components
Returns:
Formatted address string
"""
# Extract components
house_num = str(row.get('HOUSE_NUMBER', '')).strip()
house_alpha = str(row.get('HOUSE_ALPHA', '')).strip()
street_name = str(row.get('STREET_NAME', '')).strip()
street_type = str(row.get('STREET_TYPE', '')).strip()
street_quad = str(row.get('STREET_QUAD', '')).strip()
# Build address
address_parts = []
# House number and alpha
house_part = house_num
if house_alpha and house_alpha != 'nan':
house_part += house_alpha
if house_part:
address_parts.append(house_part)
# Street name and type
street_part = street_name
if street_type and street_type != 'nan' and street_type.upper() not in street_name.upper():
street_part += f" {street_type}"
if street_part:
address_parts.append(street_part)
# Quadrant
if street_quad and street_quad != 'nan':
address_parts.append(street_quad)
# Add city and province
address_parts.extend(["Calgary", "AB", "Canada"])
return ", ".join(address_parts)
def get_postal_code_nominatim(self, address: str, lat: float = None, lon: float = None) -> Optional[str]:
"""
Get postal code using OpenStreetMap Nominatim (free)
Args:
address: Full address string
lat: Latitude (optional, for reverse geocoding)
lon: Longitude (optional, for reverse geocoding)
Returns:
Postal code if found, None otherwise
"""
try:
# Try reverse geocoding first if coordinates available
if lat and lon:
url = "https://nominatim.openstreetmap.org/reverse"
params = {
'format': 'json',
'lat': lat,
'lon': lon,
'zoom': 18,
'addressdetails': 1
}
else:
# Forward geocoding
url = "https://nominatim.openstreetmap.org/search"
params = {
'q': address,
'format': 'json',
'addressdetails': 1,
'limit': 1,
'countrycodes': 'ca'
}
headers = {
'User-Agent': 'PostalCodeFetcher/1.0 (your-email@domain.com)'
}
response = self.session.get(url, params=params, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
if not data:
return None
# Extract postal code
if isinstance(data, list):
result = data[0] if data else {}
else:
result = data
address_details = result.get('address', {})
postal_code = address_details.get('postcode')
return postal_code
except Exception as e:
logger.error(f"Nominatim error for {address}: {e}")
return None
def get_postal_code_google(self, address: str) -> Optional[str]:
"""
Get postal code using Google Geocoding API (requires API key)
Args:
address: Full address string
Returns:
Postal code if found, None otherwise
"""
if not self.api_key:
logger.error("Google API key required")
return None
try:
url = "https://maps.googleapis.com/maps/api/geocode/json"
params = {
'address': address,
'key': self.api_key,
'region': 'ca'
}
response = self.session.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
if data.get('status') != 'OK' or not data.get('results'):
return None
result = data['results'][0]
# Extract postal code from address components
for component in result.get('address_components', []):
if 'postal_code' in component.get('types', []):
return component.get('long_name')
return None
except Exception as e:
logger.error(f"Google API error for {address}: {e}")
return None
def get_postal_code(self, row: Dict[str, Any]) -> Optional[str]:
"""
Get postal code for a single address row
Args:
row: Dictionary containing address data
Returns:
Postal code if found, None otherwise
"""
# Format the address
address = self.format_address(row)
logger.info(f"Fetching postal code for: {address}")
# Extract coordinates if available
lat = row.get('latitude')
lon = row.get('longitude')
# Convert to float if they're strings
try:
if lat and str(lat) != 'nan':
lat = float(lat)
else:
lat = None
except (ValueError, TypeError):
lat = None
try:
if lon and str(lon) != 'nan':
lon = float(lon)
else:
lon = None
except (ValueError, TypeError):
lon = None
postal_code = None
# Try different services
if self.service == 'nominatim':
postal_code = self.get_postal_code_nominatim(address, lat, lon)
elif self.service == 'google':
postal_code = self.get_postal_code_google(address)
# Rate limiting
time.sleep(self.rate_limit_delay)
return postal_code
def process_csv(self, csv_file_path: str, output_file_path: str = None) -> pd.DataFrame:
"""
Process CSV file and add postal codes
Args:
csv_file_path: Path to input CSV file
output_file_path: Path to output CSV file (optional)
Returns:
DataFrame with postal codes added
"""
logger.info(f"Processing CSV file: {csv_file_path}")
# Read CSV
df = pd.read_csv(csv_file_path)
logger.info(f"Loaded {len(df)} addresses")
# Add postal code column
df['POSTAL_CODE'] = None
# Process each row
for index, row in df.iterrows():
try:
postal_code = self.get_postal_code(row.to_dict())
df.at[index, 'POSTAL_CODE'] = postal_code
if postal_code:
logger.info(f"Row {index + 1}: Found postal code {postal_code}")
else:
logger.warning(f"Row {index + 1}: No postal code found")
except Exception as e:
logger.error(f"Error processing row {index + 1}: {e}")
df.at[index, 'POSTAL_CODE'] = None
# Save results
if output_file_path:
df.to_csv(output_file_path, index=False)
logger.info(f"Results saved to: {output_file_path}")
# Summary
found_count = df['POSTAL_CODE'].notna().sum()
logger.info(f"Successfully found postal codes for {found_count}/{len(df)} addresses")
return df
# Example usage functions
def fetch_postal_codes_free(csv_file_path: str, output_file_path: str = None) -> pd.DataFrame:
"""
Fetch postal codes using free Nominatim service
Args:
csv_file_path: Path to CSV file with address data
output_file_path: Optional output file path
Returns:
DataFrame with postal codes
"""
fetcher = PostalCodeFetcher(service='nominatim')
return fetcher.process_csv(csv_file_path, output_file_path)
def fetch_postal_codes_google(csv_file_path: str, api_key: str, output_file_path: str = None) -> pd.DataFrame:
"""
Fetch postal codes using Google Geocoding API
Args:
csv_file_path: Path to CSV file with address data
api_key: Google API key
output_file_path: Optional output file path
Returns:
DataFrame with postal codes
"""
fetcher = PostalCodeFetcher(api_key=api_key, service='google')
return fetcher.process_csv(csv_file_path, output_file_path)
# Sample usage
if __name__ == "__main__":
# Example 1: Using free Nominatim service
df = fetch_postal_codes_free('./Address.csv', 'addresses_with_postal_codes.csv')
# Example 2: Using Google API (requires API key)
# df = fetch_postal_codes_google('addresses.csv', 'YOUR_GOOGLE_API_KEY', 'addresses_with_postal_codes.csv')
# Example 3: Manual usage
#fetcher = PostalCodeFetcher(service='nominatim')
# Test with sample data
#sample_row = {
# 'HOUSE_NUMBER': '531',
# 'STREET_NAME': 'NORTHMOUNT',
# 'STREET_TYPE': 'DR',
# 'STREET_QUAD': 'NW',
# 'latitude': 51.0893695,
# 'longitude': -114.08514
#}
#postal_code = fetcher.get_postal_code(sample_row)
#print(f"Postal code: {postal_code}")