pharmacy-erp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.env.example +34 -0
  2. package/LICENSE +21 -0
  3. package/README.md +479 -0
  4. package/backend/Dockerfile +44 -0
  5. package/backend/app/__init__.py +1 -0
  6. package/backend/app/core/__init__.py +1 -0
  7. package/backend/app/core/audit.py +33 -0
  8. package/backend/app/core/config.py +16 -0
  9. package/backend/app/core/database.py +26 -0
  10. package/backend/app/core/deps.py +49 -0
  11. package/backend/app/core/exceptions.py +43 -0
  12. package/backend/app/core/security.py +32 -0
  13. package/backend/app/main.py +54 -0
  14. package/backend/app/modules/__init__.py +0 -0
  15. package/backend/app/modules/admin/__init__.py +1 -0
  16. package/backend/app/modules/admin/models.py +15 -0
  17. package/backend/app/modules/admin/router.py +353 -0
  18. package/backend/app/modules/auth/__init__.py +0 -0
  19. package/backend/app/modules/auth/models.py +16 -0
  20. package/backend/app/modules/auth/router.py +73 -0
  21. package/backend/app/modules/auth/schemas.py +29 -0
  22. package/backend/app/modules/cash_sessions/__init__.py +0 -0
  23. package/backend/app/modules/cash_sessions/models.py +19 -0
  24. package/backend/app/modules/cash_sessions/router.py +85 -0
  25. package/backend/app/modules/cash_sessions/schemas.py +31 -0
  26. package/backend/app/modules/customers/__init__.py +0 -0
  27. package/backend/app/modules/customers/models.py +37 -0
  28. package/backend/app/modules/customers/router.py +296 -0
  29. package/backend/app/modules/customers/schemas.py +72 -0
  30. package/backend/app/modules/inventory/__init__.py +0 -0
  31. package/backend/app/modules/inventory/models.py +16 -0
  32. package/backend/app/modules/inventory/router.py +150 -0
  33. package/backend/app/modules/inventory/schemas.py +24 -0
  34. package/backend/app/modules/medicines/__init__.py +0 -0
  35. package/backend/app/modules/medicines/models.py +54 -0
  36. package/backend/app/modules/medicines/router.py +631 -0
  37. package/backend/app/modules/medicines/schemas.py +153 -0
  38. package/backend/app/modules/medicines/template_generator.py +118 -0
  39. package/backend/app/modules/notifications/__init__.py +1 -0
  40. package/backend/app/modules/notifications/models.py +15 -0
  41. package/backend/app/modules/notifications/router.py +89 -0
  42. package/backend/app/modules/notifications/schemas.py +22 -0
  43. package/backend/app/modules/organizations/__init__.py +0 -0
  44. package/backend/app/modules/organizations/models.py +16 -0
  45. package/backend/app/modules/organizations/router.py +49 -0
  46. package/backend/app/modules/organizations/schemas.py +31 -0
  47. package/backend/app/modules/reports/__init__.py +0 -0
  48. package/backend/app/modules/reports/router.py +409 -0
  49. package/backend/app/modules/roles/__init__.py +1 -0
  50. package/backend/app/modules/roles/models.py +43 -0
  51. package/backend/app/modules/roles/router.py +89 -0
  52. package/backend/app/modules/roles/schemas.py +55 -0
  53. package/backend/app/modules/sales/__init__.py +0 -0
  54. package/backend/app/modules/sales/models.py +44 -0
  55. package/backend/app/modules/sales/router.py +63 -0
  56. package/backend/app/modules/sales/schemas.py +42 -0
  57. package/backend/app/modules/sales/service.py +128 -0
  58. package/backend/app/modules/suppliers/__init__.py +0 -0
  59. package/backend/app/modules/suppliers/models.py +14 -0
  60. package/backend/app/modules/suppliers/router.py +56 -0
  61. package/backend/app/modules/suppliers/schemas.py +27 -0
  62. package/backend/app/modules/users/__init__.py +0 -0
  63. package/backend/app/modules/users/router.py +74 -0
  64. package/backend/app/seed.py +275 -0
  65. package/backend/data/.gitkeep +1 -0
  66. package/backend/requirements.txt +17 -0
  67. package/bin/cli.js +288 -0
  68. package/docker-compose.dev.yml +42 -0
  69. package/docker-compose.yml +59 -0
  70. package/frontend/Dockerfile +56 -0
  71. package/frontend/next.config.ts +19 -0
  72. package/frontend/package.json +60 -0
  73. package/frontend/pnpm-lock.yaml +5678 -0
  74. package/frontend/pnpm-workspace.yaml +6 -0
  75. package/frontend/postcss.config.mjs +6 -0
  76. package/frontend/public/logo.png +0 -0
  77. package/frontend/src/app/[locale]/(app)/admin/page.tsx +1426 -0
  78. package/frontend/src/app/[locale]/(app)/catalog/products/[id]/page.tsx +505 -0
  79. package/frontend/src/app/[locale]/(app)/catalog/products/page.tsx +753 -0
  80. package/frontend/src/app/[locale]/(app)/customers/[id]/page.tsx +500 -0
  81. package/frontend/src/app/[locale]/(app)/customers/page.tsx +538 -0
  82. package/frontend/src/app/[locale]/(app)/dashboard/page.tsx +175 -0
  83. package/frontend/src/app/[locale]/(app)/inventory/page.tsx +765 -0
  84. package/frontend/src/app/[locale]/(app)/layout.tsx +60 -0
  85. package/frontend/src/app/[locale]/(app)/purchasing/page.tsx +1 -0
  86. package/frontend/src/app/[locale]/(app)/reports/page.tsx +794 -0
  87. package/frontend/src/app/[locale]/(app)/sales/page.tsx +296 -0
  88. package/frontend/src/app/[locale]/(auth)/login/page.tsx +100 -0
  89. package/frontend/src/app/[locale]/(pos)/layout.tsx +33 -0
  90. package/frontend/src/app/[locale]/(pos)/pos/page.tsx +463 -0
  91. package/frontend/src/app/[locale]/(pos)/pos/pos-data.ts +89 -0
  92. package/frontend/src/app/[locale]/layout.tsx +30 -0
  93. package/frontend/src/app/[locale]/page.tsx +6 -0
  94. package/frontend/src/app/globals.css +77 -0
  95. package/frontend/src/app/layout.tsx +23 -0
  96. package/frontend/src/app/print.css +18 -0
  97. package/frontend/src/components/pos/CartPanel.tsx +525 -0
  98. package/frontend/src/components/pos/CashSessionGuard.tsx +255 -0
  99. package/frontend/src/components/pos/CloseSessionModal.tsx +160 -0
  100. package/frontend/src/components/pos/PaymentModal.tsx +266 -0
  101. package/frontend/src/components/pos/PaymentSettingsModal.tsx +472 -0
  102. package/frontend/src/components/pos/ProductCatalog.tsx +140 -0
  103. package/frontend/src/components/pos/SaleReceipt.tsx +133 -0
  104. package/frontend/src/components/ui/badge.tsx +28 -0
  105. package/frontend/src/components/ui/bulk-import-modal.tsx +400 -0
  106. package/frontend/src/components/ui/button.tsx +60 -0
  107. package/frontend/src/components/ui/card.tsx +13 -0
  108. package/frontend/src/components/ui/confirm-dialog.tsx +50 -0
  109. package/frontend/src/components/ui/input.tsx +32 -0
  110. package/frontend/src/components/ui/notification-center.tsx +228 -0
  111. package/frontend/src/components/ui/select.tsx +59 -0
  112. package/frontend/src/components/ui/sidebar.tsx +91 -0
  113. package/frontend/src/components/ui/slide-over.tsx +43 -0
  114. package/frontend/src/components/ui/stat-card.tsx +40 -0
  115. package/frontend/src/components/ui/switch.tsx +38 -0
  116. package/frontend/src/components/ui/topbar.tsx +97 -0
  117. package/frontend/src/i18n/request.ts +13 -0
  118. package/frontend/src/i18n/routing.ts +9 -0
  119. package/frontend/src/lib/api.ts +240 -0
  120. package/frontend/src/lib/constants.ts +30 -0
  121. package/frontend/src/lib/utils.ts +32 -0
  122. package/frontend/src/messages/ar.json +106 -0
  123. package/frontend/src/messages/en.json +106 -0
  124. package/frontend/src/middleware.ts +8 -0
  125. package/frontend/src/store/app-store.ts +37 -0
  126. package/frontend/src/store/auth-store.ts +52 -0
  127. package/frontend/src/store/payment-methods-store.ts +144 -0
  128. package/frontend/src/store/pos-store.ts +344 -0
  129. package/frontend/tailwind.config.ts +85 -0
  130. package/frontend/tsconfig.json +27 -0
  131. package/img/backgraund-logo.png +0 -0
  132. package/img/full-logo.png +0 -0
  133. package/img/logo.png +0 -0
  134. package/nginx/nginx.conf +67 -0
  135. package/package.json +88 -0
  136. package/start.bat +90 -0
  137. package/start.sh +64 -0
