Parcourir la source

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

unknown il y a 6 jours
Parent
commit
a9a4f7c275

+ 3 - 1
backend/config.py

@@ -30,5 +30,7 @@ for d in [UPLOAD_DIR, PREVIEW_DIR]:
         os.makedirs(d)
         os.makedirs(d)
 
 
 # Payment Config
 # Payment Config
-ZIRO_RACUN = "510-00000000000-00"
+ZIRO_RACUN = "510-1234567890123-45"
 COMPANY_NAME = "RADIONICA 3D"
 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)
     hashed_password = auth_utils.get_password_hash(user.password)
     
     
     query = """
     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)
     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]
     return new_user[0]
 
 
 @router.post("/login", response_model=schemas.Token)
 @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)):
 async def get_me(token: str = Depends(auth_utils.oauth2_scheme)):
     payload = auth_utils.decode_token(token)
     payload = auth_utils.decode_token(token)
     if not payload: raise HTTPException(status_code=401, detail="Invalid 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")
     if not user: raise HTTPException(status_code=404, detail="User not found")
     return user[0]
     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"
         query = f"UPDATE users SET {', '.join(update_fields)} WHERE id = %s"
         params.append(user_id)
         params.append(user_id)
         db.execute_commit(query, tuple(params))
         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]
     return user[0]
 
 
 @router.get("/admin/users")
 @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")
         raise HTTPException(status_code=403, detail="Admin role required")
     
     
     offset = (page - 1) * size
     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"
     count_query = "SELECT COUNT(*) as total FROM users"
     params = []
     params = []
     if search and search.strip():
     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)
         (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]
     return user[0]
 
 
 @router.patch("/users/{target_id}/admin", response_model=schemas.UserResponse)
 @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("[]"),
     file_quantities: str = Form("[]"),
     quantity: int = Form(1),
     quantity: int = Form(1),
     color_name: Optional[str] = Form(None),
     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)
     token: str = Depends(auth_utils.oauth2_scheme_optional)
 ):
 ):
     user_id = None
     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_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]
         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
     # Snapshoting initial parameters
     original_params = json.dumps({
     original_params = json.dumps({
@@ -81,21 +85,26 @@ async def create_order(
         "phone": phone,
         "phone": phone,
         "email": email,
         "email": email,
         "shipping_address": shipping_address,
         "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 = """
     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:
     try:
         order_insert_id = db.execute_commit(order_query, order_params)
         order_insert_id = db.execute_commit(order_query, order_params)
         if parsed_ids:
         if parsed_ids:
             for idx, f_id in enumerate(parsed_ids):
             for idx, f_id in enumerate(parsed_ids):
                 qty = parsed_quantities[idx] if idx < len(parsed_quantities) else 1
                 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(order_processing.process_order_slicing, order_insert_id)
         background_tasks.add_task(event_hooks.on_order_created, 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"}
         return {"status": "success", "order_id": order_insert_id, "message": "Order submitted successfully"}
@@ -199,16 +208,29 @@ async def update_order_admin(
                 data.send_notification
                 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'):
             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]
                 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))
                 price = float(o['total_price'] if o.get('total_price') is not None else o.get('estimated_price', 0))
                 
                 
                 try:
                 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")
                     update_fields.append("invoice_path = %s")
                     params.append(pdf_path)
                     params.append(pdf_path)
                 except Exception as e:
                 except Exception as e:

+ 12 - 0
backend/schemas.py

@@ -107,6 +107,10 @@ class UserCreate(BaseModel):
     phone: Optional[str] = None
     phone: Optional[str] = None
     shipping_address: Optional[str] = None
     shipping_address: Optional[str] = None
     preferred_language: Optional[str] = "en"
     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):
 class UserUpdate(BaseModel):
     first_name: Optional[str] = None
     first_name: Optional[str] = None
@@ -116,6 +120,10 @@ class UserUpdate(BaseModel):
     preferred_language: Optional[str] = None
     preferred_language: Optional[str] = None
     can_chat: Optional[bool] = None
     can_chat: Optional[bool] = None
     is_active: 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):
 class UserLogin(BaseModel):
     email: EmailStr
     email: EmailStr
@@ -132,6 +140,10 @@ class UserResponse(BaseModel):
     role: str
     role: str
     can_chat: bool
     can_chat: bool
     is_active: 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
     ip_address: Optional[str] = None
     created_at: datetime
     created_at: datetime
     
     

+ 7 - 3
backend/services/pricing.py

@@ -1,13 +1,13 @@
 from typing import List
 from typing import List
 import db
 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).
     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,))
     material = db.execute_query("SELECT price_per_cm3 FROM materials WHERE id = %s", (material_id,))
     if not material:
     if not material:
-        return 0.0
+        return (0.0, []) if return_details else 0.0
     
     
     price_per_cm3 = float(material[0]['price_per_cm3'])
     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)
         file_quantities = [1] * len(file_sizes)
         
         
     estimated_total = 0.0
     estimated_total = 0.0
+    file_costs = []
     base_fee = 5.0 # Minimum setup fee per file
     base_fee = 5.0 # Minimum setup fee per file
     
     
     for size, qty in zip(file_sizes, file_quantities):
     for size, qty in zip(file_sizes, file_quantities):
         total_size_mb = size / (1024 * 1024)
         total_size_mb = size / (1024 * 1024)
         # Empirical conversion: ~8cm3 per 1MB of STL (binary)
         # Empirical conversion: ~8cm3 per 1MB of STL (binary)
         estimated_volume = total_size_mb * 8.0 
         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
         estimated_total += file_cost * qty
     
     
