Initial import
This commit is contained in:
commit
21ef5153ee
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
*/node_modules/
|
||||
*/build/
|
||||
0
.perm_test_apache
Normal file
0
.perm_test_apache
Normal file
0
.perm_test_exec
Normal file
0
.perm_test_exec
Normal file
38
README.md
Normal file
38
README.md
Normal 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
3
ai/__init__.py
Normal 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
420
ai/local_ai_api.py
Normal 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
0
config/__init__.py
Normal file
BIN
config/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
config/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
config/__pycache__/settings.cpython-311.pyc
Normal file
BIN
config/__pycache__/settings.cpython-311.pyc
Normal file
Binary file not shown.
BIN
config/__pycache__/urls.cpython-311.pyc
Normal file
BIN
config/__pycache__/urls.cpython-311.pyc
Normal file
Binary file not shown.
BIN
config/__pycache__/wsgi.cpython-311.pyc
Normal file
BIN
config/__pycache__/wsgi.cpython-311.pyc
Normal file
Binary file not shown.
16
config/asgi.py
Normal file
16
config/asgi.py
Normal 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
201
config/settings.py
Normal 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
35
config/urls.py
Normal 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
16
config/wsgi.py
Normal 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
0
core/__init__.py
Normal file
BIN
core/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/admin.cpython-311.pyc
Normal file
BIN
core/__pycache__/admin.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/apps.cpython-311.pyc
Normal file
BIN
core/__pycache__/apps.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/context_processors.cpython-311.pyc
Normal file
BIN
core/__pycache__/context_processors.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/models.cpython-311.pyc
Normal file
BIN
core/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/urls.cpython-311.pyc
Normal file
BIN
core/__pycache__/urls.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/views.cpython-311.pyc
Normal file
BIN
core/__pycache__/views.cpython-311.pyc
Normal file
Binary file not shown.
90
core/admin.py
Normal file
90
core/admin.py
Normal 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
6
core/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'core'
|
||||
21
core/context_processors.py
Normal file
21
core/context_processors.py
Normal 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
41
core/forms.py
Normal 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'}),
|
||||
}
|
||||
0
core/management/__init__.py
Normal file
0
core/management/__init__.py
Normal file
BIN
core/management/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/management/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
0
core/management/commands/__init__.py
Normal file
0
core/management/commands/__init__.py
Normal file
BIN
core/management/commands/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/management/commands/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
61
core/management/commands/cleanup_requests.py
Normal file
61
core/management/commands/cleanup_requests.py
Normal 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}'))
|
||||
47
core/management/commands/seed_hospitals.py
Normal file
47
core/management/commands/seed_hospitals.py
Normal 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.'))
|
||||
61
core/migrations/0001_initial.py
Normal file
61
core/migrations/0001_initial.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
30
core/migrations/0004_vaccinerecord.py
Normal file
30
core/migrations/0004_vaccinerecord.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0007_bloodbank_total_capacity.py
Normal file
18
core/migrations/0007_bloodbank_total_capacity.py
Normal 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),
|
||||
),
|
||||
]
|
||||
28
core/migrations/0008_userprofile.py
Normal file
28
core/migrations/0008_userprofile.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
16
core/migrations/0010_delete_feedback.py
Normal file
16
core/migrations/0010_delete_feedback.py
Normal 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',
|
||||
),
|
||||
]
|
||||
23
core/migrations/0011_hospital.py
Normal file
23
core/migrations/0011_hospital.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
23
core/migrations/0012_hospital_latitude_hospital_longitude.py
Normal file
23
core/migrations/0012_hospital_latitude_hospital_longitude.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0013_userprofile_profile_pic.py
Normal file
18
core/migrations/0013_userprofile_profile_pic.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
27
core/migrations/0014_message.py
Normal file
27
core/migrations/0014_message.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-18 13:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0014_message'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='last_location_update',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='latitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='longitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
]
|
||||
27
core/migrations/0016_badge_userprofile_badges.py
Normal file
27
core/migrations/0016_badge_userprofile_badges.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-18 14:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0015_userprofile_last_location_update_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Badge',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('description', models.CharField(max_length=255)),
|
||||
('icon_class', models.CharField(default='fas fa-medal', max_length=50)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='badges',
|
||||
field=models.ManyToManyField(blank=True, related_name='users', to='core.badge'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-18 15:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0016_badge_userprofile_badges'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='message',
|
||||
name='attachment',
|
||||
field=models.FileField(blank=True, null=True, upload_to='chat_attachments/'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='message',
|
||||
name='message_type',
|
||||
field=models.CharField(choices=[('text', 'Text'), ('image', 'Image'), ('video', 'Video'), ('file', 'File'), ('sticker', 'Sticker')], default='text', max_length=10),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='message',
|
||||
name='sticker_id',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='message',
|
||||
name='content',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0018_bloodrequest_image.py
Normal file
18
core/migrations/0018_bloodrequest_image.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-19 04:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0017_message_attachment_message_message_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bloodrequest',
|
||||
name='image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='blood_requests/'),
|
||||
),
|
||||
]
|
||||
32
core/migrations/0019_healthreport.py
Normal file
32
core/migrations/0019_healthreport.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-19 04:45
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0018_bloodrequest_image'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='HealthReport',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('hospital_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('report_file', models.FileField(upload_to='health_reports/')),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('report_date', models.DateField(default=django.utils.timezone.now)),
|
||||
('next_test_date', models.DateField(blank=True, null=True)),
|
||||
('allow_notifications', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='health_reports', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
core/migrations/0020_vaccinerecord_photo.py
Normal file
18
core/migrations/0020_vaccinerecord_photo.py
Normal 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/'),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0021_bloodrequest_accepted_at.py
Normal file
18
core/migrations/0021_bloodrequest_accepted_at.py
Normal 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),
|
||||
),
|
||||
]
|
||||
0
core/migrations/__init__.py
Normal file
0
core/migrations/__init__.py
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/migrations/__pycache__/0004_vaccinerecord.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0004_vaccinerecord.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/migrations/__pycache__/0008_userprofile.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0008_userprofile.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
core/migrations/__pycache__/0010_delete_feedback.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0010_delete_feedback.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/migrations/__pycache__/0011_hospital.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0011_hospital.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/migrations/__pycache__/0014_message.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0014_message.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/migrations/__pycache__/0019_healthreport.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0019_healthreport.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
217
core/models.py
Normal file
217
core/models.py
Normal 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
575
core/templates/base.html
Normal 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>
|
||||
79
core/templates/core/add_vaccination.html
Normal file
79
core/templates/core/add_vaccination.html
Normal 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 %}
|
||||
14
core/templates/core/article_detail.html
Normal file
14
core/templates/core/article_detail.html
Normal 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 %}
|
||||
165
core/templates/core/blood_bank_list.html
Normal file
165
core/templates/core/blood_bank_list.html
Normal 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 %}
|
||||
131
core/templates/core/blood_request_list.html
Normal file
131
core/templates/core/blood_request_list.html
Normal 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 %}
|
||||
612
core/templates/core/chat.html
Normal file
612
core/templates/core/chat.html
Normal 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 %}
|
||||
150
core/templates/core/donation_history.html
Normal file
150
core/templates/core/donation_history.html
Normal 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 %}
|
||||
130
core/templates/core/donor_list.html
Normal file
130
core/templates/core/donor_list.html
Normal 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 %}
|
||||
92
core/templates/core/hospital_list.html
Normal file
92
core/templates/core/hospital_list.html
Normal 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 %}
|
||||
64
core/templates/core/inbox.html
Normal file
64
core/templates/core/inbox.html
Normal 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 %}
|
||||
684
core/templates/core/index.html
Normal file
684
core/templates/core/index.html
Normal 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 %}
|
||||
157
core/templates/core/live_map.html
Normal file
157
core/templates/core/live_map.html
Normal 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 %}
|
||||
91
core/templates/core/lives_saved.html
Normal file
91
core/templates/core/lives_saved.html
Normal 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 %}
|
||||
100
core/templates/core/login.html
Normal file
100
core/templates/core/login.html
Normal 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 %}
|
||||
35
core/templates/core/notifications.html
Normal file
35
core/templates/core/notifications.html
Normal 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 %}
|
||||
195
core/templates/core/profile.html
Normal file
195
core/templates/core/profile.html
Normal 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 %}
|
||||
113
core/templates/core/public_profile.html
Normal file
113
core/templates/core/public_profile.html
Normal 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 %}
|
||||
90
core/templates/core/register.html
Normal file
90
core/templates/core/register.html
Normal 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 %}
|
||||
37
core/templates/core/register_donor.html
Normal file
37
core/templates/core/register_donor.html
Normal 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
Loading…
x
Reference in New Issue
Block a user