diff --git a/app/page.tsx b/app/page.tsx index 99410a0..850ad50 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -68,7 +68,7 @@ export default function DashboardPage() { folders={data.folders} value={selectedAccounts} onChange={setSelectedAccounts} - className="w-[280px]" + className="w-full md:w-[280px]" filteredTransactions={data.transactions} /> diff --git a/app/rules/page.tsx b/app/rules/page.tsx index 914f13b..7c37d6e 100644 --- a/app/rules/page.tsx +++ b/app/rules/page.tsx @@ -238,10 +238,14 @@ export default function RulesPage() { - {transactionGroups.length} groupe - {transactionGroups.length > 1 ? "s" : ""} de transactions similaires - {uncategorizedCount} non catégorisées + + + {transactionGroups.length} groupe + {transactionGroups.length > 1 ? "s" : ""} de transactions similaires + + + {uncategorizedCount} non catégorisées + } actions={ @@ -272,14 +276,14 @@ export default function RulesPage() { /> {transactionGroups.length === 0 ? ( -
- -

+
+ +

{uncategorizedCount === 0 ? "Toutes les transactions sont catégorisées !" : "Aucun groupe trouvé"}

-

+

{uncategorizedCount === 0 ? "Continuez à importer des transactions pour voir les suggestions de règles." : filterMinCount > 1 @@ -288,7 +292,7 @@ export default function RulesPage() {

) : ( -
+
{transactionGroups.map((group) => ( - - -
+ + +
@@ -617,7 +618,7 @@ export default function StatisticsPage() { categories={data.categories} value={selectedCategories} onChange={setSelectedCategories} - className="w-[220px]" + className="w-full md:w-[220px]" filteredTransactions={transactionsForCategoryFilter} /> @@ -632,7 +633,7 @@ export default function StatisticsPage() { } }} > - + @@ -648,7 +649,7 @@ export default function StatisticsPage() { {period === "custom" && ( - ))} {isUncategorized && ( - - + + Non catégorisé - )} @@ -954,36 +956,36 @@ function ActiveFilters({ - + {cat.name} ))} {hasPeriod && ( - - + + {getPeriodLabel(period)} - )} diff --git a/components/categories/category-card.tsx b/components/categories/category-card.tsx index 94ea533..9a815fb 100644 --- a/components/categories/category-card.tsx +++ b/components/categories/category-card.tsx @@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { CategoryIcon } from "@/components/ui/category-icon"; import { Pencil, Trash2 } from "lucide-react"; +import { useIsMobile } from "@/hooks/use-mobile"; import type { Category } from "@/lib/types"; interface CategoryCardProps { @@ -21,39 +22,48 @@ export function CategoryCard({ onEdit, onDelete, }: CategoryCardProps) { + const isMobile = useIsMobile(); + return (
- {category.name} - - {stats.count} opération{stats.count > 1 ? "s" : ""} •{" "} - {formatCurrency(stats.total)} - + {category.name} + {!isMobile && ( + + {stats.count} opération{stats.count > 1 ? "s" : ""} •{" "} + {formatCurrency(stats.total)} + + )} + {isMobile && ( + + {stats.count} 💳 + + )} {category.keywords.length > 0 && ( {category.keywords.length} )}
-
+
-
+
- @@ -115,7 +125,7 @@ export function ParentCategoryRow({ {children.length > 0 ? ( -
+
{children.map((child) => ( - + Solde Total - +
= 0 ? "text-emerald-600" : "text-red-600", )} > {formatCurrency(totalBalance)}
-

+

{data.accounts.length} compte{data.accounts.length > 1 ? "s" : ""}

@@ -69,16 +70,16 @@ export function OverviewCards({ data }: OverviewCardsProps) { - + Revenus du mois - + -
+
{formatCurrency(income)}
-

+

{monthTransactions.filter((t) => t.amount > 0).length} opération {monthTransactions.filter((t) => t.amount > 0).length > 1 ? "s" @@ -89,16 +90,16 @@ export function OverviewCards({ data }: OverviewCardsProps) { - + Dépenses du mois - + -

+
{formatCurrency(expenses)}
-

+

{monthTransactions.filter((t) => t.amount < 0).length} opération {monthTransactions.filter((t) => t.amount < 0).length > 1 ? "s" @@ -109,14 +110,14 @@ export function OverviewCards({ data }: OverviewCardsProps) { - + Pointage - + -

{reconciledPercent}%
-

+

{reconciledPercent}%
+

{reconciled} / {total} opérations pointées

@@ -124,5 +125,3 @@ export function OverviewCards({ data }: OverviewCardsProps) {
); } - -import { cn } from "@/lib/utils"; diff --git a/components/dashboard/recent-transactions.tsx b/components/dashboard/recent-transactions.tsx index 649612a..706b4af 100644 --- a/components/dashboard/recent-transactions.tsx +++ b/components/dashboard/recent-transactions.tsx @@ -60,9 +60,9 @@ export function RecentTransactions({ data }: RecentTransactionsProps) { return ( - Transactions récentes + Transactions récentes - +
{recentTransactions.map((transaction) => { const category = getCategory(transaction.categoryId); @@ -71,59 +71,74 @@ export function RecentTransactions({ data }: RecentTransactionsProps) { return (
-
- {transaction.isReconciled ? ( - - ) : ( - - )} -
- -
-

- {transaction.description} -

-
- - {formatDate(transaction.date)} - - {account && ( - - • {account.name} - - )} - {category && ( - - - {category.name} - +
+
+ {transaction.isReconciled ? ( + + ) : ( + )}
-
-
= 0 - ? "text-emerald-600" - : "text-red-600", - )} - > - {transaction.amount >= 0 ? "+" : ""} - {formatCurrency(transaction.amount)} +
+
+

+ {transaction.description} +

+
= 0 + ? "text-emerald-600" + : "text-red-600", + )} + > + {transaction.amount >= 0 ? "+" : ""} + {formatCurrency(transaction.amount)} +
+
+
+ + {formatDate(transaction.date)} + + {account && ( + + • {account.name} + + )} + {category && ( + + + {category.name} + + )} +
+
+ +
); diff --git a/components/dashboard/sidebar.tsx b/components/dashboard/sidebar.tsx index 5907f83..54c32df 100644 --- a/components/dashboard/sidebar.tsx +++ b/components/dashboard/sidebar.tsx @@ -19,6 +19,8 @@ import { LogOut, } from "lucide-react"; import { toast } from "sonner"; +import { Sheet, SheetContent } from "@/components/ui/sheet"; +import { useIsMobile } from "@/hooks/use-mobile"; const navItems = [ { href: "/", label: "Tableau de bord", icon: LayoutDashboard }, @@ -29,10 +31,14 @@ const navItems = [ { href: "/statistics", label: "Statistiques", icon: BarChart3 }, ]; -export function Sidebar() { +interface SidebarContentProps { + collapsed?: boolean; + onNavigate?: () => void; +} + +function SidebarContent({ collapsed = false, onNavigate, showHeader = false }: SidebarContentProps & { showHeader?: boolean }) { const pathname = usePathname(); const router = useRouter(); - const [collapsed, setCollapsed] = useState(false); const handleSignOut = async () => { try { @@ -46,10 +52,99 @@ export function Sidebar() { } }; + const handleLinkClick = () => { + onNavigate?.(); + }; + + return ( + <> + {showHeader && ( +
+ {!collapsed && ( +
+
+ +
+ FinTrack +
+ )} +
+ )} + + + +
+ + + + +
+ + ); +} + +interface SidebarProps { + open?: boolean; + onOpenChange?: (open: boolean) => void; +} + +export function Sidebar({ open, onOpenChange }: SidebarProps) { + const [collapsed, setCollapsed] = useState(false); + const isMobile = useIsMobile(); + + if (isMobile) { + return ( + + +
+ onOpenChange?.(false)} /> +
+
+
+ ); + } + return (
- - -
- - - - -
+ ); } diff --git a/components/layout/page-header.tsx b/components/layout/page-header.tsx index 2a3033e..99bd3e6 100644 --- a/components/layout/page-header.tsx +++ b/components/layout/page-header.tsx @@ -1,6 +1,11 @@ "use client"; import { ReactNode } from "react"; +import { Button } from "@/components/ui/button"; +import { Menu } from "lucide-react"; +import { useSidebarContext } from "@/components/layout/sidebar-context"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { cn } from "@/lib/utils"; interface PageHeaderProps { title: string; @@ -15,16 +20,39 @@ export function PageHeader({ actions, rightContent, }: PageHeaderProps) { + const { setOpen } = useSidebarContext(); + const isMobile = useIsMobile(); + return ( -
-
-

{title}

- {description && ( -
{description}
+
+
+ {isMobile && ( + )} +
+

{title}

+ {description && ( +
{description}
+ )} +
- {rightContent} - {actions &&
{actions}
} + {(rightContent || actions) && ( +
+ {rightContent} + {actions && ( +
+ {actions} +
+ )} +
+ )}
); } diff --git a/components/layout/page-layout.tsx b/components/layout/page-layout.tsx index db4bb6d..a1e4cbd 100644 --- a/components/layout/page-layout.tsx +++ b/components/layout/page-layout.tsx @@ -1,20 +1,25 @@ "use client"; import { Sidebar } from "@/components/dashboard/sidebar"; -import { ReactNode } from "react"; +import { ReactNode, useState } from "react"; +import { SidebarContext } from "@/components/layout/sidebar-context"; interface PageLayoutProps { children: ReactNode; } export function PageLayout({ children }: PageLayoutProps) { + const [sidebarOpen, setSidebarOpen] = useState(false); + return ( -
- -
-
{children}
-
-
+ +
+ +
+
{children}
+
+
+
); } diff --git a/components/layout/sidebar-context.tsx b/components/layout/sidebar-context.tsx new file mode 100644 index 0000000..eb88fea --- /dev/null +++ b/components/layout/sidebar-context.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { createContext, useContext } from "react"; + +interface SidebarContextType { + open: boolean; + setOpen: (open: boolean) => void; +} + +export const SidebarContext = createContext({ + open: false, + setOpen: () => {}, +}); + +export function useSidebarContext() { + return useContext(SidebarContext); +} + diff --git a/components/rules/rule-group-card.tsx b/components/rules/rule-group-card.tsx index b5024d2..bfc0c1f 100644 --- a/components/rules/rule-group-card.tsx +++ b/components/rules/rule-group-card.tsx @@ -5,6 +5,7 @@ import { ChevronDown, ChevronRight, Plus, Tag } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { CategoryCombobox } from "@/components/ui/category-combobox"; +import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; import type { Transaction, Category } from "@/lib/types"; @@ -38,6 +39,7 @@ export function RuleGroupCard({ formatDate, }: RuleGroupCardProps) { const [selectedCategoryId, setSelectedCategoryId] = useState(null); + const isMobile = useIsMobile(); const avgAmount = group.transactions.reduce((sum, t) => sum + t.amount, 0) / @@ -53,59 +55,87 @@ export function RuleGroupCard({
{/* Header */}
- +
+ -
-
- - {group.displayName} - - - {group.transactions.length} transaction - {group.transactions.length > 1 ? "s" : ""} - -
-
- - - {group.suggestedKeyword} - +
+
+ + {group.displayName} + + + {group.transactions.length} 💳 + +
+
+ + + {group.suggestedKeyword} + +
-
-
-
- {formatCurrency(group.totalAmount)} + {!isMobile && ( +
+
+
+ {formatCurrency(group.totalAmount)} +
+
+ Moy: {formatCurrency(avgAmount)} +
-
- Moy: {formatCurrency(avgAmount)} + +
+
e.stopPropagation()}> + +
+
+ )} -
-
e.stopPropagation()}> + {isMobile && ( +
+
e.stopPropagation()} className="flex-1">
-
+ )}
{/* Expanded transactions list */} {isExpanded && (
-
- - - - - - - - - - {group.transactions.map((transaction) => ( - - - - + + + + ))} + + ) : ( +
+
- Date - - Description - - Montant -
- {formatDate(transaction.date)} - - {transaction.description} + {isMobile ? ( +
+ {group.transactions.map((transaction) => ( +
+
+
+

+ {transaction.description} +

{transaction.memo && ( - - ({transaction.memo}) - +

+ {transaction.memo} +

)} -
+ {formatDate(transaction.date)} +

+ +
{formatCurrency(transaction.amount)} -
+ + + + + - ))} - -
+ Date + + Description + + Montant +
-
+ + + {group.transactions.map((transaction) => ( + + + {formatDate(transaction.date)} + + + {transaction.description} + {transaction.memo && ( + + ({transaction.memo}) + + )} + + + {formatCurrency(transaction.amount)} + + + ))} + + +
+ )}
)}
diff --git a/components/rules/rules-search-bar.tsx b/components/rules/rules-search-bar.tsx index 8e3f69f..deae7ad 100644 --- a/components/rules/rules-search-bar.tsx +++ b/components/rules/rules-search-bar.tsx @@ -28,24 +28,24 @@ export function RulesSearchBar({ onFilterMinCountChange, }: RulesSearchBarProps) { return ( -
+
- + onSearchChange(e.target.value)} - className="pl-10" + className="pl-9 md:pl-10 text-sm md:text-base" />
-
+
- + @@ -125,7 +125,7 @@ export function TransactionFilters({ } }} > - + @@ -141,7 +141,7 @@ export function TransactionFilters({ {period === "custom" && ( - + + + { + e.stopPropagation(); + onCreateRule(transaction); + }} + > + + Créer une règle + + + { + e.stopPropagation(); + if ( + confirm( + `Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}` + ) + ) { + onDelete(transaction.id); + } + }} + className="text-red-600 focus:text-red-600" + > + + Supprimer + + + +
+
+ ); + })} +
+
+
) : (
{/* Header fixe */} diff --git a/hooks/use-mobile.ts b/hooks/use-mobile.ts index a93d583..b8df250 100644 --- a/hooks/use-mobile.ts +++ b/hooks/use-mobile.ts @@ -8,13 +8,22 @@ export function useIsMobile() { ); React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + const checkMobile = () => { + const mobile = window.innerWidth < MOBILE_BREAKPOINT; + setIsMobile((prev) => { + // Éviter les re-renders inutiles si la valeur n'a pas changé + if (prev === mobile) return prev; + return mobile; + }); }; - mql.addEventListener("change", onChange); - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - return () => mql.removeEventListener("change", onChange); + + // Vérification initiale + checkMobile(); + + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + mql.addEventListener("change", checkMobile); + + return () => mql.removeEventListener("change", checkMobile); }, []); return !!isMobile;