added weight and updated prices

This commit is contained in:
2026-06-03 15:49:38 -04:00
parent 130130a7ed
commit 4a623e76b4
8 changed files with 186 additions and 54 deletions

View File

@ -7,7 +7,7 @@ import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([ export default defineConfig([
globalIgnores(['dist']), globalIgnores(['dist']),
{ {
files: ['**/*.{js,jsx}'], files: ['src/**/*.{js,jsx}'],
extends: [ extends: [
js.configs.recommended, js.configs.recommended,
reactHooks.configs.flat.recommended, reactHooks.configs.flat.recommended,
@ -26,4 +26,21 @@ export default defineConfig([
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], '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_]' }],
},
}
]) ])

Binary file not shown.

BIN
public/itemsold.xlsx Normal file

Binary file not shown.

View File

@ -641,5 +641,54 @@
} }
], ],
"shippingCost": 0 "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
} }
] ]

View File

@ -10,8 +10,8 @@ import './index.css';
function App() { function App() {
const [catalogItems, setCatalogItems] = useState([]); const [catalogItems, setCatalogItems] = useState([]);
const [quoteItems, setQuoteItems] = useState([]); const [quoteItems, setQuoteItems] = useState([]);
const [quoteId, setQuoteId] = useState(''); const [quoteId, setQuoteId] = useState(() => Math.floor(Math.random() * 10000) + 1000);
const [quoteDate, setQuoteDate] = useState(''); const [quoteDate, setQuoteDate] = useState(() => new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }));
const [savedQuotes, setSavedQuotes] = useState([]); const [savedQuotes, setSavedQuotes] = useState([]);
const [shippingCost, setShippingCost] = useState(0); const [shippingCost, setShippingCost] = useState(0);
const [isWorkOrderMode, setIsWorkOrderMode] = useState(false); const [isWorkOrderMode, setIsWorkOrderMode] = useState(false);
@ -27,9 +27,25 @@ function App() {
email: '' 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 // Initialize quote ID and date
useEffect(() => { 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(); loadSavedQuotes();
const handleAfterPrint = () => { const handleAfterPrint = () => {
@ -47,23 +63,6 @@ function App() {
fetchCatalog(); 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) => { const handleAddItem = (item) => {
setQuoteItems([...quoteItems, item]); setQuoteItems([...quoteItems, item]);
}; };

View File

@ -39,6 +39,16 @@ export default function QuoteSummary({ items, customer, quoteId, quoteDate, ship
const tax = (subtotal + shipping) * taxRate; const tax = (subtotal + shipping) * taxRate;
const total = subtotal + shipping + tax; 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) => { const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(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
</table> </table>
</div> </div>
{!isWorkOrderMode && ( <div className="totals">
<div className="totals"> <div className="total-row weight-row">
<div className="total-row"> <span>Total Weight:</span>
<span>Subtotal:</span> <span>{totalWeight.toFixed(1)} lbs</span>
<span>{formatCurrency(subtotal)}</span>
</div>
<div className="total-row">
<span>Shipping:</span>
<div className="no-print" style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ marginRight: '0.25rem' }}>$</span>
<input
type="number"
value={shippingCost}
onChange={(e) => onShippingChange(e.target.value)}
style={{ width: '80px', padding: '0.25rem', fontSize: '1rem', textAlign: 'right' }}
/>
</div>
<span className="print-only-block">{formatCurrency(shipping)}</span>
</div>
<div className="total-row">
<span>Tax ({(taxRate * 100).toFixed(1)}%):</span>
<span>{formatCurrency(tax)}</span>
</div>
<div className="total-row grand-total">
<span>Total:</span>
<span>{formatCurrency(total)}</span>
</div>
</div> </div>
)} {!isWorkOrderMode && (
<>
<div className="total-row">
<span>Subtotal:</span>
<span>{formatCurrency(subtotal)}</span>
</div>
<div className="total-row">
<span>Shipping:</span>
<div className="no-print" style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ marginRight: '0.25rem' }}>$</span>
<input
type="number"
value={shippingCost}
onChange={(e) => onShippingChange(e.target.value)}
style={{ width: '80px', padding: '0.25rem', fontSize: '1rem', textAlign: 'right' }}
/>
</div>
<span className="print-only-block">{formatCurrency(shipping)}</span>
</div>
<div className="total-row">
<span>Tax ({(taxRate * 100).toFixed(1)}%):</span>
<span>{formatCurrency(tax)}</span>
</div>
<div className="total-row grand-total">
<span>Total:</span>
<span>{formatCurrency(total)}</span>
</div>
</>
)}
</div>
</div> </div>
); );
} }

View File

@ -6,6 +6,22 @@ export default function SavedQuotesList({ savedQuotes, onLoadQuote, onDeleteQuot
return null; 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) => { const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
}; };
@ -21,25 +37,48 @@ export default function SavedQuotesList({ savedQuotes, onLoadQuote, onDeleteQuot
<th>Date</th> <th>Date</th>
<th>Customer Name</th> <th>Customer Name</th>
<th>Items</th> <th>Items</th>
<th>Weight</th>
<th>Total</th> <th>Total</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{savedQuotes.map((quote) => { {savedQuotes.map((quote) => {
// Calculate total for display // Calculate subtotal with item discounts
const subtotal = quote.items.reduce((total, item) => total + (item.Price * item.quantity), 0); const subtotal = quote.items.reduce((total, item) => {
const total = subtotal + (subtotal * 0.08); // Assuming 8% tax as in QuoteSummary 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 ( return (
<tr key={quote.id}> <tr key={quote.id}>
<td>{quote.id}</td> <td>{quote.id}</td>
<td>{quote.date}</td> <td>{quote.date}</td>
<td> <td>
{quote.customer.name || 'N/A'} {quote.customer?.name || 'N/A'}
{quote.customer.contactName && <div style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>Attn: {quote.customer.contactName}</div>} {quote.customer?.contactName && <div style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>Attn: {quote.customer.contactName}</div>}
</td> </td>
<td>{quote.items.length}</td> <td>{quote.items.length}</td>
<td>{totalWeight.toFixed(1)} lbs</td>
<td>{formatCurrency(total)}</td> <td>{formatCurrency(total)}</td>
<td> <td>
<div style={{ display: 'flex', gap: '0.5rem' }}> <div style={{ display: 'flex', gap: '0.5rem' }}>

View File

@ -217,6 +217,18 @@ td {
font-size: 1.1rem; 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 { .total-row.grand-total {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;