Add ui for csv update and import (broken) + fix build issues
This commit is contained in:
@@ -6,8 +6,8 @@ const dotenv = require('dotenv');
|
||||
|
||||
// For testing purposes, limit the number of rows to import (0 = no limit)
|
||||
const PRODUCTS_TEST_LIMIT = 0;
|
||||
const ORDERS_TEST_LIMIT = 5000;
|
||||
const PURCHASE_ORDERS_TEST_LIMIT = 0;
|
||||
const ORDERS_TEST_LIMIT = 10000;
|
||||
const PURCHASE_ORDERS_TEST_LIMIT = 10000;
|
||||
|
||||
dotenv.config({ path: path.join(__dirname, '../.env') });
|
||||
|
||||
|
||||
@@ -4,18 +4,37 @@ const mysql = require('mysql2/promise');
|
||||
const productsRouter = require('./routes/products');
|
||||
const dashboardRouter = require('./routes/dashboard');
|
||||
const ordersRouter = require('./routes/orders');
|
||||
const csvRoutes = require('./routes/csv');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
// Debug middleware to log all requests
|
||||
app.use((req, res, next) => {
|
||||
console.log(`[App Debug] ${new Date().toISOString()} - ${req.method} ${req.path}`);
|
||||
next();
|
||||
});
|
||||
|
||||
// Configure CORS with specific options
|
||||
app.use(cors({
|
||||
origin: [
|
||||
'http://localhost:5173', // Local development
|
||||
'https://inventory.kent.pw', // Production frontend
|
||||
/\.kent\.pw$/ // Any subdomain of kent.pw
|
||||
],
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true,
|
||||
optionsSuccessStatus: 200
|
||||
}));
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Database connection
|
||||
const pool = mysql.createPool({
|
||||
host: 'localhost',
|
||||
user: 'root',
|
||||
password: '',
|
||||
database: 'inventory',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'inventory',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
@@ -24,12 +43,73 @@ const pool = mysql.createPool({
|
||||
// Make db pool available in routes
|
||||
app.locals.pool = pool;
|
||||
|
||||
// Routes
|
||||
// Debug endpoint to list all registered routes
|
||||
app.get('/api/debug/routes', (req, res) => {
|
||||
console.log('Debug routes endpoint hit');
|
||||
const routes = [];
|
||||
app._router.stack.forEach(middleware => {
|
||||
if (middleware.route) {
|
||||
routes.push({
|
||||
path: middleware.route.path,
|
||||
methods: Object.keys(middleware.route.methods)
|
||||
});
|
||||
} else if (middleware.name === 'router') {
|
||||
middleware.handle.stack.forEach(handler => {
|
||||
if (handler.route) {
|
||||
const fullPath = (middleware.regexp.source === '^\\/?(?=\\/|$)' ? '' : middleware.regexp.source.replace(/\\\//g, '/').replace(/\^|\$/g, '')) + handler.route.path;
|
||||
routes.push({
|
||||
path: fullPath,
|
||||
methods: Object.keys(handler.route.methods)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
res.json(routes);
|
||||
});
|
||||
|
||||
// Test endpoint to verify server is running
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
// Mount all routes under /api
|
||||
console.log('Mounting routes...');
|
||||
|
||||
console.log('Mounting products routes...');
|
||||
app.use('/api/products', productsRouter);
|
||||
|
||||
console.log('Mounting dashboard routes...');
|
||||
app.use('/api/dashboard', dashboardRouter);
|
||||
|
||||
console.log('Mounting orders routes...');
|
||||
app.use('/api/orders', ordersRouter);
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
console.log('Mounting CSV routes...');
|
||||
app.use('/api/csv', csvRoutes);
|
||||
console.log('CSV routes mounted');
|
||||
|
||||
console.log('All routes mounted');
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Error:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
});
|
||||
|
||||
// 404 handler
|
||||
app.use((req, res) => {
|
||||
console.log('404 Not Found:', req.method, req.path);
|
||||
res.status(404).json({ error: 'Not Found' });
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3010;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
console.log('Available routes:');
|
||||
console.log('- GET /api/health');
|
||||
console.log('- GET /api/debug/routes');
|
||||
console.log('- GET /api/csv/status');
|
||||
console.log('- GET /api/csv/test');
|
||||
console.log('- POST /api/csv/update');
|
||||
});
|
||||
96
inventory-server/src/routes/csv.js
Normal file
96
inventory-server/src/routes/csv.js
Normal file
@@ -0,0 +1,96 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
// Debug middleware MUST be first
|
||||
router.use((req, res, next) => {
|
||||
console.log(`[CSV Route Debug] ${req.method} ${req.path}`);
|
||||
next();
|
||||
});
|
||||
|
||||
// Store active import process and its progress
|
||||
let activeImport = null;
|
||||
let importProgress = null;
|
||||
|
||||
// SSE clients for progress updates
|
||||
const clients = new Set();
|
||||
|
||||
// Helper to send progress to all connected clients
|
||||
function sendProgressToClients(progress) {
|
||||
clients.forEach(client => {
|
||||
client.write(`data: ${JSON.stringify(progress)}\n\n`);
|
||||
});
|
||||
}
|
||||
|
||||
// Debug endpoint to verify route registration
|
||||
router.get('/test', (req, res) => {
|
||||
console.log('CSV test endpoint hit');
|
||||
res.json({ message: 'CSV routes are working' });
|
||||
});
|
||||
|
||||
// Route to check import status
|
||||
router.get('/status', (req, res) => {
|
||||
console.log('CSV status endpoint hit');
|
||||
res.json({
|
||||
active: !!activeImport,
|
||||
progress: importProgress
|
||||
});
|
||||
});
|
||||
|
||||
// Route to update CSV files
|
||||
router.post('/update', async (req, res) => {
|
||||
console.log('CSV update endpoint hit');
|
||||
|
||||
if (activeImport) {
|
||||
console.log('Import already in progress');
|
||||
return res.status(409).json({ error: 'Import already in progress' });
|
||||
}
|
||||
|
||||
try {
|
||||
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'update-csv.js');
|
||||
console.log('Running script:', scriptPath);
|
||||
|
||||
if (!require('fs').existsSync(scriptPath)) {
|
||||
console.error('Script not found:', scriptPath);
|
||||
return res.status(500).json({ error: 'Update script not found' });
|
||||
}
|
||||
|
||||
activeImport = spawn('node', [scriptPath]);
|
||||
|
||||
activeImport.stdout.on('data', (data) => {
|
||||
console.log(`CSV Update: ${data}`);
|
||||
importProgress = data.toString();
|
||||
sendProgressToClients({ status: 'running', progress: importProgress });
|
||||
});
|
||||
|
||||
activeImport.stderr.on('data', (data) => {
|
||||
console.error(`CSV Update Error: ${data}`);
|
||||
sendProgressToClients({ status: 'error', error: data.toString() });
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
activeImport.on('close', (code) => {
|
||||
console.log(`CSV update process exited with code ${code}`);
|
||||
if (code === 0) {
|
||||
sendProgressToClients({ status: 'complete' });
|
||||
resolve();
|
||||
} else {
|
||||
sendProgressToClients({ status: 'error', error: `Process exited with code ${code}` });
|
||||
reject(new Error(`Update process exited with code ${code}`));
|
||||
}
|
||||
activeImport = null;
|
||||
importProgress = null;
|
||||
});
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error updating CSV files:', error);
|
||||
activeImport = null;
|
||||
importProgress = null;
|
||||
res.status(500).json({ error: 'Failed to update CSV files', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
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