Initial import

This commit is contained in:
Flatlogic Bot 2026-03-11 07:41:51 +00:00
commit 21ef5153ee
750 changed files with 136010 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
*/node_modules/
*/build/

0
.perm_test_apache Normal file
View File

0
.perm_test_exec Normal file
View File

38
README.md Normal file
View File

@ -0,0 +1,38 @@
# 🩸 RaktaPulse - Community Blood Management System
Welcome to **RaktaPulse**, a community-driven platform built during the hackathon to connect blood donors with those in urgent need. Our mission is to ensure that no life is lost due to a lack of blood availability by leveraging real-time location data and community alerts.
## ✨ Features
- **📍 Real-time Donor Matching**: Find donors within a 10km radius using geolocation.
- **🚨 Emergency Alerts**: Simulated SMS broadcast to nearby donors for critical requests.
- **💬 P2P Chat**: Integrated messaging for donors and requesters to coordinate.
- **🛡️ Health Tracking**: Manage vaccination records and digital health reports.
- **🏆 Gamification**: Earn badges for your contributions to the community.
## 🛠️ Technical Stack
- **Backend**: Python 3.11, Django 5.x
- **Database**: MariaDB/MySQL
- **Frontend**: Bootstrap 5, FontAwesome, Leaflet.js (Maps)
- **Deployment**: Apache, Cloudflare Tunnel
## 🚀 Getting Started
1. **Install Dependencies**:
```bash
python3 -m pip install -r requirements.txt
```
2. **Database Setup**:
```bash
python3 manage.py migrate
```
3. **Run the Server**:
```bash
python3 manage.py runserver
```
---
*Developed with ❤️ for the Hackathon 2026. Let's save lives, one drop at a time.*

3
ai/__init__.py Normal file
View File

@ -0,0 +1,3 @@
"""Helpers for interacting with the Flatlogic AI proxy from Django code."""
from .local_ai_api import LocalAIApi, create_response, request, decode_json_from_response # noqa: F401

420
ai/local_ai_api.py Normal file
View File

@ -0,0 +1,420 @@
"""
LocalAIApi lightweight Python client for the Flatlogic AI proxy.
Usage (inside the Django workspace):
from ai.local_ai_api import LocalAIApi
response = LocalAIApi.create_response({
"input": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Summarise this text in two sentences."},
],
"text": {"format": {"type": "json_object"}},
})
if response.get("success"):
data = LocalAIApi.decode_json_from_response(response)
# ...
# Typical successful payload (truncated):
# {
# "id": "resp_xxx",
# "status": "completed",
# "output": [
# {"type": "reasoning", "summary": []},
# {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]}
# ],
# "usage": { "input_tokens": 123, "output_tokens": 456 }
# }
The helper automatically injects the project UUID header and falls back to
reading executor/.env if environment variables are missing.
"""
from __future__ import annotations
import json
import os
import time
import ssl
from typing import Any, Dict, Iterable, Optional
from urllib import error as urlerror
from urllib import request as urlrequest
__all__ = [
"LocalAIApi",
"create_response",
"request",
"fetch_status",
"await_response",
"extract_text",
"decode_json_from_response",
]
_CONFIG_CACHE: Optional[Dict[str, Any]] = None
class LocalAIApi:
"""Static helpers mirroring the PHP implementation."""
@staticmethod
def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
return create_response(params, options or {})
@staticmethod
def request(path: Optional[str] = None, payload: Optional[Dict[str, Any]] = None,
options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
return request(path, payload or {}, options or {})
@staticmethod
def extract_text(response: Dict[str, Any]) -> str:
return extract_text(response)
@staticmethod
def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]:
return decode_json_from_response(response)
def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Signature compatible with the OpenAI Responses API."""
options = options or {}
payload = dict(params)
if not isinstance(payload.get("input"), list) or not payload["input"]:
return {
"success": False,
"error": "input_missing",
"message": 'Parameter "input" is required and must be a non-empty list.',
}
cfg = _config()
if not payload.get("model"):
payload["model"] = cfg["default_model"]
initial = request(options.get("path"), payload, options)
if not initial.get("success"):
return initial
data = initial.get("data")
if isinstance(data, dict) and "ai_request_id" in data:
ai_request_id = data["ai_request_id"]
poll_timeout = int(options.get("poll_timeout", 300))
poll_interval = int(options.get("poll_interval", 5))
return await_response(ai_request_id, {
"interval": poll_interval,
"timeout": poll_timeout,
"headers": options.get("headers"),
"timeout_per_call": options.get("timeout"),
})
return initial
def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Perform a raw request to the AI proxy."""
cfg = _config()
options = options or {}
resolved_path = path or options.get("path") or cfg["responses_path"]
if not resolved_path:
return {
"success": False,
"error": "project_id_missing",
"message": "PROJECT_ID is not defined; cannot resolve AI proxy endpoint.",
}
project_uuid = cfg["project_uuid"]
if not project_uuid:
return {
"success": False,
"error": "project_uuid_missing",
"message": "PROJECT_UUID is not defined; aborting AI request.",
}
if "project_uuid" not in payload and project_uuid:
payload["project_uuid"] = project_uuid
url = _build_url(resolved_path, cfg["base_url"])
opt_timeout = options.get("timeout")
timeout = int(cfg["timeout"] if opt_timeout is None else opt_timeout)
verify_tls = options.get("verify_tls", cfg["verify_tls"])
headers: Dict[str, str] = {
"Content-Type": "application/json",
"Accept": "application/json",
cfg["project_header"]: project_uuid,
}
extra_headers = options.get("headers")
if isinstance(extra_headers, Iterable):
for header in extra_headers:
if isinstance(header, str) and ":" in header:
name, value = header.split(":", 1)
headers[name.strip()] = value.strip()
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
return _http_request(url, "POST", body, headers, timeout, verify_tls)
def fetch_status(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Fetch status for a queued AI request."""
cfg = _config()
options = options or {}
project_uuid = cfg["project_uuid"]
if not project_uuid:
return {
"success": False,
"error": "project_uuid_missing",
"message": "PROJECT_UUID is not defined; aborting status check.",
}
status_path = _resolve_status_path(ai_request_id, cfg)
url = _build_url(status_path, cfg["base_url"])
opt_timeout = options.get("timeout")
timeout = int(cfg["timeout"] if opt_timeout is None else opt_timeout)
verify_tls = options.get("verify_tls", cfg["verify_tls"])
headers: Dict[str, str] = {
"Accept": "application/json",
cfg["project_header"]: project_uuid,
}
extra_headers = options.get("headers")
if isinstance(extra_headers, Iterable):
for header in extra_headers:
if isinstance(header, str) and ":" in header:
name, value = header.split(":", 1)
headers[name.strip()] = value.strip()
return _http_request(url, "GET", None, headers, timeout, verify_tls)
def await_response(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Poll status endpoint until the request is complete or timed out."""
options = options or {}
timeout = int(options.get("timeout", 300))
interval = int(options.get("interval", 5))
if interval <= 0:
interval = 5
per_call_timeout = options.get("timeout_per_call")
deadline = time.time() + max(timeout, interval)
while True:
status_resp = fetch_status(ai_request_id, {
"headers": options.get("headers"),
"timeout": per_call_timeout,
"verify_tls": options.get("verify_tls"),
})
if status_resp.get("success"):
data = status_resp.get("data") or {}
if isinstance(data, dict):
status_value = data.get("status")
if status_value == "success":
return {
"success": True,
"status": 200,
"data": data.get("response", data),
}
if status_value == "failed":
return {
"success": False,
"status": 500,
"error": str(data.get("error") or "AI request failed"),
"data": data,
}
else:
return status_resp
if time.time() >= deadline:
return {
"success": False,
"error": "timeout",
"message": "Timed out waiting for AI response.",
}
time.sleep(interval)
def extract_text(response: Dict[str, Any]) -> str:
"""Public helper to extract plain text from a Responses payload."""
return _extract_text(response)
def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Attempt to decode JSON emitted by the model (handles markdown fences)."""
text = _extract_text(response)
if text == "":
return None
try:
decoded = json.loads(text)
if isinstance(decoded, dict):
return decoded
except json.JSONDecodeError:
pass
stripped = text.strip()
if stripped.startswith("```json"):
stripped = stripped[7:]
if stripped.endswith("```"):
stripped = stripped[:-3]
stripped = stripped.strip()
if stripped and stripped != text:
try:
decoded = json.loads(stripped)
if isinstance(decoded, dict):
return decoded
except json.JSONDecodeError:
return None
return None
def _extract_text(response: Dict[str, Any]) -> str:
payload = response.get("data") if response.get("success") else response.get("response")
if isinstance(payload, dict):
output = payload.get("output")
if isinstance(output, list):
combined = ""
for item in output:
content = item.get("content") if isinstance(item, dict) else None
if isinstance(content, list):
for block in content:
if isinstance(block, dict) and block.get("type") == "output_text" and block.get("text"):
combined += str(block["text"])
if combined:
return combined
choices = payload.get("choices")
if isinstance(choices, list) and choices:
message = choices[0].get("message")
if isinstance(message, dict) and message.get("content"):
return str(message["content"])
if isinstance(payload, str):
return payload
return ""
def _config() -> Dict[str, Any]:
global _CONFIG_CACHE # noqa: PLW0603
if _CONFIG_CACHE is not None:
return _CONFIG_CACHE
_ensure_env_loaded()
base_url = os.getenv("AI_PROXY_BASE_URL", "https://flatlogic.com")
project_id = os.getenv("PROJECT_ID") or None
responses_path = os.getenv("AI_RESPONSES_PATH")
if not responses_path and project_id:
responses_path = f"/projects/{project_id}/ai-request"
_CONFIG_CACHE = {
"base_url": base_url,
"responses_path": responses_path,
"project_id": project_id,
"project_uuid": os.getenv("PROJECT_UUID"),
"project_header": os.getenv("AI_PROJECT_HEADER", "project-uuid"),
"default_model": os.getenv("AI_DEFAULT_MODEL", "gpt-5-mini"),
"timeout": int(os.getenv("AI_TIMEOUT", "30")),
"verify_tls": os.getenv("AI_VERIFY_TLS", "true").lower() not in {"0", "false", "no"},
}
return _CONFIG_CACHE
def _build_url(path: str, base_url: str) -> str:
trimmed = path.strip()
if trimmed.startswith("http://") or trimmed.startswith("https://"):
return trimmed
if trimmed.startswith("/"):
return f"{base_url}{trimmed}"
return f"{base_url}/{trimmed}"
def _resolve_status_path(ai_request_id: Any, cfg: Dict[str, Any]) -> str:
base_path = (cfg.get("responses_path") or "").rstrip("/")
if not base_path:
return f"/ai-request/{ai_request_id}/status"
if not base_path.endswith("/ai-request"):
base_path = f"{base_path}/ai-request"
return f"{base_path}/{ai_request_id}/status"
def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[str, str],
timeout: int, verify_tls: bool) -> Dict[str, Any]:
"""
Shared HTTP helper for GET/POST requests.
"""
req = urlrequest.Request(url, data=body, method=method.upper())
for name, value in headers.items():
req.add_header(name, value)
context = None
if not verify_tls:
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
try:
with urlrequest.urlopen(req, timeout=timeout, context=context) as resp:
status = resp.getcode()
response_body = resp.read().decode("utf-8", errors="replace")
except urlerror.HTTPError as exc:
status = exc.getcode()
response_body = exc.read().decode("utf-8", errors="replace")
except Exception as exc: # pylint: disable=broad-except
return {
"success": False,
"error": "request_failed",
"message": str(exc),
}
decoded = None
if response_body:
try:
decoded = json.loads(response_body)
except json.JSONDecodeError:
decoded = None
if 200 <= status < 300:
return {
"success": True,
"status": status,
"data": decoded if decoded is not None else response_body,
}
error_message = "AI proxy request failed"
if isinstance(decoded, dict):
error_message = decoded.get("error") or decoded.get("message") or error_message
elif response_body:
error_message = response_body
return {
"success": False,
"status": status,
"error": error_message,
"response": decoded if decoded is not None else response_body,
}
def _ensure_env_loaded() -> None:
"""Populate os.environ from executor/.env if variables are missing."""
if os.getenv("PROJECT_UUID") and os.getenv("PROJECT_ID"):
return
env_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".env"))
if not os.path.exists(env_path):
return
try:
with open(env_path, "r", encoding="utf-8") as handle:
for line in handle:
stripped = line.strip()
if not stripped or stripped.startswith("#") or "=" not in stripped:
continue
key, value = stripped.split("=", 1)
key = key.strip()
value = value.strip().strip('\'"')
if key and not os.getenv(key):
os.environ[key] = value
except OSError:
pass

0
config/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
config/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for config project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_asgi_application()

201
config/settings.py Normal file
View File

@ -0,0 +1,201 @@
"""
Django settings for config project.
Generated by 'django-admin startproject' using Django 5.2.7.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
import os
from dotenv import load_dotenv
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR.parent / ".env")
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "change-me")
DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
ALLOWED_HOSTS = [
"127.0.0.1",
"localhost",
"raktapulse-platform.flatlogic.app",
os.getenv("HOST_FQDN", ""),
]
CSRF_TRUSTED_ORIGINS = [
origin for origin in [
"raktapulse-platform.flatlogic.app",
os.getenv("HOST_FQDN", ""),
os.getenv("CSRF_TRUSTED_ORIGIN", "")
] if origin
]
CSRF_TRUSTED_ORIGINS = [
f"https://{host}" if not host.startswith(("http://", "https://")) else host
for host in CSRF_TRUSTED_ORIGINS
]
# Cookies must always be HTTPS-only; SameSite=Lax keeps CSRF working behind the proxy.
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = "None"
CSRF_COOKIE_SAMESITE = "None"
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'core',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
X_FRAME_OPTIONS = 'ALLOWALL'
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
# IMPORTANT: do not remove injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
'core.context_processors.project_context',
'core.context_processors.unread_messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.getenv('DB_NAME', ''),
'USER': os.getenv('DB_USER', ''),
'PASSWORD': os.getenv('DB_PASS', ''),
'HOST': os.getenv('DB_HOST', '127.0.0.1'),
'PORT': os.getenv('DB_PORT', '3306'),
'OPTIONS': {
'charset': 'utf8mb4',
},
},
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
LANGUAGES = [
('en', 'English'),
('ne', 'Nepali'),
]
LOCALE_PATHS = [
BASE_DIR / 'locale',
]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'static',
BASE_DIR / 'assets',
BASE_DIR / 'node_modules',
]
# Email
EMAIL_BACKEND = os.getenv(
"EMAIL_BACKEND",
"django.core.mail.backends.smtp.EmailBackend"
)
EMAIL_HOST = os.getenv("EMAIL_HOST", "127.0.0.1")
EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587"))
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "true").lower() == "true"
EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "false").lower() == "true"
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "no-reply@example.com")
CONTACT_EMAIL_TO = [
item.strip()
for item in os.getenv("CONTACT_EMAIL_TO", DEFAULT_FROM_EMAIL).split(",")
if item.strip()
]
# When both TLS and SSL flags are enabled, prefer SSL explicitly
if EMAIL_USE_SSL:
EMAIL_USE_TLS = False
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

35
config/urls.py Normal file
View File

