Add provider sub-tabs and collapsible panels to Bank Connections settings
Split Bank Connections into SimpleFIN/Plaid sub-tabs with institution cards as collapsible expansion panels. Persist active tab, provider sub-tab, and panel expand/collapse state to localStorage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -127,58 +127,59 @@
|
||||
<!-- Tab 2: Bank Connections -->
|
||||
<v-tabs-window-item value="connections">
|
||||
|
||||
<!-- SimpleFIN Section -->
|
||||
<v-card class="mb-6">
|
||||
<v-card-title class="d-flex align-center flex-wrap ga-2">
|
||||
<v-icon class="mr-1">mdi-link-variant</v-icon>
|
||||
<v-tabs v-model="activeProviderTab" color="primary" class="mb-4">
|
||||
<v-tab value="simplefin">
|
||||
<v-icon start size="small">mdi-link-variant</v-icon>
|
||||
SimpleFIN
|
||||
<v-chip color="green" size="small">{{ simpleFinConnections.length }}</v-chip>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
prepend-icon="mdi-link-plus"
|
||||
@click="showSimpleFinDialog = true"
|
||||
>
|
||||
Connect
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
prepend-icon="mdi-sync"
|
||||
:loading="syncingAllSimpleFin"
|
||||
:disabled="simpleFinConnections.filter(c => c.isActive).length === 0"
|
||||
@click="syncAllSimpleFin"
|
||||
>
|
||||
Sync All
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
prepend-icon="mdi-refresh"
|
||||
:loading="resyncingAllSimpleFin"
|
||||
:disabled="simpleFinConnections.filter(c => c.isActive).length === 0"
|
||||
@click="resyncAllSimpleFin"
|
||||
>
|
||||
Re-sync All
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-chip v-if="simpleFinConnections.length" color="green" size="x-small" class="ml-2">{{ simpleFinConnections.length }}</v-chip>
|
||||
</v-tab>
|
||||
<v-tab value="plaid">
|
||||
<v-icon start size="small">mdi-bank</v-icon>
|
||||
Plaid
|
||||
<v-chip v-if="plaidConnections.length" color="blue" size="x-small" class="ml-2">{{ plaidConnections.length }}</v-chip>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center mb-4">
|
||||
<v-tabs-window v-model="activeProviderTab">
|
||||
<!-- SimpleFIN sub-tab -->
|
||||
<v-tabs-window-item value="simplefin">
|
||||
<div class="d-flex align-center flex-wrap ga-2 mb-4">
|
||||
<v-btn size="small" variant="tonal" color="secondary" prepend-icon="mdi-link-plus" @click="showSimpleFinDialog = true">
|
||||
Connect
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
prepend-icon="mdi-sync"
|
||||
:loading="syncingAllSimpleFin"
|
||||
:disabled="simpleFinConnections.filter(c => c.isActive).length === 0"
|
||||
@click="syncAllSimpleFin"
|
||||
>
|
||||
Sync All
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
prepend-icon="mdi-refresh"
|
||||
:loading="resyncingAllSimpleFin"
|
||||
:disabled="simpleFinConnections.filter(c => c.isActive).length === 0"
|
||||
@click="resyncAllSimpleFin"
|
||||
>
|
||||
Re-sync All
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-select
|
||||
v-model="simpleFinGlobalInterval"
|
||||
:items="intervalOptions"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
label="Sync Interval (all SimpleFIN)"
|
||||
label="Sync Interval"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
style="max-width: 260px"
|
||||
style="max-width: 220px"
|
||||
@update:model-value="updateSimpleFinGlobalInterval"
|
||||
/>
|
||||
</div>
|
||||
@@ -187,281 +188,269 @@
|
||||
No SimpleFIN connections yet. Click Connect to add one.
|
||||
</v-alert>
|
||||
|
||||
<v-card v-for="conn in simpleFinConnections" :key="conn.id" variant="outlined" class="mb-3">
|
||||
<v-card-title class="d-flex align-center text-subtitle-1 py-2">
|
||||
{{ conn.institutionName }}
|
||||
<v-spacer />
|
||||
<v-chip
|
||||
:color="conn.isActive ? 'success' : 'grey'"
|
||||
size="x-small"
|
||||
class="mr-2"
|
||||
>
|
||||
{{ conn.isActive ? 'Active' : 'Inactive' }}
|
||||
</v-chip>
|
||||
<v-btn
|
||||
variant="text"
|
||||
:color="conn.isActive ? 'warning' : 'success'"
|
||||
size="small"
|
||||
@click="toggleConnection(conn)"
|
||||
>
|
||||
{{ conn.isActive ? 'Disable' : 'Enable' }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="error"
|
||||
size="small"
|
||||
icon="mdi-delete"
|
||||
@click="confirmDeleteConnection(conn)"
|
||||
/>
|
||||
</v-card-title>
|
||||
<v-expansion-panels v-model="expandedSimpleFinPanels" multiple>
|
||||
<v-expansion-panel v-for="conn in simpleFinConnections" :key="conn.id" :value="conn.id">
|
||||
<v-expansion-panel-title>
|
||||
<div class="d-flex align-center flex-grow-1 mr-2">
|
||||
<span class="text-subtitle-2">{{ conn.institutionName }}</span>
|
||||
<v-chip :color="conn.isActive ? 'success' : 'grey'" size="x-small" class="ml-2">
|
||||
{{ conn.isActive ? 'Active' : 'Inactive' }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" variant="tonal" class="ml-1">
|
||||
{{ conn.linkedAccounts.length }} account{{ conn.linkedAccounts.length !== 1 ? 's' : '' }}
|
||||
</v-chip>
|
||||
<v-chip v-if="syncingConnections[conn.id]" size="x-small" color="info" class="ml-1">
|
||||
Syncing...
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<div class="d-flex align-center flex-wrap ga-2 mb-3">
|
||||
<v-btn variant="text" :color="conn.isActive ? 'warning' : 'success'" size="small" @click="toggleConnection(conn)">
|
||||
{{ conn.isActive ? 'Disable' : 'Enable' }}
|
||||
</v-btn>
|
||||
<v-btn variant="text" color="error" size="small" prepend-icon="mdi-delete" @click="confirmDeleteConnection(conn)">
|
||||
Delete
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<span class="text-body-2 text-medium-emphasis">
|
||||
Last synced: {{ conn.lastSyncAt ? new Date(conn.lastSyncAt).toLocaleString() : 'Never' }}
|
||||
</span>
|
||||
</div>
|
||||
<v-alert v-if="conn.lastSyncError" type="error" variant="tonal" density="compact" class="mb-3">
|
||||
{{ conn.lastSyncError }}
|
||||
</v-alert>
|
||||
|
||||
<v-card-text class="pt-0">
|
||||
<div class="text-body-2 text-medium-emphasis mb-2">
|
||||
Last synced: {{ conn.lastSyncAt ? new Date(conn.lastSyncAt).toLocaleString() : 'Never' }}
|
||||
<v-chip v-if="syncingConnections[conn.id]" size="x-small" color="info" class="ml-2">
|
||||
Syncing...
|
||||
</v-chip>
|
||||
<span v-if="conn.lastSyncError" class="text-error ml-2">{{ conn.lastSyncError }}</span>
|
||||
<v-table density="compact" v-if="conn.linkedAccounts.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>External Account</th>
|
||||
<th>Type</th>
|
||||
<th>Balance</th>
|
||||
<th>Purrse Account</th>
|
||||
<th>Enabled</th>
|
||||
<th>Last Synced</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="la in conn.linkedAccounts" :key="la.id">
|
||||
<td>{{ la.externalAccountName }}</td>
|
||||
<td>{{ la.externalAccountType }}</td>
|
||||
<td>{{ la.lastKnownBalance != null ? formatCurrency(la.lastKnownBalance) : '-' }}</td>
|
||||
<td>
|
||||
<v-select
|
||||
:model-value="la.accountId"
|
||||
:items="accountOptions"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
placeholder="Select account..."
|
||||
@update:model-value="(val: string | null) => updateLinkedAccount(la, val)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-switch
|
||||
:model-value="la.isEnabled"
|
||||
density="compact"
|
||||
hide-details
|
||||
color="primary"
|
||||
@update:model-value="(val: boolean | null) => toggleLinkedAccount(la, val ?? false)"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-body-2">{{ la.lastSyncAt ? new Date(la.lastSyncAt).toLocaleString() : 'Never' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<!-- Plaid sub-tab -->
|
||||
<v-tabs-window-item value="plaid">
|
||||
<!-- Plaid API Credentials -->
|
||||
<v-card variant="outlined" class="mb-4">
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon size="small" class="mr-1">mdi-key</v-icon>
|
||||
<span class="text-subtitle-2">API Credentials</span>
|
||||
<v-chip v-if="plaidCredentials.isConfigured" color="success" size="x-small" class="ml-2">Configured</v-chip>
|
||||
<v-chip v-else color="warning" size="x-small" class="ml-2">Not configured</v-chip>
|
||||
</div>
|
||||
|
||||
<v-table density="compact" v-if="conn.linkedAccounts.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>External Account</th>
|
||||
<th>Type</th>
|
||||
<th>Balance</th>
|
||||
<th>Purrse Account</th>
|
||||
<th>Enabled</th>
|
||||
<th>Last Synced</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="la in conn.linkedAccounts" :key="la.id">
|
||||
<td>{{ la.externalAccountName }}</td>
|
||||
<td>{{ la.externalAccountType }}</td>
|
||||
<td>{{ la.lastKnownBalance != null ? formatCurrency(la.lastKnownBalance) : '-' }}</td>
|
||||
<td>
|
||||
<v-select
|
||||
:model-value="la.accountId"
|
||||
:items="accountOptions"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
placeholder="Select account..."
|
||||
@update:model-value="(val: string | null) => updateLinkedAccount(la, val)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-switch
|
||||
:model-value="la.isEnabled"
|
||||
density="compact"
|
||||
hide-details
|
||||
color="primary"
|
||||
@update:model-value="(val: boolean | null) => toggleLinkedAccount(la, val ?? false)"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-body-2">{{ la.lastSyncAt ? new Date(la.lastSyncAt).toLocaleString() : 'Never' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
<p class="text-body-2 text-medium-emphasis mb-3">
|
||||
Enter your Plaid developer credentials. Get them from your Plaid dashboard.
|
||||
</p>
|
||||
<v-row dense>
|
||||
<v-col cols="12" sm="4">
|
||||
<v-text-field
|
||||
v-model="plaidForm.clientId"
|
||||
label="Client ID"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:placeholder="plaidCredentials.isConfigured ? '(saved)' : ''"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4">
|
||||
<v-text-field
|
||||
v-model="plaidForm.secret"
|
||||
label="Secret"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
type="password"
|
||||
:placeholder="plaidCredentials.isConfigured ? '(saved)' : ''"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="2">
|
||||
<v-select
|
||||
v-model="plaidForm.environment"
|
||||
:items="plaidEnvironments"
|
||||
label="Environment"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="2" class="d-flex align-center">
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="plaidCredsSaving"
|
||||
:disabled="!plaidForm.clientId || !plaidForm.secret"
|
||||
@click="savePlaidCredentials"
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Plaid Section -->
|
||||
<v-card class="mb-6">
|
||||
<v-card-title class="d-flex align-center flex-wrap ga-2">
|
||||
<v-icon class="mr-1">mdi-bank</v-icon>
|
||||
Plaid
|
||||
<v-chip color="blue" size="small">{{ plaidConnections.length }}</v-chip>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
prepend-icon="mdi-bank-plus"
|
||||
:loading="plaidLoading"
|
||||
:disabled="!plaidCredentials.isConfigured"
|
||||
@click="startPlaidLink"
|
||||
>
|
||||
Connect
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
prepend-icon="mdi-sync"
|
||||
:loading="syncingAllPlaid"
|
||||
:disabled="plaidConnections.filter(c => c.isActive).length === 0"
|
||||
@click="syncAllPlaid"
|
||||
>
|
||||
Sync All
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<!-- Plaid API Credentials -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon size="small" class="mr-1">mdi-key</v-icon>
|
||||
<span class="text-subtitle-2">API Credentials</span>
|
||||
<v-chip v-if="plaidCredentials.isConfigured" color="success" size="x-small" class="ml-2">Configured</v-chip>
|
||||
<v-chip v-else color="warning" size="x-small" class="ml-2">Not configured</v-chip>
|
||||
</div>
|
||||
<p class="text-body-2 text-medium-emphasis mb-3">
|
||||
Enter your Plaid developer credentials. Get them from your Plaid dashboard.
|
||||
</p>
|
||||
<v-row dense>
|
||||
<v-col cols="12" sm="4">
|
||||
<v-text-field
|
||||
v-model="plaidForm.clientId"
|
||||
label="Client ID"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:placeholder="plaidCredentials.isConfigured ? '(saved)' : ''"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4">
|
||||
<v-text-field
|
||||
v-model="plaidForm.secret"
|
||||
label="Secret"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
type="password"
|
||||
:placeholder="plaidCredentials.isConfigured ? '(saved)' : ''"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="2">
|
||||
<v-select
|
||||
v-model="plaidForm.environment"
|
||||
:items="plaidEnvironments"
|
||||
label="Environment"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="2" class="d-flex align-center">
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="plaidCredsSaving"
|
||||
:disabled="!plaidForm.clientId || !plaidForm.secret"
|
||||
@click="savePlaidCredentials"
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="d-flex align-center flex-wrap ga-2 mb-4">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
prepend-icon="mdi-bank-plus"
|
||||
:loading="plaidLoading"
|
||||
:disabled="!plaidCredentials.isConfigured"
|
||||
@click="startPlaidLink"
|
||||
>
|
||||
Connect
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
prepend-icon="mdi-sync"
|
||||
:loading="syncingAllPlaid"
|
||||
:disabled="plaidConnections.filter(c => c.isActive).length === 0"
|
||||
@click="syncAllPlaid"
|
||||
>
|
||||
Sync All
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-divider class="mb-4" />
|
||||
|
||||
<v-alert v-if="plaidConnections.length === 0 && !loading" type="info" variant="tonal" class="mb-4">
|
||||
No Plaid connections yet. Configure your API credentials above, then click Connect.
|
||||
</v-alert>
|
||||
|
||||
<v-card v-for="conn in plaidConnections" :key="conn.id" variant="outlined" class="mb-3">
|
||||
<v-card-title class="d-flex align-center text-subtitle-1 py-2">
|
||||
{{ conn.institutionName }}
|
||||
<v-spacer />
|
||||
<v-chip
|
||||
:color="conn.isActive ? 'success' : 'grey'"
|
||||
size="x-small"
|
||||
class="mr-2"
|
||||
>
|
||||
{{ conn.isActive ? 'Active' : 'Inactive' }}
|
||||
</v-chip>
|
||||
<v-btn
|
||||
variant="text"
|
||||
:color="conn.isActive ? 'warning' : 'success'"
|
||||
size="small"
|
||||
@click="toggleConnection(conn)"
|
||||
>
|
||||
{{ conn.isActive ? 'Disable' : 'Enable' }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="error"
|
||||
size="small"
|
||||
icon="mdi-delete"
|
||||
@click="confirmDeleteConnection(conn)"
|
||||
/>
|
||||
</v-card-title>
|
||||
<v-expansion-panels v-model="expandedPlaidPanels" multiple>
|
||||
<v-expansion-panel v-for="conn in plaidConnections" :key="conn.id" :value="conn.id">
|
||||
<v-expansion-panel-title>
|
||||
<div class="d-flex align-center flex-grow-1 mr-2">
|
||||
<span class="text-subtitle-2">{{ conn.institutionName }}</span>
|
||||
<v-chip :color="conn.isActive ? 'success' : 'grey'" size="x-small" class="ml-2">
|
||||
{{ conn.isActive ? 'Active' : 'Inactive' }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" variant="tonal" class="ml-1">
|
||||
{{ conn.linkedAccounts.length }} account{{ conn.linkedAccounts.length !== 1 ? 's' : '' }}
|
||||
</v-chip>
|
||||
<v-chip v-if="syncingConnections[conn.id]" size="x-small" color="info" class="ml-1">
|
||||
Syncing...
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<div class="d-flex align-center flex-wrap ga-2 mb-3">
|
||||
<v-btn variant="text" :color="conn.isActive ? 'warning' : 'success'" size="small" @click="toggleConnection(conn)">
|
||||
{{ conn.isActive ? 'Disable' : 'Enable' }}
|
||||
</v-btn>
|
||||
<v-btn variant="text" color="error" size="small" prepend-icon="mdi-delete" @click="confirmDeleteConnection(conn)">
|
||||
Delete
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<span class="text-body-2 text-medium-emphasis">
|
||||
Last synced: {{ conn.lastSyncAt ? new Date(conn.lastSyncAt).toLocaleString() : 'Never' }}
|
||||
</span>
|
||||
</div>
|
||||
<v-alert v-if="conn.lastSyncError" type="error" variant="tonal" density="compact" class="mb-3">
|
||||
{{ conn.lastSyncError }}
|
||||
</v-alert>
|
||||
|
||||
<v-card-text class="pt-0">
|
||||
<div class="text-body-2 text-medium-emphasis mb-2">
|
||||
Last synced: {{ conn.lastSyncAt ? new Date(conn.lastSyncAt).toLocaleString() : 'Never' }}
|
||||
<v-chip v-if="syncingConnections[conn.id]" size="x-small" color="info" class="ml-2">
|
||||
Syncing...
|
||||
</v-chip>
|
||||
<span v-if="conn.lastSyncError" class="text-error ml-2">{{ conn.lastSyncError }}</span>
|
||||
</div>
|
||||
<v-table density="compact" v-if="conn.linkedAccounts.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>External Account</th>
|
||||
<th>Type</th>
|
||||
<th>Balance</th>
|
||||
<th>Purrse Account</th>
|
||||
<th>Enabled</th>
|
||||
<th>Last Synced</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="la in conn.linkedAccounts" :key="la.id">
|
||||
<td>{{ la.externalAccountName }}</td>
|
||||
<td>{{ la.externalAccountType }}</td>
|
||||
<td>{{ la.lastKnownBalance != null ? formatCurrency(la.lastKnownBalance) : '-' }}</td>
|
||||
<td>
|
||||
<v-select
|
||||
:model-value="la.accountId"
|
||||
:items="accountOptions"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
placeholder="Select account..."
|
||||
@update:model-value="(val: string | null) => updateLinkedAccount(la, val)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-switch
|
||||
:model-value="la.isEnabled"
|
||||
density="compact"
|
||||
hide-details
|
||||
color="primary"
|
||||
@update:model-value="(val: boolean | null) => toggleLinkedAccount(la, val ?? false)"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-body-2">{{ la.lastSyncAt ? new Date(la.lastSyncAt).toLocaleString() : 'Never' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
||||
<v-table density="compact" v-if="conn.linkedAccounts.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>External Account</th>
|
||||
<th>Type</th>
|
||||
<th>Balance</th>
|
||||
<th>Purrse Account</th>
|
||||
<th>Enabled</th>
|
||||
<th>Last Synced</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="la in conn.linkedAccounts" :key="la.id">
|
||||
<td>{{ la.externalAccountName }}</td>
|
||||
<td>{{ la.externalAccountType }}</td>
|
||||
<td>{{ la.lastKnownBalance != null ? formatCurrency(la.lastKnownBalance) : '-' }}</td>
|
||||
<td>
|
||||
<v-select
|
||||
:model-value="la.accountId"
|
||||
:items="accountOptions"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
placeholder="Select account..."
|
||||
@update:model-value="(val: string | null) => updateLinkedAccount(la, val)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-switch
|
||||
:model-value="la.isEnabled"
|
||||
density="compact"
|
||||
hide-details
|
||||
color="primary"
|
||||
@update:model-value="(val: boolean | null) => toggleLinkedAccount(la, val ?? false)"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-body-2">{{ la.lastSyncAt ? new Date(la.lastSyncAt).toLocaleString() : 'Never' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
||||
<div class="d-flex align-center mt-3 ga-2">
|
||||
<v-select
|
||||
v-model="syncIntervals[conn.id]"
|
||||
:items="intervalOptions"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
label="Sync Interval"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
style="max-width: 200px"
|
||||
@update:model-value="() => updateConnectionInterval(conn)"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div class="d-flex align-center mt-3 ga-2">
|
||||
<v-select
|
||||
v-model="syncIntervals[conn.id]"
|
||||
:items="intervalOptions"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
label="Sync Interval"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
style="max-width: 200px"
|
||||
@update:model-value="() => updateConnectionInterval(conn)"
|
||||
/>
|
||||
</div>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
|
||||
<!-- Account Mapping Dialog -->
|
||||
<v-dialog v-model="showMappingDialog" max-width="700" persistent>
|
||||
@@ -840,7 +829,32 @@ import type { SyncConnection, LinkedAccountInfo, Account, SyncLogEntry, PlaidCre
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const activeTab = ref('profile')
|
||||
// UI state persistence
|
||||
const SETTINGS_UI_KEY = 'purrse-settings-ui-state'
|
||||
|
||||
function loadUiState(): Record<string, any> | null {
|
||||
try {
|
||||
const stored = localStorage.getItem(SETTINGS_UI_KEY)
|
||||
return stored ? JSON.parse(stored) : null
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
function saveUiState() {
|
||||
localStorage.setItem(SETTINGS_UI_KEY, JSON.stringify({
|
||||
settingsTab: activeTab.value,
|
||||
providerTab: activeProviderTab.value,
|
||||
expandedSimpleFin: expandedSimpleFinPanels.value,
|
||||
expandedPlaid: expandedPlaidPanels.value
|
||||
}))
|
||||
}
|
||||
|
||||
const _savedUiState = loadUiState()
|
||||
const activeTab = ref(_savedUiState?.settingsTab || 'profile')
|
||||
const activeProviderTab = ref(_savedUiState?.providerTab || 'simplefin')
|
||||
const expandedSimpleFinPanels = ref<string[]>(_savedUiState?.expandedSimpleFin || [])
|
||||
const expandedPlaidPanels = ref<string[]>(_savedUiState?.expandedPlaid || [])
|
||||
|
||||
watch([activeTab, activeProviderTab, expandedSimpleFinPanels, expandedPlaidPanels], saveUiState, { deep: true })
|
||||
|
||||
// Profile
|
||||
const profile = ref<UserProfile | null>(null)
|
||||
|
||||
Reference in New Issue
Block a user