added weight and updated prices
This commit is contained in:
@ -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_]' }],
|
||||
},
|
||||
}
|
||||
])
|
||||
|
||||
Binary file not shown.
BIN
public/itemsold.xlsx
Normal file
BIN
public/itemsold.xlsx
Normal file
Binary file not shown.
49
quotes.json
49
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
|
||||
}
|
||||
]
|
||||
39
src/App.jsx
39
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]);
|
||||
};
|
||||
|
||||
@ -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
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{!isWorkOrderMode && (
|
||||
<div className="totals">
|
||||
<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 className="totals">
|
||||
<div className="total-row weight-row">
|
||||
<span>Total Weight:</span>
|
||||
<span>{totalWeight.toFixed(1)} lbs</span>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
<th>Date</th>
|
||||
<th>Customer Name</th>
|
||||
<th>Items</th>
|
||||
<th>Weight</th>
|
||||
<th>Total</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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 (
|
||||
<tr key={quote.id}>
|
||||
<td>{quote.id}</td>
|
||||
<td>{quote.date}</td>
|
||||
<td>
|
||||
{quote.customer.name || 'N/A'}
|
||||
{quote.customer.contactName && <div style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>Attn: {quote.customer.contactName}</div>}
|
||||
{quote.customer?.name || 'N/A'}
|
||||
{quote.customer?.contactName && <div style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>Attn: {quote.customer.contactName}</div>}
|
||||
</td>
|
||||
<td>{quote.items.length}</td>
|
||||
<td>{totalWeight.toFixed(1)} lbs</td>
|
||||
<td>{formatCurrency(total)}</td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user