mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-13 23:12:16 +00:00
Reformat files.
This commit is contained in:
committed by
Elisiário Couto
parent
65404848aa
commit
eb38264c68
@@ -4,11 +4,17 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="48x48" />
|
||||
<link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||
/>
|
||||
<title>Leggen</title>
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="description" content="Personal finance management application" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Personal finance management application"
|
||||
/>
|
||||
<meta name="application-name" content="Leggen" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
@@ -21,8 +27,16 @@
|
||||
|
||||
<!-- Dynamic theme-color - will be updated by JavaScript -->
|
||||
<meta name="theme-color" content="#0b74de" id="theme-color-meta" />
|
||||
<meta name="msapplication-navbutton-color" content="#0b74de" id="ms-theme-color-meta" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" id="apple-status-bar-meta" />
|
||||
<meta
|
||||
name="msapplication-navbutton-color"
|
||||
content="#0b74de"
|
||||
id="ms-theme-color-meta"
|
||||
/>
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="default"
|
||||
id="apple-status-bar-meta"
|
||||
/>
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
|
||||
|
||||
@@ -144,7 +144,8 @@ export default function AccountSettings() {
|
||||
<CardHeader>
|
||||
<CardTitle>Account Management</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your connected bank accounts and customize their display names
|
||||
Manage your connected bank accounts and customize their display
|
||||
names
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -324,7 +325,8 @@ export default function AccountSettings() {
|
||||
<CardHeader>
|
||||
<CardTitle>Add New Bank Account</CardTitle>
|
||||
<CardDescription>
|
||||
Connect additional bank accounts to track all your finances in one place
|
||||
Connect additional bank accounts to track all your finances in one
|
||||
place
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
@@ -332,7 +334,8 @@ export default function AccountSettings() {
|
||||
<div className="p-4 bg-muted rounded-lg">
|
||||
<Plus className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Bank connection functionality is coming soon. Stay tuned for updates!
|
||||
Bank connection functionality is coming soon. Stay tuned for
|
||||
updates!
|
||||
</p>
|
||||
</div>
|
||||
<Button disabled variant="outline">
|
||||
|
||||
@@ -69,7 +69,11 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
asChild
|
||||
className="data-[slot=sidebar-menu-button]:!p-1.5"
|
||||
>
|
||||
<Link to="/" className="flex items-center space-x-2" onClick={handleNavigationClick}>
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center space-x-2"
|
||||
onClick={handleNavigationClick}
|
||||
>
|
||||
<Logo size={24} />
|
||||
<span className="text-base font-semibold">Leggen</span>
|
||||
</Link>
|
||||
@@ -138,7 +142,10 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<div className="border-t border-border/50 max-h-48 overflow-y-auto">
|
||||
{accounts.map((account) => {
|
||||
const primaryBalance = account.balances?.[0]?.amount || 0;
|
||||
const currency = account.balances?.[0]?.currency || account.currency || "EUR";
|
||||
const currency =
|
||||
account.balances?.[0]?.currency ||
|
||||
account.currency ||
|
||||
"EUR";
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -151,7 +158,9 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
</div>
|
||||
<div className="space-y-1 min-w-0 flex-1">
|
||||
<p className="text-xs font-medium text-foreground truncate">
|
||||
{account.display_name || account.name || "Unnamed Account"}
|
||||
{account.display_name ||
|
||||
account.name ||
|
||||
"Unnamed Account"}
|
||||
</p>
|
||||
<p className="text-xs font-semibold text-foreground">
|
||||
{formatCurrency(primaryBalance, currency)}
|
||||
|
||||
@@ -36,7 +36,11 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "./ui/select";
|
||||
import type { NotificationSettings, NotificationService, SyncOperationsResponse } from "../types/api";
|
||||
import type {
|
||||
NotificationSettings,
|
||||
NotificationService,
|
||||
SyncOperationsResponse,
|
||||
} from "../types/api";
|
||||
|
||||
export default function Notifications() {
|
||||
const [testService, setTestService] = useState("");
|
||||
@@ -163,7 +167,8 @@ export default function Notifications() {
|
||||
No sync operations yet
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Sync operations will appear here once you start syncing your accounts.
|
||||
Sync operations will appear here once you start syncing your
|
||||
accounts.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -171,9 +176,9 @@ export default function Notifications() {
|
||||
{syncOperations.operations.slice(0, 5).map((operation) => {
|
||||
const startedAt = new Date(operation.started_at);
|
||||
const isRunning = !operation.completed_at;
|
||||
const duration = operation.duration_seconds
|
||||
const duration = operation.duration_seconds
|
||||
? `${Math.round(operation.duration_seconds)}s`
|
||||
: '';
|
||||
: "";
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -181,13 +186,15 @@ export default function Notifications() {
|
||||
className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent transition-colors"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`p-2 rounded-full ${
|
||||
isRunning
|
||||
? 'bg-blue-100 text-blue-600'
|
||||
: operation.success
|
||||
? 'bg-green-100 text-green-600'
|
||||
: 'bg-red-100 text-red-600'
|
||||
}`}>
|
||||
<div
|
||||
className={`p-2 rounded-full ${
|
||||
isRunning
|
||||
? "bg-blue-100 text-blue-600"
|
||||
: operation.success
|
||||
? "bg-green-100 text-green-600"
|
||||
: "bg-red-100 text-red-600"
|
||||
}`}
|
||||
>
|
||||
{isRunning ? (
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
) : operation.success ? (
|
||||
@@ -199,7 +206,11 @@ export default function Notifications() {
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="text-sm font-medium text-foreground">
|
||||
{isRunning ? 'Sync Running' : operation.success ? 'Sync Completed' : 'Sync Failed'}
|
||||
{isRunning
|
||||
? "Sync Running"
|
||||
: operation.success
|
||||
? "Sync Completed"
|
||||
: "Sync Failed"}
|
||||
</h4>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{operation.trigger_type}
|
||||
@@ -208,11 +219,12 @@ export default function Notifications() {
|
||||
<div className="flex items-center space-x-4 mt-1 text-xs text-muted-foreground">
|
||||
<span className="flex items-center space-x-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{startedAt.toLocaleDateString()} {startedAt.toLocaleTimeString()}</span>
|
||||
<span>
|
||||
{startedAt.toLocaleDateString()}{" "}
|
||||
{startedAt.toLocaleTimeString()}
|
||||
</span>
|
||||
</span>
|
||||
{duration && (
|
||||
<span>Duration: {duration}</span>
|
||||
)}
|
||||
{duration && <span>Duration: {duration}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,7 +235,9 @@ export default function Notifications() {
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
<span>{operation.transactions_added} new transactions</span>
|
||||
<span>
|
||||
{operation.transactions_added} new transactions
|
||||
</span>
|
||||
</div>
|
||||
{operation.errors.length > 0 && (
|
||||
<div className="flex items-center space-x-2 mt-1 text-red-600">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { X, Download, RotateCcw } from "lucide-react";
|
||||
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt(): Promise<void>;
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
||||
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
|
||||
}
|
||||
|
||||
interface PWAPromptProps {
|
||||
@@ -11,7 +11,8 @@ interface PWAPromptProps {
|
||||
}
|
||||
|
||||
export function PWAInstallPrompt({ onInstall }: PWAPromptProps) {
|
||||
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
||||
const [deferredPrompt, setDeferredPrompt] =
|
||||
useState<BeforeInstallPromptEvent | null>(null);
|
||||
const [showPrompt, setShowPrompt] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -96,7 +97,10 @@ interface PWAUpdatePromptProps {
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
export function PWAUpdatePrompt({ updateAvailable, onUpdate }: PWAUpdatePromptProps) {
|
||||
export function PWAUpdatePrompt({
|
||||
updateAvailable,
|
||||
onUpdate,
|
||||
}: PWAUpdatePromptProps) {
|
||||
const [showPrompt, setShowPrompt] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -43,7 +43,12 @@ import {
|
||||
} from "./ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
|
||||
import AccountsSkeleton from "./AccountsSkeleton";
|
||||
import type { Account, Balance, NotificationSettings, NotificationService } from "../types/api";
|
||||
import type {
|
||||
Account,
|
||||
Balance,
|
||||
NotificationSettings,
|
||||
NotificationService,
|
||||
} from "../types/api";
|
||||
|
||||
// Helper function to get status indicator color and styles
|
||||
const getStatusIndicator = (status: string) => {
|
||||
@@ -83,7 +88,9 @@ export default function Settings() {
|
||||
const [editingAccountId, setEditingAccountId] = useState<string | null>(null);
|
||||
const [editingName, setEditingName] = useState("");
|
||||
const [testService, setTestService] = useState("");
|
||||
const [testMessage, setTestMessage] = useState("Test notification from Leggen");
|
||||
const [testMessage, setTestMessage] = useState(
|
||||
"Test notification from Leggen",
|
||||
);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -239,7 +246,10 @@ export default function Settings() {
|
||||
<User className="h-4 w-4" />
|
||||
<span>Accounts</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="notifications" className="flex items-center space-x-2">
|
||||
<TabsTrigger
|
||||
value="notifications"
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Bell className="h-4 w-4" />
|
||||
<span>Notifications</span>
|
||||
</TabsTrigger>
|
||||
@@ -251,7 +261,8 @@ export default function Settings() {
|
||||
<CardHeader>
|
||||
<CardTitle>Account Management</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your connected bank accounts and customize their display names
|
||||
Manage your connected bank accounts and customize their display
|
||||
names
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -319,7 +330,8 @@ export default function Settings() {
|
||||
autoComplete="off"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleEditSave();
|
||||
if (e.key === "Escape") handleEditCancel();
|
||||
if (e.key === "Escape")
|
||||
handleEditCancel();
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
@@ -428,7 +440,8 @@ export default function Settings() {
|
||||
<CardHeader>
|
||||
<CardTitle>Add New Bank Account</CardTitle>
|
||||
<CardDescription>
|
||||
Connect additional bank accounts to track all your finances in one place
|
||||
Connect additional bank accounts to track all your finances in
|
||||
one place
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
@@ -436,7 +449,8 @@ export default function Settings() {
|
||||
<div className="p-4 bg-muted rounded-lg">
|
||||
<Plus className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Bank connection functionality is coming soon. Stay tuned for updates!
|
||||
Bank connection functionality is coming soon. Stay tuned for
|
||||
updates!
|
||||
</p>
|
||||
</div>
|
||||
<Button disabled variant="outline">
|
||||
@@ -498,7 +512,9 @@ export default function Settings() {
|
||||
disabled={!testService || testMutation.isPending}
|
||||
>
|
||||
<Send className="h-4 w-4 mr-2" />
|
||||
{testMutation.isPending ? "Sending..." : "Send Test Notification"}
|
||||
{testMutation.isPending
|
||||
? "Sending..."
|
||||
: "Send Test Notification"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -511,7 +527,9 @@ export default function Settings() {
|
||||
<Bell className="h-5 w-5 text-primary" />
|
||||
<span>Notification Services</span>
|
||||
</CardTitle>
|
||||
<CardDescription>Manage your notification services</CardDescription>
|
||||
<CardDescription>
|
||||
Manage your notification services
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
{!services || services.length === 0 ? (
|
||||
@@ -521,7 +539,8 @@ export default function Settings() {
|
||||
No notification services configured
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Configure notification services in your backend to receive alerts.
|
||||
Configure notification services in your backend to receive
|
||||
alerts.
|
||||
</p>
|
||||
</CardContent>
|
||||
) : (
|
||||
@@ -537,7 +556,9 @@ export default function Settings() {
|
||||
<div className="p-3 bg-muted rounded-full">
|
||||
{service.name.toLowerCase().includes("discord") ? (
|
||||
<MessageSquare className="h-6 w-6 text-muted-foreground" />
|
||||
) : service.name.toLowerCase().includes("telegram") ? (
|
||||
) : service.name
|
||||
.toLowerCase()
|
||||
.includes("telegram") ? (
|
||||
<Send className="h-6 w-6 text-muted-foreground" />
|
||||
) : (
|
||||
<Bell className="h-6 w-6 text-muted-foreground" />
|
||||
@@ -612,8 +633,11 @@ export default function Settings() {
|
||||
Case Insensitive Filters
|
||||
</Label>
|
||||
<p className="text-sm text-foreground">
|
||||
{notificationSettings.filters.case_insensitive.length > 0
|
||||
? notificationSettings.filters.case_insensitive.join(", ")
|
||||
{notificationSettings.filters.case_insensitive
|
||||
.length > 0
|
||||
? notificationSettings.filters.case_insensitive.join(
|
||||
", ",
|
||||
)
|
||||
: "None"}
|
||||
</p>
|
||||
</div>
|
||||
@@ -623,8 +647,11 @@ export default function Settings() {
|
||||
</Label>
|
||||
<p className="text-sm text-foreground">
|
||||
{notificationSettings.filters.case_sensitive &&
|
||||
notificationSettings.filters.case_sensitive.length > 0
|
||||
? notificationSettings.filters.case_sensitive.join(", ")
|
||||
notificationSettings.filters.case_sensitive.length >
|
||||
0
|
||||
? notificationSettings.filters.case_sensitive.join(
|
||||
", ",
|
||||
)
|
||||
: "None"}
|
||||
</p>
|
||||
</div>
|
||||
@@ -634,8 +661,8 @@ export default function Settings() {
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p>
|
||||
Configure notification settings through your backend API to
|
||||
customize filters and service configurations.
|
||||
Configure notification settings through your backend API
|
||||
to customize filters and service configurations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,9 @@ export default function System() {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-center">
|
||||
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-muted-foreground">Loading system status...</span>
|
||||
<span className="ml-2 text-muted-foreground">
|
||||
Loading system status...
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -55,8 +57,8 @@ export default function System() {
|
||||
<AlertTitle>Failed to load system data</AlertTitle>
|
||||
<AlertDescription className="space-y-3">
|
||||
<p>
|
||||
Unable to connect to the Leggen API. Please check your configuration
|
||||
and ensure the API server is running.
|
||||
Unable to connect to the Leggen API. Please check your
|
||||
configuration and ensure the API server is running.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => refetchSyncOperations()}
|
||||
@@ -81,7 +83,9 @@ export default function System() {
|
||||
<Activity className="h-5 w-5 text-primary" />
|
||||
<span>Recent Sync Operations</span>
|
||||
</CardTitle>
|
||||
<CardDescription>Latest synchronization activities and their status</CardDescription>
|
||||
<CardDescription>
|
||||
Latest synchronization activities and their status
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!syncOperations || syncOperations.operations.length === 0 ? (
|
||||
@@ -91,7 +95,8 @@ export default function System() {
|
||||
No sync operations yet
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Sync operations will appear here once you start syncing your accounts.
|
||||
Sync operations will appear here once you start syncing your
|
||||
accounts.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -101,7 +106,7 @@ export default function System() {
|
||||
const isRunning = !operation.completed_at;
|
||||
const duration = operation.duration_seconds
|
||||
? `${Math.round(operation.duration_seconds)}s`
|
||||
: '';
|
||||
: "";
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -109,13 +114,15 @@ export default function System() {
|
||||
className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent transition-colors"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`p-2 rounded-full ${
|
||||
isRunning
|
||||
? 'bg-blue-100 text-blue-600'
|
||||
: operation.success
|
||||
? 'bg-green-100 text-green-600'
|
||||
: 'bg-red-100 text-red-600'
|
||||
}`}>
|
||||
<div
|
||||
className={`p-2 rounded-full ${
|
||||
isRunning
|
||||
? "bg-blue-100 text-blue-600"
|
||||
: operation.success
|
||||
? "bg-green-100 text-green-600"
|
||||
: "bg-red-100 text-red-600"
|
||||
}`}
|
||||
>
|
||||
{isRunning ? (
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
) : operation.success ? (
|
||||
@@ -127,7 +134,11 @@ export default function System() {
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="text-sm font-medium text-foreground">
|
||||
{isRunning ? 'Sync Running' : operation.success ? 'Sync Completed' : 'Sync Failed'}
|
||||
{isRunning
|
||||
? "Sync Running"
|
||||
: operation.success
|
||||
? "Sync Completed"
|
||||
: "Sync Failed"}
|
||||
</h4>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{operation.trigger_type}
|
||||
@@ -136,11 +147,12 @@ export default function System() {
|
||||
<div className="flex items-center space-x-4 mt-1 text-xs text-muted-foreground">
|
||||
<span className="flex items-center space-x-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{startedAt.toLocaleDateString()} {startedAt.toLocaleTimeString()}</span>
|
||||
<span>
|
||||
{startedAt.toLocaleDateString()}{" "}
|
||||
{startedAt.toLocaleTimeString()}
|
||||
</span>
|
||||
</span>
|
||||
{duration && (
|
||||
<span>Duration: {duration}</span>
|
||||
)}
|
||||
{duration && <span>Duration: {duration}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,7 +163,9 @@ export default function System() {
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
<span>{operation.transactions_added} new transactions</span>
|
||||
<span>
|
||||
{operation.transactions_added} new transactions
|
||||
</span>
|
||||
</div>
|
||||
{operation.errors.length > 0 && (
|
||||
<div className="flex items-center space-x-2 mt-1 text-red-600">
|
||||
@@ -175,25 +189,31 @@ export default function System() {
|
||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
||||
<span>System Health</span>
|
||||
</CardTitle>
|
||||
<CardDescription>Overall system status and performance</CardDescription>
|
||||
<CardDescription>
|
||||
Overall system status and performance
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="text-center p-4 bg-green-50 rounded-lg border border-green-200">
|
||||
<div className="text-2xl font-bold text-green-700">
|
||||
{syncOperations?.operations.filter(op => op.success).length || 0}
|
||||
{syncOperations?.operations.filter((op) => op.success).length ||
|
||||
0}
|
||||
</div>
|
||||
<div className="text-sm text-green-600">Successful Syncs</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-red-50 rounded-lg border border-red-200">
|
||||
<div className="text-2xl font-bold text-red-700">
|
||||
{syncOperations?.operations.filter(op => !op.success && op.completed_at).length || 0}
|
||||
{syncOperations?.operations.filter(
|
||||
(op) => !op.success && op.completed_at,
|
||||
).length || 0}
|
||||
</div>
|
||||
<div className="text-sm text-red-600">Failed Syncs</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<div className="text-2xl font-bold text-blue-700">
|
||||
{syncOperations?.operations.filter(op => !op.completed_at).length || 0}
|
||||
{syncOperations?.operations.filter((op) => !op.completed_at)
|
||||
.length || 0}
|
||||
</div>
|
||||
<div className="text-sm text-blue-600">Running Operations</div>
|
||||
</div>
|
||||
|
||||
@@ -97,7 +97,6 @@ export default function TransactionsTable() {
|
||||
queryFn: apiClient.getAccounts,
|
||||
});
|
||||
|
||||
|
||||
const {
|
||||
data: transactionsResponse,
|
||||
isLoading: transactionsLoading,
|
||||
@@ -141,11 +140,7 @@ export default function TransactionsTable() {
|
||||
// Reset pagination when filters change
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [
|
||||
filterState.selectedAccount,
|
||||
filterState.startDate,
|
||||
filterState.endDate,
|
||||
]);
|
||||
}, [filterState.selectedAccount, filterState.startDate, filterState.endDate]);
|
||||
|
||||
const handleViewRaw = (transaction: Transaction) => {
|
||||
setSelectedTransaction(transaction);
|
||||
@@ -163,7 +158,6 @@ export default function TransactionsTable() {
|
||||
filterState.startDate ||
|
||||
filterState.endDate;
|
||||
|
||||
|
||||
// Define columns
|
||||
const columns: ColumnDef<Transaction>[] = [
|
||||
{
|
||||
@@ -427,10 +421,7 @@ export default function TransactionsTable() {
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<tr key={row.id} className="hover:bg-muted/50">
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className="px-6 py-4 whitespace-nowrap"
|
||||
>
|
||||
<td key={cell.id} className="px-6 py-4 whitespace-nowrap">
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
|
||||
@@ -29,13 +29,9 @@ export default function StatCard({
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
{title}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-muted-foreground">{title}</p>
|
||||
<div className="flex items-baseline">
|
||||
<p className="text-2xl font-bold text-foreground">
|
||||
{value}
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-foreground">{value}</p>
|
||||
{trend && (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -51,29 +47,31 @@ export default function StatCard({
|
||||
)}
|
||||
</div>
|
||||
{subtitle && (
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{subtitle}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn(
|
||||
"p-3 rounded-full",
|
||||
iconColor === "green" && "bg-green-100 dark:bg-green-900/20",
|
||||
iconColor === "blue" && "bg-blue-100 dark:bg-blue-900/20",
|
||||
iconColor === "red" && "bg-red-100 dark:bg-red-900/20",
|
||||
iconColor === "purple" && "bg-purple-100 dark:bg-purple-900/20",
|
||||
iconColor === "orange" && "bg-orange-100 dark:bg-orange-900/20",
|
||||
iconColor === "default" && "bg-muted"
|
||||
)}>
|
||||
<Icon className={cn(
|
||||
"h-6 w-6",
|
||||
iconColor === "green" && "text-green-600",
|
||||
iconColor === "blue" && "text-blue-600",
|
||||
iconColor === "red" && "text-red-600",
|
||||
iconColor === "purple" && "text-purple-600",
|
||||
iconColor === "orange" && "text-orange-600",
|
||||
iconColor === "default" && "text-muted-foreground"
|
||||
)} />
|
||||
<div
|
||||
className={cn(
|
||||
"p-3 rounded-full",
|
||||
iconColor === "green" && "bg-green-100 dark:bg-green-900/20",
|
||||
iconColor === "blue" && "bg-blue-100 dark:bg-blue-900/20",
|
||||
iconColor === "red" && "bg-red-100 dark:bg-red-900/20",
|
||||
iconColor === "purple" && "bg-purple-100 dark:bg-purple-900/20",
|
||||
iconColor === "orange" && "bg-orange-100 dark:bg-orange-900/20",
|
||||
iconColor === "default" && "bg-muted",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={cn(
|
||||
"h-6 w-6",
|
||||
iconColor === "green" && "text-green-600",
|
||||
iconColor === "blue" && "text-blue-600",
|
||||
iconColor === "red" && "text-red-600",
|
||||
iconColor === "purple" && "text-purple-600",
|
||||
iconColor === "orange" && "text-orange-600",
|
||||
iconColor === "default" && "text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -70,7 +70,6 @@ export function ActiveFilterChips({
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const handleRemoveChip = (key: keyof FilterState) => {
|
||||
switch (key) {
|
||||
case "startDate":
|
||||
|
||||
@@ -91,7 +91,6 @@ export function FilterBar({
|
||||
className="w-[220px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Mobile Layout */}
|
||||
@@ -129,7 +128,6 @@ export function FilterBar({
|
||||
onDateRangeChange={handleDateRangeChange}
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
@@ -31,27 +31,27 @@ const buttonVariants = cva(
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button, buttonVariants };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
@@ -9,14 +9,14 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input }
|
||||
export { Input };
|
||||
|
||||
@@ -15,7 +15,9 @@ export function Logo({ className = "", size = 32 }: LogoProps) {
|
||||
aria-labelledby="logo-title logo-desc"
|
||||
>
|
||||
<title id="logo-title">leggen — stylized italic L</title>
|
||||
<desc id="logo-desc">Square gradient background with italic white L.</desc>
|
||||
<desc id="logo-desc">
|
||||
Square gradient background with italic white L.
|
||||
</desc>
|
||||
|
||||
<defs>
|
||||
<linearGradient id="logo-bg" x1="0" y1="0" x2="1" y2="1">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
@@ -9,7 +9,7 @@ const Separator = React.forwardRef<
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
ref,
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
@@ -18,12 +18,12 @@ const Separator = React.forwardRef<
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator }
|
||||
export { Separator };
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
@@ -22,13 +22,13 @@ const SheetOverlay = React.forwardRef<
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
@@ -46,8 +46,8 @@ const sheetVariants = cva(
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
@@ -71,8 +71,8 @@ const SheetContent = React.forwardRef<
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
@@ -81,12 +81,12 @@ const SheetHeader = ({
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
);
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
@@ -95,12 +95,12 @@ const SheetFooter = ({
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
);
|
||||
SheetFooter.displayName = "SheetFooter";
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
@@ -111,8 +111,8 @@ const SheetTitle = React.forwardRef<
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
@@ -123,8 +123,8 @@ const SheetDescription = React.forwardRef<
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
@@ -137,4 +137,4 @@ export {
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { PanelLeft } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { PanelLeft } from "lucide-react";
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
} from "@/components/ui/sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "16rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed"
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
openMobile: boolean
|
||||
setOpenMobile: (open: boolean) => void
|
||||
isMobile: boolean
|
||||
toggleSidebar: () => void
|
||||
}
|
||||
state: "expanded" | "collapsed";
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext)
|
||||
const context = React.useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.");
|
||||
}
|
||||
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
||||
const SidebarProvider = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
>(
|
||||
(
|
||||
@@ -69,36 +69,36 @@ const SidebarProvider = React.forwardRef<
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const isMobile = useIsMobile()
|
||||
const [openMobile, setOpenMobile] = React.useState(false)
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
||||
const open = openProp ?? _open
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const open = openProp ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
const openState = typeof value === "function" ? value(open) : value;
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState)
|
||||
setOpenProp(openState);
|
||||
} else {
|
||||
_setOpen(openState)
|
||||
_setOpen(openState);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open]
|
||||
)
|
||||
[setOpenProp, open],
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile
|
||||
? setOpenMobile((open) => !open)
|
||||
: setOpen((open) => !open)
|
||||
}, [isMobile, setOpen, setOpenMobile])
|
||||
: setOpen((open) => !open);
|
||||
}, [isMobile, setOpen, setOpenMobile]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
@@ -107,18 +107,18 @@ const SidebarProvider = React.forwardRef<
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault()
|
||||
toggleSidebar()
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [toggleSidebar])
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
const state = open ? "expanded" : "collapsed";
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
@@ -130,8 +130,16 @@ const SidebarProvider = React.forwardRef<
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
)
|
||||
[
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
@@ -146,7 +154,7 @@ const SidebarProvider = React.forwardRef<
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
@@ -155,17 +163,17 @@ const SidebarProvider = React.forwardRef<
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarProvider.displayName = "SidebarProvider"
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarProvider.displayName = "SidebarProvider";
|
||||
|
||||
const Sidebar = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right"
|
||||
variant?: "sidebar" | "floating" | "inset"
|
||||
collapsible?: "offcanvas" | "icon" | "none"
|
||||
side?: "left" | "right";
|
||||
variant?: "sidebar" | "floating" | "inset";
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
}
|
||||
>(
|
||||
(
|
||||
@@ -177,23 +185,23 @@ const Sidebar = React.forwardRef<
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
@@ -217,7 +225,7 @@ const Sidebar = React.forwardRef<
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -237,7 +245,7 @@ const Sidebar = React.forwardRef<
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
@@ -250,7 +258,7 @@ const Sidebar = React.forwardRef<
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -262,16 +270,16 @@ const Sidebar = React.forwardRef<
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
Sidebar.displayName = "Sidebar"
|
||||
);
|
||||
},
|
||||
);
|
||||
Sidebar.displayName = "Sidebar";
|
||||
|
||||
const SidebarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof Button>,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, onClick, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -281,23 +289,23 @@ const SidebarTrigger = React.forwardRef<
|
||||
size="icon"
|
||||
className={cn("h-7 w-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
toggleSidebar()
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeft />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
SidebarTrigger.displayName = "SidebarTrigger"
|
||||
);
|
||||
});
|
||||
SidebarTrigger.displayName = "SidebarTrigger";
|
||||
|
||||
const SidebarRail = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button">
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -314,13 +322,13 @@ const SidebarRail = React.forwardRef<
|
||||
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarRail.displayName = "SidebarRail"
|
||||
);
|
||||
});
|
||||
SidebarRail.displayName = "SidebarRail";
|
||||
|
||||
const SidebarInset = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -332,13 +340,13 @@ const SidebarInset = React.forwardRef<
|
||||
className={cn(
|
||||
"relative flex w-full flex-1 flex-col bg-background",
|
||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarInset.displayName = "SidebarInset"
|
||||
);
|
||||
});
|
||||
SidebarInset.displayName = "SidebarInset";
|
||||
|
||||
const SidebarInput = React.forwardRef<
|
||||
React.ElementRef<typeof Input>,
|
||||
@@ -350,13 +358,13 @@ const SidebarInput = React.forwardRef<
|
||||
data-sidebar="input"
|
||||
className={cn(
|
||||
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarInput.displayName = "SidebarInput"
|
||||
);
|
||||
});
|
||||
SidebarInput.displayName = "SidebarInput";
|
||||
|
||||
const SidebarHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -369,9 +377,9 @@ const SidebarHeader = React.forwardRef<
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarHeader.displayName = "SidebarHeader"
|
||||
);
|
||||
});
|
||||
SidebarHeader.displayName = "SidebarHeader";
|
||||
|
||||
const SidebarFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -384,9 +392,9 @@ const SidebarFooter = React.forwardRef<
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarFooter.displayName = "SidebarFooter"
|
||||
);
|
||||
});
|
||||
SidebarFooter.displayName = "SidebarFooter";
|
||||
|
||||
const SidebarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof Separator>,
|
||||
@@ -399,9 +407,9 @@ const SidebarSeparator = React.forwardRef<
|
||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarSeparator.displayName = "SidebarSeparator"
|
||||
);
|
||||
});
|
||||
SidebarSeparator.displayName = "SidebarSeparator";
|
||||
|
||||
const SidebarContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -413,13 +421,13 @@ const SidebarContent = React.forwardRef<
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarContent.displayName = "SidebarContent"
|
||||
);
|
||||
});
|
||||
SidebarContent.displayName = "SidebarContent";
|
||||
|
||||
const SidebarGroup = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -432,15 +440,15 @@ const SidebarGroup = React.forwardRef<
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarGroup.displayName = "SidebarGroup"
|
||||
);
|
||||
});
|
||||
SidebarGroup.displayName = "SidebarGroup";
|
||||
|
||||
const SidebarGroupLabel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@@ -449,19 +457,19 @@ const SidebarGroupLabel = React.forwardRef<
|
||||
className={cn(
|
||||
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarGroupLabel.displayName = "SidebarGroupLabel"
|
||||
);
|
||||
});
|
||||
SidebarGroupLabel.displayName = "SidebarGroupLabel";
|
||||
|
||||
const SidebarGroupAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@@ -472,13 +480,13 @@ const SidebarGroupAction = React.forwardRef<
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarGroupAction.displayName = "SidebarGroupAction"
|
||||
);
|
||||
});
|
||||
SidebarGroupAction.displayName = "SidebarGroupAction";
|
||||
|
||||
const SidebarGroupContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -490,8 +498,8 @@ const SidebarGroupContent = React.forwardRef<
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SidebarGroupContent.displayName = "SidebarGroupContent"
|
||||
));
|
||||
SidebarGroupContent.displayName = "SidebarGroupContent";
|
||||
|
||||
const SidebarMenu = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
@@ -503,8 +511,8 @@ const SidebarMenu = React.forwardRef<
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SidebarMenu.displayName = "SidebarMenu"
|
||||
));
|
||||
SidebarMenu.displayName = "SidebarMenu";
|
||||
|
||||
const SidebarMenuItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
@@ -516,8 +524,8 @@ const SidebarMenuItem = React.forwardRef<
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SidebarMenuItem.displayName = "SidebarMenuItem"
|
||||
));
|
||||
SidebarMenuItem.displayName = "SidebarMenuItem";
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
@@ -538,15 +546,15 @@ const sidebarMenuButtonVariants = cva(
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
const SidebarMenuButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
isActive?: boolean
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||
>(
|
||||
(
|
||||
@@ -559,10 +567,10 @@ const SidebarMenuButton = React.forwardRef<
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const { isMobile, state } = useSidebar()
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
@@ -573,16 +581,16 @@ const SidebarMenuButton = React.forwardRef<
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -595,19 +603,19 @@ const SidebarMenuButton = React.forwardRef<
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarMenuButton.displayName = "SidebarMenuButton"
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarMenuButton.displayName = "SidebarMenuButton";
|
||||
|
||||
const SidebarMenuAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
showOnHover?: boolean
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}
|
||||
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@@ -623,13 +631,13 @@ const SidebarMenuAction = React.forwardRef<
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarMenuAction.displayName = "SidebarMenuAction"
|
||||
);
|
||||
});
|
||||
SidebarMenuAction.displayName = "SidebarMenuAction";
|
||||
|
||||
const SidebarMenuBadge = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -645,23 +653,23 @@ const SidebarMenuBadge = React.forwardRef<
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SidebarMenuBadge.displayName = "SidebarMenuBadge"
|
||||
));
|
||||
SidebarMenuBadge.displayName = "SidebarMenuBadge";
|
||||
|
||||
const SidebarMenuSkeleton = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean
|
||||
showIcon?: boolean;
|
||||
}
|
||||
>(({ className, showIcon = false, ...props }, ref) => {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||
}, [])
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -686,9 +694,9 @@ const SidebarMenuSkeleton = React.forwardRef<
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
|
||||
);
|
||||
});
|
||||
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
|
||||
|
||||
const SidebarMenuSub = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
@@ -700,28 +708,28 @@ const SidebarMenuSub = React.forwardRef<
|
||||
className={cn(
|
||||
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SidebarMenuSub.displayName = "SidebarMenuSub"
|
||||
));
|
||||
SidebarMenuSub.displayName = "SidebarMenuSub";
|
||||
|
||||
const SidebarMenuSubItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ ...props }, ref) => <li ref={ref} {...props} />)
|
||||
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
|
||||
>(({ ...props }, ref) => <li ref={ref} {...props} />);
|
||||
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
|
||||
|
||||
const SidebarMenuSubButton = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
size?: "sm" | "md"
|
||||
isActive?: boolean
|
||||
asChild?: boolean;
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
}
|
||||
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@@ -735,13 +743,13 @@ const SidebarMenuSubButton = React.forwardRef<
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
|
||||
);
|
||||
});
|
||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
@@ -768,4 +776,4 @@ export {
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
@@ -9,7 +9,7 @@ function Skeleton({
|
||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
export { Skeleton };
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
@@ -13,12 +13,12 @@ const TabsList = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
@@ -28,12 +28,12 @@ const TabsTrigger = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
@@ -43,11 +43,11 @@ const TabsContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
@@ -19,12 +19,12 @@ const TooltipContent = React.forwardRef<
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
@@ -51,22 +51,29 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const themeColor = THEME_COLORS[resolvedTheme];
|
||||
|
||||
// Update theme-color meta tag
|
||||
const themeColorMeta = document.getElementById("theme-color-meta") as HTMLMetaElement;
|
||||
const themeColorMeta = document.getElementById(
|
||||
"theme-color-meta",
|
||||
) as HTMLMetaElement;
|
||||
if (themeColorMeta) {
|
||||
themeColorMeta.content = themeColor;
|
||||
}
|
||||
|
||||
// Update Microsoft tile color
|
||||
const msThemeColorMeta = document.getElementById("ms-theme-color-meta") as HTMLMetaElement;
|
||||
const msThemeColorMeta = document.getElementById(
|
||||
"ms-theme-color-meta",
|
||||
) as HTMLMetaElement;
|
||||
if (msThemeColorMeta) {
|
||||
msThemeColorMeta.content = themeColor;
|
||||
}
|
||||
|
||||
// Update Apple status bar style for better iOS integration
|
||||
const appleStatusBarMeta = document.getElementById("apple-status-bar-meta") as HTMLMetaElement;
|
||||
const appleStatusBarMeta = document.getElementById(
|
||||
"apple-status-bar-meta",
|
||||
) as HTMLMetaElement;
|
||||
if (appleStatusBarMeta) {
|
||||
// Use 'black-translucent' for dark theme, 'default' for light theme
|
||||
appleStatusBarMeta.content = resolvedTheme === "dark" ? "black-translucent" : "default";
|
||||
appleStatusBarMeta.content =
|
||||
resolvedTheme === "dark" ? "black-translucent" : "default";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile
|
||||
return !!isMobile;
|
||||
}
|
||||
|
||||
@@ -7,26 +7,30 @@ interface PWAUpdate {
|
||||
|
||||
export function usePWA(): PWAUpdate {
|
||||
const [updateAvailable, setUpdateAvailable] = useState(false);
|
||||
const [updateSW, setUpdateSW] = useState<() => Promise<void>>(() => async () => {});
|
||||
const [updateSW, setUpdateSW] = useState<() => Promise<void>>(
|
||||
() => async () => {},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if SW registration is available
|
||||
if ("serviceWorker" in navigator) {
|
||||
// Import the registerSW function
|
||||
import("virtual:pwa-register").then(({ registerSW }) => {
|
||||
const updateSWFunction = registerSW({
|
||||
onNeedRefresh() {
|
||||
setUpdateAvailable(true);
|
||||
setUpdateSW(() => updateSWFunction);
|
||||
},
|
||||
onOfflineReady() {
|
||||
console.log("App ready to work offline");
|
||||
},
|
||||
import("virtual:pwa-register")
|
||||
.then(({ registerSW }) => {
|
||||
const updateSWFunction = registerSW({
|
||||
onNeedRefresh() {
|
||||
setUpdateAvailable(true);
|
||||
setUpdateSW(() => updateSWFunction);
|
||||
},
|
||||
onOfflineReady() {
|
||||
console.log("App ready to work offline");
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// PWA not available in development mode or when disabled
|
||||
console.log("PWA registration not available");
|
||||
});
|
||||
}).catch(() => {
|
||||
// PWA not available in development mode or when disabled
|
||||
console.log("PWA registration not available");
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -3,10 +3,7 @@ import { AppSidebar } from "../components/AppSidebar";
|
||||
import { SiteHeader } from "../components/SiteHeader";
|
||||
import { PWAInstallPrompt, PWAUpdatePrompt } from "../components/PWAPrompts";
|
||||
import { usePWA } from "../hooks/usePWA";
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
} from "../components/ui/sidebar";
|
||||
import { SidebarInset, SidebarProvider } from "../components/ui/sidebar";
|
||||
|
||||
function RootLayout() {
|
||||
const { updateAvailable, updateSW } = usePWA();
|
||||
|
||||
@@ -234,7 +234,7 @@ export interface SyncOperation {
|
||||
duration_seconds?: number;
|
||||
errors: string[];
|
||||
logs: string[];
|
||||
trigger_type: 'manual' | 'scheduled' | 'api';
|
||||
trigger_type: "manual" | "scheduled" | "api";
|
||||
}
|
||||
|
||||
export interface SyncOperationsResponse {
|
||||
|
||||
@@ -3,71 +3,71 @@ export default {
|
||||
darkMode: ["class"],
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
spacing: {
|
||||
'safe-top': 'var(--safe-area-inset-top)',
|
||||
'safe-bottom': 'var(--safe-area-inset-bottom)',
|
||||
'safe-left': 'var(--safe-area-inset-left)',
|
||||
'safe-right': 'var(--safe-area-inset-right)'
|
||||
},
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
'1': 'hsl(var(--chart-1))',
|
||||
'2': 'hsl(var(--chart-2))',
|
||||
'3': 'hsl(var(--chart-3))',
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))'
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||
foreground: 'hsl(var(--sidebar-foreground))',
|
||||
primary: 'hsl(var(--sidebar-primary))',
|
||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||
accent: 'hsl(var(--sidebar-accent))',
|
||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||
border: 'hsl(var(--sidebar-border))',
|
||||
ring: 'hsl(var(--sidebar-ring))'
|
||||
}
|
||||
}
|
||||
}
|
||||
extend: {
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
spacing: {
|
||||
"safe-top": "var(--safe-area-inset-top)",
|
||||
"safe-bottom": "var(--safe-area-inset-bottom)",
|
||||
"safe-left": "var(--safe-area-inset-left)",
|
||||
"safe-right": "var(--safe-area-inset-right)",
|
||||
},
|
||||
colors: {
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
chart: {
|
||||
1: "hsl(var(--chart-1))",
|
||||
2: "hsl(var(--chart-2))",
|
||||
3: "hsl(var(--chart-3))",
|
||||
4: "hsl(var(--chart-4))",
|
||||
5: "hsl(var(--chart-5))",
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: "hsl(var(--sidebar-background))",
|
||||
foreground: "hsl(var(--sidebar-foreground))",
|
||||
primary: "hsl(var(--sidebar-primary))",
|
||||
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
||||
accent: "hsl(var(--sidebar-accent))",
|
||||
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
||||
border: "hsl(var(--sidebar-border))",
|
||||
ring: "hsl(var(--sidebar-ring))",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("@tailwindcss/forms"), require("tailwindcss-animate")],
|
||||
};
|
||||
|
||||
@@ -10,7 +10,12 @@ export default defineConfig({
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: "autoUpdate",
|
||||
includeAssets: ["favicon.ico", "apple-touch-icon-180x180.png", "maskable-icon-512x512.png", "robots.txt"],
|
||||
includeAssets: [
|
||||
"favicon.ico",
|
||||
"apple-touch-icon-180x180.png",
|
||||
"maskable-icon-512x512.png",
|
||||
"robots.txt",
|
||||
],
|
||||
manifest: {
|
||||
name: "Leggen",
|
||||
short_name: "Leggen",
|
||||
@@ -28,38 +33,38 @@ export default defineConfig({
|
||||
short_name: "Transactions",
|
||||
description: "View and manage transactions",
|
||||
url: "/transactions",
|
||||
icons: [{ src: "/pwa-192x192.png", sizes: "192x192" }]
|
||||
icons: [{ src: "/pwa-192x192.png", sizes: "192x192" }],
|
||||
},
|
||||
{
|
||||
name: "Analytics",
|
||||
short_name: "Analytics",
|
||||
description: "View financial analytics",
|
||||
url: "/analytics",
|
||||
icons: [{ src: "/pwa-192x192.png", sizes: "192x192" }]
|
||||
}
|
||||
icons: [{ src: "/pwa-192x192.png", sizes: "192x192" }],
|
||||
},
|
||||
],
|
||||
icons: [
|
||||
{
|
||||
src: "pwa-64x64.png",
|
||||
sizes: "64x64",
|
||||
type: "image/png"
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "pwa-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png"
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "pwa-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png"
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "maskable-icon-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "maskable"
|
||||
}
|
||||
purpose: "maskable",
|
||||
},
|
||||
],
|
||||
},
|
||||
workbox: {
|
||||
|
||||
@@ -216,13 +216,13 @@ async def stop_scheduler() -> APIResponse:
|
||||
|
||||
|
||||
@router.get("/sync/operations", response_model=APIResponse)
|
||||
async def get_sync_operations(
|
||||
limit: int = 50, offset: int = 0
|
||||
) -> APIResponse:
|
||||
async def get_sync_operations(limit: int = 50, offset: int = 0) -> APIResponse:
|
||||
"""Get sync operations history"""
|
||||
try:
|
||||
operations = await sync_service.database.get_sync_operations(limit=limit, offset=offset)
|
||||
|
||||
operations = await sync_service.database.get_sync_operations(
|
||||
limit=limit, offset=offset
|
||||
)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"operations": operations, "count": len(operations)},
|
||||
|
||||
@@ -20,7 +20,9 @@ class SyncService:
|
||||
"""Get current sync status"""
|
||||
return self._sync_status
|
||||
|
||||
async def sync_all_accounts(self, force: bool = False, trigger_type: str = "manual") -> SyncResult:
|
||||
async def sync_all_accounts(
|
||||
self, force: bool = False, trigger_type: str = "manual"
|
||||
) -> SyncResult:
|
||||
"""Sync all connected accounts"""
|
||||
if self._sync_status.is_running and not force:
|
||||
raise Exception("Sync is already running")
|
||||
@@ -149,21 +151,25 @@ class SyncService:
|
||||
self._sync_status.last_sync = end_time
|
||||
|
||||
# Update sync operation with final results
|
||||
sync_operation.update({
|
||||
"completed_at": end_time.isoformat(),
|
||||
"success": len(errors) == 0,
|
||||
"accounts_processed": accounts_processed,
|
||||
"transactions_added": transactions_added,
|
||||
"transactions_updated": transactions_updated,
|
||||
"balances_updated": balances_updated,
|
||||
"duration_seconds": duration,
|
||||
"errors": errors,
|
||||
"logs": logs,
|
||||
})
|
||||
sync_operation.update(
|
||||
{
|
||||
"completed_at": end_time.isoformat(),
|
||||
"success": len(errors) == 0,
|
||||
"accounts_processed": accounts_processed,
|
||||
"transactions_added": transactions_added,
|
||||
"transactions_updated": transactions_updated,
|
||||
"balances_updated": balances_updated,
|
||||
"duration_seconds": duration,
|
||||
"errors": errors,
|
||||
"logs": logs,
|
||||
}
|
||||
)
|
||||
|
||||
# Persist sync operation to database
|
||||
try:
|
||||
operation_id = await self.database.persist_sync_operation(sync_operation)
|
||||
operation_id = await self.database.persist_sync_operation(
|
||||
sync_operation
|
||||
)
|
||||
logger.debug(f"Saved sync operation with ID: {operation_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to persist sync operation: {e}")
|
||||
@@ -183,7 +189,9 @@ class SyncService:
|
||||
logger.info(
|
||||
f"Sync completed: {accounts_processed} accounts, {transactions_added} new transactions"
|
||||
)
|
||||
logs.append(f"Sync completed: {accounts_processed} accounts, {transactions_added} new transactions")
|
||||
logs.append(
|
||||
f"Sync completed: {accounts_processed} accounts, {transactions_added} new transactions"
|
||||
)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
@@ -195,23 +203,29 @@ class SyncService:
|
||||
# Save failed sync operation
|
||||
end_time = datetime.now()
|
||||
duration = (end_time - start_time).total_seconds()
|
||||
sync_operation.update({
|
||||
"completed_at": end_time.isoformat(),
|
||||
"success": False,
|
||||
"accounts_processed": accounts_processed,
|
||||
"transactions_added": transactions_added,
|
||||
"transactions_updated": transactions_updated,
|
||||
"balances_updated": balances_updated,
|
||||
"duration_seconds": duration,
|
||||
"errors": errors,
|
||||
"logs": logs,
|
||||
})
|
||||
sync_operation.update(
|
||||
{
|
||||
"completed_at": end_time.isoformat(),
|
||||
"success": False,
|
||||
"accounts_processed": accounts_processed,
|
||||
"transactions_added": transactions_added,
|
||||
"transactions_updated": transactions_updated,
|
||||
"balances_updated": balances_updated,
|
||||
"duration_seconds": duration,
|
||||
"errors": errors,
|
||||
"logs": logs,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
operation_id = await self.database.persist_sync_operation(sync_operation)
|
||||
operation_id = await self.database.persist_sync_operation(
|
||||
sync_operation
|
||||
)
|
||||
logger.debug(f"Saved failed sync operation with ID: {operation_id}")
|
||||
except Exception as persist_error:
|
||||
logger.error(f"Failed to persist failed sync operation: {persist_error}")
|
||||
logger.error(
|
||||
f"Failed to persist failed sync operation: {persist_error}"
|
||||
)
|
||||
|
||||
raise
|
||||
finally:
|
||||
@@ -229,8 +243,10 @@ class SyncService:
|
||||
try:
|
||||
# For now, delegate to sync_all_accounts but with specific filtering
|
||||
# This could be optimized later to only process specified accounts
|
||||
result = await self.sync_all_accounts(force=force, trigger_type=trigger_type)
|
||||
|
||||
result = await self.sync_all_accounts(
|
||||
force=force, trigger_type=trigger_type
|
||||
)
|
||||
|
||||
# Filter results to only specified accounts if needed
|
||||
# For simplicity, we'll return the full result for now
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user