feat: add company detail page and transfer ownership UI

- Add company-detail.html with status cards, info sections, vendors list
- Add company-edit.html with transfer ownership modal
- Add company-detail.js and company-edit.js
- Add user search autocomplete for transfer ownership
- Add inline validation errors for transfer form
- Add View button to companies list page
- Add route for /admin/companies/{id} detail page

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-02 19:39:40 +01:00
parent 66c967a04e
commit 4ef3e6eba3
7 changed files with 1385 additions and 9 deletions

View File

@@ -12,6 +12,9 @@ Routes:
- GET / → Redirect to /admin/login
- GET /login → Admin login page (no auth)
- GET /dashboard → Admin dashboard (auth required)
- GET /companies → Company list page (auth required)
- GET /companies/create → Create company form (auth required)
- GET /companies/{company_id}/edit → Edit company form (auth required)
- GET /vendors → Vendor list page (auth required)
- GET /vendors/create → Create vendor form (auth required)
- GET /vendors/{vendor_code} → Vendor details (auth required)
@@ -152,6 +155,50 @@ async def admin_company_create_page(
)
@router.get(
"/companies/{company_id}", response_class=HTMLResponse, include_in_schema=False
)
async def admin_company_detail_page(
request: Request,
company_id: int = Path(..., description="Company ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db),
):
"""
Render company detail view.
"""
return templates.TemplateResponse(
"admin/company-detail.html",
{
"request": request,
"user": current_user,
"company_id": company_id,
},
)
@router.get(
"/companies/{company_id}/edit", response_class=HTMLResponse, include_in_schema=False
)
async def admin_company_edit_page(
request: Request,
company_id: int = Path(..., description="Company ID"),
current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db),
):
"""
Render company edit form.
"""
return templates.TemplateResponse(
"admin/company-edit.html",
{
"request": request,
"user": current_user,
"company_id": company_id,
},
)
# ============================================================================
# VENDOR MANAGEMENT ROUTES
# ============================================================================

View File

@@ -178,6 +178,15 @@
<!-- Actions -->
<td class="px-4 py-3">
<div class="flex items-center space-x-2 text-sm">
<!-- View Button -->
<a
:href="'/admin/companies/' + company.id"
class="flex items-center justify-center p-2 text-blue-600 rounded-lg hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="View company"
>
<span x-html="$icon('eye', 'w-5 h-5')"></span>
</a>
<!-- Edit Button -->
<button
@click="editCompany(company.id)"

View File

