Autosave: 20260219-045105

This commit is contained in:
Flatlogic Bot 2026-02-19 04:51:05 +00:00
parent e38c53b0c9
commit fd84fa729b
27 changed files with 947 additions and 86 deletions

View File

@ -15,14 +15,10 @@ class UserRegisterForm(UserCreationForm):
fields = ['username', 'email'] fields = ['username', 'email']
class UserUpdateForm(forms.ModelForm): class UserUpdateForm(forms.ModelForm):
email = forms.EmailField()
class Meta: class Meta:
model = User model = User
fields = ['username', 'email', 'first_name', 'last_name'] fields = ['first_name', 'last_name']
widgets = { widgets = {
'username': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'first_name': forms.TextInput(attrs={'class': 'form-control'}), 'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_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 ProfileUpdateForm(forms.ModelForm):
class Meta: class Meta:
model = UserProfile model = UserProfile
fields = ['bio', 'location', 'phone', 'blood_group', 'profile_pic'] fields = ['bio', 'location', 'phone', 'birth_date', 'blood_group', 'profile_pic']
widgets = { widgets = {
'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), 'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'location': forms.TextInput(attrs={'class': 'form-control'}), 'location': forms.TextInput(attrs={'class': 'form-control'}),
'phone': 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), 'blood_group': forms.Select(attrs={'class': 'form-control'}, choices=BLOOD_GROUPS),
'profile_pic': forms.FileInput(attrs={'class': 'form-control'}), 'profile_pic': forms.FileInput(attrs={'class': 'form-control'}),
} }

View File

@ -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),
),
]

View 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'),
),
]

View File

@ -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),
),
]

View 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/'),
),
]

View 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)),
],
),
]

View File

@ -11,14 +11,26 @@ BLOOD_GROUPS = [
('AB+', 'AB+'), ('AB-', 'AB-'), ('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): class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
bio = models.TextField(max_length=500, blank=True) bio = models.TextField(max_length=500, blank=True)
location = models.CharField(max_length=100, 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) birth_date = models.DateField(null=True, blank=True)
phone = models.CharField(max_length=20, blank=True) phone = models.CharField(max_length=20, blank=True)
blood_group = models.CharField(max_length=5, choices=BLOOD_GROUPS, 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) profile_pic = models.ImageField(upload_to='profile_pics', blank=True, null=True)
badges = models.ManyToManyField(Badge, blank=True, related_name='users')
def __str__(self): def __str__(self):
return self.user.username 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.name = f"{instance.user.first_name} {instance.user.last_name}".strip() or instance.user.username
donor.blood_group = instance.blood_group donor.blood_group = instance.blood_group
donor.location = instance.location donor.location = instance.location
donor.latitude = instance.latitude
donor.longitude = instance.longitude
donor.phone = instance.phone donor.phone = instance.phone
donor.save() donor.save()
@ -103,6 +117,7 @@ class BloodRequest(models.Model):
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, 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) longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
contact_number = models.CharField(max_length=20) 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) required_date = models.DateField(default=timezone.now)
status = models.CharField(max_length=20, default='Active') status = models.CharField(max_length=20, default='Active')
created_at = models.DateTimeField(auto_now_add=True) 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]}..." return f"Notification for {self.user.username}: {self.message[:20]}..."
class Message(models.Model): 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') sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages')
receiver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_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) timestamp = models.DateTimeField(auto_now_add=True)
is_read = models.BooleanField(default=False) is_read = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return f"From {self.sender.username} to {self.receiver.username} at {self.timestamp}" 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}"

View File

