initial commit
This commit is contained in:
@@ -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