Parcourir la source

feat: full order editing capabilities in admin

unknown il y a 1 jour
Parent
commit
39202c9fac
3 fichiers modifiés avec 129 ajouts et 2 suppressions
  1. 19 0
      backend/routers/orders.py
  2. 7 0
      backend/schemas.py
  3. 103 2
      src/pages/Admin.vue

+ 19 - 0
backend/routers/orders.py

@@ -377,6 +377,25 @@ async def update_order(
         update_fields.append("jikr = %s")
         params.append(data.jikr)
         
+    if data.first_name is not None:
+        update_fields.append("first_name = %s")
+        params.append(data.first_name)
+    if data.last_name is not None:
+        update_fields.append("last_name = %s")
+        params.append(data.last_name)
+    if data.email is not None:
+        update_fields.append("email = %s")
+        params.append(data.email)
+    if data.phone is not None:
+        update_fields.append("phone = %s")
+        params.append(data.phone)
+    if data.shipping_address is not None:
+        update_fields.append("shipping_address = %s")
+        params.append(data.shipping_address)
+    if data.notes is not None:
+        update_fields.append("notes = %s")
+        params.append(data.notes)
+
     if data.review_text is not None:
         update_fields.append("review_text = %s")
         params.append(data.review_text)

+ 7 - 0
backend/schemas.py

@@ -177,6 +177,13 @@ class AdminOrderUpdate(BaseModel):
     fiscal_qr_url: Optional[str] = None
     ikof: Optional[str] = None
     jikr: Optional[str] = None
+    # Customer data
+    first_name: Optional[str] = None
+    last_name: Optional[str] = None
+    email: Optional[EmailStr] = None
+    phone: Optional[str] = None
+    shipping_address: Optional[str] = None
+    notes: Optional[str] = None
     # Review management
     review_text: Optional[str] = None
     rating: Optional[int] = None

+ 103 - 2
src/pages/Admin.vue

@@ -66,6 +66,7 @@
           @close-chat="adminChatId = null"
           @update-notify="(id, val) => notifyStatusMap[id] = val"
           @update-fiscal="handleUpdateFiscal"
+          @edit-order="handleEditOrder"
         />
 
         <MaterialsSection
@@ -138,7 +139,76 @@
          <div v-if="showAddModal" class="fixed inset-0 z-[100] flex items-center justify-center p-4">
            <div class="absolute inset-0 bg-background/80 backdrop-blur-sm" @click="closeModals" />
            <div class="relative w-full max-w-lg bg-card border border-border/50 rounded-3xl p-8 shadow-2xl overflow-y-auto max-h-[90vh]">
-              <h3 class="text-xl font-bold font-display mb-6">{{ editingMaterial || editingService || editingPost ? t('admin.actions.edit') : t("admin.addNew") }}</h3>
+              <h3 class="text-xl font-bold font-display mb-6">{{ editingMaterial || editingService || editingPost || editingOrder ? t('admin.actions.edit') : t("admin.addNew") }}</h3>
+              
+              <!-- Order Edit Form -->
+              <form v-if="editingOrder" @submit.prevent="handleSaveOrder" class="space-y-4">
+                <div class="grid grid-cols-2 gap-4">
+                  <div class="space-y-1">
+                    <label class="text-[10px] font-bold uppercase ml-1">{{ t("auth.fields.firstName") }}</label>
+                    <input v-model="orderForm.first_name" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
+                  </div>
+                  <div class="space-y-1">
+                    <label class="text-[10px] font-bold uppercase ml-1">{{ t("auth.fields.lastName") }}</label>
+                    <input v-model="orderForm.last_name" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
+                  </div>
+                </div>
+                <div class="grid grid-cols-2 gap-4">
+                  <div class="space-y-1">
+                    <label class="text-[10px] font-bold uppercase ml-1">{{ t("auth.fields.email") }}</label>
+                    <input v-model="orderForm.email" type="email" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
+                  </div>
+                  <div class="space-y-1">
+                    <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.phone") }}</label>
+                    <input v-model="orderForm.phone" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
+                  </div>
+                </div>
+                <div class="space-y-1">
+                  <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.address") }}</label>
+                  <input v-model="orderForm.shipping_address" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
+                </div>
+                <div class="space-y-1">
+                  <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.projectNotes") }}</label>
+                  <textarea v-model="orderForm.notes" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm h-20" />
+                </div>
+
+                <div class="grid grid-cols-2 gap-4 pt-4 border-t border-border/10">
+                  <div class="space-y-1">
+                    <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.totalPrice") }} (EUR)</label>
+                    <input v-model.number="orderForm.total_price" type="number" step="0.01" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
+                  </div>
+                  <div class="space-y-1">
+                    <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.quantity") }}</label>
+                    <input v-model.number="orderForm.quantity" type="number" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
+                  </div>
+                </div>
+                <div class="grid grid-cols-2 gap-4">
+                  <div class="space-y-1">
+                    <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.material") }}</label>
+                    <input v-model="orderForm.material_name" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
+                  </div>
+                  <div class="space-y-1">
+                    <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.colors") }}</label>
+                    <input v-model="orderForm.color_name" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
+                  </div>
+                </div>
+                <!-- Review Edit fallback -->
+                <div v-if="orderForm.review_text" class="pt-4 border-t border-border/10 space-y-4">
+                   <div class="space-y-1">
+                     <label class="text-[10px] font-bold uppercase ml-1">Review Text</label>
+                     <textarea v-model="orderForm.review_text" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm h-20" />
+                   </div>
+                   <div class="space-y-1">
+                     <label class="text-[10px] font-bold uppercase ml-1">Rating (1-5)</label>
+                     <input v-model.number="orderForm.rating" type="number" min="1" max="5" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
+                   </div>
+                </div>
+
+                <div class="flex gap-3 pt-6 border-t border-border/10">
+                  <Button type="button" variant="ghost" class="flex-1" @click="closeModals">{{ t("admin.actions.cancel") }}</Button>
+                  <Button type="submit" variant="hero" class="flex-1">{{ t("admin.actions.save") }}</Button>
+                </div>
+              </form>
               
               <!-- Material Modal Form -->
               <form v-if="activeTab === 'materials'" @submit.prevent="handleSaveMaterial" class="space-y-4">
@@ -314,11 +384,13 @@ const showAddModal = ref(false);
 const editingMaterial = ref<any | null>(null);
 const editingService = ref<any | null>(null);
 const editingPost = ref<any | null>(null);
+const editingOrder = ref<any | null>(null);
 
 const matForm = reactive({ name_en: "", name_ru: "", name_ua: "", name_me: "", desc_en: "", desc_ru: "", desc_ua: "", desc_me: "", price_per_cm3: 0, available_colors: [] as string[], is_active: true });
 const svcForm = reactive({ name_en: "", name_ru: "", name_ua: "", name_me: "", desc_en: "", desc_ru: "", desc_ua: "", desc_me: "", tech_type: "", is_active: true });
 const postForm = reactive({ slug: "", title_en: "", title_me: "", title_ru: "", title_ua: "", category: "Technology", image_url: "", is_published: true });
 const userForm = reactive({ email: "", password: "", first_name: "", last_name: "", phone: "" });
+const orderForm = reactive({ total_price: 0, material_name: "", color_name: "", quantity: 1, first_name: "", last_name: "", email: "", phone: "", shipping_address: "", notes: "", review_text: "", rating: 0 });
 
 const newColor = ref("");
 
@@ -378,6 +450,25 @@ const handleUpdateFiscal = async (id: number, data: any) => {
   } catch (err: any) { toast.error(err.message); }
 };
 
