commit 23f6b359cab28779c9ebade7386bd4f2c4f75107 Author: Mann Patel <130435633+Patel-Mann@users.noreply.github.com> Date: Tue Aug 26 14:13:09 2025 -0600 Initial commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/.DS_Store differ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b09cd78 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..5db9ae3 --- /dev/null +++ b/README.MD @@ -0,0 +1,2 @@ +# Poll-system + diff --git a/app/.DS_Store b/app/.DS_Store new file mode 100644 index 0000000..6d70eae Binary files /dev/null and b/app/.DS_Store differ diff --git a/app/.env b/app/.env new file mode 100644 index 0000000..2a92f76 --- /dev/null +++ b/app/.env @@ -0,0 +1,9 @@ +DB_USER=mannpatel +DB_HOST=localhost +DB_NAME=poll_database +DB_PASSWORD=admin +DB_PORT=5432 +PORT=3000 +JWT_SECRET=r683gi77ft92fg923keyfasdfas2r123 +ALLOWED_ORIGINS=http://localhost:5173 +NODE_ENV=development diff --git a/app/go.mod b/app/go.mod new file mode 100644 index 0000000..f0122ff --- /dev/null +++ b/app/go.mod @@ -0,0 +1,9 @@ +module github.com/patel-mann/poll-system/app + +go 1.24.4 + +require ( + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/lib/pq v1.10.9 + golang.org/x/crypto v0.41.0 +) diff --git a/app/go.sum b/app/go.sum new file mode 100644 index 0000000..0444594 --- /dev/null +++ b/app/go.sum @@ -0,0 +1,6 @@ +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= diff --git a/app/internal/.DS_Store b/app/internal/.DS_Store new file mode 100644 index 0000000..f30de6c Binary files /dev/null and b/app/internal/.DS_Store differ diff --git a/app/internal/handlers/admin.go b/app/internal/handlers/admin.go new file mode 100644 index 0000000..8f05e3e --- /dev/null +++ b/app/internal/handlers/admin.go @@ -0,0 +1,402 @@ +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 +} diff --git a/app/internal/handlers/admin_addresses.go b/app/internal/handlers/admin_addresses.go new file mode 100644 index 0000000..c53c708 --- /dev/null +++ b/app/internal/handlers/admin_addresses.go @@ -0,0 +1,184 @@ +package handlers + +import ( + "log" + "net/http" + "strconv" + + "github.com/patel-mann/poll-system/app/internal/models" + "github.com/patel-mann/poll-system/app/internal/utils" +) + +// PaginationInfo holds pagination metadata +type PaginationInfo struct { + CurrentPage int + TotalPages int + TotalRecords int + PageSize int + HasPrevious bool + HasNext bool + StartRecord int + EndRecord int + PreviousPage int + NextPage int + FirstPage int + LastPage int + PageNumbers []PageNumber +} + +type PageNumber struct { + Number int + IsCurrent bool +} + +func AddressHandler(w http.ResponseWriter, r *http.Request) { + // Get pagination parameters from query string + pageStr := r.URL.Query().Get("page") + pageSizeStr := r.URL.Query().Get("pageSize") + + // Default values + page := 1 + pageSize := 20 // Default page size + + // Parse page number + if pageStr != "" { + if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { + page = p + } + } + + // Parse page size + if pageSizeStr != "" { + if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 && ps <= 100 { + pageSize = ps + } + } + + // Calculate offset + offset := (page - 1) * pageSize + + // Get total count first + var totalRecords int + err := models.DB.QueryRow(`SELECT COUNT(*) FROM "address_database"`).Scan(&totalRecords) + if err != nil { + log.Println("Count query error:", err) + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + + // Calculate pagination info + totalPages := (totalRecords + pageSize - 1) / pageSize + if totalPages == 0 { + totalPages = 1 + } + + // Ensure current page is within bounds + if page > totalPages { + page = totalPages + offset = (page - 1) * pageSize + } + + // Get paginated results + 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" + WHERE street_quadrant = 'ne' + ORDER BY address_id + LIMIT $1 OFFSET $2 + `, pageSize, offset) + if err != nil { + log.Println("Query error:", err) + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var addresses []models.AddressDatabase + for rows.Next() { + var a models.AddressDatabase + err := rows.Scan( + &a.AddressID, + &a.Address, + &a.StreetName, + &a.StreetType, + &a.StreetQuadrant, + &a.HouseNumber, + &a.HouseAlpha, + &a.Longitude, + &a.Latitude, + &a.VisitedValidated, + ) + if err != nil { + log.Println("Scan error:", err) + continue + } + addresses = append(addresses, a) + } + + // Calculate start and end record numbers for display + startRecord := offset + 1 + endRecord := offset + len(addresses) + if totalRecords == 0 { + startRecord = 0 + } + + // Generate page numbers for pagination controls + pageNumbers := generatePageNumbers(page, totalPages) + + pagination := PaginationInfo{ + CurrentPage: page, + TotalPages: totalPages, + TotalRecords: totalRecords, + PageSize: pageSize, + HasPrevious: page > 1, + HasNext: page < totalPages, + StartRecord: startRecord, + EndRecord: endRecord, + PreviousPage: page - 1, + NextPage: page + 1, + FirstPage: 1, + LastPage: totalPages, + PageNumbers: pageNumbers, + } + + utils.Render(w, "address/address.html", map[string]interface{}{ + "Title": "Addresses", + "IsAuthenticated": true, + "ShowAdminNav": true, + "ActiveSection": "address", // Add this line + "Addresses": addresses, + "Role": "admin", + "Pagination": pagination, + }) +} + +func generatePageNumbers(currentPage, totalPages int) []PageNumber { + var pageNumbers []PageNumber + + // Generate page numbers to show (max 7 pages) + start := currentPage - 3 + end := currentPage + 3 + + if start < 1 { + end += 1 - start + start = 1 + } + if end > totalPages { + start -= end - totalPages + end = totalPages + } + if start < 1 { + start = 1 + } + + for i := start; i <= end; i++ { + pageNumbers = append(pageNumbers, PageNumber{ + Number: i, + IsCurrent: i == currentPage, + }) + } + + return pageNumbers +} diff --git a/app/internal/handlers/admin_dashboard.go b/app/internal/handlers/admin_dashboard.go new file mode 100644 index 0000000..fd10f5e --- /dev/null +++ b/app/internal/handlers/admin_dashboard.go @@ -0,0 +1,81 @@ +package handlers + +import ( + "log" + "net/http" + + "github.com/patel-mann/poll-system/app/internal/models" + "github.com/patel-mann/poll-system/app/internal/utils" +) + + +func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) { + currentAdminID := r.Context().Value("user_id").(int) + + role, _ := r.Context().Value("uesr_role").(int) + + var volunteerCount int + var totalDonations float64 + var validatedCount int + var housesLeftPercent float64 + + // 1. Count volunteers assigned to this admin + err := models.DB.QueryRow(` + SELECT COUNT(av.volunteer_id) + FROM admin_volunteers av + WHERE av.admin_id = $1 AND av.is_active = TRUE; + `, currentAdminID).Scan(&volunteerCount) + if err != nil { + log.Println("Volunteer query error:", err) + volunteerCount = 0 // Set default value on error + } + + // 2. Total donations from polls + err = models.DB.QueryRow(` + SELECT COALESCE(SUM(amount_donated), 0) + FROM poll; + `).Scan(&totalDonations) + if err != nil { + log.Println("Donation query error:", err) + totalDonations = 0 // Set default value on error + } + + // 3. Count validated addresses + err = models.DB.QueryRow(` + SELECT COUNT(*) + FROM address_database + WHERE visited_validated = TRUE; + `).Scan(&validatedCount) + if err != nil { + log.Println("Validated addresses query error:", err) + validatedCount = 0 // Set default value on error + } + + // 4. Calculate percentage of houses left to visit + err = models.DB.QueryRow(` + SELECT + CASE + WHEN COUNT(*) = 0 THEN 0 + ELSE ROUND( + (COUNT(*) FILTER (WHERE visited_validated = FALSE)::numeric / COUNT(*)::numeric) * 100, 2 + ) + END + FROM address_database; + `).Scan(&housesLeftPercent) + if err != nil { + log.Println("Houses left query error:", err) + housesLeftPercent = 0 // Set default value on error + } + + utils.Render(w, "dashboard/dashboard.html", map[string]interface{}{ + "Title": "Admin Dashboard", + "IsAuthenticated": true, + "VolunteerCount": volunteerCount, + "TotalDonations": totalDonations, + "ValidatedCount": validatedCount, + "HousesLeftPercent": housesLeftPercent, + "ShowAdminNav": true, + "Role": role, + "ActiveSection": "dashboard", + }) +} \ No newline at end of file diff --git a/app/internal/handlers/admin_post.go b/app/internal/handlers/admin_post.go new file mode 100644 index 0000000..9cbd632 --- /dev/null +++ b/app/internal/handlers/admin_post.go @@ -0,0 +1,160 @@ +// Updated admin_post.go with better image handling + +package handlers + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/patel-mann/poll-system/app/internal/models" + "github.com/patel-mann/poll-system/app/internal/utils" +) + +func PostsHandler(w http.ResponseWriter, r *http.Request) { + userID := r.Context().Value("user_id").(int) + role := r.Context().Value("user_role").(int) + + if r.Method == http.MethodPost { + // Parse multipart form + err := r.ParseMultipartForm(10 << 20) // 10MB max + if err != nil { + fmt.Printf("Error parsing form: %v\n", err) + http.Error(w, "Invalid form", http.StatusBadRequest) + return + } + + content := r.FormValue("content") + if strings.TrimSpace(content) == "" { + http.Error(w, "Content cannot be empty", http.StatusBadRequest) + return + } + + var imagePath string + file, handler, err := r.FormFile("image") + if err == nil && file != nil { + defer file.Close() + + // Validate file type + allowedTypes := map[string]bool{ + ".jpg": true, + ".jpeg": true, + ".png": true, + ".gif": true, + ".webp": true, + } + + ext := strings.ToLower(filepath.Ext(handler.Filename)) + if !allowedTypes[ext] { + http.Error(w, "Invalid file type. Only images allowed.", http.StatusBadRequest) + return + } + + // Ensure uploads folder exists + uploadDir := "uploads" + if err := os.MkdirAll(uploadDir, 0755); err != nil { + fmt.Printf("Error creating upload directory: %v\n", err) + http.Error(w, "Unable to create upload directory", http.StatusInternalServerError) + return + } + + // Create unique filename + filename := fmt.Sprintf("%d_%d%s", userID, time.Now().UnixNano(), ext) + savePath := filepath.Join(uploadDir, filename) + + out, err := os.Create(savePath) + if err != nil { + fmt.Printf("Error creating file: %v\n", err) + http.Error(w, "Unable to save file", http.StatusInternalServerError) + return + } + defer out.Close() + + _, err = io.Copy(out, file) + if err != nil { + fmt.Printf("Error copying file: %v\n", err) + http.Error(w, "Failed to save file", http.StatusInternalServerError) + return + } + + // Save path relative to the static route + imagePath = "/uploads/" + filename + fmt.Printf("Image saved at: %s\n", imagePath) + } else if err != http.ErrMissingFile { + fmt.Printf("Error getting file: %v\n", err) + } + + // Insert post + _, err = models.DB.Exec(`INSERT INTO post (author_id, content, image_url) VALUES ($1, $2, $3)`, + userID, content, imagePath) + if err != nil { + fmt.Printf("Database error: %v\n", err) + http.Error(w, "Failed to create post", http.StatusInternalServerError) + return + } + + fmt.Printf("Post created successfully with image: %s\n", imagePath) + http.Redirect(w, r, "/posts", http.StatusSeeOther) + return + } + + // GET request: fetch posts + rows, err := models.DB.Query(` + SELECT p.post_id, p.author_id, u.first_name || ' ' || u.last_name AS author_name, + p.content, COALESCE(p.image_url, '') as image_url, p.created_at + FROM post p + JOIN users u ON p.author_id = u.user_id + ORDER BY p.created_at DESC + `) + if err != nil { + fmt.Printf("Database query error: %v\n", err) + http.Error(w, "Failed to fetch posts", http.StatusInternalServerError) + return + } + defer rows.Close() + + var posts []models.Post + for rows.Next() { + var p models.Post + err := rows.Scan(&p.PostID, &p.AuthorID, &p.AuthorName, &p.Content, &p.ImageURL, &p.CreatedAt) + if err != nil { + fmt.Printf("Row scan error: %v\n", err) + continue + } + posts = append(posts, p) + } + + // Add cache busting parameter to image URLs + for i := range posts { + if posts[i].ImageURL != "" { + posts[i].ImageURL += "?t=" + strconv.FormatInt(time.Now().UnixNano(), 10) + fmt.Printf("Post %d image URL: %s\n", posts[i].PostID, posts[i].ImageURL) + } + } + + // Get navigation flags + showAdminNav, showVolunteerNav := getNavFlags(role) + + fmt.Printf("Rendering %d posts\n", len(posts)) + + utils.Render(w, "posts.html", map[string]interface{}{ + "Title": "Posts", + "IsAuthenticated": true, + "ShowAdminNav": showAdminNav, + "ShowVolunteerNav": showVolunteerNav, + "Posts": posts, + "ActiveSection": "posts", + }) +} + +// Helper function (add this to your main.go if not already there) +func getNavFlags(role int) (bool, bool) { + showAdminNav := role == 1 // Admin role + showVolunteerNav := role == 3 // Volunteer role + return showAdminNav, showVolunteerNav +} \ No newline at end of file diff --git a/app/internal/handlers/admin_voluteers.go b/app/internal/handlers/admin_voluteers.go new file mode 100644 index 0000000..8e370f1 --- /dev/null +++ b/app/internal/handlers/admin_voluteers.go @@ -0,0 +1,219 @@ +package handlers + +import ( + "fmt" + "log" + "net/http" + "strconv" + + "github.com/patel-mann/poll-system/app/internal/models" + "github.com/patel-mann/poll-system/app/internal/utils" +) + + +func VolunteerHandler(w http.ResponseWriter, r *http.Request) { + // TODO: Replace this with actual session/jwt extraction + currentAdminID := r.Context().Value("user_id").(int) + + rows, err := models.DB.Query(` + SELECT u.user_id, u.email, u.role_id, u.first_name, u.last_name, u.phone + FROM "users" u + JOIN admin_volunteers av ON u.user_id = av.volunteer_id + WHERE av.admin_id = $1 AND ( u.role_id = 3 OR u.role_id = 2 ) + `, currentAdminID) + if err != nil { + http.Error(w, "Query error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var user []models.User + for rows.Next() { + var b models.User + err := rows.Scan(&b.UserID, &b.Email, &b.RoleID, &b.FirstName, &b.LastName, &b.Phone) + if err != nil { + log.Println("Scan error:", err) + continue + } + user = append(user, b) + } + + utils.Render(w, "volunteer/volunteer.html", map[string]interface{}{ + "Title": "Assigned Volunteers", + "IsAuthenticated": true, + "ShowAdminNav": true, + "Users": user, + "ActiveSection": "volunteer", + }) +} + +func EditVolunteerHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + volunteerID := r.URL.Query().Get("id") + var user models.User + err := models.DB.QueryRow(` + SELECT user_id, email, role_id, first_name, last_name, phone + FROM "users" + WHERE user_id = $1 AND (role_id = 3 OR role_id = 2) + `, volunteerID).Scan(&user.UserID, &user.Email, &user.RoleID, &user.FirstName, &user.LastName, &user.Phone) + + if err != nil { + http.Error(w, "Volunteer not found", http.StatusNotFound) + return + } + + utils.Render(w, "volunteer/edit_volunteer.html", map[string]interface{}{ + "Title": "Edit Volunteer", + "IsAuthenticated": true, + "ShowAdminNav": true, + "Volunteer": user, + "ActiveSection": "volunteer", + }) + return + } + + if r.Method == http.MethodPost { + err := r.ParseForm() + if err != nil { + http.Error(w, "Invalid form", http.StatusBadRequest) + return + } + + volunteerID := r.FormValue("user_id") + firstName := r.FormValue("first_name") + lastName := r.FormValue("last_name") + email := r.FormValue("email") + phone := r.FormValue("phone") + roleID := r.FormValue("role_id") + + rid, err := strconv.Atoi(roleID) + if err != nil || (rid != 2 && rid != 3) { + http.Error(w, "Invalid role selection", http.StatusBadRequest) + return + } + + _, err = models.DB.Exec(` + UPDATE "users" + SET first_name = $1, last_name = $2, email = $3, phone = $4, role_id = $5 + WHERE user_id = $6 + `, firstName, lastName, email, phone, rid, volunteerID) + + if err != nil { + fmt.Print(err) + http.Error(w, "Update failed", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/volunteers", http.StatusSeeOther) + } +} + +type User struct { + ID int + Name string +} + +type TeamLead struct { + ID int + Name string + Volunteers []User +} + +type TeamBuilderData struct { + TeamLeads []TeamLead + UnassignedVolunteers []User +} + + + +func TeamBuilderHandler(w http.ResponseWriter, r *http.Request) { + // GET request: show team leads and unassigned volunteers + if r.Method == http.MethodGet { + var teamLeads []TeamLead + var unassignedVolunteers []User + + // Get all team leads (role_id = 2) + tlRows, err := models.DB.Query(`SELECT user_id, first_name || ' ' || last_name AS name FROM users WHERE role_id = 2`) + if err != nil { + http.Error(w, "Error fetching team leads", http.StatusInternalServerError) + return + } + defer tlRows.Close() + for tlRows.Next() { + var tl TeamLead + tlRows.Scan(&tl.ID, &tl.Name) + + // Get assigned volunteers for this team lead + vRows, _ := models.DB.Query(`SELECT u.user_id, u.first_name || ' ' || u.last_name AS name + FROM users u + JOIN team t ON u.user_id = t.volunteer_id + WHERE t.team_lead_id = $1`, tl.ID) + + for vRows.Next() { + var vol User + vRows.Scan(&vol.ID, &vol.Name) + tl.Volunteers = append(tl.Volunteers, vol) + } + + teamLeads = append(teamLeads, tl) + } + + // Get unassigned volunteers (role_id = 3) + vRows, _ := models.DB.Query(`SELECT user_id, first_name || ' ' || last_name AS name + FROM users + WHERE role_id = 3 + AND user_id NOT IN (SELECT volunteer_id FROM team)`) + for vRows.Next() { + var vol User + vRows.Scan(&vol.ID, &vol.Name) + unassignedVolunteers = append(unassignedVolunteers, vol) + } + + utils.Render(w, "volunteer/team_builder.html", map[string]interface{}{ + "Title": "Team Builder", + "IsAuthenticated": true, + "ShowAdminNav": true, + "TeamLeads": teamLeads, + "UnassignedVolunteers": unassignedVolunteers, + "ActiveSection": "team_builder", + }) + return + } + + // POST request: assign volunteer to a team lead + if r.Method == http.MethodPost { + if err := r.ParseForm(); err != nil { + http.Error(w, "Invalid form", http.StatusBadRequest) + return + } + + volunteerID, err := strconv.Atoi(r.FormValue("volunteer_id")) + if err != nil { + http.Error(w, "Invalid volunteer ID", http.StatusBadRequest) + return + } + teamLeadID, err := strconv.Atoi(r.FormValue("team_lead_id")) + if err != nil { + http.Error(w, "Invalid team lead ID", http.StatusBadRequest) + return + } + + _, err = models.DB.Exec(`INSERT INTO team (volunteer_id, team_lead_id) VALUES ($1, $2)`, volunteerID, teamLeadID) + if err != nil { + fmt.Println(err) + http.Error(w, "Failed to assign volunteer", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/team_builder", http.StatusSeeOther) + } +} + + + + + +//assign volunterr the title of team_leader +//Team View +//edit volnteer data +// diff --git a/app/internal/handlers/login.go b/app/internal/handlers/login.go new file mode 100644 index 0000000..5fcef08 --- /dev/null +++ b/app/internal/handlers/login.go @@ -0,0 +1,367 @@ +package handlers + +import ( + "context" + "log" + "net/http" + "time" + + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" + + "github.com/patel-mann/poll-system/app/internal/models" + "github.com/patel-mann/poll-system/app/internal/utils" +) + +var jwtKey = []byte("your-secret-key") //TODO: Move to env/config + +// Helper function to get redirect URL based on role +func getDefaultRedirectURL(role int) string { + switch role { + case 1: // Admin + return "/dashboard" + case 2: // Volunteer + return "/volunteer/dashboard" + case 3: // Volunteer + return "/volunteer/dashboard" + default: + return "/" // Fallback to login page + } +} + +// Helper function to render error pages with consistent data +func renderLoginError(w http.ResponseWriter, errorMsg string) { + utils.Render(w, "login.html", map[string]interface{}{ + "Error": errorMsg, + "Title": "Login", + "IsAuthenticated": false, + }) +} + +func renderRegisterError(w http.ResponseWriter, errorMsg string) { + utils.Render(w, "register.html", map[string]interface{}{ + "Error": errorMsg, + "Title": "Register", + "IsAuthenticated": false, + }) +} + +// Helper function to create and sign JWT token +func createJWTToken(userID, role int) (string, time.Time, error) { + expirationTime := time.Now().Add(12 * time.Hour) + claims := &models.Claims{ + UserID: userID, + Role: role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expirationTime), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString(jwtKey) + return tokenString, expirationTime, err +} + +// Helper function to set session cookie +func setSessionCookie(w http.ResponseWriter, tokenString string, expirationTime time.Time) { + http.SetCookie(w, &http.Cookie{ + Name: "session", + Value: tokenString, + Path: "/", + HttpOnly: true, + Secure: false, // Set to true in production with HTTPS + SameSite: http.SameSiteStrictMode, + Expires: expirationTime, + }) +} + +// Helper function to clear session cookie +func clearSessionCookie(w http.ResponseWriter) { + http.SetCookie(w, &http.Cookie{ + Name: "session", + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: true, + Secure: false, // Set to true in production with HTTPS + SameSite: http.SameSiteStrictMode, + }) +} + +// func LoginPage(w http.ResponseWriter, r *http.Request) { +// utils.Render(w, "login.html", map[string]interface{}{ +// "Title": "Login", +// "IsAuthenticated": false, +// }) +// } + +func LoginHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + email := r.FormValue("email") + password := r.FormValue("password") + + // Input validation + if email == "" || password == "" { + renderLoginError(w, "Email and password are required") + return + } + + // Get user from database + var storedHash string + var userID int + var role int + + err := models.DB.QueryRow(` + SELECT user_id, password, role_id + FROM "users" + WHERE email = $1 + `, email).Scan(&userID, &storedHash, &role) + + if err != nil { + log.Printf("Login failed for email %s: %v", email, err) + renderLoginError(w, "Invalid email or password") + return + } + + // Verify password + err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) + if err != nil { + log.Printf("Password verification failed for user ID %d", userID) + renderLoginError(w, "Invalid email or password") + return + } + + // Create JWT token + tokenString, expirationTime, err := createJWTToken(userID, role) + if err != nil { + log.Printf("JWT token creation failed for user ID %d: %v", userID, err) + http.Error(w, "Could not log in", http.StatusInternalServerError) + return + } + + // Set session cookie + setSessionCookie(w, tokenString, expirationTime) + + // Redirect based on user role + redirectURL := getDefaultRedirectURL(role) + log.Printf("User %d (role %d) logged in successfully, redirecting to %s", userID, role, redirectURL) + http.Redirect(w, r, redirectURL, http.StatusSeeOther) +} + +func RegisterHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + utils.Render(w, "register.html", map[string]interface{}{ + "Title": "Register", + "IsAuthenticated": false, + }) + return + } + + firstName := r.FormValue("first_name") + lastName := r.FormValue("last_name") + email := r.FormValue("email") + phone := r.FormValue("phone") + role := r.FormValue("role") + password := r.FormValue("password") + + // Input validation + if firstName == "" || lastName == "" || email == "" || password == "" || role == "" { + renderRegisterError(w, "All fields are required") + return + } + + // Hash password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + log.Printf("Password hashing failed: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + // Insert user into database + _, err = models.DB.Exec(` + INSERT INTO "users" (first_name, last_name, email, phone, password, role_id) + VALUES ($1, $2, $3, $4, $5, $6) + `, firstName, lastName, email, phone, string(hashedPassword), role) + + if err != nil { + log.Printf("User registration failed for email %s: %v", email, err) + renderRegisterError(w, "Could not create account. Email might already be in use.") + return + } + + log.Printf("User registered successfully: %s %s (%s)", firstName, lastName, email) + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +func LogoutHandler(w http.ResponseWriter, r *http.Request) { + clearSessionCookie(w) + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +// // Admin Dashboard Handler +// func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) { +// role := r.Context().Value("user_role").(int) +// userID := r.Context().Value("user_id").(int) + +// // TODO: Fetch real data from database +// dashboardData := map[string]interface{}{ +// "UserID": userID, +// "TotalUsers": 100, // Example: get from database +// "TotalVolunteers": 50, // Example: get from database +// "TotalAddresses": 200, // Example: get from database +// "RecentActivity": []string{"User logged in", "New volunteer registered"}, // Example +// } + +// data := createTemplateData("Admin Dashboard", "dashboard", role, true, dashboardData) +// utils.Render(w, "dashboard/dashboard.html", data) +// } + +// // Volunteer Management Handler +// func VolunteerHandler(w http.ResponseWriter, r *http.Request) { +// role := r.Context().Value("user_role").(int) + +// // TODO: Fetch real volunteer data from database +// volunteerData := map[string]interface{}{ +// "Volunteers": []map[string]interface{}{ +// {"ID": 1, "Name": "John Doe", "Email": "john@example.com", "Status": "Active"}, +// {"ID": 2, "Name": "Jane Smith", "Email": "jane@example.com", "Status": "Active"}, +// }, // Example: get from database +// } + +// data := createTemplateData("Volunteers", "volunteer", role, true, volunteerData) +// utils.Render(w, "volunteers/volunteers.html", data) +// } + +// // Address Management Handler +// func AddressHandler(w http.ResponseWriter, r *http.Request) { +// role := r.Context().Value("user_role").(int) + +// // TODO: Fetch real address data from database +// addressData := map[string]interface{}{ +// "Addresses": []map[string]interface{}{ +// {"ID": 1, "Street": "123 Main St", "City": "Calgary", "Status": "Validated"}, +// {"ID": 2, "Street": "456 Oak Ave", "City": "Calgary", "Status": "Pending"}, +// }, // Example: get from database +// } + +// data := createTemplateData("Addresses", "address", role, true, addressData) +// utils.Render(w, "addresses/addresses.html", data) +// } + +// // Reports Handler +// func ReportHandler(w http.ResponseWriter, r *http.Request) { +// role := r.Context().Value("user_role").(int) + +// // TODO: Fetch real report data from database +// reportData := map[string]interface{}{ +// "Reports": []map[string]interface{}{ +// {"ID": 1, "Name": "Weekly Summary", "Date": "2025-08-25", "Status": "Complete"}, +// {"ID": 2, "Name": "Monthly Analytics", "Date": "2025-08-01", "Status": "Pending"}, +// }, // Example: get from database +// } + +// data := createTemplateData("Reports", "report", role, true, reportData) +// utils.Render(w, "reports/reports.html", data) +// } + +// // Profile Handler (works for both admin and volunteer) +// func ProfileHandler(w http.ResponseWriter, r *http.Request) { +// role := r.Context().Value("user_role").(int) +// userID := r.Context().Value("user_id").(int) + +// // Fetch real user data from database +// var firstName, lastName, email, phone string +// err := models.DB.QueryRow(` +// SELECT first_name, last_name, email, phone +// FROM "users" +// WHERE user_id = $1 +// `, userID).Scan(&firstName, &lastName, &email, &phone) + +// profileData := map[string]interface{}{ +// "UserID": userID, +// } + +// if err != nil { +// log.Printf("Error fetching user profile for ID %d: %v", userID, err) +// profileData["Error"] = "Could not load profile data" +// } else { +// profileData["FirstName"] = firstName +// profileData["LastName"] = lastName +// profileData["Email"] = email +// profileData["Phone"] = phone +// } + +// data := createTemplateData("Profile", "profile", role, true, profileData) +// utils.Render(w, "profile/profile.html", data) +// } + +// // Volunteer Dashboard Handler +// func VolunteerDashboardHandler(w http.ResponseWriter, r *http.Request) { +// role := r.Context().Value("user_role").(int) +// userID := r.Context().Value("user_id").(int) + +// // TODO: Fetch volunteer-specific data from database +// dashboardData := map[string]interface{}{ +// "UserID": userID, +// "AssignedTasks": 5, // Example: get from database +// "CompletedTasks": 12, // Example: get from database +// "UpcomingEvents": []string{"Community Meeting - Aug 30", "Training Session - Sep 5"}, // Example +// } + +// data := createTemplateData("Volunteer Dashboard", "dashboard", role, true, dashboardData) +// utils.Render(w, "volunteer/dashboard.html", data) +// } + +// // Schedule Handler for Volunteers +// func ScheduleHandler(w http.ResponseWriter, r *http.Request) { +// role := r.Context().Value("user_role").(int) +// userID := r.Context().Value("user_id").(int) + +// // TODO: Fetch schedule data from database +// scheduleData := map[string]interface{}{ +// "UserID": userID, +// "Schedule": []map[string]interface{}{ +// {"Date": "2025-08-26", "Time": "10:00 AM", "Task": "Door-to-door survey", "Location": "Downtown"}, +// {"Date": "2025-08-28", "Time": "2:00 PM", "Task": "Data entry", "Location": "Office"}, +// }, // Example: get from database +// } + +// data := createTemplateData("My Schedule", "schedual", role, true, scheduleData) +// utils.Render(w, "volunteer/schedule.html", data) +// } + +// Enhanced middleware to check JWT auth and add user context +func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("session") + if err != nil { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + claims := &models.Claims{} + token, err := jwt.ParseWithClaims(cookie.Value, claims, func(token *jwt.Token) (interface{}, error) { + return jwtKey, nil + }) + + if err != nil || !token.Valid { + log.Printf("Invalid token: %v", err) + clearSessionCookie(w) // Clear invalid cookie + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + // Add user info to context + ctx := context.WithValue(r.Context(), "user_id", claims.UserID) + ctx = context.WithValue(ctx, "user_role", claims.Role) + r = r.WithContext(ctx) + + next.ServeHTTP(w, r) + } +} \ No newline at end of file diff --git a/app/internal/handlers/profile.go b/app/internal/handlers/profile.go new file mode 100644 index 0000000..29c8418 --- /dev/null +++ b/app/internal/handlers/profile.go @@ -0,0 +1,98 @@ +package handlers + +import ( + "log" + "net/http" + + "github.com/patel-mann/poll-system/app/internal/models" + "github.com/patel-mann/poll-system/app/internal/utils" +) + +func ProfileHandler(w http.ResponseWriter, r *http.Request) { + // Extract current user ID from session/jwt + currentUserID := r.Context().Value("user_id").(int) + + var user models.User + err := models.DB.QueryRow(` + SELECT user_id, first_name, last_name, email, phone, role_id, created_at, updated_at + FROM "users" + WHERE user_id = $1 + `, currentUserID).Scan( + &user.UserID, + &user.FirstName, + &user.LastName, + &user.Email, + &user.Phone, + &user.RoleID, + &user.CreatedAt, + &user.UpdatedAt, + ) + if err != nil { + log.Println("Profile query error:", err) + http.Error(w, "Could not load profile", http.StatusInternalServerError) + return + } + + role := r.Context().Value("user_role").(int) + adminnav := false + volunteernav := false + + if role == 1{ + adminnav = true + volunteernav = false + }else{ + volunteernav = true + adminnav = false + } + + utils.Render(w, "profile/profile.html", map[string]interface{}{ + "Title": "My Profile", + "IsAuthenticated": true, + "ShowAdminNav": adminnav, + "ShowVolunteerNav": volunteernav, + "User": user, + "ActiveSection": "profile", + }) +} + +// ProfileUpdateHandler handles profile form submissions +func ProfileUpdateHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Redirect(w, r, "/profile", http.StatusSeeOther) + return + } + + // Extract current user ID from session/jwt + currentUserID := r.Context().Value("user_id").(int) + + // Parse form values + err := r.ParseForm() + if err != nil { + log.Println("Form parse error:", err) + http.Error(w, "Invalid form submission", http.StatusBadRequest) + return + } + + firstName := r.FormValue("first_name") + lastName := r.FormValue("last_name") + phone := r.FormValue("phone") + + // Update in DB + _, err = models.DB.Exec(` + UPDATE "users" + SET first_name = $1, + last_name = $2, + phone = $3, + updated_at = NOW() + WHERE user_id = $4 + `, firstName, lastName, phone, currentUserID) + + if err != nil { + log.Println("Profile update error:", err) + http.Error(w, "Could not update profile", http.StatusInternalServerError) + return + } + + // Redirect back to profile with success + http.Redirect(w, r, "/profile?success=1", http.StatusSeeOther) +} diff --git a/app/internal/handlers/volunteer_posts.go b/app/internal/handlers/volunteer_posts.go new file mode 100644 index 0000000..3472859 --- /dev/null +++ b/app/internal/handlers/volunteer_posts.go @@ -0,0 +1,74 @@ +// Add this to your handlers package (create volunteer_posts.go or add to existing file) + +package handlers + +import ( + "fmt" + "net/http" + "strconv" + "time" + + "github.com/patel-mann/poll-system/app/internal/models" + "github.com/patel-mann/poll-system/app/internal/utils" +) + +// VolunteerPostsHandler - Read-only posts view for volunteers +func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) { + // Only allow GET requests for volunteers + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Get user info from context + role := r.Context().Value("user_role").(int) + + // Fetch posts from database + rows, err := models.DB.Query(` + SELECT p.post_id, p.author_id, u.first_name || ' ' || u.last_name AS author_name, + p.content, COALESCE(p.image_url, '') as image_url, p.created_at + FROM post p + JOIN users u ON p.author_id = u.user_id + ORDER BY p.created_at DESC + `) + if err != nil { + fmt.Printf("Database query error: %v\n", err) + http.Error(w, "Failed to fetch posts", http.StatusInternalServerError) + return + } + defer rows.Close() + + var posts []models.Post + for rows.Next() { + var p models.Post + err := rows.Scan(&p.PostID, &p.AuthorID, &p.AuthorName, &p.Content, &p.ImageURL, &p.CreatedAt) + if err != nil { + fmt.Printf("Row scan error: %v\n", err) + continue + } + posts = append(posts, p) + } + + // Add cache busting parameter to image URLs + for i := range posts { + if posts[i].ImageURL != "" { + posts[i].ImageURL += "?t=" + strconv.FormatInt(time.Now().UnixNano(), 10) + } + } + + // Get navigation flags + showAdminNav, showVolunteerNav := getNavFlags(role) + + fmt.Printf("Volunteer viewing %d posts\n", len(posts)) + + utils.Render(w, "dashboard/volunteer_dashboard.html", map[string]interface{}{ + "Title": "Community Posts", + "IsAuthenticated": true, + "ShowAdminNav": showAdminNav, + "ShowVolunteerNav": showVolunteerNav, + "Posts": posts, + "ActiveSection": "posts", + "IsVolunteer": true, // Flag to indicate this is volunteer view + }) +} + diff --git a/app/internal/models/db.go b/app/internal/models/db.go new file mode 100644 index 0000000..e1fc40a --- /dev/null +++ b/app/internal/models/db.go @@ -0,0 +1,31 @@ +package models + +import ( + "database/sql" + "fmt" + "log" + + _ "github.com/lib/pq" +) + +var DB *sql.DB + +func InitDB() { + var err error + + // Example DSN format for PostgreSQL: + // "postgres://username:password@host:port/dbname?sslmode=disable" + dsn := "postgres://mannpatel:Admin@localhost:5432/poll_database?sslmode=disable" + + DB, err = sql.Open("postgres", dsn) + if err != nil { + log.Fatalf("Failed to connect to DB: %v", err) + } + + err = DB.Ping() + if err != nil { + log.Fatalf("Failed to ping DB: %v", err) + } + + fmt.Println("Database connection successful") +} diff --git a/app/internal/models/structs.go b/app/internal/models/structs.go new file mode 100644 index 0000000..6be0847 --- /dev/null +++ b/app/internal/models/structs.go @@ -0,0 +1,176 @@ +package models + +import ( + "time" + + "github.com/golang-jwt/jwt/v5" +) + + +type Claims struct { + UserID int + Role int + jwt.RegisteredClaims +} + +type TokenResponse struct { + Token string + User User +} + +type ErrorResponse struct { + Error string + Details []string +} + +type Role struct { + RoleID int + Name string + CreatedAt time.Time + UpdatedAt time.Time +} + +type User struct { + UserID int + FirstName string + LastName string + Email string + Phone string + Password string + RoleID int + CreatedAt time.Time + UpdatedAt time.Time +} + +type UserAddress struct { + UserID int + AddressLine1 string + AddressLine2 string + City string + Province string + Country string + PostalCode string + CreatedAt time.Time + UpdatedAt time.Time +} + +// ===================== +// Address Database +// ===================== + +type AddressDatabase struct { + AddressID int + Address string + StreetName string + StreetType string + StreetQuadrant string + HouseNumber string + HouseAlpha *string + Longitude float64 + Latitude float64 + VisitedValidated bool + CreatedAt time.Time + UpdatedAt time.Time +} + +// ===================== +// Teams & Assignments +// ===================== + + +type Team struct { + TeamID int + TeamLeadID int + VolunteerID int + CreatedAt time.Time + UpdatedAt time.Time +} + +type AdminVolunteer struct { + AdminID int + VolunteerID int + IsActive bool + CreatedAt time.Time + UpdatedAt time.Time +} + +type Appointment struct { + SchedID int + UserID int + AddressID int + AppointmentDate time.Time + AppointmentTime time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +// ===================== +// Polls & Responses +// ===================== + +type Poll struct { + PollID int + AddressID int + UserID int + ResponseURL string + AmountDonated float64 + CreatedAt time.Time + UpdatedAt time.Time +} + +type PollResponse struct { + ResponseID int + PollID int + Signage bool + VotingChoice string + DonationAmount float64 + CreatedAt time.Time +} + +// ===================== +// Updates & Reactions +// ===================== + +type Post struct { + PostID int + AuthorID int + AuthorName string // for display + Content string + ImageURL string + CreatedAt time.Time +} + + +type Reaction struct { + ReactionID int + PostID int + UserID int + ReactionType string + CreatedAt time.Time +} + +// ===================== +// Volunteer Availability +// ===================== + +type Availability struct { + AvailabilityID int + UserID int + DayOfWeek string + StartTime time.Time + EndTime time.Time + CreatedAt time.Time +} + +// ===================== +// Chat Links +// ===================== + +type ChatLink struct { + ChatID int + Platform string + URL string + UserID *int + TeamID *int + CreatedAt time.Time +} diff --git a/app/internal/models/token.go b/app/internal/models/token.go new file mode 100644 index 0000000..d398833 --- /dev/null +++ b/app/internal/models/token.go @@ -0,0 +1,28 @@ +package models + +import ( + "fmt" + + "github.com/golang-jwt/jwt/v5" +) + +var jwtKey = []byte("your-secret-key") //TODO: Move to env/config + + +func ExtractClaims(tokenStr string) (*Claims, error) { + claims := &Claims{} + + token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) { + return jwtKey, nil + }) + + if err != nil { + return nil, err + } + + if !token.Valid { + return nil, fmt.Errorf("invalid token") + } + + return claims, nil +} \ No newline at end of file diff --git a/app/internal/templates/address/address.html b/app/internal/templates/address/address.html new file mode 100644 index 0000000..b0fe18e --- /dev/null +++ b/app/internal/templates/address/address.html @@ -0,0 +1,201 @@ +{{ define "content" }} + +
+ +
+
+
+
+ + Address Database +
+
+ + {{if .Pagination}} +
+ Showing {{.Pagination.StartRecord}}-{{.Pagination.EndRecord}} of + {{.Pagination.TotalRecords}} addresses +
+ {{end}} +
+
+ + +
+
+ +
+
+
+ + +
+
+
+ +
+
+ + + {{if .Pagination}} +
+ +
+ + +
+ + +
+ + + + + + {{.Pagination.CurrentPage}} / {{.Pagination.TotalPages}} + + + + +
+
+ {{end}} +
+
+ + +
+ + + + + + + + + + + + + + + + + {{ range .Addresses }} + + + + + + + + + + {{ else }} + + + + {{ end }} + +
IDAddressStreetHouse #LongitudeLatitudeValidated
{{ .AddressID }}{{ .Address }} + {{ .StreetName }} {{ .StreetType }} {{ .StreetQuadrant }} + {{ .HouseNumber }}{{ .Longitude }}{{ .Latitude }} + {{ if .VisitedValidated }} + + Valid + + {{ else }} + + Invalid + + {{ end }} +
+ No addresses found +
+
+ + + {{if .Pagination}} +
+
+ +
+ Showing {{.Pagination.StartRecord}}-{{.Pagination.EndRecord}} of + {{.Pagination.TotalRecords}} addresses +
+
+
+ {{end}} +
+ + +{{ end }} diff --git a/app/internal/templates/dashboard/dashboard.html b/app/internal/templates/dashboard/dashboard.html new file mode 100644 index 0000000..a34882a --- /dev/null +++ b/app/internal/templates/dashboard/dashboard.html @@ -0,0 +1,230 @@ +{{ define "content" }} + + + + + + {{.Title}} + + + + + + +
+ +
+
+
+
+
+ +
+ + Dashboard Overview + +
+
+ + +
+
+
+
+ + +
+ +
+ +
+
+
+ +
+
+

