Emmanuel Frimpong Asante
commited on
Commit
·
8e2341a
1
Parent(s):
ae868d8
update space
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .idea/Generative_AI_with_poultry_disease_detection_system_v2.iml +1 -0
- .idea/misc.xml +1 -1
- .idea/workspace.xml +63 -5
- app.py +0 -89
- models/auth.h5 +0 -3
- models/diseases.h5 +0 -3
- models/schemas/group_schema.py +0 -40
- models/schemas/health_record_schema.py +0 -51
- models/schemas/inquiry_schema.py +0 -41
- models/schemas/inventory_schema.py +0 -58
- models/schemas/log_schema.py +0 -28
- models/schemas/todo_schema.py +0 -76
- models/schemas/user_schema.py +0 -19
- requirements.txt +10 -8
- routes/administration.py +0 -417
- routes/authentication.py +0 -126
- routes/chatbot.py +0 -97
- routes/disease_detection.py +0 -93
- scripts/populate_health_records.py +0 -57
- scripts/populate_inventory.py +0 -52
- services/auth_service.py +0 -171
- services/disease_detection_service.py +0 -289
- services/email_notification_service.py +0 -76
- services/health_alerts_service.py +0 -42
- services/health_monitoring_service.py +0 -91
- services/image_preprocessing.py +0 -43
- services/utils.py +0 -204
- static/css/adminlte.css +0 -0
- static/css/adminlte.css.map +0 -0
- static/css/adminlte.min.css +0 -0
- static/css/adminlte.min.css.map +0 -0
- static/css/adminlte.rtl.css +0 -0
- static/css/adminlte.rtl.css.map +0 -0
- static/css/adminlte.rtl.min.css +0 -0
- static/css/adminlte.rtl.min.css.map +0 -0
- static/images/landing-bg.jpg +0 -0
- static/images/logo.png +0 -0
- static/js/adminlte.js +0 -715
- static/js/adminlte.js.map +0 -1
- static/js/adminlte.min.js +0 -7
- static/js/adminlte.min.js.map +0 -1
- templates/admin/dashboard.html +0 -43
- templates/admin/group/add.html +0 -30
- templates/admin/group/add_member.html +0 -30
- templates/admin/group/delete.html +0 -25
- templates/admin/group/list.html +0 -45
- templates/admin/group/share_task.html +0 -30
- templates/admin/group/view.html +0 -52
- templates/admin/inventory/add.html +0 -45
- templates/admin/inventory/delete.html +0 -31
.idea/Generative_AI_with_poultry_disease_detection_system_v2.iml
CHANGED
@@ -4,6 +4,7 @@
|
|
4 |
<content url="file://$MODULE_DIR$">
|
5 |
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
6 |
</content>
|
|
|
7 |
<orderEntry type="sourceFolder" forTests="false" />
|
8 |
<orderEntry type="library" name="jquery-3.6.0" level="application" />
|
9 |
</component>
|
|
|
4 |
<content url="file://$MODULE_DIR$">
|
5 |
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
6 |
</content>
|
7 |
+
<orderEntry type="jdk" jdkName="poultry" jdkType="Python SDK" />
|
8 |
<orderEntry type="sourceFolder" forTests="false" />
|
9 |
<orderEntry type="library" name="jquery-3.6.0" level="application" />
|
10 |
</component>
|
.idea/misc.xml
CHANGED
@@ -3,5 +3,5 @@
|
|
3 |
<component name="Black">
|
4 |
<option name="sdkName" value="Python 3.12 (Generative_AI_with_poultry)" />
|
5 |
</component>
|
6 |
-
<component name="ProjectRootManager" version="2" project-jdk-name="
|
7 |
</project>
|
|
|
3 |
<component name="Black">
|
4 |
<option name="sdkName" value="Python 3.12 (Generative_AI_with_poultry)" />
|
5 |
</component>
|
6 |
+
<component name="ProjectRootManager" version="2" project-jdk-name="poultry" project-jdk-type="Python SDK" />
|
7 |
</project>
|
.idea/workspace.xml
CHANGED
@@ -5,10 +5,69 @@
|
|
5 |
</component>
|
6 |
<component name="ChangeListManager">
|
7 |
<list default="true" id="27c9ae1a-a6fa-4472-8bcd-a7087620894b" name="Changes" comment="update space">
|
8 |
-
<change beforePath="$PROJECT_DIR$/app.py" beforeDir="false"
|
9 |
-
<change beforePath="$PROJECT_DIR$/
|
10 |
-
<change beforePath="$PROJECT_DIR$/
|
11 |
-
<change beforePath="$PROJECT_DIR$/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
</list>
|
13 |
<option name="SHOW_DIALOG" value="false" />
|
14 |
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
@@ -86,7 +145,6 @@
|
|
86 |
<option name="INTERPRETER_OPTIONS" value="" />
|
87 |
<option name="PARENT_ENVS" value="true" />
|
88 |
<option name="SDK_HOME" value="$PROJECT_DIR$/.venv/Scripts/python.exe" />
|
89 |
-
<option name="SDK_NAME" value="Python 3.12 (Generative_AI_with_poultry_disease_detection_system_v2)" />
|
90 |
<option name="WORKING_DIRECTORY" value="" />
|
91 |
<option name="IS_MODULE_SDK" value="false" />
|
92 |
<option name="ADD_CONTENT_ROOTS" value="true" />
|
|
|
5 |
</component>
|
6 |
<component name="ChangeListManager">
|
7 |
<list default="true" id="27c9ae1a-a6fa-4472-8bcd-a7087620894b" name="Changes" comment="update space">
|
8 |
+
<change beforePath="$PROJECT_DIR$/app.py" beforeDir="false" />
|
9 |
+
<change beforePath="$PROJECT_DIR$/models/auth.h5" beforeDir="false" />
|
10 |
+
<change beforePath="$PROJECT_DIR$/models/diseases.h5" beforeDir="false" />
|
11 |
+
<change beforePath="$PROJECT_DIR$/models/schemas/group_schema.py" beforeDir="false" />
|
12 |
+
<change beforePath="$PROJECT_DIR$/models/schemas/health_record_schema.py" beforeDir="false" />
|
13 |
+
<change beforePath="$PROJECT_DIR$/models/schemas/inquiry_schema.py" beforeDir="false" />
|
14 |
+
<change beforePath="$PROJECT_DIR$/models/schemas/inventory_schema.py" beforeDir="false" />
|
15 |
+
<change beforePath="$PROJECT_DIR$/models/schemas/log_schema.py" beforeDir="false" />
|
16 |
+
<change beforePath="$PROJECT_DIR$/models/schemas/todo_schema.py" beforeDir="false" />
|
17 |
+
<change beforePath="$PROJECT_DIR$/models/schemas/user_schema.py" beforeDir="false" />
|
18 |
+
<change beforePath="$PROJECT_DIR$/routes/administration.py" beforeDir="false" />
|
19 |
+
<change beforePath="$PROJECT_DIR$/routes/authentication.py" beforeDir="false" />
|
20 |
+
<change beforePath="$PROJECT_DIR$/routes/chatbot.py" beforeDir="false" />
|
21 |
+
<change beforePath="$PROJECT_DIR$/routes/disease_detection.py" beforeDir="false" />
|
22 |
+
<change beforePath="$PROJECT_DIR$/scripts/populate_health_records.py" beforeDir="false" />
|
23 |
+
<change beforePath="$PROJECT_DIR$/scripts/populate_inventory.py" beforeDir="false" />
|
24 |
+
<change beforePath="$PROJECT_DIR$/services/auth_service.py" beforeDir="false" />
|
25 |
+
<change beforePath="$PROJECT_DIR$/services/disease_detection_service.py" beforeDir="false" />
|
26 |
+
<change beforePath="$PROJECT_DIR$/services/email_notification_service.py" beforeDir="false" />
|
27 |
+
<change beforePath="$PROJECT_DIR$/services/health_alerts_service.py" beforeDir="false" />
|
28 |
+
<change beforePath="$PROJECT_DIR$/services/health_monitoring_service.py" beforeDir="false" />
|
29 |
+
<change beforePath="$PROJECT_DIR$/services/image_preprocessing.py" beforeDir="false" />
|
30 |
+
<change beforePath="$PROJECT_DIR$/services/utils.py" beforeDir="false" />
|
31 |
+
<change beforePath="$PROJECT_DIR$/static/css/adminlte.css" beforeDir="false" />
|
32 |
+
<change beforePath="$PROJECT_DIR$/static/css/adminlte.css.map" beforeDir="false" />
|
33 |
+
<change beforePath="$PROJECT_DIR$/static/css/adminlte.min.css" beforeDir="false" />
|
34 |
+
<change beforePath="$PROJECT_DIR$/static/css/adminlte.min.css.map" beforeDir="false" />
|
35 |
+
<change beforePath="$PROJECT_DIR$/static/css/adminlte.rtl.css" beforeDir="false" />
|
36 |
+
<change beforePath="$PROJECT_DIR$/static/css/adminlte.rtl.css.map" beforeDir="false" />
|
37 |
+
<change beforePath="$PROJECT_DIR$/static/css/adminlte.rtl.min.css" beforeDir="false" />
|
38 |
+
<change beforePath="$PROJECT_DIR$/static/css/adminlte.rtl.min.css.map" beforeDir="false" />
|
39 |
+
<change beforePath="$PROJECT_DIR$/static/images/landing-bg.jpg" beforeDir="false" />
|
40 |
+
<change beforePath="$PROJECT_DIR$/static/images/logo.png" beforeDir="false" />
|
41 |
+
<change beforePath="$PROJECT_DIR$/static/js/adminlte.js" beforeDir="false" />
|
42 |
+
<change beforePath="$PROJECT_DIR$/static/js/adminlte.js.map" beforeDir="false" />
|
43 |
+
<change beforePath="$PROJECT_DIR$/static/js/adminlte.min.js" beforeDir="false" />
|
44 |
+
<change beforePath="$PROJECT_DIR$/static/js/adminlte.min.js.map" beforeDir="false" />
|
45 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/dashboard.html" beforeDir="false" />
|
46 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/group/add.html" beforeDir="false" />
|
47 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/group/add_member.html" beforeDir="false" />
|
48 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/group/delete.html" beforeDir="false" />
|
49 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/group/list.html" beforeDir="false" />
|
50 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/group/share_task.html" beforeDir="false" />
|
51 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/group/view.html" beforeDir="false" />
|
52 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/inventory/add.html" beforeDir="false" />
|
53 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/inventory/delete.html" beforeDir="false" />
|
54 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/inventory/edit.html" beforeDir="false" />
|
55 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/inventory/list.html" beforeDir="false" />
|
56 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/tasks/add.html" beforeDir="false" />
|
57 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/tasks/delete.html" beforeDir="false" />
|
58 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/tasks/edit.html" beforeDir="false" />
|
59 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/tasks/list.html" beforeDir="false" />
|
60 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/todo/add.html" beforeDir="false" />
|
61 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/todo/delete.html" beforeDir="false" />
|
62 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/todo/edit.html" beforeDir="false" />
|
63 |
+
<change beforePath="$PROJECT_DIR$/templates/admin/todo/list.html" beforeDir="false" />
|
64 |
+
<change beforePath="$PROJECT_DIR$/templates/auth/login.html" beforeDir="false" />
|
65 |
+
<change beforePath="$PROJECT_DIR$/templates/auth/register.html" beforeDir="false" />
|
66 |
+
<change beforePath="$PROJECT_DIR$/templates/chatbot.html" beforeDir="false" />
|
67 |
+
<change beforePath="$PROJECT_DIR$/templates/favicon.ico" beforeDir="false" />
|
68 |
+
<change beforePath="$PROJECT_DIR$/templates/index.html" beforeDir="false" />
|
69 |
+
<change beforePath="$PROJECT_DIR$/templates/main/auth-base.html" beforeDir="false" />
|
70 |
+
<change beforePath="$PROJECT_DIR$/templates/main/base.html" beforeDir="false" />
|
71 |
</list>
|
72 |
<option name="SHOW_DIALOG" value="false" />
|
73 |
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
|
145 |
<option name="INTERPRETER_OPTIONS" value="" />
|
146 |
<option name="PARENT_ENVS" value="true" />
|
147 |
<option name="SDK_HOME" value="$PROJECT_DIR$/.venv/Scripts/python.exe" />
|
|
|
148 |
<option name="WORKING_DIRECTORY" value="" />
|
149 |
<option name="IS_MODULE_SDK" value="false" />
|
150 |
<option name="ADD_CONTENT_ROOTS" value="true" />
|
app.py
DELETED
@@ -1,89 +0,0 @@
|
|
1 |
-
# app.py
|
2 |
-
from sys import prefix
|
3 |
-
from typing import Optional
|
4 |
-
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, Depends
|
5 |
-
from fastapi.templating import Jinja2Templates
|
6 |
-
from fastapi.responses import HTMLResponse, RedirectResponse
|
7 |
-
from fastapi.staticfiles import StaticFiles
|
8 |
-
# import tensorflow as tf
|
9 |
-
import os
|
10 |
-
import logging
|
11 |
-
import time
|
12 |
-
from routes.authentication import auth_router
|
13 |
-
from routes.chatbot import chatbot_router
|
14 |
-
from routes.disease_detection import disease_router
|
15 |
-
from routes.administration import dashboard_router
|
16 |
-
from services.health_monitoring_service import evaluate_health_data, send_alerts
|
17 |
-
from huggingface_hub import login
|
18 |
-
|
19 |
-
# Setup logging
|
20 |
-
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
21 |
-
logger = logging.getLogger(__name__)
|
22 |
-
|
23 |
-
# # Check GPU availability for TensorFlow
|
24 |
-
# gpu_devices = tf.config.list_physical_devices('GPU')
|
25 |
-
# logger.info(f"{'GPUs available' if gpu_devices else 'Using CPU'} for TensorFlow")
|
26 |
-
|
27 |
-
# Initialize FastAPI app and templates
|
28 |
-
app = FastAPI()
|
29 |
-
templates = Jinja2Templates(directory="templates")
|
30 |
-
|
31 |
-
# Mount static files if directory exists
|
32 |
-
static_dir = "static"
|
33 |
-
if os.path.isdir(static_dir):
|
34 |
-
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
35 |
-
logger.info("Mounted static directory at /static")
|
36 |
-
else:
|
37 |
-
logger.error("Static directory not found.")
|
38 |
-
raise HTTPException(status_code=500, detail="Static directory not found.")
|
39 |
-
|
40 |
-
# Load Hugging Face Token
|
41 |
-
HF_TOKEN = os.environ.get("HF_Token")
|
42 |
-
if HF_TOKEN:
|
43 |
-
login(token=HF_TOKEN, add_to_git_credential=True)
|
44 |
-
else:
|
45 |
-
logger.warning("Hugging Face token not found in environment variables.")
|
46 |
-
|
47 |
-
# Include routers for different modules
|
48 |
-
app.include_router(auth_router, prefix="/auth", tags=["Authentication"])
|
49 |
-
app.include_router(disease_router, prefix="/disease", tags=["Disease Detection"])
|
50 |
-
app.include_router(dashboard_router, prefix="/admin", tags=["Admin Dashboard"])
|
51 |
-
app.include_router(chatbot_router, prefix="/chatbot", tags=["Chat Bot"])
|
52 |
-
@app.get("/", response_class=HTMLResponse)
|
53 |
-
async def landing_page(request: Request): # current_user: Optional[str] = Depends(optional_get_current_user)):
|
54 |
-
"""
|
55 |
-
Render the landing page if not logged in, otherwise redirect to the appropriate dashboard based on user role.
|
56 |
-
"""
|
57 |
-
# if current_user:
|
58 |
-
# user_data = db.get_collection("users").find_one({"username": current_user})
|
59 |
-
# if user_data:
|
60 |
-
# user_role = user_data.get("role", "farmer")
|
61 |
-
# redirect_url = "/admin/"
|
62 |
-
# logger.info(f"Redirecting {current_user} to {redirect_url}")
|
63 |
-
# return RedirectResponse(url=redirect_url)
|
64 |
-
return templates.TemplateResponse("index.html", {"request": request})
|
65 |
-
|
66 |
-
# Health metrics for periodic monitoring
|
67 |
-
health_metrics = {
|
68 |
-
"weight_loss_percentage": 6,
|
69 |
-
"mortality_rate": 1,
|
70 |
-
"reduced_feed_intake_percentage": 12
|
71 |
-
}
|
72 |
-
|
73 |
-
|
74 |
-
def monitor_health():
|
75 |
-
"""Evaluate health data and log notifications if thresholds are crossed."""
|
76 |
-
while True:
|
77 |
-
notifications = evaluate_health_data(health_metrics)
|
78 |
-
if notifications["notifications"]:
|
79 |
-
logger.info(f"Health Notifications: {notifications['notifications']}")
|
80 |
-
send_alerts(notifications["notifications"], os.getenv("FARMER_EMAIL"))
|
81 |
-
time.sleep(3600) # Run every hour
|
82 |
-
|
83 |
-
|
84 |
-
@app.on_event("startup")
|
85 |
-
async def startup_event():
|
86 |
-
"""Initialize background health monitoring on startup."""
|
87 |
-
logger.info("Starting background health monitoring.")
|
88 |
-
BackgroundTasks().add_task(monitor_health)
|
89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
models/auth.h5
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
version https://git-lfs.github.com/spec/v1
|
2 |
-
oid sha256:55ca5e07d8f4d45eab021364be749ced18402f85f5edb7425486ed76ea5c3093
|
3 |
-
size 234256896
|
|
|
|
|
|
|
|
models/diseases.h5
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
version https://git-lfs.github.com/spec/v1
|
2 |
-
oid sha256:e6fbc0b00b8e4d86b50707fcb39a39f99eecccdb6f4732ea53cdfee793a052c4
|
3 |
-
size 234256896
|
|
|
|
|
|
|
|
models/schemas/group_schema.py
DELETED
@@ -1,40 +0,0 @@
|
|
1 |
-
# models/schemas/group_schema.py
|
2 |
-
|
3 |
-
from pydantic import BaseModel, Field
|
4 |
-
from bson import ObjectId
|
5 |
-
from typing import List, Optional
|
6 |
-
from datetime import datetime
|
7 |
-
|
8 |
-
class PyObjectId(ObjectId):
|
9 |
-
"""Custom ObjectId for Pydantic validation."""
|
10 |
-
@classmethod
|
11 |
-
def __get_validators__(cls):
|
12 |
-
yield cls.validate
|
13 |
-
|
14 |
-
@classmethod
|
15 |
-
def validate(cls, v):
|
16 |
-
if not ObjectId.is_valid(v):
|
17 |
-
raise ValueError("Invalid ObjectId")
|
18 |
-
return ObjectId(v)
|
19 |
-
|
20 |
-
class Group(BaseModel):
|
21 |
-
id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
|
22 |
-
name: str
|
23 |
-
description: Optional[str] = None
|
24 |
-
created_by: str # Admin who created the group
|
25 |
-
members: List[str] = [] # List of user IDs for farmers in the group
|
26 |
-
tasks: List[PyObjectId] = [] # List of task IDs shared with the group
|
27 |
-
created_at: datetime = Field(default_factory=datetime.utcnow)
|
28 |
-
|
29 |
-
class Config:
|
30 |
-
json_encoders = {ObjectId: str}
|
31 |
-
schema_extra = {
|
32 |
-
"example": {
|
33 |
-
"name": "Morning Farm Crew",
|
34 |
-
"description": "Group for morning shift farmers.",
|
35 |
-
"created_by": "admin_id_123",
|
36 |
-
"members": ["farmer_id_1", "farmer_id_2"],
|
37 |
-
"tasks": ["task_id_1", "task_id_2"],
|
38 |
-
"created_at": "2024-10-31T10:00:00Z"
|
39 |
-
}
|
40 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
models/schemas/health_record_schema.py
DELETED
@@ -1,51 +0,0 @@
|
|
1 |
-
# models/schemas/health_record_schema.py
|
2 |
-
# check model
|
3 |
-
from pydantic import BaseModel, Field, validator
|
4 |
-
from datetime import datetime
|
5 |
-
from bson import ObjectId
|
6 |
-
from typing import Optional
|
7 |
-
|
8 |
-
class PyObjectId(ObjectId):
|
9 |
-
"""Custom ObjectId type for Pydantic to enable validation and proper MongoDB support."""
|
10 |
-
@classmethod
|
11 |
-
def __get_validators__(cls):
|
12 |
-
yield cls.validate
|
13 |
-
|
14 |
-
@classmethod
|
15 |
-
def validate(cls, value):
|
16 |
-
if not ObjectId.is_valid(value):
|
17 |
-
raise ValueError("Invalid ObjectId format")
|
18 |
-
return ObjectId(value)
|
19 |
-
|
20 |
-
class HealthRecord(BaseModel):
|
21 |
-
id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
|
22 |
-
bird_id: str = Field(..., description="Unique identifier for a specific bird or batch of birds")
|
23 |
-
date: datetime = Field(default_factory=datetime.utcnow, description="Timestamp of the health record entry")
|
24 |
-
weight: float = Field(..., gt=0, description="Weight measurement in kilograms")
|
25 |
-
mortality_rate: float = Field(..., ge=0, le=100, description="Mortality rate as a percentage")
|
26 |
-
feed_intake: float = Field(..., ge=0, description="Daily feed intake in kilograms")
|
27 |
-
disease_detected: Optional[str] = Field(None, description="Detected disease name, if any")
|
28 |
-
status: str = Field(default="Healthy", description="Health status determined by metrics")
|
29 |
-
treatment_recommendation: Optional[str] = Field(None, description="Suggested treatment based on diagnosis")
|
30 |
-
|
31 |
-
@validator('status')
|
32 |
-
def validate_status(cls, value):
|
33 |
-
allowed_statuses = {"Healthy", "At Risk", "Critical"}
|
34 |
-
if value not in allowed_statuses:
|
35 |
-
raise ValueError(f"Status must be one of {allowed_statuses}")
|
36 |
-
return value
|
37 |
-
|
38 |
-
class Config:
|
39 |
-
json_encoders = {ObjectId: str}
|
40 |
-
schema_extra = {
|
41 |
-
"example": {
|
42 |
-
"bird_id": "batch_123",
|
43 |
-
"date": "2024-11-01T14:30:00Z",
|
44 |
-
"weight": 1.5,
|
45 |
-
"mortality_rate": 2.0,
|
46 |
-
"feed_intake": 0.4,
|
47 |
-
"disease_detected": "Coccidiosis",
|
48 |
-
"status": "Critical",
|
49 |
-
"treatment_recommendation": "Administer anti-coccidial medication and improve hygiene"
|
50 |
-
}
|
51 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
models/schemas/inquiry_schema.py
DELETED
@@ -1,41 +0,0 @@
|
|
1 |
-
# models/schemas/inquiry_schema.py
|
2 |
-
|
3 |
-
from pydantic import BaseModel, Field
|
4 |
-
from datetime import datetime
|
5 |
-
from typing import Optional, List
|
6 |
-
from bson import ObjectId
|
7 |
-
|
8 |
-
class PyObjectId(ObjectId):
|
9 |
-
@classmethod
|
10 |
-
def __get_validators__(cls):
|
11 |
-
yield cls.validate
|
12 |
-
|
13 |
-
@classmethod
|
14 |
-
def validate(cls, v):
|
15 |
-
if not ObjectId.is_valid(v):
|
16 |
-
raise ValueError("Invalid ObjectId")
|
17 |
-
return ObjectId(v)
|
18 |
-
|
19 |
-
class Inquiry(BaseModel):
|
20 |
-
id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
|
21 |
-
user_id: str
|
22 |
-
inquiry_text: str
|
23 |
-
response_text: str
|
24 |
-
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
25 |
-
attached_image: Optional[str] = None # File path or image ID in storage
|
26 |
-
detected_disease: Optional[str] = None
|
27 |
-
disease_confidence: Optional[float] = None
|
28 |
-
|
29 |
-
class Config:
|
30 |
-
json_encoders = {ObjectId: str}
|
31 |
-
schema_extra = {
|
32 |
-
"example": {
|
33 |
-
"user_id": "user123",
|
34 |
-
"inquiry_text": "What is the treatment for coccidiosis?",
|
35 |
-
"response_text": "The recommended treatment for coccidiosis is...",
|
36 |
-
"timestamp": "2024-11-01T12:00:00Z",
|
37 |
-
"attached_image": "image123.jpg",
|
38 |
-
"detected_disease": "Coccidiosis",
|
39 |
-
"disease_confidence": 85.5
|
40 |
-
}
|
41 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
models/schemas/inventory_schema.py
DELETED
@@ -1,58 +0,0 @@
|
|
1 |
-
# models/schemas/inventory_schema.py
|
2 |
-
|
3 |
-
from pydantic import BaseModel, Field
|
4 |
-
from bson import ObjectId
|
5 |
-
from typing import Optional
|
6 |
-
from datetime import datetime
|
7 |
-
|
8 |
-
class PyObjectId(ObjectId):
|
9 |
-
"""Custom ObjectId for Pydantic validation."""
|
10 |
-
@classmethod
|
11 |
-
def __get_validators__(cls):
|
12 |
-
yield cls.validate
|
13 |
-
|
14 |
-
@classmethod
|
15 |
-
def validate(cls, v):
|
16 |
-
if not ObjectId.is_valid(v):
|
17 |
-
raise ValueError("Invalid ObjectId")
|
18 |
-
return ObjectId(v)
|
19 |
-
|
20 |
-
class InventoryItem(BaseModel):
|
21 |
-
id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
|
22 |
-
item_name: str
|
23 |
-
category: Optional[str] = "General"
|
24 |
-
quantity: int
|
25 |
-
restock_level: int # Threshold to trigger restock alert
|
26 |
-
supplier: Optional[str] = None
|
27 |
-
last_updated: datetime = Field(default_factory=datetime.utcnow)
|
28 |
-
|
29 |
-
class Config:
|
30 |
-
json_encoders = {ObjectId: str}
|
31 |
-
schema_extra = {
|
32 |
-
"example": {
|
33 |
-
"item_name": "Chicken Feed",
|
34 |
-
"category": "Feed",
|
35 |
-
"quantity": 200,
|
36 |
-
"restock_level": 50,
|
37 |
-
"supplier": "Farm Supplies Inc.",
|
38 |
-
"last_updated": "2024-10-31T10:00:00Z"
|
39 |
-
}
|
40 |
-
}
|
41 |
-
|
42 |
-
class InventoryUpdate(BaseModel):
|
43 |
-
"""
|
44 |
-
Schema for updating inventory items.
|
45 |
-
"""
|
46 |
-
quantity: Optional[int] = None
|
47 |
-
restock_level: Optional[int] = None
|
48 |
-
last_updated: datetime = Field(default_factory=datetime.utcnow)
|
49 |
-
|
50 |
-
class Config:
|
51 |
-
json_encoders = {ObjectId: str}
|
52 |
-
schema_extra = {
|
53 |
-
"example": {
|
54 |
-
"quantity": 150,
|
55 |
-
"restock_level": 30,
|
56 |
-
"last_updated": "2024-11-01T15:00:00Z"
|
57 |
-
}
|
58 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
models/schemas/log_schema.py
DELETED
@@ -1,28 +0,0 @@
|
|
1 |
-
# models/schemas/log_schema.py
|
2 |
-
|
3 |
-
from pydantic import BaseModel, Field
|
4 |
-
from datetime import datetime
|
5 |
-
from typing import Optional
|
6 |
-
from bson import ObjectId
|
7 |
-
|
8 |
-
class PyObjectId(ObjectId):
|
9 |
-
"""Custom ObjectId class for Pydantic validation."""
|
10 |
-
@classmethod
|
11 |
-
def __get_validators__(cls):
|
12 |
-
yield cls.validate
|
13 |
-
|
14 |
-
@classmethod
|
15 |
-
def validate(cls, v):
|
16 |
-
if not ObjectId.is_valid(v):
|
17 |
-
raise ValueError("Invalid ObjectId")
|
18 |
-
return ObjectId(v)
|
19 |
-
|
20 |
-
class ActivityLog(BaseModel):
|
21 |
-
id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
|
22 |
-
activity_type: str # e.g., "health_check", "feeding", "medication"
|
23 |
-
description: str
|
24 |
-
user_id: str
|
25 |
-
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
26 |
-
|
27 |
-
class Config:
|
28 |
-
json_encoders = {ObjectId: str}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
models/schemas/todo_schema.py
DELETED
@@ -1,76 +0,0 @@
|
|
1 |
-
from pydantic import BaseModel, Field
|
2 |
-
from typing import Optional
|
3 |
-
from datetime import datetime
|
4 |
-
from bson import ObjectId
|
5 |
-
|
6 |
-
class PyObjectId(ObjectId):
|
7 |
-
"""Custom ObjectId class for Pydantic validation."""
|
8 |
-
@classmethod
|
9 |
-
def __get_validators__(cls):
|
10 |
-
yield cls.validate
|
11 |
-
|
12 |
-
@classmethod
|
13 |
-
def validate(cls, v):
|
14 |
-
if not ObjectId.is_valid(v):
|
15 |
-
raise ValueError("Invalid ObjectId")
|
16 |
-
return ObjectId(v)
|
17 |
-
|
18 |
-
class ToDoItem(BaseModel):
|
19 |
-
id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
|
20 |
-
title: str
|
21 |
-
description: Optional[str] = None
|
22 |
-
due_date: datetime
|
23 |
-
is_completed: bool = False
|
24 |
-
assigned_to: Optional[str] = None # Username or User ID of the farmer
|
25 |
-
created_at: datetime = Field(default_factory=datetime.utcnow)
|
26 |
-
updated_at: Optional[datetime] = None # Track the latest update timestamp
|
27 |
-
|
28 |
-
class Config:
|
29 |
-
json_encoders = {ObjectId: str}
|
30 |
-
schema_extra = {
|
31 |
-
"example": {
|
32 |
-
"title": "Feed the chickens",
|
33 |
-
"description": "Provide feed and check water levels for all chicken pens",
|
34 |
-
"due_date": "2024-11-05T14:00:00Z",
|
35 |
-
"is_completed": False,
|
36 |
-
"assigned_to": "farmer_username",
|
37 |
-
"created_at": "2024-10-31T10:00:00Z",
|
38 |
-
"updated_at": "2024-10-31T12:00:00Z"
|
39 |
-
}
|
40 |
-
}
|
41 |
-
|
42 |
-
class ToDoCreate(BaseModel):
|
43 |
-
"""Schema for creating a new to-do item."""
|
44 |
-
title: str
|
45 |
-
description: Optional[str] = None
|
46 |
-
due_date: datetime
|
47 |
-
assigned_to: Optional[str] = None # Optional assignment at creation
|
48 |
-
|
49 |
-
class Config:
|
50 |
-
schema_extra = {
|
51 |
-
"example": {
|
52 |
-
"title": "Inspect chicken feed storage",
|
53 |
-
"description": "Check for any signs of spoilage or infestation",
|
54 |
-
"due_date": "2024-11-02T10:00:00Z",
|
55 |
-
"assigned_to": "farmer_username"
|
56 |
-
}
|
57 |
-
}
|
58 |
-
|
59 |
-
class ToDoUpdate(BaseModel):
|
60 |
-
"""Schema for updating an existing to-do item."""
|
61 |
-
title: Optional[str] = None
|
62 |
-
description: Optional[str] = None
|
63 |
-
due_date: Optional[datetime] = None
|
64 |
-
is_completed: Optional[bool] = None # Allow marking as completed
|
65 |
-
assigned_to: Optional[str] = None # Allow reassignment if needed
|
66 |
-
|
67 |
-
class Config:
|
68 |
-
schema_extra = {
|
69 |
-
"example": {
|
70 |
-
"title": "Check chicken feed storage",
|
71 |
-
"description": "Ensure feed is properly stored to avoid spoilage",
|
72 |
-
"due_date": "2024-11-03T08:00:00Z",
|
73 |
-
"is_completed": True,
|
74 |
-
"assigned_to": "another_farmer_username"
|
75 |
-
}
|
76 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
models/schemas/user_schema.py
DELETED
@@ -1,19 +0,0 @@
|
|
1 |
-
# models/schemas/user_schema.py
|
2 |
-
|
3 |
-
from pydantic import BaseModel, EmailStr, Field
|
4 |
-
from enum import Enum
|
5 |
-
|
6 |
-
class UserRole(str, Enum):
|
7 |
-
FARMER = "farmer"
|
8 |
-
ADMIN = "admin"
|
9 |
-
|
10 |
-
class UserBase(BaseModel):
|
11 |
-
username: str = Field(..., min_length=3, max_length=50)
|
12 |
-
email: EmailStr
|
13 |
-
role: UserRole
|
14 |
-
|
15 |
-
class UserCreate(UserBase):
|
16 |
-
password: str = Field(..., min_length=6)
|
17 |
-
|
18 |
-
class UserResponse(UserBase):
|
19 |
-
id: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
@@ -1,21 +1,23 @@
|
|
1 |
tensorflow[and-cuda]==2.12.0
|
2 |
opencv-python-headless
|
3 |
-
fastapi
|
4 |
passlib[bcrypt]
|
5 |
-
pydantic[email]
|
6 |
-
pymongo
|
7 |
-
python-dotenv
|
8 |
python-jose[cryptography]
|
9 |
starlette
|
10 |
uvicorn
|
11 |
jinja2
|
12 |
python-multipart
|
13 |
numpy<2.0.0
|
14 |
-
pillow
|
15 |
huggingface_hub
|
16 |
-
transformers
|
17 |
keras
|
18 |
-
torch
|
19 |
-
bcrypt
|
20 |
reportlab
|
21 |
pandas
|
|
|
|
|
|
1 |
tensorflow[and-cuda]==2.12.0
|
2 |
opencv-python-headless
|
3 |
+
fastapi~=0.112.4
|
4 |
passlib[bcrypt]
|
5 |
+
pydantic[email]~=2.9.2
|
6 |
+
pymongo~=4.8.0
|
7 |
+
python-dotenv~=1.0.1
|
8 |
python-jose[cryptography]
|
9 |
starlette
|
10 |
uvicorn
|
11 |
jinja2
|
12 |
python-multipart
|
13 |
numpy<2.0.0
|
14 |
+
pillow~=10.4.0
|
15 |
huggingface_hub
|
16 |
+
transformers~=4.46.2
|
17 |
keras
|
18 |
+
torch~=2.5.1
|
19 |
+
bcrypt~=3.1.7
|
20 |
reportlab
|
21 |
pandas
|
22 |
+
pyjwt~=2.9.0
|
23 |
+
huggingface-hub~=0.26.2
|
routes/administration.py
DELETED
@@ -1,417 +0,0 @@
|
|
1 |
-
# routes/administration.py
|
2 |
-
|
3 |
-
import io
|
4 |
-
import logging
|
5 |
-
from datetime import datetime
|
6 |
-
import pandas as pd
|
7 |
-
from bson import ObjectId
|
8 |
-
from fastapi import APIRouter, Depends, Request, HTTPException, status
|
9 |
-
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse, FileResponse
|
10 |
-
from fastapi.templating import Jinja2Templates
|
11 |
-
from reportlab.lib.pagesizes import letter
|
12 |
-
from reportlab.pdfgen import canvas
|
13 |
-
from models.schemas.inventory_schema import InventoryItem, InventoryUpdate
|
14 |
-
from models.schemas.todo_schema import ToDoItem, ToDoCreate, ToDoUpdate
|
15 |
-
from services.auth_service import get_current_user, is_admin_user
|
16 |
-
from services.health_monitoring_service import evaluate_health_data
|
17 |
-
from services.utils import db, log_activity
|
18 |
-
from models.schemas.group_schema import Group, PyObjectId
|
19 |
-
|
20 |
-
# Initialize the router and template engine
|
21 |
-
dashboard_router = APIRouter()
|
22 |
-
templates = Jinja2Templates(directory="templates")
|
23 |
-
|
24 |
-
# MongoDB Collections
|
25 |
-
todo_collection = db.get_collection("todo_items")
|
26 |
-
health_collection = db.get_collection("health_records")
|
27 |
-
activity_collection = db.get_collection("activity_logs")
|
28 |
-
user_collection = db.get_collection("users")
|
29 |
-
inventory_collection = db.get_collection("inventory")
|
30 |
-
group_collection = db.get_collection("groups")
|
31 |
-
|
32 |
-
admin_email = "[email protected]" # Admin email for notifications
|
33 |
-
|
34 |
-
# --- Dashboard and Health Monitoring Routes ---
|
35 |
-
|
36 |
-
@dashboard_router.get("/", response_class=HTMLResponse)
|
37 |
-
async def health_dashboard(request: Request, current_user: str = Depends(get_current_user)):
|
38 |
-
"""
|
39 |
-
Display the admin dashboard with real-time health alerts and historical alerts.
|
40 |
-
"""
|
41 |
-
current_health_metrics = {"weight_loss_percentage": 4, "mortality_rate": 1, "reduced_feed_intake_percentage": 8}
|
42 |
-
real_time_alerts = evaluate_health_data(current_health_metrics).get("notifications", [])
|
43 |
-
historical_alerts = list(health_collection.find().sort("timestamp", -1).limit(20))
|
44 |
-
|
45 |
-
context = {
|
46 |
-
"request": request,
|
47 |
-
"user": current_user,
|
48 |
-
"real_time_alerts": real_time_alerts,
|
49 |
-
"historical_alerts": historical_alerts,
|
50 |
-
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
51 |
-
}
|
52 |
-
return templates.TemplateResponse("admin/dashboard.html", context)
|
53 |
-
|
54 |
-
|
55 |
-
# --- To-Do Task Management Routes ---
|
56 |
-
|
57 |
-
@dashboard_router.get("/todo", response_class=HTMLResponse)
|
58 |
-
async def todo_list(request: Request, current_user: str = Depends(get_current_user)):
|
59 |
-
"""
|
60 |
-
Display a list of all to-do items.
|
61 |
-
"""
|
62 |
-
todos = list(todo_collection.find({}))
|
63 |
-
|
64 |
-
context = {
|
65 |
-
"request": request,
|
66 |
-
"user": current_user,
|
67 |
-
"todos": todos,
|
68 |
-
"title": "Todo List",
|
69 |
-
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
70 |
-
}
|
71 |
-
return templates.TemplateResponse("admin/todo/list.html", context)
|
72 |
-
|
73 |
-
|
74 |
-
@dashboard_router.get("/todo/add", response_class=HTMLResponse)
|
75 |
-
async def add_todo_form(request: Request, current_user: str = Depends(get_current_user)):
|
76 |
-
"""
|
77 |
-
Show form to add a new to-do item.
|
78 |
-
"""
|
79 |
-
context = {
|
80 |
-
"request": request,
|
81 |
-
"user": current_user,
|
82 |
-
"title": "Add Todo List",
|
83 |
-
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
84 |
-
}
|
85 |
-
return templates.TemplateResponse("admin/todo/add.html", context)
|
86 |
-
|
87 |
-
|
88 |
-
@dashboard_router.post("/todo/add")
|
89 |
-
async def create_todo(todo_data: ToDoCreate, current_user: str = Depends(get_current_user)):
|
90 |
-
"""
|
91 |
-
Create a new to-do item.
|
92 |
-
"""
|
93 |
-
if not is_admin_user(current_user):
|
94 |
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required.")
|
95 |
-
|
96 |
-
todo = todo_data.dict()
|
97 |
-
todo.update({"created_by": current_user, "is_completed": False, "created_at": datetime.utcnow()})
|
98 |
-
result = todo_collection.insert_one(todo)
|
99 |
-
|
100 |
-
if result.inserted_id:
|
101 |
-
log_activity("To-Do Created", f"To-Do '{todo['title']}' created by {current_user}.", current_user)
|
102 |
-
return {"message": "To-Do item created successfully."}
|
103 |
-
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="To-Do creation failed.")
|
104 |
-
|
105 |
-
|
106 |
-
@dashboard_router.get("/todo/{todo_id}/edit", response_class=HTMLResponse)
|
107 |
-
async def edit_todo_form(todo_id: str, request: Request, current_user: str = Depends(get_current_user)):
|
108 |
-
"""
|
109 |
-
Show form to edit an existing to-do item.
|
110 |
-
"""
|
111 |
-
todo = todo_collection.find_one({"_id": ObjectId(todo_id)})
|
112 |
-
if not todo:
|
113 |
-
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="To-Do item not found.")
|
114 |
-
context = {
|
115 |
-
"request": request,
|
116 |
-
"user": current_user,
|
117 |
-
"title": "Edit Todo List",
|
118 |
-
"todo": todo,
|
119 |
-
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
120 |
-
}
|
121 |
-
return templates.TemplateResponse("admin/todo/edit.html", context)
|
122 |
-
|
123 |
-
|
124 |
-
@dashboard_router.post("/todo/{todo_id}/edit")
|
125 |
-
async def update_todo(todo_id: str, todo_data: ToDoUpdate, current_user: str = Depends(get_current_user)):
|
126 |
-
"""
|
127 |
-
Update an existing to-do item.
|
128 |
-
"""
|
129 |
-
if not is_admin_user(current_user):
|
130 |
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required.")
|
131 |
-
|
132 |
-
update_data = {k: v for k, v in todo_data.dict().items() if v is not None}
|
133 |
-
result = todo_collection.update_one({"_id": ObjectId(todo_id)}, {"$set": update_data})
|
134 |
-
|
135 |
-
if result.modified_count:
|
136 |
-
log_activity("To-Do Updated", f"To-Do '{todo_id}' updated by {current_user}.", current_user)
|
137 |
-
return {"message": "To-Do item updated successfully."}
|
138 |
-
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="To-Do item not found or no changes made.")
|
139 |
-
|
140 |
-
|
141 |
-
@dashboard_router.get("/todo/{todo_id}/delete", response_class=HTMLResponse)
|
142 |
-
async def delete_todo_confirm(todo_id: str, request: Request, current_user: str = Depends(get_current_user)):
|
143 |
-
"""
|
144 |
-
Show confirmation page before deleting a to-do item.
|
145 |
-
"""
|
146 |
-
todo = todo_collection.find_one({"_id": ObjectId(todo_id)})
|
147 |
-
if not todo:
|
148 |
-
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="To-Do item not found.")
|
149 |
-
|
150 |
-
context = {
|
151 |
-
"request": request,
|
152 |
-
"user": current_user,
|
153 |
-
"title": "Delete Todo List",
|
154 |
-
"todo": todo,
|
155 |
-
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
156 |
-
}
|
157 |
-
|
158 |
-
return templates.TemplateResponse("admin/todo/delete.html", context)
|
159 |
-
|
160 |
-
|
161 |
-
@dashboard_router.post("/todo/{todo_id}/delete")
|
162 |
-
async def delete_todo(todo_id: str, current_user: str = Depends(get_current_user)):
|
163 |
-
"""
|
164 |
-
Delete a specific to-do item.
|
165 |
-
"""
|
166 |
-
if not is_admin_user(current_user):
|
167 |
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required.")
|
168 |
-
|
169 |
-
result = todo_collection.delete_one({"_id": ObjectId(todo_id)})
|
170 |
-
if result.deleted_count:
|
171 |
-
log_activity("To-Do Deleted", f"To-Do '{todo_id}' deleted by {current_user}.", current_user)
|
172 |
-
return {"message": "To-Do item deleted successfully."}
|
173 |
-
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="To-Do item not found.")
|
174 |
-
|
175 |
-
|
176 |
-
# --- Inventory Management Routes ---
|
177 |
-
|
178 |
-
@dashboard_router.get("/inventory", response_class=HTMLResponse)
|
179 |
-
async def inventory_list(request: Request, current_user: str = Depends(get_current_user)):
|
180 |
-
"""
|
181 |
-
Display a list of all inventory items.
|
182 |
-
"""
|
183 |
-
items = list(inventory_collection.find({}))
|
184 |
-
|
185 |
-
context = {
|
186 |
-
"request": request,
|
187 |
-
"user": current_user,
|
188 |
-
"title": "Inventory ",
|
189 |
-
"items": items,
|
190 |
-
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
191 |
-
}
|
192 |
-
return templates.TemplateResponse("admin/inventory/list.html", context)
|
193 |
-
|
194 |
-
|
195 |
-
@dashboard_router.get("/inventory/add", response_class=HTMLResponse)
|
196 |
-
async def add_inventory_form(request: Request, current_user: str = Depends(get_current_user)):
|
197 |
-
"""
|
198 |
-
Show form to add a new inventory item.
|
199 |
-
"""
|
200 |
-
context = {
|
201 |
-
"request": request,
|
202 |
-
"user": current_user,
|
203 |
-
"title": "Add to Inventory ",
|
204 |
-
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
205 |
-
}
|
206 |
-
return templates.TemplateResponse("admin/inventory/add.html",context)
|
207 |
-
|
208 |
-
|
209 |
-
@dashboard_router.post("/inventory/add")
|
210 |
-
async def create_inventory(item: InventoryItem, current_user: str = Depends(get_current_user)):
|
211 |
-
"""
|
212 |
-
Create a new inventory item.
|
213 |
-
"""
|
214 |
-
if not is_admin_user(current_user):
|
215 |
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required.")
|
216 |
-
|
217 |
-
item_data = item.dict()
|
218 |
-
item_data["last_updated"] = datetime.utcnow()
|
219 |
-
result = inventory_collection.insert_one(item_data)
|
220 |
-
|
221 |
-
if result.inserted_id:
|
222 |
-
log_activity("Inventory Created", f"Inventory item '{item.item_name}' added by {current_user}.", current_user)
|
223 |
-
return {"message": "Inventory item created successfully."}
|
224 |
-
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Inventory creation failed.")
|
225 |
-
|
226 |
-
|
227 |
-
@dashboard_router.get("/inventory/{item_id}/edit", response_class=HTMLResponse)
|
228 |
-
async def edit_inventory_form(item_id: str, request: Request, current_user: str = Depends(get_current_user)):
|
229 |
-
"""
|
230 |
-
Show form to edit an existing inventory item.
|
231 |
-
"""
|
232 |
-
|
233 |
-
item = inventory_collection.find_one({"_id": ObjectId(item_id)})
|
234 |
-
if not item:
|
235 |
-
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Inventory item not found.")
|
236 |
-
|
237 |
-
context = {
|
238 |
-
"request": request,
|
239 |
-
"user": current_user,
|
240 |
-
"title": "Edit Inventory ",
|
241 |
-
"item": item,
|
242 |
-
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
243 |
-
}
|
244 |
-
return templates.TemplateResponse("admin/inventory/edit.html", context)
|
245 |
-
|
246 |
-
|
247 |
-
@dashboard_router.post("/inventory/{item_id}/edit")
|
248 |
-
async def update_inventory(item_id: str, item_update: InventoryUpdate, current_user: str = Depends(get_current_user)):
|
249 |
-
"""
|
250 |
-
Update an existing inventory item.
|
251 |
-
"""
|
252 |
-
if not is_admin_user(current_user):
|
253 |
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required.")
|
254 |
-
|
255 |
-
update_data = item_update.dict(exclude_unset=True)
|
256 |
-
update_data["last_updated"] = datetime.utcnow()
|
257 |
-
result = inventory_collection.update_one({"_id": ObjectId(item_id)}, {"$set": update_data})
|
258 |
-
|
259 |
-
if result.modified_count:
|
260 |
-
log_activity("Inventory Updated", f"Inventory item '{item_id}' updated by {current_user}.", current_user)
|
261 |
-
return {"message": "Inventory item updated successfully."}
|
262 |
-
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Inventory item not found or no changes made.")
|
263 |
-
|
264 |
-
|
265 |
-
@dashboard_router.get("/inventory/{item_id}/delete", response_class=HTMLResponse)
|
266 |
-
async def delete_inventory_confirm(item_id: str, request: Request, current_user: str = Depends(get_current_user)):
|
267 |
-
"""
|
268 |
-
Show confirmation page before deleting an inventory item.
|
269 |
-
"""
|
270 |
-
item = inventory_collection.find_one({"_id": ObjectId(item_id)})
|
271 |
-
if not item:
|
272 |
-
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Inventory item not found.")
|
273 |
-
|
274 |
-
context = {
|
275 |
-
"request": request,
|
276 |
-
"user": current_user,
|
277 |
-
"title": "Delete Item",
|
278 |
-
"item": item,
|
279 |
-
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
280 |
-
}
|
281 |
-
return templates.TemplateResponse("admin/inventory/delete.html", context)
|
282 |
-
|
283 |
-
|
284 |
-
@dashboard_router.post("/inventory/{item_id}/delete")
|
285 |
-
async def delete_inventory(item_id: str, current_user: str = Depends(get_current_user)):
|
286 |
-
"""
|
287 |
-
Delete a specific inventory item.
|
288 |
-
"""
|
289 |
-
if not is_admin_user(current_user):
|
290 |
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required.")
|
291 |
-
|
292 |
-
result = inventory_collection.delete_one({"_id": ObjectId(item_id)})
|
293 |
-
if result.deleted_count:
|
294 |
-
log_activity("Inventory Deleted", f"Inventory item '{item_id}' deleted by {current_user}.", current_user)
|
295 |
-
return {"message": "Inventory item deleted successfully."}
|
296 |
-
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Inventory item not found.")
|
297 |
-
|
298 |
-
|
299 |
-
# --- Group Management Routes ---
|
300 |
-
|
301 |
-
@dashboard_router.get("/groups", response_class=HTMLResponse)
|
302 |
-
async def group_list(request: Request, current_user: str = Depends(get_current_user)):
|
303 |
-
"""
|
304 |
-
Display a list of all groups.
|
305 |
-
"""
|
306 |
-
groups = list(group_collection.find({}))
|
307 |
-
context = {
|
308 |
-
"request": request,
|
309 |
-
"user": current_user,
|
310 |
-
"title": "Groups",
|
311 |
-
"groups": groups,
|
312 |
-
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
313 |
-
}
|
314 |
-
return templates.TemplateResponse("admin/group/list.html", context)
|
315 |
-
|
316 |
-
|
317 |
-
@dashboard_router.get("/groups/add", response_class=HTMLResponse)
|
318 |
-
async def add_group_form(request: Request, current_user: str = Depends(get_current_user)):
|
319 |
-
"""
|
320 |
-
Show form to create a new group.
|
321 |
-
"""
|
322 |
-
context = {
|
323 |
-
"request": request,
|
324 |
-
"user": current_user,
|
325 |
-
"title": "Groups",
|
326 |
-
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
327 |
-
}
|
328 |
-
return templates.TemplateResponse("admin/group/add.html", context)
|
329 |
-
|
330 |
-
|
331 |
-
@dashboard_router.post("/groups/add")
|
332 |
-
async def create_group(group: Group, current_user: str = Depends(get_current_user)):
|
333 |
-
"""
|
334 |
-
Create a new group.
|
335 |
-
"""
|
336 |
-
if not is_admin_user(current_user):
|
337 |
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required.")
|
338 |
-
|
339 |
-
group_data = group.dict(by_alias=True)
|
340 |
-
group_data["created_by"] = current_user
|
341 |
-
group_data["created_at"] = datetime.utcnow()
|
342 |
-
result = group_collection.insert_one(group_data)
|
343 |
-
|
344 |
-
if result.inserted_id:
|
345 |
-
log_activity("Group Created", f"Group '{group.group_name}' created by {current_user}.", current_user)
|
346 |
-
return {"message": "Group created successfully."}
|
347 |
-
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Group creation failed.")
|
348 |
-
|
349 |
-
|
350 |
-
@dashboard_router.get("/groups/{group_id}/add_member", response_class=HTMLResponse)
|
351 |
-
async def add_member_form(group_id: str, request: Request, current_user: str = Depends(get_current_user)):
|
352 |
-
"""
|
353 |
-
Show form to add a member to a group.
|
354 |
-
"""
|
355 |
-
group = group_collection.find_one({"_id": ObjectId(group_id)})
|
356 |
-
if not group:
|
357 |
-
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Group not found.")
|
358 |
-
users = list(user_collection.find({"role": "farmer"}))
|
359 |
-
context = {
|
360 |
-
"request": request,
|
361 |
-
"user": current_user,
|
362 |
-
"title": "Groups",
|
363 |
-
"group": group,
|
364 |
-
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
365 |
-
}
|
366 |
-
|
367 |
-
return templates.TemplateResponse("admin/group/add_member.html", {"request": request, "group": group, "users": users})
|
368 |
-
|
369 |
-
|
370 |
-
@dashboard_router.post("/groups/{group_id}/add_member")
|
371 |
-
async def add_member_to_group(group_id: str, user_id: str, current_user: str = Depends(get_current_user)):
|
372 |
-
"""
|
373 |
-
Add a farmer to a group.
|
374 |
-
"""
|
375 |
-
if not is_admin_user(current_user):
|
376 |
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required.")
|
377 |
-
|
378 |
-
group = group_collection.find_one({"_id": ObjectId(group_id)})
|
379 |
-
if not group:
|
380 |
-
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Group not found.")
|
381 |
-
|
382 |
-
if user_id in group["members"]:
|
383 |
-
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="User already a member.")
|
384 |
-
|
385 |
-
group_collection.update_one({"_id": ObjectId(group_id)}, {"$push": {"members": user_id}})
|
386 |
-
return {"message": "User added to group successfully."}
|
387 |
-
|
388 |
-
|
389 |
-
# --- Health Data Export Route ---
|
390 |
-
|
391 |
-
@dashboard_router.get("/export/health/pdf")
|
392 |
-
async def export_health_pdf(current_user: str = Depends(get_current_user)):
|
393 |
-
"""
|
394 |
-
Export health records as a PDF.
|
395 |
-
"""
|
396 |
-
buffer = io.BytesIO()
|
397 |
-
pdf = canvas.Canvas(buffer, pagesize=letter)
|
398 |
-
pdf.setTitle("Health Records Report")
|
399 |
-
|
400 |
-
health_data = list(health_collection.find({}, {"_id": 0}))
|
401 |
-
if not health_data:
|
402 |
-
raise HTTPException(status_code=404, detail="No health data available for export.")
|
403 |
-
|
404 |
-
pdf.drawString(100, 750, "Health Records Report")
|
405 |
-
pdf.drawString(100, 730, f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
406 |
-
y = 710
|
407 |
-
for record in health_data:
|
408 |
-
pdf.drawString(100, y, str(record))
|
409 |
-
y -= 20
|
410 |
-
if y < 100:
|
411 |
-
pdf.showPage()
|
412 |
-
y = 750
|
413 |
-
pdf.save()
|
414 |
-
buffer.seek(0)
|
415 |
-
log_activity("Exported PDF", "Health data exported as PDF", current_user)
|
416 |
-
return StreamingResponse(buffer, media_type="application/pdf",
|
417 |
-
headers={"Content-Disposition": "attachment;filename=health_records.pdf"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
routes/authentication.py
DELETED
@@ -1,126 +0,0 @@
|
|
1 |
-
# routes/authentication.py
|
2 |
-
|
3 |
-
from datetime import timedelta, datetime
|
4 |
-
import logging
|
5 |
-
from fastapi import APIRouter, Depends, status, Request, HTTPException, Form
|
6 |
-
from fastapi.responses import RedirectResponse, JSONResponse
|
7 |
-
from fastapi.security import OAuth2PasswordRequestForm
|
8 |
-
from fastapi.templating import Jinja2Templates
|
9 |
-
from starlette.responses import HTMLResponse
|
10 |
-
from pydantic import ValidationError
|
11 |
-
from models.schemas.user_schema import UserCreate, UserResponse
|
12 |
-
from services.auth_service import (
|
13 |
-
create_user,
|
14 |
-
authenticate_user,
|
15 |
-
create_access_token,
|
16 |
-
ACCESS_TOKEN_EXPIRE_MINUTES,
|
17 |
-
get_current_user,
|
18 |
-
blacklist_token,
|
19 |
-
Token,
|
20 |
-
oauth2_scheme
|
21 |
-
)
|
22 |
-
from services.utils import log_to_db
|
23 |
-
|
24 |
-
# Configure router and templates
|
25 |
-
auth_router = APIRouter()
|
26 |
-
templates = Jinja2Templates(directory="templates")
|
27 |
-
|
28 |
-
# Set up advanced logging
|
29 |
-
logger = logging.getLogger(__name__)
|
30 |
-
|
31 |
-
@auth_router.get("/register", response_class=HTMLResponse)
|
32 |
-
async def register_page(request: Request):
|
33 |
-
"""Render the registration form for new users."""
|
34 |
-
logger.info("Accessed registration page.")
|
35 |
-
return templates.TemplateResponse("auth/register.html", {"request": request})
|
36 |
-
|
37 |
-
@auth_router.post("/register", response_model=UserResponse)
|
38 |
-
async def register(
|
39 |
-
request: Request,
|
40 |
-
username: str = Form(...),
|
41 |
-
email: str = Form(...),
|
42 |
-
password: str = Form(...),
|
43 |
-
role: str = Form("farmer") # Default role set to "farmer"
|
44 |
-
):
|
45 |
-
"""Handle user registration, defaulting to the 'farmer' role."""
|
46 |
-
logger.info("Received registration request.")
|
47 |
-
|
48 |
-
try:
|
49 |
-
user = UserCreate(username=username, email=email, password=password, role=role)
|
50 |
-
logger.info(f"Attempting to register new user: {user.username}")
|
51 |
-
except ValidationError as e:
|
52 |
-
logger.error(f"User registration validation failed: {e}")
|
53 |
-
log_to_db("ERROR", f"User registration validation failed: {e}")
|
54 |
-
return templates.TemplateResponse("auth/register.html", {
|
55 |
-
"request": request,
|
56 |
-
"error": "Validation error in registration details."
|
57 |
-
})
|
58 |
-
|
59 |
-
result = create_user(user)
|
60 |
-
if result["status"] == "error":
|
61 |
-
error_message = result["message"]
|
62 |
-
logger.warning(f"Registration failed for user {user.username}: {error_message}")
|
63 |
-
log_to_db("WARNING", f"Registration failed for user {user.username}: {error_message}")
|
64 |
-
return templates.TemplateResponse("auth/register.html", {
|
65 |
-
"request": request,
|
66 |
-
"error": error_message
|
67 |
-
})
|
68 |
-
|
69 |
-
logger.info(f"User {user.username} registered successfully.")
|
70 |
-
log_to_db("INFO", f"User {user.username} registered successfully.")
|
71 |
-
return RedirectResponse(url="/auth/login", status_code=status.HTTP_302_FOUND)
|
72 |
-
|
73 |
-
@auth_router.get("/login", response_class=HTMLResponse)
|
74 |
-
async def login_page(request: Request):
|
75 |
-
"""Render the login form for users."""
|
76 |
-
logger.info("Accessed login page.")
|
77 |
-
return templates.TemplateResponse("auth/login.html", {"request": request})
|
78 |
-
|
79 |
-
@auth_router.post("/login", response_model=Token)
|
80 |
-
async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):
|
81 |
-
"""Authenticate user, issue JWT, and set it in a secure cookie."""
|
82 |
-
logger.info(f"User login attempt: {form_data.username}")
|
83 |
-
user = authenticate_user(form_data.username, form_data.password)
|
84 |
-
|
85 |
-
if not user:
|
86 |
-
error_message = "Invalid username or password"
|
87 |
-
logger.warning(f"Failed login attempt for user {form_data.username}: {error_message}")
|
88 |
-
return templates.TemplateResponse("auth/login.html", {"request": request, "error": error_message})
|
89 |
-
|
90 |
-
# Create the JWT token
|
91 |
-
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
92 |
-
access_token = create_access_token(data={"sub": user["username"]}, expires_delta=access_token_expires)
|
93 |
-
|
94 |
-
# Set the token in a secure cookie
|
95 |
-
response = RedirectResponse(url="/admin/", status_code=status.HTTP_302_FOUND)
|
96 |
-
response.set_cookie(
|
97 |
-
key="access_token",
|
98 |
-
value=access_token,
|
99 |
-
httponly=True,
|
100 |
-
max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
101 |
-
secure=False,
|
102 |
-
samesite="Lax"
|
103 |
-
)
|
104 |
-
logger.info(f"User {form_data.username} logged in successfully and redirected to dashboard.")
|
105 |
-
return response
|
106 |
-
@auth_router.get("/me", response_model=UserResponse)
|
107 |
-
async def read_users_me(current_user: str = Depends(get_current_user)):
|
108 |
-
"""Retrieve information about the current authenticated user."""
|
109 |
-
logger.info(f"Fetching data for current user: {current_user}")
|
110 |
-
if not current_user:
|
111 |
-
error_message = "User not authenticated"
|
112 |
-
logger.warning(error_message)
|
113 |
-
log_to_db("WARNING", error_message)
|
114 |
-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
|
115 |
-
|
116 |
-
logger.info(f"Data retrieved for user: {current_user}")
|
117 |
-
log_to_db("INFO", f"Data retrieved for user {current_user}")
|
118 |
-
return {"username": current_user}
|
119 |
-
|
120 |
-
@auth_router.get("/logout")
|
121 |
-
async def logout(request: Request):
|
122 |
-
"""Logout the user by clearing the token cookie."""
|
123 |
-
response = RedirectResponse(url="/auth/login", status_code=status.HTTP_302_FOUND)
|
124 |
-
response.delete_cookie("access_token")
|
125 |
-
logger.info("User logged out successfully.")
|
126 |
-
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
routes/chatbot.py
DELETED
@@ -1,97 +0,0 @@
|
|
1 |
-
# routes/chatbot.py
|
2 |
-
|
3 |
-
import cv2
|
4 |
-
import numpy as np
|
5 |
-
import logging
|
6 |
-
from fastapi.templating import Jinja2Templates
|
7 |
-
from services.auth_service import get_current_user
|
8 |
-
from services.utils import db
|
9 |
-
from fastapi import APIRouter, UploadFile, File, Depends, Request, Form
|
10 |
-
from fastapi.responses import HTMLResponse, JSONResponse
|
11 |
-
from datetime import datetime
|
12 |
-
from services.disease_detection_service import bot
|
13 |
-
from models.schemas.inquiry_schema import Inquiry
|
14 |
-
|
15 |
-
# Initialize router and template engine
|
16 |
-
chatbot_router = APIRouter()
|
17 |
-
templates = Jinja2Templates(directory="templates")
|
18 |
-
|
19 |
-
# MongoDB collection for inquiries
|
20 |
-
inquiry_collection = db.get_collection("inquiries")
|
21 |
-
|
22 |
-
# Configure logging
|
23 |
-
logging.basicConfig(level=logging.INFO)
|
24 |
-
logger = logging.getLogger(__name__)
|
25 |
-
|
26 |
-
async def greeting_based_on_time():
|
27 |
-
logger.info("Generating greeting based on current time.")
|
28 |
-
current_hour = datetime.now().hour
|
29 |
-
if 5 <= current_hour < 12:
|
30 |
-
return "Good morning"
|
31 |
-
elif 12 <= current_hour < 17:
|
32 |
-
return "Good afternoon"
|
33 |
-
else:
|
34 |
-
return "Good evening"
|
35 |
-
|
36 |
-
@chatbot_router.get("/", response_class=HTMLResponse)
|
37 |
-
async def chatbot_page(request: Request, current_user: str = Depends(get_current_user)):
|
38 |
-
logger.info("Loading chatbot page for the user.")
|
39 |
-
greeting = await greeting_based_on_time()
|
40 |
-
context = {"request": request, "greeting": f"{greeting}, {current_user}! How can I help you today?"}
|
41 |
-
logger.info("Returning chatbot HTML page with greeting.")
|
42 |
-
return templates.TemplateResponse("chatbot.html", context)
|
43 |
-
|
44 |
-
@chatbot_router.post("/inquire")
|
45 |
-
async def inquire(
|
46 |
-
text: str = Form(...),
|
47 |
-
current_user: str = Depends(get_current_user),
|
48 |
-
file: UploadFile = None
|
49 |
-
):
|
50 |
-
"""Handle inquiries with text and optional image for disease detection."""
|
51 |
-
logger.info("Received inquiry request.")
|
52 |
-
response_text = ""
|
53 |
-
detected_disease, disease_confidence = None, None
|
54 |
-
|
55 |
-
# Handle image inquiries with disease detection
|
56 |
-
if file:
|
57 |
-
logger.info(f"Image file received: {file.filename}")
|
58 |
-
image_data = await file.read()
|
59 |
-
image_np = np.frombuffer(image_data, np.uint8)
|
60 |
-
image = cv2.imdecode(image_np, cv2.IMREAD_COLOR)
|
61 |
-
response, disease_name, status, recommendation, confidence = bot.diagnose_and_respond(image)
|
62 |
-
response_text = response
|
63 |
-
detected_disease, disease_confidence = disease_name, confidence
|
64 |
-
logger.info(f"Disease detected: {detected_disease} with confidence {disease_confidence}%")
|
65 |
-
else:
|
66 |
-
logger.info("Processing text-only inquiry.")
|
67 |
-
response_text = bot.llama_response(text)
|
68 |
-
logger.info(f"Generated response: {response_text}")
|
69 |
-
|
70 |
-
# Log inquiry into MongoDB
|
71 |
-
try:
|
72 |
-
inquiry = Inquiry(
|
73 |
-
user_id=current_user,
|
74 |
-
inquiry_text=text,
|
75 |
-
response_text=response_text,
|
76 |
-
attached_image=file.filename if file else None,
|
77 |
-
detected_disease=detected_disease,
|
78 |
-
disease_confidence=disease_confidence
|
79 |
-
)
|
80 |
-
inquiry_collection.insert_one(inquiry.dict(by_alias=True))
|
81 |
-
logger.info("Inquiry logged successfully in MongoDB.")
|
82 |
-
except Exception as e:
|
83 |
-
logger.error("Error logging inquiry in MongoDB.", exc_info=True)
|
84 |
-
|
85 |
-
return JSONResponse(content={"response_text": response_text, "detected_disease": detected_disease, "confidence": disease_confidence})
|
86 |
-
|
87 |
-
@chatbot_router.get("/history")
|
88 |
-
async def get_inquiry_history(current_user: str = Depends(get_current_user)):
|
89 |
-
"""Fetch recent inquiry history for the current user."""
|
90 |
-
logger.info("Fetching inquiry history for the user.")
|
91 |
-
try:
|
92 |
-
history = list(inquiry_collection.find({"user_id": current_user}).sort("timestamp", -1).limit(10))
|
93 |
-
logger.info("Inquiry history retrieved successfully.")
|
94 |
-
return JSONResponse(content={"history": history})
|
95 |
-
except Exception as e:
|
96 |
-
logger.error("Error fetching inquiry history from MongoDB.", exc_info=True)
|
97 |
-
return JSONResponse(content={"error": "Failed to fetch inquiry history."}, status_code=500)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
routes/disease_detection.py
DELETED
@@ -1,93 +0,0 @@
|
|
1 |
-
# routes/disease_detection.py
|
2 |
-
|
3 |
-
import datetime
|
4 |
-
import logging
|
5 |
-
import io
|
6 |
-
from fastapi import APIRouter, UploadFile, File, HTTPException
|
7 |
-
from fastapi.responses import JSONResponse
|
8 |
-
from PIL import Image
|
9 |
-
import numpy as np
|
10 |
-
|
11 |
-
from routes.administration import health_collection
|
12 |
-
from services.disease_detection_service import bot
|
13 |
-
from services.health_alerts_service import check_health_thresholds
|
14 |
-
|
15 |
-
# Configure logging for this module
|
16 |
-
logger = logging.getLogger(__name__)
|
17 |
-
|
18 |
-
# Initialize the disease detection router
|
19 |
-
disease_router = APIRouter()
|
20 |
-
|
21 |
-
|
22 |
-
@disease_router.post("/detect_disease", response_class=JSONResponse)
|
23 |
-
async def detect_disease(file: UploadFile = File(...)):
|
24 |
-
"""
|
25 |
-
Detect disease from an uploaded poultry image and provide treatment recommendations.
|
26 |
-
|
27 |
-
Parameters:
|
28 |
-
file (UploadFile): The image file to analyze.
|
29 |
-
|
30 |
-
Returns:
|
31 |
-
JSONResponse: JSON with disease prediction, confidence, treatment recommendation,
|
32 |
-
and other details.
|
33 |
-
"""
|
34 |
-
logger.info("Received disease detection request.")
|
35 |
-
|
36 |
-
# Step 1: Validate the uploaded file type
|
37 |
-
if file.content_type not in ["image/jpeg", "image/png"]:
|
38 |
-
logger.warning(f"Invalid file type: {file.content_type}")
|
39 |
-
raise HTTPException(status_code=400, detail="Invalid file type. Please upload a JPEG or PNG image.")
|
40 |
-
|
41 |
-
try:
|
42 |
-
# Step 2: Read and process the image
|
43 |
-
image_data = await file.read()
|
44 |
-
image = Image.open(io.BytesIO(image_data)).convert("RGB")
|
45 |
-
image_np = np.array(image)
|
46 |
-
logger.info("Image successfully loaded and converted to RGB format.")
|
47 |
-
except Exception as e:
|
48 |
-
logger.error(f"Failed to load image: {e}")
|
49 |
-
raise HTTPException(status_code=500, detail="Error processing the uploaded image.")
|
50 |
-
|
51 |
-
# Step 3: Perform disease detection
|
52 |
-
try:
|
53 |
-
detailed_response, disease_name, status, recommendation, confidence = bot.diagnose_and_respond(image_np)
|
54 |
-
logger.info(f"Disease detected: {disease_name} with {confidence:.2f}% confidence.")
|
55 |
-
except Exception as e:
|
56 |
-
logger.error(f"Error in disease detection: {e}")
|
57 |
-
raise HTTPException(status_code=500, detail="Error processing the image for disease detection.")
|
58 |
-
|
59 |
-
# Step 4: Check if a valid disease was detected
|
60 |
-
if not disease_name:
|
61 |
-
logger.warning("No disease detected in the image.")
|
62 |
-
return JSONResponse(content={"error": "Unable to detect disease. Please try a different image."},
|
63 |
-
status_code=500)
|
64 |
-
|
65 |
-
# Step 5: Log the detected disease and recommended treatment in health records
|
66 |
-
record = {
|
67 |
-
"bird_id": "batch_123", # Placeholder, replace with actual bird/batch ID
|
68 |
-
"date": datetime.datetime.utcnow(),
|
69 |
-
"disease_detected": disease_name,
|
70 |
-
"status": status,
|
71 |
-
"confidence": confidence,
|
72 |
-
"treatment_recommendation": recommendation,
|
73 |
-
"weight": 1.4, # Placeholder, replace with actual weight measurement
|
74 |
-
"mortality_rate": 0.5, # Placeholder, replace with actual mortality rate
|
75 |
-
"feed_intake": 0.35 # Placeholder, replace with actual feed intake
|
76 |
-
}
|
77 |
-
health_collection.insert_one(record)
|
78 |
-
logger.info("Disease and treatment recommendation logged in health records.")
|
79 |
-
|
80 |
-
# Step 6: Check for health alerts based on recorded data
|
81 |
-
check_health_thresholds(record)
|
82 |
-
logger.info("Health thresholds checked for potential alerts.")
|
83 |
-
|
84 |
-
# Step 7: Construct and return response
|
85 |
-
response_content = {
|
86 |
-
"disease": disease_name,
|
87 |
-
"status": status,
|
88 |
-
"confidence": f"{confidence:.2f}%",
|
89 |
-
"recommendation": recommendation,
|
90 |
-
"details": detailed_response
|
91 |
-
}
|
92 |
-
logger.info(f"Response content prepared: {response_content}")
|
93 |
-
return JSONResponse(content=response_content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/populate_health_records.py
DELETED
@@ -1,57 +0,0 @@
|
|
1 |
-
# scripts/populate_health_records.py
|
2 |
-
import os
|
3 |
-
import random
|
4 |
-
from datetime import datetime, timedelta
|
5 |
-
from pymongo import MongoClient
|
6 |
-
from models.schemas.health_record_schema import HealthRecord
|
7 |
-
from bson import ObjectId
|
8 |
-
|
9 |
-
# Connect to MongoDB
|
10 |
-
client = MongoClient(os.getenv("MONGO_URI")) # Replace with your MongoDB URI
|
11 |
-
db = client["poultry_management"]
|
12 |
-
health_collection = db["health_records"]
|
13 |
-
|
14 |
-
# Sample data for generating health records
|
15 |
-
statuses = ["Healthy", "At Risk", "Critical"]
|
16 |
-
diseases = ["Coccidiosis", "New Castle Disease", "Salmonella", None]
|
17 |
-
treatments = {
|
18 |
-
"Coccidiosis": "Administer anti-coccidial medication and improve hygiene",
|
19 |
-
"New Castle Disease": "Isolate affected birds and consult a veterinarian",
|
20 |
-
"Salmonella": "Administer antibiotics as prescribed and ensure biosecurity",
|
21 |
-
None: "No treatment necessary; maintain regular monitoring"
|
22 |
-
}
|
23 |
-
|
24 |
-
# Function to generate random health record data
|
25 |
-
def generate_random_health_record():
|
26 |
-
bird_id = f"batch_{random.randint(100, 999)}" # Random bird batch ID
|
27 |
-
date = datetime.utcnow() - timedelta(days=random.randint(1, 365)) # Random date within last year
|
28 |
-
weight = round(random.uniform(0.8, 2.5), 2) # Weight in kg, between 0.8 and 2.5
|
29 |
-
mortality_rate = round(random.uniform(0, 10), 2) # Mortality rate as a percentage
|
30 |
-
feed_intake = round(random.uniform(0.1, 0.5), 2) # Feed intake in kg
|
31 |
-
disease_detected = random.choice(diseases) # Random disease or None
|
32 |
-
status = random.choices(statuses, weights=[70, 20, 10], k=1)[0] # Weighted choice for realistic distribution
|
33 |
-
treatment_recommendation = treatments[disease_detected] if disease_detected else "No treatment necessary"
|
34 |
-
|
35 |
-
# Create HealthRecord instance
|
36 |
-
health_record = HealthRecord(
|
37 |
-
id=ObjectId(),
|
38 |
-
bird_id=bird_id,
|
39 |
-
date=date,
|
40 |
-
weight=weight,
|
41 |
-
mortality_rate=mortality_rate,
|
42 |
-
feed_intake=feed_intake,
|
43 |
-
disease_detected=disease_detected,
|
44 |
-
status=status,
|
45 |
-
treatment_recommendation=treatment_recommendation
|
46 |
-
)
|
47 |
-
return health_record.dict(by_alias=True) # Convert to dictionary for MongoDB insertion
|
48 |
-
|
49 |
-
# Generate and insert a large dataset
|
50 |
-
def populate_health_records(num_records=1000):
|
51 |
-
health_data = [generate_random_health_record() for _ in range(num_records)]
|
52 |
-
result = health_collection.insert_many(health_data)
|
53 |
-
print(f"Inserted {len(result.inserted_ids)} health records into the database.")
|
54 |
-
|
55 |
-
# Run the data population script
|
56 |
-
if __name__ == "__main__":
|
57 |
-
populate_health_records()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/populate_inventory.py
DELETED
@@ -1,52 +0,0 @@
|
|
1 |
-
# scripts/populate_inventory.py
|
2 |
-
import os
|
3 |
-
import random
|
4 |
-
from datetime import datetime, timedelta
|
5 |
-
from pymongo import MongoClient
|
6 |
-
from models.schemas.inventory_schema import InventoryItem
|
7 |
-
from bson import ObjectId
|
8 |
-
|
9 |
-
# Connect to MongoDB
|
10 |
-
client = MongoClient(os.getenv("MONGO_URI")) # Replace with your MongoDB URI
|
11 |
-
db = client["poultry_management"]
|
12 |
-
inventory_collection = db["inventory"]
|
13 |
-
|
14 |
-
# Sample data to generate inventory items
|
15 |
-
categories = ["Feed", "Medicine", "Supplies"]
|
16 |
-
suppliers = ["Farm Supplies Inc.", "Agri Products Co.", "Livestock Solutions", "Rural Provisions"]
|
17 |
-
items = {
|
18 |
-
"Feed": ["Chicken Feed", "Layer Feed", "Broiler Feed", "Starter Feed"],
|
19 |
-
"Medicine": ["Antibiotic", "Vitamin Supplement", "Dewormer", "Probiotic"],
|
20 |
-
"Supplies": ["Water Feeder", "Nesting Box", "Feeding Trough", "Heat Lamp"]
|
21 |
-
}
|
22 |
-
|
23 |
-
# Function to generate random inventory item
|
24 |
-
def generate_random_inventory_item():
|
25 |
-
category = random.choice(categories)
|
26 |
-
item_name = random.choice(items[category])
|
27 |
-
quantity = random.randint(10, 500) # Random quantity between 10 and 500
|
28 |
-
restock_level = random.randint(10, 50) # Random restock level between 10 and 50
|
29 |
-
supplier = random.choice(suppliers)
|
30 |
-
last_updated = datetime.utcnow() - timedelta(days=random.randint(1, 30)) # Random recent date
|
31 |
-
|
32 |
-
# Create the InventoryItem
|
33 |
-
inventory_item = InventoryItem(
|
34 |
-
id=ObjectId(),
|
35 |
-
item_name=item_name,
|
36 |
-
category=category,
|
37 |
-
quantity=quantity,
|
38 |
-
restock_level=restock_level,
|
39 |
-
supplier=supplier,
|
40 |
-
last_updated=last_updated
|
41 |
-
)
|
42 |
-
return inventory_item.dict(by_alias=True) # Convert to dictionary for MongoDB insertion
|
43 |
-
|
44 |
-
# Generate and insert a large dataset
|
45 |
-
def populate_inventory_data(num_items=1000):
|
46 |
-
inventory_data = [generate_random_inventory_item() for _ in range(num_items)]
|
47 |
-
result = inventory_collection.insert_many(inventory_data)
|
48 |
-
print(f"Inserted {len(result.inserted_ids)} inventory items into the database.")
|
49 |
-
|
50 |
-
# Run the data population script
|
51 |
-
if __name__ == "__main__":
|
52 |
-
populate_inventory_data()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/auth_service.py
DELETED
@@ -1,171 +0,0 @@
|
|
1 |
-
# services/auth_service.py
|
2 |
-
|
3 |
-
import os
|
4 |
-
import logging
|
5 |
-
from datetime import datetime, timedelta
|
6 |
-
from typing import Optional, Union
|
7 |
-
from fastapi.security import OAuth2PasswordBearer
|
8 |
-
from passlib.context import CryptContext
|
9 |
-
from pymongo.errors import DuplicateKeyError
|
10 |
-
from models.schemas.user_schema import UserCreate
|
11 |
-
from fastapi import Depends, HTTPException, status
|
12 |
-
from pydantic import BaseModel
|
13 |
-
from jose import JWTError, jwt
|
14 |
-
from fastapi import Cookie
|
15 |
-
from services.utils import mongo_instance, log_to_db, db # MongoDB and logging utilities
|
16 |
-
|
17 |
-
# Configure logging with a detailed format for better traceability
|
18 |
-
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
19 |
-
logger = logging.getLogger(__name__)
|
20 |
-
|
21 |
-
# Set up OAuth2 scheme and password hashing
|
22 |
-
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
|
23 |
-
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
24 |
-
|
25 |
-
# JWT settings
|
26 |
-
SECRET_KEY = os.getenv("SECRET_KEY", "your-secure-secret-key") # Set a secure key in production
|
27 |
-
ALGORITHM = "HS256"
|
28 |
-
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
29 |
-
|
30 |
-
# MongoDB collections
|
31 |
-
user_collection = mongo_instance.get_collection("users")
|
32 |
-
blacklist_collection = mongo_instance.get_collection("token_blacklist")
|
33 |
-
|
34 |
-
# Token Pydantic model for response structure
|
35 |
-
class Token(BaseModel):
|
36 |
-
access_token: str
|
37 |
-
token_type: str
|
38 |
-
|
39 |
-
def blacklist_token(token: str, expiration: datetime):
|
40 |
-
"""
|
41 |
-
Adds a token to the blacklist to prevent future use.
|
42 |
-
"""
|
43 |
-
try:
|
44 |
-
blacklist_collection.insert_one({"token": token, "expires": expiration})
|
45 |
-
logger.info(f"Token blacklisted successfully. Expires at {expiration}.")
|
46 |
-
log_to_db("INFO", f"Token blacklisted: Expires at {expiration}")
|
47 |
-
except Exception as e:
|
48 |
-
logger.error(f"Failed to blacklist token: {e}", exc_info=True)
|
49 |
-
log_to_db("ERROR", f"Failed to blacklist token: {e}")
|
50 |
-
|
51 |
-
def is_token_blacklisted(token: str) -> bool:
|
52 |
-
"""
|
53 |
-
Checks if a token is blacklisted.
|
54 |
-
"""
|
55 |
-
entry = blacklist_collection.find_one({"token": token})
|
56 |
-
if entry:
|
57 |
-
logger.warning("Attempted access with a blacklisted token.")
|
58 |
-
return entry is not None
|
59 |
-
|
60 |
-
def decode_access_token(token: str) -> Optional[str]:
|
61 |
-
"""
|
62 |
-
Decodes and validates a JWT token, ensuring it's not blacklisted.
|
63 |
-
"""
|
64 |
-
if is_token_blacklisted(token):
|
65 |
-
logger.warning("Access attempted with blacklisted token.")
|
66 |
-
return None
|
67 |
-
try:
|
68 |
-
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
69 |
-
logger.info("Token decoded successfully.")
|
70 |
-
return payload.get("sub")
|
71 |
-
except JWTError as e:
|
72 |
-
logger.error(f"Token decoding failed: {e}", exc_info=True)
|
73 |
-
return None
|
74 |
-
|
75 |
-
def hash_password(password: str) -> str:
|
76 |
-
"""
|
77 |
-
Hashes a plaintext password.
|
78 |
-
"""
|
79 |
-
logger.debug("Hashing password.")
|
80 |
-
return pwd_context.hash(password)
|
81 |
-
|
82 |
-
def create_user(user_data: UserCreate) -> dict:
|
83 |
-
"""
|
84 |
-
Registers a new user by hashing the password and storing it in MongoDB.
|
85 |
-
"""
|
86 |
-
try:
|
87 |
-
hashed_password = hash_password(user_data.password)
|
88 |
-
user_document = {
|
89 |
-
"username": user_data.username,
|
90 |
-
"email": user_data.email,
|
91 |
-
"role": user_data.role.value,
|
92 |
-
"password": hashed_password
|
93 |
-
}
|
94 |
-
user_collection.insert_one(user_document)
|
95 |
-
logger.info(f"User {user_data.username} registered successfully.")
|
96 |
-
log_to_db("INFO", f"User {user_data.username} registered successfully.")
|
97 |
-
return {"status": "success", "message": "User registered successfully!"}
|
98 |
-
except DuplicateKeyError:
|
99 |
-
logger.warning(f"Registration failed - username or email already exists: {user_data.username}.")
|
100 |
-
log_to_db("WARNING", f"Registration failed for {user_data.username}: Username or email already exists.")
|
101 |
-
return {"status": "error", "message": "Username or email already exists."}
|
102 |
-
except Exception as e:
|
103 |
-
logger.error(f"Error during user registration: {e}", exc_info=True)
|
104 |
-
log_to_db("ERROR", f"Error registering user {user_data.username}: {e}")
|
105 |
-
return {"status": "error", "message": f"Error registering user: {e}"}
|
106 |
-
|
107 |
-
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
108 |
-
"""
|
109 |
-
Verifies a plaintext password against a hashed password.
|
110 |
-
"""
|
111 |
-
return pwd_context.verify(plain_password, hashed_password)
|
112 |
-
|
113 |
-
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
114 |
-
"""
|
115 |
-
Generates a JWT token with an expiration time.
|
116 |
-
"""
|
117 |
-
to_encode = data.copy()
|
118 |
-
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
|
119 |
-
to_encode.update({"exp": expire})
|
120 |
-
token = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
121 |
-
logger.info(f"Access token created with expiration: {expire}")
|
122 |
-
return token
|
123 |
-
|
124 |
-
|
125 |
-
async def get_current_user(access_token: str = Cookie(None)):
|
126 |
-
"""
|
127 |
-
Retrieves the current user by decoding the access token.
|
128 |
-
"""
|
129 |
-
logger.info(f"Cookies received: {access_token}")
|
130 |
-
if not access_token:
|
131 |
-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token missing")
|
132 |
-
|
133 |
-
username = decode_access_token(access_token)
|
134 |
-
if not username:
|
135 |
-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
136 |
-
logger.info(f"Access granted to user: {username}")
|
137 |
-
return username
|
138 |
-
|
139 |
-
def is_admin_user(current_user: str) -> bool:
|
140 |
-
"""
|
141 |
-
Checks if the current user has an admin role.
|
142 |
-
"""
|
143 |
-
user = db.get_collection("users").find_one({"username": current_user})
|
144 |
-
is_admin = user and user.get("role") == "admin"
|
145 |
-
logger.info(f"Admin access check for user {current_user}: {'Granted' if is_admin else 'Denied'}")
|
146 |
-
return is_admin
|
147 |
-
|
148 |
-
def optional_get_current_user(token: str = Depends(oauth2_scheme)) -> Optional[str]:
|
149 |
-
"""
|
150 |
-
Optionally retrieves the current user without raising an exception if unauthorized.
|
151 |
-
"""
|
152 |
-
try:
|
153 |
-
return get_current_user(token)
|
154 |
-
except HTTPException as e:
|
155 |
-
if e.status_code == status.HTTP_401_UNAUTHORIZED:
|
156 |
-
logger.info("No authenticated user found, continuing as guest.")
|
157 |
-
return None
|
158 |
-
raise
|
159 |
-
|
160 |
-
def authenticate_user(username: str, password: str) -> Optional[dict]:
|
161 |
-
"""
|
162 |
-
Authenticates a user by checking username and password.
|
163 |
-
"""
|
164 |
-
user = user_collection.find_one({"username": username})
|
165 |
-
if user and verify_password(password, user["password"]):
|
166 |
-
logger.info(f"User {username} authenticated successfully.")
|
167 |
-
log_to_db("INFO", f"User {username} authenticated successfully.")
|
168 |
-
return user
|
169 |
-
logger.warning(f"Authentication failed for user {username}.")
|
170 |
-
log_to_db("WARNING", f"Authentication failed for user {username}.")
|
171 |
-
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/disease_detection_service.py
DELETED
@@ -1,289 +0,0 @@
|
|
1 |
-
# services/disease_detection_service.py
|
2 |
-
|
3 |
-
import os
|
4 |
-
import logging
|
5 |
-
import threading
|
6 |
-
import tensorflow as tf
|
7 |
-
import torch
|
8 |
-
import cv2
|
9 |
-
import numpy as np
|
10 |
-
from keras.models import load_model
|
11 |
-
from huggingface_hub import login
|
12 |
-
from transformers import AutoTokenizer, AutoModelForCausalLM
|
13 |
-
from services.utils import db # MongoDB instance from utils
|
14 |
-
import configparser
|
15 |
-
|
16 |
-
# Configure logging
|
17 |
-
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
18 |
-
logger = logging.getLogger(__name__)
|
19 |
-
|
20 |
-
# Load Hugging Face API token for secure model access
|
21 |
-
HF_TOKEN = os.environ.get('HF_Token')
|
22 |
-
if HF_TOKEN:
|
23 |
-
try:
|
24 |
-
# Login to Hugging Face using the token from environment variables
|
25 |
-
login(token=HF_TOKEN, add_to_git_credential=True)
|
26 |
-
logger.info("Hugging Face login successful.")
|
27 |
-
except Exception as e:
|
28 |
-
logger.error("Failed to login to Hugging Face", exc_info=True)
|
29 |
-
raise RuntimeError("Failed to login to Hugging Face. Verify API token.")
|
30 |
-
else:
|
31 |
-
logger.warning("Hugging Face token missing. Please set HF_Token in environment variables.")
|
32 |
-
|
33 |
-
# Load configuration for model paths
|
34 |
-
config = configparser.ConfigParser()
|
35 |
-
try:
|
36 |
-
config.read('config.ini')
|
37 |
-
logger.info("Configuration loaded from config.ini.")
|
38 |
-
except Exception as e:
|
39 |
-
logger.error("Failed to load configuration from config.ini", exc_info=True)
|
40 |
-
raise RuntimeError("Failed to load configuration file.")
|
41 |
-
|
42 |
-
# Configure TensorFlow for GPU and mixed precision if supported
|
43 |
-
gpu_devices = tf.config.list_physical_devices('GPU')
|
44 |
-
if gpu_devices:
|
45 |
-
logger.info(f"Number of GPUs found: {len(gpu_devices)}")
|
46 |
-
for gpu in gpu_devices:
|
47 |
-
try:
|
48 |
-
# Enable memory growth to prevent TensorFlow from allocating all GPU memory at once
|
49 |
-
tf.config.experimental.set_memory_growth(gpu, True)
|
50 |
-
logger.info(f"Enabled memory growth for GPU: {gpu.name}")
|
51 |
-
except Exception as e:
|
52 |
-
logger.error(f"Failed to set memory growth for GPU: {gpu.name}", exc_info=True)
|
53 |
-
# Enable mixed precision if GPU has sufficient compute capability
|
54 |
-
try:
|
55 |
-
if tf.config.experimental.get_device_details(gpu_devices[0]).get('compute_capability', (0, 0))[0] >= 7:
|
56 |
-
from tensorflow.keras import mixed_precision
|
57 |
-
policy = mixed_precision.Policy('mixed_float16')
|
58 |
-
mixed_precision.set_global_policy(policy)
|
59 |
-
logger.info("Mixed precision enabled on supported GPU.")
|
60 |
-
except Exception as e:
|
61 |
-
logger.error("Failed to enable mixed precision", exc_info=True)
|
62 |
-
else:
|
63 |
-
logger.info("No GPU detected, using CPU without mixed precision.")
|
64 |
-
|
65 |
-
# Check if GPU is available for PyTorch
|
66 |
-
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
67 |
-
logger.info(f"Using device for PyTorch: {device}")
|
68 |
-
|
69 |
-
# Global model variables and threading event for synchronization
|
70 |
-
disease_model, auth_model = None, None
|
71 |
-
llama_model, llama_tokenizer = None, None
|
72 |
-
model_loading_event = threading.Event()
|
73 |
-
|
74 |
-
# Utility function to load models
|
75 |
-
def load_model_util(model_path, model_name):
|
76 |
-
"""Load and compile a Keras model from the provided path."""
|
77 |
-
try:
|
78 |
-
logger.info(f"Loading {model_name} from path: {model_path}")
|
79 |
-
model = load_model(model_path, compile=True)
|
80 |
-
logger.info(f"{model_name} loaded successfully.")
|
81 |
-
return model
|
82 |
-
except Exception as e:
|
83 |
-
logger.error(f"Error loading {model_name}", exc_info=True)
|
84 |
-
raise RuntimeError(f"Failed to load {model_name}") from e
|
85 |
-
|
86 |
-
# Function to load the disease detection model
|
87 |
-
def load_disease_model():
|
88 |
-
# Load the disease detection model from the configured path
|
89 |
-
model_path = config.get('MODELS', 'DISEASE_MODEL_PATH', fallback="models/diseases.h5")
|
90 |
-
try:
|
91 |
-
return load_model_util(model_path, "Disease detection model")
|
92 |
-
except RuntimeError as e:
|
93 |
-
logger.error("Failed to load disease detection model", exc_info=True)
|
94 |
-
raise
|
95 |
-
|
96 |
-
# Function to load the authentication model
|
97 |
-
def load_auth_model():
|
98 |
-
# Load the authentication model from the configured path
|
99 |
-
auth_model_path = config.get('MODELS', 'AUTH_MODEL_PATH', fallback="models/auth.h5")
|
100 |
-
try:
|
101 |
-
return load_model_util(auth_model_path, "Authentication model")
|
102 |
-
except RuntimeError as e:
|
103 |
-
logger.error("Failed to load authentication model", exc_info=True)
|
104 |
-
raise
|
105 |
-
|
106 |
-
# Function to load the Llama 3.2 model for language generation
|
107 |
-
def load_llama_model():
|
108 |
-
"""Load the Llama model and tokenizer for language generation."""
|
109 |
-
global llama_model, llama_tokenizer
|
110 |
-
try:
|
111 |
-
logger.info("Loading Llama 3.2 model and tokenizer using PyTorch-compatible model.")
|
112 |
-
model_name = "meta-llama/Llama-3.2-1B"
|
113 |
-
|
114 |
-
# Load the tokenizer and model from Hugging Face
|
115 |
-
llama_tokenizer = AutoTokenizer.from_pretrained(model_name)
|
116 |
-
logger.info("Tokenizer loaded successfully.")
|
117 |
-
llama_model = AutoModelForCausalLM.from_pretrained(model_name).to(device)
|
118 |
-
logger.info("Model loaded successfully.")
|
119 |
-
|
120 |
-
# Add a padding token if it's missing
|
121 |
-
if llama_tokenizer.pad_token is None:
|
122 |
-
logger.info("Adding padding token to tokenizer.")
|
123 |
-
llama_tokenizer.add_special_tokens({'pad_token': '[PAD]'})
|
124 |
-
llama_model.resize_token_embeddings(len(llama_tokenizer))
|
125 |
-
|
126 |
-
logger.info("Llama model and tokenizer loaded successfully.")
|
127 |
-
model_loading_event.set() # Signal that the model has been loaded
|
128 |
-
return llama_model, llama_tokenizer
|
129 |
-
except Exception as e:
|
130 |
-
logger.error("Failed to load Llama model", exc_info=True)
|
131 |
-
model_loading_event.set() # Signal that model loading failed
|
132 |
-
raise RuntimeError("Failed to load the Llama 3.2 model. Verify compatibility.")
|
133 |
-
|
134 |
-
# Disease mappings and recommended treatments
|
135 |
-
name_disease = {0: 'Coccidiosis', 1: 'Healthy', 2: 'New Castle Disease', 3: 'Salmonella'}
|
136 |
-
status_map = {0: 'Critical', 1: 'No issue', 2: 'Critical', 3: 'Critical'}
|
137 |
-
recommendations = {
|
138 |
-
0: 'Administer anti-coccidial medication, maintain hygiene, and ensure proper litter management.',
|
139 |
-
1: 'No treatment necessary; maintain regular monitoring and hygiene.',
|
140 |
-
2: 'Isolate affected birds and seek veterinary consultation for targeted treatment.',
|
141 |
-
3: 'Administer antibiotics as prescribed by a veterinarian and ensure biosecurity.'
|
142 |
-
}
|
143 |
-
|
144 |
-
class PoultryFarmBot:
|
145 |
-
def __init__(self, disease_model, auth_model):
|
146 |
-
self.disease_model = disease_model
|
147 |
-
self.auth_model = auth_model
|
148 |
-
self.logger = logging.getLogger(__name__)
|
149 |
-
self.logger.info("PoultryFarmBot initialized with provided models.")
|
150 |
-
|
151 |
-
def preprocess_image(self, image: np.ndarray) -> np.ndarray:
|
152 |
-
"""Preprocess the input image for model prediction."""
|
153 |
-
try:
|
154 |
-
self.logger.info(f"Original image shape: {image.shape}")
|
155 |
-
# Resize the image to the required input size for the model
|
156 |
-
image = cv2.resize(image, (224, 224))
|
157 |
-
self.logger.info(f"Image resized to: {image.shape}")
|
158 |
-
# Normalize pixel values to be between 0 and 1
|
159 |
-
image = image / 255.0
|
160 |
-
self.logger.info("Image normalized for model input.")
|
161 |
-
return image
|
162 |
-
except Exception as e:
|
163 |
-
self.logger.error("Error in image preprocessing", exc_info=True)
|
164 |
-
raise ValueError("Invalid image format or empty image provided.")
|
165 |
-
|
166 |
-
def generate_detailed_response(self, disease_name: str, status: str, recommendation: str) -> str:
|
167 |
-
"""Generate detailed response using Llama model based on prediction."""
|
168 |
-
try:
|
169 |
-
# Create a prompt with detailed information about the disease
|
170 |
-
prompt = (
|
171 |
-
f"The detected disease is {disease_name}, classified as {status}. "
|
172 |
-
f"Suggested action: {recommendation}. "
|
173 |
-
f"Here is additional information on {disease_name}: causes, symptoms, and effective management."
|
174 |
-
)
|
175 |
-
self.logger.info(f"Generated prompt for Llama model: {prompt}")
|
176 |
-
# Use the Llama model to generate a more detailed response
|
177 |
-
response = self.llama_response(prompt)
|
178 |
-
self.logger.info("Generated detailed response from Llama model.")
|
179 |
-
# Remove the original prompt from the response and return only the generated text
|
180 |
-
return response.replace(prompt, "").strip()
|
181 |
-
except Exception as e:
|
182 |
-
self.logger.error("Error generating detailed response", exc_info=True)
|
183 |
-
return "Error generating detailed response."
|
184 |
-
|
185 |
-
def predict_disease(self, image: np.ndarray):
|
186 |
-
"""Predict disease from preprocessed image and provide detailed results."""
|
187 |
-
try:
|
188 |
-
# Preprocess the image for prediction
|
189 |
-
self.logger.info("Starting image preprocessing for disease prediction.")
|
190 |
-
preprocessed_image = self.preprocess_image(image)
|
191 |
-
|
192 |
-
# Use auth_model to verify the image is poultry-related
|
193 |
-
self.logger.info("Verifying if the image is poultry-related.")
|
194 |
-
is_poultry = self.auth_model.predict(preprocessed_image.reshape(1, 224, 224, 3)).argmax()
|
195 |
-
self.logger.info(f"Auth model prediction result: {is_poultry}")
|
196 |
-
if is_poultry != 0:
|
197 |
-
self.logger.info("Image not recognized as poultry.")
|
198 |
-
return {
|
199 |
-
"message": "Image not recognized as poultry.",
|
200 |
-
"disease_name": "N/A",
|
201 |
-
"status": "N/A",
|
202 |
-
"recommendation": "N/A",
|
203 |
-
"confidence": None
|
204 |
-
}
|
205 |
-
|
206 |
-
# Predict disease if image is verified as poultry
|
207 |
-
self.logger.info("Predicting disease from the image.")
|
208 |
-
prediction = self.disease_model.predict(preprocessed_image.reshape(1, 224, 224, 3))
|
209 |
-
predicted_class = prediction.argmax()
|
210 |
-
self.logger.info(f"Disease model prediction result: {predicted_class}")
|
211 |
-
disease_name = name_disease.get(predicted_class, "Unknown disease")
|
212 |
-
disease_status = status_map.get(predicted_class, "Unknown status")
|
213 |
-
recommendation = recommendations.get(predicted_class, "No recommendation available")
|
214 |
-
confidence = float(prediction[0][predicted_class] * 100)
|
215 |
-
self.logger.info(f"Disease Prediction: {disease_name} with {confidence:.2f}% confidence.")
|
216 |
-
|
217 |
-
# Generate a detailed response using Llama model
|
218 |
-
detailed_response = self.generate_detailed_response(disease_name, disease_status, recommendation)
|
219 |
-
self.logger.info("Generated detailed response for disease prediction.")
|
220 |
-
return {
|
221 |
-
"message": detailed_response,
|
222 |
-
"disease_name": disease_name,
|
223 |
-
"status": disease_status,
|
224 |
-
"recommendation": recommendation,
|
225 |
-
"confidence": confidence
|
226 |
-
}
|
227 |
-
except Exception as e:
|
228 |
-
self.logger.error("Error in disease prediction", exc_info=True)
|
229 |
-
return {
|
230 |
-
"message": "Prediction failed.",
|
231 |
-
"disease_name": None,
|
232 |
-
"status": None,
|
233 |
-
"recommendation": None,
|
234 |
-
"confidence": None
|
235 |
-
}
|
236 |
-
|
237 |
-
def llama_response(self, prompt: str) -> str:
|
238 |
-
"""Generate a response from the Llama model based on a prompt."""
|
239 |
-
try:
|
240 |
-
# Limit the maximum length of the generated response
|
241 |
-
max_length = min(len(prompt) + 50, 512)
|
242 |
-
self.logger.info(f"Generating response with max length: {max_length}")
|
243 |
-
# Tokenize the input prompt
|
244 |
-
inputs = llama_tokenizer(prompt, return_tensors="pt", max_length=max_length, truncation=True, padding=True).to(device)
|
245 |
-
self.logger.info("Input prompt tokenized successfully.")
|
246 |
-
# Generate a response using the Llama model
|
247 |
-
outputs = llama_model.generate(inputs["input_ids"], max_length=max_length, do_sample=True, temperature=0.7, top_p=0.9)
|
248 |
-
self.logger.info("Response generated by Llama model.")
|
249 |
-
# Decode the generated response into a readable string
|
250 |
-
response = llama_tokenizer.decode(outputs[0], skip_special_tokens=True)
|
251 |
-
self.logger.info("Response decoded successfully.")
|
252 |
-
return response
|
253 |
-
except Exception as e:
|
254 |
-
logger.error("Error generating response from Llama model", exc_info=True)
|
255 |
-
return "Error generating detailed response."
|
256 |
-
|
257 |
-
def diagnose_and_respond(self, image: np.ndarray):
|
258 |
-
"""Diagnose disease and provide comprehensive response."""
|
259 |
-
# Check if the provided image is valid
|
260 |
-
self.logger.info("Starting diagnosis process.")
|
261 |
-
if image is None or image.size == 0:
|
262 |
-
self.logger.warning("Invalid image provided for diagnosis.")
|
263 |
-
return {
|
264 |
-
"message": "Provide a valid poultry fecal image.",
|
265 |
-
"disease_name": None,
|
266 |
-
"status": None,
|
267 |
-
"recommendation": None,
|
268 |
-
"confidence": None
|
269 |
-
}
|
270 |
-
# Predict disease from the provided image
|
271 |
-
return self.predict_disease(image)
|
272 |
-
|
273 |
-
# Load models upon service startup
|
274 |
-
logger.info("Loading models for disease detection service.")
|
275 |
-
try:
|
276 |
-
auth_model = load_auth_model()
|
277 |
-
disease_model = load_disease_model()
|
278 |
-
llama_model, llama_tokenizer = load_llama_model()
|
279 |
-
except RuntimeError as e:
|
280 |
-
logger.error("Critical error during model loading. Service cannot start.", exc_info=True)
|
281 |
-
raise
|
282 |
-
|
283 |
-
# Initialize the bot instance with MongoDB
|
284 |
-
try:
|
285 |
-
bot = PoultryFarmBot(disease_model, auth_model)
|
286 |
-
logger.info("Bot instance initialized with models loaded.")
|
287 |
-
except Exception as e:
|
288 |
-
logger.error("Failed to initialize PoultryFarmBot", exc_info=True)
|
289 |
-
raise RuntimeError("Failed to initialize PoultryFarmBot.") from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/email_notification_service.py
DELETED
@@ -1,76 +0,0 @@
|
|
1 |
-
# services/email_notification_service.py
|
2 |
-
|
3 |
-
import os
|
4 |
-
import smtplib
|
5 |
-
import logging
|
6 |
-
from email.mime.multipart import MIMEMultipart
|
7 |
-
from email.mime.text import MIMEText
|
8 |
-
from dotenv import load_dotenv
|
9 |
-
|
10 |
-
# Load environment variables
|
11 |
-
load_dotenv()
|
12 |
-
|
13 |
-
# Set up logging for email service
|
14 |
-
logging.basicConfig(level=logging.INFO)
|
15 |
-
logger = logging.getLogger(__name__)
|
16 |
-
|
17 |
-
# Email configuration
|
18 |
-
SMTP_SERVER = os.getenv("SMTP_SERVER")
|
19 |
-
SMTP_PORT = int(os.getenv("SMTP_PORT", 587))
|
20 |
-
EMAIL_USER = os.getenv("EMAIL_USER")
|
21 |
-
EMAIL_PASSWORD = os.getenv("EMAIL_PASSWORD")
|
22 |
-
EMAIL_FROM_NAME = os.getenv("EMAIL_FROM_NAME", "Poultry Management System")
|
23 |
-
DEFAULT_RECEIVER = os.getenv("EMAIL_RECEIVER")
|
24 |
-
|
25 |
-
def send_email(subject: str, body: str, to_email: str = DEFAULT_RECEIVER):
|
26 |
-
"""
|
27 |
-
Sends an email notification.
|
28 |
-
|
29 |
-
Parameters:
|
30 |
-
subject (str): Subject of the email.
|
31 |
-
body (str): Email body content.
|
32 |
-
to_email (str): Recipient's email address.
|
33 |
-
"""
|
34 |
-
# Check SMTP configuration
|
35 |
-
if not all([SMTP_SERVER, SMTP_PORT, EMAIL_USER, EMAIL_PASSWORD, EMAIL_FROM_NAME]):
|
36 |
-
logger.error("SMTP configuration is incomplete. Check environment variables.")
|
37 |
-
return
|
38 |
-
|
39 |
-
try:
|
40 |
-
# Set up the email headers
|
41 |
-
msg = MIMEMultipart()
|
42 |
-
msg["From"] = f"{EMAIL_FROM_NAME} <{EMAIL_USER}>"
|
43 |
-
msg["To"] = to_email
|
44 |
-
msg["Subject"] = subject
|
45 |
-
|
46 |
-
# Attach the email body
|
47 |
-
msg.attach(MIMEText(body, "html"))
|
48 |
-
|
49 |
-
# Establish connection to the SMTP server and send the email
|
50 |
-
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
|
51 |
-
server.starttls()
|
52 |
-
server.login(EMAIL_USER, EMAIL_PASSWORD)
|
53 |
-
server.sendmail(EMAIL_USER, to_email, msg.as_string())
|
54 |
-
logger.info(f"Email sent to {to_email} with subject: '{subject}'")
|
55 |
-
except Exception as e:
|
56 |
-
logger.error(f"Failed to send email to {to_email}: {e}")
|
57 |
-
|
58 |
-
def generate_notification_template(title: str, message: str) -> str:
|
59 |
-
"""
|
60 |
-
Generates an HTML email template for notifications.
|
61 |
-
|
62 |
-
Parameters:
|
63 |
-
title (str): Title of the notification.
|
64 |
-
message (str): Message content.
|
65 |
-
|
66 |
-
Returns:
|
67 |
-
str: HTML formatted email body.
|
68 |
-
"""
|
69 |
-
return f"""
|
70 |
-
<html>
|
71 |
-
<body>
|
72 |
-
<h2>{title}</h2>
|
73 |
-
<p>{message}</p>
|
74 |
-
</body>
|
75 |
-
</html>
|
76 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/health_alerts_service.py
DELETED
@@ -1,42 +0,0 @@
|
|
1 |
-
# services/health_alerts_service.py
|
2 |
-
|
3 |
-
import os
|
4 |
-
from services.utils import db
|
5 |
-
from datetime import datetime
|
6 |
-
|
7 |
-
health_collection = db.get_collection("health_records")
|
8 |
-
alert_collection = db.get_collection("alerts")
|
9 |
-
|
10 |
-
# Define thresholds for automated alerts
|
11 |
-
THRESHOLDS = {
|
12 |
-
"weight": 1.0, # Example: Weight below 1.0 kg triggers alert
|
13 |
-
"mortality_rate": 2.0, # Mortality rate over 2% triggers alert
|
14 |
-
"feed_intake": 0.3 # Feed intake below 0.3 kg triggers alert
|
15 |
-
}
|
16 |
-
|
17 |
-
def check_health_thresholds(record):
|
18 |
-
alerts = []
|
19 |
-
if record["weight"] < THRESHOLDS["weight"]:
|
20 |
-
alerts.append("Low weight detected")
|
21 |
-
|
22 |
-
if record["mortality_rate"] > THRESHOLDS["mortality_rate"]:
|
23 |
-
alerts.append("High mortality rate detected")
|
24 |
-
|
25 |
-
if record["feed_intake"] < THRESHOLDS["feed_intake"]:
|
26 |
-
alerts.append("Reduced feed intake detected")
|
27 |
-
|
28 |
-
if alerts:
|
29 |
-
alert_record = {
|
30 |
-
"bird_id": record["bird_id"],
|
31 |
-
"date": datetime.utcnow(),
|
32 |
-
"alerts": alerts,
|
33 |
-
"status": record["status"],
|
34 |
-
"treatment_recommendation": record["treatment_recommendation"]
|
35 |
-
}
|
36 |
-
alert_collection.insert_one(alert_record)
|
37 |
-
# Send alert notification (e.g., email or SMS)
|
38 |
-
send_alert_notification(alert_record)
|
39 |
-
|
40 |
-
def send_alert_notification(alert):
|
41 |
-
farmer_email = os.getenv("FARMER_EMAIL")
|
42 |
-
# Logic to send notification via email or SMS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/health_monitoring_service.py
DELETED
@@ -1,91 +0,0 @@
|
|
1 |
-
# services/health_monitoring_service.py
|
2 |
-
|
3 |
-
from datetime import datetime, timedelta
|
4 |
-
from typing import Dict, List
|
5 |
-
from services.email_notification_service import send_email
|
6 |
-
|
7 |
-
# Dictionary to map diseases to suggested treatments
|
8 |
-
TREATMENT_GUIDELINES = {
|
9 |
-
"Healthy": {
|
10 |
-
"notification": "The flock is healthy.",
|
11 |
-
"suggestion": "No treatment necessary; maintain regular monitoring and hygiene."
|
12 |
-
},
|
13 |
-
"Coccidiosis": {
|
14 |
-
"notification": "Coccidiosis symptoms detected.",
|
15 |
-
"suggestion": "Administer anti-coccidial medication, maintain hygiene, and ensure proper litter management."
|
16 |
-
},
|
17 |
-
"New Castle Disease": {
|
18 |
-
"notification": "New Castle Disease suspected.",
|
19 |
-
"suggestion": "Isolate affected birds and seek veterinary consultation for targeted treatment."
|
20 |
-
},
|
21 |
-
"Salmonella": {
|
22 |
-
"notification": "Salmonella infection detected.",
|
23 |
-
"suggestion": "Administer antibiotics as prescribed by a veterinarian and ensure biosecurity."
|
24 |
-
}
|
25 |
-
}
|
26 |
-
|
27 |
-
# Notification thresholds for health metrics
|
28 |
-
HEALTH_THRESHOLDS = {
|
29 |
-
"weight_loss_percentage": 5, # Alert if weight loss > 5% in a week
|
30 |
-
"mortality_rate": 2, # Alert if mortality rate > 2% in a week
|
31 |
-
"reduced_feed_intake_percentage": 10 # Alert if feed intake drops > 10%
|
32 |
-
}
|
33 |
-
|
34 |
-
def get_health_alerts(health_metrics: dict) -> List[str]:
|
35 |
-
"""
|
36 |
-
Check health metrics and return alerts if thresholds are crossed.
|
37 |
-
|
38 |
-
Parameters:
|
39 |
-
health_metrics (dict): Health metrics like weight loss, mortality rate, and feed intake.
|
40 |
-
|
41 |
-
Returns:
|
42 |
-
list: Notifications and suggestions if any health issues are detected.
|
43 |
-
"""
|
44 |
-
alerts = []
|
45 |
-
if health_metrics.get("weight_loss_percentage", 0) > HEALTH_THRESHOLDS["weight_loss_percentage"]:
|
46 |
-
alerts.append("Alert: Significant weight loss detected. Check feed quality and health.")
|
47 |
-
|
48 |
-
if health_metrics.get("mortality_rate", 0) > HEALTH_THRESHOLDS["mortality_rate"]:
|
49 |
-
alerts.append("Alert: Increased mortality rate observed. Review conditions and consult a vet.")
|
50 |
-
|
51 |
-
if health_metrics.get("reduced_feed_intake_percentage", 0) > HEALTH_THRESHOLDS["reduced_feed_intake_percentage"]:
|
52 |
-
alerts.append("Alert: Feed intake has dropped. Check for signs of illness or environmental issues.")
|
53 |
-
|
54 |
-
return alerts
|
55 |
-
|
56 |
-
def send_alerts(alerts: List[str], farmer_email: str):
|
57 |
-
"""Send alert notifications to the farmer's email."""
|
58 |
-
if alerts:
|
59 |
-
message = "\n".join(alerts)
|
60 |
-
subject = "Poultry Health Alert"
|
61 |
-
send_email(farmer_email, subject, message)
|
62 |
-
|
63 |
-
def get_treatment_recommendation(disease: str) -> Dict[str, str]:
|
64 |
-
"""Get notification and treatment suggestion based on disease detected."""
|
65 |
-
return TREATMENT_GUIDELINES.get(disease, {
|
66 |
-
"notification": "Unknown disease detected.",
|
67 |
-
"suggestion": "Consult a veterinarian for diagnosis and treatment."
|
68 |
-
})
|
69 |
-
|
70 |
-
def evaluate_health_data(health_metrics: dict) -> Dict[str, List[str]]:
|
71 |
-
"""
|
72 |
-
Evaluate health data and trigger alerts if thresholds are crossed.
|
73 |
-
|
74 |
-
Parameters:
|
75 |
-
health_metrics (dict): Health metrics like weight loss, mortality rate, and feed intake.
|
76 |
-
|
77 |
-
Returns:
|
78 |
-
dict: Notifications if any health issues are detected.
|
79 |
-
"""
|
80 |
-
notifications = []
|
81 |
-
|
82 |
-
if health_metrics.get("weight_loss_percentage", 0) > HEALTH_THRESHOLDS["weight_loss_percentage"]:
|
83 |
-
notifications.append("Significant weight loss detected. Monitor feed quality and check for underlying issues.")
|
84 |
-
|
85 |
-
if health_metrics.get("mortality_rate", 0) > HEALTH_THRESHOLDS["mortality_rate"]:
|
86 |
-
notifications.append("Increased mortality rate. Review flock conditions and investigate potential health issues.")
|
87 |
-
|
88 |
-
if health_metrics.get("reduced_feed_intake_percentage", 0) > HEALTH_THRESHOLDS["reduced_feed_intake_percentage"]:
|
89 |
-
notifications.append("Feed intake has dropped. Check for signs of illness or environmental stress.")
|
90 |
-
|
91 |
-
return {"notifications": notifications}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/image_preprocessing.py
DELETED
@@ -1,43 +0,0 @@
|
|
1 |
-
# services/image_preprocessing.py
|
2 |
-
|
3 |
-
from PIL import Image
|
4 |
-
import numpy as np
|
5 |
-
import logging
|
6 |
-
|
7 |
-
# Setup logging
|
8 |
-
logging.basicConfig(level=logging.INFO)
|
9 |
-
logger = logging.getLogger(__name__)
|
10 |
-
|
11 |
-
|
12 |
-
def preprocess_image(image: Image.Image, target_size=(224, 224)):
|
13 |
-
"""
|
14 |
-
Preprocesses the uploaded image for disease detection.
|
15 |
-
|
16 |
-
Parameters:
|
17 |
-
image (PIL.Image.Image): Input image to preprocess.
|
18 |
-
target_size (tuple): Target size for resizing the image.
|
19 |
-
|
20 |
-
Returns:
|
21 |
-
np.ndarray: Preprocessed image ready for model input.
|
22 |
-
"""
|
23 |
-
try:
|
24 |
-
# Ensure the image has 3 color channels (RGB)
|
25 |
-
image = image.convert("RGB")
|
26 |
-
logger.info("Image converted to RGB.")
|
27 |
-
|
28 |
-
# Resize image to the target size
|
29 |
-
image = image.resize(target_size)
|
30 |
-
logger.info(f"Image resized to {target_size}.")
|
31 |
-
|
32 |
-
# Normalize the image pixels to [0, 1] range
|
33 |
-
image_array = np.array(image) / 255.0
|
34 |
-
logger.info("Image normalized to [0, 1] range.")
|
35 |
-
|
36 |
-
# Add a batch dimension to the image array
|
37 |
-
image_array = np.expand_dims(image_array, axis=0)
|
38 |
-
logger.info("Batch dimension added to image array.")
|
39 |
-
|
40 |
-
return image_array
|
41 |
-
except Exception as e:
|
42 |
-
logger.error(f"Error preprocessing image: {e}")
|
43 |
-
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/utils.py
DELETED
@@ -1,204 +0,0 @@
|
|
1 |
-
# services/utils.py
|
2 |
-
|
3 |
-
import os
|
4 |
-
import logging
|
5 |
-
import time
|
6 |
-
from datetime import datetime
|
7 |
-
from typing import Optional
|
8 |
-
from dotenv import load_dotenv
|
9 |
-
from pymongo import MongoClient, errors
|
10 |
-
import smtplib
|
11 |
-
from email.mime.text import MIMEText
|
12 |
-
from email.mime.multipart import MIMEMultipart
|
13 |
-
|
14 |
-
# Load environment variables
|
15 |
-
load_dotenv()
|
16 |
-
|
17 |
-
# Configure logging
|
18 |
-
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
19 |
-
logger = logging.getLogger(__name__)
|
20 |
-
|
21 |
-
# MongoDB connection setup
|
22 |
-
MONGO_URI = os.getenv("MONGO_URI")
|
23 |
-
if not MONGO_URI:
|
24 |
-
logger.error("MONGO_URI environment variable is not set.")
|
25 |
-
raise ValueError("MONGO_URI environment variable is required.")
|
26 |
-
|
27 |
-
# MongoDB connection retry settings
|
28 |
-
MAX_RETRIES = 3
|
29 |
-
RETRY_DELAY = 5 # in seconds
|
30 |
-
|
31 |
-
# Email configuration
|
32 |
-
SMTP_SERVER = os.getenv("SMTP_SERVER")
|
33 |
-
SMTP_PORT = int(os.getenv("SMTP_PORT", 587))
|
34 |
-
EMAIL_USER = os.getenv("EMAIL_USER")
|
35 |
-
EMAIL_PASSWORD = os.getenv("EMAIL_PASSWORD")
|
36 |
-
ADMIN_EMAIL = os.getenv("ADMIN_EMAIL")
|
37 |
-
|
38 |
-
|
39 |
-
def connect_to_mongo(uri: str, retries: int, delay: int) -> MongoClient:
|
40 |
-
"""
|
41 |
-
Establishes a MongoDB connection with retry logic.
|
42 |
-
"""
|
43 |
-
for attempt in range(retries):
|
44 |
-
try:
|
45 |
-
logger.info(f"Attempting MongoDB connection (Attempt {attempt + 1}/{retries}).")
|
46 |
-
client = MongoClient(uri, serverSelectionTimeoutMS=5000)
|
47 |
-
client.server_info() # Check connection
|
48 |
-
logger.info("MongoDB connection established successfully.")
|
49 |
-
return client
|
50 |
-
except errors.ServerSelectionTimeoutError as e:
|
51 |
-
logger.error(f"MongoDB connection attempt {attempt + 1} failed: {e}")
|
52 |
-
if attempt < retries - 1:
|
53 |
-
time.sleep(delay)
|
54 |
-
else:
|
55 |
-
logger.critical("Failed to connect to MongoDB after multiple attempts.")
|
56 |
-
raise ConnectionError("Could not connect to MongoDB. Verify MONGO_URI and database availability.")
|
57 |
-
|
58 |
-
|
59 |
-
# Initialize MongoDB client and database
|
60 |
-
client = connect_to_mongo(MONGO_URI, MAX_RETRIES, RETRY_DELAY)
|
61 |
-
db = client.poultry_management # Default database
|
62 |
-
|
63 |
-
# MongoDB Collections
|
64 |
-
logs_collection = db.logs
|
65 |
-
activity_logs_collection = db.get_collection("activity_logs")
|
66 |
-
|
67 |
-
|
68 |
-
# Logging activity to MongoDB
|
69 |
-
def log_to_db(level: str, message: str):
|
70 |
-
"""
|
71 |
-
Logs a message to MongoDB.
|
72 |
-
"""
|
73 |
-
log_entry = {"level": level, "message": message, "timestamp": datetime.utcnow()}
|
74 |
-
try:
|
75 |
-
logs_collection.insert_one(log_entry)
|
76 |
-
logger.debug(f"Log entry added to MongoDB: {log_entry}")
|
77 |
-
except Exception as e:
|
78 |
-
logger.error(f"Failed to log to MongoDB: {e}")
|
79 |
-
|
80 |
-
|
81 |
-
# Custom logging handler for MongoDB
|
82 |
-
class MongoHandler(logging.Handler):
|
83 |
-
def emit(self, record):
|
84 |
-
log_to_db(record.levelname, self.format(record))
|
85 |
-
|
86 |
-
|
87 |
-
mongo_handler = MongoHandler()
|
88 |
-
mongo_handler.setLevel(logging.INFO)
|
89 |
-
mongo_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
|
90 |
-
logger.addHandler(mongo_handler)
|
91 |
-
|
92 |
-
|
93 |
-
# MongoDB wrapper class
|
94 |
-
class MongoDB:
|
95 |
-
def __init__(self, uri: str, db_name: str):
|
96 |
-
self.client = MongoClient(uri, serverSelectionTimeoutMS=5000)
|
97 |
-
self.db = self.client[db_name]
|
98 |
-
logger.info(f"Connected to MongoDB database: {db_name}")
|
99 |
-
|
100 |
-
def test_connection(self) -> str:
|
101 |
-
"""
|
102 |
-
Tests MongoDB connection.
|
103 |
-
"""
|
104 |
-
try:
|
105 |
-
test_doc = {"status": "connection_test"}
|
106 |
-
self.db.test_collection.insert_one(test_doc)
|
107 |
-
self.db.test_collection.delete_many({"status": "connection_test"})
|
108 |
-
logger.info("MongoDB connection verified successfully.")
|
109 |
-
return "Connection successful!"
|
110 |
-
except (errors.ConnectionFailure, errors.OperationFailure) as e:
|
111 |
-
logger.error(f"Database connection error: {e}")
|
112 |
-
return f"Database connection error: {e}"
|
113 |
-
|
114 |
-
def get_collection(self, collection_name: str):
|
115 |
-
"""
|
116 |
-
Retrieves a specified MongoDB collection.
|
117 |
-
"""
|
118 |
-
return self.db[collection_name]
|
119 |
-
|
120 |
-
def insert_log(self, level: str, message: str):
|
121 |
-
"""
|
122 |
-
Adds a log entry to the MongoDB logs collection.
|
123 |
-
"""
|
124 |
-
log_entry = {"level": level, "message": message, "timestamp": datetime.utcnow()}
|
125 |
-
try:
|
126 |
-
self.db.logs.insert_one(log_entry)
|
127 |
-
logger.debug(f"Database log entry added: {log_entry}")
|
128 |
-
except Exception as e:
|
129 |
-
logger.error(f"Failed to insert log into database: {e}")
|
130 |
-
|
131 |
-
|
132 |
-
# Create a MongoDB instance for centralized database access
|
133 |
-
mongo_instance = MongoDB(uri=MONGO_URI, db_name="poultry_management")
|
134 |
-
|
135 |
-
|
136 |
-
# Email utility function for sending alerts
|
137 |
-
def send_email(recipient: str, subject: str, body: str):
|
138 |
-
"""
|
139 |
-
Sends an email alert.
|
140 |
-
"""
|
141 |
-
if not SMTP_SERVER or not EMAIL_USER or not EMAIL_PASSWORD:
|
142 |
-
logger.error("SMTP configuration is incomplete.")
|
143 |
-
raise EnvironmentError("SMTP configuration is required.")
|
144 |
-
|
145 |
-
msg = MIMEMultipart()
|
146 |
-
msg["From"] = EMAIL_USER
|
147 |
-
msg["To"] = recipient
|
148 |
-
msg["Subject"] = subject
|
149 |
-
msg.attach(MIMEText(body, "plain"))
|
150 |
-
|
151 |
-
try:
|
152 |
-
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
|
153 |
-
server.starttls()
|
154 |
-
server.login(EMAIL_USER, EMAIL_PASSWORD)
|
155 |
-
server.sendmail(EMAIL_USER, recipient, msg.as_string())
|
156 |
-
logger.info(f"Email sent to {recipient}.")
|
157 |
-
except Exception as e:
|
158 |
-
logger.error(f"Failed to send email: {e}")
|
159 |
-
|
160 |
-
|
161 |
-
# Logging activity for user actions or system events
|
162 |
-
def log_activity(activity_type: str, description: str, user_id: Optional[str] = None):
|
163 |
-
"""
|
164 |
-
Logs a user or system activity in MongoDB.
|
165 |
-
"""
|
166 |
-
log_entry = {
|
167 |
-
"activity_type": activity_type,
|
168 |
-
"description": description,
|
169 |
-
"user_id": user_id,
|
170 |
-
"timestamp": datetime.utcnow()
|
171 |
-
}
|
172 |
-
try:
|
173 |
-
activity_logs_collection.insert_one(log_entry)
|
174 |
-
logger.info(f"Activity logged: {activity_type} - {description}")
|
175 |
-
except Exception as e:
|
176 |
-
logger.error(f"Failed to log activity: {e}")
|
177 |
-
|
178 |
-
|
179 |
-
# Helper function for testing the database connection
|
180 |
-
def test_db_connection():
|
181 |
-
"""
|
182 |
-
Tests MongoDB connection and logs the result.
|
183 |
-
"""
|
184 |
-
result = mongo_instance.test_connection()
|
185 |
-
logger.info(result)
|
186 |
-
return result
|
187 |
-
|
188 |
-
|
189 |
-
# Inventory stock alert
|
190 |
-
def check_inventory_levels():
|
191 |
-
"""
|
192 |
-
Checks inventory levels and sends alerts for items below the restock level.
|
193 |
-
"""
|
194 |
-
inventory_collection = db.get_collection("inventory")
|
195 |
-
low_stock_items = inventory_collection.find({"$expr": {"$lt": ["$quantity", "$restock_level"]}})
|
196 |
-
|
197 |
-
for item in low_stock_items:
|
198 |
-
alert_message = (
|
199 |
-
f"Inventory Alert: '{item['item_name']}' is below the restock level. "
|
200 |
-
f"Current stock: {item['quantity']}, Restock level: {item['restock_level']}."
|
201 |
-
)
|
202 |
-
send_email(ADMIN_EMAIL, "Low Stock Alert", alert_message)
|
203 |
-
logger.info(f"Low stock alert sent for '{item['item_name']}'.")
|
204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/css/adminlte.css
DELETED
The diff for this file is too large to render.
See raw diff
|
|
static/css/adminlte.css.map
DELETED
The diff for this file is too large to render.
See raw diff
|
|
static/css/adminlte.min.css
DELETED
The diff for this file is too large to render.
See raw diff
|
|
static/css/adminlte.min.css.map
DELETED
The diff for this file is too large to render.
See raw diff
|
|
static/css/adminlte.rtl.css
DELETED
The diff for this file is too large to render.
See raw diff
|
|
static/css/adminlte.rtl.css.map
DELETED
The diff for this file is too large to render.
See raw diff
|
|
static/css/adminlte.rtl.min.css
DELETED
The diff for this file is too large to render.
See raw diff
|
|
static/css/adminlte.rtl.min.css.map
DELETED
The diff for this file is too large to render.
See raw diff
|
|
static/images/landing-bg.jpg
DELETED
Binary file (694 kB)
|
|
static/images/logo.png
DELETED
Binary file (854 kB)
|
|
static/js/adminlte.js
DELETED
@@ -1,715 +0,0 @@
|
|
1 |
-
/*!
|
2 |
-
* AdminLTE v4.0.0-beta2 (https://adminlte.io)
|
3 |
-
* Copyright 2014-2024 Colorlib <https://colorlib.com>
|
4 |
-
* Licensed under MIT (https://github.com/ColorlibHQ/AdminLTE/blob/master/LICENSE)
|
5 |
-
*/
|
6 |
-
(function (global, factory) {
|
7 |
-
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
8 |
-
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
9 |
-
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.adminlte = {}));
|
10 |
-
})(this, (function (exports) { 'use strict';
|
11 |
-
|
12 |
-
const domContentLoadedCallbacks = [];
|
13 |
-
const onDOMContentLoaded = (callback) => {
|
14 |
-
if (document.readyState === 'loading') {
|
15 |
-
// add listener on the first call when the document is in loading state
|
16 |
-
if (!domContentLoadedCallbacks.length) {
|
17 |
-
document.addEventListener('DOMContentLoaded', () => {
|
18 |
-
for (const callback of domContentLoadedCallbacks) {
|
19 |
-
callback();
|
20 |
-
}
|
21 |
-
});
|
22 |
-
}
|
23 |
-
domContentLoadedCallbacks.push(callback);
|
24 |
-
}
|
25 |
-
else {
|
26 |
-
callback();
|
27 |
-
}
|
28 |
-
};
|
29 |
-
/* SLIDE UP */
|
30 |
-
const slideUp = (target, duration = 500) => {
|
31 |
-
target.style.transitionProperty = 'height, margin, padding';
|
32 |
-
target.style.transitionDuration = `${duration}ms`;
|
33 |
-
target.style.boxSizing = 'border-box';
|
34 |
-
target.style.height = `${target.offsetHeight}px`;
|
35 |
-
target.style.overflow = 'hidden';
|
36 |
-
window.setTimeout(() => {
|
37 |
-
target.style.height = '0';
|
38 |
-
target.style.paddingTop = '0';
|
39 |
-
target.style.paddingBottom = '0';
|
40 |
-
target.style.marginTop = '0';
|
41 |
-
target.style.marginBottom = '0';
|
42 |
-
}, 1);
|
43 |
-
window.setTimeout(() => {
|
44 |
-
target.style.display = 'none';
|
45 |
-
target.style.removeProperty('height');
|
46 |
-
target.style.removeProperty('padding-top');
|
47 |
-
target.style.removeProperty('padding-bottom');
|
48 |
-
target.style.removeProperty('margin-top');
|
49 |
-
target.style.removeProperty('margin-bottom');
|
50 |
-
target.style.removeProperty('overflow');
|
51 |
-
target.style.removeProperty('transition-duration');
|
52 |
-
target.style.removeProperty('transition-property');
|
53 |
-
}, duration);
|
54 |
-
};
|
55 |
-
/* SLIDE DOWN */
|
56 |
-
const slideDown = (target, duration = 500) => {
|
57 |
-
target.style.removeProperty('display');
|
58 |
-
let { display } = window.getComputedStyle(target);
|
59 |
-
if (display === 'none') {
|
60 |
-
display = 'block';
|
61 |
-
}
|
62 |
-
target.style.display = display;
|
63 |
-
const height = target.offsetHeight;
|
64 |
-
target.style.overflow = 'hidden';
|
65 |
-
target.style.height = '0';
|
66 |
-
target.style.paddingTop = '0';
|
67 |
-
target.style.paddingBottom = '0';
|
68 |
-
target.style.marginTop = '0';
|
69 |
-
target.style.marginBottom = '0';
|
70 |
-
window.setTimeout(() => {
|
71 |
-
target.style.boxSizing = 'border-box';
|
72 |
-
target.style.transitionProperty = 'height, margin, padding';
|
73 |
-
target.style.transitionDuration = `${duration}ms`;
|
74 |
-
target.style.height = `${height}px`;
|
75 |
-
target.style.removeProperty('padding-top');
|
76 |
-
target.style.removeProperty('padding-bottom');
|
77 |
-
target.style.removeProperty('margin-top');
|
78 |
-
target.style.removeProperty('margin-bottom');
|
79 |
-
}, 1);
|
80 |
-
window.setTimeout(() => {
|
81 |
-
target.style.removeProperty('height');
|
82 |
-
target.style.removeProperty('overflow');
|
83 |
-
target.style.removeProperty('transition-duration');
|
84 |
-
target.style.removeProperty('transition-property');
|
85 |
-
}, duration);
|
86 |
-
};
|
87 |
-
|
88 |
-
/**
|
89 |
-
* --------------------------------------------
|
90 |
-
* @file AdminLTE layout.ts
|
91 |
-
* @description Layout for AdminLTE.
|
92 |
-
* @license MIT
|
93 |
-
* --------------------------------------------
|
94 |
-
*/
|
95 |
-
/**
|
96 |
-
* ------------------------------------------------------------------------
|
97 |
-
* Constants
|
98 |
-
* ------------------------------------------------------------------------
|
99 |
-
*/
|
100 |
-
const CLASS_NAME_HOLD_TRANSITIONS = 'hold-transition';
|
101 |
-
const CLASS_NAME_APP_LOADED = 'app-loaded';
|
102 |
-
/**
|
103 |
-
* Class Definition
|
104 |
-
* ====================================================
|
105 |
-
*/
|
106 |
-
class Layout {
|
107 |
-
constructor(element) {
|
108 |
-
this._element = element;
|
109 |
-
}
|
110 |
-
holdTransition() {
|
111 |
-
let resizeTimer;
|
112 |
-
window.addEventListener('resize', () => {
|
113 |
-
document.body.classList.add(CLASS_NAME_HOLD_TRANSITIONS);
|
114 |
-
clearTimeout(resizeTimer);
|
115 |
-
resizeTimer = setTimeout(() => {
|
116 |
-
document.body.classList.remove(CLASS_NAME_HOLD_TRANSITIONS);
|
117 |
-
}, 400);
|
118 |
-
});
|
119 |
-
}
|
120 |
-
}
|
121 |
-
onDOMContentLoaded(() => {
|
122 |
-
const data = new Layout(document.body);
|
123 |
-
data.holdTransition();
|
124 |
-
setTimeout(() => {
|
125 |
-
document.body.classList.add(CLASS_NAME_APP_LOADED);
|
126 |
-
}, 400);
|
127 |
-
});
|
128 |
-
|
129 |
-
/**
|
130 |
-
* --------------------------------------------
|
131 |
-
* @file AdminLTE push-menu.ts
|
132 |
-
* @description Push menu for AdminLTE.
|
133 |
-
* @license MIT
|
134 |
-
* --------------------------------------------
|
135 |
-
*/
|
136 |
-
/**
|
137 |
-
* ------------------------------------------------------------------------
|
138 |
-
* Constants
|
139 |
-
* ------------------------------------------------------------------------
|
140 |
-
*/
|
141 |
-
const DATA_KEY$4 = 'lte.push-menu';
|
142 |
-
const EVENT_KEY$4 = `.${DATA_KEY$4}`;
|
143 |
-
const EVENT_OPEN = `open${EVENT_KEY$4}`;
|
144 |
-
const EVENT_COLLAPSE = `collapse${EVENT_KEY$4}`;
|
145 |
-
const CLASS_NAME_SIDEBAR_MINI = 'sidebar-mini';
|
146 |
-
const CLASS_NAME_SIDEBAR_COLLAPSE = 'sidebar-collapse';
|
147 |
-
const CLASS_NAME_SIDEBAR_OPEN = 'sidebar-open';
|
148 |
-
const CLASS_NAME_SIDEBAR_EXPAND = 'sidebar-expand';
|
149 |
-
const CLASS_NAME_SIDEBAR_OVERLAY = 'sidebar-overlay';
|
150 |
-
const CLASS_NAME_MENU_OPEN$1 = 'menu-open';
|
151 |
-
const SELECTOR_APP_SIDEBAR = '.app-sidebar';
|
152 |
-
const SELECTOR_SIDEBAR_MENU = '.sidebar-menu';
|
153 |
-
const SELECTOR_NAV_ITEM$1 = '.nav-item';
|
154 |
-
const SELECTOR_NAV_TREEVIEW = '.nav-treeview';
|
155 |
-
const SELECTOR_APP_WRAPPER = '.app-wrapper';
|
156 |
-
const SELECTOR_SIDEBAR_EXPAND = `[class*="${CLASS_NAME_SIDEBAR_EXPAND}"]`;
|
157 |
-
const SELECTOR_SIDEBAR_TOGGLE = '[data-lte-toggle="sidebar"]';
|
158 |
-
const Defaults = {
|
159 |
-
sidebarBreakpoint: 992
|
160 |
-
};
|
161 |
-
/**
|
162 |
-
* Class Definition
|
163 |
-
* ====================================================
|
164 |
-
*/
|
165 |
-
class PushMenu {
|
166 |
-
constructor(element, config) {
|
167 |
-
this._element = element;
|
168 |
-
this._config = Object.assign(Object.assign({}, Defaults), config);
|
169 |
-
}
|
170 |
-
// TODO
|
171 |
-
menusClose() {
|
172 |
-
const navTreeview = document.querySelectorAll(SELECTOR_NAV_TREEVIEW);
|
173 |
-
navTreeview.forEach(navTree => {
|
174 |
-
navTree.style.removeProperty('display');
|
175 |
-
navTree.style.removeProperty('height');
|
176 |
-
});
|
177 |
-
const navSidebar = document.querySelector(SELECTOR_SIDEBAR_MENU);
|
178 |
-
const navItem = navSidebar === null || navSidebar === void 0 ? void 0 : navSidebar.querySelectorAll(SELECTOR_NAV_ITEM$1);
|
179 |
-
if (navItem) {
|
180 |
-
navItem.forEach(navI => {
|
181 |
-
navI.classList.remove(CLASS_NAME_MENU_OPEN$1);
|
182 |
-
});
|
183 |
-
}
|
184 |
-
}
|
185 |
-
expand() {
|
186 |
-
const event = new Event(EVENT_OPEN);
|
187 |
-
document.body.classList.remove(CLASS_NAME_SIDEBAR_COLLAPSE);
|
188 |
-
document.body.classList.add(CLASS_NAME_SIDEBAR_OPEN);
|
189 |
-
this._element.dispatchEvent(event);
|
190 |
-
}
|
191 |
-
collapse() {
|
192 |
-
const event = new Event(EVENT_COLLAPSE);
|
193 |
-
document.body.classList.remove(CLASS_NAME_SIDEBAR_OPEN);
|
194 |
-
document.body.classList.add(CLASS_NAME_SIDEBAR_COLLAPSE);
|
195 |
-
this._element.dispatchEvent(event);
|
196 |
-
}
|
197 |
-
addSidebarBreakPoint() {
|
198 |
-
var _a, _b, _c;
|
199 |
-
const sidebarExpandList = (_b = (_a = document.querySelector(SELECTOR_SIDEBAR_EXPAND)) === null || _a === void 0 ? void 0 : _a.classList) !== null && _b !== void 0 ? _b : [];
|
200 |
-
const sidebarExpand = (_c = Array.from(sidebarExpandList).find(className => className.startsWith(CLASS_NAME_SIDEBAR_EXPAND))) !== null && _c !== void 0 ? _c : '';
|
201 |
-
const sidebar = document.getElementsByClassName(sidebarExpand)[0];
|
202 |
-
const sidebarContent = window.getComputedStyle(sidebar, '::before').getPropertyValue('content');
|
203 |
-
this._config = Object.assign(Object.assign({}, this._config), { sidebarBreakpoint: Number(sidebarContent.replace(/[^\d.-]/g, '')) });
|
204 |
-
if (window.innerWidth <= this._config.sidebarBreakpoint) {
|
205 |
-
this.collapse();
|
206 |
-
}
|
207 |
-
else {
|
208 |
-
if (!document.body.classList.contains(CLASS_NAME_SIDEBAR_MINI)) {
|
209 |
-
this.expand();
|
210 |
-
}
|
211 |
-
if (document.body.classList.contains(CLASS_NAME_SIDEBAR_MINI) && document.body.classList.contains(CLASS_NAME_SIDEBAR_COLLAPSE)) {
|
212 |
-
this.collapse();
|
213 |
-
}
|
214 |
-
}
|
215 |
-
}
|
216 |
-
toggle() {
|
217 |
-
if (document.body.classList.contains(CLASS_NAME_SIDEBAR_COLLAPSE)) {
|
218 |
-
this.expand();
|
219 |
-
}
|
220 |
-
else {
|
221 |
-
this.collapse();
|
222 |
-
}
|
223 |
-
}
|
224 |
-
init() {
|
225 |
-
this.addSidebarBreakPoint();
|
226 |
-
}
|
227 |
-
}
|
228 |
-
/**
|
229 |
-
* ------------------------------------------------------------------------
|
230 |
-
* Data Api implementation
|
231 |
-
* ------------------------------------------------------------------------
|
232 |
-
*/
|
233 |
-
onDOMContentLoaded(() => {
|
234 |
-
var _a;
|
235 |
-
const sidebar = document === null || document === void 0 ? void 0 : document.querySelector(SELECTOR_APP_SIDEBAR);
|
236 |
-
if (sidebar) {
|
237 |
-
const data = new PushMenu(sidebar, Defaults);
|
238 |
-
data.init();
|
239 |
-
window.addEventListener('resize', () => {
|
240 |
-
data.init();
|
241 |
-
});
|
242 |
-
}
|
243 |
-
const sidebarOverlay = document.createElement('div');
|
244 |
-
sidebarOverlay.className = CLASS_NAME_SIDEBAR_OVERLAY;
|
245 |
-
(_a = document.querySelector(SELECTOR_APP_WRAPPER)) === null || _a === void 0 ? void 0 : _a.append(sidebarOverlay);
|
246 |
-
sidebarOverlay.addEventListener('touchstart', event => {
|
247 |
-
event.preventDefault();
|
248 |
-
const target = event.currentTarget;
|
249 |
-
const data = new PushMenu(target, Defaults);
|
250 |
-
data.collapse();
|
251 |
-
}, { passive: true });
|
252 |
-
sidebarOverlay.addEventListener('click', event => {
|
253 |
-
event.preventDefault();
|
254 |
-
const target = event.currentTarget;
|
255 |
-
const data = new PushMenu(target, Defaults);
|
256 |
-
data.collapse();
|
257 |
-
});
|
258 |
-
const fullBtn = document.querySelectorAll(SELECTOR_SIDEBAR_TOGGLE);
|
259 |
-
fullBtn.forEach(btn => {
|
260 |
-
btn.addEventListener('click', event => {
|
261 |
-
event.preventDefault();
|
262 |
-
let button = event.currentTarget;
|
263 |
-
if ((button === null || button === void 0 ? void 0 : button.dataset.lteToggle) !== 'sidebar') {
|
264 |
-
button = button === null || button === void 0 ? void 0 : button.closest(SELECTOR_SIDEBAR_TOGGLE);
|
265 |
-
}
|
266 |
-
if (button) {
|
267 |
-
event === null || event === void 0 ? void 0 : event.preventDefault();
|
268 |
-
const data = new PushMenu(button, Defaults);
|
269 |
-
data.toggle();
|
270 |
-
}
|
271 |
-
});
|
272 |
-
});
|
273 |
-
});
|
274 |
-
|
275 |
-
/**
|
276 |
-
* --------------------------------------------
|
277 |
-
* @file AdminLTE treeview.ts
|
278 |
-
* @description Treeview plugin for AdminLTE.
|
279 |
-
* @license MIT
|
280 |
-
* --------------------------------------------
|
281 |
-
*/
|
282 |
-
/**
|
283 |
-
* ------------------------------------------------------------------------
|
284 |
-
* Constants
|
285 |
-
* ------------------------------------------------------------------------
|
286 |
-
*/
|
287 |
-
// const NAME = 'Treeview'
|
288 |
-
const DATA_KEY$3 = 'lte.treeview';
|
289 |
-
const EVENT_KEY$3 = `.${DATA_KEY$3}`;
|
290 |
-
const EVENT_EXPANDED$2 = `expanded${EVENT_KEY$3}`;
|
291 |
-
const EVENT_COLLAPSED$2 = `collapsed${EVENT_KEY$3}`;
|
292 |
-
// const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`
|
293 |
-
const CLASS_NAME_MENU_OPEN = 'menu-open';
|
294 |
-
const SELECTOR_NAV_ITEM = '.nav-item';
|
295 |
-
const SELECTOR_NAV_LINK = '.nav-link';
|
296 |
-
const SELECTOR_TREEVIEW_MENU = '.nav-treeview';
|
297 |
-
const SELECTOR_DATA_TOGGLE$1 = '[data-lte-toggle="treeview"]';
|
298 |
-
const Default$1 = {
|
299 |
-
animationSpeed: 300,
|
300 |
-
accordion: true
|
301 |
-
};
|
302 |
-
/**
|
303 |
-
* Class Definition
|
304 |
-
* ====================================================
|
305 |
-
*/
|
306 |
-
class Treeview {
|
307 |
-
constructor(element, config) {
|
308 |
-
this._element = element;
|
309 |
-
this._config = Object.assign(Object.assign({}, Default$1), config);
|
310 |
-
}
|
311 |
-
open() {
|
312 |
-
var _a, _b;
|
313 |
-
const event = new Event(EVENT_EXPANDED$2);
|
314 |
-
if (this._config.accordion) {
|
315 |
-
const openMenuList = (_a = this._element.parentElement) === null || _a === void 0 ? void 0 : _a.querySelectorAll(`${SELECTOR_NAV_ITEM}.${CLASS_NAME_MENU_OPEN}`);
|
316 |
-
openMenuList === null || openMenuList === void 0 ? void 0 : openMenuList.forEach(openMenu => {
|
317 |
-
if (openMenu !== this._element.parentElement) {
|
318 |
-
openMenu.classList.remove(CLASS_NAME_MENU_OPEN);
|
319 |
-
const childElement = openMenu === null || openMenu === void 0 ? void 0 : openMenu.querySelector(SELECTOR_TREEVIEW_MENU);
|
320 |
-
if (childElement) {
|
321 |
-
slideUp(childElement, this._config.animationSpeed);
|
322 |
-
}
|
323 |
-
}
|
324 |
-
});
|
325 |
-
}
|
326 |
-
this._element.classList.add(CLASS_NAME_MENU_OPEN);
|
327 |
-
const childElement = (_b = this._element) === null || _b === void 0 ? void 0 : _b.querySelector(SELECTOR_TREEVIEW_MENU);
|
328 |
-
if (childElement) {
|
329 |
-
slideDown(childElement, this._config.animationSpeed);
|
330 |
-
}
|
331 |
-
this._element.dispatchEvent(event);
|
332 |
-
}
|
333 |
-
close() {
|
334 |
-
var _a;
|
335 |
-
const event = new Event(EVENT_COLLAPSED$2);
|
336 |
-
this._element.classList.remove(CLASS_NAME_MENU_OPEN);
|
337 |
-
const childElement = (_a = this._element) === null || _a === void 0 ? void 0 : _a.querySelector(SELECTOR_TREEVIEW_MENU);
|
338 |
-
if (childElement) {
|
339 |
-
slideUp(childElement, this._config.animationSpeed);
|
340 |
-
}
|
341 |
-
this._element.dispatchEvent(event);
|
342 |
-
}
|
343 |
-
toggle() {
|
344 |
-
if (this._element.classList.contains(CLASS_NAME_MENU_OPEN)) {
|
345 |
-
this.close();
|
346 |
-
}
|
347 |
-
else {
|
348 |
-
this.open();
|
349 |
-
}
|
350 |
-
}
|
351 |
-
}
|
352 |
-
/**
|
353 |
-
* ------------------------------------------------------------------------
|
354 |
-
* Data Api implementation
|
355 |
-
* ------------------------------------------------------------------------
|
356 |
-
*/
|
357 |
-
onDOMContentLoaded(() => {
|
358 |
-
const button = document.querySelectorAll(SELECTOR_DATA_TOGGLE$1);
|
359 |
-
button.forEach(btn => {
|
360 |
-
btn.addEventListener('click', event => {
|
361 |
-
const target = event.target;
|
362 |
-
const targetItem = target.closest(SELECTOR_NAV_ITEM);
|
363 |
-
const targetLink = target.closest(SELECTOR_NAV_LINK);
|
364 |
-
if ((target === null || target === void 0 ? void 0 : target.getAttribute('href')) === '#' || (targetLink === null || targetLink === void 0 ? void 0 : targetLink.getAttribute('href')) === '#') {
|
365 |
-
event.preventDefault();
|
366 |
-
}
|
367 |
-
if (targetItem) {
|
368 |
-
const data = new Treeview(targetItem, Default$1);
|
369 |
-
data.toggle();
|
370 |
-
}
|
371 |
-
});
|
372 |
-
});
|
373 |
-
});
|
374 |
-
|
375 |
-
/**
|
376 |
-
* --------------------------------------------
|
377 |
-
* @file AdminLTE direct-chat.ts
|
378 |
-
* @description Direct chat for AdminLTE.
|
379 |
-
* @license MIT
|
380 |
-
* --------------------------------------------
|
381 |
-
*/
|
382 |
-
/**
|
383 |
-
* Constants
|
384 |
-
* ====================================================
|
385 |
-
*/
|
386 |
-
const DATA_KEY$2 = 'lte.direct-chat';
|
387 |
-
const EVENT_KEY$2 = `.${DATA_KEY$2}`;
|
388 |
-
const EVENT_EXPANDED$1 = `expanded${EVENT_KEY$2}`;
|
389 |
-
const EVENT_COLLAPSED$1 = `collapsed${EVENT_KEY$2}`;
|
390 |
-
const SELECTOR_DATA_TOGGLE = '[data-lte-toggle="chat-pane"]';
|
391 |
-
const SELECTOR_DIRECT_CHAT = '.direct-chat';
|
392 |
-
const CLASS_NAME_DIRECT_CHAT_OPEN = 'direct-chat-contacts-open';
|
393 |
-
/**
|
394 |
-
* Class Definition
|
395 |
-
* ====================================================
|
396 |
-
*/
|
397 |
-
class DirectChat {
|
398 |
-
constructor(element) {
|
399 |
-
this._element = element;
|
400 |
-
}
|
401 |
-
toggle() {
|
402 |
-
if (this._element.classList.contains(CLASS_NAME_DIRECT_CHAT_OPEN)) {
|
403 |
-
const event = new Event(EVENT_COLLAPSED$1);
|
404 |
-
this._element.classList.remove(CLASS_NAME_DIRECT_CHAT_OPEN);
|
405 |
-
this._element.dispatchEvent(event);
|
406 |
-
}
|
407 |
-
else {
|
408 |
-
const event = new Event(EVENT_EXPANDED$1);
|
409 |
-
this._element.classList.add(CLASS_NAME_DIRECT_CHAT_OPEN);
|
410 |
-
this._element.dispatchEvent(event);
|
411 |
-
}
|
412 |
-
}
|
413 |
-
}
|
414 |
-
/**
|
415 |
-
*
|
416 |
-
* Data Api implementation
|
417 |
-
* ====================================================
|
418 |
-
*/
|
419 |
-
onDOMContentLoaded(() => {
|
420 |
-
const button = document.querySelectorAll(SELECTOR_DATA_TOGGLE);
|
421 |
-
button.forEach(btn => {
|
422 |
-
btn.addEventListener('click', event => {
|
423 |
-
event.preventDefault();
|
424 |
-
const target = event.target;
|
425 |
-
const chatPane = target.closest(SELECTOR_DIRECT_CHAT);
|
426 |
-
if (chatPane) {
|
427 |
-
const data = new DirectChat(chatPane);
|
428 |
-
data.toggle();
|
429 |
-
}
|
430 |
-
});
|
431 |
-
});
|
432 |
-
});
|
433 |
-
|
434 |
-
/**
|
435 |
-
* --------------------------------------------
|
436 |
-
* @file AdminLTE card-widget.ts
|
437 |
-
* @description Card widget for AdminLTE.
|
438 |
-
* @license MIT
|
439 |
-
* --------------------------------------------
|
440 |
-
*/
|
441 |
-
/**
|
442 |
-
* Constants
|
443 |
-
* ====================================================
|
444 |
-
*/
|
445 |
-
const DATA_KEY$1 = 'lte.card-widget';
|
446 |
-
const EVENT_KEY$1 = `.${DATA_KEY$1}`;
|
447 |
-
const EVENT_COLLAPSED = `collapsed${EVENT_KEY$1}`;
|
448 |
-
const EVENT_EXPANDED = `expanded${EVENT_KEY$1}`;
|
449 |
-
const EVENT_REMOVE = `remove${EVENT_KEY$1}`;
|
450 |
-
const EVENT_MAXIMIZED$1 = `maximized${EVENT_KEY$1}`;
|
451 |
-
const EVENT_MINIMIZED$1 = `minimized${EVENT_KEY$1}`;
|
452 |
-
const CLASS_NAME_CARD = 'card';
|
453 |
-
const CLASS_NAME_COLLAPSED = 'collapsed-card';
|
454 |
-
const CLASS_NAME_COLLAPSING = 'collapsing-card';
|
455 |
-
const CLASS_NAME_EXPANDING = 'expanding-card';
|
456 |
-
const CLASS_NAME_WAS_COLLAPSED = 'was-collapsed';
|
457 |
-
const CLASS_NAME_MAXIMIZED = 'maximized-card';
|
458 |
-
const SELECTOR_DATA_REMOVE = '[data-lte-toggle="card-remove"]';
|
459 |
-
const SELECTOR_DATA_COLLAPSE = '[data-lte-toggle="card-collapse"]';
|
460 |
-
const SELECTOR_DATA_MAXIMIZE = '[data-lte-toggle="card-maximize"]';
|
461 |
-
const SELECTOR_CARD = `.${CLASS_NAME_CARD}`;
|
462 |
-
const SELECTOR_CARD_BODY = '.card-body';
|
463 |
-
const SELECTOR_CARD_FOOTER = '.card-footer';
|
464 |
-
const Default = {
|
465 |
-
animationSpeed: 500,
|
466 |
-
collapseTrigger: SELECTOR_DATA_COLLAPSE,
|
467 |
-
removeTrigger: SELECTOR_DATA_REMOVE,
|
468 |
-
maximizeTrigger: SELECTOR_DATA_MAXIMIZE
|
469 |
-
};
|
470 |
-
class CardWidget {
|
471 |
-
constructor(element, config) {
|
472 |
-
this._element = element;
|
473 |
-
this._parent = element.closest(SELECTOR_CARD);
|
474 |
-
if (element.classList.contains(CLASS_NAME_CARD)) {
|
475 |
-
this._parent = element;
|
476 |
-
}
|
477 |
-
this._config = Object.assign(Object.assign({}, Default), config);
|
478 |
-
}
|
479 |
-
collapse() {
|
480 |
-
var _a, _b;
|
481 |
-
const event = new Event(EVENT_COLLAPSED);
|
482 |
-
if (this._parent) {
|
483 |
-
this._parent.classList.add(CLASS_NAME_COLLAPSING);
|
484 |
-
const elm = (_a = this._parent) === null || _a === void 0 ? void 0 : _a.querySelectorAll(`${SELECTOR_CARD_BODY}, ${SELECTOR_CARD_FOOTER}`);
|
485 |
-
elm.forEach(el => {
|
486 |
-
if (el instanceof HTMLElement) {
|
487 |
-
slideUp(el, this._config.animationSpeed);
|
488 |
-
}
|
489 |
-
});
|
490 |
-
setTimeout(() => {
|
491 |
-
if (this._parent) {
|
492 |
-
this._parent.classList.add(CLASS_NAME_COLLAPSED);
|
493 |
-
this._parent.classList.remove(CLASS_NAME_COLLAPSING);
|
494 |
-
}
|
495 |
-
}, this._config.animationSpeed);
|
496 |
-
}
|
497 |
-
(_b = this._element) === null || _b === void 0 ? void 0 : _b.dispatchEvent(event);
|
498 |
-
}
|
499 |
-
expand() {
|
500 |
-
var _a, _b;
|
501 |
-
const event = new Event(EVENT_EXPANDED);
|
502 |
-
if (this._parent) {
|
503 |
-
this._parent.classList.add(CLASS_NAME_EXPANDING);
|
504 |
-
const elm = (_a = this._parent) === null || _a === void 0 ? void 0 : _a.querySelectorAll(`${SELECTOR_CARD_BODY}, ${SELECTOR_CARD_FOOTER}`);
|
505 |
-
elm.forEach(el => {
|
506 |
-
if (el instanceof HTMLElement) {
|
507 |
-
slideDown(el, this._config.animationSpeed);
|
508 |
-
}
|
509 |
-
});
|
510 |
-
setTimeout(() => {
|
511 |
-
if (this._parent) {
|
512 |
-
this._parent.classList.remove(CLASS_NAME_COLLAPSED);
|
513 |
-
this._parent.classList.remove(CLASS_NAME_EXPANDING);
|
514 |
-
}
|
515 |
-
}, this._config.animationSpeed);
|
516 |
-
}
|
517 |
-
(_b = this._element) === null || _b === void 0 ? void 0 : _b.dispatchEvent(event);
|
518 |
-
}
|
519 |
-
remove() {
|
520 |
-
var _a;
|
521 |
-
const event = new Event(EVENT_REMOVE);
|
522 |
-
if (this._parent) {
|
523 |
-
slideUp(this._parent, this._config.animationSpeed);
|
524 |
-
}
|
525 |
-
(_a = this._element) === null || _a === void 0 ? void 0 : _a.dispatchEvent(event);
|
526 |
-
}
|
527 |
-
toggle() {
|
528 |
-
var _a;
|
529 |
-
if ((_a = this._parent) === null || _a === void 0 ? void 0 : _a.classList.contains(CLASS_NAME_COLLAPSED)) {
|
530 |
-
this.expand();
|
531 |
-
return;
|
532 |
-
}
|
533 |
-
this.collapse();
|
534 |
-
}
|
535 |
-
maximize() {
|
536 |
-
var _a;
|
537 |
-
const event = new Event(EVENT_MAXIMIZED$1);
|
538 |
-
if (this._parent) {
|
539 |
-
this._parent.style.height = `${this._parent.offsetHeight}px`;
|
540 |
-
this._parent.style.width = `${this._parent.offsetWidth}px`;
|
541 |
-
this._parent.style.transition = 'all .15s';
|
542 |
-
setTimeout(() => {
|
543 |
-
const htmlTag = document.querySelector('html');
|
544 |
-
if (htmlTag) {
|
545 |
-
htmlTag.classList.add(CLASS_NAME_MAXIMIZED);
|
546 |
-
}
|
547 |
-
if (this._parent) {
|
548 |
-
this._parent.classList.add(CLASS_NAME_MAXIMIZED);
|
549 |
-
if (this._parent.classList.contains(CLASS_NAME_COLLAPSED)) {
|
550 |
-
this._parent.classList.add(CLASS_NAME_WAS_COLLAPSED);
|
551 |
-
}
|
552 |
-
}
|
553 |
-
}, 150);
|
554 |
-
}
|
555 |
-
(_a = this._element) === null || _a === void 0 ? void 0 : _a.dispatchEvent(event);
|
556 |
-
}
|
557 |
-
minimize() {
|
558 |
-
var _a;
|
559 |
-
const event = new Event(EVENT_MINIMIZED$1);
|
560 |
-
if (this._parent) {
|
561 |
-
this._parent.style.height = 'auto';
|
562 |
-
this._parent.style.width = 'auto';
|
563 |
-
this._parent.style.transition = 'all .15s';
|
564 |
-
setTimeout(() => {
|
565 |
-
var _a;
|
566 |
-
const htmlTag = document.querySelector('html');
|
567 |
-
if (htmlTag) {
|
568 |
-
htmlTag.classList.remove(CLASS_NAME_MAXIMIZED);
|
569 |
-
}
|
570 |
-
if (this._parent) {
|
571 |
-
this._parent.classList.remove(CLASS_NAME_MAXIMIZED);
|
572 |
-
if ((_a = this._parent) === null || _a === void 0 ? void 0 : _a.classList.contains(CLASS_NAME_WAS_COLLAPSED)) {
|
573 |
-
this._parent.classList.remove(CLASS_NAME_WAS_COLLAPSED);
|
574 |
-
}
|
575 |
-
}
|
576 |
-
}, 10);
|
577 |
-
}
|
578 |
-
(_a = this._element) === null || _a === void 0 ? void 0 : _a.dispatchEvent(event);
|
579 |
-
}
|
580 |
-
toggleMaximize() {
|
581 |
-
var _a;
|
582 |
-
if ((_a = this._parent) === null || _a === void 0 ? void 0 : _a.classList.contains(CLASS_NAME_MAXIMIZED)) {
|
583 |
-
this.minimize();
|
584 |
-
return;
|
585 |
-
}
|
586 |
-
this.maximize();
|
587 |
-
}
|
588 |
-
}
|
589 |
-
/**
|
590 |
-
*
|
591 |
-
* Data Api implementation
|
592 |
-
* ====================================================
|
593 |
-
*/
|
594 |
-
onDOMContentLoaded(() => {
|
595 |
-
const collapseBtn = document.querySelectorAll(SELECTOR_DATA_COLLAPSE);
|
596 |
-
collapseBtn.forEach(btn => {
|
597 |
-
btn.addEventListener('click', event => {
|
598 |
-
event.preventDefault();
|
599 |
-
const target = event.target;
|
600 |
-
const data = new CardWidget(target, Default);
|
601 |
-
data.toggle();
|
602 |
-
});
|
603 |
-
});
|
604 |
-
const removeBtn = document.querySelectorAll(SELECTOR_DATA_REMOVE);
|
605 |
-
removeBtn.forEach(btn => {
|
606 |
-
btn.addEventListener('click', event => {
|
607 |
-
event.preventDefault();
|
608 |
-
const target = event.target;
|
609 |
-
const data = new CardWidget(target, Default);
|
610 |
-
data.remove();
|
611 |
-
});
|
612 |
-
});
|
613 |
-
const maxBtn = document.querySelectorAll(SELECTOR_DATA_MAXIMIZE);
|
614 |
-
maxBtn.forEach(btn => {
|
615 |
-
btn.addEventListener('click', event => {
|
616 |
-
event.preventDefault();
|
617 |
-
const target = event.target;
|
618 |
-
const data = new CardWidget(target, Default);
|
619 |
-
data.toggleMaximize();
|
620 |
-
});
|
621 |
-
});
|
622 |
-
});
|
623 |
-
|
624 |
-
/**
|
625 |
-
* --------------------------------------------
|
626 |
-
* @file AdminLTE fullscreen.ts
|
627 |
-
* @description Fullscreen plugin for AdminLTE.
|
628 |
-
* @license MIT
|
629 |
-
* --------------------------------------------
|
630 |
-
*/
|
631 |
-
/**
|
632 |
-
* Constants
|
633 |
-
* ============================================================================
|
634 |
-
*/
|
635 |
-
const DATA_KEY = 'lte.fullscreen';
|
636 |
-
const EVENT_KEY = `.${DATA_KEY}`;
|
637 |
-
const EVENT_MAXIMIZED = `maximized${EVENT_KEY}`;
|
638 |
-
const EVENT_MINIMIZED = `minimized${EVENT_KEY}`;
|
639 |
-
const SELECTOR_FULLSCREEN_TOGGLE = '[data-lte-toggle="fullscreen"]';
|
640 |
-
const SELECTOR_MAXIMIZE_ICON = '[data-lte-icon="maximize"]';
|
641 |
-
const SELECTOR_MINIMIZE_ICON = '[data-lte-icon="minimize"]';
|
642 |
-
/**
|
643 |
-
* Class Definition.
|
644 |
-
* ============================================================================
|
645 |
-
*/
|
646 |
-
class FullScreen {
|
647 |
-
constructor(element, config) {
|
648 |
-
this._element = element;
|
649 |
-
this._config = config;
|
650 |
-
}
|
651 |
-
inFullScreen() {
|
652 |
-
const event = new Event(EVENT_MAXIMIZED);
|
653 |
-
const iconMaximize = document.querySelector(SELECTOR_MAXIMIZE_ICON);
|
654 |
-
const iconMinimize = document.querySelector(SELECTOR_MINIMIZE_ICON);
|
655 |
-
void document.documentElement.requestFullscreen();
|
656 |
-
if (iconMaximize) {
|
657 |
-
iconMaximize.style.display = 'none';
|
658 |
-
}
|
659 |
-
if (iconMinimize) {
|
660 |
-
iconMinimize.style.display = 'block';
|
661 |
-
}
|
662 |
-
this._element.dispatchEvent(event);
|
663 |
-
}
|
664 |
-
outFullscreen() {
|
665 |
-
const event = new Event(EVENT_MINIMIZED);
|
666 |
-
const iconMaximize = document.querySelector(SELECTOR_MAXIMIZE_ICON);
|
667 |
-
const iconMinimize = document.querySelector(SELECTOR_MINIMIZE_ICON);
|
668 |
-
void document.exitFullscreen();
|
669 |
-
if (iconMaximize) {
|
670 |
-
iconMaximize.style.display = 'block';
|
671 |
-
}
|
672 |
-
if (iconMinimize) {
|
673 |
-
iconMinimize.style.display = 'none';
|
674 |
-
}
|
675 |
-
this._element.dispatchEvent(event);
|
676 |
-
}
|
677 |
-
toggleFullScreen() {
|
678 |
-
if (document.fullscreenEnabled) {
|
679 |
-
if (document.fullscreenElement) {
|
680 |
-
this.outFullscreen();
|
681 |
-
}
|
682 |
-
else {
|
683 |
-
this.inFullScreen();
|
684 |
-
}
|
685 |
-
}
|
686 |
-
}
|
687 |
-
}
|
688 |
-
/**
|
689 |
-
* Data Api implementation
|
690 |
-
* ============================================================================
|
691 |
-
*/
|
692 |
-
onDOMContentLoaded(() => {
|
693 |
-
const buttons = document.querySelectorAll(SELECTOR_FULLSCREEN_TOGGLE);
|
694 |
-
buttons.forEach(btn => {
|
695 |
-
btn.addEventListener('click', event => {
|
696 |
-
event.preventDefault();
|
697 |
-
const target = event.target;
|
698 |
-
const button = target.closest(SELECTOR_FULLSCREEN_TOGGLE);
|
699 |
-
if (button) {
|
700 |
-
const data = new FullScreen(button, undefined);
|
701 |
-
data.toggleFullScreen();
|
702 |
-
}
|
703 |
-
});
|
704 |
-
});
|
705 |
-
});
|
706 |
-
|
707 |
-
exports.CardWidget = CardWidget;
|
708 |
-
exports.DirectChat = DirectChat;
|
709 |
-
exports.FullScreen = FullScreen;
|
710 |
-
exports.Layout = Layout;
|
711 |
-
exports.PushMenu = PushMenu;
|
712 |
-
exports.Treeview = Treeview;
|
713 |
-
|
714 |
-
}));
|
715 |
-
//# sourceMappingURL=adminlte.js.map
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/js/adminlte.js.map
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
{"version":3,"file":"adminlte.js","sources":["../../src/ts/util/index.ts","../../src/ts/layout.ts","../../src/ts/push-menu.ts","../../src/ts/treeview.ts","../../src/ts/direct-chat.ts","../../src/ts/card-widget.ts","../../src/ts/fullscreen.ts"],"sourcesContent":["const domContentLoadedCallbacks: Array<() => void> = []\n\nconst onDOMContentLoaded = (callback: () => void): void => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!domContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of domContentLoadedCallbacks) {\n callback()\n }\n })\n }\n\n domContentLoadedCallbacks.push(callback)\n } else {\n callback()\n }\n}\n\n/* SLIDE UP */\nconst slideUp = (target: HTMLElement, duration = 500) => {\n target.style.transitionProperty = 'height, margin, padding'\n target.style.transitionDuration = `${duration}ms`\n target.style.boxSizing = 'border-box'\n target.style.height = `${target.offsetHeight}px`\n target.style.overflow = 'hidden'\n\n window.setTimeout(() => {\n target.style.height = '0'\n target.style.paddingTop = '0'\n target.style.paddingBottom = '0'\n target.style.marginTop = '0'\n target.style.marginBottom = '0'\n }, 1)\n\n window.setTimeout(() => {\n target.style.display = 'none'\n target.style.removeProperty('height')\n target.style.removeProperty('padding-top')\n target.style.removeProperty('padding-bottom')\n target.style.removeProperty('margin-top')\n target.style.removeProperty('margin-bottom')\n target.style.removeProperty('overflow')\n target.style.removeProperty('transition-duration')\n target.style.removeProperty('transition-property')\n }, duration)\n}\n\n/* SLIDE DOWN */\nconst slideDown = (target: HTMLElement, duration = 500) => {\n target.style.removeProperty('display')\n let { display } = window.getComputedStyle(target)\n\n if (display === 'none') {\n display = 'block'\n }\n\n target.style.display = display\n const height = target.offsetHeight\n target.style.overflow = 'hidden'\n target.style.height = '0'\n target.style.paddingTop = '0'\n target.style.paddingBottom = '0'\n target.style.marginTop = '0'\n target.style.marginBottom = '0'\n\n window.setTimeout(() => {\n target.style.boxSizing = 'border-box'\n target.style.transitionProperty = 'height, margin, padding'\n target.style.transitionDuration = `${duration}ms`\n target.style.height = `${height}px`\n target.style.removeProperty('padding-top')\n target.style.removeProperty('padding-bottom')\n target.style.removeProperty('margin-top')\n target.style.removeProperty('margin-bottom')\n }, 1)\n\n window.setTimeout(() => {\n target.style.removeProperty('height')\n target.style.removeProperty('overflow')\n target.style.removeProperty('transition-duration')\n target.style.removeProperty('transition-property')\n }, duration)\n}\n\n/* TOGGLE */\nconst slideToggle = (target: HTMLElement, duration = 500) => {\n if (window.getComputedStyle(target).display === 'none') {\n slideDown(target, duration)\n return\n }\n\n slideUp(target, duration)\n}\n\nexport {\n onDOMContentLoaded,\n slideUp,\n slideDown,\n slideToggle\n}\n","/**\n * --------------------------------------------\n * @file AdminLTE layout.ts\n * @description Layout for AdminLTE.\n * @license MIT\n * --------------------------------------------\n */\n\nimport {\n onDOMContentLoaded\n} from './util/index'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst CLASS_NAME_HOLD_TRANSITIONS = 'hold-transition'\nconst CLASS_NAME_APP_LOADED = 'app-loaded'\n\n/**\n * Class Definition\n * ====================================================\n */\n\nclass Layout {\n _element: HTMLElement\n\n constructor(element: HTMLElement) {\n this._element = element\n }\n\n holdTransition(): void {\n let resizeTimer: ReturnType<typeof setTimeout>\n window.addEventListener('resize', () => {\n document.body.classList.add(CLASS_NAME_HOLD_TRANSITIONS)\n clearTimeout(resizeTimer)\n resizeTimer = setTimeout(() => {\n document.body.classList.remove(CLASS_NAME_HOLD_TRANSITIONS)\n }, 400)\n })\n }\n}\n\nonDOMContentLoaded(() => {\n const data = new Layout(document.body)\n data.holdTransition()\n setTimeout(() => {\n document.body.classList.add(CLASS_NAME_APP_LOADED)\n }, 400)\n})\n\nexport default Layout\n","/**\n * --------------------------------------------\n * @file AdminLTE push-menu.ts\n * @description Push menu for AdminLTE.\n * @license MIT\n * --------------------------------------------\n */\n\nimport {\n onDOMContentLoaded\n} from './util/index'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst DATA_KEY = 'lte.push-menu'\nconst EVENT_KEY = `.${DATA_KEY}`\n\nconst EVENT_OPEN = `open${EVENT_KEY}`\nconst EVENT_COLLAPSE = `collapse${EVENT_KEY}`\n\nconst CLASS_NAME_SIDEBAR_MINI = 'sidebar-mini'\nconst CLASS_NAME_SIDEBAR_COLLAPSE = 'sidebar-collapse'\nconst CLASS_NAME_SIDEBAR_OPEN = 'sidebar-open'\nconst CLASS_NAME_SIDEBAR_EXPAND = 'sidebar-expand'\nconst CLASS_NAME_SIDEBAR_OVERLAY = 'sidebar-overlay'\nconst CLASS_NAME_MENU_OPEN = 'menu-open'\n\nconst SELECTOR_APP_SIDEBAR = '.app-sidebar'\nconst SELECTOR_SIDEBAR_MENU = '.sidebar-menu'\nconst SELECTOR_NAV_ITEM = '.nav-item'\nconst SELECTOR_NAV_TREEVIEW = '.nav-treeview'\nconst SELECTOR_APP_WRAPPER = '.app-wrapper'\nconst SELECTOR_SIDEBAR_EXPAND = `[class*=\"${CLASS_NAME_SIDEBAR_EXPAND}\"]`\nconst SELECTOR_SIDEBAR_TOGGLE = '[data-lte-toggle=\"sidebar\"]'\n\ntype Config = {\n sidebarBreakpoint: number;\n}\n\nconst Defaults = {\n sidebarBreakpoint: 992\n}\n\n/**\n * Class Definition\n * ====================================================\n */\n\nclass PushMenu {\n _element: HTMLElement\n _config: Config\n\n constructor(element: HTMLElement, config: Config) {\n this._element = element\n this._config = { ...Defaults, ...config }\n }\n\n // TODO\n menusClose() {\n const navTreeview = document.querySelectorAll<HTMLElement>(SELECTOR_NAV_TREEVIEW)\n\n navTreeview.forEach(navTree => {\n navTree.style.removeProperty('display')\n navTree.style.removeProperty('height')\n })\n\n const navSidebar = document.querySelector(SELECTOR_SIDEBAR_MENU)\n const navItem = navSidebar?.querySelectorAll(SELECTOR_NAV_ITEM)\n\n if (navItem) {\n navItem.forEach(navI => {\n navI.classList.remove(CLASS_NAME_MENU_OPEN)\n })\n }\n }\n\n expand() {\n const event = new Event(EVENT_OPEN)\n\n document.body.classList.remove(CLASS_NAME_SIDEBAR_COLLAPSE)\n document.body.classList.add(CLASS_NAME_SIDEBAR_OPEN)\n\n this._element.dispatchEvent(event)\n }\n\n collapse() {\n const event = new Event(EVENT_COLLAPSE)\n\n document.body.classList.remove(CLASS_NAME_SIDEBAR_OPEN)\n document.body.classList.add(CLASS_NAME_SIDEBAR_COLLAPSE)\n\n this._element.dispatchEvent(event)\n }\n\n addSidebarBreakPoint() {\n const sidebarExpandList = document.querySelector(SELECTOR_SIDEBAR_EXPAND)?.classList ?? []\n const sidebarExpand = Array.from(sidebarExpandList).find(className => className.startsWith(CLASS_NAME_SIDEBAR_EXPAND)) ?? ''\n const sidebar = document.getElementsByClassName(sidebarExpand)[0]\n const sidebarContent = window.getComputedStyle(sidebar, '::before').getPropertyValue('content')\n this._config = { ...this._config, sidebarBreakpoint: Number(sidebarContent.replace(/[^\\d.-]/g, '')) }\n\n if (window.innerWidth <= this._config.sidebarBreakpoint) {\n this.collapse()\n } else {\n if (!document.body.classList.contains(CLASS_NAME_SIDEBAR_MINI)) {\n this.expand()\n }\n\n if (document.body.classList.contains(CLASS_NAME_SIDEBAR_MINI) && document.body.classList.contains(CLASS_NAME_SIDEBAR_COLLAPSE)) {\n this.collapse()\n }\n }\n }\n\n toggle() {\n if (document.body.classList.contains(CLASS_NAME_SIDEBAR_COLLAPSE)) {\n this.expand()\n } else {\n this.collapse()\n }\n }\n\n init() {\n this.addSidebarBreakPoint()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\nonDOMContentLoaded(() => {\n const sidebar = document?.querySelector(SELECTOR_APP_SIDEBAR) as HTMLElement | undefined\n\n if (sidebar) {\n const data = new PushMenu(sidebar, Defaults)\n data.init()\n\n window.addEventListener('resize', () => {\n data.init()\n })\n }\n\n const sidebarOverlay = document.createElement('div')\n sidebarOverlay.className = CLASS_NAME_SIDEBAR_OVERLAY\n document.querySelector(SELECTOR_APP_WRAPPER)?.append(sidebarOverlay)\n\n sidebarOverlay.addEventListener('touchstart', event => {\n event.preventDefault()\n const target = event.currentTarget as HTMLElement\n const data = new PushMenu(target, Defaults)\n data.collapse()\n }, { passive: true })\n sidebarOverlay.addEventListener('click', event => {\n event.preventDefault()\n const target = event.currentTarget as HTMLElement\n const data = new PushMenu(target, Defaults)\n data.collapse()\n })\n\n const fullBtn = document.querySelectorAll(SELECTOR_SIDEBAR_TOGGLE)\n\n fullBtn.forEach(btn => {\n btn.addEventListener('click', event => {\n event.preventDefault()\n\n let button = event.currentTarget as HTMLElement | undefined\n\n if (button?.dataset.lteToggle !== 'sidebar') {\n button = button?.closest(SELECTOR_SIDEBAR_TOGGLE) as HTMLElement | undefined\n }\n\n if (button) {\n event?.preventDefault()\n const data = new PushMenu(button, Defaults)\n data.toggle()\n }\n })\n })\n})\n\nexport default PushMenu\n","/**\n * --------------------------------------------\n * @file AdminLTE treeview.ts\n * @description Treeview plugin for AdminLTE.\n * @license MIT\n * --------------------------------------------\n */\n\nimport {\n onDOMContentLoaded,\n slideDown,\n slideUp\n} from './util/index'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n// const NAME = 'Treeview'\nconst DATA_KEY = 'lte.treeview'\nconst EVENT_KEY = `.${DATA_KEY}`\n\nconst EVENT_EXPANDED = `expanded${EVENT_KEY}`\nconst EVENT_COLLAPSED = `collapsed${EVENT_KEY}`\n// const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`\n\nconst CLASS_NAME_MENU_OPEN = 'menu-open'\nconst SELECTOR_NAV_ITEM = '.nav-item'\nconst SELECTOR_NAV_LINK = '.nav-link'\nconst SELECTOR_TREEVIEW_MENU = '.nav-treeview'\nconst SELECTOR_DATA_TOGGLE = '[data-lte-toggle=\"treeview\"]'\n\nconst Default = {\n animationSpeed: 300,\n accordion: true\n}\n\ntype Config = {\n animationSpeed: number;\n accordion: boolean;\n}\n\n/**\n * Class Definition\n * ====================================================\n */\n\nclass Treeview {\n _element: HTMLElement\n _config: Config\n\n constructor(element: HTMLElement, config: Config) {\n this._element = element\n this._config = { ...Default, ...config }\n }\n\n open(): void {\n const event = new Event(EVENT_EXPANDED)\n\n if (this._config.accordion) {\n const openMenuList = this._element.parentElement?.querySelectorAll(`${SELECTOR_NAV_ITEM}.${CLASS_NAME_MENU_OPEN}`)\n\n openMenuList?.forEach(openMenu => {\n if (openMenu !== this._element.parentElement) {\n openMenu.classList.remove(CLASS_NAME_MENU_OPEN)\n const childElement = openMenu?.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined\n if (childElement) {\n slideUp(childElement, this._config.animationSpeed)\n }\n }\n })\n }\n\n this._element.classList.add(CLASS_NAME_MENU_OPEN)\n\n const childElement = this._element?.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined\n if (childElement) {\n slideDown(childElement, this._config.animationSpeed)\n }\n\n this._element.dispatchEvent(event)\n }\n\n close(): void {\n const event = new Event(EVENT_COLLAPSED)\n\n this._element.classList.remove(CLASS_NAME_MENU_OPEN)\n\n const childElement = this._element?.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined\n if (childElement) {\n slideUp(childElement, this._config.animationSpeed)\n }\n\n this._element.dispatchEvent(event)\n }\n\n toggle(): void {\n if (this._element.classList.contains(CLASS_NAME_MENU_OPEN)) {\n this.close()\n } else {\n this.open()\n }\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\nonDOMContentLoaded(() => {\n const button = document.querySelectorAll(SELECTOR_DATA_TOGGLE)\n\n button.forEach(btn => {\n btn.addEventListener('click', event => {\n const target = event.target as HTMLElement\n const targetItem = target.closest(SELECTOR_NAV_ITEM) as HTMLElement | undefined\n const targetLink = target.closest(SELECTOR_NAV_LINK) as HTMLAnchorElement | undefined\n\n if (target?.getAttribute('href') === '#' || targetLink?.getAttribute('href') === '#') {\n event.preventDefault()\n }\n\n if (targetItem) {\n const data = new Treeview(targetItem, Default)\n data.toggle()\n }\n })\n })\n})\n\nexport default Treeview\n","/**\n * --------------------------------------------\n * @file AdminLTE direct-chat.ts\n * @description Direct chat for AdminLTE.\n * @license MIT\n * --------------------------------------------\n */\n\nimport {\n onDOMContentLoaded\n} from './util/index'\n\n/**\n * Constants\n * ====================================================\n */\n\nconst DATA_KEY = 'lte.direct-chat'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst EVENT_EXPANDED = `expanded${EVENT_KEY}`\nconst EVENT_COLLAPSED = `collapsed${EVENT_KEY}`\n\nconst SELECTOR_DATA_TOGGLE = '[data-lte-toggle=\"chat-pane\"]'\nconst SELECTOR_DIRECT_CHAT = '.direct-chat'\n\nconst CLASS_NAME_DIRECT_CHAT_OPEN = 'direct-chat-contacts-open'\n\n/**\n * Class Definition\n * ====================================================\n */\n\nclass DirectChat {\n _element: HTMLElement\n constructor(element: HTMLElement) {\n this._element = element\n }\n\n toggle(): void {\n if (this._element.classList.contains(CLASS_NAME_DIRECT_CHAT_OPEN)) {\n const event = new Event(EVENT_COLLAPSED)\n\n this._element.classList.remove(CLASS_NAME_DIRECT_CHAT_OPEN)\n\n this._element.dispatchEvent(event)\n } else {\n const event = new Event(EVENT_EXPANDED)\n\n this._element.classList.add(CLASS_NAME_DIRECT_CHAT_OPEN)\n\n this._element.dispatchEvent(event)\n }\n }\n}\n\n/**\n *\n * Data Api implementation\n * ====================================================\n */\n\nonDOMContentLoaded(() => {\n const button = document.querySelectorAll(SELECTOR_DATA_TOGGLE)\n\n button.forEach(btn => {\n btn.addEventListener('click', event => {\n event.preventDefault()\n const target = event.target as HTMLElement\n const chatPane = target.closest(SELECTOR_DIRECT_CHAT) as HTMLElement | undefined\n\n if (chatPane) {\n const data = new DirectChat(chatPane)\n data.toggle()\n }\n })\n })\n})\n\nexport default DirectChat\n","/**\n * --------------------------------------------\n * @file AdminLTE card-widget.ts\n * @description Card widget for AdminLTE.\n * @license MIT\n * --------------------------------------------\n */\n\nimport {\n onDOMContentLoaded,\n slideUp,\n slideDown\n} from './util/index'\n\n/**\n * Constants\n * ====================================================\n */\n\nconst DATA_KEY = 'lte.card-widget'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst EVENT_COLLAPSED = `collapsed${EVENT_KEY}`\nconst EVENT_EXPANDED = `expanded${EVENT_KEY}`\nconst EVENT_REMOVE = `remove${EVENT_KEY}`\nconst EVENT_MAXIMIZED = `maximized${EVENT_KEY}`\nconst EVENT_MINIMIZED = `minimized${EVENT_KEY}`\n\nconst CLASS_NAME_CARD = 'card'\nconst CLASS_NAME_COLLAPSED = 'collapsed-card'\nconst CLASS_NAME_COLLAPSING = 'collapsing-card'\nconst CLASS_NAME_EXPANDING = 'expanding-card'\nconst CLASS_NAME_WAS_COLLAPSED = 'was-collapsed'\nconst CLASS_NAME_MAXIMIZED = 'maximized-card'\n\nconst SELECTOR_DATA_REMOVE = '[data-lte-toggle=\"card-remove\"]'\nconst SELECTOR_DATA_COLLAPSE = '[data-lte-toggle=\"card-collapse\"]'\nconst SELECTOR_DATA_MAXIMIZE = '[data-lte-toggle=\"card-maximize\"]'\nconst SELECTOR_CARD = `.${CLASS_NAME_CARD}`\nconst SELECTOR_CARD_BODY = '.card-body'\nconst SELECTOR_CARD_FOOTER = '.card-footer'\n\ntype Config = {\n animationSpeed: number;\n collapseTrigger: string;\n removeTrigger: string;\n maximizeTrigger: string;\n}\n\nconst Default: Config = {\n animationSpeed: 500,\n collapseTrigger: SELECTOR_DATA_COLLAPSE,\n removeTrigger: SELECTOR_DATA_REMOVE,\n maximizeTrigger: SELECTOR_DATA_MAXIMIZE\n}\n\nclass CardWidget {\n _element: HTMLElement\n _parent: HTMLElement | undefined\n _clone: HTMLElement | undefined\n _config: Config\n\n constructor(element: HTMLElement, config: Config) {\n this._element = element\n this._parent = element.closest(SELECTOR_CARD) as HTMLElement | undefined\n\n if (element.classList.contains(CLASS_NAME_CARD)) {\n this._parent = element\n }\n\n this._config = { ...Default, ...config }\n }\n\n collapse() {\n const event = new Event(EVENT_COLLAPSED)\n\n if (this._parent) {\n this._parent.classList.add(CLASS_NAME_COLLAPSING)\n\n const elm = this._parent?.querySelectorAll(`${SELECTOR_CARD_BODY}, ${SELECTOR_CARD_FOOTER}`)\n\n elm.forEach(el => {\n if (el instanceof HTMLElement) {\n slideUp(el, this._config.animationSpeed)\n }\n })\n\n setTimeout(() => {\n if (this._parent) {\n this._parent.classList.add(CLASS_NAME_COLLAPSED)\n this._parent.classList.remove(CLASS_NAME_COLLAPSING)\n }\n }, this._config.animationSpeed)\n }\n\n this._element?.dispatchEvent(event)\n }\n\n expand() {\n const event = new Event(EVENT_EXPANDED)\n\n if (this._parent) {\n this._parent.classList.add(CLASS_NAME_EXPANDING)\n\n const elm = this._parent?.querySelectorAll(`${SELECTOR_CARD_BODY}, ${SELECTOR_CARD_FOOTER}`)\n\n elm.forEach(el => {\n if (el instanceof HTMLElement) {\n slideDown(el, this._config.animationSpeed)\n }\n })\n\n setTimeout(() => {\n if (this._parent) {\n this._parent.classList.remove(CLASS_NAME_COLLAPSED)\n this._parent.classList.remove(CLASS_NAME_EXPANDING)\n }\n }, this._config.animationSpeed)\n }\n\n this._element?.dispatchEvent(event)\n }\n\n remove() {\n const event = new Event(EVENT_REMOVE)\n\n if (this._parent) {\n slideUp(this._parent, this._config.animationSpeed)\n }\n\n this._element?.dispatchEvent(event)\n }\n\n toggle() {\n if (this._parent?.classList.contains(CLASS_NAME_COLLAPSED)) {\n this.expand()\n return\n }\n\n this.collapse()\n }\n\n maximize() {\n const event = new Event(EVENT_MAXIMIZED)\n\n if (this._parent) {\n this._parent.style.height = `${this._parent.offsetHeight}px`\n this._parent.style.width = `${this._parent.offsetWidth}px`\n this._parent.style.transition = 'all .15s'\n\n setTimeout(() => {\n const htmlTag = document.querySelector('html')\n\n if (htmlTag) {\n htmlTag.classList.add(CLASS_NAME_MAXIMIZED)\n }\n\n if (this._parent) {\n this._parent.classList.add(CLASS_NAME_MAXIMIZED)\n\n if (this._parent.classList.contains(CLASS_NAME_COLLAPSED)) {\n this._parent.classList.add(CLASS_NAME_WAS_COLLAPSED)\n }\n }\n }, 150)\n }\n\n this._element?.dispatchEvent(event)\n }\n\n minimize() {\n const event = new Event(EVENT_MINIMIZED)\n\n if (this._parent) {\n this._parent.style.height = 'auto'\n this._parent.style.width = 'auto'\n this._parent.style.transition = 'all .15s'\n\n setTimeout(() => {\n const htmlTag = document.querySelector('html')\n\n if (htmlTag) {\n htmlTag.classList.remove(CLASS_NAME_MAXIMIZED)\n }\n\n if (this._parent) {\n this._parent.classList.remove(CLASS_NAME_MAXIMIZED)\n\n if (this._parent?.classList.contains(CLASS_NAME_WAS_COLLAPSED)) {\n this._parent.classList.remove(CLASS_NAME_WAS_COLLAPSED)\n }\n }\n }, 10)\n }\n\n this._element?.dispatchEvent(event)\n }\n\n toggleMaximize() {\n if (this._parent?.classList.contains(CLASS_NAME_MAXIMIZED)) {\n this.minimize()\n return\n }\n\n this.maximize()\n }\n}\n\n/**\n *\n * Data Api implementation\n * ====================================================\n */\n\nonDOMContentLoaded(() => {\n const collapseBtn = document.querySelectorAll(SELECTOR_DATA_COLLAPSE)\n\n collapseBtn.forEach(btn => {\n btn.addEventListener('click', event => {\n event.preventDefault()\n const target = event.target as HTMLElement\n const data = new CardWidget(target, Default)\n data.toggle()\n })\n })\n\n const removeBtn = document.querySelectorAll(SELECTOR_DATA_REMOVE)\n\n removeBtn.forEach(btn => {\n btn.addEventListener('click', event => {\n event.preventDefault()\n const target = event.target as HTMLElement\n const data = new CardWidget(target, Default)\n data.remove()\n })\n })\n\n const maxBtn = document.querySelectorAll(SELECTOR_DATA_MAXIMIZE)\n\n maxBtn.forEach(btn => {\n btn.addEventListener('click', event => {\n event.preventDefault()\n const target = event.target as HTMLElement\n const data = new CardWidget(target, Default)\n data.toggleMaximize()\n })\n })\n})\n\nexport default CardWidget\n","/**\n * --------------------------------------------\n * @file AdminLTE fullscreen.ts\n * @description Fullscreen plugin for AdminLTE.\n * @license MIT\n * --------------------------------------------\n */\n\nimport {\n onDOMContentLoaded\n} from './util/index'\n\n/**\n * Constants\n * ============================================================================\n */\nconst DATA_KEY = 'lte.fullscreen'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst EVENT_MAXIMIZED = `maximized${EVENT_KEY}`\nconst EVENT_MINIMIZED = `minimized${EVENT_KEY}`\n\nconst SELECTOR_FULLSCREEN_TOGGLE = '[data-lte-toggle=\"fullscreen\"]'\nconst SELECTOR_MAXIMIZE_ICON = '[data-lte-icon=\"maximize\"]'\nconst SELECTOR_MINIMIZE_ICON = '[data-lte-icon=\"minimize\"]'\n\n/**\n * Class Definition.\n * ============================================================================\n */\nclass FullScreen {\n _element: HTMLElement\n _config: undefined\n\n constructor(element: HTMLElement, config?: undefined) {\n this._element = element\n this._config = config\n }\n\n inFullScreen(): void {\n const event = new Event(EVENT_MAXIMIZED)\n\n const iconMaximize = document.querySelector<HTMLElement>(SELECTOR_MAXIMIZE_ICON)\n const iconMinimize = document.querySelector<HTMLElement>(SELECTOR_MINIMIZE_ICON)\n\n void document.documentElement.requestFullscreen()\n\n if (iconMaximize) {\n iconMaximize.style.display = 'none'\n }\n\n if (iconMinimize) {\n iconMinimize.style.display = 'block'\n }\n\n this._element.dispatchEvent(event)\n }\n\n outFullscreen(): void {\n const event = new Event(EVENT_MINIMIZED)\n\n const iconMaximize = document.querySelector<HTMLElement>(SELECTOR_MAXIMIZE_ICON)\n const iconMinimize = document.querySelector<HTMLElement>(SELECTOR_MINIMIZE_ICON)\n\n void document.exitFullscreen()\n\n if (iconMaximize) {\n iconMaximize.style.display = 'block'\n }\n\n if (iconMinimize) {\n iconMinimize.style.display = 'none'\n }\n\n this._element.dispatchEvent(event)\n }\n\n toggleFullScreen(): void {\n if (document.fullscreenEnabled) {\n if (document.fullscreenElement) {\n this.outFullscreen()\n } else {\n this.inFullScreen()\n }\n }\n }\n}\n\n/**\n * Data Api implementation\n * ============================================================================\n */\nonDOMContentLoaded(() => {\n const buttons = document.querySelectorAll(SELECTOR_FULLSCREEN_TOGGLE)\n\n buttons.forEach(btn => {\n btn.addEventListener('click', event => {\n event.preventDefault()\n\n const target = event.target as HTMLElement\n const button = target.closest(SELECTOR_FULLSCREEN_TOGGLE) as HTMLElement | undefined\n\n if (button) {\n const data = new FullScreen(button, undefined)\n data.toggleFullScreen()\n }\n })\n })\n})\n\nexport default FullScreen\n"],"names":["DATA_KEY","EVENT_KEY","CLASS_NAME_MENU_OPEN","SELECTOR_NAV_ITEM","EVENT_EXPANDED","EVENT_COLLAPSED","SELECTOR_DATA_TOGGLE","Default","EVENT_MAXIMIZED","EVENT_MINIMIZED"],"mappings":"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"}
|
|
|
|
static/js/adminlte.min.js
DELETED
@@ -1,7 +0,0 @@
|
|
1 |
-
/*!
|
2 |
-
* AdminLTE v4.0.0-beta2 (https://adminlte.io)
|
3 |
-
* Copyright 2014-2024 Colorlib <https://colorlib.com>
|
4 |
-
* Licensed under MIT (https://github.com/ColorlibHQ/AdminLTE/blob/master/LICENSE)
|
5 |
-
*/
|
6 |
-
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).adminlte={})}(this,(function(e){"use strict";const t=[],n=e=>{"loading"===document.readyState?(t.length||document.addEventListener("DOMContentLoaded",(()=>{for(const e of t)e()})),t.push(e)):e()},s=(e,t=500)=>{e.style.transitionProperty="height, margin, padding",e.style.transitionDuration=`${t}ms`,e.style.boxSizing="border-box",e.style.height=`${e.offsetHeight}px`,e.style.overflow="hidden",window.setTimeout((()=>{e.style.height="0",e.style.paddingTop="0",e.style.paddingBottom="0",e.style.marginTop="0",e.style.marginBottom="0"}),1),window.setTimeout((()=>{e.style.display="none",e.style.removeProperty("height"),e.style.removeProperty("padding-top"),e.style.removeProperty("padding-bottom"),e.style.removeProperty("margin-top"),e.style.removeProperty("margin-bottom"),e.style.removeProperty("overflow"),e.style.removeProperty("transition-duration"),e.style.removeProperty("transition-property")}),t)},i=(e,t=500)=>{e.style.removeProperty("display");let{display:n}=window.getComputedStyle(e);"none"===n&&(n="block"),e.style.display=n;const s=e.offsetHeight;e.style.overflow="hidden",e.style.height="0",e.style.paddingTop="0",e.style.paddingBottom="0",e.style.marginTop="0",e.style.marginBottom="0",window.setTimeout((()=>{e.style.boxSizing="border-box",e.style.transitionProperty="height, margin, padding",e.style.transitionDuration=`${t}ms`,e.style.height=`${s}px`,e.style.removeProperty("padding-top"),e.style.removeProperty("padding-bottom"),e.style.removeProperty("margin-top"),e.style.removeProperty("margin-bottom")}),1),window.setTimeout((()=>{e.style.removeProperty("height"),e.style.removeProperty("overflow"),e.style.removeProperty("transition-duration"),e.style.removeProperty("transition-property")}),t)},o="hold-transition";class l{constructor(e){this._element=e}holdTransition(){let e;window.addEventListener("resize",(()=>{document.body.classList.add(o),clearTimeout(e),e=setTimeout((()=>{document.body.classList.remove(o)}),400)}))}}n((()=>{new l(document.body).holdTransition(),setTimeout((()=>{document.body.classList.add("app-loaded")}),400)}));const a=".lte.push-menu",r=`open${a}`,c=`collapse${a}`,d="sidebar-mini",m="sidebar-collapse",p="sidebar-open",h="sidebar-expand",u=`[class*="${h}"]`,v='[data-lte-toggle="sidebar"]',y={sidebarBreakpoint:992};class g{constructor(e,t){this._element=e,this._config=Object.assign(Object.assign({},y),t)}menusClose(){document.querySelectorAll(".nav-treeview").forEach((e=>{e.style.removeProperty("display"),e.style.removeProperty("height")}));const e=document.querySelector(".sidebar-menu"),t=null==e?void 0:e.querySelectorAll(".nav-item");t&&t.forEach((e=>{e.classList.remove("menu-open")}))}expand(){const e=new Event(r);document.body.classList.remove(m),document.body.classList.add(p),this._element.dispatchEvent(e)}collapse(){const e=new Event(c);document.body.classList.remove(p),document.body.classList.add(m),this._element.dispatchEvent(e)}addSidebarBreakPoint(){var e,t,n;const s=null!==(t=null===(e=document.querySelector(u))||void 0===e?void 0:e.classList)&&void 0!==t?t:[],i=null!==(n=Array.from(s).find((e=>e.startsWith(h))))&&void 0!==n?n:"",o=document.getElementsByClassName(i)[0],l=window.getComputedStyle(o,"::before").getPropertyValue("content");this._config=Object.assign(Object.assign({},this._config),{sidebarBreakpoint:Number(l.replace(/[^\d.-]/g,""))}),window.innerWidth<=this._config.sidebarBreakpoint?this.collapse():(document.body.classList.contains(d)||this.expand(),document.body.classList.contains(d)&&document.body.classList.contains(m)&&this.collapse())}toggle(){document.body.classList.contains(m)?this.expand():this.collapse()}init(){this.addSidebarBreakPoint()}}n((()=>{var e;const t=null===document||void 0===document?void 0:document.querySelector(".app-sidebar");if(t){const e=new g(t,y);e.init(),window.addEventListener("resize",(()=>{e.init()}))}const n=document.createElement("div");n.className="sidebar-overlay",null===(e=document.querySelector(".app-wrapper"))||void 0===e||e.append(n),n.addEventListener("touchstart",(e=>{e.preventDefault();const t=e.currentTarget;new g(t,y).collapse()}),{passive:!0}),n.addEventListener("click",(e=>{e.preventDefault();const t=e.currentTarget;new g(t,y).collapse()})),document.querySelectorAll(v).forEach((e=>{e.addEventListener("click",(e=>{e.preventDefault();let t=e.currentTarget;"sidebar"!==(null==t?void 0:t.dataset.lteToggle)&&(t=null==t?void 0:t.closest(v)),t&&(null==e||e.preventDefault(),new g(t,y).toggle())}))}))}));const f=".lte.treeview",_=`expanded${f}`,E=`collapsed${f}`,b="menu-open",w=".nav-item",L=".nav-treeview",S={animationSpeed:300,accordion:!0};class x{constructor(e,t){this._element=e,this._config=Object.assign(Object.assign({},S),t)}open(){var e,t;const n=new Event(_);if(this._config.accordion){const t=null===(e=this._element.parentElement)||void 0===e?void 0:e.querySelectorAll(`${w}.${b}`);null==t||t.forEach((e=>{if(e!==this._element.parentElement){e.classList.remove(b);const t=null==e?void 0:e.querySelector(L);t&&s(t,this._config.animationSpeed)}}))}this._element.classList.add(b);const o=null===(t=this._element)||void 0===t?void 0:t.querySelector(L);o&&i(o,this._config.animationSpeed),this._element.dispatchEvent(n)}close(){var e;const t=new Event(E);this._element.classList.remove(b);const n=null===(e=this._element)||void 0===e?void 0:e.querySelector(L);n&&s(n,this._config.animationSpeed),this._element.dispatchEvent(t)}toggle(){this._element.classList.contains(b)?this.close():this.open()}}n((()=>{document.querySelectorAll('[data-lte-toggle="treeview"]').forEach((e=>{e.addEventListener("click",(e=>{const t=e.target,n=t.closest(w),s=t.closest(".nav-link");"#"!==(null==t?void 0:t.getAttribute("href"))&&"#"!==(null==s?void 0:s.getAttribute("href"))||e.preventDefault(),n&&new x(n,S).toggle()}))}))}));const T=".lte.direct-chat",$=`expanded${T}`,q=`collapsed${T}`,P="direct-chat-contacts-open";class z{constructor(e){this._element=e}toggle(){if(this._element.classList.contains(P)){const e=new Event(q);this._element.classList.remove(P),this._element.dispatchEvent(e)}else{const e=new Event($);this._element.classList.add(P),this._element.dispatchEvent(e)}}}n((()=>{document.querySelectorAll('[data-lte-toggle="chat-pane"]').forEach((e=>{e.addEventListener("click",(e=>{e.preventDefault();const t=e.target.closest(".direct-chat");t&&new z(t).toggle()}))}))}));const k=".lte.card-widget",A=`collapsed${k}`,D=`expanded${k}`,B=`remove${k}`,j=`maximized${k}`,F=`minimized${k}`,O="card",C="collapsed-card",M="collapsing-card",H="expanding-card",W="was-collapsed",N="maximized-card",V='[data-lte-toggle="card-remove"]',G='[data-lte-toggle="card-collapse"]',I='[data-lte-toggle="card-maximize"]',J=`.${O}`,K=".card-body",Q=".card-footer",R={animationSpeed:500,collapseTrigger:G,removeTrigger:V,maximizeTrigger:I};class U{constructor(e,t){this._element=e,this._parent=e.closest(J),e.classList.contains(O)&&(this._parent=e),this._config=Object.assign(Object.assign({},R),t)}collapse(){var e,t;const n=new Event(A);this._parent&&(this._parent.classList.add(M),(null===(e=this._parent)||void 0===e?void 0:e.querySelectorAll(`${K}, ${Q}`)).forEach((e=>{e instanceof HTMLElement&&s(e,this._config.animationSpeed)})),setTimeout((()=>{this._parent&&(this._parent.classList.add(C),this._parent.classList.remove(M))}),this._config.animationSpeed)),null===(t=this._element)||void 0===t||t.dispatchEvent(n)}expand(){var e,t;const n=new Event(D);this._parent&&(this._parent.classList.add(H),(null===(e=this._parent)||void 0===e?void 0:e.querySelectorAll(`${K}, ${Q}`)).forEach((e=>{e instanceof HTMLElement&&i(e,this._config.animationSpeed)})),setTimeout((()=>{this._parent&&(this._parent.classList.remove(C),this._parent.classList.remove(H))}),this._config.animationSpeed)),null===(t=this._element)||void 0===t||t.dispatchEvent(n)}remove(){var e;const t=new Event(B);this._parent&&s(this._parent,this._config.animationSpeed),null===(e=this._element)||void 0===e||e.dispatchEvent(t)}toggle(){var e;(null===(e=this._parent)||void 0===e?void 0:e.classList.contains(C))?this.expand():this.collapse()}maximize(){var e;const t=new Event(j);this._parent&&(this._parent.style.height=`${this._parent.offsetHeight}px`,this._parent.style.width=`${this._parent.offsetWidth}px`,this._parent.style.transition="all .15s",setTimeout((()=>{const e=document.querySelector("html");e&&e.classList.add(N),this._parent&&(this._parent.classList.add(N),this._parent.classList.contains(C)&&this._parent.classList.add(W))}),150)),null===(e=this._element)||void 0===e||e.dispatchEvent(t)}minimize(){var e;const t=new Event(F);this._parent&&(this._parent.style.height="auto",this._parent.style.width="auto",this._parent.style.transition="all .15s",setTimeout((()=>{var e;const t=document.querySelector("html");t&&t.classList.remove(N),this._parent&&(this._parent.classList.remove(N),(null===(e=this._parent)||void 0===e?void 0:e.classList.contains(W))&&this._parent.classList.remove(W))}),10)),null===(e=this._element)||void 0===e||e.dispatchEvent(t)}toggleMaximize(){var e;(null===(e=this._parent)||void 0===e?void 0:e.classList.contains(N))?this.minimize():this.maximize()}}n((()=>{document.querySelectorAll(G).forEach((e=>{e.addEventListener("click",(e=>{e.preventDefault();const t=e.target;new U(t,R).toggle()}))})),document.querySelectorAll(V).forEach((e=>{e.addEventListener("click",(e=>{e.preventDefault();const t=e.target;new U(t,R).remove()}))})),document.querySelectorAll(I).forEach((e=>{e.addEventListener("click",(e=>{e.preventDefault();const t=e.target;new U(t,R).toggleMaximize()}))}))}));const X=".lte.fullscreen",Y=`maximized${X}`,Z=`minimized${X}`,ee='[data-lte-toggle="fullscreen"]',te='[data-lte-icon="maximize"]',ne='[data-lte-icon="minimize"]';class se{constructor(e,t){this._element=e,this._config=t}inFullScreen(){const e=new Event(Y),t=document.querySelector(te),n=document.querySelector(ne);document.documentElement.requestFullscreen(),t&&(t.style.display="none"),n&&(n.style.display="block"),this._element.dispatchEvent(e)}outFullscreen(){const e=new Event(Z),t=document.querySelector(te),n=document.querySelector(ne);document.exitFullscreen(),t&&(t.style.display="block"),n&&(n.style.display="none"),this._element.dispatchEvent(e)}toggleFullScreen(){document.fullscreenEnabled&&(document.fullscreenElement?this.outFullscreen():this.inFullScreen())}}n((()=>{document.querySelectorAll(ee).forEach((e=>{e.addEventListener("click",(e=>{e.preventDefault();const t=e.target.closest(ee);t&&new se(t,void 0).toggleFullScreen()}))}))})),e.CardWidget=U,e.DirectChat=z,e.FullScreen=se,e.Layout=l,e.PushMenu=g,e.Treeview=x}));
|
7 |
-
//# sourceMappingURL=adminlte.min.js.map
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/js/adminlte.min.js.map
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
{"version":3,"names":["domContentLoadedCallbacks","onDOMContentLoaded","callback","document","readyState","length","addEventListener","push","slideUp","target","duration","style","transitionProperty","transitionDuration","boxSizing","height","offsetHeight","overflow","window","setTimeout","paddingTop","paddingBottom","marginTop","marginBottom","display","removeProperty","slideDown","getComputedStyle","CLASS_NAME_HOLD_TRANSITIONS","Layout","constructor","element","this","_element","holdTransition","resizeTimer","body","classList","add","clearTimeout","remove","EVENT_KEY","EVENT_OPEN","EVENT_COLLAPSE","CLASS_NAME_SIDEBAR_MINI","CLASS_NAME_SIDEBAR_COLLAPSE","CLASS_NAME_SIDEBAR_OPEN","CLASS_NAME_SIDEBAR_EXPAND","SELECTOR_SIDEBAR_EXPAND","SELECTOR_SIDEBAR_TOGGLE","Defaults","sidebarBreakpoint","PushMenu","config","_config","Object","assign","menusClose","querySelectorAll","forEach","navTree","navSidebar","querySelector","navItem","navI","expand","event","Event","dispatchEvent","collapse","addSidebarBreakPoint","sidebarExpandList","_b","_a","sidebarExpand","_c","Array","from","find","className","startsWith","sidebar","getElementsByClassName","sidebarContent","getPropertyValue","Number","replace","innerWidth","contains","toggle","init","data","sidebarOverlay","createElement","append","preventDefault","currentTarget","passive","btn","button","dataset","lteToggle","closest","EVENT_EXPANDED","EVENT_COLLAPSED","CLASS_NAME_MENU_OPEN","SELECTOR_NAV_ITEM","SELECTOR_TREEVIEW_MENU","Default","animationSpeed","accordion","Treeview","open","openMenuList","parentElement","openMenu","childElement","close","targetItem","targetLink","getAttribute","CLASS_NAME_DIRECT_CHAT_OPEN","DirectChat","chatPane","EVENT_REMOVE","EVENT_MAXIMIZED","EVENT_MINIMIZED","CLASS_NAME_CARD","CLASS_NAME_COLLAPSED","CLASS_NAME_COLLAPSING","CLASS_NAME_EXPANDING","CLASS_NAME_WAS_COLLAPSED","CLASS_NAME_MAXIMIZED","SELECTOR_DATA_REMOVE","SELECTOR_DATA_COLLAPSE","SELECTOR_DATA_MAXIMIZE","SELECTOR_CARD","SELECTOR_CARD_BODY","SELECTOR_CARD_FOOTER","collapseTrigger","removeTrigger","maximizeTrigger","CardWidget","_parent","el","HTMLElement","maximize","width","offsetWidth","transition","htmlTag","minimize","toggleMaximize","SELECTOR_FULLSCREEN_TOGGLE","SELECTOR_MAXIMIZE_ICON","SELECTOR_MINIMIZE_ICON","FullScreen","inFullScreen","iconMaximize","iconMinimize","documentElement","requestFullscreen","outFullscreen","exitFullscreen","toggleFullScreen","fullscreenEnabled","fullscreenElement","undefined"],"sources":["../../src/ts/util/index.ts","../../src/ts/layout.ts","../../src/ts/push-menu.ts","../../src/ts/treeview.ts","../../src/ts/direct-chat.ts","../../src/ts/card-widget.ts","../../src/ts/fullscreen.ts"],"sourcesContent":["const domContentLoadedCallbacks: Array<() => void> = []\n\nconst onDOMContentLoaded = (callback: () => void): void => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!domContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of domContentLoadedCallbacks) {\n callback()\n }\n })\n }\n\n domContentLoadedCallbacks.push(callback)\n } else {\n callback()\n }\n}\n\n/* SLIDE UP */\nconst slideUp = (target: HTMLElement, duration = 500) => {\n target.style.transitionProperty = 'height, margin, padding'\n target.style.transitionDuration = `${duration}ms`\n target.style.boxSizing = 'border-box'\n target.style.height = `${target.offsetHeight}px`\n target.style.overflow = 'hidden'\n\n window.setTimeout(() => {\n target.style.height = '0'\n target.style.paddingTop = '0'\n target.style.paddingBottom = '0'\n target.style.marginTop = '0'\n target.style.marginBottom = '0'\n }, 1)\n\n window.setTimeout(() => {\n target.style.display = 'none'\n target.style.removeProperty('height')\n target.style.removeProperty('padding-top')\n target.style.removeProperty('padding-bottom')\n target.style.removeProperty('margin-top')\n target.style.removeProperty('margin-bottom')\n target.style.removeProperty('overflow')\n target.style.removeProperty('transition-duration')\n target.style.removeProperty('transition-property')\n }, duration)\n}\n\n/* SLIDE DOWN */\nconst slideDown = (target: HTMLElement, duration = 500) => {\n target.style.removeProperty('display')\n let { display } = window.getComputedStyle(target)\n\n if (display === 'none') {\n display = 'block'\n }\n\n target.style.display = display\n const height = target.offsetHeight\n target.style.overflow = 'hidden'\n target.style.height = '0'\n target.style.paddingTop = '0'\n target.style.paddingBottom = '0'\n target.style.marginTop = '0'\n target.style.marginBottom = '0'\n\n window.setTimeout(() => {\n target.style.boxSizing = 'border-box'\n target.style.transitionProperty = 'height, margin, padding'\n target.style.transitionDuration = `${duration}ms`\n target.style.height = `${height}px`\n target.style.removeProperty('padding-top')\n target.style.removeProperty('padding-bottom')\n target.style.removeProperty('margin-top')\n target.style.removeProperty('margin-bottom')\n }, 1)\n\n window.setTimeout(() => {\n target.style.removeProperty('height')\n target.style.removeProperty('overflow')\n target.style.removeProperty('transition-duration')\n target.style.removeProperty('transition-property')\n }, duration)\n}\n\n/* TOGGLE */\nconst slideToggle = (target: HTMLElement, duration = 500) => {\n if (window.getComputedStyle(target).display === 'none') {\n slideDown(target, duration)\n return\n }\n\n slideUp(target, duration)\n}\n\nexport {\n onDOMContentLoaded,\n slideUp,\n slideDown,\n slideToggle\n}\n","/**\n * --------------------------------------------\n * @file AdminLTE layout.ts\n * @description Layout for AdminLTE.\n * @license MIT\n * --------------------------------------------\n */\n\nimport {\n onDOMContentLoaded\n} from './util/index'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst CLASS_NAME_HOLD_TRANSITIONS = 'hold-transition'\nconst CLASS_NAME_APP_LOADED = 'app-loaded'\n\n/**\n * Class Definition\n * ====================================================\n */\n\nclass Layout {\n _element: HTMLElement\n\n constructor(element: HTMLElement) {\n this._element = element\n }\n\n holdTransition(): void {\n let resizeTimer: ReturnType<typeof setTimeout>\n window.addEventListener('resize', () => {\n document.body.classList.add(CLASS_NAME_HOLD_TRANSITIONS)\n clearTimeout(resizeTimer)\n resizeTimer = setTimeout(() => {\n document.body.classList.remove(CLASS_NAME_HOLD_TRANSITIONS)\n }, 400)\n })\n }\n}\n\nonDOMContentLoaded(() => {\n const data = new Layout(document.body)\n data.holdTransition()\n setTimeout(() => {\n document.body.classList.add(CLASS_NAME_APP_LOADED)\n }, 400)\n})\n\nexport default Layout\n","/**\n * --------------------------------------------\n * @file AdminLTE push-menu.ts\n * @description Push menu for AdminLTE.\n * @license MIT\n * --------------------------------------------\n */\n\nimport {\n onDOMContentLoaded\n} from './util/index'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst DATA_KEY = 'lte.push-menu'\nconst EVENT_KEY = `.${DATA_KEY}`\n\nconst EVENT_OPEN = `open${EVENT_KEY}`\nconst EVENT_COLLAPSE = `collapse${EVENT_KEY}`\n\nconst CLASS_NAME_SIDEBAR_MINI = 'sidebar-mini'\nconst CLASS_NAME_SIDEBAR_COLLAPSE = 'sidebar-collapse'\nconst CLASS_NAME_SIDEBAR_OPEN = 'sidebar-open'\nconst CLASS_NAME_SIDEBAR_EXPAND = 'sidebar-expand'\nconst CLASS_NAME_SIDEBAR_OVERLAY = 'sidebar-overlay'\nconst CLASS_NAME_MENU_OPEN = 'menu-open'\n\nconst SELECTOR_APP_SIDEBAR = '.app-sidebar'\nconst SELECTOR_SIDEBAR_MENU = '.sidebar-menu'\nconst SELECTOR_NAV_ITEM = '.nav-item'\nconst SELECTOR_NAV_TREEVIEW = '.nav-treeview'\nconst SELECTOR_APP_WRAPPER = '.app-wrapper'\nconst SELECTOR_SIDEBAR_EXPAND = `[class*=\"${CLASS_NAME_SIDEBAR_EXPAND}\"]`\nconst SELECTOR_SIDEBAR_TOGGLE = '[data-lte-toggle=\"sidebar\"]'\n\ntype Config = {\n sidebarBreakpoint: number;\n}\n\nconst Defaults = {\n sidebarBreakpoint: 992\n}\n\n/**\n * Class Definition\n * ====================================================\n */\n\nclass PushMenu {\n _element: HTMLElement\n _config: Config\n\n constructor(element: HTMLElement, config: Config) {\n this._element = element\n this._config = { ...Defaults, ...config }\n }\n\n // TODO\n menusClose() {\n const navTreeview = document.querySelectorAll<HTMLElement>(SELECTOR_NAV_TREEVIEW)\n\n navTreeview.forEach(navTree => {\n navTree.style.removeProperty('display')\n navTree.style.removeProperty('height')\n })\n\n const navSidebar = document.querySelector(SELECTOR_SIDEBAR_MENU)\n const navItem = navSidebar?.querySelectorAll(SELECTOR_NAV_ITEM)\n\n if (navItem) {\n navItem.forEach(navI => {\n navI.classList.remove(CLASS_NAME_MENU_OPEN)\n })\n }\n }\n\n expand() {\n const event = new Event(EVENT_OPEN)\n\n document.body.classList.remove(CLASS_NAME_SIDEBAR_COLLAPSE)\n document.body.classList.add(CLASS_NAME_SIDEBAR_OPEN)\n\n this._element.dispatchEvent(event)\n }\n\n collapse() {\n const event = new Event(EVENT_COLLAPSE)\n\n document.body.classList.remove(CLASS_NAME_SIDEBAR_OPEN)\n document.body.classList.add(CLASS_NAME_SIDEBAR_COLLAPSE)\n\n this._element.dispatchEvent(event)\n }\n\n addSidebarBreakPoint() {\n const sidebarExpandList = document.querySelector(SELECTOR_SIDEBAR_EXPAND)?.classList ?? []\n const sidebarExpand = Array.from(sidebarExpandList).find(className => className.startsWith(CLASS_NAME_SIDEBAR_EXPAND)) ?? ''\n const sidebar = document.getElementsByClassName(sidebarExpand)[0]\n const sidebarContent = window.getComputedStyle(sidebar, '::before').getPropertyValue('content')\n this._config = { ...this._config, sidebarBreakpoint: Number(sidebarContent.replace(/[^\\d.-]/g, '')) }\n\n if (window.innerWidth <= this._config.sidebarBreakpoint) {\n this.collapse()\n } else {\n if (!document.body.classList.contains(CLASS_NAME_SIDEBAR_MINI)) {\n this.expand()\n }\n\n if (document.body.classList.contains(CLASS_NAME_SIDEBAR_MINI) && document.body.classList.contains(CLASS_NAME_SIDEBAR_COLLAPSE)) {\n this.collapse()\n }\n }\n }\n\n toggle() {\n if (document.body.classList.contains(CLASS_NAME_SIDEBAR_COLLAPSE)) {\n this.expand()\n } else {\n this.collapse()\n }\n }\n\n init() {\n this.addSidebarBreakPoint()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\nonDOMContentLoaded(() => {\n const sidebar = document?.querySelector(SELECTOR_APP_SIDEBAR) as HTMLElement | undefined\n\n if (sidebar) {\n const data = new PushMenu(sidebar, Defaults)\n data.init()\n\n window.addEventListener('resize', () => {\n data.init()\n })\n }\n\n const sidebarOverlay = document.createElement('div')\n sidebarOverlay.className = CLASS_NAME_SIDEBAR_OVERLAY\n document.querySelector(SELECTOR_APP_WRAPPER)?.append(sidebarOverlay)\n\n sidebarOverlay.addEventListener('touchstart', event => {\n event.preventDefault()\n const target = event.currentTarget as HTMLElement\n const data = new PushMenu(target, Defaults)\n data.collapse()\n }, { passive: true })\n sidebarOverlay.addEventListener('click', event => {\n event.preventDefault()\n const target = event.currentTarget as HTMLElement\n const data = new PushMenu(target, Defaults)\n data.collapse()\n })\n\n const fullBtn = document.querySelectorAll(SELECTOR_SIDEBAR_TOGGLE)\n\n fullBtn.forEach(btn => {\n btn.addEventListener('click', event => {\n event.preventDefault()\n\n let button = event.currentTarget as HTMLElement | undefined\n\n if (button?.dataset.lteToggle !== 'sidebar') {\n button = button?.closest(SELECTOR_SIDEBAR_TOGGLE) as HTMLElement | undefined\n }\n\n if (button) {\n event?.preventDefault()\n const data = new PushMenu(button, Defaults)\n data.toggle()\n }\n })\n })\n})\n\nexport default PushMenu\n","/**\n * --------------------------------------------\n * @file AdminLTE treeview.ts\n * @description Treeview plugin for AdminLTE.\n * @license MIT\n * --------------------------------------------\n */\n\nimport {\n onDOMContentLoaded,\n slideDown,\n slideUp\n} from './util/index'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n// const NAME = 'Treeview'\nconst DATA_KEY = 'lte.treeview'\nconst EVENT_KEY = `.${DATA_KEY}`\n\nconst EVENT_EXPANDED = `expanded${EVENT_KEY}`\nconst EVENT_COLLAPSED = `collapsed${EVENT_KEY}`\n// const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`\n\nconst CLASS_NAME_MENU_OPEN = 'menu-open'\nconst SELECTOR_NAV_ITEM = '.nav-item'\nconst SELECTOR_NAV_LINK = '.nav-link'\nconst SELECTOR_TREEVIEW_MENU = '.nav-treeview'\nconst SELECTOR_DATA_TOGGLE = '[data-lte-toggle=\"treeview\"]'\n\nconst Default = {\n animationSpeed: 300,\n accordion: true\n}\n\ntype Config = {\n animationSpeed: number;\n accordion: boolean;\n}\n\n/**\n * Class Definition\n * ====================================================\n */\n\nclass Treeview {\n _element: HTMLElement\n _config: Config\n\n constructor(element: HTMLElement, config: Config) {\n this._element = element\n this._config = { ...Default, ...config }\n }\n\n open(): void {\n const event = new Event(EVENT_EXPANDED)\n\n if (this._config.accordion) {\n const openMenuList = this._element.parentElement?.querySelectorAll(`${SELECTOR_NAV_ITEM}.${CLASS_NAME_MENU_OPEN}`)\n\n openMenuList?.forEach(openMenu => {\n if (openMenu !== this._element.parentElement) {\n openMenu.classList.remove(CLASS_NAME_MENU_OPEN)\n const childElement = openMenu?.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined\n if (childElement) {\n slideUp(childElement, this._config.animationSpeed)\n }\n }\n })\n }\n\n this._element.classList.add(CLASS_NAME_MENU_OPEN)\n\n const childElement = this._element?.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined\n if (childElement) {\n slideDown(childElement, this._config.animationSpeed)\n }\n\n this._element.dispatchEvent(event)\n }\n\n close(): void {\n const event = new Event(EVENT_COLLAPSED)\n\n this._element.classList.remove(CLASS_NAME_MENU_OPEN)\n\n const childElement = this._element?.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined\n if (childElement) {\n slideUp(childElement, this._config.animationSpeed)\n }\n\n this._element.dispatchEvent(event)\n }\n\n toggle(): void {\n if (this._element.classList.contains(CLASS_NAME_MENU_OPEN)) {\n this.close()\n } else {\n this.open()\n }\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\nonDOMContentLoaded(() => {\n const button = document.querySelectorAll(SELECTOR_DATA_TOGGLE)\n\n button.forEach(btn => {\n btn.addEventListener('click', event => {\n const target = event.target as HTMLElement\n const targetItem = target.closest(SELECTOR_NAV_ITEM) as HTMLElement | undefined\n const targetLink = target.closest(SELECTOR_NAV_LINK) as HTMLAnchorElement | undefined\n\n if (target?.getAttribute('href') === '#' || targetLink?.getAttribute('href') === '#') {\n event.preventDefault()\n }\n\n if (targetItem) {\n const data = new Treeview(targetItem, Default)\n data.toggle()\n }\n })\n })\n})\n\nexport default Treeview\n","/**\n * --------------------------------------------\n * @file AdminLTE direct-chat.ts\n * @description Direct chat for AdminLTE.\n * @license MIT\n * --------------------------------------------\n */\n\nimport {\n onDOMContentLoaded\n} from './util/index'\n\n/**\n * Constants\n * ====================================================\n */\n\nconst DATA_KEY = 'lte.direct-chat'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst EVENT_EXPANDED = `expanded${EVENT_KEY}`\nconst EVENT_COLLAPSED = `collapsed${EVENT_KEY}`\n\nconst SELECTOR_DATA_TOGGLE = '[data-lte-toggle=\"chat-pane\"]'\nconst SELECTOR_DIRECT_CHAT = '.direct-chat'\n\nconst CLASS_NAME_DIRECT_CHAT_OPEN = 'direct-chat-contacts-open'\n\n/**\n * Class Definition\n * ====================================================\n */\n\nclass DirectChat {\n _element: HTMLElement\n constructor(element: HTMLElement) {\n this._element = element\n }\n\n toggle(): void {\n if (this._element.classList.contains(CLASS_NAME_DIRECT_CHAT_OPEN)) {\n const event = new Event(EVENT_COLLAPSED)\n\n this._element.classList.remove(CLASS_NAME_DIRECT_CHAT_OPEN)\n\n this._element.dispatchEvent(event)\n } else {\n const event = new Event(EVENT_EXPANDED)\n\n this._element.classList.add(CLASS_NAME_DIRECT_CHAT_OPEN)\n\n this._element.dispatchEvent(event)\n }\n }\n}\n\n/**\n *\n * Data Api implementation\n * ====================================================\n */\n\nonDOMContentLoaded(() => {\n const button = document.querySelectorAll(SELECTOR_DATA_TOGGLE)\n\n button.forEach(btn => {\n btn.addEventListener('click', event => {\n event.preventDefault()\n const target = event.target as HTMLElement\n const chatPane = target.closest(SELECTOR_DIRECT_CHAT) as HTMLElement | undefined\n\n if (chatPane) {\n const data = new DirectChat(chatPane)\n data.toggle()\n }\n })\n })\n})\n\nexport default DirectChat\n","/**\n * --------------------------------------------\n * @file AdminLTE card-widget.ts\n * @description Card widget for AdminLTE.\n * @license MIT\n * --------------------------------------------\n */\n\nimport {\n onDOMContentLoaded,\n slideUp,\n slideDown\n} from './util/index'\n\n/**\n * Constants\n * ====================================================\n */\n\nconst DATA_KEY = 'lte.card-widget'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst EVENT_COLLAPSED = `collapsed${EVENT_KEY}`\nconst EVENT_EXPANDED = `expanded${EVENT_KEY}`\nconst EVENT_REMOVE = `remove${EVENT_KEY}`\nconst EVENT_MAXIMIZED = `maximized${EVENT_KEY}`\nconst EVENT_MINIMIZED = `minimized${EVENT_KEY}`\n\nconst CLASS_NAME_CARD = 'card'\nconst CLASS_NAME_COLLAPSED = 'collapsed-card'\nconst CLASS_NAME_COLLAPSING = 'collapsing-card'\nconst CLASS_NAME_EXPANDING = 'expanding-card'\nconst CLASS_NAME_WAS_COLLAPSED = 'was-collapsed'\nconst CLASS_NAME_MAXIMIZED = 'maximized-card'\n\nconst SELECTOR_DATA_REMOVE = '[data-lte-toggle=\"card-remove\"]'\nconst SELECTOR_DATA_COLLAPSE = '[data-lte-toggle=\"card-collapse\"]'\nconst SELECTOR_DATA_MAXIMIZE = '[data-lte-toggle=\"card-maximize\"]'\nconst SELECTOR_CARD = `.${CLASS_NAME_CARD}`\nconst SELECTOR_CARD_BODY = '.card-body'\nconst SELECTOR_CARD_FOOTER = '.card-footer'\n\ntype Config = {\n animationSpeed: number;\n collapseTrigger: string;\n removeTrigger: string;\n maximizeTrigger: string;\n}\n\nconst Default: Config = {\n animationSpeed: 500,\n collapseTrigger: SELECTOR_DATA_COLLAPSE,\n removeTrigger: SELECTOR_DATA_REMOVE,\n maximizeTrigger: SELECTOR_DATA_MAXIMIZE\n}\n\nclass CardWidget {\n _element: HTMLElement\n _parent: HTMLElement | undefined\n _clone: HTMLElement | undefined\n _config: Config\n\n constructor(element: HTMLElement, config: Config) {\n this._element = element\n this._parent = element.closest(SELECTOR_CARD) as HTMLElement | undefined\n\n if (element.classList.contains(CLASS_NAME_CARD)) {\n this._parent = element\n }\n\n this._config = { ...Default, ...config }\n }\n\n collapse() {\n const event = new Event(EVENT_COLLAPSED)\n\n if (this._parent) {\n this._parent.classList.add(CLASS_NAME_COLLAPSING)\n\n const elm = this._parent?.querySelectorAll(`${SELECTOR_CARD_BODY}, ${SELECTOR_CARD_FOOTER}`)\n\n elm.forEach(el => {\n if (el instanceof HTMLElement) {\n slideUp(el, this._config.animationSpeed)\n }\n })\n\n setTimeout(() => {\n if (this._parent) {\n this._parent.classList.add(CLASS_NAME_COLLAPSED)\n this._parent.classList.remove(CLASS_NAME_COLLAPSING)\n }\n }, this._config.animationSpeed)\n }\n\n this._element?.dispatchEvent(event)\n }\n\n expand() {\n const event = new Event(EVENT_EXPANDED)\n\n if (this._parent) {\n this._parent.classList.add(CLASS_NAME_EXPANDING)\n\n const elm = this._parent?.querySelectorAll(`${SELECTOR_CARD_BODY}, ${SELECTOR_CARD_FOOTER}`)\n\n elm.forEach(el => {\n if (el instanceof HTMLElement) {\n slideDown(el, this._config.animationSpeed)\n }\n })\n\n setTimeout(() => {\n if (this._parent) {\n this._parent.classList.remove(CLASS_NAME_COLLAPSED)\n this._parent.classList.remove(CLASS_NAME_EXPANDING)\n }\n }, this._config.animationSpeed)\n }\n\n this._element?.dispatchEvent(event)\n }\n\n remove() {\n const event = new Event(EVENT_REMOVE)\n\n if (this._parent) {\n slideUp(this._parent, this._config.animationSpeed)\n }\n\n this._element?.dispatchEvent(event)\n }\n\n toggle() {\n if (this._parent?.classList.contains(CLASS_NAME_COLLAPSED)) {\n this.expand()\n return\n }\n\n this.collapse()\n }\n\n maximize() {\n const event = new Event(EVENT_MAXIMIZED)\n\n if (this._parent) {\n this._parent.style.height = `${this._parent.offsetHeight}px`\n this._parent.style.width = `${this._parent.offsetWidth}px`\n this._parent.style.transition = 'all .15s'\n\n setTimeout(() => {\n const htmlTag = document.querySelector('html')\n\n if (htmlTag) {\n htmlTag.classList.add(CLASS_NAME_MAXIMIZED)\n }\n\n if (this._parent) {\n this._parent.classList.add(CLASS_NAME_MAXIMIZED)\n\n if (this._parent.classList.contains(CLASS_NAME_COLLAPSED)) {\n this._parent.classList.add(CLASS_NAME_WAS_COLLAPSED)\n }\n }\n }, 150)\n }\n\n this._element?.dispatchEvent(event)\n }\n\n minimize() {\n const event = new Event(EVENT_MINIMIZED)\n\n if (this._parent) {\n this._parent.style.height = 'auto'\n this._parent.style.width = 'auto'\n this._parent.style.transition = 'all .15s'\n\n setTimeout(() => {\n const htmlTag = document.querySelector('html')\n\n if (htmlTag) {\n htmlTag.classList.remove(CLASS_NAME_MAXIMIZED)\n }\n\n if (this._parent) {\n this._parent.classList.remove(CLASS_NAME_MAXIMIZED)\n\n if (this._parent?.classList.contains(CLASS_NAME_WAS_COLLAPSED)) {\n this._parent.classList.remove(CLASS_NAME_WAS_COLLAPSED)\n }\n }\n }, 10)\n }\n\n this._element?.dispatchEvent(event)\n }\n\n toggleMaximize() {\n if (this._parent?.classList.contains(CLASS_NAME_MAXIMIZED)) {\n this.minimize()\n return\n }\n\n this.maximize()\n }\n}\n\n/**\n *\n * Data Api implementation\n * ====================================================\n */\n\nonDOMContentLoaded(() => {\n const collapseBtn = document.querySelectorAll(SELECTOR_DATA_COLLAPSE)\n\n collapseBtn.forEach(btn => {\n btn.addEventListener('click', event => {\n event.preventDefault()\n const target = event.target as HTMLElement\n const data = new CardWidget(target, Default)\n data.toggle()\n })\n })\n\n const removeBtn = document.querySelectorAll(SELECTOR_DATA_REMOVE)\n\n removeBtn.forEach(btn => {\n btn.addEventListener('click', event => {\n event.preventDefault()\n const target = event.target as HTMLElement\n const data = new CardWidget(target, Default)\n data.remove()\n })\n })\n\n const maxBtn = document.querySelectorAll(SELECTOR_DATA_MAXIMIZE)\n\n maxBtn.forEach(btn => {\n btn.addEventListener('click', event => {\n event.preventDefault()\n const target = event.target as HTMLElement\n const data = new CardWidget(target, Default)\n data.toggleMaximize()\n })\n })\n})\n\nexport default CardWidget\n","/**\n * --------------------------------------------\n * @file AdminLTE fullscreen.ts\n * @description Fullscreen plugin for AdminLTE.\n * @license MIT\n * --------------------------------------------\n */\n\nimport {\n onDOMContentLoaded\n} from './util/index'\n\n/**\n * Constants\n * ============================================================================\n */\nconst DATA_KEY = 'lte.fullscreen'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst EVENT_MAXIMIZED = `maximized${EVENT_KEY}`\nconst EVENT_MINIMIZED = `minimized${EVENT_KEY}`\n\nconst SELECTOR_FULLSCREEN_TOGGLE = '[data-lte-toggle=\"fullscreen\"]'\nconst SELECTOR_MAXIMIZE_ICON = '[data-lte-icon=\"maximize\"]'\nconst SELECTOR_MINIMIZE_ICON = '[data-lte-icon=\"minimize\"]'\n\n/**\n * Class Definition.\n * ============================================================================\n */\nclass FullScreen {\n _element: HTMLElement\n _config: undefined\n\n constructor(element: HTMLElement, config?: undefined) {\n this._element = element\n this._config = config\n }\n\n inFullScreen(): void {\n const event = new Event(EVENT_MAXIMIZED)\n\n const iconMaximize = document.querySelector<HTMLElement>(SELECTOR_MAXIMIZE_ICON)\n const iconMinimize = document.querySelector<HTMLElement>(SELECTOR_MINIMIZE_ICON)\n\n void document.documentElement.requestFullscreen()\n\n if (iconMaximize) {\n iconMaximize.style.display = 'none'\n }\n\n if (iconMinimize) {\n iconMinimize.style.display = 'block'\n }\n\n this._element.dispatchEvent(event)\n }\n\n outFullscreen(): void {\n const event = new Event(EVENT_MINIMIZED)\n\n const iconMaximize = document.querySelector<HTMLElement>(SELECTOR_MAXIMIZE_ICON)\n const iconMinimize = document.querySelector<HTMLElement>(SELECTOR_MINIMIZE_ICON)\n\n void document.exitFullscreen()\n\n if (iconMaximize) {\n iconMaximize.style.display = 'block'\n }\n\n if (iconMinimize) {\n iconMinimize.style.display = 'none'\n }\n\n this._element.dispatchEvent(event)\n }\n\n toggleFullScreen(): void {\n if (document.fullscreenEnabled) {\n if (document.fullscreenElement) {\n this.outFullscreen()\n } else {\n this.inFullScreen()\n }\n }\n }\n}\n\n/**\n * Data Api implementation\n * ============================================================================\n */\nonDOMContentLoaded(() => {\n const buttons = document.querySelectorAll(SELECTOR_FULLSCREEN_TOGGLE)\n\n buttons.forEach(btn => {\n btn.addEventListener('click', event => {\n event.preventDefault()\n\n const target = event.target as HTMLElement\n const button = target.closest(SELECTOR_FULLSCREEN_TOGGLE) as HTMLElement | undefined\n\n if (button) {\n const data = new FullScreen(button, undefined)\n data.toggleFullScreen()\n }\n })\n })\n})\n\nexport default FullScreen\n"],"mappings":"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","ignoreList":[]}
|
|
|
|
templates/admin/dashboard.html
DELETED
@@ -1,43 +0,0 @@
|
|
1 |
-
<!-- templates/dashboard.html -->
|
2 |
-
|
3 |
-
{% extends "main/base.html" %}
|
4 |
-
|
5 |
-
{% block title %}Admin Dashboard{% endblock %}
|
6 |
-
|
7 |
-
{% block content %}
|
8 |
-
<!-- Dashboard Header -->
|
9 |
-
|
10 |
-
<div class="row mb-4">
|
11 |
-
<div class="col-sm-6 text-right"> <small>Last Updated: {{ last_updated }} </small> </div>
|
12 |
-
</div> <!-- Real-Time Health Metrics and Inventory Summary -->
|
13 |
-
<div class="row">
|
14 |
-
<div class="col-md-6">
|
15 |
-
<div class="card">
|
16 |
-
<div class="card-header">
|
17 |
-
<h3 class="card-title">Real-Time Health Metrics</h3>
|
18 |
-
</div>
|
19 |
-
<div class="card-body"> <canvas id="realTimeHealthChart"></canvas> </div>
|
20 |
-
</div>
|
21 |
-
</div>
|
22 |
-
<div class="col-md-6">
|
23 |
-
<div class="card">
|
24 |
-
<div class="card-header">
|
25 |
-
<h3 class="card-title">Inventory Status</h3>
|
26 |
-
</div>
|
27 |
-
<div class="card-body"> <canvas id="inventoryChart"></canvas> </div>
|
28 |
-
</div>
|
29 |
-
</div>
|
30 |
-
</div> <!-- Historical Health Alerts -->
|
31 |
-
<div class="row mt-4">
|
32 |
-
<div class="col-md-12">
|
33 |
-
<div class="card">
|
34 |
-
<div class="card-header">
|
35 |
-
<h3 class="card-title">Historical Health Alerts</h3>
|
36 |
-
</div>
|
37 |
-
<div class="card-body"> <canvas id="historicalAlertsChart"></canvas> </div>
|
38 |
-
</div>
|
39 |
-
</div>
|
40 |
-
</div>
|
41 |
-
|
42 |
-
|
43 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/admin/group/add.html
DELETED
@@ -1,30 +0,0 @@
|
|
1 |
-
{% extends "main/base.html" %}
|
2 |
-
|
3 |
-
{% block title %}Add New Group{% endblock %}
|
4 |
-
|
5 |
-
{% block content %}
|
6 |
-
<div class="row g-4">
|
7 |
-
<div class="col-md-6">
|
8 |
-
<div class="card card-primary">
|
9 |
-
<div class="card-header">
|
10 |
-
<h3 class="card-title">Create New Group</h3>
|
11 |
-
</div>
|
12 |
-
<form method="POST" action="/group/add">
|
13 |
-
<div class="card-body">
|
14 |
-
<div class="form-group">
|
15 |
-
<label for="groupName">Group Name</label>
|
16 |
-
<input type="text" class="form-control" id="groupName" name="name" required>
|
17 |
-
</div>
|
18 |
-
<div class="form-group">
|
19 |
-
<label for="description">Description</label>
|
20 |
-
<textarea class="form-control" id="description" name="description"></textarea>
|
21 |
-
</div>
|
22 |
-
</div>
|
23 |
-
<div class="card-footer">
|
24 |
-
<button type="submit" class="btn btn-secondary shadow">Create Group</button>
|
25 |
-
</div>
|
26 |
-
</form>
|
27 |
-
</div>
|
28 |
-
</div>
|
29 |
-
</div>
|
30 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/admin/group/add_member.html
DELETED
@@ -1,30 +0,0 @@
|
|
1 |
-
{% extends "main/base.html" %}
|
2 |
-
|
3 |
-
{% block title %}Add Member{% endblock %}
|
4 |
-
|
5 |
-
{% block content %}
|
6 |
-
<div class="row g-4">
|
7 |
-
<div class="col-md-6">
|
8 |
-
<div class="card card-success">
|
9 |
-
<div class="card-header">
|
10 |
-
<h3 class="card-title">Add Member to {{ group.name }}</h3>
|
11 |
-
</div>
|
12 |
-
<form method="POST" action="/group/{{ group._id }}/add_member">
|
13 |
-
<div class="card-body">
|
14 |
-
<div class="form-group">
|
15 |
-
<label for="userId">Select Member</label>
|
16 |
-
<select class="form-control" id="userId" name="user_id">
|
17 |
-
{% for user in available_users %}
|
18 |
-
<option value="{{ user.username }}">{{ user.username }}</option>
|
19 |
-
{% endfor %}
|
20 |
-
</select>
|
21 |
-
</div>
|
22 |
-
</div>
|
23 |
-
<div class="card-footer">
|
24 |
-
<button type="submit" class="btn btn-success">Add Member</button>
|
25 |
-
</div>
|
26 |
-
</form>
|
27 |
-
</div>
|
28 |
-
</div>
|
29 |
-
</div>
|
30 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/admin/group/delete.html
DELETED
@@ -1,25 +0,0 @@
|
|
1 |
-
{% extends "main/base.html" %}
|
2 |
-
|
3 |
-
{% block title %}Delete Group{% endblock %}
|
4 |
-
|
5 |
-
{% block content %}
|
6 |
-
<div class="row g-4">
|
7 |
-
<div class="col-md-6">
|
8 |
-
<div class="card card-danger">
|
9 |
-
<div class="card-header">
|
10 |
-
<h3 class="card-title">Delete Group</h3>
|
11 |
-
</div>
|
12 |
-
<div class="card-body">
|
13 |
-
<p>Are you sure you want to delete the group <strong>{{ group.name }}</strong>?</p>
|
14 |
-
<p>This action cannot be undone.</p>
|
15 |
-
</div>
|
16 |
-
<div class="card-footer">
|
17 |
-
<form method="POST" action="/group/{{ group._id }}/delete" style="display:inline;">
|
18 |
-
<button type="submit" class="btn btn-danger">Yes, Delete Group</button>
|
19 |
-
</form>
|
20 |
-
<a href="/group/list" class="btn btn-secondary">Cancel</a>
|
21 |
-
</div>
|
22 |
-
</div>
|
23 |
-
</div>
|
24 |
-
</div>
|
25 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/admin/group/list.html
DELETED
@@ -1,45 +0,0 @@
|
|
1 |
-
{% extends "main/base.html" %}
|
2 |
-
|
3 |
-
{% block title %}Group List{% endblock %}
|
4 |
-
|
5 |
-
{% block content %}
|
6 |
-
<div class="row g-4">
|
7 |
-
<div class="col-md-6">
|
8 |
-
<div class="card">
|
9 |
-
<div class="card-header">
|
10 |
-
<h3 class="card-title">User Groups</h3>
|
11 |
-
<div class="card-tools">
|
12 |
-
<a href="/group/add" class="btn btn-secondary shadow btn-sm">Add New Group</a>
|
13 |
-
</div>
|
14 |
-
</div>
|
15 |
-
<div class="card-body">
|
16 |
-
<table class="table table-bordered table-hover">
|
17 |
-
<thead>
|
18 |
-
<tr>
|
19 |
-
<th>Group Name</th>
|
20 |
-
<th>Description</th>
|
21 |
-
<th>Created By</th>
|
22 |
-
<th>Members</th>
|
23 |
-
<th>Actions</th>
|
24 |
-
</tr>
|
25 |
-
</thead>
|
26 |
-
<tbody>
|
27 |
-
{% for group in groups %}
|
28 |
-
<tr>
|
29 |
-
<td>{{ group.name }}</td>
|
30 |
-
<td>{{ group.description }}</td>
|
31 |
-
<td>{{ group.created_by }}</td>
|
32 |
-
<td>{{ group.members | length }}</td>
|
33 |
-
<td>
|
34 |
-
<a href="/group/{{ group._id }}/view" class="btn btn-info btn-sm">View</a>
|
35 |
-
<a href="/group/{{ group._id }}/delete" class="btn btn-danger btn-sm">Delete</a>
|
36 |
-
</td>
|
37 |
-
</tr>
|
38 |
-
{% endfor %}
|
39 |
-
</tbody>
|
40 |
-
</table>
|
41 |
-
</div>
|
42 |
-
</div>
|
43 |
-
</div>
|
44 |
-
</div>
|
45 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/admin/group/share_task.html
DELETED
@@ -1,30 +0,0 @@
|
|
1 |
-
{% extends "main/base.html" %}
|
2 |
-
|
3 |
-
{% block title %}Share Task with Group{% endblock %}
|
4 |
-
|
5 |
-
{% block content %}
|
6 |
-
<div class="row g-4">
|
7 |
-
<div class="col-md-6">
|
8 |
-
<div class="card card-info">
|
9 |
-
<div class="card-header">
|
10 |
-
<h3 class="card-title">Share Task with {{ group.name }}</h3>
|
11 |
-
</div>
|
12 |
-
<form method="POST" action="/group/{{ group._id }}/share_task">
|
13 |
-
<div class="card-body">
|
14 |
-
<div class="form-group">
|
15 |
-
<label for="taskId">Select Task</label>
|
16 |
-
<select class="form-control" id="taskId" name="task_id">
|
17 |
-
{% for task in available_tasks %}
|
18 |
-
<option value="{{ task._id }}">{{ task.title }}</option>
|
19 |
-
{% endfor %}
|
20 |
-
</select>
|
21 |
-
</div>
|
22 |
-
</div>
|
23 |
-
<div class="card-footer">
|
24 |
-
<button type="submit" class="btn btn-info">Share Task</button>
|
25 |
-
</div>
|
26 |
-
</form>
|
27 |
-
</div>
|
28 |
-
</div>
|
29 |
-
</div>
|
30 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/admin/group/view.html
DELETED
@@ -1,52 +0,0 @@
|
|
1 |
-
{% extends "main/base.html" %}
|
2 |
-
|
3 |
-
{% block title %}Group Details{% endblock %}
|
4 |
-
|
5 |
-
{% block content %}
|
6 |
-
<div class="row g-4">
|
7 |
-
<div class="col-md-6">
|
8 |
-
<div class="card">
|
9 |
-
<div class="card-header">
|
10 |
-
<h3 class="card-title">{{ group.name }}</h3>
|
11 |
-
<div class="card-tools">
|
12 |
-
<a href="/group/{{ group._id }}/add_member" class="btn btn-success btn-sm">Add Member</a>
|
13 |
-
<a href="/group/{{ group._id }}/share_task" class="btn btn-info btn-sm">Share Task</a>
|
14 |
-
</div>
|
15 |
-
</div>
|
16 |
-
<div class="card-body">
|
17 |
-
<h5>Description</h5>
|
18 |
-
<p>{{ group.description }}</p>
|
19 |
-
|
20 |
-
<h5>Members</h5>
|
21 |
-
<ul>
|
22 |
-
{% for member in group.members %}
|
23 |
-
<li>{{ member }}</li>
|
24 |
-
{% endfor %}
|
25 |
-
</ul>
|
26 |
-
|
27 |
-
<h5>Shared Tasks</h5>
|
28 |
-
<table class="table table-bordered table-hover">
|
29 |
-
<thead>
|
30 |
-
<tr>
|
31 |
-
<th>Task Title</th>
|
32 |
-
<th>Description</th>
|
33 |
-
<th>Due Date</th>
|
34 |
-
<th>Status</th>
|
35 |
-
</tr>
|
36 |
-
</thead>
|
37 |
-
<tbody>
|
38 |
-
{% for task in tasks %}
|
39 |
-
<tr>
|
40 |
-
<td>{{ task.title }}</td>
|
41 |
-
<td>{{ task.description }}</td>
|
42 |
-
<td>{{ task.due_date }}</td>
|
43 |
-
<td>{{ 'Completed' if task.is_completed else 'Pending' }}</td>
|
44 |
-
</tr>
|
45 |
-
{% endfor %}
|
46 |
-
</tbody>
|
47 |
-
</table>
|
48 |
-
</div>
|
49 |
-
</div>
|
50 |
-
</div>
|
51 |
-
</div>
|
52 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/admin/inventory/add.html
DELETED
@@ -1,45 +0,0 @@
|
|
1 |
-
{% extends "main/base.html" %}
|
2 |
-
|
3 |
-
{% block title %}Add Inventory Item{% endblock %}
|
4 |
-
|
5 |
-
{% block content %}
|
6 |
-
<div class="row mb-4">
|
7 |
-
<div class="col-sm-6 text-right"> <small>Last Updated: {{ last_updated }} </small> </div>
|
8 |
-
</div>
|
9 |
-
<div class="row g-4">
|
10 |
-
<div class="col-md-6">
|
11 |
-
<div class="card card-secondary shadow">
|
12 |
-
<div class="card-header">
|
13 |
-
<div class="card-title">Add New Inventory Item</div>
|
14 |
-
</div>
|
15 |
-
<form method="POST" action="/inventory/add">
|
16 |
-
<div class="card-body">
|
17 |
-
<div class="form-group">
|
18 |
-
<label for="itemName">Item Name</label>
|
19 |
-
<input type="text" class="form-control" id="itemName" name="item_name" required>
|
20 |
-
</div>
|
21 |
-
<div class="form-group">
|
22 |
-
<label for="category">Category</label>
|
23 |
-
<input type="text" class="form-control" id="category" name="category" required>
|
24 |
-
</div>
|
25 |
-
<div class="form-group">
|
26 |
-
<label for="quantity">Quantity</label>
|
27 |
-
<input type="number" class="form-control" id="quantity" name="quantity" required>
|
28 |
-
</div>
|
29 |
-
<div class="form-group">
|
30 |
-
<label for="restockLevel">Restock Level</label>
|
31 |
-
<input type="number" class="form-control" id="restockLevel" name="restock_level" required>
|
32 |
-
</div>
|
33 |
-
<div class="form-group">
|
34 |
-
<label for="supplier">Supplier</label>
|
35 |
-
<input type="text" class="form-control" id="supplier" name="supplier">
|
36 |
-
</div>
|
37 |
-
</div>
|
38 |
-
<div class="card-footer">
|
39 |
-
<button type="submit" class="btn btn-secondary shadow">Add Item</button>
|
40 |
-
</div>
|
41 |
-
</form>
|
42 |
-
</div>
|
43 |
-
</div>
|
44 |
-
</div>
|
45 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/admin/inventory/delete.html
DELETED
@@ -1,31 +0,0 @@
|
|
1 |
-
{% extends "main/base.html" %}
|
2 |
-
|
3 |
-
{% block title %}Delete Inventory Item{% endblock %}
|
4 |
-
|
5 |
-
{% block content %}
|
6 |
-
<div class="row g-4">
|
7 |
-
<div class="col-md-6">
|
8 |
-
<div class="card card-danger">
|
9 |
-
<div class="card-header">
|
10 |
-
<h3 class="card-title">Delete Inventory Item</h3>
|
11 |
-
</div>
|
12 |
-
<div class="card-body">
|
13 |
-
<p>Are you sure you want to delete the following item?</p>
|
14 |
-
<ul>
|
15 |
-
<li><strong>Item Name:</strong> {{ item.item_name }}</li>
|
16 |
-
<li><strong>Category:</strong> {{ item.category }}</li>
|
17 |
-
<li><strong>Quantity:</strong> {{ item.quantity }}</li>
|
18 |
-
<li><strong>Restock Level:</strong> {{ item.restock_level }}</li>
|
19 |
-
<li><strong>Supplier:</strong> {{ item.supplier }}</li>
|
20 |
-
</ul>
|
21 |
-
</div>
|
22 |
-
<div class="card-footer">
|
23 |
-
<form method="POST" action="/admin/inventory/delete/{{ item._id }}" style="display:inline;">
|
24 |
-
<button type="submit" class="btn btn-danger">Yes, Delete</button>
|
25 |
-
</form>
|
26 |
-
<a href="admin/inventory" class="btn btn-secondary">Cancel</a>
|
27 |
-
</div>
|
28 |
-
</div>
|
29 |
-
</div>
|
30 |
-
</div>
|
31 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|