@ -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> <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 %} {% 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 '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 %} {% endif %}
<li><a href="/admin/"><i class="bi bi-gear-fill"></i> {% trans "Settings" %}</a></li> <li><a href="/admin/"><i class="bi bi-gear-fill"></i> {% trans "Settings" %}</a></li>
</ul> </ul>
@ -511,5 +512,46 @@
</script> </script>
{% block scripts %}{% endblock %} {% 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> </body>
</html> </html>

View File

@ -47,7 +47,14 @@
</div> </div>
</div> </div>
<h5 class="mb-1 fw-bold text-dark">{{ req.patient_name }}</h5> <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="mt-auto pt-3 border-top">
<div class="d-flex justify-content-between align-items-center"> <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> <span class="small text-muted"><i class="bi bi-clock me-1"></i> {{ req.created_at|timesince }} ago</span>

View File

@ -128,7 +128,28 @@
<div class="chat-messages" id="chatMessages"> <div class="chat-messages" id="chatMessages">
{% for msg in chat_messages %} {% for msg in chat_messages %}
<div class="message {% if msg.sender == user %}message-sent{% else %}message-received{% endif %}"> <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 %}"> <span class="message-time {% if msg.sender == user %}text-white-50{% else %}text-secondary{% endif %}">
{{ msg.timestamp|date:"g:i a" }} {{ msg.timestamp|date:"g:i a" }}
</span> </span>
@ -140,16 +161,86 @@
{% endfor %} {% endfor %}
</div> </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 %} {% csrf_token %}
<input type="text" name="content" class="form-control rounded-pill px-4" placeholder="Type your message..." autocomplete="off" required> <div id="attachmentPreview" class="p-2 border rounded-3 bg-light d-none align-items-center gap-2">
<button type="submit" class="btn btn-danger rounded-circle d-flex align-items-center justify-content-center" style="width: 45px; height: 45px;"> <div id="previewContent" class="flex-grow-1 small text-truncate"></div>
<i class="bi bi-send-fill"></i> <button type="button" class="btn btn-sm btn-outline-danger border-0 rounded-circle" onclick="clearAttachment()">
</button> <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> </form>
</div> </div>
</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 --> <!-- Call Modal -->
<div class="modal fade" id="callModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-hidden="true"> <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"> <div class="modal-dialog modal-lg modal-dialog-centered">
@ -228,11 +319,42 @@
{ url: 'stun:stun4.l.google.com:19302' }, { url: 'stun:stun4.l.google.com:19302' },
] ]
}, },
debug: 1 debug: 3
}); });
let localStream; let localStream;
let currentCall; 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 callModal = new bootstrap.Modal(document.getElementById('callModal'));
const incomingCallUI = document.getElementById('incomingCallUI'); const incomingCallUI = document.getElementById('incomingCallUI');
@ -264,7 +386,6 @@
peer.on('call', (call) => { peer.on('call', (call) => {
console.log('Incoming call from: ' + call.peer); console.log('Incoming call from: ' + call.peer);
if (call.peer === PEER_ID_PREFIX + sanitizeId(OTHER_USERNAME)) { if (call.peer === PEER_ID_PREFIX + sanitizeId(OTHER_USERNAME)) {
// If already in a call, busy logic could go here
currentCall = call; currentCall = call;
incomingCallUI.style.display = 'block'; incomingCallUI.style.display = 'block';
playRingtone('incoming'); playRingtone('incoming');
@ -280,12 +401,14 @@
// Start Call Function // Start Call Function
async function startCall(videoEnabled = true) { 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 { try {
callStatus.innerText = "Requesting permissions..."; callStatus.innerText = "Requesting permissions...";
localStream = await navigator.mediaDevices.getUserMedia({ localStream = await getLocalStream(videoEnabled);
video: videoEnabled,
audio: true
});
localVideo.srcObject = localStream; localVideo.srcObject = localStream;
callModal.show(); callModal.show();
@ -296,7 +419,10 @@
handleCall(call); handleCall(call);
} catch (err) { } catch (err) {
console.error('Failed to get local stream', 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(); stopRingtones();
try { try {
callStatus.innerText = "Answering..."; callStatus.innerText = "Answering...";
localStream = await navigator.mediaDevices.getUserMedia({ // Try to get video if possible, but fallback to audio
video: true, localStream = await getLocalStream(true);
audio: true
});
localVideo.srcObject = localStream; localVideo.srcObject = localStream;
callModal.show(); callModal.show();
currentCall.answer(localStream); 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 // Handle cleanup on page unload
window.addEventListener('beforeunload', () => { window.addEventListener('beforeunload', () => {
endCall(); endCall();
peer.destroy(); peer.destroy();
if (cameraStream) {
cameraStream.getTracks().forEach(track => track.stop());
}
}); });
</script> </script>
{% endblock %} {% endblock %}

View 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 %}

View File

@ -112,11 +112,23 @@
{% block content %} {% block content %}
<div class="container-fluid p-0"> <div class="container-fluid p-0">
<!-- Welcome Header --> <!-- Welcome Header -->
<div class="row mb-4"> <div class="row mb-4 align-items-center">
<div class="col-12"> <div class="col-md-7">
<h2 class="brand-font mb-1">{% trans "RaktaPulse Community Dashboard" %}</h2> <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> </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> </div>
{% if involved_events %} {% if involved_events %}
@ -143,42 +155,51 @@
</div> </div>
{% endif %} {% endif %}
<!-- Quick Stats --> <!-- Quick Stats & Impact Counter -->
<div class="row g-4 mb-5"> <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"> <a href="{% url 'donor_list' %}" class="stat-card">
<div class="icon-box bg-danger bg-opacity-10 text-danger"> <div class="icon-box bg-danger bg-opacity-10 text-danger">
<i class="bi bi-people"></i> <i class="bi bi-people"></i>
</div> </div>
<div class="stat-value">{{ stats.total_donors }}</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> </a>
</div> </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"> <a href="{% url 'blood_request_list' %}?status=Active" class="stat-card">
<div class="icon-box bg-warning bg-opacity-10 text-warning"> <div class="icon-box bg-warning bg-opacity-10 text-warning">
<i class="bi bi-activity"></i> <i class="bi bi-activity"></i>
</div> </div>
<div class="stat-value">{{ stats.active_requests }}</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> </a>
</div> </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="stat-card">
<div class="icon-box bg-primary bg-opacity-10 text-primary"> <div class="icon-box bg-primary bg-opacity-10 text-primary">
<i class="bi bi-droplet"></i> <i class="bi bi-droplet"></i>
</div> </div>
<div class="stat-value">{{ stats.total_stock }} <small class="fs-6 fw-normal">/ {{ stats.total_capacity }}</small></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 class="stat-label">{% trans "Stock Level" %}</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> </div>
</div> </div>
</div> </div>
@ -308,6 +329,12 @@
Urgent Requests Urgent Requests
<span class="badge bg-danger rounded-pill px-2" style="font-size: 0.6rem;">HOT</span> <span class="badge bg-danger rounded-pill px-2" style="font-size: 0.6rem;">HOT</span>
</h5> </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"> <div class="request-feed">
{% for req in blood_requests %} {% for req in blood_requests %}
<div class="mb-4 border-bottom border-light pb-3"> <div class="mb-4 border-bottom border-light pb-3">
@ -367,36 +394,22 @@
</div> </div>
</div> </div>
<!-- Blood & Vaccine Facts Section --> <!-- Myths vs Facts Section -->
<div class="row g-4 mt-2 mb-5"> <div class="row g-4 mt-2 mb-5">
<div class="col-12"> <div class="col-12">
<div class="glass-card"> <div class="glass-card bg-light bg-opacity-10 border-danger">
<h4 class="brand-font mb-4"><i class="bi bi-info-circle text-danger me-2"></i>Blood & Vaccine Facts</h4> <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="row g-4">
<div class="col-md-3"> {% for item in myths_vs_facts %}
<div class="p-3 border border-light rounded-4 h-100 bg-light bg-opacity-10"> <div class="col-md-4 col-lg-2 {% if forloop.first %}offset-lg-1{% endif %}">
<h6 class="fw-bold text-danger"><i class="bi bi-droplet-fill me-2"></i>Lifesaver</h6> <div class="myth-fact-card p-3 h-100 bg-white rounded-4 shadow-sm border-bottom border-danger border-3">
<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 class="text-danger small fw-bold mb-2">{% trans "MYTH" %}</div>
</div> <p class="small text-muted mb-3 italic">"{{ item.myth }}"</p>
</div> <div class="text-success small fw-bold mb-1">{% trans "FACT" %}</div>
<div class="col-md-3"> <p class="extra-small text-dark mb-0">{{ item.fact }}</p>
<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>
{% endfor %}
</div> </div>
</div> </div>
</div> </div>
@ -472,5 +485,47 @@
.bindPopup("<b>{{ donor.name }}</b><br>{{ donor.blood_group }} - {{ donor.location }}"); .bindPopup("<b>{{ donor.name }}</b><br>{{ donor.blood_group }} - {{ donor.location }}");
{% endfor %} {% 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> </script>
{% endblock %} {% endblock %}

View File

@ -27,12 +27,12 @@
<h5 class="fw-bold mb-3 border-bottom pb-2">Account Information</h5> <h5 class="fw-bold mb-3 border-bottom pb-2">Account Information</h5>
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label small fw-bold">Username</label> <label class="form-label small fw-bold text-muted">Username (Permanent)</label>
{{ u_form.username }} <input type="text" class="form-control bg-light border-0" value="{{ user.username }}" readonly>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label small fw-bold">Email Address</label> <label class="form-label small fw-bold text-muted">Email Address (Permanent)</label>
{{ u_form.email }} <input type="text" class="form-control bg-light border-0" value="{{ user.email }}" readonly>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label small fw-bold">First Name</label> <label class="form-label small fw-bold">First Name</label>
@ -60,21 +60,40 @@
{{ p_form.phone }} {{ p_form.phone }}
</div> </div>
<div class="col-md-4"> <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> <label class="form-label small fw-bold">Location</label>
{{ p_form.location }} {{ p_form.location }}
</div> </div>
<div class="col-12"> <div class="col-md-6">
<label class="form-label small fw-bold">Profile Picture</label> <label class="form-label small fw-bold">Profile Picture</label>
{{ p_form.profile_pic }} {{ p_form.profile_pic }}
</div> </div>
</div> </div>
<div class="d-grid"> <div class="d-grid mb-5">
<button type="submit" class="btn btn-danger py-2 fw-bold"> <button type="submit" class="btn btn-danger py-2 fw-bold">
<i class="bi bi-check-circle me-2"></i>Update Profile <i class="bi bi-check-circle me-2"></i>Update Profile
</button> </button>
</div> </div>
</form> </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> </div>
</div> </div>

View File

@ -16,7 +16,7 @@
<p class="text-secondary">Fill in the details to alert nearby donors.</p> <p class="text-secondary">Fill in the details to alert nearby donors.</p>
</div> </div>
<form method="POST" id="requestForm"> <form method="POST" id="requestForm" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
@ -52,6 +52,11 @@
<label class="form-label small fw-bold text-secondary">Contact Number *</label> <label class="form-label small fw-bold text-secondary">Contact Number *</label>
<input type="text" name="contact_number" class="form-control rounded-3" required> <input type="text" name="contact_number" class="form-control rounded-3" required>
</div> </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 --> <!-- Hidden coordinates -->
<input type="hidden" name="latitude" id="id_latitude"> <input type="hidden" name="latitude" id="id_latitude">

View 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 %}

View File

@ -6,7 +6,9 @@ from .views import (
vaccination_dashboard, add_vaccination, live_map, vaccination_dashboard, add_vaccination, live_map,
request_blood, profile, volunteer_for_request, request_blood, profile, volunteer_for_request,
complete_donation, notifications_view, 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 = [ urlpatterns = [
@ -25,11 +27,16 @@ urlpatterns = [
path("vaccination/", vaccination_info, name="vaccination_info"), path("vaccination/", vaccination_info, name="vaccination_info"),
path("vaccination/dashboard/", vaccination_dashboard, name="vaccination_dashboard"), path("vaccination/dashboard/", vaccination_dashboard, name="vaccination_dashboard"),
path("vaccination/add/", add_vaccination, name="add_vaccination"), 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("live-map/", live_map, name="live_map"),
path("request-blood/", request_blood, name="request_blood"), 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("volunteer/<int:request_id>/", volunteer_for_request, name="volunteer_for_request"),
path("complete-donation/<int:event_id>/", complete_donation, name="complete_donation"), path("complete-donation/<int:event_id>/", complete_donation, name="complete_donation"),
path("notifications/", notifications_view, name="notifications"), path("notifications/", notifications_view, name="notifications"),
path("register-donor/", register_donor, name="register_donor"), path("register-donor/", register_donor, name="register_donor"),
path("hospitals/", hospital_list, name="hospital_list"), 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"),
] ]

View File

@ -9,10 +9,87 @@ from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.utils import timezone from django.utils import timezone
from django.contrib.auth.models import User 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 .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): def hospital_list(request):
user_lat = request.GET.get('lat') user_lat = request.GET.get('lat')
user_lng = request.GET.get('lng') user_lng = request.GET.get('lng')
@ -61,6 +138,32 @@ def profile(request):
return render(request, 'core/profile.html', context) 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): def haversine(lat1, lon1, lat2, lon2):
# Radius of the Earth in km # Radius of the Earth in km
R = 6371.0 R = 6371.0
@ -122,6 +225,12 @@ def home(request):
user_lat = request.GET.get('lat') user_lat = request.GET.get('lat')
user_lng = request.GET.get('lng') 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() donors = Donor.objects.all()
if query_blood: if query_blood:
donors = donors.filter(blood_group=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_requests = BloodRequest.objects.filter(status='Active').order_by('-urgency', '-created_at')
blood_banks = BloodBank.objects.all() 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 = { stats = {
"total_donors": Donor.objects.count(), "total_donors": Donor.objects.count() + demo_donors,
"active_requests": BloodRequest.objects.filter(status='Active').count(), "active_requests": BloodRequest.objects.filter(status='Active').count(),
"total_stock": sum([ "total_stock": sum([
bb.stock_a_plus + bb.stock_a_minus + bb.stock_b_plus + bb.stock_b_minus + 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 for bb in blood_banks
]), ]),
"total_capacity": sum([bb.total_capacity * 8 for bb in blood_banks]), # 8 blood types "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"] total_d = stats["total_donors"]
@ -166,6 +283,14 @@ def home(request):
vaccinated_count = Donor.objects.filter(vaccination_status__icontains='Fully').count() vaccinated_count = Donor.objects.filter(vaccination_status__icontains='Fully').count()
stats["vaccinated_percentage"] = int((vaccinated_count / total_d) * 100) 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 = { context = {
"donors": donor_list_data[:8], "donors": donor_list_data[:8],
"blood_requests": blood_requests[:6], "blood_requests": blood_requests[:6],
@ -174,15 +299,17 @@ def home(request):
"stats": stats, "stats": stats,
"project_name": "RaktaPulse", "project_name": "RaktaPulse",
"current_time": timezone.now(), "current_time": timezone.now(),
"myths_vs_facts": myths_vs_facts,
} }
if request.user.is_authenticated: if request.user.is_authenticated:
# Get active involvements (where user is donor or requester) # Get active involvements
involved_events = DonationEvent.objects.filter( involved_events = DonationEvent.objects.filter(
(Q(donor_user=request.user) | Q(request__user=request.user)), (Q(donor_user=request.user) | Q(request__user=request.user)),
is_completed=False is_completed=False
) )
context["involved_events"] = involved_events context["involved_events"] = involved_events
context["user_badges"] = request.user.profile.badges.all()
return render(request, "core/index.html", context) return render(request, "core/index.html", context)
@ -292,6 +419,7 @@ def request_blood(request):
urgency = request.POST.get('urgency') urgency = request.POST.get('urgency')
hospital = request.POST.get('hospital') hospital = request.POST.get('hospital')
contact_number = request.POST.get('contact_number') contact_number = request.POST.get('contact_number')
image = request.FILES.get('image')
latitude = request.POST.get('latitude') latitude = request.POST.get('latitude')
longitude = request.POST.get('longitude') longitude = request.POST.get('longitude')
@ -304,6 +432,7 @@ def request_blood(request):
urgency=urgency, urgency=urgency,
hospital=hospital, hospital=hospital,
contact_number=contact_number, contact_number=contact_number,
image=image,
latitude=latitude if latitude else None, latitude=latitude if latitude else None,
longitude=longitude if longitude else None longitude=longitude if longitude else None
) )
@ -387,15 +516,34 @@ def volunteer_for_request(request, request_id):
@login_required @login_required
def complete_donation(request, event_id): def complete_donation(request, event_id):
event = DonationEvent.objects.get(id=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): if request.user == event.donor_user or (event.request.user and request.user == event.request.user):
event.is_completed = True event.is_completed = True
event.save() 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 # Notify both
Notification.objects.create( Notification.objects.create(
user=event.donor_user, 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: if event.request.user:
Notification.objects.create( Notification.objects.create(
@ -485,11 +633,29 @@ def chat(request, username):
other_user = User.objects.get(username=username) other_user = User.objects.get(username=username)
if request.method == "POST": if request.method == "POST":
content = request.POST.get('content') 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( Message.objects.create(
sender=request.user, sender=request.user,
receiver=other_user, receiver=other_user,
content=content content=content,
attachment=attachment,
message_type=msg_type,
sticker_id=sticker_id
) )
return redirect('chat', username=username) 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) 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}) 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')

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB