Creating admin interface
This commit is contained in:
@@ -216,10 +216,67 @@ The API sends real-time updates via WebSocket for:
|
|||||||
- **Username:** `admin`
|
- **Username:** `admin`
|
||||||
- **Password:** `admin123`
|
- **Password:** `admin123`
|
||||||
|
|
||||||
|
## Journal System
|
||||||
|
|
||||||
|
All PPR changes are automatically logged in a journal system for audit trail purposes.
|
||||||
|
|
||||||
|
### Get PPR Journal Entries
|
||||||
|
|
||||||
|
**Endpoint:** `GET /api/v1/pprs/{ppr_id}/journal`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"ppr_id": 3,
|
||||||
|
"entry": "PPR created for G-ADMIN",
|
||||||
|
"user": "admin",
|
||||||
|
"ip": "172.23.0.1",
|
||||||
|
"entry_dt": "2025-10-21T20:01:01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"ppr_id": 3,
|
||||||
|
"entry": "captain changed from 'Test Admin' to 'Updated Admin User'",
|
||||||
|
"user": "admin",
|
||||||
|
"ip": "172.23.0.1",
|
||||||
|
"entry_dt": "2025-10-21T20:01:17"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Journal Logging
|
||||||
|
|
||||||
|
The system automatically logs:
|
||||||
|
- PPR creation
|
||||||
|
- All field changes with old and new values
|
||||||
|
- Status changes
|
||||||
|
- User and IP address for each change
|
||||||
|
|
||||||
|
## Web Interfaces
|
||||||
|
|
||||||
|
### Public Arrivals/Departures Board
|
||||||
|
- **URL:** http://localhost:8082
|
||||||
|
- **Features:** Real-time arrivals and departures display
|
||||||
|
- **Authentication:** None required
|
||||||
|
|
||||||
|
### Admin Interface
|
||||||
|
- **URL:** http://localhost:8082/admin.html
|
||||||
|
- **Features:**
|
||||||
|
- Complete PPR management (CRUD operations)
|
||||||
|
- Advanced filtering by status, date range
|
||||||
|
- Inline editing with modal interface
|
||||||
|
- Journal/audit trail viewing
|
||||||
|
- Quick status updates (Confirm, Land, Depart, Cancel)
|
||||||
|
- New PPR entry creation
|
||||||
|
- **Authentication:** Username/password prompt (uses API token)
|
||||||
|
|
||||||
## Development URLs
|
## Development URLs
|
||||||
|
|
||||||
- API Base: http://localhost:8001/api/v1
|
- API Base: http://localhost:8001/api/v1
|
||||||
- Public Web Interface: http://localhost:8082
|
- Public Web Interface: http://localhost:8082
|
||||||
|
- Admin Interface: http://localhost:8082/admin.html
|
||||||
- API Documentation: http://localhost:8001/docs
|
- API Documentation: http://localhost:8001/docs
|
||||||
- Database: localhost:3307 (MySQL)
|
- Database: localhost:3307 (MySQL)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ from sqlalchemy.orm import Session
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from app.api.deps import get_db, get_current_active_user
|
from app.api.deps import get_db, get_current_active_user
|
||||||
from app.crud.crud_ppr import ppr as crud_ppr
|
from app.crud.crud_ppr import ppr as crud_ppr
|
||||||
from app.schemas.ppr import PPR, PPRCreate, PPRUpdate, PPRStatus, PPRStatusUpdate
|
from app.crud.crud_journal import journal as crud_journal
|
||||||
|
from app.schemas.ppr import PPR, PPRCreate, PPRUpdate, PPRStatus, PPRStatusUpdate, Journal
|
||||||
from app.models.ppr import User
|
from app.models.ppr import User
|
||||||
|
from app.core.utils import get_client_ip
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -37,7 +39,8 @@ async def create_ppr(
|
|||||||
current_user: User = Depends(get_current_active_user)
|
current_user: User = Depends(get_current_active_user)
|
||||||
):
|
):
|
||||||
"""Create a new PPR record"""
|
"""Create a new PPR record"""
|
||||||
ppr = crud_ppr.create(db, obj_in=ppr_in, created_by=current_user.username)
|
client_ip = get_client_ip(request)
|
||||||
|
ppr = crud_ppr.create(db, obj_in=ppr_in, created_by=current_user.username, user_ip=client_ip)
|
||||||
|
|
||||||
# Send real-time update via WebSocket
|
# Send real-time update via WebSocket
|
||||||
if hasattr(request.app.state, 'connection_manager'):
|
if hasattr(request.app.state, 'connection_manager'):
|
||||||
@@ -85,7 +88,8 @@ async def update_ppr(
|
|||||||
detail="PPR record not found"
|
detail="PPR record not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in)
|
client_ip = get_client_ip(request)
|
||||||
|
ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in, user=current_user.username, user_ip=client_ip)
|
||||||
|
|
||||||
# Send real-time update
|
# Send real-time update
|
||||||
if hasattr(request.app.state, 'connection_manager'):
|
if hasattr(request.app.state, 'connection_manager'):
|
||||||
@@ -117,8 +121,8 @@ async def patch_ppr(
|
|||||||
detail="PPR record not found"
|
detail="PPR record not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# For PATCH, we only update fields that are explicitly provided (not None)
|
client_ip = get_client_ip(request)
|
||||||
ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in)
|
ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in, user=current_user.username, user_ip=client_ip)
|
||||||
|
|
||||||
# Send real-time update
|
# Send real-time update
|
||||||
if hasattr(request.app.state, 'connection_manager'):
|
if hasattr(request.app.state, 'connection_manager'):
|
||||||
@@ -143,15 +147,20 @@ async def update_ppr_status(
|
|||||||
current_user: User = Depends(get_current_active_user)
|
current_user: User = Depends(get_current_active_user)
|
||||||
):
|
):
|
||||||
"""Update PPR status (LANDED, DEPARTED, etc.)"""
|
"""Update PPR status (LANDED, DEPARTED, etc.)"""
|
||||||
ppr = crud_ppr.update_status(db, ppr_id=ppr_id, status=status_update.status)
|
client_ip = get_client_ip(request)
|
||||||
|
ppr = crud_ppr.update_status(
|
||||||
|
db,
|
||||||
|
ppr_id=ppr_id,
|
||||||
|
status=status_update.status,
|
||||||
|
user=current_user.username,
|
||||||
|
user_ip=client_ip
|
||||||
|
)
|
||||||
if not ppr:
|
if not ppr:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="PPR record not found"
|
detail="PPR record not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Log the status change (you might want to create a journal entry here)
|
|
||||||
|
|
||||||
# Send real-time update
|
# Send real-time update
|
||||||
if hasattr(request.app.state, 'connection_manager'):
|
if hasattr(request.app.state, 'connection_manager'):
|
||||||
await request.app.state.connection_manager.broadcast({
|
await request.app.state.connection_manager.broadcast({
|
||||||
@@ -175,7 +184,8 @@ async def delete_ppr(
|
|||||||
current_user: User = Depends(get_current_active_user)
|
current_user: User = Depends(get_current_active_user)
|
||||||
):
|
):
|
||||||
"""Delete (soft delete) a PPR record"""
|
"""Delete (soft delete) a PPR record"""
|
||||||
ppr = crud_ppr.delete(db, ppr_id=ppr_id)
|
client_ip = get_client_ip(request)
|
||||||
|
ppr = crud_ppr.delete(db, ppr_id=ppr_id, user=current_user.username, user_ip=client_ip)
|
||||||
if not ppr:
|
if not ppr:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
@@ -193,3 +203,21 @@ async def delete_ppr(
|
|||||||
})
|
})
|
||||||
|
|
||||||
return ppr
|
return ppr
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{ppr_id}/journal", response_model=List[Journal])
|
||||||
|
async def get_ppr_journal(
|
||||||
|
ppr_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
"""Get journal entries for a specific PPR"""
|
||||||
|
# Verify PPR exists
|
||||||
|
ppr = crud_ppr.get(db, ppr_id=ppr_id)
|
||||||
|
if not ppr:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="PPR record not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return crud_journal.get_by_ppr_id(db, ppr_id=ppr_id)
|
||||||
14
backend/app/core/utils.py
Normal file
14
backend/app/core/utils.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_ip(request: Request) -> str:
|
||||||
|
"""Extract client IP address from request"""
|
||||||
|
forwarded = request.headers.get("X-Forwarded-For")
|
||||||
|
if forwarded:
|
||||||
|
return forwarded.split(",")[0].strip()
|
||||||
|
|
||||||
|
real_ip = request.headers.get("X-Real-IP")
|
||||||
|
if real_ip:
|
||||||
|
return real_ip
|
||||||
|
|
||||||
|
return request.client.host if request.client else "127.0.0.1"
|
||||||
35
backend/app/crud/crud_journal.py
Normal file
35
backend/app/crud/crud_journal.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from typing import List
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.models.ppr import Journal
|
||||||
|
from app.schemas.ppr import JournalCreate
|
||||||
|
|
||||||
|
|
||||||
|
class CRUDJournal:
|
||||||
|
def create(self, db: Session, obj_in: JournalCreate) -> Journal:
|
||||||
|
db_obj = Journal(**obj_in.dict())
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def get_by_ppr_id(self, db: Session, ppr_id: int) -> List[Journal]:
|
||||||
|
return db.query(Journal).filter(Journal.ppr_id == ppr_id).order_by(Journal.entry_dt.desc()).all()
|
||||||
|
|
||||||
|
def log_change(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
ppr_id: int,
|
||||||
|
entry: str,
|
||||||
|
user: str,
|
||||||
|
ip: str
|
||||||
|
) -> Journal:
|
||||||
|
journal_in = JournalCreate(
|
||||||
|
ppr_id=ppr_id,
|
||||||
|
entry=entry,
|
||||||
|
user=user,
|
||||||
|
ip=ip
|
||||||
|
)
|
||||||
|
return self.create(db, journal_in)
|
||||||
|
|
||||||
|
|
||||||
|
journal = CRUDJournal()
|
||||||
@@ -4,6 +4,7 @@ from sqlalchemy import and_, or_, func, desc
|
|||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from app.models.ppr import PPRRecord, PPRStatus
|
from app.models.ppr import PPRRecord, PPRStatus
|
||||||
from app.schemas.ppr import PPRCreate, PPRUpdate
|
from app.schemas.ppr import PPRCreate, PPRUpdate
|
||||||
|
from app.crud.crud_journal import journal as crud_journal
|
||||||
|
|
||||||
|
|
||||||
class CRUDPPR:
|
class CRUDPPR:
|
||||||
@@ -62,7 +63,7 @@ class CRUDPPR:
|
|||||||
)
|
)
|
||||||
).order_by(PPRRecord.etd).all()
|
).order_by(PPRRecord.etd).all()
|
||||||
|
|
||||||
def create(self, db: Session, obj_in: PPRCreate, created_by: str) -> PPRRecord:
|
def create(self, db: Session, obj_in: PPRCreate, created_by: str, user_ip: str = "127.0.0.1") -> PPRRecord:
|
||||||
db_obj = PPRRecord(
|
db_obj = PPRRecord(
|
||||||
**obj_in.dict(),
|
**obj_in.dict(),
|
||||||
created_by=created_by,
|
created_by=created_by,
|
||||||
@@ -71,28 +72,52 @@ class CRUDPPR:
|
|||||||
db.add(db_obj)
|
db.add(db_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_obj)
|
db.refresh(db_obj)
|
||||||
|
|
||||||
|
# Log creation in journal
|
||||||
|
crud_journal.log_change(
|
||||||
|
db,
|
||||||
|
db_obj.id,
|
||||||
|
f"PPR created for {db_obj.ac_reg}",
|
||||||
|
created_by,
|
||||||
|
user_ip
|
||||||
|
)
|
||||||
|
|
||||||
return db_obj
|
return db_obj
|
||||||
|
|
||||||
def update(self, db: Session, db_obj: PPRRecord, obj_in: PPRUpdate) -> PPRRecord:
|
def update(self, db: Session, db_obj: PPRRecord, obj_in: PPRUpdate, user: str = "system", user_ip: str = "127.0.0.1") -> PPRRecord:
|
||||||
update_data = obj_in.dict(exclude_unset=True)
|
update_data = obj_in.dict(exclude_unset=True)
|
||||||
for field, value in update_data.items():
|
changes = []
|
||||||
setattr(db_obj, field, value)
|
|
||||||
|
for field, value in update_data.items():
|
||||||
|
old_value = getattr(db_obj, field)
|
||||||
|
if old_value != value:
|
||||||
|
changes.append(f"{field} changed from '{old_value}' to '{value}'")
|
||||||
|
setattr(db_obj, field, value)
|
||||||
|
|
||||||
|
if changes:
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
|
||||||
|
# Log changes in journal
|
||||||
|
for change in changes:
|
||||||
|
crud_journal.log_change(db, db_obj.id, change, user, user_ip)
|
||||||
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
return db_obj
|
||||||
|
|
||||||
def update_status(
|
def update_status(
|
||||||
self,
|
self,
|
||||||
db: Session,
|
db: Session,
|
||||||
ppr_id: int,
|
ppr_id: int,
|
||||||
status: PPRStatus
|
status: PPRStatus,
|
||||||
|
user: str = "system",
|
||||||
|
user_ip: str = "127.0.0.1"
|
||||||
) -> Optional[PPRRecord]:
|
) -> Optional[PPRRecord]:
|
||||||
db_obj = self.get(db, ppr_id)
|
db_obj = self.get(db, ppr_id)
|
||||||
if not db_obj:
|
if not db_obj:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
old_status = db_obj.status
|
||||||
db_obj.status = status
|
db_obj.status = status
|
||||||
|
|
||||||
# Set timestamps based on status
|
# Set timestamps based on status
|
||||||
@@ -104,16 +129,36 @@ class CRUDPPR:
|
|||||||
db.add(db_obj)
|
db.add(db_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_obj)
|
db.refresh(db_obj)
|
||||||
|
|
||||||
|
# Log status change in journal
|
||||||
|
crud_journal.log_change(
|
||||||
|
db,
|
||||||
|
db_obj.id,
|
||||||
|
f"Status changed from {old_status.value} to {status.value}",
|
||||||
|
user,
|
||||||
|
user_ip
|
||||||
|
)
|
||||||
|
|
||||||
return db_obj
|
return db_obj
|
||||||
|
|
||||||
def delete(self, db: Session, ppr_id: int) -> Optional[PPRRecord]:
|
def delete(self, db: Session, ppr_id: int, user: str = "system", user_ip: str = "127.0.0.1") -> Optional[PPRRecord]:
|
||||||
db_obj = self.get(db, ppr_id)
|
db_obj = self.get(db, ppr_id)
|
||||||
if db_obj:
|
if db_obj:
|
||||||
|
old_status = db_obj.status
|
||||||
# Soft delete by setting status
|
# Soft delete by setting status
|
||||||
db_obj.status = PPRStatus.DELETED
|
db_obj.status = PPRStatus.DELETED
|
||||||
db.add(db_obj)
|
db.add(db_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_obj)
|
db.refresh(db_obj)
|
||||||
|
|
||||||
|
# Log the deletion in journal
|
||||||
|
crud_journal.log_change(
|
||||||
|
db,
|
||||||
|
db_obj.id,
|
||||||
|
f"PPR marked as DELETED (was {old_status.value})",
|
||||||
|
user,
|
||||||
|
user_ip
|
||||||
|
)
|
||||||
return db_obj
|
return db_obj
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1078
web/admin.html
Normal file
1078
web/admin.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user