Delete misc-code directory
This commit is contained in:
408996
misc-code/Address.csv
408996
misc-code/Address.csv
File diff suppressed because it is too large
Load Diff
@@ -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
|
|
||||||
// }
|
|
||||||
@@ -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",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
Name,Address,Email,Phone,Phone
|
|
||||||
Bob Johnson,1782 cornerstone bv ne,bob@email.com,555-0125,555-0125
|
|
||||||
|
@@ -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 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>
|
|
||||||
@@ -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}")
|
|
||||||
Reference in New Issue
Block a user