+ Active Volunteers +

+

+ {{.VolunteerCount}} +

+
+
+
+ + +
+
+
+ +
+
+

+ Addresses Visited +

+

+ {{.ValidatedCount}} +

+
+
+
+ + +
+
+
+ +
+
+

Donation

+

+ ${{.TotalDonations}} +

+
+
+
+ + +
+
+
+ +
+
+

+ Houses Left +

+

+ {{.HousesLeftPercent}}% +

+
+
+
+
+ + +
+
+

+ Location Analytics +

+
+
+
+
+
+ + + + + + +{{ end }} diff --git a/app/internal/templates/dashboard/volunteer_dashboard.html b/app/internal/templates/dashboard/volunteer_dashboard.html new file mode 100644 index 0000000..61fe5fe --- /dev/null +++ b/app/internal/templates/dashboard/volunteer_dashboard.html @@ -0,0 +1,122 @@ +{{ define "content" }} +
+ +
+

Community

+
+ + +
+ + {{range .Posts}} +
+ +
+
+
+ {{slice .AuthorName 0 1}} +
+
+
+

{{.AuthorName}}

+

+ {{.CreatedAt.Format "Jan 2, 2006"}} +

+
+
+ + + {{if .ImageURL}} +
+ Post image +
+ {{end}} + + +
+
+ + +
+
+ + + {{if .Content}} +
+

