Fred808 commited on
Commit
b70ff07
·
verified ·
1 Parent(s): f52867b

Upload 86 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .coverage +0 -0
  2. .dockerignore +43 -0
  3. .env +14 -0
  4. Dockerfile +58 -0
  5. alembic.ini +77 -0
  6. alembic/README +1 -0
  7. alembic/__pycache__/env.cpython-312.pyc +0 -0
  8. alembic/env.py +63 -0
  9. alembic/script.py.mako +26 -0
  10. app/__init__.py +0 -0
  11. app/__pycache__/__init__.cpython-312.pyc +0 -0
  12. app/__pycache__/main.cpython-312.pyc +0 -0
  13. app/api/__init__.py +0 -0
  14. app/api/__pycache__/__init__.cpython-312.pyc +0 -0
  15. app/api/__pycache__/analytics.cpython-312.pyc +0 -0
  16. app/api/__pycache__/auth.cpython-312.pyc +0 -0
  17. app/api/__pycache__/calendar.cpython-312.pyc +0 -0
  18. app/api/__pycache__/files.cpython-312.pyc +0 -0
  19. app/api/__pycache__/maintenance.cpython-312.pyc +0 -0
  20. app/api/__pycache__/notifications.cpython-312.pyc +0 -0
  21. app/api/__pycache__/orders.cpython-312.pyc +0 -0
  22. app/api/__pycache__/products.cpython-312.pyc +0 -0
  23. app/api/__pycache__/scheduler.cpython-312.pyc +0 -0
  24. app/api/__pycache__/users.cpython-312.pyc +0 -0
  25. app/api/analytics.py +232 -0
  26. app/api/auth.py +70 -0
  27. app/api/branches.py +89 -0
  28. app/api/calendar.py +156 -0
  29. app/api/files.py +53 -0
  30. app/api/maintenance.py +133 -0
  31. app/api/notifications.py +86 -0
  32. app/api/orders.py +186 -0
  33. app/api/products.py +131 -0
  34. app/api/scheduler.py +203 -0
  35. app/api/users.py +127 -0
  36. app/core/__init__.py +0 -0
  37. app/core/__pycache__/__init__.cpython-312.pyc +0 -0
  38. app/core/__pycache__/config.cpython-312.pyc +0 -0
  39. app/core/__pycache__/dependencies.cpython-312.pyc +0 -0
  40. app/core/__pycache__/security.cpython-312.pyc +0 -0
  41. app/core/config.py +36 -0
  42. app/core/dependencies.py +52 -0
  43. app/core/security.py +23 -0
  44. app/db/__init__.py +0 -0
  45. app/db/__pycache__/__init__.cpython-312.pyc +0 -0
  46. app/db/__pycache__/database.cpython-312.pyc +0 -0
  47. app/db/__pycache__/models.cpython-312.pyc +0 -0
  48. app/db/__pycache__/schemas.cpython-312.pyc +0 -0
  49. app/db/database.py +55 -0
  50. app/db/init_db.py +77 -0
.coverage ADDED
Binary file (53.2 kB). View file
 
