diff --git a/eslint.config.js b/eslint.config.js index 4fa125d..cefc303 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -7,7 +7,7 @@ import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ globalIgnores(['dist']), { - files: ['**/*.{js,jsx}'], + files: ['src/**/*.{js,jsx}'], extends: [ js.configs.recommended, reactHooks.configs.flat.recommended, @@ -26,4 +26,21 @@ export default defineConfig([ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], }, }, + { + files: ['server.js', 'migrate.js'], + extends: [ + js.configs.recommended, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.node, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + } ]) diff --git a/public/items.xlsx b/public/items.xlsx index 66e6a46..08be525 100644 Binary files a/public/items.xlsx and b/public/items.xlsx differ diff --git a/public/itemsold.xlsx b/public/itemsold.xlsx new file mode 100644 index 0000000..66e6a46 Binary files /dev/null and b/public/itemsold.xlsx differ diff --git a/quotes.json b/quotes.json index 34635ca..6751347 100644 --- a/quotes.json +++ b/quotes.json @@ -641,5 +641,54 @@ } ], "shippingCost": 0 + }, + { + "id": 5690, + "date": "June 3, 2026", + "customer": { + "name": "PERFECT POST", + "contactName": "", + "address": "", + "city": "", + "province": "", + "postalCode": "", + "phone": "", + "email": "" + }, + "items": [ + { + "Item ID": "MDL-5HPR7", + "Description": "Earth Drill, 5.5HP Honda EnginRoll Cage and Flat Free Tires", + "Weight": 161, + "Price": 5342.1757875, + "quantity": 1, + "discount": 0 + }, + { + "Item ID": "10X42-SSC", + "Description": "Auger, Snap-on, Carbide Point", + "Weight": 28, + "Price": 855.1449449999998, + "quantity": 1, + "discount": 0 + }, + { + "Item ID": "4064", + "Description": "Rivet, 3/16\" Aluminum Pop Rivet1/4\" Grip", + "Weight": 0.01, + "Price": 0.9523655999999998, + "quantity": 1, + "discount": 0 + }, + { + "Item ID": "16X42-SSPF", + "Description": "Auger, Snap-On, Full Flighted,Pengo Style", + "Weight": 60, + "Price": 1410.691545, + "quantity": 1, + "discount": 0 + } + ], + "shippingCost": 0 } ] \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 388f708..c02a3ce 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,8 +10,8 @@ import './index.css'; function App() { const [catalogItems, setCatalogItems] = useState([]); const [quoteItems, setQuoteItems] = useState([]); - const [quoteId, setQuoteId] = useState(''); - const [quoteDate, setQuoteDate] = useState(''); + const [quoteId, setQuoteId] = useState(() => Math.floor(Math.random() * 10000) + 1000); + const [quoteDate, setQuoteDate] = useState(() => new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })); const [savedQuotes, setSavedQuotes] = useState([]); const [shippingCost, setShippingCost] = useState(0); const [isWorkOrderMode, setIsWorkOrderMode] = useState(false); @@ -27,9 +27,25 @@ function App() { email: '' }); + const generateNewQuoteId = () => { + setQuoteId(Math.floor(Math.random() * 10000) + 1000); + setQuoteDate(new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })); + }; + // Initialize quote ID and date useEffect(() => { - generateNewQuoteId(); + const loadSavedQuotes = async () => { + try { + const response = await fetch('/api/quotes'); + if (response.ok) { + const data = await response.json(); + setSavedQuotes(data); + } + } catch (error) { + console.error("Error loading quotes:", error); + } + }; + loadSavedQuotes(); const handleAfterPrint = () => { @@ -47,23 +63,6 @@ function App() { fetchCatalog(); }, []); - const generateNewQuoteId = () => { - setQuoteId(Math.floor(Math.random() * 10000) + 1000); - setQuoteDate(new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })); - }; - - const loadSavedQuotes = async () => { - try { - const response = await fetch('/api/quotes'); - if (response.ok) { - const data = await response.json(); - setSavedQuotes(data); - } - } catch (error) { - console.error("Error loading quotes:", error); - } - }; - const handleAddItem = (item) => { setQuoteItems([...quoteItems, item]); }; diff --git a/src/components/QuoteSummary.jsx b/src/components/QuoteSummary.jsx index 3d9f619..e1a059d 100644 --- a/src/components/QuoteSummary.jsx +++ b/src/components/QuoteSummary.jsx @@ -39,6 +39,16 @@ export default function QuoteSummary({ items, customer, quoteId, quoteDate, ship const tax = (subtotal + shipping) * taxRate; const total = subtotal + shipping + tax; + const calculateTotalWeight = () => { + return items.reduce((total, item) => { + const qty = parseInt(item.quantity, 10) || 0; + const weight = parseFloat(item.Weight) || 0; + return total + (weight * qty); + }, 0); + }; + + const totalWeight = calculateTotalWeight(); + const formatCurrency = (amount) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); }; @@ -174,35 +184,41 @@ export default function QuoteSummary({ items, customer, quoteId, quoteDate, ship - {!isWorkOrderMode && ( -
-
- Subtotal: - {formatCurrency(subtotal)} -
-
- Shipping: -
- $ - onShippingChange(e.target.value)} - style={{ width: '80px', padding: '0.25rem', fontSize: '1rem', textAlign: 'right' }} - /> -
- {formatCurrency(shipping)} -
-
- Tax ({(taxRate * 100).toFixed(1)}%): - {formatCurrency(tax)} -
-
- Total: - {formatCurrency(total)} -
+
+
+ Total Weight: + {totalWeight.toFixed(1)} lbs
- )} + {!isWorkOrderMode && ( + <> +
+ Subtotal: + {formatCurrency(subtotal)} +
+
+ Shipping: +
+ $ + onShippingChange(e.target.value)} + style={{ width: '80px', padding: '0.25rem', fontSize: '1rem', textAlign: 'right' }} + /> +
+ {formatCurrency(shipping)} +
+
+ Tax ({(taxRate * 100).toFixed(1)}%): + {formatCurrency(tax)} +
+
+ Total: + {formatCurrency(total)} +
+ + )} +
); } diff --git a/src/components/SavedQuotesList.jsx b/src/components/SavedQuotesList.jsx index 343ee88..68c050d 100644 --- a/src/components/SavedQuotesList.jsx +++ b/src/components/SavedQuotesList.jsx @@ -6,6 +6,22 @@ export default function SavedQuotesList({ savedQuotes, onLoadQuote, onDeleteQuot return null; } + const provinceTaxRates = { + 'ON': 0.13, // Ontario (HST) + 'QC': 0.05, // Quebec (GST) + 'NS': 0.15, // Nova Scotia (HST) + 'NB': 0.15, // New Brunswick (HST) + 'MB': 0.05, // Manitoba (GST) + 'BC': 0.05, // British Columbia (GST) + 'PE': 0.15, // Prince Edward Island (HST) + 'SK': 0.05, // Saskatchewan (GST) + 'AB': 0.05, // Alberta (GST) + 'NL': 0.15, // Newfoundland and Labrador (HST) + 'NT': 0.05, // Northwest Territories (GST) + 'YT': 0.05, // Yukon (GST) + 'NU': 0.05 // Nunavut (GST) + }; + const formatCurrency = (amount) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); }; @@ -21,25 +37,48 @@ export default function SavedQuotesList({ savedQuotes, onLoadQuote, onDeleteQuot Date Customer Name Items + Weight Total Actions {savedQuotes.map((quote) => { - // Calculate total for display - const subtotal = quote.items.reduce((total, item) => total + (item.Price * item.quantity), 0); - const total = subtotal + (subtotal * 0.08); // Assuming 8% tax as in QuoteSummary + // Calculate subtotal with item discounts + const subtotal = quote.items.reduce((total, item) => { + const qty = parseInt(item.quantity, 10) || 0; + const price = parseFloat(item.Price) || 0; + const itemTotal = price * qty; + const discountAmount = itemTotal * ((item.discount || 0) / 100); + return total + (itemTotal - discountAmount); + }, 0); + + // Calculate shipping + const shipping = parseFloat(quote.shippingCost) || 0; + + // Calculate tax based on province + const province = quote.customer?.province?.toUpperCase().trim(); + const taxRate = provinceTaxRates[province] || 0.13; + const tax = (subtotal + shipping) * taxRate; + const total = subtotal + shipping + tax; + + // Calculate total weight + const totalWeight = quote.items.reduce((total, item) => { + const qty = parseInt(item.quantity, 10) || 0; + const weight = parseFloat(item.Weight) || 0; + return total + (weight * qty); + }, 0); return ( {quote.id} {quote.date} - {quote.customer.name || 'N/A'} - {quote.customer.contactName &&
Attn: {quote.customer.contactName}
} + {quote.customer?.name || 'N/A'} + {quote.customer?.contactName &&
Attn: {quote.customer.contactName}
} {quote.items.length} + {totalWeight.toFixed(1)} lbs {formatCurrency(total)}
diff --git a/src/index.css b/src/index.css index 19cb734..9813026 100644 --- a/src/index.css +++ b/src/index.css @@ -217,6 +217,18 @@ td { font-size: 1.1rem; } +/* New rule for weight row alignment */ +.weight-row { + justify-content: flex-start; + align-self: flex-start; + width: auto; +} + +.weight-row span:first-child { + margin-right: 0.5rem; +} + + .total-row.grand-total { font-size: 1.5rem; font-weight: 700;