Explorar o código

Remember the status tab on the work order, quote, inspection and billing lists (#557)

A tab clicked on a list is kept in a cookie per browser and opened again
when the list is reached without one in the URL, as the sort already is.
Links that name a tab still open it without replacing the choice (#554).
Bernt Christian Egeland hai 2 días
pai
achega
ef395f9dfc

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

@@ -41,6 +41,7 @@ import { cn } from '@/lib/utils'
 import { deliveryState } from '@/features/billing/Lib/deliveryState'
 import { deliveryState } from '@/features/billing/Lib/deliveryState'
 import { useFormatCurrency } from '@/components/currency-settings-context'
 import { useFormatCurrency } from '@/components/currency-settings-context'
 import { useRememberedSort } from '@/hooks/use-remembered-sort'
 import { useRememberedSort } from '@/hooks/use-remembered-sort'
+import { LIST_STATUS_TABS, type ListStatus, rememberListStatus } from '@/lib/list-status-preference'
 import { rememberCompletedOnly } from '@/lib/completed-only-preference'
 import { rememberCompletedOnly } from '@/lib/completed-only-preference'
 import { ListEmpty } from '@/components/list-empty'
 import { ListEmpty } from '@/components/list-empty'
 
 
@@ -98,12 +99,17 @@ interface BillingClientProps {
   sortOrder?: 'asc' | 'desc'
   sortOrder?: 'asc' | 'desc'
 }
 }
 
 
-const STATUS_TABS = [
-  { titleKey: 'history.statusAll', value: 'all' },
-  { titleKey: 'history.statusPaid', value: 'paid' },
-  { titleKey: 'history.statusPartial', value: 'partial' },
-  { titleKey: 'history.statusUnpaid', value: 'unpaid' },
-] as const
+const STATUS_TITLE_KEYS: Record<ListStatus<'billing'>, string> = {
+  all: 'history.statusAll',
+  paid: 'history.statusPaid',
+  partial: 'history.statusPartial',
+  unpaid: 'history.statusUnpaid',
+}
+
+const STATUS_TABS = LIST_STATUS_TABS.billing.map((value) => ({
+  value,
+  titleKey: STATUS_TITLE_KEYS[value],
+}))
 
 
 export default function BillingClient({
 export default function BillingClient({
   data,
   data,
@@ -162,14 +168,12 @@ export default function BillingClient({
     )
     )
   }
   }
 
 
-  const handleStatusChange = (status: string) => {
+  // Remembered as well as put in the URL, like the completed-only switch.
+  // "all" stays in the URL too: a bare path means the remembered tab.
+  const handleStatusChange = (status: ListStatus<'billing'>) => {
+    rememberListStatus('billing', status)
     startTransition(() => {
     startTransition(() => {
-      router.push(
-        `${pathname}?${createQueryString({
-          status: status === 'all' ? '' : status,
-          page: '1',
-        })}`
-      )
+      router.push(`${pathname}?${createQueryString({ status, page: '1' })}`)
     })
     })
   }
   }
 
 
@@ -384,6 +388,7 @@ export default function BillingClient({
               }
               }
               size="sm"
               size="sm"
               className="h-9 shrink-0 sm:h-8"
               className="h-9 shrink-0 sm:h-8"
+              aria-pressed={statusFilter === tab.value}
               onClick={() => handleStatusChange(tab.value)}
               onClick={() => handleStatusChange(tab.value)}
               disabled={isPending}
               disabled={isPending}
             >
             >

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

@@ -1,5 +1,6 @@
 import { resolveCurrencyCode } from '@/lib/currencies'
 import { resolveCurrencyCode } from '@/lib/currencies'
 import { resolveListSort } from '@/lib/list-sort-preference.server'
 import { resolveListSort } from '@/lib/list-sort-preference.server'
+import { resolveListStatus } from '@/lib/list-status-preference.server'
 import { getBillingHistory } from '@/features/billing/Actions/billingActions'
 import { getBillingHistory } from '@/features/billing/Actions/billingActions'
 import { getDisplaySettings } from '@/features/settings/Actions/settingsActions'
 import { getDisplaySettings } from '@/features/settings/Actions/settingsActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
@@ -27,7 +28,7 @@ export default async function BillingPage({
   const page = Number(params.page) || 1
   const page = Number(params.page) || 1
   const pageSize = Number(params.pageSize) || 20
   const pageSize = Number(params.pageSize) || 20
   const search = params.search || ''
   const search = params.search || ''
-  const statusFilter = params.status || 'all'
+  const statusFilter = await resolveListStatus('billing', params.status)
   const deliveryFilter = params.delivery === 'unviewed' ? 'unviewed' : ''
   const deliveryFilter = params.delivery === 'unviewed' ? 'unviewed' : ''
   // The URL wins, as with sorting; otherwise the switch as this browser left it.
   // The URL wins, as with sorting; otherwise the switch as this browser left it.
   const completedOnly =
   const completedOnly =

+ 17 - 6
src/app/(authenticated)/inspections/inspections-client.tsx

@@ -1,6 +1,7 @@
 'use client'
 'use client'
 
 
 import { useRememberedSort } from '@/hooks/use-remembered-sort'
 import { useRememberedSort } from '@/hooks/use-remembered-sort'
+import { LIST_STATUS_TABS, type ListStatus, rememberListStatus } from '@/lib/list-status-preference'
 import { useTableKeyboardNav } from '@/hooks/use-table-keyboard-nav'
 import { useTableKeyboardNav } from '@/hooks/use-table-keyboard-nav'
 import { interactiveRow } from '@/lib/interactive-row'
 import { interactiveRow } from '@/lib/interactive-row'
 import { useDebouncedSearch } from '@/hooks/use-debounced-search'
 import { useDebouncedSearch } from '@/hooks/use-debounced-search'
@@ -78,11 +79,16 @@ interface TemplateOption {
   isDefault: boolean
   isDefault: boolean
 }
 }
 
 
-const statusTabs = [
-  { key: 'all', labelKey: 'tabAll' },
-  { key: 'in_progress', labelKey: 'tabInProgress' },
-  { key: 'completed', labelKey: 'tabCompleted' },
-] as const
+const statusLabelKeys: Record<ListStatus<'inspections'>, string> = {
+  all: 'tabAll',
+  in_progress: 'tabInProgress',
+  completed: 'tabCompleted',
+}
+
+const statusTabs = LIST_STATUS_TABS.inspections.map((key) => ({
+  key,
+  labelKey: statusLabelKeys[key],
+}))
 
 
 const statusColors: Record<string, string> = {
 const statusColors: Record<string, string> = {
   in_progress: 'bg-blue-500/10 text-blue-500 border-blue-500/20',
   in_progress: 'bg-blue-500/10 text-blue-500 border-blue-500/20',
@@ -273,7 +279,12 @@ export function InspectionsClient({
               variant={isActive ? 'default' : 'outline'}
               variant={isActive ? 'default' : 'outline'}
               size="sm"
               size="sm"
               className="h-9 shrink-0 sm:h-8"
               className="h-9 shrink-0 sm:h-8"
-              onClick={() => navigate({ status: tab.key === 'all' ? undefined : tab.key })}
+              aria-pressed={isActive}
+              onClick={() => {
+                rememberListStatus('inspections', tab.key)
+                // "all" stays in the URL: a bare path means the remembered tab.
+                navigate({ status: tab.key })
+              }}
             >
             >
               {t(tab.labelKey)}
               {t(tab.labelKey)}
               {count !== undefined && (
               {count !== undefined && (

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

@@ -1,4 +1,5 @@
 import { resolveListSort } from '@/lib/list-sort-preference.server'
 import { resolveListSort } from '@/lib/list-sort-preference.server'
+import { resolveListStatus } from '@/lib/list-status-preference.server'
 import { getInspectionsPaginated } from '@/features/inspections/Actions/inspectionActions'
 import { getInspectionsPaginated } from '@/features/inspections/Actions/inspectionActions'
 import { getTemplates } from '@/features/inspections/Actions/templateActions'
 import { getTemplates } from '@/features/inspections/Actions/templateActions'
 import { InspectionsClient } from './inspections-client'
 import { InspectionsClient } from './inspections-client'
@@ -22,12 +23,13 @@ export default async function InspectionsPage({
     sortBy: undefined,
     sortBy: undefined,
     sortOrder: 'desc',
     sortOrder: 'desc',
   })
   })
+  const status = await resolveListStatus('inspections', params.status)
   const [result, templatesResult] = await Promise.all([
   const [result, templatesResult] = await Promise.all([
     getInspectionsPaginated({
     getInspectionsPaginated({
       page: params.page ? parseInt(params.page) : 1,
       page: params.page ? parseInt(params.page) : 1,
       pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
       pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
       search: params.search,
       search: params.search,
-      status: params.status || 'all',
+      status,
       sortBy: sort.sortBy,
       sortBy: sort.sortBy,
       sortOrder: sort.sortOrder,
       sortOrder: sort.sortOrder,
     }),
     }),
@@ -55,7 +57,7 @@ export default async function InspectionsPage({
           data={result.data}
           data={result.data}
           templates={templates}
           templates={templates}
           search={params.search || ''}
           search={params.search || ''}
-          statusFilter={params.status || 'all'}
+          statusFilter={status}
           sortBy={sort.sortBy || ''}
           sortBy={sort.sortBy || ''}
           sortOrder={sort.sortOrder}
           sortOrder={sort.sortOrder}
         />
         />

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

@@ -1,5 +1,6 @@
 import { resolveCurrencyCode } from '@/lib/currencies'
 import { resolveCurrencyCode } from '@/lib/currencies'
 import { resolveListSort } from '@/lib/list-sort-preference.server'
 import { resolveListSort } from '@/lib/list-sort-preference.server'
+import { resolveListStatus } from '@/lib/list-status-preference.server'
 import { getQuotesPaginated } from '@/features/quotes/Actions/quoteActions'
 import { getQuotesPaginated } from '@/features/quotes/Actions/quoteActions'
 import { getDisplaySettings } from '@/features/settings/Actions/settingsActions'
 import { getDisplaySettings } from '@/features/settings/Actions/settingsActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
@@ -24,12 +25,13 @@ export default async function QuotesPage({
     sortBy: undefined,
     sortBy: undefined,
     sortOrder: 'desc',
     sortOrder: 'desc',
   })
   })
+  const status = await resolveListStatus('quotes', params.status)
   const [result, settingsResult] = await Promise.all([
   const [result, settingsResult] = await Promise.all([
     getQuotesPaginated({
     getQuotesPaginated({
       page: params.page ? parseInt(params.page) : 1,
       page: params.page ? parseInt(params.page) : 1,
       pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
       pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
       search: params.search,
       search: params.search,
-      status: params.status || 'all',
+      status,
       sortBy: sort.sortBy,
       sortBy: sort.sortBy,
       sortOrder: sort.sortOrder,
       sortOrder: sort.sortOrder,
     }),
     }),
@@ -58,7 +60,7 @@ export default async function QuotesPage({
           data={result.data}
           data={result.data}
           currencyCode={currencyCode}
           currencyCode={currencyCode}
           search={params.search || ''}
           search={params.search || ''}
-          statusFilter={params.status || 'all'}
+          statusFilter={status}
           sortBy={sort.sortBy || ''}
           sortBy={sort.sortBy || ''}
           sortOrder={sort.sortOrder}
           sortOrder={sort.sortOrder}
         />
         />

+ 18 - 10
src/app/(authenticated)/quotes/quotes-client.tsx

@@ -2,6 +2,7 @@
 
 
 import { DEFAULT_CURRENCY_CODE } from '@/lib/currencies'
 import { DEFAULT_CURRENCY_CODE } from '@/lib/currencies'
 import { useRememberedSort } from '@/hooks/use-remembered-sort'
 import { useRememberedSort } from '@/hooks/use-remembered-sort'
+import { LIST_STATUS_TABS, type ListStatus, rememberListStatus } from '@/lib/list-status-preference'
 import { interactiveRow } from '@/lib/interactive-row'
 import { interactiveRow } from '@/lib/interactive-row'
 import { useTableKeyboardNav } from '@/hooks/use-table-keyboard-nav'
 import { useTableKeyboardNav } from '@/hooks/use-table-keyboard-nav'
 import { useDebouncedSearch } from '@/hooks/use-debounced-search'
 import { useDebouncedSearch } from '@/hooks/use-debounced-search'
@@ -80,15 +81,17 @@ interface PaginatedData {
   statusCounts: Record<string, number>
   statusCounts: Record<string, number>
 }
 }
 
 
-const statusTabs = [
-  { key: 'all', titleKey: 'list.statusAll' },
-  { key: 'attention', titleKey: 'list.statusAttention' },
-  { key: 'draft', titleKey: 'list.statusDraft' },
-  { key: 'sent', titleKey: 'list.statusSent' },
-  { key: 'accepted', titleKey: 'list.statusAccepted' },
-  { key: 'rejected', titleKey: 'list.statusRejected' },
-  { key: 'converted', titleKey: 'list.statusConverted' },
-] as const
+const statusTitleKeys: Record<ListStatus<'quotes'>, string> = {
+  all: 'list.statusAll',
+  attention: 'list.statusAttention',
+  draft: 'list.statusDraft',
+  sent: 'list.statusSent',
+  accepted: 'list.statusAccepted',
+  rejected: 'list.statusRejected',
+  converted: 'list.statusConverted',
+}
+
+const statusTabs = LIST_STATUS_TABS.quotes.map((key) => ({ key, titleKey: statusTitleKeys[key] }))
 
 
 const statusColors: Record<string, string> = {
 const statusColors: Record<string, string> = {
   draft: 'bg-gray-500/10 text-gray-500 border-gray-500/20',
   draft: 'bg-gray-500/10 text-gray-500 border-gray-500/20',
@@ -222,7 +225,12 @@ export function QuotesClient({
               variant={isActive ? 'default' : 'outline'}
               variant={isActive ? 'default' : 'outline'}
               size="sm"
               size="sm"
               className="h-9 shrink-0 sm:h-8"
               className="h-9 shrink-0 sm:h-8"
-              onClick={() => navigate({ status: tab.key === 'all' ? undefined : tab.key })}
+              aria-pressed={isActive}
+              onClick={() => {
+                rememberListStatus('quotes', tab.key)
+                // "all" stays in the URL: a bare path means the remembered tab.
+                navigate({ status: tab.key })
+              }}
             >
             >
               {t(tab.titleKey)}
               {t(tab.titleKey)}
               {count !== undefined && (
               {count !== undefined && (

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

@@ -1,5 +1,6 @@
 import { resolveCurrencyCode } from '@/lib/currencies'
 import { resolveCurrencyCode } from '@/lib/currencies'
 import { resolveListSort } from '@/lib/list-sort-preference.server'
 import { resolveListSort } from '@/lib/list-sort-preference.server'
+import { resolveListStatus } from '@/lib/list-status-preference.server'
 import { resolveListColumns } from '@/lib/list-columns.server'
 import { resolveListColumns } from '@/lib/list-columns.server'
 import { WORK_ORDER_COLUMNS } from '@/lib/list-columns'
 import { WORK_ORDER_COLUMNS } from '@/lib/list-columns'
 import { getTranslations } from 'next-intl/server'
 import { getTranslations } from 'next-intl/server'
@@ -33,12 +34,13 @@ export default async function WorkOrdersPage({
     sortOrder: 'desc',
     sortOrder: 'desc',
   })
   })
   const columns = await resolveListColumns('workOrders', WORK_ORDER_COLUMNS)
   const columns = await resolveListColumns('workOrders', WORK_ORDER_COLUMNS)
+  const status = await resolveListStatus('workOrders', params.status)
   const [result, settingsResult, authCtx] = await Promise.all([
   const [result, settingsResult, authCtx] = await Promise.all([
     getWorkOrders({
     getWorkOrders({
       page: params.page ? parseInt(params.page) : 1,
       page: params.page ? parseInt(params.page) : 1,
       pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
       pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
       search: params.search,
       search: params.search,
-      status: params.status,
+      status,
       sortBy: sort.sortBy,
       sortBy: sort.sortBy,
       sortOrder: sort.sortOrder,
       sortOrder: sort.sortOrder,
       due: params.due,
       due: params.due,
@@ -74,7 +76,7 @@ export default async function WorkOrdersPage({
           data={result.data}
           data={result.data}
           currencyCode={currencyCode}
           currencyCode={currencyCode}
           search={params.search || ''}
           search={params.search || ''}
-          statusFilter={params.status || 'all'}
+          statusFilter={status}
           dueFilter={params.due === 'today' ? 'today' : ''}
           dueFilter={params.due === 'today' ? 'today' : ''}
           columns={columns}
           columns={columns}
           sortBy={sort.sortBy || ''}
           sortBy={sort.sortBy || ''}

+ 8 - 10
src/app/(authenticated)/work-orders/work-orders-client.tsx

@@ -2,6 +2,7 @@
 
 
 import { DEFAULT_CURRENCY_CODE } from '@/lib/currencies'
 import { DEFAULT_CURRENCY_CODE } from '@/lib/currencies'
 import { useRememberedSort } from '@/hooks/use-remembered-sort'
 import { useRememberedSort } from '@/hooks/use-remembered-sort'
+import { LIST_STATUS_TABS, type ListStatus, rememberListStatus } from '@/lib/list-status-preference'
 import { interactiveRow } from '@/lib/interactive-row'
 import { interactiveRow } from '@/lib/interactive-row'
 import { useTableKeyboardNav } from '@/hooks/use-table-keyboard-nav'
 import { useTableKeyboardNav } from '@/hooks/use-table-keyboard-nav'
 import { useDebouncedSearch } from '@/hooks/use-debounced-search'
 import { useDebouncedSearch } from '@/hooks/use-debounced-search'
@@ -118,16 +119,9 @@ interface PaginatedData {
   statusCounts: Record<string, number>
   statusCounts: Record<string, number>
 }
 }
 
 
-const statusTabKeys = [
-  'all',
-  'active',
-  'pending',
-  'in-progress',
-  'waiting-parts',
-  'completed',
-] as const
+const statusTabKeys = LIST_STATUS_TABS.workOrders
 
 
-const statusTabI18nMap: Record<string, string> = {
+const statusTabI18nMap: Record<ListStatus<'workOrders'>, string> = {
   all: 'all',
   all: 'all',
   active: 'active',
   active: 'active',
   pending: 'pending',
   pending: 'pending',
@@ -338,7 +332,11 @@ export function WorkOrdersClient({
               variant={isActive ? 'default' : 'outline'}
               variant={isActive ? 'default' : 'outline'}
               size="sm"
               size="sm"
               className="h-9 shrink-0 sm:h-8"
               className="h-9 shrink-0 sm:h-8"
-              onClick={() => navigate({ status: key || undefined })}
+              aria-pressed={isActive}
+              onClick={() => {
+                rememberListStatus('workOrders', key)
+                navigate({ status: key })
+              }}
             >
             >
               {t(`statusTabs.${statusTabI18nMap[key]}`)}
               {t(`statusTabs.${statusTabI18nMap[key]}`)}
               {count !== undefined && (
               {count !== undefined && (

+ 25 - 0
src/lib/list-status-preference.server.ts

@@ -0,0 +1,25 @@
+import 'server-only'
+
+import { cookies } from 'next/headers'
+import {
+  LIST_STATUS_COOKIE,
+  parseListStatusCookie,
+  type StatusListKey,
+} from './list-status-preference'
+
+/**
+ * The status tab a list page should render with: a `?status=` in the URL
+ * always wins, then the tab this browser last switched the list to, then All.
+ *
+ * The URL winning matters: a dashboard card, a shared link and the back
+ * button all put the tab in the URL, and none of them should be overridden by
+ * what was picked last.
+ */
+export async function resolveListStatus(
+  list: StatusListKey,
+  status: string | undefined
+): Promise<string> {
+  if (status) return status
+  const store = await cookies()
+  return parseListStatusCookie(store.get(LIST_STATUS_COOKIE)?.value)[list] ?? 'all'
+}

+ 91 - 0
src/lib/list-status-preference.ts

@@ -0,0 +1,91 @@
+/**
+ * Remembered status tab for the list pages that have one.
+ *
+ * The tab lives in the URL (`?status=active`), so it survives a refresh but
+ * not a trip through the sidebar: the sidebar opens a bare path and the list
+ * fell back to All. This keeps the tab each list was last switched to, so a
+ * person who only works the active jobs sees those every time (issue #554).
+ *
+ * Only a click on the list's own tabs is remembered. A link from elsewhere,
+ * such as the dashboard's "Active jobs" card, opens the tab it names without
+ * replacing the choice. A cookie, like the remembered sort, so the page reads
+ * it on the server and the first paint is already filtered. It is kept per
+ * browser, as every viewing preference in the app is.
+ */
+
+/**
+ * The tabs each list shows, in order. The lists build their buttons from
+ * these, so a value can never be remembered that the list does not offer.
+ * Values are stored, so keep them stable.
+ */
+export const LIST_STATUS_TABS = {
+  workOrders: ['all', 'active', 'pending', 'in-progress', 'waiting-parts', 'completed'],
+  quotes: ['all', 'attention', 'draft', 'sent', 'accepted', 'rejected', 'converted'],
+  inspections: ['all', 'in_progress', 'completed'],
+  billing: ['all', 'paid', 'partial', 'unpaid'],
+} as const
+
+export type StatusListKey = keyof typeof LIST_STATUS_TABS
+export type ListStatus<L extends StatusListKey> = (typeof LIST_STATUS_TABS)[L][number]
+
+export const LIST_STATUS_COOKIE = 'listStatus'
+
+/** A year, the same as the remembered sort. */
+export const LIST_STATUS_COOKIE_MAX_AGE = 60 * 60 * 24 * 365
+
+function isStatusList(key: string): key is StatusListKey {
+  return Object.hasOwn(LIST_STATUS_TABS, key)
+}
+
+/** Whether `status` is one of the tabs `list` shows. */
+export function isListStatus<L extends StatusListKey>(
+  list: L,
+  status: string | undefined
+): status is ListStatus<L> {
+  return !!status && (LIST_STATUS_TABS[list] as readonly string[]).includes(status)
+}
+
+/**
+ * Parses the cookie: `{"workOrders":"active"}`. Unknown lists and tabs a list
+ * no longer has are dropped, and anything malformed is an empty map, since a
+ * hand-edited or truncated cookie must never break a page render or leave a
+ * list filtered by something it cannot show.
+ */
+export function parseListStatusCookie(
+  raw: string | undefined
+): Partial<Record<StatusListKey, string>> {
+  if (!raw) return {}
+  try {
+    const parsed = JSON.parse(raw)
+    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
+    const out: Partial<Record<StatusListKey, string>> = {}
+    for (const [key, value] of Object.entries(parsed)) {
+      if (isStatusList(key) && typeof value === 'string' && isListStatus(key, value)) {
+        out[key] = value
+      }
+    }
+    return out
+  } catch {
+    return {}
+  }
+}
+
+/** Remembers the tab a list was switched to, keeping what the other lists have. */
+export function rememberListStatus<L extends StatusListKey>(list: L, status: ListStatus<L>): void {
+  if (typeof document === 'undefined' || !isListStatus(list, status)) return
+  const match = document.cookie.split('; ').find((c) => c.startsWith(`${LIST_STATUS_COOKIE}=`))
+  let current: Partial<Record<StatusListKey, string>> = {}
+  if (match) {
+    try {
+      current = parseListStatusCookie(
+        decodeURIComponent(match.slice(LIST_STATUS_COOKIE.length + 1))
+      )
+    } catch {
+      current = {}
+    }
+  }
+  const next = { ...current, [list]: status }
+  document.cookie = `${LIST_STATUS_COOKIE}=${encodeURIComponent(
+    JSON.stringify(next)
+  )}; path=/; max-age=${LIST_STATUS_COOKIE_MAX_AGE}; SameSite=Lax`
+}