.dockerignore ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Git
2
+ .git
3
+ .gitignore
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+ .Python
11
+ env/
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+
28
+ # Virtual Environment
29
+ venv/
30
+ ENV/
31
+
32
+ # IDE
33
+ .idea/
34
+ .vscode/
35
+ *.swp
36
+ *.swo
37
+
38
+ # Project specific
39
+ logs/
40
+ uploads/
41
+ backups/
42
+ .env
43
+ *.log
.env ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ PROJECT_NAME=Admin Dashboard API
2
+ VERSION=1.0.0
3
+ API_V1_STR=/api/v1
4
+
5
+ # Security
6
+ SECRET_KEY=your-secret-key-here-change-in-production
7
+ ACCESS_TOKEN_EXPIRE_MINUTES=30
8
+ ALGORITHM=HS256
9
+
10
+ # Database
11
+ DATABASE_URL=postgresql+asyncpg://postgres:Lovyelias5584.@db.juycnkjuzylnbruwaqmp.supabase.co:5432/postgres
12
+ # Redis Cache
13
+ REDIS_HOST=localhost
14
+ REDIS_PORT=6379
Dockerfile ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.11 slim as base image
2
+ FROM python:3.11-slim as builder
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ gcc \
10
+ libpq-dev \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Install Python dependencies
14
+ COPY requirements.txt .
15
+ RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
16
+
17
+ # Final stage
18
+ FROM python:3.11-slim
19
+
20
+ # Create non-root user
21
+ RUN useradd -m appuser
22
+
23
+ # Set working directory
24
+ WORKDIR /app
25
+
26
+ # Install system dependencies
27
+ RUN apt-get update && apt-get install -y \
28
+ libpq5 \
29
+ && rm -rf /var/lib/apt/lists/*
30
+
31
+ # Copy wheels from builder stage
32
+ COPY --from=builder /app/wheels /wheels
33
+ COPY --from=builder /app/requirements.txt .
34
+
35
+ # Install Python packages
36
+ RUN pip install --no-cache /wheels/*
37
+
38
+ # Copy application code
39
+ COPY ./app app/
40
+ COPY ./alembic.ini .
41
+ COPY ./alembic alembic/
42
+
43
+ # Create necessary directories with proper permissions
44
+ RUN mkdir -p /app/logs /app/uploads/images /app/uploads/documents /app/backups && \
45
+ chown -R appuser:appuser /app
46
+
47
+ # Switch to non-root user
48
+ USER appuser
49
+
50
+ # Set environment variables
51
+ ENV PYTHONPATH=/app \
52
+ PYTHONUNBUFFERED=1
53
+
54
+ # Expose port
55
+ EXPOSE 8000
56
+
57
+ # Start the application with uvicorn
58
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
alembic.ini ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts
5
+ script_location = alembic
6
+
7
+ # template used to generate migration files
8
+ file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(rev)s_%%(slug)s
9
+
10
+ # timezone to use when rendering the date within the migration file
11
+ # as well as the filename.
12
+ timezone = UTC
13
+
14
+ # max length of characters to apply to the "slug" field
15
+ truncate_slug_length = 40
16
+
17
+ # set to 'true' to run the environment during
18
+ # the 'revision' command, regardless of autogenerate
19
+ revision_environment = false
20
+
21
+ # set to 'true' to allow .pyc and .pyo files without
22
+ # a source .py file to be detected as revisions in the
23
+ # versions/ directory
24
+ sourceless = false
25
+
26
+ # version location specification
27
+ version_locations = alembic/versions
28
+
29
+ # version path separator
30
+ version_path_separator = os
31
+
32
+ # the output encoding used when revision files
33
+ # are written from script.py.mako
34
+ output_encoding = utf-8
35
+
36
+ sqlalchemy.url = postgresql+psycopg2://postgres:Lovyelias5584.@db.mqyrkmsdgugdhxiucukb.supabase.co:5432/postgres
37
+
38
+ [post_write_hooks]
39
+ # format using "black"
40
+ hooks = black
41
+ black.type = console_scripts
42
+ black.entrypoint = black
43
+ black.options = -l 79 REVISION_SCRIPT_FILENAME
44
+
45
+ [loggers]
46
+ keys = root,sqlalchemy,alembic
47
+
48
+ [handlers]
49
+ keys = console
50
+
51
+ [formatters]
52
+ keys = generic
53
+
54
+ [logger_root]
55
+ level = WARN
56
+ handlers = console
57
+ qualname =
58
+
59
+ [logger_sqlalchemy]
60
+ level = WARN
61
+ handlers =
62
+ qualname = sqlalchemy.engine
63
+
64
+ [logger_alembic]
65
+ level = INFO
66
+ handlers =
67
+ qualname = alembic
68
+
69
+ [handler_console]
70
+ class = StreamHandler
71
+ args = (sys.stderr,)
72
+ level = NOTSET
73
+ formatter = generic
74
+
75
+ [formatter_generic]
76
+ format = %(levelname)-5.5s [%(name)s] %(message)s
77
+ datefmt = %H:%M:%S
alembic/README ADDED
@@ -0,0 +1 @@
 
 
1
+ Generic single-database configuration.
alembic/__pycache__/env.cpython-312.pyc ADDED
Binary file (2.93 kB). View file
 
alembic/env.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from logging.config import fileConfig
2
+ from sqlalchemy import engine_from_config
3
+ from sqlalchemy import pool
4
+ from alembic import context
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ # Add the parent directory to the Python path
10
+ parent_dir = str(Path(__file__).resolve().parents[1])
11
+ sys.path.append(parent_dir)
12
+
13
+ from app.core.config import settings
14
+ from app.db.models import Base
15
+
16
+ config = context.config
17
+
18
+ if config.config_file_name is not None:
19
+ fileConfig(config.config_file_name)
20
+
21
+ def get_url():
22
+ return str(settings.DATABASE_URL).replace("+asyncpg", "+psycopg2")
23
+
24
+ config.set_main_option("sqlalchemy.url", get_url())
25
+
26
+ target_metadata = Base.metadata
27
+
28
+ def run_migrations_offline() -> None:
29
+ """Run migrations in 'offline' mode."""
30
+ url = get_url()
31
+ context.configure(
32
+ url=url,
33
+ target_metadata=target_metadata,
34
+ literal_binds=True,
35
+ dialect_opts={"paramstyle": "named"},
36
+ )
37
+
38
+ with context.begin_transaction():
39
+ context.run_migrations()
40
+
41
+ def run_migrations_online() -> None:
42
+ """Run migrations in 'online' mode."""
43
+ configuration = config.get_section(config.config_ini_section)
44
+ configuration["sqlalchemy.url"] = get_url()
45
+ connectable = engine_from_config(
46
+ configuration,
47
+ prefix="sqlalchemy.",
48
+ poolclass=pool.NullPool,
49
+ )
50
+
51
+ with connectable.connect() as connection:
52
+ context.configure(
53
+ connection=connection,
54
+ target_metadata=target_metadata
55
+ )
56
+
57
+ with context.begin_transaction():
58
+ context.run_migrations()
59
+
60
+ if context.is_offline_mode():
61
+ run_migrations_offline()
62
+ else:
63
+ run_migrations_online()
alembic/script.py.mako ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ ${upgrades if upgrades else "pass"}
23
+
24
+
25
+ def downgrade() -> None:
26
+ ${downgrades if downgrades else "pass"}
app/__init__.py ADDED
File without changes
app/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (168 Bytes). View file
 
app/__pycache__/main.cpython-312.pyc ADDED
Binary file (6 kB). View file
 
app/api/__init__.py ADDED
File without changes
app/api/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (172 Bytes). View file
 
app/api/__pycache__/analytics.cpython-312.pyc ADDED
Binary file (11.4 kB). View file
 
app/api/__pycache__/auth.cpython-312.pyc ADDED
Binary file (3.42 kB). View file
 
app/api/__pycache__/calendar.cpython-312.pyc ADDED
Binary file (7.59 kB). View file
 
app/api/__pycache__/files.cpython-312.pyc ADDED
Binary file (2.83 kB). View file
 
app/api/__pycache__/maintenance.cpython-312.pyc ADDED
Binary file (6.57 kB). View file
 
app/api/__pycache__/notifications.cpython-312.pyc ADDED
Binary file (5.22 kB). View file
 
app/api/__pycache__/orders.cpython-312.pyc ADDED
Binary file (8.61 kB). View file
 
app/api/__pycache__/products.cpython-312.pyc ADDED
Binary file (6.76 kB). View file
 
app/api/__pycache__/scheduler.cpython-312.pyc ADDED
Binary file (9.57 kB). View file
 
app/api/__pycache__/users.cpython-312.pyc ADDED
Binary file (6.79 kB). View file
 
app/api/analytics.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, Query, HTTPException
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select, func, cast, Date, and_
4
+ from datetime import datetime, timedelta
5
+ from typing import Dict, Any, Optional
6
+ from ..core.dependencies import get_current_active_user
7
+ from ..db.database import get_db
8
+ from ..db.models import Order, Product, User
9
+
10
+ router = APIRouter()
11
+
12
+ @router.get("/sales")
13
+ async def get_sales_analytics(
14
+ start_date: datetime = Query(default=None),
15
+ end_date: datetime = Query(default=None),
16
+ branch_id: Optional[int] = Query(None, description="Filter analytics by branch"),
17
+ current_user: User = Depends(get_current_active_user),
18
+ db: AsyncSession = Depends(get_db)
19
+ ) -> Dict[str, Any]:
20
+ if not start_date:
21
+ start_date = datetime.now() - timedelta(days=30)
22
+ if not end_date:
23
+ end_date = datetime.now()
24
+
25
+ # Build query conditions
26
+ conditions = [
27
+ Order.created_at.between(start_date, end_date),
28
+ Order.status.in_(['completed', 'delivered'])
29
+ ]
30
+
31
+ # Add branch filter
32
+ if branch_id:
33
+ if not current_user.is_superuser and branch_id != current_user.branch_id:
34
+ raise HTTPException(
35
+ status_code=403,
36
+ detail="You can only view analytics from your own branch"
37
+ )
38
+ conditions.append(Order.branch_id == branch_id)
39
+ elif not current_user.is_superuser:
40
+ # Non-superusers can only see their branch's analytics
41
+ conditions.append(Order.branch_id == current_user.branch_id)
42
+
43
+ # Daily sales query
44
+ stmt = select(
45
+ cast(Order.created_at, Date).label('date'),
46
+ func.sum(Order.total_amount).label('total_sales'),
47
+ func.count().label('order_count')
48
+ ).where(
49
+ and_(*conditions)
50
+ ).group_by(
51
+ cast(Order.created_at, Date)
52
+ ).order_by(
53
+ cast(Order.created_at, Date)
54
+ )
55
+
56
+ result = await db.execute(stmt)
57
+ daily_sales = result.all()
58
+
59
+ # Calculate totals
60
+ total_revenue = sum(day.total_sales for day in daily_sales)
61
+ total_orders = sum(day.order_count for day in daily_sales)
62
+ avg_order_value = total_revenue / total_orders if total_orders > 0 else 0
63
+
64
+ return {
65
+ "daily_sales": [
66
+ {"date": day.date, "total_sales": day.total_sales, "order_count": day.order_count}
67
+ for day in daily_sales
68
+ ],
69
+ "total_revenue": total_revenue,
70
+ "total_orders": total_orders,
71
+ "average_order_value": avg_order_value
72
+ }
73
+
74
+ @router.get("/products")
75
+ async def get_product_analytics(
76
+ branch_id: Optional[int] = Query(None, description="Filter analytics by branch"),
77
+ current_user: User = Depends(get_current_active_user),
78
+ db: AsyncSession = Depends(get_db)
79
+ ) -> Dict[str, Any]:
80
+ # Build base conditions
81
+ conditions = []
82
+
83
+ # Add branch filter
84
+ if branch_id:
85
+ if not current_user.is_superuser and branch_id != current_user.branch_id:
86
+ raise HTTPException(
87
+ status_code=403,
88
+ detail="You can only view analytics from your own branch"
89
+ )
90
+ conditions.append(Product.branch_id == branch_id)
91
+ elif not current_user.is_superuser:
92
+ conditions.append(Product.branch_id == current_user.branch_id)
93
+
94
+ # Top selling products
95
+ stmt = select(
96
+ Product,
97
+ func.sum(Order.total_amount).label('total_revenue'),
98
+ func.count().label('total_orders')
99
+ ).join(
100
+ Order, Product.id == Order.id
101
+ ).where(
102
+ and_(*conditions)
103
+ ).group_by(
104
+ Product.id
105
+ ).order_by(
106
+ func.sum(Order.total_amount).desc()
107
+ ).limit(10)
108
+
109
+ result = await db.execute(stmt)
110
+ top_products = result.all()
111
+
112
+ # Count total and low stock products
113
+ total_products = await db.scalar(
114
+ select(func.count()).select_from(Product).where(and_(*conditions))
115
+ )
116
+
117
+ low_stock_conditions = conditions + [Product.inventory_count < 10]
118
+ low_stock_count = await db.scalar(
119
+ select(func.count()).select_from(Product).where(and_(*low_stock_conditions))
120
+ )
121
+
122
+ return {
123
+ "top_products": [
124
+ {
125
+ "id": product.id,
126
+ "name": product.name,
127
+ "total_revenue": revenue,
128
+ "total_orders": orders
129
+ }
130
+ for product, revenue, orders in top_products
131
+ ],
132
+ "total_products": total_products,
133
+ "low_stock_products": low_stock_count
134
+ }
135
+
136
+ @router.get("/customers")
137
+ async def get_customer_analytics(
138
+ branch_id: Optional[int] = Query(None, description="Filter analytics by branch"),
139
+ current_user: User = Depends(get_current_active_user),
140
+ db: AsyncSession = Depends(get_db)
141
+ ) -> Dict[str, Any]:
142
+ # Build base conditions
143
+ conditions = []
144
+
145
+ # Add branch filter
146
+ if branch_id:
147
+ if not current_user.is_superuser and branch_id != current_user.branch_id:
148
+ raise HTTPException(
149
+ status_code=403,
150
+ detail="You can only view analytics from your own branch"
151
+ )
152
+ conditions.append(Order.branch_id == branch_id)
153
+ elif not current_user.is_superuser:
154
+ conditions.append(Order.branch_id == current_user.branch_id)
155
+
156
+ # Customer statistics
157
+ stmt = select(
158
+ User,
159
+ func.sum(Order.total_amount).label('total_spent'),
160
+ func.count().label('total_orders')
161
+ ).join(
162
+ Order, User.id == Order.customer_id
163
+ ).where(
164
+ and_(*conditions)
165
+ ).group_by(
166
+ User.id
167
+ ).order_by(
168
+ func.sum(Order.total_amount).desc()
169
+ )
170
+
171
+ result = await db.execute(stmt)
172
+ customer_data = result.all()
173
+
174
+ total_customers = len(customer_data)
175
+ total_revenue = sum(spent for _, spent, _ in customer_data)
176
+ avg_customer_value = total_revenue / total_customers if total_customers > 0 else 0
177
+
178
+ # Customer segments
179
+ segments = {
180
+ "high_value": len([c for c, spent, _ in customer_data if spent > 1000]),
181
+ "medium_value": len([c for c, spent, _ in customer_data if 500 <= spent <= 1000]),
182
+ "low_value": len([c for c, spent, _ in customer_data if spent < 500])
183
+ }
184
+
185
+ return {
186
+ "total_customers": total_customers,
187
+ "average_customer_value": avg_customer_value,
188
+ "customer_segments": segments,
189
+ "top_customers": [
190
+ {
191
+ "id": customer.id,
192
+ "email": customer.email,
193
+ "total_spent": spent,
194
+ "total_orders": orders
195
+ }
196
+ for customer, spent, orders in customer_data[:10] # Top 10 customers
197
+ ]
198
+ }
199
+
200
+ @router.get("/dashboard")
201
+ async def get_dashboard_analytics(
202
+ branch_id: Optional[int] = Query(None, description="Filter analytics by branch"),
203
+ current_user: User = Depends(get_current_active_user),
204
+ db: AsyncSession = Depends(get_db)
205
+ ) -> Dict[str, Any]:
206
+ """Get a comprehensive dashboard with key metrics"""
207
+ # Get last 30 days of sales data
208
+ start_date = datetime.now() - timedelta(days=30)
209
+ end_date = datetime.now()
210
+
211
+ sales_data = await get_sales_analytics(start_date, end_date, branch_id, current_user, db)
212
+ product_data = await get_product_analytics(branch_id, current_user, db)
213
+ customer_data = await get_customer_analytics(branch_id, current_user, db)
214
+
215
+ return {
216
+ "sales_summary": {
217
+ "total_revenue": sales_data["total_revenue"],
218
+ "total_orders": sales_data["total_orders"],
219
+ "average_order_value": sales_data["average_order_value"],
220
+ "daily_sales": sales_data["daily_sales"][-7:] # Last 7 days
221
+ },
222
+ "product_summary": {
223
+ "total_products": product_data["total_products"],
224
+ "low_stock_products": product_data["low_stock_products"],
225
+ "top_selling_products": product_data["top_products"][:5] # Top 5 products
226
+ },
227
+ "customer_summary": {
228
+ "total_customers": customer_data["total_customers"],
229
+ "average_customer_value": customer_data["average_customer_value"],
230
+ "customer_segments": customer_data["customer_segments"]
231
+ }
232
+ }
app/api/auth.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+ from sqlalchemy import select
5
+ from ..core.security import create_access_token, verify_password, get_password_hash
6
+ from ..db.database import get_db
7
+ from ..db.models import User
8
+ from ..db.schemas import UserInDB
9
+ from datetime import timedelta
10
+ from typing import Any
11
+
12
+ router = APIRouter()
13
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
14
+
15
+ @router.post("/login")
16
+ async def login(
17
+ form_data: OAuth2PasswordRequestForm = Depends(),
18
+ db: AsyncSession = Depends(get_db)
19
+ ) -> Any:
20
+ stmt = select(User).where(User.email == form_data.username)
21
+ result = await db.execute(stmt)
22
+ user = result.scalar_one_or_none()
23
+
24
+ if not user:
25
+ raise HTTPException(
26
+ status_code=status.HTTP_401_UNAUTHORIZED,
27
+ detail="Incorrect email or password",
28
+ )
29
+
30
+ if not verify_password(form_data.password, user.hashed_password):
31
+ raise HTTPException(
32
+ status_code=status.HTTP_401_UNAUTHORIZED,
33
+ detail="Incorrect email or password",
34
+ )
35
+
36
+ access_token = create_access_token(user.id)
37
+ return {"access_token": access_token, "token_type": "bearer"}
38
+
39
+ @router.post("/register", response_model=UserInDB)
40
+ async def register(
41
+ user_data: OAuth2PasswordRequestForm = Depends(),
42
+ db: AsyncSession = Depends(get_db)
43
+ ) -> Any:
44
+ # Check if user exists
45
+ stmt = select(User).where(User.email == user_data.username)
46
+ result = await db.execute(stmt)
47
+ existing_user = result.scalar_one_or_none()
48
+
49
+ if existing_user:
50
+ raise HTTPException(
51
+ status_code=status.HTTP_400_BAD_REQUEST,
52
+ detail="Email already registered",
53
+ )
54
+
55
+ # Create new user
56
+ user = User(
57
+ email=user_data.username,
58
+ hashed_password=get_password_hash(user_data.password),
59
+ full_name=user_data.username, # You might want to add this as a separate field in the form
60
+ username=user_data.username,
61
+ is_active=True,
62
+ is_superuser=False,
63
+ roles=["user"]
64
+ )
65
+
66
+ db.add(user)
67
+ await db.commit()
68
+ await db.refresh(user)
69
+
70
+ return user
app/api/branches.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Depends
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select
4
+ from typing import List
5
+ from ..core.dependencies import get_current_superuser
6
+ from ..db.database import get_db
7
+ from ..db.models import Branch
8
+ from ..db.schemas import BranchCreate, BranchInDB
9
+
10
+ router = APIRouter()
11
+
12
+ @router.post("/", response_model=BranchInDB)
13
+ async def create_branch(
14
+ branch: BranchCreate,
15
+ current_user = Depends(get_current_superuser),
16
+ db: AsyncSession = Depends(get_db)
17
+ ) -> BranchInDB:
18
+ """Create a new branch (superuser only)"""
19
+ db_branch = Branch(**branch.dict())
20
+ db.add(db_branch)
21
+ await db.commit()
22
+ await db.refresh(db_branch)
23
+ return db_branch
24
+
25
+ @router.get("/", response_model=List[BranchInDB])
26
+ async def list_branches(
27
+ skip: int = 0,
28
+ limit: int = 100,
29
+ db: AsyncSession = Depends(get_db)
30
+ ) -> List[BranchInDB]:
31
+ """List all branches"""
32
+ query = select(Branch).offset(skip).limit(limit)
33
+ result = await db.execute(query)
34
+ return result.scalars().all()
35
+
36
+ @router.get("/{branch_id}", response_model=BranchInDB)
37
+ async def get_branch(
38
+ branch_id: int,
39
+ db: AsyncSession = Depends(get_db)
40
+ ) -> BranchInDB:
41
+ """Get a specific branch"""
42
+ stmt = select(Branch).where(Branch.id == branch_id)
43
+ result = await db.execute(stmt)
44
+ branch = result.scalar_one_or_none()
45
+
46
+ if not branch:
47
+ raise HTTPException(status_code=404, detail="Branch not found")
48
+ return branch
49
+
50
+ @router.put("/{branch_id}", response_model=BranchInDB)
51
+ async def update_branch(
52
+ branch_id: int,
53
+ branch_update: BranchCreate,
54
+ current_user = Depends(get_current_superuser),
55
+ db: AsyncSession = Depends(get_db)
56
+ ) -> BranchInDB:
57
+ """Update a branch (superuser only)"""
58
+ stmt = select(Branch).where(Branch.id == branch_id)
59
+ result = await db.execute(stmt)
60
+ branch = result.scalar_one_or_none()
61
+
62
+ if not branch:
63
+ raise HTTPException(status_code=404, detail="Branch not found")
64
+
65
+ # Update branch fields
66
+ for field, value in branch_update.dict().items():
67
+ setattr(branch, field, value)
68
+
69
+ await db.commit()
70
+ await db.refresh(branch)
71
+ return branch
72
+
73
+ @router.delete("/{branch_id}")
74
+ async def delete_branch(
75
+ branch_id: int,
76
+ current_user = Depends(get_current_superuser),
77
+ db: AsyncSession = Depends(get_db)
78
+ ):
79
+ """Delete a branch (superuser only)"""
80
+ stmt = select(Branch).where(Branch.id == branch_id)
81
+ result = await db.execute(stmt)
82
+ branch = result.scalar_one_or_none()
83
+
84
+ if not branch:
85
+ raise HTTPException(status_code=404, detail="Branch not found")
86
+
87
+ await db.delete(branch)
88
+ await db.commit()
89
+ return {"status": "success", "message": "Branch deleted"}
app/api/calendar.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, Query
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select, or_
4
+ from typing import List, Dict, Any
5
+ from datetime import datetime, timedelta
6
+ from ..core.dependencies import get_current_active_user
7
+ from ..db.database import get_db
8
+ from ..db.models import Event, User
9
+ from ..db.schemas import EventCreate, EventUpdate, EventInDB, RecurringEventCreate
10
+
11
+ router = APIRouter()
12
+
13
+ @router.post("/events", response_model=EventInDB)
14
+ async def create_event(
15
+ event: EventCreate,
16
+ current_user: User = Depends(get_current_active_user),
17
+ db: AsyncSession = Depends(get_db)
18
+ ) -> EventInDB:
19
+ """Create a new calendar event"""
20
+ db_event = Event(
21
+ user_id=current_user.id,
22
+ title=event.title,
23
+ description=event.description,
24
+ start_time=event.start_time,
25
+ end_time=event.end_time,
26
+ attendees=event.attendees,
27
+ is_all_day=event.is_all_day,
28
+ reminder_minutes=event.reminder_minutes,
29
+ status="scheduled",
30
+ attendee_responses={}
31
+ )
32
+
33
+ db.add(db_event)
34
+ await db.commit()
35
+ await db.refresh(db_event)
36
+ return db_event
37
+
38
+ @router.get("/events", response_model=List[EventInDB])
39
+ async def get_events(
40
+ start_date: datetime = Query(default=None),
41
+ end_date: datetime = Query(default=None),
42
+ include_attendee_events: bool = True,
43
+ current_user: User = Depends(get_current_active_user),
44
+ db: AsyncSession = Depends(get_db)
45
+ ) -> List[EventInDB]:
46
+ """Get user's events within a date range"""
47
+ if not start_date:
48
+ start_date = datetime.now()
49
+ if not end_date:
50
+ end_date = start_date + timedelta(days=30)
51
+
52
+ query = select(Event).where(
53
+ Event.start_time >= start_date,
54
+ Event.end_time <= end_date
55
+ )
56
+
57
+ if include_attendee_events:
58
+ query = query.where(or_(
59
+ Event.user_id == current_user.id,
60
+ Event.attendees.contains([str(current_user.id)])
61
+ ))
62
+ else:
63
+ query = query.where(Event.user_id == current_user.id)
64
+
65
+ query = query.order_by(Event.start_time)
66
+ result = await db.execute(query)
67
+ return result.scalars().all()
68
+
69
+ @router.put("/events/{event_id}", response_model=EventInDB)
70
+ async def update_event(
71
+ event_id: int,
72
+ event_update: EventUpdate,
73
+ current_user: User = Depends(get_current_active_user),
74
+ db: AsyncSession = Depends(get_db)
75
+ ) -> EventInDB:
76
+ """Update an event"""
77
+ stmt = select(Event).where(
78
+ Event.id == event_id,
79
+ Event.user_id == current_user.id
80
+ )
81
+ result = await db.execute(stmt)
82
+ event = result.scalar_one_or_none()
83
+
84
+ if not event:
85
+ raise HTTPException(
86
+ status_code=404,
87
+ detail="Event not found or you don't have permission to update it"
88
+ )
89
+
90
+ # Update event fields
91
+ update_data = event_update.dict(exclude_unset=True)
92
+ for field, value in update_data.items():
93
+ setattr(event, field, value)
94
+
95
+ event.updated_at = datetime.utcnow()
96
+ await db.commit()
97
+ await db.refresh(event)
98
+ return event
99
+
100
+ @router.delete("/events/{event_id}")
101
+ async def delete_event(
102
+ event_id: int,
103
+ current_user: User = Depends(get_current_active_user),
104
+ db: AsyncSession = Depends(get_db)
105
+ ) -> Dict[str, bool]:
106
+ """Delete an event"""
107
+ stmt = select(Event).where(
108
+ Event.id == event_id,
109
+ Event.user_id == current_user.id
110
+ )
111
+ result = await db.execute(stmt)
112
+ event = result.scalar_one_or_none()
113
+
114
+ if not event:
115
+ raise HTTPException(
116
+ status_code=404,
117
+ detail="Event not found or you don't have permission to delete it"
118
+ )
119
+
120
+ await db.delete(event)
121
+ await db.commit()
122
+ return {"success": True}
123
+
124
+ @router.post("/events/{event_id}/respond")
125
+ async def respond_to_event(
126
+ event_id: int,
127
+ response: str,
128
+ current_user: User = Depends(get_current_active_user),
129
+ db: AsyncSession = Depends(get_db)
130
+ ) -> Dict[str, bool]:
131
+ """Respond to an event invitation"""
132
+ if response not in ["accepted", "declined", "maybe"]:
133
+ raise HTTPException(
134
+ status_code=400,
135
+ detail="Invalid response. Must be one of: accepted, declined, maybe"
136
+ )
137
+
138
+ stmt = select(Event).where(
139
+ Event.id == event_id,
140
+ Event.attendees.contains([str(current_user.id)])
141
+ )
142
+ result = await db.execute(stmt)
143
+ event = result.scalar_one_or_none()
144
+
145
+ if not event:
146
+ raise HTTPException(
147
+ status_code=404,
148
+ detail="Event not found or you are not invited to this event"
149
+ )
150
+
151
+ # Update the response in the attendee_responses dictionary
152
+ event.attendee_responses[str(current_user.id)] = response
153
+ event.updated_at = datetime.utcnow()
154
+
155
+ await db.commit()
156
+ return {"success": True}
app/api/files.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, UploadFile, File, Depends, HTTPException
2
+ from fastapi.responses import FileResponse
3
+ from typing import List
4
+ from ..core.dependencies import get_current_active_user
5
+ from ..utils.file_storage import file_storage
6
+ from ..utils.logger import logger
7
+ from pathlib import Path
8
+
9
+ router = APIRouter()
10
+
11
+ @router.post("/upload")
12
+ async def upload_file(
13
+ file: UploadFile = File(...),
14
+ category: str = "documents",
15
+ current_user = Depends(get_current_active_user)
16
+ ) -> dict:
17
+ try:
18
+ file_path = await file_storage.save_file(file, category)
19
+ if not file_path:
20
+ raise HTTPException(status_code=400, detail="Failed to upload file")
21
+
22
+ return {
23
+ "filename": file.filename,
24
+ "stored_path": file_path,
25
+ "url": file_storage.get_file_url(file_path)
26
+ }
27
+ except ValueError as e:
28
+ raise HTTPException(status_code=400, detail=str(e))
29
+ except Exception as e:
30
+ logger.error(f"File upload error: {str(e)}")
31
+ raise HTTPException(status_code=500, detail="Internal server error")
32
+
33
+ @router.delete("/{file_path:path}")
34
+ async def delete_file(
35
+ file_path: str,
36
+ current_user = Depends(get_current_active_user)
37
+ ) -> dict:
38
+ success = await file_storage.delete_file(file_path)
39
+ if not success:
40
+ raise HTTPException(status_code=404, detail="File not found")
41
+
42
+ return {"status": "success", "message": "File deleted successfully"}
43
+
44
+ @router.get("/{file_path:path}")
45
+ async def get_file(
46
+ file_path: str,
47
+ current_user = Depends(get_current_active_user)
48
+ ):
49
+ full_path = Path("uploads") / file_path
50
+ if not full_path.exists():
51
+ raise HTTPException(status_code=404, detail="File not found")
52
+
53
+ return FileResponse(str(full_path))
app/api/maintenance.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select, delete, func
4
+ from typing import Dict, Any, List
5
+ from datetime import datetime, timedelta
6
+ from ..core.dependencies import get_current_active_user
7
+ from ..db.database import get_db
8
+ from ..db.models import User, Order, Notification, Event
9
+ from ..utils.logger import logger
10
+
11
+ router = APIRouter()
12
+
13
+ @router.post("/sessions/cleanup")
14
+ async def cleanup_sessions(
15
+ current_user: User = Depends(get_current_active_user),
16
+ db: AsyncSession = Depends(get_db)
17
+ ) -> Dict[str, int]:
18
+ """Manually trigger session cleanup"""
19
+ if "admin" not in current_user.roles:
20
+ raise HTTPException(
21
+ status_code=403,
22
+ detail="Only administrators can perform maintenance operations"
23
+ )
24
+
25
+ cutoff_date = datetime.utcnow() - timedelta(days=7)
26
+ stmt = delete(Event).where(Event.created_at < cutoff_date)
27
+ result = await db.execute(stmt)
28
+ await db.commit()
29
+
30
+ return {"deleted_sessions": result.rowcount}
31
+
32
+ @router.post("/data/archive")
33
+ async def archive_data(
34
+ current_user: User = Depends(get_current_active_user),
35
+ db: AsyncSession = Depends(get_db)
36
+ ) -> Dict[str, int]:
37
+ """Manually trigger data archiving"""
38
+ if "admin" not in current_user.roles:
39
+ raise HTTPException(
40
+ status_code=403,
41
+ detail="Only administrators can perform maintenance operations"
42
+ )
43
+
44
+ archive_date = datetime.utcnow() - timedelta(days=365)
45
+ archived = {}
46
+
47
+ # Archive old orders
48
+ orders_stmt = delete(Order).where(
49
+ Order.created_at < archive_date,
50
+ Order.status.in_(["delivered", "cancelled"])
51
+ )
52
+ orders_result = await db.execute(orders_stmt)
53
+ archived["orders"] = orders_result.rowcount
54
+
55
+ # Archive old notifications
56
+ notif_stmt = delete(Notification).where(
57
+ Notification.created_at < archive_date,
58
+ Notification.read == True
59
+ )
60
+ notif_result = await db.execute(notif_stmt)
61
+ archived["notifications"] = notif_result.rowcount
62
+
63
+ await db.commit()
64
+ return archived
65
+
66
+ @router.get("/health")
67
+ async def check_health(
68
+ current_user: User = Depends(get_current_active_user),
69
+ db: AsyncSession = Depends(get_db)
70
+ ) -> Dict[str, Any]:
71
+ """Check system health metrics"""
72
+ if "admin" not in current_user.roles:
73
+ raise HTTPException(
74
+ status_code=403,
75
+ detail="Only administrators can view system health"
76
+ )
77
+
78
+ try:
79
+ # Check database connection
80
+ await db.execute(select(1))
81
+
82
+ # Get database statistics
83
+ total_users = await db.scalar(select(func.count()).select_from(User))
84
+ total_orders = await db.scalar(select(func.count()).select_from(Order))
85
+ total_notifications = await db.scalar(select(func.count()).select_from(Notification))
86
+
87
+ return {
88
+ "status": "healthy",
89
+ "timestamp": datetime.utcnow(),
90
+ "database": {
91
+ "connected": True,
92
+ "total_users": total_users,
93
+ "total_orders": total_orders,
94
+ "total_notifications": total_notifications
95
+ }
96
+ }
97
+ except Exception as e:
98
+ logger.error(f"Health check error: {str(e)}")
99
+ return {
100
+ "status": "unhealthy",
101
+ "error": str(e),
102
+ "timestamp": datetime.utcnow()
103
+ }
104
+
105
+ @router.post("/database/maintenance")
106
+ async def perform_db_maintenance(
107
+ current_user: User = Depends(get_current_active_user),
108
+ db: AsyncSession = Depends(get_db)
109
+ ) -> Dict[str, Any]:
110
+ """Manually trigger database maintenance"""
111
+ if "admin" not in current_user.roles:
112
+ raise HTTPException(
113
+ status_code=403,
114
+ detail="Only administrators can perform maintenance operations"
115
+ )
116
+
117
+ try:
118
+ # Cleanup expired sessions
119
+ await cleanup_sessions(current_user, db)
120
+
121
+ # Run VACUUM ANALYZE (requires raw SQL)
122
+ await db.execute("VACUUM ANALYZE;")
123
+
124
+ return {
125
+ "status": "success",
126
+ "message": "Database maintenance completed successfully"
127
+ }
128
+ except Exception as e:
129
+ logger.error(f"Database maintenance error: {str(e)}")
130
+ raise HTTPException(
131
+ status_code=500,
132
+ detail=f"Database maintenance failed: {str(e)}"
133
+ )
app/api/notifications.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, Query
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select, update
4
+ from typing import List, Dict, Any, Optional
5
+ from ..core.dependencies import get_current_active_user
6
+ from ..db.database import get_db
7
+ from ..db.models import Notification, User
8
+ from ..db.schemas import NotificationCreate, NotificationInDB
9
+
10
+ router = APIRouter()
11
+
12
+ @router.get("/", response_model=List[NotificationInDB])
13
+ async def get_notifications(
14
+ skip: int = Query(0, ge=0),
15
+ limit: int = Query(50, ge=1, le=100),
16
+ unread_only: bool = False,
17
+ current_user: User = Depends(get_current_active_user),
18
+ db: AsyncSession = Depends(get_db)
19
+ ) -> List[NotificationInDB]:
20
+ """Get user's notifications"""
21
+ query = select(Notification).where(Notification.user_id == current_user.id)
22
+
23
+ if unread_only:
24
+ query = query.where(Notification.read == False)
25
+
26
+ query = query.order_by(Notification.created_at.desc()).offset(skip).limit(limit)
27
+ result = await db.execute(query)
28
+ return result.scalars().all()
29
+
30
+ @router.post("/mark-read/{notification_id}", response_model=NotificationInDB)
31
+ async def mark_notification_read(
32
+ notification_id: int,
33
+ current_user: User = Depends(get_current_active_user),
34
+ db: AsyncSession = Depends(get_db)
35
+ ) -> NotificationInDB:
36
+ """Mark a notification as read"""
37
+ stmt = select(Notification).where(
38
+ Notification.id == notification_id,
39
+ Notification.user_id == current_user.id
40
+ )
41
+ result = await db.execute(stmt)
42
+ notification = result.scalar_one_or_none()
43
+
44
+ if not notification:
45
+ raise HTTPException(status_code=404, detail="Notification not found")
46
+
47
+ notification.read = True
48
+ await db.commit()
49
+ await db.refresh(notification)
50
+ return notification
51
+
52
+ @router.post("/mark-all-read")
53
+ async def mark_all_notifications_read(
54
+ current_user: User = Depends(get_current_active_user),
55
+ db: AsyncSession = Depends(get_db)
56
+ ) -> Dict[str, int]:
57
+ """Mark all notifications as read"""
58
+ stmt = update(Notification).where(
59
+ Notification.user_id == current_user.id,
60
+ Notification.read == False
61
+ ).values(read=True)
62
+
63
+ result = await db.execute(stmt)
64
+ await db.commit()
65
+ return {"marked_count": result.rowcount}
66
+
67
+ @router.delete("/{notification_id}")
68
+ async def delete_notification(
69
+ notification_id: int,
70
+ current_user: User = Depends(get_current_active_user),
71
+ db: AsyncSession = Depends(get_db)
72
+ ) -> Dict[str, bool]:
73
+ """Delete a notification"""
74
+ stmt = select(Notification).where(
75
+ Notification.id == notification_id,
76
+ Notification.user_id == current_user.id
77
+ )
78
+ result = await db.execute(stmt)
79
+ notification = result.scalar_one_or_none()
80
+
81
+ if not notification:
82
+ raise HTTPException(status_code=404, detail="Notification not found")
83
+
84
+ await db.delete(notification)
85
+ await db.commit()
86
+ return {"success": True}
app/api/orders.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, status, Depends, Query
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select
4
+ from typing import List, Optional
5
+ from ..core.dependencies import get_current_active_user
6
+ from ..db.database import get_db
7
+ from ..db.models import Order, Product, OrderItem, User
8
+ from ..db.schemas import OrderCreate, OrderInDB
9
+ from datetime import datetime
10
+
11
+ router = APIRouter()
12
+
13
+ @router.post("/", response_model=OrderInDB)
14
+ async def create_order(
15
+ order: OrderCreate,
16
+ current_user: User = Depends(get_current_active_user),
17
+ db: AsyncSession = Depends(get_db)
18
+ ) -> OrderInDB:
19
+ # Ensure user belongs to the branch they're creating the order for
20
+ if current_user.branch_id != order.branch_id and not current_user.is_superuser:
21
+ raise HTTPException(
22
+ status_code=403,
23
+ detail="You can only create orders for your own branch"
24
+ )
25
+
26
+ # Calculate total and validate products
27
+ total = 0
28
+ order_items = []
29
+
30
+ for item in order.items:
31
+ # Get product
32
+ stmt = select(Product).where(
33
+ Product.id == item.product_id,
34
+ Product.branch_id == order.branch_id # Ensure product belongs to the same branch
35
+ )
36
+ result = await db.execute(stmt)
37
+ product = result.scalar_one_or_none()
38
+
39
+ if not product:
40
+ raise HTTPException(
41
+ status_code=404,
42
+ detail=f"Product {item.product_id} not found in this branch"
43
+ )
44
+
45
+ if product.inventory_count < item.quantity:
46
+ raise HTTPException(
47
+ status_code=400,
48
+ detail=f"Insufficient inventory for product {item.product_id}"
49
+ )
50
+
51
+ # Update inventory
52
+ product.inventory_count -= item.quantity
53
+ total += product.price * item.quantity
54
+
55
+ # Create order item
56
+ order_item = OrderItem(
57
+ product_id=item.product_id,
58
+ quantity=item.quantity,
59
+ price=product.price
60
+ )
61
+ order_items.append(order_item)
62
+
63
+ # Create order
64
+ db_order = Order(
65
+ customer_id=order.customer_id,
66
+ branch_id=order.branch_id,
67
+ total_amount=total,
68
+ status="pending",
69
+ items=order_items
70
+ )
71
+
72
+ db.add(db_order)
73
+ await db.commit()
74
+ await db.refresh(db_order)
75
+ return db_order
76
+
77
+ @router.get("/", response_model=List[OrderInDB])
78
+ async def list_orders(
79
+ skip: int = 0,
80
+ limit: int = 10,
81
+ status: Optional[str] = None,
82
+ branch_id: Optional[int] = Query(None, description="Filter orders by branch"),
83
+ current_user: User = Depends(get_current_active_user),
84
+ db: AsyncSession = Depends(get_db)
85
+ ) -> List[OrderInDB]:
86
+ query = select(Order)
87
+
88
+ # Filter by status if provided
89
+ if status:
90
+ query = query.where(Order.status == status)
91
+
92
+ # Filter by branch if provided, otherwise use user's branch
93
+ if branch_id:
94
+ if not current_user.is_superuser and branch_id != current_user.branch_id:
95
+ raise HTTPException(
96
+ status_code=403,
97
+ detail="You can only view orders from your own branch"
98
+ )
99
+ query = query.where(Order.branch_id == branch_id)
100
+ elif not current_user.is_superuser:
101
+ # Non-superusers can only see orders from their branch
102
+ query = query.where(Order.branch_id == current_user.branch_id)
103
+
104
+ query = query.offset(skip).limit(limit)
105
+ result = await db.execute(query)
106
+ return result.scalars().all()
107
+
108
+ @router.get("/{order_id}", response_model=OrderInDB)
109
+ async def get_order(
110
+ order_id: int,
111
+ current_user: User = Depends(get_current_active_user),
112
+ db: AsyncSession = Depends(get_db)
113
+ ) -> OrderInDB:
114
+ stmt = select(Order).where(Order.id == order_id)
115
+ result = await db.execute(stmt)
116
+ order = result.scalar_one_or_none()
117
+
118
+ if not order:
119
+ raise HTTPException(status_code=404, detail="Order not found")
120
+
121
+ # Check if user has access to this order's branch
122
+ if not current_user.is_superuser and order.branch_id != current_user.branch_id:
123
+ raise HTTPException(status_code=403, detail="You cannot access orders from other branches")
124
+
125
+ return order
126
+
127
+ @router.put("/{order_id}/status", response_model=OrderInDB)
128
+ async def update_order_status(
129
+ order_id: int,
130
+ status: str,
131
+ current_user: User = Depends(get_current_active_user),
132
+ db: AsyncSession = Depends(get_db)
133
+ ) -> OrderInDB:
134
+ valid_statuses = ["pending", "processing", "shipped", "delivered", "cancelled"]
135
+ if status not in valid_statuses:
136
+ raise HTTPException(status_code=400, detail="Invalid status")
137
+
138
+ stmt = select(Order).where(Order.id == order_id)
139
+ result = await db.execute(stmt)
140
+ order = result.scalar_one_or_none()
141
+
142
+ if not order:
143
+ raise HTTPException(status_code=404, detail="Order not found")
144
+
145
+ # Check if user has access to this order's branch
146
+ if not current_user.is_superuser and order.branch_id != current_user.branch_id:
147
+ raise HTTPException(status_code=403, detail="You cannot modify orders from other branches")
148
+
149
+ order.status = status
150
+ order.updated_at = datetime.utcnow()
151
+
152
+ await db.commit()
153
+ await db.refresh(order)
154
+ return order
155
+
156
+ @router.delete("/{order_id}")
157
+ async def delete_order(
158
+ order_id: int,
159
+ current_user: User = Depends(get_current_active_user),
160
+ db: AsyncSession = Depends(get_db)
161
+ ):
162
+ # Get the order
163
+ stmt = select(Order).where(Order.id == order_id)
164
+ result = await db.execute(stmt)
165
+ order = result.scalar_one_or_none()
166
+
167
+ if not order:
168
+ raise HTTPException(status_code=404, detail="Order not found")
169
+
170
+ # Check if user has access to this order's branch
171
+ if not current_user.is_superuser and order.branch_id != current_user.branch_id:
172
+ raise HTTPException(status_code=403, detail="You cannot delete orders from other branches")
173
+
174
+ # Restore inventory for each product
175
+ for item in order.items:
176
+ product_stmt = select(Product).where(Product.id == item.product_id)
177
+ product_result = await db.execute(product_stmt)
178
+ product = product_result.scalar_one_or_none()
179
+
180
+ if product:
181
+ product.inventory_count += item.quantity
182
+
183
+ await db.delete(order)
184
+ await db.commit()
185
+
186
+ return {"status": "success", "message": "Order deleted and inventory restored"}
app/api/products.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, status, Depends, Query
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select
4
+ from typing import List, Optional
5
+ from ..core.dependencies import get_current_active_user
6
+ from ..db.database import get_db
7
+ from ..db.models import Product, User
8
+ from ..db.schemas import ProductCreate, ProductInDB
9
+
10
+ router = APIRouter()
11
+
12
+ @router.post("/", response_model=ProductInDB)
13
+ async def create_product(
14
+ product: ProductCreate,
15
+ current_user: User = Depends(get_current_active_user),
16
+ db: AsyncSession = Depends(get_db)
17
+ ) -> ProductInDB:
18
+ # Ensure user belongs to the branch they're creating the product for
19
+ if current_user.branch_id != product.branch_id and not current_user.is_superuser:
20
+ raise HTTPException(
21
+ status_code=403,
22
+ detail="You can only create products for your own branch"
23
+ )
24
+
25
+ db_product = Product(**product.dict())
26
+ db.add(db_product)
27
+ await db.commit()
28
+ await db.refresh(db_product)
29
+ return db_product
30
+
31
+ @router.get("/", response_model=List[ProductInDB])
32
+ async def list_products(
33
+ skip: int = 0,
34
+ limit: int = 10,
35
+ category: Optional[str] = None,
36
+ branch_id: Optional[int] = Query(None, description="Filter products by branch"),
37
+ current_user: User = Depends(get_current_active_user),
38
+ db: AsyncSession = Depends(get_db)
39
+ ) -> List[ProductInDB]:
40
+ query = select(Product)
41
+
42
+ # Filter by category if provided
43
+ if category:
44
+ query = query.where(Product.category == category)
45
+
46
+ # Filter by branch if provided, otherwise use user's branch
47
+ if branch_id:
48
+ if not current_user.is_superuser and branch_id != current_user.branch_id:
49
+ raise HTTPException(
50
+ status_code=403,
51
+ detail="You can only view products from your own branch"
52
+ )
53
+ query = query.where(Product.branch_id == branch_id)
54
+ elif not current_user.is_superuser:
55
+ # Non-superusers can only see products from their branch
56
+ query = query.where(Product.branch_id == current_user.branch_id)
57
+
58
+ query = query.offset(skip).limit(limit)
59
+ result = await db.execute(query)
60
+ return result.scalars().all()
61
+
62
+ @router.get("/{product_id}", response_model=ProductInDB)
63
+ async def get_product(
64
+ product_id: int,
65
+ current_user: User = Depends(get_current_active_user),
66
+ db: AsyncSession = Depends(get_db)
67
+ ) -> ProductInDB:
68
+ stmt = select(Product).where(Product.id == product_id)
69
+ result = await db.execute(stmt)
70
+ product = result.scalar_one_or_none()
71
+
72
+ if not product:
73
+ raise HTTPException(status_code=404, detail="Product not found")
74
+
75
+ # Check if user has access to this product's branch
76
+ if not current_user.is_superuser and product.branch_id != current_user.branch_id:
77
+ raise HTTPException(status_code=403, detail="You cannot access products from other branches")
78
+
79
+ return product
80
+
81
+ @router.put("/{product_id}", response_model=ProductInDB)
82
+ async def update_product(
83
+ product_id: int,
84
+ product_update: ProductCreate,
85
+ current_user: User = Depends(get_current_active_user),
86
+ db: AsyncSession = Depends(get_db)
87
+ ) -> ProductInDB:
88
+ stmt = select(Product).where(Product.id == product_id)
89
+ result = await db.execute(stmt)
90
+ product = result.scalar_one_or_none()
91
+
92
+ if not product:
93
+ raise HTTPException(status_code=404, detail="Product not found")
94
+
95
+ # Check if user has access to this product's branch
96
+ if not current_user.is_superuser and product.branch_id != current_user.branch_id:
97
+ raise HTTPException(status_code=403, detail="You cannot modify products from other branches")
98
+
99
+ # Ensure the branch isn't being changed to a different branch
100
+ if product_update.branch_id != product.branch_id:
101
+ raise HTTPException(status_code=400, detail="Cannot change product's branch")
102
+
103
+ # Update product fields
104
+ update_data = product_update.dict(exclude_unset=True)
105
+ for field, value in update_data.items():
106
+ setattr(product, field, value)
107
+
108
+ await db.commit()
109
+ await db.refresh(product)
110
+ return product
111
+
112
+ @router.delete("/{product_id}")
113
+ async def delete_product(
114
+ product_id: int,
115
+ current_user: User = Depends(get_current_active_user),
116
+ db: AsyncSession = Depends(get_db)
117
+ ):
118
+ stmt = select(Product).where(Product.id == product_id)
119
+ result = await db.execute(stmt)
120
+ product = result.scalar_one_or_none()
121
+
122
+ if not product:
123
+ raise HTTPException(status_code=404, detail="Product not found")
124
+
125
+ # Check if user has access to this product's branch
126
+ if not current_user.is_superuser and product.branch_id != current_user.branch_id:
127
+ raise HTTPException(status_code=403, detail="You cannot delete products from other branches")
128
+
129
+ await db.delete(product)
130
+ await db.commit()
131
+ return {"status": "success", "message": "Product deleted"}
app/api/scheduler.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select, delete
4
+ from typing import List, Dict, Any, Optional
5
+ from datetime import datetime, timedelta
6
+ from ..core.dependencies import get_current_active_user
7
+ from ..db.database import get_db
8
+ from ..db.models import Event, User
9
+ from pydantic import BaseModel
10
+
11
+ router = APIRouter()
12
+
13
+ class RecurringEventCreate(BaseModel):
14
+ title: str
15
+ description: str
16
+ start_time: datetime
17
+ end_time: datetime
18
+ recurrence_pattern: str
19
+ recurrence_end_date: Optional[datetime] = None
20
+ attendees: List[str] = []
21
+ reminder_minutes: int = 30
22
+
23
+ class RecurringEventUpdate(BaseModel):
24
+ title: Optional[str] = None
25
+ description: Optional[str] = None
26
+ start_time: Optional[datetime] = None
27
+ end_time: Optional[datetime] = None
28
+ attendees: Optional[List[str]] = None
29
+ reminder_minutes: Optional[int] = None
30
+
31
+ @router.post("/recurring-events")
32
+ async def create_recurring_event(
33
+ event_data: RecurringEventCreate,
34
+ current_user: User = Depends(get_current_active_user),
35
+ db: AsyncSession = Depends(get_db)
36
+ ) -> List[Dict[str, Any]]:
37
+ """Create a new recurring event"""
38
+ if event_data.recurrence_pattern not in ["daily", "weekly", "monthly", "yearly"]:
39
+ raise HTTPException(
40
+ status_code=400,
41
+ detail="Invalid recurrence pattern. Must be one of: daily, weekly, monthly, yearly"
42
+ )
43
+
44
+ if event_data.start_time >= event_data.end_time:
45
+ raise HTTPException(
46
+ status_code=400,
47
+ detail="End time must be after start time"
48
+ )
49
+
50
+ events = []
51
+ current_start = event_data.start_time
52
+ current_end = event_data.end_time
53
+ duration = event_data.end_time - event_data.start_time
54
+ sequence_number = 0
55
+
56
+ while True:
57
+ if event_data.recurrence_end_date and current_start > event_data.recurrence_end_date:
58
+ break
59
+
60
+ event = Event(
61
+ user_id=current_user.id,
62
+ title=event_data.title,
63
+ description=event_data.description,
64
+ start_time=current_start,
65
+ end_time=current_end,
66
+ attendees=event_data.attendees,
67
+ reminder_minutes=event_data.reminder_minutes,
68
+ is_recurring=True,
69
+ recurrence_pattern=event_data.recurrence_pattern,
70
+ sequence_number=sequence_number,
71
+ status="scheduled"
72
+ )
73
+ db.add(event)
74
+ events.append(event)
75
+
76
+ # Calculate next occurrence
77
+ sequence_number += 1
78
+ if event_data.recurrence_pattern == "daily":
79
+ current_start += timedelta(days=1)
80
+ elif event_data.recurrence_pattern == "weekly":
81
+ current_start += timedelta(weeks=1)
82
+ elif event_data.recurrence_pattern == "monthly":
83
+ # Add one month (approximately)
84
+ if current_start.month == 12:
85
+ current_start = current_start.replace(year=current_start.year + 1, month=1)
86
+ else:
87
+ current_start = current_start.replace(month=current_start.month + 1)
88
+ elif event_data.recurrence_pattern == "yearly":
89
+ current_start = current_start.replace(year=current_start.year + 1)
90
+
91
+ current_end = current_start + duration
92
+
93
+ await db.commit()
94
+
95
+ # Refresh all events to get their IDs
96
+ for event in events:
97
+ await db.refresh(event)
98
+
99
+ return events
100
+
101
+ @router.put("/recurring-events/{event_id}")
102
+ async def update_recurring_event(
103
+ event_id: int,
104
+ event_update: RecurringEventUpdate,
105
+ update_future: bool = True,
106
+ current_user: User = Depends(get_current_active_user),
107
+ db: AsyncSession = Depends(get_db)
108
+ ) -> List[Dict[str, Any]]:
109
+ """Update a recurring event and optionally its future occurrences"""
110
+ update_data = event_update.dict(exclude_unset=True)
111
+ if not update_data:
112
+ raise HTTPException(status_code=400, detail="No update data provided")
113
+
114
+ # Get the original event
115
+ stmt = select(Event).where(
116
+ Event.id == event_id,
117
+ Event.user_id == current_user.id
118
+ )
119
+ result = await db.execute(stmt)
120
+ event = result.scalar_one_or_none()
121
+
122
+ if not event:
123
+ raise HTTPException(
124
+ status_code=404,
125
+ detail="Event not found or you don't have permission to update it"
126
+ )
127
+
128
+ updated_events = [event]
129
+
130
+ # Update future occurrences if requested
131
+ if update_future and event.is_recurring:
132
+ future_stmt = select(Event).where(
133
+ Event.recurrence_group == event.recurrence_group,
134
+ Event.sequence_number > event.sequence_number,
135
+ Event.user_id == current_user.id
136
+ )
137
+ future_result = await db.execute(future_stmt)
138
+ future_events = future_result.scalars().all()
139
+
140
+ for future_event in future_events:
141
+ for field, value in update_data.items():
142
+ setattr(future_event, field, value)
143
+ updated_events.append(future_event)
144
+
145
+ await db.commit()
146
+ return updated_events
147
+
148
+ @router.delete("/recurring-events/{event_id}")
149
+ async def delete_recurring_event(
150
+ event_id: int,
151
+ delete_future: bool = True,
152
+ current_user: User = Depends(get_current_active_user),
153
+ db: AsyncSession = Depends(get_db)
154
+ ) -> Dict[str, bool]:
155
+ """Delete a recurring event and optionally its future occurrences"""
156
+ stmt = select(Event).where(
157
+ Event.id == event_id,
158
+ Event.user_id == current_user.id
159
+ )
160
+ result = await db.execute(stmt)
161
+ event = result.scalar_one_or_none()
162
+
163
+ if not event:
164
+ raise HTTPException(
165
+ status_code=404,
166
+ detail="Event not found or you don't have permission to delete it"
167
+ )
168
+
169
+ if delete_future and event.is_recurring:
170
+ delete_stmt = delete(Event).where(
171
+ Event.recurrence_group == event.recurrence_group,
172
+ Event.sequence_number >= event.sequence_number,
173
+ Event.user_id == current_user.id
174
+ )
175
+ await db.execute(delete_stmt)
176
+ else:
177
+ await db.delete(event)
178
+
179
+ await db.commit()
180
+ return {"success": True}
181
+
182
+ @router.get("/recurring-events/upcoming")
183
+ async def get_upcoming_recurring_events(
184
+ days: int = 30,
185
+ current_user: User = Depends(get_current_active_user),
186
+ db: AsyncSession = Depends(get_db)
187
+ ) -> List[Dict[str, Any]]:
188
+ """Get upcoming recurring events for the next N days"""
189
+ if days <= 0 or days > 365:
190
+ raise HTTPException(
191
+ status_code=400,
192
+ detail="Days parameter must be between 1 and 365"
193
+ )
194
+
195
+ end_date = datetime.utcnow() + timedelta(days=days)
196
+ stmt = select(Event).where(
197
+ Event.user_id == current_user.id,
198
+ Event.start_time <= end_date,
199
+ Event.is_recurring == True
200
+ ).order_by(Event.start_time)
201
+
202
+ result = await db.execute(stmt)
203
+ return result.scalars().all()
app/api/users.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, status, Depends
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select
4
+ from typing import List, Optional
5
+ from ..db.database import get_db
6
+ from ..db.models import User
7
+ from ..db.schemas import UserCreate, UserInDB
8
+ from ..core.dependencies import get_current_superuser, get_current_active_user
9
+ from ..core.security import get_password_hash
10
+
11
+ router = APIRouter()
12
+
13
+ @router.get("/me", response_model=UserInDB)
14
+ async def read_user_me(current_user: User = Depends(get_current_active_user)):
15
+ return current_user
16
+
17
+ @router.get("/", response_model=List[UserInDB])
18
+ async def list_users(
19
+ skip: int = 0,
20
+ limit: int = 10,
21
+ current_user: User = Depends(get_current_superuser),
22
+ db: AsyncSession = Depends(get_db)
23
+ ) -> List[UserInDB]:
24
+ stmt = select(User).offset(skip).limit(limit)
25
+ result = await db.execute(stmt)
26
+ return result.scalars().all()
27
+
28
+ @router.post("/", response_model=UserInDB)
29
+ async def create_user(
30
+ user: UserCreate,
31
+ current_user: User = Depends(get_current_superuser),
32
+ db: AsyncSession = Depends(get_db)
33
+ ) -> UserInDB:
34
+ # Check if email exists
35
+ stmt = select(User).where(User.email == user.email)
36
+ result = await db.execute(stmt)
37
+ if result.scalar_one_or_none():
38
+ raise HTTPException(
39
+ status_code=status.HTTP_400_BAD_REQUEST,
40
+ detail="Email already registered"
41
+ )
42
+
43
+ # Create new user
44
+ db_user = User(
45
+ email=user.email,
46
+ username=user.username,
47
+ full_name=user.full_name,
48
+ hashed_password=get_password_hash(user.password),
49
+ is_active=user.is_active,
50
+ is_superuser=user.is_superuser,
51
+ roles=user.roles
52
+ )
53
+
54
+ db.add(db_user)
55
+ await db.commit()
56
+ await db.refresh(db_user)
57
+ return db_user
58
+
59
+ @router.put("/{user_id}", response_model=UserInDB)
60
+ async def update_user(
61
+ user_id: int,
62
+ user_update: UserCreate,
63
+ current_user: User = Depends(get_current_superuser),
64
+ db: AsyncSession = Depends(get_db)
65
+ ) -> UserInDB:
66
+ stmt = select(User).where(User.id == user_id)
67
+ result = await db.execute(stmt)
68
+ db_user = result.scalar_one_or_none()
69
+
70
+ if not db_user:
71
+ raise HTTPException(status_code=404, detail="User not found")
72
+
73
+ # Update user fields
74
+ update_data = user_update.dict(exclude_unset=True)
75
+ if "password" in update_data:
76
+ update_data["hashed_password"] = get_password_hash(update_data.pop("password"))
77
+
78
+ for field, value in update_data.items():
79
+ setattr(db_user, field, value)
80
+
81
+ await db.commit()
82
+ await db.refresh(db_user)
83
+ return db_user
84
+
85
+ @router.delete("/{user_id}")
86
+ async def delete_user(
87
+ user_id: int,
88
+ current_user: User = Depends(get_current_superuser),
89
+ db: AsyncSession = Depends(get_db)
90
+ ):
91
+ stmt = select(User).where(User.id == user_id)
92
+ result = await db.execute(stmt)
93
+ user = result.scalar_one_or_none()
94
+
95
+ if not user:
96
+ raise HTTPException(status_code=404, detail="User not found")
97
+
98
+ await db.delete(user)
99
+ await db.commit()
100
+ return {"status": "success", "message": "User deleted"}
101
+
102
+ @router.put("/{user_id}/roles", response_model=UserInDB)
103
+ async def update_user_roles(
104
+ user_id: int,
105
+ roles: List[str],
106
+ current_user: User = Depends(get_current_superuser),
107
+ db: AsyncSession = Depends(get_db)
108
+ ) -> UserInDB:
109
+ valid_roles = ["user", "admin", "manager", "support"]
110
+ invalid_roles = [role for role in roles if role not in valid_roles]
111
+ if invalid_roles:
112
+ raise HTTPException(
113
+ status_code=400,
114
+ detail=f"Invalid roles: {', '.join(invalid_roles)}"
115
+ )
116
+
117
+ stmt = select(User).where(User.id == user_id)
118
+ result = await db.execute(stmt)
119
+ user = result.scalar_one_or_none()
120
+
121
+ if not user:
122
+ raise HTTPException(status_code=404, detail="User not found")
123
+
124
+ user.roles = roles
125
+ await db.commit()
126
+ await db.refresh(user)
127
+ return user
app/core/__init__.py ADDED
File without changes
app/core/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (173 Bytes). View file
 
app/core/__pycache__/config.cpython-312.pyc ADDED
Binary file (1.65 kB). View file
 
app/core/__pycache__/dependencies.cpython-312.pyc ADDED
Binary file (2.6 kB). View file
 
app/core/__pycache__/security.cpython-312.pyc ADDED
Binary file (1.72 kB). View file
 
app/core/config.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+ from typing import ClassVar
3
+
4
+
5
+ class Settings(BaseSettings):
6
+ API_V1_STR: str = "/api/v1"
7
+ PROJECT_NAME: str = "Admin Dashboard"
8
+ VERSION: str = "1.0.0"
9
+
10
+ # PostgreSQL Database settings
11
+ DATABASE_URL: ClassVar[str] = "postgresql+asyncpg://postgres.juycnkjuzylnbruwaqmp:Lovyelias5584.@aws-0-eu-central-1.pooler.supabase.com:5432/postgres"
12
+
13
+ # JWT Settings
14
+ SECRET_KEY: str = "your-secret-key-here"
15
+ ALGORITHM: str = "HS256"
16
+ ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
17
+
18
+ # Redis settings
19
+ REDIS_HOST: str = "localhost"
20
+ REDIS_PORT: int = 6379
21
+
22
+ # Email settings
23
+ MAIL_USERNAME: str = "yungdml31@gmail.com"
24
+ MAIL_PASSWORD: str = ""
25
+ MAIL_FROM: str = "admin@angelo.com"
26
+ MAIL_PORT: int = 587
27
+ MAIL_SERVER: str = "smtp.gmail.com"
28
+
29
+ # Frontend URL
30
+ FRONTEND_URL: str = "http://localhost:3000"
31
+
32
+ class Config:
33
+ case_sensitive = True
34
+
35
+
36
+ settings = Settings()
app/core/dependencies.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Depends, HTTPException, status
2
+ from fastapi.security import OAuth2PasswordBearer
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+ from sqlalchemy import select
5
+ from jose import JWTError, jwt
6
+ from ..db.database import get_db
7
+ from ..db.models import User
8
+ from ..core.config import settings
9
+
10
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login")
11
+
12
+ async def get_current_user(
13
+ token: str = Depends(oauth2_scheme),
14
+ db: AsyncSession = Depends(get_db)
15
+ ):
16
+ credentials_exception = HTTPException(
17
+ status_code=status.HTTP_401_UNAUTHORIZED,
18
+ detail="Could not validate credentials",
19
+ headers={"WWW-Authenticate": "Bearer"},
20
+ )
21
+
22
+ try:
23
+ payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
24
+ user_id: str = payload.get("sub")
25
+ if user_id is None:
26
+ raise credentials_exception
27
+ except JWTError:
28
+ raise credentials_exception
29
+
30
+ stmt = select(User).where(User.id == int(user_id))
31
+ result = await db.execute(stmt)
32
+ user = result.scalar_one_or_none()
33
+
34
+ if user is None:
35
+ raise credentials_exception
36
+ return user
37
+
38
+ async def get_current_active_user(
39
+ current_user: User = Depends(get_current_user)
40
+ ):
41
+ if not current_user.is_active:
42
+ raise HTTPException(status_code=400, detail="Inactive user")
43
+ return current_user
44
+
45
+ async def get_current_superuser(
46
+ current_user: User = Depends(get_current_user)
47
+ ):
48
+ if not current_user.is_superuser:
49
+ raise HTTPException(
50
+ status_code=403, detail="The user doesn't have enough privileges"
51
+ )
52
+ return current_user
app/core/security.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+ from typing import Any, Optional
3
+ from jose import jwt
4
+ from passlib.context import CryptContext
5
+ from .config import settings
6
+
7
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
8
+
9
+ def create_access_token(subject: Any, expires_delta: Optional[timedelta] = None) -> str:
10
+ if expires_delta:
11
+ expire = datetime.utcnow() + expires_delta
12
+ else:
13
+ expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
14
+
15
+ to_encode = {"exp": expire, "sub": str(subject)}
16
+ encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
17
+ return encoded_jwt
18
+
19
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
20
+ return pwd_context.verify(plain_password, hashed_password)
21
+
22
+ def get_password_hash(password: str) -> str:
23
+ return pwd_context.hash(password)
app/db/__init__.py ADDED
File without changes
app/db/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (171 Bytes). View file
 
app/db/__pycache__/database.cpython-312.pyc ADDED
Binary file (2.61 kB). View file
 
app/db/__pycache__/models.cpython-312.pyc ADDED
Binary file (10.8 kB). View file
 
app/db/__pycache__/schemas.cpython-312.pyc ADDED
Binary file (16.2 kB). View file
 
app/db/database.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
2
+ from sqlalchemy.orm import declarative_base
3
+ from ..core.config import settings
4
+ import contextlib
5
+
6
+ # Create async engine for FastAPI
7
+ async_engine = create_async_engine(
8
+ settings.DATABASE_URL,
9
+ echo=True,
10
+ future=True,
11
+ pool_pre_ping=True
12
+ )
13
+
14
+ # Create async session factory
15
+ AsyncSessionLocal = async_sessionmaker(
16
+ bind=async_engine,
17
+ class_=AsyncSession,
18
+ expire_on_commit=False
19
+ )
20
+
21
+ # Create declarative base for models
22
+ Base = declarative_base()
23
+
24
+ # Database dependency for FastAPI routes
25
+ async def get_db():
26
+ async with AsyncSessionLocal() as session:
27
+ try:
28
+ yield session
29
+ finally:
30
+ await session.close()
31
+
32
+ # Database access for background tasks and services
33
+ class Database:
34
+ def __init__(self):
35
+ self._session_factory = AsyncSessionLocal
36
+
37
+ @contextlib.asynccontextmanager
38
+ async def session(self):
39
+ """Get a database session with automatic commit/rollback"""
40
+ session = self._session_factory()
41
+ try:
42
+ yield session
43
+ await session.commit()
44
+ except:
45
+ await session.rollback()
46
+ raise
47
+ finally:
48
+ await session.close()
49
+
50
+ async def get_session(self):
51
+ """Get a session for manual management"""
52
+ return self._session_factory()
53
+
54
+ # Create singleton instance for database access
55
+ db = Database()
app/db/init_db.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.orm import sessionmaker
3
+ from ..core.config import settings
4
+ from ..core.security import get_password_hash
5
+ from datetime import datetime
6
+ from .models import Base, User, Product
7
+ import asyncio
8
+
9
+ def init_db():
10
+ # Create synchronous engine for initialization
11
+ engine = create_engine(
12
+ settings.DATABASE_URL.replace("+asyncpg", ""),
13
+ echo=True
14
+ )
15
+
16
+ # Create all tables
17
+ Base.metadata.create_all(bind=engine)
18
+
19
+ # Create session
20
+ SessionLocal = sessionmaker(bind=engine)
21
+ session = SessionLocal()
22
+
23
+ try:
24
+ # Create default admin user if not exists
25
+ admin_user = session.query(User).filter_by(email="admin@example.com").first()
26
+ if not admin_user:
27
+ admin_user = User(
28
+ email="admin@example.com",
29
+ username="admin",
30
+ full_name="System Administrator",
31
+ hashed_password=get_password_hash("admin123"), # Change in production
32
+ is_active=True,
33
+ is_superuser=True,
34
+ roles=["admin"],
35
+ created_at=datetime.utcnow()
36
+ )
37
+ session.add(admin_user)
38
+ print("Created default admin user.")
39
+
40
+ # Create default product categories as products
41
+ categories = [
42
+ "Soups & Stews",
43
+ "Rice Dishes",
44
+ "Swallow & Fufu",
45
+ "Snacks & Small Chops",
46
+ "Protein & Meat",
47
+ "Drinks"
48
+ ]
49
+
50
+ for category in categories:
51
+ exists = session.query(Product).filter_by(name=category).first()
52
+ if not exists:
53
+ product = Product(
54
+ name=category,
55
+ description=f"Category: {category}",
56
+ price=0.0, # Category products have zero price
57
+ category=category,
58
+ inventory_count=0, # Categories don't have inventory
59
+ seller_id=admin_user.id if admin_user else 1, # Link to admin user
60
+ created_at=datetime.utcnow()
61
+ )
62
+ session.add(product)
63
+
64
+ print("Initialized product categories.")
65
+
66
+ # Commit changes
67
+ session.commit()
68
+
69
+ except Exception as e:
70
+ print(f"Error during initialization: {e}")
71
+ session.rollback()
72
+ raise
73
+ finally:
74
+ session.close()
75
+
76
+ if __name__ == "__main__":
77
+ init_db()