feat: enhance responsive design and layout consistency across various components, including dashboard, statistics, and rules pages

This commit is contained in:
Julien Froidefond
2025-12-01 08:34:28 +01:00
parent 86236aeb04
commit b3b25412ad
19 changed files with 731 additions and 349 deletions

View File

@@ -91,7 +91,7 @@ export function TransactionFilters({
folders={folders}
value={selectedAccounts}
onChange={onAccountsChange}
className="w-[280px]"
className="w-full md:w-[280px]"
filteredTransactions={transactionsForAccountFilter}
/>
@@ -99,12 +99,12 @@ export function TransactionFilters({
categories={categories}
value={selectedCategories}
onChange={onCategoriesChange}
className="w-[220px]"
className="w-full md:w-[220px]"
filteredTransactions={transactionsForCategoryFilter}
/>
<Select value={showReconciled} onValueChange={onReconciledChange}>
<SelectTrigger className="w-[160px]">
<SelectTrigger className="w-full md:w-[160px]">
<SelectValue placeholder="Pointage" />
</SelectTrigger>
<SelectContent>
@@ -125,7 +125,7 @@ export function TransactionFilters({
}
}}
>
<SelectTrigger className="w-[150px]">
<SelectTrigger className="w-full md:w-[150px]">
<SelectValue placeholder="Période" />
</SelectTrigger>
<SelectContent>
@@ -141,7 +141,7 @@ export function TransactionFilters({
{period === "custom" && (
<Popover open={isCustomDatePickerOpen} onOpenChange={onCustomDatePickerOpenChange}>
<PopoverTrigger asChild>
<Button variant="outline" className="w-[280px] justify-start text-left font-normal">
<Button variant="outline" className="w-full md:w-[280px] justify-start text-left font-normal">
<Calendar className="mr-2 h-4 w-4" />
{customStartDate && customEndDate ? (
<>

View File

@@ -27,6 +27,7 @@ import {
} from "lucide-react";
import { DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { useIsMobile } from "@/hooks/use-mobile";
import type { Transaction, Account, Category } from "@/lib/types";
type SortField = "date" | "amount" | "description";
@@ -146,11 +147,14 @@ export function TransactionTable({
}: TransactionTableProps) {
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
const parentRef = useRef<HTMLDivElement>(null);
const isMobile = useIsMobile();
const MOBILE_ROW_HEIGHT = 120;
const virtualizer = useVirtualizer({
count: transactions.length,
getScrollElement: () => parentRef.current,
estimateSize: () => ROW_HEIGHT,
estimateSize: () => (isMobile ? MOBILE_ROW_HEIGHT : ROW_HEIGHT),
overscan: 10,
});
@@ -201,17 +205,165 @@ export function TransactionTable({
setFocusedIndex(null);
}, [transactions.length]);
const getAccount = (accountId: string) => {
const getAccount = useCallback((accountId: string) => {
return accounts.find((a) => a.id === accountId);
};
}, [accounts]);
const getCategory = useCallback((categoryId: string | null) => {
if (!categoryId) return null;
return categories.find((c) => c.id === categoryId);
}, [categories]);
return (
<Card>
<Card className="overflow-hidden">
<CardContent className="p-0">
{transactions.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12">
<p className="text-muted-foreground">Aucune transaction trouvée</p>
</div>
) : isMobile ? (
<div className="divide-y divide-border">
<div
ref={parentRef}
className="overflow-auto"
style={{ height: "calc(100vh - 300px)", minHeight: "400px" }}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
const transaction = transactions[virtualRow.index];
const account = getAccount(transaction.accountId);
const category = getCategory(transaction.categoryId);
const isFocused = focusedIndex === virtualRow.index;
return (
<div
key={transaction.id}
data-index={virtualRow.index}
ref={virtualizer.measureElement}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
onClick={() => {
// Désactiver le pointage au clic sur mobile
if (!isMobile) {
handleRowClick(virtualRow.index, transaction.id);
}
}}
className={cn(
"p-4 space-y-3 hover:bg-muted/50 cursor-pointer border-b border-border",
transaction.isReconciled && "bg-emerald-500/5",
isFocused && "bg-primary/10 ring-1 ring-primary/30"
)}
>
<div className="flex items-start justify-between gap-2">
<div className="flex items-start gap-3 flex-1 min-w-0">
<Checkbox
checked={selectedTransactions.has(transaction.id)}
onCheckedChange={() => {
onToggleSelectTransaction(transaction.id);
}}
onClick={(e) => e.stopPropagation()}
/>
<div className="flex-1 min-w-0">
<p className="font-medium text-xs md:text-sm truncate">
{transaction.description}
</p>
{transaction.memo && (
<p className="text-[10px] md:text-xs text-muted-foreground truncate mt-1">
{transaction.memo}
</p>
)}
</div>
</div>
<div
className={cn(
"font-semibold tabular-nums text-sm md:text-base shrink-0",
transaction.amount >= 0
? "text-emerald-600"
: "text-red-600"
)}
>
{transaction.amount >= 0 ? "+" : ""}
{formatCurrency(transaction.amount)}
</div>
</div>
<div className="flex items-center gap-2 md:gap-3 flex-wrap">
<span className="text-[10px] md:text-xs text-muted-foreground">
{formatDate(transaction.date)}
</span>
{account && (
<span className="text-[10px] md:text-xs text-muted-foreground">
{account.name}
</span>
)}
<div onClick={(e) => e.stopPropagation()} className="flex-1">
<CategoryCombobox
categories={categories}
value={transaction.categoryId}
onChange={(categoryId) =>
onSetCategory(transaction.id, categoryId)
}
showBadge
align="start"
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0"
>
<MoreVertical className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onCreateRule(transaction);
}}
>
<Wand2 className="w-4 h-4 mr-2" />
Créer une règle
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => {
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"
>
<Trash2 className="w-4 h-4 mr-2" />
Supprimer
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
})}
</div>
</div>
</div>
) : (
<div className="overflow-x-auto">
{/* Header fixe */}