diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index f5fccd3..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/README.MD b/README.MD index b222e4e..49bfa09 100644 --- a/README.MD +++ b/README.MD @@ -1,4 +1,16 @@ # Poll-system -- TODO: Reports Generation, Export csv, Print Pdf, Show Charts -- TODO: VOlunteer Schedual or avilablity +- TODO: volunteer Available +- TODO: Update assign address func to take into account availability + + +- 18'' Metal Ruler +- Sketching Material (dollaram)(sketch) +- Adhesive (staples/dollaram) + +Done: +- Exacto +- A Large Cutting Mat +- Black Construction Paper +- And a lock for your locker +- White Foam Core or Cardstock Paper (dollaram) diff --git a/app/.DS_Store b/app/.DS_Store deleted file mode 100644 index 6d70eae..0000000 Binary files a/app/.DS_Store and /dev/null differ diff --git a/app/internal/handlers/admin_addresses.go b/app/internal/handlers/admin_addresses.go index 470cadc..c3a3bfe 100644 --- a/app/internal/handlers/admin_addresses.go +++ b/app/internal/handlers/admin_addresses.go @@ -5,6 +5,7 @@ import ( "net/http" "strconv" "time" + "fmt" "github.com/patel-mann/poll-system/app/internal/models" "github.com/patel-mann/poll-system/app/internal/utils" @@ -82,22 +83,22 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) { // Query addresses with appointment + user info rows, err := models.DB.Query(` - SELECT + SELECT a.address_id, - a.address, - a.street_name, + a.address, + a.street_name, a.street_type, - a.street_quadrant, + a.street_quadrant, a.house_number, COALESCE(a.house_alpha, '') as house_alpha, - a.longitude, + a.longitude, a.latitude, a.visited_validated, a.created_at, a.updated_at, - CASE - WHEN ap.sched_id IS NOT NULL THEN true - ELSE false + CASE + WHEN ap.sched_id IS NOT NULL THEN true + ELSE false END as assigned, ap.user_id, COALESCE(u.first_name || ' ' || u.last_name, '') as user_name, @@ -146,12 +147,12 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) { log.Println("Scan error:", err) continue } - + // Handle nullable house_alpha if houseAlpha != "" { a.HouseAlpha = &houseAlpha } - + addresses = append(addresses, a) } @@ -303,10 +304,13 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) { } // Parse and validate the start time - _, err = time.Parse("15:04", startTime) - if err != nil { - http.Error(w, "Invalid start time format", http.StatusBadRequest) + parsedTime, err := time.Parse("15:04", startTime) + is_valid := ValidatedFreeTime(parsedDate, parsedTime, userID) + if is_valid != true { + http.Error(w, "User is not availabile", http.StatusBadRequest) return + }else{ + fmt.Print("hello") } // Verify the user exists and is associated with the current admin @@ -330,7 +334,7 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) { // Check if this address is already assigned to any user var exists int err = models.DB.QueryRow(` - SELECT COUNT(*) FROM appointment + SELECT COUNT(*) FROM appointment WHERE address_id = $1 `, addressID).Scan(&exists) if err != nil { @@ -346,7 +350,7 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) { // Check if the user already has an appointment at the same date and time var timeConflict int err = models.DB.QueryRow(` - SELECT COUNT(*) FROM appointment + SELECT COUNT(*) FROM appointment WHERE user_id = $1 AND appointment_date = $2 AND appointment_time = $3 `, userID, appointmentDate, startTime).Scan(&timeConflict) if err != nil { @@ -410,7 +414,7 @@ func RemoveAssignedAddressHandler(w http.ResponseWriter, r *http.Request) { currentAdminID := r.Context().Value("user_id").(int) var userExists int err = models.DB.QueryRow(` - SELECT COUNT(*) + SELECT COUNT(*) FROM admin_volunteers av JOIN appointment ap ON av.volunteer_id = ap.user_id WHERE av.admin_id = $1 AND ap.user_id = $2 AND ap.address_id = $3 diff --git a/app/internal/handlers/admin_dashboard.go b/app/internal/handlers/admin_dashboard.go index 244ea90..8a45ad5 100644 --- a/app/internal/handlers/admin_dashboard.go +++ b/app/internal/handlers/admin_dashboard.go @@ -21,7 +21,7 @@ func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) { // 1. Count volunteers assigned to this admin err := models.DB.QueryRow(` - SELECT COUNT(av.volunteer_id) + SELECT COUNT(av.volunteer_id) FROM admin_volunteers av WHERE av.admin_id = $1 AND av.is_active = TRUE; `, currentAdminID).Scan(&volunteerCount) @@ -32,7 +32,7 @@ func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) { // 2. Total donations from polls err = models.DB.QueryRow(` - SELECT COALESCE(SUM(amount_donated), 0) + SELECT COALESCE(SUM(amount_donated), 0) FROM poll_response; `).Scan(&totalDonations) if err != nil { @@ -42,7 +42,7 @@ func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) { // 3. Count validated addresses err = models.DB.QueryRow(` - SELECT COUNT(*) + SELECT COUNT(*) FROM address_database WHERE visited_validated = TRUE; `).Scan(&validatedCount) @@ -53,8 +53,8 @@ func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) { // 4. Calculate percentage of houses left to visit err = models.DB.QueryRow(` - SELECT - CASE + SELECT + CASE WHEN COUNT(*) = 0 THEN 0 ELSE ROUND( (COUNT(*) FILTER (WHERE visited_validated = FALSE)::numeric / COUNT(*)::numeric) * 100, 2 @@ -79,4 +79,4 @@ func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) { "Role": role, "ActiveSection": "dashboard", }) -} \ No newline at end of file +} diff --git a/app/internal/handlers/admin_reports.go b/app/internal/handlers/admin_reports.go index 2c3699b..92fe42d 100644 --- a/app/internal/handlers/admin_reports.go +++ b/app/internal/handlers/admin_reports.go @@ -177,577 +177,425 @@ func getAllReportDefinitions() map[string][]ReportDefinition { return map[string][]ReportDefinition{ "users": { { - ID: "volunteer_participation_rate", // get all the appointment(done, notdone, total) poll(done, not doen, total) - Name: "Volunteer participation rate", - Description: "Count of users grouped by their role", - SQL: `SELECT - u.user_id, - u.first_name, - u.last_name, - COUNT(p.poll_id) AS total_polls, - COUNT(a.user_id) AS total_appointments, - case - WHEN COUNT(a.user_id) = 0 THEN NULL -- avoid division by zero - ELSE ROUND(CAST(COUNT(p.poll_id) AS numeric) / COUNT(a.user_id), 2) - END AS poll_to_appointment_rate - from users u - LEFT JOIN poll p ON u.user_id = p.user_id - LEFT JOIN appointment a ON u.user_id = a.user_id - GROUP BY u.user_id, u.first_name, u.last_name;`, - }, - { - ID: "volunteer_activity", - Name: "Volunteer Activity Summary", - Description: "Summary of volunteer activities including appointments and polls", + ID: "volunteer_participation_rate", + Name: "Volunteer Participation Rate", + Description: "Poll responses and donations collected by each volunteer/team lead", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, u.email, - COUNT(DISTINCT a.sched_id) as appointments_count, - COUNT(DISTINCT p.poll_id) as polls_created, - u.created_at as joined_date - FROM users u - LEFT JOIN appointment a ON u.user_id = a.user_id AND a.created_at BETWEEN ?1 AND ?2 - LEFT JOIN poll p ON u.user_id = p.user_id AND p.created_at BETWEEN ?1 AND ?2 - WHERE u.role_id = 2 - GROUP BY u.user_id, u.first_name, u.last_name, u.email, u.created_at - ORDER BY appointments_count DESC, polls_created DESC`, - }, - { - ID: "team_performance", - Name: "Team Performance Report", - Description: "Performance metrics for each team", - SQL: `SELECT - t.team_id, - ul.first_name || ' ' || ul.last_name as team_lead, - uv.first_name || ' ' || uv.last_name as volunteer, - COUNT(DISTINCT a.sched_id) as appointments, - COUNT(DISTINCT p.poll_id) as polls_created, - t.created_at as team_formed - FROM team t - LEFT JOIN users ul ON t.team_lead_id = ul.user_id - LEFT JOIN users uv ON t.volunteer_id = uv.user_id - LEFT JOIN appointment a ON uv.user_id = a.user_id AND a.created_at BETWEEN ?1 AND ?2 - LEFT JOIN poll p ON uv.user_id = p.user_id AND p.created_at BETWEEN ?1 AND ?2 - GROUP BY t.team_id, ul.first_name, ul.last_name, uv.first_name, uv.last_name, t.created_at - ORDER BY appointments DESC`, - }, - { - ID: "admin_workload", - Name: "Admin Workload Analysis", - Description: "Workload distribution across admins", - SQL: `SELECT - u.first_name || ' ' || u.last_name as admin_name, - u.email, - COUNT(DISTINCT t.team_id) as teams_managed, + r.name as role, COUNT(DISTINCT p.poll_id) as polls_created, + COUNT(DISTINCT pr.poll_response_id) as responses_collected, + COALESCE(SUM(pr.question6_donation_amount), 0) as total_donations, COUNT(DISTINCT a.sched_id) as appointments_scheduled FROM users u - LEFT JOIN team t ON u.user_id = t.team_lead_id + JOIN role r ON u.role_id = r.role_id LEFT JOIN poll p ON u.user_id = p.user_id AND p.created_at BETWEEN ?1 AND ?2 + LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id LEFT JOIN appointment a ON u.user_id = a.user_id AND a.created_at BETWEEN ?1 AND ?2 - WHERE u.role_id = 1 - GROUP BY u.user_id, u.first_name, u.last_name, u.email - ORDER BY teams_managed DESC, polls_created DESC`, + WHERE u.role_id IN (2, 3) + GROUP BY u.user_id, u.first_name, u.last_name, u.email, r.name + ORDER BY responses_collected DESC, total_donations DESC`, }, { - ID: "inactive_users", - Name: "Inactive Users Report", - Description: "Users with no recent activity", + ID: "top_performing_volunteers", + Name: "Top-Performing Volunteers & Team Leads", + Description: "Volunteers ranked by responses collected and donations secured", SQL: `SELECT - u.first_name || ' ' || u.last_name as user_name, - u.email, - CASE - WHEN u.role_id = 1 THEN 'Admin' - WHEN u.role_id = 2 THEN 'Volunteer' - ELSE 'Unknown' - END as role, - u.created_at as joined_date, - COALESCE(MAX(a.created_at), MAX(p.created_at)) as last_activity + u.first_name || ' ' || u.last_name as volunteer_name, + r.name as role, + COUNT(DISTINCT pr.poll_response_id) as responses_collected, + COALESCE(SUM(pr.question6_donation_amount), 0) as donations_secured, + COUNT(DISTINCT p.poll_id) as polls_created, + AVG(pr.question6_donation_amount) as avg_donation_per_poll FROM users u - LEFT JOIN appointment a ON u.user_id = a.user_id - LEFT JOIN poll p ON u.user_id = p.user_id - GROUP BY u.user_id, u.first_name, u.last_name, u.email, u.role_id, u.created_at - HAVING COALESCE(MAX(a.created_at), MAX(p.created_at)) < ?1 OR COALESCE(MAX(a.created_at), MAX(p.created_at)) IS NULL - ORDER BY last_activity DESC`, + JOIN role r ON u.role_id = r.role_id + JOIN poll p ON u.user_id = p.user_id + LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id + WHERE u.role_id IN (2, 3) AND p.created_at BETWEEN ?1 AND ?2 + GROUP BY u.user_id, u.first_name, u.last_name, r.name + HAVING COUNT(DISTINCT pr.poll_response_id) > 0 + ORDER BY responses_collected DESC, donations_secured DESC + LIMIT 20`, + }, + { + ID: "response_donation_ratio", + Name: "Response-to-Donation Ratio per Volunteer", + Description: "Efficiency measure showing donation amount per response collected", + SQL: `SELECT + u.first_name || ' ' || u.last_name as volunteer_name, + COUNT(DISTINCT pr.poll_response_id) as total_responses, + COALESCE(SUM(pr.question6_donation_amount), 0) as total_donations, + CASE + WHEN COUNT(DISTINCT pr.poll_response_id) > 0 + THEN COALESCE(SUM(pr.question6_donation_amount), 0) / COUNT(DISTINCT pr.poll_response_id) + ELSE 0 + END as donation_per_response, + CASE + WHEN COALESCE(SUM(pr.question6_donation_amount), 0) > 0 + THEN COUNT(DISTINCT pr.poll_response_id) / COALESCE(SUM(pr.question6_donation_amount), 1) + ELSE COUNT(DISTINCT pr.poll_response_id) + END as responses_per_dollar + FROM users u + JOIN poll p ON u.user_id = p.user_id + LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id + WHERE u.role_id IN (2, 3) AND p.created_at BETWEEN ?1 AND ?2 + GROUP BY u.user_id, u.first_name, u.last_name + HAVING COUNT(DISTINCT pr.poll_response_id) > 0 + ORDER BY donation_per_response DESC`, + }, + { + ID: "user_address_coverage", + Name: "User Address Coverage", + Description: "Number of unique addresses covered by each volunteer/team lead", + SQL: `SELECT + u.first_name || ' ' || u.last_name as volunteer_name, + COUNT(DISTINCT p.address_id) as unique_addresses_polled, + COUNT(DISTINCT a.address_id) as unique_addresses_appointed, + COUNT(DISTINCT COALESCE(p.address_id, a.address_id)) as total_unique_addresses, + COUNT(DISTINCT pr.poll_response_id) as total_responses + FROM users u + LEFT JOIN poll p ON u.user_id = p.user_id AND p.created_at BETWEEN ?1 AND ?2 + LEFT JOIN appointment a ON u.user_id = a.user_id AND a.created_at BETWEEN ?1 AND ?2 + LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id + WHERE u.role_id IN (2, 3) + GROUP BY u.user_id, u.first_name, u.last_name + HAVING COUNT(DISTINCT COALESCE(p.address_id, a.address_id)) > 0 + ORDER BY total_unique_addresses DESC, total_responses DESC`, }, }, "addresses": { { - ID: "coverage_by_area", - Name: "Coverage by Area", - Description: "Address coverage statistics by geographical area", + ID: "poll_responses_by_address", + Name: "Total Poll Responses by Address", + Description: "Shows engagement hotspots - addresses with most poll responses", SQL: `SELECT - COALESCE(NULLIF(TRIM(SPLIT_PART(address, ',', -1)), ''), 'Unknown') as area, - COUNT(*) as total_addresses, - COUNT(CASE WHEN visited_validated = true THEN 1 END) as visited_count, - ROUND(COUNT(CASE WHEN visited_validated = true THEN 1 END) * 100.0 / COUNT(*), 2) as coverage_percentage - FROM address_database - WHERE created_at BETWEEN ?1 AND ?2 - GROUP BY area - ORDER BY total_addresses DESC`, - }, - { - ID: "visits_by_postal", - Name: "Visits by Postal Code", - Description: "Visit statistics grouped by postal code", - SQL: `SELECT - COALESCE(NULLIF(TRIM(SUBSTRING(address FROM '[A-Za-z][0-9][A-Za-z] ?[0-9][A-Za-z][0-9]')), ''), 'No Postal Code') as postal_code, - COUNT(*) as addresses, - COUNT(CASE WHEN visited_validated = true THEN 1 END) as visited, - COUNT(CASE WHEN visited_validated = false THEN 1 END) as unvisited - FROM address_database - WHERE created_at BETWEEN ?1 AND ?2 - GROUP BY postal_code - ORDER BY addresses DESC + ad.address, + ad.postal_code, + ad.street_quadrant, + COUNT(DISTINCT pr.poll_response_id) as total_responses, + COUNT(DISTINCT p.poll_id) as polls_conducted, + COALESCE(SUM(pr.question6_donation_amount), 0) as total_donations, + CASE + WHEN COUNT(DISTINCT pr.poll_response_id) > 0 + THEN COALESCE(SUM(pr.question6_donation_amount), 0) / COUNT(DISTINCT pr.poll_response_id) + ELSE 0 + END as avg_donation_per_response + FROM address_database ad + JOIN poll p ON ad.address_id = p.address_id + LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id + WHERE p.created_at BETWEEN ?1 AND ?2 + GROUP BY ad.address_id, ad.address, ad.postal_code, ad.street_quadrant + ORDER BY total_responses DESC, total_donations DESC LIMIT 50`, }, { - ID: "unvisited_addresses", - Name: "Unvisited Addresses", - Description: "List of addresses that haven't been visited", + ID: "donations_by_address", + Name: "Total Donations by Address", + Description: "Shows financially supportive areas - addresses with highest donations", SQL: `SELECT - address_id, - address, - latitude, - longitude, - created_at as added_date - FROM address_database - WHERE visited_validated = false - AND created_at BETWEEN ?1 AND ?2 - ORDER BY created_at DESC - LIMIT 100`, - }, - { - ID: "donations_by_location", - Name: "Donations by Location", - Description: "Donation amounts grouped by address location", - SQL: `SELECT - a.address, - COUNT(p.poll_id) as total_polls, - COALESCE(SUM(p.amount_donated), 0) as total_donations, - COALESCE(AVG(p.amount_donated), 0) as avg_donation - FROM address_database a - LEFT JOIN poll p ON a.address_id = p.address_id AND p.created_at BETWEEN ?1 AND ?2 - GROUP BY a.address_id, a.address - HAVING COUNT(p.poll_id) > 0 + ad.address, + ad.postal_code, + ad.street_quadrant, + COALESCE(SUM(pr.question6_donation_amount), 0) as total_donations, + COUNT(DISTINCT p.poll_id) as polls_conducted, + COUNT(DISTINCT pr.poll_response_id) as total_responses, + AVG(pr.question6_donation_amount) as avg_donation_per_poll + FROM address_database ad + JOIN poll p ON ad.address_id = p.address_id + LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id + WHERE p.created_at BETWEEN ?1 AND ?2 AND pr.question6_donation_amount > 0 + GROUP BY ad.address_id, ad.address, ad.postal_code, ad.street_quadrant ORDER BY total_donations DESC LIMIT 50`, }, { - ID: "address_validation_status", - Name: "Address Validation Status", - Description: "Status of address validation across the database", + ID: "street_level_breakdown", + Name: "Street-Level Breakdown (Responses & Donations)", + Description: "Granular view for targeting - responses and donations by street", SQL: `SELECT - CASE - WHEN visited_validated = true THEN 'Validated' - WHEN visited_validated = false THEN 'Not Validated' - ELSE 'Unknown' - END as validation_status, - COUNT(*) as address_count, - ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM address_database), 2) as percentage - FROM address_database - WHERE created_at BETWEEN ?1 AND ?2 - GROUP BY visited_validated - ORDER BY address_count DESC`, + ad.street_name, + ad.street_type, + ad.street_quadrant, + COUNT(DISTINCT ad.address_id) as unique_addresses, + COUNT(DISTINCT pr.poll_response_id) as total_responses, + COALESCE(SUM(pr.question6_donation_amount), 0) as total_donations, + COUNT(DISTINCT p.poll_id) as polls_conducted + FROM address_database ad + LEFT JOIN poll p ON ad.address_id = p.address_id AND p.created_at BETWEEN ?1 AND ?2 + LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id + WHERE ad.street_name IS NOT NULL + GROUP BY ad.street_name, ad.street_type, ad.street_quadrant + HAVING COUNT(DISTINCT pr.poll_response_id) > 0 OR COALESCE(SUM(pr.question6_donation_amount), 0) > 0 + ORDER BY total_responses DESC, total_donations DESC`, + }, + { + ID: "quadrant_summary", + Name: "Quadrant-Level Summary (NE, NW, SE, SW)", + Description: "Higher-level trend view by city quadrants", + SQL: `SELECT + COALESCE(ad.street_quadrant, 'Unknown') as quadrant, + COUNT(DISTINCT ad.address_id) as unique_addresses, + COUNT(DISTINCT p.poll_id) as polls_conducted, + COUNT(DISTINCT pr.poll_response_id) as total_responses, + COALESCE(SUM(pr.question6_donation_amount), 0) as total_donations, + AVG(pr.question6_donation_amount) as avg_donation_per_poll, + COUNT(DISTINCT a.sched_id) as appointments_scheduled + FROM address_database ad + LEFT JOIN poll p ON ad.address_id = p.address_id AND p.created_at BETWEEN ?1 AND ?2 + LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id + LEFT JOIN appointment a ON ad.address_id = a.address_id AND a.created_at BETWEEN ?1 AND ?2 + GROUP BY ad.street_quadrant + ORDER BY total_responses DESC, total_donations DESC`, }, }, "appointments": { { - ID: "appointments_by_day", - Name: "Appointments by Day", - Description: "Daily breakdown of appointment scheduling", - SQL: `SELECT - appointment_date, - COUNT(*) as appointments_scheduled, - COUNT(DISTINCT user_id) as unique_volunteers, - COUNT(DISTINCT address_id) as unique_addresses - FROM appointment - WHERE appointment_date BETWEEN ?1 AND ?2 - GROUP BY appointment_date - ORDER BY appointment_date DESC`, - }, - { - ID: "completion_rates", - Name: "Completion Rates", - Description: "Appointment completion statistics by volunteer", + ID: "upcoming_appointments", + Name: "Upcoming Appointments per Volunteer/Team Lead", + Description: "Scheduling load - upcoming appointments by user", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, - COUNT(a.sched_id) as total_appointments, - COUNT(CASE WHEN ad.visited_validated = true THEN 1 END) as completed_visits, - ROUND(COUNT(CASE WHEN ad.visited_validated = true THEN 1 END) * 100.0 / COUNT(a.sched_id), 2) as completion_rate + r.name as role, + COUNT(*) as upcoming_appointments, + MIN(a.appointment_date) as earliest_appointment, + MAX(a.appointment_date) as latest_appointment, + COUNT(CASE WHEN a.appointment_date = CURRENT_DATE THEN 1 END) as today_appointments, + COUNT(CASE WHEN a.appointment_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '7 days' THEN 1 END) as week_appointments + FROM appointment a + JOIN users u ON a.user_id = u.user_id + JOIN role r ON u.role_id = r.role_id + WHERE a.appointment_date >= CURRENT_DATE AND a.appointment_date BETWEEN ?1 AND ?2 + GROUP BY u.user_id, u.first_name, u.last_name, r.name + ORDER BY upcoming_appointments DESC`, + }, + { + ID: "missed_vs_completed", + Name: "Missed vs Completed Appointments", + Description: "Reliability metric - appointment completion rates", + SQL: `SELECT + u.first_name || ' ' || u.last_name as volunteer_name, + COUNT(*) as total_appointments, + COUNT(CASE WHEN a.appointment_date < CURRENT_DATE THEN 1 END) as past_appointments, + COUNT(CASE WHEN a.appointment_date >= CURRENT_DATE THEN 1 END) as upcoming_appointments, + COUNT(CASE WHEN a.appointment_date < CURRENT_DATE AND EXISTS ( + SELECT 1 FROM poll p WHERE p.user_id = a.user_id AND p.address_id = a.address_id + AND DATE(p.created_at) = a.appointment_date + ) THEN 1 END) as completed_appointments, + COUNT(CASE WHEN a.appointment_date < CURRENT_DATE AND NOT EXISTS ( + SELECT 1 FROM poll p WHERE p.user_id = a.user_id AND p.address_id = a.address_id + AND DATE(p.created_at) = a.appointment_date + ) THEN 1 END) as missed_appointments, + CASE + WHEN COUNT(CASE WHEN a.appointment_date < CURRENT_DATE THEN 1 END) > 0 + THEN COUNT(CASE WHEN a.appointment_date < CURRENT_DATE AND EXISTS ( + SELECT 1 FROM poll p WHERE p.user_id = a.user_id AND p.address_id = a.address_id + AND DATE(p.created_at) = a.appointment_date + ) THEN 1 END) * 100.0 / COUNT(CASE WHEN a.appointment_date < CURRENT_DATE THEN 1 END) + ELSE 0 + END as completion_rate_percent FROM appointment a JOIN users u ON a.user_id = u.user_id - LEFT JOIN address_database ad ON a.address_id = ad.address_id WHERE a.created_at BETWEEN ?1 AND ?2 GROUP BY u.user_id, u.first_name, u.last_name - HAVING COUNT(a.sched_id) > 0 - ORDER BY completion_rate DESC, total_appointments DESC`, + ORDER BY completion_rate_percent DESC, total_appointments DESC`, }, { - ID: "volunteer_schedules", - Name: "Volunteer Schedules", - Description: "Current volunteer scheduling overview", + ID: "appointments_by_quadrant", + Name: "Appointments by Quadrant/Region", + Description: "Geographic distribution of appointments", + SQL: `SELECT + COALESCE(ad.street_quadrant, 'Unknown') as quadrant, + COUNT(*) as total_appointments, + COUNT(CASE WHEN a.appointment_date >= CURRENT_DATE THEN 1 END) as upcoming, + COUNT(CASE WHEN a.appointment_date < CURRENT_DATE THEN 1 END) as past, + COUNT(DISTINCT a.user_id) as unique_volunteers, + COUNT(DISTINCT a.address_id) as unique_addresses + FROM appointment a + JOIN address_database ad ON a.address_id = ad.address_id + WHERE a.created_at BETWEEN ?1 AND ?2 + GROUP BY ad.street_quadrant + ORDER BY total_appointments DESC`, + }, + { + ID: "scheduling_lead_time", + Name: "Average Lead Time (Scheduled vs Actual Date)", + Description: "Scheduling efficiency - time between scheduling and appointment", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, - a.appointment_date, - a.appointment_time, - ad.address, - a.created_at as scheduled_date + COUNT(*) as total_appointments, + AVG(a.appointment_date - DATE(a.created_at)) as avg_lead_time_days, + MIN(a.appointment_date - DATE(a.created_at)) as min_lead_time_days, + MAX(a.appointment_date - DATE(a.created_at)) as max_lead_time_days, + COUNT(CASE WHEN a.appointment_date - DATE(a.created_at) < 1 THEN 1 END) as same_day_appointments, + COUNT(CASE WHEN a.appointment_date - DATE(a.created_at) BETWEEN 1 AND 7 THEN 1 END) as week_ahead_appointments FROM appointment a JOIN users u ON a.user_id = u.user_id - JOIN address_database ad ON a.address_id = ad.address_id - WHERE a.appointment_date BETWEEN ?1 AND ?2 - ORDER BY a.appointment_date, a.appointment_time`, - }, - { - ID: "missed_appointments", - Name: "Missed Appointments", - Description: "Appointments that were scheduled but addresses remain unvisited", - SQL: `SELECT - u.first_name || ' ' || u.last_name as volunteer_name, - a.appointment_date, - a.appointment_time, - ad.address, - CASE - WHEN a.appointment_date < CURRENT_DATE THEN 'Overdue' - ELSE 'Upcoming' - END as status - FROM appointment a - JOIN users u ON a.user_id = u.user_id - JOIN address_database ad ON a.address_id = ad.address_id - WHERE ad.visited_validated = false - AND a.appointment_date BETWEEN ?1 AND ?2 - ORDER BY a.appointment_date DESC`, - }, - { - ID: "peak_hours", - Name: "Peak Activity Hours", - Description: "Most popular appointment times", - SQL: `SELECT - appointment_time, - COUNT(*) as appointment_count, - COUNT(DISTINCT user_id) as unique_volunteers - FROM appointment - WHERE appointment_date BETWEEN ?1 AND ?2 - GROUP BY appointment_time - ORDER BY appointment_count DESC`, + WHERE a.created_at BETWEEN ?1 AND ?2 + GROUP BY u.user_id, u.first_name, u.last_name + HAVING COUNT(*) > 0 + ORDER BY avg_lead_time_days ASC`, }, }, "polls": { { - ID: "poll_creation_stats", - Name: "Poll Creation Statistics", - Description: "Overview of poll creation activity", - SQL: `SELECT - u.first_name || ' ' || u.last_name as creator_name, - COUNT(p.poll_id) as polls_created, - COUNT(CASE WHEN p.is_active = true THEN 1 END) as active_polls, - COALESCE(SUM(p.amount_donated), 0) as total_donations, - COALESCE(AVG(p.amount_donated), 0) as avg_donation_per_poll - FROM poll p - JOIN users u ON p.user_id = u.user_id - WHERE p.created_at BETWEEN ?1 AND ?2 - GROUP BY u.user_id, u.first_name, u.last_name - ORDER BY polls_created DESC`, - }, - { - ID: "donation_analysis", - Name: "Donation Analysis", - Description: "Detailed analysis of donation patterns", + ID: "response_distribution", + Name: "Response Distribution (Yes/No/Neutral)", + Description: "Outcome summary - distribution of poll responses", SQL: `SELECT CASE - WHEN amount_donated = 0 THEN 'No Donation' - WHEN amount_donated BETWEEN 0.01 AND 25 THEN '$1 - $25' - WHEN amount_donated BETWEEN 25.01 AND 50 THEN '$26 - $50' - WHEN amount_donated BETWEEN 50.01 AND 100 THEN '$51 - $100' - ELSE 'Over $100' - END as donation_range, - COUNT(*) as poll_count, - COALESCE(SUM(amount_donated), 0) as total_amount, - ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM poll WHERE created_at BETWEEN ?1 AND ?2), 2) as percentage - FROM poll - WHERE created_at BETWEEN ?1 AND ?2 - GROUP BY donation_range - ORDER BY - CASE donation_range - WHEN 'No Donation' THEN 1 - WHEN '$1 - $25' THEN 2 - WHEN '$26 - $50' THEN 3 - WHEN '$51 - $100' THEN 4 - WHEN 'Over $100' THEN 5 - END`, - }, - { - ID: "active_vs_inactive", - Name: "Active vs Inactive Polls", - Description: "Comparison of active and inactive polls", - SQL: `SELECT - CASE - WHEN is_active = true THEN 'Active' - ELSE 'Inactive' - END as poll_status, - COUNT(*) as poll_count, - COALESCE(SUM(amount_donated), 0) as total_donations, - COALESCE(AVG(amount_donated), 0) as avg_donation - FROM poll - WHERE created_at BETWEEN ?1 AND ?2 - GROUP BY is_active - ORDER BY poll_count DESC`, - }, - { - ID: "poll_trends", - Name: "Poll Activity Trends", - Description: "Poll creation trends over time", - SQL: `SELECT - DATE(created_at) as creation_date, - COUNT(*) as polls_created, - COUNT(CASE WHEN is_active = true THEN 1 END) as active_polls, - COALESCE(SUM(amount_donated), 0) as daily_donations - FROM poll - WHERE created_at BETWEEN ?1 AND ?2 - GROUP BY DATE(created_at) - ORDER BY creation_date DESC`, - }, - { - ID: "creator_performance", - Name: "Creator Performance", - Description: "Performance metrics for poll creators", - SQL: `SELECT - u.first_name || ' ' || u.last_name as creator_name, - u.email, - COUNT(p.poll_id) as total_polls, - COALESCE(SUM(p.amount_donated), 0) as total_raised, - COALESCE(MAX(p.amount_donated), 0) as highest_donation, - COUNT(CASE WHEN p.is_active = true THEN 1 END) as active_polls - FROM users u - JOIN poll p ON u.user_id = p.user_id - WHERE p.created_at BETWEEN ?1 AND ?2 - GROUP BY u.user_id, u.first_name, u.last_name, u.email - ORDER BY total_raised DESC, total_polls DESC`, - }, - }, - "responses": { - { - ID: "voter_status", - Name: "Voter Status Report", - Description: "Analysis of voter status from poll responses", - SQL: `SELECT - voter_before as voted_before, + WHEN question1_voted_before = true AND question2_vote_again = true THEN 'Previous Voter - Will Vote Again' + WHEN question1_voted_before = true AND question2_vote_again = false THEN 'Previous Voter - Will Not Vote Again' + WHEN question1_voted_before = false AND question2_vote_again = true THEN 'New Voter - Will Vote' + WHEN question1_voted_before = false AND question2_vote_again = false THEN 'New Voter - Will Not Vote' + WHEN question1_voted_before IS NULL OR question2_vote_again IS NULL THEN 'Incomplete Response' + END as response_category, COUNT(*) as response_count, - COUNT(CASE WHEN will_vote_again = true THEN 1 END) as will_vote_again_count, - ROUND(COUNT(CASE WHEN will_vote_again = true THEN 1 END) * 100.0 / COUNT(*), 2) as vote_again_percentage + COUNT(*) * 100.0 / (SELECT COUNT(*) FROM poll_response pr2 + JOIN poll p2 ON pr2.poll_id = p2.poll_id + WHERE p2.created_at BETWEEN ?1 AND ?2) as percentage FROM poll_response pr JOIN poll p ON pr.poll_id = p.poll_id WHERE p.created_at BETWEEN ?1 AND ?2 - GROUP BY voter_before + GROUP BY response_category ORDER BY response_count DESC`, }, { - ID: "sign_requests", - Name: "Sign Requests Summary", - Description: "Summary of lawn sign and banner requests", + ID: "average_poll_response", + Name: "Average Poll Response (Yes/No %)", + Description: "Overall sentiment - percentage breakdown of responses", SQL: `SELECT - 'Lawn Signs' as sign_type, - SUM(lawn_sign) as total_requested, - SUM(CASE WHEN lawn_sign_status = 'delivered' THEN lawn_sign ELSE 0 END) as delivered, - SUM(CASE WHEN lawn_sign_status = 'cancelled' THEN lawn_sign ELSE 0 END) as cancelled + 'Previous Voters' as voter_type, + COUNT(*) as total_responses, + COUNT(CASE WHEN question2_vote_again = true THEN 1 END) as positive_responses, + COUNT(CASE WHEN question2_vote_again = false THEN 1 END) as negative_responses, + COUNT(CASE WHEN question2_vote_again = true THEN 1 END) * 100.0 / COUNT(*) as positive_percentage FROM poll_response pr JOIN poll p ON pr.poll_id = p.poll_id - WHERE p.created_at BETWEEN ?1 AND ?2 + WHERE p.created_at BETWEEN ?1 AND ?2 AND question1_voted_before = true UNION ALL SELECT - 'Banner Signs' as sign_type, - SUM(banner_sign) as total_requested, - SUM(CASE WHEN banner_sign_status = 'delivered' THEN banner_sign ELSE 0 END) as delivered, - SUM(CASE WHEN banner_sign_status = 'cancelled' THEN banner_sign ELSE 0 END) as cancelled + 'New Voters' as voter_type, + COUNT(*) as total_responses, + COUNT(CASE WHEN question2_vote_again = true THEN 1 END) as positive_responses, + COUNT(CASE WHEN question2_vote_again = false THEN 1 END) as negative_responses, + COUNT(CASE WHEN question2_vote_again = true THEN 1 END) * 100.0 / COUNT(*) as positive_percentage + FROM poll_response pr + JOIN poll p ON pr.poll_id = p.poll_id + WHERE p.created_at BETWEEN ?1 AND ?2 AND question1_voted_before = false + UNION ALL + SELECT + 'Overall' as voter_type, + COUNT(*) as total_responses, + COUNT(CASE WHEN question2_vote_again = true THEN 1 END) as positive_responses, + COUNT(CASE WHEN question2_vote_again = false THEN 1 END) as negative_responses, + COUNT(CASE WHEN question2_vote_again = true THEN 1 END) * 100.0 / COUNT(*) as positive_percentage FROM poll_response pr JOIN poll p ON pr.poll_id = p.poll_id WHERE p.created_at BETWEEN ?1 AND ?2`, }, { - ID: "feedback_analysis", - Name: "Feedback Analysis", - Description: "Analysis of open-text feedback from responses", + ID: "donations_by_poll", + Name: "Donations by Poll", + Description: "Which polls drive donations - donation amounts per poll", SQL: `SELECT - LENGTH(thoughts) as feedback_length_category, - COUNT(*) as response_count + p.poll_id, + u.first_name || ' ' || u.last_name as creator_name, + ad.address, + pr.question6_donation_amount, + COUNT(pr.poll_response_id) as response_count, + CASE + WHEN COUNT(pr.poll_response_id) > 0 + THEN pr.question6_donation_amount / COUNT(pr.poll_response_id) + ELSE 0 + END as donation_per_response, + p.created_at as poll_date + FROM poll p + JOIN users u ON p.user_id = u.user_id + JOIN address_database ad ON p.address_id = ad.address_id + LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id + WHERE p.created_at BETWEEN ?1 AND ?2 AND pr.question6_donation_amount > 0 + GROUP BY p.poll_id, u.first_name, u.last_name, ad.address, pr.question6_donation_amount, p.created_at + ORDER BY pr.question6_donation_amount DESC, response_count DESC`, + }, + { + ID: "response_donation_correlation", + Name: "Response-to-Donation Correlation", + Description: "Are positive responses linked to donations?", + SQL: `SELECT + CASE + WHEN question2_vote_again = true THEN 'Will Vote Again' + WHEN question2_vote_again = false THEN 'Will Not Vote Again' + ELSE 'No Response' + END as response_type, + COUNT(*) as response_count, + COUNT(CASE WHEN pr.question6_donation_amount > 0 THEN 1 END) as responses_with_donations, + COALESCE(SUM(pr.question6_donation_amount), 0) as total_donations, + AVG(pr.question6_donation_amount) as avg_donation, + COUNT(CASE WHEN pr.question6_donation_amount > 0 THEN 1 END) * 100.0 / COUNT(*) as donation_rate_percent FROM poll_response pr JOIN poll p ON pr.poll_id = p.poll_id WHERE p.created_at BETWEEN ?1 AND ?2 - AND thoughts IS NOT NULL - AND TRIM(thoughts) != '' - GROUP BY - CASE - WHEN LENGTH(thoughts) <= 50 THEN 'Short (1-50 chars)' - WHEN LENGTH(thoughts) <= 150 THEN 'Medium (51-150 chars)' - ELSE 'Long (150+ chars)' - END - ORDER BY response_count DESC`, - }, - { - ID: "response_trends", - Name: "Response Trends", - Description: "Poll response trends over time", - SQL: `SELECT - DATE(pr.created_at) as response_date, - COUNT(*) as responses, - COUNT(CASE WHEN voter_before = true THEN 1 END) as returning_voters, - COUNT(CASE WHEN will_vote_again = true THEN 1 END) as committed_future_voters - FROM poll_response pr - JOIN poll p ON pr.poll_id = p.poll_id - WHERE pr.created_at BETWEEN ?1 AND ?2 - GROUP BY DATE(pr.created_at) - ORDER BY response_date DESC`, - }, - { - ID: "repeat_voters", - Name: "Repeat Voters Analysis", - Description: "Analysis of voters who have responded to multiple polls", - SQL: `SELECT - pr.name, - pr.email, - COUNT(DISTINCT pr.poll_id) as polls_responded, - SUM(CASE WHEN voter_before = true THEN 1 ELSE 0 END) as times_voted_before, - SUM(CASE WHEN will_vote_again = true THEN 1 ELSE 0 END) as times_will_vote_again - FROM poll_response pr - JOIN poll p ON pr.poll_id = p.poll_id - WHERE p.created_at BETWEEN ?1 AND ?2 - GROUP BY pr.name, pr.email - HAVING COUNT(DISTINCT pr.poll_id) > 1 - ORDER BY polls_responded DESC`, - }, - }, - "posts": { - { - ID: "posts_by_user", - Name: "Posts by User", - Description: "Post creation statistics by user", - SQL: `SELECT - u.first_name || ' ' || u.last_name as author_name, - u.email, - COUNT(po.post_id) as total_posts, - MIN(po.created_at) as first_post, - MAX(po.created_at) as latest_post - FROM users u - JOIN posts po ON u.user_id = po.user_id - WHERE po.created_at BETWEEN ?1 AND ?2 - GROUP BY u.user_id, u.first_name, u.last_name, u.email - ORDER BY total_posts DESC`, - }, - { - ID: "engagement_timeline", - Name: "Engagement Timeline", - Description: "Post creation timeline", - SQL: `SELECT - DATE(created_at) as post_date, - COUNT(*) as posts_created, - COUNT(DISTINCT user_id) as active_users - FROM posts - WHERE created_at BETWEEN ?1 AND ?2 - GROUP BY DATE(created_at) - ORDER BY post_date DESC`, - }, - { - ID: "content_analysis", - Name: "Content Analysis", - Description: "Analysis of post content length and characteristics", - SQL: `SELECT - CASE - WHEN LENGTH(content) <= 100 THEN 'Short (1-100 chars)' - WHEN LENGTH(content) <= 300 THEN 'Medium (101-300 chars)' - ELSE 'Long (300+ chars)' - END as content_length, - COUNT(*) as post_count, - ROUND(AVG(LENGTH(content)), 2) as avg_length - FROM posts - WHERE created_at BETWEEN ?1 AND ?2 - AND content IS NOT NULL - GROUP BY content_length - ORDER BY post_count DESC`, - }, - { - ID: "post_frequency", - Name: "Post Frequency Report", - Description: "Posting frequency patterns", - SQL: `SELECT - u.first_name || ' ' || u.last_name as author_name, - COUNT(*) as total_posts, - ROUND(COUNT(*) * 1.0 / GREATEST(1, EXTRACT(days FROM (?2::date - ?1::date))), 2) as posts_per_day, - DATE(MIN(po.created_at)) as first_post, - DATE(MAX(po.created_at)) as last_post - FROM posts po - JOIN users u ON po.user_id = u.user_id - WHERE po.created_at BETWEEN ?1 AND ?2 - GROUP BY u.user_id, u.first_name, u.last_name - HAVING COUNT(*) > 1 - ORDER BY posts_per_day DESC`, + GROUP BY question2_vote_again + ORDER BY total_donations DESC`, }, }, "availability": { { - ID: "volunteer_availability", - Name: "Volunteer Availability", - Description: "Current volunteer availability schedules", + ID: "volunteer_availability_schedule", + Name: "Volunteer Availability by Date Range", + Description: "Who can work when - current volunteer availability schedules", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, + u.email, av.day_of_week, av.start_time, av.end_time, + EXTRACT(EPOCH FROM (av.end_time - av.start_time))/3600 as hours_available, av.created_at as schedule_updated - FROM volunteer_availability av + FROM availability av JOIN users u ON av.user_id = u.user_id - WHERE av.created_at BETWEEN ?1 AND ?2 - ORDER BY u.first_name, u.last_name, av.day_of_week, av.start_time`, + WHERE u.role_id IN (2, 3) AND av.created_at BETWEEN ?1 AND ?2 + ORDER BY u.first_name, u.last_name, + CASE av.day_of_week + WHEN 'Monday' THEN 1 + WHEN 'Tuesday' THEN 2 + WHEN 'Wednesday' THEN 3 + WHEN 'Thursday' THEN 4 + WHEN 'Friday' THEN 5 + WHEN 'Saturday' THEN 6 + WHEN 'Sunday' THEN 7 + END, av.start_time`, }, { - ID: "peak_availability", - Name: "Peak Availability Times", - Description: "Times when most volunteers are available", - SQL: `SELECT - day_of_week, - start_time, - end_time, - COUNT(*) as volunteers_available - FROM volunteer_availability av - JOIN users u ON av.user_id = u.user_id - WHERE av.created_at BETWEEN ?1 AND ?2 - GROUP BY day_of_week, start_time, end_time - ORDER BY volunteers_available DESC, day_of_week, start_time`, - }, - { - ID: "coverage_gaps", - Name: "Coverage Gaps", - Description: "Time periods with limited volunteer availability", - SQL: `SELECT - day_of_week, - start_time, - end_time, - COUNT(*) as volunteers_available - FROM volunteer_availability av - WHERE av.created_at BETWEEN ?1 AND ?2 - GROUP BY day_of_week, start_time, end_time - HAVING COUNT(*) <= 2 - ORDER BY volunteers_available ASC, day_of_week, start_time`, - }, - { - ID: "schedule_conflicts", - Name: "Schedule Conflicts", - Description: "Appointments scheduled outside volunteer availability", + ID: "volunteer_fulfillment", + Name: "Volunteer Fulfillment (Available vs Actually Worked)", + Description: "Reliability measure - comparing availability to actual appointments", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, - a.appointment_date, - a.appointment_time, - ad.address, - 'No availability recorded' as conflict_reason - FROM appointment a - JOIN users u ON a.user_id = u.user_id - JOIN address_database ad ON a.address_id = ad.address_id - LEFT JOIN volunteer_availability av ON u.user_id = av.user_id - AND EXTRACT(dow FROM a.appointment_date) = av.day_of_week - AND a.appointment_time BETWEEN av.start_time AND av.end_time - WHERE a.appointment_date BETWEEN ?1 AND ?2 - AND av.user_id IS NULL - ORDER BY a.appointment_date, a.appointment_time`, + COUNT(DISTINCT av.availability_id) as availability_slots, + SUM(EXTRACT(EPOCH FROM (av.end_time - av.start_time))/3600) as total_hours_available, + COUNT(DISTINCT a.sched_id) as appointments_scheduled, + COUNT(DISTINCT CASE WHEN a.appointment_date < CURRENT_DATE THEN a.sched_id END) as past_appointments, + COUNT(DISTINCT CASE WHEN a.appointment_date < CURRENT_DATE AND EXISTS ( + SELECT 1 FROM poll p WHERE p.user_id = a.user_id AND p.address_id = a.address_id + AND DATE(p.created_at) = a.appointment_date + ) THEN a.sched_id END) as completed_appointments, + CASE + WHEN COUNT(DISTINCT CASE WHEN a.appointment_date < CURRENT_DATE THEN a.sched_id END) > 0 + THEN COUNT(DISTINCT CASE WHEN a.appointment_date < CURRENT_DATE AND EXISTS ( + SELECT 1 FROM poll p WHERE p.user_id = a.user_id AND p.address_id = a.address_id + AND DATE(p.created_at) = a.appointment_date + ) THEN a.sched_id END) * 100.0 / COUNT(DISTINCT CASE WHEN a.appointment_date < CURRENT_DATE THEN a.sched_id END) + ELSE 0 + END as fulfillment_rate_percent + FROM users u + LEFT JOIN availability av ON u.user_id = av.user_id AND av.created_at BETWEEN ?1 AND ?2 + LEFT JOIN appointment a ON u.user_id = a.user_id AND a.created_at BETWEEN ?1 AND ?2 + WHERE u.role_id IN (2, 3) + GROUP BY u.user_id, u.first_name, u.last_name + HAVING COUNT(DISTINCT av.availability_id) > 0 OR COUNT(DISTINCT a.sched_id) > 0 + ORDER BY fulfillment_rate_percent DESC, total_hours_available DESC`, }, }, } diff --git a/app/internal/handlers/volunteer_schedual.go b/app/internal/handlers/volunteer_schedual.go index 6621006..6deb0b6 100644 --- a/app/internal/handlers/volunteer_schedual.go +++ b/app/internal/handlers/volunteer_schedual.go @@ -2,10 +2,33 @@ package handlers import ( "fmt" - "net/http" + "time" + + "github.com/patel-mann/poll-system/app/internal/models" ) -func VolunteerSchedualHandler(w *http.ResponseWriter, r http.Request) { +func ValidatedFreeTime(parsedDate time.Time, assignTime time.Time, userID int) (bool) { + var startTime, endTime time.Time - fmt.Print("Not Implementated Yet!!!") + dateOnly := parsedDate.Format("2006-01-02") + + err := models.DB.QueryRow( + `SELECT start_time, end_time + FROM availability + WHERE user_id = $1 AND day = $2`, + userID, dateOnly, + ).Scan(&startTime, &endTime) + + if err != nil { + fmt.Printf("Database query failed: %v\n", err) + return false + } + + if assignTime.After(startTime) && assignTime.Before(endTime) { + return true + }else{ + return false + } + + return false } diff --git a/app/internal/templates/address.html b/app/internal/templates/address.html index 0ae5933..fe68045 100644 --- a/app/internal/templates/address.html +++ b/app/internal/templates/address.html @@ -18,10 +18,10 @@
- +
- +