Files
Poll-system/app/internal/templates/address.html
2025-09-05 15:39:06 -06:00

534 lines
20 KiB
HTML

{{ define "content" }}
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Toolbar -->
<div class="bg-white border-b border-gray-200 px-4 md:px-6 py-4">
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<!-- Search -->
<div class="relative w-full sm:w-auto">
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-sm"></i>
<input
type="text"
placeholder="Search Addresses"
class="w-full sm:w-80 pl-10 pr-4 py-2 text-sm border border-gray-200 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<!-- Pagination Controls -->
{{if .Pagination}}
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-4 w-full sm:w-auto">
<div class="flex items-center gap-2">
<button
class="px-6 py-2.5 bg-green-600 text-white text-sm font-medium hover:bg-green-700 transition-colors"
onclick="window.location.href='/addresses/upload-csv'"
>
<i class="fas fa-file-import mr-2"></i>Import Data
</button>
</div>
<div class="flex items-center gap-2">
<label for="pageSize" class="text-sm text-gray-600 whitespace-nowrap">Per page:</label>
<select
id="pageSize"
onchange="changePageSize(this.value)"
class="px-3 py-2 text-sm border border-gray-200 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="20" {{if eq .Pagination.PageSize 20}}selected{{end}}>20</option>
<option value="50" {{if eq .Pagination.PageSize 50}}selected{{end}}>50</option>
<option value="100" {{if eq .Pagination.PageSize 100}}selected{{end}}>100</option>
</select>
</div>
<div class="flex items-center gap-2">
<button
onclick="goToPage({{.Pagination.PreviousPage}})"
{{if not .Pagination.HasPrevious}}disabled{{end}}
class="px-3 py-2 text-sm border border-gray-200 rounded-lg {{if .Pagination.HasPrevious}}hover:bg-gray-50 text-gray-700{{else}}text-gray-400 cursor-not-allowed{{end}} transition-colors"
>
<i class="fas fa-chevron-left"></i>
</button>
<span class="px-3 py-2 text-sm text-gray-600 whitespace-nowrap">
{{.Pagination.CurrentPage}} / {{.Pagination.TotalPages}}
</span>
<button
onclick="goToPage({{.Pagination.NextPage}})"
{{if not .Pagination.HasNext}}disabled{{end}}
class="px-3 py-2 text-sm border border-gray-200 rounded-lg {{if .Pagination.HasNext}}hover:bg-gray-50 text-gray-700{{else}}text-gray-400 cursor-not-allowed{{end}} transition-colors"
>
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<!-- Table Container -->
<div class="flex-1 p-4 md:p-6 overflow-auto">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<!-- Desktop Table -->
<div class="hidden lg:block overflow-x-auto">
<table class="w-full min-w-full">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
Status
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Address
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
Coordinates
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
Assigned User
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
Appointment
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-100">
{{ range .Addresses }}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4">
{{ if .VisitedValidated }}
<span class="inline-flex items-center px-2 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full">
<i class="fas fa-check mr-1"></i> Valid
</span>
{{ else }}
<span class="inline-flex items-center px-2 py-1 text-xs font-medium bg-red-100 text-red-700 rounded-full">
<i class="fas fa-times mr-1"></i> Invalid
</span>
{{ end }}
</td>
<td class="px-6 py-4">
<div class="text-sm font-medium text-gray-900">{{ .Address }}</div>
</td>
<td class="px-6 py-4">
<a
href="https://www.google.com/maps/search/?api=1&query={{ .Latitude }},{{ .Longitude }}"
target="_blank"
class="text-blue-600 hover:text-blue-800 text-sm hover:underline"
>
({{ .Latitude }}, {{ .Longitude }})
</a>
</td>
<td class="px-6 py-4">
{{ if .UserName }}
<div class="text-sm font-medium text-gray-900">{{ .UserName }}</div>
<div class="text-sm text-gray-500">{{ .UserEmail }}</div>
{{ else }}
<span class="text-sm text-gray-400">Unassigned</span>
{{ end }}
</td>
<td class="px-6 py-4">
{{ if .AppointmentDate }}
<div class="text-sm text-gray-900">{{ .AppointmentDate }}</div>
<div class="text-sm text-gray-500">{{ .AppointmentTime }}</div>
{{ else }}
<span class="text-sm text-gray-400">No appointment</span>
{{ end }}
</td>
<td class="px-6 py-4">
<div class="flex items-center space-x-2">
{{ if .Assigned }}
<button
class="px-3 py-1 bg-gray-100 text-gray-500 text-sm rounded-md cursor-not-allowed"
disabled
>
Assigned
</button>
<form action="/remove_assigned_address" method="POST" class="inline-block">
<input type="hidden" name="address_id" value="{{ .AddressID }}" />
<input type="hidden" name="user_id" value="{{ .UserID }}" />
<button
type="submit"
class="text-red-400 hover:text-red-600 p-1"
title="Remove assignment"
>
<i class="fas fa-trash"></i>
</button>
</form>
{{ else }}
<button
class="px-3 py-1 bg-blue-500 text-white text-sm rounded-md hover:bg-blue-600 transition-colors"
onclick="openAssignModal({{ .AddressID }}, '{{ .Address }}')"
>
Assign
</button>
{{ end }}
</div>
</td>
</tr>
{{ else }}
<tr>
<td colspan="6" class="px-6 py-8 text-center text-gray-500">
No addresses found
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<!-- Mobile Cards -->
<div class="lg:hidden">
<div class="space-y-4 p-4">
{{ range .Addresses }}
<div class="bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
<!-- Card Header -->
<div class="bg-gray-50 px-4 py-3 border-b border-gray-200 flex items-center justify-between">
<div class="flex items-center space-x-2">
<i class="fas fa-map-marker-alt text-gray-400"></i>
<span class="text-sm font-semibold text-gray-900">Address</span>
</div>
{{ if .VisitedValidated }}
<span class="inline-flex items-center px-2 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full">
<i class="fas fa-check mr-1"></i> Valid
</span>
{{ else }}
<span class="inline-flex items-center px-2 py-1 text-xs font-medium bg-red-100 text-red-700 rounded-full">
<i class="fas fa-times mr-1"></i> Invalid
</span>
{{ end }}
</div>
<!-- Card Content -->
<div class="p-4 space-y-3">
<!-- Address -->
<div class="flex flex-col">
<span class="text-sm font-medium text-gray-900">{{ .Address }}</span>
</div>
<!-- Coordinates -->
<div class="flex justify-between items-center">
<span class="text-sm text-gray-500">Coordinates</span>
<a
href="https://www.google.com/maps/search/?api=1&query={{ .Latitude }},{{ .Longitude }}"
target="_blank"
class="text-blue-600 hover:text-blue-800 text-sm hover:underline"
>
({{ .Latitude }}, {{ .Longitude }})
</a>
</div>
<!-- Assigned User -->
<div class="flex justify-between items-start">
<span class="text-sm text-gray-500">Assigned User</span>
<div class="text-right">
{{ if .UserName }}
<div class="text-sm font-medium text-gray-900">{{ .UserName }}</div>
<div class="text-sm text-gray-500">{{ .UserEmail }}</div>
{{ else }}
<span class="text-sm text-gray-400">Unassigned</span>
{{ end }}
</div>
</div>
<!-- Appointment -->
<div class="flex justify-between items-start">
<span class="text-sm text-gray-500">Appointment</span>
<div class="text-right">
{{ if .AppointmentDate }}
<div class="text-sm text-gray-900">{{ .AppointmentDate }}</div>
<div class="text-sm text-gray-500">{{ .AppointmentTime }}</div>
{{ else }}
<span class="text-sm text-gray-400">No appointment</span>
{{ end }}
</div>
</div>
<!-- Actions -->
<div class="flex justify-center space-x-4 pt-3 border-t border-gray-100">
{{ if .Assigned }}
<button
class="flex-1 px-4 py-2 bg-gray-100 text-gray-500 text-sm rounded-md cursor-not-allowed"
disabled
>
Already Assigned
</button>
<form action="/remove_assigned_address" method="POST" class="inline-block">
<input type="hidden" name="address_id" value="{{ .AddressID }}" />
<input type="hidden" name="user_id" value="{{ .UserID }}" />
<button
type="submit"
class="px-4 py-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-md transition-colors text-sm font-medium"
>
<i class="fas fa-trash mr-1"></i> Remove
</button>
</form>
{{ else }}
<button
class="flex-1 px-4 py-2 bg-blue-500 text-white text-sm rounded-md hover:bg-blue-600 transition-colors font-medium"
onclick="openAssignModal({{ .AddressID }}, '{{ .Address }}')"
>
<i class="fas fa-user-plus mr-1"></i> Assign User
</button>
{{ end }}
</div>
</div>
</div>
{{ else }}
<div class="text-center py-12">
<div class="text-gray-400 mb-4">
<i class="fas fa-map-marker-alt text-4xl"></i>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">No addresses found</h3>
<p class="text-gray-500">Try adjusting your search criteria.</p>
</div>
{{ end }}
</div>
</div>
</div>
</div>
<!-- Assign Panel Overlay -->
<div
id="assignPanelOverlay"
class="fixed inset-0 bg-black bg-opacity-50 hidden z-40"
></div>
<!-- Assign Drawer Panel -->
<div
id="assignPanel"
class="fixed top-0 right-0 h-full w-full max-w-md bg-white shadow-xl transform translate-x-full transition-transform duration-300 ease-in-out z-50 flex flex-col"
>
<!-- Panel Header -->
<div class="flex justify-between items-center px-6 py-4 border-b border-gray-200 bg-gray-50">
<div class="flex items-center space-x-2">
<i class="fas fa-user-plus text-blue-500"></i>
<h2 class="text-lg font-semibold text-gray-900">Assign Address</h2>
</div>
<button
onclick="closeAssignPanel()"
class="text-gray-400 hover:text-gray-600 focus:outline-none p-1"
>
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- Panel Body -->
<form id="assignForm" method="POST" action="/assign_address" class="flex-1 overflow-y-auto p-6 space-y-6">
<input type="hidden" name="address_id" id="panelAddressID" />
<!-- Selected Address Display -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-center space-x-2 mb-2">
<i class="fas fa-map-marker-alt text-blue-500"></i>
<span class="font-medium text-gray-900">Selected Address:</span>
</div>
<div class="text-sm text-gray-700" id="panel-selected-address">None selected</div>
</div>
<!-- User Selection -->
<div>
<label for="user_id" class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-user mr-2 text-gray-400"></i>Select User
</label>
<select
name="user_id"
id="user_id"
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required
>
<option value="">-- Select User --</option>
{{ range .Users }}
<option value="{{ .ID }}">{{ .Name }}</option>
{{ end }}
</select>
</div>
<!-- Date Selection -->
<div>
<label for="appointment-date" class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-calendar mr-2 text-gray-400"></i>Appointment Date
</label>
<input
type="date"
id="appointment-date"
name="appointment_date"
required
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
min=""
/>
</div>
<!-- Time Selection -->
<div>
<label for="time" class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-clock mr-2 text-gray-400"></i>Appointment Time
</label>
<select
id="time"
name="time"
required
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">Select Time</option>
</select>
</div>
</form>
<!-- Panel Footer -->
<div class="flex justify-end gap-3 px-6 py-4 border-t border-gray-200 bg-gray-50">
<button
type="button"
onclick="closeAssignPanel()"
class="px-6 py-2 border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded-lg font-medium transition-colors"
>
Cancel
</button>
<button
type="submit"
form="assignForm"
class="px-6 py-2 bg-blue-500 text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded-lg font-medium transition-colors"
>
<i class="fas fa-check mr-2"></i> Assign
</button>
</div>
</div>
</div>
<style>
/* Consistent styling */
input, select, button {
transition: all 0.2s ease;
}
button {
font-weight: 500;
letter-spacing: 0.025em;
}
/* Mobile responsive adjustments */
@media (max-width: 640px) {
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
}
</style>
<script>
// Generate time options in 20-minute increments
function generateTimeOptions() {
const times = [];
for (let hour = 8; hour < 18; hour++) { // Business hours 8 AM to 6 PM
for (let minute = 0; minute < 60; minute += 20) {
const timeString = String(hour).padStart(2, "0") + ":" + String(minute).padStart(2, "0");
const displayTime = formatTime12Hour(hour, minute);
times.push({ value: timeString, display: displayTime });
}
}
return times;
}
// Format time to 12-hour format
function formatTime12Hour(hour, minute) {
const ampm = hour >= 12 ? "PM" : "AM";
const displayHour = hour % 12 || 12;
return displayHour + ":" + String(minute).padStart(2, "0") + " " + ampm;
}
// Populate time dropdown
function populateTimeSelect() {
const timeSelect = document.getElementById("time");
const times = generateTimeOptions();
timeSelect.innerHTML = '<option value="">Select Time</option>';
times.forEach((time) => {
const option = new Option(time.display, time.value);
timeSelect.appendChild(option);
});
}
function openAssignModal(addressID, address) {
document.getElementById("panelAddressID").value = addressID;
document.getElementById("panel-selected-address").textContent =
address || "Address ID: " + addressID;
// Set minimum date to today
const today = new Date().toISOString().split("T")[0];
document.getElementById("appointment-date").min = today;
document.getElementById("appointment-date").value = today;
// Show overlay + panel
document.getElementById("assignPanelOverlay").classList.remove("hidden");
document
.getElementById("assignPanel")
.classList.remove("translate-x-full");
setTimeout(() => {
document.getElementById("user_id").focus();
}, 100);
}
function closeAssignPanel() {
document
.getElementById("assignPanel")
.classList.add("translate-x-full");
document
.getElementById("assignPanelOverlay")
.classList.add("hidden");
document.getElementById("assignForm").reset();
document.getElementById("panel-selected-address").textContent =
"None selected";
}
// Close when clicking overlay
document
.getElementById("assignPanelOverlay")
.addEventListener("click", closeAssignPanel);
// Close on Escape key
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
closeAssignPanel();
}
});
function goToPage(page) {
var urlParams = new URLSearchParams(window.location.search);
urlParams.set("page", page);
window.location.search = urlParams.toString();
}
function changePageSize(pageSize) {
var urlParams = new URLSearchParams(window.location.search);
urlParams.set("pageSize", pageSize);
urlParams.set("page", 1);
window.location.search = urlParams.toString();
}
// Initialize when page loads
document.addEventListener("DOMContentLoaded", function () {
populateTimeSelect();
// Close panel when clicking outside
document.getElementById("assignPanelOverlay").addEventListener("click", function (e) {
closeAssignPanel();
});
// Close panel on Escape key
document.addEventListener("keydown", function(e) {
if (e.key === "Escape") {
const overlay = document.getElementById("assignPanelOverlay");
if (!overlay.classList.contains("invisible")) {
closeAssignPanel();
}
}
});
});
</script>
{{ end }}