+ {{.AuthorName}} {{.Content}} +

+
+ {{end}} +
+ {{else}} +
+
+ + + +

No posts yet

+

+ Be the first to share something with the community! +

+
+
+ {{end}} +
+ +{{ end }} diff --git a/app/internal/templates/layout.html b/app/internal/templates/layout.html new file mode 100644 index 0000000..b3f2e1c --- /dev/null +++ b/app/internal/templates/layout.html @@ -0,0 +1,490 @@ +{{ define "layout" }} + + + + + + {{if .Title}}{{.Title}}{{else}}Poll System{{end}} + + + + + + {{ if .IsAuthenticated }} + +
+ +
+
+
+ L +
+ Poll System +
+
+ + + +
+
+ +
+ + + + +
+
+ {{ template "content" . }} +
+
+
+
+ {{else}} + +
+ + + + +
+

+ Streamline Your
+ Polling Operations +

+

+ Manage volunteers, organize addresses, and track progress with our comprehensive polling system. +

+
+ + +
+
+ + +
+
+

Powerful Features

+

Everything you need to manage your polling operations efficiently and effectively.

+
+
+
+
+ +
+

Volunteer Management

+

Organize and coordinate your volunteer teams efficiently with role-based access and scheduling.

+
+
+
+ +
+

Address Tracking

