add contact and workorder

This commit is contained in:
2026-04-22 14:22:42 -04:00
parent 4bebb04c5f
commit 8a5092c56d
4 changed files with 106 additions and 47 deletions

View File

@ -14,9 +14,11 @@ function App() {
const [quoteDate, setQuoteDate] = useState('');
const [savedQuotes, setSavedQuotes] = useState([]);
const [shippingCost, setShippingCost] = useState(0);
const [isWorkOrderMode, setIsWorkOrderMode] = useState(false);
const [customer, setCustomer] = useState({
name: '',
contactName: '',
address: '',
city: '',
province: '',
@ -29,6 +31,12 @@ function App() {
useEffect(() => {
generateNewQuoteId();
loadSavedQuotes();
const handleAfterPrint = () => {
setIsWorkOrderMode(false);
};
window.addEventListener('afterprint', handleAfterPrint);
return () => window.removeEventListener('afterprint', handleAfterPrint);
}, []);
useEffect(() => {
@ -66,8 +74,18 @@ function App() {
setQuoteItems(newItems);
};
const handlePrint = () => {
window.print();
const handlePrintQuote = () => {
setIsWorkOrderMode(false);
setTimeout(() => {
window.print();
}, 100);
};
const handlePrintWorkOrder = () => {
setIsWorkOrderMode(true);
setTimeout(() => {
window.print();
}, 100);
};
const handleSaveQuote = async () => {
@ -144,7 +162,7 @@ function App() {
const handleNewQuote = () => {
generateNewQuoteId();
setCustomer({ name: '', address: '', city: '', province: '', postalCode: '', phone: '', email: '' });
setCustomer({ name: '', contactName: '', address: '', city: '', province: '', postalCode: '', phone: '', email: '' });
setQuoteItems([]);
setShippingCost(0);
};
@ -155,7 +173,7 @@ function App() {
<div className="header">
<img src="/Logo.jpg" alt="Company Logo" style={{ height: '80px', marginBottom: '1rem' }} />
<h1>Little Beaver Earth Augers</h1>
<p>Official Quote Generator</p>
<p>{isWorkOrderMode ? 'Work Order' : 'Official Quote Generator'}</p>
</div>
@ -165,15 +183,16 @@ function App() {
<div>
<h3>Quote For:</h3>
<p>{customer.name || 'N/A'}</p>
{customer.contactName && <p><strong>Attn:</strong> {customer.contactName}</p>}
<p>{customer.address || 'N/A'}</p>
<p>{customer.city ? `${customer.city}, ` : ''}{customer.province} {customer.postalCode}</p>
<p>{customer.phone || 'N/A'}</p>
<p>{customer.email || 'N/A'}</p>
</div>
<div style={{ textAlign: 'right' }}>
<h3>Quote Details:</h3>
<h3>{isWorkOrderMode ? 'Work Order Details:' : 'Quote Details:'}</h3>
<p><strong>Date:</strong> {quoteDate}</p>
<p><strong>Quote #:</strong> {quoteId}</p>
<p><strong>{isWorkOrderMode ? 'Work Order #:' : 'Quote #:'}</strong> {quoteId}</p>
</div>
</div>
</div>
@ -181,7 +200,7 @@ function App() {
<div className="no-print" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
<h2 style={{ margin: 0 }}>Current Quote: #{quoteId}</h2>
<h2 style={{ margin: 0 }}>{isWorkOrderMode ? 'Current Work Order' : 'Current Quote'}: #{quoteId}</h2>
<button className="btn" style={{ backgroundColor: 'white', border: '1px solid var(--border-color)', color: 'var(--text-main)' }} onClick={handleNewQuote}>
<FilePlus size={18} /> New Quote
</button>
@ -203,6 +222,7 @@ function App() {
shippingCost={shippingCost}
onShippingChange={setShippingCost}
onRemoveItem={handleRemoveItem}
isWorkOrderMode={isWorkOrderMode}
/>
{/* Action Buttons */}
@ -211,9 +231,12 @@ function App() {
<button className="btn btn-primary" onClick={handleSaveQuote} style={{ padding: '1rem 2rem', fontSize: '1.1rem', backgroundColor: '#10b981' }}>
<Save style={{ marginRight: '0.5rem' }} /> Save Quote
</button>
<button className="btn btn-primary" onClick={handlePrint} style={{ padding: '1rem 2rem', fontSize: '1.1rem' }}>
<button className="btn btn-primary" onClick={handlePrintQuote} style={{ padding: '1rem 2rem', fontSize: '1.1rem' }}>
<Printer style={{ marginRight: '0.5rem' }} /> Print Quote
</button>
<button className="btn btn-primary" onClick={handlePrintWorkOrder} style={{ padding: '1rem 2rem', fontSize: '1.1rem', backgroundColor: '#3b82f6' }}>
<Printer style={{ marginRight: '0.5rem' }} /> Print Work Order
</button>
</div>
)}

View File

@ -53,13 +53,24 @@ export default function CustomerForm({ customer, onChange, savedQuotes = [] }) {
</div>
<div className="form-grid">
<div className="form-group">
<label htmlFor="name">Name</label>
<label htmlFor="name">Company / Name</label>
<input
type="text"
id="name"
name="name"
value={customer.name}
onChange={handleChange}
placeholder="ABC Corp"
/>
</div>
<div className="form-group">
<label htmlFor="contactName">Contact Name (Optional)</label>
<input
type="text"
id="contactName"
name="contactName"
value={customer.contactName || ''}
onChange={handleChange}
placeholder="John Doe"
/>
</div>
@ -87,14 +98,27 @@ export default function CustomerForm({ customer, onChange, savedQuotes = [] }) {
</div>
<div className="form-group">
<label htmlFor="province">Province / State</label>
<input
type="text"
<select
id="province"
name="province"
value={customer.province}
onChange={handleChange}
placeholder="ON"
/>
>
<option value="">-- Select --</option>
<option value="AB">Alberta (AB)</option>
<option value="BC">British Columbia (BC)</option>
<option value="MB">Manitoba (MB)</option>
<option value="NB">New Brunswick (NB)</option>
<option value="NL">Newfoundland and Labrador (NL)</option>
<option value="NS">Nova Scotia (NS)</option>
<option value="NT">Northwest Territories (NT)</option>
<option value="NU">Nunavut (NU)</option>
<option value="ON">Ontario (ON)</option>
<option value="PE">Prince Edward Island (PE)</option>
<option value="QC">Quebec (QC)</option>
<option value="SK">Saskatchewan (SK)</option>
<option value="YT">Yukon (YT)</option>
</select>
</div>
<div className="form-group">
<label htmlFor="postalCode">Postal Code</label>

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Trash2 } from 'lucide-react';
export default function QuoteSummary({ items, customer, shippingCost, onShippingChange, onRemoveItem }) {
export default function QuoteSummary({ items, customer, shippingCost, onShippingChange, onRemoveItem, isWorkOrderMode }) {
const provinceTaxRates = {
'ON': 0.13, // Ontario (HST)
'QC': 0.05, // Quebec (GST)
@ -51,17 +51,18 @@ export default function QuoteSummary({ items, customer, shippingCost, onShipping
return (
<div className="glass-card">
<h2>Quote Summary</h2>
<h2>{isWorkOrderMode ? 'Work Order Summary' : 'Quote Summary'}</h2>
<div className="table-container">
<table>
<thead>
<tr>
<th>Item ID</th>
<th>Description</th>
<th>Unit Price</th>
<th>Discount</th>
{!isWorkOrderMode && <th>Unit Price</th>}
{!isWorkOrderMode && <th>Discount</th>}
<th>Quantity</th>
<th>Total</th>
{!isWorkOrderMode && <th>Total</th>}
{isWorkOrderMode && <th style={{ width: '80px', textAlign: 'center' }}>Done</th>}
<th className="no-print">Action</th>
</tr>
</thead>
@ -74,10 +75,16 @@ export default function QuoteSummary({ items, customer, shippingCost, onShipping
<tr key={index}>
<td>{item['Item ID']}</td>
<td>{item.Description}</td>
<td>{formatCurrency(item.Price)}</td>
<td>{item.discount ? `${item.discount}%` : '-'}</td>
{!isWorkOrderMode && <td>{formatCurrency(item.Price)}</td>}
{!isWorkOrderMode && <td>{item.discount ? `${item.discount}%` : '-'}</td>}
<td>{item.quantity}</td>
<td>{formatCurrency(finalPrice)}</td>
{!isWorkOrderMode && <td>{formatCurrency(finalPrice)}</td>}
{isWorkOrderMode && (
<td style={{ textAlign: 'center' }}>
<div style={{ width: '20px', height: '20px', border: '1px solid black', margin: '0 auto' }} className="print-only-block" />
<div style={{ width: '20px', height: '20px', border: '1px solid #ccc', margin: '0 auto' }} className="no-print" />
</td>
)}
<td className="no-print">
<button
className="btn btn-icon btn-danger"
@ -94,33 +101,35 @@ export default function QuoteSummary({ items, customer, shippingCost, onShipping
</table>
</div>
<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' }}
/>
{!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" style={{ display: 'none' }}>{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>
<span className="print-only-block" style={{ display: 'none' }}>{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>
);
}

View File

@ -35,7 +35,10 @@ export default function SavedQuotesList({ savedQuotes, onLoadQuote, onDeleteQuot
<tr key={quote.id}>
<td>{quote.id}</td>
<td>{quote.date}</td>
<td>{quote.customer.name || 'N/A'}</td>
<td>
{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>{formatCurrency(total)}</td>
<td>