refactor: migrate templates and static files to self-contained modules
Templates Migration: - Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.) - Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.) - Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms) - Migrate public templates to modules (billing, marketplace, cms) - Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/) - Migrate letzshop partials to marketplace module Static Files Migration: - Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file) - Migrate vendor JS to modules: tenancy (4 files), core (2 files) - Migrate shared JS: vendor-selector.js to core, media-picker.js to cms - Migrate storefront JS: storefront-layout.js to core - Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/) - Update all template references to use module_static paths Naming Consistency: - Rename static/platform/ to static/public/ - Rename app/templates/platform/ to app/templates/public/ - Update all extends and static references Documentation: - Update module-system.md with shared templates documentation - Update frontend-structure.md with new module JS organization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,210 +0,0 @@
|
||||
{# app/templates/admin/partials/letzshop-exceptions-tab.html #}
|
||||
{# Exceptions tab for admin Letzshop management - Order Item Exception Resolution #}
|
||||
{% from 'shared/macros/pagination.html' import pagination %}
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Product Exceptions</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="selectedVendor ? 'Resolve unmatched products from order imports' : 'All exceptions across vendors'"></p>
|
||||
</div>
|
||||
<button
|
||||
@click="loadExceptions()"
|
||||
:disabled="loadingExceptions"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
<span x-show="!loadingExceptions" x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="loadingExceptions" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Status Cards -->
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-4">
|
||||
<!-- Pending Exceptions -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:bg-orange-900">
|
||||
<span x-html="$icon('exclamation-circle', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Pending</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="exceptionStats.pending || 0"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resolved Exceptions -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:bg-green-900">
|
||||
<span x-html="$icon('check-circle', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Resolved</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="exceptionStats.resolved || 0"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ignored Exceptions -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-gray-500 bg-gray-100 rounded-full dark:bg-gray-700">
|
||||
<span x-html="$icon('ban', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Ignored</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="exceptionStats.ignored || 0"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Orders Affected -->
|
||||
<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:bg-purple-900">
|
||||
<span x-html="$icon('shopping-cart', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Orders Affected</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="exceptionStats.orders_with_exceptions || 0"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="mb-4 flex flex-wrap gap-4 items-center">
|
||||
<!-- Search input -->
|
||||
<div class="relative flex-1 min-w-[200px] max-w-md">
|
||||
<span x-html="$icon('search', 'w-4 h-4 absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400')"></span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="exceptionsSearch"
|
||||
@input.debounce.300ms="pagination.page = 1; loadExceptions()"
|
||||
placeholder="Search by GTIN, product name, or order..."
|
||||
class="w-full pl-9 pr-8 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
x-show="exceptionsSearch"
|
||||
@click="exceptionsSearch = ''; pagination.page = 1; loadExceptions()"
|
||||
class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<span x-html="$icon('x', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Status filter -->
|
||||
<select
|
||||
x-model="exceptionsFilter"
|
||||
@change="pagination.page = 1; loadExceptions()"
|
||||
class="px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="resolved">Resolved</option>
|
||||
<option value="ignored">Ignored</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Exceptions Table -->
|
||||
<div class="w-full overflow-hidden rounded-lg shadow-xs">
|
||||
<div class="w-full 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-800">
|
||||
<th class="px-4 py-3">Product Info</th>
|
||||
<th x-show="!selectedVendor" class="px-4 py-3">Vendor</th>
|
||||
<th class="px-4 py-3">GTIN</th>
|
||||
<th class="px-4 py-3">Order</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3">Created</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-if="loadingExceptions && exceptions.length === 0">
|
||||
<tr>
|
||||
<td :colspan="selectedVendor ? 6 : 7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<span x-html="$icon('spinner', 'w-6 h-6 mx-auto mb-2')"></span>
|
||||
<p>Loading exceptions...</p>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-if="!loadingExceptions && exceptions.length === 0">
|
||||
<tr>
|
||||
<td :colspan="selectedVendor ? 6 : 7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<span x-html="$icon('check-circle', 'w-12 h-12 mx-auto mb-2 text-green-300')"></span>
|
||||
<p class="font-medium">No exceptions found</p>
|
||||
<p class="text-sm mt-1">All order items are properly matched to products</p>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-for="exc in exceptions" :key="exc.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center text-sm">
|
||||
<div>
|
||||
<p class="font-semibold text-gray-700 dark:text-gray-200" x-text="exc.original_product_name || 'Unknown Product'"></p>
|
||||
<p class="text-xs text-gray-500" x-show="exc.original_sku" x-text="'SKU: ' + exc.original_sku"></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Vendor column (only in cross-vendor view) -->
|
||||
<td x-show="!selectedVendor" class="px-4 py-3 text-sm">
|
||||
<p class="font-medium text-gray-700 dark:text-gray-200" x-text="exc.vendor_name || 'N/A'"></p>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<code class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded" x-text="exc.original_gtin || 'No GTIN'"></code>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<a
|
||||
:href="'/admin/orders/' + exc.order_id"
|
||||
class="text-purple-600 hover:text-purple-800 dark:text-purple-400"
|
||||
x-text="exc.order_number"
|
||||
></a>
|
||||
<p class="text-xs text-gray-500" x-text="formatDate(exc.order_date)"></p>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span
|
||||
class="px-2 py-1 font-semibold leading-tight rounded-full"
|
||||
:class="{
|
||||
'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100': exc.status === 'pending',
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': exc.status === 'resolved',
|
||||
'text-gray-700 bg-gray-100 dark:bg-gray-600 dark:text-gray-100': exc.status === 'ignored'
|
||||
}"
|
||||
x-text="exc.status.toUpperCase()"
|
||||
></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="formatDate(exc.created_at)"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<template x-if="exc.status === 'pending'">
|
||||
<button
|
||||
@click="openResolveModal(exc)"
|
||||
class="flex items-center justify-center px-2 py-1 text-sm text-green-600 transition-colors duration-150 rounded-md hover:bg-green-100 dark:hover:bg-green-900"
|
||||
title="Resolve - Assign Product"
|
||||
>
|
||||
<span x-html="$icon('check', 'w-4 h-4 mr-1')"></span>
|
||||
Resolve
|
||||
</button>
|
||||
</template>
|
||||
<template x-if="exc.status === 'pending'">
|
||||
<button
|
||||
@click="ignoreException(exc)"
|
||||
class="flex items-center justify-center px-2 py-1 text-sm text-gray-600 transition-colors duration-150 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
title="Ignore Exception"
|
||||
>
|
||||
<span x-html="$icon('ban', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</template>
|
||||
<template x-if="exc.status === 'resolved'">
|
||||
<span class="text-xs text-gray-500">
|
||||
<span x-html="$icon('check', 'w-3 h-3 inline mr-1')"></span>
|
||||
Resolved
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ pagination(show_condition="!loadingExceptions && pagination.total > 0") }}
|
||||
</div>
|
||||
@@ -1,326 +0,0 @@
|
||||
{# app/templates/admin/partials/letzshop-jobs-table.html #}
|
||||
{# Unified jobs table for admin Letzshop management - Import, Export, and Sync jobs #}
|
||||
{% from 'shared/macros/pagination.html' import pagination %}
|
||||
|
||||
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Recent Jobs</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
<span x-show="selectedVendor">Product imports, exports, and order sync history</span>
|
||||
<span x-show="!selectedVendor">All Letzshop jobs across all vendors</span>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@click="loadJobs()"
|
||||
:disabled="loadingJobs"
|
||||
class="flex items-center px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none disabled:opacity-50"
|
||||
>
|
||||
<span x-show="!loadingJobs" x-html="$icon('refresh', 'w-4 h-4 mr-1')"></span>
|
||||
<span x-show="loadingJobs" x-html="$icon('spinner', 'w-4 h-4 mr-1')"></span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="mb-4 flex flex-wrap gap-3">
|
||||
<select
|
||||
x-model="jobsFilter.type"
|
||||
@change="loadJobs()"
|
||||
class="px-3 py-1.5 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<option value="">All Types</option>
|
||||
<option value="import">Product Import</option>
|
||||
<option value="export">Product Export</option>
|
||||
<option value="historical_import">Historical Order Import</option>
|
||||
<option value="order_sync">Order Sync</option>
|
||||
</select>
|
||||
<select
|
||||
x-model="jobsFilter.status"
|
||||
@change="loadJobs()"
|
||||
class="px-3 py-1.5 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="processing">Processing</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="failed">Failed</option>
|
||||
<option value="completed_with_errors">Completed with Errors</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Jobs Table -->
|
||||
<div class="w-full 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">ID</th>
|
||||
<th class="px-4 py-3">Vendor</th>
|
||||
<th class="px-4 py-3">Type</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3">Records</th>
|
||||
<th class="px-4 py-3">Started</th>
|
||||
<th class="px-4 py-3">Duration</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-if="loadingJobs && jobs.length === 0">
|
||||
<tr>
|
||||
<td colspan="8" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<span x-html="$icon('spinner', 'w-6 h-6 mx-auto mb-2')"></span>
|
||||
<p>Loading jobs...</p>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-if="!loadingJobs && jobs.length === 0">
|
||||
<tr>
|
||||
<td colspan="8" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<span x-html="$icon('collection', 'w-12 h-12 mx-auto mb-2 text-gray-300')"></span>
|
||||
<p class="font-medium">No jobs found</p>
|
||||
<p class="text-sm mt-1">Import products or sync orders to see job history</p>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-for="job in jobs" :key="job.id + '-' + job.type">
|
||||
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="px-4 py-3 text-sm font-medium">
|
||||
<span x-text="'#' + job.id"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="job.vendor_code || job.vendor_name || '-'"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-medium rounded-full"
|
||||
:class="{
|
||||
'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300': job.type === 'import',
|
||||
'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300': job.type === 'export',
|
||||
'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300': job.type === 'historical_import',
|
||||
'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300': job.type === 'order_sync'
|
||||
}"
|
||||
>
|
||||
<span x-show="job.type === 'import'" x-html="$icon('cloud-download', 'inline w-3 h-3 mr-1')"></span>
|
||||
<span x-show="job.type === 'export'" x-html="$icon('cloud-upload', 'inline w-3 h-3 mr-1')"></span>
|
||||
<span x-show="job.type === 'historical_import'" x-html="$icon('clock', 'inline w-3 h-3 mr-1')"></span>
|
||||
<span x-show="job.type === 'order_sync'" x-html="$icon('refresh', 'inline w-3 h-3 mr-1')"></span>
|
||||
<span x-text="job.type === 'import' ? 'Product Import' : job.type === 'export' ? 'Product Export' : job.type === 'historical_import' ? 'Historical Import' : 'Order Sync'"></span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-semibold leading-tight rounded-full"
|
||||
:class="{
|
||||
'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-300': job.status === 'pending',
|
||||
'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100': job.status === 'processing',
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': job.status === 'completed' || job.status === 'success',
|
||||
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': job.status === 'failed',
|
||||
'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100': job.status === 'completed_with_errors' || job.status === 'partial'
|
||||
}"
|
||||
x-text="job.status.replace(/_/g, ' ').toUpperCase()"
|
||||
></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-green-600 dark:text-green-400" x-text="job.records_succeeded || 0"></span>
|
||||
<span class="text-gray-400">/</span>
|
||||
<span x-text="job.records_processed || 0"></span>
|
||||
<span x-show="job.records_failed > 0" class="text-red-600 dark:text-red-400">
|
||||
(<span x-text="job.records_failed"></span> failed)
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="formatDate(job.started_at || job.created_at)"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="formatDuration(job.started_at, job.completed_at)"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
x-show="(job.type === 'import' || job.type === 'historical_import') && (job.status === 'failed' || job.status === 'completed_with_errors')"
|
||||
@click="viewJobErrors(job)"
|
||||
class="flex items-center justify-center px-2 py-1 text-sm text-red-600 transition-colors duration-150 rounded-md hover:bg-red-100 dark:hover:bg-red-900"
|
||||
title="View Errors"
|
||||
>
|
||||
<span x-html="$icon('exclamation-circle', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="viewJobDetails(job)"
|
||||
class="flex items-center justify-center px-2 py-1 text-sm text-gray-600 transition-colors duration-150 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
title="View Details"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{ pagination(show_condition="!loadingJobs && pagination.total > 0") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Details Modal -->
|
||||
<div
|
||||
x-show="showJobDetailsModal"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 z-30 flex items-center justify-center bg-black bg-opacity-50"
|
||||
@click.self="showJobDetailsModal = false"
|
||||
x-cloak
|
||||
>
|
||||
<div
|
||||
x-show="showJobDetailsModal"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
class="w-full max-w-lg bg-white dark:bg-gray-800 rounded-lg shadow-xl"
|
||||
>
|
||||
<!-- Header -->
|
||||
<header class="flex justify-between items-center px-6 py-4 border-b dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Job Details</h3>
|
||||
<button
|
||||
@click="showJobDetailsModal = false"
|
||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<span x-html="$icon('close', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="px-6 py-4 space-y-4">
|
||||
<!-- Job Info Grid -->
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Job ID:</span>
|
||||
<span class="ml-2 text-gray-900 dark:text-gray-100">#<span x-text="selectedJobDetails?.id"></span></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Type:</span>
|
||||
<span class="ml-2">
|
||||
<span
|
||||
class="px-2 py-0.5 text-xs font-medium rounded-full"
|
||||
:class="{
|
||||
'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300': selectedJobDetails?.type === 'import',
|
||||
'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300': selectedJobDetails?.type === 'export',
|
||||
'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300': selectedJobDetails?.type === 'historical_import',
|
||||
'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300': selectedJobDetails?.type === 'order_sync'
|
||||
}"
|
||||
x-text="selectedJobDetails?.type === 'import' ? 'Product Import' : selectedJobDetails?.type === 'export' ? 'Product Export' : selectedJobDetails?.type === 'historical_import' ? 'Historical Import' : 'Order Sync'"
|
||||
></span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Status:</span>
|
||||
<span class="ml-2">
|
||||
<span
|
||||
class="px-2 py-0.5 text-xs font-semibold rounded-full"
|
||||
:class="{
|
||||
'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-300': selectedJobDetails?.status === 'pending',
|
||||
'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100': selectedJobDetails?.status === 'processing',
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': selectedJobDetails?.status === 'completed' || selectedJobDetails?.status === 'success',
|
||||
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': selectedJobDetails?.status === 'failed',
|
||||
'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100': selectedJobDetails?.status === 'completed_with_errors' || selectedJobDetails?.status === 'partial'
|
||||
}"
|
||||
x-text="selectedJobDetails?.status?.replace(/_/g, ' ').toUpperCase()"
|
||||
></span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Vendor:</span>
|
||||
<span class="ml-2 text-gray-900 dark:text-gray-100" x-text="selectedJobDetails?.vendor_code || selectedJobDetails?.vendor_name || selectedVendor?.name || '-'"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Records Info -->
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-2">Records</h4>
|
||||
<div class="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-green-600 dark:text-green-400" x-text="selectedJobDetails?.records_succeeded || 0"></div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Succeeded</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-gray-600 dark:text-gray-300" x-text="selectedJobDetails?.records_processed || 0"></div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Processed</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-red-600 dark:text-red-400" x-text="selectedJobDetails?.records_failed || 0"></div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Failed</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timestamps -->
|
||||
<div class="text-sm space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Started:</span>
|
||||
<span class="text-gray-900 dark:text-gray-100" x-text="formatDate(selectedJobDetails?.started_at || selectedJobDetails?.created_at)"></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Completed:</span>
|
||||
<span class="text-gray-900 dark:text-gray-100" x-text="selectedJobDetails?.completed_at ? formatDate(selectedJobDetails?.completed_at) : 'In progress...'"></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Duration:</span>
|
||||
<span class="text-gray-900 dark:text-gray-100" x-text="formatDuration(selectedJobDetails?.started_at || selectedJobDetails?.created_at, selectedJobDetails?.completed_at)"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export Details (for export jobs) -->
|
||||
<template x-if="selectedJobDetails?.type === 'export' && selectedJobDetails?.error_details">
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
|
||||
<h4 class="font-medium text-blue-700 dark:text-blue-300 mb-2">Export Details</h4>
|
||||
<p class="text-sm text-blue-600 dark:text-blue-400 mb-2">
|
||||
Products exported: <span class="font-medium" x-text="selectedJobDetails?.error_details?.products_exported || 0"></span>
|
||||
</p>
|
||||
<template x-if="selectedJobDetails?.error_details?.files">
|
||||
<div class="space-y-1">
|
||||
<template x-for="file in selectedJobDetails.error_details.files" :key="file.language">
|
||||
<div class="text-xs flex justify-between items-center py-1 border-b border-blue-100 dark:border-blue-800 last:border-0">
|
||||
<span class="font-medium text-blue-700 dark:text-blue-300" x-text="file.language?.toUpperCase()"></span>
|
||||
<span x-show="file.error" class="text-red-600 dark:text-red-400" x-text="'Failed: ' + file.error"></span>
|
||||
<span x-show="!file.error" class="text-blue-600 dark:text-blue-400">
|
||||
<span x-text="file.filename"></span>
|
||||
<span class="text-gray-400 ml-1">(<span x-text="(file.size_bytes / 1024).toFixed(1)"></span> KB)</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Error Details -->
|
||||
<template x-if="selectedJobDetails?.error_message || (selectedJobDetails?.error_details?.error && selectedJobDetails?.type !== 'export')">
|
||||
<div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-4">
|
||||
<h4 class="font-medium text-red-700 dark:text-red-300 mb-2">Error</h4>
|
||||
<p class="text-sm text-red-600 dark:text-red-400" x-text="selectedJobDetails?.error_message || selectedJobDetails?.error_details?.error"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="px-6 py-4 border-t dark:border-gray-700 flex justify-end">
|
||||
<button
|
||||
@click="showJobDetailsModal = false"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,307 +0,0 @@
|
||||
{# app/templates/admin/partials/letzshop-orders-tab.html #}
|
||||
{# Orders tab for admin Letzshop management #}
|
||||
{% from 'shared/macros/pagination.html' import pagination %}
|
||||
|
||||
<!-- Header with Import Buttons -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Orders</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="selectedVendor ? 'Manage Letzshop orders for this vendor' : 'All Letzshop orders across vendors'"></p>
|
||||
</div>
|
||||
<!-- Import buttons only shown when vendor is selected -->
|
||||
<div x-show="selectedVendor" class="flex gap-2">
|
||||
<button
|
||||
@click="importHistoricalOrders()"
|
||||
:disabled="!letzshopStatus.is_configured || importingHistorical"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Import all historical confirmed and declined orders"
|
||||
>
|
||||
<span x-show="!importingHistorical" x-html="$icon('archive', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="importingHistorical" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="!importingHistorical">Import History</span>
|
||||
<span x-show="importingHistorical" x-text="historicalImportProgress?.message || 'Starting...'"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="importOrders()"
|
||||
:disabled="!letzshopStatus.is_configured || importingOrders"
|
||||
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="!importingOrders" x-html="$icon('download', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="importingOrders" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="importingOrders ? 'Importing...' : 'Import New'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Historical Import Progress -->
|
||||
<div x-show="historicalImportProgress && importingHistorical" x-transition class="mb-6 p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('spinner', 'w-5 h-5 text-purple-500 mr-3')"></span>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium text-purple-800 dark:text-purple-200">Historical Import in Progress</h4>
|
||||
<p class="text-sm text-purple-700 dark:text-purple-300 mt-1" x-text="historicalImportProgress?.message"></p>
|
||||
<div class="flex gap-4 mt-2 text-xs text-purple-600 dark:text-purple-400">
|
||||
<span x-show="historicalImportProgress?.current_phase">
|
||||
Phase: <strong x-text="historicalImportProgress?.current_phase"></strong>
|
||||
</span>
|
||||
<span x-show="historicalImportProgress?.shipments_fetched > 0">
|
||||
Fetched: <strong x-text="historicalImportProgress?.shipments_fetched"></strong>
|
||||
</span>
|
||||
<span x-show="historicalImportProgress?.orders_processed > 0">
|
||||
Processed: <strong x-text="historicalImportProgress?.orders_processed"></strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Historical Import Result -->
|
||||
<div x-show="historicalImportResult" x-transition class="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start">
|
||||
<span x-html="$icon('check-circle', 'w-5 h-5 text-blue-500 mr-3 mt-0.5')"></span>
|
||||
<div>
|
||||
<h4 class="font-medium text-blue-800 dark:text-blue-200">Historical Import Complete</h4>
|
||||
<div class="text-sm text-blue-700 dark:text-blue-300 mt-1">
|
||||
<span x-text="historicalImportResult?.imported + ' imported'"></span> ·
|
||||
<span x-text="historicalImportResult?.updated + ' updated'"></span> ·
|
||||
<span x-text="historicalImportResult?.skipped + ' skipped'"></span>
|
||||
</div>
|
||||
<div x-show="historicalImportResult?.products_matched > 0 || historicalImportResult?.products_not_found > 0" class="text-sm text-blue-600 dark:text-blue-400 mt-1">
|
||||
<span x-text="historicalImportResult?.products_matched + ' products matched by EAN'"></span> ·
|
||||
<span x-text="historicalImportResult?.products_not_found + ' not found'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="historicalImportResult = null" class="text-blue-500 hover:text-blue-700">
|
||||
<span x-html="$icon('x', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Cards -->
|
||||
<div class="grid gap-6 mb-8" :class="selectedVendor ? 'md:grid-cols-5' : 'md:grid-cols-4'">
|
||||
<!-- Connection Status (only when vendor selected) -->
|
||||
<div x-show="selectedVendor" class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div :class="letzshopStatus.is_configured ? 'bg-green-100 dark:bg-green-900' : 'bg-gray-100 dark:bg-gray-700'" class="p-3 mr-4 rounded-full">
|
||||
<span x-html="$icon(letzshopStatus.is_configured ? 'check' : 'x', letzshopStatus.is_configured ? 'w-5 h-5 text-green-500' : 'w-5 h-5 text-gray-400')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Connection</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="letzshopStatus.is_configured ? 'Configured' : 'Not Configured'"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pending Orders -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:bg-orange-900">
|
||||
<span x-html="$icon('clock', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Pending</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="orderStats.pending"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmed/Processing Orders -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:bg-green-900">
|
||||
<span x-html="$icon('check-circle', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Confirmed</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="orderStats.processing"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Declined/Cancelled Orders -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-red-500 bg-red-100 rounded-full dark:bg-red-900">
|
||||
<span x-html="$icon('x-circle', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Declined</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="orderStats.cancelled"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipped Orders -->
|
||||
<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:bg-blue-900">
|
||||
<span x-html="$icon('truck', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Shipped</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="orderStats.shipped"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="mb-4 flex flex-wrap gap-4 items-center">
|
||||
<!-- Search input -->
|
||||
<div class="relative flex-1 min-w-[200px] max-w-md">
|
||||
<span x-html="$icon('search', 'w-4 h-4 absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400')"></span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="ordersSearch"
|
||||
@input.debounce.300ms="pagination.page = 1; loadOrders()"
|
||||
placeholder="Search by order #, name, or email..."
|
||||
class="w-full pl-9 pr-8 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
x-show="ordersSearch"
|
||||
@click="ordersSearch = ''; pagination.page = 1; loadOrders()"
|
||||
class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<span x-html="$icon('x', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Status filter -->
|
||||
<select
|
||||
x-model="ordersFilter"
|
||||
@change="ordersHasDeclinedItems = false; pagination.page = 1; loadOrders()"
|
||||
class="px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="processing">Confirmed</option>
|
||||
<option value="cancelled">Declined</option>
|
||||
<option value="shipped">Shipped</option>
|
||||
</select>
|
||||
|
||||
<!-- Declined items filter -->
|
||||
<button
|
||||
@click="ordersFilter = ''; ordersHasDeclinedItems = !ordersHasDeclinedItems; pagination.page = 1; loadOrders()"
|
||||
:class="ordersHasDeclinedItems ? 'bg-red-100 dark:bg-red-900 border-red-300 dark:border-red-700 text-red-700 dark:text-red-300' : 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300'"
|
||||
class="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
:title="ordersHasDeclinedItems ? 'Showing orders with declined items' : 'Show only orders with declined items'"
|
||||
>
|
||||
<span x-html="$icon('x-circle', 'w-4 h-4 inline mr-1')"></span>
|
||||
Has Declined Items
|
||||
<span x-show="orderStats.has_declined_items > 0" class="ml-1 px-1.5 py-0.5 text-xs bg-red-200 dark:bg-red-800 rounded-full" x-text="orderStats.has_declined_items"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Not Configured Warning (only when vendor selected) -->
|
||||
<div x-show="selectedVendor && !letzshopStatus.is_configured" class="mb-6 p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('exclamation', 'w-5 h-5 text-yellow-500 mr-3')"></span>
|
||||
<div>
|
||||
<h4 class="font-medium text-yellow-800 dark:text-yellow-200">API Not Configured</h4>
|
||||
<p class="text-sm text-yellow-700 dark:text-yellow-300">Configure the Letzshop API key in the Settings tab to import and manage orders.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Orders Table -->
|
||||
<div class="w-full overflow-hidden rounded-lg shadow-xs">
|
||||
<div class="w-full 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-800">
|
||||
<th class="px-4 py-3">Order</th>
|
||||
<th x-show="!selectedVendor" class="px-4 py-3">Vendor</th>
|
||||
<th class="px-4 py-3">Customer</th>
|
||||
<th class="px-4 py-3">Total</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3">Date</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-if="loadingOrders && orders.length === 0">
|
||||
<tr>
|
||||
<td :colspan="selectedVendor ? 6 : 7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<span x-html="$icon('spinner', 'w-6 h-6 mx-auto mb-2')"></span>
|
||||
<p>Loading orders...</p>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-if="!loadingOrders && orders.length === 0">
|
||||
<tr>
|
||||
<td :colspan="selectedVendor ? 6 : 7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<span x-html="$icon('inbox', 'w-12 h-12 mx-auto mb-2 text-gray-300')"></span>
|
||||
<p class="font-medium">No orders found</p>
|
||||
<p class="text-sm mt-1" x-text="selectedVendor ? 'Click Import Orders to fetch orders from Letzshop' : 'Select a vendor to import orders'"></p>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-for="order in orders" :key="order.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center text-sm">
|
||||
<div>
|
||||
<p class="font-semibold" x-text="order.external_order_number || order.order_number"></p>
|
||||
<p class="text-xs text-gray-500" x-text="'#' + order.id"></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Vendor column (only in cross-vendor view) -->
|
||||
<td x-show="!selectedVendor" class="px-4 py-3 text-sm">
|
||||
<p class="font-medium text-gray-700 dark:text-gray-200" x-text="order.vendor_name || 'N/A'"></p>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<p x-text="order.customer_email || 'N/A'"></p>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="order.total_amount ? order.total_amount + ' ' + order.currency : 'N/A'"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span
|
||||
class="px-2 py-1 font-semibold leading-tight rounded-full"
|
||||
:class="{
|
||||
'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100': order.status === 'pending',
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': order.status === 'processing',
|
||||
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': order.status === 'cancelled',
|
||||
'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100': order.status === 'shipped'
|
||||
}"
|
||||
x-text="order.status === 'cancelled' ? 'DECLINED' : (order.status === 'processing' ? 'CONFIRMED' : order.status.toUpperCase())"
|
||||
></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="formatDate(order.order_date || order.created_at)"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<button
|
||||
x-show="order.status === 'pending'"
|
||||
@click="confirmOrder(order)"
|
||||
class="flex items-center justify-center px-2 py-1 text-sm text-green-600 transition-colors duration-150 rounded-md hover:bg-green-100 dark:hover:bg-green-900"
|
||||
title="Confirm Order"
|
||||
>
|
||||
<span x-html="$icon('check', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
<button
|
||||
x-show="order.status === 'pending'"
|
||||
@click="declineOrder(order)"
|
||||
class="flex items-center justify-center px-2 py-1 text-sm text-red-600 transition-colors duration-150 rounded-md hover:bg-red-100 dark:hover:bg-red-900"
|
||||
title="Decline Order"
|
||||
>
|
||||
<span x-html="$icon('x', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
<button
|
||||
x-show="order.status === 'processing'"
|
||||
@click="openTrackingModal(order)"
|
||||
class="flex items-center justify-center px-2 py-1 text-sm text-blue-600 transition-colors duration-150 rounded-md hover:bg-blue-100 dark:hover:bg-blue-900"
|
||||
title="Set Tracking"
|
||||
>
|
||||
<span x-html="$icon('truck', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="viewOrderDetails(order)"
|
||||
class="flex items-center justify-center px-2 py-1 text-sm text-gray-600 transition-colors duration-150 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
title="View Details"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ pagination(show_condition="!loadingOrders && pagination.total > 0") }}
|
||||
</div>
|
||||
@@ -1,362 +0,0 @@
|
||||
{# app/templates/admin/partials/letzshop-products-tab.html #}
|
||||
{# Products tab for admin Letzshop management - Product listing with Import/Export #}
|
||||
{% from 'shared/macros/pagination.html' import pagination %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper %}
|
||||
|
||||
<!-- Header with Import/Export Buttons -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Letzshop Products</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
<span x-show="selectedVendor" x-text="'Products from ' + (selectedVendor?.name || '')"></span>
|
||||
<span x-show="!selectedVendor">All Letzshop marketplace products</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3" x-show="selectedVendor">
|
||||
<!-- Import Button (only when vendor selected) -->
|
||||
<button
|
||||
@click="showImportModal = true"
|
||||
:disabled="importing"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<span x-show="!importing" x-html="$icon('cloud-download', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="importing" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="importing ? 'Importing...' : 'Import'"></span>
|
||||
</button>
|
||||
<!-- Export Button (only when vendor selected) -->
|
||||
<button
|
||||
@click="exportAllLanguages()"
|
||||
:disabled="exporting"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<span x-show="!exporting" x-html="$icon('upload', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="exporting" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="exporting ? 'Exporting...' : 'Export'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid gap-4 mb-6 md:grid-cols-2 xl:grid-cols-4">
|
||||
<!-- Total Products -->
|
||||
<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('cube', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Products</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="productStats.total || 0"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Products -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
|
||||
<span x-html="$icon('check-circle', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Active</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="productStats.active || 0"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inactive Products -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-red-500 bg-red-100 rounded-full dark:text-red-100 dark:bg-red-500">
|
||||
<span x-html="$icon('x-circle', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Inactive</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="productStats.inactive || 0"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last Sync -->
|
||||
<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('refresh', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Last Sync</p>
|
||||
<p class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="productStats.last_sync || 'Never'"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filters Bar -->
|
||||
<div class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||||
<!-- Search Input -->
|
||||
<div class="flex-1 max-w-xl">
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<span x-html="$icon('search', 'w-5 h-5 text-gray-400')"></span>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="productFilters.search"
|
||||
@input.debounce.300ms="loadProducts()"
|
||||
placeholder="Search by title, GTIN, or SKU..."
|
||||
class="w-full pl-10 pr-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<!-- Status Filter -->
|
||||
<select
|
||||
x-model="productFilters.is_active"
|
||||
@change="pagination.page = 1; loadProducts()"
|
||||
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="true">Active</option>
|
||||
<option value="false">Inactive</option>
|
||||
</select>
|
||||
|
||||
<!-- Refresh Button -->
|
||||
<button
|
||||
@click="loadProducts()"
|
||||
:disabled="loadingProducts"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="Refresh products"
|
||||
>
|
||||
<span x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div x-show="loadingProducts" class="flex items-center justify-center py-12">
|
||||
<span x-html="$icon('spinner', 'w-8 h-8 text-purple-600')"></span>
|
||||
<span class="ml-3 text-gray-600 dark:text-gray-400">Loading products...</span>
|
||||
</div>
|
||||
|
||||
<!-- Products Table -->
|
||||
<div x-show="!loadingProducts">
|
||||
{% call table_wrapper() %}
|
||||
<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-800">
|
||||
<th class="px-4 py-3">Product</th>
|
||||
<th class="px-4 py-3" x-show="!selectedVendor">Vendor</th>
|
||||
<th class="px-4 py-3">Identifiers</th>
|
||||
<th class="px-4 py-3">Price</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">
|
||||
<!-- Empty State -->
|
||||
<template x-if="products.length === 0">
|
||||
<tr>
|
||||
<td :colspan="selectedVendor ? 5 : 6" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('cube', 'w-12 h-12 mb-2 text-gray-300')"></span>
|
||||
<p class="font-medium">No products found</p>
|
||||
<p class="text-xs mt-1" x-text="productFilters.search ? 'Try adjusting your search' : (selectedVendor ? 'Import products to get started' : 'No Letzshop products in the catalog')"></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Product Rows -->
|
||||
<template x-for="product in products" :key="product.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400">
|
||||
<!-- Product Info -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center">
|
||||
<!-- Product Image -->
|
||||
<div class="w-10 h-10 mr-3 rounded-lg overflow-hidden bg-gray-100 dark:bg-gray-700 flex-shrink-0">
|
||||
<template x-if="product.image_link">
|
||||
<img :src="product.image_link" :alt="product.title" class="w-full h-full object-cover" loading="lazy" />
|
||||
</template>
|
||||
<template x-if="!product.image_link">
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<span x-html="$icon('photograph', 'w-5 h-5 text-gray-400')"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- Product Details -->
|
||||
<div class="min-w-0">
|
||||
<a :href="'/admin/letzshop/products/' + product.id" class="font-semibold text-sm truncate max-w-xs hover:text-purple-600 dark:hover:text-purple-400" x-text="product.title || 'Untitled'"></a>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="product.brand || 'No brand'"></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Vendor (shown when no vendor filter) -->
|
||||
<td class="px-4 py-3 text-sm" x-show="!selectedVendor">
|
||||
<span class="font-medium" x-text="product.vendor_name || '-'"></span>
|
||||
</td>
|
||||
|
||||
<!-- Identifiers -->
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<div class="space-y-1">
|
||||
<template x-if="product.gtin">
|
||||
<p class="text-xs"><span class="text-gray-500">GTIN:</span> <span x-text="product.gtin" class="font-mono"></span></p>
|
||||
</template>
|
||||
<template x-if="product.sku">
|
||||
<p class="text-xs"><span class="text-gray-500">SKU:</span> <span x-text="product.sku" class="font-mono"></span></p>
|
||||
</template>
|
||||
<template x-if="!product.gtin && !product.sku">
|
||||
<p class="text-xs text-gray-400">No identifiers</p>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Price -->
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<template x-if="product.price_numeric">
|
||||
<p class="font-medium" x-text="formatPrice(product.price_numeric, product.currency || 'EUR')"></p>
|
||||
</template>
|
||||
<template x-if="!product.price_numeric">
|
||||
<p class="text-gray-400">-</p>
|
||||
</template>
|
||||
</td>
|
||||
|
||||
<!-- Status -->
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full text-xs"
|
||||
:class="product.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="product.is_active ? 'Active' : 'Inactive'">
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<div class="flex items-center space-x-2">
|
||||
<a
|
||||
:href="'/admin/letzshop/products/' + product.id"
|
||||
class="flex items-center justify-center px-2 py-1 text-xs font-medium leading-5 text-purple-600 rounded-lg dark:text-purple-400 focus:outline-none hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
title="View Details"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-4 h-4')"></span>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
|
||||
{{ pagination(show_condition="!loadingProducts && pagination.total > 0") }}
|
||||
</div>
|
||||
|
||||
<!-- Import Modal -->
|
||||
<div
|
||||
x-show="showImportModal"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 z-30 flex items-end bg-black bg-opacity-50 sm:items-center sm:justify-center"
|
||||
@click.self="showImportModal = false"
|
||||
x-cloak
|
||||
>
|
||||
<div
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 transform translate-y-1/2"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0 transform translate-y-1/2"
|
||||
class="w-full px-6 py-4 overflow-hidden bg-white rounded-t-lg dark:bg-gray-800 sm:rounded-lg sm:m-4 sm:max-w-lg"
|
||||
@click.stop
|
||||
>
|
||||
<header class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Import Products from Letzshop</h3>
|
||||
<button @click="showImportModal = false" class="text-gray-400 hover:text-gray-600">
|
||||
<span x-html="$icon('x', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Import products from Letzshop CSV feeds. All languages will be imported.
|
||||
</p>
|
||||
|
||||
<!-- Quick Fill Buttons -->
|
||||
<div class="mb-4" x-show="selectedVendor?.letzshop_csv_url_fr || selectedVendor?.letzshop_csv_url_en || selectedVendor?.letzshop_csv_url_de">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Quick Import
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="startImportAllLanguages()"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
<span x-html="$icon('cloud-download', 'w-4 h-4 mr-2')"></span>
|
||||
Import All Languages
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Imports products from all configured CSV URLs (FR, EN, DE)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-600 pt-4 mt-4">
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Or import from custom URL:</p>
|
||||
<form @submit.prevent="startImportFromUrl()">
|
||||
<!-- CSV URL -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
CSV URL
|
||||
</label>
|
||||
<input
|
||||
x-model="importForm.csv_url"
|
||||
type="url"
|
||||
placeholder="https://letzshop.lu/feeds/products.csv"
|
||||
class="block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Language Selection -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Language for this URL
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<button type="button" @click="importForm.language = 'fr'"
|
||||
:class="importForm.language === 'fr' ? 'bg-purple-100 border-purple-500 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300' : 'bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300'"
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm font-medium border rounded-lg">
|
||||
<span class="fi fi-fr"></span> FR
|
||||
</button>
|
||||
<button type="button" @click="importForm.language = 'de'"
|
||||
:class="importForm.language === 'de' ? 'bg-purple-100 border-purple-500 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300' : 'bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300'"
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm font-medium border rounded-lg">
|
||||
<span class="fi fi-de"></span> DE
|
||||
</button>
|
||||
<button type="button" @click="importForm.language = 'en'"
|
||||
:class="importForm.language === 'en' ? 'bg-purple-100 border-purple-500 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300' : 'bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300'"
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm font-medium border rounded-lg">
|
||||
<span class="fi fi-gb"></span> EN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="showImportModal = false"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="importing || !importForm.csv_url"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||
>
|
||||
<span x-show="importing" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,410 +0,0 @@
|
||||
{# app/templates/admin/partials/letzshop-settings-tab.html #}
|
||||
{# Settings tab for admin Letzshop management - API credentials, CSV URLs, Import/Export settings #}
|
||||
{% from 'shared/macros/inputs.html' import number_stepper %}
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<!-- API Configuration Card -->
|
||||
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-6">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Letzshop API Configuration
|
||||
</h3>
|
||||
|
||||
<form @submit.prevent="saveCredentials()">
|
||||
<!-- API Key -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
API Key <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
:type="showApiKey ? 'text' : 'password'"
|
||||
x-model="settingsForm.api_key"
|
||||
:placeholder="credentials ? credentials.api_key_masked : 'Enter Letzshop API key'"
|
||||
class="block w-full px-3 py-2 pr-10 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none focus:shadow-outline-purple"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="showApiKey = !showApiKey"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<span x-html="$icon(showApiKey ? 'eye-off' : 'eye', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Get your API key from the Letzshop merchant portal
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Test Mode -->
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
x-model="settingsForm.test_mode_enabled"
|
||||
class="form-checkbox h-5 w-5 text-orange-600 rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:ring-orange-500"
|
||||
/>
|
||||
<span class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-400">Test Mode</span>
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-8">
|
||||
When enabled, operations (confirm, reject, tracking) will NOT be sent to Letzshop API
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Test Mode Warning -->
|
||||
<div x-show="settingsForm.test_mode_enabled" class="mb-4 p-3 bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('exclamation', 'w-5 h-5 text-orange-500 mr-2')"></span>
|
||||
<span class="text-sm text-orange-700 dark:text-orange-300 font-medium">Test Mode Active</span>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-orange-600 dark:text-orange-400 ml-7">
|
||||
All Letzshop API mutations are disabled. Orders can be imported but confirmations/rejections will only be saved locally.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Auto Sync -->
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
x-model="settingsForm.auto_sync_enabled"
|
||||
class="form-checkbox h-5 w-5 text-purple-600 rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:ring-purple-500"
|
||||
/>
|
||||
<span class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-400">Enable Auto-Sync</span>
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-8">
|
||||
Automatically import new orders periodically
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Sync Interval -->
|
||||
<div class="mb-6" x-show="settingsForm.auto_sync_enabled">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Sync Interval
|
||||
</label>
|
||||
<select
|
||||
x-model="settingsForm.sync_interval_minutes"
|
||||
class="block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<option value="15">Every 15 minutes</option>
|
||||
<option value="30">Every 30 minutes</option>
|
||||
<option value="60">Every hour</option>
|
||||
<option value="120">Every 2 hours</option>
|
||||
<option value="360">Every 6 hours</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Last Sync Info -->
|
||||
<div x-show="credentials" class="mb-6 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Last Sync</h4>
|
||||
<div class="grid gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>
|
||||
<span class="font-medium">Status:</span>
|
||||
<span
|
||||
class="ml-2 px-2 py-0.5 text-xs rounded-full"
|
||||
:class="{
|
||||
'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300': credentials?.last_sync_status === 'success',
|
||||
'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300': credentials?.last_sync_status === 'partial',
|
||||
'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300': credentials?.last_sync_status === 'failed',
|
||||
'bg-gray-100 text-gray-700 dark:bg-gray-600 dark:text-gray-300': !credentials?.last_sync_status
|
||||
}"
|
||||
x-text="credentials?.last_sync_status || 'Never'"
|
||||
></span>
|
||||
</p>
|
||||
<p x-show="credentials?.last_sync_at">
|
||||
<span class="font-medium">Time:</span>
|
||||
<span class="ml-2" x-text="formatDate(credentials?.last_sync_at)"></span>
|
||||
</p>
|
||||
<p x-show="credentials?.last_sync_error" class="text-red-600 dark:text-red-400">
|
||||
<span class="font-medium">Error:</span>
|
||||
<span class="ml-2" x-text="credentials?.last_sync_error"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="savingCredentials"
|
||||
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"
|
||||
>
|
||||
<span x-show="!savingCredentials" x-html="$icon('save', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="savingCredentials" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="savingCredentials ? 'Saving...' : 'Save Credentials'"></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click="testConnection()"
|
||||
:disabled="testingConnection || !letzshopStatus.is_configured"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none disabled:opacity-50"
|
||||
>
|
||||
<span x-show="!testingConnection" x-html="$icon('lightning-bolt', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="testingConnection" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="testingConnection ? 'Testing...' : 'Test Connection'"></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
x-show="credentials"
|
||||
@click="deleteCredentials()"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-red-600 transition-colors duration-150 bg-white dark:bg-gray-800 border border-red-300 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 focus:outline-none"
|
||||
>
|
||||
<span x-html="$icon('trash', 'w-4 h-4 mr-2')"></span>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CSV URLs Card -->
|
||||
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-6">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Letzshop CSV URLs
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
Configure the CSV feed URLs for product imports. These URLs are used for quick-fill in the Products tab.
|
||||
</p>
|
||||
|
||||
<form @submit.prevent="saveCsvUrls()">
|
||||
<!-- French CSV URL -->
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
<span class="fi fi-fr mr-2"></span>
|
||||
French CSV URL
|
||||
</label>
|
||||
<input
|
||||
x-model="settingsForm.letzshop_csv_url_fr"
|
||||
type="url"
|
||||
placeholder="https://letzshop.lu/feeds/products_fr.csv"
|
||||
class="block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none focus:shadow-outline-purple"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- English CSV URL -->
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
<span class="fi fi-gb mr-2"></span>
|
||||
English CSV URL
|
||||
</label>
|
||||
<input
|
||||
x-model="settingsForm.letzshop_csv_url_en"
|
||||
type="url"
|
||||
placeholder="https://letzshop.lu/feeds/products_en.csv"
|
||||
class="block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none focus:shadow-outline-purple"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- German CSV URL -->
|
||||
<div class="mb-6">
|
||||
<label class="flex items-center text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
<span class="fi fi-de mr-2"></span>
|
||||
German CSV URL
|
||||
</label>
|
||||
<input
|
||||
x-model="settingsForm.letzshop_csv_url_de"
|
||||
type="url"
|
||||
placeholder="https://letzshop.lu/feeds/products_de.csv"
|
||||
class="block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none focus:shadow-outline-purple"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="savingCsvUrls"
|
||||
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"
|
||||
>
|
||||
<span x-show="!savingCsvUrls" x-html="$icon('save', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="savingCsvUrls" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="savingCsvUrls ? 'Saving...' : 'Save CSV URLs'"></span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<div class="flex">
|
||||
<span x-html="$icon('information-circle', 'w-5 h-5 text-blue-500 mr-2 flex-shrink-0')"></span>
|
||||
<div class="text-sm text-blue-700 dark:text-blue-300">
|
||||
<p class="font-medium">About CSV URLs</p>
|
||||
<p class="mt-1">These URLs should point to the vendor's product feed on Letzshop. The feed is typically provided by Letzshop as part of the merchant integration.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import/Export Settings Card -->
|
||||
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-6">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Import / Export Settings
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
Configure settings for product import and export operations.
|
||||
</p>
|
||||
|
||||
<!-- Import Settings -->
|
||||
<div class="mb-6">
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3 flex items-center">
|
||||
<span x-html="$icon('cloud-download', 'w-4 h-4 mr-2 text-purple-500')"></span>
|
||||
Import Settings
|
||||
</h4>
|
||||
<div class="pl-6">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Batch Size
|
||||
</label>
|
||||
{{ number_stepper(model='importForm.batch_size', min=100, max=5000, step=100, label='Batch Size') }}
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Products processed per batch (100-5000). Higher = faster but more memory.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export Settings -->
|
||||
<div class="border-t border-gray-200 dark:border-gray-600 pt-6">
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3 flex items-center">
|
||||
<span x-html="$icon('upload', 'w-4 h-4 mr-2 text-green-500')"></span>
|
||||
Export Settings
|
||||
</h4>
|
||||
<div class="pl-6">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
x-model="exportIncludeInactive"
|
||||
class="form-checkbox h-5 w-5 text-purple-600 rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:ring-purple-500"
|
||||
/>
|
||||
<span class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-400">Include inactive products</span>
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-8">
|
||||
Export products that are currently marked as inactive
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export Info Box -->
|
||||
<div class="mt-6 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Export Behavior</h4>
|
||||
<ul class="text-xs text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<li class="flex items-center">
|
||||
<span x-html="$icon('check', 'w-3 h-3 mr-2 text-green-500')"></span>
|
||||
Exports all languages (FR, DE, EN) automatically
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<span x-html="$icon('check', 'w-3 h-3 mr-2 text-green-500')"></span>
|
||||
CSV files are placed in a folder for Letzshop pickup
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<span x-html="$icon('check', 'w-3 h-3 mr-2 text-green-500')"></span>
|
||||
Letzshop scheduler fetches files periodically
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Carrier Settings Card -->
|
||||
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 lg:col-span-2">
|
||||
<div class="p-6">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Carrier Settings
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
Configure default carrier and label URL prefixes for shipping labels.
|
||||
</p>
|
||||
|
||||
<form @submit.prevent="saveCarrierSettings()">
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<!-- Default Carrier -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Default Carrier
|
||||
</label>
|
||||
<select
|
||||
x-model="settingsForm.default_carrier"
|
||||
class="block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<option value="">-- Select carrier --</option>
|
||||
<option value="greco">Greco</option>
|
||||
<option value="colissimo">Colissimo</option>
|
||||
<option value="xpresslogistics">XpressLogistics</option>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Letzshop automatically assigns carriers based on shipment data
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Placeholder for alignment -->
|
||||
<div></div>
|
||||
|
||||
<!-- Greco Label URL -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
<span class="inline-flex items-center">
|
||||
<span class="w-3 h-3 rounded-full bg-blue-500 mr-2"></span>
|
||||
Greco Label URL Prefix
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
x-model="settingsForm.carrier_greco_label_url"
|
||||
type="url"
|
||||
placeholder="https://dispatchweb.fr/Tracky/Home/"
|
||||
class="block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Label URL = Prefix + Shipment Number (e.g., H74683403433)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Colissimo Label URL -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
<span class="inline-flex items-center">
|
||||
<span class="w-3 h-3 rounded-full bg-yellow-500 mr-2"></span>
|
||||
Colissimo Label URL Prefix
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
x-model="settingsForm.carrier_colissimo_label_url"
|
||||
type="url"
|
||||
placeholder="https://..."
|
||||
class="block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- XpressLogistics Label URL -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
<span class="inline-flex items-center">
|
||||
<span class="w-3 h-3 rounded-full bg-green-500 mr-2"></span>
|
||||
XpressLogistics Label URL Prefix
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
x-model="settingsForm.carrier_xpresslogistics_label_url"
|
||||
type="url"
|
||||
placeholder="https://..."
|
||||
class="block w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="savingCarrierSettings"
|
||||
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"
|
||||
>
|
||||
<span x-show="!savingCarrierSettings" x-html="$icon('save', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="savingCarrierSettings" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="savingCarrierSettings ? 'Saving...' : 'Save Carrier Settings'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user