feat(accounting): move Treuhänder export onto the Tax page
The standalone 'Treuhänder export' tab duplicated the Tax page's period/ currency filters over the same data. Fold the collective-journal export into the Tax page as a third export action (target-tool format picker: generic / Banana / bexio), beside Export CSV/PDF, with a link to its Chart-of-accounts config. Removes the Accounting sub-nav 'export' tab (old /export route now redirects to the Tax page); keeps Chart of accounts as its own setup tab. Deletes the now-orphaned LedgerExportPage. Build + JSON parse green.
This commit is contained in:
@@ -70,7 +70,6 @@ import { AccountingLayout, AccountingIndex } from './components/admin/Accounting
|
|||||||
import { AccountingInboxPage } from './pages/admin/accounting/AccountingInboxPage';
|
import { AccountingInboxPage } from './pages/admin/accounting/AccountingInboxPage';
|
||||||
import { ExpensesLedgerPage } from './pages/admin/accounting/ExpensesLedgerPage';
|
import { ExpensesLedgerPage } from './pages/admin/accounting/ExpensesLedgerPage';
|
||||||
import { ChartOfAccountsPage } from './pages/admin/accounting/ChartOfAccountsPage';
|
import { ChartOfAccountsPage } from './pages/admin/accounting/ChartOfAccountsPage';
|
||||||
import { LedgerExportPage } from './pages/admin/accounting/LedgerExportPage';
|
|
||||||
import { RequireFeature } from './components/admin/RequireFeature';
|
import { RequireFeature } from './components/admin/RequireFeature';
|
||||||
import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon, RobotsMetaTags, CMSContentBlock, Loading } from './components/common';
|
import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon, RobotsMetaTags, CMSContentBlock, Loading } from './components/common';
|
||||||
import { MaintenanceWrapper } from './components/MaintenanceWrapper';
|
import { MaintenanceWrapper } from './components/MaintenanceWrapper';
|
||||||
@@ -288,7 +287,9 @@ function App() {
|
|||||||
</Route>
|
</Route>
|
||||||
<Route element={<RequireFeature flag="taxReport" />}>
|
<Route element={<RequireFeature flag="taxReport" />}>
|
||||||
<Route path="tax-report" element={<TaxReportPage />} />
|
<Route path="tax-report" element={<TaxReportPage />} />
|
||||||
<Route path="export" element={<LedgerExportPage />} />
|
{/* Treuhänder export moved onto the Tax page; keep
|
||||||
|
the old path working for bookmarks. */}
|
||||||
|
<Route path="export" element={<Navigate to="/admin/accounting/tax-report" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
{/* Chart of accounts + VAT codes (Layer A) — gated by
|
{/* Chart of accounts + VAT codes (Layer A) — gated by
|
||||||
the accounting master flag alongside the section. */}
|
the accounting master flag alongside the section. */}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavLink, Outlet, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
import { NavLink, Outlet, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Landmark, Calculator, Inbox, Wallet, BookOpen, FileSpreadsheet } from 'lucide-react';
|
import { Landmark, Calculator, Inbox, Wallet, BookOpen } from 'lucide-react';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
import { useFeatureFlags, type FeatureKey } from '../../contexts/FeatureFlagsContext';
|
import { useFeatureFlags, type FeatureKey } from '../../contexts/FeatureFlagsContext';
|
||||||
|
|
||||||
@@ -45,18 +45,13 @@ export const AccountingLayout: React.FC = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'tax-report',
|
key: 'tax-report',
|
||||||
|
// The Treuhänder export now lives ON the Tax page (same period/currency
|
||||||
|
// filters, same data) instead of a separate sub-tab — see TaxReportPage.
|
||||||
to: '/admin/accounting/tax-report',
|
to: '/admin/accounting/tax-report',
|
||||||
label: t('accounting.subnav.taxReport', 'Tax'),
|
label: t('accounting.subnav.taxReport', 'Tax'),
|
||||||
icon: Calculator,
|
icon: Calculator,
|
||||||
featureFlag: 'taxReport',
|
featureFlag: 'taxReport',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'export',
|
|
||||||
to: '/admin/accounting/export',
|
|
||||||
label: t('accounting.subnav.export', 'Treuhänder export'),
|
|
||||||
icon: FileSpreadsheet,
|
|
||||||
featureFlag: 'taxReport',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'ledger',
|
key: 'ledger',
|
||||||
to: '/admin/accounting/ledger',
|
to: '/admin/accounting/ledger',
|
||||||
|
|||||||
@@ -3937,6 +3937,9 @@
|
|||||||
"currency": "Währung"
|
"currency": "Währung"
|
||||||
},
|
},
|
||||||
"exportPdf": "PDF exportieren",
|
"exportPdf": "PDF exportieren",
|
||||||
|
"ledgerExport": "Treuhänder-Export",
|
||||||
|
"ledgerExportHint": "Doppelte Buchung für Ihren Treuhänder, abgebildet über Ihren Kontenplan.",
|
||||||
|
"ledgerExportConfigure": "Einrichten →",
|
||||||
"exportCsv": "CSV exportieren",
|
"exportCsv": "CSV exportieren",
|
||||||
"exportFailed": "Export fehlgeschlagen. Bitte erneut versuchen.",
|
"exportFailed": "Export fehlgeschlagen. Bitte erneut versuchen.",
|
||||||
"errorTitle": "Steuerliste konnte nicht geladen werden",
|
"errorTitle": "Steuerliste konnte nicht geladen werden",
|
||||||
|
|||||||
@@ -3937,6 +3937,9 @@
|
|||||||
"currency": "Currency"
|
"currency": "Currency"
|
||||||
},
|
},
|
||||||
"exportPdf": "Export PDF",
|
"exportPdf": "Export PDF",
|
||||||
|
"ledgerExport": "Treuhänder export",
|
||||||
|
"ledgerExportHint": "Double-entry journal for your accountant, mapped via your Chart of accounts.",
|
||||||
|
"ledgerExportConfigure": "Configure →",
|
||||||
"exportCsv": "Export CSV",
|
"exportCsv": "Export CSV",
|
||||||
"exportFailed": "Export failed. Please try again.",
|
"exportFailed": "Export failed. Please try again.",
|
||||||
"errorTitle": "Could not load tax report",
|
"errorTitle": "Could not load tax report",
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
/**
|
|
||||||
* Accounting → Treuhänder export (Layer A).
|
|
||||||
*
|
|
||||||
* Picks a period + currency + target tool, then downloads the collective
|
|
||||||
* journal (accrual Buchungssätze) as CSV for import into the Treuhänder's
|
|
||||||
* double-entry software. Output is a guideline — disclaimer on the page.
|
|
||||||
*/
|
|
||||||
import React, { useMemo, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import { FileSpreadsheet, Download, AlertCircle } from 'lucide-react';
|
|
||||||
import { Button, Card, CardContent, LocalizedDateInput } from '../../../components/common';
|
|
||||||
import { ledgerService, type ExportFormat } from '../../../services/ledger.service';
|
|
||||||
|
|
||||||
const selectCls = 'w-full rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 px-3 py-2 text-sm text-neutral-900 dark:text-neutral-100 focus:outline-none focus:ring-2 focus:ring-primary-500';
|
|
||||||
const labelCls = 'block text-xs font-medium text-neutral-700 dark:text-neutral-300 mb-1';
|
|
||||||
|
|
||||||
type PeriodPreset = 'thisYear' | 'lastYear' | 'thisQuarter' | 'lastQuarter' | 'custom';
|
|
||||||
const FORMATS: ExportFormat[] = ['generic', 'banana', 'bexio'];
|
|
||||||
|
|
||||||
function periodForPreset(preset: PeriodPreset, today = new Date()): { from: string; to: string } {
|
|
||||||
const y = today.getFullYear();
|
|
||||||
const pad = (n: number) => String(n).padStart(2, '0');
|
|
||||||
if (preset === 'thisYear') return { from: `${y}-01-01`, to: `${y}-12-31` };
|
|
||||||
if (preset === 'lastYear') return { from: `${y - 1}-01-01`, to: `${y - 1}-12-31` };
|
|
||||||
const quarter = Math.floor(today.getMonth() / 3);
|
|
||||||
if (preset === 'thisQuarter') {
|
|
||||||
const sm = quarter * 3; const em = sm + 2;
|
|
||||||
return { from: `${y}-${pad(sm + 1)}-01`, to: `${y}-${pad(em + 1)}-${pad(new Date(y, em + 1, 0).getDate())}` };
|
|
||||||
}
|
|
||||||
let qy = y; let q = quarter - 1; if (q < 0) { q = 3; qy = y - 1; }
|
|
||||||
const sm = q * 3; const em = sm + 2;
|
|
||||||
return { from: `${qy}-${pad(sm + 1)}-01`, to: `${qy}-${pad(em + 1)}-${pad(new Date(qy, em + 1, 0).getDate())}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
function triggerDownload(url: string, filename: string) {
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url; a.download = filename; a.style.display = 'none';
|
|
||||||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 4000);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LedgerExportPage: React.FC = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [preset, setPreset] = useState<PeriodPreset>('thisYear');
|
|
||||||
const initial = useMemo(() => periodForPreset('thisYear'), []);
|
|
||||||
const [from, setFrom] = useState(initial.from);
|
|
||||||
const [to, setTo] = useState(initial.to);
|
|
||||||
const [currency, setCurrency] = useState('CHF');
|
|
||||||
const [format, setFormat] = useState<ExportFormat>('generic');
|
|
||||||
const [busy, setBusy] = useState(false);
|
|
||||||
|
|
||||||
const onPreset = (next: PeriodPreset) => {
|
|
||||||
setPreset(next);
|
|
||||||
if (next !== 'custom') { const p = periodForPreset(next); setFrom(p.from); setTo(p.to); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExport = async () => {
|
|
||||||
setBusy(true);
|
|
||||||
try {
|
|
||||||
const { url, filename } = await ledgerService.downloadExportUrl({ from, to, currency, format });
|
|
||||||
triggerDownload(url, filename);
|
|
||||||
} catch (e: any) {
|
|
||||||
toast.error(e?.response?.data?.error || e.message || t('ledger.export.failed', 'Export failed.'));
|
|
||||||
} finally { setBusy(false); }
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card padding="md">
|
|
||||||
<CardContent className="p-0">
|
|
||||||
<div className="flex items-start gap-3 mb-4">
|
|
||||||
<div className="w-10 h-10 rounded-lg bg-accent-soft text-on-accent-soft flex items-center justify-center flex-shrink-0">
|
|
||||||
<FileSpreadsheet className="w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">{t('ledger.export.title', 'Treuhänder export')}</h1>
|
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-0.5">{t('ledger.export.intro', 'Download the collective journal (revenue + costs as accrual postings with account and VAT codes) for import into your Treuhänder’s accounting software.')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3 max-w-md">
|
|
||||||
<div>
|
|
||||||
<label className={labelCls}>{t('taxReport.filters.period', 'Period')}</label>
|
|
||||||
<select value={preset} onChange={(e) => onPreset(e.target.value as PeriodPreset)} className={selectCls}>
|
|
||||||
<option value="thisYear">{t('taxReport.filters.thisYear', 'This year')}</option>
|
|
||||||
<option value="lastYear">{t('taxReport.filters.lastYear', 'Last year')}</option>
|
|
||||||
<option value="thisQuarter">{t('taxReport.filters.thisQuarter', 'This quarter')}</option>
|
|
||||||
<option value="lastQuarter">{t('taxReport.filters.lastQuarter', 'Last quarter')}</option>
|
|
||||||
<option value="custom">{t('taxReport.filters.custom', 'Custom range')}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<div><label className={labelCls}>{t('taxReport.filters.from', 'From')}</label><LocalizedDateInput value={from} onChange={(iso) => { setFrom(iso); setPreset('custom'); }} /></div>
|
|
||||||
<div><label className={labelCls}>{t('taxReport.filters.to', 'To')}</label><LocalizedDateInput value={to} onChange={(iso) => { setTo(iso); setPreset('custom'); }} /></div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<div>
|
|
||||||
<label className={labelCls}>{t('taxReport.filters.currency', 'Currency')}</label>
|
|
||||||
<select value={currency} onChange={(e) => setCurrency(e.target.value)} className={selectCls}>
|
|
||||||
<option value="CHF">CHF</option><option value="EUR">EUR</option><option value="USD">USD</option><option value="GBP">GBP</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelCls}>{t('ledger.export.format', 'Target tool')}</label>
|
|
||||||
<select value={format} onChange={(e) => setFormat(e.target.value as ExportFormat)} className={selectCls}>
|
|
||||||
{FORMATS.map((f) => <option key={f} value={f}>{t(`ledger.export.format_${f}`, f)}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end pt-1">
|
|
||||||
<Button onClick={handleExport} disabled={busy || !from || !to} isLoading={busy} leftIcon={<Download className="w-4 h-4" />}>
|
|
||||||
{t('ledger.export.download', 'Download CSV')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="flex items-start gap-2 mt-5 text-xs text-neutral-500 dark:text-neutral-400">
|
|
||||||
<AlertCircle className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
|
||||||
<span>{t('ledger.export.disclaimer', 'Accrual basis only (document dates) — payments/bank movements are not included. Account + VAT codes follow your Chart-of-accounts mapping. Always review the import with your Treuhänder before filing.')}</span>
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LedgerExportPage;
|
|
||||||
@@ -17,7 +17,8 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Calculator, Download, FileDown, AlertCircle } from 'lucide-react';
|
import { Calculator, Download, FileDown, FileSpreadsheet, AlertCircle } from 'lucide-react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { Button, Card, Loading, LocalizedDateInput } from '../../../components/common';
|
import { Button, Card, Loading, LocalizedDateInput } from '../../../components/common';
|
||||||
|
|
||||||
// Lightweight native select styled to match Input — the common barrel
|
// Lightweight native select styled to match Input — the common barrel
|
||||||
@@ -26,9 +27,12 @@ import { Button, Card, Loading, LocalizedDateInput } from '../../../components/c
|
|||||||
const selectClassName =
|
const selectClassName =
|
||||||
'w-full rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 px-3 py-2 text-sm text-neutral-900 dark:text-neutral-100 focus:outline-none focus:ring-2 focus:ring-primary-500';
|
'w-full rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 px-3 py-2 text-sm text-neutral-900 dark:text-neutral-100 focus:outline-none focus:ring-2 focus:ring-primary-500';
|
||||||
import { taxReportService, type TaxReportParams } from '../../../services/taxReport.service';
|
import { taxReportService, type TaxReportParams } from '../../../services/taxReport.service';
|
||||||
|
import { ledgerService, type ExportFormat } from '../../../services/ledger.service';
|
||||||
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
|
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
const LEDGER_FORMATS: ExportFormat[] = ['generic', 'banana', 'bexio'];
|
||||||
|
|
||||||
type PeriodPreset = 'thisYear' | 'lastYear' | 'thisQuarter' | 'lastQuarter' | 'custom';
|
type PeriodPreset = 'thisYear' | 'lastYear' | 'thisQuarter' | 'lastQuarter' | 'custom';
|
||||||
|
|
||||||
function isoDate(d: Date): string {
|
function isoDate(d: Date): string {
|
||||||
@@ -95,7 +99,10 @@ export const TaxReportPage: React.FC = () => {
|
|||||||
const [from, setFrom] = useState(initialPeriod.from);
|
const [from, setFrom] = useState(initialPeriod.from);
|
||||||
const [to, setTo] = useState(initialPeriod.to);
|
const [to, setTo] = useState(initialPeriod.to);
|
||||||
const [currency, setCurrency] = useState<string>('CHF');
|
const [currency, setCurrency] = useState<string>('CHF');
|
||||||
const [isExporting, setIsExporting] = useState<'pdf' | 'csv' | null>(null);
|
const [isExporting, setIsExporting] = useState<'pdf' | 'csv' | 'ledger' | null>(null);
|
||||||
|
// Treuhänder (collective-journal) export — same period/currency as the
|
||||||
|
// report; target tool picks the import format (generic / Banana / bexio).
|
||||||
|
const [ledgerFormat, setLedgerFormat] = useState<ExportFormat>('generic');
|
||||||
// Unified-ledger sort (#5). Defaults to date ascending — matches the
|
// Unified-ledger sort (#5). Defaults to date ascending — matches the
|
||||||
// server-side order so the first paint is stable.
|
// server-side order so the first paint is stable.
|
||||||
const [sort, setSort] = useState<{ key: string; dir: 'asc' | 'desc' }>({ key: 'date', dir: 'asc' });
|
const [sort, setSort] = useState<{ key: string; dir: 'asc' | 'desc' }>({ key: 'date', dir: 'asc' });
|
||||||
@@ -136,6 +143,22 @@ export const TaxReportPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Treuhänder collective-journal export (double-entry postings with account +
|
||||||
|
// VAT codes) — reuses the page's period/currency, adds the target-tool format.
|
||||||
|
const handleLedgerExport = async () => {
|
||||||
|
setIsExporting('ledger');
|
||||||
|
try {
|
||||||
|
const { url, filename } = await ledgerService.downloadExportUrl({ from, to, currency, format: ledgerFormat });
|
||||||
|
triggerBrowserDownload(url, filename);
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error(err?.response?.data?.error || t('taxReport.exportFailed', 'Export failed. Please try again.'));
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
setIsExporting(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Per maintainer: every CH/LI/DE/AT-based business writes 1'000.00
|
// Per maintainer: every CH/LI/DE/AT-based business writes 1'000.00
|
||||||
// regardless of document language, so we default to de-CH (the only
|
// regardless of document language, so we default to de-CH (the only
|
||||||
// Intl locale producing apostrophe thousands). Non-DACH operators
|
// Intl locale producing apostrophe thousands). Non-DACH operators
|
||||||
@@ -271,7 +294,8 @@ export const TaxReportPage: React.FC = () => {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center justify-end gap-2 pt-1">
|
<div className="space-y-2 pt-1">
|
||||||
|
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleExport('csv')}
|
onClick={() => handleExport('csv')}
|
||||||
@@ -291,6 +315,38 @@ export const TaxReportPage: React.FC = () => {
|
|||||||
{t('taxReport.exportPdf', 'Export PDF')}
|
{t('taxReport.exportPdf', 'Export PDF')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Treuhänder collective-journal export — moved here from its own
|
||||||
|
tab; reuses the same period/currency. Format = target tool. */}
|
||||||
|
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||||
|
<select
|
||||||
|
value={ledgerFormat}
|
||||||
|
onChange={(e) => setLedgerFormat(e.target.value as ExportFormat)}
|
||||||
|
disabled={exportsDisabled}
|
||||||
|
aria-label={t('ledger.export.format', 'Target tool') as string}
|
||||||
|
className={`${selectClassName} w-auto`}
|
||||||
|
>
|
||||||
|
{LEDGER_FORMATS.map((f) => (
|
||||||
|
<option key={f} value={f}>{t(`ledger.export.format_${f}`, f)}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleLedgerExport}
|
||||||
|
disabled={exportsDisabled}
|
||||||
|
isLoading={isExporting === 'ledger'}
|
||||||
|
leftIcon={<FileSpreadsheet className="w-4 h-4" />}
|
||||||
|
>
|
||||||
|
{t('taxReport.ledgerExport', 'Treuhänder export')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-neutral-400 dark:text-neutral-500 text-right">
|
||||||
|
{t('taxReport.ledgerExportHint', 'Double-entry journal for your accountant, mapped via your Chart of accounts.')}{' '}
|
||||||
|
<Link to="/admin/accounting/ledger" className="underline hover:text-neutral-600 dark:hover:text-neutral-300">
|
||||||
|
{t('taxReport.ledgerExportConfigure', 'Configure →')}
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user