initial commit
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>
|
||||
Civixfix - {% block title %}Community Issue Reporting Platform{% endblock %}
|
||||
</title>
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link rel="preload" as="image" href="https://a.tile.openstreetmap.org/13/4521/2632.png">
|
||||
<link rel="preload" as="image" href="https://b.tile.openstreetmap.org/13/4521/2632.png">
|
||||
|
||||
<link rel="preload" as="font"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/webfonts/fa-solid-900.woff2" type="font/woff2"
|
||||
crossorigin>
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
||||
<!-- Custom CSS -->
|
||||
<style>
|
||||
.hero-section {
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)),
|
||||
url("https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?ixlib=rb-4.0.3");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
color: white;
|
||||
padding: 100px 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.issue-card {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.issue-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
</style>
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{% url 'home' %}">
|
||||
<i class="fas fa-bullhorn me-2"></i>Civixfix
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="{% url 'home' %}">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#features">Features</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#how-it-works">How It Works</a>
|
||||
</li>
|
||||
{% if user.is_authenticated and user.is_citizen %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'citizen_dashboard' %}">Dashboard</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<div class="d-flex">
|
||||
{% if user.is_authenticated %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-light dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-user me-1"></i> {{ user.username }}
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
{% if user.is_citizen %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'citizen_dashboard' %}">
|
||||
<i class="fas fa-users me-2 text-primary"></i>Citizen Dashboard</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_resolver %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'department_dashboard' %}">
|
||||
<i class="fas fa-building me-2 text-success"></i>Department Dashboard</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_superuser %}
|
||||
<li>
|
||||
<a class="dropdown-item text-danger fw-bold" href="{% url 'superadmin_dashboard' %}">
|
||||
<i class="fas fa-crown me-1 text-warning fw-bold"></i> Super Admin
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li>
|
||||
<hr class="dropdown-divider" />
|
||||
</li>
|
||||
<li>
|
||||
<form method="post" action="{% url 'logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="dropdown-item"><i class="fas fa-right-from-bracket me-2"></i>Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Login / Register buttons for anonymous users -->
|
||||
<a href="{% url 'login' %}" class="btn btn-outline-light me-2">Login</a>
|
||||
<a href="{% url 'register' %}" class="btn btn-primary">Register</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>{% block content %}{% endblock %}</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="text-black py-4 mt-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<h5><i class="fas fa-bullhorn me-2"></i>Civixfix</h5>
|
||||
<p class="text-muted">
|
||||
Empowering communities through transparent issue reporting and
|
||||
resolution.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<h5>Quick Links</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<a href="#features" class="text-decoration-none text-muted">Features</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#how-it-works" class="text-decoration-none text-muted">How It Works</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="text-decoration-none text-muted">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<h5>Contact</h5>
|
||||
<ul class="list-unstyled text-muted">
|
||||
<li>
|
||||
<i class="fas fa-envelope me-2"></i> gokuldevse2001@gmail.com
|
||||
</li>
|
||||
<li><i class="fas fa-phone me-2"></i> +91 8129329073</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-4 bg-secondary" />
|
||||
<div class="text-center text-muted">
|
||||
<small>© {% now "Y" %} Civixfix. All rights reserved.</small>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle with Popper -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,297 @@
|
||||
{% extends "core/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section text-center">
|
||||
<div class="container">
|
||||
<h1 class="display-4 fw-bold mb-4">Report. Resolve. Rejoice.</h1>
|
||||
<p class="lead mb-5">Your voice matters in making our community better. Report local issues and track their
|
||||
resolution in real-time.</p>
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||
{% if user.is_authenticated %}
|
||||
<a href="{% url 'citizen_dashboard' %}" class="btn btn-primary btn-lg px-4 gap-3">Report an Issue</a>
|
||||
{% else %}
|
||||
<a href="{% url 'login' %}" class="btn btn-primary btn-lg px-4 gap-3">Report an Issue</a>
|
||||
{% endif %}
|
||||
<a href="#how-it-works" class="btn btn-outline-light btn-lg px-4">Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<section class="container mb-5">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-3">
|
||||
<h3 class="fw-bold text-primary">{{ total_issues|default:"0" }}+</h3>
|
||||
<p class="text-muted">Issues Reported</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h3 class="fw-bold text-primary">{{ resolved_issues|default:"0" }}+</h3>
|
||||
<p class="text-muted">Issues Resolved</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h3 class="fw-bold text-primary">{{ active_users|default:"0" }}+</h3>
|
||||
<p class="text-muted">Active Users</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h3 class="fw-bold text-primary">{{ total_departments|default:"0" }}</h3>
|
||||
<p class="text-muted">Municipal Departments</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Features Section -->
|
||||
<section id="features" class="container mb-5">
|
||||
<div class="text-center mb-5">
|
||||
<h2 class="fw-bold">Why Choose Civixfix?</h2>
|
||||
<p class="lead text-muted">Transparent, efficient, and community-driven problem solving</p>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body text-center p-4">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-map-marked-alt"></i>
|
||||
</div>
|
||||
<h5 class="card-title">Location-Based Reporting</h5>
|
||||
<p class="card-text text-muted">Pinpoint issues on an interactive map for accurate location tracking
|
||||
and faster resolution.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body text-center p-4">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-tasks"></i>
|
||||
</div>
|
||||
<h5 class="card-title">Real-Time Tracking</h5>
|
||||
<p class="card-text text-muted">Follow your reported issues through every stage from reporting to
|
||||
resolution.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body text-center p-4">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<h5 class="card-title">Community Engagement</h5>
|
||||
<p class="card-text text-muted">Vote and comment on issues to help prioritize what matters most to
|
||||
your neighborhood.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- How It Works Section -->
|
||||
<section id="how-it-works" class="bg-light py-5 mb-5">
|
||||
<div class="container">
|
||||
<div class="text-center mb-5">
|
||||
<h2 class="fw-bold">How Civixfix Works</h2>
|
||||
<p class="lead text-muted">Simple steps to make your community better</p>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body text-center p-4">
|
||||
<span class="badge bg-primary rounded-circle mb-3"
|
||||
style="width: 50px; height: 50px; line-height: 50px; font-size: 1.5rem;">1</span>
|
||||
<h5 class="card-title">Report an Issue</h5>
|
||||
<p class="card-text text-muted">Take a photo, add details, and drop a pin on the map to report
|
||||
problems in your area.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body text-center p-4">
|
||||
<span class="badge bg-primary rounded-circle mb-3"
|
||||
style="width: 50px; height: 50px; line-height: 50px; font-size: 1.5rem;">2</span>
|
||||
<h5 class="card-title">Community Support</h5>
|
||||
<p class="card-text text-muted">Others can vote and comment to show support and add details to
|
||||
your report.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body text-center p-4">
|
||||
<span class="badge bg-primary rounded-circle mb-3"
|
||||
style="width: 50px; height: 50px; line-height: 50px; font-size: 1.5rem;">3</span>
|
||||
<h5 class="card-title">Official Response</h5>
|
||||
<p class="card-text text-muted">Municipal authorities receive, prioritize, and work on resolving
|
||||
the issues.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Recent Issues Section -->
|
||||
<section class="container mb-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">Recently Reported Issues</h2>
|
||||
{% if user.is_authenticated %}
|
||||
<a href="{% url 'view_all_issues' %}" class="btn btn-primary">View All Issues</a>
|
||||
{% else %}
|
||||
<a href="{% url 'login' %}" class="btn btn-outline-primary">View All Issues</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
{% for issue in recent_issues %}
|
||||
<div class="col-md-4">
|
||||
<div class="card issue-card h-100">
|
||||
{% if issue.photo %}
|
||||
<img src="{{ issue.photo.url }}" class="card-img-top" alt="{{ issue.title }}"
|
||||
style="height: 200px; object-fit: cover;">
|
||||
{% else %}
|
||||
<img src="https://via.placeholder.com/300x200/6c757d/ffffff?text=No+Image" class="card-img-top"
|
||||
alt="No image">
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
<span class="badge
|
||||
{% if issue.department.name == 'Roads & Transportation' %} bg-primary
|
||||
{% elif issue.department.name == 'Sanitation & Waste Management' %} bg-success
|
||||
{% elif issue.department.name == 'Public Safety' %} bg-danger
|
||||
{% elif issue.department.name == 'Water & Sewage' %} bg-info text-dark
|
||||
{% elif issue.department.name == 'Parks & Recreation' %} bg-warning text-dark
|
||||
{% elif issue.department.name == 'Electricity & Utilities' %} bg-dark
|
||||
{% elif issue.department.name == 'Environmental Services' %} bg-secondary
|
||||
{% elif issue.department.name == 'Public Works' %} bg-teal text-white
|
||||
{% else %} bg-light text-dark
|
||||
{% endif %} mb-2">
|
||||
{{ issue.department.name|default:"General" }}
|
||||
</span>
|
||||
<h5 class="card-title">{{ issue.title|truncatewords:5 }}</h5>
|
||||
<p class="card-text text-muted">{{ issue.description|truncatewords:15 }}</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted"><i class="fas fa-map-marker-alt me-1"></i> {{ issue.location|truncatewords:2 }}</small>
|
||||
|
||||
<!-- Vote Button -->
|
||||
<div class="vote-section">
|
||||
{% if user.is_authenticated %}
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary vote-btn {% if issue.user_has_voted %}active{% endif %}"
|
||||
data-issue-id="{{ issue.id }}">
|
||||
<i class="fas fa-thumbs-up"></i>
|
||||
<span class="vote-count">{{ issue.vote_count }}</span>
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="{% url 'login' %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-thumbs-up"></i>
|
||||
<span class="vote-count">{{ issue.vote_count }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent">
|
||||
<small
|
||||
class="text-{% if issue.status == 'resolved' %}success{% elif issue.status == 'in_progress' %}warning{% else %}info{% endif %}">
|
||||
<i
|
||||
class="fas fa-{% if issue.status == 'resolved' %}check-circle{% elif issue.status == 'in_progress' %}tasks{% else %}clock{% endif %} me-1"></i>
|
||||
{{ issue.get_status_display }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
||||
<h4>No issues reported yet</h4>
|
||||
<p class="text-muted">Be the first to report an issue in your community!</p>
|
||||
<a href="{% url 'login' %}" class="btn btn-primary">Get Started</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Call to Action -->
|
||||
<section class="bg-primary text-white py-5">
|
||||
<div class="container text-center">
|
||||
<h2 class="fw-bold mb-4">Ready to make a difference in your community?</h2>
|
||||
<p class="lead mb-4">Join {{ active_users|default:"thousands of" }} citizens who are actively improving their
|
||||
neighborhoods.</p>
|
||||
{% if user.is_authenticated %}
|
||||
<a href="{% url 'citizen_dashboard' %}" class="btn btn-light btn-lg px-4">Report an Issue</a>
|
||||
{% else %}
|
||||
<a href="{% url 'login' %}" class="btn btn-light btn-lg px-4">Get Started Now</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// Vote functionality
|
||||
$('.vote-btn').click(function () {
|
||||
const issueId = $(this).data('issue-id');
|
||||
const button = $(this);
|
||||
|
||||
// Show loading state
|
||||
const originalHtml = button.html();
|
||||
button.prop('disabled', true);
|
||||
button.html('<i class="fas fa-spinner fa-spin"></i>');
|
||||
|
||||
// Send vote request
|
||||
$.ajax({
|
||||
url: "{% url 'vote_issue' 0 %}".replace('0', issueId),
|
||||
type: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
// Update button state
|
||||
if (response.voted) {
|
||||
button.addClass('active');
|
||||
} else {
|
||||
button.removeClass('active');
|
||||
}
|
||||
|
||||
// Update vote count
|
||||
button.find('.vote-count').text(response.vote_count);
|
||||
|
||||
// Update button text
|
||||
button.html('<i class="fas fa-thumbs-up"></i> <span class="vote-count">' + response.vote_count + '</span>');
|
||||
} else {
|
||||
alert('Error: ' + response.error);
|
||||
button.html(originalHtml);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
alert('Error voting. Please try again.');
|
||||
button.html(originalHtml);
|
||||
},
|
||||
complete: function () {
|
||||
button.prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.vote-btn.active {
|
||||
background-color: #0d6efd;
|
||||
color: white;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.vote-btn:hover:not(:disabled) {
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.vote-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,54 @@
|
||||
{% extends "core/base.html" %}
|
||||
|
||||
{% block title %}Login{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="card-title text-center mb-4">Login to Your Account</h2>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="id_username" class="form-label">Username</label>
|
||||
<input type="text" name="username" class="form-control" id="id_username" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="id_password" class="form-label">Password</label>
|
||||
<input type="password" name="password" class="form-control" id="id_password" required>
|
||||
</div>
|
||||
|
||||
<div class="d-grid mb-3">
|
||||
<button type="submit" class="btn btn-primary btn-lg">Login</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="#" class="text-decoration-none">Forgot password?</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<p class="text-center text-muted">
|
||||
Don't have an account? <a href="{% url 'register' %}" class="text-decoration-none">Register here</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,62 @@
|
||||
{% extends "core/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||
<h3>Manage Citizens</h3>
|
||||
<a href="{% url 'superadmin_dashboard' %}" class="btn btn-light btn-sm">Back to Dashboard</a>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
{% if citizens %}
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>No.</th>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Phone No.</th>
|
||||
<th>Date Joined</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for citizen in citizens %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td>{{ citizen.username }}</td>
|
||||
<td>{{ citizen.email }}</td>
|
||||
<td>{{ citizen.phone }}</td>
|
||||
<td>{{ citizen.date_joined|date:"M d, Y" }}</td>
|
||||
<td>
|
||||
{% if citizen.is_currently_banned %}
|
||||
<span class="badge bg-danger">Banned until {{ citizen.banned_until|date:"M d, Y" }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if citizen.is_currently_banned %}
|
||||
<a href="{% url 'unban_user' citizen.id %}" class="btn btn-sm btn-success">
|
||||
<i class="fas fa-unlock"></i> Unban
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'ban_user' citizen.id %}" class="btn btn-sm btn-danger">
|
||||
<i class="fas fa-ban"></i> Ban
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% else %}
|
||||
<p class="text-muted">No citizen users found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,103 @@
|
||||
{% extends "core/base.html" %}
|
||||
{% block title %}Register as Citizen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="card-title text-center mb-4">Create Citizen Account</h2>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Messages block - FIXED SYNTAX #}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{# Form errors block - FIXED SYNTAX #}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger">
|
||||
<strong>Error!</strong> Please correct the following:
|
||||
<ul>
|
||||
{% for field, errors in form.errors.items %}
|
||||
{% for error in errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="id_username" class="form-label">Username</label>
|
||||
{{ form.username }}
|
||||
<small class="text-muted">Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="id_email" class="form-label">Email</label>
|
||||
{{ form.email }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="id_phone" class="form-label">Phone (Optional)</label>
|
||||
{{ form.phone }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="id_password1" class="form-label">Password</label>
|
||||
{{ form.password1 }}
|
||||
<small class="text-muted">
|
||||
<ul>
|
||||
<li>Your password can't be too similar to your other personal information.</li>
|
||||
<li>Your password must contain at least 8 characters.</li>
|
||||
<li>Your password can't be a commonly used password.</li>
|
||||
<li>Your password can't be entirely numeric.</li>
|
||||
</ul>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="id_password2" class="form-label">Password Confirmation</label>
|
||||
{{ form.password2 }}
|
||||
<small class="text-muted">Enter the same password as before, for verification.</small>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg">Register</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<p class="text-center text-muted">
|
||||
Already have an account? <a href="{% url 'login' %}" class="text-decoration-none">Login here</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// Add Bootstrap classes to all input fields
|
||||
$('input:not([type="checkbox"]):not([type="radio"])').addClass('form-control');
|
||||
$('input[type="checkbox"]').removeClass('form-control').addClass('form-check-input');
|
||||
$('input[type="radio"]').removeClass('form-control').addClass('form-check-input');
|
||||
|
||||
// Add Bootstrap classes to select fields
|
||||
$('select').addClass('form-select');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,91 @@
|
||||
{% extends "core/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<h3>Analytics & Reports</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<h5>Total Issues Reported:
|
||||
<span class="badge bg-dark">{{ total_issues }}</span>
|
||||
</h5>
|
||||
|
||||
<canvas id="statusChart" height="120" class="mt-4"></canvas>
|
||||
<canvas id="deptChart" height="120" class="mt-4"></canvas>
|
||||
<canvas id="citizenChart" height="120" class="mt-4"></canvas>
|
||||
<canvas id="trendChart" height="120" class="mt-4"></canvas>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
{# Safely pass JSON to JS #}
|
||||
{{ status_counts|json_script:"status-data" }}
|
||||
{{ top_departments|json_script:"dept-data" }}
|
||||
{{ top_citizens|json_script:"citizen-data" }}
|
||||
{{ issues_last_30_days|json_script:"trend-data" }}
|
||||
|
||||
<script>
|
||||
const statusCounts = JSON.parse(document.getElementById("status-data").textContent);
|
||||
const deptCounts = JSON.parse(document.getElementById("dept-data").textContent);
|
||||
const citizenCounts = JSON.parse(document.getElementById("citizen-data").textContent);
|
||||
const trendCounts = JSON.parse(document.getElementById("trend-data").textContent);
|
||||
|
||||
// Status chart
|
||||
new Chart(document.getElementById('statusChart'), {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: statusCounts.map(item => item.status),
|
||||
datasets: [{
|
||||
data: statusCounts.map(item => item.count),
|
||||
backgroundColor: ['#dc3545', '#0dcaf0', '#ffc107', '#198754']
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Department chart
|
||||
new Chart(document.getElementById('deptChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: deptCounts.map(item => item.department__name),
|
||||
datasets: [{
|
||||
label: 'Issues by Department',
|
||||
data: deptCounts.map(item => item.count),
|
||||
backgroundColor: '#0d6efd'
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Citizens chart
|
||||
new Chart(document.getElementById('citizenChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: citizenCounts.map(item => item.reporter__username),
|
||||
datasets: [{
|
||||
label: 'Top Citizens',
|
||||
data: citizenCounts.map(item => item.count),
|
||||
backgroundColor: '#6610f2'
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Issues over time
|
||||
new Chart(document.getElementById('trendChart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: trendCounts.map(item => item.day),
|
||||
datasets: [{
|
||||
label: 'Issues Last 30 Days',
|
||||
data: trendCounts.map(item => item.count),
|
||||
borderColor: '#fd7e14',
|
||||
fill: false,
|
||||
tension: 0.2
|
||||
}]
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,289 @@
|
||||
{% extends "core/base.html" %}
|
||||
{% block title %}Citizen Dashboard - CivixFix{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.dashboard-card {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.dashboard-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.map-container {
|
||||
height: 300px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 2px dashed #dee2e6;
|
||||
}
|
||||
.issue-status {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row">
|
||||
<!-- Sidebar - Quick Actions -->
|
||||
<div class="col-md-3">
|
||||
<div class="card shadow-sm dashboard-card">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Welcome, {{ user.username }}</h5>
|
||||
<p class="text-muted">Citizen Dashboard</p>
|
||||
|
||||
<div class="d-grid gap-2 mb-3">
|
||||
<button class="btn btn-primary btn-lg" data-bs-toggle="modal" data-bs-target="#reportIssueModal">
|
||||
<i class="fas fa-plus me-2"></i>Report New Issue
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h6>Quick Stats</h6>
|
||||
<div class="row text-center">
|
||||
<div class="col-6">
|
||||
<div class="bg-light p-2 rounded">
|
||||
<h4 class="mb-0 text-primary">{{ user_issues.count }}</h4>
|
||||
<small>My Reports</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="bg-light p-2 rounded">
|
||||
<h4 class="mb-0 text-success">
|
||||
{{ resolved_count }}
|
||||
</h4>
|
||||
<small>Resolved</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content - Recent Issues -->
|
||||
<div class="col-md-9">
|
||||
<!-- Welcome Message -->
|
||||
<div class="card shadow-sm dashboard-card mb-4">
|
||||
<div class="card-body">
|
||||
<h4>Welcome to CivixFix!</h4>
|
||||
<p class="text-muted mb-0">
|
||||
Report community issues, track their progress, and help make your neighborhood better.
|
||||
Start by reporting an issue using the button on the left.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- My Recent Issues -->
|
||||
<div class="card shadow-sm dashboard-card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">My Recent Reports</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% for issue in user_issues %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="card-title mb-1">{{ issue.title }}</h6>
|
||||
<p class="card-text text-muted mb-2 small">
|
||||
{{ issue.description|truncatewords:15 }}
|
||||
</p>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<span class="badge
|
||||
{% if issue.department.name == 'Roads & Transportation' %} bg-primary
|
||||
{% elif issue.department.name == 'Sanitation & Waste Management' %} bg-success
|
||||
{% elif issue.department.name == 'Public Safety' %} bg-danger
|
||||
{% elif issue.department.name == 'Water & Sewage' %} bg-info text-dark
|
||||
{% elif issue.department.name == 'Parks & Recreation' %} bg-warning text-dark
|
||||
{% elif issue.department.name == 'Electricity & Utilities' %} bg-dark
|
||||
{% elif issue.department.name == 'Environmental Services' %} bg-secondary
|
||||
{% elif issue.department.name == 'Public Works' %} bg-teal text-white
|
||||
{% else %} bg-light text-dark
|
||||
{% endif %}">
|
||||
{{ issue.department.name|default:"No Department" }}
|
||||
</span>
|
||||
|
||||
<!-- 🔹 Status badge -->
|
||||
<span class="badge bg-{% if issue.status == 'resolved' %}success
|
||||
{% elif issue.status == 'in_progress' %}warning
|
||||
{% else %}primary{% endif %} issue-status">
|
||||
{{ issue.get_status_display }}
|
||||
</span>
|
||||
|
||||
<small class="text-muted">{{ issue.created_at|date:"M d, Y" }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if issue.photo %}
|
||||
<div class="ms-3">
|
||||
<img src="{{ issue.photo.url }}" alt="Issue photo" class="img-fluid rounded"
|
||||
style="max-height: 60px; max-width: 80px;">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
||||
<h5>No issues reported yet</h5>
|
||||
<p class="text-muted">Click "Report New Issue" to get started!</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Report Issue Modal -->
|
||||
<div class="modal fade" id="reportIssueModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Report New Issue</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="issueForm" method="post" enctype="multipart/form-data" action="{% url 'report_issue' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show mb-3">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Issue Title*</label>
|
||||
{{ issue_form.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description*</label>
|
||||
{{ issue_form.description }}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Location*</label>
|
||||
{{ issue_form.location }}
|
||||
<small class="text-muted">Click on the map to select location</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Photo</label>
|
||||
{{ issue_form.photo }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden latitude/longitude fields -->
|
||||
{{ issue_form.latitude }}
|
||||
{{ issue_form.longitude }}
|
||||
|
||||
<div class="map-container mb-3">
|
||||
<div id="map" style="height: 100%;"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" form="issueForm" class="btn btn-primary">
|
||||
<i class="fas fa-paper-plane me-2"></i>Report Issue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
let map = null, marker = null;
|
||||
|
||||
// Lazy load Leaflet only when modal opens
|
||||
const ensureLeafletLoaded = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (window.L) return resolve();
|
||||
|
||||
const css = document.createElement("link");
|
||||
css.rel = "stylesheet";
|
||||
css.href = "https://unpkg.com/leaflet@1.9.3/dist/leaflet.css";
|
||||
document.head.appendChild(css);
|
||||
|
||||
const js = document.createElement("script");
|
||||
js.src = "https://unpkg.com/leaflet@1.9.3/dist/leaflet.js";
|
||||
js.onload = resolve;
|
||||
document.body.appendChild(js);
|
||||
});
|
||||
};
|
||||
|
||||
const initMap = () => {
|
||||
if (map) return;
|
||||
|
||||
map = L.map("map", {
|
||||
center: [12.9716, 77.5946],
|
||||
zoom: 13
|
||||
});
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
map.on("click", (e) => {
|
||||
if (!marker) {
|
||||
marker = L.marker(e.latlng).addTo(map);
|
||||
} else {
|
||||
marker.setLatLng(e.latlng);
|
||||
}
|
||||
marker.bindPopup("Selected location").openPopup();
|
||||
|
||||
// update hidden fields immediately
|
||||
document.getElementById("id_latitude").value = e.latlng.lat.toFixed(6);
|
||||
document.getElementById("id_longitude").value = e.latlng.lng.toFixed(6);
|
||||
|
||||
// defer reverse geocode
|
||||
requestIdleCallback(() => {
|
||||
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${e.latlng.lat}&lon=${e.latlng.lng}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
document.getElementById("id_location").value =
|
||||
data?.display_name ||
|
||||
`Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}`;
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById("id_location").value =
|
||||
`Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}`;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const modal = document.getElementById("reportIssueModal");
|
||||
modal.addEventListener("shown.bs.modal", async () => {
|
||||
await ensureLeafletLoaded();
|
||||
initMap();
|
||||
|
||||
// fix resize/centering after animation
|
||||
setTimeout(() => {
|
||||
map.invalidateSize();
|
||||
if (marker) map.setView(marker.getLatLng(), 15);
|
||||
}, 200);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,80 @@
|
||||
{% extends "core/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h3>Department Dashboard</h3>
|
||||
<p>
|
||||
Welcome, {{ request.user.username }}. Department:
|
||||
{% for dept in departments %}
|
||||
<span class="badge bg-light text-dark">{{ dept.name }}</span>
|
||||
{% empty %}
|
||||
<span class="text-muted">No department assigned.</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if issues %}
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Title</th>
|
||||
<th>Reported By</th>
|
||||
<th>Location</th>
|
||||
<th>Status</th>
|
||||
<th>Created At</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for issue in issues %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td>{{ issue.title }}</td>
|
||||
<td>{{ issue.reporter.username }}</td>
|
||||
<td>{{ issue.location }}</td>
|
||||
<td>
|
||||
{% if issue.status == "reported" %}
|
||||
<span class="badge bg-danger">Reported</span>
|
||||
{% elif issue.status == "acknowledged" %}
|
||||
<span class="badge bg-info text-dark">Acknowledged</span>
|
||||
{% elif issue.status == "in_progress" %}
|
||||
<span class="badge bg-warning text-dark">In Progress</span>
|
||||
{% elif issue.status == "resolved" %}
|
||||
<span class="badge bg-success">Resolved</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Unknown</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>{{ issue.created_at|date:"M d, Y H:i" }}</td>
|
||||
<td>
|
||||
{% if issue.status in "reported,acknowledged" %}
|
||||
<form method="post" action="{% url 'update_issue_status' issue.id %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="status" value="in_progress">
|
||||
<button type="submit" class="btn btn-sm btn-warning">In Progress</button>
|
||||
</form>
|
||||
{% elif issue.status == "in_progress" %}
|
||||
<form method="post" action="{% url 'update_issue_status' issue.id %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="status" value="resolved">
|
||||
<button type="submit" class="btn btn-sm btn-success">Resolved</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-muted">No actions</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-muted">No issues assigned to your departments yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% extends "core/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h3>Super Admin Dashboard</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Welcome, {{ request.user.username }} <i class="fas fa-crown text-warning fw-bold"></i></p>
|
||||
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<i class="fas fa-users me-2 text-primary"></i>
|
||||
<a href="{% url 'manage_users' %}">Manage Users</a>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<i class="fas fa-building me-2 text-success"></i>
|
||||
<a href="{% url 'manage_departments' %}">Manage Departments</a>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<i class="fas fa-exclamation-circle me-2 text-danger"></i>
|
||||
<a href="{% url 'manage_issues' %}">Manage Issues</a>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<i class="fas fa-chart-line me-2 text-warning"></i>
|
||||
<a href="{% url 'superadmin_reports' %}">View Analytics & Reports</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,77 @@
|
||||
{% extends "core/base.html" %}
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||
<h3>{{ department.name }} Department</h3>
|
||||
<a href="{% url 'manage_departments' %}" class="btn btn-light btn-sm">Back</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<!-- Department Users -->
|
||||
<h5>Department Users</h5>
|
||||
{% if users %}
|
||||
<ul class="list-group mb-3">
|
||||
{% for user in users %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<strong>{{ user.username }}</strong> — {{ user.email }}
|
||||
{% if department.admin and department.admin.id == user.id %}
|
||||
<span class="badge bg-success ms-2">Admin</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
<!-- Assign as Admin button -->
|
||||
{% if not department.admin or department.admin.id != user.id %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="assign_admin" value="1">
|
||||
<input type="hidden" name="admin_user_id" value="{{ user.id }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">
|
||||
Make Admin
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-muted">No users registered in this department yet.</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Remove Admin -->
|
||||
{% if department.admin %}
|
||||
<form method="post" class="mt-2">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="remove_admin" value="1">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
Remove Admin
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Register New User -->
|
||||
<h5>Register New User for {{ department.name }}</h5>
|
||||
<form method="post" class="row g-2">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="create_user" value="1">
|
||||
<div class="col-md-3">
|
||||
<input type="text" name="username" class="form-control" placeholder="Username" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="email" name="email" class="form-control" placeholder="Email">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="password" name="password" class="form-control" placeholder="Password" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="submit" class="btn btn-primary w-100">Create User</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,48 @@
|
||||
<!-- core/templates/core/manage_departments.html -->
|
||||
{% extends "core/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
||||
<h3>Manage Departments</h3>
|
||||
<a href="{% url 'superadmin_dashboard' %}" class="btn btn-light btn-sm">Back to Dashboard</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- List departments -->
|
||||
{% if departments %}
|
||||
<ul class="list-group mb-3">
|
||||
{% for dept in departments %}
|
||||
<a href="{% url 'department_detail' dept.pk %}"
|
||||
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<strong>{{ dept.name }}</strong><br>
|
||||
<small class="text-muted">{{ dept.description }}</small>
|
||||
</span>
|
||||
<span class="badge bg-primary rounded-pill">Manage</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-muted">No departments added yet.</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Add new department form -->
|
||||
<form method="post" class="mt-3">
|
||||
{% csrf_token %}
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<input type="text" name="name" class="form-control" placeholder="Department name" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<input type="text" name="description" class="form-control" placeholder="Description (optional)">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-success w-100"><i class="fas fa-plus me-2"></i>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,69 @@
|
||||
{% extends "core/base.html" %}
|
||||
{% load humanize %}
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<!-- Issue details -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
{% if issue.photo %}
|
||||
<div class="text-center my-3">
|
||||
<img src="{{ issue.photo.url }}" class="img-fluid rounded shadow-sm" style="max-height: 400px; object-fit: contain;"
|
||||
alt="{{ issue.title }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
<h3>{{ issue.title }}</h3>
|
||||
<p class="text-muted">{{ issue.description }}</p>
|
||||
<p><i class="fas fa-map-marker-alt"></i> {{ issue.location }}</p>
|
||||
<span class="badge bg-info">{{ issue.get_status_display }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comments Section -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Comments</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% for comment in comments %}
|
||||
<div class="mb-3">
|
||||
<strong>{{ comment.user.username }}</strong>
|
||||
<small class="text-muted">{{ comment.created_at|naturaltime }}</small>
|
||||
<p>{{ comment.content }}</p>
|
||||
|
||||
<!-- Replies -->
|
||||
<div class="ms-4">
|
||||
{% for reply in comment.replies.all %}
|
||||
<div class="mb-2">
|
||||
<strong>{{ reply.user.username }}</strong>
|
||||
<small class="text-muted">{{ reply.created_at|naturaltime }}</small>
|
||||
<p>{{ reply.content }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Reply form -->
|
||||
{% if user.is_authenticated %}
|
||||
<form method="post" action="{% url 'add_comment' issue.id comment.id %}">
|
||||
{% csrf_token %}
|
||||
<input type="content" name="content" class="form-control form-control-sm" placeholder="Reply...">
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-muted">No comments yet. Be the first!</p>
|
||||
{% endfor %}
|
||||
|
||||
<!-- New comment form -->
|
||||
{% if user.is_authenticated %}
|
||||
<form method="post" action="{% url 'add_comment' issue.id %}" class="mt-3">
|
||||
{% csrf_token %}
|
||||
<textarea name="content" class="form-control" placeholder="Write a comment..."></textarea>
|
||||
<button type="submit" class="btn btn-primary btn-sm mt-2">Post</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p><a href="{% url 'login' %}">Login</a> to comment.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,81 @@
|
||||
{% extends "core/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-header bg-warning text-black d-flex justify-content-between align-items-center">
|
||||
<h3>Manage Issues</h3>
|
||||
<a href="{% url 'superadmin_dashboard' %}" class="btn btn-light btn-sm">Back to Dashboard</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if issues %}
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>No.</th>
|
||||
<th>Title</th>
|
||||
<th>Reported By</th>
|
||||
<th>Status</th>
|
||||
<th>Created At</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for issue in issues %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td>{{ issue.title }}</td>
|
||||
<td>{{ issue.reporter.username }}</td>
|
||||
<td>
|
||||
{% if issue.status == "reported" %}
|
||||
<span class="badge bg-danger">Reported</span>
|
||||
{% elif issue.status == "acknowledged" %}
|
||||
<span class="badge bg-info text-dark">Acknowledged</span>
|
||||
{% elif issue.status == "in_progress" %}
|
||||
<span class="badge bg-warning text-dark">In Progress</span>
|
||||
{% elif issue.status == "resolved" %}
|
||||
<span class="badge bg-success">Resolved</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Unknown</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ issue.created_at|date:"M d, Y H:i" }}</td>
|
||||
<td>
|
||||
{% if issue.department %}
|
||||
<span class="fw-bold text-primary">{{ issue.department.name }}</span>
|
||||
{% else %}
|
||||
<!-- Assign Department Form -->
|
||||
<form method="post" action="" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="issue_id" value="{{ issue.id }}">
|
||||
<select name="department" class="form-select form-select-sm d-inline w-auto">
|
||||
<option value="">— Select Department —</option>
|
||||
{% for dept in departments %}
|
||||
<option value="{{ dept.id }}">{{ dept.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm btn-primary">Assign</button>
|
||||
</form>
|
||||
|
||||
<!-- Report Fake Button -->
|
||||
<a href="{% url 'delete_fake_issue' issue.id %}" class="btn btn-sm btn-danger ms-1"
|
||||
onclick="return confirm('Are you sure you want to delete this issue as FAKE?');">
|
||||
<i class="fas fa-ban"></i> Report Fake
|
||||
</a>
|
||||
<a href="{% url 'delete_issue' issue.id %}" class="btn btn-sm btn-outline-danger ms-1"
|
||||
onclick="return confirm('Are you sure you want to permanently delete this issue? This cannot be undone.');">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-muted">No issues reported yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,251 @@
|
||||
{% extends "core/base.html" %}
|
||||
{% load humanize %}
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<!-- Header + Filter -->
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-3">
|
||||
<h2 class="fw-bold mb-0">All Reported Issues</h2>
|
||||
<form method="get" class="d-flex flex-wrap gap-2">
|
||||
<select name="status" class="form-select">
|
||||
<option value="">All Status</option>
|
||||
<option value="reported" {% if request.GET.status|default:'' == "reported" %}selected{% endif %}>Reported
|
||||
</option>
|
||||
<option value="acknowledged" {% if request.GET.status|default:'' == "acknowledged" %}selected{% endif %}>
|
||||
Acknowledged
|
||||
</option>
|
||||
<option value="in_progress" {% if request.GET.status|default:'' == "in_progress" %}selected{% endif %}>In
|
||||
Progress
|
||||
</option>
|
||||
<option value="resolved" {% if request.GET.status|default:'' == "resolved" %}selected{% endif %}>Resolved
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<select name="department" class="form-select">
|
||||
<option value="">All Departments</option>
|
||||
{% for dept in departments %}
|
||||
{% with dept.id|stringformat:"s" as dept_id %}
|
||||
<option value="{{ dept_id }}" {% if request.GET.department|default:'' == dept_id %}selected{% endif %}>
|
||||
{{ dept.name }}
|
||||
</option>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-filter me-1"></i> Filter
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Issue Cards -->
|
||||
<div class="row g-4">
|
||||
{% for issue in issues %}
|
||||
<div class="col-md-4">
|
||||
<div class="card issue-card h-100 shadow-sm border-0">
|
||||
{% if issue.photo %}
|
||||
<img src="{{ issue.photo.url }}" class="card-img-top issue-image" alt="{{ issue.title }}"
|
||||
style="height: 200px; object-fit: cover; cursor: pointer" data-bs-toggle="modal"
|
||||
data-bs-target="#imageModal" data-image="{{ issue.photo.url }}" />
|
||||
{% else %}
|
||||
<img src="https://via.placeholder.com/300x200/6c757d/ffffff?text=No+Image" class="card-img-top"
|
||||
alt="No image" />
|
||||
{% endif %}
|
||||
|
||||
<div class="card-body d-flex flex-column">
|
||||
<!-- Department Badge -->
|
||||
<span class="badge
|
||||
{% if issue.department.name == 'Roads & Transportation' %} bg-primary
|
||||
{% elif issue.department.name == 'Sanitation & Waste Management' %} bg-success
|
||||
{% elif issue.department.name == 'Public Safety' %} bg-danger
|
||||
{% elif issue.department.name == 'Water & Sewage' %} bg-info text-dark
|
||||
{% elif issue.department.name == 'Parks & Recreation' %} bg-warning text-dark
|
||||
{% elif issue.department.name == 'Electricity & Utilities' %} bg-dark
|
||||
{% elif issue.department.name == 'Environmental Services' %} bg-secondary
|
||||
{% elif issue.department.name == 'Public Works' %} bg-teal text-white
|
||||
{% else %} bg-light text-dark
|
||||
{% endif %} mb-2">
|
||||
{{ issue.department.name|default:"General" }}
|
||||
</span>
|
||||
|
||||
|
||||
<!-- Title + Description -->
|
||||
<h5 class="card-title">{{ issue.title|truncatewords:6 }}</h5>
|
||||
<p class="card-text text-muted flex-grow-1">
|
||||
{{ issue.description|truncatewords:18 }}
|
||||
</p>
|
||||
|
||||
<!-- Location + Vote -->
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-map-marker-alt me-1"></i> {{ issue.location|truncatewords:2 }}
|
||||
</small>
|
||||
<div class="vote-section">
|
||||
{% if user.is_authenticated %}
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary vote-btn {% if issue.user_has_voted %}active{% endif %}"
|
||||
data-issue-id="{{ issue.id }}">
|
||||
<i class="fas fa-thumbs-up"></i>
|
||||
<span class="vote-count">{{ issue.vote_count }}</span>
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="{% url 'login' %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-thumbs-up"></i>
|
||||
<span class="vote-count">{{ issue.vote_count }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-transparent d-flex justify-content-between align-items-center">
|
||||
<small
|
||||
class="text-{% if issue.status == 'resolved' %}success{% elif issue.status == 'in_progress' %}warning{% else %}info{% endif %}">
|
||||
<i
|
||||
class="fas fa-{% if issue.status == 'resolved' %}check-circle{% elif issue.status == 'in_progress' %}tasks{% else %}clock{% endif %} me-1"></i>
|
||||
{{ issue.get_status_display }}
|
||||
</small>
|
||||
|
||||
<a href="{% url 'issue_detail' issue.id %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-comments"></i> Comments
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
||||
<h4>No issues reported yet</h4>
|
||||
<p class="text-muted">
|
||||
Be the first to report an issue in your community!
|
||||
</p>
|
||||
<a href="{% url 'citizen_dashboard' %}" class="btn btn-primary">Report an Issue</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="imageModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content bg-light">
|
||||
<div class="modal-body text-center p-0">
|
||||
<img id="modalImage" src="" class="img-fluid zoomable" alt="Zoomed image" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Styling -->
|
||||
<style>
|
||||
.issue-card {
|
||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.issue-card:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.vote-btn.active {
|
||||
background-color: #0d6efd;
|
||||
color: white;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.vote-btn:hover:not(:disabled) {
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.vote-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#modalImage {
|
||||
max-height: 85vh;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
transition: transform 0.3s ease-in-out;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
#modalImage.zoomed {
|
||||
transform: scale(1.5);
|
||||
cursor: zoom-out;
|
||||
}
|
||||
</style>
|
||||
{% endblock %} {% block extra_js %}
|
||||
<script>
|
||||
// CSRF Helper
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== "") {
|
||||
document.cookie.split(";").forEach((cookie) => {
|
||||
cookie = cookie.trim();
|
||||
if (cookie.startsWith(name + "=")) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
}
|
||||
});
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
const csrftoken = getCookie("csrftoken");
|
||||
|
||||
// Handle vote clicks
|
||||
document.addEventListener("click", function (e) {
|
||||
const btn = e.target.closest(".vote-btn");
|
||||
if (!btn) return;
|
||||
|
||||
e.preventDefault();
|
||||
const issueId = btn.dataset.issueId;
|
||||
if (!issueId) return;
|
||||
|
||||
const originalHTML = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
|
||||
fetch("{% url 'vote_issue' 0 %}".replace("0", issueId), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRFToken": csrftoken,
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (!data.success) throw new Error(data.error || "Vote failed");
|
||||
btn.classList.toggle("active", data.voted);
|
||||
btn.innerHTML =
|
||||
'<i class="fas fa-thumbs-up"></i> <span class="vote-count">' +
|
||||
data.vote_count +
|
||||
"</span>";
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
alert("Error: " + err.message);
|
||||
btn.innerHTML = originalHTML;
|
||||
})
|
||||
.finally(() => {
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// 🔥 Handle image modal
|
||||
const imageModal = document.getElementById("imageModal");
|
||||
const modalImage = document.getElementById("modalImage");
|
||||
|
||||
document.querySelectorAll(".issue-image").forEach((img) => {
|
||||
img.addEventListener("click", function () {
|
||||
const src = this.getAttribute("data-image");
|
||||
modalImage.src = src;
|
||||
modalImage.classList.remove("zoomed");
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle zoom on click inside modal
|
||||
modalImage.addEventListener("click", function () {
|
||||
this.classList.toggle("zoomed");
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user