add contact and workorder
This commit is contained in:
37
src/App.jsx
37
src/App.jsx
@ -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 = () => {
|
||||
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>
|
||||
)}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,6 +101,7 @@ export default function QuoteSummary({ items, customer, shippingCost, onShipping
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{!isWorkOrderMode && (
|
||||
<div className="totals">
|
||||
<div className="total-row">
|
||||
<span>Subtotal:</span>
|
||||
@ -121,6 +129,7 @@ export default function QuoteSummary({ items, customer, shippingCost, onShipping
|
||||
<span>{formatCurrency(total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user