+const handleEditOrder = (order: any) => {
+  editingOrder.value = order;
+  Object.assign(orderForm, {
+    total_price: order.invoice_amount || 0,
+    material_name: order.material_name || "",
+    color_name: order.color_name || "",
+    quantity: order.quantity || 1,
+    first_name: order.first_name || "",
+    last_name: order.last_name || "",
+    email: order.email || "",
+    phone: order.phone || "",
+    shipping_address: order.shipping_address || "",
+    notes: order.notes || "",
+    review_text: order.review_text || "",
+    rating: order.rating || 0
+  });
+  showAddModal.value = true;
+};
+
 const handleApproveReview = async (id: number) => {
   try {
     await approveOrderReview(id); toast.success("Review approved"); fetchData();
@@ -445,11 +536,12 @@ const handleResetPassword = async (id: number) => {
 const handleAddNew = () => { closeModals(); showAddModal.value = true; };
 
 const closeModals = () => {
-  showAddModal.value = false; editingMaterial.value = null; editingService.value = null; editingPost.value = null;
+  showAddModal.value = false; editingMaterial.value = null; editingService.value = null; editingPost.value = null; editingOrder.value = null;
   Object.assign(matForm, { name_en: "", name_ru: "", name_me: "", name_ua: "", desc_en: "", desc_ru: "", desc_ua: "", desc_me: "", price_per_cm3: 0, available_colors: [], is_active: true });
   Object.assign(svcForm, { name_en: "", name_ru: "", name_me: "", name_ua: "", desc_en: "", desc_ru: "", desc_ua: "", desc_me: "", tech_type: "", is_active: true });
   Object.assign(userForm, { email: "", password: "", first_name: "", last_name: "", phone: "" });
   Object.assign(postForm, { slug: "", title_en: "", title_me: "", title_ru: "", title_ua: "", category: "Technology", image_url: "", is_published: true });
+  Object.assign(orderForm, { total_price: 0, material_name: "", color_name: "", quantity: 1, first_name: "", last_name: "", email: "", phone: "", shipping_address: "", notes: "", review_text: "", rating: 0 });
 };
 
 function addColor() { if (newColor.value) { matForm.available_colors.push(newColor.value); newColor.value = ""; } }
@@ -476,6 +568,15 @@ async function handleSavePost() {
     closeModals(); fetchData();
   } catch (err: any) { toast.error(err.message); }
 }
+async function handleSaveOrder() {
+  if (!editingOrder.value) return;
+  try {
+    await adminUpdateOrder(editingOrder.value.id, orderForm);
+    closeModals(); fetchData();
+    toast.success("Order updated");
+  } catch (err: any) { toast.error(err.message); }
+}
+
 async function handleSaveUser() { try { await adminCreateUser(userForm); closeModals(); fetchUsers(); } catch (err: any) { toast.error(err.message); } }
 
 // Lifecycle