Add ui for csv update and import (broken) + fix build issues
This commit is contained in:
8
inventory/package-lock.json
generated
8
inventory/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
157
inventory/src/pages/Settings.tsx
Normal file
157
inventory/src/pages/Settings.tsx
Normal 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
@@ -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"}
|
||||
@@ -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"); },
|
||||
|
||||
Reference in New Issue
Block a user