@@ -33,15 +33,17 @@
<span x-html="$icon('check-circle', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
<div class="flex-1">
<p class="font-semibold">Company Created Successfully!</p>
<div x-show="ownerCredentials" class="mt-2 p-3 bg-white rounded border border-green-300">
<p class="text-sm font-semibold mb-2">Owner Login Credentials (Save these!):</p>
<div class="space-y-1 text-sm font-mono">
<div><span class="font-bold">Email:</span> <span x-text="ownerCredentials.email"></span></div>
<div><span class="font-bold">Password:</span> <span x-text="ownerCredentials.password" class="bg-yellow-100 px-2 py-1 rounded"></span></div>
<div><span class="font-bold">Login URL:</span> <span x-text="ownerCredentials.login_url"></span></div>
<template x-if="ownerCredentials">
<div class="mt-2 p-3 bg-white rounded border border-green-300">
<p class="text-sm font-semibold mb-2">Owner Login Credentials (Save these!):</p>
<div class="space-y-1 text-sm font-mono">
<div><span class="font-bold">Email:</span> <span x-text="ownerCredentials.email"></span></div>
<div><span class="font-bold">Password:</span> <span x-text="ownerCredentials.password" class="bg-yellow-100 px-2 py-1 rounded"></span></div>
<div><span class="font-bold">Login URL:</span> <span x-text="ownerCredentials.login_url"></span></div>
</div>
<p class="mt-2 text-xs text-red-600">⚠️ The password will only be shown once. Please save it now!</p>
</div>
<p class="mt-2 text-xs text-red-600">⚠️ The password will only be shown once. Please save it now!</p>
</div>
</template>
</div>
</div>
</div>
@@ -259,7 +261,7 @@ function adminCompanyCreate() {
try {
console.log('Creating company:', this.formData);
const response = await apiClient.post('/api/v1/admin/companies', this.formData);
const response = await apiClient.post('/admin/companies', this.formData);
console.log('Company created successfully:', response);

View File

@@ -0,0 +1,291 @@
{# app/templates/admin/company-detail.html #}
{% extends "admin/base.html" %}
{% block title %}Company Details{% endblock %}
{% block alpine_data %}adminCompanyDetail(){% endblock %}
{% block content %}
<!-- Page Header -->
<div class="flex items-center justify-between my-6">
<div>
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="company?.name || 'Company Details'">
Company Details
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1" x-show="company">
ID: <span x-text="companyId"></span>
<span class="text-gray-400 mx-2">|</span>
<span x-text="company?.vendor_count || 0"></span> vendor(s)
</p>
</div>
<a href="/admin/companies"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-400 focus:outline-none">
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
Back
</a>
</div>
<!-- Loading State -->
<div x-show="loading" class="text-center py-12">
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading company details...</p>
</div>
<!-- Error State -->
<div x-show="error && !loading" class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg flex items-start">
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
<div>
<p class="font-semibold">Error loading company</p>
<p class="text-sm" x-text="error"></p>
</div>
</div>
<!-- Company Details -->
<div x-show="!loading && company">
<!-- Quick Actions Card -->
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Quick Actions
</h3>
<div class="flex flex-wrap items-center gap-3">
<a
:href="`/admin/companies/${companyId}/edit`"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
<span x-html="$icon('edit', 'w-4 h-4 mr-2')"></span>
Edit Company
</a>
<button
@click="deleteCompany()"
:disabled="company?.vendor_count > 0"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-red-600 border border-transparent rounded-lg hover:bg-red-700 focus:outline-none focus:shadow-outline-red disabled:opacity-50 disabled:cursor-not-allowed"
:title="company?.vendor_count > 0 ? 'Cannot delete company with vendors' : 'Delete company'">
<span x-html="$icon('delete', 'w-4 h-4 mr-2')"></span>
Delete Company
</button>
</div>
</div>
<!-- Status Cards -->
<div class="grid gap-6 mb-8 md:grid-cols-4">
<!-- Verification Status -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 rounded-full"
:class="company?.is_verified ? 'text-green-500 bg-green-100 dark:text-green-100 dark:bg-green-500' : 'text-orange-500 bg-orange-100 dark:text-orange-100 dark:bg-orange-500'">
<span x-html="$icon(company?.is_verified ? 'badge-check' : 'clock', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Verification
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="company?.is_verified ? 'Verified' : 'Pending'">
-
</p>
</div>
</div>
<!-- Active Status -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 rounded-full"
:class="company?.is_active ? 'text-green-500 bg-green-100 dark:text-green-100 dark:bg-green-500' : 'text-red-500 bg-red-100 dark:text-red-100 dark:bg-red-500'">
<span x-html="$icon(company?.is_active ? 'check-circle' : 'x-circle', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Status
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="company?.is_active ? 'Active' : 'Inactive'">
-
</p>
</div>
</div>
<!-- Vendor Count -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
<span x-html="$icon('shopping-bag', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Vendors
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="company?.vendor_count || 0">
0
</p>
</div>
</div>
<!-- Created Date -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
<span x-html="$icon('calendar', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Created
</p>
<p class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="formatDate(company?.created_at)">
-
</p>
</div>
</div>
</div>
<!-- Main Info Cards -->
<div class="grid gap-6 mb-8 md:grid-cols-2">
<!-- Basic Information -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Basic Information
</h3>
<div class="space-y-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Company Name</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.name || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Description</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.description || 'No description provided'">-</p>
</div>
</div>
</div>
<!-- Contact Information -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Contact Information
</h3>
<div class="space-y-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Contact Email</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.contact_email || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Phone</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.contact_phone || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Website</p>
<a
x-show="company?.website"
:href="company?.website"
target="_blank"
class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400"
x-text="company?.website">
</a>
<span x-show="!company?.website" class="text-sm text-gray-700 dark:text-gray-300">-</span>
</div>
</div>
</div>
</div>
<!-- Business Details -->
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Business Details
</h3>
<div class="grid gap-6 md:grid-cols-2">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Business Address</p>
<p class="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line" x-text="company?.business_address || 'No address provided'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Tax Number</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.tax_number || 'Not provided'">-</p>
</div>
</div>
</div>
<!-- Owner Information -->
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Owner Information
</h3>
<div class="grid gap-6 md:grid-cols-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Owner User ID</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.owner_user_id || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Owner Username</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.owner_username || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Owner Email</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.owner_email || '-'">-</p>
</div>
</div>
</div>
<!-- Vendors Section -->
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800" x-show="company?.vendors && company?.vendors.length > 0">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
<span x-html="$icon('shopping-bag', 'inline w-5 h-5 mr-2')"></span>
Vendors (<span x-text="company?.vendors?.length || 0"></span>)
</h3>
<div class="overflow-x-auto">
<table class="w-full whitespace-no-wrap">
<thead>
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-700">
<th class="px-4 py-3">Vendor</th>
<th class="px-4 py-3">Subdomain</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
<template x-for="vendor in company?.vendors || []" :key="vendor.id">
<tr class="text-gray-700 dark:text-gray-400">
<td class="px-4 py-3">
<div class="flex items-center text-sm">
<div>
<p class="font-semibold" x-text="vendor.name"></p>
<p class="text-xs text-gray-600 dark:text-gray-400" x-text="vendor.vendor_code"></p>
</div>
</div>
</td>
<td class="px-4 py-3 text-sm" x-text="vendor.subdomain"></td>
<td class="px-4 py-3 text-xs">
<span class="px-2 py-1 font-semibold leading-tight rounded-full"
:class="vendor.is_active ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100'"
x-text="vendor.is_active ? 'Active' : 'Inactive'">
</span>
</td>
<td class="px-4 py-3">
<a :href="'/admin/vendors/' + vendor.vendor_code"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 text-sm">
View
</a>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- More Actions -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
More Actions
</h3>
<div class="flex flex-wrap gap-3">
<!-- Create Vendor Button -->
<a
href="/admin/vendors/create"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition-colors duration-150 bg-green-600 border border-transparent rounded-lg hover:bg-green-700 focus:outline-none focus:shadow-outline-green"
>
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
Create Vendor
</a>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
<span x-html="$icon('information-circle', 'w-4 h-4 inline mr-1')"></span>
Vendors created will be associated with this company.
</p>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='admin/js/company-detail.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,496 @@
{# app/templates/admin/company-edit.html #}
{% extends "admin/base.html" %}
{% block title %}Edit Company{% endblock %}
{% block alpine_data %}adminCompanyEdit(){% endblock %}
{% block content %}
<!-- Page Header -->
<div class="flex items-center justify-between my-6">
<div>
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
Edit Company
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1" x-show="company">
<span x-text="company?.name"></span>
</p>
</div>
<a href="/admin/companies"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-400 focus:outline-none">
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
Back to Companies
</a>
</div>
<!-- Loading State -->
<div x-show="loadingCompany" class="text-center py-12">
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading company...</p>
</div>
<!-- Edit Form -->
<div x-show="!loadingCompany && company">
<!-- Quick Actions Card -->
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Quick Actions
</h3>
<div class="flex flex-wrap items-center gap-3">
<button
@click="toggleVerification()"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
:class="{ 'bg-orange-600 hover:bg-orange-700': company && company.is_verified, 'bg-green-600 hover:bg-green-700': company && !company.is_verified }">
<span x-html="$icon(company?.is_verified ? 'x-circle' : 'badge-check', 'w-4 h-4 mr-2')"></span>
<span x-text="company?.is_verified ? 'Unverify Company' : 'Verify Company'"></span>
</button>
<button
@click="toggleActive()"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
:class="{ 'bg-red-600 hover:bg-red-700': company && company.is_active, 'bg-green-600 hover:bg-green-700': company && !company.is_active }">
<span x-html="$icon(company?.is_active ? 'lock-closed' : 'lock-open', 'w-4 h-4 mr-2')"></span>
<span x-text="company?.is_active ? 'Deactivate' : 'Activate'"></span>
</button>
<!-- Status Badges -->
<div class="ml-auto flex items-center gap-2">
<span
x-show="company?.is_verified"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100">
<span x-html="$icon('badge-check', 'w-3 h-3 mr-1')"></span>
Verified
</span>
<span
x-show="!company?.is_verified"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-orange-700 bg-orange-100 rounded-full dark:bg-orange-700 dark:text-orange-100">
<span x-html="$icon('clock', 'w-3 h-3 mr-1')"></span>
Pending
</span>
<span
x-show="company?.is_active"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100">
Active
</span>
<span
x-show="!company?.is_active"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-red-700 bg-red-100 rounded-full dark:bg-red-700 dark:text-red-100">
Inactive
</span>
</div>
</div>
</div>
<!-- Form Card -->
<form @submit.prevent="handleSubmit" class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="grid gap-6 mb-8 md:grid-cols-2">
<!-- Left Column: Basic Info -->
<div>
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Basic Information
</h3>
<!-- Company ID (readonly) -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Company ID
</span>
<input
type="text"
:value="company?.id"
disabled
class="block w-full mt-1 text-sm bg-gray-100 border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 cursor-not-allowed"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
System-generated identifier
</span>
</label>
<!-- Name -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Company Name <span class="text-red-600">*</span>
</span>
<input
type="text"
x-model="formData.name"
required
maxlength="255"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': errors.name }"
>
<span x-show="errors.name" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.name"></span>
</label>
<!-- Description -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Description
</span>
<textarea
x-model="formData.description"
rows="3"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
></textarea>
</label>
</div>
<!-- Right Column: Contact Info -->
<div>
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Contact Information
</h3>
<!-- Owner Info (readonly) -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Owner
</span>
<input
type="text"
:value="company?.owner_username ? company.owner_username + ' (' + company.owner_email + ')' : 'User ID: ' + company?.owner_user_id"
disabled
class="block w-full mt-1 text-sm bg-gray-100 border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 cursor-not-allowed"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Use "Transfer Ownership" in More Actions to change
</span>
</label>
<!-- Contact Email -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Contact Email <span class="text-red-600">*</span>
</span>
<input
type="email"
x-model="formData.contact_email"
required
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': errors.contact_email }"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Public business contact email
</span>
<span x-show="errors.contact_email" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.contact_email"></span>
</label>
<!-- Phone -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Phone
</span>
<input
type="tel"
x-model="formData.contact_phone"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
</label>
<!-- Website -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Website
</span>
<input
type="url"
x-model="formData.website"
:disabled="saving"
placeholder="https://example.com"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
</label>
</div>
</div>
<!-- Business Details -->
<div class="mb-8">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Business Details
</h3>
<div class="grid gap-6 md:grid-cols-2">
<!-- Business Address -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">
Business Address
</span>
<textarea
x-model="formData.business_address"
rows="3"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
></textarea>
</label>
<!-- Tax Number -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">
Tax Number
</span>
<input
type="text"
x-model="formData.tax_number"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
</label>
</div>
</div>
<!-- Company Statistics (readonly) -->
<template x-if="company?.vendor_count !== undefined">
<div class="mb-8">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Company Statistics
</h3>
<div class="grid gap-4 md:grid-cols-2">
<div class="p-4 bg-gray-50 rounded-lg dark:bg-gray-700">
<p class="text-sm text-gray-600 dark:text-gray-400">Total Vendors</p>
<p class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="company.vendor_count || 0"></p>
</div>
<div class="p-4 bg-gray-50 rounded-lg dark:bg-gray-700">
<p class="text-sm text-gray-600 dark:text-gray-400">Active Vendors</p>
<p class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="company.active_vendor_count || 0"></p>
</div>
</div>
</div>
</template>
<!-- Save Button -->
<div class="flex items-center justify-end gap-3 pt-6 border-t dark:border-gray-700">
<a
href="/admin/companies"
class="px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-400 focus:outline-none">
Cancel
</a>
<button
type="submit"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!saving">
<span x-html="$icon('check', 'w-4 h-4 mr-2 inline')"></span>
Save Changes
</span>
<span x-show="saving" class="flex items-center">
<span x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
Saving...
</span>
</button>
</div>
</form>
<!-- More Actions Section -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
More Actions
</h3>
<div class="flex flex-wrap gap-3">
<!-- Transfer Ownership Button -->
<button
@click="showTransferOwnershipModal = true"
:disabled="saving"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition-colors duration-150 bg-orange-600 border border-transparent rounded-lg hover:bg-orange-700 focus:outline-none focus:shadow-outline-orange disabled:opacity-50"
>
<span x-html="$icon('switch-horizontal', 'w-4 h-4 mr-2')"></span>
Transfer Ownership
</button>
<!-- Delete Company Button -->
<button
@click="deleteCompany()"
:disabled="saving || (company?.vendor_count > 0)"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition-colors duration-150 bg-red-600 border border-transparent rounded-lg hover:bg-red-700 focus:outline-none focus:shadow-outline-red disabled:opacity-50"
:title="company?.vendor_count > 0 ? 'Cannot delete company with vendors' : 'Delete this company'"
>
<span x-html="$icon('delete', 'w-4 h-4 mr-2')"></span>
Delete Company
</button>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
<span x-html="$icon('information-circle', 'w-4 h-4 inline mr-1')"></span>
Ownership transfer affects all vendors under this company.
<span x-show="company?.vendor_count > 0" class="text-orange-600 dark:text-orange-400">
Company cannot be deleted while it has vendors (<span x-text="company?.vendor_count"></span> vendors).
</span>
</p>
</div>
<!-- Transfer Ownership Modal -->
<div
x-show="showTransferOwnershipModal"
x-cloak
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50"
@keydown.escape.window="showTransferOwnershipModal = false"
>
<div
@click.away="showTransferOwnershipModal = false"
class="w-full max-w-md bg-white rounded-lg shadow-lg dark:bg-gray-800"
>
<!-- Modal Header -->
<div class="flex items-center justify-between p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
Transfer Company Ownership
</h3>
<button
@click="showTransferOwnershipModal = false"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<span x-html="$icon('x', 'w-5 h-5')"></span>
</button>
</div>
<!-- Modal Body -->
<div class="p-4">
<div class="mb-4 p-3 bg-orange-100 border border-orange-300 text-orange-700 rounded-lg dark:bg-orange-900 dark:border-orange-700 dark:text-orange-300">
<p class="flex items-start text-sm">
<span x-html="$icon('exclamation', 'w-5 h-5 mr-2 flex-shrink-0')"></span>
<span>
<strong>Warning:</strong> This will transfer ownership of the company
"<span x-text="company?.name"></span>" and all its vendors to another user.
</span>
</p>
</div>
<form @submit.prevent="transferOwnership()">
<!-- New Owner Search -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
New Owner <span class="text-red-600">*</span>
</span>
<div class="relative">
<input
type="text"
x-model="userSearchQuery"
@input="searchUsers()"
@focus="showUserDropdown = true"
:disabled="transferring"
placeholder="Search by name or email..."
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
<!-- Search Results Dropdown -->
<div
x-show="showUserDropdown && userSearchResults.length > 0"
x-cloak
@click.away="showUserDropdown = false"
class="absolute z-10 w-full mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-48 overflow-y-auto"
>
<template x-for="user in userSearchResults" :key="user.id">
<button
type="button"
@click="selectUser(user)"
class="w-full px-4 py-2 text-left text-sm hover:bg-purple-50 dark:hover:bg-gray-600 focus:outline-none"
:class="{ 'bg-purple-50 dark:bg-gray-600': transferData.new_owner_user_id === user.id }"
>
<div class="font-medium text-gray-700 dark:text-gray-200" x-text="user.username"></div>
<div class="text-xs text-gray-500 dark:text-gray-400" x-text="user.email"></div>
</button>
</template>
</div>
<!-- No Results -->
<div
x-show="showUserDropdown && userSearchQuery.length >= 2 && userSearchResults.length === 0 && !searchingUsers"
class="absolute z-10 w-full mt-1 px-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg text-sm text-gray-500 dark:text-gray-400"
>
No users found
</div>
<!-- Loading -->
<div
x-show="searchingUsers"
class="absolute z-10 w-full mt-1 px-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg text-sm text-gray-500 dark:text-gray-400"
>
<span x-html="$icon('spinner', 'w-4 h-4 inline mr-2')"></span>
Searching...
</div>
</div>
<!-- Selected User Display -->
<div x-show="selectedUser" class="mt-2 p-2 bg-purple-50 dark:bg-purple-900 rounded-lg text-sm">
<span class="text-purple-700 dark:text-purple-300">
Selected: <strong x-text="selectedUser?.username"></strong> (<span x-text="selectedUser?.email"></span>)
</span>
<button type="button" @click="clearSelectedUser()" class="ml-2 text-purple-600 hover:text-purple-800 dark:text-purple-400">
<span x-html="$icon('x', 'w-4 h-4 inline')"></span>
</button>
</div>
<span class="text-xs text-gray-500 dark:text-gray-400 mt-1">
Current owner: <span x-text="company?.owner_username || 'User ID ' + company?.owner_user_id"></span>
</span>
<p x-show="showOwnerError && !transferData.new_owner_user_id" class="mt-1 text-xs text-red-600 dark:text-red-400">
<span x-html="$icon('exclamation', 'w-4 h-4 inline mr-1')"></span>
Please select a new owner
</p>
</label>
<!-- Transfer Reason -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Reason (optional)
</span>
<textarea
x-model="transferData.transfer_reason"
rows="2"
:disabled="transferring"
placeholder="Enter reason for transfer (for audit log)"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
></textarea>
</label>
<!-- Confirmation Checkbox -->
<label class="flex items-center text-sm">
<input
type="checkbox"
x-model="transferData.confirm_transfer"
:disabled="transferring"
class="form-checkbox text-purple-600"
>
<span class="ml-2 text-gray-700 dark:text-gray-400">
I confirm I want to transfer ownership
</span>
</label>
<p x-show="showConfirmError && !transferData.confirm_transfer" class="mt-1 mb-4 text-xs text-red-600 dark:text-red-400">
<span x-html="$icon('exclamation', 'w-4 h-4 inline mr-1')"></span>
Please confirm the transfer by checking the box above
</p>
<div x-show="!showConfirmError || transferData.confirm_transfer" class="mb-4"></div>
<!-- Modal Actions -->
<div class="flex justify-end gap-3">
<button
type="button"
@click="showTransferOwnershipModal = false"
:disabled="transferring"
class="px-4 py-2 text-sm font-medium text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-400 focus:outline-none disabled:opacity-50"
>
Cancel
</button>
<button
type="submit"
:disabled="transferring"
class="flex items-center px-4 py-2 text-sm font-medium text-white transition-colors duration-150 bg-orange-600 border border-transparent rounded-lg hover:bg-orange-700 focus:outline-none focus:shadow-outline-orange disabled:opacity-50 disabled:cursor-not-allowed"
>
<span x-show="!transferring">
<span x-html="$icon('switch-horizontal', 'w-4 h-4 mr-2 inline')"></span>
Transfer Ownership
</span>
<span x-show="transferring" class="flex items-center">
<span x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
Transferring...
</span>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='admin/js/company-edit.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,145 @@
// static/admin/js/company-detail.js
// Create custom logger for company detail
const companyDetailLog = window.LogConfig.createLogger('COMPANY-DETAIL');
function adminCompanyDetail() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Company detail page specific state
currentPage: 'company-detail',
company: null,
loading: false,
error: null,
companyId: null,
// Initialize
async init() {
companyDetailLog.info('=== COMPANY DETAIL PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._companyDetailInitialized) {
companyDetailLog.warn('Company detail page already initialized, skipping...');
return;
}
window._companyDetailInitialized = true;
// Get company ID from URL
const path = window.location.pathname;
const match = path.match(/\/admin\/companies\/(\d+)$/);
if (match) {
this.companyId = match[1];
companyDetailLog.info('Viewing company:', this.companyId);
await this.loadCompany();
} else {
companyDetailLog.error('No company ID in URL');
this.error = 'Invalid company URL';
Utils.showToast('Invalid company URL', 'error');
}
companyDetailLog.info('=== COMPANY DETAIL PAGE INITIALIZATION COMPLETE ===');
},
// Load company data
async loadCompany() {
companyDetailLog.info('Loading company details...');
this.loading = true;
this.error = null;
try {
const url = `/admin/companies/${this.companyId}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load Company Details', duration);
this.company = response;
companyDetailLog.info(`Company loaded in ${duration}ms`, {
id: this.company.id,
name: this.company.name,
is_verified: this.company.is_verified,
is_active: this.company.is_active,
vendor_count: this.company.vendor_count
});
companyDetailLog.debug('Full company data:', this.company);
} catch (error) {
window.LogConfig.logError(error, 'Load Company Details');
this.error = error.message || 'Failed to load company details';
Utils.showToast('Failed to load company details', 'error');
} finally {
this.loading = false;
}
},
// Format date (matches dashboard pattern)
formatDate(dateString) {
if (!dateString) {
companyDetailLog.debug('formatDate called with empty dateString');
return '-';
}
const formatted = Utils.formatDate(dateString);
companyDetailLog.debug(`Date formatted: ${dateString} -> ${formatted}`);
return formatted;
},
// Delete company
async deleteCompany() {
companyDetailLog.info('Delete company requested:', this.companyId);
if (this.company?.vendor_count > 0) {
Utils.showToast(`Cannot delete company with ${this.company.vendor_count} vendor(s). Delete vendors first.`, 'error');
return;
}
if (!confirm(`Are you sure you want to delete company "${this.company.name}"?\n\nThis action cannot be undone.`)) {
companyDetailLog.info('Delete cancelled by user');
return;
}
// Second confirmation for safety
if (!confirm(`FINAL CONFIRMATION\n\nAre you absolutely sure you want to delete "${this.company.name}"?`)) {
companyDetailLog.info('Delete cancelled by user (second confirmation)');
return;
}
try {
const url = `/admin/companies/${this.companyId}?confirm=true`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
companyDetailLog.info('Deleting company:', this.companyId);
await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, null, 'response');
Utils.showToast('Company deleted successfully', 'success');
companyDetailLog.info('Company deleted successfully');
// Redirect to companies list
setTimeout(() => window.location.href = '/admin/companies', 1500);
} catch (error) {
window.LogConfig.logError(error, 'Delete Company');
Utils.showToast(error.message || 'Failed to delete company', 'error');
}
},
// Refresh company data
async refresh() {
companyDetailLog.info('=== COMPANY REFRESH TRIGGERED ===');
await this.loadCompany();
Utils.showToast('Company details refreshed', 'success');
companyDetailLog.info('=== COMPANY REFRESH COMPLETE ===');
}
};
}
companyDetailLog.info('Company detail module loaded');

View File

@@ -0,0 +1,386 @@
// static/admin/js/company-edit.js
// Create custom logger for company edit
const companyEditLog = window.LogConfig.createLogger('COMPANY-EDIT');
function adminCompanyEdit() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Company edit page specific state
currentPage: 'company-edit',
company: null,
formData: {},
errors: {},
loadingCompany: false,
saving: false,
companyId: null,
// Transfer ownership state
showTransferOwnershipModal: false,
transferring: false,
transferData: {
new_owner_user_id: null,
confirm_transfer: false,
transfer_reason: ''
},
// User search state
userSearchQuery: '',
userSearchResults: [],
selectedUser: null,
showUserDropdown: false,
searchingUsers: false,
searchDebounceTimer: null,
showConfirmError: false,
showOwnerError: false,
// Initialize
async init() {
companyEditLog.info('=== COMPANY EDIT PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._companyEditInitialized) {
companyEditLog.warn('Company edit page already initialized, skipping...');
return;
}
window._companyEditInitialized = true;
// Get company ID from URL
const path = window.location.pathname;
const match = path.match(/\/admin\/companies\/(\d+)\/edit/);
if (match) {
this.companyId = parseInt(match[1], 10);
companyEditLog.info('Editing company:', this.companyId);
await this.loadCompany();
} else {
companyEditLog.error('No company ID in URL');
Utils.showToast('Invalid company URL', 'error');
setTimeout(() => window.location.href = '/admin/companies', 2000);
}
companyEditLog.info('=== COMPANY EDIT PAGE INITIALIZATION COMPLETE ===');
},
// Load company data
async loadCompany() {
companyEditLog.info('Loading company data...');
this.loadingCompany = true;
try {
const url = `/admin/companies/${this.companyId}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load Company', duration);
this.company = response;
// Initialize form data
this.formData = {
name: response.name || '',
description: response.description || '',
contact_email: response.contact_email || '',
contact_phone: response.contact_phone || '',
website: response.website || '',
business_address: response.business_address || '',
tax_number: response.tax_number || ''
};
companyEditLog.info(`Company loaded in ${duration}ms`, {
company_id: this.company.id,
name: this.company.name
});
companyEditLog.debug('Form data initialized:', this.formData);
} catch (error) {
window.LogConfig.logError(error, 'Load Company');
Utils.showToast('Failed to load company', 'error');
setTimeout(() => window.location.href = '/admin/companies', 2000);
} finally {
this.loadingCompany = false;
}
},
// Submit form
async handleSubmit() {
companyEditLog.info('=== SUBMITTING COMPANY UPDATE ===');
companyEditLog.debug('Form data:', this.formData);
this.errors = {};
this.saving = true;
try {
const url = `/admin/companies/${this.companyId}`;
window.LogConfig.logApiCall('PUT', url, this.formData, 'request');
const startTime = performance.now();
const response = await apiClient.put(url, this.formData);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('PUT', url, response, 'response');
window.LogConfig.logPerformance('Update Company', duration);
this.company = response;
Utils.showToast('Company updated successfully', 'success');
companyEditLog.info(`Company updated successfully in ${duration}ms`, response);
} catch (error) {
window.LogConfig.logError(error, 'Update Company');
// Handle validation errors
if (error.details && error.details.validation_errors) {
error.details.validation_errors.forEach(err => {
const field = err.loc?.[1] || err.loc?.[0];
if (field) {
this.errors[field] = err.msg;
}
});
companyEditLog.debug('Validation errors:', this.errors);
}
Utils.showToast(error.message || 'Failed to update company', 'error');
} finally {
this.saving = false;
companyEditLog.info('=== COMPANY UPDATE COMPLETE ===');
}
},
// Toggle verification
async toggleVerification() {
const action = this.company.is_verified ? 'unverify' : 'verify';
companyEditLog.info(`Toggle verification: ${action}`);
if (!confirm(`Are you sure you want to ${action} this company?`)) {
companyEditLog.info('Verification toggle cancelled by user');
return;
}
this.saving = true;
try {
const url = `/admin/companies/${this.companyId}/verification`;
const payload = { is_verified: !this.company.is_verified };
window.LogConfig.logApiCall('PUT', url, payload, 'request');
const response = await apiClient.put(url, payload);
window.LogConfig.logApiCall('PUT', url, response, 'response');
this.company = response;
Utils.showToast(`Company ${action}ed successfully`, 'success');
companyEditLog.info(`Company ${action}ed successfully`);
} catch (error) {
window.LogConfig.logError(error, `Toggle Verification (${action})`);
Utils.showToast(`Failed to ${action} company`, 'error');
} finally {
this.saving = false;
}
},
// Toggle active status
async toggleActive() {
const action = this.company.is_active ? 'deactivate' : 'activate';
companyEditLog.info(`Toggle active status: ${action}`);
if (!confirm(`Are you sure you want to ${action} this company?\n\nThis will affect all vendors under this company.`)) {
companyEditLog.info('Active status toggle cancelled by user');
return;
}
this.saving = true;
try {
const url = `/admin/companies/${this.companyId}/status`;
const payload = { is_active: !this.company.is_active };
window.LogConfig.logApiCall('PUT', url, payload, 'request');
const response = await apiClient.put(url, payload);
window.LogConfig.logApiCall('PUT', url, response, 'response');
this.company = response;
Utils.showToast(`Company ${action}d successfully`, 'success');
companyEditLog.info(`Company ${action}d successfully`);
} catch (error) {
window.LogConfig.logError(error, `Toggle Active Status (${action})`);
Utils.showToast(`Failed to ${action} company`, 'error');
} finally {
this.saving = false;
}
},
// Transfer company ownership
async transferOwnership() {
companyEditLog.info('=== TRANSFERRING COMPANY OWNERSHIP ===');
companyEditLog.debug('Transfer data:', this.transferData);
if (!this.transferData.new_owner_user_id) {
this.showOwnerError = true;
return;
}
if (!this.transferData.confirm_transfer) {
this.showConfirmError = true;
return;
}
// Clear errors
this.showOwnerError = false;
this.showConfirmError = false;
this.transferring = true;
try {
const url = `/admin/companies/${this.companyId}/transfer-ownership`;
const payload = {
new_owner_user_id: parseInt(this.transferData.new_owner_user_id, 10),
confirm_transfer: true,
transfer_reason: this.transferData.transfer_reason || null
};
window.LogConfig.logApiCall('POST', url, payload, 'request');
const response = await apiClient.post(url, payload);
window.LogConfig.logApiCall('POST', url, response, 'response');
Utils.showToast('Ownership transferred successfully', 'success');
companyEditLog.info('Ownership transferred successfully', response);
// Close modal and reload company data
this.showTransferOwnershipModal = false;
this.resetTransferData();
await this.loadCompany();
} catch (error) {
window.LogConfig.logError(error, 'Transfer Ownership');
Utils.showToast(error.message || 'Failed to transfer ownership', 'error');
} finally {
this.transferring = false;
companyEditLog.info('=== OWNERSHIP TRANSFER COMPLETE ===');
}
},
// Reset transfer data
resetTransferData() {
this.transferData = {
new_owner_user_id: null,
confirm_transfer: false,
transfer_reason: ''
};
this.userSearchQuery = '';
this.userSearchResults = [];
this.selectedUser = null;
this.showUserDropdown = false;
this.showConfirmError = false;
this.showOwnerError = false;
},
// Search users for transfer ownership
searchUsers() {
// Debounce search
clearTimeout(this.searchDebounceTimer);
if (this.userSearchQuery.length < 2) {
this.userSearchResults = [];
this.showUserDropdown = false;
return;
}
this.searchDebounceTimer = setTimeout(async () => {
companyEditLog.info('Searching users:', this.userSearchQuery);
this.searchingUsers = true;
this.showUserDropdown = true;
try {
const url = `/admin/users/search?q=${encodeURIComponent(this.userSearchQuery)}&limit=10`;
const response = await apiClient.get(url);
this.userSearchResults = response.users || response || [];
companyEditLog.debug('User search results:', this.userSearchResults);
} catch (error) {
window.LogConfig.logError(error, 'Search Users');
this.userSearchResults = [];
} finally {
this.searchingUsers = false;
}
}, 300);
},
// Select a user from search results
selectUser(user) {
companyEditLog.info('Selected user:', user);
this.selectedUser = user;
this.transferData.new_owner_user_id = user.id;
this.userSearchQuery = user.username;
this.showUserDropdown = false;
this.userSearchResults = [];
},
// Clear selected user
clearSelectedUser() {
this.selectedUser = null;
this.transferData.new_owner_user_id = null;
this.userSearchQuery = '';
this.userSearchResults = [];
},
// Delete company
async deleteCompany() {
companyEditLog.info('=== DELETING COMPANY ===');
if (this.company.vendor_count > 0) {
Utils.showToast(`Cannot delete company with ${this.company.vendor_count} vendors. Remove vendors first.`, 'error');
return;
}
if (!confirm(`Are you sure you want to delete company "${this.company.name}"?\n\nThis action cannot be undone.`)) {
companyEditLog.info('Company deletion cancelled by user');
return;
}
// Double confirmation for critical action
if (!confirm(`FINAL CONFIRMATION: Delete "${this.company.name}"?\n\nThis will permanently delete the company and all its data.`)) {
companyEditLog.info('Company deletion cancelled at final confirmation');
return;
}
this.saving = true;
try {
const url = `/admin/companies/${this.companyId}?confirm=true`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
const response = await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, response, 'response');
Utils.showToast('Company deleted successfully', 'success');
companyEditLog.info('Company deleted successfully');
// Redirect to companies list
setTimeout(() => {
window.location.href = '/admin/companies';
}, 1500);
} catch (error) {
window.LogConfig.logError(error, 'Delete Company');
Utils.showToast(error.message || 'Failed to delete company', 'error');
} finally {
this.saving = false;
companyEditLog.info('=== COMPANY DELETION COMPLETE ===');
}
}
};
}
companyEditLog.info('Company edit module loaded');