Toast refactor, hts tweaks
This commit is contained in:
Generated
-35
@@ -31,7 +31,6 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
"@radix-ui/react-tabs": "^1.1.2",
|
"@radix-ui/react-tabs": "^1.1.2",
|
||||||
"@radix-ui/react-toast": "^1.2.6",
|
|
||||||
"@radix-ui/react-toggle": "^1.1.10",
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.1.6",
|
"@radix-ui/react-tooltip": "^1.1.6",
|
||||||
@@ -2434,40 +2433,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-toast": {
|
|
||||||
"version": "1.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
|
|
||||||
"integrity": "sha512-gN4dpuIVKEgpLn1z5FhzT9mYRUitbfZq9XqN/7kkBMUgFTzTG8x/KszWJugJXHcwxckY8xcKDZPz7kG3o6DsUA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@radix-ui/primitive": "1.1.1",
|
|
||||||
"@radix-ui/react-collection": "1.1.2",
|
|
||||||
"@radix-ui/react-compose-refs": "1.1.1",
|
|
||||||
"@radix-ui/react-context": "1.1.1",
|
|
||||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
|
||||||
"@radix-ui/react-portal": "1.1.4",
|
|
||||||
"@radix-ui/react-presence": "1.1.2",
|
|
||||||
"@radix-ui/react-primitive": "2.0.2",
|
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
|
||||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
|
||||||
"@radix-ui/react-visually-hidden": "1.1.2"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"@types/react-dom": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-toggle": {
|
"node_modules/@radix-ui/react-toggle": {
|
||||||
"version": "1.1.10",
|
"version": "1.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
"@radix-ui/react-tabs": "^1.1.2",
|
"@radix-ui/react-tabs": "^1.1.2",
|
||||||
"@radix-ui/react-toast": "^1.2.6",
|
|
||||||
"@radix-ui/react-toggle": "^1.1.10",
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.1.6",
|
"@radix-ui/react-tooltip": "^1.1.6",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Lock, Delete } from "lucide-react";
|
import { Lock, Delete } from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const MAX_ATTEMPTS = 3;
|
const MAX_ATTEMPTS = 3;
|
||||||
const LOCKOUT_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
|
const LOCKOUT_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
|
||||||
@@ -27,8 +27,6 @@ const PinProtection = ({ onSuccess }) => {
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer;
|
let timer;
|
||||||
if (lockoutTime > 0) {
|
if (lockoutTime > 0) {
|
||||||
@@ -59,34 +57,25 @@ const PinProtection = ({ onSuccess }) => {
|
|||||||
|
|
||||||
if (newAttempts >= MAX_ATTEMPTS) {
|
if (newAttempts >= MAX_ATTEMPTS) {
|
||||||
setLockoutTime(LOCKOUT_DURATION);
|
setLockoutTime(LOCKOUT_DURATION);
|
||||||
toast({
|
toast.error("Too many attempts", {
|
||||||
title: "Too many attempts",
|
|
||||||
description: `Please try again in ${Math.ceil(LOCKOUT_DURATION / 60000)} minutes`,
|
description: `Please try again in ${Math.ceil(LOCKOUT_DURATION / 60000)} minutes`,
|
||||||
variant: "destructive",
|
|
||||||
});
|
});
|
||||||
setPin("");
|
setPin("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === "892312") {
|
if (value === "892312") {
|
||||||
toast({
|
toast.success("PIN accepted");
|
||||||
title: "Success",
|
|
||||||
description: "PIN accepted",
|
|
||||||
});
|
|
||||||
// Reset attempts on success
|
// Reset attempts on success
|
||||||
setAttempts(0);
|
setAttempts(0);
|
||||||
localStorage.removeItem('pinAttempts');
|
localStorage.removeItem('pinAttempts');
|
||||||
localStorage.removeItem('lastAttemptTime');
|
localStorage.removeItem('lastAttemptTime');
|
||||||
onSuccess();
|
onSuccess();
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast.error(`Incorrect PIN. ${MAX_ATTEMPTS - newAttempts} attempts remaining`);
|
||||||
title: "Error",
|
|
||||||
description: `Incorrect PIN. ${MAX_ATTEMPTS - newAttempts} attempts remaining`,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
setPin("");
|
setPin("");
|
||||||
}
|
}
|
||||||
}, [attempts, lockoutTime, onSuccess, toast]);
|
}, [attempts, lockoutTime, onSuccess]);
|
||||||
|
|
||||||
const handleKeyPress = (value) => {
|
const handleKeyPress = (value) => {
|
||||||
if (pin.length < 6) {
|
if (pin.length < 6) {
|
||||||
|
|||||||
@@ -229,8 +229,8 @@ export const BASE_IMPORT_FIELDS = [
|
|||||||
{
|
{
|
||||||
label: "Weight",
|
label: "Weight",
|
||||||
key: "weight",
|
key: "weight",
|
||||||
description: "Product weight (in lbs)",
|
description: "Product weight (in oz — convert lbs ×16, kg ×35.274)",
|
||||||
alternateMatches: ["weight (lbs.)"],
|
alternateMatches: ["weight (lbs.)", "weight (oz)", "weight (oz.)"],
|
||||||
fieldType: { type: "input" },
|
fieldType: { type: "input" },
|
||||||
width: 100,
|
width: 100,
|
||||||
validations: [
|
validations: [
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { SelectHeaderTable } from "./components/SelectHeaderTable"
|
|||||||
import { useRsi } from "../../hooks/useRsi"
|
import { useRsi } from "../../hooks/useRsi"
|
||||||
import type { RawData } from "../../types"
|
import type { RawData } from "../../types"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { useToast } from "@/hooks/use-toast"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
type SelectHeaderProps = {
|
type SelectHeaderProps = {
|
||||||
data: RawData[]
|
data: RawData[]
|
||||||
@@ -24,7 +24,6 @@ const isRowCompletelyEmpty = (row: RawData): boolean => {
|
|||||||
|
|
||||||
export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps) => {
|
export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps) => {
|
||||||
const { translations } = useRsi()
|
const { translations } = useRsi()
|
||||||
const { toast } = useToast()
|
|
||||||
const [selectedRows, setSelectedRows] = useState<ReadonlySet<number>>(new Set([0]))
|
const [selectedRows, setSelectedRows] = useState<ReadonlySet<number>>(new Set([0]))
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
@@ -133,19 +132,15 @@ export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps
|
|||||||
setLocalData(filteredRows);
|
setLocalData(filteredRows);
|
||||||
setSelectedRows(new Set([newSelectedIndex]));
|
setSelectedRows(new Set([newSelectedIndex]));
|
||||||
|
|
||||||
toast({
|
toast.success("Rows removed", {
|
||||||
title: "Rows removed",
|
|
||||||
description: `Removed ${localData.length - filteredRows.length} empty, single-value, or duplicate rows`,
|
description: `Removed ${localData.length - filteredRows.length} empty, single-value, or duplicate rows`,
|
||||||
variant: "default"
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast.info("No rows removed", {
|
||||||
title: "No rows removed",
|
|
||||||
description: "No empty, single-value, or duplicate rows were found",
|
description: "No empty, single-value, or duplicate rows were found",
|
||||||
variant: "default"
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [localData, selectedRows, toast]);
|
}, [localData, selectedRows]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-[calc(100vh-9.5rem)]">
|
<div className="flex flex-col h-[calc(100vh-9.5rem)]">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { exceedsMaxRecords } from "../utils/exceedsMaxRecords"
|
|||||||
import { useRsi } from "../hooks/useRsi"
|
import { useRsi } from "../hooks/useRsi"
|
||||||
import type { RawData, Data } from "../types"
|
import type { RawData, Data } from "../types"
|
||||||
import { Progress } from "@/components/ui/progress"
|
import { Progress } from "@/components/ui/progress"
|
||||||
import { useToast } from "@/hooks/use-toast"
|
import { toast } from "sonner"
|
||||||
import { addErrorsAndRunHooks } from "./ValidationStep/utils/dataMutations"
|
import { addErrorsAndRunHooks } from "./ValidationStep/utils/dataMutations"
|
||||||
import { computeMappingSignature, type MappingSignature } from "./ValidationStep/utils/mappingSignature"
|
import { computeMappingSignature, type MappingSignature } from "./ValidationStep/utils/mappingSignature"
|
||||||
import { useValidationStore } from "./ValidationStep/store/validationStore"
|
import { useValidationStore } from "./ValidationStep/store/validationStore"
|
||||||
@@ -88,7 +88,6 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
|
|||||||
tableHook,
|
tableHook,
|
||||||
onSubmit } = useRsi()
|
onSubmit } = useRsi()
|
||||||
const [uploadedFile, setUploadedFile] = useState<File | null>(null)
|
const [uploadedFile, setUploadedFile] = useState<File | null>(null)
|
||||||
const { toast } = useToast()
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const resetValidationStore = useValidationStore((state) => state.reset)
|
const resetValidationStore = useValidationStore((state) => state.reset)
|
||||||
|
|
||||||
@@ -116,13 +115,9 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
|
|||||||
}, [queryClient, resetValidationStore]);
|
}, [queryClient, resetValidationStore]);
|
||||||
const errorToast = useCallback(
|
const errorToast = useCallback(
|
||||||
(description: string) => {
|
(description: string) => {
|
||||||
toast({
|
toast.error(translations.alerts.toast.error, { description })
|
||||||
variant: "destructive",
|
|
||||||
title: translations.alerts.toast.error,
|
|
||||||
description,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
[toast, translations],
|
[translations],
|
||||||
)
|
)
|
||||||
|
|
||||||
// Keep track of global selections across steps
|
// Keep track of global selections across steps
|
||||||
|
|||||||
@@ -6,14 +6,6 @@ import { StepType } from "../UploadFlow"
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { AuthContext } from "@/contexts/AuthContext"
|
import { AuthContext } from "@/contexts/AuthContext"
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog"
|
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Bug } from "lucide-react"
|
import { Bug } from "lucide-react"
|
||||||
@@ -27,16 +19,22 @@ type UploadProps = {
|
|||||||
onRestoreSession?: (session: ImportSession) => void
|
onRestoreSession?: (session: ImportSession) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OrSeparator = () => (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Separator className="w-24" />
|
||||||
|
<span className="px-3 text-muted-foreground text-sm font-medium">OR</span>
|
||||||
|
<Separator className="w-24" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
export const UploadStep = ({ onContinue, setInitialState, onRestoreSession }: UploadProps) => {
|
export const UploadStep = ({ onContinue, setInitialState, onRestoreSession }: UploadProps) => {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const { translations } = useRsi()
|
const { translations } = useRsi()
|
||||||
const { user } = useContext(AuthContext)
|
const { user } = useContext(AuthContext)
|
||||||
const hasDebugPermission = Boolean(user?.is_admin || user?.permissions?.includes("admin:debug"))
|
const hasDebugPermission = Boolean(user?.is_admin || user?.permissions?.includes("admin:debug"))
|
||||||
|
|
||||||
// Debug import state
|
const [jsonInput, setJsonInput] = useState("")
|
||||||
const [debugDialogOpen, setDebugDialogOpen] = useState(false)
|
const [jsonError, setJsonError] = useState<string | null>(null)
|
||||||
const [debugJsonInput, setDebugJsonInput] = useState("")
|
|
||||||
const [debugError, setDebugError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const handleOnContinue = useCallback(
|
const handleOnContinue = useCallback(
|
||||||
async (data: XLSX.WorkBook, file: File) => {
|
async (data: XLSX.WorkBook, file: File) => {
|
||||||
@@ -53,16 +51,16 @@ export const UploadStep = ({ onContinue, setInitialState, onRestoreSession }: Up
|
|||||||
}
|
}
|
||||||
}, [setInitialState])
|
}, [setInitialState])
|
||||||
|
|
||||||
const handleDebugImport = useCallback(() => {
|
const handleJsonImport = useCallback(() => {
|
||||||
setDebugError(null)
|
setJsonError(null)
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(debugJsonInput)
|
const parsed = JSON.parse(jsonInput)
|
||||||
|
|
||||||
// Handle both array and object with products property
|
// Handle both array and object with products property
|
||||||
let products: any[] = Array.isArray(parsed) ? parsed : parsed.products
|
const products: any[] = Array.isArray(parsed) ? parsed : parsed.products
|
||||||
|
|
||||||
if (!Array.isArray(products) || products.length === 0) {
|
if (!Array.isArray(products) || products.length === 0) {
|
||||||
setDebugError("JSON must be an array of products or an object with a 'products' array")
|
setJsonError("JSON must be an array of products or an object with a 'products' array")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,46 +77,58 @@ export const UploadStep = ({ onContinue, setInitialState, onRestoreSession }: Up
|
|||||||
isFromScratch: true
|
isFromScratch: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setDebugDialogOpen(false)
|
|
||||||
setDebugJsonInput("")
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setDebugError(`Invalid JSON: ${e instanceof Error ? e.message : "Parse error"}`)
|
setJsonError(`Invalid JSON: ${e instanceof Error ? e.message : "Parse error"}`)
|
||||||
}
|
}
|
||||||
}, [debugJsonInput, setInitialState])
|
}, [jsonInput, setInitialState])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<div className="flex items-baseline justify-between">
|
|
||||||
<h2 className="text-3xl font-semibold mb-8 text-left">{translations.uploadStep.title}</h2>
|
<h2 className="text-3xl font-semibold mb-8 text-left">{translations.uploadStep.title}</h2>
|
||||||
|
<div className="max-w-xl mx-auto w-full space-y-8">
|
||||||
{hasDebugPermission && (
|
{hasDebugPermission && (
|
||||||
<>
|
<>
|
||||||
|
<div className="rounded-lg border border-amber-600/40 p-6 space-y-3">
|
||||||
|
<Label htmlFor="import-json" className="flex items-center gap-2 text-amber-600 font-semibold">
|
||||||
<div className="flex justify-center">
|
<Bug className="h-4 w-4" />
|
||||||
<Button
|
|
||||||
onClick={() => setDebugDialogOpen(true)}
|
|
||||||
variant="outline"
|
|
||||||
className="min-w-[200px] text-amber-600 border-amber-600 hover:bg-amber-50"
|
|
||||||
disabled={!setInitialState}
|
|
||||||
>
|
|
||||||
<Bug className="mr-2 h-4 w-4" />
|
|
||||||
Import JSON
|
Import JSON
|
||||||
|
</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Paste product data in the same JSON format as the API submission. The data will be loaded into the validation step.
|
||||||
|
</p>
|
||||||
|
<Textarea
|
||||||
|
id="import-json"
|
||||||
|
placeholder='[{"supplier": "...", "company": "...", "name": "...", "product_images": "url1,url2", ...}]'
|
||||||
|
value={jsonInput}
|
||||||
|
onChange={(e) => {
|
||||||
|
setJsonInput(e.target.value)
|
||||||
|
setJsonError(null)
|
||||||
|
}}
|
||||||
|
className="min-h-[160px] font-mono text-sm"
|
||||||
|
/>
|
||||||
|
{jsonError && (
|
||||||
|
<p className="text-sm text-destructive">{jsonError}</p>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={handleJsonImport}
|
||||||
|
disabled={!setInitialState || !jsonInput.trim()}
|
||||||
|
className="bg-amber-600 hover:bg-amber-700"
|
||||||
|
>
|
||||||
|
Import & Go to Validation
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OrSeparator />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
<div className="max-w-xl mx-auto w-full space-y-8">
|
|
||||||
<div className="rounded-lg p-6 flex flex-col items-center">
|
<div className="rounded-lg p-6 flex flex-col items-center">
|
||||||
<DropZone onContinue={handleOnContinue} isLoading={isLoading} />
|
<DropZone onContinue={handleOnContinue} isLoading={isLoading} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center">
|
<OrSeparator />
|
||||||
<Separator className="w-24" />
|
|
||||||
<span className="px-3 text-muted-foreground text-sm font-medium">OR</span>
|
|
||||||
<Separator className="w-24" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-center pb-8">
|
<div className="flex justify-center pb-8">
|
||||||
<Button
|
<Button
|
||||||
@@ -136,50 +146,6 @@ export const UploadStep = ({ onContinue, setInitialState, onRestoreSession }: Up
|
|||||||
<SavedSessionsList onRestore={onRestoreSession} />
|
<SavedSessionsList onRestore={onRestoreSession} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={debugDialogOpen} onOpenChange={setDebugDialogOpen}>
|
|
||||||
<DialogContent className="max-w-2xl max-h-[80vh]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="flex items-center gap-2 text-amber-600">
|
|
||||||
<Bug className="h-5 w-5" />
|
|
||||||
Debug: Import JSON Data
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Paste product data in the same JSON format as the API submission. The data will be loaded into the validation step.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="debug-json">Product JSON</Label>
|
|
||||||
<Textarea
|
|
||||||
id="debug-json"
|
|
||||||
placeholder='[{"supplier": "...", "company": "...", "name": "...", "product_images": "url1,url2", ...}]'
|
|
||||||
value={debugJsonInput}
|
|
||||||
onChange={(e) => {
|
|
||||||
setDebugJsonInput(e.target.value)
|
|
||||||
setDebugError(null)
|
|
||||||
}}
|
|
||||||
className="min-h-[300px] font-mono text-sm"
|
|
||||||
/>
|
|
||||||
{debugError && (
|
|
||||||
<p className="text-sm text-destructive">{debugError}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setDebugDialogOpen(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleDebugImport}
|
|
||||||
disabled={!debugJsonInput.trim()}
|
|
||||||
className="bg-amber-600 hover:bg-amber-700"
|
|
||||||
>
|
|
||||||
Import & Go to Validation
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from "react"
|
|||||||
import { useRsi } from "../../../hooks/useRsi"
|
import { useRsi } from "../../../hooks/useRsi"
|
||||||
import { readFileAsync } from "../utils/readFilesAsync"
|
import { readFileAsync } from "../utils/readFilesAsync"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { useToast } from "@/hooks/use-toast"
|
import { toast } from "sonner"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
type DropZoneProps = {
|
type DropZoneProps = {
|
||||||
@@ -14,7 +14,6 @@ type DropZoneProps = {
|
|||||||
|
|
||||||
export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
||||||
const { translations, maxFileSize, dateFormat, parseRaw } = useRsi()
|
const { translations, maxFileSize, dateFormat, parseRaw } = useRsi()
|
||||||
const { toast } = useToast()
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
||||||
noClick: true,
|
noClick: true,
|
||||||
@@ -29,9 +28,7 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
|||||||
onDropRejected: (fileRejections) => {
|
onDropRejected: (fileRejections) => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
fileRejections.forEach((fileRejection) => {
|
fileRejections.forEach((fileRejection) => {
|
||||||
toast({
|
toast.error(`${fileRejection.file.name} ${translations.uploadStep.dropzone.errorToastDescription}`, {
|
||||||
variant: "destructive",
|
|
||||||
title: `${fileRejection.file.name} ${translations.uploadStep.dropzone.errorToastDescription}`,
|
|
||||||
description: fileRejection.errors[0].message,
|
description: fileRejection.errors[0].message,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Username</FormLabel>
|
<FormLabel>Username</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} autoComplete="off" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -292,6 +292,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
{...field}
|
{...field}
|
||||||
placeholder={user ? "Leave blank to keep current password" : ""}
|
placeholder={user ? "Leave blank to keep current password" : ""}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
import { UserForm } from "./UserForm";
|
import { UserForm } from "./UserForm";
|
||||||
|
import { toast } from "sonner";
|
||||||
import config from "@/config";
|
import config from "@/config";
|
||||||
import { AuthContext } from "@/contexts/AuthContext";
|
import { AuthContext } from "@/contexts/AuthContext";
|
||||||
import { ShieldAlert } from "lucide-react";
|
import { ShieldAlert } from "lucide-react";
|
||||||
@@ -242,6 +243,15 @@ export function UserManagement() {
|
|||||||
|
|
||||||
console.log("Server response after saving user:", responseData);
|
console.log("Server response after saving user:", responseData);
|
||||||
|
|
||||||
|
// Explicitly confirm whether a password was part of this save — the form
|
||||||
|
// silently drops an empty password on edit, so without this there is no
|
||||||
|
// way to tell if a password change actually went through.
|
||||||
|
toast.success(
|
||||||
|
userData.id
|
||||||
|
? (formattedUserData.password ? "User updated — password changed" : "User updated (password not changed)")
|
||||||
|
: "User created"
|
||||||
|
);
|
||||||
|
|
||||||
// Reset the form state
|
// Reset the form state
|
||||||
setSelectedUser(null);
|
setSelectedUser(null);
|
||||||
setIsAddingUser(false);
|
setIsAddingUser(false);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
import { useDebounce } from '@/hooks/useDebounce';
|
import { useDebounce } from '@/hooks/useDebounce';
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { toast } from "sonner";
|
||||||
import config from "@/config";
|
import config from "@/config";
|
||||||
|
|
||||||
interface VendorSetting {
|
interface VendorSetting {
|
||||||
@@ -34,7 +34,6 @@ export function VendorSettings() {
|
|||||||
const [searchInputValue, setSearchInputValue] = useState('');
|
const [searchInputValue, setSearchInputValue] = useState('');
|
||||||
const searchQuery = useDebounce(searchInputValue, 300); // 300ms debounce
|
const searchQuery = useDebounce(searchInputValue, 300); // 300ms debounce
|
||||||
const [pendingChanges, setPendingChanges] = useState<Record<string, boolean>>({});
|
const [pendingChanges, setPendingChanges] = useState<Record<string, boolean>>({});
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
// Use useCallback to avoid unnecessary re-renders
|
// Use useCallback to avoid unnecessary re-renders
|
||||||
const loadSettings = useCallback(async () => {
|
const loadSettings = useCallback(async () => {
|
||||||
@@ -50,15 +49,11 @@ export function VendorSettings() {
|
|||||||
setSettings(data.items);
|
setSettings(data.items);
|
||||||
setTotalCount(data.total);
|
setTotalCount(data.total);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast.error(`Failed to load settings: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
title: "Error",
|
|
||||||
description: `Failed to load settings: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [page, searchQuery, pageSize, toast]);
|
}, [page, searchQuery, pageSize]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
@@ -93,19 +88,12 @@ export function VendorSettings() {
|
|||||||
throw new Error(data.error || 'Failed to update vendor setting');
|
throw new Error(data.error || 'Failed to update vendor setting');
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast.success(`Settings updated for vendor ${vendor}`);
|
||||||
title: "Success",
|
|
||||||
description: `Settings updated for vendor ${vendor}`,
|
|
||||||
});
|
|
||||||
setPendingChanges(prev => ({ ...prev, [vendor]: false }));
|
setPendingChanges(prev => ({ ...prev, [vendor]: false }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast.error(`Failed to update setting: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
title: "Error",
|
|
||||||
description: `Failed to update setting: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [settings, toast]);
|
}, [settings]);
|
||||||
|
|
||||||
const handleResetToDefault = useCallback(async (vendor: string) => {
|
const handleResetToDefault = useCallback(async (vendor: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -119,19 +107,12 @@ export function VendorSettings() {
|
|||||||
throw new Error(data.error || 'Failed to reset vendor setting');
|
throw new Error(data.error || 'Failed to reset vendor setting');
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast.success(`Settings reset for vendor ${vendor}`);
|
||||||
title: "Success",
|
|
||||||
description: `Settings reset for vendor ${vendor}`,
|
|
||||||
});
|
|
||||||
loadSettings(); // Reload settings to get defaults
|
loadSettings(); // Reload settings to get defaults
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast.error(`Failed to reset setting: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
title: "Error",
|
|
||||||
description: `Failed to reset setting: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [loadSettings, toast]);
|
}, [loadSettings]);
|
||||||
|
|
||||||
const totalPages = useMemo(() => Math.ceil(totalCount / pageSize), [totalCount, pageSize]);
|
const totalPages = useMemo(() => Math.ceil(totalCount / pageSize), [totalCount, pageSize]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
import { X } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const ToastProvider = ToastPrimitives.Provider
|
|
||||||
|
|
||||||
const ToastViewport = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Viewport
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
|
||||||
|
|
||||||
const toastVariants = cva(
|
|
||||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "border bg-background text-foreground",
|
|
||||||
destructive:
|
|
||||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const Toast = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
|
||||||
VariantProps<typeof toastVariants>
|
|
||||||
>(({ className, variant, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<ToastPrimitives.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(toastVariants({ variant }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
Toast.displayName = ToastPrimitives.Root.displayName
|
|
||||||
|
|
||||||
const ToastAction = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Action
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
|
||||||
|
|
||||||
const ToastClose = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Close
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
toast-close=""
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</ToastPrimitives.Close>
|
|
||||||
))
|
|
||||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
|
||||||
|
|
||||||
const ToastTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
|
||||||
|
|
||||||
const ToastDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm opacity-90", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
|
||||||
|
|
||||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
|
||||||
|
|
||||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
|
||||||
|
|
||||||
export {
|
|
||||||
type ToastProps,
|
|
||||||
type ToastActionElement,
|
|
||||||
ToastProvider,
|
|
||||||
ToastViewport,
|
|
||||||
Toast,
|
|
||||||
ToastTitle,
|
|
||||||
ToastDescription,
|
|
||||||
ToastClose,
|
|
||||||
ToastAction,
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { useToast } from "@/hooks/use-toast"
|
|
||||||
import {
|
|
||||||
Toast,
|
|
||||||
ToastClose,
|
|
||||||
ToastDescription,
|
|
||||||
ToastProvider,
|
|
||||||
ToastTitle,
|
|
||||||
ToastViewport,
|
|
||||||
} from "@/components/ui/toast"
|
|
||||||
|
|
||||||
export function Toaster() {
|
|
||||||
const { toasts } = useToast()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToastProvider>
|
|
||||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
|
||||||
return (
|
|
||||||
<Toast key={id} {...props}>
|
|
||||||
<div className="grid gap-1">
|
|
||||||
{title && <ToastTitle>{title}</ToastTitle>}
|
|
||||||
{description && (
|
|
||||||
<ToastDescription>{description}</ToastDescription>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{action}
|
|
||||||
<ToastClose />
|
|
||||||
</Toast>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<ToastViewport />
|
|
||||||
</ToastProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
// Inspired by react-hot-toast library
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import type {
|
|
||||||
ToastActionElement,
|
|
||||||
ToastProps,
|
|
||||||
} from "@/components/ui/toast"
|
|
||||||
|
|
||||||
const TOAST_LIMIT = 1
|
|
||||||
const TOAST_REMOVE_DELAY = 1000000
|
|
||||||
|
|
||||||
type ToasterToast = ToastProps & {
|
|
||||||
id: string
|
|
||||||
title?: React.ReactNode
|
|
||||||
description?: React.ReactNode
|
|
||||||
action?: ToastActionElement
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionTypes = {
|
|
||||||
ADD_TOAST: "ADD_TOAST",
|
|
||||||
UPDATE_TOAST: "UPDATE_TOAST",
|
|
||||||
DISMISS_TOAST: "DISMISS_TOAST",
|
|
||||||
REMOVE_TOAST: "REMOVE_TOAST",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
let count = 0
|
|
||||||
|
|
||||||
function genId() {
|
|
||||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
|
||||||
return count.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActionType = typeof actionTypes
|
|
||||||
|
|
||||||
type Action =
|
|
||||||
| {
|
|
||||||
type: ActionType["ADD_TOAST"]
|
|
||||||
toast: ToasterToast
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: ActionType["UPDATE_TOAST"]
|
|
||||||
toast: Partial<ToasterToast>
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: ActionType["DISMISS_TOAST"]
|
|
||||||
toastId?: ToasterToast["id"]
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: ActionType["REMOVE_TOAST"]
|
|
||||||
toastId?: ToasterToast["id"]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
toasts: ToasterToast[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
|
||||||
|
|
||||||
const addToRemoveQueue = (toastId: string) => {
|
|
||||||
if (toastTimeouts.has(toastId)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
toastTimeouts.delete(toastId)
|
|
||||||
dispatch({
|
|
||||||
type: "REMOVE_TOAST",
|
|
||||||
toastId: toastId,
|
|
||||||
})
|
|
||||||
}, TOAST_REMOVE_DELAY)
|
|
||||||
|
|
||||||
toastTimeouts.set(toastId, timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const reducer = (state: State, action: Action): State => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "ADD_TOAST":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
|
||||||
}
|
|
||||||
|
|
||||||
case "UPDATE_TOAST":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: state.toasts.map((t) =>
|
|
||||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
case "DISMISS_TOAST": {
|
|
||||||
const { toastId } = action
|
|
||||||
|
|
||||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
|
||||||
// but I'll keep it here for simplicity
|
|
||||||
if (toastId) {
|
|
||||||
addToRemoveQueue(toastId)
|
|
||||||
} else {
|
|
||||||
state.toasts.forEach((toast) => {
|
|
||||||
addToRemoveQueue(toast.id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: state.toasts.map((t) =>
|
|
||||||
t.id === toastId || toastId === undefined
|
|
||||||
? {
|
|
||||||
...t,
|
|
||||||
open: false,
|
|
||||||
}
|
|
||||||
: t
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "REMOVE_TOAST":
|
|
||||||
if (action.toastId === undefined) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const listeners: Array<(state: State) => void> = []
|
|
||||||
|
|
||||||
let memoryState: State = { toasts: [] }
|
|
||||||
|
|
||||||
function dispatch(action: Action) {
|
|
||||||
memoryState = reducer(memoryState, action)
|
|
||||||
listeners.forEach((listener) => {
|
|
||||||
listener(memoryState)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type Toast = Omit<ToasterToast, "id">
|
|
||||||
|
|
||||||
function toast({ ...props }: Toast) {
|
|
||||||
const id = genId()
|
|
||||||
|
|
||||||
const update = (props: ToasterToast) =>
|
|
||||||
dispatch({
|
|
||||||
type: "UPDATE_TOAST",
|
|
||||||
toast: { ...props, id },
|
|
||||||
})
|
|
||||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "ADD_TOAST",
|
|
||||||
toast: {
|
|
||||||
...props,
|
|
||||||
id,
|
|
||||||
open: true,
|
|
||||||
onOpenChange: (open) => {
|
|
||||||
if (!open) dismiss()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: id,
|
|
||||||
dismiss,
|
|
||||||
update,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function useToast() {
|
|
||||||
const [state, setState] = React.useState<State>(memoryState)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
listeners.push(setState)
|
|
||||||
return () => {
|
|
||||||
const index = listeners.indexOf(setState)
|
|
||||||
if (index > -1) {
|
|
||||||
listeners.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [state])
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toast,
|
|
||||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { useToast, toast }
|
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
SurchargeConfig,
|
SurchargeConfig,
|
||||||
CogsCalculationMode,
|
CogsCalculationMode,
|
||||||
} from "@/types/discount-simulator";
|
} from "@/types/discount-simulator";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { toast } from "sonner";
|
||||||
import { toDateOnly } from "@/utils/businessTime";
|
import { toDateOnly } from "@/utils/businessTime";
|
||||||
|
|
||||||
const DEFAULT_POINT_VALUE = 0.005;
|
const DEFAULT_POINT_VALUE = 0.005;
|
||||||
@@ -53,7 +53,6 @@ const defaultShippingPromo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function DiscountSimulator() {
|
export function DiscountSimulator() {
|
||||||
const { toast } = useToast();
|
|
||||||
const [dateRange, setDateRange] = useState<DateRange>(() => getDefaultDateRange());
|
const [dateRange, setDateRange] = useState<DateRange>(() => getDefaultDateRange());
|
||||||
const [selectedPromoId, setSelectedPromoId] = useState<number | undefined>(undefined);
|
const [selectedPromoId, setSelectedPromoId] = useState<number | undefined>(undefined);
|
||||||
const [productPromo, setProductPromo] = useState(defaultProductPromo);
|
const [productPromo, setProductPromo] = useState(defaultProductPromo);
|
||||||
@@ -214,10 +213,8 @@ export function DiscountSimulator() {
|
|||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error('Simulation error', error);
|
console.error('Simulation error', error);
|
||||||
toast({
|
toast.error('Simulation failed', {
|
||||||
title: 'Simulation failed',
|
|
||||||
description: error instanceof Error ? error.message : 'Unable to run discount simulation right now.',
|
description: error instanceof Error ? error.message : 'Unable to run discount simulation right now.',
|
||||||
variant: 'destructive',
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type HtsProduct = {
|
type HtsProduct = {
|
||||||
@@ -47,7 +47,6 @@ type HtsLookupResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function HtsLookup() {
|
export default function HtsLookup() {
|
||||||
const { toast } = useToast();
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [submittedTerm, setSubmittedTerm] = useState("");
|
const [submittedTerm, setSubmittedTerm] = useState("");
|
||||||
const [copiedCode, setCopiedCode] = useState<string | null>(null);
|
const [copiedCode, setCopiedCode] = useState<string | null>(null);
|
||||||
@@ -93,13 +92,9 @@ export default function HtsLookup() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
toast({
|
toast.error("Search failed", { description: error.message });
|
||||||
title: "Search failed",
|
|
||||||
description: error.message,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [error, toast]);
|
}, [error]);
|
||||||
|
|
||||||
const totalMatches = data?.total ?? 0;
|
const totalMatches = data?.total ?? 0;
|
||||||
const groupedResults = useMemo(() => data?.results ?? [], [data]);
|
const groupedResults = useMemo(() => data?.results ?? [], [data]);
|
||||||
@@ -110,10 +105,8 @@ export default function HtsLookup() {
|
|||||||
const valueToCopy = code === "Unspecified" ? "" : code;
|
const valueToCopy = code === "Unspecified" ? "" : code;
|
||||||
|
|
||||||
if (!navigator?.clipboard) {
|
if (!navigator?.clipboard) {
|
||||||
toast({
|
toast.error("Clipboard unavailable", {
|
||||||
title: "Clipboard unavailable",
|
|
||||||
description: "Your browser did not allow copying the code.",
|
description: "Your browser did not allow copying the code.",
|
||||||
variant: "destructive",
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -125,15 +118,12 @@ export default function HtsLookup() {
|
|||||||
}
|
}
|
||||||
setCopiedCode(code);
|
setCopiedCode(code);
|
||||||
copyTimerRef.current = window.setTimeout(() => setCopiedCode(null), 1200);
|
copyTimerRef.current = window.setTimeout(() => setCopiedCode(null), 1200);
|
||||||
toast({
|
toast.success("Copied HTS code", {
|
||||||
title: "Copied HTS code",
|
|
||||||
description: valueToCopy || "Empty code copied",
|
description: valueToCopy || "Empty code copied",
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast.error("Copy failed", {
|
||||||
title: "Copy failed",
|
|
||||||
description: err instanceof Error ? err.message : "Unable to copy code",
|
description: err instanceof Error ? err.message : "Unable to copy code",
|
||||||
variant: "destructive",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -143,8 +133,7 @@ export default function HtsLookup() {
|
|||||||
const trimmed = searchTerm.trim();
|
const trimmed = searchTerm.trim();
|
||||||
|
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
toast({
|
toast.info("Enter a search term", {
|
||||||
title: "Enter a search term",
|
|
||||||
description: "Search by title, SKU, vendor, barcode, or HTS code.",
|
description: "Search by title, SKU, vendor, barcode, or HTS code.",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { ProductDetail } from "@/components/products/ProductDetail";
|
|||||||
import { ProductViews } from "@/components/products/ProductViews";
|
import { ProductViews } from "@/components/products/ProductViews";
|
||||||
import { ProductTableSkeleton } from "@/components/products/ProductTableSkeleton";
|
import { ProductTableSkeleton } from "@/components/products/ProductTableSkeleton";
|
||||||
import { ProductSummaryCards } from "@/components/products/ProductSummaryCards";
|
import { ProductSummaryCards } from "@/components/products/ProductSummaryCards";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { toast } from "sonner";
|
||||||
import { useDebounce } from "@/hooks/useDebounce";
|
import { useDebounce } from "@/hooks/useDebounce";
|
||||||
import { AVAILABLE_COLUMNS, VIEW_COLUMNS, COLUMNS_BY_GROUP } from "@/components/products/columnDefinitions";
|
import { AVAILABLE_COLUMNS, VIEW_COLUMNS, COLUMNS_BY_GROUP } from "@/components/products/columnDefinitions";
|
||||||
import { transformMetricsRow, OPERATOR_MAP } from "@/utils/transformUtils";
|
import { transformMetricsRow, OPERATOR_MAP } from "@/utils/transformUtils";
|
||||||
@@ -160,7 +160,6 @@ export function Products() {
|
|||||||
const [showNonReplenishable, setShowNonReplenishable] = useState(false);
|
const [showNonReplenishable, setShowNonReplenishable] = useState(false);
|
||||||
const [showInvisible, setShowInvisible] = useState(false);
|
const [showInvisible, setShowInvisible] = useState(false);
|
||||||
const [selectedProductId, setSelectedProductId] = useState<number | null>(null);
|
const [selectedProductId, setSelectedProductId] = useState<number | null>(null);
|
||||||
const { toast } = useToast();
|
|
||||||
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Store visible columns and order for each view
|
// Store visible columns and order for each view
|
||||||
@@ -310,14 +309,10 @@ export function Products() {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching products:', error);
|
console.error('Error fetching products:', error);
|
||||||
toast({
|
toast.error("Failed to fetch products. Please try again.");
|
||||||
title: "Error",
|
|
||||||
description: "Failed to fetch products. Please try again.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [currentPage, pageSize, sortColumn, sortDirection, activeView, effectiveFilters, showNonReplenishable, showInvisible, toast]);
|
}, [currentPage, pageSize, sortColumn, sortDirection, activeView, effectiveFilters, showNonReplenishable, showInvisible]);
|
||||||
|
|
||||||
// Query for filter options
|
// Query for filter options
|
||||||
const { data: filterOptionsData, isLoading: isLoadingFilterOptions } = useQuery({
|
const { data: filterOptionsData, isLoading: isLoadingFilterOptions } = useQuery({
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type NumericAggregate = {
|
type NumericAggregate = {
|
||||||
@@ -112,7 +112,6 @@ function formatCurrency(n: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SpecLookup() {
|
export default function SpecLookup() {
|
||||||
const { toast } = useToast();
|
|
||||||
const [company, setCompany] = useState("");
|
const [company, setCompany] = useState("");
|
||||||
const [term, setTerm] = useState("");
|
const [term, setTerm] = useState("");
|
||||||
const [companyOpen, setCompanyOpen] = useState(false);
|
const [companyOpen, setCompanyOpen] = useState(false);
|
||||||
@@ -168,9 +167,9 @@ export default function SpecLookup() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
toast({ title: "Search failed", description: error.message, variant: "destructive" });
|
toast.error("Search failed", { description: error.message });
|
||||||
}
|
}
|
||||||
}, [error, toast]);
|
}, [error]);
|
||||||
|
|
||||||
const handleSubmit = (event?: FormEvent) => {
|
const handleSubmit = (event?: FormEvent) => {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
@@ -178,7 +177,7 @@ export default function SpecLookup() {
|
|||||||
const trimmedTerm = term.trim();
|
const trimmedTerm = term.trim();
|
||||||
|
|
||||||
if (!trimmedCompany && !trimmedTerm) {
|
if (!trimmedCompany && !trimmedTerm) {
|
||||||
toast({ title: "Enter a company or product type" });
|
toast.info("Enter a company or product type");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +192,7 @@ export default function SpecLookup() {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (!navigator?.clipboard) {
|
if (!navigator?.clipboard) {
|
||||||
toast({ title: "Clipboard unavailable", variant: "destructive" });
|
toast.error("Clipboard unavailable");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -202,10 +201,8 @@ export default function SpecLookup() {
|
|||||||
setCopied(key);
|
setCopied(key);
|
||||||
copyTimerRef.current = window.setTimeout(() => setCopied(null), 1200);
|
copyTimerRef.current = window.setTimeout(() => setCopied(null), 1200);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast.error("Copy failed", {
|
||||||
title: "Copy failed",
|
|
||||||
description: err instanceof Error ? err.message : "Unable to copy",
|
description: err instanceof Error ? err.message : "Unable to copy",
|
||||||
variant: "destructive",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user