+

Keep track of all polling locations and assignments with real-time updates and mapping.

+
+
+
+ +
+

Real-time Reports

+

Monitor progress with comprehensive analytics and detailed reporting dashboards.

+
+
+
+ + +
+
+
+
+

About Poll System

+

+ Poll System was created to simplify and streamline the complex process of managing polling operations. + Our platform brings together volunteers, administrators, and team leaders in one unified system. +

+

+ With years of experience in civic technology, we understand the challenges faced by polling organizations. + Our solution provides the tools needed to coordinate effectively and ensure smooth operations. +

+
+
+
+ +
+ Streamlined volunteer coordination +
+
+
+ +
+ Real-time progress tracking +
+
+
+ +
+ Comprehensive reporting tools +
+
+
+
+
+
+ +

Trusted by Organizations

+

+ Join hundreds of organizations already using Poll System to manage their operations efficiently. +

+
+
+
500+
+
Volunteers
+
+
+
50+
+
Organizations
+
+
+
1000+
+
Addresses
+
+
+
+
+
+
+
+
+ + + +
+ + + + + + + {{end}} + + + + +{{ end }} \ No newline at end of file diff --git a/app/internal/templates/posts.html b/app/internal/templates/posts.html new file mode 100644 index 0000000..435a0b0 --- /dev/null +++ b/app/internal/templates/posts.html @@ -0,0 +1,369 @@ +{{ define "content" }} +
+ +
+
+

