Prechádzať zdrojové kódy

feat: business account support and proforma invoice (Predracun) generation

unknown 6 dní pred
rodič
commit
a9a4f7c275

+ 3 - 1
backend/config.py

@@ -30,5 +30,7 @@ for d in [UPLOAD_DIR, PREVIEW_DIR]:
         os.makedirs(d)
 
 # Payment Config
-ZIRO_RACUN = "510-00000000000-00"
+ZIRO_RACUN = "510-1234567890123-45"
 COMPANY_NAME = "RADIONICA 3D"
+COMPANY_PIB = "01234567"
+COMPANY_ADDRESS = "Cetinjski Put, Podgorica, Montenegro"

+ 8 - 8
backend/routers/auth.py

@@ -21,13 +21,13 @@ async def register(request: Request, user: schemas.UserCreate, lang: str = "en")
     hashed_password = auth_utils.get_password_hash(user.password)
     
     query = """
-    INSERT INTO users (email, password_hash, first_name, last_name, phone, shipping_address, preferred_language, role, ip_address)
-    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
+    INSERT INTO users (email, password_hash, first_name, last_name, phone, shipping_address, preferred_language, role, ip_address, is_company, company_name, company_pib, company_address)
+    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
     """
-    params = (user.email, hashed_password, user.first_name, user.last_name, user.phone, user.shipping_address, user.preferred_language, 'user', ip_address)
+    params = (user.email, hashed_password, user.first_name, user.last_name, user.phone, user.shipping_address, user.preferred_language, 'user', ip_address, user.is_company, user.company_name, user.company_pib, user.company_address)
     
     user_id = db.execute_commit(query, params)
-    new_user = db.execute_query("SELECT id, email, first_name, last_name, phone, shipping_address, preferred_language, role, can_chat, is_active, ip_address, created_at FROM users WHERE id = %s", (user_id,))
+    new_user = db.execute_query("SELECT id, email, first_name, last_name, phone, shipping_address, preferred_language, role, can_chat, is_active, is_company, company_name, company_pib, company_address, ip_address, created_at FROM users WHERE id = %s", (user_id,))
     return new_user[0]
 
 @router.post("/login", response_model=schemas.Token)
@@ -94,7 +94,7 @@ async def reset_password(request: schemas.ResetPassword):
 async def get_me(token: str = Depends(auth_utils.oauth2_scheme)):
     payload = auth_utils.decode_token(token)
     if not payload: raise HTTPException(status_code=401, detail="Invalid token")
-    user = db.execute_query("SELECT id, email, first_name, last_name, phone, shipping_address, preferred_language, role, can_chat, is_active, ip_address, created_at FROM users WHERE id = %s", (payload.get("id"),))
+    user = db.execute_query("SELECT id, email, first_name, last_name, phone, shipping_address, preferred_language, role, can_chat, is_active, is_company, company_name, company_pib, company_address, ip_address, created_at FROM users WHERE id = %s", (payload.get("id"),))
     if not user: raise HTTPException(status_code=404, detail="User not found")
     return user[0]
 
