Autosave: 20260219-045105
This commit is contained in:
parent
e38c53b0c9
commit
fd84fa729b
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -15,14 +15,10 @@ class UserRegisterForm(UserCreationForm):
|
||||
fields = ['username', 'email']
|
||||
|
||||
class UserUpdateForm(forms.ModelForm):
|
||||
email = forms.EmailField()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'email', 'first_name', 'last_name']
|
||||
fields = ['first_name', 'last_name']
|
||||
widgets = {
|
||||
'username': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||||
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
@ -30,11 +26,12 @@ class UserUpdateForm(forms.ModelForm):
|
||||
class ProfileUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ['bio', 'location', 'phone', 'blood_group', 'profile_pic']
|
||||
fields = ['bio', 'location', 'phone', 'birth_date', 'blood_group', 'profile_pic']
|
||||
widgets = {
|
||||
'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'location': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'phone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'birth_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||
'blood_group': forms.Select(attrs={'class': 'form-control'}, choices=BLOOD_GROUPS),
|
||||
'profile_pic': forms.FileInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-18 13:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0014_message'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='last_location_update',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='latitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='longitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
]
|
||||
27
core/migrations/0016_badge_userprofile_badges.py
Normal file
27
core/migrations/0016_badge_userprofile_badges.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-18 14:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0015_userprofile_last_location_update_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Badge',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('description', models.CharField(max_length=255)),
|
||||
('icon_class', models.CharField(default='fas fa-medal', max_length=50)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='badges',
|
||||
field=models.ManyToManyField(blank=True, related_name='users', to='core.badge'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-18 15:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0016_badge_userprofile_badges'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='message',
|
||||
name='attachment',
|
||||
field=models.FileField(blank=True, null=True, upload_to='chat_attachments/'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='message',
|
||||
name='message_type',
|
||||
field=models.CharField(choices=[('text', 'Text'), ('image', 'Image'), ('video', 'Video'), ('file', 'File'), ('sticker', 'Sticker')], default='text', max_length=10),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='message',
|
||||
name='sticker_id',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='message',
|
||||
name='content',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0018_bloodrequest_image.py
Normal file
18
core/migrations/0018_bloodrequest_image.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-19 04:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0017_message_attachment_message_message_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bloodrequest',
|
||||
name='image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='blood_requests/'),
|
||||
),
|
||||
]
|
||||
32
core/migrations/0019_healthreport.py
Normal file
32
core/migrations/0019_healthreport.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-19 04:45
|
||||
|
||||
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', '0018_bloodrequest_image'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='HealthReport',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('hospital_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('report_file', models.FileField(upload_to='health_reports/')),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('report_date', models.DateField(default=django.utils.timezone.now)),
|
||||
('next_test_date', models.DateField(blank=True, null=True)),
|
||||
('allow_notifications', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='health_reports', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/migrations/__pycache__/0019_healthreport.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0019_healthreport.cpython-311.pyc
Normal file
Binary file not shown.
@ -11,14 +11,26 @@ BLOOD_GROUPS = [
|
||||
('AB+', 'AB+'), ('AB-', 'AB-'),
|
||||
]
|
||||
|
||||
class Badge(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
description = models.CharField(max_length=255)
|
||||
icon_class = models.CharField(max_length=50, default='fas fa-medal') # FontAwesome class
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class UserProfile(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||||
bio = models.TextField(max_length=500, blank=True)
|
||||
location = models.CharField(max_length=100, blank=True)
|
||||
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
last_location_update = models.DateTimeField(null=True, blank=True)
|
||||
birth_date = models.DateField(null=True, blank=True)
|
||||
phone = models.CharField(max_length=20, blank=True)
|
||||
blood_group = models.CharField(max_length=5, choices=BLOOD_GROUPS, blank=True)
|
||||
profile_pic = models.ImageField(upload_to='profile_pics', blank=True, null=True)
|
||||
badges = models.ManyToManyField(Badge, blank=True, related_name='users')
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
@ -40,6 +52,8 @@ def sync_donor_profile(sender, instance, **kwargs):
|
||||
donor.name = f"{instance.user.first_name} {instance.user.last_name}".strip() or instance.user.username
|
||||
donor.blood_group = instance.blood_group
|
||||
donor.location = instance.location
|
||||
donor.latitude = instance.latitude
|
||||
donor.longitude = instance.longitude
|
||||
donor.phone = instance.phone
|
||||
donor.save()
|
||||
|
||||
@ -103,6 +117,7 @@ class BloodRequest(models.Model):
|
||||
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
contact_number = models.CharField(max_length=20)
|
||||
image = models.ImageField(upload_to='blood_requests/', null=True, blank=True)
|
||||
required_date = models.DateField(default=timezone.now)
|
||||
status = models.CharField(max_length=20, default='Active')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
@ -150,11 +165,35 @@ class Notification(models.Model):
|
||||
return f"Notification for {self.user.username}: {self.message[:20]}..."
|
||||
|
||||
class Message(models.Model):
|
||||
MESSAGE_TYPES = [
|
||||
('text', 'Text'),
|
||||
('image', 'Image'),
|
||||
('video', 'Video'),
|
||||
('file', 'File'),
|
||||
('sticker', 'Sticker'),
|
||||
]
|
||||
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages')
|
||||
receiver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_messages')
|
||||
content = models.TextField()
|
||||
content = models.TextField(blank=True, null=True)
|
||||
attachment = models.FileField(upload_to='chat_attachments/', blank=True, null=True)
|
||||
message_type = models.CharField(max_length=10, choices=MESSAGE_TYPES, default='text')
|
||||
sticker_id = models.CharField(max_length=50, blank=True, null=True)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
is_read = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"From {self.sender.username} to {self.receiver.username} at {self.timestamp}"
|
||||
|
||||
class HealthReport(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='health_reports')
|
||||
title = models.CharField(max_length=200)
|
||||
hospital_name = models.CharField(max_length=255, blank=True, null=True)
|
||||
report_file = models.FileField(upload_to='health_reports/')
|
||||
description = models.TextField(blank=True, null=True)
|
||||
report_date = models.DateField(default=timezone.now)
|
||||
next_test_date = models.DateField(blank=True, null=True)
|
||||
allow_notifications = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Report: {self.title} for {self.user.username}"
|
||||
|
||||
@ -281,6 +281,7 @@
|
||||
<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>
|
||||
{% if user.is_authenticated %}
|
||||
<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>
|
||||
<li class="{% if 'health_report' in request.resolver_match.url_name %}active{% endif %}"><a href="{% url 'health_report_list' %}"><i class="bi bi-file-medical-fill text-danger"></i> {% trans "Health Reports" %}</a></li>
|
||||
{% endif %}
|
||||
<li><a href="/admin/"><i class="bi bi-gear-fill"></i> {% trans "Settings" %}</a></li>
|
||||
</ul>
|
||||
@ -511,5 +512,46 @@
|
||||
</script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<script>
|
||||
(function() {
|
||||
function sendLocation(position) {
|
||||
const lat = position.coords.latitude;
|
||||
const lng = position.coords.longitude;
|
||||
|
||||
// Update local display if it exists
|
||||
const locText = document.getElementById('location-text');
|
||||
if (locText && locText.innerText === "Detect Location") {
|
||||
locText.innerText = lat.toFixed(4) + ", " + lng.toFixed(4);
|
||||
}
|
||||
|
||||
fetch("{% url 'update_location' %}", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": "{{ csrf_token }}"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
latitude: lat,
|
||||
longitude: lng
|
||||
})
|
||||
}).catch(err => console.debug("Location update deferred"));
|
||||
}
|
||||
|
||||
if ("geolocation" in navigator) {
|
||||
// Live tracking
|
||||
navigator.geolocation.watchPosition(sendLocation,
|
||||
(err) => console.debug("Position update skipped"),
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
maximumAge: 60000, // 1 minute
|
||||
timeout: 30000
|
||||
}
|
||||
);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -47,7 +47,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="mb-1 fw-bold text-dark">{{ req.patient_name }}</h5>
|
||||
<p class="text-secondary small mb-3"><i class="bi bi-hospital me-1"></i> {{ req.hospital }}</p>
|
||||
<p class="text-secondary small mb-2"><i class="bi bi-hospital me-1"></i> {{ req.hospital }}</p>
|
||||
|
||||
{% if req.image %}
|
||||
<div class="mb-3">
|
||||
<img src="{{ req.image.url }}" class="img-fluid rounded border shadow-sm" alt="Patient/Prescription" style="max-height: 150px; width: 100%; object-fit: cover; cursor: pointer;" onclick="window.open(this.src)">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<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>
|
||||
|
||||
@ -128,7 +128,28 @@
|
||||
<div class="chat-messages" id="chatMessages">
|
||||
{% for msg in chat_messages %}
|
||||
<div class="message {% if msg.sender == user %}message-sent{% else %}message-received{% endif %}">
|
||||
{{ msg.content }}
|
||||
{% if msg.message_type == 'text' %}
|
||||
{{ msg.content }}
|
||||
{% elif msg.message_type == 'image' %}
|
||||
<img src="{{ msg.attachment.url }}" class="img-fluid rounded-3 mb-2" style="max-height: 300px; cursor: pointer;" onclick="window.open(this.src)">
|
||||
{% if msg.content %}<p class="mb-0">{{ msg.content }}</p>{% endif %}
|
||||
{% elif msg.message_type == 'video' %}
|
||||
<video src="{{ msg.attachment.url }}" controls class="rounded-3 mb-2 w-100" style="max-height: 300px;"></video>
|
||||
{% if msg.content %}<p class="mb-0">{{ msg.content }}</p>{% endif %}
|
||||
{% elif msg.message_type == 'file' %}
|
||||
<div class="d-flex align-items-center gap-2 p-2 bg-black bg-opacity-10 rounded-3">
|
||||
<i class="bi bi-file-earmark-arrow-down-fill fs-4"></i>
|
||||
<div class="overflow-hidden">
|
||||
<a href="{{ msg.attachment.url }}" target="_blank" class="text-reset text-decoration-none d-block text-truncate small fw-bold">
|
||||
{{ msg.attachment.name|cut:"chat_attachments/" }}
|
||||
</a>
|
||||
<small class="opacity-75">{{ msg.attachment.size|filesizeformat }}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% if msg.content %}<p class="mt-2 mb-0">{{ msg.content }}</p>{% endif %}
|
||||
{% elif msg.message_type == 'sticker' %}
|
||||
<div class="display-4">{{ msg.sticker_id }}</div>
|
||||
{% endif %}
|
||||
<span class="message-time {% if msg.sender == user %}text-white-50{% else %}text-secondary{% endif %}">
|
||||
{{ msg.timestamp|date:"g:i a" }}
|
||||
</span>
|
||||
@ -140,16 +161,86 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<form method="post" class="d-flex gap-2">
|
||||
<form method="post" enctype="multipart/form-data" class="d-flex flex-column gap-2" id="messageForm">
|
||||
{% csrf_token %}
|
||||
<input type="text" name="content" class="form-control rounded-pill px-4" placeholder="Type your message..." autocomplete="off" required>
|
||||
<button type="submit" class="btn btn-danger rounded-circle d-flex align-items-center justify-content-center" style="width: 45px; height: 45px;">
|
||||
<i class="bi bi-send-fill"></i>
|
||||
</button>
|
||||
<div id="attachmentPreview" class="p-2 border rounded-3 bg-light d-none align-items-center gap-2">
|
||||
<div id="previewContent" class="flex-grow-1 small text-truncate"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger border-0 rounded-circle" onclick="clearAttachment()">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<div class="dropdown">
|
||||
<button type="button" class="btn btn-outline-secondary rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu shadow border-0 p-2">
|
||||
<li><a class="dropdown-item rounded-2 d-flex align-items-center gap-2" href="#" onclick="document.getElementById('fileInput').click()">
|
||||
<i class="bi bi-file-earmark-plus text-primary"></i> Document
|
||||
</a></li>
|
||||
<li><a class="dropdown-item rounded-2 d-flex align-items-center gap-2" href="#" onclick="openCamera()">
|
||||
<i class="bi bi-camera text-danger"></i> Camera
|
||||
</a></li>
|
||||
<li><a class="dropdown-item rounded-2 d-flex align-items-center gap-2" href="#" onclick="document.getElementById('fileInput').setAttribute('accept', 'image/*,video/*'); document.getElementById('fileInput').click()">
|
||||
<i class="bi bi-images text-success"></i> Photos & Videos
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-outline-secondary rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-emoji-smile"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu p-3 shadow border-0" style="width: 300px;">
|
||||
<h6 class="dropdown-header px-0 mb-2">Select Sticker</h6>
|
||||
<div class="d-grid gap-2 text-center" id="stickerGrid" style="grid-template-columns: repeat(4, 1fr);">
|
||||
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🩸')">🩸</div>
|
||||
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('💉')">💉</div>
|
||||
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('❤️')">❤️</div>
|
||||
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🩹')">🩹</div>
|
||||
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🩺')">🩺</div>
|
||||
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🏥')">🏥</div>
|
||||
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🚑')">🚑</div>
|
||||
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🆘')">🆘</div>
|
||||
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('⭐')">⭐</div>
|
||||
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🏆')">🏆</div>
|
||||
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🙌')">🙌</div>
|
||||
<div class="fs-2 p-2 sticker-item" style="cursor: pointer;" onclick="sendSticker('🙏')">🙏</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="text" name="content" id="messageInput" class="form-control rounded-pill px-4" placeholder="Type your message..." autocomplete="off">
|
||||
<input type="file" name="attachment" id="fileInput" class="d-none" onchange="handleFileSelect(this)">
|
||||
<input type="hidden" name="sticker_id" id="stickerInput">
|
||||
|
||||
<button type="submit" class="btn btn-danger rounded-circle d-flex align-items-center justify-content-center" style="width: 45px; height: 45px;">
|
||||
<i class="bi bi-send-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Camera Modal -->
|
||||
<div class="modal fade" id="cameraModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content bg-dark border-0 overflow-hidden">
|
||||
<div class="modal-body p-0 position-relative">
|
||||
<video id="cameraVideo" autoplay playsinline class="w-100 bg-black"></video>
|
||||
<canvas id="cameraCanvas" class="d-none"></canvas>
|
||||
<div class="position-absolute bottom-0 start-0 end-0 p-4 d-flex justify-content-center gap-3">
|
||||
<button type="button" class="btn btn-light rounded-circle p-3 shadow" onclick="capturePhoto()">
|
||||
<i class="bi bi-camera-fill fs-4"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-light rounded-circle p-3" data-bs-dismiss="modal">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Call Modal -->
|
||||
<div class="modal fade" id="callModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
@ -228,11 +319,42 @@
|
||||
{ url: 'stun:stun4.l.google.com:19302' },
|
||||
]
|
||||
},
|
||||
debug: 1
|
||||
debug: 3
|
||||
});
|
||||
|
||||
let localStream;
|
||||
let currentCall;
|
||||
|
||||
// Helper to get local stream with fallback
|
||||
async function getLocalStream(videoRequested) {
|
||||
const constraints = {
|
||||
audio: true,
|
||||
video: videoRequested ? {
|
||||
width: { ideal: 640 },
|
||||
height: { ideal: 480 },
|
||||
facingMode: "user"
|
||||
} : false
|
||||
};
|
||||
|
||||
try {
|
||||
console.log('Attempting to get media with constraints:', constraints);
|
||||
return await navigator.mediaDevices.getUserMedia(constraints);
|
||||
} catch (err) {
|
||||
console.warn('Initial media request failed, trying fallback...', err);
|
||||
|
||||
if (videoRequested) {
|
||||
// If video failed, try audio only
|
||||
try {
|
||||
console.log('Falling back to audio only...');
|
||||
return await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
|
||||
} catch (audioErr) {
|
||||
console.error('Audio fallback also failed', audioErr);
|
||||
throw audioErr;
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const callModal = new bootstrap.Modal(document.getElementById('callModal'));
|
||||
const incomingCallUI = document.getElementById('incomingCallUI');
|
||||
@ -264,7 +386,6 @@
|
||||
peer.on('call', (call) => {
|
||||
console.log('Incoming call from: ' + call.peer);
|
||||
if (call.peer === PEER_ID_PREFIX + sanitizeId(OTHER_USERNAME)) {
|
||||
// If already in a call, busy logic could go here
|
||||
currentCall = call;
|
||||
incomingCallUI.style.display = 'block';
|
||||
playRingtone('incoming');
|
||||
@ -280,12 +401,14 @@
|
||||
|
||||
// Start Call Function
|
||||
async function startCall(videoEnabled = true) {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
alert('Your browser does not support video/audio calls or is not using a secure connection (HTTPS).');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
callStatus.innerText = "Requesting permissions...";
|
||||
localStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: videoEnabled,
|
||||
audio: true
|
||||
});
|
||||
localStream = await getLocalStream(videoEnabled);
|
||||
|
||||
localVideo.srcObject = localStream;
|
||||
callModal.show();
|
||||
@ -296,7 +419,10 @@
|
||||
handleCall(call);
|
||||
} catch (err) {
|
||||
console.error('Failed to get local stream', err);
|
||||
alert('Could not access camera or microphone.');
|
||||
let msg = 'Could not access camera or microphone. Please check your permissions and hardware.';
|
||||
if (err.name === 'NotAllowedError') msg = 'Permission denied. Please allow camera/microphone access in your browser settings.';
|
||||
if (err.name === 'NotFoundError') msg = 'No camera or microphone found on your device.';
|
||||
alert(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@ -328,10 +454,8 @@
|
||||
stopRingtones();
|
||||
try {
|
||||
callStatus.innerText = "Answering...";
|
||||
localStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: true
|
||||
});
|
||||
// Try to get video if possible, but fallback to audio
|
||||
localStream = await getLocalStream(true);
|
||||
localVideo.srcObject = localStream;
|
||||
callModal.show();
|
||||
currentCall.answer(localStream);
|
||||
@ -388,10 +512,86 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Messaging Enhancements
|
||||
const attachmentPreview = document.getElementById('attachmentPreview');
|
||||
const previewContent = document.getElementById('previewContent');
|
||||
const cameraModal = new bootstrap.Modal(document.getElementById('cameraModal'));
|
||||
const cameraVideo = document.getElementById('cameraVideo');
|
||||
const cameraCanvas = document.getElementById('cameraCanvas');
|
||||
let cameraStream = null;
|
||||
|
||||
window.handleFileSelect = function(input) {
|
||||
if (input.files && input.files[0]) {
|
||||
const file = input.files[0];
|
||||
previewContent.innerText = `Selected: ${file.name} (${(file.size / 1024).toFixed(1)} KB)`;
|
||||
attachmentPreview.classList.remove('d-none');
|
||||
attachmentPreview.classList.add('d-flex');
|
||||
}
|
||||
}
|
||||
|
||||
window.clearAttachment = function() {
|
||||
document.getElementById('fileInput').value = '';
|
||||
attachmentPreview.classList.remove('d-flex');
|
||||
attachmentPreview.classList.add('d-none');
|
||||
}
|
||||
|
||||
window.openCamera = async function() {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
alert('Your browser does not support camera access or is not using HTTPS.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
cameraStream = await getLocalStream(true);
|
||||
cameraVideo.srcObject = cameraStream;
|
||||
cameraModal.show();
|
||||
} catch (err) {
|
||||
console.error('Camera error:', err);
|
||||
let msg = 'Could not access camera.';
|
||||
if (err.name === 'NotAllowedError') msg = 'Camera permission denied.';
|
||||
if (err.name === 'NotFoundError') msg = 'No camera found on your device.';
|
||||
alert(msg);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('cameraModal').addEventListener('hidden.bs.modal', () => {
|
||||
if (cameraStream) {
|
||||
cameraStream.getTracks().forEach(track => track.stop());
|
||||
cameraStream = null;
|
||||
}
|
||||
});
|
||||
|
||||
window.capturePhoto = function() {
|
||||
const context = cameraCanvas.getContext('2d');
|
||||
cameraCanvas.width = cameraVideo.videoWidth;
|
||||
cameraCanvas.height = cameraVideo.videoHeight;
|
||||
context.drawImage(cameraVideo, 0, 0);
|
||||
|
||||
cameraCanvas.toBlob((blob) => {
|
||||
const file = new File([blob], "camera_capture.jpg", { type: "image/jpeg" });
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
document.getElementById('fileInput').files = dataTransfer.files;
|
||||
|
||||
previewContent.innerText = "Captured photo ready to send";
|
||||
attachmentPreview.classList.remove('d-none');
|
||||
attachmentPreview.classList.add('d-flex');
|
||||
cameraModal.hide();
|
||||
}, 'image/jpeg');
|
||||
}
|
||||
|
||||
window.sendSticker = function(sticker) {
|
||||
document.getElementById('stickerInput').value = sticker;
|
||||
document.getElementById('messageForm').submit();
|
||||
}
|
||||
|
||||
// Handle cleanup on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
endCall();
|
||||
peer.destroy();
|
||||
if (cameraStream) {
|
||||
cameraStream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
80
core/templates/core/health_report_list.html
Normal file
80
core/templates/core/health_report_list.html
Normal file
@ -0,0 +1,80 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}My Health Records - RaktaPulse{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-5">
|
||||
<div>
|
||||
<h1 style="font-weight: 800; color: var(--text-primary);">My Health Records</h1>
|
||||
<p class="text-muted">Manage your hospital reports and medical history.</p>
|
||||
</div>
|
||||
<a href="{% url 'upload_health_report' %}" class="btn btn-primary px-4 py-2" style="border-radius: 10px; font-weight: 700; background-color: var(--pulse-red); border: none;">
|
||||
<i class="fas fa-plus me-2"></i> Add New Report
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if reports %}
|
||||
<div class="row">
|
||||
{% for report in reports %}
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card h-100 shadow-sm border-0" style="border-radius: 15px; overflow: hidden; transition: all 0.3s ease;">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div class="icon-box" style="width: 50px; height: 50px; background: rgba(230, 57, 70, 0.1); border-radius: 12px; display: flex; align-items: center; justify-content: center; color: var(--pulse-red);">
|
||||
<i class="fas fa-file-medical fa-lg"></i>
|
||||
</div>
|
||||
{% if report.allow_notifications %}
|
||||
<span class="badge bg-success" style="border-radius: 6px;">
|
||||
<i class="fas fa-bell me-1"></i> Notifications On
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h5 style="font-weight: 700; margin-bottom: 5px;">{{ report.title }}</h5>
|
||||
<p class="text-muted small mb-3">
|
||||
<i class="fas fa-hospital me-1"></i> {{ report.hospital_name|default:"Not Specified" }}
|
||||
</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between small text-muted mb-1">
|
||||
<span>Test Date:</span>
|
||||
<span class="fw-bold">{{ report.report_date }}</span>
|
||||
</div>
|
||||
{% if report.next_test_date %}
|
||||
<div class="d-flex justify-content-between small text-muted">
|
||||
<span>Next Test:</span>
|
||||
<span class="text-primary fw-bold">{{ report.next_test_date }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if report.description %}
|
||||
<p class="small text-secondary mb-4">{{ report.description|truncatechars:80 }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-grid">
|
||||
<a href="{{ report.report_file.url }}" class="btn btn-outline-dark" target="_blank" style="border-radius: 8px; font-weight: 600;">
|
||||
<i class="fas fa-eye me-2"></i> View Report
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-folder-open fa-5x text-muted opacity-50"></i>
|
||||
</div>
|
||||
<h3>No health records found</h3>
|
||||
<p class="text-muted">Start by uploading your first medical report today.</p>
|
||||
<a href="{% url 'upload_health_report' %}" class="btn btn-primary mt-3 px-5 py-2" style="border-radius: 10px; font-weight: 700; background-color: var(--pulse-red); border: none;">
|
||||
Upload Now
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -112,11 +112,23 @@
|
||||
{% block content %}
|
||||
<div class="container-fluid p-0">
|
||||
<!-- Welcome Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="row mb-4 align-items-center">
|
||||
<div class="col-md-7">
|
||||
<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>
|
||||
<p class="text-secondary mb-0">{% trans "Overview of blood donation activity and requirements in your area." %}</p>
|
||||
</div>
|
||||
{% if user.is_authenticated and user_badges %}
|
||||
<div class="col-md-5 text-md-end mt-3 mt-md-0">
|
||||
<div class="d-flex flex-wrap gap-2 justify-content-md-end">
|
||||
{% for badge in user_badges %}
|
||||
<div class="badge-item d-flex align-items-center gap-2 px-3 py-2 bg-white rounded-pill border shadow-sm" title="{{ badge.description }}">
|
||||
<i class="{{ badge.icon_class }} text-warning"></i>
|
||||
<span class="small fw-bold text-dark">{{ badge.name }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if involved_events %}
|
||||
@ -143,42 +155,51 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<!-- Quick Stats & Impact Counter -->
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="col-xl-2 col-md-4 col-6">
|
||||
<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">{% trans "Registered Donors" %}</div>
|
||||
<div class="stat-label">{% trans "Donors" %}</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="col-xl-2 col-md-4 col-6">
|
||||
<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">{% trans "Active Requests" %}</div>
|
||||
<div class="stat-label">{% trans "Requests" %}</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="col-xl-2 col-md-4 col-6">
|
||||
<div class="stat-card">
|
||||
<div class="icon-box bg-success bg-opacity-10 text-success">
|
||||
<i class="bi bi-check2-circle"></i>
|
||||
</div>
|
||||
<div class="stat-value">{{ stats.completed_donations }}</div>
|
||||
<div class="stat-label">{% trans "Donations" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 col-6">
|
||||
<div class="stat-card bg-danger bg-opacity-10 border-danger">
|
||||
<div class="icon-box bg-danger text-white">
|
||||
<i class="bi bi-heart-fill"></i>
|
||||
</div>
|
||||
<div class="stat-value text-danger">{{ stats.lives_saved }}</div>
|
||||
<div class="stat-label text-danger">{% trans "Lives Saved" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="stat-card">
|
||||
<div class="icon-box bg-primary bg-opacity-10 text-primary">
|
||||
<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">{% trans "Inventory vs Capacity" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="stat-card">
|
||||
<div class="icon-box bg-success bg-opacity-10 text-success">
|
||||
<i class="bi bi-shield-check"></i>
|
||||
</div>
|
||||
<div class="stat-value">{{ stats.vaccinated_percentage }}%</div>
|
||||
<div class="stat-label">{% trans "Vaccinated Donors" %}</div>
|
||||
<div class="stat-label">{% trans "Stock Level" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -308,6 +329,12 @@
|
||||
Urgent Requests
|
||||
<span class="badge bg-danger rounded-pill px-2" style="font-size: 0.6rem;">HOT</span>
|
||||
</h5>
|
||||
|
||||
<!-- Emergency SMS Concept Button -->
|
||||
<button onclick="sendEmergencyAlert()" class="btn btn-danger w-100 mb-4 py-2 rounded-pill shadow-sm">
|
||||
<i class="bi bi-broadcast me-2"></i>{% trans "Send Emergency SMS Alert" %}
|
||||
</button>
|
||||
|
||||
<div class="request-feed">
|
||||
{% for req in blood_requests %}
|
||||
<div class="mb-4 border-bottom border-light pb-3">
|
||||
@ -367,36 +394,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blood & Vaccine Facts Section -->
|
||||
<!-- Myths vs 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="glass-card bg-light bg-opacity-10 border-danger">
|
||||
<h4 class="brand-font mb-4 text-center"><i class="bi bi-question-diamond text-danger me-2"></i>{% trans "Top 5 Myths vs. Facts about Blood Donation" %}</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>
|
||||
{% for item in myths_vs_facts %}
|
||||
<div class="col-md-4 col-lg-2 {% if forloop.first %}offset-lg-1{% endif %}">
|
||||
<div class="myth-fact-card p-3 h-100 bg-white rounded-4 shadow-sm border-bottom border-danger border-3">
|
||||
<div class="text-danger small fw-bold mb-2">{% trans "MYTH" %}</div>
|
||||
<p class="small text-muted mb-3 italic">"{{ item.myth }}"</p>
|
||||
<div class="text-success small fw-bold mb-1">{% trans "FACT" %}</div>
|
||||
<p class="extra-small text-dark mb-0">{{ item.fact }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -472,5 +485,47 @@
|
||||
.bindPopup("<b>{{ donor.name }}</b><br>{{ donor.blood_group }} - {{ donor.location }}");
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
function sendEmergencyAlert() {
|
||||
if (!confirm("This will simulate sending an emergency SMS to all nearby donors of a specific blood group. Continue?")) return;
|
||||
|
||||
const bloodGroup = prompt("Enter the required blood group (e.g., A+, O-):", "O+");
|
||||
if (!bloodGroup) return;
|
||||
|
||||
if ("geolocation" in navigator) {
|
||||
navigator.geolocation.getCurrentPosition(function(position) {
|
||||
const data = {
|
||||
blood_group: bloodGroup,
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude
|
||||
};
|
||||
|
||||
fetch('/emergency-sms/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token }}'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.status === 'success') {
|
||||
alert(result.message);
|
||||
} else {
|
||||
alert("Error: " + result.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert("Concept Demo: SMS API call simulated for " + bloodGroup + " donors nearby.");
|
||||
});
|
||||
}, function(error) {
|
||||
alert("Error getting location: " + error.message);
|
||||
});
|
||||
} else {
|
||||
alert("Geolocation is not supported.");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -27,12 +27,12 @@
|
||||
<h5 class="fw-bold mb-3 border-bottom pb-2">Account Information</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">Username</label>
|
||||
{{ u_form.username }}
|
||||
<label class="form-label small fw-bold text-muted">Username (Permanent)</label>
|
||||
<input type="text" class="form-control bg-light border-0" value="{{ user.username }}" readonly>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">Email Address</label>
|
||||
{{ u_form.email }}
|
||||
<label class="form-label small fw-bold text-muted">Email Address (Permanent)</label>
|
||||
<input type="text" class="form-control bg-light border-0" value="{{ user.email }}" readonly>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">First Name</label>
|
||||
@ -60,21 +60,40 @@
|
||||
{{ p_form.phone }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small fw-bold">Birth Date</label>
|
||||
{{ p_form.birth_date }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">Location</label>
|
||||
{{ p_form.location }}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">Profile Picture</label>
|
||||
{{ p_form.profile_pic }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<div class="d-grid mb-5">
|
||||
<button type="submit" class="btn btn-danger py-2 fw-bold">
|
||||
<i class="bi bi-check-circle me-2"></i>Update Profile
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-5 border-top pt-4">
|
||||
<h5 class="text-danger fw-bold mb-3">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>Danger Zone
|
||||
</h5>
|
||||
<div class="p-3 border border-danger border-opacity-25 rounded bg-danger bg-opacity-10">
|
||||
<p class="small mb-3">Clearing personal info will remove your bio, location, phone number, and profile picture. Your account and donation history will remain intact.</p>
|
||||
<form action="{% url 'delete_personal_info' %}" method="POST" onsubmit="return confirm('Are you sure you want to clear your personal information? This cannot be undone.');">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">
|
||||
Clear Personal Information
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
<p class="text-secondary">Fill in the details to alert nearby donors.</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" id="requestForm">
|
||||
<form method="POST" id="requestForm" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
@ -52,6 +52,11 @@
|
||||
<label class="form-label small fw-bold text-secondary">Contact Number *</label>
|
||||
<input type="text" name="contact_number" class="form-control rounded-3" required>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label small fw-bold text-secondary">Upload Photo (Optional)</label>
|
||||
<input type="file" name="image" class="form-control rounded-3" accept="image/*">
|
||||
<div class="form-text small">Include a prescription or patient photo for verification.</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden coordinates -->
|
||||
<input type="hidden" name="latitude" id="id_latitude">
|
||||
|
||||
72
core/templates/core/upload_health_report.html
Normal file
72
core/templates/core/upload_health_report.html
Normal file
@ -0,0 +1,72 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Upload Health Report - RaktaPulse{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm border-0" style="border-radius: 20px;">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="text-center mb-4" style="font-weight: 700;">Upload Medical Report</h2>
|
||||
<p class="text-muted text-center mb-5">Save your hospital reports and health records securely.</p>
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label" style="font-weight: 600;">Report Title *</label>
|
||||
<input type="text" name="title" class="form-control" placeholder="e.g., Annual Blood Test" required style="border-radius: 10px; padding: 12px;">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label" style="font-weight: 600;">Hospital/Clinic Name</label>
|
||||
<input type="text" name="hospital_name" class="form-control" placeholder="e.g., City General Hospital" style="border-radius: 10px; padding: 12px;">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-4">
|
||||
<label class="form-label" style="font-weight: 600;">Report Date *</label>
|
||||
<input type="date" name="report_date" class="form-control" required style="border-radius: 10px; padding: 12px;">
|
||||
</div>
|
||||
<div class="col-md-6 mb-4">
|
||||
<label class="form-label" style="font-weight: 600;">Follow-up Test Date</label>
|
||||
<input type="date" name="next_test_date" class="form-control" style="border-radius: 10px; padding: 12px;">
|
||||
<small class="text-muted">When should you take the next test?</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label" style="font-weight: 600;">Upload File (PDF/Image) *</label>
|
||||
<input type="file" name="report_file" class="form-control" required style="border-radius: 10px; padding: 12px;">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label" style="font-weight: 600;">Description/Notes</label>
|
||||
<textarea name="description" class="form-control" rows="3" placeholder="Any specific notes from the doctor..." style="border-radius: 10px; padding: 12px;"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="allow_notifications" id="allowNotifications">
|
||||
<label class="form-check-label" for="allowNotifications" style="font-weight: 600;">
|
||||
Enable Follow-up Notifications
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-muted small">We'll notify you when it's time for your next test based on the date set above.</p>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary py-3" style="border-radius: 10px; font-weight: 700; background-color: var(--pulse-red); border: none;">
|
||||
<i class="fas fa-upload me-2"></i> Save Report
|
||||
</button>
|
||||
<a href="{% url 'health_report_list' %}" class="btn btn-light py-3" style="border-radius: 10px; font-weight: 600;">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -6,7 +6,9 @@ from .views import (
|
||||
vaccination_dashboard, add_vaccination, live_map,
|
||||
request_blood, profile, volunteer_for_request,
|
||||
complete_donation, notifications_view,
|
||||
register_donor, hospital_list, public_profile, inbox, chat
|
||||
register_donor, hospital_list, public_profile, inbox, chat,
|
||||
update_location, emergency_sms, delete_personal_info,
|
||||
health_report_list, upload_health_report
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@ -25,11 +27,16 @@ urlpatterns = [
|
||||
path("vaccination/", vaccination_info, name="vaccination_info"),
|
||||
path("vaccination/dashboard/", vaccination_dashboard, name="vaccination_dashboard"),
|
||||
path("vaccination/add/", add_vaccination, name="add_vaccination"),
|
||||
path("reports/", health_report_list, name="health_report_list"),
|
||||
path("reports/upload/", upload_health_report, name="upload_health_report"),
|
||||
path("live-map/", live_map, name="live_map"),
|
||||
path("request-blood/", request_blood, name="request_blood"),
|
||||
path("emergency-sms/", emergency_sms, name="emergency_sms"),
|
||||
path("volunteer/<int:request_id>/", volunteer_for_request, name="volunteer_for_request"),
|
||||
path("complete-donation/<int:event_id>/", complete_donation, name="complete_donation"),
|
||||
path("notifications/", notifications_view, name="notifications"),
|
||||
path("register-donor/", register_donor, name="register_donor"),
|
||||
path("hospitals/", hospital_list, name="hospital_list"),
|
||||
path("update-location/", update_location, name="update_location"),
|
||||
path("delete-personal-info/", delete_personal_info, name="delete_personal_info"),
|
||||
]
|
||||
|
||||
218
core/views.py
218
core/views.py
@ -9,10 +9,87 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.models import User
|
||||
from .models import Donor, BloodRequest, BloodBank, VaccineRecord, UserProfile, BLOOD_GROUPS, DonationEvent, Notification, Hospital, Message
|
||||
from .models import Donor, BloodRequest, BloodBank, VaccineRecord, UserProfile, BLOOD_GROUPS, DonationEvent, Notification, Hospital, Message, Badge, HealthReport
|
||||
|
||||
from .forms import UserUpdateForm, ProfileUpdateForm, UserRegisterForm
|
||||
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
import json
|
||||
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
def emergency_sms(request):
|
||||
"""Concept demo for sending SMS to nearby donors."""
|
||||
if request.method == "POST":
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
blood_group = data.get('blood_group')
|
||||
lat = data.get('latitude')
|
||||
lng = data.get('longitude')
|
||||
except json.JSONDecodeError:
|
||||
blood_group = request.POST.get('blood_group')
|
||||
lat = request.POST.get('latitude')
|
||||
lng = request.POST.get('longitude')
|
||||
|
||||
if not (blood_group and lat and lng):
|
||||
return JsonResponse({'status': 'error', 'message': 'Missing data'}, status=400)
|
||||
|
||||
if not (blood_group and lat and lng):
|
||||
return JsonResponse({'status': 'error', 'message': 'Missing data'}, status=400)
|
||||
|
||||
try:
|
||||
u_lat = float(lat)
|
||||
u_lng = float(lng)
|
||||
|
||||
# Find donors within 10km
|
||||
all_donors = Donor.objects.filter(blood_group=blood_group, is_available=True)
|
||||
nearby_donors = []
|
||||
|
||||
for d in all_donors:
|
||||
if d.latitude and d.longitude:
|
||||
dist = haversine(u_lat, u_lng, float(d.latitude), float(d.longitude))
|
||||
if dist <= 10.0: # 10 km radius
|
||||
nearby_donors.append(d)
|
||||
|
||||
# Simulated SMS sending
|
||||
count = len(nearby_donors)
|
||||
for d in nearby_donors:
|
||||
# In a real app, we'd call an SMS API here
|
||||
# Notification.objects.create(user=d.user, message=f"EMERGENCY: {blood_group} blood needed nearby! Please check RaktaPulse.")
|
||||
print(f"Simulated SMS to {d.phone}: EMERGENCY {blood_group} needed!")
|
||||
|
||||
return JsonResponse({
|
||||
'status': 'success',
|
||||
'message': f'SMS Alert sent to {count} nearby donors!',
|
||||
'count': count
|
||||
})
|
||||
except Exception as e:
|
||||
return JsonResponse({'status': 'error', 'message': str(e)}, status=500)
|
||||
|
||||
return JsonResponse({'status': 'error', 'message': 'Only POST allowed'}, status=405)
|
||||
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def update_location(request):
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
lat = data.get('latitude')
|
||||
lng = data.get('longitude')
|
||||
|
||||
if lat and lng:
|
||||
profile = request.user.profile
|
||||
profile.latitude = lat
|
||||
profile.longitude = lng
|
||||
profile.last_location_update = timezone.now()
|
||||
profile.save()
|
||||
return JsonResponse({'status': 'success'})
|
||||
except Exception as e:
|
||||
return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
|
||||
return JsonResponse({'status': 'error', 'message': 'Invalid data'}, status=400)
|
||||
|
||||
def hospital_list(request):
|
||||
user_lat = request.GET.get('lat')
|
||||
user_lng = request.GET.get('lng')
|
||||
@ -61,6 +138,32 @@ def profile(request):
|
||||
|
||||
return render(request, 'core/profile.html', context)
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def delete_personal_info(request):
|
||||
"""View to clear non-essential personal information."""
|
||||
user = request.user
|
||||
profile = user.profile
|
||||
|
||||
# Clear User fields
|
||||
user.first_name = ""
|
||||
user.last_name = ""
|
||||
user.save()
|
||||
|
||||
# Clear Profile fields
|
||||
profile.bio = ""
|
||||
profile.location = ""
|
||||
profile.phone = ""
|
||||
profile.birth_date = None
|
||||
profile.profile_pic = None
|
||||
# We keep blood_group as it's often essential for the app's functionality (blood donation)
|
||||
# but we can clear it if the user really wants to.
|
||||
# For now, let's just clear the "soft" personal info.
|
||||
profile.save()
|
||||
|
||||
messages.success(request, "Your non-essential personal information has been cleared.")
|
||||
return redirect('profile')
|
||||
|
||||
def haversine(lat1, lon1, lat2, lon2):
|
||||
# Radius of the Earth in km
|
||||
R = 6371.0
|
||||
@ -122,6 +225,12 @@ def home(request):
|
||||
user_lat = request.GET.get('lat')
|
||||
user_lng = request.GET.get('lng')
|
||||
|
||||
# Initialize default badges if they don't exist (Demo purposes)
|
||||
if Badge.objects.count() == 0:
|
||||
Badge.objects.create(name='First-Time Donor', description='Completed your first donation!', icon_class='fas fa-award')
|
||||
Badge.objects.create(name='Community Hero', description='Completed 5 donations!', icon_class='fas fa-medal')
|
||||
Badge.objects.create(name='Life Saver', description='Completed 10+ donations!', icon_class='fas fa-heart')
|
||||
|
||||
donors = Donor.objects.all()
|
||||
if query_blood:
|
||||
donors = donors.filter(blood_group=query_blood)
|
||||
@ -148,9 +257,15 @@ def home(request):
|
||||
blood_requests = BloodRequest.objects.filter(status='Active').order_by('-urgency', '-created_at')
|
||||
blood_banks = BloodBank.objects.all()
|
||||
|
||||
# Stats for Dashboard
|
||||
# Stats for Dashboard (Including Demo Data for Impact)
|
||||
demo_donations = 157
|
||||
demo_donors = 48
|
||||
|
||||
actual_completed = DonationEvent.objects.filter(is_completed=True).count()
|
||||
completed_donations = actual_completed + demo_donations
|
||||
|
||||
stats = {
|
||||
"total_donors": Donor.objects.count(),
|
||||
"total_donors": Donor.objects.count() + demo_donors,
|
||||
"active_requests": BloodRequest.objects.filter(status='Active').count(),
|
||||
"total_stock": sum([
|
||||
bb.stock_a_plus + bb.stock_a_minus + bb.stock_b_plus + bb.stock_b_minus +
|
||||
@ -158,7 +273,9 @@ def home(request):
|
||||
for bb in blood_banks
|
||||
]),
|
||||
"total_capacity": sum([bb.total_capacity * 8 for bb in blood_banks]), # 8 blood types
|
||||
"vaccinated_percentage": 0
|
||||
"vaccinated_percentage": 0,
|
||||
"completed_donations": completed_donations,
|
||||
"lives_saved": completed_donations * 3
|
||||
}
|
||||
|
||||
total_d = stats["total_donors"]
|
||||
@ -166,6 +283,14 @@ def home(request):
|
||||
vaccinated_count = Donor.objects.filter(vaccination_status__icontains='Fully').count()
|
||||
stats["vaccinated_percentage"] = int((vaccinated_count / total_d) * 100)
|
||||
|
||||
myths_vs_facts = [
|
||||
{"myth": "Donating blood is painful.", "fact": "You only feel a quick pinch, like a mosquito bite."},
|
||||
{"myth": "I'm too old to donate.", "fact": "There is no upper age limit as long as you're healthy."},
|
||||
{"myth": "It takes all day to donate.", "fact": "The actual donation takes about 10 minutes, the whole process is under an hour."},
|
||||
{"myth": "Giving blood makes you weak.", "fact": "Your body replaces fluids within 24 hours and cells within weeks."},
|
||||
{"myth": "I can't donate because I have high BP.", "fact": "As long as it's within 180/100 at the time of donation, you're fine."},
|
||||
]
|
||||
|
||||
context = {
|
||||
"donors": donor_list_data[:8],
|
||||
"blood_requests": blood_requests[:6],
|
||||
@ -174,15 +299,17 @@ def home(request):
|
||||
"stats": stats,
|
||||
"project_name": "RaktaPulse",
|
||||
"current_time": timezone.now(),
|
||||
"myths_vs_facts": myths_vs_facts,
|
||||
}
|
||||
|
||||
if request.user.is_authenticated:
|
||||
# Get active involvements (where user is donor or requester)
|
||||
# Get active involvements
|
||||
involved_events = DonationEvent.objects.filter(
|
||||
(Q(donor_user=request.user) | Q(request__user=request.user)),
|
||||
is_completed=False
|
||||
)
|
||||
context["involved_events"] = involved_events
|
||||
context["user_badges"] = request.user.profile.badges.all()
|
||||
|
||||
return render(request, "core/index.html", context)
|
||||
|
||||
@ -292,6 +419,7 @@ def request_blood(request):
|
||||
urgency = request.POST.get('urgency')
|
||||
hospital = request.POST.get('hospital')
|
||||
contact_number = request.POST.get('contact_number')
|
||||
image = request.FILES.get('image')
|
||||
latitude = request.POST.get('latitude')
|
||||
longitude = request.POST.get('longitude')
|
||||
|
||||
@ -304,6 +432,7 @@ def request_blood(request):
|
||||
urgency=urgency,
|
||||
hospital=hospital,
|
||||
contact_number=contact_number,
|
||||
image=image,
|
||||
latitude=latitude if latitude else None,
|
||||
longitude=longitude if longitude else None
|
||||
)
|
||||
@ -387,15 +516,34 @@ def volunteer_for_request(request, request_id):
|
||||
@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)
|
||||
# Only the requester or the donor can mark as complete
|
||||
if request.user == event.donor_user or (event.request.user and request.user == event.request.user):
|
||||
event.is_completed = True
|
||||
event.save()
|
||||
|
||||
# Award Badges Logic
|
||||
donor_profile = event.donor_user.profile
|
||||
completed_count = DonationEvent.objects.filter(donor_user=event.donor_user, is_completed=True).count()
|
||||
|
||||
if completed_count >= 1:
|
||||
badge = Badge.objects.filter(name='First-Time Donor').first()
|
||||
if badge:
|
||||
donor_profile.badges.add(badge)
|
||||
|
||||
if completed_count >= 5:
|
||||
badge = Badge.objects.filter(name='Community Hero').first()
|
||||
if badge:
|
||||
donor_profile.badges.add(badge)
|
||||
|
||||
if completed_count >= 10:
|
||||
badge = Badge.objects.filter(name='Life Saver').first()
|
||||
if badge:
|
||||
donor_profile.badges.add(badge)
|
||||
|
||||
# Notify both
|
||||
Notification.objects.create(
|
||||
user=event.donor_user,
|
||||
message=f"Thank you for your donation to {event.request.patient_name}!"
|
||||
message=f"Thank you for your donation to {event.request.patient_name}! You've earned recognition for your impact."
|
||||
)
|
||||
if event.request.user:
|
||||
Notification.objects.create(
|
||||
@ -485,11 +633,29 @@ def chat(request, username):
|
||||
other_user = User.objects.get(username=username)
|
||||
if request.method == "POST":
|
||||
content = request.POST.get('content')
|
||||
if content:
|
||||
attachment = request.FILES.get('attachment')
|
||||
sticker_id = request.POST.get('sticker_id')
|
||||
|
||||
msg_type = 'text'
|
||||
if sticker_id:
|
||||
msg_type = 'sticker'
|
||||
elif attachment:
|
||||
content_type = attachment.content_type
|
||||
if content_type.startswith('image/'):
|
||||
msg_type = 'image'
|
||||
elif content_type.startswith('video/'):
|
||||
msg_type = 'video'
|
||||
else:
|
||||
msg_type = 'file'
|
||||
|
||||
if content or attachment or sticker_id:
|
||||
Message.objects.create(
|
||||
sender=request.user,
|
||||
receiver=other_user,
|
||||
content=content
|
||||
content=content,
|
||||
attachment=attachment,
|
||||
message_type=msg_type,
|
||||
sticker_id=sticker_id
|
||||
)
|
||||
return redirect('chat', username=username)
|
||||
|
||||
@ -502,3 +668,37 @@ def chat(request, username):
|
||||
Message.objects.filter(sender=other_user, receiver=request.user, is_read=False).update(is_read=True)
|
||||
|
||||
return render(request, 'core/chat.html', {'other_user': other_user, 'chat_messages': messages})
|
||||
|
||||
@login_required
|
||||
def health_report_list(request):
|
||||
reports = HealthReport.objects.filter(user=request.user).order_by('-report_date')
|
||||
return render(request, 'core/health_report_list.html', {'reports': reports})
|
||||
|
||||
@login_required
|
||||
def upload_health_report(request):
|
||||
if request.method == "POST":
|
||||
title = request.POST.get('title')
|
||||
hospital_name = request.POST.get('hospital_name')
|
||||
report_file = request.FILES.get('report_file')
|
||||
description = request.POST.get('description')
|
||||
report_date = request.POST.get('report_date')
|
||||
next_test_date = request.POST.get('next_test_date')
|
||||
allow_notifications = request.POST.get('allow_notifications') == 'on'
|
||||
|
||||
if title and report_file and report_date:
|
||||
HealthReport.objects.create(
|
||||
user=request.user,
|
||||
title=title,
|
||||
hospital_name=hospital_name,
|
||||
report_file=report_file,
|
||||
description=description,
|
||||
report_date=report_date,
|
||||
next_test_date=next_test_date if next_test_date else None,
|
||||
allow_notifications=allow_notifications
|
||||
)
|
||||
messages.success(request, "Health report uploaded successfully!")
|
||||
return redirect('health_report_list')
|
||||
else:
|
||||
messages.error(request, "Please fill in all required fields.")
|
||||
|
||||
return render(request, 'core/upload_health_report.html')
|
||||
|
||||
BIN
media/profile_pics/1000048209.jpg
Normal file
BIN
media/profile_pics/1000048209.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
Loading…
x
Reference in New Issue
Block a user