ソースを参照

Scroll lists inside themselves instead of scrolling the page (#323)

* Scroll the vehicles list inside itself instead of scrolling the page

* Scroll every list inside itself instead of scrolling the page
Bernt Christian Egeland 3 週間 前
親
コミット
2069a252d1
31 ファイル変更、486 行追加、334 行削除
  1. 1 1
      src/app/(authenticated)/admin/layout.tsx
  2. 10 7
      src/app/(authenticated)/audit-log/audit-log-client.tsx
  3. 3 2
      src/app/(authenticated)/audit-log/page.tsx
  4. 13 10
      src/app/(authenticated)/billing/billing-client.tsx
  5. 3 2
      src/app/(authenticated)/billing/page.tsx
  6. 3 2
      src/app/(authenticated)/billing/recurring/page.tsx
  7. 193 192
      src/app/(authenticated)/customers/customers-client.tsx
  8. 3 2
      src/app/(authenticated)/customers/page.tsx
  9. 12 9
      src/app/(authenticated)/inspections/inspections-client.tsx
  10. 3 2
      src/app/(authenticated)/inspections/page.tsx
  11. 11 8
      src/app/(authenticated)/inventory/inventory-client.tsx
  12. 3 2
      src/app/(authenticated)/inventory/page.tsx
  13. 10 7
      src/app/(authenticated)/labor-presets/labor-presets-client.tsx
  14. 3 2
      src/app/(authenticated)/labor-presets/page.tsx
  15. 11 8
      src/app/(authenticated)/observations/observations-client.tsx
  16. 3 2
      src/app/(authenticated)/observations/page.tsx
  17. 3 2
      src/app/(authenticated)/quotes/page.tsx
  18. 12 9
      src/app/(authenticated)/quotes/quotes-client.tsx
  19. 3 2
      src/app/(authenticated)/tire-hotel/page.tsx
  20. 11 8
      src/app/(authenticated)/tire-hotel/tire-hotel-client.tsx
  21. 3 2
      src/app/(authenticated)/vehicles/page.tsx
  22. 14 11
      src/app/(authenticated)/vehicles/vehicles-client.tsx
  23. 3 2
      src/app/(authenticated)/work-orders/page.tsx
  24. 12 9
      src/app/(authenticated)/work-orders/work-orders-client.tsx
  25. 1 1
      src/components/data-table-pagination.tsx
  26. 72 0
      src/components/list-page.tsx
  27. 1 1
      src/components/table-context-menu-hint.tsx
  28. 37 4
      src/components/ui/table.tsx
  29. 7 7
      src/features/admin/Components/admin-organizations.tsx
  30. 10 7
      src/features/admin/Components/admin-users.tsx
  31. 12 11
      src/features/billing/Components/RecurringInvoicesClient.tsx

+ 1 - 1
src/app/(authenticated)/admin/layout.tsx

@@ -31,7 +31,7 @@ export default async function AdminLayout({ children }: { children: React.ReactN
           <aside className="w-full shrink-0 overflow-y-auto md:w-56 lg:w-64">
           <aside className="w-full shrink-0 overflow-y-auto md:w-56 lg:w-64">
             <AdminNav />
             <AdminNav />
           </aside>
           </aside>
-          <div className="min-w-0 flex-1 overflow-y-auto pb-8">{children}</div>
+          <div className="flex min-w-0 flex-1 flex-col overflow-y-auto pb-8">{children}</div>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>

+ 10 - 7
src/app/(authenticated)/audit-log/audit-log-client.tsx

@@ -148,9 +148,9 @@ export function AuditLogClient({
   )
   )
 
 
   return (
   return (
-    <div className="space-y-4">
+    <div className="flex min-h-0 flex-1 flex-col gap-4">
       {/* Filters */}
       {/* Filters */}
-      <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
+      <div className="flex shrink-0 flex-col gap-3 sm:flex-row sm:items-center">
         <div className="relative flex-1">
         <div className="relative flex-1">
           <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
           <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
           <Input
           <Input
@@ -212,7 +212,7 @@ export function AuditLogClient({
       </div>
       </div>
 
 
       {/* Card list (phones + small tablets) */}
       {/* Card list (phones + small tablets) */}
-      <div className="space-y-2 md:hidden">
+      <div className="min-h-0 flex-1 space-y-2 overflow-y-auto md:hidden">
         {data.logs.length === 0 ? (
         {data.logs.length === 0 ? (
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
             {t('noLogsFound')}
             {t('noLogsFound')}
@@ -253,11 +253,14 @@ export function AuditLogClient({
         )}
         )}
       </div>
       </div>
 
 
-      {/* Table (md and up) */}
-      <div className="hidden rounded-md border md:block" {...tableNav.containerProps}>
+      {/* Table (md and up) - only the rows scroll */}
+      <div
+        className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-md border md:flex"
+        {...tableNav.containerProps}
+      >
         <TableContextMenuHint />
         <TableContextMenuHint />
-        <Table>
-          <TableHeader>
+        <Table containerClassName="min-h-0 flex-1">
+          <TableHeader sticky>
             <TableRow>
             <TableRow>
               <TableHead className="w-[170px]">{t('timestamp')}</TableHead>
               <TableHead className="w-[170px]">{t('timestamp')}</TableHead>
               <TableHead className="w-[140px]">{t('user')}</TableHead>
               <TableHead className="w-[140px]">{t('user')}</TableHead>

+ 3 - 2
src/app/(authenticated)/audit-log/page.tsx

@@ -1,5 +1,6 @@
 import { getAuditLogsPaginated } from '@/features/audit/Actions/auditActions'
 import { getAuditLogsPaginated } from '@/features/audit/Actions/auditActions'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
+import { ListPage } from '@/components/list-page'
 import { AuditLogClient } from './audit-log-client'
 import { AuditLogClient } from './audit-log-client'
 
 
 export default async function AuditLogPage({
 export default async function AuditLogPage({
@@ -39,7 +40,7 @@ export default async function AuditLogPage({
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+      <ListPage>
         <AuditLogClient
         <AuditLogClient
           data={result.data}
           data={result.data}
           search={params.search || ''}
           search={params.search || ''}
@@ -47,7 +48,7 @@ export default async function AuditLogPage({
           entityFilter={params.entity || 'all'}
           entityFilter={params.entity || 'all'}
           userFilter={params.userId || 'all'}
           userFilter={params.userId || 'all'}
         />
         />
-      </div>
+      </ListPage>
     </>
     </>
   )
   )
 }
 }

+ 13 - 10
src/app/(authenticated)/billing/billing-client.tsx

@@ -229,9 +229,9 @@ export default function BillingClient({
   }
   }
 
 
   return (
   return (
-    <div className="space-y-6">
+    <div className="flex min-h-0 flex-1 flex-col gap-6">
       {/* Navigation */}
       {/* Navigation */}
-      <div className="flex items-center gap-3">
+      <div className="flex shrink-0 items-center gap-3">
         <Button variant="outline" size="sm" disabled>
         <Button variant="outline" size="sm" disabled>
           {t('history.title')}
           {t('history.title')}
         </Button>
         </Button>
@@ -244,7 +244,7 @@ export default function BillingClient({
 
 
       {/* Summary Cards — hidden below md so the invoice list is what a phone
       {/* Summary Cards — hidden below md so the invoice list is what a phone
           or portrait tablet opens on. */}
           or portrait tablet opens on. */}
-      <div className="hidden gap-3 md:grid md:grid-cols-3">
+      <div className="hidden shrink-0 gap-3 md:grid md:grid-cols-3">
         <Card>
         <Card>
           <CardContent className="flex items-center gap-3 px-4 py-3">
           <CardContent className="flex items-center gap-3 px-4 py-3">
             <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-blue-500/10">
             <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-blue-500/10">
@@ -283,7 +283,7 @@ export default function BillingClient({
       </div>
       </div>
 
 
       {/* Status Tabs and Search */}
       {/* Status Tabs and Search */}
-      <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
+      <div className="flex shrink-0 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
         {/* A single scrollable row on phones, so four filters never wrap. */}
         {/* A single scrollable row on phones, so four filters never wrap. */}
         <div className="-mx-1 flex gap-2 overflow-x-auto px-1 pb-1 sm:mx-0 sm:px-0 sm:pb-0">
         <div className="-mx-1 flex gap-2 overflow-x-auto px-1 pb-1 sm:mx-0 sm:px-0 sm:pb-0">
           {STATUS_TABS.map((tab) => (
           {STATUS_TABS.map((tab) => (
@@ -344,8 +344,8 @@ export default function BillingClient({
         </form>
         </form>
       </div>
       </div>
 
 
-      {/* Card list (phones + small tablets) */}
-      <div className="space-y-2 md:hidden">
+      {/* Card list (phones + small tablets) - only this scrolls */}
+      <div className="min-h-0 flex-1 space-y-2 overflow-y-auto md:hidden">
         {data.records.length === 0 ? (
         {data.records.length === 0 ? (
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
             {t('history.noRecords')}
             {t('history.noRecords')}
@@ -391,10 +391,13 @@ export default function BillingClient({
         )}
         )}
       </div>
       </div>
 
 
-      {/* Table (md and up) */}
-      <div className="hidden rounded-md border md:block" {...tableNav.containerProps}>
-        <Table className="table-fixed">
-          <TableHeader>
+      {/* Table (md and up) - only the rows scroll */}
+      <div
+        className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-md border md:flex"
+        {...tableNav.containerProps}
+      >
+        <Table containerClassName="min-h-0 flex-1" className="table-fixed">
+          <TableHeader sticky>
             <TableRow>
             <TableRow>
               <TableHead className="w-[110px]">
               <TableHead className="w-[110px]">
                 <button
                 <button

+ 3 - 2
src/app/(authenticated)/billing/page.tsx

@@ -2,6 +2,7 @@ import { getBillingHistory } from '@/features/billing/Actions/billingActions'
 import { getSettings } from '@/features/settings/Actions/settingsActions'
 import { getSettings } from '@/features/settings/Actions/settingsActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
+import { ListPage } from '@/components/list-page'
 import BillingClient from './billing-client'
 import BillingClient from './billing-client'
 
 
 export default async function BillingPage({
 export default async function BillingPage({
@@ -46,7 +47,7 @@ export default async function BillingPage({
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+      <ListPage>
         <BillingClient
         <BillingClient
           data={result.data}
           data={result.data}
           currencyCode={currencyCode}
           currencyCode={currencyCode}
@@ -55,7 +56,7 @@ export default async function BillingPage({
           sortBy={sortBy}
           sortBy={sortBy}
           sortOrder={sortOrder}
           sortOrder={sortOrder}
         />
         />
-      </div>
+      </ListPage>
     </>
     </>
   )
   )
 }
 }

+ 3 - 2
src/app/(authenticated)/billing/recurring/page.tsx

@@ -2,6 +2,7 @@ import { getRecurringInvoices } from '@/features/billing/Actions/recurringInvoic
 import { getSettings } from '@/features/settings/Actions/settingsActions'
 import { getSettings } from '@/features/settings/Actions/settingsActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
+import { ListPage } from '@/components/list-page'
 import RecurringInvoicesClient from '@/features/billing/Components/RecurringInvoicesClient'
 import RecurringInvoicesClient from '@/features/billing/Components/RecurringInvoicesClient'
 import { db } from '@/lib/db'
 import { db } from '@/lib/db'
 import { getCachedSession, getCachedMembership } from '@/lib/cached-session'
 import { getCachedSession, getCachedMembership } from '@/lib/cached-session'
@@ -37,13 +38,13 @@ export default async function RecurringInvoicesPage() {
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+      <ListPage>
         <RecurringInvoicesClient
         <RecurringInvoicesClient
           invoices={invoices}
           invoices={invoices}
           vehicles={vehicles}
           vehicles={vehicles}
           currencyCode={currencyCode}
           currencyCode={currencyCode}
         />
         />
-      </div>
+      </ListPage>
     </>
     </>
   )
   )
 }
 }

+ 193 - 192
src/app/(authenticated)/customers/customers-client.tsx

@@ -216,9 +216,9 @@ export function CustomersClient({
   }
   }
 
 
   return (
   return (
-    <div className="space-y-4">
+    <div className="flex min-h-0 flex-1 flex-col gap-4">
       {/* Toolbar */}
       {/* Toolbar */}
-      <div className="flex items-center justify-between gap-3">
+      <div className="flex shrink-0 items-center justify-between gap-3">
         {selected.size > 0 ? (
         {selected.size > 0 ? (
           <>
           <>
             <div className="flex items-center gap-2 text-sm text-muted-foreground">
             <div className="flex items-center gap-2 text-sm text-muted-foreground">
@@ -280,8 +280,8 @@ export function CustomersClient({
         )}
         )}
       </div>
       </div>
 
 
-      {/* Card list (phones + small tablets) */}
-      <div className="space-y-2 md:hidden">
+      {/* Card list (phones + small tablets) - only this scrolls */}
+      <div className="min-h-0 flex-1 space-y-2 overflow-y-auto md:hidden">
         {data.customers.length === 0 ? (
         {data.customers.length === 0 ? (
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
             {search ? t('emptySearch') : t('empty')}
             {search ? t('emptySearch') : t('empty')}
@@ -353,197 +353,198 @@ export function CustomersClient({
         )}
         )}
       </div>
       </div>
 
 
-      {/* Table (md and up) - only this scrolls */}
-      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
+      {/* Table (md and up) - only the rows scroll */}
+      <div
+        className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-lg border md:flex"
+        {...tableNav.containerProps}
+      >
         <TableContextMenuHint />
         <TableContextMenuHint />
-        <div className="overflow-auto max-h-[calc(100vh-220px)]">
-          <Table className="table-fixed">
-            <TableHeader className="sticky top-0 z-10 bg-background">
+        <Table containerClassName="min-h-0 flex-1" className="table-fixed">
+          <TableHeader sticky>
+            <TableRow>
+              <TableHead className="w-[40px]">
+                <Checkbox
+                  checked={
+                    data.customers.length > 0 && selected.size === data.customers.length
+                      ? true
+                      : selected.size > 0
+                        ? 'indeterminate'
+                        : false
+                  }
+                  onCheckedChange={toggleSelectAll}
+                  onClick={(e) => e.stopPropagation()}
+                />
+              </TableHead>
+              <TableHead className="w-[80px]">
+                <button
+                  type="button"
+                  className="flex items-center hover:text-foreground"
+                  onClick={() => handleSort('number')}
+                >
+                  {t('table.number')}
+                  <SortIcon column="number" />
+                </button>
+              </TableHead>
+              <TableHead>
+                <button
+                  type="button"
+                  className="flex items-center hover:text-foreground"
+                  onClick={() => handleSort('name')}
+                >
+                  {t('table.name')}
+                  <SortIcon column="name" />
+                </button>
+              </TableHead>
+              <TableHead className="hidden w-[20%] sm:table-cell">
+                <button
+                  type="button"
+                  className="flex items-center hover:text-foreground"
+                  onClick={() => handleSort('company')}
+                >
+                  {t('table.company')}
+                  <SortIcon column="company" />
+                </button>
+              </TableHead>
+              <TableHead className="hidden w-[16%] md:table-cell">
+                <button
+                  type="button"
+                  className="flex items-center hover:text-foreground"
+                  onClick={() => handleSort('phone')}
+                >
+                  {t('table.phone')}
+                  <SortIcon column="phone" />
+                </button>
+              </TableHead>
+              <TableHead className="hidden w-[22%] lg:table-cell">
+                <button
+                  type="button"
+                  className="flex items-center hover:text-foreground"
+                  onClick={() => handleSort('email')}
+                >
+                  {t('table.email')}
+                  <SortIcon column="email" />
+                </button>
+              </TableHead>
+              <TableHead className="w-[80px]">
+                <button
+                  type="button"
+                  className="mx-auto flex items-center hover:text-foreground"
+                  onClick={() => handleSort('vehicles')}
+                >
+                  {t('table.vehicles')}
+                  <SortIcon column="vehicles" />
+                </button>
+              </TableHead>
+              <TableHead className="w-[50px]" />
+            </TableRow>
+          </TableHeader>
+          <TableBody>
+            {data.customers.length === 0 ? (
               <TableRow>
               <TableRow>
-                <TableHead className="w-[40px]">
-                  <Checkbox
-                    checked={
-                      data.customers.length > 0 && selected.size === data.customers.length
-                        ? true
-                        : selected.size > 0
-                          ? 'indeterminate'
-                          : false
-                    }
-                    onCheckedChange={toggleSelectAll}
-                    onClick={(e) => e.stopPropagation()}
-                  />
-                </TableHead>
-                <TableHead className="w-[80px]">
-                  <button
-                    type="button"
-                    className="flex items-center hover:text-foreground"
-                    onClick={() => handleSort('number')}
-                  >
-                    {t('table.number')}
-                    <SortIcon column="number" />
-                  </button>
-                </TableHead>
-                <TableHead>
-                  <button
-                    type="button"
-                    className="flex items-center hover:text-foreground"
-                    onClick={() => handleSort('name')}
-                  >
-                    {t('table.name')}
-                    <SortIcon column="name" />
-                  </button>
-                </TableHead>
-                <TableHead className="hidden w-[20%] sm:table-cell">
-                  <button
-                    type="button"
-                    className="flex items-center hover:text-foreground"
-                    onClick={() => handleSort('company')}
-                  >
-                    {t('table.company')}
-                    <SortIcon column="company" />
-                  </button>
-                </TableHead>
-                <TableHead className="hidden w-[16%] md:table-cell">
-                  <button
-                    type="button"
-                    className="flex items-center hover:text-foreground"
-                    onClick={() => handleSort('phone')}
-                  >
-                    {t('table.phone')}
-                    <SortIcon column="phone" />
-                  </button>
-                </TableHead>
-                <TableHead className="hidden w-[22%] lg:table-cell">
-                  <button
-                    type="button"
-                    className="flex items-center hover:text-foreground"
-                    onClick={() => handleSort('email')}
-                  >
-                    {t('table.email')}
-                    <SortIcon column="email" />
-                  </button>
-                </TableHead>
-                <TableHead className="w-[80px]">
-                  <button
-                    type="button"
-                    className="mx-auto flex items-center hover:text-foreground"
-                    onClick={() => handleSort('vehicles')}
-                  >
-                    {t('table.vehicles')}
-                    <SortIcon column="vehicles" />
-                  </button>
-                </TableHead>
-                <TableHead className="w-[50px]" />
+                <TableCell colSpan={8} className="h-32 text-center text-muted-foreground">
+                  {search ? t('emptySearch') : t('empty')}
+                </TableCell>
               </TableRow>
               </TableRow>
-            </TableHeader>
-            <TableBody>
-              {data.customers.length === 0 ? (
-                <TableRow>
-                  <TableCell colSpan={8} className="h-32 text-center text-muted-foreground">
-                    {search ? t('emptySearch') : t('empty')}
-                  </TableCell>
-                </TableRow>
-              ) : (
-                data.customers.map((c) => (
-                  <ContextMenu key={c.id} modal={false}>
-                    <ContextMenuTrigger asChild>
-                      <TableRow
-                        className={`cursor-pointer ${selected.has(c.id) ? 'bg-muted/50' : ''}`}
-                        {...interactiveRow(() => router.push(`/customers/${c.id}`))}
-                      >
-                        <TableCell className="w-[40px]">
-                          <Checkbox
-                            checked={selected.has(c.id)}
-                            onCheckedChange={() => toggleSelect(c.id)}
-                            onClick={(e) => e.stopPropagation()}
-                          />
-                        </TableCell>
-                        <TableCell className="font-mono text-xs text-muted-foreground">
-                          {c.customerNumber || '-'}
-                        </TableCell>
-                        <TableCell>
-                          <div className="flex items-center gap-2">
-                            <Users className="h-4 w-4 text-muted-foreground shrink-0" />
-                            <span className="truncate font-medium">{c.name}</span>
-                          </div>
-                        </TableCell>
-                        <TableCell className="hidden truncate sm:table-cell text-muted-foreground">
-                          {c.company || '-'}
-                        </TableCell>
-                        <TableCell className="hidden truncate md:table-cell text-muted-foreground">
-                          {c.phone || '-'}
-                        </TableCell>
-                        <TableCell className="hidden truncate lg:table-cell text-muted-foreground">
-                          {c.email || '-'}
-                        </TableCell>
-                        <TableCell className="text-center">{c._count.vehicles}</TableCell>
-                        <TableCell>
-                          <DropdownMenu>
-                            <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
-                              <Button
-                                variant="ghost"
-                                size="icon"
-                                className="h-8 w-8"
-                                aria-label={t('openMenu')}
-                              >
-                                <MoreVertical className="h-4 w-4" />
-                              </Button>
-                            </DropdownMenuTrigger>
-                            <DropdownMenuContent align="end">
-                              <DropdownMenuItem
-                                onClick={(e) => {
-                                  e.stopPropagation()
-                                  setEditCustomer(c)
-                                  setShowForm(true)
-                                }}
-                              >
-                                <Pencil className="mr-2 h-4 w-4" />
-                                {tc('buttons.edit')}
-                              </DropdownMenuItem>
-                              <DropdownMenuItem
-                                className="text-destructive"
-                                onClick={(e) => {
-                                  e.stopPropagation()
-                                  handleDelete(c.id, c.name)
-                                }}
-                              >
-                                <Trash2 className="mr-2 h-4 w-4" />
-                                {tc('buttons.delete')}
-                              </DropdownMenuItem>
-                            </DropdownMenuContent>
-                          </DropdownMenu>
-                        </TableCell>
-                      </TableRow>
-                    </ContextMenuTrigger>
-                    <ContextMenuContent className="min-w-52">
-                      <ContextMenuItem onClick={() => router.push(`/customers/${c.id}`)}>
-                        <ExternalLink className="mr-2 h-4 w-4" />
-                        {tcm('open')}
-                      </ContextMenuItem>
-                      <ContextMenuSeparator />
-                      <ContextMenuItem
-                        onClick={() => {
-                          setEditCustomer(c)
-                          setShowForm(true)
-                        }}
-                      >
-                        <Pencil className="mr-2 h-4 w-4" />
-                        {tc('buttons.edit')}
-                      </ContextMenuItem>
-                      <ContextMenuItem
-                        variant="destructive"
-                        onClick={() => handleDelete(c.id, c.name)}
-                      >
-                        <Trash2 className="mr-2 h-4 w-4" />
-                        {tc('buttons.delete')}
-                      </ContextMenuItem>
-                    </ContextMenuContent>
-                  </ContextMenu>
-                ))
-              )}
-            </TableBody>
-          </Table>
-        </div>
+            ) : (
+              data.customers.map((c) => (
+                <ContextMenu key={c.id} modal={false}>
+                  <ContextMenuTrigger asChild>
+                    <TableRow
+                      className={`cursor-pointer ${selected.has(c.id) ? 'bg-muted/50' : ''}`}
+                      {...interactiveRow(() => router.push(`/customers/${c.id}`))}
+                    >
+                      <TableCell className="w-[40px]">
+                        <Checkbox
+                          checked={selected.has(c.id)}
+                          onCheckedChange={() => toggleSelect(c.id)}
+                          onClick={(e) => e.stopPropagation()}
+                        />
+                      </TableCell>
+                      <TableCell className="font-mono text-xs text-muted-foreground">
+                        {c.customerNumber || '-'}
+                      </TableCell>
+                      <TableCell>
+                        <div className="flex items-center gap-2">
+                          <Users className="h-4 w-4 text-muted-foreground shrink-0" />
+                          <span className="truncate font-medium">{c.name}</span>
+                        </div>
+                      </TableCell>
+                      <TableCell className="hidden truncate sm:table-cell text-muted-foreground">
+                        {c.company || '-'}
+                      </TableCell>
+                      <TableCell className="hidden truncate md:table-cell text-muted-foreground">
+                        {c.phone || '-'}
+                      </TableCell>
+                      <TableCell className="hidden truncate lg:table-cell text-muted-foreground">
+                        {c.email || '-'}
+                      </TableCell>
+                      <TableCell className="text-center">{c._count.vehicles}</TableCell>
+                      <TableCell>
+                        <DropdownMenu>
+                          <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
+                            <Button
+                              variant="ghost"
+                              size="icon"
+                              className="h-8 w-8"
+                              aria-label={t('openMenu')}
+                            >
+                              <MoreVertical className="h-4 w-4" />
+                            </Button>
+                          </DropdownMenuTrigger>
+                          <DropdownMenuContent align="end">
+                            <DropdownMenuItem
+                              onClick={(e) => {
+                                e.stopPropagation()
+                                setEditCustomer(c)
+                                setShowForm(true)
+                              }}
+                            >
+                              <Pencil className="mr-2 h-4 w-4" />
+                              {tc('buttons.edit')}
+                            </DropdownMenuItem>
+                            <DropdownMenuItem
+                              className="text-destructive"
+                              onClick={(e) => {
+                                e.stopPropagation()
+                                handleDelete(c.id, c.name)
+                              }}
+                            >
+                              <Trash2 className="mr-2 h-4 w-4" />
+                              {tc('buttons.delete')}
+                            </DropdownMenuItem>
+                          </DropdownMenuContent>
+                        </DropdownMenu>
+                      </TableCell>
+                    </TableRow>
+                  </ContextMenuTrigger>
+                  <ContextMenuContent className="min-w-52">
+                    <ContextMenuItem onClick={() => router.push(`/customers/${c.id}`)}>
+                      <ExternalLink className="mr-2 h-4 w-4" />
+                      {tcm('open')}
+                    </ContextMenuItem>
+                    <ContextMenuSeparator />
+                    <ContextMenuItem
+                      onClick={() => {
+                        setEditCustomer(c)
+                        setShowForm(true)
+                      }}
+                    >
+                      <Pencil className="mr-2 h-4 w-4" />
+                      {tc('buttons.edit')}
+                    </ContextMenuItem>
+                    <ContextMenuItem
+                      variant="destructive"
+                      onClick={() => handleDelete(c.id, c.name)}
+                    >
+                      <Trash2 className="mr-2 h-4 w-4" />
+                      {tc('buttons.delete')}
+                    </ContextMenuItem>
+                  </ContextMenuContent>
+                </ContextMenu>
+              ))
+            )}
+          </TableBody>
+        </Table>
       </div>
       </div>
 
 
       <DataTablePagination
       <DataTablePagination

+ 3 - 2
src/app/(authenticated)/customers/page.tsx

@@ -3,6 +3,7 @@ import { getTranslations } from 'next-intl/server'
 import { getCustomersPaginated } from '@/features/customers/Actions/customerActions'
 import { getCustomersPaginated } from '@/features/customers/Actions/customerActions'
 import { CustomersClient } from './customers-client'
 import { CustomersClient } from './customers-client'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
+import { ListPage } from '@/components/list-page'
 
 
 export default async function CustomersPage({
 export default async function CustomersPage({
   searchParams,
   searchParams,
@@ -43,14 +44,14 @@ export default async function CustomersPage({
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+      <ListPage>
         <CustomersClient
         <CustomersClient
           data={result.data}
           data={result.data}
           search={params.search || ''}
           search={params.search || ''}
           sortBy={sort.sortBy || ''}
           sortBy={sort.sortBy || ''}
           sortOrder={sort.sortOrder}
           sortOrder={sort.sortOrder}
         />
         />
-      </div>
+      </ListPage>
     </>
     </>
   )
   )
 }
 }

+ 12 - 9
src/app/(authenticated)/inspections/inspections-client.tsx

@@ -233,9 +233,9 @@ export function InspectionsClient({
   }
   }
 
 
   return (
   return (
-    <div className="space-y-4">
+    <div className="flex min-h-0 flex-1 flex-col gap-4">
       {/* Status filters: one scrollable row on phones, wrapped above sm. */}
       {/* Status filters: one scrollable row on phones, wrapped above sm. */}
-      <div className="-mx-1 flex gap-2 overflow-x-auto px-1 pb-1 sm:mx-0 sm:flex-wrap sm:overflow-visible sm:px-0 sm:pb-0">
+      <div className="-mx-1 flex shrink-0 gap-2 overflow-x-auto px-1 pb-1 sm:mx-0 sm:flex-wrap sm:overflow-visible sm:px-0 sm:pb-0">
         {statusTabs.map((tab) => {
         {statusTabs.map((tab) => {
           const isActive = statusFilter === tab.key
           const isActive = statusFilter === tab.key
           const count = tab.key === 'all' ? undefined : data.statusCounts[tab.key] || 0
           const count = tab.key === 'all' ? undefined : data.statusCounts[tab.key] || 0
@@ -258,7 +258,7 @@ export function InspectionsClient({
         })}
         })}
       </div>
       </div>
 
 
-      <div className="flex items-center justify-between gap-2">
+      <div className="flex shrink-0 items-center justify-between gap-2">
         <div className="flex flex-1 items-center gap-2">
         <div className="flex flex-1 items-center gap-2">
           <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
           <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
             <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
             <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@@ -284,8 +284,8 @@ export function InspectionsClient({
         </Button>
         </Button>
       </div>
       </div>
 
 
-      {/* Card list (phones + small tablets) */}
-      <div className="space-y-2 md:hidden">
+      {/* Card list (phones + small tablets) - only this scrolls */}
+      <div className="min-h-0 flex-1 space-y-2 overflow-y-auto md:hidden">
         {data.records.length === 0 ? (
         {data.records.length === 0 ? (
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
             {t('empty')}
             {t('empty')}
@@ -331,11 +331,14 @@ export function InspectionsClient({
         )}
         )}
       </div>
       </div>
 
 
-      {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
+      {/* Table (md and up) - only the rows scroll */}
+      <div
+        className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-lg border md:flex"
+        {...tableNav.containerProps}
+      >
         <TableContextMenuHint />
         <TableContextMenuHint />
-        <Table className="table-fixed">
-          <TableHeader>
+        <Table containerClassName="min-h-0 flex-1" className="table-fixed">
+          <TableHeader sticky>
             <TableRow>
             <TableRow>
               <TableHead>
               <TableHead>
                 <button
                 <button

+ 3 - 2
src/app/(authenticated)/inspections/page.tsx

@@ -3,6 +3,7 @@ import { getInspectionsPaginated } from '@/features/inspections/Actions/inspecti
 import { getTemplates } from '@/features/inspections/Actions/templateActions'
 import { getTemplates } from '@/features/inspections/Actions/templateActions'
 import { InspectionsClient } from './inspections-client'
 import { InspectionsClient } from './inspections-client'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
+import { ListPage } from '@/components/list-page'
 
 
 export default async function InspectionsPage({
 export default async function InspectionsPage({
   searchParams,
   searchParams,
@@ -49,7 +50,7 @@ export default async function InspectionsPage({
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+      <ListPage>
         <InspectionsClient
         <InspectionsClient
           data={result.data}
           data={result.data}
           templates={templates}
           templates={templates}
@@ -58,7 +59,7 @@ export default async function InspectionsPage({
           sortBy={sort.sortBy || ''}
           sortBy={sort.sortBy || ''}
           sortOrder={sort.sortOrder}
           sortOrder={sort.sortOrder}
         />
         />
-      </div>
+      </ListPage>
     </>
     </>
   )
   )
 }
 }

+ 11 - 8
src/app/(authenticated)/inventory/inventory-client.tsx

@@ -366,9 +366,9 @@ export function InventoryClient({
   )
   )
 
 
   return (
   return (
-    <div className="space-y-4">
+    <div className="flex min-h-0 flex-1 flex-col gap-4">
       {/* Toolbar */}
       {/* Toolbar */}
-      <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
+      <div className="flex shrink-0 flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
         {selected.size > 0 ? (
         {selected.size > 0 ? (
           <div className="flex flex-1 items-center gap-2">
           <div className="flex flex-1 items-center gap-2">
             <span className="text-sm text-muted-foreground">
             <span className="text-sm text-muted-foreground">
@@ -482,8 +482,8 @@ export function InventoryClient({
         </div>
         </div>
       </div>
       </div>
 
 
-      {/* Card list (phones + small tablets) */}
-      <div className="space-y-2 md:hidden">
+      {/* Card list (phones + small tablets) - only this scrolls */}
+      <div className="min-h-0 flex-1 space-y-2 overflow-y-auto md:hidden">
         {data.parts.length === 0 ? (
         {data.parts.length === 0 ? (
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
             {emptyMessage}
             {emptyMessage}
@@ -585,11 +585,14 @@ export function InventoryClient({
         )}
         )}
       </div>
       </div>
 
 
-      {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
+      {/* Table (md and up) - only the rows scroll */}
+      <div
+        className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-lg border md:flex"
+        {...tableNav.containerProps}
+      >
         <TableContextMenuHint />
         <TableContextMenuHint />
-        <Table>
-          <TableHeader>
+        <Table containerClassName="min-h-0 flex-1">
+          <TableHeader sticky>
             <TableRow>
             <TableRow>
               <TableHead className="w-[40px]">
               <TableHead className="w-[40px]">
                 <Checkbox
                 <Checkbox

+ 3 - 2
src/app/(authenticated)/inventory/page.tsx

@@ -8,6 +8,7 @@ import { getSettings } from '@/features/settings/Actions/settingsActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { InventoryClient } from './inventory-client'
 import { InventoryClient } from './inventory-client'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
+import { ListPage } from '@/components/list-page'
 
 
 export default async function InventoryPage({
 export default async function InventoryPage({
   searchParams,
   searchParams,
@@ -67,7 +68,7 @@ export default async function InventoryPage({
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+      <ListPage>
         <InventoryClient
         <InventoryClient
           data={result.data}
           data={result.data}
           search={params.search || ''}
           search={params.search || ''}
@@ -83,7 +84,7 @@ export default async function InventoryPage({
           lowStockOnly={params.lowStock === '1'}
           lowStockOnly={params.lowStock === '1'}
           hasAnyReorderPoint={reorderResult.data ?? false}
           hasAnyReorderPoint={reorderResult.data ?? false}
         />
         />
-      </div>
+      </ListPage>
     </>
     </>
   )
   )
 }
 }

+ 10 - 7
src/app/(authenticated)/labor-presets/labor-presets-client.tsx

@@ -188,9 +188,9 @@ export function LaborPresetsClient({
   }
   }
 
 
   return (
   return (
-    <div className="space-y-4">
+    <div className="flex min-h-0 flex-1 flex-col gap-4">
       {/* Toolbar */}
       {/* Toolbar */}
-      <div className="flex items-center justify-between gap-3">
+      <div className="flex shrink-0 items-center justify-between gap-3">
         <div className="flex flex-1 items-center gap-2">
         <div className="flex flex-1 items-center gap-2">
           <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
           <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
             <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
             <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@@ -217,7 +217,7 @@ export function LaborPresetsClient({
       </div>
       </div>
 
 
       {/* Card list (phones + small tablets) */}
       {/* Card list (phones + small tablets) */}
-      <div className="space-y-2 md:hidden">
+      <div className="min-h-0 flex-1 space-y-2 overflow-y-auto md:hidden">
         {data.presets.length === 0 ? (
         {data.presets.length === 0 ? (
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
             {search ? t('empty.noMatch') : t('empty.noPresets')}
             {search ? t('empty.noMatch') : t('empty.noPresets')}
@@ -288,11 +288,14 @@ export function LaborPresetsClient({
         )}
         )}
       </div>
       </div>
 
 
-      {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
+      {/* Table (md and up) - only the rows scroll */}
+      <div
+        className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-lg border md:flex"
+        {...tableNav.containerProps}
+      >
         <TableContextMenuHint />
         <TableContextMenuHint />
-        <Table>
-          <TableHeader>
+        <Table containerClassName="min-h-0 flex-1">
+          <TableHeader sticky>
             <TableRow>
             <TableRow>
               <TableHead>
               <TableHead>
                 <button
                 <button

+ 3 - 2
src/app/(authenticated)/labor-presets/page.tsx

@@ -5,6 +5,7 @@ import { getSettings } from '@/features/settings/Actions/settingsActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { LaborPresetsClient } from './labor-presets-client'
 import { LaborPresetsClient } from './labor-presets-client'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
+import { ListPage } from '@/components/list-page'
 
 
 export default async function LaborPresetsPage({
 export default async function LaborPresetsPage({
   searchParams,
   searchParams,
@@ -66,7 +67,7 @@ export default async function LaborPresetsPage({
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+      <ListPage>
         <LaborPresetsClient
         <LaborPresetsClient
           data={result.data}
           data={result.data}
           search={params.search || ''}
           search={params.search || ''}
@@ -76,7 +77,7 @@ export default async function LaborPresetsPage({
           defaultLaborRate={defaultLaborRate}
           defaultLaborRate={defaultLaborRate}
           inventoryParts={inventoryParts}
           inventoryParts={inventoryParts}
         />
         />
-      </div>
+      </ListPage>
     </>
     </>
   )
   )
 }
 }

+ 11 - 8
src/app/(authenticated)/observations/observations-client.tsx

@@ -114,9 +114,9 @@ export function ObservationsClient({
   )
   )
 
 
   return (
   return (
-    <div className="space-y-4">
+    <div className="flex min-h-0 flex-1 flex-col gap-4">
       {/* Filters */}
       {/* Filters */}
-      <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
+      <div className="flex shrink-0 flex-col gap-3 sm:flex-row sm:items-center">
         <div className="relative flex-1">
         <div className="relative flex-1">
           <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
           <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
           <Input
           <Input
@@ -159,8 +159,8 @@ export function ObservationsClient({
         </div>
         </div>
       </div>
       </div>
 
 
-      {/* Card list (phones + small tablets) */}
-      <div className="space-y-2 md:hidden">
+      {/* Card list (phones + small tablets) - only this scrolls */}
+      <div className="min-h-0 flex-1 space-y-2 overflow-y-auto md:hidden">
         {data.records.length === 0 ? (
         {data.records.length === 0 ? (
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
             {t('empty')}
             {t('empty')}
@@ -211,11 +211,14 @@ export function ObservationsClient({
         )}
         )}
       </div>
       </div>
 
 
-      {/* Table (md and up) */}
-      <div className="hidden rounded-md border md:block" {...tableNav.containerProps}>
+      {/* Table (md and up) - only the rows scroll */}
+      <div
+        className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-md border md:flex"
+        {...tableNav.containerProps}
+      >
         <TableContextMenuHint />
         <TableContextMenuHint />
-        <Table>
-          <TableHeader>
+        <Table containerClassName="min-h-0 flex-1">
+          <TableHeader sticky>
             <TableRow>
             <TableRow>
               <TableHead className="w-[110px]">{t('columns.severity')}</TableHead>
               <TableHead className="w-[110px]">{t('columns.severity')}</TableHead>
               <TableHead className="w-[100px]">{t('columns.status')}</TableHead>
               <TableHead className="w-[100px]">{t('columns.status')}</TableHead>

+ 3 - 2
src/app/(authenticated)/observations/page.tsx

@@ -1,5 +1,6 @@
 import { getObservationsPaginated } from '@/features/vehicles/Actions/findingActions'
 import { getObservationsPaginated } from '@/features/vehicles/Actions/findingActions'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
+import { ListPage } from '@/components/list-page'
 import { ObservationsClient } from './observations-client'
 import { ObservationsClient } from './observations-client'
 
 
 export default async function ObservationsPage({
 export default async function ObservationsPage({
@@ -37,14 +38,14 @@ export default async function ObservationsPage({
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+      <ListPage>
         <ObservationsClient
         <ObservationsClient
           data={result.data}
           data={result.data}
           search={params.search || ''}
           search={params.search || ''}
           statusFilter={params.status || 'open'}
           statusFilter={params.status || 'open'}
           severityFilter={params.severity || 'all'}
           severityFilter={params.severity || 'all'}
         />
         />
-      </div>
+      </ListPage>
     </>
     </>
   )
   )
 }
 }

+ 3 - 2
src/app/(authenticated)/quotes/page.tsx

@@ -4,6 +4,7 @@ import { getSettings } from '@/features/settings/Actions/settingsActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { QuotesClient } from './quotes-client'
 import { QuotesClient } from './quotes-client'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
+import { ListPage } from '@/components/list-page'
 
 
 export default async function QuotesPage({
 export default async function QuotesPage({
   searchParams,
   searchParams,
@@ -51,7 +52,7 @@ export default async function QuotesPage({
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+      <ListPage>
         <QuotesClient
         <QuotesClient
           data={result.data}
           data={result.data}
           currencyCode={currencyCode}
           currencyCode={currencyCode}
@@ -60,7 +61,7 @@ export default async function QuotesPage({
           sortBy={sort.sortBy || ''}
           sortBy={sort.sortBy || ''}
           sortOrder={sort.sortOrder}
           sortOrder={sort.sortOrder}
         />
         />
-      </div>
+      </ListPage>
     </>
     </>
   )
   )
 }
 }

+ 12 - 9
src/app/(authenticated)/quotes/quotes-client.tsx

@@ -173,9 +173,9 @@ export function QuotesClient({
   }
   }
 
 
   return (
   return (
-    <div className="space-y-4">
+    <div className="flex min-h-0 flex-1 flex-col gap-4">
       {/* Status filters: a single scrollable row on phones, wrapped above sm. */}
       {/* Status filters: a single scrollable row on phones, wrapped above sm. */}
-      <div className="-mx-1 flex gap-2 overflow-x-auto px-1 pb-1 sm:mx-0 sm:flex-wrap sm:overflow-visible sm:px-0 sm:pb-0">
+      <div className="-mx-1 flex shrink-0 gap-2 overflow-x-auto px-1 pb-1 sm:mx-0 sm:flex-wrap sm:overflow-visible sm:px-0 sm:pb-0">
         {statusTabs.map((tab) => {
         {statusTabs.map((tab) => {
           const isActive = statusFilter === tab.key
           const isActive = statusFilter === tab.key
           const count = tab.key === 'all' ? undefined : data.statusCounts[tab.key] || 0
           const count = tab.key === 'all' ? undefined : data.statusCounts[tab.key] || 0
@@ -198,7 +198,7 @@ export function QuotesClient({
         })}
         })}
       </div>
       </div>
 
 
-      <div className="flex items-center justify-between gap-2">
+      <div className="flex shrink-0 items-center justify-between gap-2">
         <div className="flex flex-1 items-center gap-2">
         <div className="flex flex-1 items-center gap-2">
           <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
           <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
             <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
             <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@@ -224,8 +224,8 @@ export function QuotesClient({
         </Button>
         </Button>
       </div>
       </div>
 
 
-      {/* Card list (phones + small tablets) */}
-      <div className="space-y-2 md:hidden">
+      {/* Card list (phones + small tablets) - only this scrolls */}
+      <div className="min-h-0 flex-1 space-y-2 overflow-y-auto md:hidden">
         {data.records.length === 0 ? (
         {data.records.length === 0 ? (
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
             {t('list.noQuotes')}
             {t('list.noQuotes')}
@@ -273,11 +273,14 @@ export function QuotesClient({
         )}
         )}
       </div>
       </div>
 
 
-      {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
+      {/* Table (md and up) - only the rows scroll */}
+      <div
+        className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-lg border md:flex"
+        {...tableNav.containerProps}
+      >
         <TableContextMenuHint />
         <TableContextMenuHint />
-        <Table className="table-fixed">
-          <TableHeader>
+        <Table containerClassName="min-h-0 flex-1" className="table-fixed">
+          <TableHeader sticky>
             <TableRow>
             <TableRow>
               <TableHead className="w-25">
               <TableHead className="w-25">
                 <button
                 <button

+ 3 - 2
src/app/(authenticated)/tire-hotel/page.tsx

@@ -8,6 +8,7 @@ import { getSettings } from '@/features/settings/Actions/settingsActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { totalFree as sumFree } from '@/features/tire-hotel/Lib/capacity'
 import { totalFree as sumFree } from '@/features/tire-hotel/Lib/capacity'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
+import { ListPage } from '@/components/list-page'
 import { TireHotelClient } from './tire-hotel-client'
 import { TireHotelClient } from './tire-hotel-client'
 
 
 export default async function TireHotelPage({
 export default async function TireHotelPage({
@@ -71,7 +72,7 @@ export default async function TireHotelPage({
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+      <ListPage>
         <TireHotelClient
         <TireHotelClient
           data={data}
           data={data}
           locations={locations}
           locations={locations}
@@ -86,7 +87,7 @@ export default async function TireHotelPage({
           statusFilter={status}
           statusFilter={status}
           totalFree={sumFree(locations)}
           totalFree={sumFree(locations)}
         />
         />
-      </div>
+      </ListPage>
     </>
     </>
   )
   )
 }
 }

+ 11 - 8
src/app/(authenticated)/tire-hotel/tire-hotel-client.tsx

@@ -160,8 +160,8 @@ export function TireHotelClient({
   } = useDebouncedSearch(search, (term) => navigate({ search: term }))
   } = useDebouncedSearch(search, (term) => navigate({ search: term }))
 
 
   return (
   return (
-    <div className="space-y-4">
-      <div className="-mx-1 flex gap-2 overflow-x-auto px-1 pb-1 sm:mx-0 sm:flex-wrap sm:overflow-visible sm:px-0 sm:pb-0">
+    <div className="flex min-h-0 flex-1 flex-col gap-4">
+      <div className="-mx-1 flex shrink-0 gap-2 overflow-x-auto px-1 pb-1 sm:mx-0 sm:flex-wrap sm:overflow-visible sm:px-0 sm:pb-0">
         {statusTabs.map((tab) => {
         {statusTabs.map((tab) => {
           const isActive = statusFilter === tab.key
           const isActive = statusFilter === tab.key
           const count = tab.key === 'all' ? undefined : data.statusCounts[tab.key] || 0
           const count = tab.key === 'all' ? undefined : data.statusCounts[tab.key] || 0
@@ -204,7 +204,7 @@ export function TireHotelClient({
         </div>
         </div>
       </div>
       </div>
 
 
-      <div className="flex items-center justify-between gap-2">
+      <div className="flex shrink-0 items-center justify-between gap-2">
         <div className="flex flex-1 items-center gap-2">
         <div className="flex flex-1 items-center gap-2">
           <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
           <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
             <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
             <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@@ -240,7 +240,7 @@ export function TireHotelClient({
       </div>
       </div>
 
 
       {/* Card list (phones and small tablets) */}
       {/* Card list (phones and small tablets) */}
-      <div className="space-y-2 md:hidden">
+      <div className="min-h-0 flex-1 space-y-2 overflow-y-auto md:hidden">
         {data.records.length === 0 ? (
         {data.records.length === 0 ? (
           <EmptyState message={hasShelves ? t('list.empty') : t('list.emptyNoShelves')} />
           <EmptyState message={hasShelves ? t('list.empty') : t('list.emptyNoShelves')} />
         ) : (
         ) : (
@@ -293,11 +293,14 @@ export function TireHotelClient({
         )}
         )}
       </div>
       </div>
 
 
-      {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
+      {/* Table (md and up) - only the rows scroll */}
+      <div
+        className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-lg border md:flex"
+        {...tableNav.containerProps}
+      >
         <TableContextMenuHint />
         <TableContextMenuHint />
-        <Table className="table-fixed">
-          <TableHeader>
+        <Table containerClassName="min-h-0 flex-1" className="table-fixed">
+          <TableHeader sticky>
             <TableRow>
             <TableRow>
               <TableHead className="w-14">{t('list.reference')}</TableHead>
               <TableHead className="w-14">{t('list.reference')}</TableHead>
               {/* Vehicle takes what is left rather than a share of the table:
               {/* Vehicle takes what is left rather than a share of the table:

+ 3 - 2
src/app/(authenticated)/vehicles/page.tsx

@@ -5,6 +5,7 @@ import { getVehiclesPaginated } from '@/features/vehicles/Actions/vehicleActions
 import { getCustomersList } from '@/features/customers/Actions/customerActions'
 import { getCustomersList } from '@/features/customers/Actions/customerActions'
 import { VehiclesClient } from './vehicles-client'
 import { VehiclesClient } from './vehicles-client'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
+import { ListPage } from '@/components/list-page'
 
 
 export default async function VehiclesPage({
 export default async function VehiclesPage({
   searchParams,
   searchParams,
@@ -65,7 +66,7 @@ export default async function VehiclesPage({
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+      <ListPage>
         <VehiclesClient
         <VehiclesClient
           data={result.data}
           data={result.data}
           customers={customersResult.data ?? []}
           customers={customersResult.data ?? []}
@@ -78,7 +79,7 @@ export default async function VehiclesPage({
           inspectionDue={inspectionDue}
           inspectionDue={inspectionDue}
           hasInspectionData={result.data.hasInspectionData}
           hasInspectionData={result.data.hasInspectionData}
         />
         />
-      </div>
+      </ListPage>
     </>
     </>
   )
   )
 }
 }

+ 14 - 11
src/app/(authenticated)/vehicles/vehicles-client.tsx

@@ -273,9 +273,9 @@ export function VehiclesClient({
   }
   }
 
 
   return (
   return (
-    <div className="space-y-4">
+    <div className="flex min-h-0 flex-1 flex-col gap-4">
       {/* Toolbar */}
       {/* Toolbar */}
-      <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
+      <div className="flex shrink-0 flex-col justify-between gap-3 sm:flex-row sm:items-center">
         <div className="flex flex-1 items-center gap-2 min-w-0">
         <div className="flex flex-1 items-center gap-2 min-w-0">
           <div className="flex gap-1 rounded-lg border p-1 shrink-0">
           <div className="flex gap-1 rounded-lg border p-1 shrink-0">
             <button
             <button
@@ -412,13 +412,13 @@ export function VehiclesClient({
       </div>
       </div>
 
 
       {data.vehicles.length === 0 ? (
       {data.vehicles.length === 0 ? (
-        <div className="flex h-32 items-center justify-center rounded-lg border text-muted-foreground">
+        <div className="flex h-32 shrink-0 items-center justify-center rounded-lg border text-muted-foreground">
           {search ? t('emptySearch') : isArchived ? t('emptyArchived') : t('empty')}
           {search ? t('emptySearch') : isArchived ? t('emptyArchived') : t('empty')}
         </div>
         </div>
       ) : view === 'table' ? (
       ) : view === 'table' ? (
         <>
         <>
-          {/* Card list (phones + small tablets) */}
-          <div className="space-y-2 md:hidden">
+          {/* Card list (phones + small tablets) - only this scrolls */}
+          <div className="min-h-0 flex-1 space-y-2 overflow-y-auto md:hidden">
             {data.vehicles.map((v) => (
             {data.vehicles.map((v) => (
               <div key={v.id} className="flex items-start gap-2 rounded-lg border bg-card p-3">
               <div key={v.id} className="flex items-start gap-2 rounded-lg border bg-card p-3">
                 <button
                 <button
@@ -504,11 +504,14 @@ export function VehiclesClient({
             ))}
             ))}
           </div>
           </div>
 
 
-          {/* Table (md and up) */}
-          <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
+          {/* Table (md and up) - only the rows scroll */}
+          <div
+            className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-lg border md:flex"
+            {...tableNav.containerProps}
+          >
             <TableContextMenuHint />
             <TableContextMenuHint />
-            <Table className="table-fixed">
-              <TableHeader>
+            <Table containerClassName="min-h-0 flex-1" className="table-fixed">
+              <TableHeader sticky>
                 <TableRow>
                 <TableRow>
                   <TableHead className="w-[120px]">
                   <TableHead className="w-[120px]">
                     <button
                     <button
@@ -720,7 +723,7 @@ export function VehiclesClient({
       ) : isPending ? (
       ) : isPending ? (
         /* Grid skeleton */
         /* Grid skeleton */
         <div
         <div
-          className={`grid gap-4 sm:grid-cols-2 lg:grid-cols-3 ${view === 'grid6' ? 'xl:grid-cols-4 2xl:grid-cols-6' : 'xl:grid-cols-4'}`}
+          className={`-m-1 grid min-h-0 flex-1 content-start gap-4 overflow-y-auto p-1 sm:grid-cols-2 lg:grid-cols-3 ${view === 'grid6' ? 'xl:grid-cols-4 2xl:grid-cols-6' : 'xl:grid-cols-4'}`}
         >
         >
           {Array.from({ length: view === 'grid6' ? 12 : 6 }).map((_, i) => (
           {Array.from({ length: view === 'grid6' ? 12 : 6 }).map((_, i) => (
             <Card key={i} className="overflow-hidden border-0 py-0 gap-0 shadow-sm">
             <Card key={i} className="overflow-hidden border-0 py-0 gap-0 shadow-sm">
@@ -735,7 +738,7 @@ export function VehiclesClient({
       ) : (
       ) : (
         /* Grid view */
         /* Grid view */
         <div
         <div
-          className={`grid gap-4 sm:grid-cols-2 lg:grid-cols-3 ${view === 'grid6' ? 'xl:grid-cols-4 2xl:grid-cols-6' : 'xl:grid-cols-4'}`}
+          className={`-m-1 grid min-h-0 flex-1 content-start gap-4 overflow-y-auto p-1 sm:grid-cols-2 lg:grid-cols-3 ${view === 'grid6' ? 'xl:grid-cols-4 2xl:grid-cols-6' : 'xl:grid-cols-4'}`}
         >
         >
           {data.vehicles.map((v) => (
           {data.vehicles.map((v) => (
             <ContextMenu key={v.id} modal={false}>
             <ContextMenu key={v.id} modal={false}>

+ 3 - 2
src/app/(authenticated)/work-orders/page.tsx

@@ -9,6 +9,7 @@ import { getAuthContext } from '@/lib/get-auth-context'
 import { getFeatures } from '@/lib/features'
 import { getFeatures } from '@/lib/features'
 import { WorkOrdersClient } from './work-orders-client'
 import { WorkOrdersClient } from './work-orders-client'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
+import { ListPage } from '@/components/list-page'
 
 
 export default async function WorkOrdersPage({
 export default async function WorkOrdersPage({
   searchParams,
   searchParams,
@@ -75,7 +76,7 @@ export default async function WorkOrdersPage({
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+      <ListPage>
         <WorkOrdersClient
         <WorkOrdersClient
           data={result.data}
           data={result.data}
           vehicles={vehicles}
           vehicles={vehicles}
@@ -88,7 +89,7 @@ export default async function WorkOrdersPage({
           smsEnabled={features?.sms ?? false}
           smsEnabled={features?.sms ?? false}
           emailEnabled={features?.smtp ?? false}
           emailEnabled={features?.smtp ?? false}
         />
         />
-      </div>
+      </ListPage>
     </>
     </>
   )
   )
 }
 }

+ 12 - 9
src/app/(authenticated)/work-orders/work-orders-client.tsx

@@ -263,9 +263,9 @@ export function WorkOrdersClient({
   }
   }
 
 
   return (
   return (
-    <div className="space-y-4">
+    <div className="flex min-h-0 flex-1 flex-col gap-4">
       {/* Status tabs: one scrollable row on phones, wrapped above sm. */}
       {/* Status tabs: one scrollable row on phones, wrapped above sm. */}
-      <div className="-mx-1 flex gap-2 overflow-x-auto px-1 pb-1 sm:mx-0 sm:flex-wrap sm:overflow-visible sm:px-0 sm:pb-0">
+      <div className="-mx-1 flex shrink-0 gap-2 overflow-x-auto px-1 pb-1 sm:mx-0 sm:flex-wrap sm:overflow-visible sm:px-0 sm:pb-0">
         {statusTabKeys.map((key) => {
         {statusTabKeys.map((key) => {
           const isActive = statusFilter === key
           const isActive = statusFilter === key
           const count = key === 'all' || key === 'active' ? undefined : data.statusCounts[key] || 0
           const count = key === 'all' || key === 'active' ? undefined : data.statusCounts[key] || 0
@@ -289,7 +289,7 @@ export function WorkOrdersClient({
       </div>
       </div>
 
 
       {/* Toolbar */}
       {/* Toolbar */}
-      <div className="flex items-center justify-between gap-2">
+      <div className="flex shrink-0 items-center justify-between gap-2">
         <div className="flex flex-1 items-center gap-2">
         <div className="flex flex-1 items-center gap-2">
           <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
           <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
             <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
             <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@@ -315,8 +315,8 @@ export function WorkOrdersClient({
         </Button>
         </Button>
       </div>
       </div>
 
 
-      {/* Card list (phones + small tablets) */}
-      <div className="space-y-2 md:hidden">
+      {/* Card list (phones + small tablets) - only this scrolls */}
+      <div className="min-h-0 flex-1 space-y-2 overflow-y-auto md:hidden">
         {data.records.length === 0 ? (
         {data.records.length === 0 ? (
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
             {t('empty')}
             {t('empty')}
@@ -378,14 +378,17 @@ export function WorkOrdersClient({
         )}
         )}
       </div>
       </div>
 
 
-      {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
+      {/* Table (md and up) - only the rows scroll */}
+      <div
+        className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-lg border md:flex"
+        {...tableNav.containerProps}
+      >
         <TableContextMenuHint />
         <TableContextMenuHint />
         {/* Low-priority columns drop out at sm/md/lg; the min-width stops what
         {/* Low-priority columns drop out at sm/md/lg; the min-width stops what
             is left from being squeezed to a few characters, scrolling the
             is left from being squeezed to a few characters, scrolling the
             table sideways instead (the Table wrapper handles the overflow) */}
             table sideways instead (the Table wrapper handles the overflow) */}
-        <Table className="min-w-[36rem] table-fixed">
-          <TableHeader>
+        <Table containerClassName="min-h-0 flex-1" className="min-w-[36rem] table-fixed">
+          <TableHeader sticky>
             <TableRow>
             <TableRow>
               <TableHead className="hidden sm:table-cell w-[100px]">
               <TableHead className="hidden sm:table-cell w-[100px]">
                 <button
                 <button

+ 1 - 1
src/components/data-table-pagination.tsx

@@ -51,7 +51,7 @@ export function DataTablePagination({
   const goTo = (target: number) => onNavigate({ [pageParam]: target })
   const goTo = (target: number) => onNavigate({ [pageParam]: target })
 
 
   return (
   return (
-    <div className="flex flex-col items-center justify-between gap-3 sm:flex-row">
+    <div className="flex shrink-0 flex-col items-center justify-between gap-3 sm:flex-row">
       <div className="flex items-center gap-2 text-xs text-muted-foreground sm:text-sm">
       <div className="flex items-center gap-2 text-xs text-muted-foreground sm:text-sm">
         <span>{t('showing', { start: startItem, end: endItem, total })}</span>
         <span>{t('showing', { start: startItem, end: endItem, total })}</span>
         <Select
         <Select

+ 72 - 0
src/components/list-page.tsx

@@ -0,0 +1,72 @@
+'use client'
+
+import { useEffect, useRef } from 'react'
+import { cn } from '@/lib/utils'
+
+/**
+ * Body of a list page: it ends at the bottom of the window instead of growing
+ * with its rows, so the list scrolls inside itself and the page around it
+ * (header, toolbar, pagination) never moves.
+ *
+ * Height is measured from wherever the element actually starts rather than
+ * assumed, because the chrome above it is not fixed: an update notice or a
+ * licence warning pushes the whole page down, and a guessed offset would leave
+ * the pagination row hanging below the fold. The CSS fallback covers the first
+ * paint (the 4rem header, plus on phones the 3.5rem bottom nav strip the app
+ * layout reserves) so nothing jumps once the measurement lands.
+ *
+ * Children lay themselves out as flex rows: `shrink-0` for the parts that stay
+ * put, `min-h-0 flex-1` with its own `overflow-y-auto` for the part that
+ * scrolls.
+ */
+export function ListPage({
+  className,
+  children,
+}: {
+  className?: string
+  children: React.ReactNode
+}) {
+  const ref = useRef<HTMLDivElement>(null)
+
+  useEffect(() => {
+    const el = ref.current
+    if (!el) return
+
+    const fit = () => {
+      const top = el.getBoundingClientRect().top + window.scrollY
+      // Whatever the layout reserves below us (the mobile bottom nav) is
+      // padding on our parent, and it has to come out of the height or the
+      // document gains exactly that much scroll.
+      const parent = el.parentElement
+      const reserved = parent ? parseFloat(getComputedStyle(parent).paddingBottom) || 0 : 0
+      const height = `${Math.max(240, window.innerHeight - top - reserved)}px`
+      // Our own height feeds back into the body height the observer watches,
+      // so only write when it actually changed.
+      if (el.style.height !== height) el.style.height = height
+    }
+
+    fit()
+    window.addEventListener('resize', fit)
+    // Catches the chrome above changing height while the page is open: a
+    // banner appearing, a warning being dismissed.
+    const observer = new ResizeObserver(fit)
+    observer.observe(document.body)
+    return () => {
+      window.removeEventListener('resize', fit)
+      observer.disconnect()
+    }
+  }, [])
+
+  return (
+    <div
+      ref={ref}
+      className={cn(
+        'flex min-h-0 flex-col gap-4 p-4 pt-0',
+        'h-[calc(100svh-7.5rem)] md:h-[calc(100svh-4rem)]',
+        className
+      )}
+    >
+      {children}
+    </div>
+  )
+}

+ 1 - 1
src/components/table-context-menu-hint.tsx

@@ -10,7 +10,7 @@ import { useTranslations } from 'next-intl'
 export function TableContextMenuHint() {
 export function TableContextMenuHint() {
   const t = useTranslations('common.contextMenu')
   const t = useTranslations('common.contextMenu')
   return (
   return (
-    <div className="hidden items-center gap-1.5 border-b bg-muted/30 px-3 py-1.5 text-xs text-muted-foreground md:flex">
+    <div className="hidden shrink-0 items-center gap-1.5 border-b bg-muted/30 px-3 py-1.5 text-xs text-muted-foreground md:flex">
       <MousePointerClick className="h-3.5 w-3.5" />
       <MousePointerClick className="h-3.5 w-3.5" />
       {t('hint')}
       {t('hint')}
     </div>
     </div>

+ 37 - 4
src/components/ui/table.tsx

@@ -4,9 +4,22 @@ import * as React from 'react'
 
 
 import { cn } from '@/lib/utils'
 import { cn } from '@/lib/utils'
 
 
-function Table({ className, ...props }: React.ComponentProps<'table'>) {
+/**
+ * `containerClassName` styles the scroll box around the table. A sticky header
+ * sticks to the nearest scrolling ancestor, and that is always this box — so a
+ * list that scrolls its rows has to make the height limit and the overflow land
+ * here, not on a wrapper further out, or the header scrolls away with them.
+ */
+function Table({
+  className,
+  containerClassName,
+  ...props
+}: React.ComponentProps<'table'> & { containerClassName?: string }) {
   return (
   return (
-    <div data-slot="table-container" className="relative w-full overflow-x-auto">
+    <div
+      data-slot="table-container"
+      className={cn('relative w-full overflow-auto', containerClassName)}
+    >
       <table
       <table
         data-slot="table"
         data-slot="table"
         className={cn('w-full caption-bottom text-sm', className)}
         className={cn('w-full caption-bottom text-sm', className)}
@@ -16,8 +29,28 @@ function Table({ className, ...props }: React.ComponentProps<'table'>) {
   )
   )
 }
 }
 
 
-function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
-  return <thead data-slot="table-header" className={cn('[&_tr]:border-b', className)} {...props} />
+/**
+ * `sticky` pins the header while the rows scroll under it. The bottom rule is
+ * drawn as an inset shadow because a collapsed table border belongs to the
+ * cells and does not travel with a stuck header.
+ */
+function TableHeader({
+  className,
+  sticky,
+  ...props
+}: React.ComponentProps<'thead'> & { sticky?: boolean }) {
+  return (
+    <thead
+      data-slot="table-header"
+      className={cn(
+        '[&_tr]:border-b',
+        sticky &&
+          'sticky top-0 z-10 bg-background [&_tr]:border-b-0 [&_th]:shadow-[inset_0_-1px_0_var(--border)]',
+        className
+      )}
+      {...props}
+    />
+  )
 }
 }
 
 
 function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
 function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {

+ 7 - 7
src/features/admin/Components/admin-organizations.tsx

@@ -120,8 +120,8 @@ export function AdminOrganizations({ data, search }: { data: PaginatedData; sear
   }
   }
 
 
   return (
   return (
-    <div className="space-y-4">
-      <div className="flex items-center gap-2">
+    <div className="flex min-h-0 flex-1 flex-col gap-4">
+      <div className="flex shrink-0 items-center gap-2">
         <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
         <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
           <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
           <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
           <Input
           <Input
@@ -135,7 +135,7 @@ export function AdminOrganizations({ data, search }: { data: PaginatedData; sear
       </div>
       </div>
 
 
       {/* Card list (phones + small tablets) */}
       {/* Card list (phones + small tablets) */}
-      <div className="space-y-2 md:hidden">
+      <div className="min-h-0 flex-1 space-y-2 overflow-y-auto md:hidden">
         {data.organizations.length === 0 ? (
         {data.organizations.length === 0 ? (
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
             {search ? t('organizations.noResults') : t('organizations.noOrganizations')}
             {search ? t('organizations.noResults') : t('organizations.noOrganizations')}
@@ -190,10 +190,10 @@ export function AdminOrganizations({ data, search }: { data: PaginatedData; sear
         )}
         )}
       </div>
       </div>
 
 
-      {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block">
-        <Table>
-          <TableHeader>
+      {/* Table (md and up) - only the rows scroll */}
+      <div className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-lg border md:flex">
+        <Table containerClassName="min-h-0 flex-1">
+          <TableHeader sticky>
             <TableRow>
             <TableRow>
               <TableHead>{t('organizations.name')}</TableHead>
               <TableHead>{t('organizations.name')}</TableHead>
               <TableHead>{t('organizations.owner')}</TableHead>
               <TableHead>{t('organizations.owner')}</TableHead>

+ 10 - 7
src/features/admin/Components/admin-users.tsx

@@ -190,8 +190,8 @@ export function AdminUsers({
   }
   }
 
 
   return (
   return (
-    <div className="space-y-4">
-      <div className="flex items-center gap-2">
+    <div className="flex min-h-0 flex-1 flex-col gap-4">
+      <div className="flex shrink-0 items-center gap-2">
         <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
         <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
           <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
           <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
           <Input
           <Input
@@ -206,7 +206,7 @@ export function AdminUsers({
       </div>
       </div>
 
 
       {/* Card list (phones + small tablets) */}
       {/* Card list (phones + small tablets) */}
-      <div className="space-y-2 md:hidden">
+      <div className="min-h-0 flex-1 space-y-2 overflow-y-auto md:hidden">
         {data.users.length === 0 ? (
         {data.users.length === 0 ? (
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
           <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
             {search ? t('users.noResults') : t('users.noUsers')}
             {search ? t('users.noResults') : t('users.noUsers')}
@@ -298,10 +298,13 @@ export function AdminUsers({
         )}
         )}
       </div>
       </div>
 
 
-      {/* Table (md and up) */}
-      <div className="hidden rounded-lg border md:block" {...tableNav.containerProps}>
-        <Table>
-          <TableHeader>
+      {/* Table (md and up) - only the rows scroll */}
+      <div
+        className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-lg border md:flex"
+        {...tableNav.containerProps}
+      >
+        <Table containerClassName="min-h-0 flex-1">
+          <TableHeader sticky>
             <TableRow>
             <TableRow>
               <TableHead>
               <TableHead>
                 <button
                 <button

+ 12 - 11
src/features/billing/Components/RecurringInvoicesClient.tsx

@@ -283,9 +283,9 @@ export default function RecurringInvoicesClient({
   const fmt = (n: number) => formatCurrency(n, currencyCode)
   const fmt = (n: number) => formatCurrency(n, currencyCode)
 
 
   return (
   return (
-    <div className="space-y-4">
+    <div className="flex min-h-0 flex-1 flex-col gap-4">
       {/* Header */}
       {/* Header */}
-      <div className="flex flex-wrap items-center justify-between gap-2">
+      <div className="flex shrink-0 flex-wrap items-center justify-between gap-2">
         <div className="flex items-center gap-3">
         <div className="flex items-center gap-3">
           <Link href="/billing">
           <Link href="/billing">
             <Button variant="outline" size="sm">
             <Button variant="outline" size="sm">
@@ -329,16 +329,16 @@ export default function RecurringInvoicesClient({
 
 
       {/* Table */}
       {/* Table */}
       {invoices.length === 0 ? (
       {invoices.length === 0 ? (
-        <Card>
+        <Card className="shrink-0">
           <CardContent className="flex flex-col items-center justify-center py-12">
           <CardContent className="flex flex-col items-center justify-center py-12">
             <p className="text-muted-foreground">{t('recurring.noInvoices')}</p>
             <p className="text-muted-foreground">{t('recurring.noInvoices')}</p>
           </CardContent>
           </CardContent>
         </Card>
         </Card>
       ) : (
       ) : (
-        <Card>
-          <CardContent className="p-0">
-            {/* Card list (phones + small tablets) */}
-            <div className="space-y-2 p-3 md:hidden">
+        <Card className="flex min-h-0 flex-1 flex-col overflow-hidden">
+          <CardContent className="flex min-h-0 flex-1 flex-col p-0">
+            {/* Card list (phones + small tablets) - only this scrolls */}
+            <div className="min-h-0 flex-1 space-y-2 overflow-y-auto p-3 md:hidden">
               {invoices.map((inv) => {
               {invoices.map((inv) => {
                 const partsTotal = inv.templateParts.reduce(
                 const partsTotal = inv.templateParts.reduce(
                   (s, p) => s + p.quantity * p.unitPrice,
                   (s, p) => s + p.quantity * p.unitPrice,
@@ -403,11 +403,12 @@ export default function RecurringInvoicesClient({
               })}
               })}
             </div>
             </div>
 
 
-            {/* Table (md and up) */}
-            <div className="hidden md:block">
+            {/* Table (md and up) - only the rows scroll */}
+            <div className="hidden min-h-0 flex-1 flex-col md:flex">
               <TableContextMenuHint />
               <TableContextMenuHint />
-              <Table>
-                <TableHeader>
+              <Table containerClassName="min-h-0 flex-1">
+                {/* Inside a Card, so the pinned header takes the card surface. */}
+                <TableHeader sticky className="bg-card">
                   <TableRow>
                   <TableRow>
                     <TableHead>{t('recurring.columnTitle')}</TableHead>
                     <TableHead>{t('recurring.columnTitle')}</TableHead>
                     <TableHead>{t('recurring.columnVehicle')}</TableHead>
                     <TableHead>{t('recurring.columnVehicle')}</TableHead>