Posts

+
+
+ +
+ +
+
+
+
+
+ U +
+
+
+ +
+
+ +
+
+ + +
+ +
+ + + +
+
+ + +
+ {{range .Posts}} +
+ +
+
+
+ {{slice .AuthorName 0 1}} +
+
+
+

{{.AuthorName}}

+

+ {{.CreatedAt.Format "Jan 2, 2006"}} +

+
+
+ + + {{if .ImageURL}} +
+ Post image +
+ {{end}} + + +
+
+ + + + + +
+
+ + + {{if .Content}} +
+

+ {{.AuthorName}} {{.Content}} +

+
+ {{end}} +
+ {{else}} +
+
+ + + +

No posts yet

+

+ Be the first to share something with the community! +

+
+
+ {{end}} +
+
+
+ + + + +{{ end }} diff --git a/app/internal/templates/profile/profile.html b/app/internal/templates/profile/profile.html new file mode 100644 index 0000000..0e44ca4 --- /dev/null +++ b/app/internal/templates/profile/profile.html @@ -0,0 +1,274 @@ +{{ define "content" }} +
+ +
+
+
+ +

User Profile

+
+
+ + Secure Profile Management +
+
+
+ + +
+ +
+
+

+ + Profile Overview +

+
+
+
+ +
+
+
+