@ -0,0 +1,35 @@
"""
URL configuration for config project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path("admin/", admin.site.urls),
path("i18n/", include("django.conf.urls.i18n")),
path("", include("core.urls")),
]
admin.site.site_header = "Blood Bank Management System"
admin.site.site_title = "Admin Portal"
admin.site.index_title = "Welcome to Blood Bank Management Portal"
if settings.DEBUG:
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

16
config/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for config project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_wsgi_application()

0
core/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

90
core/admin.py Normal file
View File

@ -0,0 +1,90 @@
from django.contrib import admin
import csv
from django.http import HttpResponse
from .models import (
Donor, BloodRequest, BloodBank, VaccineRecord,
DonationEvent, Hospital, UserProfile, Badge,
Notification, Message, HealthReport
)
def export_as_csv(self, request, queryset):
meta = self.model._meta
field_names = [field.name for field in meta.fields]
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename={meta}.csv'
writer = csv.writer(response)
writer.writerow(field_names)
for obj in queryset:
writer.writerow([getattr(obj, field) for field in field_names])
return response
export_as_csv.short_description = "Export Selected to CSV"
@admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
list_display = ('user', 'blood_group', 'location', 'phone')
list_filter = ('blood_group',)
search_fields = ('user__username', 'phone', 'location')
actions = [export_as_csv]
@admin.register(Badge)
class BadgeAdmin(admin.ModelAdmin):
list_display = ('name', 'description')
@admin.register(VaccineRecord)
class VaccineRecordAdmin(admin.ModelAdmin):
list_display = ('vaccine_name', 'user', 'dose_number', 'date_taken', 'location')
list_filter = ('vaccine_name', 'date_taken')
search_fields = ('vaccine_name', 'user__username', 'location')
actions = [export_as_csv]
@admin.register(Donor)
class DonorAdmin(admin.ModelAdmin):
list_display = ('name', 'blood_group', 'location', 'is_available', 'is_verified')
list_filter = ('blood_group', 'is_available', 'is_verified')
search_fields = ('name', 'location', 'phone')
actions = [export_as_csv]
@admin.register(Hospital)
class HospitalAdmin(admin.ModelAdmin):
list_display = ('name', 'location', 'phone')
search_fields = ('name', 'location')
@admin.register(BloodRequest)
class BloodRequestAdmin(admin.ModelAdmin):
list_display = ('patient_name', 'blood_group', 'urgency', 'status', 'hospital', 'created_at')
list_filter = ('blood_group', 'urgency', 'status')
search_fields = ('patient_name', 'hospital')
actions = [export_as_csv]
@admin.register(BloodBank)
class BloodBankAdmin(admin.ModelAdmin):
list_display = ('name', 'location', 'stock_a_plus', 'stock_b_plus', 'stock_o_plus', 'stock_ab_plus')
search_fields = ('name', 'location')
actions = [export_as_csv]
@admin.register(DonationEvent)
class DonationEventAdmin(admin.ModelAdmin):
list_display = ('donor', 'request', 'date', 'is_completed')
list_filter = ('is_completed', 'date')
search_fields = ('donor__name', 'request__patient_name')
actions = [export_as_csv]
@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
list_display = ('user', 'message', 'is_read', 'created_at')
list_filter = ('is_read', 'created_at')
@admin.register(Message)
class MessageAdmin(admin.ModelAdmin):
list_display = ('sender', 'receiver', 'message_type', 'timestamp', 'is_read')
list_filter = ('message_type', 'is_read', 'timestamp')
@admin.register(HealthReport)
class HealthReportAdmin(admin.ModelAdmin):
list_display = ('title', 'user', 'hospital_name', 'report_date')
list_filter = ('report_date',)
search_fields = ('title', 'user__username', 'hospital_name')

6
core/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core'

View File

@ -0,0 +1,21 @@
import os
import time
from .models import Message
def unread_messages(request):
if request.user.is_authenticated:
return {
'unread_messages_count': Message.objects.filter(receiver=request.user, is_read=False).count()
}
return {'unread_messages_count': 0}
def project_context(request):
"""
Adds project-specific environment variables to the template context globally.
"""
return {
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
# Used for cache-busting static assets
"deployment_timestamp": int(time.time()),
}

41
core/forms.py Normal file
View File

@ -0,0 +1,41 @@
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
from .models import UserProfile, BLOOD_GROUPS
# Registration
class UserRegisterForm(UserCreationForm):
"""Custom registration form to capture blood group and location."""
email = forms.EmailField(required=True)
blood_group = forms.ChoiceField(choices=BLOOD_GROUPS, required=True)
location = forms.CharField(max_length=255, required=True)
phone = forms.CharField(max_length=20, required=True)
class Meta:
model = User
fields = ['username', 'email']
# Profile Management
class UserUpdateForm(forms.ModelForm):
class Meta:
model = User
fields = ['first_name', 'last_name']
widgets = {
'first_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'First Name'}),
'last_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Last Name'}),
}
class ProfileUpdateForm(forms.ModelForm):
class Meta:
model = UserProfile
fields = ['bio', 'location', 'phone', 'birth_date', 'blood_group', 'profile_pic']
widgets = {
'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Tell us a bit about yourself...'}),
'location': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Current City/Area'}),
'phone': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '+977...'}),
'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'}),
}

View File

Binary file not shown.

View File

View File

@ -0,0 +1,61 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from django.db.models import Q
from core.models import BloodRequest
from datetime import timedelta
class Command(BaseCommand):
help = 'Deletes old blood requests based on urgency and status'
def handle(self, *args, **options):
now = timezone.now()
# Define thresholds (days)
thresholds = {
'CRITICAL': 3,
'URGENT': 7,
'NORMAL': 15,
}
deleted_count = 0
# 1. Delete by Urgency
for urgency, days in thresholds.items():
cutoff = now - timedelta(days=days)
old_requests = BloodRequest.objects.filter(
urgency=urgency,
created_at__lt=cutoff
)
count = old_requests.count()
if count > 0:
old_requests.delete()
self.stdout.write(self.style.SUCCESS(f'Deleted {count} {urgency} requests older than {days} days.'))
deleted_count += count
# 2. Delete non-Active and non-Accepted requests older than 7 days
cutoff_inactive = now - timedelta(days=7)
inactive_requests = BloodRequest.objects.exclude(
Q(status='Active') | Q(status='Accepted')
).filter(created_at__lt=cutoff_inactive)
count_inactive = inactive_requests.count()
if count_inactive > 0:
inactive_requests.delete()
self.stdout.write(self.style.SUCCESS(f'Deleted {count_inactive} non-active/non-accepted requests older than 7 days.'))
deleted_count += count_inactive
# 3. For Accepted requests, we keep them for 30 days after acceptance for history
cutoff_accepted = now - timedelta(days=30)
old_accepted = BloodRequest.objects.filter(
status='Accepted',
accepted_at__lt=cutoff_accepted
)
count_accepted = old_accepted.count()
if count_accepted > 0:
old_accepted.delete()
self.stdout.write(self.style.SUCCESS(f'Deleted {count_accepted} accepted requests older than 30 days.'))
deleted_count += count_accepted
if deleted_count == 0:
self.stdout.write(self.style.SUCCESS('No old requests to delete.'))
else:
self.stdout.write(self.style.SUCCESS(f'Cleanup complete. Total deleted: {deleted_count}'))

View File

@ -0,0 +1,47 @@
from django.core.management.base import BaseCommand
from core.models import Hospital
class Command(BaseCommand):
help = 'Seed the database with Kathmandu hospitals and coordinates'
def handle(self, *args, **kwargs):
hospitals = [
{"name": "Bir Hospital", "location": "Kanti Path, Kathmandu", "phone": "01-4221988", "lat": 27.7058, "lng": 85.3135},
{"name": "Kathmandu Valley Hospital", "location": "Bagh darbar marg, Kathmandu", "phone": "01-4255330", "lat": 27.7015, "lng": 85.3115},
{"name": "Civil Service Hospital of Nepal", "location": "Minbhawan marg, Kathmandu", "phone": "01-4793000", "website": "https://csh.gov.np/ne", "lat": 27.6841, "lng": 85.3385},
{"name": "Venus Hospital", "location": "Devkota Sadak, Kathmandu", "phone": "01-4475120", "lat": 27.6945, "lng": 85.3415},
{"name": "Vayodha Hospital", "location": "Balkhu, Kathmandu", "phone": "01-4281666", "website": "https://www.vayodhahospitals.com/", "lat": 27.6855, "lng": 85.2935},
{"name": "Grande City Hospital", "location": "Kanti path, Kathmandu", "phone": "01-4163500", "website": "http://grandecityhospital.com/", "lat": 27.7065, "lng": 85.3135},
{"name": "Teaching Hospital", "location": "Maharajgunj, Kathmandu", "phone": "01-4412303", "website": "http://iom.edu.np/", "lat": 27.7351, "lng": 85.3315},
{"name": "Kathmandu Hospital", "location": "Tripureshwor marg, Kathmandu", "phone": "01-4229656", "lat": 27.6935, "lng": 85.3145},
{"name": "Kathmandu Neuro & General Hospital", "location": "Bagh Durbar marg, Kathmandu", "phone": "01-5327735", "lat": 27.7018, "lng": 85.3118},
{"name": "Teku Hospital", "location": "Teku, Kathmandu", "phone": "01-4253396", "website": "http://www.stidh.gov.np/", "lat": 27.6961, "lng": 85.3025},
{"name": "Nepal Eye Hospital", "location": "Tripureswor, Kathmandu", "phone": "01-4260813", "website": "https://www.nepaleyehospital.org/", "lat": 27.6940, "lng": 85.3140},
{"name": "Everest Hospital Pvt. Ltd.", "location": "Kathmandu", "phone": "01-4793024", "website": "http://everesthospital.org.np/", "lat": 27.6925, "lng": 85.3345},
{"name": "Om Hospital & Research Center", "location": "Chabil, Kathmandu", "phone": "01-4476225", "website": "https://omhospitalnepal.com/", "lat": 27.7175, "lng": 85.3485},
{"name": "Annapurna Neuro Hospital", "location": "Maitighar mandala", "phone": "01-4256656", "website": "https://www.annapurnahospitals.com", "lat": 27.6945, "lng": 85.3215},
{"name": "Norvic International Hospital", "location": "Kathmandu", "phone": "01-5970032", "website": "https://www.norvichospital.com/", "lat": 27.6897, "lng": 85.3235},
{"name": "Blue Cross Hospital", "location": "Tripura marga, Kathmandu", "phone": "01-4262027", "lat": 27.6948, "lng": 85.3148},
{"name": "Paropakar Maternity and Womens Hospital", "location": "Thapathali, Kathmandu", "phone": "01-4261363", "lat": 27.6915, "lng": 85.3215},
{"name": "ERA INTERNATIONAL HOSPITAL Pvt. Ltd.", "location": "Kathmandu", "phone": "01-4352447", "website": "https://www.era-hospital.com/", "lat": 27.7215, "lng": 85.3015},
{"name": "Birendra Military Hospital", "location": "Kathmandu", "phone": "01-4271941", "website": "https://birendrahospital.nepalarmy.mil.np/", "lat": 27.7085, "lng": 85.2815},
{"name": "Capital Hospital", "location": "Kathmandu", "phone": "01-4168222", "lat": 27.7045, "lng": 85.3215},
{"name": "Valley Maternity Nursing Home", "location": "Kathmandu", "phone": "01-4420224", "lat": 27.7125, "lng": 85.3245},
{"name": "Medicare National Hospital & Research Center", "location": "Ring road, kathmandu", "phone": "01-4467067", "website": "http://medicarehosp.com/", "lat": 27.7145, "lng": 85.3415},
{"name": "CIWEC Hospital Pvt. Ltd.", "location": "Kathmandu", "phone": "01-4435232", "lat": 27.7165, "lng": 85.3215},
{"name": "Nepal-Bharat Maitri Hospital", "location": "Dakshinha murti marga, Kathmandu", "phone": "01-5241288", "lat": 27.7185, "lng": 85.3515},
{"name": "Kantipur Hospital", "location": "Shriganesh Marg, Tripureshwor", "phone": "01-4111", "lat": 27.6938, "lng": 85.3142},
]
for h_data in hospitals:
Hospital.objects.update_or_create(
name=h_data['name'],
defaults={
'location': h_data['location'],
'phone': h_data['phone'],
'website': h_data.get('website', ''),
'latitude': h_data.get('lat'),
'longitude': h_data.get('lng'),
}
)
self.stdout.write(self.style.SUCCESS(f'Successfully updated {len(hospitals)} hospitals with coordinates.'))

View File

@ -0,0 +1,61 @@
# Generated by Django 5.2.7 on 2026-02-17 13:38
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='BloodBank',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('location', models.CharField(max_length=255)),
('stock_a_plus', models.IntegerField(default=0)),
('stock_a_minus', models.IntegerField(default=0)),
('stock_b_plus', models.IntegerField(default=0)),
('stock_b_minus', models.IntegerField(default=0)),
('stock_o_plus', models.IntegerField(default=0)),
('stock_o_minus', models.IntegerField(default=0)),
('stock_ab_plus', models.IntegerField(default=0)),
('stock_ab_minus', models.IntegerField(default=0)),
],
),
migrations.CreateModel(
name='BloodRequest',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('patient_name', models.CharField(max_length=100)),
('blood_group', models.CharField(choices=[('A+', 'A+'), ('A-', 'A-'), ('B+', 'B+'), ('B-', 'B-'), ('O+', 'O+'), ('O-', 'O-'), ('AB+', 'AB+'), ('AB-', 'AB-')], max_length=5)),
('location', models.CharField(max_length=255)),
('urgency', models.CharField(choices=[('CRITICAL', 'Critical'), ('URGENT', 'Urgent'), ('NORMAL', 'Normal')], default='NORMAL', max_length=10)),
('hospital', models.CharField(max_length=255)),
('contact_number', models.CharField(max_length=20)),
('required_date', models.DateField(default=django.utils.timezone.now)),
('status', models.CharField(default='Active', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='Donor',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('blood_group', models.CharField(choices=[('A+', 'A+'), ('A-', 'A-'), ('B+', 'B+'), ('B-', 'B-'), ('O+', 'O+'), ('O-', 'O-'), ('AB+', 'AB+'), ('AB-', 'AB-')], max_length=5)),
('location', models.CharField(max_length=255)),
('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('phone', models.CharField(max_length=20)),
('is_available', models.BooleanField(default=True)),
('last_donation_date', models.DateField(blank=True, null=True)),
('vaccination_status', models.CharField(default='Unknown', max_length=100)),
],
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 5.2.7 on 2026-02-17 13:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='bloodbank',
name='contact_number',
field=models.CharField(blank=True, max_length=20, null=True),
),
migrations.AddField(
model_name='bloodbank',
name='is_24_7',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='donor',
name='avatar_url',
field=models.URLField(blank=True, null=True),
),
migrations.AddField(
model_name='donor',
name='email',
field=models.EmailField(blank=True, max_length=254, null=True),
),
migrations.AddField(
model_name='donor',
name='last_vaccination_date',
field=models.DateField(blank=True, null=True),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2026-02-17 13:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_bloodbank_contact_number_bloodbank_is_24_7_and_more'),
]
operations = [
migrations.AddField(
model_name='donor',
name='citizenship_no',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AddField(
model_name='donor',
name='district',
field=models.CharField(default='Kathmandu', max_length=100),
),
migrations.AddField(
model_name='donor',
name='is_verified',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,30 @@
# Generated by Django 5.2.7 on 2026-02-17 14:37
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_donor_citizenship_no_donor_district_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='VaccineRecord',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('vaccine_name', models.CharField(max_length=100)),
('dose_number', models.PositiveIntegerField(default=1)),
('date_taken', models.DateField()),
('location', models.CharField(max_length=255)),
('center_name', models.CharField(blank=True, max_length=255, null=True)),
('notes', models.TextField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vaccine_records', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2026-02-17 15:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_vaccinerecord'),
]
operations = [
migrations.AddField(
model_name='bloodbank',
name='latitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
migrations.AddField(
model_name='bloodbank',
name='longitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2026-02-17 15:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_bloodbank_latitude_bloodbank_longitude'),
]
operations = [
migrations.AddField(
model_name='bloodrequest',
name='latitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
migrations.AddField(
model_name='bloodrequest',
name='longitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-02-17 16:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0006_bloodrequest_latitude_bloodrequest_longitude'),
]
operations = [
migrations.AddField(
model_name='bloodbank',
name='total_capacity',
field=models.IntegerField(default=1000),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2026-02-17 16:13
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0007_bloodbank_total_capacity'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('bio', models.TextField(blank=True, max_length=500)),
('location', models.CharField(blank=True, max_length=100)),
('birth_date', models.DateField(blank=True, null=True)),
('phone', models.CharField(blank=True, max_length=20)),
('blood_group', models.CharField(blank=True, choices=[('A+', 'A+'), ('A-', 'A-'), ('B+', 'B+'), ('B-', 'B-'), ('O+', 'O+'), ('O-', 'O-'), ('AB+', 'AB+'), ('AB-', 'AB-')], max_length=5)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,58 @@
# Generated by Django 5.2.7 on 2026-02-18 05:21
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0008_userprofile'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='bloodrequest',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='blood_requests', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='donor',
name='user',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='donor_profile', to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name='DonationEvent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateTimeField(default=django.utils.timezone.now)),
('is_completed', models.BooleanField(default=False)),
('donor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='donations', to='core.donor')),
('donor_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='my_donations', to=settings.AUTH_USER_MODEL)),
('request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='donations', to='core.bloodrequest')),
],
),
migrations.CreateModel(
name='Feedback',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField()),
('rating', models.PositiveIntegerField(default=5)),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Notification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('message', models.TextField()),
('is_read', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,16 @@
# Generated by Django 5.2.7 on 2026-02-18 05:57
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0009_bloodrequest_user_donor_user_donationevent_feedback_and_more'),
]
operations = [
migrations.DeleteModel(
name='Feedback',
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2026-02-18 06:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0010_delete_feedback'),
]
operations = [
migrations.CreateModel(
name='Hospital',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('location', models.CharField(max_length=255)),
('phone', models.CharField(blank=True, max_length=100, null=True)),
('website', models.URLField(blank=True, null=True)),
],
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2026-02-18 06:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0011_hospital'),
]
operations = [
migrations.AddField(
model_name='hospital',
name='latitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
migrations.AddField(
model_name='hospital',
name='longitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-02-18 07:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0012_hospital_latitude_hospital_longitude'),
]
operations = [
migrations.AddField(
model_name='userprofile',
name='profile_pic',
field=models.ImageField(blank=True, null=True, upload_to='profile_pics'),
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 5.2.7 on 2026-02-18 07:37
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0013_userprofile_profile_pic'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Message',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
('is_read', models.BooleanField(default=False)),
('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL)),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
],
),
]

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

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-02-19 05:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0019_healthreport'),
]
operations = [
migrations.AddField(
model_name='vaccinerecord',
name='photo',
field=models.ImageField(blank=True, null=True, upload_to='vaccine_photos/'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-02-27 13:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0020_vaccinerecord_photo'),
]
operations = [
migrations.AddField(
model_name='bloodrequest',
name='accepted_at',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

Binary file not shown.

217
core/models.py Normal file
View File

@ -0,0 +1,217 @@
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
# --- Constants & Choices ---
BLOOD_GROUPS = [
('A+', 'A+'), ('A-', 'A-'),
('B+', 'B+'), ('B-', 'B-'),
('O+', 'O+'), ('O-', 'O-'),
('AB+', 'AB+'), ('AB-', 'AB-'),
]
# --- Models ---
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
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 f"{self.user.username}'s Profile"
# --- Signals for Data Consistency ---
@receiver(post_save, sender=User)
def create_or_save_user_profile(sender, instance, created, **kwargs):
"""Auto-creates a profile when a new User is registered."""
if created:
UserProfile.objects.get_or_create(user=instance)
else:
# Fallback for users created without signals (e.g. management commands)
if not hasattr(instance, 'profile'):
UserProfile.objects.create(user=instance)
instance.profile.save()
@receiver(post_save, sender=UserProfile)
def sync_donor_profile(sender, instance, **kwargs):
"""Keeps the Donor record in sync with the UserProfile."""
if instance.blood_group:
donor, _ = Donor.objects.get_or_create(user=instance.user)
# Use first/last name if available, otherwise fallback to username
full_name = f"{instance.user.first_name} {instance.user.last_name}".strip()
donor.name = full_name 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()
# --- Health & Medical Records ---
class VaccineRecord(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='vaccine_records')
vaccine_name = models.CharField(max_length=100)
dose_number = models.PositiveIntegerField(default=1)
date_taken = models.DateField()
location = models.CharField(max_length=255)
center_name = models.CharField(max_length=255, null=True, blank=True)
notes = models.TextField(blank=True, null=True)
photo = models.ImageField(upload_to='vaccine_photos/', null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.vaccine_name} (Dose {self.dose_number}) - {self.user.username}"
# --- Core Blood Donation Models ---
class Donor(models.Model):
user = models.OneToOneField(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='donor_profile')
name = models.CharField(max_length=100)
blood_group = models.CharField(max_length=5, choices=BLOOD_GROUPS)
district = models.CharField(max_length=100, default='Kathmandu')
location = models.CharField(max_length=255)
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)
phone = models.CharField(max_length=20)
email = models.EmailField(null=True, blank=True)
is_available = models.BooleanField(default=True)
is_verified = models.BooleanField(default=False)
citizenship_no = models.CharField(max_length=50, null=True, blank=True)
last_donation_date = models.DateField(null=True, blank=True)
vaccination_status = models.CharField(max_length=100, default='Unknown')
last_vaccination_date = models.DateField(null=True, blank=True)
avatar_url = models.URLField(null=True, blank=True)
def __str__(self):
status = "Verified" if self.is_verified else "Pending"
return f"{self.name} [{self.blood_group}] - {status}"
class Hospital(models.Model):
name = models.CharField(max_length=255)
location = models.CharField(max_length=255)
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)
phone = models.CharField(max_length=100, null=True, blank=True)
website = models.URLField(null=True, blank=True)
def __str__(self):
return self.name
class BloodRequest(models.Model):
URGENCY_LEVELS = [
('CRITICAL', 'Critical'),
('URGENT', 'Urgent'),
('NORMAL', 'Normal'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blood_requests', null=True, blank=True)
patient_name = models.CharField(max_length=100)
blood_group = models.CharField(max_length=5, choices=BLOOD_GROUPS)
location = models.CharField(max_length=255)
urgency = models.CharField(max_length=10, choices=URGENCY_LEVELS, default='NORMAL')
hospital = models.CharField(max_length=255)
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')
accepted_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.blood_group} for {self.patient_name}"
class BloodBank(models.Model):
name = models.CharField(max_length=100)
location = models.CharField(max_length=255)
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, null=True, blank=True)
is_24_7 = models.BooleanField(default=True)
stock_a_plus = models.IntegerField(default=0)
stock_a_minus = models.IntegerField(default=0)
stock_b_plus = models.IntegerField(default=0)
stock_b_minus = models.IntegerField(default=0)
stock_o_plus = models.IntegerField(default=0)
stock_o_minus = models.IntegerField(default=0)
stock_ab_plus = models.IntegerField(default=0)
stock_ab_minus = models.IntegerField(default=0)
total_capacity = models.IntegerField(default=1000)
def __str__(self):
return self.name
class DonationEvent(models.Model):
donor = models.ForeignKey(Donor, on_delete=models.CASCADE, related_name='donations')
request = models.ForeignKey(BloodRequest, on_delete=models.CASCADE, related_name='donations')
donor_user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='my_donations')
date = models.DateTimeField(default=timezone.now)
is_completed = models.BooleanField(default=False)
def __str__(self):
return f"Donation by {self.donor.name} for {self.request.patient_name}"
class Notification(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications')
message = models.TextField()
is_read = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Notification for {self.user.username}: {self.message[:20]}..."
class 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(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}"

575
core/templates/base.html Normal file
View File

@ -0,0 +1,575 @@
{% load i18n %}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}RaktaPulse Dashboard{% endblock %}</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
:root {
--dark-bg: #f8f9fa;
--sidebar-bg: #ffffff;
--card-bg: #ffffff;
--pulse-red: #E63946;
--pulse-red-light: #fff5f5;
--pulse-red-glow: rgba(230, 57, 70, 0.1);
--text-primary: #2b2d42;
--text-secondary: #6c757d;
--border-color: rgba(0, 0, 0, 0.08);
--sidebar-width: 260px;
}
body {
background-color: var(--dark-bg);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
margin: 0;
overflow-x: hidden;
}
/* Remove blue underline from links */
a {
text-decoration: none !important;
color: inherit;
}
a:hover {
color: var(--pulse-red);
}
@keyframes blink {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(1.1); }
100% { opacity: 1; transform: scale(1); }
}
.blinking-logo {
animation: blink 1.5s infinite ease-in-out;
display: inline-block;
}
h1, h2, h3, .brand-font {
font-family: 'Outfit', sans-serif;
}
/* F-Shape Layout */
.wrapper {
display: flex;
width: 100%;
align-items: stretch;
}
/* Sidebar (Vertical stroke of F) */
#sidebar {
width: var(--sidebar-width);
background: var(--sidebar-bg);
height: 100vh;
position: fixed;
left: 0;
top: 0;
z-index: 1000;
border-right: 1px solid var(--border-color);
transition: all 0.3s;
overflow-y: auto;
}
#sidebar.collapsed {
margin-left: calc(-1 * var(--sidebar-width));
}
.sidebar-header {
padding: 30px 25px;
display: flex;
align-items: center;
}
.sidebar-menu {
padding: 0;
list-style: none;
}
.sidebar-menu li a {
padding: 15px 25px;
display: flex;
align-items: center;
color: var(--text-secondary);
text-decoration: none;
transition: all 0.3s;
font-weight: 500;
border-left: 4px solid transparent;
}
.sidebar-menu li a:hover, .sidebar-menu li.active a {
color: var(--pulse-red);
background: var(--pulse-red-light);
border-left-color: var(--pulse-red);
}
.sidebar-menu li a i {
margin-right: 15px;
font-size: 1.2rem;
}
/* Content Area */
#content {
width: calc(100% - var(--sidebar-width));
margin-left: var(--sidebar-width);
min-height: 100vh;
transition: all 0.3s;
}
#content.expanded {
width: 100%;
margin-left: 0;
}
/* Top Bar (Horizontal stroke 1 of F) */
.top-bar {
height: 70px;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
padding: 0 30px;
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 999;
}
.search-box {
background: #f1f3f5;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 6px 15px;
display: flex;
align-items: center;
width: 300px;
}
.search-box input {
background: transparent;
border: none;
color: var(--text-primary);
padding-left: 10px;
width: 100%;
}
.search-box input:focus {
outline: none;
}
/* Glass Cards */
.glass-card {
background: var(--card-bg);
border-radius: 16px;
border: 1px solid var(--border-color);
padding: 24px;
transition: transform 0.3s ease;
}
.glass-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
/* Responsive */
@media (max-width: 992px) {
#sidebar { margin-left: calc(-1 * var(--sidebar-width)); }
#content { width: 100%; margin-left: 0; }
#sidebar.active { margin-left: 0; }
}
/* SOS Floating Button */
.sos-btn {
position: fixed;
bottom: 30px;
right: 30px;
width: 65px;
height: 65px;
background-color: var(--pulse-red);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
box-shadow: 0 4px 15px rgba(230, 57, 70, 0.4);
z-index: 9999;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
font-weight: 800;
font-size: 1.1rem;
border: 3px solid white;
}
.sos-btn:hover {
transform: scale(1.1);
color: white;
box-shadow: 0 6px 20px rgba(230, 57, 70, 0.6);
}
.sos-btn .pulse-ring {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: var(--pulse-red);
opacity: 0.6;
animation: sos-pulse 2s infinite;
z-index: -1;
}
.dropdown-toggle.no-caret::after {
display: none;
}
.sos-content {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1;
}
.sos-text {
font-size: 1.1rem;
font-weight: 800;
}
.sos-number {
font-size: 0.7rem;
font-weight: 600;
margin-top: 2px;
}
@keyframes sos-pulse {
0% { transform: scale(1); opacity: 0.6; }
100% { transform: scale(1.8); opacity: 0; }
}
@media (max-width: 576px) {
.sos-btn {
bottom: 20px;
right: 20px;
width: 55px;
height: 55px;
font-size: 0.9rem;
}
}
{% block head %}{% endblock %}
</style>
</head>
<body>
<div class="wrapper">
<!-- Sidebar -->
<nav id="sidebar">
<div class="sidebar-header">
<a href="/" class="text-decoration-none d-flex align-items-center">
<div class="bg-danger rounded-circle d-flex align-items-center justify-content-center me-2 blinking-logo" style="width: 35px; height: 35px;">
<i class="bi bi-droplet-fill text-white"></i>
</div>
<span class="fs-4 fw-bold text-danger brand-font">RaktaPulse</span>
</a>
</div>
<ul class="sidebar-menu mt-4">
<li class="{% if request.resolver_match.url_name == 'home' %}active{% endif %}"><a href="{% url 'home' %}"><i class="bi bi-grid-1x2-fill"></i> {% trans "Dashboard" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'donor_list' %}active{% endif %}"><a href="{% url 'donor_list' %}"><i class="bi bi-people-fill"></i> {% trans "Donors" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'blood_request_list' %}active{% endif %}"><a href="{% url 'blood_request_list' %}"><i class="bi bi-megaphone-fill"></i> {% trans "Blood Requests" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'donation_history' %}active{% endif %}"><a href="{% url 'donation_history' %}"><i class="bi bi-clock-history"></i> {% trans "Donation History" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'lives_saved' %}active{% endif %}"><a href="{% url 'lives_saved' %}"><i class="bi bi-heart-pulse"></i> {% trans "Lives Saved" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'blood_bank_list' %}active{% endif %}"><a href="{% url 'blood_bank_list' %}"><i class="bi bi-droplet-half"></i> {% trans "Blood Banks" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'hospital_list' %}active{% endif %}"><a href="{% url 'hospital_list' %}"><i class="bi bi-hospital-fill"></i> {% trans "Hospitals" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'live_map' %}active{% endif %}"><a href="{% url 'live_map' %}"><i class="bi bi-map text-danger"></i> {% trans "Live Alerts" %}</a></li>
<li class="{% if request.resolver_match.url_name == 'vaccination_info' %}active{% endif %}"><a href="{% url 'vaccination_info' %}"><i class="bi bi-shield-check"></i> {% trans "Vaccination" %}</a></li>
{% 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>
{% endif %}
{% if user.is_superuser %}
<li><a href="/admin/"><i class="bi bi-gear-fill"></i> {% trans "Settings" %}</a></li>
{% endif %}
</ul>
</nav>
<!-- Page Content -->
<div id="content">
<!-- Top Bar -->
<div class="top-bar justify-content-between flex-wrap">
<div class="d-flex align-items-center">
<button type="button" id="sidebarCollapse" class="btn btn-link text-danger me-3 p-0">
<i class="bi bi-list fs-3"></i>
</button>
<form action="{% url 'donor_list' %}" method="GET" class="search-box d-none d-lg-flex">
<button type="submit" class="btn btn-link p-0 text-secondary border-0">
<i class="bi bi-search"></i>
</button>
<input type="text" name="q" placeholder="Search anything..." value="{{ request.GET.q }}">
</form>
</div>
<div class="d-flex align-items-center gap-2 gap-md-4">
<!-- Language Switcher -->
<div class="dropdown">
<button class="btn btn-link text-secondary dropdown-toggle d-flex align-items-center gap-1 p-0 text-decoration-none small" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-translate"></i>
<span class="d-none d-sm-inline">{{ LANGUAGE_CODE|upper }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<li>
<form action="{% url 'set_language' %}" method="post">
{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<input name="language" type="hidden" value="{{ language.code }}">
<button type="submit" class="dropdown-item {% if language.code == LANGUAGE_CODE %}active{% endif %}">
{{ language.name_local }} ({{ language.code|upper }})
</button>
</form>
</li>
{% endfor %}
</ul>
</div>
<div id="location-status" class="d-none d-xl-flex align-items-center text-secondary small cursor-pointer" style="cursor: pointer;" onclick="detectLocation()">
<i class="bi bi-geo-alt me-1"></i>
<span id="location-text">Detect Location</span>
</div>
<div class="d-none d-md-flex align-items-center text-secondary small">
<i class="bi bi-calendar3 me-1"></i>
{{ current_time|date:"M d" }}
</div>
{% if user.is_authenticated %}
<div class="d-flex align-items-center gap-2">
<!-- Notification Bell -->
<a href="{% url 'notifications' %}" class="text-decoration-none me-2 position-relative">
<i class="bi bi-bell-fill fs-5 text-secondary"></i>
{% if unread_notifications_count > 0 %}
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="font-size: 0.6rem;">
{{ unread_notifications_count }}
</span>
{% endif %}
</a>
<!-- Messages Inbox -->
<a href="{% url 'inbox' %}" class="text-decoration-none me-2 position-relative">
<i class="bi bi-chat-dots-fill fs-5 text-secondary"></i>
{% if unread_messages_count > 0 %}
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="font-size: 0.6rem;">
{{ unread_messages_count }}
</span>
{% endif %}
</a>
<!-- Profile Dropdown -->
<div class="dropdown">
<a href="#" class="text-decoration-none d-flex align-items-center gap-2 dropdown-toggle no-caret" id="profileDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<div class="rounded-circle overflow-hidden border border-danger-subtle d-flex align-items-center justify-content-center" style="width: 35px; height: 35px;">
{% if user.profile.profile_pic %}
<img src="{{ user.profile.profile_pic.url }}" alt="{{ user.username }}" class="w-100 h-100 object-fit-cover">
{% else %}
<div class="bg-danger bg-opacity-10 w-100 h-100 d-flex align-items-center justify-content-center">
<i class="bi bi-person-fill text-danger"></i>
</div>
{% endif %}
</div>
<span class="d-none d-sm-inline fw-bold text-dark">{{ user.username }}</span>
</a>
<ul class="dropdown-menu dropdown-menu-end shadow border-0 mt-2" aria-labelledby="profileDropdown">
<li>
<a class="dropdown-item d-flex align-items-center gap-2 py-2" href="{% url 'profile' %}">
<i class="bi bi-person-circle text-danger"></i>
<span>{% trans "My Profile" %}</span>
</a>
</li>
<li>
<a class="dropdown-item d-flex align-items-center gap-2 py-2" href="{% url 'profile' %}">
<i class="bi bi-pencil-square text-danger"></i>
<span>{% trans "Change Name" %}</span>
</a>
</li>
<li><hr class="dropdown-divider opacity-50"></li>
<li>
<a class="dropdown-item d-flex align-items-center gap-2 py-2 text-danger" href="{% url 'logout' %}">
<i class="bi bi-box-arrow-right"></i>
<span>{% trans "Logout" %}</span>
</a>
</li>
</ul>
</div>
</div>
{% else %}
<div class="d-flex align-items-center gap-2">
<a href="{% url 'login' %}" class="btn btn-danger btn-sm px-2 px-sm-3">Login</a>
<a href="{% url 'register' %}" class="btn btn-outline-danger btn-sm px-2 px-sm-3">Register</a>
</div>
{% endif %}
</div>
</div>
<div class="p-4 p-md-5">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show shadow-sm border-0 mb-4" role="alert">
{% if message.tags == 'success' %}<i class="bi bi-check-circle-fill me-2"></i>{% endif %}
{% if message.tags == 'error' %}<i class="bi bi-exclamation-triangle-fill me-2"></i>{% endif %}
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% block content %}{% endblock %}
</div>
</div>
</div>
<!-- Bootstrap 5 Bundle JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
document.getElementById('sidebarCollapse').addEventListener('click', function() {
document.getElementById('sidebar').classList.toggle('collapsed');
document.getElementById('content').classList.toggle('expanded');
// For mobile view, handle the 'active' class as well if needed
if (window.innerWidth <= 992) {
document.getElementById('sidebar').classList.toggle('active');
}
});
function detectLocation() {
const text = document.getElementById('location-text');
const originalText = text.innerText;
text.innerText = "Locating...";
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(function(position) {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
text.innerText = lat.toFixed(4) + ", " + lng.toFixed(4);
// Save to cookies for 1 day
document.cookie = `user_lat=${lat}; path=/; max-age=86400`;
document.cookie = `user_lng=${lng}; path=/; max-age=86400`;
// If we are on a list page without lat/lng, reload with them
const urlParams = new URLSearchParams(window.location.search);
if (!urlParams.has('lat')) {
urlParams.set('lat', lat);
urlParams.set('lng', lng);
window.location.search = urlParams.toString();
}
}, function(error) {
text.innerText = "Error";
console.error(error);
});
} else {
text.innerText = "Not supported";
}
}
// Check if location is already in cookies
function checkLocationCookie() {
const cookies = document.cookie.split(';');
let lat = null, lng = null;
for (let cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'user_lat') lat = value;
if (name === 'user_lng') lng = value;
}
if (lat && lng) {
document.getElementById('location-text').innerText = parseFloat(lat).toFixed(4) + ", " + parseFloat(lng).toFixed(4);
}
}
checkLocationCookie();
</script>
<a href="javascript:void(0);" class="sos-btn" id="sosButton" title="Single click: Request Form | Double click: Call 1115">
<div class="pulse-ring"></div>
<div class="sos-content">
<span class="sos-text">SOS</span>
<span class="sos-number">1115</span>
</div>
</a>
<script>
(function() {
const sosBtn = document.getElementById('sosButton');
let clickTimer = null;
sosBtn.addEventListener('click', function(e) {
e.preventDefault();
if (clickTimer === null) {
// First click: start a timer
clickTimer = setTimeout(function() {
clickTimer = null;
// Single click action: Go to request form
window.location.href = "{% url 'request_blood' %}";
}, 300); // 300ms window for double click
} else {
// Second click within 300ms: clear timer and trigger call
clearTimeout(clickTimer);
clickTimer = null;
window.location.href = "tel:1115";
}
});
})();
</script>
{% block scripts %}{% endblock %}
{% 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>

View File

@ -0,0 +1,79 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Add Vaccination Record - {{ project_name }}{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="glass-card shadow-lg p-4 p-md-5">
<div class="text-center mb-4">
<div class="icon-box bg-danger bg-opacity-10 text-danger rounded-circle d-inline-flex p-3 mb-3">
<i class="bi bi-shield-plus fs-1"></i>
</div>
<h2 class="brand-font text-dark">Add Vaccine Record</h2>
<p class="text-secondary">Keep your immunization data accurate and up to date.</p>
</div>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-3">
<label class="form-label text-dark fw-bold">Vaccine Name</label>
<select name="vaccine_name" class="form-select bg-light border-secondary text-dark" required>
<option value="">Select Vaccine...</option>
<option value="COVID-19 (Covishield/AstraZeneca)">COVID-19 (Covishield/AstraZeneca)</option>
<option value="COVID-19 (Vero Cell)">COVID-19 (Vero Cell)</option>
<option value="COVID-19 (Pfizer/BioNTech)">COVID-19 (Pfizer/BioNTech)</option>
<option value="COVID-19 (Moderna)">COVID-19 (Moderna)</option>
<option value="COVID-19 (Johnson & Johnson)">COVID-19 (Johnson & Johnson)</option>
<option value="Hepatitis B">Hepatitis B</option>
<option value="Influenza (Flu)">Influenza (Flu)</option>
<option value="HPV">HPV</option>
<option value="Tetanus">Tetanus</option>
<option value="Other">Other</option>
</select>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label text-dark fw-bold">Dose Number</label>
<input type="number" name="dose_number" class="form-control bg-light border-secondary text-dark" min="1" value="1" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label text-dark fw-bold">Date Taken</label>
<input type="date" name="date_taken" class="form-control bg-light border-secondary text-dark" required>
</div>
</div>
<div class="mb-3">
<label class="form-label text-dark fw-bold">City/District</label>
<input type="text" name="location" class="form-control bg-light border-secondary text-dark" placeholder="e.g. Kathmandu" required>
</div>
<div class="mb-3">
<label class="form-label text-dark fw-bold">Center Name</label>
<input type="text" name="center_name" class="form-control bg-light border-secondary text-dark" placeholder="e.g. Teaching Hospital">
</div>
<div class="mb-3">
<label class="form-label text-dark fw-bold">Certificate Photo (Optional)</label>
<input type="file" name="photo" class="form-control bg-light border-secondary text-dark" accept="image/*">
</div>
<div class="mb-4">
<label class="form-label text-dark fw-bold">Notes (Optional)</label>
<textarea name="notes" class="form-control bg-light border-secondary text-dark" rows="3" placeholder="Any side effects or remarks..."></textarea>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-danger py-3 rounded-pill fw-bold shadow-sm">Save Record</button>
<a href="{% url 'vaccination_dashboard' %}" class="btn btn-light py-3 rounded-pill fw-bold">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% block title %}{{ article.title }}{% endblock %}
{% block content %}
<div class="container mt-5">
<h1>{{ article.title }}</h1>
<p class="text-muted">Published on {{ article.created_at|date:"F d, Y" }}</p>
<hr>
<div>
{{ article.content|safe }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,165 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Blood Banks - RaktaPulse{% endblock %}
{% block content %}
<div class="container-fluid p-0">
<div class="row mb-4">
<div class="col-md-8">
<h2 class="brand-font mb-1">Blood Banks</h2>
<p class="text-secondary">Official blood repositories and their current inventory levels.</p>
</div>
<div class="col-md-4 text-md-end">
<button type="button" id="findNearestBankBtn" class="btn btn-primary shadow-sm">
<i class="bi bi-geo-alt-fill me-1"></i> Nearest First
</button>
<a href="{% url 'blood_bank_list' %}" class="btn btn-outline-secondary ms-2">Reset</a>
<form id="bankFilterForm" method="GET" style="display:none;">
<input type="hidden" name="lat" id="latInputBank">
<input type="hidden" name="lng" id="lngInputBank">
</form>
</div>
</div>
<div class="row">
{% for bank in banks %}
<div class="col-md-6 mb-4">
<div class="glass-card h-100">
<div class="d-flex justify-content-between align-items-center mb-2">
<h4 class="brand-font mb-0 text-dark">{{ bank.name }}</h4>
{% if bank.is_24_7 %}
<span class="badge bg-success bg-opacity-10 text-success">24/7 Available</span>
{% endif %}
</div>
{% if bank.distance and bank.distance < 1000 %}
<div class="mb-3">
<span class="badge bg-info bg-opacity-10 text-info" style="font-size: 0.75rem;">
<i class="bi bi-distribute-vertical me-1"></i> {{ bank.distance|floatformat:1 }} km away from you
</span>
</div>
{% endif %}
<p class="text-secondary mb-3"><i class="bi bi-geo-alt me-2"></i> {{ bank.location }}</p>
<p class="text-secondary mb-4">
<a href="tel:{{ bank.contact_number }}" class="text-decoration-none text-secondary">
<i class="bi bi-telephone me-2"></i> {{ bank.contact_number }}
</a>
</p>
<h6 class="fw-bold text-dark mb-3">Inventory Levels (Max {{ bank.total_capacity }} units/type)</h6>
<div class="row g-2 mb-4">
<div class="col-3">
<div class="p-2 border rounded text-center bg-light">
<div class="extra-small text-muted">A+</div>
<div class="fw-bold">{{ bank.stock_a_plus }}</div>
<div class="progress mt-1" style="height: 4px;">
<div class="progress-bar bg-danger" style="width: {% widthratio bank.stock_a_plus bank.total_capacity 100 %}%"></div>
</div>
</div>
</div>
<div class="col-3">
<div class="p-2 border rounded text-center bg-light">
<div class="extra-small text-muted">A-</div>
<div class="fw-bold">{{ bank.stock_a_minus }}</div>
<div class="progress mt-1" style="height: 4px;">
<div class="progress-bar bg-danger" style="width: {% widthratio bank.stock_a_minus bank.total_capacity 100 %}%"></div>
</div>
</div>
</div>
<div class="col-3">
<div class="p-2 border rounded text-center bg-light">
<div class="extra-small text-muted">B+</div>
<div class="fw-bold">{{ bank.stock_b_plus }}</div>
<div class="progress mt-1" style="height: 4px;">
<div class="progress-bar bg-danger" style="width: {% widthratio bank.stock_b_plus bank.total_capacity 100 %}%"></div>
</div>
</div>
</div>
<div class="col-3">
<div class="p-2 border rounded text-center bg-light">
<div class="extra-small text-muted">B-</div>
<div class="fw-bold">{{ bank.stock_b_minus }}</div>
<div class="progress mt-1" style="height: 4px;">
<div class="progress-bar bg-danger" style="width: {% widthratio bank.stock_b_minus bank.total_capacity 100 %}%"></div>
</div>
</div>
</div>
<div class="col-3">
<div class="p-2 border rounded text-center bg-light">
<div class="extra-small text-muted">O+</div>
<div class="fw-bold">{{ bank.stock_o_plus }}</div>
<div class="progress mt-1" style="height: 4px;">
<div class="progress-bar bg-danger" style="width: {% widthratio bank.stock_o_plus bank.total_capacity 100 %}%"></div>
</div>
</div>
</div>
<div class="col-3">
<div class="p-2 border rounded text-center bg-light">
<div class="extra-small text-muted">O-</div>
<div class="fw-bold">{{ bank.stock_o_minus }}</div>
<div class="progress mt-1" style="height: 4px;">
<div class="progress-bar bg-danger" style="width: {% widthratio bank.stock_o_minus bank.total_capacity 100 %}%"></div>
</div>
</div>
</div>
<div class="col-3">
<div class="p-2 border rounded text-center bg-light">
<div class="extra-small text-muted">AB+</div>
<div class="fw-bold">{{ bank.stock_ab_plus }}</div>
<div class="progress mt-1" style="height: 4px;">
<div class="progress-bar bg-danger" style="width: {% widthratio bank.stock_ab_plus bank.total_capacity 100 %}%"></div>
</div>
</div>
</div>
<div class="col-3">
<div class="p-2 border rounded text-center bg-light">
<div class="extra-small text-muted">AB-</div>
<div class="fw-bold">{{ bank.stock_ab_minus }}</div>
<div class="progress mt-1" style="height: 4px;">
<div class="progress-bar bg-danger" style="width: {% widthratio bank.stock_ab_minus bank.total_capacity 100 %}%"></div>
</div>
</div>
</div>
</div>
<a href="tel:{{ bank.contact_number }}" class="btn btn-danger w-100 rounded-pill">Contact Bank</a>
</div>
</div>
{% empty %}
<div class="col-12 text-center py-5">
<i class="bi bi-hospital fs-1 text-secondary opacity-25"></i>
<p class="text-secondary mt-3">No blood banks registered in the system.</p>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.getElementById('findNearestBankBtn').addEventListener('click', function() {
const btn = this;
const originalContent = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Locating...';
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(function(position) {
document.getElementById('latInputBank').value = position.coords.latitude;
document.getElementById('lngInputBank').value = position.coords.longitude;
document.getElementById('bankFilterForm').submit();
}, function(error) {
alert("Error getting location: " + error.message);
btn.disabled = false;
btn.innerHTML = originalContent;
});
} else {
alert("Geolocation is not supported by this browser.");
btn.disabled = false;
btn.innerHTML = originalContent;
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,131 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Blood Requests - RaktaPulse{% endblock %}
{% block content %}
<style>
.urgency-badge {
padding: 4px 10px;
border-radius: 6px;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
display: inline-flex;
align-items: center;
gap: 5px;
}
.bg-critical { background: #FF4D4D; color: #fff; box-shadow: 0 0 10px rgba(255, 77, 77, 0.4); }
.bg-urgent { background: #FFA500; color: #000; }
.bg-normal { background: #4CAF50; color: #fff; }
.request-card {
border-left: 5px solid transparent !important;
transition: all 0.3s ease;
}
.request-card.border-critical { border-left-color: #FF4D4D !important; }
.request-card.border-urgent { border-left-color: #FFA500 !important; }
.request-card.border-normal { border-left-color: #4CAF50 !important; }
@keyframes pulse-red {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.8; }
100% { transform: scale(1); opacity: 1; }
}
.pulse-icon {
animation: pulse-red 2s infinite ease-in-out;
}
</style>
<div class="container-fluid p-0">
<div class="row mb-4">
<div class="col-12 d-flex justify-content-between align-items-center">
<div>
<h2 class="brand-font mb-1">{% if current_status %}{{ current_status }} {% endif %}Blood Requests</h2>
<p class="text-secondary">{% if current_status == 'Active' %}Current urgent requirements for blood.{% else %}History of blood requests in our community.{% endif %}</p>
</div>
<a href="{% url 'request_blood' %}" class="btn btn-danger rounded-pill px-4">Post a Request</a>
</div>
</div>
<div class="glass-card mb-4">
<div class="row">
{% for req in requests %}
<div class="col-md-6 col-xl-4 mb-4">
<div class="p-3 border rounded h-100 bg-light d-flex flex-column request-card border-{{ req.urgency|lower }}">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex gap-2">
<span class="urgency-badge bg-{{ req.urgency|lower }}">
{% if req.urgency == 'CRITICAL' %}
<i class="bi bi-exclamation-triangle-fill pulse-icon"></i>
{% elif req.urgency == 'URGENT' %}
<i class="bi bi-exclamation-circle-fill"></i>
{% else %}
<i class="bi bi-info-circle-fill"></i>
{% endif %}
{{ req.urgency }}
</span>
{% if req.status == 'Active' %}
<span class="badge bg-success bg-opacity-10 text-success small" style="font-size: 0.7rem;">Active</span>
{% else %}
<span class="badge bg-secondary bg-opacity-10 text-secondary small" style="font-size: 0.7rem;">{{ req.status }}</span>
{% endif %}
</div>
<div class="bg-danger bg-opacity-10 text-danger rounded-circle d-flex align-items-center justify-content-center fw-bold" style="width: 40px; height: 40px;">
{{ req.blood_group }}
</div>
</div>
<h5 class="mb-1 fw-bold text-dark">{{ req.patient_name }}</h5>
<p class="text-secondary small mb-2"><i class="bi bi-hospital me-1"></i> {{ req.hospital }}</p>
{% with acceptance=req.donations.first %}
{% if acceptance %}
<div class="mb-2 p-2 rounded bg-success bg-opacity-10 border border-success border-opacity-25">
<p class="mb-0 small text-success">
<i class="bi bi-person-check-fill me-1"></i>
Accepted by: <strong>{{ acceptance.donor.name }}</strong>
</p>
</div>
{% endif %}
{% endwith %}
{% 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>
<div class="d-flex gap-2">
{% if req.user %}
<a href="{% url 'chat' req.user.username %}" class="btn btn-sm btn-outline-secondary px-2 rounded-pill" title="Message Requester">
<i class="bi bi-chat-dots-fill"></i>
</a>
{% endif %}
{% if user.is_authenticated and user.donor_profile and req.user != user %}
{% if req.can_volunteer %}
<a href="{% url 'volunteer_for_request' req.id %}" class="btn btn-sm btn-outline-danger px-3 rounded-pill">Volunteer</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary px-3 rounded-pill" disabled title="{{ req.ineligibility_reason }}">
{{ req.ineligibility_reason }}
</button>
{% endif %}
{% endif %}
<a href="tel:{{ req.contact_number }}" class="btn btn-sm btn-danger px-3 rounded-pill">Call</a>
</div>
</div>
</div>
</div>
</div>
{% empty %}
<div class="col-12 text-center py-5">
<i class="bi bi-megaphone fs-1 text-secondary opacity-25"></i>
<p class="text-secondary mt-3">No active blood requests at the moment.</p>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,612 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}Chat with {{ other_user.username }} - RaktaPulse{% endblock %}
{% block head %}
<style>
.chat-container {
height: calc(100vh - 250px);
display: flex;
flex-direction: column;
}
.chat-messages {
flex-grow: 1;
overflow-y: auto;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
.message {
max-width: 75%;
padding: 12px 16px;
border-radius: 16px;
position: relative;
font-size: 0.95rem;
line-height: 1.4;
}
.message-sent {
align-self: flex-end;
background-color: var(--pulse-red);
color: white;
border-bottom-right-radius: 4px;
}
.message-received {
align-self: flex-start;
background-color: white;
color: var(--text-primary);
border-bottom-left-radius: 4px;
border: 1px solid var(--border-color);
}
.message-time {
font-size: 0.7rem;
opacity: 0.7;
margin-top: 4px;
display: block;
}
#videoGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
background: #000;
border-radius: 12px;
overflow: hidden;
position: relative;
}
.video-wrapper {
position: relative;
aspect-ratio: 4/3;
background: #222;
}
video {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-label {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0,0,0,0.5);
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.8rem;
}
.call-controls {
display: flex;
justify-content: center;
gap: 15px;
padding: 15px;
background: rgba(0,0,0,0.8);
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}
</style>
{% endblock %}
{% block content %}
<div class="container py-4">
<div class="glass-card chat-container">
<div class="d-flex align-items-center gap-3 pb-3 mb-3 border-bottom">
<a href="{% url 'inbox' %}" class="btn btn-link text-secondary p-0">
<i class="bi bi-arrow-left fs-4"></i>
</a>
<div class="rounded-circle overflow-hidden border border-danger-subtle" style="width: 45px; height: 45px;">
{% if other_user.profile.profile_pic %}
<img src="{{ other_user.profile.profile_pic.url }}" alt="{{ other_user.username }}" class="w-100 h-100 object-fit-cover">
{% else %}
<div class="bg-danger bg-opacity-10 w-100 h-100 d-flex align-items-center justify-content-center">
<i class="bi bi-person-fill text-danger"></i>
</div>
{% endif %}
</div>
<div>
<h6 class="fw-bold mb-0">
<a href="{% url 'public_profile' other_user.username %}" class="text-decoration-none text-dark">
{{ other_user.first_name }} {{ other_user.last_name|default:other_user.username }}
</a>
</h6>
<small class="text-success d-flex align-items-center gap-1">
<span class="rounded-circle bg-success" style="width: 8px; height: 8px;"></span> Online
</small>
</div>
<div class="ms-auto d-flex gap-2 align-items-center">
<button id="startAudioCall" class="btn btn-outline-secondary rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;" title="Audio Call">
<i class="bi bi-telephone-fill"></i>
</button>
<button id="startVideoCall" class="btn btn-outline-danger rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;" title="Video Call">
<i class="bi bi-camera-video-fill"></i>
</button>
<div class="dropdown">
<button class="btn btn-link text-secondary p-0" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical fs-5"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow border-0">
<li>
<form action="{% url 'delete_messages' other_user.username %}" method="POST" onsubmit="return confirm('Are you sure you want to delete all messages in this conversation?');">
{% csrf_token %}
<button type="submit" class="dropdown-item text-danger d-flex align-items-center gap-2">
<i class="bi bi-trash"></i> Delete Conversation
</button>
</form>
</li>
</ul>
</div>
</div>
</div>
<div class="chat-messages" id="chatMessages">
{% for msg in chat_messages %}
<div class="message {% if msg.sender == user %}message-sent{% else %}message-received{% endif %}">
{% 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>
</div>
{% empty %}
<div class="text-center my-auto text-secondary">
<p class="mb-0">No messages yet. Say hi!</p>
</div>
{% endfor %}
</div>
<form method="post" enctype="multipart/form-data" class="d-flex flex-column gap-2" id="messageForm">
{% csrf_token %}
<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">
<div class="modal-content bg-dark border-0 overflow-hidden rounded-4">
<div class="modal-body p-0">
<div id="videoGrid">
<div class="video-wrapper" id="localVideoWrapper">
<video id="localVideo" autoplay muted playsinline></video>
<div class="video-label">{% trans "You" %}</div>
</div>
<div class="video-wrapper" id="remoteVideoWrapper" style="display: none;">
<video id="remoteVideo" autoplay playsinline></video>
<div class="video-label" id="remoteLabel">{{ other_user.username }}</div>
</div>
</div>
<div class="call-controls">
<div id="callStatus" class="position-absolute top-0 start-50 translate-middle-x mt-3 text-white-50 small">Connecting...</div>
<button id="toggleMic" class="btn btn-outline-light rounded-circle d-flex align-items-center justify-content-center" style="width: 45px; height: 45px;"><i class="bi bi-mic-fill"></i></button>
<button id="toggleVideo" class="btn btn-outline-light rounded-circle d-flex align-items-center justify-content-center" style="width: 45px; height: 45px;"><i class="bi bi-camera-video-fill"></i></button>
<button id="endCall" class="btn btn-danger rounded-pill px-4 fw-bold">{% trans "End Call" %}</button>
</div>
</div>
</div>
</div>
</div>
<!-- Audio Assets -->
<audio id="incomingRing" loop>
<source src="https://assets.mixkit.co/active_storage/sfx/2358/2358-preview.mp3" type="audio/mpeg">
</audio>
<audio id="outgoingRing" loop>
<source src="https://assets.mixkit.co/active_storage/sfx/1359/1359-preview.mp3" type="audio/mpeg">
</audio>
<!-- Incoming Call UI -->
<div id="incomingCallUI" class="position-fixed top-0 start-50 translate-middle-x mt-4 glass-card p-3 shadow-lg border-danger animate__animated animate__fadeInDown" style="display: none; z-index: 9999; min-width: 320px; border: 2px solid var(--pulse-red) !important;">
<div class="d-flex align-items-center gap-3">
<div class="bg-danger bg-opacity-10 p-2 rounded-circle">
<i class="bi bi-telephone-inbound-fill text-danger fs-4"></i>
</div>
<div class="flex-grow-1">
<h6 class="mb-0 fw-bold">{% trans "Incoming Call" %}</h6>
<small class="text-secondary">{{ other_user.username }} {% trans "is calling..." %}</small>
</div>
<div class="d-flex gap-2">
<button id="rejectCall" class="btn btn-light rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;"><i class="bi bi-x-lg"></i></button>
<button id="acceptCall" class="btn btn-danger rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;"><i class="bi bi-check-lg"></i></button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
<script>
// Chat UI logic
const chatMessages = document.getElementById('chatMessages');
chatMessages.scrollTop = chatMessages.scrollHeight;
// Video/Audio Call Logic
const MY_USERNAME = "{{ user.username }}";
const OTHER_USERNAME = "{{ other_user.username }}";
const PEER_ID_PREFIX = "raktapulse_";
// Sanitize username for PeerJS ID (only alphanumeric, -, _)
const sanitizeId = (id) => id.replace(/[^a-zA-Z0-9-_]/g, '_');
// Initialize PeerJS with STUN servers for reliability
let peer = new Peer(PEER_ID_PREFIX + sanitizeId(MY_USERNAME), {
config: {
'iceServers': [
{ url: 'stun:stun.l.google.com:19302' },
{ url: 'stun:stun1.l.google.com:19302' },
{ url: 'stun:stun2.l.google.com:19302' },
{ url: 'stun:stun3.l.google.com:19302' },
{ url: 'stun:stun4.l.google.com:19302' },
]
},
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');
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
const remoteVideoWrapper = document.getElementById('remoteVideoWrapper');
const callStatus = document.getElementById('callStatus');
const incomingRing = document.getElementById('incomingRing');
const outgoingRing = document.getElementById('outgoingRing');
function playRingtone(type) {
if (type === 'incoming') incomingRing.play().catch(e => console.log('Audio blocked'));
if (type === 'outgoing') outgoingRing.play().catch(e => console.log('Audio blocked'));
}
function stopRingtones() {
incomingRing.pause();
incomingRing.currentTime = 0;
outgoingRing.pause();
outgoingRing.currentTime = 0;
}
// Handle Peer Open
peer.on('open', (id) => {
console.log('My peer ID is: ' + id);
});
// Handle incoming calls
peer.on('call', (call) => {
console.log('Incoming call from: ' + call.peer);
if (call.peer === PEER_ID_PREFIX + sanitizeId(OTHER_USERNAME)) {
currentCall = call;
incomingCallUI.style.display = 'block';
playRingtone('incoming');
setTimeout(() => {
if (incomingCallUI.style.display === 'block') {
incomingCallUI.style.display = 'none';
stopRingtones();
}
}, 30000);
}
});
// 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 getLocalStream(videoEnabled);
localVideo.srcObject = localStream;
callModal.show();
callStatus.innerText = "Calling...";
playRingtone('outgoing');
const call = peer.call(PEER_ID_PREFIX + sanitizeId(OTHER_USERNAME), localStream);
handleCall(call);
} catch (err) {
console.error('Failed to get local stream', err);
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);
}
}
function handleCall(call) {
currentCall = call;
call.on('stream', (remoteStream) => {
console.log('Received remote stream');
stopRingtones();
callStatus.innerText = "Connected";
remoteVideo.srcObject = remoteStream;
remoteVideoWrapper.style.display = 'block';
});
call.on('close', () => {
endCall();
});
call.on('error', (err) => {
console.error('Call error:', err);
stopRingtones();
endCall();
});
}
// Button Listeners
document.getElementById('startVideoCall').addEventListener('click', () => startCall(true));
document.getElementById('startAudioCall').addEventListener('click', () => startCall(false));
document.getElementById('acceptCall').addEventListener('click', async () => {
incomingCallUI.style.display = 'none';
stopRingtones();
try {
callStatus.innerText = "Answering...";
// Try to get video if possible, but fallback to audio
localStream = await getLocalStream(true);
localVideo.srcObject = localStream;
callModal.show();
currentCall.answer(localStream);
handleCall(currentCall);
} catch (err) {
console.error('Failed to get local stream', err);
currentCall.close();
alert('Could not access camera or microphone.');
}
});
document.getElementById('rejectCall').addEventListener('click', () => {
incomingCallUI.style.display = 'none';
stopRingtones();
if (currentCall) currentCall.close();
});
document.getElementById('endCall').addEventListener('click', () => {
endCall();
});
function endCall() {
stopRingtones();
if (currentCall) currentCall.close();
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
}
callModal.hide();
remoteVideoWrapper.style.display = 'none';
localVideo.srcObject = null;
remoteVideo.srcObject = null;
}
// Toggle Mic/Video
document.getElementById('toggleMic').addEventListener('click', function() {
if (!localStream) return;
const audioTrack = localStream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = !audioTrack.enabled;
this.innerHTML = audioTrack.enabled ? '<i class="bi bi-mic-fill"></i>' : '<i class="bi bi-mic-mute-fill"></i>';
this.classList.toggle('btn-outline-light');
this.classList.toggle('btn-danger');
}
});
document.getElementById('toggleVideo').addEventListener('click', function() {
if (!localStream) return;
const videoTrack = localStream.getVideoTracks()[0];
if (videoTrack) {
videoTrack.enabled = !videoTrack.enabled;
this.innerHTML = videoTrack.enabled ? '<i class="bi bi-camera-video-fill"></i>' : '<i class="bi bi-camera-video-off-fill"></i>';
this.classList.toggle('btn-outline-light');
this.classList.toggle('btn-danger');
}
});
// 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 %}

View File

@ -0,0 +1,150 @@
{% extends 'base.html' %}
{% load static %}
{% block content %}
<div class="container py-5">
<div class="row mb-4">
<div class="col-md-8">
<h2 class="display-5 fw-bold text-dark mb-2">{{ title }}</h2>
<p class="text-muted">Explore the complete log of blood donations within the RaktaPulse community.</p>
</div>
<div class="col-md-4 text-md-end d-flex align-items-center justify-content-md-end">
<div class="stats-badge p-3 bg-white rounded shadow-sm border-start border-4 border-danger me-3">
<span class="d-block text-muted small text-uppercase fw-bold">Total Impact</span>
<span class="h4 fw-bold mb-0 text-danger">{{ donations.count }} Donations</span>
</div>
<a href="?export=csv{% if current_filters.blood_group %}&blood_group={{ current_filters.blood_group }}{% endif %}{% if current_filters.location %}&location={{ current_filters.location }}{% endif %}" class="btn btn-success rounded-pill shadow-sm">
<i class="fas fa-file-csv me-2"></i> Export
</a>
</div>
</div>
<!-- Filters Section -->
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<form method="GET" class="row g-3">
<div class="col-md-4">
<label class="form-label small fw-bold text-muted">Blood Group</label>
<select name="blood_group" class="form-select rounded-pill border-light">
<option value="">All Groups</option>
{% for group in blood_groups %}
<option value="{{ group }}" {% if current_filters.blood_group == group %}selected{% endif %}>{{ group }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-5">
<label class="form-label small fw-bold text-muted">Location</label>
<div class="input-group">
<span class="input-group-text bg-white border-light border-end-0 rounded-start-pill">
<i class="fas fa-map-marker-alt text-muted"></i>
</span>
<input type="text" name="location" class="form-control border-light border-start-0 rounded-end-pill" placeholder="Search by city or area..." value="{{ current_filters.location|default:'' }}">
</div>
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="submit" class="btn btn-danger w-100 rounded-pill">
<i class="fas fa-filter me-2"></i> Apply Filters
</button>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-header bg-white py-3 border-bottom border-light">
<div class="row align-items-center">
<div class="col">
<h5 class="mb-0 fw-bold">Detailed Logs</h5>
</div>
<div class="col-auto text-muted small">
<i class="fas fa-info-circle me-1"></i> Showing all completed events
</div>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light text-muted small text-uppercase">
<tr>
<th class="ps-4 border-0 py-3">Donor</th>
<th class="border-0 py-3">Blood Group</th>
<th class="border-0 py-3">Recipient/Patient</th>
<th class="border-0 py-3">Location</th>
<th class="border-0 py-3">Date</th>
<th class="pe-4 border-0 py-3 text-end">Status</th>
</tr>
</thead>
<tbody>
{% for donation in donations %}
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<div class="avatar-sm rounded-circle bg-danger bg-opacity-10 text-danger d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
<i class="fas fa-user"></i>
</div>
<div>
<div class="fw-bold">{{ donation.donor_user.username }}</div>
<div class="text-muted small">Verified Champion</div>
</div>
</div>
</td>
<td>
<span class="badge bg-danger rounded-pill">{{ donation.request.blood_group }}</span>
</td>
<td>
<div class="fw-medium text-dark">{{ donation.request.patient_name }}</div>
</td>
<td>
<div class="text-muted">
<i class="fas fa-hospital-alt me-1 small"></i> {{ donation.request.hospital }}
</div>
<div class="small text-muted opacity-75">{{ donation.request.location }}</div>
</td>
<td>
<div class="fw-medium text-dark">{{ donation.date|date:"M d, Y" }}</div>
<div class="small text-muted">{{ donation.date|time:"H:i" }}</div>
</td>
<td class="pe-4 text-end">
<span class="badge bg-success bg-opacity-10 text-success px-3 py-2 rounded-pill">
<i class="fas fa-check-circle me-1"></i> Completed
</span>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-5 text-muted">
<div class="mb-3">
<i class="fas fa-history fa-3x opacity-25"></i>
</div>
<h5>No donation history available yet.</h5>
<p class="mb-0">When donations are completed, they will appear here.</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="mt-4 text-center">
<a href="{% url 'home' %}" class="btn btn-outline-secondary rounded-pill px-4">
<i class="fas fa-arrow-left me-2"></i> Back to Dashboard
</a>
</div>
</div>
<style>
.avatar-sm {
flex-shrink: 0;
}
.table th {
font-weight: 600;
letter-spacing: 0.5px;
}
.table td {
padding-top: 1.25rem;
padding-bottom: 1.25rem;
}
</style>
{% endblock %}

View File

@ -0,0 +1,130 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Donors - RaktaPulse{% endblock %}
{% block content %}
<div class="container-fluid p-0">
<div class="row mb-4">
<div class="col-12">
<h2 class="brand-font mb-1">Blood Donors</h2>
<p class="text-secondary">Find and connect with blood donors in your community.</p>
</div>
</div>
<div class="glass-card mb-4">
<form method="GET" id="donorFilterForm" class="row g-3 mb-4">
<div class="col-md-3">
<input type="text" name="q" class="form-control bg-light border-secondary text-dark"
placeholder="Search name or location..." value="{{ request.GET.q }}">
</div>
<div class="col-md-2">
<select name="blood_group" class="form-select bg-light border-secondary text-dark">
<option value="">All Blood Groups</option>
{% for group in blood_groups %}
<option value="{{ group }}" {% if request.GET.blood_group == group %}selected{% endif %}>{{ group }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<input type="text" name="district" class="form-control bg-light border-secondary text-dark"
placeholder="District..." value="{{ request.GET.district }}">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-danger w-100 shadow-sm">Filter</button>
</div>
<div class="col-md-2">
<button type="button" id="findNearestBtn" class="btn btn-primary w-100 shadow-sm">
<i class="bi bi-geo-alt-fill me-1"></i> Nearest
</button>
</div>
<div class="col-md-1">
<a href="{% url 'donor_list' %}" class="btn btn-outline-secondary w-100 px-0" title="Reset">Reset</a>
</div>
<input type="hidden" name="lat" id="latInput" value="{{ request.GET.lat }}">
<input type="hidden" name="lng" id="lngInput" value="{{ request.GET.lng }}">
</form>
<div class="donor-list">
{% for donor in donors %}
<div class="donor-row d-flex align-items-center justify-content-between flex-wrap gap-3 p-3 mb-3 border rounded bg-light">
<div class="d-flex align-items-center gap-3">
<div class="position-relative">
{% if donor.user and donor.user.profile.profile_pic %}
<img src="{{ donor.user.profile.profile_pic.url }}" alt="{{ donor.name }}" class="rounded-circle" style="width: 50px; height: 50px; object-fit: cover;">
<span class="position-absolute bottom-0 end-0 badge rounded-pill bg-danger border border-white" style="font-size: 0.65rem; padding: 0.25rem 0.45rem;">
{{ donor.blood_group }}
</span>
{% else %}
<div class="bg-danger bg-opacity-10 text-danger rounded p-2 fw-bold" style="width: 45px; height: 45px; display: flex; align-items: center; justify-content: center;">
{{ donor.blood_group }}
</div>
{% endif %}
</div>
<div>
<h6 class="mb-0 fw-bold text-dark">
{{ donor.name }}
{% if donor.is_verified %}
<span class="ms-1" title="Verified Donor"><i class="bi bi-patch-check-fill text-success"></i></span>
{% endif %}
{% if donor.distance and donor.distance < 1000 %}
<span class="ms-2 badge bg-info bg-opacity-10 text-info" style="font-size: 0.7rem;">
<i class="bi bi-distribute-vertical me-1"></i> {{ donor.distance|floatformat:1 }} km away
</span>
{% endif %}
</h6>
<p class="mb-0 text-secondary small"><i class="bi bi-geo-alt me-1"></i> {{ donor.location }}, {{ donor.district }}</p>
</div>
</div>
<div class="d-flex align-items-center gap-4">
<div class="text-end d-none d-sm-block">
<span class="badge {% if donor.on_break %}bg-warning{% elif donor.is_available %}bg-success{% else %}bg-secondary{% endif %} bg-opacity-10 text-{% if donor.on_break %}warning{% elif donor.is_available %}success{% else %}secondary{% endif %} mb-1">
{% if donor.on_break %}On Break ({{ donor.days_remaining }}d){% elif donor.is_available %}Available{% else %}Unavailable{% endif %}
</span>
<p class="mb-0 text-muted extra-small" style="font-size: 0.7rem;">Phone: {{ donor.phone }}</p>
</div>
<div class="d-flex gap-2">
{% if donor.user %}
<a href="{% url 'public_profile' donor.user.username %}" class="btn btn-outline-secondary btn-sm rounded-pill" title="View Profile">
<i class="bi bi-person-circle"></i>
</a>
<a href="{% url 'chat' donor.user.username %}" class="btn btn-outline-danger btn-sm rounded-pill" title="Send Message">
<i class="bi bi-chat-dots-fill"></i>
</a>
{% endif %}
<a href="tel:{{ donor.phone }}" class="btn btn-danger btn-sm px-3 rounded-pill">Call</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.getElementById('findNearestBtn').addEventListener('click', function() {
const btn = this;
const originalContent = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Locating...';
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(function(position) {
document.getElementById('latInput').value = position.coords.latitude;
document.getElementById('lngInput').value = position.coords.longitude;
document.getElementById('donorFilterForm').submit();
}, function(error) {
alert("Error getting location: " + error.message);
btn.disabled = false;
btn.innerHTML = originalContent;
});
} else {
alert("Geolocation is not supported by this browser.");
btn.disabled = false;
btn.innerHTML = originalContent;
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,92 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Hospitals - RaktaPulse{% endblock %}
{% block content %}
<div class="container-fluid p-0">
<div class="row mb-4">
<div class="col-md-8">
<h2 class="brand-font mb-1">Hospitals in Kathmandu</h2>
<p class="text-secondary">A comprehensive list of public and private hospitals for blood requests and emergency care.</p>
</div>
<div class="col-md-4 text-md-end d-flex align-items-center justify-content-md-end">
<button id="nearMeBtn" class="btn btn-glass btn-sm rounded-pill">
<i class="bi bi-geo-fill me-1"></i> Nearest First
</button>
</div>
</div>
<div class="row">
{% for hospital in hospitals %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="glass-card h-100 d-flex flex-column">
<div class="mb-3">
<div class="d-flex justify-content-between align-items-start">
<h4 class="brand-font mb-1 text-dark">{{ hospital.name }}</h4>
{% if hospital.distance %}
<span class="badge bg-danger-soft text-danger rounded-pill small">
{{ hospital.distance|floatformat:1 }} km away
</span>
{% endif %}
</div>
<p class="text-secondary small mb-0"><i class="bi bi-geo-alt me-1"></i> {{ hospital.location }}</p>
</div>
<div class="mt-auto">
{% if hospital.phone %}
<p class="mb-2">
<a href="tel:{{ hospital.phone }}" class="text-decoration-none text-danger fw-bold">
<i class="bi bi-telephone me-2"></i> {{ hospital.phone }}
</a>
</p>
{% endif %}
{% if hospital.website %}
<p class="mb-3">
<a href="{{ hospital.website }}" target="_blank" class="text-decoration-none text-primary small">
<i class="bi bi-globe me-2"></i> Visit Website
</a>
</p>
{% endif %}
<a href="{% url 'request_blood' %}?hospital={{ hospital.name|urlencode }}" class="btn btn-outline-danger btn-sm w-100 rounded-pill"><i class="bi bi-plus-circle me-1"></i> Request Blood Here</a>
</div>
</div>
</div>
{% empty %}
<div class="col-12 text-center py-5">
<i class="bi bi-hospital fs-1 text-secondary opacity-25"></i>
<p class="text-secondary mt-3">No hospitals registered in the system.</p>
</div>
{% endfor %}
</div>
</div>
<script>
document.getElementById('nearMeBtn').addEventListener('click', function() {
if (navigator.geolocation) {
this.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span> Locating...';
navigator.geolocation.getCurrentPosition(function(position) {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
window.location.href = `?lat=${lat}&lng=${lng}`;
}, function(error) {
console.error("Error getting location: ", error);
alert("Could not get your location. Please ensure location services are enabled.");
document.getElementById('nearMeBtn').innerHTML = '<i class="bi bi-geo-fill me-1"></i> Nearest First';
});
} else {
alert("Geolocation is not supported by this browser.");
}
});
// Automatically check if we should show distance
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('lat') && urlParams.has('lng')) {
document.getElementById('nearMeBtn').classList.add('btn-danger');
document.getElementById('nearMeBtn').classList.remove('btn-glass');
document.getElementById('nearMeBtn').innerHTML = '<i class="bi bi-check-circle-fill me-1"></i> Sorted by Proximity';
}
</script>
{% endblock %}

View File

@ -0,0 +1,64 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}Inbox - RaktaPulse{% endblock %}
{% block content %}
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Messages</h2>
</div>
<div class="glass-card p-0 overflow-hidden">
{% if conversations %}
<div class="list-group list-group-flush">
{% for conv in conversations %}
<div class="list-group-item p-0 border-0 border-bottom d-flex align-items-center">
<a href="{% url 'chat' conv.user.username %}" class="list-group-item-action p-4 d-flex align-items-center gap-3 flex-grow-1 text-decoration-none">
<div class="rounded-circle overflow-hidden border border-danger-subtle flex-shrink-0" style="width: 60px; height: 60px;">
{% if conv.user.profile.profile_pic %}
<img src="{{ conv.user.profile.profile_pic.url }}" alt="{{ conv.user.username }}" class="w-100 h-100 object-fit-cover">
{% else %}
<div class="bg-danger bg-opacity-10 w-100 h-100 d-flex align-items-center justify-content-center">
<i class="bi bi-person-fill text-danger fs-4"></i>
</div>
{% endif %}
</div>
<div class="flex-grow-1 overflow-hidden">
<div class="d-flex justify-content-between align-items-center mb-1">
<h6 class="fw-bold mb-0 text-dark">{{ conv.user.first_name }} {{ conv.user.last_name|default:conv.user.username }}</h6>
<small class="text-secondary">{{ conv.last_message.timestamp|date:"M d, g:i a" }}</small>
</div>
<p class="text-secondary mb-0 text-truncate {% if not conv.last_message.is_read and conv.last_message.receiver == user %}fw-bold text-dark{% endif %}">
{% if conv.last_message.sender == user %}You: {% endif %}
{{ conv.last_message.content }}
</p>
</div>
{% if not conv.last_message.is_read and conv.last_message.receiver == user %}
<div class="bg-danger rounded-circle" style="width: 10px; height: 10px;"></div>
{% endif %}
</a>
<div class="pe-4">
<form action="{% url 'delete_messages' conv.user.username %}" method="POST" onsubmit="return confirm('Delete conversation with {{ conv.user.username }}?');">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-light text-secondary border-0 rounded-circle p-2" title="Delete conversation">
<i class="bi bi-trash fs-5"></i>
</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="p-5 text-center">
<div class="bg-light rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
<i class="bi bi-chat-dots text-secondary fs-1"></i>
</div>
<h5 class="text-dark">No messages yet</h5>
<p class="text-secondary">Start a conversation with a donor or requester!</p>
<a href="{% url 'donor_list' %}" class="btn btn-danger px-4 mt-2">Find Donors</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,684 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "RaktaPulse Dashboard" %} - {% trans "Lifeline of the Community" %}{% endblock %}
{% block head %}
<style>
.stat-card {
background: #ffffff;
border-radius: 20px;
padding: 24px;
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0,0,0,0.03);
transition: all 0.3s ease;
text-decoration: none;
display: block;
color: inherit;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(230, 57, 70, 0.1);
border-color: var(--pulse-red);
cursor: pointer;
}
.stat-card .icon-box {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin-bottom: 15px;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
font-family: 'Outfit', sans-serif;
}
.stat-label {
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 500;
}
.blood-group-pill {
width: 40px;
height: 40px;
border-radius: 10px;
background: rgba(230, 57, 70, 0.1);
color: var(--pulse-red);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
}
.donor-row {
background: #f8f9fa;
border-radius: 12px;
padding: 15px;
margin-bottom: 12px;
border: 1px solid #eee;
transition: all 0.3s;
}
.donor-row:hover {
border-color: var(--pulse-red);
background: var(--pulse-red-light);
}
.urgency-badge {
padding: 4px 10px;
border-radius: 6px;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
display: inline-flex;
align-items: center;
gap: 5px;
}
.bg-critical { background: #FF4D4D; color: #fff; box-shadow: 0 0 10px rgba(255, 77, 77, 0.4); }
.bg-urgent { background: #FFA500; color: #000; }
.bg-normal { background: #4CAF50; color: #fff; }
.request-item {
border-left: 4px solid transparent;
transition: all 0.3s ease;
padding-left: 12px;
}
.request-item.border-critical { border-left-color: #FF4D4D; background: rgba(255, 77, 77, 0.03); }
.request-item.border-urgent { border-left-color: #FFA500; background: rgba(255, 165, 0, 0.03); }
.request-item.border-normal { border-left-color: #4CAF50; background: rgba(76, 175, 80, 0.03); }
@keyframes pulse-red {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.8; }
100% { transform: scale(1); opacity: 1; }
}
.pulse-icon {
animation: pulse-red 2s infinite ease-in-out;
}
.progress {
background-color: #2A2A2A;
border-radius: 10px;
}
.vaccination-card {
background: linear-gradient(135deg, #E63946 0%, #d62828 100%);
border-radius: 20px;
padding: 25px;
color: white !important;
}
.donor-list-container {
max-height: 550px;
overflow-y: auto;
padding-right: 10px;
}
.donor-list-container::-webkit-scrollbar {
width: 6px;
}
.donor-list-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.donor-list-container::-webkit-scrollbar-thumb {
background: var(--pulse-red);
border-radius: 10px;
}
.filter-btn {
background: #ffffff;
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 8px 16px;
border-radius: 10px;
font-size: 0.85rem;
}
.filter-btn.active {
background: var(--pulse-red);
border-color: var(--pulse-red);
}
/* Scrollable Request Feed */
.request-feed {
max-height: 500px;
overflow-y: auto;
padding-right: 10px;
scrollbar-width: thin;
scrollbar-color: rgba(230, 57, 70, 0.3) transparent;
}
.request-feed::-webkit-scrollbar {
width: 5px;
}
.request-feed::-webkit-scrollbar-track {
background: transparent;
}
.request-feed::-webkit-scrollbar-thumb {
background: rgba(230, 57, 70, 0.2);
border-radius: 10px;
}
.request-feed::-webkit-scrollbar-thumb:hover {
background: var(--pulse-red);
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid p-0">
<!-- Welcome Header -->
<div class="row mb-4 align-items-center">
<div class="col-md-7">
<div class="d-flex align-items-center gap-3 mb-1">
<h2 class="brand-font mb-0">{% trans "RaktaPulse Community Dashboard" %}</h2>
<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25 rounded-pill px-3 py-2 d-flex align-items-center gap-2" style="font-size: 0.7rem;">
<span class="d-inline-block rounded-circle bg-success" style="width: 8px; height: 8px;"></span>
SYSTEM ONLINE
</span>
</div>
<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 {% if badge.name == 'First-Time Donor' %}d-md-none{% endif %}" 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 %}
<!-- Active involvements / Message section -->
<div class="row mb-4">
<div class="col-12">
<div class="glass-card border-start border-primary border-4 bg-primary bg-opacity-10">
<h5 class="brand-font mb-3"><i class="bi bi-chat-dots-fill me-2"></i>{% trans "Action Required: Donations in Progress" %}</h5>
<div class="row g-3">
{% for event in involved_events %}
<div class="col-md-6 col-lg-4">
<div class="p-3 border rounded bg-white shadow-sm">
<p class="mb-2"><strong>{{ event.donor.name }}</strong> is helping <strong>{{ event.request.patient_name }}</strong></p>
<div class="d-flex justify-content-between">
<span class="small text-muted">{{ event.date|date:"M d, Y" }}</span>
<a href="{% url 'complete_donation' event.id %}" class="btn btn-sm btn-success px-3 rounded-pill">{% trans "Mark Completed" %}</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
<!-- Quick Stats & Impact Counter -->
<div class="row g-4 mb-5">
<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 "Donors" %}</div>
</a>
</div>
<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 "Requests" %}</div>
</a>
</div>
<div class="col-xl-2 col-md-4 col-6">
<a href="{% url 'donation_history' %}" 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>
</a>
</div>
<div class="col-xl-3 col-md-6 col-6">
<a href="{% url 'lives_saved' %}" 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>
</a>
</div>
<div class="col-xl-3 col-md-6 col-12">
<a href="#blood-bank-inventory" class="stat-card text-decoration-none">
<div class="icon-box bg-primary bg-opacity-10 text-primary">
<i class="bi bi-droplet"></i>
</div>
<div class="stat-value text-dark">{{ stats.total_stock }} <small class="fs-6 fw-normal text-muted">/ {{ stats.total_capacity }}</small></div>
<div class="stat-label text-secondary">{% trans "Stock Level" %}</div>
</a>
</div>
</div>
<div class="row g-4">
<!-- Main Donor Search & Grid -->
<div class="col-lg-8">
<div class="glass-card mb-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex gap-2">
<button class="filter-btn active" id="btn-list">List View</button>
<button class="filter-btn" id="btn-map">Map View</button>
<button class="filter-btn" id="btn-nearest">Nearest First</button>
</div>
</div>
<div id="map-container" style="display: none; height: 400px; border-radius: 12px; margin-bottom: 20px; overflow: hidden; border: 1px solid var(--border-color);">
<div id="map" style="height: 100%;"></div>
</div>
<form method="GET" id="donorHomeFilterForm" class="row g-3 mb-4">
<div class="col-md-5">
<select name="blood_group" class="form-select bg-light border-secondary text-dark">
<option value="">All Blood Groups</option>
{% for group in blood_groups %}
<option value="{{ group }}" {% if request.GET.blood_group == group %}selected{% endif %}>{{ group }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-5">
<input type="text" name="location" class="form-control bg-light border-secondary text-dark"
placeholder="Search location (e.g. Baluwatar)..." value="{{ request.GET.location }}">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-danger w-100 shadow-sm">Find</button>
</div>
<input type="hidden" name="lat" id="latInputHome" value="{{ request.GET.lat }}">
<input type="hidden" name="lng" id="lngInputHome" value="{{ request.GET.lng }}">
</form>
<div class="donor-list-container">
<div class="donor-list">
{% for donor in donors %}
<div class="donor-row d-flex align-items-center justify-content-between flex-wrap gap-3">
<div class="d-flex align-items-center gap-3">
<div class="position-relative">
{% if donor.user and donor.user.profile.profile_pic %}
<img src="{{ donor.user.profile.profile_pic.url }}" alt="{{ donor.name }}" class="rounded-circle" style="width: 45px; height: 45px; object-fit: cover;">
<span class="position-absolute bottom-0 end-0 badge rounded-pill bg-danger border border-white" style="font-size: 0.6rem; padding: 0.2rem 0.4rem;">
{{ donor.blood_group }}
</span>
{% else %}
<div class="blood-group-pill">{{ donor.blood_group }}</div>
{% endif %}
</div>
<div>
<h6 class="mb-0 fw-bold text-dark">
{{ donor.name }}
{% if donor.is_verified %}
<span class="ms-1" title="Verified Donor"><i class="bi bi-patch-check-fill text-success"></i></span>
{% endif %}
{% if donor.distance and donor.distance < 1000 %}
<span class="ms-2 badge bg-info bg-opacity-10 text-info" style="font-size: 0.65rem;">
{{ donor.distance|floatformat:1 }} km
</span>
{% endif %}
</h6>
<p class="mb-0 text-secondary small"><i class="bi bi-geo-alt me-1"></i> {{ donor.location }}, {{ donor.district }}</p>
</div>
</div>
<div class="d-flex align-items-center gap-4">
<div class="text-end d-none d-sm-block">
<span class="badge {% if donor.is_available %}bg-success{% else %}bg-secondary{% endif %} bg-opacity-10 text-{% if donor.is_available %}success{% else %}secondary{% endif %} mb-1">
{% if donor.is_available %}Available{% else %}Unavailable{% endif %}
</span>
<p class="mb-0 text-muted extra-small" style="font-size: 0.7rem;">Last Donated: {{ donor.last_donation_date|default:"Never" }}</p>
</div>
<div class="d-flex gap-2">
{% if donor.user %}
<a href="{% url 'chat' donor.user.username %}" class="btn btn-outline-danger btn-sm rounded-pill" title="Message">
<i class="bi bi-chat-dots-fill"></i>
</a>
{% endif %}
<a href="tel:{{ donor.phone }}" class="btn btn-danger btn-sm px-3 rounded-pill">Call</a>
</div>
</div>
</div>
{% empty %}
<div class="text-center py-5">
<i class="bi bi-search fs-1 text-secondary opacity-25"></i>
<p class="text-secondary mt-3">No donors match your search criteria.</p>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Vaccination Monitoring Section -->
<div class="vaccination-card mb-4">
<div class="row align-items-center">
<div class="col-md-7">
<h4 class="brand-font text-white mb-2">Vaccination & Eligibility</h4>
<p class="text-secondary small">We prioritize donors who are fully vaccinated to ensure maximum safety for recipients.</p>
<div class="mb-4">
<div class="d-flex justify-content-between small text-white mb-2">
<span>Community Immunity Level</span>
<span>{{ stats.vaccinated_percentage }}%</span>
</div>
<div class="progress" style="height: 10px;">
<div class="progress-bar bg-success" style="width: {{ stats.vaccinated_percentage }}%"></div>
</div>
</div>
<a href="{% url 'vaccination_dashboard' %}" class="btn btn-success btn-sm px-4">Update Status</a>
</div>
<div class="col-md-5 d-none d-md-block text-center">
<i class="bi bi-shield-plus text-success" style="font-size: 5rem; opacity: 0.2;"></i>
</div>
</div>
</div>
<!-- Myths vs Facts Section -->
<div class="glass-card bg-light bg-opacity-10 border-danger mb-4">
<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-3">
{% for item in myths_vs_facts %}
<div class="col-md-6 col-lg-4">
<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>
<!-- Sidebar Components -->
<div class="col-lg-4">
<!-- Urgency Insights (Pie Chart) -->
<div class="glass-card mb-4 border-bottom border-danger border-3 shadow-sm">
<h6 class="brand-font mb-3 text-center text-secondary uppercase small fw-bold">Urgency Distribution</h6>
<div style="height: 180px; width: 100%; position: relative;">
<canvas id="urgencyChart"></canvas>
</div>
</div>
<!-- Urgent Requests -->
<div class="glass-card mb-4 border-start border-danger border-4">
<h5 class="brand-font mb-4 d-flex justify-content-between align-items-center">
Urgent Requests
<span class="badge bg-danger rounded-pill px-2" style="font-size: 0.6rem;">HOT</span>
</h5>
<!-- Emergency SMS Concept Button -->
{% if user.is_staff %}
<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>
{% endif %}
<div class="request-feed">
{% for req in blood_requests %}
<div class="mb-4 border-bottom border-light pb-3 request-item border-{{ req.urgency|lower }}">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="urgency-badge bg-{{ req.urgency|lower }}">
{% if req.urgency == 'CRITICAL' %}
<i class="bi bi-exclamation-triangle-fill pulse-icon"></i>
{% elif req.urgency == 'URGENT' %}
<i class="bi bi-exclamation-circle-fill"></i>
{% else %}
<i class="bi bi-info-circle-fill"></i>
{% endif %}
{{ req.urgency }}
</span>
<span class="fw-bold text-danger">{{ req.blood_group }}</span>
</div>
<h6 class="mb-1 text-dark">{{ req.patient_name }}</h6>
<p class="text-secondary extra-small mb-2"><i class="bi bi-hospital me-1"></i> {{ req.hospital }}</p>
{% with acceptance=req.donations.first %}
{% if acceptance %}
<div class="mb-2 p-1 px-2 rounded bg-success bg-opacity-10">
<p class="mb-0 extra-small text-success">
<i class="bi bi-person-check-fill me-1"></i>
Accepted by: <strong>{{ acceptance.donor.name }}</strong>
</p>
</div>
{% endif %}
{% endwith %}
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted" style="font-size: 0.7rem;">{{ req.created_at|timesince }} ago</small>
<div class="d-flex gap-2 align-items-center">
{% if req.user %}
<a href="{% url 'chat' req.user.username %}" class="text-secondary" title="Message">
<i class="bi bi-chat-dots-fill"></i>
</a>
{% endif %}
<a href="tel:{{ req.contact_number }}" class="btn btn-link text-danger p-0 text-decoration-none small">Help Now →</a>
</div>
</div>
</div>
{% empty %}
<p class="text-secondary text-center">No active requests found.</p>
{% endfor %}
</div>
</div>
<!-- Blood Bank Inventory -->
<div class="glass-card" id="blood-bank-inventory">
<h5 class="brand-font mb-4">Blood Bank Inventory</h5>
<div class="inventory-list">
{% for bank in blood_banks %}
<div class="mb-3">
<div class="d-flex justify-content-between mb-1">
<span class="small fw-bold text-dark">{{ bank.name }}</span>
<span class="small text-secondary">24/7 Available</span>
</div>
<div class="d-flex gap-1 flex-wrap">
<span class="badge bg-dark border {% if bank.stock_a_plus < 5 %}border-danger text-danger{% else %}border-secondary text-secondary{% endif %} extra-small" style="font-size: 0.65rem;">A+: {{ bank.stock_a_plus }}</span>
<span class="badge bg-dark border {% if bank.stock_a_minus < 5 %}border-danger text-danger{% else %}border-secondary text-secondary{% endif %} extra-small" style="font-size: 0.65rem;">A-: {{ bank.stock_a_minus }}</span>
<span class="badge bg-dark border {% if bank.stock_b_plus < 5 %}border-danger text-danger{% else %}border-secondary text-secondary{% endif %} extra-small" style="font-size: 0.65rem;">B+: {{ bank.stock_b_plus }}</span>
<span class="badge bg-dark border {% if bank.stock_b_minus < 5 %}border-danger text-danger{% else %}border-secondary text-secondary{% endif %} extra-small" style="font-size: 0.65rem;">B-: {{ bank.stock_b_minus }}</span>
<span class="badge bg-dark border {% if bank.stock_o_plus < 5 %}border-danger text-danger{% else %}border-secondary text-secondary{% endif %} extra-small" style="font-size: 0.65rem;">O+: {{ bank.stock_o_plus }}</span>
<span class="badge bg-dark border {% if bank.stock_o_minus < 5 %}border-danger text-danger{% else %}border-secondary text-secondary{% endif %} extra-small" style="font-size: 0.65rem;">O-: {{ bank.stock_o_minus }}</span>
<span class="badge bg-dark border {% if bank.stock_ab_plus < 5 %}border-danger text-danger{% else %}border-secondary text-secondary{% endif %} extra-small" style="font-size: 0.65rem;">AB+: {{ bank.stock_ab_plus }}</span>
<span class="badge bg-dark border {% if bank.stock_ab_minus < 5 %}border-danger text-danger{% else %}border-secondary text-secondary{% endif %} extra-small" style="font-size: 0.65rem;">AB-: {{ bank.stock_ab_minus }}</span>
</div>
</div>
{% empty %}
<p class="text-secondary text-center">No blood banks registered.</p>
{% endfor %}
</div>
{% if user.is_staff %}
<a href="/admin/core/bloodbank/" class="btn btn-outline-secondary w-100 btn-sm mt-3">Manage Banks</a>
{% endif %}
</div>
<!-- Extra Feature: Community Tip -->
<div class="glass-card mt-4 bg-gradient" style="background: linear-gradient(135deg, #1e1e1e 0%, #2d1b1b 100%);">
<h6 class="text-danger mb-2"><i class="bi bi-lightbulb me-2"></i>Lifesaver Tip</h6>
<p class="small text-secondary mb-0">Donating once can save up to three lives. Make sure to stay hydrated and rest before your appointment!</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Urgency Chart Implementation
document.addEventListener('DOMContentLoaded', function() {
const urgencyData = JSON.parse('{{ urgency_counts|safe }}');
const ctx = document.getElementById('urgencyChart').getContext('2d');
new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Critical', 'Urgent', 'Normal'],
datasets: [{
data: [urgencyData.CRITICAL, urgencyData.URGENT, urgencyData.NORMAL],
backgroundColor: ['#FF4D4D', '#FFA500', '#4CAF50'],
borderWidth: 0,
hoverOffset: 10
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
padding: 15,
font: {
size: 10,
family: "'Inter', sans-serif"
}
}
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.label || '';
if (label) {
label += ': ';
}
if (context.parsed !== null) {
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = Math.round((context.parsed / total) * 100);
label += context.parsed + ' (' + percentage + '%)';
}
return label;
}
}
}
},
cutout: '70%'
}
});
});
const btnList = document.getElementById('btn-list');
const btnMap = document.getElementById('btn-map');
const mapContainer = document.getElementById('map-container');
const donorList = document.querySelector('.donor-list');
let map;
btnMap.addEventListener('click', () => {
btnMap.classList.add('active');
btnList.classList.remove('active');
mapContainer.style.display = 'block';
donorList.style.display = 'none';
if (!map) {
initMap();
} else {
// Need to invalidate size if it was hidden when initialized
setTimeout(() => { map.invalidateSize(); }, 200);
}
});
btnList.addEventListener('click', () => {
btnList.classList.add('active');
btnMap.classList.remove('active');
mapContainer.style.display = 'none';
donorList.style.display = 'block';
});
const btnNearest = document.getElementById('btn-nearest');
btnNearest.addEventListener('click', () => {
const btn = btnNearest;
const originalContent = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(function(position) {
document.getElementById('latInputHome').value = position.coords.latitude;
document.getElementById('lngInputHome').value = position.coords.longitude;
document.getElementById('donorHomeFilterForm').submit();
}, function(error) {
alert("Error getting location: " + error.message);
btn.disabled = false;
btn.innerHTML = originalContent;
});
} else {
alert("Geolocation is not supported by this browser.");
btn.disabled = false;
btn.innerHTML = originalContent;
}
});
function initMap() {
map = L.map('map').setView([27.7226, 85.3312], 14); // Centered on Baluwatar, Kathmandu
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// Add dummy markers for donors
{% for donor in donors %}
// Randomly offset from center for demo
L.marker([27.7226 + (Math.random() - 0.5) * 0.05, 85.3312 + (Math.random() - 0.5) * 0.05])
.addTo(map)
.bindPopup("<b>{{ donor.name }}</b><br>{{ donor.blood_group }} - {{ donor.location }}");
{% endfor %}
}
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 %}

View File

@ -0,0 +1,157 @@
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-map text-danger"></i> Live Alert Map</h2>
<span class="badge bg-danger pulse">LIVE UPDATES</span>
</div>
<div class="card shadow-sm mb-4">
<div class="card-body p-0">
<div id="live-map" style="height: 600px; width: 100%; border-radius: 8px;"></div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0">Recent Alerts</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Patient</th>
<th>Blood Group</th>
<th>Hospital</th>
<th>Urgency</th>
<th>Status / Accepted By</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for req in requests %}
<tr>
<td>{{ req.patient_name }}</td>
<td><span class="badge bg-danger">{{ req.blood_group }}</span></td>
<td>{{ req.hospital }}</td>
<td>
{% if req.urgency == 'CRITICAL' %}
<span class="badge bg-danger d-inline-flex align-items-center gap-1 pulse">
<i class="bi bi-exclamation-triangle-fill"></i> CRITICAL
</span>
{% elif req.urgency == 'URGENT' %}
<span class="badge bg-warning text-dark d-inline-flex align-items-center gap-1">
<i class="bi bi-exclamation-circle-fill"></i> URGENT
</span>
{% else %}
<span class="badge bg-info d-inline-flex align-items-center gap-1">
<i class="bi bi-info-circle-fill"></i> NORMAL
</span>
{% endif %}
</td>
<td>
{% with acceptance=req.donations.first %}
{% if acceptance %}
<span class="badge bg-success bg-opacity-10 text-success">
<i class="bi bi-person-check-fill"></i> {{ acceptance.donor.name }}
</span>
{% else %}
<span class="badge bg-light text-secondary">Awaiting Help</span>
{% endif %}
{% endwith %}
</td>
<td>
<button class="btn btn-sm btn-outline-danger" onclick="focusOnMarker({{ req.latitude }}, {{ req.longitude }})">
<i class="bi bi-eye"></i> View on Map
</button>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center">No active alerts found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.pulse {
animation: pulse-red 2s infinite;
}
@keyframes pulse-red {
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7); }
70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(220, 53, 69, 0); }
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); }
}
</style>
<script>
var map;
var markers = {};
function initMap() {
// Center on Baluwatar, Kathmandu
map = L.map('live-map').setView([27.7226, 85.3312], 15);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
{% for req in requests %}
{% if req.latitude and req.longitude %}
var markerColor = 'blue';
{% if req.urgency == 'CRITICAL' %}
markerColor = 'red';
{% elif req.urgency == 'URGENT' %}
markerColor = 'orange';
{% endif %}
var marker = L.circleMarker([{{ req.latitude }}, {{ req.longitude }}], {
radius: 10,
fillColor: markerColor,
color: "#fff",
weight: 1,
opacity: 1,
fillOpacity: 0.8
}).addTo(map);
marker.bindPopup(`
<strong>{{ req.patient_name }}</strong><br>
Blood Group: <span class="badge bg-danger">{{ req.blood_group }}</span><br>
Hospital: {{ req.hospital }}<br>
Urgency: {{ req.urgency }}<br>
{% with acceptance=req.donations.first %}
{% if acceptance %}
<div class="mt-1 text-success small"><i class="bi bi-person-check-fill"></i> Accepted by: {{ acceptance.donor.name }}</div>
{% endif %}
{% endwith %}
<a href="tel:{{ req.contact_number }}" class="btn btn-sm btn-primary mt-2">Contact</a>
`);
markers["{{ req.id }}"] = marker;
{% endif %}
{% endfor %}
}
function focusOnMarker(lat, lng) {
map.setView([lat, lng], 16);
// Find marker at this location and open its popup if possible
// (Simplified for now)
window.scrollTo({ top: 0, behavior: 'smooth' });
}
document.addEventListener('DOMContentLoaded', initMap);
</script>
{% endblock %}

View File

@ -0,0 +1,91 @@
{% extends 'base.html' %}
{% load static %}
{% block content %}
<div class="container py-5">
<div class="row mb-5 text-center">
<div class="col-lg-8 mx-auto">
<h1 class="display-4 fw-bold text-danger mb-3">{{ title }}</h1>
<p class="lead text-muted">Every single donation has the potential to save up to three lives. Together, we are building a safer community.</p>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-6">
<div class="card border-0 shadow-sm rounded-4 h-100 bg-danger text-white">
<div class="card-body p-5 text-center">
<div class="mb-3">
<i class="fas fa-heartbeat fa-4x opacity-75"></i>
</div>
<h3 class="display-3 fw-bold mb-0">{{ total_impact }}</h3>
<p class="text-uppercase fw-bold opacity-75">Total Lives Saved</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-0 shadow-sm rounded-4 h-100">
<div class="card-body p-5 text-center">
<div class="mb-3">
<i class="fas fa-tint fa-4x text-danger opacity-75"></i>
</div>
<h3 class="display-3 fw-bold mb-0 text-dark">{{ total_donations }}</h3>
<p class="text-uppercase fw-bold text-muted">Successful Donations</p>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4 mb-5">
<div class="card-header bg-white py-4 border-bottom border-light text-center">
<h4 class="mb-0 fw-bold">Recent Life-Saving Events</h4>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light text-muted small text-uppercase">
<tr>
<th class="ps-4 border-0 py-3">Champion</th>
<th class="border-0 py-3">Impact</th>
<th class="border-0 py-3">Location</th>
<th class="pe-4 border-0 py-3 text-end">Date</th>
</tr>
</thead>
<tbody>
{% for donation in donations %}
<tr>
<td class="ps-4">
<div class="fw-bold">{{ donation.donor_user.username }}</div>
<div class="text-muted small">Verified Donor</div>
</td>
<td>
<span class="badge bg-success rounded-pill px-3">
<i class="fas fa-plus me-1"></i> 3 Lives Saved
</span>
</td>
<td>
<div class="text-muted">{{ donation.request.location }}</div>
</td>
<td class="pe-4 text-end">
<div class="fw-medium">{{ donation.date|date:"M d, Y" }}</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center py-5">
<p class="text-muted mb-0">No recent events to display.</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="text-center">
<a href="{% url 'donation_history' %}" class="btn btn-danger rounded-pill px-5 py-3 shadow-sm">
<i class="fas fa-history me-2"></i> View Full Donation History
</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,100 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}Login - RaktaPulse{% endblock %}
{% block content %}
<div class="row justify-content-center align-items-center py-5" style="min-height: 80vh;">
<div class="col-md-5">
<div class="glass-card shadow-lg border-0 p-4 p-md-5" style="border-top: 5px solid #E63946 !important;">
<div class="text-center mb-5">
<div class="d-inline-flex align-items-center justify-content-center mb-4" style="width: 60px; height: 60px; background: #fff5f5; border-radius: 12px;">
<i class="bi bi-shield-lock text-danger fs-2"></i>
</div>
<h2 class="fw-bold text-dark brand-font mb-2">{% trans "System Login" %}</h2>
<p class="text-secondary small">{% trans "Secure access for registered members" %}</p>
</div>
{% if form.non_field_errors %}
<div class="alert alert-danger border-0 rounded-3 mb-4 small">
{% for error in form.non_field_errors %}
<div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
{{ error }}
</div>
{% endfor %}
</div>
{% endif %}
<form method="post" class="needs-validation">
{% csrf_token %}
{% for field in form %}
<div class="mb-4">
<label class="form-label small fw-bold text-secondary text-uppercase mb-2" for="{{ field.id_for_label }}">
<i class="bi bi-{% if 'username' in field.name %}person{% else %}key{% endif %} me-1"></i> {{ field.label }}
</label>
{{ field }}
{% if field.errors %}
<div class="text-danger small mt-1">
{% for error in field.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="rememberMe">
<label class="form-check-label small text-secondary" for="rememberMe">Remember me</label>
</div>
<a href="#" class="text-danger small text-decoration-none fw-500">Forgot password?</a>
</div>
<button type="submit" class="btn btn-danger w-100 py-3 fw-bold mt-2 shadow-sm" style="background-color: #E63946; border: none; border-radius: 8px;">
{% trans "LOG IN" %} <i class="bi bi-arrow-right-short ms-1"></i>
</button>
</form>
<div class="text-center mt-5">
<p class="text-secondary small mb-4">Don't have an account? <a href="{% url 'register' %}" class="text-danger fw-bold text-decoration-none">Create one now</a></p>
<hr class="opacity-10 mb-4">
<a href="{% url 'home' %}" class="btn btn-link btn-sm text-secondary text-decoration-none">
<i class="bi bi-house-door me-1"></i> Back to Home
</a>
</div>
</div>
</div>
</div>
<style>
input:not([type="checkbox"]) {
display: block;
width: 100%;
padding: 0.85rem 1.25rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #2b2d42;
background-color: #ffffff;
background-clip: padding-box;
border: 1px solid #e9ecef;
border-radius: 12px;
transition: all 0.2s ease-in-out;
}
input:not([type="checkbox"]):focus {
border-color: #E63946;
outline: 0;
box-shadow: 0 0 0 4px rgba(230, 57, 70, 0.08);
background-color: #fff;
}
.glass-card {
background: #ffffff;
border-radius: 24px;
border: 1px solid rgba(0,0,0,0.05);
}
.fw-500 { font-weight: 500; }
.shadow-danger {
box-shadow: 0 10px 20px rgba(230, 57, 70, 0.2);
}
</style>
{% endblock %}

View File

@ -0,0 +1,35 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="glass-card p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">{% trans "Notifications" %}</h2>
<span class="badge bg-primary">{{ notifications.count }}</span>
</div>
{% if notifications %}
<div class="list-group list-group-flush">
{% for note in notifications %}
<div class="list-group-item bg-transparent border-0 mb-3 p-3 glass-card {% if not note.is_read %}border-start border-primary border-4{% endif %}">
<div class="d-flex justify-content-between">
<p class="mb-1">{{ note.message }}</p>
<small class="text-muted">{{ note.created_at|timesince }} {% trans "ago" %}</small>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-bell-slash fa-3x text-muted mb-3"></i>
<p>{% trans "No notifications yet." %}</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,195 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}User Profile - RaktaPulse{% endblock %}
{% block content %}
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="glass-card">
<div class="d-flex align-items-center mb-4">
{% if user.profile.profile_pic %}
<img src="{{ user.profile.profile_pic.url }}" alt="{{ user.username }}" class="rounded-circle me-3 object-fit-cover" style="width: 64px; height: 64px;">
{% else %}
<div class="bg-danger rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 60px; height: 60px;">
<i class="bi bi-person-fill text-white fs-2"></i>
</div>
{% endif %}
<div>
<h2 class="mb-0 fw-bold">{{ user.username }}</h2>
<p class="text-secondary mb-0">Member since {{ user.date_joined|date:"M d, Y" }}</p>
</div>
</div>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<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 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 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>
{{ u_form.first_name }}
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">Last Name</label>
{{ u_form.last_name }}
</div>
</div>
<h5 class="fw-bold mb-3 border-bottom pb-2">Profile Details</h5>
<div class="row g-3 mb-4">
<div class="col-12">
<label class="form-label small fw-bold">Bio</label>
{{ p_form.bio }}
<div class="form-text">Write a short bio about yourself.</div>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Blood Group</label>
{{ p_form.blood_group }}
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Phone</label>
{{ 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-md-6">
<label class="form-label small fw-bold">Profile Picture</label>
{{ p_form.profile_pic }}
</div>
</div>
<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>
<h5 class="fw-bold mb-3 border-bottom pb-2">
<i class="bi bi-clock-history me-2 text-danger"></i>Transaction & Operation History
</h5>
<div class="mb-5">
<h6 class="fw-bold text-muted small mb-3">Donor Record (People You've Helped)</h6>
<div class="stats-mini bg-light p-3 rounded mb-3">
<div class="row text-center">
<div class="col-6 border-end">
<div class="h4 mb-0 fw-bold text-danger">{{ donations_made.count }}</div>
<div class="small text-muted">Total Volunteers</div>
</div>
<div class="col-6">
<div class="h4 mb-0 fw-bold text-success">{{ completed_donations_count }}</div>
<div class="small text-muted">Successful Donations</div>
</div>
</div>
</div>
{% if donations_made %}
<div class="list-group list-group-flush border rounded overflow-hidden">
{% for donation in donations_made %}
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="fw-bold">Helped {{ donation.request.patient_name }}</span>
<div class="small text-muted">{{ donation.request.hospital }} | {{ donation.date|date:"M d, Y" }}</div>
</div>
<div>
{% if donation.is_completed %}
<span class="badge bg-success">Completed</span>
{% else %}
<span class="badge bg-warning text-dark">Pending</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center p-4 bg-light rounded border border-dashed">
<p class="text-muted small mb-0">You haven't volunteered for any requests yet.</p>
</div>
{% endif %}
</div>
<div class="mb-5">
<h6 class="fw-bold text-muted small mb-3">Request History (Who Helped You)</h6>
{% if requests_made %}
<div class="list-group list-group-flush border rounded overflow-hidden">
{% for br in requests_made %}
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div>
<span class="fw-bold">{{ br.blood_group }} for {{ br.patient_name }}</span>
<div class="small text-muted">{{ br.created_at|date:"M d, Y" }} | Status: <strong>{{ br.status }}</strong></div>
<div class="mt-2">
<p class="small fw-bold mb-1">Volunteers:</p>
{% with br.donations.all as donations %}
{% if donations %}
<div class="d-flex flex-wrap gap-2">
{% for d in donations %}
<a href="{% url 'public_profile' d.donor_user.username %}" class="btn btn-sm btn-outline-danger py-0 px-2 rounded-pill small">
<i class="bi bi-person-heart me-1"></i>{{ d.donor_user.username }}
</a>
{% endfor %}
</div>
{% else %}
<span class="text-muted small italic">No volunteers yet</span>
{% endif %}
{% endwith %}
</div>
</div>
<span class="badge {% if br.status == 'Active' %}bg-danger{% elif br.status == 'Accepted' %}bg-info{% else %}bg-secondary{% endif %}">
{{ br.status }}
</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center p-4 bg-light rounded border border-dashed">
<p class="text-muted small mb-0">You haven't made any blood requests yet.</p>
</div>
{% endif %}
</div>
<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.');" class="mb-2">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger btn-sm w-100 mb-2">
Clear Personal Information
</button>
</form>
<form action="{% url 'delete_account' %}" method="POST" onsubmit="return confirm('WARNING: Are you sure you want to delete your account? This will permanently remove all your data, including messages and donation history. This action cannot be undone.');">
{% csrf_token %}
<button type="submit" class="btn btn-danger btn-sm w-100">
<i class="bi bi-trash-fill me-1"></i>Delete My Account Permanently
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,113 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{{ profile_user.username }}'s Profile - RaktaPulse{% endblock %}
{% block content %}
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="glass-card">
<div class="text-center mb-4">
<div class="rounded-circle overflow-hidden border border-danger-subtle d-inline-block p-1 mb-3" style="width: 150px; height: 150px;">
{% if profile.profile_pic %}
<img src="{{ profile.profile_pic.url }}" alt="{{ profile_user.username }}" class="w-100 h-100 object-fit-cover rounded-circle">
{% else %}
<div class="bg-danger bg-opacity-10 w-100 h-100 d-flex align-items-center justify-content-center rounded-circle">
<i class="bi bi-person-fill text-danger" style="font-size: 5rem;"></i>
</div>
{% endif %}
</div>
<h2 class="fw-bold mb-0">{{ profile_user.first_name }} {{ profile_user.last_name }}</h2>
<p class="text-secondary mb-3">@{{ profile_user.username }}</p>
{% if donor %}
<div class="d-flex justify-content-center gap-2 mb-4">
<span class="badge bg-danger fs-5 px-3">{{ donor.blood_group }}</span>
{% if donor.is_available %}
<span class="badge bg-success d-flex align-items-center"><i class="bi bi-check-circle-fill me-1"></i> Available</span>
{% else %}
<span class="badge bg-secondary d-flex align-items-center"><i class="bi bi-clock-fill me-1"></i> Not Available</span>
{% endif %}
</div>
{% endif %}
</div>
<div class="row g-4 mb-4">
<div class="col-sm-6">
<div class="p-3 bg-light rounded-3 h-100">
<h6 class="text-secondary fw-bold small text-uppercase mb-2">Location</h6>
<p class="mb-0"><i class="bi bi-geo-alt-fill text-danger me-2"></i>{{ profile.location|default:"Not specified" }}</p>
</div>
</div>
<div class="col-sm-6">
<div class="p-3 bg-light rounded-3 h-100">
<h6 class="text-secondary fw-bold small text-uppercase mb-2">Member Since</h6>
<p class="mb-0"><i class="bi bi-calendar-event-fill text-danger me-2"></i>{{ profile_user.date_joined|date:"F Y" }}</p>
</div>
</div>
</div>
<div class="mb-4">
<h5 class="fw-bold mb-3">About</h5>
<div class="p-3 border rounded-3 bg-light-subtle">
{{ profile.bio|default:"No bio provided."|linebreaks }}
</div>
</div>
<div class="mb-4">
<h5 class="fw-bold mb-3">Impact & Reliability</h5>
<div class="row g-3 text-center">
<div class="col-6">
<div class="p-3 border rounded-3 bg-light">
<div class="h2 fw-bold text-danger mb-0">{{ donations_made.count }}</div>
<div class="small text-muted text-uppercase fw-bold">Volunteered</div>
</div>
</div>
<div class="col-6">
<div class="p-3 border rounded-3 bg-light">
<div class="h2 fw-bold text-success mb-0">{{ completed_donations_count }}</div>
<div class="small text-muted text-uppercase fw-bold">Successful</div>
</div>
</div>
</div>
</div>
{% if donations_made %}
<div class="mb-4">
<h5 class="fw-bold mb-3">Recent Donations</h5>
<div class="list-group list-group-flush border rounded-3 overflow-hidden">
{% for donation in donations_made|slice:":5" %}
<div class="list-group-item bg-light-subtle">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="fw-bold">Donated to {{ donation.request.patient_name }}</div>
<div class="small text-muted">{{ donation.request.hospital }} | {{ donation.date|date:"M d, Y" }}</div>
</div>
{% if donation.is_completed %}
<span class="badge bg-success-subtle text-success border border-success-subtle">Verified</span>
{% else %}
<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle">Pending</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="d-grid gap-2">
{% if user.is_authenticated and user != profile_user %}
<a href="{% url 'chat' profile_user.username %}" class="btn btn-danger py-2">
<i class="bi bi-chat-dots-fill me-2"></i> Send Message
</a>
{% endif %}
<a href="{% url 'donor_list' %}" class="btn btn-outline-secondary py-2">
<i class="bi bi-arrow-left me-2"></i> Back to Donors
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,90 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}Create Account - RaktaPulse{% endblock %}
{% block content %}
<div class="row justify-content-center align-items-center py-5" style="min-height: 80vh;">
<div class="col-md-6 col-lg-5">
<div class="glass-card shadow-lg border-0 p-4 p-md-5" style="border-top: 5px solid #E63946 !important;">
<div class="text-center mb-5">
<div class="d-inline-flex align-items-center justify-content-center mb-4" style="width: 60px; height: 60px; background: #fff5f5; border-radius: 12px;">
<i class="bi bi-person-plus text-danger fs-2"></i>
</div>
<h2 class="fw-bold text-dark brand-font mb-2">{% trans "Create Account" %}</h2>
<p class="text-secondary small">{% trans "Join the next-gen management system" %}</p>
</div>
<form method="post" class="needs-validation">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label class="form-label small fw-bold text-secondary text-uppercase mb-2" for="{{ field.id_for_label }}">
{{ field.label }}
</label>
{{ field }}
{% if field.help_text %}
<div class="form-text small mb-1">{{ field.help_text|safe }}</div>
{% endif %}
{% if field.errors %}
<div class="text-danger small mt-1">
{% for error in field.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
<button type="submit" class="btn btn-danger w-100 py-3 fw-bold mt-4 shadow-sm" style="background-color: #E63946; border: none; border-radius: 8px;">
{% trans "CREATE ACCOUNT" %} <i class="bi bi-check-circle ms-1"></i>
</button>
</form>
<div class="text-center mt-5">
<p class="text-secondary small mb-4">Already have an account? <a href="{% url 'login' %}" class="text-danger fw-bold text-decoration-none">Log in here</a></p>
<hr class="opacity-10 mb-4">
<a href="{% url 'home' %}" class="btn btn-link btn-sm text-secondary text-decoration-none">
<i class="bi bi-house-door me-1"></i> Back to Home
</a>
</div>
</div>
</div>
</div>
<style>
input:not([type="checkbox"]) {
display: block;
width: 100%;
padding: 0.85rem 1.25rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #2b2d42;
background-color: #ffffff;
background-clip: padding-box;
border: 1px solid #e9ecef;
border-radius: 12px;
transition: all 0.2s ease-in-out;
}
input:not([type="checkbox"]):focus {
border-color: #E63946;
outline: 0;
box-shadow: 0 0 0 4px rgba(230, 57, 70, 0.08);
background-color: #fff;
}
.glass-card {
background: #ffffff;
border-radius: 24px;
border: 1px solid rgba(0,0,0,0.05);
}
.shadow-danger {
box-shadow: 0 10px 20px rgba(230, 57, 70, 0.2);
}
.helptext ul {
list-style: none;
padding-left: 0;
margin-top: 5px;
color: #6c757d;
font-size: 0.8rem;
}
</style>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="glass-card p-4">
<h2 class="mb-4">{% trans "Become a Blood Donor" %}</h2>
<p class="text-muted">{% trans "Join our community of lifesavers. Your donation can save lives." %}</p>
<form method="POST">
{% csrf_token %}
<div class="mb-3">
<label for="blood_group" class="form-label">{% trans "Blood Group" %}</label>
<select name="blood_group" id="blood_group" class="form-select" required>
<option value="">{% trans "Select Blood Group" %}</option>
{% for group in blood_groups %}
<option value="{{ group }}">{{ group }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="phone" class="form-label">{% trans "Phone Number" %}</label>
<input type="text" name="phone" id="phone" class="form-control" required>
</div>
<div class="mb-3">
<label for="location" class="form-label">{% trans "Location" %}</label>
<input type="text" name="location" id="location" class="form-control" placeholder="e.g. Baluwatar, Kathmandu">
</div>
<button type="submit" class="btn btn-danger w-100">{% trans "Register as Donor" %}</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More