Autosave: 20260218-053623
This commit is contained in:
parent
0f9878cadc
commit
800e9db512
Binary file not shown.
Binary file not shown.
@ -61,6 +61,7 @@ INSTALLED_APPS = [
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
@ -141,6 +142,15 @@ USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
LANGUAGES = [
|
||||
('en', 'English'),
|
||||
('ne', 'Nepali'),
|
||||
]
|
||||
|
||||
LOCALE_PATHS = [
|
||||
BASE_DIR / 'locale',
|
||||
]
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
|
||||
@ -21,6 +21,7 @@ from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("i18n/", include("django.conf.urls.i18n")),
|
||||
path("", include("core.urls")),
|
||||
]
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,58 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-18 05:21
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0008_userprofile'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bloodrequest',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='blood_requests', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='donor',
|
||||
name='user',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='donor_profile', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DonationEvent',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('is_completed', models.BooleanField(default=False)),
|
||||
('donor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='donations', to='core.donor')),
|
||||
('donor_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='my_donations', to=settings.AUTH_USER_MODEL)),
|
||||
('request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='donations', to='core.bloodrequest')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Feedback',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('content', models.TextField()),
|
||||
('rating', models.PositiveIntegerField(default=5)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('message', models.TextField()),
|
||||
('is_read', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -46,6 +46,7 @@ class VaccineRecord(models.Model):
|
||||
return f"{self.vaccine_name} - Dose {self.dose_number} for {self.user.username}"
|
||||
|
||||
class Donor(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='donor_profile')
|
||||
name = models.CharField(max_length=100)
|
||||
blood_group = models.CharField(max_length=5, choices=BLOOD_GROUPS)
|
||||
district = models.CharField(max_length=100, default='Kathmandu')
|
||||
@ -71,6 +72,7 @@ class BloodRequest(models.Model):
|
||||
('URGENT', 'Urgent'),
|
||||
('NORMAL', 'Normal'),
|
||||
]
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blood_requests', null=True, blank=True)
|
||||
patient_name = models.CharField(max_length=100)
|
||||
blood_group = models.CharField(max_length=5, choices=BLOOD_GROUPS)
|
||||
location = models.CharField(max_length=255)
|
||||
@ -105,3 +107,31 @@ class BloodBank(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class DonationEvent(models.Model):
|
||||
donor = models.ForeignKey(Donor, on_delete=models.CASCADE, related_name='donations')
|
||||
request = models.ForeignKey(BloodRequest, on_delete=models.CASCADE, related_name='donations')
|
||||
donor_user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='my_donations')
|
||||
date = models.DateTimeField(default=timezone.now)
|
||||
is_completed = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"Donation by {self.donor.name} for {self.request.patient_name}"
|
||||
|
||||
class Notification(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications')
|
||||
message = models.TextField()
|
||||
is_read = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Notification for {self.user.username}: {self.message[:20]}..."
|
||||
|
||||
class Feedback(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='feedbacks')
|
||||
content = models.TextField()
|
||||
rating = models.PositiveIntegerField(default=5)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Feedback from {self.user.username} - {self.rating} stars"
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="{{ LANGUAGE_CODE }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@ -178,6 +179,78 @@
|
||||
#sidebar.active { margin-left: 0; }
|
||||
}
|
||||
|
||||
/* SOS Floating Button */
|
||||
.sos-btn {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
background-color: var(--pulse-red);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 4px 15px rgba(230, 57, 70, 0.4);
|
||||
z-index: 9999;
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
font-weight: 800;
|
||||
font-size: 1.1rem;
|
||||
border: 3px solid white;
|
||||
}
|
||||
|
||||
.sos-btn:hover {
|
||||
transform: scale(1.1);
|
||||
color: white;
|
||||
box-shadow: 0 6px 20px rgba(230, 57, 70, 0.6);
|
||||
}
|
||||
|
||||
.sos-btn .pulse-ring {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: var(--pulse-red);
|
||||
opacity: 0.6;
|
||||
animation: sos-pulse 2s infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.sos-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sos-text {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.sos-number {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
@keyframes sos-pulse {
|
||||
0% { transform: scale(1); opacity: 0.6; }
|
||||
100% { transform: scale(1.8); opacity: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.sos-btn {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</style>
|
||||
</head>
|
||||
@ -195,25 +268,19 @@
|
||||
</div>
|
||||
|
||||
<ul class="sidebar-menu mt-4">
|
||||
<li class="{% if request.resolver_match.url_name == 'home' %}active{% endif %}"><a href="{% url 'home' %}"><i class="bi bi-grid-1x2-fill"></i> Dashboard</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'donor_list' %}active{% endif %}"><a href="{% url 'donor_list' %}"><i class="bi bi-people-fill"></i> Donors</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'blood_request_list' %}active{% endif %}"><a href="{% url 'blood_request_list' %}"><i class="bi bi-megaphone-fill"></i> Blood Requests</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'blood_bank_list' %}active{% endif %}"><a href="{% url 'blood_bank_list' %}"><i class="bi bi-hospital-fill"></i> Blood Banks</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'live_map' %}active{% endif %}"><a href="{% url 'live_map' %}"><i class="bi bi-map text-danger"></i> Live Alerts</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'vaccination_info' %}active{% endif %}"><a href="{% url 'vaccination_info' %}"><i class="bi bi-shield-check"></i> Vaccination</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'home' %}active{% endif %}"><a href="{% url 'home' %}"><i class="bi bi-grid-1x2-fill"></i> {% trans "Dashboard" %}</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'donor_list' %}active{% endif %}"><a href="{% url 'donor_list' %}"><i class="bi bi-people-fill"></i> {% trans "Donors" %}</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'blood_request_list' %}active{% endif %}"><a href="{% url 'blood_request_list' %}"><i class="bi bi-megaphone-fill"></i> {% trans "Blood Requests" %}</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'blood_bank_list' %}active{% endif %}"><a href="{% url 'blood_bank_list' %}"><i class="bi bi-hospital-fill"></i> {% trans "Blood Banks" %}</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'live_map' %}active{% endif %}"><a href="{% url 'live_map' %}"><i class="bi bi-map text-danger"></i> {% trans "Live Alerts" %}</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'vaccination_info' %}active{% endif %}"><a href="{% url 'vaccination_info' %}"><i class="bi bi-shield-check"></i> {% trans "Vaccination" %}</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'submit_feedback' %}active{% endif %}"><a href="{% url 'submit_feedback' %}"><i class="bi bi-chat-left-heart-fill"></i> {% trans "Feedback" %}</a></li>
|
||||
{% if user.is_authenticated %}
|
||||
<li class="{% if request.resolver_match.url_name == 'profile' %}active{% endif %}"><a href="{% url 'profile' %}"><i class="bi bi-person-bounding-box"></i> My Profile</a></li>
|
||||
<li class="{% if 'vaccination_dashboard' in request.resolver_match.url_name or 'add_vaccination' in request.resolver_match.url_name %}active{% endif %}"><a href="{% url 'vaccination_dashboard' %}"><i class="bi bi-journal-check"></i> My Records</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'profile' %}active{% endif %}"><a href="{% url 'profile' %}"><i class="bi bi-person-bounding-box"></i> {% trans "My Profile" %}</a></li>
|
||||
<li class="{% if 'vaccination_dashboard' in request.resolver_match.url_name or 'add_vaccination' in request.resolver_match.url_name %}active{% endif %}"><a href="{% url 'vaccination_dashboard' %}"><i class="bi bi-journal-check"></i> {% trans "My Records" %}</a></li>
|
||||
{% endif %}
|
||||
<li><a href="/admin/"><i class="bi bi-gear-fill"></i> Settings</a></li>
|
||||
<li><a href="/admin/"><i class="bi bi-gear-fill"></i> {% trans "Settings" %}</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="position-absolute bottom-0 w-100 p-4">
|
||||
<div class="glass-card p-3 bg-opacity-10 border-danger border-opacity-25" style="background: rgba(230, 57, 70, 0.05);">
|
||||
<p class="small text-secondary mb-2">Need Help?</p>
|
||||
<a href="tel:1115" class="btn btn-sm btn-danger w-100 fw-bold">Emergency: 1115</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
@ -231,6 +298,31 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2 gap-md-4">
|
||||
<!-- Language Switcher -->
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link text-secondary dropdown-toggle d-flex align-items-center gap-1 p-0 text-decoration-none small" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-translate"></i>
|
||||
<span class="d-none d-sm-inline">{{ LANGUAGE_CODE|upper }}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_available_languages as LANGUAGES %}
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
{% for language in languages %}
|
||||
<li>
|
||||
<form action="{% url 'set_language' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
<input name="language" type="hidden" value="{{ language.code }}">
|
||||
<button type="submit" class="dropdown-item {% if language.code == LANGUAGE_CODE %}active{% endif %}">
|
||||
{{ language.name_local }} ({{ language.code|upper }})
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="location-status" class="d-none d-xl-flex align-items-center text-secondary small cursor-pointer" style="cursor: pointer;" onclick="detectLocation()">
|
||||
<i class="bi bi-geo-alt me-1"></i>
|
||||
<span id="location-text">Detect Location</span>
|
||||
@ -241,6 +333,13 @@
|
||||
</div>
|
||||
{% if user.is_authenticated %}
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<!-- Notification Bell -->
|
||||
<a href="{% url 'notifications' %}" class="text-decoration-none me-2 position-relative">
|
||||
<i class="bi bi-bell-fill fs-5 text-secondary"></i>
|
||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="font-size: 0.6rem;">
|
||||
{{ user.notifications.count }}
|
||||
</span>
|
||||
</a>
|
||||
<a href="{% url 'profile' %}" class="text-decoration-none d-flex align-items-center gap-2">
|
||||
<div class="bg-danger bg-opacity-10 rounded-circle p-1 d-flex align-items-center justify-content-center" style="width: 32px; height: 32px;">
|
||||
<i class="bi bi-person-fill text-danger"></i>
|
||||
@ -335,6 +434,39 @@
|
||||
}
|
||||
checkLocationCookie();
|
||||
</script>
|
||||
<a href="javascript:void(0);" class="sos-btn" id="sosButton" title="Single click: Request Form | Double click: Call 1115">
|
||||
<div class="pulse-ring"></div>
|
||||
<div class="sos-content">
|
||||
<span class="sos-text">SOS</span>
|
||||
<span class="sos-number">1115</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const sosBtn = document.getElementById('sosButton');
|
||||
let clickTimer = null;
|
||||
|
||||
sosBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (clickTimer === null) {
|
||||
// First click: start a timer
|
||||
clickTimer = setTimeout(function() {
|
||||
clickTimer = null;
|
||||
// Single click action: Go to request form
|
||||
window.location.href = "{% url 'request_blood' %}";
|
||||
}, 300); // 300ms window for double click
|
||||
} else {
|
||||
// Second click within 300ms: clear timer and trigger call
|
||||
clearTimeout(clickTimer);
|
||||
clickTimer = null;
|
||||
window.location.href = "tel:1115";
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -21,8 +21,8 @@
|
||||
<div class="row mb-4">
|
||||
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h2 class="brand-font mb-1">Blood Requests</h2>
|
||||
<p class="text-secondary">Current active requirements for blood in various hospitals.</p>
|
||||
<h2 class="brand-font mb-1">{% if current_status %}{{ current_status }} {% endif %}Blood Requests</h2>
|
||||
<p class="text-secondary">{% if current_status == 'Active' %}Current urgent requirements for blood.{% else %}History of blood requests in our community.{% endif %}</p>
|
||||
</div>
|
||||
<a href="{% url 'request_blood' %}" class="btn btn-danger rounded-pill px-4">Post a Request</a>
|
||||
</div>
|
||||
@ -34,7 +34,14 @@
|
||||
<div class="col-md-6 col-xl-4 mb-4">
|
||||
<div class="p-3 border rounded h-100 bg-light d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span class="urgency-badge bg-{{ req.urgency|lower }}">{{ req.urgency }}</span>
|
||||
<div class="d-flex gap-2">
|
||||
<span class="urgency-badge bg-{{ req.urgency|lower }}">{{ req.urgency }}</span>
|
||||
{% if req.status == 'Active' %}
|
||||
<span class="badge bg-success bg-opacity-10 text-success small" style="font-size: 0.7rem;">Active</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary bg-opacity-10 text-secondary small" style="font-size: 0.7rem;">{{ req.status }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="bg-danger bg-opacity-10 text-danger rounded-circle d-flex align-items-center justify-content-center fw-bold" style="width: 40px; height: 40px;">
|
||||
{{ req.blood_group }}
|
||||
</div>
|
||||
@ -44,7 +51,10 @@
|
||||
<div class="mt-auto pt-3 border-top">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="small text-muted"><i class="bi bi-clock me-1"></i> {{ req.created_at|timesince }} ago</span>
|
||||
<a href="tel:{{ req.contact_number }}" class="btn btn-sm btn-danger px-3 rounded-pill">Help Now</a>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'volunteer_for_request' req.id %}" class="btn btn-sm btn-outline-danger px-3 rounded-pill">Volunteer</a>
|
||||
<a href="tel:{{ req.contact_number }}" class="btn btn-sm btn-danger px-3 rounded-pill">Call</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -71,8 +71,9 @@
|
||||
<p class="mb-0 text-muted extra-small" style="font-size: 0.7rem;">Phone: {{ donor.phone }}</p>
|
||||
</div>
|
||||
<a href="tel:{{ donor.phone }}" class="btn btn-outline-danger btn-sm px-3 rounded-pill">Contact Now</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
34
core/templates/core/feedback.html
Normal file
34
core/templates/core/feedback.html
Normal file
@ -0,0 +1,34 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="glass-card p-4">
|
||||
<h2 class="mb-4">{% trans "Share Your Feedback" %}</h2>
|
||||
<p class="text-muted">{% trans "We value your experience. Please let us know how we can improve." %}</p>
|
||||
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="rating" class="form-label">{% trans "Rating" %}</label>
|
||||
<select name="rating" id="rating" class="form-select">
|
||||
<option value="5">⭐⭐⭐⭐⭐ (Excellent)</option>
|
||||
<option value="4">⭐⭐⭐⭐ (Good)</option>
|
||||
<option value="3">⭐⭐⭐ (Average)</option>
|
||||
<option value="2">⭐⭐ (Poor)</option>
|
||||
<option value="1">⭐ (Very Poor)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="content" class="form-label">{% trans "Your Feedback" %}</label>
|
||||
<textarea name="content" id="content" rows="5" class="form-control" placeholder="{% trans 'Tell us about your experience...' %}" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">{% trans "Submit Feedback" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,7 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}RaktaPulse Dashboard - Lifeline of the Community{% endblock %}
|
||||
{% block title %}{% trans "RaktaPulse Dashboard" %} - {% trans "Lifeline of the Community" %}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
@ -13,6 +14,16 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.03);
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
color: inherit;
|
||||
}
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(230, 57, 70, 0.1);
|
||||
border-color: var(--pulse-red);
|
||||
cursor: pointer;
|
||||
}
|
||||
.stat-card .icon-box {
|
||||
width: 48px;
|
||||
@ -103,30 +114,54 @@
|
||||
<!-- Welcome Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h2 class="brand-font mb-1">RaktaPulse Community Dashboard</h2>
|
||||
<p class="text-secondary">Overview of blood donation activity and requirements in your area.</p>
|
||||
<h2 class="brand-font mb-1">{% trans "RaktaPulse Community Dashboard" %}</h2>
|
||||
<p class="text-secondary">{% trans "Overview of blood donation activity and requirements in your area." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if involved_events %}
|
||||
<!-- Active involvements / Message section -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="glass-card border-start border-primary border-4 bg-primary bg-opacity-10">
|
||||
<h5 class="brand-font mb-3"><i class="bi bi-chat-dots-fill me-2"></i>{% trans "Action Required: Donations in Progress" %}</h5>
|
||||
<div class="row g-3">
|
||||
{% for event in involved_events %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="p-3 border rounded bg-white shadow-sm">
|
||||
<p class="mb-2"><strong>{{ event.donor.name }}</strong> is helping <strong>{{ event.request.patient_name }}</strong></p>
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="small text-muted">{{ event.date|date:"M d, Y" }}</span>
|
||||
<a href="{% url 'complete_donation' event.id %}" class="btn btn-sm btn-success px-3 rounded-pill">{% trans "Mark Completed" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="stat-card">
|
||||
<a href="{% url 'donor_list' %}" class="stat-card">
|
||||
<div class="icon-box bg-danger bg-opacity-10 text-danger">
|
||||
<i class="bi bi-people"></i>
|
||||
</div>
|
||||
<div class="stat-value">{{ stats.total_donors }}</div>
|
||||
<div class="stat-label">Registered Donors</div>
|
||||
</div>
|
||||
<div class="stat-label">{% trans "Registered Donors" %}</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="stat-card">
|
||||
<a href="{% url 'blood_request_list' %}?status=Active" class="stat-card">
|
||||
<div class="icon-box bg-warning bg-opacity-10 text-warning">
|
||||
<i class="bi bi-activity"></i>
|
||||
</div>
|
||||
<div class="stat-value">{{ stats.active_requests }}</div>
|
||||
<div class="stat-label">Active Requests</div>
|
||||
</div>
|
||||
<div class="stat-label">{% trans "Active Requests" %}</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="stat-card">
|
||||
@ -134,7 +169,7 @@
|
||||
<i class="bi bi-droplet"></i>
|
||||
</div>
|
||||
<div class="stat-value">{{ stats.total_stock }} <small class="fs-6 fw-normal">/ {{ stats.total_capacity }}</small></div>
|
||||
<div class="stat-label">Inventory vs Capacity</div>
|
||||
<div class="stat-label">{% trans "Inventory vs Capacity" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
@ -143,7 +178,7 @@
|
||||
<i class="bi bi-shield-check"></i>
|
||||
</div>
|
||||
<div class="stat-value">{{ stats.vaccinated_percentage }}%</div>
|
||||
<div class="stat-label">Vaccinated Donors</div>
|
||||
<div class="stat-label">{% trans "Vaccinated Donors" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -176,7 +211,7 @@
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input type="text" name="location" class="form-control bg-light border-secondary text-dark"
|
||||
placeholder="Search location (e.g. Kathmandu)..." value="{{ request.GET.location }}">
|
||||
placeholder="Search location (e.g. Baluwatar)..." value="{{ request.GET.location }}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-danger w-100 shadow-sm">Find</button>
|
||||
@ -307,6 +342,41 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blood & Vaccine Facts Section -->
|
||||
<div class="row g-4 mt-2 mb-5">
|
||||
<div class="col-12">
|
||||
<div class="glass-card">
|
||||
<h4 class="brand-font mb-4"><i class="bi bi-info-circle text-danger me-2"></i>Blood & Vaccine Facts</h4>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-3">
|
||||
<div class="p-3 border border-light rounded-4 h-100 bg-light bg-opacity-10">
|
||||
<h6 class="fw-bold text-danger"><i class="bi bi-droplet-fill me-2"></i>Lifesaver</h6>
|
||||
<p class="small text-secondary mb-0">A single blood donation can save up to three lives. Your contribution has a massive impact on the community.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="p-3 border border-light rounded-4 h-100 bg-light bg-opacity-10">
|
||||
<h6 class="fw-bold text-danger"><i class="bi bi-shield-check me-2"></i>Vaccines & Donation</h6>
|
||||
<p class="small text-secondary mb-0">Most vaccines (like COVID-19 or Flu) don't stop you from donating if you feel well. Stay protected and keep saving lives!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="p-3 border border-light rounded-4 h-100 bg-light bg-opacity-10">
|
||||
<h6 class="fw-bold text-danger"><i class="bi bi-award me-2"></i>Universal Type</h6>
|
||||
<p class="small text-secondary mb-0">O-Negative is the universal donor type. It's vital for emergency rooms and trauma centers during critical hours.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="p-3 border border-light rounded-4 h-100 bg-light bg-opacity-10">
|
||||
<h6 class="fw-bold text-danger"><i class="bi bi-lightning-charge me-2"></i>Quick Recovery</h6>
|
||||
<p class="small text-secondary mb-0">Your body replaces blood plasma within 24-48 hours. Just stay hydrated and have a light snack after your donation!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -365,15 +435,15 @@
|
||||
});
|
||||
|
||||
function initMap() {
|
||||
map = L.map('map').setView([27.7172, 85.3240], 12); // Centered on Kathmandu
|
||||
map = L.map('map').setView([27.7226, 85.3312], 14); // Centered on Baluwatar, Kathmandu
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Add dummy markers for donors
|
||||
{% for donor in donors %}
|
||||
// Randomly offset from center for demo (within Nepal context)
|
||||
L.marker([27.7172 + (Math.random() - 0.5) * 0.1, 85.3240 + (Math.random() - 0.5) * 0.1])
|
||||
// Randomly offset from center for demo
|
||||
L.marker([27.7226 + (Math.random() - 0.5) * 0.05, 85.3312 + (Math.random() - 0.5) * 0.05])
|
||||
.addTo(map)
|
||||
.bindPopup("<b>{{ donor.name }}</b><br>{{ donor.blood_group }} - {{ donor.location }}");
|
||||
{% endfor %}
|
||||
|
||||
@ -84,8 +84,8 @@
|
||||
var markers = {};
|
||||
|
||||
function initMap() {
|
||||
// Center on Kathmandu by default
|
||||
map = L.map('live-map').setView([27.7172, 85.3240], 13);
|
||||
// Center on Baluwatar, Kathmandu
|
||||
map = L.map('live-map').setView([27.7226, 85.3312], 15);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
|
||||
35
core/templates/core/notifications.html
Normal file
35
core/templates/core/notifications.html
Normal file
@ -0,0 +1,35 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="glass-card p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0">{% trans "Notifications" %}</h2>
|
||||
<span class="badge bg-primary">{{ notifications.count }}</span>
|
||||
</div>
|
||||
|
||||
{% if notifications %}
|
||||
<div class="list-group list-group-flush">
|
||||
{% for note in notifications %}
|
||||
<div class="list-group-item bg-transparent border-0 mb-3 p-3 glass-card {% if not note.is_read %}border-start border-primary border-4{% endif %}">
|
||||
<div class="d-flex justify-content-between">
|
||||
<p class="mb-1">{{ note.message }}</p>
|
||||
<small class="text-muted">{{ note.created_at|timesince }} {% trans "ago" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-bell-slash fa-3x text-muted mb-3"></i>
|
||||
<p>{% trans "No notifications yet." %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
37
core/templates/core/register_donor.html
Normal file
37
core/templates/core/register_donor.html
Normal file
@ -0,0 +1,37 @@
|
||||
{% extends 'core/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="glass-card p-4">
|
||||
<h2 class="mb-4">{% trans "Become a Blood Donor" %}</h2>
|
||||
<p class="text-muted">{% trans "Join our community of lifesavers. Your donation can save lives." %}</p>
|
||||
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="blood_group" class="form-label">{% trans "Blood Group" %}</label>
|
||||
<select name="blood_group" id="blood_group" class="form-select" required>
|
||||
<option value="">{% trans "Select Blood Group" %}</option>
|
||||
{% for group in blood_groups %}
|
||||
<option value="{{ group }}">{{ group }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="phone" class="form-label">{% trans "Phone Number" %}</label>
|
||||
<input type="text" name="phone" id="phone" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="location" class="form-label">{% trans "Location" %}</label>
|
||||
<input type="text" name="location" id="location" class="form-control" placeholder="e.g. Baluwatar, Kathmandu">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger w-100">{% trans "Register as Donor" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -36,6 +36,22 @@
|
||||
<li class="list-group-item bg-transparent border-light"><i class="bi bi-check-circle-fill text-success me-2"></i> At least 14 days must have passed since the last dose.</li>
|
||||
<li class="list-group-item bg-transparent border-light"><i class="bi bi-check-circle-fill text-success me-2"></i> Must be in good health on the day of donation.</li>
|
||||
</ul>
|
||||
|
||||
<h5 class="fw-bold mb-3 mt-4">Interesting Vaccination Facts</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6">
|
||||
<div class="p-3 bg-light rounded border h-100">
|
||||
<h6 class="fw-bold text-primary small">Ancient Roots</h6>
|
||||
<p class="small mb-0 text-secondary">The word "vaccine" comes from the Latin <em>vacca</em> (cow), honoring the first smallpox vaccine derived from cowpox.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="p-3 bg-light rounded border h-100">
|
||||
<h6 class="fw-bold text-primary small">Immune Memory</h6>
|
||||
<p class="small mb-0 text-secondary">Vaccines create "memory cells" that allow your body to recognize and fight pathogens years after the initial shot.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
14
core/urls.py
14
core/urls.py
@ -1,6 +1,13 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import home, login_view, logout_view, register_view, donor_list, blood_request_list, blood_bank_list, vaccination_info, vaccination_dashboard, add_vaccination, live_map, request_blood, profile
|
||||
from .views import (
|
||||
home, login_view, logout_view, register_view, donor_list,
|
||||
blood_request_list, blood_bank_list, vaccination_info,
|
||||
vaccination_dashboard, add_vaccination, live_map,
|
||||
request_blood, profile, volunteer_for_request,
|
||||
complete_donation, submit_feedback, notifications_view,
|
||||
register_donor
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
@ -16,4 +23,9 @@ urlpatterns = [
|
||||
path("vaccination/add/", add_vaccination, name="add_vaccination"),
|
||||
path("live-map/", live_map, name="live_map"),
|
||||
path("request-blood/", request_blood, name="request_blood"),
|
||||
path("volunteer/<int:request_id>/", volunteer_for_request, name="volunteer_for_request"),
|
||||
path("complete-donation/<int:event_id>/", complete_donation, name="complete_donation"),
|
||||
path("feedback/", submit_feedback, name="submit_feedback"),
|
||||
path("notifications/", notifications_view, name="notifications"),
|
||||
path("register-donor/", register_donor, name="register_donor"),
|
||||
]
|
||||
|
||||
124
core/views.py
124
core/views.py
@ -1,12 +1,13 @@
|
||||
import os
|
||||
import platform
|
||||
from django.db import models
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth import login, logout, authenticate
|
||||
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.utils import timezone
|
||||
from .models import Donor, BloodRequest, BloodBank, VaccineRecord, UserProfile, BLOOD_GROUPS
|
||||
from .models import Donor, BloodRequest, BloodBank, VaccineRecord, UserProfile, BLOOD_GROUPS, DonationEvent, Notification, Feedback
|
||||
from .forms import UserUpdateForm, ProfileUpdateForm
|
||||
import math
|
||||
|
||||
@ -135,6 +136,15 @@ def home(request):
|
||||
"project_name": "RaktaPulse",
|
||||
"current_time": timezone.now(),
|
||||
}
|
||||
|
||||
if request.user.is_authenticated:
|
||||
# Get active involvements (where user is donor or requester)
|
||||
involved_events = DonationEvent.objects.filter(
|
||||
(models.Q(donor_user=request.user) | models.Q(request__user=request.user)),
|
||||
is_completed=False
|
||||
)
|
||||
context["involved_events"] = involved_events
|
||||
|
||||
return render(request, "core/index.html", context)
|
||||
|
||||
def donor_list(request):
|
||||
@ -173,9 +183,15 @@ def donor_list(request):
|
||||
return render(request, 'core/donor_list.html', context)
|
||||
|
||||
def blood_request_list(request):
|
||||
requests = BloodRequest.objects.all().order_by('-created_at')
|
||||
status = request.GET.get('status', '')
|
||||
requests = BloodRequest.objects.all()
|
||||
if status:
|
||||
requests = requests.filter(status=status)
|
||||
|
||||
requests = requests.order_by('-created_at')
|
||||
context = {
|
||||
'requests': requests,
|
||||
'current_status': status,
|
||||
}
|
||||
return render(request, 'core/blood_request_list.html', context)
|
||||
|
||||
@ -242,6 +258,7 @@ def request_blood(request):
|
||||
|
||||
if patient_name and blood_group and hospital and contact_number:
|
||||
BloodRequest.objects.create(
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
patient_name=patient_name,
|
||||
blood_group=blood_group,
|
||||
location=location,
|
||||
@ -297,3 +314,106 @@ def add_vaccination(request):
|
||||
messages.error(request, "Please fill in all required fields.")
|
||||
|
||||
return render(request, 'core/add_vaccination.html')
|
||||
|
||||
@login_required
|
||||
def volunteer_for_request(request, request_id):
|
||||
blood_request = BloodRequest.objects.get(id=request_id)
|
||||
donor_profile = getattr(request.user, 'donor_profile', None)
|
||||
|
||||
if not donor_profile:
|
||||
messages.error(request, "You need to be registered as a donor to volunteer.")
|
||||
return redirect('donor_list')
|
||||
|
||||
# Check if already volunteered
|
||||
if DonationEvent.objects.filter(donor=donor_profile, request=blood_request).exists():
|
||||
messages.warning(request, "You have already volunteered for this request.")
|
||||
else:
|
||||
DonationEvent.objects.create(
|
||||
donor=donor_profile,
|
||||
request=blood_request,
|
||||
donor_user=request.user
|
||||
)
|
||||
messages.success(request, "Thank you for volunteering! The requester has been notified.")
|
||||
|
||||
# Notify the requester
|
||||
if blood_request.user:
|
||||
Notification.objects.create(
|
||||
user=blood_request.user,
|
||||
message=f"Donor {request.user.username} has volunteered to help {blood_request.patient_name}!"
|
||||
)
|
||||
|
||||
return redirect('blood_request_list')
|
||||
|
||||
@login_required
|
||||
def complete_donation(request, event_id):
|
||||
event = DonationEvent.objects.get(id=event_id)
|
||||
# Only the requester or the donor can mark as complete (for simplicity, letting both)
|
||||
if request.user == event.donor_user or (event.request.user and request.user == event.request.user):
|
||||
event.is_completed = True
|
||||
event.save()
|
||||
|
||||
# Notify both
|
||||
Notification.objects.create(
|
||||
user=event.donor_user,
|
||||
message=f"Thank you for your donation to {event.request.patient_name}! Your feedback is valuable to us."
|
||||
)
|
||||
if event.request.user:
|
||||
Notification.objects.create(
|
||||
user=event.request.user,
|
||||
message=f"We hope the donation for {event.request.patient_name} went well. Please share your feedback!"
|
||||
)
|
||||
|
||||
messages.success(request, "Donation marked as completed. Thank you!")
|
||||
else:
|
||||
messages.error(request, "You are not authorized to complete this event.")
|
||||
|
||||
return redirect('home')
|
||||
|
||||
@login_required
|
||||
def submit_feedback(request):
|
||||
if request.method == "POST":
|
||||
content = request.POST.get('content')
|
||||
rating = request.POST.get('rating')
|
||||
if content:
|
||||
Feedback.objects.create(
|
||||
user=request.user,
|
||||
content=content,
|
||||
rating=rating if rating else 5
|
||||
)
|
||||
messages.success(request, "Thank you for your feedback!")
|
||||
return redirect('home')
|
||||
return render(request, 'core/feedback.html')
|
||||
|
||||
@login_required
|
||||
def notifications_view(request):
|
||||
notifications = Notification.objects.filter(user=request.user).order_by('-created_at')
|
||||
# Mark as read when viewed
|
||||
notifications.filter(is_read=False).update(is_read=True)
|
||||
return render(request, 'core/notifications.html', {'notifications': notifications})
|
||||
|
||||
@login_required
|
||||
def register_donor(request):
|
||||
if hasattr(request.user, 'donor_profile'):
|
||||
messages.info(request, "You are already registered as a donor.")
|
||||
return redirect('profile')
|
||||
|
||||
if request.method == "POST":
|
||||
blood_group = request.POST.get('blood_group')
|
||||
location = request.POST.get('location')
|
||||
phone = request.POST.get('phone')
|
||||
|
||||
if blood_group and phone:
|
||||
Donor.objects.create(
|
||||
user=request.user,
|
||||
name=request.user.username,
|
||||
blood_group=blood_group,
|
||||
location=location,
|
||||
phone=phone,
|
||||
is_available=True
|
||||
)
|
||||
messages.success(request, "Congratulations! You are now a registered donor.")
|
||||
return redirect('donor_list')
|
||||
else:
|
||||
messages.error(request, "Please fill in all required fields.")
|
||||
|
||||
return render(request, 'core/register_donor.html', {'blood_groups': [g[0] for g in BLOOD_GROUPS]})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user