Initial commit

This commit is contained in:
root
2025-12-21 09:55:58 -05:00
committed by Sebastian Krupinski
commit 169b7b4c91
57 changed files with 10105 additions and 0 deletions

375
src/pages/AccountsPage.vue Normal file
View File

@@ -0,0 +1,375 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useServicesStore } from '@/stores/servicesStore'
import AddAccountDialog from '@/components/AddAccountDialog.vue'
import type { ServiceObject } from '@/models'
const servicesStore = useServicesStore()
const showAddDialog = ref(false)
const showEditDialog = ref(false)
const showDeleteConfirm = ref(false)
const showTestResult = ref(false)
const selectedAccount = ref<any>({})
const loading = ref(false)
const saving = ref(false)
const deleting = ref(false)
const testingId = ref<string | null>(null)
const testResult = ref<any>(null)
const groupedServices = computed(() => servicesStore.servicesByProvider)
const hasAccounts = computed(() => servicesStore.has)
onMounted(async () => {
loading.value = true
try {
await servicesStore.list()
} finally {
loading.value = false
}
})
function getProviderIcon(providerId: string): string {
const icons: Record<string, string> = {
'smtp': 'mdi-email-multiple',
'jmap': 'mdi-api',
'exchange': 'mdi-microsoft',
}
return icons[providerId] || 'mdi-email'
}
function getProviderLabel(providerId: string): string {
const labels: Record<string, string> = {
'smtp': 'SMTP/IMAP',
'jmap': 'JMAP',
'exchange': 'Microsoft Exchange',
}
return labels[providerId] || providerId.toUpperCase()
}
function editAccount(account: any) {
selectedAccount.value = { ...account }
showEditDialog.value = true
}
async function saveEdit() {
saving.value = true
try {
await servicesStore.update(
selectedAccount.value.provider,
selectedAccount.value.identifier,
selectedAccount.value
)
showEditDialog.value = false
} catch (error) {
console.error('Failed to update account:', error)
} finally {
saving.value = false
}
}
function confirmDelete(account: any) {
selectedAccount.value = account
showDeleteConfirm.value = true
}
async function deleteAccount() {
deleting.value = true
try {
await servicesStore.delete(
selectedAccount.value.provider,
selectedAccount.value.identifier
)
showDeleteConfirm.value = false
selectedAccount.value = {}
} catch (error) {
console.error('Failed to delete account:', error)
} finally {
deleting.value = false
}
}
async function testAccount(service: ServiceObject) {
try {
const result = await servicesStore.test(
service.provider,
service.identifier,
service.location,
service.identity
)
testResult.value = result
showTestResult.value = true
} catch (error: any) {
testResult.value = {
success: false,
message: error.message || 'Connection test failed'
}
showTestResult.value = true
} finally {
testingId.value = null
}
}
async function handleAccountSaved() {
await servicesStore.list()
}
</script>
<template>
<v-container fluid>
<!-- Page Header -->
<div class="d-flex align-center justify-space-between mb-6">
<div>
<h1 class="text-h4 mb-1">Mail Accounts</h1>
<p class="text-body-2 text-medium-emphasis">
Manage your email accounts across all providers
</p>
</div>
<v-btn
color="primary"
prepend-icon="mdi-plus"
@click="showAddDialog = true"
>
Add Account
</v-btn>
</div>
<!-- Loading State -->
<v-row v-if="loading" class="mt-8">
<v-col
v-for="i in 3"
:key="i"
cols="12"
md="6"
lg="4"
>
<v-skeleton-loader type="card" />
</v-col>
</v-row>
<!-- Empty State -->
<v-card
v-else-if="!hasAccounts"
class="text-center pa-12"
variant="flat"
>
<v-icon
size="80"
color="grey-lighten-1"
class="mb-4"
>
mdi-email-off-outline
</v-icon>
<h2 class="text-h5 mb-2">No Mail Accounts</h2>
<p class="text-body-1 text-medium-emphasis mb-6">
Add your first mail account to start sending and receiving emails
</p>
<v-btn
color="primary"
size="large"
prepend-icon="mdi-plus"
@click="showAddDialog = true"
>
Add Your First Account
</v-btn>
</v-card>
<!-- Accounts by Provider -->
<template v-else>
<div
v-for="(services, providerId) in groupedServices"
:key="providerId"
class="mb-8"
>
<!-- Provider Header -->
<div class="d-flex align-center mb-4">
<v-icon
:icon="getProviderIcon(providerId)"
class="mr-2"
/>
<h2 class="text-h6">
{{ getProviderLabel(providerId) }}
</h2>
<v-chip
size="small"
class="ml-2"
variant="text"
>
{{ services.length }} account{{ services.length !== 1 ? 's' : '' }}
</v-chip>
</div>
<!-- Service Cards -->
<v-row>
<v-col
v-for="service in services"
:key="`${service.provider}:${service.identifier}`"
cols="12"
md="6"
lg="4"
>
<v-card
:class="{ 'border-error': !service.enabled }"
variant="outlined"
hover
>
<v-card-text>
<div class="d-flex align-center justify-space-between mb-2">
<div class="d-flex align-center">
<v-avatar
size="40"
:color="service.enabled ? 'primary' : 'grey'"
class="mr-3"
>
<v-icon color="white">
{{ service.enabled ? 'mdi-email' : 'mdi-email-off' }}
</v-icon>
</v-avatar>
<div>
<h3 class="text-h6">{{ service.label }}</h3>
<p class="text-caption text-medium-emphasis">
{{ service.primaryAddress || (service.identity?.type === 'BA' ? service.identity.identity : 'No email configured') }}
</p>
</div>
</div>
<v-chip
:color="service.enabled ? 'success' : 'error'"
size="small"
variant="flat"
>
{{ service.enabled ? 'Enabled' : 'Disabled' }}
</v-chip>
</div>
<!-- Service Stats/Info -->
<v-divider class="my-3" />
<div class="text-caption text-medium-emphasis">
<div class="d-flex align-center mb-1">
<v-icon size="small" class="mr-1">mdi-identifier</v-icon>
ID: {{ service.identifier }}
</div>
</div>
</v-card-text>
<v-card-actions>
<v-btn
variant="text"
size="small"
prepend-icon="mdi-pencil"
@click="editAccount(service)"
>
Edit
</v-btn>
<v-btn
variant="text"
size="small"
prepend-icon="mdi-connection"
:loading="testingId === service.identifier"
@click="testAccount(service)"
>
Test
</v-btn>
<v-spacer />
<v-btn
variant="text"
size="small"
color="error"
icon="mdi-delete"
@click="confirmDelete(service)"
/>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</div>
</template>
<!-- Add Account Dialog -->
<AddAccountDialog
v-model="showAddDialog"
@saved="handleAccountSaved"
/>
<!-- Edit Account Dialog -->
<v-dialog
v-model="showEditDialog"
max-width="600"
>
<v-card>
<v-card-title>Edit Account</v-card-title>
<v-card-text>
<v-text-field
v-model="selectedAccount.label"
label="Account Name"
variant="outlined"
/>
<v-switch
v-model="selectedAccount.enabled"
label="Enable this account"
color="primary"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
variant="text"
@click="showEditDialog = false"
>
Cancel
</v-btn>
<v-btn
color="primary"
:loading="saving"
@click="saveEdit"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Delete Confirmation Dialog -->
<v-dialog
v-model="showDeleteConfirm"
max-width="400"
>
<v-card>
<v-card-title class="text-h6">Delete Account?</v-card-title>
<v-card-text>
Are you sure you want to delete <strong>{{ selectedAccount?.label }}</strong>?
This action cannot be undone.
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
variant="text"
@click="showDeleteConfirm = false"
>
Cancel
</v-btn>
<v-btn
color="error"
variant="flat"
:loading="deleting"
@click="deleteAccount"
>
Delete
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Test Result Snackbar -->
<v-snackbar
v-model="showTestResult"
:color="testResult?.success ? 'success' : 'error'"
:timeout="5000"
>
<v-icon start>
{{ testResult?.success ? 'mdi-check-circle' : 'mdi-alert-circle' }}
</v-icon>
{{ testResult?.message || 'Connection test completed' }}
</v-snackbar>
</v-container>
</template>