diff --git a/app/dashboard/backend/router.py b/app/dashboard/backend/router.py new file mode 100644 index 0000000..950e0b9 --- /dev/null +++ b/app/dashboard/backend/router.py @@ -0,0 +1,64 @@ +from fastapi import APIRouter, HTTPException +from app.core.database import execute_query +from typing import Dict, Any, List +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter() + +@router.get("/stats", response_model=Dict[str, Any]) +async def get_dashboard_stats(): + """ + Get aggregated statistics for the dashboard + """ + try: + logger.info("📊 Fetching dashboard stats...") + + # 1. Customer Counts + logger.info("Fetching customer count...") + customer_res = execute_query("SELECT COUNT(*) as count FROM customers WHERE deleted_at IS NULL", fetchone=True) + customer_count = customer_res['count'] if customer_res else 0 + + # 2. Contact Counts + logger.info("Fetching contact count...") + contact_res = execute_query("SELECT COUNT(*) as count FROM contacts", fetchone=True) + contact_count = contact_res['count'] if contact_res else 0 + + # 3. Vendor Counts + logger.info("Fetching vendor count...") + vendor_res = execute_query("SELECT COUNT(*) as count FROM vendors", fetchone=True) + vendor_count = vendor_res['count'] if vendor_res else 0 + + # 4. Recent Customers (Real "Activity") + logger.info("Fetching recent customers...") + recent_customers = execute_query(""" + SELECT id, name, created_at, 'customer' as type + FROM customers + WHERE deleted_at IS NULL + ORDER BY created_at DESC + LIMIT 5 + """) + + # 5. Vendor Categories Distribution + logger.info("Fetching vendor distribution...") + vendor_categories = execute_query(""" + SELECT category, COUNT(*) as count + FROM vendors + GROUP BY category + """) + + logger.info("✅ Dashboard stats fetched successfully") + return { + "counts": { + "customers": customer_count, + "contacts": contact_count, + "vendors": vendor_count + }, + "recent_activity": recent_customers or [], + "vendor_distribution": vendor_categories or [], + "system_status": "online" + } + except Exception as e: + logger.error(f"❌ Error fetching dashboard stats: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/dashboard/backend/views.py b/app/dashboard/backend/views.py index 5d66642..376969f 100644 --- a/app/dashboard/backend/views.py +++ b/app/dashboard/backend/views.py @@ -5,7 +5,7 @@ from fastapi.responses import HTMLResponse router = APIRouter() templates = Jinja2Templates(directory="app") -@router.get("/", response_class=HTMLResponse) +@router.get("/dashboard", response_class=HTMLResponse) async def dashboard(request: Request): """ Render the dashboard page diff --git a/app/dashboard/frontend/index.html b/app/dashboard/frontend/index.html index 14525b9..1e80c08 100644 --- a/app/dashboard/frontend/index.html +++ b/app/dashboard/frontend/index.html @@ -9,123 +9,199 @@

Velkommen tilbage, Christian

- - +
+ + +
+
+
-

Aktive Kunder

+

Kunder

+ +
+

-

+ Aktive i systemet +
+
+
+
+
+

Kontakter

-

124

- 12% denne måned +

-

+ Tilknyttede personer
-

Hardware

- +

Leverandører

+
-

856

- Enheder online +

-

+ Aktive leverandøraftaler
-

Support

- +

System Status

+
-

12

- 3 kræver handling -
-
-
-
-
-

Omsætning

- -
-

450k

- Over budget +

Online