@@ -112,7 +112,7 @@ async def update_me(data: schemas.UserUpdate, token: str = Depends(auth_utils.oa
         query = f"UPDATE users SET {', '.join(update_fields)} WHERE id = %s"
         params.append(user_id)
         db.execute_commit(query, tuple(params))
-    user = db.execute_query("SELECT id, email, first_name, last_name, phone, shipping_address, preferred_language, role, can_chat, is_active, ip_address, created_at FROM users WHERE id = %s", (user_id,))
+    user = db.execute_query("SELECT id, email, first_name, last_name, phone, shipping_address, preferred_language, role, can_chat, is_active, is_company, company_name, company_pib, company_address, ip_address, created_at FROM users WHERE id = %s", (user_id,))
     return user[0]
 
 @router.get("/admin/users")
@@ -122,7 +122,7 @@ async def admin_get_users(page: int = 1, size: int = 50, search: Optional[str] =
         raise HTTPException(status_code=403, detail="Admin role required")
     
     offset = (page - 1) * size
-    base_query = "SELECT id, email, first_name, last_name, phone, role, can_chat, is_active, ip_address, created_at FROM users"
+    base_query = "SELECT id, email, first_name, last_name, phone, role, can_chat, is_active, is_company, company_name, company_pib, company_address, ip_address, created_at FROM users"
     count_query = "SELECT COUNT(*) as total FROM users"
     params = []
     if search and search.strip():
@@ -155,7 +155,7 @@ async def admin_create_user(data: schemas.UserCreate, token: str = Depends(auth_
         (data.email, hashed_password, data.first_name, data.last_name, data.phone, 'user', True)
     )
     
-    user = db.execute_query("SELECT id, email, first_name, last_name, phone, role, can_chat, is_active, created_at FROM users WHERE id = %s", (user_id,))
+    user = db.execute_query("SELECT id, email, first_name, last_name, phone, role, can_chat, is_active, is_company, company_name, company_pib, company_address, created_at FROM users WHERE id = %s", (user_id,))
     return user[0]
 
 @router.patch("/users/{target_id}/admin", response_model=schemas.UserResponse)

+ 33 - 11
backend/routers/orders.py

@@ -36,6 +36,10 @@ async def create_order(
     file_quantities: str = Form("[]"),
     quantity: int = Form(1),
     color_name: Optional[str] = Form(None),
+    is_company: bool = Form(False),
+    company_name: Optional[str] = Form(None),
+    company_pib: Optional[str] = Form(None),
+    company_address: Optional[str] = Form(None),
     token: str = Depends(auth_utils.oauth2_scheme_optional)
 ):
     user_id = None
@@ -66,7 +70,7 @@ async def create_order(
         file_rows = db.execute_query(f"SELECT file_size FROM order_files WHERE id IN ({format_strings})", tuple(parsed_ids))
         file_sizes = [r['file_size'] for r in file_rows]
 
-    estimated_price = pricing.calculate_estimated_price(material_id, file_sizes, parsed_quantities if parsed_quantities else None)
+    estimated_price, item_prices = pricing.calculate_estimated_price(material_id, file_sizes, parsed_quantities if parsed_quantities else None, return_details=True)
 
     # Snapshoting initial parameters
     original_params = json.dumps({
@@ -81,21 +85,26 @@ async def create_order(
         "phone": phone,
         "email": email,
         "shipping_address": shipping_address,
-        "model_link": model_link
+        "model_link": model_link,
+        "is_company": is_company,
+        "company_name": company_name,
+        "company_pib": company_pib,
+        "company_address": company_address
     })
 
     order_query = """
-    INSERT INTO orders (user_id, material_id, first_name, last_name, phone, email, shipping_address, model_link, status, allow_portfolio, estimated_price, material_name, material_price, color_name, quantity, notes, original_params)
-    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 'pending', %s, %s, %s, %s, %s, %s, %s, %s)
+    INSERT INTO orders (user_id, material_id, first_name, last_name, phone, email, shipping_address, model_link, status, is_company, company_name, company_pib, company_address, allow_portfolio, estimated_price, material_name, material_price, color_name, quantity, notes, original_params)
+    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 'pending', %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
     """
-    order_params = (user_id, material_id, first_name, last_name, phone, email, shipping_address, model_link, allow_portfolio, estimated_price, mat_name, mat_price, color_name, quantity, notes, original_params)
+    order_params = (user_id, material_id, first_name, last_name, phone, email, shipping_address, model_link, is_company, company_name, company_pib, company_address, allow_portfolio, estimated_price, mat_name, mat_price, color_name, quantity, notes, original_params)
     
     try:
         order_insert_id = db.execute_commit(order_query, order_params)
         if parsed_ids:
             for idx, f_id in enumerate(parsed_ids):
                 qty = parsed_quantities[idx] if idx < len(parsed_quantities) else 1
-                db.execute_commit("UPDATE order_files SET order_id = %s, quantity = %s WHERE id = %s", (order_insert_id, qty, f_id))
+                unit_p = item_prices[idx] if idx < len(item_prices) else 0.0
+                db.execute_commit("UPDATE order_files SET order_id = %s, quantity = %s, unit_price = %s WHERE id = %s", (order_insert_id, qty, unit_p, f_id))
         background_tasks.add_task(order_processing.process_order_slicing, order_insert_id)
         background_tasks.add_task(event_hooks.on_order_created, order_insert_id)
         return {"status": "success", "order_id": order_insert_id, "message": "Order submitted successfully"}
@@ -199,16 +208,29 @@ async def update_order_admin(
                 data.send_notification
             )
             
-            # Generate Uplatnica PDF Document on moving to "shipped" step
+            # Generate PDF Document on moving to "shipped" step
             if data.status == 'shipped' and not order_info[0].get('invoice_path'):
-                from services.uplatnica_generator import generate_uplatnica
+                from services.uplatnica_generator import generate_uplatnica, generate_predracun
                 o = order_info[0]
-                payer_name = f"{o['first_name']} {o.get('last_name', '')}".strip()
-                addr = o.get('shipping_address', '')
                 price = float(o['total_price'] if o.get('total_price') is not None else o.get('estimated_price', 0))
                 
                 try:
-                    pdf_path = generate_uplatnica(order_id, payer_name, addr, price)
+                    if o.get('is_company'):
+                        # Fetch items for Predracun
+                        files = db.execute_query("SELECT filename as name, quantity, unit_price as price FROM order_files WHERE order_id = %s", (order_id,))
+                        pdf_path = generate_predracun(
+                            order_id, 
+                            o.get('company_name'), 
+                            o.get('company_pib'), 
+                            o.get('company_address') or o.get('shipping_address'),
+                            price,
+                            files
+                        )
+                    else:
+                        payer_name = f"{o['first_name']} {o.get('last_name', '')}".strip()
+                        addr = o.get('shipping_address', '')
+                        pdf_path = generate_uplatnica(order_id, payer_name, addr, price)
+                    
                     update_fields.append("invoice_path = %s")
                     params.append(pdf_path)
                 except Exception as e:

+ 12 - 0
backend/schemas.py

@@ -107,6 +107,10 @@ class UserCreate(BaseModel):
     phone: Optional[str] = None
     shipping_address: Optional[str] = None
     preferred_language: Optional[str] = "en"
+    is_company: bool = False
+    company_name: Optional[str] = None
+    company_pib: Optional[str] = None
+    company_address: Optional[str] = None
 
 class UserUpdate(BaseModel):
     first_name: Optional[str] = None
@@ -116,6 +120,10 @@ class UserUpdate(BaseModel):
     preferred_language: Optional[str] = None
     can_chat: Optional[bool] = None
     is_active: Optional[bool] = None
+    is_company: Optional[bool] = None
+    company_name: Optional[str] = None
+    company_pib: Optional[str] = None
+    company_address: Optional[str] = None
 
 class UserLogin(BaseModel):
     email: EmailStr
@@ -132,6 +140,10 @@ class UserResponse(BaseModel):
     role: str
     can_chat: bool
     is_active: bool
+    is_company: bool
+    company_name: Optional[str] = None
+    company_pib: Optional[str] = None
+    company_address: Optional[str] = None
     ip_address: Optional[str] = None
     created_at: datetime
     

+ 7 - 3
backend/services/pricing.py

@@ -1,13 +1,13 @@
 from typing import List
 import db
 
-def calculate_estimated_price(material_id: int, file_sizes: List[int], file_quantities: List[int] = None) -> float:
+def calculate_estimated_price(material_id: int, file_sizes: List[int], file_quantities: List[int] = None, return_details=False) -> float:
     """
     Internal logic to estimate price per file based on file size (proxy for volume).
     """
     material = db.execute_query("SELECT price_per_cm3 FROM materials WHERE id = %s", (material_id,))
     if not material:
-        return 0.0
+        return (0.0, []) if return_details else 0.0
     
     price_per_cm3 = float(material[0]['price_per_cm3'])
     
@@ -15,13 +15,17 @@ def calculate_estimated_price(material_id: int, file_sizes: List[int], file_quan
         file_quantities = [1] * len(file_sizes)
         
     estimated_total = 0.0
+    file_costs = []
     base_fee = 5.0 # Minimum setup fee per file
     
     for size, qty in zip(file_sizes, file_quantities):
         total_size_mb = size / (1024 * 1024)
         # Empirical conversion: ~8cm3 per 1MB of STL (binary)
         estimated_volume = total_size_mb * 8.0 
-        file_cost = base_fee + (estimated_volume * price_per_cm3)
+        file_cost = round(base_fee + (estimated_volume * price_per_cm3), 2)
+        file_costs.append(file_cost)
         estimated_total += file_cost * qty
     
+    if return_details:
+        return round(estimated_total, 2), file_costs
     return round(estimated_total, 2)

+ 80 - 0
backend/services/uplatnica_generator.py

@@ -132,3 +132,83 @@ def generate_uplatnica(order_id, payer_name, payer_address, amount):
     pdf.output(filepath)
 
     return os.path.join("uploads", "invoices", filename).replace("\\", "/")
+
+
+def generate_predracun(order_id, company_name, company_pib, company_address, amount, items=None):
+    """Generates a formal Proforma Invoice (Predračun) for companies"""
+    pdf = FPDF(orientation='P', unit='mm', format='A4')
+    pdf.add_page()
+    
+    # Fonts
+    pdf.set_font("helvetica", "B", 16)
+    pdf.cell(0, 10, "PREDRACUN (Proforma Invoice)", ln=True, align='C')
+    pdf.set_font("helvetica", "", 10)
+    pdf.cell(0, 10, f"Broj narudzbe / Order ID: {order_id}", ln=True, align='C')
+    pdf.ln(5)
+
+    # Supplier info (Left)
+    pdf.set_font("helvetica", "B", 10)
+    pdf.cell(95, 5, "PRODAVAC / SUPPLIER:", ln=False)
+    # Customer info (Right)
+    pdf.cell(95, 5, "KUPAC / CUSTOMER:", ln=True)
+    
+    pdf.set_font("helvetica", "", 10)
+    current_y = pdf.get_y()
+    
+    # Left column (Supplier)
+    pdf.set_xy(10, current_y)
+    pdf.multi_cell(90, 5, f"{config.COMPANY_NAME}\n{config.COMPANY_ADDRESS}\nPIB: {config.COMPANY_PIB}\nZiro racun: {config.ZIRO_RACUN}")
+    
+    # Right column (Customer)
+    pdf.set_xy(105, current_y)
+    pdf.multi_cell(90, 5, f"{company_name}\nAddress: {company_address}\nPIB: {company_pib}")
+    
+    pdf.ln(10)
+    pdf.set_xy(10, pdf.get_y() + 5)
+
+    # Table Header
+    pdf.set_font("helvetica", "B", 10)
+    pdf.set_fill_color(240, 240, 240)
+    pdf.cell(10, 8, "#", border=1, fill=True)
+    pdf.cell(100, 8, "Opis / Description", border=1, fill=True)
+    pdf.cell(20, 8, "Kol / Qty", border=1, fill=True, align='C')
+    pdf.cell(30, 8, "Cjena / Price", border=1, fill=True, align='R')
+    pdf.cell(30, 8, "Iznos / Total", border=1, fill=True, align='R', ln=True)
+
+    # Table Content
+    pdf.set_font("helvetica", "", 10)
+    if not items:
+        # Fallback if no specific file items provided
+        pdf.cell(10, 8, "1", border=1)
+        pdf.cell(100, 8, f"Usluge 3D stampe (Narudzba {order_id})", border=1)
+        pdf.cell(20, 8, "1", border=1, align='C')
+        pdf.cell(30, 8, format_amount(amount), border=1, align='R')
+        pdf.cell(30, 8, format_amount(amount), border=1, align='R', ln=True)
+    else:
+        for i, item in enumerate(items):
+            pdf.cell(10, 8, str(i + 1), border=1)
+            pdf.cell(100, 8, str(item.get('name', '3D Print'))[:50], border=1)
+            pdf.cell(20, 8, str(item.get('quantity', 1)), border=1, align='C')
+            price = float(item.get('price', 0))
+            pdf.cell(30, 8, format_amount(price), border=1, align='R')
+            pdf.cell(30, 8, format_amount(price * item.get('quantity', 1)), border=1, align='R', ln=True)
+
+    # Total
+    pdf.ln(5)
+    pdf.set_font("helvetica", "B", 12)
+    pdf.cell(160, 10, "UKUPNO / TOTAL (EUR):", align='R')
+    pdf.cell(30, 10, format_amount(amount), align='R', ln=True)
+
+    # Footer
+    pdf.ln(20)
+    pdf.set_font("helvetica", "I", 8)
+    pdf.multi_cell(0, 5, "Predracun je validan bez pecata i potpisa. Placanje se vrsi u roku od 3 dana na navedeni ziro racun.\nThis is a proforma invoice and is valid without stamp and signature.")
+
+    # Save
+    pdf_dir = os.path.join(config.UPLOAD_DIR, "invoices")
+    os.makedirs(pdf_dir, exist_ok=True)
+    filename = f"predracun_order_{order_id}.pdf"
+    filepath = os.path.join(pdf_dir, filename)
+    pdf.output(filepath)
+
+    return os.path.join("uploads", "invoices", filename).replace("\\", "/")

+ 61 - 14
src/components/ModelUploadSection.vue

@@ -30,23 +30,55 @@
           </div>
         </div>
 
-        <div class="grid sm:grid-cols-2 gap-6">
-          <div class="space-y-1.5">
-            <label class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
-              {{ t("upload.phone") }} *
-            </label>
-            <input v-model="phone" type="tel" required placeholder="+382..."
-              class="w-full bg-secondary/50 border border-black/[0.03] rounded-2xl px-4 py-2.5 focus:outline-none focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all text-sm font-medium" />
           </div>
-          <div class="space-y-1.5">
-            <label class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
-              {{ t("upload.email") }} *
-            </label>
-            <input v-model="email" type="email" required placeholder="example@mail.com"
-              class="w-full bg-secondary/50 border border-black/[0.03] rounded-2xl px-4 py-2.5 focus:outline-none focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all text-sm font-medium" />
+        </div>
+
+        <!-- Account Type Toggle -->
+        <div class="space-y-4 p-5 bg-secondary/30 rounded-[2rem] border border-black/[0.02]">
+          <h3 class="text-[10px] font-bold uppercase tracking-[0.2em] text-foreground/40 flex items-center gap-2 px-1">
+            {{ t("auth.fields.accountType") }}
+          </h3>
+          <div class="flex p-1 bg-white border border-black/[0.03] rounded-2xl gap-1">
+            <button type="button" @click="isCompany = false"
+              :class="['flex-1 px-4 py-2.5 rounded-xl text-xs font-bold transition-all', !isCompany ? 'bg-primary text-primary-foreground shadow-lg' : 'text-foreground/40 hover:bg-black/5']">
+              {{ t("auth.fields.individual") }}
+            </button>
+            <button type="button" @click="isCompany = true"
+              :class="['flex-1 px-4 py-2.5 rounded-xl text-xs font-bold transition-all', isCompany ? 'bg-primary text-primary-foreground shadow-lg' : 'text-foreground/40 hover:bg-black/5']">
+              {{ t("auth.fields.company") }}
+            </button>
           </div>
         </div>
 
+        <!-- Company Fields -->
+        <Transition enter-active-class="transition duration-300" enter-from-class="opacity-0 -translate-y-4" enter-to-class="opacity-100 translate-y-0">
+          <div v-if="isCompany" class="space-y-6 sm:space-y-8 p-6 bg-secondary/20 rounded-[2.5rem] border border-primary/10">
+            <div class="grid sm:grid-cols-2 gap-6">
+              <div class="space-y-1.5">
+                <label class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
+                  {{ t("auth.fields.companyName") }} *
+                </label>
+                <input v-model="companyName" type="text" :required="isCompany" :placeholder="t('auth.fields.companyName')"
+                  class="w-full bg-white border border-black/[0.03] rounded-2xl px-4 py-2.5 focus:outline-none focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all text-sm font-medium" />
+              </div>
+              <div class="space-y-1.5">
+                <label class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
+                  {{ t("auth.fields.companyPIB") }} *
+                </label>
+                <input v-model="companyPib" type="text" :required="isCompany" placeholder="PIB / Tax ID"
+                  class="w-full bg-white border border-black/[0.03] rounded-2xl px-4 py-2.5 focus:outline-none focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all text-sm font-medium" />
+              </div>
+            </div>
+            <div class="space-y-1.5">
+              <label class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-foreground/40 ml-1">
+                {{ t("auth.fields.companyAddress") }}
+              </label>
+              <textarea v-model="companyAddress" rows="2" :placeholder="t('auth.fields.companyAddress')"
+                class="w-full bg-white border border-black/[0.03] rounded-2xl px-4 py-3 focus:outline-none focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all text-sm font-medium resize-none" />
+            </div>
+          </div>
+        </Transition>
+
         <!-- Material Selection -->
         <div class="space-y-4 p-5 bg-secondary/30 rounded-[2rem] border border-black/[0.02]">
           <h3 class="text-[10px] font-bold uppercase tracking-[0.2em] text-foreground/40 flex items-center gap-2 px-1">
@@ -262,6 +294,10 @@ const firstName = ref("");
 const lastName = ref("");
 const phone = ref("");
 const email = ref("");
+const isCompany = ref(false);
+const companyName = ref("");
+const companyPib = ref("");
+const companyAddress = ref("");
 const allowPortfolio = ref(false);
 const materials = ref<any[]>([]);
 const selectedMaterial = ref("1");
@@ -300,6 +336,10 @@ onMounted(async () => {
         phone.value = user.phone ?? "";
         email.value = user.email ?? "";
         address.value = user.shipping_address ?? "";
+        isCompany.value = user.is_company ?? false;
+        companyName.value = user.company_name ?? "";
+        companyPib.value = user.company_pib ?? "";
+        companyAddress.value = user.company_address ?? "";
       }
     }
   } catch (e) { console.error("Failed to load initial data:", e); }
@@ -329,7 +369,8 @@ const isFormValid = computed(() =>
   firstName.value.trim() !== "" && lastName.value.trim() !== "" &&
   phone.value.trim() !== "" && email.value.trim() !== "" &&
   address.value.trim() !== "" && selectedMaterial.value !== "" &&
-  (files.value.length > 0 || modelLink.value.trim() !== "")
+  (files.value.length > 0 || modelLink.value.trim() !== "") &&
+  (!isCompany.value || (companyName.value.trim() !== "" && companyPib.value.trim() !== ""))
 );
 
 async function addFiles(rawFiles: File[]) {
@@ -393,6 +434,12 @@ async function handleSubmit() {
   fd.append("notes", notes.value);
   fd.append("material_id", selectedMaterial.value);
   if (selectedColor.value) fd.append("color_name", selectedColor.value);
+  fd.append("is_company", String(isCompany.value));
+  if (isCompany.value) {
+    fd.append("company_name", companyName.value);
+    fd.append("company_pib", companyPib.value);
+    fd.append("company_address", companyAddress.value);
+  }
   
   const uploadedFiles = files.value.filter(f => f.dbId);
   fd.append("file_ids", JSON.stringify(uploadedFiles.map(f => f.dbId)));

+ 7 - 1
src/locales/en.json

@@ -189,7 +189,13 @@
       "confirmPassword": "Confirm Password",
       "email": "Email",
       "newPassword": "New Password",
-      "password": "Password"
+      "password": "Password",
+      "accountType": "Account Type",
+      "individual": "Individual",
+      "company": "Company",
+      "companyName": "Company Name",
+      "companyPIB": "Tax ID (PIB)",
+      "companyAddress": "Company HQ Address"
     },
     "forgot": {
       "link": "Forgot Password?",

+ 7 - 1
src/locales/me.json

@@ -189,7 +189,13 @@
       "confirmPassword": "Potvrdi lozinku",
       "email": "Email",
       "newPassword": "Nova lozinka",
-      "password": "Lozinka"
+      "password": "Lozinka",
+      "accountType": "Tip naloga",
+      "individual": "Fizičko lice",
+      "company": "Pravno lice / Firma",
+      "companyName": "Naziv firme",
+      "companyPIB": "PIB",
+      "companyAddress": "Adresa sjedišta"
     },
     "forgot": {
       "link": "Zaboravljena lozinka?",

+ 7 - 1
src/locales/ru.json

@@ -189,7 +189,13 @@
       "confirmPassword": "Подтвердите пароль",
       "email": "Email",
       "newPassword": "Новый пароль",
-      "password": "Пароль"
+      "password": "Пароль",
+      "accountType": "Тип аккаунта",
+      "individual": "Частное лицо",
+      "company": "Компания",
+      "companyName": "Название компании",
+      "companyPIB": "ИНН (PIB)",
+      "companyAddress": "Юридический адрес"
     },
     "forgot": {
       "link": "Забыли пароль?",

+ 36 - 0
src/locales/translations.json

@@ -924,6 +924,42 @@
         "me": "Lozinka",
         "ru": "Пароль",
         "ua": "Пароль"
+      },
+      "accountType": {
+        "en": "Account Type",
+        "me": "Tip naloga",
+        "ru": "Тип аккаунта",
+        "ua": "Тип акаунту"
+      },
+      "individual": {
+        "en": "Individual",
+        "me": "Fizičko lice",
+        "ru": "Частное лицо",
+        "ua": "Приватна особа"
+      },
+      "company": {
+        "en": "Company",
+        "me": "Pravno lice / Firma",
+        "ru": "Компания",
+        "ua": "Компанія"
+      },
+      "companyName": {
+        "en": "Company Name",
+        "me": "Naziv firme",
+        "ru": "Название компании",
+        "ua": "Назва компанії"
+      },
+      "companyPIB": {
+        "en": "Tax ID (PIB)",
+        "me": "PIB",
+        "ru": "ИНН (PIB)",
+        "ua": "ІПН (PIB)"
+      },
+      "companyAddress": {
+        "en": "Company HQ Address",
+        "me": "Adresa sjedišta",
+        "ru": "Юридический адрес",
+        "ua": "Юридична адреса"
       }
     },
     "forgot": {

+ 7 - 1
src/locales/ua.json

@@ -189,7 +189,13 @@
       "confirmPassword": "Підтвердьте пароль",
       "email": "Email",
       "newPassword": "Новий пароль",
-      "password": "Пароль"
+      "password": "Пароль",
+      "accountType": "Тип акаунту",
+      "individual": "Приватна особа",
+      "company": "Компанія",
+      "companyName": "Назва компанії",
+      "companyPIB": "ІПН (PIB)",
+      "companyAddress": "Юридична адреса"
     },
     "forgot": {
       "link": "Забули свій пароль?",

+ 16 - 1
src/pages/Admin.vue

@@ -73,7 +73,13 @@
                 <div class="flex items-center gap-2">
                   <div class="flex flex-col">
                     <h3 class="font-bold leading-none">{{ order.first_name }} {{ order.last_name }}</h3>
-                    <p class="text-[10px] text-muted-foreground mt-1">{{ order.email }}</p>
+                    <div v-if="order.is_company" class="mt-1 flex flex-col gap-0.5">
+                      <span class="text-[9px] font-bold uppercase py-0.5 px-1.5 bg-primary text-primary-foreground rounded-md w-fit">{{ t("auth.fields.company") }}</span>
+                      <p class="text-[10px] font-bold text-primary truncate max-w-[150px]">{{ order.company_name }}</p>
+                      <p class="text-[8px] text-muted-foreground font-mono">PIB: {{ order.company_pib }}</p>
+                    </div>
+                    <p v-else class="text-[10px] text-muted-foreground mt-1">{{ order.email }}</p>
+                    <p v-if="order.is_company" class="text-[10px] text-muted-foreground">{{ order.email }}</p>
                   </div>
                   <span :title="order.is_online ? 'Online' : 'Offline'" :class="['w-2 h-2 rounded-full', order.is_online ? 'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.8)]' : 'bg-muted-foreground/30']"></span>
                   
@@ -329,6 +335,7 @@
                 <tr class="bg-muted/30 border-b border-border/50">
                   <th class="p-4 text-[10px] font-bold uppercase tracking-wider">{{ t("admin.labels.user") }}</th>
                   <th class="p-4 text-[10px] font-bold uppercase tracking-wider">{{ t("admin.labels.contact") }}</th>
+                  <th class="p-4 text-[10px] font-bold uppercase tracking-wider">{{ t("auth.fields.accountType") }}</th>
                   <th class="p-4 text-[10px] font-bold uppercase tracking-wider">{{ t("admin.labels.role") }}</th>
                   <th class="p-4 text-[10px] font-bold uppercase tracking-wider text-center">{{ t("admin.labels.chat") }}</th>
                   <th class="p-4 text-[10px] font-bold uppercase tracking-wider text-center">{{ t("admin.fields.active") }}</th>
@@ -353,6 +360,14 @@
                   <td class="p-4">
                     <span :class="`px-2 py-0.5 rounded-full text-[10px] font-bold uppercase ${u.role === 'admin' ? 'bg-rose-500/10 text-rose-500 border border-rose-500/20' : 'bg-muted text-muted-foreground border border-border/50'}`">{{ u.role }}</span>
                   </td>
+                  <td class="p-4">
+                    <div class="flex flex-col">
+                      <span :class="`px-2 py-0.5 rounded-full text-[9px] font-bold uppercase inline-block w-fit ${u.is_company ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`">
+                        {{ u.is_company ? t("auth.fields.company") : t("auth.fields.individual") }}
+                      </span>
+                      <span v-if="u.is_company" class="text-[10px] font-bold mt-1 text-primary truncate max-w-[120px]">{{ u.company_name }}</span>
+                    </div>
+                  </td>
                   <td class="p-4 text-center">
                     <button @click="handleToggleUserChat(u.id, u.can_chat)" class="inline-flex">
                       <component :is="u.can_chat ? ToggleRight : ToggleLeft" class="w-6 h-6" :class="u.can_chat ? 'text-emerald-500' : 'text-muted-foreground'" />

+ 63 - 2
src/pages/Auth.vue

@@ -57,6 +57,21 @@
 
           <!-- Register extra fields -->
           <div v-if="mode === 'register'" class="space-y-4">
+            <!-- Account Type Selector -->
+            <div class="space-y-2">
+              <label class="text-xs font-semibold uppercase tracking-wider text-muted-foreground ml-1">{{ t("auth.fields.accountType") }}</label>
+              <div class="flex p-1 bg-background/50 border border-border/50 rounded-2xl gap-1">
+                <button type="button" @click="formData.is_company = false"
+                  :class="['flex-1 px-4 py-2 rounded-xl text-[11px] font-bold uppercase transition-all', !formData.is_company ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:bg-white/5']">
+                  {{ t("auth.fields.individual") }}
+                </button>
+                <button type="button" @click="formData.is_company = true"
+                  :class="['flex-1 px-4 py-2 rounded-xl text-[11px] font-bold uppercase transition-all', formData.is_company ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:bg-white/5']">
+                  {{ t("auth.fields.company") }}
+                </button>
+              </div>
+            </div>
+
             <div class="grid grid-cols-2 gap-3">
               <div class="space-y-1.5">
                 <label class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 ml-1">{{ t("upload.firstName") }}</label>
@@ -79,6 +94,27 @@
               <textarea v-model="formData.address" rows="2"
                 class="w-full bg-background/50 border border-border/50 rounded-xl px-4 py-2.5 focus:outline-none focus:ring-1 focus:ring-primary/30 text-sm resize-none" />
             </div>
+
+            <!-- Company specific fields -->
+            <Transition enter-active-class="transition duration-200" enter-from-class="opacity-0 -translate-y-2" enter-to-class="opacity-100 translate-y-0">
+               <div v-if="formData.is_company" class="space-y-4 pt-4 border-t border-border/50">
+                  <div class="space-y-1.5">
+                    <label class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 ml-1">{{ t("auth.fields.companyName") }}</label>
+                    <input v-model="formData.company_name" type="text" :required="formData.is_company"
+                      class="w-full bg-background/50 border border-border/50 rounded-xl px-4 py-2.5 focus:outline-none focus:ring-1 focus:ring-primary/30 text-sm" />
+                  </div>
+                  <div class="space-y-1.5">
+                    <label class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 ml-1">{{ t("auth.fields.companyPIB") }}</label>
+                    <input v-model="formData.company_pib" type="text" :required="formData.is_company"
+                      class="w-full bg-background/50 border border-border/50 rounded-xl px-4 py-2.5 focus:outline-none focus:ring-1 focus:ring-primary/30 text-sm" />
+                  </div>
+                  <div class="space-y-1.5">
+                    <label class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60 ml-1">{{ t("auth.fields.companyAddress") }}</label>
+                    <textarea v-model="formData.company_address" rows="2"
+                      class="w-full bg-background/50 border border-border/50 rounded-xl px-4 py-2.5 focus:outline-none focus:ring-1 focus:ring-primary/30 text-sm resize-none" />
+                  </div>
+               </div>
+            </Transition>
           </div>
 
           <!-- Password -->
@@ -187,7 +223,20 @@ const route = useRoute();
 const authStore = useAuthStore();
 const mode = ref<AuthMode>("login");
 const isLoading = ref(false);
-const formData = reactive({ email: "", password: "", confirmPassword: "", firstName: "", lastName: "", phone: "", address: "", token: "" });
+const formData = reactive({ 
+  email: "", 
+  password: "", 
+  confirmPassword: "", 
+  firstName: "", 
+  lastName: "", 
+  phone: "", 
+  address: "", 
+  token: "",
+  is_company: false,
+  company_name: "",
+  company_pib: "",
+  company_address: ""
+});
 
 onMounted(() => {
   const token = route.query.token as string;
@@ -219,7 +268,19 @@ async function handleSubmit() {
       toast.success(t("auth.toasts.welcomeBack"));
       router.push("/");
     } else if (mode.value === "register") {
-      await registerUser({ email: formData.email, password: formData.password, first_name: formData.firstName, last_name: formData.lastName, phone: formData.phone, shipping_address: formData.address, preferred_language: currentLanguage() });
+      await registerUser({ 
+        email: formData.email, 
+        password: formData.password, 
+        first_name: formData.firstName, 
+        last_name: formData.lastName, 
+        phone: formData.phone, 
+        shipping_address: formData.address, 
+        preferred_language: currentLanguage(),
+        is_company: formData.is_company,
+        company_name: formData.is_company ? formData.company_name : null,
+        company_pib: formData.is_company ? formData.company_pib : null,
+        company_address: formData.is_company ? formData.company_address : null
+      });
       toast.success(t("auth.toasts.accountCreated"));
       mode.value = "login";
     } else if (mode.value === "forgot") {