Ver código fonte

feat(admin): allow editing individual file quantities in the order edit modal

unknown 3 meses atrás
pai
commit
2c3be56362
3 arquivos alterados com 53 adições e 23 exclusões
  1. 25 19
      backend/routers/orders.py
  2. 15 0
      src/lib/api.ts
  3. 13 4
      src/pages/Admin.vue

+ 25 - 19
backend/routers/orders.py

@@ -445,6 +445,7 @@ async def update_order(
 @router.post("/{order_id}/attach-file")
 async def admin_attach_file(
     order_id: int,
+    background_tasks: BackgroundTasks,
     file: UploadFile = File(...),
     admin: dict = Depends(require_admin)
 ):
@@ -459,31 +460,18 @@ async def admin_attach_file(
             sha256_hash.update(chunk)
             buffer.write(chunk)
     
-    preview_path = None
-    db_preview_path = None
-    if file_path.lower().endswith(".stl"):
-        preview_filename = f"{uuid.uuid4()}.png"
-        preview_path = os.path.join(config.PREVIEW_DIR, preview_filename)
-        db_preview_path = f"uploads/previews/{preview_filename}"
-        preview_utils.generate_stl_preview(file_path, preview_path)
-
-    filament_g = None
-    print_time = None
-    if file_path.lower().endswith(".stl"):
-        result = slicer_utils.slice_model(file_path)
-        if result and result.get('success'):
-            filament_g = result.get('filament_g')
-            print_time = result.get('print_time_str')
-
-    query = "INSERT INTO order_files (order_id, filename, file_path, file_size, quantity, file_hash, print_time, filament_g, preview_path) VALUES (%s, %s, %s, %s, 1, %s, %s, %s, %s)"
-    f_id = db.execute_commit(query, (order_id, file.filename, db_file_path, file.size, sha256_hash.hexdigest(), print_time, filament_g, db_preview_path))
+    query = "INSERT INTO order_files (order_id, filename, file_path, file_size, quantity, file_hash, print_time, filament_g, preview_path) VALUES (%s, %s, %s, %s, 1, %s, NULL, NULL, NULL)"
+    f_id = db.execute_commit(query, (order_id, file.filename, db_file_path, file.size, sha256_hash.hexdigest()))
+    
+    # Run heavy processing in background to avoid worker timeouts
+    background_tasks.add_task(order_processing.process_single_file_async, f_id)
     
     # NOTIFY USER VIA WEBSOCKET
     order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
     if order_info:
         await global_manager.notify_order_update(order_info[0]['user_id'], order_id)
 
-    return {"file_id": f_id, "filename": file.filename, "preview_path": db_preview_path, "filament_g": filament_g, "print_time": print_time}
+    return {"file_id": f_id, "filename": file.filename, "status": "processing"}
 
 @router.delete("/{order_id}/files/{file_id}")
 async def admin_delete_file(
@@ -522,6 +510,24 @@ async def admin_delete_file(
         await global_manager.notify_order_update(order_info[0]['user_id'], order_id)
 
     return {"status": "success"}
+
+@router.patch("/{order_id}/files/{file_id}")
+async def admin_update_file(
+    order_id: int,
+    file_id: int,
+    data: dict,
+    admin: dict = Depends(require_admin)
+):
+    if "quantity" in data:
+        db.execute_commit("UPDATE order_files SET quantity = %s WHERE id = %s AND order_id = %s", (data['quantity'], file_id, order_id))
+    
+    # Notify user/admin via WS
+    order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
+    if order_info:
+        await global_manager.notify_order_update(order_info[0]['user_id'], order_id)
+        
+    return {"status": "success"}
+
     
 class OrderItemSchema(BaseModel):
     description: str

+ 15 - 0
src/lib/api.ts

@@ -485,6 +485,21 @@ export const adminDeleteFile = async (orderId: number, fileId: number) => {
   return response.json();
 };
 
+export const adminUpdateFile = async (orderId: number, fileId: number, data: any) => {
+  const token = localStorage.getItem("token");
+  const response = await fetch(`${API_BASE_URL}/orders/${orderId}/files/${fileId}?lang=${i18n.global.locale.value}`, {
+    method: 'PATCH',
+    headers: { 
+      'Authorization': `Bearer ${token}`,
+      'Content-Type': 'application/json'
+    },
+    body: JSON.stringify(data)
+  });
+  if (!response.ok) throw new Error("Failed to update file");
+  return response.json();
+};
+
+
 export const adminUpdatePhotoStatus = async (photoId: number, data: { is_public: boolean }) => {
   const token = localStorage.getItem("token");
   const response = await fetch(`${API_BASE_URL}/admin/photos/${photoId}?lang=${i18n.global.locale.value}`, {

+ 13 - 4
src/pages/Admin.vue

@@ -218,9 +218,18 @@
                     </label>
                   </div>
                   <div class="space-y-2 max-h-[200px] overflow-y-auto pr-1 custom-scrollbar">
-                    <div v-for="f in (editingOrder?.files || [])" :key="f.id" class="flex items-center justify-between p-2 bg-background/50 rounded-xl border border-border/50 text-[11px]">
-                       <span class="truncate max-w-[150px] font-medium">{{ f.filename }}</span>
-                       <div class="flex gap-1">
+                    <div v-for="f in (editingOrder?.files || [])" :key="f.id" class="flex items-center justify-between p-2 bg-background/50 rounded-xl border border-border/50 text-[11px] gap-2">
+                       <span class="truncate flex-1 font-medium">{{ f.filename }}</span>
+                       <div class="flex items-center gap-2">
+                          <div class="flex items-center bg-muted/50 rounded-lg px-2 py-1 border border-border/50">
+                             <span class="text-[9px] font-bold text-muted-foreground mr-1">QTY:</span>
+                             <input 
+                               type="number" 
+                               v-model.number="f.quantity" 
+                               @change="adminUpdateFile(editingOrder.id, f.id, { quantity: f.quantity })"
+                               class="w-8 bg-transparent text-center font-black outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" 
+                             />
+                          </div>
                           <a :href="`${RESOURCES_BASE_URL}/${f.file_path}`" target="_blank" class="p-1.5 hover:bg-primary/10 rounded-md text-primary transition-colors"><Database class="w-3 h-3" /></a>
                           <button type="button" @click="handleDeleteFile(editingOrder.id, f.id, f.filename)" class="p-1.5 hover:bg-rose-500/10 rounded-md text-rose-500 transition-colors"><Trash2 class="w-3 h-3" /></button>
                        </div>
@@ -358,7 +367,7 @@ import {
   adminGetOrders, adminUpdateOrder, adminGetMaterials, adminUpdateMaterial, 
   adminDeleteMaterial, adminCreateMaterial, adminGetServices, adminUpdateService, 
   adminDeleteService, adminCreateService, adminUploadOrderPhoto, adminUpdatePhotoStatus, 
-  adminDeletePhoto, adminGetAllPhotos, adminAttachFile, adminDeleteFile, adminDeleteOrder, 
+  adminDeletePhoto, adminGetAllPhotos, adminAttachFile, adminUpdateFile, adminDeleteFile, adminDeleteOrder, 
   getBlogPosts, adminUpdatePost, adminDeletePost, adminCreatePost, 
   adminGetUsers, adminUpdateUser, adminCreateUser, 
   adminGetAuditLogs, approveOrderReview, RESOURCES_BASE_URL