mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge pull request #105 from Tria-plc/freight/feat/contract-signuture
freight/feat/contract signuture
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
/** Shared surfaces for booking list & detail — frosted glass, neutral accents. */
|
||||
|
||||
export const bookingGlass = {
|
||||
card:
|
||||
"border border-border/50 bg-card/75 shadow-sm backdrop-blur-md supports-[backdrop-filter]:bg-card/60",
|
||||
card: "border border-border/50 bg-card/75 shadow-sm backdrop-blur-md supports-[backdrop-filter]:bg-card/60",
|
||||
panel:
|
||||
"border border-border/50 bg-card/80 shadow-sm backdrop-blur-md supports-[backdrop-filter]:bg-card/65",
|
||||
rail:
|
||||
"border border-border/40 bg-muted/20 backdrop-blur-sm supports-[backdrop-filter]:bg-muted/15",
|
||||
rail: "border border-border/40 bg-muted/20 backdrop-blur-sm supports-[backdrop-filter]:bg-muted/15",
|
||||
iconWell:
|
||||
"border border-border/50 bg-background/70 text-foreground/75 shadow-sm backdrop-blur-sm supports-[backdrop-filter]:bg-background/50",
|
||||
iconWellHero:
|
||||
@@ -22,9 +20,8 @@ export const bookingGlass = {
|
||||
} as const;
|
||||
|
||||
export const bookingSurface = {
|
||||
page:
|
||||
"min-h-screen bg-gradient-to-b from-muted/30 via-background to-background",
|
||||
pageInner: "mx-auto max-w-[1600px] space-y-5 p-6 lg:p-8",
|
||||
page: "min-h-screen bg-background ",
|
||||
pageInner: "mx-auto max-w-[1600px] space-y-5 ",
|
||||
hero: `relative overflow-hidden rounded-2xl ${bookingGlass.card}`,
|
||||
heroGlow:
|
||||
"pointer-events-none absolute -right-24 -top-24 size-72 rounded-full bg-muted/40 blur-3xl",
|
||||
|
||||
@@ -56,7 +56,10 @@ const FreightDashboardHeader = ({
|
||||
if (!isUserMenuOpen) return;
|
||||
|
||||
const handlePointerDown = (event: MouseEvent) => {
|
||||
if (userMenuRef.current && !userMenuRef.current.contains(event.target as Node)) {
|
||||
if (
|
||||
userMenuRef.current &&
|
||||
!userMenuRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsUserMenuOpen(false);
|
||||
}
|
||||
};
|
||||
@@ -74,10 +77,14 @@ const FreightDashboardHeader = ({
|
||||
}, [isUserMenuOpen]);
|
||||
|
||||
return (
|
||||
<header className="flex h-[82px] shrink-0 items-center justify-between gap-4 px-6">
|
||||
<header className="flex h-20 shrink-0 items-center justify-between gap-4 px-6">
|
||||
<div className="min-w-0">
|
||||
<h1 className="truncate text-xl font-bold tracking-tight text-gray-900">{pageMeta.title}</h1>
|
||||
<p className="mt-0.5 truncate text-sm text-gray-500">{pageMeta.subtitle}</p>
|
||||
<h1 className="truncate text-xl font-bold tracking-tight text-foreground">
|
||||
{pageMeta.title}
|
||||
</h1>
|
||||
<p className="mt-0.5 truncate text-sm text-secondary-foreground">
|
||||
{pageMeta.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
@@ -85,14 +92,24 @@ const FreightDashboardHeader = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleTheme}
|
||||
aria-label={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|
||||
aria-label={
|
||||
theme === "dark" ? "Switch to light mode" : "Switch to dark mode"
|
||||
}
|
||||
className={iconButtonClass}
|
||||
>
|
||||
{theme === "dark" ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
|
||||
{theme === "dark" ? (
|
||||
<Sun className="h-5 w-5" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<button type="button" aria-label="Change language" className={iconButtonClass}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Change language"
|
||||
className={iconButtonClass}
|
||||
>
|
||||
<Languages className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
@@ -101,7 +118,11 @@ const FreightDashboardHeader = ({
|
||||
<span className="absolute right-2 top-2 h-2 w-2 rounded-full bg-red-500 ring-2 ring-white" />
|
||||
</button>
|
||||
|
||||
<button type="button" aria-label="Notifications" className={iconButtonClass}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Notifications"
|
||||
className={iconButtonClass}
|
||||
>
|
||||
<Bell className="h-5 w-5" />
|
||||
<span className="absolute right-2 top-2 h-2 w-2 rounded-full bg-red-500 ring-2 ring-white" />
|
||||
</button>
|
||||
@@ -136,8 +157,12 @@ const FreightDashboardHeader = ({
|
||||
className="absolute right-0 top-full z-50 mt-2 w-52 overflow-hidden rounded-xl border border-gray-200 bg-white py-1 shadow-lg"
|
||||
>
|
||||
<div className="border-b border-gray-100 px-4 py-3">
|
||||
<p className="text-sm font-semibold text-gray-900">{userName}</p>
|
||||
{userEmail ? <p className="text-xs text-gray-500">{userEmail}</p> : null}
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
{userName}
|
||||
</p>
|
||||
{userEmail ? (
|
||||
<p className="text-xs text-gray-500">{userEmail}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<a
|
||||
href="#profile"
|
||||
|
||||
@@ -12,7 +12,9 @@ function getInitialTheme(): Theme {
|
||||
if (typeof window === "undefined") return "light";
|
||||
const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (stored === "dark" || stored === "light") return stored;
|
||||
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
return window.matchMedia?.("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
export interface FreightDashboardLayoutProps {
|
||||
@@ -29,7 +31,7 @@ export interface FreightDashboardLayoutProps {
|
||||
}
|
||||
|
||||
const panelClass =
|
||||
"rounded-2xl border border-gray-200/80 bg-white shadow-[0_1px_3px_rgba(15,23,42,0.06)]";
|
||||
"rounded-lg border border-gray-200/80 bg-white shadow-[0_1px_3px_rgba(15,23,42,0.06)]";
|
||||
|
||||
const FreightDashboardLayout = ({
|
||||
sidebarSections,
|
||||
@@ -59,7 +61,8 @@ const FreightDashboardLayout = ({
|
||||
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||
}, [theme, enableThemeToggle]);
|
||||
|
||||
const toggleTheme = () => setTheme((current) => (current === "dark" ? "light" : "dark"));
|
||||
const toggleTheme = () =>
|
||||
setTheme((current) => (current === "dark" ? "light" : "dark"));
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -71,17 +74,17 @@ const FreightDashboardLayout = ({
|
||||
/>
|
||||
|
||||
<div
|
||||
className="flex h-[100dvh] overflow-hidden bg-[#eceef2] p-3 antialiased md:p-4"
|
||||
className="flex h-[100dvh] overflow-hidden bg-[#eceef2] p-2 antialiased"
|
||||
style={{ fontFamily: "'Outfit', var(--font-sans)" }}
|
||||
>
|
||||
<div className="flex h-full min-h-0 w-full gap-3 md:gap-3">
|
||||
<div className="flex h-full min-h-0 w-full gap-2">
|
||||
<FreightSidebar
|
||||
sections={sidebarSections}
|
||||
activeHref={activeHref}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
|
||||
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col gap-3 md:gap-3">
|
||||
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col gap-2">
|
||||
<div className={`shrink-0 ${panelClass}`}>
|
||||
<FreightDashboardHeader
|
||||
pageMeta={pageMeta}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { type MouseEvent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
type MouseEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -20,21 +26,28 @@ const collectSidebarHrefs = (items: SidebarItem[]): string[] =>
|
||||
items.flatMap((item) => {
|
||||
const hrefs: string[] = [];
|
||||
if (item.href) hrefs.push(item.href.toLowerCase());
|
||||
if (item.children?.length) hrefs.push(...collectSidebarHrefs(item.children));
|
||||
if (item.children?.length)
|
||||
hrefs.push(...collectSidebarHrefs(item.children));
|
||||
return hrefs;
|
||||
});
|
||||
|
||||
const flattenSectionItems = (sections: SidebarSection[]) =>
|
||||
sections.flatMap((section) => section.items);
|
||||
|
||||
const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProps) => {
|
||||
const FreightSidebar = ({
|
||||
sections,
|
||||
activeHref,
|
||||
onNavigate,
|
||||
}: FreightSidebarProps) => {
|
||||
const items = useMemo(() => flattenSectionItems(sections), [sections]);
|
||||
const activePath = activeHref?.toLowerCase() ?? "";
|
||||
|
||||
const isHrefActive = useCallback(
|
||||
(href: string) => {
|
||||
const normalized = href.toLowerCase();
|
||||
return activePath === normalized || activePath.startsWith(`${normalized}/`);
|
||||
return (
|
||||
activePath === normalized || activePath.startsWith(`${normalized}/`)
|
||||
);
|
||||
},
|
||||
[activePath],
|
||||
);
|
||||
@@ -72,7 +85,8 @@ const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProp
|
||||
return acc;
|
||||
}, [activePath, branchContainsActive, isHrefActive, items]);
|
||||
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>(defaultExpanded);
|
||||
const [expanded, setExpanded] =
|
||||
useState<Record<string, boolean>>(defaultExpanded);
|
||||
|
||||
useEffect(() => {
|
||||
setExpanded((current) => ({ ...defaultExpanded, ...current }));
|
||||
@@ -108,7 +122,11 @@ const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProp
|
||||
: "text-gray-900",
|
||||
);
|
||||
|
||||
const renderNavBranch = (children: SidebarItem[], depth: number, parentKey: string) =>
|
||||
const renderNavBranch = (
|
||||
children: SidebarItem[],
|
||||
depth: number,
|
||||
parentKey: string,
|
||||
) =>
|
||||
children.map((child) => {
|
||||
const key = sidebarItemKey(child, parentKey);
|
||||
const isGroup = Boolean(child.children?.length) && !child.href;
|
||||
@@ -181,7 +199,9 @@ const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProp
|
||||
|
||||
const hasChildren = Boolean(item.children?.length);
|
||||
const itemHref = item.href.toLowerCase();
|
||||
const childActive = hasChildren ? branchContainsActive(item.children!) : false;
|
||||
const childActive = hasChildren
|
||||
? branchContainsActive(item.children!)
|
||||
: false;
|
||||
const isCurrentItem = hasChildren
|
||||
? activePath === itemHref
|
||||
: isHrefActive(itemHref);
|
||||
@@ -206,10 +226,12 @@ const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProp
|
||||
href={item.href}
|
||||
onClick={(event) => navigateTo(event, item.href!)}
|
||||
aria-current={isCurrentItem ? "page" : undefined}
|
||||
className="flex min-w-0 flex-1 items-center gap-3 px-3 py-2.5 text-base font-medium leading-snug"
|
||||
className="flex min-w-0 flex-1 items-center gap-3 px-3 py-2.5 text-sm leading-snug"
|
||||
>
|
||||
{item.icon ? (
|
||||
<span className={iconClass(leafActive, isActive)}>{item.icon}</span>
|
||||
<span className={iconClass(leafActive, isActive)}>
|
||||
{item.icon}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="truncate">{item.label}</span>
|
||||
</a>
|
||||
@@ -257,10 +279,12 @@ const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProp
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="flex h-full max-h-full w-[340px] shrink-0 flex-col overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
|
||||
<aside className="flex h-full max-h-full w-[280px] shrink-0 flex-col overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
|
||||
<div className="flex shrink-0 items-center gap-2.5 border-b border-gray-100 px-5 py-5">
|
||||
<img src={EDR_LOGO} alt="EDR Freight" className="h-9 w-auto" />
|
||||
<span className="text-lg font-semibold tracking-tight text-gray-900">EDR Freight</span>
|
||||
<span className="text-lg font-semibold tracking-tight text-gray-900">
|
||||
EDR Freight
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<nav className="flex min-h-0 flex-1 flex-col gap-5 overflow-y-auto overscroll-contain px-3 py-4">
|
||||
@@ -270,7 +294,7 @@ const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProp
|
||||
className={cn(
|
||||
"px-3 pb-1 text-xs font-semibold uppercase tracking-wide",
|
||||
// Use a very light gray for ALL section titles, not just when mutedTitle is specified
|
||||
"text-gray-400"
|
||||
"text-sidebar-secondary-foreground",
|
||||
)}
|
||||
>
|
||||
{section.title}
|
||||
|
||||
@@ -88,12 +88,12 @@ export const URL_CONSTANTS = {
|
||||
|
||||
BOOKINGS: {
|
||||
BASE: "/bookings",
|
||||
BY_ID: (id: string | number) => `/bookings/${id}`,
|
||||
CONTRACT_VIEW: (id: string) => `/bookings/${id}/contract/view`,
|
||||
CONTRACT_DOCUMENT: (id: string) => `/bookings/${id}/contract/document`,
|
||||
CONTRACT_SIGN: (id: string) => `/bookings/${id}/contract/sign`,
|
||||
CONTRACT_DOWNLOAD: (id: string) => `/bookings/${id}/contract`,
|
||||
CANCEL: (id: string | number) => `/bookings/${id}/cancel`,
|
||||
CONFIRM: (id: string | number) => `/bookings/${id}/confirm`,
|
||||
BY_ID: (id: string | number) => `/api/bookings/${id}`,
|
||||
CONTRACT_VIEW: (id: string) => `/api/bookings/${id}/contract/view`,
|
||||
CONTRACT_DOCUMENT: (id: string) => `/api/bookings/${id}/contract/document`,
|
||||
CONTRACT_SIGN: (id: string) => `/api/bookings/${id}/contract/sign`,
|
||||
CONTRACT_DOWNLOAD: (id: string) => `/api/bookings/${id}/contract`,
|
||||
CANCEL: (id: string | number) => `/api/bookings/${id}/cancel`,
|
||||
CONFIRM: (id: string | number) => `/api/bookings/${id}/confirm`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -172,7 +172,6 @@ const useAuth = () => {
|
||||
});
|
||||
localStorage.clear();
|
||||
queryClient.clear();
|
||||
window.location.href = "/login";
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -21,6 +21,7 @@ export default function BookingContractPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const qc = useQueryClient();
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const [signOpen, setSignOpen] = useState(false);
|
||||
const [signerName, setSignerName] = useState("");
|
||||
const [signatureData, setSignatureData] = useState<string | null>(null);
|
||||
@@ -58,6 +59,12 @@ export default function BookingContractPage() {
|
||||
}
|
||||
}, [id, data?.reference]);
|
||||
|
||||
const handlePrint = useCallback(() => {
|
||||
if (iframeRef.current?.contentWindow) {
|
||||
iframeRef.current.contentWindow.print();
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex min-h-[40vh] items-center justify-center">
|
||||
@@ -77,8 +84,6 @@ export default function BookingContractPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const bodyHtml = extractBodyHtml(data.html);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-muted/30 p-4 md:p-8">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
@@ -88,7 +93,7 @@ export default function BookingContractPage() {
|
||||
Back to booking
|
||||
</Button>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => window.print()}>
|
||||
<Button variant="outline" size="sm" onClick={handlePrint}>
|
||||
<Printer className="mr-2 size-4" />
|
||||
Print
|
||||
</Button>
|
||||
@@ -105,9 +110,12 @@ export default function BookingContractPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article
|
||||
className="contract-document rounded-lg border bg-white p-6 shadow-sm print:shadow-none md:p-10"
|
||||
dangerouslySetInnerHTML={{ __html: bodyHtml }}
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
srcDoc={data.html}
|
||||
className="w-full rounded-lg border bg-white shadow-sm"
|
||||
style={{ minHeight: "80vh" }}
|
||||
title="Contract document"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -158,8 +166,3 @@ export default function BookingContractPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function extractBodyHtml(fullHtml: string): string {
|
||||
const match = fullHtml.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||||
return match ? match[1] : fullHtml;
|
||||
}
|
||||
|
||||
@@ -844,28 +844,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{(booking.status === "CONTRACT_READY" ||
|
||||
booking.status === "SIGNED_CUSTOMER" ||
|
||||
booking.status === "FULLY_EXECUTED") && (
|
||||
<Card className="border-primary/30 bg-primary/5">
|
||||
<CardContent className="flex flex-col gap-4 p-6 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="font-semibold text-foreground">Contract ready</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Review the agreement and apply your digital signature.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
onClick={() => navigate(`/bookings/${booking.id}/contract`)}
|
||||
>
|
||||
<FileSignature className="size-4" />
|
||||
View & sign contract
|
||||
</button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{renderContractCard(booking, navigate)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -1218,6 +1197,78 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderContractCard(
|
||||
booking: Freight.IBooking,
|
||||
navigate: ReturnType<typeof useNavigate>,
|
||||
) {
|
||||
const s = booking.status;
|
||||
if (
|
||||
s !== "APPROVED_PENDING_SIGNATURE" &&
|
||||
s !== "CONTRACT_READY" &&
|
||||
s !== "SIGNED_CUSTOMER" &&
|
||||
s !== "FULLY_EXECUTED"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const config: Record<
|
||||
string,
|
||||
{ title: string; description: string; buttonLabel?: string }
|
||||
> = {
|
||||
APPROVED_PENDING_SIGNATURE: {
|
||||
title: "Contract being prepared",
|
||||
description:
|
||||
"Your booking has been approved. The contract is being generated and will be available shortly.",
|
||||
},
|
||||
CONTRACT_READY: {
|
||||
title: "Contract ready for signature",
|
||||
description:
|
||||
"Review the agreement and apply your digital signature.",
|
||||
buttonLabel: "View & sign contract",
|
||||
},
|
||||
SIGNED_CUSTOMER: {
|
||||
title: "You have signed the contract",
|
||||
description:
|
||||
"Your signature has been submitted. Awaiting staff signature to finalize.",
|
||||
buttonLabel: "View contract",
|
||||
},
|
||||
FULLY_EXECUTED: {
|
||||
title: "Contract fully executed",
|
||||
description:
|
||||
"The contract has been fully signed and executed by all parties.",
|
||||
buttonLabel: "View contract",
|
||||
},
|
||||
};
|
||||
|
||||
const c = config[s];
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
"border-primary/30 bg-primary/5",
|
||||
s === "FULLY_EXECUTED" && "border-emerald-300 bg-emerald-50",
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex flex-col gap-4 p-6 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="font-semibold text-foreground">{c.title}</p>
|
||||
<p className="text-sm text-muted-foreground">{c.description}</p>
|
||||
</div>
|
||||
{c.buttonLabel && (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
onClick={() => navigate(`/bookings/${booking.id}/contract`)}
|
||||
>
|
||||
<FileSignature className="size-4" />
|
||||
{c.buttonLabel}
|
||||
</button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function RouteEndpoint({
|
||||
label,
|
||||
station,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
UseQueryOptions,
|
||||
QueryObserverOptions,
|
||||
} from "@tanstack/react-query";
|
||||
import { UseQueryOptions, QueryObserverOptions } from "@tanstack/react-query";
|
||||
import axios, { AxiosError, InternalAxiosRequestConfig } from "axios";
|
||||
import { URL_CONSTANTS } from "@/constants/URLS";
|
||||
|
||||
@@ -23,11 +20,15 @@ function setCookie(name: string, value: string, days: number) {
|
||||
}
|
||||
|
||||
function clearAuthCookies() {
|
||||
["auth-token", "refresh-token", "auth-user", "current-position-id", "selected-position-id"].forEach(
|
||||
(name) => {
|
||||
document.cookie = `${name}=; Max-Age=0; path=/`;
|
||||
},
|
||||
);
|
||||
[
|
||||
"auth-token",
|
||||
"refresh-token",
|
||||
"auth-user",
|
||||
"current-position-id",
|
||||
"selected-position-id",
|
||||
].forEach((name) => {
|
||||
document.cookie = `${name}=; Max-Age=0; path=/`;
|
||||
});
|
||||
}
|
||||
|
||||
// Attach auth token to every request
|
||||
@@ -96,17 +97,13 @@ client.interceptors.response.use(
|
||||
if (!refreshToken) {
|
||||
isRefreshing = false;
|
||||
clearAuthCookies();
|
||||
if (window.location.pathname !== "/login") {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await client.post<{ data: { token: string; refreshToken: string } }>(
|
||||
URL_CONSTANTS.AUTH.REFRESH_TOKEN,
|
||||
{ refreshToken },
|
||||
);
|
||||
const { data } = await client.post<{
|
||||
data: { token: string; refreshToken: string };
|
||||
}>(URL_CONSTANTS.AUTH.REFRESH_TOKEN, { refreshToken });
|
||||
const { token, refreshToken: newRefreshToken } = data.data;
|
||||
setCookie("auth-token", token, 7);
|
||||
setCookie("refresh-token", newRefreshToken, 7);
|
||||
|
||||
Reference in New Issue
Block a user