chore: merge main into branch, resolve conflict in models.py
Both sets of UserProfile columns are retained: - is_complimentary (from main, migration 019_add_is_complimentary) - subscription_change_pending_tier / subscription_change_pending_date (our branch, renamed to migration 020_add_subscription_change_pending with down_revision updated to chain after 019_add_is_complimentary)
This commit is contained in:
@@ -23,6 +23,13 @@
|
||||
>
|
||||
<i class="fas fa-user-plus mr-2" aria-hidden="true"></i> Add User Profile
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="openCreateLocalUserModal()"
|
||||
class="inline-flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
||||
>
|
||||
<i class="fas fa-user-lock mr-2" aria-hidden="true"></i> Create Local Account
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ── Alert banner ───────────────────────────────────────────────────────── -->
|
||||
@@ -140,6 +147,13 @@
|
||||
></i>
|
||||
<span x-text="user.subscription_tier || 'free'"></span>
|
||||
</span>
|
||||
<span
|
||||
x-show="user.is_complimentary"
|
||||
class="ml-1 inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-700"
|
||||
aria-label="Complimentary plan — not billed"
|
||||
>
|
||||
<i class="fas fa-gift" aria-hidden="true"></i>
|
||||
</span>
|
||||
</td>
|
||||
<!-- Upload limit -->
|
||||
<td class="px-4 py-3 text-sm text-center text-gray-700">
|
||||
@@ -375,6 +389,25 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Complimentary plan toggle -->
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
x-model="form.is_complimentary"
|
||||
class="sr-only peer"
|
||||
id="modal-complimentary"
|
||||
role="switch"
|
||||
:aria-checked="form.is_complimentary.toString()"
|
||||
/>
|
||||
<div class="w-10 h-6 bg-gray-200 peer-focus:ring-2 peer-focus:ring-blue-400 rounded-full peer peer-checked:bg-green-500 after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:after:translate-x-4"></div>
|
||||
</label>
|
||||
<label for="modal-complimentary" class="text-sm font-medium text-gray-700">
|
||||
Complimentary plan
|
||||
<span class="text-xs text-gray-400 font-normal">(user keeps tier benefits but is never billed — set automatically for admin accounts)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
@@ -399,6 +432,200 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Local Accounts section ────────────────────────────────────────────── -->
|
||||
<div class="bg-white shadow rounded-lg mt-8">
|
||||
<div class="px-6 py-4 border-b flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
<i class="fas fa-user-lock text-green-600" aria-hidden="true"></i>
|
||||
Local User Accounts
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500 mt-0.5">
|
||||
Email/password accounts created directly on this server.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="openCreateLocalUserModal()"
|
||||
class="inline-flex items-center px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
||||
>
|
||||
<i class="fas fa-plus mr-1" aria-hidden="true"></i> New Account
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200" aria-label="Local user accounts">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Username</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Display Name</th>
|
||||
<th scope="col" class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th scope="col" class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Role</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<template x-if="localUsersLoading">
|
||||
<tr>
|
||||
<td colspan="7" class="px-4 py-6 text-center text-gray-400">
|
||||
<i class="fas fa-spinner fa-spin mr-2" aria-hidden="true"></i> Loading…
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-if="!localUsersLoading && localUsers.length === 0">
|
||||
<tr>
|
||||
<td colspan="7" class="px-4 py-6 text-center text-gray-400">
|
||||
No local accounts yet.
|
||||
<button type="button" @click="openCreateLocalUserModal()" class="text-green-600 hover:underline ml-1">Create one.</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-for="lu in localUsers" :key="lu.id">
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 text-sm font-mono text-gray-800" x-text="lu.username"></td>
|
||||
<td class="px-4 py-3 text-sm text-gray-600" x-text="lu.email"></td>
|
||||
<td class="px-4 py-3 text-sm text-gray-600" x-text="lu.display_name || '—'"></td>
|
||||
<td class="px-4 py-3 text-sm text-center">
|
||||
<span
|
||||
:class="lu.is_active ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'"
|
||||
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
||||
x-text="lu.is_active ? 'Active' : 'Unverified'"
|
||||
></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-center">
|
||||
<span
|
||||
:class="lu.is_admin ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-600'"
|
||||
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
||||
x-text="lu.is_admin ? 'Admin' : 'User'"
|
||||
></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-500 whitespace-nowrap" x-text="lu.created_at ? formatDate(lu.created_at) : '—'"></td>
|
||||
<td class="px-4 py-3 text-sm text-right">
|
||||
<button
|
||||
type="button"
|
||||
@click="confirmDeleteLocalUser(lu)"
|
||||
class="text-red-600 hover:text-red-800 focus:outline-none"
|
||||
:aria-label="`Delete account for ${lu.username}`"
|
||||
>
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Create local user modal ────────────────────────────────────────────── -->
|
||||
<div
|
||||
x-show="localUserModal.open"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 px-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="create-local-user-title"
|
||||
>
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg" @click.outside="localUserModal.open = false">
|
||||
<div class="px-6 py-4 border-b flex items-center justify-between">
|
||||
<h2 id="create-local-user-title" class="text-lg font-semibold text-gray-900">Create Local Account</h2>
|
||||
<button type="button" @click="localUserModal.open = false" aria-label="Close" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<form @submit.prevent="submitCreateLocalUser" class="px-6 py-5 space-y-4">
|
||||
<div>
|
||||
<label for="lu-email" class="block text-sm font-medium text-gray-700">Email <span aria-hidden="true" class="text-red-500">*</span></label>
|
||||
<input type="email" id="lu-email" x-model="localUserModal.form.email" required autocomplete="off"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring focus:ring-green-500 focus:ring-opacity-50 text-sm"
|
||||
style="min-height:40px;" aria-required="true">
|
||||
</div>
|
||||
<div>
|
||||
<label for="lu-username" class="block text-sm font-medium text-gray-700">Username <span aria-hidden="true" class="text-red-500">*</span></label>
|
||||
<input type="text" id="lu-username" x-model="localUserModal.form.username" required autocomplete="off"
|
||||
pattern="^[a-zA-Z0-9_-]+$" minlength="3" maxlength="64"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring focus:ring-green-500 focus:ring-opacity-50 text-sm"
|
||||
style="min-height:40px;" aria-required="true" aria-describedby="lu-username-hint">
|
||||
<p id="lu-username-hint" class="mt-1 text-xs text-gray-500">3–64 characters. Letters, numbers, hyphens and underscores only.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="lu-display-name" class="block text-sm font-medium text-gray-700">Display Name <span class="text-gray-400">(optional)</span></label>
|
||||
<input type="text" id="lu-display-name" x-model="localUserModal.form.display_name" autocomplete="off" maxlength="255"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring focus:ring-green-500 focus:ring-opacity-50 text-sm"
|
||||
style="min-height:40px;">
|
||||
</div>
|
||||
<div>
|
||||
<label for="lu-password" class="block text-sm font-medium text-gray-700">Password <span aria-hidden="true" class="text-red-500">*</span></label>
|
||||
<input type="password" id="lu-password" x-model="localUserModal.form.password" required autocomplete="new-password"
|
||||
minlength="8" maxlength="128"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring focus:ring-green-500 focus:ring-opacity-50 text-sm"
|
||||
style="min-height:40px;" aria-required="true" aria-describedby="lu-password-hint">
|
||||
<p id="lu-password-hint" class="mt-1 text-xs text-gray-500">Minimum 8 characters.</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="checkbox" id="lu-is-admin" x-model="localUserModal.form.is_admin"
|
||||
class="h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500">
|
||||
<label for="lu-is-admin" class="text-sm text-gray-700">Grant admin privileges</label>
|
||||
</div>
|
||||
<div x-show="localUserModal.error" x-cloak
|
||||
class="bg-red-50 border-l-4 border-red-500 text-red-700 p-3 rounded text-sm"
|
||||
role="alert" aria-live="assertive" x-text="localUserModal.error">
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<button type="button" @click="localUserModal.open = false"
|
||||
class="px-4 py-2 text-sm font-medium border border-gray-300 rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" :disabled="localUserModal.saving"
|
||||
class="px-4 py-2 text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-green-500 disabled:opacity-50">
|
||||
<span x-show="!localUserModal.saving">Create Account</span>
|
||||
<span x-show="localUserModal.saving" x-cloak><i class="fas fa-spinner fa-spin mr-1" aria-hidden="true"></i> Creating…</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Delete local user confirmation modal ───────────────────────────────── -->
|
||||
<div
|
||||
x-show="deleteLocalUserModal.open"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 px-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="delete-local-user-title"
|
||||
>
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-md" @click.outside="deleteLocalUserModal.open = false">
|
||||
<div class="px-6 py-4 border-b">
|
||||
<h2 id="delete-local-user-title" class="text-lg font-semibold text-gray-900">Delete Local Account</h2>
|
||||
</div>
|
||||
<div class="px-6 py-5">
|
||||
<p class="text-sm text-gray-700">
|
||||
Are you sure you want to delete the account for
|
||||
<strong class="font-mono" x-text="deleteLocalUserModal.username"></strong>
|
||||
(<span class="font-mono" x-text="deleteLocalUserModal.email"></span>)?
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 mt-2">This cannot be undone. Documents owned by this user are <strong>not</strong> deleted.</p>
|
||||
</div>
|
||||
<div class="px-6 py-4 border-t flex justify-end gap-3">
|
||||
<button type="button" @click="deleteLocalUserModal.open = false"
|
||||
class="px-4 py-2 text-sm font-medium border border-gray-300 rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" @click="executeDeleteLocalUser()" :disabled="deleteLocalUserModal.deleting"
|
||||
class="px-4 py-2 text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 disabled:opacity-50">
|
||||
<span x-show="!deleteLocalUserModal.deleting">Delete Account</span>
|
||||
<span x-show="deleteLocalUserModal.deleting" x-cloak><i class="fas fa-spinner fa-spin mr-1" aria-hidden="true"></i> Deleting…</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Delete confirmation modal ──────────────────────────────────────────── -->
|
||||
<div
|
||||
x-show="deleteModal.open"
|
||||
@@ -472,6 +699,7 @@ function adminUsersApp() {
|
||||
subscription_tier: 'free',
|
||||
subscription_billing_cycle: 'monthly',
|
||||
subscription_period_start: null,
|
||||
is_complimentary: false,
|
||||
},
|
||||
|
||||
// Delete modal
|
||||
@@ -481,6 +709,23 @@ function adminUsersApp() {
|
||||
deleting: false,
|
||||
},
|
||||
|
||||
// Local users
|
||||
localUsers: [],
|
||||
localUsersLoading: true,
|
||||
localUserModal: {
|
||||
open: false,
|
||||
saving: false,
|
||||
error: '',
|
||||
form: { email: '', username: '', display_name: '', password: '', is_admin: false },
|
||||
},
|
||||
deleteLocalUserModal: {
|
||||
open: false,
|
||||
id: null,
|
||||
username: '',
|
||||
email: '',
|
||||
deleting: false,
|
||||
},
|
||||
|
||||
// Alert
|
||||
alert: { show: false, type: 'success', title: '', message: '' },
|
||||
|
||||
@@ -491,7 +736,7 @@ function adminUsersApp() {
|
||||
},
|
||||
|
||||
async init() {
|
||||
await this.fetchUsers(1);
|
||||
await Promise.all([this.fetchUsers(1), this.fetchLocalUsers()]);
|
||||
},
|
||||
|
||||
async fetchUsers(page) {
|
||||
@@ -524,7 +769,7 @@ function adminUsersApp() {
|
||||
openCreateModal() {
|
||||
this.isCreate = true;
|
||||
this.modalTitle = 'Add User Profile';
|
||||
this.form = { user_id: '', display_name: '', daily_upload_limit: null, notes: '', is_blocked: false, subscription_tier: 'free', subscription_billing_cycle: 'monthly', subscription_period_start: null };
|
||||
this.form = { user_id: '', display_name: '', daily_upload_limit: null, notes: '', is_blocked: false, subscription_tier: 'free', subscription_billing_cycle: 'monthly', subscription_period_start: null, is_complimentary: false };
|
||||
this.modalOpen = true;
|
||||
},
|
||||
|
||||
@@ -541,6 +786,7 @@ function adminUsersApp() {
|
||||
subscription_tier: user.subscription_tier || 'free',
|
||||
subscription_billing_cycle: user.subscription_billing_cycle || 'monthly',
|
||||
subscription_period_start: user.subscription_period_start ? user.subscription_period_start.substring(0, 10) : null,
|
||||
is_complimentary: !!user.is_complimentary,
|
||||
};
|
||||
this.modalOpen = true;
|
||||
},
|
||||
@@ -555,6 +801,7 @@ function adminUsersApp() {
|
||||
notes: this.form.notes || null,
|
||||
is_blocked: !!this.form.is_blocked,
|
||||
subscription_tier: this.form.subscription_tier || 'free',
|
||||
is_complimentary: !!this.form.is_complimentary,
|
||||
};
|
||||
const uid = encodeURIComponent(this.form.user_id);
|
||||
const resp = await fetch(`/api/admin/users/${uid}`, {
|
||||
@@ -633,6 +880,90 @@ function adminUsersApp() {
|
||||
this.alert = { show: true, type, title, message };
|
||||
setTimeout(() => { this.alert.show = false; }, type === 'success' ? 5000 : 10000);
|
||||
},
|
||||
|
||||
async fetchLocalUsers() {
|
||||
this.localUsersLoading = true;
|
||||
try {
|
||||
const resp = await fetch('/api/admin/users/local', {
|
||||
headers: { 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '' },
|
||||
});
|
||||
if (!resp.ok) {
|
||||
this.showAlert('error', 'Failed to load local users', resp.statusText);
|
||||
return;
|
||||
}
|
||||
this.localUsers = await resp.json();
|
||||
} catch (e) {
|
||||
this.showAlert('error', 'Network error', e.message);
|
||||
} finally {
|
||||
this.localUsersLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
openCreateLocalUserModal() {
|
||||
this.localUserModal.form = { email: '', username: '', display_name: '', password: '', is_admin: false };
|
||||
this.localUserModal.error = '';
|
||||
this.localUserModal.saving = false;
|
||||
this.localUserModal.open = true;
|
||||
},
|
||||
|
||||
async submitCreateLocalUser() {
|
||||
this.localUserModal.error = '';
|
||||
this.localUserModal.saving = true;
|
||||
try {
|
||||
const resp = await fetch('/api/admin/users/local', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||
},
|
||||
body: JSON.stringify(this.localUserModal.form),
|
||||
});
|
||||
if (resp.ok) {
|
||||
this.localUserModal.open = false;
|
||||
this.showAlert('success', 'Account created', `Local account for "${this.localUserModal.form.username}" was created successfully.`);
|
||||
await this.fetchLocalUsers();
|
||||
} else {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
this.localUserModal.error = err.detail || 'Failed to create account.';
|
||||
}
|
||||
} catch (e) {
|
||||
this.localUserModal.error = 'Network error: ' + e.message;
|
||||
} finally {
|
||||
this.localUserModal.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
confirmDeleteLocalUser(lu) {
|
||||
this.deleteLocalUserModal.id = lu.id;
|
||||
this.deleteLocalUserModal.username = lu.username;
|
||||
this.deleteLocalUserModal.email = lu.email;
|
||||
this.deleteLocalUserModal.deleting = false;
|
||||
this.deleteLocalUserModal.open = true;
|
||||
},
|
||||
|
||||
async executeDeleteLocalUser() {
|
||||
this.deleteLocalUserModal.deleting = true;
|
||||
try {
|
||||
const resp = await fetch(`/api/admin/users/local/${this.deleteLocalUserModal.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '' },
|
||||
});
|
||||
if (resp.status === 204) {
|
||||
this.deleteLocalUserModal.open = false;
|
||||
this.showAlert('success', 'Deleted', `Account for "${this.deleteLocalUserModal.username}" has been removed.`);
|
||||
await this.fetchLocalUsers();
|
||||
} else {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
this.showAlert('error', 'Delete failed', err.detail || resp.statusText);
|
||||
this.deleteLocalUserModal.open = false;
|
||||
}
|
||||
} catch (e) {
|
||||
this.showAlert('error', 'Network error', e.message);
|
||||
this.deleteLocalUserModal.open = false;
|
||||
} finally {
|
||||
this.deleteLocalUserModal.deleting = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50 min-h-screen flex flex-col"
|
||||
data-multi-user="{{ 'true' if multi_user_enabled else 'false' }}">
|
||||
data-multi-user="{{ 'true' if multi_user_enabled else 'false' }}"
|
||||
data-allow-signup="{{ 'true' if allow_signup else 'false' }}">
|
||||
<!-- Skip to main content link for keyboard/screen reader users -->
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
|
||||
|
||||
@@ -1,12 +1,143 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard – DocuElevate{% endblock %}
|
||||
{% block title %}{% if multi_user_enabled and not is_logged_in %}DocuElevate – Intelligent Document Processing{% else %}Dashboard – DocuElevate{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="{% if multi_user_enabled and not is_logged_in %}bg-gray-50 min-h-screen{% else %}container mx-auto px-4 py-8{% endif %}">
|
||||
|
||||
{% if multi_user_enabled %}
|
||||
{% if multi_user_enabled and not is_logged_in %}
|
||||
{# ══════════════════════════════════════════════════════════════════════════ #}
|
||||
{# MULTI-USER / SAAS DASHBOARD #}
|
||||
{# PUBLIC LANDING PAGE (multi-user, visitor not signed in) #}
|
||||
{# ══════════════════════════════════════════════════════════════════════════ #}
|
||||
|
||||
<!-- Hero -->
|
||||
<div class="bg-gradient-to-br from-blue-700 via-indigo-700 to-purple-700 text-white py-20 px-4">
|
||||
<div class="max-w-4xl mx-auto text-center">
|
||||
<span class="inline-block bg-white/20 text-white text-xs font-semibold uppercase tracking-widest px-3 py-1 rounded-full mb-4">
|
||||
Intelligent Document Processing
|
||||
</span>
|
||||
<h1 class="text-4xl sm:text-5xl font-extrabold mb-4 leading-tight">
|
||||
From upload to insight — automatically.
|
||||
</h1>
|
||||
<p class="text-indigo-100 text-lg max-w-2xl mx-auto mb-8">
|
||||
DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to
|
||||
Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
{% if allow_signup %}
|
||||
<a href="/signup"
|
||||
class="bg-white text-indigo-700 hover:bg-indigo-50 font-bold py-3 px-8 rounded-xl shadow-lg transition duration-200 flex items-center min-h-[44px]">
|
||||
<i class="fas fa-user-plus mr-2" aria-hidden="true"></i> Get Started — it's free
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/login"
|
||||
class="bg-white text-indigo-700 hover:bg-indigo-50 font-bold py-3 px-8 rounded-xl shadow-lg transition duration-200 flex items-center min-h-[44px]">
|
||||
<i class="fas fa-sign-in-alt mr-2" aria-hidden="true"></i> Log In
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/pricing"
|
||||
class="bg-transparent border-2 border-white text-white hover:bg-white hover:text-indigo-700 font-bold py-3 px-8 rounded-xl transition duration-200 flex items-center min-h-[44px]">
|
||||
<i class="fas fa-tags mr-2" aria-hidden="true"></i> View Plans & Pricing
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature grid -->
|
||||
<div class="max-w-6xl mx-auto px-4 py-16">
|
||||
<h2 class="text-2xl font-bold text-center text-gray-800 mb-10">Everything you need for smart document workflows</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
|
||||
<div class="bg-white rounded-2xl shadow p-6 flex gap-4">
|
||||
<div class="h-12 w-12 rounded-xl bg-blue-100 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-file-alt text-blue-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">OCR & Text Extraction</h3>
|
||||
<p class="text-gray-500 text-sm">Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow p-6 flex gap-4">
|
||||
<div class="h-12 w-12 rounded-xl bg-indigo-100 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-brain text-indigo-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">AI Metadata Extraction</h3>
|
||||
<p class="text-gray-500 text-sm">OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow p-6 flex gap-4">
|
||||
<div class="h-12 w-12 rounded-xl bg-green-100 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-cloud-upload-alt text-green-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">Multi-Cloud Storage</h3>
|
||||
<p class="text-gray-500 text-sm">Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow p-6 flex gap-4">
|
||||
<div class="h-12 w-12 rounded-xl bg-yellow-100 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-mail-bulk text-yellow-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">Email & IMAP Ingestion</h3>
|
||||
<p class="text-gray-500 text-sm">Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow p-6 flex gap-4">
|
||||
<div class="h-12 w-12 rounded-xl bg-purple-100 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-search text-purple-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">Full-Text Search</h3>
|
||||
<p class="text-gray-500 text-sm">Instantly find any document by content, metadata, or tags across your entire archive.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow p-6 flex gap-4">
|
||||
<div class="h-12 w-12 rounded-xl bg-pink-100 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-project-diagram text-pink-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-1">Custom Pipelines</h3>
|
||||
<p class="text-gray-500 text-sm">Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA banner -->
|
||||
<div class="bg-indigo-700 text-white py-14 px-4">
|
||||
<div class="max-w-2xl mx-auto text-center">
|
||||
<h2 class="text-3xl font-extrabold mb-4">Ready to elevate your document workflow?</h2>
|
||||
<p class="text-indigo-200 mb-8">Join teams already automating their document processing with DocuElevate.</p>
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
{% if allow_signup %}
|
||||
<a href="/signup"
|
||||
class="bg-white text-indigo-700 hover:bg-indigo-50 font-bold py-3 px-8 rounded-xl shadow-lg transition duration-200 flex items-center min-h-[44px]">
|
||||
<i class="fas fa-rocket mr-2" aria-hidden="true"></i> Create a free account
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/login"
|
||||
class="bg-white text-indigo-700 hover:bg-indigo-50 font-bold py-3 px-8 rounded-xl shadow-lg transition duration-200 flex items-center min-h-[44px]">
|
||||
<i class="fas fa-sign-in-alt mr-2" aria-hidden="true"></i> Log In
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/pricing"
|
||||
class="bg-transparent border-2 border-white text-white hover:bg-white hover:text-indigo-700 font-bold py-3 px-8 rounded-xl transition duration-200 flex items-center min-h-[44px]">
|
||||
<i class="fas fa-tags mr-2" aria-hidden="true"></i> See pricing
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif multi_user_enabled %}
|
||||
{# ══════════════════════════════════════════════════════════════════════════ #}
|
||||
{# MULTI-USER / SAAS DASHBOARD (logged-in user) #}
|
||||
{# ══════════════════════════════════════════════════════════════════════════ #}
|
||||
|
||||
<!-- Hero -->
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
Choose the plan that's right for you
|
||||
</h1>
|
||||
<p class="text-indigo-100 text-lg max-w-2xl mx-auto">
|
||||
From free exploration to unlimited enterprise processing — scale as your document workflows grow.
|
||||
One price per person, per month — from casual exploration to power-user workflows. No team plans, no per-seat tiers.
|
||||
</p>
|
||||
|
||||
<!-- Annual / Monthly toggle (cosmetic — actual billing handled separately) -->
|
||||
@@ -318,7 +318,7 @@
|
||||
<td class="px-4 py-3 text-center text-xs text-gray-500">Community</td>
|
||||
<td class="px-4 py-3 text-center text-xs text-gray-700">Email</td>
|
||||
<td class="px-4 py-3 text-center text-xs text-gray-700 font-medium">Priority email</td>
|
||||
<td class="px-4 py-3 text-center text-xs text-indigo-700 font-bold">Dedicated</td>
|
||||
<td class="px-4 py-3 text-center text-xs text-indigo-700 font-bold">Priority</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
|
||||
@@ -50,7 +50,12 @@
|
||||
})
|
||||
});
|
||||
if (resp.ok) {
|
||||
window.location.href = '/verify-email-sent';
|
||||
const data = await resp.json();
|
||||
if (data.email_verification_required) {
|
||||
window.location.href = '/verify-email-sent';
|
||||
} else {
|
||||
window.location.href = '/login?message=Account+created+successfully.+You+can+now+log+in.';
|
||||
}
|
||||
} else {
|
||||
const data = await resp.json();
|
||||
this.error = data.detail || 'Registration failed. Please try again.';
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
{% if tier_id == 'free' %}fa-seedling text-gray-500
|
||||
{% elif tier_id == 'starter' %}fa-rocket text-blue-600
|
||||
{% elif tier_id == 'professional' %}fa-star text-indigo-600
|
||||
{% else %}fa-building text-purple-600{% endif %}
|
||||
{% else %}fa-bolt text-purple-600{% endif %}
|
||||
text-2xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<p class="text-xs font-semibold text-gray-400 uppercase tracking-widest">Current Plan</p>
|
||||
|
||||
Reference in New Issue
Block a user