feat: add email settings with database overrides for admin and vendor
Platform Email Settings (Admin): - Add GET/PUT/DELETE /admin/settings/email/* endpoints - Settings stored in admin_settings table override .env values - Support all providers: SMTP, SendGrid, Mailgun, Amazon SES - Edit mode UI with provider-specific configuration forms - Reset to .env defaults functionality - Test email to verify configuration Vendor Email Settings: - Add VendorEmailSettings model with one-to-one vendor relationship - Migration: v0a1b2c3d4e5_add_vendor_email_settings.py - Service: vendor_email_settings_service.py with tier validation - API endpoints: /vendor/email-settings/* (CRUD, status, verify) - Email tab in vendor settings page with full configuration - Warning banner until email is configured (like billing warnings) - Premium providers (SendGrid, Mailgun, SES) tier-gated to Business+ Email Service Updates: - get_platform_email_config(db) checks DB first, then .env - Configurable provider classes accept config dict - EmailService uses database-aware providers - Vendor emails use vendor's own SMTP (Wizamart doesn't pay) - "Powered by Wizamart" footer for Essential/Professional tiers - White-label (no footer) for Business/Enterprise tiers Other: - Add scripts/install.py for first-time platform setup - Add make install target - Update init-prod to include email template seeding 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
{% call tabs_nav() %}
|
||||
{{ tab_button('display', 'Display', icon='view-grid') }}
|
||||
{{ tab_button('logging', 'Logging', icon='document-text') }}
|
||||
{{ tab_button('email', 'Email', icon='envelope') }}
|
||||
{{ tab_button('shipping', 'Shipping', icon='truck') }}
|
||||
{{ tab_button('system', 'System', icon='cog') }}
|
||||
{{ tab_button('security', 'Security', icon='shield-check') }}
|
||||
@@ -218,6 +219,349 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email Settings Tab -->
|
||||
<div x-show="activeTab === 'email'" x-transition>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Platform Email Configuration
|
||||
</h3>
|
||||
<!-- Edit/Cancel buttons -->
|
||||
<div class="flex items-center gap-2">
|
||||
<template x-if="!emailEditMode">
|
||||
<button
|
||||
@click="enableEmailEditing()"
|
||||
class="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"
|
||||
>
|
||||
<span x-html="$icon('pencil', 'w-4 h-4 inline mr-1')"></span>
|
||||
Edit Settings
|
||||
</button>
|
||||
</template>
|
||||
<template x-if="emailEditMode">
|
||||
<button
|
||||
@click="cancelEmailEditing()"
|
||||
class="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"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
Configure the platform's email settings for system emails (billing, subscriptions, admin notifications).
|
||||
Vendor emails use each vendor's own email settings.
|
||||
</p>
|
||||
|
||||
<!-- Current Status -->
|
||||
<div class="mb-6 p-4 rounded-lg" :class="emailSettings.is_configured ? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800' : 'bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800'">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<template x-if="emailSettings.is_configured">
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('check-circle', 'w-5 h-5 text-green-600 dark:text-green-400 mr-2')"></span>
|
||||
<span class="text-green-800 dark:text-green-300 font-medium">Email configured</span>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!emailSettings.is_configured">
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('exclamation-triangle', 'w-5 h-5 text-yellow-600 dark:text-yellow-400 mr-2')"></span>
|
||||
<span class="text-yellow-800 dark:text-yellow-300 font-medium">Email not fully configured</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template x-if="emailSettings.has_db_overrides">
|
||||
<span class="px-2 py-1 text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded">
|
||||
Database overrides active
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== READ-ONLY VIEW ===== -->
|
||||
<template x-if="!emailEditMode">
|
||||
<div>
|
||||
<!-- Provider Selection (Read-only) -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Email Provider
|
||||
</label>
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
<template x-for="provider in ['smtp', 'sendgrid', 'mailgun', 'ses', 'debug']" :key="provider">
|
||||
<div
|
||||
class="p-3 border-2 rounded-lg text-center"
|
||||
:class="emailSettings.provider === provider
|
||||
? 'border-purple-500 bg-purple-50 dark:bg-purple-900/30'
|
||||
: 'border-gray-200 dark:border-gray-600'"
|
||||
>
|
||||
<div class="text-sm font-medium text-gray-700 dark:text-gray-300 capitalize" x-text="provider === 'ses' ? 'Amazon SES' : provider"></div>
|
||||
<template x-if="emailSettings.provider === provider">
|
||||
<span x-html="$icon('check-circle', 'w-4 h-4 text-purple-600 dark:text-purple-400 mx-auto mt-1')"></span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Settings (Read-only) -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-md font-medium text-gray-700 dark:text-gray-300 border-b dark:border-gray-600 pb-2">
|
||||
Current Configuration
|
||||
</h4>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">From Email</label>
|
||||
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg text-sm text-gray-700 dark:text-gray-300" x-text="emailSettings.from_email || 'Not configured'"></div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">From Name</label>
|
||||
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg text-sm text-gray-700 dark:text-gray-300" x-text="emailSettings.from_name || 'Not configured'"></div>
|
||||
</div>
|
||||
<template x-if="emailSettings.provider === 'smtp'">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Host</label>
|
||||
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg text-sm text-gray-700 dark:text-gray-300" x-text="emailSettings.smtp_host || 'Not configured'"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="emailSettings.provider === 'smtp'">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Port</label>
|
||||
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg text-sm text-gray-700 dark:text-gray-300" x-text="emailSettings.smtp_port || 'Not configured'"></div>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Debug Mode</label>
|
||||
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg text-sm" :class="emailSettings.debug ? 'text-yellow-600 dark:text-yellow-400' : 'text-gray-700 dark:text-gray-300'" x-text="emailSettings.debug ? 'Enabled (emails logged, not sent)' : 'Disabled'"></div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email Sending</label>
|
||||
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg text-sm" :class="emailSettings.enabled ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'" x-text="emailSettings.enabled ? 'Enabled' : 'Disabled'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<span x-html="$icon('information-circle', 'w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3 flex-shrink-0')"></span>
|
||||
<div class="text-sm text-blue-800 dark:text-blue-200">
|
||||
<p class="font-medium mb-1">Configuration Priority</p>
|
||||
<p>Settings can be configured via environment variables (.env) or overridden in the database using the Edit button above. Database settings take priority over .env values.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reset to .env button (only show if DB overrides exist) -->
|
||||
<template x-if="emailSettings.has_db_overrides">
|
||||
<div class="mt-4">
|
||||
<button
|
||||
@click="resetEmailSettings()"
|
||||
:disabled="saving"
|
||||
class="px-4 py-2 text-sm font-medium text-red-600 dark:text-red-400 bg-white dark:bg-gray-700 border border-red-300 dark:border-red-600 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 disabled:opacity-50"
|
||||
>
|
||||
<span x-html="$icon('refresh', 'w-4 h-4 inline mr-1')"></span>
|
||||
Reset to .env Defaults
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ===== EDIT MODE ===== -->
|
||||
<template x-if="emailEditMode">
|
||||
<div>
|
||||
<!-- Provider Selection (Editable) -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Email Provider
|
||||
</label>
|
||||
<select
|
||||
x-model="emailForm.provider"
|
||||
class="block w-full md:w-1/2 px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||
>
|
||||
<option value="smtp">SMTP</option>
|
||||
<option value="sendgrid">SendGrid</option>
|
||||
<option value="mailgun">Mailgun</option>
|
||||
<option value="ses">Amazon SES</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Common Settings -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">From Email *</label>
|
||||
<input
|
||||
type="email"
|
||||
x-model="emailForm.from_email"
|
||||
placeholder="noreply@yourplatform.com"
|
||||
class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">From Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="emailForm.from_name"
|
||||
placeholder="Your Platform"
|
||||
class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Reply-To Email</label>
|
||||
<input
|
||||
type="email"
|
||||
x-model="emailForm.reply_to"
|
||||
placeholder="support@yourplatform.com"
|
||||
class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SMTP Settings -->
|
||||
<template x-if="emailForm.provider === 'smtp'">
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg mb-6">
|
||||
<h4 class="text-md font-medium text-gray-700 dark:text-gray-300 mb-4">SMTP Settings</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Host</label>
|
||||
<input type="text" x-model="emailForm.smtp_host" placeholder="smtp.example.com" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Port</label>
|
||||
<input type="number" x-model="emailForm.smtp_port" placeholder="587" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Username</label>
|
||||
<input type="text" x-model="emailForm.smtp_user" placeholder="username" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Password</label>
|
||||
<input type="password" x-model="emailForm.smtp_password" placeholder="Enter new password" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Leave blank to keep existing</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" x-model="emailForm.smtp_use_tls" class="rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Use TLS</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" x-model="emailForm.smtp_use_ssl" class="rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Use SSL</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- SendGrid Settings -->
|
||||
<template x-if="emailForm.provider === 'sendgrid'">
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg mb-6">
|
||||
<h4 class="text-md font-medium text-gray-700 dark:text-gray-300 mb-4">SendGrid Settings</h4>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Key</label>
|
||||
<input type="password" x-model="emailForm.sendgrid_api_key" placeholder="Enter API key" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Leave blank to keep existing</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Mailgun Settings -->
|
||||
<template x-if="emailForm.provider === 'mailgun'">
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg mb-6">
|
||||
<h4 class="text-md font-medium text-gray-700 dark:text-gray-300 mb-4">Mailgun Settings</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Key</label>
|
||||
<input type="password" x-model="emailForm.mailgun_api_key" placeholder="Enter API key" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Leave blank to keep existing</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Domain</label>
|
||||
<input type="text" x-model="emailForm.mailgun_domain" placeholder="mg.yourdomain.com" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- SES Settings -->
|
||||
<template x-if="emailForm.provider === 'ses'">
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg mb-6">
|
||||
<h4 class="text-md font-medium text-gray-700 dark:text-gray-300 mb-4">Amazon SES Settings</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Access Key ID</label>
|
||||
<input type="password" x-model="emailForm.aws_access_key_id" placeholder="Enter access key" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Secret Access Key</label>
|
||||
<input type="password" x-model="emailForm.aws_secret_access_key" placeholder="Enter secret key" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Region</label>
|
||||
<select x-model="emailForm.aws_region" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600">
|
||||
<option value="us-east-1">US East (N. Virginia)</option>
|
||||
<option value="us-west-2">US West (Oregon)</option>
|
||||
<option value="eu-west-1">EU (Ireland)</option>
|
||||
<option value="eu-central-1">EU (Frankfurt)</option>
|
||||
<option value="ap-southeast-1">Asia Pacific (Singapore)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Behavior Settings -->
|
||||
<div class="flex items-center gap-6 mb-6">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" x-model="emailForm.enabled" class="rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Enable email sending</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" x-model="emailForm.debug" class="rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Debug mode (log only, don't send)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="flex items-center justify-end pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
@click="saveEmailSettings()"
|
||||
:disabled="saving"
|
||||
class="px-6 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">Save Email Settings</span>
|
||||
<span x-show="saving">Saving...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Test Email -->
|
||||
<div class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<h4 class="text-md font-medium text-gray-700 dark:text-gray-300 mb-4">Send Test Email</h4>
|
||||
<div class="flex items-center gap-3">
|
||||
<input
|
||||
type="email"
|
||||
x-model="testEmailAddress"
|
||||
placeholder="test@example.com"
|
||||
class="flex-1 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:outline-none focus:ring-2 focus:ring-purple-600"
|
||||
/>
|
||||
<button
|
||||
@click="sendTestEmail()"
|
||||
:disabled="!testEmailAddress || sendingTestEmail"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span x-show="!sendingTestEmail">Send Test</span>
|
||||
<span x-show="sendingTestEmail">Sending...</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Send a test email to verify the platform email configuration is working.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Settings Tab -->
|
||||
<div x-show="activeTab === 'shipping'" x-transition>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
|
||||
@@ -335,3 +335,38 @@
|
||||
<span>Upgrade to <span x-text="$store.upgrade.nextTier?.name"></span></span>
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{# =============================================================================
|
||||
Email Settings Warning
|
||||
Shows warning banner when vendor email settings are not configured.
|
||||
This banner appears at the top of vendor pages until email is configured.
|
||||
|
||||
Usage:
|
||||
{{ email_settings_warning() }}
|
||||
============================================================================= #}
|
||||
{% macro email_settings_warning() %}
|
||||
<div x-data="emailSettingsWarning()"
|
||||
x-show="showWarning"
|
||||
x-cloak
|
||||
class="mb-4 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 justify-between">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-yellow-600 dark:text-yellow-400 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="font-medium text-yellow-800 dark:text-yellow-300">Email not configured</p>
|
||||
<p class="text-sm text-yellow-700 dark:text-yellow-400">
|
||||
Configure your email settings to send order confirmations and customer notifications.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a :href="`/vendor/${vendorCode}/settings?tab=email`"
|
||||
class="ml-4 px-4 py-2 text-sm font-medium text-yellow-800 bg-yellow-200 rounded-lg hover:bg-yellow-300 dark:bg-yellow-800 dark:text-yellow-200 dark:hover:bg-yellow-700 whitespace-nowrap">
|
||||
Configure Email
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Address Modal -->
|
||||
{# noqa: FE-004 - Complex form modal with dynamic title and extensive form fields not suited for form_modal macro #}
|
||||
<div x-show="showAddressModal"
|
||||
x-cloak
|
||||
class="fixed inset-0 z-50 overflow-y-auto"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{# app/templates/shop/account/dashboard.html #}
|
||||
{% extends "shop/base.html" %}
|
||||
{% from 'shared/macros/modals.html' import confirm_modal %}
|
||||
|
||||
{% block title %}My Account - {{ vendor.name }}{% endblock %}
|
||||
|
||||
@@ -117,75 +118,16 @@
|
||||
</div>
|
||||
|
||||
<!-- Logout Confirmation Modal -->
|
||||
<div x-show="showLogoutModal"
|
||||
x-cloak
|
||||
class="fixed inset-0 z-50 overflow-y-auto"
|
||||
aria-labelledby="modal-title"
|
||||
role="dialog"
|
||||
aria-modal="true">
|
||||
<!-- Background overlay -->
|
||||
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!-- Overlay backdrop -->
|
||||
<div x-show="showLogoutModal"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
@click="showLogoutModal = false"
|
||||
class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-75 transition-opacity"
|
||||
aria-hidden="true">
|
||||
</div>
|
||||
|
||||
<!-- Center modal -->
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
|
||||
<!-- Modal panel -->
|
||||
<div x-show="showLogoutModal"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
|
||||
|
||||
<div class="sm:flex sm:items-start">
|
||||
<!-- Icon -->
|
||||
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<span class="h-6 w-6 text-red-600 dark:text-red-400" x-html="$icon('exclamation-triangle', 'h-6 w-6')"></span>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white" id="modal-title">
|
||||
Logout Confirmation
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Are you sure you want to logout? You'll need to sign in again to access your account.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<button @click="confirmLogout()"
|
||||
type="button"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm transition-colors">
|
||||
Logout
|
||||
</button>
|
||||
<button @click="showLogoutModal = false"
|
||||
type="button"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-700 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ confirm_modal(
|
||||
id='logoutModal',
|
||||
title='Logout Confirmation',
|
||||
message="Are you sure you want to logout? You'll need to sign in again to access your account.",
|
||||
confirm_action='confirmLogout()',
|
||||
show_var='showLogoutModal',
|
||||
confirm_text='Logout',
|
||||
cancel_text='Cancel',
|
||||
variant='danger'
|
||||
) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{# noqa: FE-001 - Custom pagination with currentPage/totalPages vars (not pagination.page/pagination.total) #}
|
||||
<template x-if="totalPages > 1">
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 px-6 py-4 flex items-center justify-between">
|
||||
<button @click="prevPage()" :disabled="currentPage === 1"
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
</p>
|
||||
|
||||
{# Quantity Controls #}
|
||||
{# noqa: FE-008 - Custom quantity stepper with async updateQuantity() per-item and :value binding #}
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
{# Add to Cart Section #}
|
||||
<div class="p-6 bg-white dark:bg-gray-800 rounded-lg border-2 border-primary">
|
||||
{# Quantity Selector #}
|
||||
{# noqa: FE-008 - Custom quantity stepper with dynamic product-based min/max and validateQuantity() handler #}
|
||||
<div class="mb-4">
|
||||
<label class="block font-semibold text-lg mb-2">Quantity:</label>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
94
app/templates/vendor/billing.html
vendored
94
app/templates/vendor/billing.html
vendored
@@ -1,16 +1,14 @@
|
||||
{# app/templates/vendor/billing.html #}
|
||||
{% extends "vendor/base.html" %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
{% from 'shared/macros/modals.html' import modal_simple %}
|
||||
|
||||
{% block title %}Billing & Subscription{% endblock %}
|
||||
|
||||
{% block alpine_data %}vendorBilling(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Billing & Subscription
|
||||
</h2>
|
||||
</div>
|
||||
{{ page_header('Billing & Subscription') }}
|
||||
|
||||
<!-- Success/Cancel Messages -->
|
||||
<template x-if="showSuccessMessage">
|
||||
@@ -260,61 +258,43 @@
|
||||
</template>
|
||||
|
||||
<!-- Tiers Modal -->
|
||||
<div x-show="showTiersModal"
|
||||
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 overflow-auto bg-black bg-opacity-50"
|
||||
@click.self="showTiersModal = false">
|
||||
<div class="w-full max-w-4xl mx-4 bg-white dark:bg-gray-800 rounded-lg shadow-xl">
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Choose Your Plan</h3>
|
||||
<button @click="showTiersModal = false" class="text-gray-400 hover:text-gray-600">
|
||||
<span x-html="$icon('x-mark', 'w-6 h-6')"></span>
|
||||
{% call modal_simple('tiersModal', 'Choose Your Plan', show_var='showTiersModal', size='xl') %}
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<template x-for="tier in tiers" :key="tier.code">
|
||||
<div :class="{'ring-2 ring-purple-600': tier.is_current}"
|
||||
class="relative p-6 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<template x-if="tier.is_current">
|
||||
<span class="absolute top-0 right-0 px-2 py-1 text-xs font-semibold text-white bg-purple-600 rounded-bl-lg rounded-tr-lg">Current</span>
|
||||
</template>
|
||||
<h4 class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="tier.name"></h4>
|
||||
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
<span x-text="formatCurrency(tier.price_monthly_cents, 'EUR')"></span>
|
||||
<span class="text-sm font-normal text-gray-500">/mo</span>
|
||||
</p>
|
||||
<ul class="mt-4 space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-center">
|
||||
<span x-html="$icon('check', 'w-4 h-4 text-green-500 mr-2')"></span>
|
||||
<span x-text="tier.orders_per_month ? `${tier.orders_per_month} orders/mo` : 'Unlimited orders'"></span>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<span x-html="$icon('check', 'w-4 h-4 text-green-500 mr-2')"></span>
|
||||
<span x-text="tier.products_limit ? `${tier.products_limit} products` : 'Unlimited products'"></span>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<span x-html="$icon('check', 'w-4 h-4 text-green-500 mr-2')"></span>
|
||||
<span x-text="tier.team_members ? `${tier.team_members} team members` : 'Unlimited team'"></span>
|
||||
</li>
|
||||
</ul>
|
||||
<button @click="selectTier(tier)"
|
||||
:disabled="tier.is_current"
|
||||
:class="tier.is_current ? 'bg-gray-300 cursor-not-allowed' : 'bg-purple-600 hover:bg-purple-700'"
|
||||
class="w-full mt-4 px-4 py-2 text-sm font-medium text-white rounded-lg transition-colors">
|
||||
<span x-text="tier.is_current ? 'Current Plan' : (tier.can_upgrade ? 'Upgrade' : 'Downgrade')"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<template x-for="tier in tiers" :key="tier.code">
|
||||
<div :class="{'ring-2 ring-purple-600': tier.is_current}"
|
||||
class="relative p-6 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<template x-if="tier.is_current">
|
||||
<span class="absolute top-0 right-0 px-2 py-1 text-xs font-semibold text-white bg-purple-600 rounded-bl-lg rounded-tr-lg">Current</span>
|
||||
</template>
|
||||
<h4 class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="tier.name"></h4>
|
||||
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
<span x-text="formatCurrency(tier.price_monthly_cents, 'EUR')"></span>
|
||||
<span class="text-sm font-normal text-gray-500">/mo</span>
|
||||
</p>
|
||||
<ul class="mt-4 space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-center">
|
||||
<span x-html="$icon('check', 'w-4 h-4 text-green-500 mr-2')"></span>
|
||||
<span x-text="tier.orders_per_month ? `${tier.orders_per_month} orders/mo` : 'Unlimited orders'"></span>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<span x-html="$icon('check', 'w-4 h-4 text-green-500 mr-2')"></span>
|
||||
<span x-text="tier.products_limit ? `${tier.products_limit} products` : 'Unlimited products'"></span>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<span x-html="$icon('check', 'w-4 h-4 text-green-500 mr-2')"></span>
|
||||
<span x-text="tier.team_members ? `${tier.team_members} team members` : 'Unlimited team'"></span>
|
||||
</li>
|
||||
</ul>
|
||||
<button @click="selectTier(tier)"
|
||||
:disabled="tier.is_current"
|
||||
:class="tier.is_current ? 'bg-gray-300 cursor-not-allowed' : 'bg-purple-600 hover:bg-purple-700'"
|
||||
class="w-full mt-4 px-4 py-2 text-sm font-medium text-white rounded-lg transition-colors">
|
||||
<span x-text="tier.is_current ? 'Current Plan' : (tier.can_upgrade ? 'Upgrade' : 'Downgrade')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
<!-- Add-ons Modal -->
|
||||
<div x-show="showAddonsModal"
|
||||
|
||||
222
app/templates/vendor/customers.html
vendored
222
app/templates/vendor/customers.html
vendored
@@ -3,6 +3,8 @@
|
||||
{% from 'shared/macros/pagination.html' import pagination %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper %}
|
||||
{% from 'shared/macros/modals.html' import modal_simple %}
|
||||
|
||||
{% block title %}Customers{% endblock %}
|
||||
|
||||
@@ -98,133 +100,123 @@
|
||||
</div>
|
||||
|
||||
<!-- Customers Table -->
|
||||
<div x-show="!loading && !error" class="w-full mb-8 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">Customer</th>
|
||||
<th class="px-4 py-3">Email</th>
|
||||
<th class="px-4 py-3">Joined</th>
|
||||
<th class="px-4 py-3">Orders</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="customer in customers" :key="customer.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400">
|
||||
<!-- Customer Info -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center text-sm">
|
||||
<div class="relative w-10 h-10 mr-3 rounded-full overflow-hidden bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
|
||||
<span class="text-sm font-semibold text-purple-600 dark:text-purple-300" x-text="getInitials(customer)"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold" x-text="`${customer.first_name || ''} ${customer.last_name || ''}`.trim() || 'Unknown'"></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="customer.phone || ''"></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Email -->
|
||||
<td class="px-4 py-3 text-sm" x-text="customer.email || '-'"></td>
|
||||
<!-- Joined -->
|
||||
<td class="px-4 py-3 text-sm" x-text="formatDate(customer.created_at)"></td>
|
||||
<!-- Orders -->
|
||||
<td class="px-4 py-3 text-sm font-semibold" x-text="customer.order_count || 0"></td>
|
||||
<!-- Actions -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<button
|
||||
@click="viewCustomer(customer)"
|
||||
class="p-1 text-gray-500 hover:text-purple-600 dark:text-gray-400 dark:hover:text-purple-400"
|
||||
title="View Details"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="viewCustomerOrders(customer)"
|
||||
class="p-1 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
|
||||
title="View Orders"
|
||||
>
|
||||
<span x-html="$icon('document-text', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="messageCustomer(customer)"
|
||||
class="p-1 text-gray-500 hover:text-green-600 dark:text-gray-400 dark:hover:text-green-400"
|
||||
title="Send Message"
|
||||
>
|
||||
<span x-html="$icon('chat-bubble-left-right', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- Empty State -->
|
||||
<tr x-show="customers.length === 0">
|
||||
<td colspan="5" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('users', 'w-12 h-12 text-gray-300 dark:text-gray-600 mb-4')"></span>
|
||||
<p class="text-lg font-medium">No customers found</p>
|
||||
<p class="text-sm">Customers will appear here when they make purchases</p>
|
||||
<div x-show="!loading && !error" class="mb-8">
|
||||
{% 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">Customer</th>
|
||||
<th class="px-4 py-3">Email</th>
|
||||
<th class="px-4 py-3">Joined</th>
|
||||
<th class="px-4 py-3">Orders</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="customer in customers" :key="customer.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400">
|
||||
<!-- Customer Info -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center text-sm">
|
||||
<div class="relative w-10 h-10 mr-3 rounded-full overflow-hidden bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
|
||||
<span class="text-sm font-semibold text-purple-600 dark:text-purple-300" x-text="getInitials(customer)"></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold" x-text="`${customer.first_name || ''} ${customer.last_name || ''}`.trim() || 'Unknown'"></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="customer.phone || ''"></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Email -->
|
||||
<td class="px-4 py-3 text-sm" x-text="customer.email || '-'"></td>
|
||||
<!-- Joined -->
|
||||
<td class="px-4 py-3 text-sm" x-text="formatDate(customer.created_at)"></td>
|
||||
<!-- Orders -->
|
||||
<td class="px-4 py-3 text-sm font-semibold" x-text="customer.order_count || 0"></td>
|
||||
<!-- Actions -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<button
|
||||
@click="viewCustomer(customer)"
|
||||
class="p-1 text-gray-500 hover:text-purple-600 dark:text-gray-400 dark:hover:text-purple-400"
|
||||
title="View Details"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="viewCustomerOrders(customer)"
|
||||
class="p-1 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
|
||||
title="View Orders"
|
||||
>
|
||||
<span x-html="$icon('document-text', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="messageCustomer(customer)"
|
||||
class="p-1 text-gray-500 hover:text-green-600 dark:text-gray-400 dark:hover:text-green-400"
|
||||
title="Send Message"
|
||||
>
|
||||
<span x-html="$icon('chat-bubble-left-right', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- Empty State -->
|
||||
<tr x-show="customers.length === 0">
|
||||
<td colspan="5" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('users', 'w-12 h-12 text-gray-300 dark:text-gray-600 mb-4')"></span>
|
||||
<p class="text-lg font-medium">No customers found</p>
|
||||
<p class="text-sm">Customers will appear here when they make purchases</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{{ pagination(show_condition="!loading && pagination.total > 0") }}
|
||||
|
||||
<!-- Customer Detail Modal -->
|
||||
<div x-show="showDetailModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50">
|
||||
<div class="w-full max-w-lg bg-white rounded-lg shadow-xl dark:bg-gray-800" @click.away="showDetailModal = false">
|
||||
<div class="flex items-center justify-between p-4 border-b dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Customer Details</h3>
|
||||
<button @click="showDetailModal = 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>
|
||||
{% call modal_simple('customerDetailModal', 'Customer Details', show_var='showDetailModal', size='md') %}
|
||||
<div x-show="selectedCustomer">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-16 h-16 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center mr-4">
|
||||
<span class="text-xl font-semibold text-purple-600 dark:text-purple-300" x-text="getInitials(selectedCustomer)"></span>
|
||||
</div>
|
||||
<div class="p-4" x-show="selectedCustomer">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-16 h-16 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center mr-4">
|
||||
<span class="text-xl font-semibold text-purple-600 dark:text-purple-300" x-text="getInitials(selectedCustomer)"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-semibold text-gray-800 dark:text-gray-200" x-text="`${selectedCustomer?.first_name || ''} ${selectedCustomer?.last_name || ''}`.trim() || 'Unknown'"></p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="selectedCustomer?.email"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Phone</p>
|
||||
<p class="font-medium text-gray-800 dark:text-gray-200" x-text="selectedCustomer?.phone || '-'"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Joined</p>
|
||||
<p class="font-medium text-gray-800 dark:text-gray-200" x-text="formatDate(selectedCustomer?.created_at)"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Total Orders</p>
|
||||
<p class="font-medium text-gray-800 dark:text-gray-200" x-text="selectedCustomer?.order_count || 0"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Total Spent</p>
|
||||
<p class="font-medium text-gray-800 dark:text-gray-200" x-text="formatPrice(selectedCustomer?.total_spent || 0)"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-semibold text-gray-800 dark:text-gray-200" x-text="`${selectedCustomer?.first_name || ''} ${selectedCustomer?.last_name || ''}`.trim() || 'Unknown'"></p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="selectedCustomer?.email"></p>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 p-4 border-t dark:border-gray-700">
|
||||
<button @click="showDetailModal = false" class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
Close
|
||||
</button>
|
||||
<button @click="messageCustomer(selectedCustomer); showDetailModal = false" class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
|
||||
Send Message
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Phone</p>
|
||||
<p class="font-medium text-gray-800 dark:text-gray-200" x-text="selectedCustomer?.phone || '-'"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Joined</p>
|
||||
<p class="font-medium text-gray-800 dark:text-gray-200" x-text="formatDate(selectedCustomer?.created_at)"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Total Orders</p>
|
||||
<p class="font-medium text-gray-800 dark:text-gray-200" x-text="selectedCustomer?.order_count || 0"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Total Spent</p>
|
||||
<p class="font-medium text-gray-800 dark:text-gray-200" x-text="formatPrice(selectedCustomer?.total_spent || 0)"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 pt-4 mt-4 border-t dark:border-gray-700">
|
||||
<button @click="showDetailModal = false" class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
Close
|
||||
</button>
|
||||
<button @click="messageCustomer(selectedCustomer); showDetailModal = false" class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
|
||||
Send Message
|
||||
</button>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
<!-- Customer Orders Modal -->
|
||||
<div x-show="showOrdersModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50">
|
||||
|
||||
5
app/templates/vendor/dashboard.html
vendored
5
app/templates/vendor/dashboard.html
vendored
@@ -8,7 +8,12 @@
|
||||
|
||||
{% block alpine_data %}vendorDashboard(){% endblock %}
|
||||
|
||||
{% from "shared/macros/feature_gate.html" import email_settings_warning %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Email Settings Warning -->
|
||||
{{ email_settings_warning() }}
|
||||
|
||||
<!-- Limit Warnings -->
|
||||
{{ limit_warning("orders") }}
|
||||
{{ limit_warning("products") }}
|
||||
|
||||
1
app/templates/vendor/email-templates.html
vendored
1
app/templates/vendor/email-templates.html
vendored
@@ -34,6 +34,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Templates Table -->
|
||||
{# noqa: FE-005 - Table has custom header section and styling not compatible with table_wrapper #}
|
||||
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 overflow-hidden">
|
||||
<div class="p-4 border-b dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Available Templates</h3>
|
||||
|
||||
186
app/templates/vendor/inventory.html
vendored
186
app/templates/vendor/inventory.html
vendored
@@ -4,6 +4,7 @@
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/modals.html' import modal_simple %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
|
||||
|
||||
{% block title %}Inventory{% endblock %}
|
||||
|
||||
@@ -154,99 +155,97 @@
|
||||
</div>
|
||||
|
||||
<!-- Inventory Table -->
|
||||
<div x-show="!loading && !error" class="w-full mb-8 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 w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="allSelected"
|
||||
:indeterminate="someSelected"
|
||||
@click="toggleSelectAll()"
|
||||
class="w-4 h-4 text-purple-600 rounded focus:ring-purple-500 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
</th>
|
||||
<th class="px-4 py-3">Product</th>
|
||||
<th class="px-4 py-3">SKU</th>
|
||||
<th class="px-4 py-3">Location</th>
|
||||
<th class="px-4 py-3">Quantity</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="item in inventory" :key="item.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400" :class="{'bg-purple-50 dark:bg-purple-900/10': isSelected(item.id)}">
|
||||
<!-- Checkbox -->
|
||||
<td class="px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isSelected(item.id)"
|
||||
@click="toggleSelect(item.id)"
|
||||
class="w-4 h-4 text-purple-600 rounded focus:ring-purple-500 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
</td>
|
||||
<!-- Product -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="text-sm">
|
||||
<p class="font-semibold" x-text="item.product_name || 'Unknown Product'"></p>
|
||||
</div>
|
||||
</td>
|
||||
<!-- SKU -->
|
||||
<td class="px-4 py-3 text-sm font-mono" x-text="item.product_sku || '-'"></td>
|
||||
<!-- Location -->
|
||||
<td class="px-4 py-3 text-sm" x-text="item.location || 'Default'"></td>
|
||||
<!-- Quantity -->
|
||||
<td class="px-4 py-3 text-sm font-semibold" x-text="formatNumber(item.quantity)"></td>
|
||||
<!-- Status -->
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span
|
||||
:class="{
|
||||
'px-2 py-1 font-semibold leading-tight rounded-full': true,
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': getStockStatus(item) === 'ok',
|
||||
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': getStockStatus(item) === 'low',
|
||||
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': getStockStatus(item) === 'out'
|
||||
}"
|
||||
>
|
||||
<span x-text="getStockStatus(item) === 'out' ? 'Out of Stock' : (getStockStatus(item) === 'low' ? 'Low Stock' : 'In Stock')"></span>
|
||||
</span>
|
||||
</td>
|
||||
<!-- Actions -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<button
|
||||
@click="openAdjustModal(item)"
|
||||
class="p-1 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
|
||||
title="Adjust Stock"
|
||||
>
|
||||
<span x-html="$icon('plus-minus', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="openSetModal(item)"
|
||||
class="p-1 text-gray-500 hover:text-purple-600 dark:text-gray-400 dark:hover:text-purple-400"
|
||||
title="Set Quantity"
|
||||
>
|
||||
<span x-html="$icon('pencil', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- Empty State -->
|
||||
<tr x-show="inventory.length === 0">
|
||||
<td colspan="7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('archive', 'w-12 h-12 text-gray-300 dark:text-gray-600 mb-4')"></span>
|
||||
<p class="text-lg font-medium">No inventory found</p>
|
||||
<p class="text-sm">Add products and set their stock levels</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div x-show="!loading && !error" class="mb-8">
|
||||
{% 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 w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="allSelected"
|
||||
:indeterminate="someSelected"
|
||||
@click="toggleSelectAll()"
|
||||
class="w-4 h-4 text-purple-600 rounded focus:ring-purple-500 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
</th>
|
||||
<th class="px-4 py-3">Product</th>
|
||||
<th class="px-4 py-3">SKU</th>
|
||||
<th class="px-4 py-3">Location</th>
|
||||
<th class="px-4 py-3">Quantity</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="item in inventory" :key="item.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400" :class="{'bg-purple-50 dark:bg-purple-900/10': isSelected(item.id)}">
|
||||
<!-- Checkbox -->
|
||||
<td class="px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isSelected(item.id)"
|
||||
@click="toggleSelect(item.id)"
|
||||
class="w-4 h-4 text-purple-600 rounded focus:ring-purple-500 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
</td>
|
||||
<!-- Product -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="text-sm">
|
||||
<p class="font-semibold" x-text="item.product_name || 'Unknown Product'"></p>
|
||||
</div>
|
||||
</td>
|
||||
<!-- SKU -->
|
||||
<td class="px-4 py-3 text-sm font-mono" x-text="item.product_sku || '-'"></td>
|
||||
<!-- Location -->
|
||||
<td class="px-4 py-3 text-sm" x-text="item.location || 'Default'"></td>
|
||||
<!-- Quantity -->
|
||||
<td class="px-4 py-3 text-sm font-semibold" x-text="formatNumber(item.quantity)"></td>
|
||||
<!-- Status -->
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span
|
||||
:class="{
|
||||
'px-2 py-1 font-semibold leading-tight rounded-full': true,
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': getStockStatus(item) === 'ok',
|
||||
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': getStockStatus(item) === 'low',
|
||||
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': getStockStatus(item) === 'out'
|
||||
}"
|
||||
>
|
||||
<span x-text="getStockStatus(item) === 'out' ? 'Out of Stock' : (getStockStatus(item) === 'low' ? 'Low Stock' : 'In Stock')"></span>
|
||||
</span>
|
||||
</td>
|
||||
<!-- Actions -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<button
|
||||
@click="openAdjustModal(item)"
|
||||
class="p-1 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
|
||||
title="Adjust Stock"
|
||||
>
|
||||
<span x-html="$icon('plus-minus', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="openSetModal(item)"
|
||||
class="p-1 text-gray-500 hover:text-purple-600 dark:text-gray-400 dark:hover:text-purple-400"
|
||||
title="Set Quantity"
|
||||
>
|
||||
<span x-html="$icon('pencil', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- Empty State -->
|
||||
<tr x-show="inventory.length === 0">
|
||||
<td colspan="7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('archive', 'w-12 h-12 text-gray-300 dark:text-gray-600 mb-4')"></span>
|
||||
<p class="text-lg font-medium">No inventory found</p>
|
||||
<p class="text-sm">Add products and set their stock levels</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@@ -263,6 +262,7 @@
|
||||
</p>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Adjustment (+ or -)</label>
|
||||
{# noqa: FE-008 - Adjustment input accepts +/- values, not a quantity stepper #}
|
||||
<input
|
||||
type="number"
|
||||
x-model.number="adjustForm.quantity"
|
||||
|
||||
209
app/templates/vendor/invoices.html
vendored
209
app/templates/vendor/invoices.html
vendored
@@ -1,6 +1,10 @@
|
||||
{# app/templates/vendor/invoices.html #}
|
||||
{% extends "vendor/base.html" %}
|
||||
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper, table_header, simple_pagination %}
|
||||
{% from 'shared/macros/modals.html' import form_modal %}
|
||||
|
||||
{% block title %}Invoices{% endblock %}
|
||||
|
||||
{% block alpine_data %}vendorInvoices(){% endblock %}
|
||||
@@ -11,36 +15,18 @@
|
||||
|
||||
{% 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">
|
||||
Invoices
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Create and manage invoices for your orders
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="openCreateModal()"
|
||||
:disabled="!hasSettings"
|
||||
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"
|
||||
:title="!hasSettings ? 'Configure invoice settings first' : 'Create new invoice'"
|
||||
>
|
||||
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
|
||||
Create Invoice
|
||||
</button>
|
||||
<button
|
||||
@click="refreshData()"
|
||||
:disabled="loading"
|
||||
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="!loading" x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="loading" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% call page_header_flex(title='Invoices', subtitle='Create and manage invoices for your orders') %}
|
||||
<button
|
||||
@click="openCreateModal()"
|
||||
:disabled="!hasSettings"
|
||||
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"
|
||||
:title="!hasSettings ? 'Configure invoice settings first' : 'Create new invoice'"
|
||||
>
|
||||
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
|
||||
Create Invoice
|
||||
</button>
|
||||
{{ refresh_button(loading_var='loading', onclick='refreshData()', variant='secondary') }}
|
||||
{% endcall %}
|
||||
|
||||
<!-- Success Message -->
|
||||
<div x-show="successMessage" x-transition class="mb-6 p-4 bg-green-100 dark:bg-green-900/30 border border-green-400 dark:border-green-600 text-green-700 dark:text-green-300 rounded-lg flex items-start">
|
||||
@@ -54,6 +40,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
{# noqa: FE-003 - Uses dismissible close button not supported by error_state macro #}
|
||||
<div x-show="error" x-transition class="mb-6 p-4 bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-600 text-red-700 dark:text-red-300 rounded-lg flex items-start">
|
||||
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
|
||||
<div>
|
||||
@@ -169,20 +156,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Invoices 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">Invoice #</th>
|
||||
<th class="px-4 py-3">Customer</th>
|
||||
<th class="px-4 py-3">Date</th>
|
||||
<th class="px-4 py-3">Amount</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">
|
||||
{% call table_wrapper() %}
|
||||
{{ table_header(['Invoice #', 'Customer', 'Date', 'Amount', 'Status', 'Actions']) }}
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-if="loading && invoices.length === 0">
|
||||
<tr>
|
||||
<td colspan="6" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
@@ -269,41 +245,11 @@
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<div x-show="totalInvoices > perPage" class="grid px-4 py-3 text-xs font-semibold tracking-wide text-gray-500 uppercase border-t dark:border-gray-700 bg-gray-50 sm:grid-cols-9 dark:text-gray-400 dark:bg-gray-800">
|
||||
<span class="flex items-center col-span-3">
|
||||
Showing <span x-text="((page - 1) * perPage) + 1" class="mx-1"></span>-<span x-text="Math.min(page * perPage, totalInvoices)" class="mx-1"></span> of <span x-text="totalInvoices" class="mx-1"></span>
|
||||
</span>
|
||||
<span class="col-span-2"></span>
|
||||
<span class="flex col-span-4 mt-2 sm:mt-auto sm:justify-end">
|
||||
<nav aria-label="Table navigation">
|
||||
<ul class="inline-flex items-center">
|
||||
<li>
|
||||
<button
|
||||
@click="page--; loadInvoices()"
|
||||
:disabled="page <= 1"
|
||||
class="px-3 py-1 rounded-md rounded-l-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
|
||||
>
|
||||
<span x-html="$icon('chevron-left', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
@click="page++; loadInvoices()"
|
||||
:disabled="page * perPage >= totalInvoices"
|
||||
class="px-3 py-1 rounded-md rounded-r-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
|
||||
>
|
||||
<span x-html="$icon('chevron-right', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
|
||||
<!-- Pagination -->
|
||||
{{ simple_pagination(page_var='page', total_var='totalInvoices', limit_var='perPage', on_change='loadInvoices()') }}
|
||||
</div>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
@@ -522,81 +468,34 @@
|
||||
</div>
|
||||
|
||||
<!-- Create Invoice Modal -->
|
||||
<div
|
||||
x-show="showCreateModal"
|
||||
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="showCreateModal = false"
|
||||
>
|
||||
<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-md"
|
||||
@click.stop
|
||||
>
|
||||
<header class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Create Invoice</h3>
|
||||
<button @click="showCreateModal = false" class="text-gray-400 hover:text-gray-600">
|
||||
<span x-html="$icon('x', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<form @submit.prevent="createInvoice()">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Order ID <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
x-model="createForm.order_id"
|
||||
required
|
||||
placeholder="Enter order ID"
|
||||
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">
|
||||
Enter the order ID to generate an invoice for
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Notes (Optional)
|
||||
</label>
|
||||
<textarea
|
||||
x-model="createForm.notes"
|
||||
rows="3"
|
||||
placeholder="Any additional notes for the invoice..."
|
||||
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"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="showCreateModal = 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="creatingInvoice"
|
||||
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="creatingInvoice" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="creatingInvoice ? 'Creating...' : 'Create Invoice'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% call form_modal('createInvoiceModal', 'Create Invoice', show_var='showCreateModal', submit_action='createInvoice()', submit_text='Create Invoice', loading_var='creatingInvoice', loading_text='Creating...', size='sm') %}
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Order ID <span class="text-red-500">*</span>
|
||||
</label>
|
||||
{# noqa: FE-008 - Order ID is a reference field, not a quantity stepper #}
|
||||
<input
|
||||
type="number"
|
||||
x-model="createForm.order_id"
|
||||
required
|
||||
placeholder="Enter order ID"
|
||||
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">
|
||||
Enter the order ID to generate an invoice for
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Notes (Optional)
|
||||
</label>
|
||||
<textarea
|
||||
x-model="createForm.notes"
|
||||
rows="3"
|
||||
placeholder="Any additional notes for the invoice..."
|
||||
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"
|
||||
></textarea>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
248
app/templates/vendor/letzshop.html
vendored
248
app/templates/vendor/letzshop.html
vendored
@@ -1,5 +1,8 @@
|
||||
{# app/templates/vendor/letzshop.html #}
|
||||
{% extends "vendor/base.html" %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
|
||||
{% from 'shared/macros/modals.html' import form_modal %}
|
||||
|
||||
{% block title %}Letzshop Orders{% endblock %}
|
||||
|
||||
@@ -11,36 +14,26 @@
|
||||
|
||||
{% 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">
|
||||
Letzshop Orders
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Manage orders from Letzshop marketplace
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="importOrders()"
|
||||
:disabled="!status.is_configured || importing"
|
||||
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="!importing" x-html="$icon('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 Orders'"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="refreshData()"
|
||||
:disabled="loading"
|
||||
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="!loading" x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="loading" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% call page_header_flex(title='Letzshop Orders', subtitle='Manage orders from Letzshop marketplace') %}
|
||||
<button
|
||||
@click="importOrders()"
|
||||
:disabled="!status.is_configured || importing"
|
||||
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="!importing" x-html="$icon('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 Orders'"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="refreshData()"
|
||||
:disabled="loading"
|
||||
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="!loading" x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="loading" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
Refresh
|
||||
</button>
|
||||
{% endcall %}
|
||||
|
||||
<!-- Success Message -->
|
||||
<div x-show="successMessage" x-transition class="mb-6 p-4 bg-green-100 dark:bg-green-900/30 border border-green-400 dark:border-green-600 text-green-700 dark:text-green-300 rounded-lg flex items-start">
|
||||
@@ -53,6 +46,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# noqa: FE-003 - Custom dismissible error with dark mode support not available in error_state macro #}
|
||||
<!-- Error Message -->
|
||||
<div x-show="error" x-transition class="mb-6 p-4 bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-600 text-red-700 dark:text-red-300 rounded-lg flex items-start">
|
||||
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
|
||||
@@ -167,19 +161,8 @@
|
||||
</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 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>
|
||||
{% call table_wrapper() %}
|
||||
{{ table_header(['Order', 'Customer', 'Total', 'Status', 'Date', 'Actions']) }}
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-if="loading && orders.length === 0">
|
||||
<tr>
|
||||
@@ -268,39 +251,38 @@
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<div x-show="totalOrders > limit" class="grid px-4 py-3 text-xs font-semibold tracking-wide text-gray-500 uppercase border-t dark:border-gray-700 bg-gray-50 sm:grid-cols-9 dark:text-gray-400 dark:bg-gray-800">
|
||||
<span class="flex items-center col-span-3">
|
||||
Showing <span x-text="((page - 1) * limit) + 1"></span>-<span x-text="Math.min(page * limit, totalOrders)"></span> of <span x-text="totalOrders"></span>
|
||||
</span>
|
||||
<span class="col-span-2"></span>
|
||||
<span class="flex col-span-4 mt-2 sm:mt-auto sm:justify-end">
|
||||
<nav aria-label="Table navigation">
|
||||
<ul class="inline-flex items-center">
|
||||
<li>
|
||||
<button
|
||||
@click="page--; loadOrders()"
|
||||
:disabled="page <= 1"
|
||||
class="px-3 py-1 rounded-md rounded-l-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
|
||||
>
|
||||
<span x-html="$icon('chevron-left', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
@click="page++; loadOrders()"
|
||||
:disabled="page * limit >= totalOrders"
|
||||
class="px-3 py-1 rounded-md rounded-r-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
|
||||
>
|
||||
<span x-html="$icon('chevron-right', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</span>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{# noqa: FE-001 - Uses flat variables (page, limit, totalOrders) instead of pagination object expected by macro #}
|
||||
<!-- Pagination -->
|
||||
<div x-show="totalOrders > limit" class="grid px-4 py-3 text-xs font-semibold tracking-wide text-gray-500 uppercase border-t dark:border-gray-700 bg-gray-50 sm:grid-cols-9 dark:text-gray-400 dark:bg-gray-800">
|
||||
<span class="flex items-center col-span-3">
|
||||
Showing <span x-text="((page - 1) * limit) + 1"></span>-<span x-text="Math.min(page * limit, totalOrders)"></span> of <span x-text="totalOrders"></span>
|
||||
</span>
|
||||
<span class="col-span-2"></span>
|
||||
<span class="flex col-span-4 mt-2 sm:mt-auto sm:justify-end">
|
||||
<nav aria-label="Table navigation">
|
||||
<ul class="inline-flex items-center">
|
||||
<li>
|
||||
<button
|
||||
@click="page--; loadOrders()"
|
||||
:disabled="page <= 1"
|
||||
class="px-3 py-1 rounded-md rounded-l-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
|
||||
>
|
||||
<span x-html="$icon('chevron-left', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
@click="page++; loadOrders()"
|
||||
:disabled="page * limit >= totalOrders"
|
||||
class="px-3 py-1 rounded-md rounded-r-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
|
||||
>
|
||||
<span x-html="$icon('chevron-right', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -582,88 +564,40 @@
|
||||
</div>
|
||||
|
||||
<!-- Tracking Modal -->
|
||||
<div
|
||||
x-show="showTrackingModal"
|
||||
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="showTrackingModal = false"
|
||||
>
|
||||
<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-md"
|
||||
@click.stop
|
||||
>
|
||||
<header class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Set Tracking Information</h3>
|
||||
<button @click="showTrackingModal = false" class="text-gray-400 hover:text-gray-600">
|
||||
<span x-html="$icon('x', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<form @submit.prevent="submitTracking()">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Tracking Number <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="trackingForm.tracking_number"
|
||||
required
|
||||
placeholder="1Z999AA10123456784"
|
||||
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 class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Carrier <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
x-model="trackingForm.tracking_carrier"
|
||||
required
|
||||
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="dhl">DHL</option>
|
||||
<option value="ups">UPS</option>
|
||||
<option value="fedex">FedEx</option>
|
||||
<option value="post_lu">Post Luxembourg</option>
|
||||
<option value="dpd">DPD</option>
|
||||
<option value="gls">GLS</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="showTrackingModal = 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="submittingTracking"
|
||||
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="submittingTracking" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="submittingTracking ? 'Saving...' : 'Save Tracking'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% call form_modal('trackingModal', 'Set Tracking Information', show_var='showTrackingModal', submit_action='submitTracking()', submit_text='Save Tracking', loading_var='submittingTracking', loading_text='Saving...', size='sm') %}
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Tracking Number <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="trackingForm.tracking_number"
|
||||
required
|
||||
placeholder="1Z999AA10123456784"
|
||||
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>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
|
||||
Carrier <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
x-model="trackingForm.tracking_carrier"
|
||||
required
|
||||
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="dhl">DHL</option>
|
||||
<option value="ups">UPS</option>
|
||||
<option value="fedex">FedEx</option>
|
||||
<option value="post_lu">Post Luxembourg</option>
|
||||
<option value="dpd">DPD</option>
|
||||
<option value="gls">GLS</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
<!-- Order Details Modal -->
|
||||
<div
|
||||
|
||||
180
app/templates/vendor/marketplace.html
vendored
180
app/templates/vendor/marketplace.html
vendored
@@ -1,6 +1,10 @@
|
||||
{# app/templates/vendor/marketplace.html #}
|
||||
{% extends "vendor/base.html" %}
|
||||
{% from 'shared/macros/inputs.html' import number_stepper %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/alerts.html' import error_state %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper, table_header, simple_pagination %}
|
||||
{% from 'shared/macros/modals.html' import job_details_modal %}
|
||||
|
||||
{% block title %}Marketplace Import{% endblock %}
|
||||
|
||||
@@ -12,25 +16,9 @@
|
||||
|
||||
{% 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">
|
||||
Marketplace Import
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Import products from Letzshop marketplace CSV feeds
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@click="refreshJobs()"
|
||||
:disabled="loading"
|
||||
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="!loading" x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="loading" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="loading ? 'Loading...' : 'Refresh'"></span>
|
||||
</button>
|
||||
</div>
|
||||
{% call page_header_flex(title='Marketplace Import', subtitle='Import products from Letzshop marketplace CSV feeds') %}
|
||||
{{ refresh_button(loading_var='loading', onclick='refreshJobs()') }}
|
||||
{% endcall %}
|
||||
|
||||
<!-- Success Message -->
|
||||
<div x-show="successMessage" x-transition class="mb-6 p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg flex items-start">
|
||||
@@ -41,13 +29,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div x-show="error" x-transition 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</p>
|
||||
<p class="text-sm" x-text="error"></p>
|
||||
</div>
|
||||
</div>
|
||||
{{ error_state(title='Error', error_var='error', show_condition='error') }}
|
||||
|
||||
<!-- Import Form Card -->
|
||||
<div class="mb-8 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
@@ -194,88 +176,77 @@
|
||||
</div>
|
||||
|
||||
<!-- Jobs Table -->
|
||||
<div x-show="!loading && jobs.length > 0" 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">Job ID</th>
|
||||
<th class="px-4 py-3">Marketplace</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3">Progress</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>
|
||||
<div x-show="!loading && jobs.length > 0">
|
||||
{% call table_wrapper() %}
|
||||
{{ table_header(['Job ID', 'Marketplace', 'Status', 'Progress', 'Started', 'Duration', 'Actions']) }}
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-for="job in jobs" :key="job.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400">
|
||||
<td class="px-4 py-3 text-sm">
|
||||
#<span x-text="job.id"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="job.marketplace"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full text-xs"
|
||||
:class="{
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': job.status === 'completed',
|
||||
'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100': job.status === 'processing',
|
||||
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': job.status === 'pending',
|
||||
'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'
|
||||
}"
|
||||
x-text="job.status.replace('_', ' ').toUpperCase()">
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<div class="space-y-1">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<span class="text-green-600 dark:text-green-400" x-text="job.imported_count"></span> imported,
|
||||
<span class="text-blue-600 dark:text-blue-400" x-text="job.updated_count"></span> updated
|
||||
</div>
|
||||
<div x-show="job.error_count > 0" class="text-xs text-red-600 dark:text-red-400">
|
||||
<span x-text="job.error_count"></span> errors
|
||||
</div>
|
||||
<div x-show="job.total_processed > 0" class="text-xs text-gray-500 dark:text-gray-500">
|
||||
Total: <span x-text="job.total_processed"></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="job.started_at ? formatDate(job.started_at) : 'Not started'"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="calculateDuration(job)"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
@click="viewJobDetails(job.id)"
|
||||
class="flex items-center justify-between px-2 py-1 text-xs font-medium leading-5 text-purple-600 rounded-lg dark:text-gray-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>
|
||||
</button>
|
||||
<button
|
||||
x-show="job.status === 'processing' || job.status === 'pending'"
|
||||
@click="refreshJobStatus(job.id)"
|
||||
class="flex items-center justify-between px-2 py-1 text-xs font-medium leading-5 text-blue-600 rounded-lg dark:text-gray-400 focus:outline-none hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
title="Refresh Status"
|
||||
>
|
||||
<span x-html="$icon('refresh', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-for="job in jobs" :key="job.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400">
|
||||
<td class="px-4 py-3 text-sm">
|
||||
#<span x-text="job.id"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="job.marketplace"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full text-xs"
|
||||
:class="{
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': job.status === 'completed',
|
||||
'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100': job.status === 'processing',
|
||||
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': job.status === 'pending',
|
||||
'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'
|
||||
}"
|
||||
x-text="job.status.replace('_', ' ').toUpperCase()">
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<div class="space-y-1">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<span class="text-green-600 dark:text-green-400" x-text="job.imported_count"></span> imported,
|
||||
<span class="text-blue-600 dark:text-blue-400" x-text="job.updated_count"></span> updated
|
||||
</div>
|
||||
<div x-show="job.error_count > 0" class="text-xs text-red-600 dark:text-red-400">
|
||||
<span x-text="job.error_count"></span> errors
|
||||
</div>
|
||||
<div x-show="job.total_processed > 0" class="text-xs text-gray-500 dark:text-gray-500">
|
||||
Total: <span x-text="job.total_processed"></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="job.started_at ? formatDate(job.started_at) : 'Not started'"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="calculateDuration(job)"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
@click="viewJobDetails(job.id)"
|
||||
class="flex items-center justify-between px-2 py-1 text-xs font-medium leading-5 text-purple-600 rounded-lg dark:text-gray-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>
|
||||
</button>
|
||||
<button
|
||||
x-show="job.status === 'processing' || job.status === 'pending'"
|
||||
@click="refreshJobStatus(job.id)"
|
||||
class="flex items-center justify-between px-2 py-1 text-xs font-medium leading-5 text-blue-600 rounded-lg dark:text-gray-400 focus:outline-none hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
title="Refresh Status"
|
||||
>
|
||||
<span x-html="$icon('refresh', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{# noqa: FE-001 - Custom pagination with text buttons and totalJobs variable #}
|
||||
<div x-show="!loading && totalJobs > limit" class="px-4 py-3 border-t dark:border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-gray-700 dark:text-gray-400">
|
||||
@@ -304,6 +275,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# noqa: FE-004 - Custom modal with different field names (imported_count vs imported) #}
|
||||
<!-- Job Details Modal -->
|
||||
<div x-show="showJobModal"
|
||||
x-cloak
|
||||
|
||||
118
app/templates/vendor/messages.html
vendored
118
app/templates/vendor/messages.html
vendored
@@ -2,6 +2,7 @@
|
||||
{% extends "vendor/base.html" %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/modals.html' import modal_simple %}
|
||||
|
||||
{% block title %}Messages{% endblock %}
|
||||
|
||||
@@ -218,72 +219,59 @@
|
||||
</div>
|
||||
|
||||
<!-- Compose Modal -->
|
||||
<div x-show="showComposeModal"
|
||||
x-cloak
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
@keydown.escape.window="showComposeModal = false">
|
||||
<div class="absolute inset-0 bg-black bg-opacity-50" @click="showComposeModal = false"></div>
|
||||
<div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-lg mx-4">
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">New Conversation</h3>
|
||||
<button @click="showComposeModal = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<span x-html="$icon('x-mark', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="createConversation()" class="p-6 space-y-4">
|
||||
<!-- Customer -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Customer</label>
|
||||
<select
|
||||
x-model="compose.recipientId"
|
||||
class="w-full px-3 py-2 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"
|
||||
>
|
||||
<option value="">Select customer...</option>
|
||||
<template x-for="r in recipients" :key="r.id">
|
||||
<option :value="r.id" x-text="r.name + ' - ' + (r.email || '')"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Subject -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Subject</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="compose.subject"
|
||||
placeholder="What is this about?"
|
||||
class="w-full px-3 py-2 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>
|
||||
|
||||
<!-- Message -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Message</label>
|
||||
<textarea
|
||||
x-model="compose.message"
|
||||
rows="4"
|
||||
placeholder="Type your message..."
|
||||
class="w-full px-3 py-2 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 resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<button type="button" @click="showComposeModal = false"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit"
|
||||
:disabled="!compose.recipientId || !compose.subject.trim() || creatingConversation"
|
||||
class="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="!creatingConversation">Start Conversation</span>
|
||||
<span x-show="creatingConversation">Creating...</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% call modal_simple('composeMessageModal', 'New Conversation', show_var='showComposeModal', size='md') %}
|
||||
<form @submit.prevent="createConversation()" class="space-y-4">
|
||||
<!-- Customer -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Customer</label>
|
||||
<select
|
||||
x-model="compose.recipientId"
|
||||
class="w-full px-3 py-2 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"
|
||||
>
|
||||
<option value="">Select customer...</option>
|
||||
<template x-for="r in recipients" :key="r.id">
|
||||
<option :value="r.id" x-text="r.name + ' - ' + (r.email || '')"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subject -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Subject</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="compose.subject"
|
||||
placeholder="What is this about?"
|
||||
class="w-full px-3 py-2 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>
|
||||
|
||||
<!-- Message -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Message</label>
|
||||
<textarea
|
||||
x-model="compose.message"
|
||||
rows="4"
|
||||
placeholder="Type your message..."
|
||||
class="w-full px-3 py-2 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 resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end gap-3 pt-4 border-t dark:border-gray-700">
|
||||
<button type="button" @click="showComposeModal = false"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit"
|
||||
:disabled="!compose.recipientId || !compose.subject.trim() || creatingConversation"
|
||||
class="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="!creatingConversation">Start Conversation</span>
|
||||
<span x-show="creatingConversation">Creating...</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
|
||||
87
app/templates/vendor/notifications.html
vendored
87
app/templates/vendor/notifications.html
vendored
@@ -3,6 +3,7 @@
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/pagination.html' import pagination_simple %}
|
||||
{% from 'shared/macros/modals.html' import modal_simple %}
|
||||
|
||||
{% block title %}Notifications{% endblock %}
|
||||
|
||||
@@ -180,56 +181,48 @@
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div x-show="showSettingsModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50">
|
||||
<div class="w-full max-w-md bg-white rounded-lg shadow-xl dark:bg-gray-800" @click.away="showSettingsModal = false">
|
||||
<div class="flex items-center justify-between p-4 border-b dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Notification Settings</h3>
|
||||
<button @click="showSettingsModal = 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>
|
||||
<div class="p-4 space-y-4">
|
||||
<!-- Email Notifications -->
|
||||
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<div>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">Email Notifications</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Receive notifications via email</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" x-model="settingsForm.email_notifications" class="sr-only peer" />
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-300 dark:peer-focus:ring-purple-800 rounded-full peer dark:bg-gray-600 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-500 peer-checked:bg-purple-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- In-App Notifications -->
|
||||
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<div>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">In-App Notifications</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Show notifications in the dashboard</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" x-model="settingsForm.in_app_notifications" class="sr-only peer" />
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-300 dark:peer-focus:ring-purple-800 rounded-full peer dark:bg-gray-600 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-500 peer-checked:bg-purple-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
Note: Full notification settings management coming soon.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 p-4 border-t dark:border-gray-700">
|
||||
<button @click="showSettingsModal = false" class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveSettings()"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700"
|
||||
>
|
||||
Save Settings
|
||||
</button>
|
||||
{% call modal_simple('notificationSettingsModal', 'Notification Settings', show_var='showSettingsModal', size='md') %}
|
||||
<div class="space-y-4">
|
||||
<!-- Email Notifications -->
|
||||
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<div>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">Email Notifications</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Receive notifications via email</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" x-model="settingsForm.email_notifications" class="sr-only peer" />
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-300 dark:peer-focus:ring-purple-800 rounded-full peer dark:bg-gray-600 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-500 peer-checked:bg-purple-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- In-App Notifications -->
|
||||
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<div>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">In-App Notifications</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Show notifications in the dashboard</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" x-model="settingsForm.in_app_notifications" class="sr-only peer" />
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-300 dark:peer-focus:ring-purple-800 rounded-full peer dark:bg-gray-600 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-500 peer-checked:bg-purple-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
Note: Full notification settings management coming soon.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 pt-4 border-t dark:border-gray-700">
|
||||
<button @click="showSettingsModal = false" class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveSettings()"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700"
|
||||
>
|
||||
Save Settings
|
||||
</button>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
|
||||
2
app/templates/vendor/onboarding.html
vendored
2
app/templates/vendor/onboarding.html
vendored
@@ -31,6 +31,7 @@
|
||||
</button>
|
||||
|
||||
<!-- Language Selector -->
|
||||
{# noqa: FE-006 - Custom language selector with flags, not suited for dropdown macro #}
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
<button @click="open = !open"
|
||||
class="flex items-center space-x-2 px-3 py-2 rounded-lg bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors">
|
||||
@@ -280,6 +281,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 dark:text-gray-400" x-text="t('step3.preorder_days')"></label>
|
||||
{# noqa: FE-008 - Simple number input, not a quantity stepper pattern #}
|
||||
<input type="number" x-model="formData.preorder_days" min="0" max="30"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-purple-500 focus:ring-purple-500" />
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400" x-text="t('step3.preorder_days_help')"></p>
|
||||
|
||||
197
app/templates/vendor/orders.html
vendored
197
app/templates/vendor/orders.html
vendored
@@ -4,6 +4,7 @@
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/modals.html' import modal_simple %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper %}
|
||||
|
||||
{% block title %}Orders{% endblock %}
|
||||
|
||||
@@ -160,105 +161,103 @@
|
||||
</div>
|
||||
|
||||
<!-- Orders Table -->
|
||||
<div x-show="!loading && !error" class="w-full mb-8 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 w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="allSelected"
|
||||
:indeterminate="someSelected"
|
||||
@click="toggleSelectAll()"
|
||||
class="w-4 h-4 text-purple-600 rounded focus:ring-purple-500 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
</th>
|
||||
<th class="px-4 py-3">Order #</th>
|
||||
<th class="px-4 py-3">Customer</th>
|
||||
<th class="px-4 py-3">Date</th>
|
||||
<th class="px-4 py-3">Total</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="order in orders" :key="order.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400" :class="{'bg-purple-50 dark:bg-purple-900/10': isSelected(order.id)}">
|
||||
<!-- Checkbox -->
|
||||
<td class="px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isSelected(order.id)"
|
||||
@click="toggleSelect(order.id)"
|
||||
class="w-4 h-4 text-purple-600 rounded focus:ring-purple-500 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
</td>
|
||||
<!-- Order Number -->
|
||||
<td class="px-4 py-3">
|
||||
<span class="font-mono font-semibold" x-text="order.order_number || `#${order.id}`"></span>
|
||||
</td>
|
||||
<!-- Customer -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="text-sm">
|
||||
<p class="font-medium" x-text="order.customer_name || 'Guest'"></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="order.customer_email || ''"></p>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Date -->
|
||||
<td class="px-4 py-3 text-sm" x-text="formatDate(order.created_at)"></td>
|
||||
<!-- Total -->
|
||||
<td class="px-4 py-3 text-sm font-semibold" x-text="formatPrice(order.total)"></td>
|
||||
<!-- Status -->
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span
|
||||
:class="{
|
||||
'px-2 py-1 font-semibold leading-tight rounded-full': true,
|
||||
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': getStatusColor(order.status) === 'yellow',
|
||||
'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100': getStatusColor(order.status) === 'blue',
|
||||
'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100': getStatusColor(order.status) === 'orange',
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': getStatusColor(order.status) === 'green',
|
||||
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': getStatusColor(order.status) === 'red',
|
||||
'text-indigo-700 bg-indigo-100 dark:bg-indigo-700 dark:text-indigo-100': getStatusColor(order.status) === 'indigo',
|
||||
'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100': getStatusColor(order.status) === 'gray'
|
||||
}"
|
||||
x-text="getStatusLabel(order.status)"
|
||||
></span>
|
||||
</td>
|
||||
<!-- Actions -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<button
|
||||
@click="viewOrder(order)"
|
||||
class="p-1 text-gray-500 hover:text-purple-600 dark:text-gray-400 dark:hover:text-purple-400"
|
||||
title="View Details"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="openStatusModal(order)"
|
||||
class="p-1 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
|
||||
title="Update Status"
|
||||
>
|
||||
<span x-html="$icon('pencil-square', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- Empty State -->
|
||||
<tr x-show="orders.length === 0">
|
||||
<td colspan="7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('document-text', 'w-12 h-12 text-gray-300 dark:text-gray-600 mb-4')"></span>
|
||||
<p class="text-lg font-medium">No orders found</p>
|
||||
<p class="text-sm">Orders will appear here when customers make purchases</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div x-show="!loading && !error" class="mb-8">
|
||||
{% 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 w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="allSelected"
|
||||
:indeterminate="someSelected"
|
||||
@click="toggleSelectAll()"
|
||||
class="w-4 h-4 text-purple-600 rounded focus:ring-purple-500 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
</th>
|
||||
<th class="px-4 py-3">Order #</th>
|
||||
<th class="px-4 py-3">Customer</th>
|
||||
<th class="px-4 py-3">Date</th>
|
||||
<th class="px-4 py-3">Total</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="order in orders" :key="order.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400" :class="{'bg-purple-50 dark:bg-purple-900/10': isSelected(order.id)}">
|
||||
<!-- Checkbox -->
|
||||
<td class="px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isSelected(order.id)"
|
||||
@click="toggleSelect(order.id)"
|
||||
class="w-4 h-4 text-purple-600 rounded focus:ring-purple-500 dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
</td>
|
||||
<!-- Order Number -->
|
||||
<td class="px-4 py-3">
|
||||
<span class="font-mono font-semibold" x-text="order.order_number || `#${order.id}`"></span>
|
||||
</td>
|
||||
<!-- Customer -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="text-sm">
|
||||
<p class="font-medium" x-text="order.customer_name || 'Guest'"></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="order.customer_email || ''"></p>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Date -->
|
||||
<td class="px-4 py-3 text-sm" x-text="formatDate(order.created_at)"></td>
|
||||
<!-- Total -->
|
||||
<td class="px-4 py-3 text-sm font-semibold" x-text="formatPrice(order.total)"></td>
|
||||
<!-- Status -->
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span
|
||||
:class="{
|
||||
'px-2 py-1 font-semibold leading-tight rounded-full': true,
|
||||
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': getStatusColor(order.status) === 'yellow',
|
||||
'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100': getStatusColor(order.status) === 'blue',
|
||||
'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100': getStatusColor(order.status) === 'orange',
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': getStatusColor(order.status) === 'green',
|
||||
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': getStatusColor(order.status) === 'red',
|
||||
'text-indigo-700 bg-indigo-100 dark:bg-indigo-700 dark:text-indigo-100': getStatusColor(order.status) === 'indigo',
|
||||
'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100': getStatusColor(order.status) === 'gray'
|
||||
}"
|
||||
x-text="getStatusLabel(order.status)"
|
||||
></span>
|
||||
</td>
|
||||
<!-- Actions -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<button
|
||||
@click="viewOrder(order)"
|
||||
class="p-1 text-gray-500 hover:text-purple-600 dark:text-gray-400 dark:hover:text-purple-400"
|
||||
title="View Details"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="openStatusModal(order)"
|
||||
class="p-1 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
|
||||
title="Update Status"
|
||||
>
|
||||
<span x-html="$icon('pencil-square', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- Empty State -->
|
||||
<tr x-show="orders.length === 0">
|
||||
<td colspan="7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('document-text', 'w-12 h-12 text-gray-300 dark:text-gray-600 mb-4')"></span>
|
||||
<p class="text-lg font-medium">No orders found</p>
|
||||
<p class="text-sm">Orders will appear here when customers make purchases</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
|
||||
394
app/templates/vendor/settings.html
vendored
394
app/templates/vendor/settings.html
vendored
@@ -27,6 +27,7 @@
|
||||
{{ tab_button('marketplace', 'Marketplace', tab_var='activeSection', icon='shopping-cart') }}
|
||||
{{ tab_button('invoices', 'Invoices', tab_var='activeSection', icon='document-text') }}
|
||||
{{ tab_button('branding', 'Branding', tab_var='activeSection', icon='color-swatch') }}
|
||||
{{ tab_button('email', 'Email', tab_var='activeSection', icon='envelope') }}
|
||||
{{ tab_button('domains', 'Domains', tab_var='activeSection', icon='globe-alt') }}
|
||||
{{ tab_button('api', 'API', tab_var='activeSection', icon='key') }}
|
||||
{{ tab_button('notifications', 'Notifications', tab_var='activeSection', icon='bell') }}
|
||||
@@ -591,6 +592,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Boost Sort -->
|
||||
{# noqa: FE-008 - Decimal input with 0.1 step and custom @input handler, not suited for number_stepper #}
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Boost Sort Priority
|
||||
@@ -803,6 +805,398 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email Settings -->
|
||||
<div x-show="activeSection === 'email'" class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-4 border-b dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Email Settings</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Configure your email sending settings for customer communications</p>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<!-- Loading state for email settings -->
|
||||
<div x-show="emailSettingsLoading" class="flex justify-center py-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
|
||||
</div>
|
||||
|
||||
<div x-show="!emailSettingsLoading" class="space-y-6">
|
||||
<!-- Configuration Status Banner -->
|
||||
<template x-if="!emailSettings?.is_configured">
|
||||
<div class="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||||
<div class="flex items-start gap-3">
|
||||
<span x-html="$icon('exclamation-triangle', 'w-5 h-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5')"></span>
|
||||
<div>
|
||||
<p class="font-medium text-yellow-800 dark:text-yellow-300">Email not configured</p>
|
||||
<p class="text-sm text-yellow-700 dark:text-yellow-400 mt-1">
|
||||
Configure your SMTP settings to send emails to your customers (order confirmations, shipping updates, etc.)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="emailSettings?.is_configured && !emailSettings?.is_verified">
|
||||
<div class="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 gap-3">
|
||||
<span x-html="$icon('information-circle', 'w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5')"></span>
|
||||
<div>
|
||||
<p class="font-medium text-blue-800 dark:text-blue-300">Email configured but not verified</p>
|
||||
<p class="text-sm text-blue-700 dark:text-blue-400 mt-1">
|
||||
Send a test email to verify your settings are working correctly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="emailSettings?.is_configured && emailSettings?.is_verified">
|
||||
<div class="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<div class="flex items-start gap-3">
|
||||
<span x-html="$icon('check-circle', 'w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5')"></span>
|
||||
<div>
|
||||
<p class="font-medium text-green-800 dark:text-green-300">Email configured and verified</p>
|
||||
<p class="text-sm text-green-700 dark:text-green-400 mt-1">
|
||||
Your email settings are ready. Emails will be sent from <span class="font-medium" x-text="emailForm.from_email"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Sender Identity -->
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-4">Sender Identity</h4>
|
||||
<div class="space-y-4">
|
||||
<!-- From Email -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
From Email <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
x-model="emailForm.from_email"
|
||||
@input="markEmailChanged()"
|
||||
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
placeholder="orders@yourstore.com"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Email address that customers will see in their inbox
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- From Name -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
From Name <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="emailForm.from_name"
|
||||
@input="markEmailChanged()"
|
||||
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
placeholder="Your Store Name"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Name that appears as the sender (e.g., "Your Store Name")
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Reply-To Email -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Reply-To Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
x-model="emailForm.reply_to_email"
|
||||
@input="markEmailChanged()"
|
||||
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
placeholder="support@yourstore.com"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Optional: Where replies should go (defaults to From Email)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email Provider -->
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-4">Email Provider</h4>
|
||||
<div class="space-y-4">
|
||||
<!-- Provider Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Provider
|
||||
</label>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<template x-for="provider in emailProviders" :key="provider.code">
|
||||
<button
|
||||
type="button"
|
||||
@click="provider.available ? (emailForm.provider = provider.code, markEmailChanged()) : null"
|
||||
:class="emailForm.provider === provider.code
|
||||
? 'border-purple-500 bg-purple-50 dark:bg-purple-900/30'
|
||||
: provider.available
|
||||
? 'border-gray-200 dark:border-gray-600 hover:border-purple-300'
|
||||
: 'border-gray-200 dark:border-gray-600 opacity-50 cursor-not-allowed'"
|
||||
class="relative p-3 border-2 rounded-lg transition-colors"
|
||||
>
|
||||
<div class="text-sm font-medium text-gray-700 dark:text-gray-300" x-text="provider.name"></div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1" x-text="provider.description"></div>
|
||||
<template x-if="!provider.available">
|
||||
<div class="absolute top-1 right-1">
|
||||
<span class="px-1.5 py-0.5 text-xs font-medium text-purple-600 bg-purple-100 rounded dark:bg-purple-900/50 dark:text-purple-400">
|
||||
Business+
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="emailForm.provider === provider.code">
|
||||
<div class="absolute top-1 right-1">
|
||||
<span x-html="$icon('check-circle', 'w-5 h-5 text-purple-600 dark:text-purple-400')"></span>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SMTP Settings -->
|
||||
<template x-if="emailForm.provider === 'smtp'">
|
||||
<div class="space-y-4 pt-4 border-t dark:border-gray-600">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- SMTP Host -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
SMTP Host <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="emailForm.smtp_host"
|
||||
@input="markEmailChanged()"
|
||||
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
placeholder="smtp.yourprovider.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- SMTP Port -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
SMTP Port <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
x-model.number="emailForm.smtp_port"
|
||||
@input="markEmailChanged()"
|
||||
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
placeholder="587"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- SMTP Username -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
SMTP Username <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="emailForm.smtp_username"
|
||||
@input="markEmailChanged()"
|
||||
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
placeholder="your-username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- SMTP Password -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
SMTP Password <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
x-model="emailForm.smtp_password"
|
||||
@input="markEmailChanged()"
|
||||
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
:placeholder="emailSettings?.smtp_password_set ? '••••••••' : 'Enter password'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TLS/SSL Options -->
|
||||
<div class="flex items-center gap-6">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
x-model="emailForm.smtp_use_tls"
|
||||
@change="markEmailChanged()"
|
||||
class="w-4 h-4 text-purple-600 bg-gray-100 border-gray-300 rounded focus:ring-purple-500"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Use TLS (STARTTLS)</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
x-model="emailForm.smtp_use_ssl"
|
||||
@change="markEmailChanged()"
|
||||
class="w-4 h-4 text-purple-600 bg-gray-100 border-gray-300 rounded focus:ring-purple-500"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Use SSL (port 465)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- SendGrid Settings -->
|
||||
<template x-if="emailForm.provider === 'sendgrid'">
|
||||
<div class="space-y-4 pt-4 border-t dark:border-gray-600">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
SendGrid API Key <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
x-model="emailForm.sendgrid_api_key"
|
||||
@input="markEmailChanged()"
|
||||
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
:placeholder="emailSettings?.sendgrid_api_key_set ? '••••••••' : 'SG.xxxxx'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Mailgun Settings -->
|
||||
<template x-if="emailForm.provider === 'mailgun'">
|
||||
<div class="space-y-4 pt-4 border-t dark:border-gray-600">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Mailgun API Key <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
x-model="emailForm.mailgun_api_key"
|
||||
@input="markEmailChanged()"
|
||||
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
:placeholder="emailSettings?.mailgun_api_key_set ? '••••••••' : 'key-xxxxx'"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Mailgun Domain <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="emailForm.mailgun_domain"
|
||||
@input="markEmailChanged()"
|
||||
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
placeholder="mg.yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- SES Settings -->
|
||||
<template x-if="emailForm.provider === 'ses'">
|
||||
<div class="space-y-4 pt-4 border-t dark:border-gray-600">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
AWS Access Key ID <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="emailForm.ses_access_key_id"
|
||||
@input="markEmailChanged()"
|
||||
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
placeholder="AKIAIOSFODNN7EXAMPLE"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
AWS Secret Access Key <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
x-model="emailForm.ses_secret_access_key"
|
||||
@input="markEmailChanged()"
|
||||
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
:placeholder="emailSettings?.ses_access_key_id_set ? '••••••••' : 'Enter secret'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
AWS Region
|
||||
</label>
|
||||
<select
|
||||
x-model="emailForm.ses_region"
|
||||
@change="markEmailChanged()"
|
||||
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
>
|
||||
<option value="eu-west-1">EU (Ireland)</option>
|
||||
<option value="eu-central-1">EU (Frankfurt)</option>
|
||||
<option value="us-east-1">US East (N. Virginia)</option>
|
||||
<option value="us-west-2">US West (Oregon)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signature (Optional) -->
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-4">Email Signature (Optional)</h4>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Plain Text Signature
|
||||
</label>
|
||||
<textarea
|
||||
x-model="emailForm.signature_text"
|
||||
@input="markEmailChanged()"
|
||||
rows="3"
|
||||
class="w-full px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
placeholder="Best regards, The Your Store Team"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row justify-between gap-4 pt-4 border-t dark:border-gray-600">
|
||||
<!-- Test Email -->
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="email"
|
||||
x-model="testEmailAddress"
|
||||
class="px-4 py-2 text-sm text-gray-700 bg-white border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
placeholder="test@example.com"
|
||||
/>
|
||||
<button
|
||||
@click="sendTestEmail()"
|
||||
:disabled="!emailSettings?.is_configured || sendingTestEmail || !testEmailAddress"
|
||||
class="px-4 py-2 text-sm font-medium text-purple-600 bg-purple-100 rounded-lg hover:bg-purple-200 disabled:opacity-50 dark:bg-purple-900/30 dark:text-purple-400"
|
||||
>
|
||||
<span x-show="!sendingTestEmail">Send Test</span>
|
||||
<span x-show="sendingTestEmail">Sending...</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<button
|
||||
@click="saveEmailSettings()"
|
||||
:disabled="saving || !hasEmailChanges"
|
||||
class="px-6 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||
>
|
||||
<span x-show="!saving">Save Email Settings</span>
|
||||
<span x-show="saving">Saving...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Domains Settings -->
|
||||
<div x-show="activeSection === 'domains'" class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-4 border-b dark:border-gray-700">
|
||||
|
||||
185
app/templates/vendor/team.html
vendored
185
app/templates/vendor/team.html
vendored
@@ -3,6 +3,7 @@
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/modals.html' import modal_simple %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper %}
|
||||
|
||||
{% block title %}Team{% endblock %}
|
||||
|
||||
@@ -64,100 +65,98 @@
|
||||
</div>
|
||||
|
||||
<!-- Team Members Table -->
|
||||
<div x-show="!loading && !error" class="w-full mb-8 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">Member</th>
|
||||
<th class="px-4 py-3">Role</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3">Joined</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="member in members" :key="member.user_id">
|
||||
<tr class="text-gray-700 dark:text-gray-400">
|
||||
<!-- Member Info -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center text-sm">
|
||||
<div class="relative w-10 h-10 mr-3 rounded-full overflow-hidden bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
|
||||
<span class="text-sm font-semibold text-purple-600 dark:text-purple-300" x-text="getInitials(member)"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold" x-text="`${member.first_name || ''} ${member.last_name || ''}`.trim() || member.email"></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="member.email"></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Role -->
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span
|
||||
class="px-2 py-1 font-semibold leading-tight text-purple-700 bg-purple-100 rounded-full dark:bg-purple-700 dark:text-purple-100"
|
||||
x-text="getRoleName(member)"
|
||||
></span>
|
||||
</td>
|
||||
<!-- Status -->
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span
|
||||
:class="{
|
||||
'px-2 py-1 font-semibold leading-tight rounded-full': true,
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': member.is_active && !member.invitation_pending,
|
||||
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': member.invitation_pending,
|
||||
'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100': !member.is_active
|
||||
}"
|
||||
x-text="member.invitation_pending ? 'Pending' : (member.is_active ? 'Active' : 'Inactive')"
|
||||
></span>
|
||||
</td>
|
||||
<!-- Joined -->
|
||||
<td class="px-4 py-3 text-sm" x-text="formatDate(member.joined_at)"></td>
|
||||
<!-- Actions -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<!-- Edit button - not for owners -->
|
||||
<button
|
||||
@click="openEditModal(member)"
|
||||
class="p-1 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
|
||||
title="Edit"
|
||||
x-show="!member.is_owner"
|
||||
>
|
||||
<span x-html="$icon('pencil', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
<!-- Owner badge -->
|
||||
<span x-show="member.is_owner" class="text-xs text-gray-400 dark:text-gray-500 italic">Owner</span>
|
||||
<!-- Remove button - not for owners -->
|
||||
<button
|
||||
@click="confirmRemove(member)"
|
||||
class="p-1 text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400"
|
||||
title="Remove"
|
||||
x-show="!member.is_owner"
|
||||
>
|
||||
<span x-html="$icon('trash', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- Empty State -->
|
||||
<tr x-show="members.length === 0">
|
||||
<td colspan="5" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('users', 'w-12 h-12 text-gray-300 dark:text-gray-600 mb-4')"></span>
|
||||
<p class="text-lg font-medium">No team members yet</p>
|
||||
<p class="text-sm">Invite your first team member to get started</p>
|
||||
<button
|
||||
@click="openInviteModal()"
|
||||
class="mt-4 px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700"
|
||||
>
|
||||
Invite Member
|
||||
</button>
|
||||
<div x-show="!loading && !error" class="mb-8">
|
||||
{% 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">Member</th>
|
||||
<th class="px-4 py-3">Role</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3">Joined</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="member in members" :key="member.user_id">
|
||||
<tr class="text-gray-700 dark:text-gray-400">
|
||||
<!-- Member Info -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center text-sm">
|
||||
<div class="relative w-10 h-10 mr-3 rounded-full overflow-hidden bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
|
||||
<span class="text-sm font-semibold text-purple-600 dark:text-purple-300" x-text="getInitials(member)"></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold" x-text="`${member.first_name || ''} ${member.last_name || ''}`.trim() || member.email"></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="member.email"></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Role -->
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span
|
||||
class="px-2 py-1 font-semibold leading-tight text-purple-700 bg-purple-100 rounded-full dark:bg-purple-700 dark:text-purple-100"
|
||||
x-text="getRoleName(member)"
|
||||
></span>
|
||||
</td>
|
||||
<!-- Status -->
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span
|
||||
:class="{
|
||||
'px-2 py-1 font-semibold leading-tight rounded-full': true,
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': member.is_active && !member.invitation_pending,
|
||||
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': member.invitation_pending,
|
||||
'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100': !member.is_active
|
||||
}"
|
||||
x-text="member.invitation_pending ? 'Pending' : (member.is_active ? 'Active' : 'Inactive')"
|
||||
></span>
|
||||
</td>
|
||||
<!-- Joined -->
|
||||
<td class="px-4 py-3 text-sm" x-text="formatDate(member.joined_at)"></td>
|
||||
<!-- Actions -->
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<!-- Edit button - not for owners -->
|
||||
<button
|
||||
@click="openEditModal(member)"
|
||||
class="p-1 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
|
||||
title="Edit"
|
||||
x-show="!member.is_owner"
|
||||
>
|
||||
<span x-html="$icon('pencil', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
<!-- Owner badge -->
|
||||
<span x-show="member.is_owner" class="text-xs text-gray-400 dark:text-gray-500 italic">Owner</span>
|
||||
<!-- Remove button - not for owners -->
|
||||
<button
|
||||
@click="confirmRemove(member)"
|
||||
class="p-1 text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400"
|
||||
title="Remove"
|
||||
x-show="!member.is_owner"
|
||||
>
|
||||
<span x-html="$icon('trash', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- Empty State -->
|
||||
<tr x-show="members.length === 0">
|
||||
<td colspan="5" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('users', 'w-12 h-12 text-gray-300 dark:text-gray-600 mb-4')"></span>
|
||||
<p class="text-lg font-medium">No team members yet</p>
|
||||
<p class="text-sm">Invite your first team member to get started</p>
|
||||
<button
|
||||
@click="openInviteModal()"
|
||||
class="mt-4 px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700"
|
||||
>
|
||||
Invite Member
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
<!-- Invite Modal -->
|
||||
|
||||
Reference in New Issue
Block a user