+ v1.0.0
+
-
-
Seneste Aktiviteter
+
+
+
Seneste Tilføjelser
+ Se alle +
- - - - + + + + - + - - - - - - - - - - - - - - - - +
KundeHandlingStatusTidNavnTypeOprettetHandling
Advokatgruppen A/SFirewall konfigurationFuldført10:23
Byg & Bo ApSLicens fornyelseAfventerI går
Cafe MøllerNetværksnedbrudKritiskI går +
+
-
-
-
System Status
- -
-
- CPU LOAD - 24% -
-
-
-
-
-
-
- MEMORY - 56% -
-
-
+ +
+
+
Leverandør Fordeling
+
+
+
- -
-
- - Alle systemer kører optimalt. -
+
+ + +
-{% endblock %} \ No newline at end of file +{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/app/settings/backend/router.py b/app/settings/backend/router.py new file mode 100644 index 0000000..f5a8361 --- /dev/null +++ b/app/settings/backend/router.py @@ -0,0 +1,239 @@ +""" +Settings and User Management API Router +""" + +from fastapi import APIRouter, HTTPException +from typing import List, Optional, Dict +from pydantic import BaseModel +from app.core.database import execute_query +import logging + +logger = logging.getLogger(__name__) +router = APIRouter() + + +# Pydantic Models +class Setting(BaseModel): + id: int + key: str + value: Optional[str] + category: str + description: Optional[str] + value_type: str + is_public: bool + + +class SettingUpdate(BaseModel): + value: str + + +class User(BaseModel): + id: int + username: str + email: Optional[str] + full_name: Optional[str] + is_active: bool + last_login: Optional[str] + created_at: str + + +class UserCreate(BaseModel): + username: str + email: str + password: str + full_name: Optional[str] = None + + +class UserUpdate(BaseModel): + email: Optional[str] = None + full_name: Optional[str] = None + is_active: Optional[bool] = None + + +# Settings Endpoints +@router.get("/settings", response_model=List[Setting], tags=["Settings"]) +async def get_settings(category: Optional[str] = None): + """Get all settings or filter by category""" + query = "SELECT * FROM settings" + params = [] + + if category: + query += " WHERE category = %s" + params.append(category) + + query += " ORDER BY category, key" + result = execute_query(query, tuple(params) if params else None) + return result or [] + + +@router.get("/settings/{key}", response_model=Setting, tags=["Settings"]) +async def get_setting(key: str): + """Get a specific setting by key""" + query = "SELECT * FROM settings WHERE key = %s" + result = execute_query(query, (key,)) + + if not result: + raise HTTPException(status_code=404, detail="Setting not found") + + return result[0] + + +@router.put("/settings/{key}", response_model=Setting, tags=["Settings"]) +async def update_setting(key: str, setting: SettingUpdate): + """Update a setting value""" + query = """ + UPDATE settings + SET value = %s, updated_at = CURRENT_TIMESTAMP + WHERE key = %s + RETURNING * + """ + result = execute_query(query, (setting.value, key)) + + if not result: + raise HTTPException(status_code=404, detail="Setting not found") + + logger.info(f"✅ Updated setting: {key}") + return result[0] + + +@router.get("/settings/categories/list", tags=["Settings"]) +async def get_setting_categories(): + """Get list of all setting categories""" + query = "SELECT DISTINCT category FROM settings ORDER BY category" + result = execute_query(query) + return [row['category'] for row in result] if result else [] + + +# User Management Endpoints +@router.get("/users", response_model=List[User], tags=["Users"]) +async def get_users(is_active: Optional[bool] = None): + """Get all users""" + query = "SELECT user_id as id, username, email, full_name, is_active, last_login, created_at FROM users" + params = [] + + if is_active is not None: + query += " WHERE is_active = %s" + params.append(is_active) + + query += " ORDER BY username" + result = execute_query(query, tuple(params) if params else None) + return result or [] + + +@router.get("/users/{user_id}", response_model=User, tags=["Users"]) +async def get_user(user_id: int): + """Get user by ID""" + query = "SELECT user_id as id, username, email, full_name, is_active, last_login, created_at FROM users WHERE user_id = %s" + result = execute_query(query, (user_id,)) + + if not result: + raise HTTPException(status_code=404, detail="User not found") + + return result[0] + + +@router.post("/users", response_model=User, tags=["Users"]) +async def create_user(user: UserCreate): + """Create a new user""" + # Check if username exists + existing = execute_query("SELECT user_id FROM users WHERE username = %s", (user.username,)) + if existing: + raise HTTPException(status_code=400, detail="Username already exists") + + # Hash password (simple SHA256 for now - should use bcrypt in production) + import hashlib + password_hash = hashlib.sha256(user.password.encode()).hexdigest() + + query = """ + INSERT INTO users (username, email, password_hash, full_name, is_active) + VALUES (%s, %s, %s, %s, true) + RETURNING user_id as id, username, email, full_name, is_active, last_login, created_at + """ + result = execute_query(query, (user.username, user.email, password_hash, user.full_name)) + + if not result: + raise HTTPException(status_code=500, detail="Failed to create user") + + logger.info(f"✅ Created user: {user.username}") + return result[0] + + +@router.put("/users/{user_id}", response_model=User, tags=["Users"]) +async def update_user(user_id: int, user: UserUpdate): + """Update user details""" + # Check if user exists + existing = execute_query("SELECT user_id FROM users WHERE user_id = %s", (user_id,)) + if not existing: + raise HTTPException(status_code=404, detail="User not found") + + # Build update query + update_fields = [] + params = [] + + if user.email is not None: + update_fields.append("email = %s") + params.append(user.email) + if user.full_name is not None: + update_fields.append("full_name = %s") + params.append(user.full_name) + if user.is_active is not None: + update_fields.append("is_active = %s") + params.append(user.is_active) + + if not update_fields: + raise HTTPException(status_code=400, detail="No fields to update") + + params.append(user_id) + query = f""" + UPDATE users + SET {', '.join(update_fields)}, updated_at = CURRENT_TIMESTAMP + WHERE user_id = %s + RETURNING user_id as id, username, email, full_name, is_active, last_login, created_at + """ + + result = execute_query(query, tuple(params)) + + if not result: + raise HTTPException(status_code=500, detail="Failed to update user") + + logger.info(f"✅ Updated user: {user_id}") + return result[0] + + +@router.delete("/users/{user_id}", tags=["Users"]) +async def deactivate_user(user_id: int): + """Deactivate a user (soft delete)""" + query = """ + UPDATE users + SET is_active = false, updated_at = CURRENT_TIMESTAMP + WHERE user_id = %s + RETURNING user_id as id + """ + result = execute_query(query, (user_id,)) + + if not result: + raise HTTPException(status_code=404, detail="User not found") + + logger.info(f"✅ Deactivated user: {user_id}") + return {"message": "User deactivated successfully"} + + +@router.post("/users/{user_id}/reset-password", tags=["Users"]) +async def reset_user_password(user_id: int, new_password: str): + """Reset user password""" + import hashlib + password_hash = hashlib.sha256(new_password.encode()).hexdigest() + + query = """ + UPDATE users + SET password_hash = %s, updated_at = CURRENT_TIMESTAMP + WHERE user_id = %s + RETURNING user_id as id + """ + result = execute_query(query, (password_hash, user_id)) + + if not result: + raise HTTPException(status_code=404, detail="User not found") + + logger.info(f"✅ Reset password for user: {user_id}") + return {"message": "Password reset successfully"} diff --git a/app/settings/backend/views.py b/app/settings/backend/views.py new file mode 100644 index 0000000..a8f396b --- /dev/null +++ b/app/settings/backend/views.py @@ -0,0 +1,19 @@ +""" +Settings Frontend Views +""" + +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates + +router = APIRouter() +templates = Jinja2Templates(directory="app") + + +@router.get("/settings", response_class=HTMLResponse, tags=["Frontend"]) +async def settings_page(request: Request): + """Render settings page""" + return templates.TemplateResponse("settings/frontend/settings.html", { + "request": request, + "title": "Indstillinger" + }) diff --git a/app/settings/frontend/settings.html b/app/settings/frontend/settings.html new file mode 100644 index 0000000..e2b5a83 --- /dev/null +++ b/app/settings/frontend/settings.html @@ -0,0 +1,508 @@ +{% extends "shared/frontend/base.html" %} + +{% block title %}Indstillinger - BMC Hub{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+
+

Indstillinger

+

System konfiguration og brugerstyring

+
+
+ +
+ + + + +
+
+ +
+
+
Firma Oplysninger
+
+
+
+
+
+
+
+ + +
+
+
vTiger CRM
+
+
+
+
+
+
+ +
+
e-conomic
+
+
+
+
+
+
+
+ + +
+
+
Notifikation Indstillinger
+
+
+
+
+
+
+
+ + +
+
+
+
Brugerstyring
+ +
+
+ + + + + + + + + + + + + + + + +
BrugerEmailStatusSidst LoginOprettetHandlinger
+
+
+
+
+
+ + +
+
+
System Indstillinger
+
+
+
+
+
+
+
+
+
+
+ + + + +{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/app/shared/frontend/base.html b/app/shared/frontend/base.html index a5f8ca3..37dc81d 100644 --- a/app/shared/frontend/base.html +++ b/app/shared/frontend/base.html @@ -170,7 +170,7 @@