@@ -0,0 +1,63 @@
1
+ from fastapi import APIRouter, Depends, Query, HTTPException
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select, desc
4
+ from app.core.database import get_db
5
+ from app.core.deps import get_current_user
6
+ from app.modules.auth.models import User
7
+ from app.modules.sales.schemas import SaleCreate, SaleResponse
8
+ from app.modules.sales.service import create_sale, return_sale
9
+ from app.modules.sales.models import Sale
10
+ import uuid
11
+ from typing import List, Optional
12
+
13
+ from app.modules.organizations.models import Branch
14
+
15
+ router = APIRouter(tags=["Sales"])
16
+
17
+ @router.post("", response_model=SaleResponse, include_in_schema=False)
18
+ @router.post("/", response_model=SaleResponse)
19
+ async def process_sale(sale_in: SaleCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
20
+ branch_res = await db.execute(select(Branch).limit(1))
21
+ branch = branch_res.scalars().first()
22
+ if not branch:
23
+ branch = Branch(name="Main Branch", code="MAIN")
24
+ db.add(branch)
25
+ await db.commit()
26
+ await db.refresh(branch)
27
+ branch_id = branch.id
28
+ sale = await create_sale(db, sale_in, branch_id, current_user.id)
29
+ return sale
30
+
31
+ @router.get("", response_model=List[SaleResponse], include_in_schema=False)
32
+ @router.get("/", response_model=List[SaleResponse])
33
+ async def list_sales(
34
+ start_date: Optional[str] = None,
35
+ end_date: Optional[str] = None,
36
+ status: Optional[str] = None,
37
+ search: Optional[str] = None,
38
+ db: AsyncSession = Depends(get_db),
39
+ current_user: User = Depends(get_current_user)
40
+ ):
41
+ query = select(Sale).order_by(desc(Sale.created_at))
42
+
43
+ if status and status != 'ALL':
44
+ query = query.where(Sale.status == status)
45
+ if search:
46
+ query = query.where(Sale.invoice_number.ilike(f"%{search}%"))
47
+
48
+ result = await db.execute(query)
49
+ return result.scalars().all()
50
+
51
+ @router.get("/{sale_id}", response_model=SaleResponse)
52
+ async def get_sale(sale_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
53
+ result = await db.execute(select(Sale).where(Sale.id == sale_id))
54
+ sale = result.scalars().first()
55
+ if not sale:
56
+ raise HTTPException(status_code=404, detail="Sale not found")
57
+ return sale
58
+
59
+ @router.post("/{sale_id}/return", response_model=SaleResponse)
60
+ async def process_return(sale_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
61
+ sale = await return_sale(db, sale_id, current_user.id)
62
+ return sale
63
+
@@ -0,0 +1,42 @@
1
+ from pydantic import BaseModel
2
+ from typing import List, Optional
3
+ import uuid
4
+ from decimal import Decimal
5
+ from datetime import datetime
6
+
7
+ class SaleItemCreate(BaseModel):
8
+ medicine_id: uuid.UUID
9
+ quantity: Decimal
10
+ unit_price: Decimal
11
+
12
+ class SaleCreate(BaseModel):
13
+ payment_method: str
14
+ items: List[SaleItemCreate]
15
+ customer_id: Optional[uuid.UUID] = None
16
+ cash_session_id: Optional[uuid.UUID] = None
17
+
18
+ class CustomerSimple(BaseModel):
19
+ id: uuid.UUID
20
+ name: str
21
+ model_config = {"from_attributes": True}
22
+
23
+ class SaleItemResponse(BaseModel):
24
+ id: uuid.UUID
25
+ medicine_id: uuid.UUID
26
+ batch_id: uuid.UUID
27
+ quantity: Decimal
28
+ unit_price: Decimal
29
+ line_total: Decimal
30
+ model_config = {"from_attributes": True}
31
+
32
+ class SaleResponse(BaseModel):
33
+ id: uuid.UUID
34
+ invoice_number: str
35
+ total_amount: Decimal
36
+ status: str
37
+ payment_method: str
38
+ created_at: datetime
39
+ customer: Optional[CustomerSimple] = None
40
+ items: List[SaleItemResponse] = []
41
+ model_config = {"from_attributes": True}
42
+
@@ -0,0 +1,128 @@
1
+ from decimal import Decimal
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select
4
+ from app.modules.medicines.models import MedicineBatch
5
+ from app.modules.sales.models import Sale, SaleItem
6
+ from app.modules.inventory.models import InventoryMovement
7
+ from app.core.exceptions import ValidationError
8
+ import uuid
9
+ from datetime import datetime
10
+
11
+ async def create_sale(db: AsyncSession, sale_data, branch_id: uuid.UUID, cashier_id: uuid.UUID):
12
+ total_amount = Decimal('0.0')
13
+ sale_items = []
14
+ movements = []
15
+
16
+ for item in sale_data.items:
17
+ result = await db.execute(
18
+ select(MedicineBatch)
19
+ .where(MedicineBatch.medicine_id == item.medicine_id)
20
+ .where(MedicineBatch.quantity_remaining > 0)
21
+ .where(MedicineBatch.is_active == True)
22
+ .where(MedicineBatch.expiry_date > datetime.utcnow())
23
+ .where(MedicineBatch.status.in_(["ACTIVE", "NEAR_EXPIRY"]))
24
+ .order_by(MedicineBatch.expiry_date.asc())
25
+ )
26
+ batches = result.scalars().all()
27
+
28
+ qty_needed = Decimal(str(item.quantity))
29
+
30
+ for batch in batches:
31
+ if qty_needed <= 0:
32
+ break
33
+
34
+ available = Decimal(str(batch.quantity_remaining))
35
+ take = min(available, qty_needed)
36
+
37
+ batch.quantity_remaining = float(available - take)
38
+ qty_needed -= take
39
+
40
+ line_total = take * Decimal(str(item.unit_price))
41
+ sale_items.append(SaleItem(
42
+ medicine_id=item.medicine_id,
43
+ batch_id=batch.id,
44
+ quantity=float(take),
45
+ unit_price=float(item.unit_price),
46
+ line_total=float(line_total)
47
+ ))
48
+ total_amount += line_total
49
+
50
+ movements.append(InventoryMovement(
51
+ medicine_id=item.medicine_id,
52
+ batch_id=batch.id,
53
+ branch_id=branch_id,
54
+ movement_type="SALE_OUT",
55
+ quantity=float(-take),
56
+ created_by=cashier_id
57
+ ))
58
+
59
+ if qty_needed > 0:
60
+ raise ValidationError(f"Insufficient stock for medicine {item.medicine_id}")
61
+
62
+ invoice_number = f"INV-{uuid.uuid4().hex[:8].upper()}"
63
+
64
+ sale = Sale(
65
+ invoice_number=invoice_number,
66
+ branch_id=branch_id,
67
+ cashier_id=cashier_id,
68
+ status="COMPLETED",
69
+ subtotal=float(total_amount),
70
+ total_amount=float(total_amount),
71
+ payment_method=sale_data.payment_method,
72
+ customer_id=sale_data.customer_id,
73
+ cash_session_id=sale_data.cash_session_id
74
+ )
75
+ db.add(sale)
76
+ await db.flush()
77
+
78
+ for si in sale_items:
79
+ si.sale_id = sale.id
80
+ db.add(si)
81
+
82
+ for mv in movements:
83
+ mv.reference_id = str(sale.id)
84
+ mv.reference_type = "SALE"
85
+ db.add(mv)
86
+
87
+ await db.commit()
88
+ await db.refresh(sale)
89
+ return sale
90
+
91
+ async def return_sale(db: AsyncSession, sale_id: uuid.UUID, cashier_id: uuid.UUID):
92
+ result = await db.execute(select(Sale).where(Sale.id == sale_id))
93
+ sale = result.scalars().first()
94
+
95
+ if not sale:
96
+ raise ValidationError(f"Sale {sale_id} not found")
97
+
98
+ if sale.status == "RETURNED":
99
+ raise ValidationError(f"Sale {sale_id} is already returned")
100
+
101
+ items_result = await db.execute(select(SaleItem).where(SaleItem.sale_id == sale_id))
102
+ sale_items = items_result.scalars().all()
103
+
104
+ from decimal import Decimal
105
+ for item in sale_items:
106
+ # Restore stock
107
+ batch_result = await db.execute(select(MedicineBatch).where(MedicineBatch.id == item.batch_id))
108
+ batch = batch_result.scalars().first()
109
+ if batch:
110
+ batch.quantity_remaining += Decimal(str(item.quantity))
111
+
112
+ # Create return movement
113
+ mv = InventoryMovement(
114
+ medicine_id=item.medicine_id,
115
+ batch_id=item.batch_id,
116
+ branch_id=sale.branch_id,
117
+ movement_type="RETURN_IN",
118
+ quantity=Decimal(str(item.quantity)),
119
+ created_by=cashier_id,
120
+ reference_id=str(sale.id),
121
+ reference_type="SALE_RETURN"
122
+ )
123
+ db.add(mv)
124
+
125
+ sale.status = "RETURNED"
126
+ await db.commit()
127
+ await db.refresh(sale)
128
+ return sale
File without changes
@@ -0,0 +1,14 @@
1
+ from sqlalchemy.orm import Mapped, mapped_column
2
+ from sqlalchemy import String, Boolean
3
+ from app.core.database import Base
4
+
5
+ class Supplier(Base):
6
+ __tablename__ = "suppliers"
7
+ name: Mapped[str] = mapped_column(String)
8
+ name_ar: Mapped[str] = mapped_column(String, nullable=True)
9
+ contact_person: Mapped[str] = mapped_column(String, nullable=True)
10
+ phone: Mapped[str] = mapped_column(String, nullable=True)
11
+ email: Mapped[str] = mapped_column(String, nullable=True)
12
+ address: Mapped[str] = mapped_column(String, nullable=True)
13
+ payment_terms: Mapped[str] = mapped_column(String, nullable=True)
14
+ is_active: Mapped[bool] = mapped_column(Boolean, default=True)
@@ -0,0 +1,56 @@
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select
4
+ from typing import List
5
+ from uuid import UUID
6
+
7
+ from app.core.database import get_db
8
+ from app.core.deps import get_current_user
9
+ from app.modules.auth.models import User
10
+ from app.modules.suppliers.models import Supplier
11
+ from app.modules.suppliers.schemas import SupplierCreate, SupplierUpdate, SupplierResponse
12
+
13
+ router = APIRouter(tags=["suppliers"])
14
+
15
+ @router.get("/", response_model=List[SupplierResponse])
16
+ async def list_suppliers(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
17
+ result = await db.execute(select(Supplier))
18
+ return result.scalars().all()
19
+
20
+ @router.get("/{id}", response_model=SupplierResponse)
21
+ async def get_supplier(id: UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
22
+ supplier = await db.get(Supplier, id)
23
+ if not supplier:
24
+ raise HTTPException(status_code=404, detail="Supplier not found")
25
+ return supplier
26
+
27
+ @router.post("/", response_model=SupplierResponse)
28
+ async def create_supplier(data: SupplierCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
29
+ supplier = Supplier(**data.model_dump())
30
+ db.add(supplier)
31
+ await db.commit()
32
+ await db.refresh(supplier)
33
+ return supplier
34
+
35
+ @router.put("/{id}", response_model=SupplierResponse)
36
+ async def update_supplier(id: UUID, data: SupplierUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
37
+ supplier = await db.get(Supplier, id)
38
+ if not supplier:
39
+ raise HTTPException(status_code=404, detail="Supplier not found")
40
+
41
+ for key, value in data.model_dump(exclude_unset=True).items():
42
+ setattr(supplier, key, value)
43
+
44
+ await db.commit()
45
+ await db.refresh(supplier)
46
+ return supplier
47
+
48
+ @router.delete("/{id}")
49
+ async def delete_supplier(id: UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
50
+ supplier = await db.get(Supplier, id)
51
+ if not supplier:
52
+ raise HTTPException(status_code=404, detail="Supplier not found")
53
+
54
+ supplier.is_active = False
55
+ await db.commit()
56
+ return {"message": "Supplier deleted successfully"}
@@ -0,0 +1,27 @@
1
+ from pydantic import BaseModel, EmailStr
2
+ from typing import Optional
3
+ from uuid import UUID
4
+ from datetime import datetime
5
+
6
+ class SupplierBase(BaseModel):
7
+ name: str
8
+ name_ar: Optional[str] = None
9
+ contact_person: Optional[str] = None
10
+ phone: Optional[str] = None
11
+ email: Optional[EmailStr] = None
12
+ address: Optional[str] = None
13
+ payment_terms: Optional[str] = None
14
+
15
+ class SupplierCreate(SupplierBase):
16
+ pass
17
+
18
+ class SupplierUpdate(SupplierBase):
19
+ pass
20
+
21
+ class SupplierResponse(SupplierBase):
22
+ id: UUID
23
+ is_active: bool
24
+ created_at: datetime
25
+
26
+ class Config:
27
+ from_attributes = True
File without changes
@@ -0,0 +1,74 @@
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select
4
+ from typing import List
5
+ from uuid import UUID
6
+
7
+ from app.core.database import get_db
8
+ from app.core.deps import get_current_user
9
+ from app.modules.auth.models import User
10
+ from pydantic import BaseModel, EmailStr
11
+ from typing import Optional
12
+
13
+ class UserUpdate(BaseModel):
14
+ full_name: Optional[str] = None
15
+ full_name_ar: Optional[str] = None
16
+ phone: Optional[str] = None
17
+ role: Optional[str] = None
18
+ is_active: Optional[bool] = None
19
+
20
+ class UserResponse(BaseModel):
21
+ id: UUID
22
+ email: str
23
+ full_name: str
24
+ full_name_ar: Optional[str] = None
25
+ role: str
26
+ phone: Optional[str] = None
27
+ is_active: bool
28
+
29
+ model_config = {"from_attributes": True}
30
+
31
+ router = APIRouter(tags=["users"])
32
+
33
+ @router.get("/", response_model=List[UserResponse])
34
+ async def list_users(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
35
+ if current_user.role != "admin":
36
+ raise HTTPException(status_code=403, detail="Not authorized")
37
+ result = await db.execute(select(User))
38
+ return result.scalars().all()
39
+
40
+ @router.get("/{id}", response_model=UserResponse)
41
+ async def get_user(id: UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
42
+ user = await db.get(User, id)
43
+ if not user:
44
+ raise HTTPException(status_code=404, detail="User not found")
45
+ return user
46
+
47
+ @router.put("/{id}", response_model=UserResponse)
48
+ async def update_user(id: UUID, data: UserUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
49
+ if current_user.role != "admin" and current_user.id != id:
50
+ raise HTTPException(status_code=403, detail="Not authorized")
51
+
52
+ user = await db.get(User, id)
53
+ if not user:
54
+ raise HTTPException(status_code=404, detail="User not found")
55
+
56
+ for key, value in data.model_dump(exclude_unset=True).items():
57
+ setattr(user, key, value)
58
+
59
+ await db.commit()
60
+ await db.refresh(user)
61
+ return user
62
+
63
+ @router.delete("/{id}")
64
+ async def delete_user(id: UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
65
+ if current_user.role != "admin":
66
+ raise HTTPException(status_code=403, detail="Not authorized")
67
+
68
+ user = await db.get(User, id)
69
+ if not user:
70
+ raise HTTPException(status_code=404, detail="User not found")
71
+
72
+ user.is_active = False
73
+ await db.commit()
74
+ return {"message": "User deleted successfully"}