+ {{ .User.FirstName }} {{ .User.LastName }} +

+

{{ .User.Email }}

+
+ + + Active User + + ID: {{ .User.UserID }} +
+
+
+
+ +
+

+ Account Information +

+
+
+ User ID: + {{ .User.UserID }} +
+
+ Role: + {{ if eq .User.RoleID 1 }}Admin + {{ else if eq .User.RoleID 2 }}Team Leader + {{ else }}Volunteer + {{ end }} +
+
+ Status: + Active +
+ +
+
+
+
+
+
+ + +
+
+

+ + Edit Profile Information +

+
+ +
+
+
+ +
+ + +
+ + +
+ + +
+ + +
+ +
+ +
+ +
+
+

+ Contact system administrator to change email +

+
+ + +
+ + +
+
+ + +
+
+ + Changes will be applied immediately after saving +
+
+ + +
+
+
+
+
+ + +
+ + + + +{{ end }} diff --git a/app/internal/templates/schedual/schedual.html b/app/internal/templates/schedual/schedual.html new file mode 100644 index 0000000..55cbe27 --- /dev/null +++ b/app/internal/templates/schedual/schedual.html @@ -0,0 +1,239 @@ +{{ define "content" }} + + + + + + Interactive Dashboard + + + + + +
+ +
+
+
+ + + Schedual Overview + +
+
+ + +
+
+
+ + +
+ +
+ +
+
+
+ +
+
+

