Add ui for csv update and import (broken) + fix build issues

This commit is contained in:
2025-01-10 00:56:14 -05:00
parent 8bdd188dfe
commit dbdf77331c
17 changed files with 406 additions and 67 deletions

View File

@@ -41,6 +41,7 @@
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.14",
"@types/node": "^22.10.5",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
@@ -2558,6 +2559,13 @@
"@types/node": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.17.14",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.14.tgz",
"integrity": "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",

View File

@@ -43,6 +43,7 @@
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.14",
"@types/node": "^22.10.5",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",

View File

@@ -5,6 +5,7 @@ import { Products } from './pages/Products';
import { Import } from './pages/Import';
import { Dashboard } from './pages/Dashboard';
import { Orders } from './pages/Orders';
import { Settings } from './pages/Settings';
const queryClient = new QueryClient();
@@ -18,6 +19,7 @@ function App() {
<Route path="/products" element={<Products />} />
<Route path="/import" element={<Import />} />
<Route path="/orders" element={<Orders />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</MainLayout>
</Router>

View File

@@ -44,7 +44,7 @@ export function SalesByCategory() {
nameKey="category"
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{data?.map((entry, index) => (
{data?.map((_, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>

View File

@@ -2,7 +2,6 @@ import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { X } from "lucide-react";
interface ProductFilters {
search: string;

View File

@@ -59,10 +59,6 @@ export function ProductTable({
return <Badge variant="secondary">In Stock</Badge>;
};
const getProfitMargin = (price: number, cost: number) => {
if (!price || !cost) return 0;
return ((price - cost) / price) * 100;
};
return (
<div className="rounded-md border">

View File

@@ -1,4 +1,3 @@
import * as React from "react";
import { format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import { DateRange } from "react-day-picker";
@@ -13,7 +12,7 @@ import {
interface DateRangePickerProps {
value: DateRange;
onChange: (range: DateRange) => void;
onChange: (range: DateRange | undefined) => void;
className?: string;
}
@@ -55,7 +54,9 @@ export function DateRangePicker({
mode="range"
defaultMonth={value?.from}
selected={value}
onSelect={onChange}
onSelect={(range) => {
if (range) onChange(range);
}}
numberOfMonths={2}
/>
</PopoverContent>

View File

@@ -1,15 +1,5 @@
interface Config {
apiUrl: string;
}
const development: Config = {
const config = {
apiUrl: 'https://inventory.kent.pw/api'
};
const production: Config = {
apiUrl: 'https://inventory.kent.pw/api'
};
const config: Config = import.meta.env.PROD ? production : development;
export default config;

View File

@@ -32,6 +32,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ArrowUpDown, Search } from "lucide-react";
import debounce from 'lodash/debounce';
import config from '../config';
import { DateRange } from 'react-day-picker';
interface Order {
order_number: string;
@@ -47,7 +48,7 @@ interface Order {
interface OrderFilters {
search: string;
status: string;
dateRange: { from: Date | null; to: Date | null };
dateRange: DateRange;
minAmount: string;
maxAmount: string;
}
@@ -59,7 +60,7 @@ export function Orders() {
const [filters, setFilters] = useState<OrderFilters>({
search: '',
status: 'all',
dateRange: { from: null, to: null },
dateRange: { from: undefined, to: undefined },
minAmount: '',
maxAmount: '',
});
@@ -218,7 +219,7 @@ export function Orders() {
</Select>
<DateRangePicker
value={filters.dateRange}
onChange={(range) => debouncedFilterChange({ dateRange: range })}
onChange={(range: DateRange | undefined) => debouncedFilterChange({ dateRange: range })}
/>
<div className="flex items-center gap-2">
<Input
@@ -285,33 +286,36 @@ export function Orders() {
</div>
{data?.pagination.pages > 1 && (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1 || isFetching}
/>
</PaginationItem>
{Array.from({ length: data.pagination.pages }, (_, i) => i + 1).map((p) => (
<PaginationItem key={p}>
<PaginationLink
onClick={() => setPage(p)}
isActive={p === page}
disabled={isFetching}
>
{p}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
onClick={() => setPage(p => Math.min(data.pagination.pages, p + 1))}
disabled={page === data.pagination.pages || isFetching}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
aria-disabled={page === 1 || isFetching}
className={page === 1 || isFetching ? 'pointer-events-none opacity-50' : ''}
onClick={() => setPage(p => Math.max(1, p - 1))}
/>
</PaginationItem>
{Array.from({ length: data.pagination.pages }, (_, i) => i + 1).map((p) => (
<PaginationItem key={p}>
<PaginationLink
isActive={p === page}
aria-disabled={isFetching}
className={isFetching ? 'pointer-events-none opacity-50' : ''}
onClick={() => setPage(p)}
>
{p}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
aria-disabled={page === data.pagination.pages || isFetching}
className={page === data.pagination.pages || isFetching ? 'pointer-events-none opacity-50' : ''}
onClick={() => setPage(p => Math.min(data.pagination.pages, p + 1))}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
);

View File

@@ -222,8 +222,9 @@ export function Products() {
<PaginationContent>
<PaginationItem>
<PaginationPrevious
aria-disabled={page === 1 || isFetching}
className={page === 1 || isFetching ? 'pointer-events-none opacity-50' : ''}
onClick={() => handlePageChange(Math.max(1, page - 1))}
disabled={page === 1 || isFetching}
/>
</PaginationItem>
@@ -232,7 +233,8 @@ export function Products() {
<PaginationItem>
<PaginationLink
onClick={() => handlePageChange(1)}
disabled={isFetching}
aria-disabled={isFetching}
className={isFetching ? 'pointer-events-none opacity-50' : ''}
>
1
</PaginationLink>
@@ -246,7 +248,8 @@ export function Products() {
<PaginationLink
onClick={() => handlePageChange(p)}
isActive={p === currentPage}
disabled={isFetching}
aria-disabled={isFetching}
className={isFetching ? 'pointer-events-none opacity-50' : ''}
>
{p}
</PaginationLink>
@@ -259,7 +262,8 @@ export function Products() {
<PaginationItem>
<PaginationLink
onClick={() => handlePageChange(totalPages)}
disabled={isFetching}
aria-disabled={isFetching}
className={isFetching ? 'pointer-events-none opacity-50' : ''}
>
{totalPages}
</PaginationLink>
@@ -269,8 +273,9 @@ export function Products() {
<PaginationItem>
<PaginationNext
aria-disabled={page === data.pagination.pages || isFetching}
className={page === data.pagination.pages || isFetching ? 'pointer-events-none opacity-50' : ''}
onClick={() => handlePageChange(Math.min(data.pagination.pages, page + 1))}
disabled={page === data.pagination.pages || isFetching}
/>
</PaginationItem>
</PaginationContent>

View File

@@ -0,0 +1,157 @@
import { useState } from 'react';
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Loader2, RefreshCw, Upload } from "lucide-react";
import config from '../config';
interface ImportProgress {
operation: string;
current: number;
total: number;
rate: number;
elapsed: string;
remaining: string;
}
export function Settings() {
const [isUpdating, setIsUpdating] = useState(false);
const [isImporting, setIsImporting] = useState(false);
const [progress, setProgress] = useState<ImportProgress | null>(null);
const handleUpdateCSV = async () => {
setIsUpdating(true);
try {
const response = await fetch(`${config.apiUrl}/csv/update`, {
method: 'POST'
});
if (!response.ok) {
throw new Error('Failed to update CSV files');
}
// After successful update, trigger import
handleImportCSV();
} catch (error) {
console.error('Error updating CSV files:', error);
} finally {
setIsUpdating(false);
}
};
const handleImportCSV = async () => {
setIsImporting(true);
try {
const response = await fetch(`${config.apiUrl}/csv/import`, {
method: 'POST'
});
if (!response.ok) {
throw new Error('Failed to start CSV import');
}
// Set up SSE connection for progress updates
const eventSource = new EventSource(`${config.apiUrl}/csv/import/progress`);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
setProgress(data);
if (data.operation === 'complete') {
eventSource.close();
setIsImporting(false);
setProgress(null);
}
};
eventSource.onerror = () => {
eventSource.close();
setIsImporting(false);
setProgress(null);
};
} catch (error) {
console.error('Error importing CSV files:', error);
setIsImporting(false);
}
};
const renderProgress = () => {
if (!progress) return null;
const percentage = (progress.current / progress.total) * 100;
return (
<div className="space-y-2">
<div className="flex justify-between text-sm text-muted-foreground">
<span>{progress.operation}</span>
<span>{Math.round(percentage)}%</span>
</div>
<Progress value={percentage} className="h-2" />
<div className="flex justify-between text-sm text-muted-foreground">
<span>{progress.current.toLocaleString()} / {progress.total.toLocaleString()} rows</span>
<span>{Math.round(progress.rate)}/s</span>
</div>
<div className="flex justify-between text-sm text-muted-foreground">
<span>Elapsed: {progress.elapsed}</span>
<span>Remaining: {progress.remaining}</span>
</div>
</div>
);
};
return (
<div className="p-8 space-y-8">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Settings</h1>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader>
<CardTitle>Data Management</CardTitle>
<CardDescription>Update and import CSV data files</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
<Button
className="w-full"
onClick={handleUpdateCSV}
disabled={isUpdating || isImporting}
>
{isUpdating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Updating CSV Files...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Update CSV Files
</>
)}
</Button>
<Button
className="w-full"
onClick={handleImportCSV}
disabled={isUpdating || isImporting}
>
{isImporting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Importing Data...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
Import Data
</>
)}
</Button>
{(isUpdating || isImporting) && renderProgress()}
</div>
</CardContent>
</Card>
</div>
</div>
);
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/layout/mainlayout.tsx","./src/lib/utils.ts","./src/pages/import.tsx","./src/pages/products.tsx"],"version":"5.6.3"}
{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/dashboard/inventorystats.tsx","./src/components/dashboard/overview.tsx","./src/components/dashboard/recentsales.tsx","./src/components/dashboard/salesbycategory.tsx","./src/components/dashboard/trendingproducts.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/products/producteditdialog.tsx","./src/components/products/productfilters.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/hooks/use-mobile.tsx","./src/lib/utils.ts","./src/pages/dashboard.tsx","./src/pages/import.tsx","./src/pages/orders.tsx","./src/pages/products.tsx","./src/pages/settings.tsx"],"version":"5.6.3"}

View File

@@ -92,7 +92,7 @@ export default defineConfig(function (_a) {
port: 5173,
proxy: isDev ? {
"/api": {
target: "http://localhost:3010",
target: "https://inventory.kent.pw",
changeOrigin: true,
secure: false,
rewrite: function (path) { return path.replace(/^\/api/, "/api"); },