catalog.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. from fastapi import APIRouter, Depends, HTTPException
  2. from typing import List, Optional
  3. import db
  4. import schemas
  5. import auth_utils
  6. import json
  7. import config
  8. from dependencies import require_admin
  9. from services.audit_service import audit_service
  10. router = APIRouter(tags=["catalog"])
  11. @router.get("/materials", response_model=List[schemas.MaterialBase])
  12. async def get_materials():
  13. # Get active materials with their effective hourly price
  14. query = """
  15. SELECT m.*,
  16. COALESCE(NULLIF(m.price_per_hour, 0), s.price_per_hour, %s) as effective_price_per_hour
  17. FROM materials m
  18. LEFT JOIN services s ON m.service_id = s.id
  19. WHERE m.is_active = TRUE
  20. """
  21. materials = db.execute_query(query, (config.DEFAULT_PRINTER_HOUR_PRICE,))
  22. # Get active stock
  23. stock = db.execute_query("SELECT material_id, color_name FROM warehouse_stock WHERE is_active = TRUE")
  24. # Map colors to materials
  25. color_map = {}
  26. for s in stock:
  27. m_id = s['material_id']
  28. if m_id not in color_map:
  29. color_map[m_id] = []
  30. color_map[m_id].append(s['color_name'])
  31. # Filter and attach colors
  32. result = []
  33. for m in materials:
  34. m['available_colors'] = color_map.get(m['id'], [])
  35. # Ensure numeric types for frontend
  36. m['price_per_cm3'] = float(m['price_per_cm3']) if m['price_per_cm3'] else 0.0
  37. m['effective_price_per_hour'] = float(m['effective_price_per_hour'])
  38. m['speed_coeff'] = float(m['speed_coeff']) if m['speed_coeff'] else 1.0
  39. # Only show on site if there are available colors in warehouse
  40. if m['available_colors']:
  41. result.append(m)
  42. return result
  43. @router.get("/services", response_model=List[schemas.ServiceBase])
  44. async def get_services():
  45. return db.execute_query("SELECT * FROM services WHERE is_active = TRUE")
  46. @router.get("/admin/materials", response_model=List[schemas.MaterialBase])
  47. async def admin_get_materials(admin: dict = Depends(require_admin)):
  48. # Get all materials
  49. materials = db.execute_query("SELECT * FROM materials ORDER BY id DESC")
  50. # Get stock (including inactive for admin)
  51. stock = db.execute_query("SELECT material_id, color_name FROM warehouse_stock")
  52. color_map = {}
  53. for s in stock:
  54. m_id = s['material_id']
  55. if m_id not in color_map:
  56. color_map[m_id] = []
  57. color_map[m_id].append(s['color_name'])
  58. for m in materials:
  59. # Merge warehouse colors with any legacy colors in available_colors
  60. legacy_colors = []
  61. if m.get('available_colors') and isinstance(m['available_colors'], str):
  62. try: legacy_colors = json.loads(m['available_colors'])
  63. except: pass
  64. # Priority to warehouse colors
  65. warehouse_colors = color_map.get(m['id'], [])
  66. # Combine them (unique)
  67. m['available_colors'] = list(set(warehouse_colors + (legacy_colors or [])))
  68. return materials
  69. @router.post("/admin/materials")
  70. async def admin_create_material(data: schemas.MaterialCreate, admin: dict = Depends(require_admin)):
  71. colors_json = json.dumps(data.available_colors) if data.available_colors else None
  72. query = "INSERT INTO materials (name_en, name_ru, name_ua, name_me, desc_en, desc_ru, desc_ua, desc_me, long_desc_en, long_desc_ru, long_desc_ua, long_desc_me, service_id, price_per_cm3, price_per_hour, speed_coeff, available_colors, is_active) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"
  73. params = (data.name_en, data.name_ru, data.name_ua, data.name_me, data.desc_en, data.desc_ru, data.desc_ua, data.desc_me, data.long_desc_en, data.long_desc_ru, data.long_desc_ua, data.long_desc_me, data.service_id, data.price_per_cm3, data.price_per_hour, data.speed_coeff, colors_json, data.is_active)
  74. mat_id = db.execute_commit(query, params)
  75. await audit_service.log(
  76. user_id=admin.get("id"),
  77. action="create_material",
  78. target_type="material",
  79. target_id=mat_id,
  80. details=data.dict()
  81. )
  82. return {"id": mat_id}
  83. @router.patch("/admin/materials/{mat_id}")
  84. async def admin_update_material(mat_id: int, data: schemas.MaterialUpdate, admin: dict = Depends(require_admin)):
  85. update_fields = []
  86. params = []
  87. for field, value in data.dict(exclude_unset=True).items():
  88. update_fields.append(f"{field} = %s")
  89. if isinstance(value, list):
  90. params.append(json.dumps(value))
  91. else:
  92. params.append(value)
  93. if update_fields:
  94. query = f"UPDATE materials SET {', '.join(update_fields)} WHERE id = %s"
  95. params.append(mat_id)
  96. db.execute_commit(query, tuple(params))
  97. await audit_service.log(
  98. user_id=admin.get("id"),
  99. action="update_material",
  100. target_type="material",
  101. target_id=mat_id,
  102. details={"updated_fields": data.dict(exclude_unset=True)}
  103. )
  104. return {"id": mat_id}
  105. @router.get("/admin/services", response_model=List[schemas.ServiceBase])
  106. async def admin_get_services(admin: dict = Depends(require_admin)):
  107. return db.execute_query("SELECT * FROM services ORDER BY id DESC")
  108. @router.post("/admin/services")
  109. async def admin_create_service(data: schemas.ServiceCreate, admin: dict = Depends(require_admin)):
  110. query = "INSERT INTO services (name_en, name_ru, name_ua, name_me, desc_en, desc_ru, desc_ua, desc_me, tech_type, price_per_hour, is_active) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"
  111. srv_id = db.execute_commit(query, (data.name_en, data.name_ru, data.name_ua, data.name_me, data.desc_en, data.desc_ru, data.desc_ua, data.desc_me, data.tech_type, data.price_per_hour, data.is_active))
  112. await audit_service.log(
  113. user_id=admin.get("id"),
  114. action="create_service",
  115. target_type="service",
  116. target_id=srv_id,
  117. details=data.dict()
  118. )
  119. return {"id": srv_id}
  120. @router.patch("/admin/services/{srv_id}")
  121. async def admin_update_service(srv_id: int, data: schemas.ServiceUpdate, admin: dict = Depends(require_admin)):
  122. update_fields = []
  123. params = []
  124. for field, value in data.dict(exclude_unset=True).items():
  125. update_fields.append(f"{field} = %s")
  126. if isinstance(value, list):
  127. params.append(json.dumps(value))
  128. else:
  129. params.append(value)
  130. if update_fields:
  131. query = f"UPDATE services SET {', '.join(update_fields)} WHERE id = %s"
  132. params.append(srv_id)
  133. db.execute_commit(query, tuple(params))
  134. await audit_service.log(
  135. user_id=admin.get("id"),
  136. action="update_service",
  137. target_type="service",
  138. target_id=srv_id,
  139. details={"updated_fields": data.dict(exclude_unset=True)}
  140. )
  141. return {"id": srv_id}
  142. @router.delete("/admin/materials/{mat_id}")
  143. async def admin_delete_material(mat_id: int, admin: dict = Depends(require_admin)):
  144. db.execute_commit("DELETE FROM materials WHERE id = %s", (mat_id,))
  145. await audit_service.log(
  146. user_id=admin.get("id"),
  147. action="delete_material",
  148. target_type="material",
  149. target_id=mat_id
  150. )
  151. return {"id": mat_id, "status": "deleted"}
  152. @router.delete("/admin/services/{srv_id}")
  153. async def admin_delete_service(srv_id: int, admin: dict = Depends(require_admin)):
  154. db.execute_commit("DELETE FROM services WHERE id = %s", (srv_id,))
  155. await audit_service.log(
  156. user_id=admin.get("id"),
  157. action="delete_service",
  158. target_type="service",
  159. target_id=srv_id
  160. )
  161. return {"id": srv_id, "status": "deleted"}