+ Active Locations +

+

24

+
+
+
+ + +
+
+
+ +
+
+

Total Visitors

+

12,847

+
+
+
+ + +
+
+
+ +
+
+

Revenue

+

$47,392

+
+
+
+ + +
+
+
+ +
+
+

Conversion Rate

+

3.2%

+
+
+
+
+ + +
+

+ Location Analytics +

+
+
+ + +
+
+

+ Performance Metrics +

+
+ + + +
+
+
+
+
+
+ + + + + + +{{ end }} diff --git a/app/internal/templates/volunteer/edit_volunteer.html b/app/internal/templates/volunteer/edit_volunteer.html new file mode 100644 index 0000000..fa43d63 --- /dev/null +++ b/app/internal/templates/volunteer/edit_volunteer.html @@ -0,0 +1,45 @@ +{{ define "content" }} +

Edit Volunteer

+
+ + + +
+ + +
+ + +
+ + +
+ +
+ + + +
+{{end}} diff --git a/app/internal/templates/volunteer/team_builder.html b/app/internal/templates/volunteer/team_builder.html new file mode 100644 index 0000000..0d25e10 --- /dev/null +++ b/app/internal/templates/volunteer/team_builder.html @@ -0,0 +1,36 @@ +{{ define "content" }} +
+

Team Builder

+ + {{range .TeamLeads}} +
+
+ {{.Name}} +
+ + + +
+
+ + + {{if .Volunteers}} + + {{else}} +

No volunteers assigned yet.

+ {{end}} +
+ {{end}} +
+{{ end }} diff --git a/app/internal/templates/volunteer/volunteer.html b/app/internal/templates/volunteer/volunteer.html new file mode 100644 index 0000000..2b3cdc8 --- /dev/null +++ b/app/internal/templates/volunteer/volunteer.html @@ -0,0 +1,299 @@ +{{ define "content" }} + +
+ +
+
+
+
+ + Volunteers +
+
+
+
+ + +
+
+ +
+
+ + +
+
+ + +
+ + +
+ + + + + +
+ + Showing of + volunteers + +
+
+
+ + +
+ + + + + + + + + + + + + + + + + + +
+
+ ID +
+
+
+ First Name +
+
+
+ Last Name +
+
+
+ Email +
+
+
+ Phone +
+
+
+ Role +
+
Actions
+ + +
+ +

No volunteers found

+

+ Try adjusting your search or filter criteria +

