WFDASH-WEBAPP/src/lib/use-split-ratio.ts
Dave d9c60aff6d feat: add split views, stock picking slips, and invoice viewer
Comprehensive changes including:
- Refactor picking slip line handling for multiple document types across print endpoints
- Add new `/api/v1/stock-purchases/picking-slip` API for stock booking picking slips
- Implement reusable Splitter component and useSplitRatio hook for resizable panels
- Add floating/fullscreen invoice viewer to collection confirm page
- Add real-time stock booking update event for invoicing tab
- Add full test coverage for all new components and APIs
- Include temporary chat attachment images for testing purposes
2026-08-09 23:16:55 +02:00

103 lines
3.3 KiB
TypeScript

"use client";
import { useCallback, useState } from "react";
/**
* localStorage-backed persistence for a numeric ratio in [min, max].
*
* Used by the collection-confirm PO card to remember the operator's
* split-view ratio (items table ↔ invoice preview) across reloads.
* The ratio is a fraction of the parent container width — e.g. 0.42
* means "items table takes 42% of the card body width".
*
* SSR-safe: `localStorage` is gated on `typeof window`. The hook
* reads the stored value lazily on first render and ignores writes
* during SSR so server-rendered HTML matches the client's initial
* paint (avoids hydration warnings).
*
* Clamping is the hook's job — callers shouldn't have to defend
* against stale localStorage values that fell outside the new bounds
* (e.g. after a layout refactor narrowed the min).
*/
const isBrowser = typeof window !== "undefined";
function readStored(key: string): number | null {
if (!isBrowser) return null;
try {
const raw = window.localStorage.getItem(key);
if (raw === null) return null;
const parsed = Number.parseFloat(raw);
return Number.isFinite(parsed) ? parsed : null;
} catch {
// Safari private mode / quota errors / SecurityError — treat as no value.
return null;
}
}
function writeStored(key: string, value: number): void {
if (!isBrowser) return;
try {
window.localStorage.setItem(key, String(value));
} catch {
// Best-effort persistence. A failed write must not break the UI —
// the in-memory state is the source of truth for the current session.
}
}
export type UseSplitRatioOptions = {
/** localStorage key. Namespace it per-feature to avoid collisions. */
storageKey: string;
/** Initial ratio when nothing is stored (default 0.42 — items ≈ preview). */
initial: number;
/** Inclusive lower bound. The hook clamps writes + reads to this. */
min?: number;
/** Inclusive upper bound. The hook clamps writes + reads to this. */
max?: number;
};
export type UseSplitRatioResult = {
ratio: number;
setRatio: (next: number) => void;
};
/**
* Persistent split-ratio hook.
*
* Returns the current ratio and a setter. The setter clamps to the
* configured min/max before persisting, so the splitter can pass
* raw cursor positions without worrying about overflow.
*/
export function useSplitRatio({
storageKey,
initial,
min = 0.15,
max = 0.85,
}: UseSplitRatioOptions): UseSplitRatioResult {
const clamp = useCallback(
(value: number) => Math.min(Math.max(value, min), max),
[min, max],
);
// Lazy init from localStorage so the very first render uses the
// stored value when present (no flicker on a reload). We don't
// need a follow-up `useEffect` to re-read on mount — `useState`'s
// initialiser already runs once per mount with the current
// localStorage value, so a hot-reload or fresh page load lands on
// the right value on the very first render.
const [ratio, setRatioState] = useState<number>(() => {
const stored = readStored(storageKey);
if (stored === null) return clamp(initial);
return clamp(stored);
});
const setRatio = useCallback(
(next: number) => {
const clamped = clamp(next);
setRatioState(clamped);
writeStored(storageKey, clamped);
},
[clamp, storageKey],
);
return { ratio, setRatio };
}