AI tweaks/fixes + backend api interface updates
This commit is contained in:
@@ -35,7 +35,7 @@ global.pool = pool;
|
||||
app.use(express.json());
|
||||
app.use(morgan('combined'));
|
||||
app.use(cors({
|
||||
origin: ['http://localhost:5175', 'http://localhost:5174', 'https://inventory.kent.pw', 'https://acot.site'],
|
||||
origin: ['http://localhost:5175', 'http://localhost:5174', 'https://inventory.kent.pw', 'https://acot.site', 'https://acob.acherryontop.com'],
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ global.pool = pool;
|
||||
app.use(express.json());
|
||||
app.use(morgan('combined'));
|
||||
app.use(cors({
|
||||
origin: ['http://localhost:5175', 'http://localhost:5174', 'https://inventory.kent.pw', 'https://acot.site'],
|
||||
origin: ['http://localhost:5175', 'http://localhost:5174', 'https://inventory.kent.pw', 'https://acot.site', 'https://acob.acherryontop.com'],
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const corsOptions = {
|
||||
origin: function(origin, callback) {
|
||||
const allowedOrigins = [
|
||||
'http://localhost:3000',
|
||||
'https://dashboard.kent.pw'
|
||||
'https://acob.acherryontop.com'
|
||||
];
|
||||
|
||||
console.log('CORS check for origin:', origin);
|
||||
|
||||
@@ -6,6 +6,7 @@ const corsMiddleware = cors({
|
||||
'https://inventory.kent.pw',
|
||||
'http://localhost:5175',
|
||||
'https://acot.site',
|
||||
'https://acob.acherryontop.com',
|
||||
/^http:\/\/192\.168\.\d+\.\d+(:\d+)?$/,
|
||||
/^http:\/\/10\.\d+\.\d+\.\d+(:\d+)?$/
|
||||
],
|
||||
@@ -27,7 +28,7 @@ const corsErrorHandler = (err, req, res, next) => {
|
||||
res.status(403).json({
|
||||
error: 'CORS not allowed',
|
||||
origin: req.get('Origin'),
|
||||
message: 'Origin not in allowed list: https://inventory.kent.pw, https://acot.site, localhost:5175, 192.168.x.x, or 10.x.x.x'
|
||||
message: 'Origin not in allowed list: https://inventory.kent.pw, https://acot.site, https://acob.acherryontop.com, localhost:5175, 192.168.x.x, or 10.x.x.x'
|
||||
});
|
||||
} else {
|
||||
next(err);
|
||||
|
||||
@@ -635,7 +635,7 @@ router.post('/upload-image', upload.single('image'), async (req, res) => {
|
||||
|
||||
// Create URL for the uploaded file - using an absolute URL with domain
|
||||
// This will generate a URL like: https://acot.site/uploads/products/filename.jpg
|
||||
const baseUrl = 'https://acot.site';
|
||||
const baseUrl = 'https://acob.acherryontop.com';
|
||||
const imageUrl = `${baseUrl}/uploads/products/${req.file.filename}`;
|
||||
|
||||
// Schedule this image for deletion in 24 hours
|
||||
@@ -715,6 +715,26 @@ router.delete('/delete-image', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Clear all taxonomy caches
|
||||
router.post('/clear-taxonomy-cache', (req, res) => {
|
||||
try {
|
||||
// Clear all entries from the query cache
|
||||
const cacheSize = connectionCache.queryCache.size;
|
||||
connectionCache.queryCache.clear();
|
||||
|
||||
console.log(`Cleared ${cacheSize} entries from taxonomy cache`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Cache cleared (${cacheSize} entries removed)`,
|
||||
clearedEntries: cacheSize
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error clearing taxonomy cache:', error);
|
||||
res.status(500).json({ error: 'Failed to clear cache' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get all options for import fields
|
||||
router.get('/field-options', async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -194,7 +194,7 @@ router.post('/upload', upload.single('image'), async (req, res) => {
|
||||
}
|
||||
|
||||
// Create URL for the uploaded file
|
||||
const baseUrl = 'https://acot.site';
|
||||
const baseUrl = 'https://acob.acherryontop.com';
|
||||
const imageUrl = `${baseUrl}/uploads/reusable/${req.file.filename}`;
|
||||
|
||||
const pool = req.app.locals.pool;
|
||||
|
||||
@@ -29,6 +29,7 @@ function buildSanityCheckUserPrompt(products, prompts) {
|
||||
tax_cat: p.tax_cat_name || p.tax_cat,
|
||||
size_cat: p.size_cat_name || p.size_cat,
|
||||
themes: p.theme_names || p.themes,
|
||||
categories: p.category_names || p.categories,
|
||||
weight: p.weight,
|
||||
length: p.length,
|
||||
width: p.width,
|
||||
@@ -59,7 +60,7 @@ function buildSanityCheckUserPrompt(products, prompts) {
|
||||
suggestion: 'Suggested fix or verification (optional)'
|
||||
}
|
||||
],
|
||||
summary: '1-2 sentences summarizing the batch quality'
|
||||
summary: '2-3 sentences summarizing the overall product quality'
|
||||
}, null, 2));
|
||||
|
||||
parts.push('');
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"build:deploy": "tsc -b && COPY_BUILD=true vite build",
|
||||
"build:deploy": "tsc -b && COPY_BUILD=true DEPLOY_TARGET=netcup DEPLOY_PATH=/var/www/html/inventory/frontend vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"mount": "../mountremote.command"
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -27,9 +27,10 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import config from "@/config";
|
||||
import { createProductCategory, type CreateProductCategoryResponse } from "@/services/apiv2";
|
||||
import { AuthContext } from "@/contexts/AuthContext";
|
||||
|
||||
type Option = {
|
||||
label: string;
|
||||
@@ -84,7 +85,11 @@ export function CreateProductCategoryDialog({
|
||||
environment = "prod",
|
||||
onCreated,
|
||||
}: CreateProductCategoryDialogProps) {
|
||||
const { user } = useContext(AuthContext);
|
||||
const hasDebugPermission = Boolean(user?.is_admin || user?.permissions?.includes("admin:debug"));
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<"line" | "subline">(defaultLineId ? "subline" : "line");
|
||||
const [companyId, setCompanyId] = useState<string>(defaultCompanyId ?? "");
|
||||
const [lineId, setLineId] = useState<string>(defaultLineId ?? "");
|
||||
const [categoryName, setCategoryName] = useState("");
|
||||
@@ -92,7 +97,8 @@ export function CreateProductCategoryDialog({
|
||||
const [isLoadingLines, setIsLoadingLines] = useState(false);
|
||||
const [lines, setLines] = useState<Option[]>([]);
|
||||
const [linesCache, setLinesCache] = useState<Record<string, Option[]>>({});
|
||||
|
||||
const [targetEnvironment, setTargetEnvironment] = useState<"dev" | "prod">(environment);
|
||||
|
||||
// Popover open states
|
||||
const [companyOpen, setCompanyOpen] = useState(false);
|
||||
const [lineOpen, setLineOpen] = useState(false);
|
||||
@@ -116,6 +122,7 @@ export function CreateProductCategoryDialog({
|
||||
setCompanyId(defaultCompanyId ?? "");
|
||||
setLineId(defaultLineId ?? "");
|
||||
setCategoryName("");
|
||||
setActiveTab(defaultLineId ? "subline" : "line");
|
||||
}
|
||||
}, [isOpen, defaultCompanyId, defaultLineId]);
|
||||
|
||||
@@ -180,8 +187,13 @@ export function CreateProductCategoryDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
const parentId = lineId || companyId;
|
||||
const creationType: "line" | "subline" = lineId ? "subline" : "line";
|
||||
if (activeTab === "subline" && !lineId) {
|
||||
toast.error("Select a parent line to create a subline");
|
||||
return;
|
||||
}
|
||||
|
||||
const parentId = activeTab === "subline" ? lineId : companyId;
|
||||
const creationType = activeTab;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
@@ -189,7 +201,7 @@ export function CreateProductCategoryDialog({
|
||||
const result = await createProductCategory({
|
||||
masterCatId: parentId,
|
||||
name: trimmedName,
|
||||
environment,
|
||||
environment: targetEnvironment,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
@@ -211,7 +223,7 @@ export function CreateProductCategoryDialog({
|
||||
}
|
||||
}
|
||||
|
||||
if (!lineId) {
|
||||
if (activeTab === "line") {
|
||||
const nextOption: Option = { label: trimmedName, value: newId ?? trimmedName };
|
||||
setLinesCache((prev) => {
|
||||
const existing = prev[companyId] ?? [];
|
||||
@@ -243,13 +255,13 @@ export function CreateProductCategoryDialog({
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: `Failed to create ${lineId ? "subline" : "product line"}.`;
|
||||
: `Failed to create ${activeTab === "line" ? "product line" : "subline"}.`;
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
},
|
||||
[categoryName, companyId, environment, lineId, onCreated],
|
||||
[activeTab, categoryName, companyId, targetEnvironment, lineId, onCreated],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -257,149 +269,172 @@ export function CreateProductCategoryDialog({
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Product Line or Subline</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new product line beneath a company or create a subline beneath an existing line.
|
||||
</DialogDescription>
|
||||
<DialogTitle>Create New Line or Subline</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Company Select - Searchable */}
|
||||
<div className="space-y-2">
|
||||
<Label>Company</Label>
|
||||
<Popover open={companyOpen} onOpenChange={setCompanyOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={companyOpen}
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
{selectedCompanyLabel || "Select a company"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search companies..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No company found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{companyOptions.map((company) => (
|
||||
<CommandItem
|
||||
key={company.value}
|
||||
value={company.label}
|
||||
onSelect={() => {
|
||||
setCompanyId(company.value);
|
||||
setLineId("");
|
||||
setCompanyOpen(false);
|
||||
}}
|
||||
>
|
||||
{company.label}
|
||||
{company.value === companyId && (
|
||||
<Check className="ml-auto h-4 w-4" />
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as "line" | "subline")}>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="line">Create Line</TabsTrigger>
|
||||
<TabsTrigger value="subline">Create Subline</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Line Select - Searchable */}
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
Parent Line <span className="text-muted-foreground">(optional)</span>
|
||||
</Label>
|
||||
<Popover open={lineOpen} onOpenChange={setLineOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={lineOpen}
|
||||
className="w-full justify-between font-normal"
|
||||
disabled={!companyId || isLoadingLines}
|
||||
>
|
||||
{!companyId
|
||||
? "Select a company first"
|
||||
: isLoadingLines
|
||||
? "Loading product lines..."
|
||||
: selectedLineLabel || "Leave empty to create a new line"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search lines..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No line found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{/* Option to clear selection */}
|
||||
<CommandItem
|
||||
value="none"
|
||||
onSelect={() => {
|
||||
setLineId("");
|
||||
setLineOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="text-muted-foreground">None (create new line)</span>
|
||||
{lineId === "" && <Check className="ml-auto h-4 w-4" />}
|
||||
</CommandItem>
|
||||
{lines.map((line) => (
|
||||
<CommandItem
|
||||
key={line.value}
|
||||
value={line.label}
|
||||
onSelect={() => {
|
||||
setLineId(line.value);
|
||||
setLineOpen(false);
|
||||
}}
|
||||
>
|
||||
{line.label}
|
||||
{line.value === lineId && (
|
||||
<Check className="ml-auto h-4 w-4" />
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{companyId && !isLoadingLines && !lines.length && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No existing lines found for this company. A new line will be created.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit} className="space-y-4 pt-4">
|
||||
{/* Company Select - shown in both tabs */}
|
||||
<div className="space-y-2">
|
||||
<Label>Company</Label>
|
||||
<Popover open={companyOpen} onOpenChange={setCompanyOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={companyOpen}
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
{selectedCompanyLabel || "Select a company"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search companies..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No company found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{companyOptions.map((company) => (
|
||||
<CommandItem
|
||||
key={company.value}
|
||||
value={company.label}
|
||||
onSelect={() => {
|
||||
setCompanyId(company.value);
|
||||
setLineId("");
|
||||
setCompanyOpen(false);
|
||||
}}
|
||||
>
|
||||
{company.label}
|
||||
{company.value === companyId && (
|
||||
<Check className="ml-auto h-4 w-4" />
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<TabsContent value="line" className="space-y-4 m-0">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-line-name">Line Name</Label>
|
||||
<Input
|
||||
id="create-line-name"
|
||||
value={categoryName}
|
||||
onChange={(event) => setCategoryName(event.target.value)}
|
||||
placeholder="Enter the new line name"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="subline" className="space-y-4 m-0">
|
||||
{/* Parent Line Select - only shown in subline tab */}
|
||||
<div className="space-y-2">
|
||||
<Label>Parent Line</Label>
|
||||
<Popover open={lineOpen} onOpenChange={setLineOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={lineOpen}
|
||||
className="w-full justify-between font-normal"
|
||||
disabled={!companyId || isLoadingLines}
|
||||
>
|
||||
{!companyId
|
||||
? "Select a company first"
|
||||
: isLoadingLines
|
||||
? "Loading product lines..."
|
||||
: selectedLineLabel || "Select a parent line"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search lines..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No line found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{lines.map((line) => (
|
||||
<CommandItem
|
||||
key={line.value}
|
||||
value={line.label}
|
||||
onSelect={() => {
|
||||
setLineId(line.value);
|
||||
setLineOpen(false);
|
||||
}}
|
||||
>
|
||||
{line.label}
|
||||
{line.value === lineId && (
|
||||
<Check className="ml-auto h-4 w-4" />
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{companyId && !isLoadingLines && !lines.length && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No existing lines found for this company. Create a line first.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-subline-name">Subline Name</Label>
|
||||
<Input
|
||||
id="create-subline-name"
|
||||
value={categoryName}
|
||||
onChange={(event) => setCategoryName(event.target.value)}
|
||||
placeholder="Enter the new subline name"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{hasDebugPermission && (
|
||||
<div className="pt-2 pb-2 border-t">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="category-api-environment"
|
||||
checked={targetEnvironment === "dev"}
|
||||
onCheckedChange={(checked) => setTargetEnvironment(checked ? "dev" : "prod")}
|
||||
/>
|
||||
<Label htmlFor="category-api-environment" className="text-sm font-medium cursor-pointer">
|
||||
Use test API
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-category-name">Name</Label>
|
||||
<Input
|
||||
id="create-category-name"
|
||||
value={categoryName}
|
||||
onChange={(event) => setCategoryName(event.target.value)}
|
||||
placeholder="Enter the new line or subline name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting || !companyId}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
"Create"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
<DialogFooter className="gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || !companyId || (activeTab === "subline" && !lineId)}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
`Create ${activeTab === "line" ? "Line" : "Subline"}`
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -143,7 +143,7 @@ export const BASE_IMPORT_FIELDS = [
|
||||
label: "Cost Each",
|
||||
key: "cost_each",
|
||||
description: "Wholesale cost per unit",
|
||||
alternateMatches: ["wholesale", "wholesale price", "supplier cost each", "cost each"],
|
||||
alternateMatches: ["wholesale", "wholesale price", "supplier cost each", "cost each","whls"],
|
||||
fieldType: {
|
||||
type: "input",
|
||||
price: true
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import config from "@/config"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { CheckCircle2, AlertCircle, EyeIcon, EyeOffIcon, ArrowRightIcon, XIcon, FileSpreadsheetIcon, LinkIcon, CheckIcon, ChevronsUpDown, Bot } from "lucide-react"
|
||||
import { CheckCircle2, AlertCircle, EyeIcon, EyeOffIcon, ArrowRightIcon, XIcon, FileSpreadsheetIcon, LinkIcon, CheckIcon, ChevronsUpDown, Bot, RefreshCw, Plus } from "lucide-react"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
@@ -611,6 +611,7 @@ const MatchColumnsStepComponent = <T extends string>({
|
||||
const { fields, autoMapHeaders, autoMapSelectValues, autoMapDistance, translations } = useRsi<T>()
|
||||
const queryClient = useQueryClient()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
|
||||
const [columns, setColumns] = useState<Columns<T>>(() => {
|
||||
// Helper function to check if a column is completely empty
|
||||
@@ -846,6 +847,43 @@ const MatchColumnsStepComponent = <T extends string>({
|
||||
[queryClient, setGlobalSelections],
|
||||
);
|
||||
|
||||
// Handle manual cache refresh
|
||||
const handleRefreshTaxonomy = useCallback(async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
// Clear backend cache
|
||||
const response = await fetch(`${config.apiUrl}/import/clear-taxonomy-cache`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to clear backend cache');
|
||||
}
|
||||
|
||||
// Clear frontend React Query cache
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['field-options'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['product-lines'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['product-lines-mapped'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['sublines'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['sublines-mapped'] }),
|
||||
]);
|
||||
|
||||
// Refetch field options immediately
|
||||
await queryClient.refetchQueries({ queryKey: ['field-options'] });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error refreshing taxonomy:', error);
|
||||
toast.error('Failed to refresh taxonomy data', {
|
||||
});
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}, [queryClient]);
|
||||
|
||||
// Check if a field is covered by global selections
|
||||
const isFieldCoveredByGlobalSelections = useCallback((key: string) => {
|
||||
return (key === 'supplier' && !!globalSelections.supplier) ||
|
||||
@@ -1815,11 +1853,11 @@ const MatchColumnsStepComponent = <T extends string>({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<div className="pt-4 flex items-center gap-2">
|
||||
<CreateProductCategoryDialog
|
||||
trigger={
|
||||
<Button variant="link" className="h-auto px-0 text-sm font-medium">
|
||||
+ New line or subline
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
<Plus className="h-3 w-3" /> New line or subline
|
||||
</Button>
|
||||
}
|
||||
companies={fieldOptions?.companies || []}
|
||||
@@ -1827,6 +1865,26 @@ const MatchColumnsStepComponent = <T extends string>({
|
||||
defaultLineId={globalSelections.line}
|
||||
onCreated={handleCategoryCreated}
|
||||
/>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={handleRefreshTaxonomy}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw className={`h-3 w-3 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
Refresh data
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[250px]">
|
||||
<p>Reload all suppliers, companies, lines, categories, and other taxonomy data from the database</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ export type StepState =
|
||||
| {
|
||||
type: StepType.validateDataNew
|
||||
data: any[]
|
||||
file?: File
|
||||
globalSelections?: GlobalSelections
|
||||
isFromScratch?: boolean
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ export const ValidationContainer = ({
|
||||
size_cat: row.size_cat as string | number | undefined,
|
||||
size_cat_name: getFieldLabel('size_cat', row.size_cat),
|
||||
themes: row.themes as string | undefined,
|
||||
categories: row.categories as string | undefined,
|
||||
weight: row.weight as string | number | undefined,
|
||||
length: row.length as string | number | undefined,
|
||||
width: row.width as string | number | undefined,
|
||||
@@ -220,14 +221,17 @@ export const ValidationContainer = ({
|
||||
}, []);
|
||||
|
||||
// Build product names lookup for sanity check dialog
|
||||
const productNames = useMemo(() => {
|
||||
// Rebuild fresh whenever dialog opens to ensure names are current after AI suggestions
|
||||
const buildProductNames = useCallback(() => {
|
||||
const rows = useValidationStore.getState().rows;
|
||||
const names: Record<number, string> = {};
|
||||
rows.forEach((row, index) => {
|
||||
names[index] = (row.name as string) || `Product ${index + 1}`;
|
||||
});
|
||||
return names;
|
||||
}, [rowCount]); // Depend on rowCount to update when rows change
|
||||
}, []);
|
||||
|
||||
const productNames = useMemo(() => buildProductNames(), [sanityCheckDialogOpen, buildProductNames]);
|
||||
|
||||
return (
|
||||
<AiSuggestionsProvider
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { useState, useCallback, useEffect, useRef, memo } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Loader2, AlertCircle } from 'lucide-react';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Check, X, Sparkles, AlertTriangle, Info, Cpu, Brain } from 'lucide-react';
|
||||
import { Protected } from '@/components/auth/Protected';
|
||||
import type { AiValidationResults, AiTokenUsage, AiValidationChange } from '../store/types';
|
||||
import type { AiValidationResults, AiTokenUsage } from '../store/types';
|
||||
|
||||
interface AiValidationResultsDialogProps {
|
||||
results: AiValidationResults;
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import type { SanityIssue, SanityCheckResult } from '../hooks/useSanityCheck';
|
||||
import type { SanityCheckResult } from '../hooks/useSanityCheck';
|
||||
|
||||
interface SanityCheckDialogProps {
|
||||
/** Whether the dialog is open */
|
||||
@@ -67,15 +67,25 @@ export function SanityCheckDialog({
|
||||
const hasAnyIssues = hasSanityIssues || hasValidationErrors;
|
||||
const allClear = !isChecking && !error && !hasSanityIssues && result;
|
||||
|
||||
// Group issues by severity/field for better organization
|
||||
// Group issues by field, then by exact issue+suggestion combination for deduplication
|
||||
const issuesByField = result?.issues?.reduce((acc, issue) => {
|
||||
const field = issue.field;
|
||||
if (!acc[field]) {
|
||||
acc[field] = [];
|
||||
acc[field] = {};
|
||||
}
|
||||
acc[field].push(issue);
|
||||
|
||||
const key = `${issue.issue}:${issue.suggestion || ''}`;
|
||||
if (!acc[field][key]) {
|
||||
acc[field][key] = {
|
||||
field: issue.field,
|
||||
issue: issue.issue,
|
||||
suggestion: issue.suggestion,
|
||||
productIndices: []
|
||||
};
|
||||
}
|
||||
acc[field][key].productIndices.push(issue.productIndex);
|
||||
return acc;
|
||||
}, {} as Record<string, SanityIssue[]>) || {};
|
||||
}, {} as Record<string, Record<string, { field: string; issue: string; suggestion?: string; productIndices: number[] }>>) || {};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -180,48 +190,59 @@ export function SanityCheckDialog({
|
||||
<div className="space-y-4">
|
||||
|
||||
|
||||
{/* Issues grouped by field */}
|
||||
{Object.entries(issuesByField).map(([field, fieldIssues]) => (
|
||||
{/* Issues grouped by field, then by unique issue+suggestion */}
|
||||
{Object.entries(issuesByField).map(([field, groups]) => (
|
||||
<div key={field} className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Badge variant="outline" className="text-xs font-semibold">
|
||||
{formatFieldName(field)}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{fieldIssues.length} issue{fieldIssues.length === 1 ? '' : 's'}
|
||||
{Object.values(groups).reduce((sum, g) => sum + g.productIndices.length, 0)} issue{Object.values(groups).reduce((sum, g) => sum + g.productIndices.length, 0) === 1 ? '' : 's'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{fieldIssues.map((issue, index) => (
|
||||
{Object.entries(groups).map(([key, group]) => (
|
||||
<div
|
||||
key={`${field}-${index}`}
|
||||
key={key}
|
||||
className="flex items-start gap-3 p-3 rounded-lg bg-gray-50 border border-gray-200 hover:border-gray-300 transition-colors"
|
||||
>
|
||||
<AlertCircle className="h-4 w-4 text-amber-500 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-sm font-medium">
|
||||
{productNames[issue.productIndex] || `Product ${issue.productIndex + 1}`}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{group.productIndices.length} product{group.productIndices.length === 1 ? '' : 's'}
|
||||
</span>
|
||||
{onScrollToProduct && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 px-1 text-xs text-blue-600 hover:text-blue-800"
|
||||
onClick={() => {
|
||||
onOpenChange(false);
|
||||
onScrollToProduct(issue.productIndex);
|
||||
}}
|
||||
>
|
||||
Go to
|
||||
<ChevronRight className="h-3 w-3 ml-0.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{issue.issue}</p>
|
||||
{issue.suggestion && (
|
||||
<div className="flex flex-wrap items-center gap-x-1 gap-y-1 mb-2">
|
||||
{group.productIndices.map((productIndex, idx) => (
|
||||
<span key={productIndex} className="inline-flex items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{productNames[productIndex] || `Product ${productIndex + 1}`}
|
||||
</span>
|
||||
{onScrollToProduct && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 px-1 text-xs text-blue-600 hover:text-blue-800"
|
||||
onClick={() => {
|
||||
onOpenChange(false);
|
||||
onScrollToProduct(productIndex);
|
||||
}}
|
||||
>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
{idx < group.productIndices.length - 1 && (
|
||||
<span className="text-gray-400 mr-1">,</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{group.issue}</p>
|
||||
{group.suggestion && (
|
||||
<p className="text-xs text-blue-600 mt-1">
|
||||
{issue.suggestion}
|
||||
{group.suggestion}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,6 @@ import { useCallback } from 'react';
|
||||
import { useValidationStore } from '../../store/validationStore';
|
||||
import type { AiValidationChange, AiValidationResults, AiTokenUsage } from '../../store/types';
|
||||
import type { Field, SelectOption } from '../../../../types';
|
||||
import type { AiValidationResponse, AiTokenUsage as ApiTokenUsage } from './useAiApi';
|
||||
|
||||
/**
|
||||
* Helper to convert a value to number or null
|
||||
|
||||
@@ -53,6 +53,8 @@ export interface ProductForSanityCheck {
|
||||
size_cat_name?: string;
|
||||
themes?: string;
|
||||
theme_names?: string;
|
||||
categories?: string;
|
||||
category_names?: string;
|
||||
weight?: string | number;
|
||||
length?: string | number;
|
||||
width?: string | number;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* only to the state they need, preventing unnecessary re-renders.
|
||||
*/
|
||||
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useValidationStore } from './validationStore';
|
||||
import type { RowData, ValidationError } from './types';
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ export interface NameValidationPayload {
|
||||
line_name?: string;
|
||||
subline_name?: string;
|
||||
siblingNames?: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,6 +119,7 @@ export interface DescriptionValidationPayload {
|
||||
company_name?: string;
|
||||
company_id?: string; // Needed by backend to load company-specific prompts (not sent to AI)
|
||||
categories?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,7 +147,7 @@ export function buildNameValidationPayload(
|
||||
): NameValidationPayload {
|
||||
// Use override line for sibling computation if provided
|
||||
const effectiveRow = overrides?.line !== undefined
|
||||
? { ...row, line: overrides.line }
|
||||
? { ...row, line: String(overrides.line) }
|
||||
: row;
|
||||
const siblingNames = computeSiblingNames(effectiveRow, allRows);
|
||||
|
||||
|
||||
@@ -5,15 +5,15 @@ const isLocal = window.location.hostname === 'localhost' || window.location.host
|
||||
const useProxy = !isLocal && (window.location.hostname === 'inventory.kent.pw' || window.location.hostname === 'inventory.acot.site' || window.location.hostname === 'acot.site');
|
||||
|
||||
const liveDashboardConfig = {
|
||||
auth: isDev || useProxy ? '/dashboard-auth' : 'https://dashboard.kent.pw/auth',
|
||||
aircall: isDev || useProxy ? '/api/aircall' : 'https://dashboard.kent.pw/api/aircall',
|
||||
klaviyo: isDev || useProxy ? '/api/klaviyo' : 'https://dashboard.kent.pw/api/klaviyo',
|
||||
meta: isDev || useProxy ? '/api/meta' : 'https://dashboard.kent.pw/api/meta',
|
||||
gorgias: isDev || useProxy ? '/api/gorgias' : 'https://dashboard.kent.pw/api/gorgias',
|
||||
analytics: isDev || useProxy ? '/api/dashboard-analytics' : 'https://dashboard.kent.pw/api/analytics',
|
||||
typeform: isDev || useProxy ? '/api/typeform' : 'https://dashboard.kent.pw/api/typeform',
|
||||
acot: isDev || useProxy ? '/api/acot' : 'https://dashboard.kent.pw/api/acot',
|
||||
clarity: isDev || useProxy ? '/api/clarity' : 'https://dashboard.kent.pw/api/clarity'
|
||||
auth: isDev || useProxy ? '/dashboard-auth' : 'https://acob.acherryontop.com/auth',
|
||||
aircall: isDev || useProxy ? '/api/aircall' : 'https://acob.acherryontop.com/api/aircall',
|
||||
klaviyo: isDev || useProxy ? '/api/klaviyo' : 'https://acob.acherryontop.com/api/klaviyo',
|
||||
meta: isDev || useProxy ? '/api/meta' : 'https://acob.acherryontop.com/api/meta',
|
||||
gorgias: isDev || useProxy ? '/api/gorgias' : 'https://acob.acherryontop.com/api/gorgias',
|
||||
analytics: isDev || useProxy ? '/api/dashboard-analytics' : 'https://acob.acherryontop.com/api/analytics',
|
||||
typeform: isDev || useProxy ? '/api/typeform' : 'https://acob.acherryontop.com/api/typeform',
|
||||
acot: isDev || useProxy ? '/api/acot' : 'https://acob.acherryontop.com/api/acot',
|
||||
clarity: isDev || useProxy ? '/api/clarity' : 'https://acob.acherryontop.com/api/clarity'
|
||||
};
|
||||
|
||||
export default liveDashboardConfig;
|
||||
@@ -29,10 +29,12 @@ export interface CreateProductCategoryResponse {
|
||||
category?: unknown;
|
||||
}
|
||||
|
||||
const DEV_ENDPOINT = "https://work-test-backend.acherryontop.com/apiv2/product/setup_new";
|
||||
const PROD_ENDPOINT = "https://backend.acherryontop.com/apiv2/product/setup_new";
|
||||
const DEV_CREATE_CATEGORY_ENDPOINT = "https://work-test-backend.acherryontop.com/apiv2/prod_cat/new";
|
||||
const PROD_CREATE_CATEGORY_ENDPOINT = "https://backend.acherryontop.com/apiv2/prod_cat/new";
|
||||
// Always use relative URLs - proxied by Vite in dev and Caddy in production
|
||||
// Frontend calls /apiv2/* -> Caddy transforms to /api/* -> proxies to www.acherryontop.com
|
||||
const DEV_ENDPOINT = "/apiv2-test/product/setup_new";
|
||||
const DEV_CREATE_CATEGORY_ENDPOINT = "/apiv2-test/prod_cat/new";
|
||||
const PROD_ENDPOINT = "/apiv2/product/setup_new";
|
||||
const PROD_CREATE_CATEGORY_ENDPOINT = "/apiv2/prod_cat/new";
|
||||
|
||||
const isHtmlResponse = (payload: string) => {
|
||||
const trimmed = payload.trim();
|
||||
@@ -44,31 +46,38 @@ export async function submitNewProducts({
|
||||
environment,
|
||||
useTestDataSource,
|
||||
}: SubmitNewProductsArgs): Promise<SubmitNewProductsResponse> {
|
||||
const authToken = import.meta.env.VITE_APIV2_AUTH_TOKEN;
|
||||
|
||||
if (!authToken) {
|
||||
throw new Error("VITE_APIV2_AUTH_TOKEN is not configured");
|
||||
}
|
||||
|
||||
const baseUrl = environment === "dev" ? DEV_ENDPOINT : PROD_ENDPOINT;
|
||||
const targetUrl = useTestDataSource ? `${baseUrl}?use_test_data_source=1` : baseUrl;
|
||||
|
||||
const payload = new URLSearchParams();
|
||||
payload.append("auth", authToken);
|
||||
|
||||
const serializedProducts = JSON.stringify(products);
|
||||
payload.append("products", serializedProducts);
|
||||
|
||||
let response: Response;
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
},
|
||||
body: payload,
|
||||
};
|
||||
|
||||
// Authentication strategy depends on endpoint
|
||||
if (environment === "dev") {
|
||||
// Test endpoint: Use auth token in request body
|
||||
const authToken = import.meta.env.VITE_APIV2_AUTH_TOKEN;
|
||||
if (authToken) {
|
||||
payload.append("auth", authToken);
|
||||
fetchOptions.body = payload;
|
||||
}
|
||||
} else {
|
||||
// Prod endpoint: Use cookies (proxied in both dev and production)
|
||||
fetchOptions.credentials = "include";
|
||||
}
|
||||
|
||||
try {
|
||||
response = await fetch(targetUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
},
|
||||
body: payload,
|
||||
});
|
||||
response = await fetch(targetUrl, fetchOptions);
|
||||
} catch (networkError) {
|
||||
throw new Error(networkError instanceof Error ? networkError.message : "Network request failed");
|
||||
}
|
||||
@@ -115,16 +124,9 @@ export async function createProductCategory({
|
||||
nameForCustoms,
|
||||
taxCodeId,
|
||||
}: CreateProductCategoryArgs): Promise<CreateProductCategoryResponse> {
|
||||
const authToken = import.meta.env.VITE_APIV2_AUTH_TOKEN;
|
||||
|
||||
if (!authToken) {
|
||||
throw new Error("VITE_APIV2_AUTH_TOKEN is not configured");
|
||||
}
|
||||
|
||||
const targetUrl = environment === "dev" ? DEV_CREATE_CATEGORY_ENDPOINT : PROD_CREATE_CATEGORY_ENDPOINT;
|
||||
|
||||
const payload = new URLSearchParams();
|
||||
payload.append("auth", authToken);
|
||||
payload.append("master_cat_id", masterCatId.toString());
|
||||
payload.append("name", name);
|
||||
|
||||
@@ -142,14 +144,29 @@ export async function createProductCategory({
|
||||
|
||||
let response: Response;
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
},
|
||||
body: payload,
|
||||
};
|
||||
|
||||
// Authentication strategy depends on endpoint
|
||||
if (environment === "dev") {
|
||||
// Test endpoint: Use auth token in request body
|
||||
const authToken = import.meta.env.VITE_APIV2_AUTH_TOKEN;
|
||||
if (authToken) {
|
||||
payload.append("auth", authToken);
|
||||
fetchOptions.body = payload;
|
||||
}
|
||||
} else {
|
||||
// Prod endpoint: Use cookies (proxied in both dev and production)
|
||||
fetchOptions.credentials = "include";
|
||||
}
|
||||
|
||||
try {
|
||||
response = await fetch(targetUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
},
|
||||
body: payload,
|
||||
});
|
||||
response = await fetch(targetUrl, fetchOptions);
|
||||
} catch (networkError) {
|
||||
throw new Error(networkError instanceof Error ? networkError.message : "Network request failed");
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@ import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { loadEnv } from "vite"
|
||||
import fs from 'fs-extra'
|
||||
import { execSync } from 'child_process'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
@@ -16,17 +17,42 @@ export default defineConfig(({ mode }) => {
|
||||
name: 'copy-build',
|
||||
closeBundle: async () => {
|
||||
if (!isDev && process.env.COPY_BUILD === 'true') {
|
||||
const sourcePath = path.resolve(__dirname, 'build');
|
||||
const targetPath = path.resolve(__dirname, '../inventory-server/frontend/build');
|
||||
const sourcePath = path.resolve(__dirname, 'build/');
|
||||
|
||||
try {
|
||||
await fs.ensureDir(path.dirname(targetPath));
|
||||
await fs.remove(targetPath);
|
||||
await fs.copy(sourcePath, targetPath);
|
||||
console.log('✓ Build copied to inventory-server/frontend/build');
|
||||
} catch (error) {
|
||||
console.error('Error copying build files:', error);
|
||||
process.exit(1);
|
||||
// Check if we should use rsync (for remote deployment)
|
||||
const useRsync = process.env.DEPLOY_TARGET;
|
||||
|
||||
if (useRsync) {
|
||||
// Use rsync over SSH - much faster than sshfs copying
|
||||
const deployTarget = process.env.DEPLOY_TARGET;
|
||||
const targetPath = process.env.DEPLOY_PATH || '/var/www/html/inventory/inventory-server/frontend';
|
||||
|
||||
try {
|
||||
console.log(`Deploying to ${deployTarget}:${targetPath}...`);
|
||||
|
||||
// Delete remote directory first, then sync
|
||||
execSync(`ssh ${deployTarget} "rm -rf ${targetPath}"`, { stdio: 'inherit' });
|
||||
execSync(`ssh ${deployTarget} "mkdir -p ${targetPath}"`, { stdio: 'inherit' });
|
||||
execSync(`rsync -avz --delete ${sourcePath} ${deployTarget}:${targetPath}/`, { stdio: 'inherit' });
|
||||
|
||||
console.log('✓ Build deployed');
|
||||
} catch (error) {
|
||||
console.error('Error deploying build files:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// Local copy (original behavior)
|
||||
const targetPath = path.resolve(__dirname, '../inventory-server/frontend/build');
|
||||
|
||||
try {
|
||||
await fs.ensureDir(path.dirname(targetPath));
|
||||
await fs.remove(targetPath);
|
||||
await fs.copy(sourcePath, targetPath);
|
||||
console.log('✓ Build copied to inventory-server/frontend/build');
|
||||
} catch (error) {
|
||||
console.error('Error copying build files:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +70,25 @@ export default defineConfig(({ mode }) => {
|
||||
host: "0.0.0.0",
|
||||
port: 5175,
|
||||
proxy: {
|
||||
"/api-testv2": {
|
||||
target: "https://work-test-backend.acherryontop.com",
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
cookieDomainRewrite: "localhost",
|
||||
rewrite: (path) => path.replace(/^\/api-testv2/, "/apiv2"),
|
||||
},
|
||||
"/apiv2": {
|
||||
target: "https://backend.acherryontop.com",
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
cookieDomainRewrite: "localhost",
|
||||
},
|
||||
"/login": {
|
||||
target: "https://backend.acherryontop.com",
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
cookieDomainRewrite: "localhost",
|
||||
},
|
||||
"/api/aircall": {
|
||||
target: "https://acot.site",
|
||||
changeOrigin: true,
|
||||
|
||||
Reference in New Issue
Block a user