+
+
+
+ + + + +{{ end }} diff --git a/app/internal/utils/render.go b/app/internal/utils/render.go new file mode 100644 index 0000000..dd345e0 --- /dev/null +++ b/app/internal/utils/render.go @@ -0,0 +1,69 @@ +package utils + +import ( + "bytes" + "html/template" + "net/http" + "path/filepath" +) + +// Helper functions for templates +var templateFuncs = template.FuncMap{ + "add": func(a, b int) int { + return a + b + }, + "sub": func(a, b int) int { + return a - b + }, + "eq": func(a, b interface{}) bool { + return a == b + }, + "pageRange": func(currentPage, totalPages int) []int { + // Generate page numbers to show (max 7 pages) + start := currentPage - 3 + end := currentPage + 3 + + if start < 1 { + end += 1 - start + start = 1 + } + if end > totalPages { + start -= end - totalPages + end = totalPages + } + if start < 1 { + start = 1 + } + + var pages []int + for i := start; i <= end; i++ { + pages = append(pages, i) + } + return pages + }, +} + +func Render(w http.ResponseWriter, tmpl string, data interface{}) { + // Paths for layout + page templates + layout := filepath.Join("/Users/mannpatel/Desktop/Poll-system/app/internal/templates/", "layout.html") + page := filepath.Join("/Users/mannpatel/Desktop/Poll-system/app/internal/templates/", tmpl) + + // Parse files with helper functions + tmpls, err := template.New("").Funcs(templateFuncs).ParseFiles(layout, page) + if err != nil { + http.Error(w, "Template parsing error: "+err.Error(), http.StatusInternalServerError) + return + } + + // Render to buffer first (catch errors before writing response) + var buf bytes.Buffer + err = tmpls.ExecuteTemplate(&buf, "layout", data) + if err != nil { + http.Error(w, "Template execution error: "+err.Error(), http.StatusInternalServerError) + return + } + + // Write final HTML to response + w.Header().Set("Content-Type", "text/html; charset=utf-8") + buf.WriteTo(w) +} diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..e9bf409 --- /dev/null +++ b/app/main.go @@ -0,0 +1,182 @@ +// Add this debugging code to your main.go + +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + + "github.com/golang-jwt/jwt/v5" + + "github.com/patel-mann/poll-system/app/internal/handlers" + "github.com/patel-mann/poll-system/app/internal/models" + "github.com/patel-mann/poll-system/app/internal/utils" + + _ "github.com/lib/pq" // use PostgreSQL +) + +var jwtSecret = []byte("your-secret-key") + +// Custom file server with logging +func loggingFileServer(dir string) http.Handler { + fs := http.FileServer(http.Dir(dir)) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Log the request + log.Printf("File request: %s", r.URL.Path) + + // Check if file exists + filePath := filepath.Join(dir, r.URL.Path) + if _, err := os.Stat(filePath); os.IsNotExist(err) { + log.Printf("File not found: %s", filePath) + http.NotFound(w, r) + return + } + + log.Printf("Serving file: %s", filePath) + fs.ServeHTTP(w, r) + }) +} + +// Helper function to determine navigation visibility based on role +func getNavFlags(role int) (bool, bool, bool) { + showAdminNav := role == 1 // Admin role + showLeaderNav := role == 2 // Volunteer role + showVolunteerNav := role == 3 // Volunteer role + return showAdminNav, showVolunteerNav, showLeaderNav +} + +// Helper function to create template data with proper nav flags +func createTemplateData(title, activeSection string, role int, isAuthenticated bool, additionalData map[string]interface{}) map[string]interface{} { + showAdminNav, showVolunteerNav, _ := getNavFlags(role) + + data := map[string]interface{}{ + "Title": title, + "IsAuthenticated": isAuthenticated, + "Role": role, + "ShowAdminNav": showAdminNav, + "ShowVolunteerNav": showVolunteerNav, + "ActiveSection": activeSection, + } + + // Add any additional data + for key, value := range additionalData { + data[key] = value + } + + return data +} + +func authMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("session") + if err != nil { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + claims := &models.Claims{} + token, err := jwt.ParseWithClaims(cookie.Value, claims, func(token *jwt.Token) (interface{}, error) { + return jwtSecret, nil + }) + if err != nil || !token.Valid { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + // Add user info to context + ctx := context.WithValue(r.Context(), "user_id", claims.UserID) + ctx = context.WithValue(ctx, "user_role", claims.Role) + r = r.WithContext(ctx) + + next.ServeHTTP(w, r) + } +} + +// Admin middleware to check if user has admin role +func adminMiddleware(next http.HandlerFunc) http.HandlerFunc { + return authMiddleware(func(w http.ResponseWriter, r *http.Request) { + role, ok := r.Context().Value("user_role").(int) + fmt.Print(role) + if !ok || role != 1 { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + next.ServeHTTP(w, r) + }) +} + +// Volunteer middleware to check if user has volunteer role +func volunteerMiddleware(next http.HandlerFunc) http.HandlerFunc { + return authMiddleware(func(w http.ResponseWriter, r *http.Request) { + role, ok := r.Context().Value("user_role").(int) + if !ok || (role != 3 && role != 2) { + fmt.Printf("Access denied: role %d not allowed\n", role) // Debug log + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + next.ServeHTTP(w, r) + }) +} + + + +// Updated handler functions using the helper +func schedualHandler(w http.ResponseWriter, r *http.Request) { + role := r.Context().Value("user_role").(int) + + data := createTemplateData("My Schedule", "schedual", role, true, nil) + utils.Render(w, "Schedual/schedual.html", data) +} + +func HomeHandler(w http.ResponseWriter, r *http.Request) { + utils.Render(w, "dashboard/dashboard.html", map[string]interface{}{ + "Title": "Admin Dashboard", + "IsAuthenticated": false, + "ActiveSection": "dashboard", + }) +} + + +func main() { + models.InitDB() + + // Static file servers with logging + fs := http.FileServer(http.Dir("static")) + http.Handle("/static/", http.StripPrefix("/static/", fs)) + + // Use logging file server for uploads + http.Handle("/uploads/", http.StripPrefix("/uploads/", loggingFileServer("uploads"))) + + // Public HTML Routes + http.HandleFunc("/", HomeHandler) + http.HandleFunc("/login", handlers.LoginHandler) + http.HandleFunc("/register", handlers.RegisterHandler) + + //--- Protected HTML Routes + http.HandleFunc("/logout", authMiddleware(handlers.LogoutHandler)) + + // Common routes (both admin and volunteer can access) + http.HandleFunc("/profile", authMiddleware(handlers.ProfileHandler)) + http.HandleFunc("/profile/update", authMiddleware(handlers.ProfileUpdateHandler)) + + //--- Admin-only routes + http.HandleFunc("/dashboard", adminMiddleware(handlers.AdminDashboardHandler)) + http.HandleFunc("/volunteers", adminMiddleware(handlers.VolunteerHandler)) + http.HandleFunc("/volunteer/edit", adminMiddleware(handlers.EditVolunteerHandler)) + + http.HandleFunc("/team_builder", adminMiddleware(handlers.TeamBuilderHandler)) + + http.HandleFunc("/addresses", adminMiddleware(handlers.AddressHandler)) + http.HandleFunc("/posts", adminMiddleware(handlers.PostsHandler)) + + //--- Volunteer-only routes + http.HandleFunc("/volunteer/dashboard", volunteerMiddleware(handlers.VolunteerPostsHandler)) + http.HandleFunc("/schedual", volunteerMiddleware(schedualHandler)) + + log.Println("Server started on localhost:8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/app/tmp/build-errors.log b/app/tmp/build-errors.log new file mode 100644 index 0000000..4d4fa3f --- /dev/null +++ b/app/tmp/build-errors.log @@ -0,0 +1 @@ +exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file diff --git a/app/tmp/main b/app/tmp/main new file mode 100755 index 0000000..cfc40c6 Binary files /dev/null and b/app/tmp/main differ diff --git a/app/uploads/3_1756233257286246000.jpg b/app/uploads/3_1756233257286246000.jpg new file mode 100644 index 0000000..b2db35a Binary files /dev/null and b/app/uploads/3_1756233257286246000.jpg differ diff --git a/app/uploads/3_1756234910125847000.jpeg b/app/uploads/3_1756234910125847000.jpeg new file mode 100644 index 0000000..c41109b Binary files /dev/null and b/app/uploads/3_1756234910125847000.jpeg differ diff --git a/app/uploads/3_1756234944747970000.jpg b/app/uploads/3_1756234944747970000.jpg new file mode 100644 index 0000000..c5f51a9 Binary files /dev/null and b/app/uploads/3_1756234944747970000.jpg differ diff --git a/app/uploads/3_1756235257999355000.jpg b/app/uploads/3_1756235257999355000.jpg new file mode 100644 index 0000000..eecbce4 Binary files /dev/null and b/app/uploads/3_1756235257999355000.jpg differ diff --git a/app/uploads/3_1756236773454920000.jpeg b/app/uploads/3_1756236773454920000.jpeg new file mode 100644 index 0000000..c41109b Binary files /dev/null and b/app/uploads/3_1756236773454920000.jpeg differ