+    if return_details:
+        return round(estimated_total, 2), file_costs
     return round(estimated_total, 2)
     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)
     pdf.output(filepath)
 
 
     return os.path.join("uploads", "invoices", filename).replace("\\", "/")
     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>
         </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>
-          <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>
         </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 -->
         <!-- Material Selection -->
         <div class="space-y-4 p-5 bg-secondary/30 rounded-[2rem] border border-black/[0.02]">
         <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">
           <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 lastName = ref("");
 const phone = ref("");
 const phone = ref("");
 const email = ref("");
 const email = ref("");
+const isCompany = ref(false);
+const companyName = ref("");
+const companyPib = ref("");
+const companyAddress = ref("");
 const allowPortfolio = ref(false);
 const allowPortfolio = ref(false);
 const materials = ref<any[]>([]);
 const materials = ref<any[]>([]);
 const selectedMaterial = ref("1");
 const selectedMaterial = ref("1");
@@ -300,6 +336,10 @@ onMounted(async () => {
         phone.value = user.phone ?? "";
         phone.value = user.phone ?? "";
         email.value = user.email ?? "";
         email.value = user.email ?? "";
         address.value = user.shipping_address ?? "";
         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); }
   } catch (e) { console.error("Failed to load initial data:", e); }
@@ -329,7 +369,8 @@ const isFormValid = computed(() =>
   firstName.value.trim() !== "" && lastName.value.trim() !== "" &&
   firstName.value.trim() !== "" && lastName.value.trim() !== "" &&
   phone.value.trim() !== "" && email.value.trim() !== "" &&
   phone.value.trim() !== "" && email.value.trim() !== "" &&
   address.value.trim() !== "" && selectedMaterial.value !== "" &&
   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[]) {
 async function addFiles(rawFiles: File[]) {
@@ -393,6 +434,12 @@ async function handleSubmit() {
   fd.append("notes", notes.value);
   fd.append("notes", notes.value);
   fd.append("material_id", selectedMaterial.value);
   fd.append("material_id", selectedMaterial.value);
   if (selectedColor.value) fd.append("color_name", selectedColor.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);
   const uploadedFiles = files.value.filter(f => f.dbId);
   fd.append("file_ids", JSON.stringify(uploadedFiles.map(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",
       "confirmPassword": "Confirm Password",
       "email": "Email",
       "email": "Email",
       "newPassword": "New Password",
       "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": {
     "forgot": {
       "link": "Forgot Password?",
       "link": "Forgot Password?",

+ 7 - 1
src/locales/me.json

@@ -189,7 +189,13 @@
       "confirmPassword": "Potvrdi lozinku",
       "confirmPassword": "Potvrdi lozinku",
       "email": "Email",
       "email": "Email",
       "newPassword": "Nova lozinka",
       "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": {
     "forgot": {
       "link": "Zaboravljena lozinka?",
       "link": "Zaboravljena lozinka?",

+ 7 - 1
src/locales/ru.json

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

+ 36 - 0
src/locales/translations.json

@@ -924,6 +924,42 @@
         "me": "Lozinka",
         "me": "Lozinka",
         "ru": "Пароль",
         "ru": "Пароль",
         "ua": "Пароль"
         "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": {
     "forgot": {

+ 7 - 1
src/locales/ua.json

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

+ 16 - 1
src/pages/Admin.vue

@@ -73,7 +73,13 @@
                 <div class="flex items-center gap-2">
                 <div class="flex items-center gap-2">
                   <div class="flex flex-col">
                   <div class="flex flex-col">
                     <h3 class="font-bold leading-none">{{ order.first_name }} {{ order.last_name }}</h3>
                     <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>
                   </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>
                   <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">
                 <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.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("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">{{ 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.labels.chat") }}</th>
                   <th class="p-4 text-[10px] font-bold uppercase tracking-wider text-center">{{ t("admin.fields.active") }}</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">
                   <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>
                     <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>
+                  <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">
                   <td class="p-4 text-center">
                     <button @click="handleToggleUserChat(u.id, u.can_chat)" class="inline-flex">
                     <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'" />
                       <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 -->
           <!-- Register extra fields -->
           <div v-if="mode === 'register'" class="space-y-4">
           <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="grid grid-cols-2 gap-3">
               <div class="space-y-1.5">
               <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>
                 <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"
               <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" />
                 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>
+
+            <!-- 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>
           </div>
 
 
           <!-- Password -->
           <!-- Password -->
@@ -187,7 +223,20 @@ const route = useRoute();
 const authStore = useAuthStore();
 const authStore = useAuthStore();
 const mode = ref<AuthMode>("login");
 const mode = ref<AuthMode>("login");
 const isLoading = ref(false);
 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(() => {
 onMounted(() => {
   const token = route.query.token as string;
   const token = route.query.token as string;
@@ -219,7 +268,19 @@ async function handleSubmit() {
       toast.success(t("auth.toasts.welcomeBack"));
       toast.success(t("auth.toasts.welcomeBack"));
       router.push("/");
       router.push("/");
     } else if (mode.value === "register") {
     } 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"));
       toast.success(t("auth.toasts.accountCreated"));
       mode.value = "login";
       mode.value = "login";
     } else if (mode.value === "forgot") {
     } else if (mode.value === "forgot") {