diff --git a/client/src/App.jsx b/client/src/App.jsx index efcd20a..827d4b1 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,11 +1,13 @@ import { useState, useEffect, useCallback } from 'react'; import Wizard from './components/Wizard'; import Login from './components/Login'; +import AdminPricing from './components/AdminPricing'; export default function App() { const [pricing, setPricing] = useState(null); const [loading, setLoading] = useState(true); const [authenticated, setAuthenticated] = useState(false); + const [isAdminMode, setIsAdminMode] = useState(false); const fetchPricing = useCallback(async () => { try { @@ -49,6 +51,7 @@ export default function App() { } setAuthenticated(false); setPricing(null); + setIsAdminMode(false); }; if (!authenticated && !loading) { @@ -82,8 +85,32 @@ export default function App() { ); } + if (isAdminMode) { + return ( +
+ setPricing(newPricing)} + onClose={() => setIsAdminMode(false)} + /> +
+ ); + } + return ( -
+
+
+ +
); diff --git a/client/src/components/AdminPricing.jsx b/client/src/components/AdminPricing.jsx new file mode 100644 index 0000000..99ec958 --- /dev/null +++ b/client/src/components/AdminPricing.jsx @@ -0,0 +1,292 @@ +import { useState } from 'react'; + +export default function AdminPricing({ pricing, onSave, onClose }) { + const [data, setData] = useState(JSON.parse(JSON.stringify(pricing))); + const [activeTab, setActiveTab] = useState('operators'); + const [saving, setSaving] = useState(false); + const [message, setMessage] = useState(null); + + const tabs = [ + { id: 'operators', label: 'Operators' }, + { id: 'loopDetectors', label: 'Loop Detectors' }, + { id: 'groundLoopStyles', label: 'Loop Styles' }, + { id: 'groundLoopSizes', label: 'Loop Sizes' }, + { id: 'groundLoopTypes', label: 'Loop Types' }, + { id: 'remoteButtonOptions', label: 'Remote Buttons' }, + { id: 'accessControl', label: 'Access Control' }, + ]; + + const handlePriceChange = (section, index, field, value) => { + const updated = { ...data }; + const numVal = parseFloat(value); + updated[section][index][field] = isNaN(numVal) ? 0 : numVal; + setData(updated); + }; + + const handleTextChange = (section, index, field, value) => { + const updated = { ...data }; + updated[section][index][field] = value; + setData(updated); + }; + + const handleOperatorPartPriceChange = (opIndex, partType, partIndex, field, value) => { + const updated = { ...data }; + const numVal = parseFloat(value); + updated.operators[opIndex][partType][partIndex][field] = isNaN(numVal) ? 0 : numVal; + setData(updated); + }; + + const handleSave = async () => { + setSaving(true); + setMessage(null); + try { + const response = await fetch('/api/pricing', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + credentials: 'include', + }); + const result = await response.json(); + if (response.ok) { + setMessage({ type: 'success', text: 'Pricing successfully saved!' }); + onSave(data); + } else { + setMessage({ type: 'error', text: result.error || 'Failed to save pricing.' }); + } + } catch (err) { + setMessage({ type: 'error', text: 'Network error saving pricing.' }); + } finally { + setSaving(false); + } + }; + + return ( +
+
+
+

Edit Pricing & Choices

+

Adjust base prices, required/optional parts, and options

+
+
+ + +
+
+ + {message && ( +
+ {message.text} +
+ )} + + {/* Tabs */} +
+ {tabs.map((tab) => ( + + ))} +
+ +
+ {activeTab === 'operators' && ( +
+ {data.operators.map((op, opIdx) => ( +
+
+
+ + handleTextChange('operators', opIdx, 'name', e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-500 bg-white" + /> +
+
+ + handleTextChange('operators', opIdx, 'model', e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-500 bg-white" + /> +
+
+ + handlePriceChange('operators', opIdx, 'basePrice', e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-500 bg-white" + /> +
+
+ + {/* Required Parts */} + {op.requiredParts && op.requiredParts.length > 0 && ( +
+

Required Parts & Fees Included

+
+ {op.requiredParts.map((part, partIdx) => ( +
+
{part.name}
+
+ + handleOperatorPartPriceChange(opIdx, 'requiredParts', partIdx, 'qty', e.target.value)} + className="w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 bg-white text-sm" + /> +
+
+ + handleOperatorPartPriceChange(opIdx, 'requiredParts', partIdx, 'unitPrice', e.target.value)} + className="w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 bg-white text-sm" + /> +
+
+ ))} +
+
+ )} + + {/* Optional Parts */} + {op.optionalParts && op.optionalParts.length > 0 && ( +
+

Optional Parts

+
+ {op.optionalParts.map((part, partIdx) => ( +
+
{part.name}
+
+ + handleOperatorPartPriceChange(opIdx, 'optionalParts', partIdx, 'qty', e.target.value)} + className="w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 bg-white text-sm" + /> +
+
+ + handleOperatorPartPriceChange(opIdx, 'optionalParts', partIdx, 'unitPrice', e.target.value)} + className="w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 bg-white text-sm" + /> +
+
+ ))} +
+
+ )} + + {/* Arm Options for barrier */} + {op.armOptions && op.armOptions.length > 0 && ( +
+

Arm Options

+
+ {op.armOptions.map((arm, armIdx) => ( +
+
{arm.name}
+
+ + handleOperatorPartPriceChange(opIdx, 'armOptions', armIdx, 'price', e.target.value)} + className="w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 bg-white text-sm" + /> +
+
+ ))} +
+
+ )} +
+ ))} +
+ )} + + {activeTab !== 'operators' && ( +
+ {data[activeTab].map((item, idx) => ( +
+
+ +
{item.name}
+
+ {item.description !== undefined && ( +
+ + handleTextChange(activeTab, idx, 'description', e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-500 bg-white text-sm" + /> +
+ )} + {item.price !== undefined && ( +
+ + handlePriceChange(activeTab, idx, 'price', e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-500 bg-white text-sm" + /> +
+ )} + {item.additionalCost !== undefined && ( +
+ + handlePriceChange(activeTab, idx, 'additionalCost', e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-500 bg-white text-sm" + /> +
+ )} +
+ ))} +
+ )} +
+
+ ); +} diff --git a/server/data/pricing.json b/server/data/pricing.json index f9b3d03..b084eb5 100644 --- a/server/data/pricing.json +++ b/server/data/pricing.json @@ -11,18 +11,48 @@ "model": "LA500", "category": "swing", "description": "24V DC Swing Gate Linear Operator ", - "basePrice": 1732.50, + "basePrice": 2236, "image": "swing", "imageFile": "liftmaster-la500-bundle.jpg", "requiredParts": [ - { "id": "op-unit-swing", "name": "Operator Unit (LA500)", "qty": 1, "unitPrice": 1247.50 }, - { "id": "secondary-arm", "name": "Secondary Arm Kit (LA500)", "qty": 1, "unitPrice": 485 }, - { "id": "mounting-post", "name": "Mounting Post", "qty": 1, "unitPrice": 85 }, - { "id": "prep-cost", "name": "In House Prep Cost", "qty": 1, "unitPrice": 150 }, - { "id": "shipping", "name": "Shipping Cost", "qty": 1, "unitPrice": 200 } + { + "id": "op-unit-swing", + "name": "Operator Unit (LA500)", + "qty": 1, + "unitPrice": 2236 + }, + { + "id": "mounting-post", + "name": "Mounting Post", + "qty": 1, + "unitPrice": 85 + }, + { + "id": "prep-cost", + "name": "In House Prep Cost", + "qty": 1, + "unitPrice": 150 + }, + { + "id": "shipping", + "name": "Shipping Cost", + "qty": 1, + "unitPrice": 200 + } ], "optionalParts": [ - { "id": "add-secondary-arm", "name": "Additional Secondary Arm Kit (LA500)", "qty": 1, "unitPrice": 485 } + { + "id": "secondary-arm", + "name": "Secondary Arm Kit (LA500)", + "qty": 1, + "unitPrice": 1404 + }, + { + "id": "add-secondary-arm", + "name": "Additional Secondary Arm Kit (LA500)", + "qty": 1, + "unitPrice": 485 + } ] }, { @@ -31,18 +61,42 @@ "model": "CSW24UL", "category": "swing", "description": "24V DC Heavy Duty Swing Gate Operator", - "basePrice": 2442.50, + "basePrice": 2442.5, "image": "swing", "imageFile": "liftmaster-csw24.jpg", "requiredParts": [ - { "id": "op-unit-swing-hd", "name": "Heavy Duty Operator Unit (CSW24UL)", "qty": 1, "unitPrice": 1997.50 }, - { "id": "mount-pad", "name": "Mounting Pad", "qty": 2, "unitPrice": 145 }, - { "id": "prep-cost", "name": "In House Prep Cost", "qty": 1, "unitPrice": 150 }, - { "id": "shipping", "name": "Shipping Cost", "qty": 1, "unitPrice": 200 }, - { "id": "mounting-post", "name": "Mounting Post", "qty": 2, "unitPrice": 85 } + { + "id": "op-unit-swing-hd", + "name": "Heavy Duty Operator Unit (CSW24UL)", + "qty": 1, + "unitPrice": 1997.5 + }, + { + "id": "mount-pad", + "name": "Mounting Pad", + "qty": 2, + "unitPrice": 145 + }, + { + "id": "prep-cost", + "name": "In House Prep Cost", + "qty": 1, + "unitPrice": 150 + }, + { + "id": "shipping", + "name": "Shipping Cost", + "qty": 1, + "unitPrice": 200 + } ], "optionalParts": [ - { "id": "second-op-csw", "name": "Second Heavy Duty Operator Unit (CSW24UL)", "qty": 1, "unitPrice": 1997.50 } + { + "id": "second-op-csw", + "name": "Second Heavy Duty Operator Unit (CSW24UL)", + "qty": 1, + "unitPrice": 1997.5 + } ] }, { @@ -55,11 +109,30 @@ "image": "slide", "imageFile": "liftmaster-csl24.jpg", "requiredParts": [ - { "id": "op-unit-slide-lc", "name": "Slide Operator Unit (CSL24UL)", "qty": 1, "unitPrice": 2495 }, - { "id": "mount-pad", "name": "Mounting Pad", "qty": 2, "unitPrice": 145 }, - { "id": "prep-cost", "name": "In House Prep Cost", "qty": 1, "unitPrice": 150 }, - { "id": "shipping", "name": "Shipping Cost", "qty": 1, "unitPrice": 200 }, - { "id": "mounting-post", "name": "Mounting Post", "qty": 2, "unitPrice": 85 } + { + "id": "op-unit-slide-lc", + "name": "Slide Operator Unit (CSL24UL)", + "qty": 1, + "unitPrice": 2495 + }, + { + "id": "mount-pad", + "name": "Mounting Pad", + "qty": 2, + "unitPrice": 145 + }, + { + "id": "prep-cost", + "name": "In House Prep Cost", + "qty": 1, + "unitPrice": 150 + }, + { + "id": "shipping", + "name": "Shipping Cost", + "qty": 1, + "unitPrice": 200 + } ] }, { @@ -72,10 +145,30 @@ "image": "slide", "imageFile": "liftmaster-ihsl24ul.jpg", "requiredParts": [ - { "id": "op-unit-slide", "name": "Slide Operator Unit (IHSL24UL)", "qty": 1, "unitPrice": 3895 }, - { "id": "prep-cost", "name": "In House Prep Cost", "qty": 1, "unitPrice": 150 }, - { "id": "shipping", "name": "Shipping Cost", "qty": 1, "unitPrice": 200 }, - { "id": "mounting-post", "name": "Mounting Post", "qty": 2, "unitPrice": 85 } + { + "id": "op-unit-slide", + "name": "Slide Operator Unit (IHSL24UL)", + "qty": 1, + "unitPrice": 3895 + }, + { + "id": "prep-cost", + "name": "In House Prep Cost", + "qty": 1, + "unitPrice": 150 + }, + { + "id": "shipping", + "name": "Shipping Cost", + "qty": 1, + "unitPrice": 200 + }, + { + "id": "mounting-post", + "name": "Mounting Post", + "qty": 2, + "unitPrice": 85 + } ] }, { @@ -88,11 +181,24 @@ "image": "barrier", "imageFile": "Mat.jpeg", "requiredParts": [ - { "id": "op-unit-barrier", "name": "Mega Arm Tower Barrier Gate Operator (MAT)", "qty": 1, "unitPrice": 3195 } + { + "id": "op-unit-barrier", + "name": "Mega Arm Tower Barrier Gate Operator (MAT)", + "qty": 1, + "unitPrice": 3195 + } ], "armOptions": [ - { "id": "arm-mat-12", "name": "Barrier Arm (12ft Aluminum)", "price": 495 }, - { "id": "arm-mat-17", "name": "Barrier Arm (17ft Aluminum)", "price": 695 } + { + "id": "arm-mat-12", + "name": "Barrier Arm (12ft Aluminum)", + "price": 495 + }, + { + "id": "arm-mat-17", + "name": "Barrier Arm (17ft Aluminum)", + "price": 695 + } ] }, { @@ -105,11 +211,24 @@ "image": "barrier", "imageFile": "techno.jpeg", "requiredParts": [ - { "id": "op-unit-barrier-hd", "name": "Techna Barrier Gate Operator (CBG24DC)", "qty": 1, "unitPrice": 5495 } + { + "id": "op-unit-barrier-hd", + "name": "Techna Barrier Gate Operator (CBG24DC)", + "qty": 1, + "unitPrice": 5495 + } ], "armOptions": [ - { "id": "arm-techna-12", "name": "Barrier Arm (12ft Steel)", "price": 595 }, - { "id": "arm-techna-14", "name": "Barrier Arm (14ft Steel)", "price": 695 } + { + "id": "arm-techna-12", + "name": "Barrier Arm (12ft Steel)", + "price": 595 + }, + { + "id": "arm-techna-14", + "name": "Barrier Arm (14ft Steel)", + "price": 695 + } ] }, { @@ -126,33 +245,132 @@ } ], "groundLoopStyles": [ - { "id": "saw-cut", "name": "Saw-Cut Installation", "description": "Loop cut into existing asphalt and sealed flush", "additionalCost": 0 }, - { "id": "pave-over", "name": "Pave-Over Installation", "description": "Loop installed in gravel ", "additionalCost": 195 } + { + "id": "saw-cut", + "name": "Saw-Cut Installation", + "description": "Loop cut into existing asphalt and sealed flush", + "additionalCost": 0 + }, + { + "id": "pave-over", + "name": "Pave-Over Installation", + "description": "Loop installed in gravel ", + "additionalCost": 195 + } ], "loopDetectors": [ - { "id": "single", "name": "Single-Channel Loop Detector", "description": "Detects vehicle presence over the loop wire", "price": 250 } + { + "id": "single", + "name": "Single-Channel Loop Detector", + "description": "Detects vehicle presence over the loop wire", + "price": 250 + } ], "groundLoopSizes": [ - { "id": "4x8", "name": "4' x 8'", "description": "Standard loop size for residential applications", "additionalCost": 0 }, - { "id": "6x12", "name": "6' x 12'", "description": "Larger loop size for commercial or wide driveways", "additionalCost": 175 } + { + "id": "4x8", + "name": "4' x 8'", + "description": "Standard loop size for residential applications", + "additionalCost": 0 + }, + { + "id": "6x12", + "name": "6' x 12'", + "description": "Larger loop size for commercial or wide driveways", + "additionalCost": 175 + } ], "groundLoopTypes": [ - { "id": "interrupt", "name": "Interrupt Loop", "description": "Primary vehicle detection loop for gate activation", "price": 425 }, - { "id": "shadow", "name": "Shadow Loop", "description": "Secondary safety loop located behind gate to prevent closure on vehicle", "price": 375 }, - { "id": "exit", "name": "Exit Loop", "description": "Free exit loop on departure side for automatic exit detection", "price": 375 } + { + "id": "interrupt", + "name": "Interrupt Loop", + "description": "Primary vehicle detection loop for gate activation", + "price": 425 + }, + { + "id": "shadow", + "name": "Shadow Loop", + "description": "Secondary safety loop located behind gate to prevent closure on vehicle", + "price": 375 + }, + { + "id": "exit", + "name": "Exit Loop", + "description": "Free exit loop on departure side for automatic exit detection", + "price": 375 + } ], "remoteButtonOptions": [ - { "id": "1", "name": "1-Button Remote", "price": 95 }, - { "id": "2", "name": "2-Button Remote", "price": 125 }, - { "id": "3", "name": "3-Button Remote", "price": 165 }, - { "id": "4", "name": "4-Button Remote", "price": 195 } + { + "id": "1", + "name": "811LMX 1-Button Remote", + "price": 25 + }, + { + "id": "2", + "name": "892LT 2-Button Remote", + "price": 61.6 + }, + { + "id": "3", + "name": "893MAX 3-Button Remote", + "price": 58.88 + }, + { + "id": "4", + "name": "894LT 4-Button Remote", + "price": 63.68 + } ], "accessControl": [ - { "id": "keypad", "name": "Digital Keypad", "model": "KP-200", "description": "Weatherproof digital keypad with backlit keys, 500 user codes", "price": 295 }, - { "id": "card-reader", "name": "Proximity Card Reader", "model": "PR-500", "description": "125kHz proximity card reader with 1000 card capacity", "price": 445 }, - { "id": "intercom", "name": "Audio/Video Intercom", "model": "AV-700", "description": "2-wire audio/video intercom with color camera", "price": 895 }, - { "id": "remote-kit", "name": "Remote Control", "model": "", "description": "Remote controls with rolling code technology", "price": 0 }, - { "id": "solar-kit", "name": "Solar Panel Kit", "model": "SP-100", "description": "100W solar panel with charge controller and battery", "price": 695 }, - { "id": "gsm-controller", "name": "GSM Cellular Controller", "model": "GSM-4G", "description": "4G cellular controller with app, no phone line needed", "price": 595 } + { + "id": "keypad", + "name": "Digital Keypad", + "model": "KPR-2000", + "description": "Single Entry Access Keypad/Proximity Reader", + "price": 338 + }, + { + "id": "card-reader", + "name": "Proximity Card Reader", + "model": "PR-500", + "description": "125kHz proximity card reader with 1000 card capacity", + "price": 445 + }, + { + "id": "intercom", + "name": "Audio/Video Intercom", + "model": "AV-700", + "description": "2-wire audio/video intercom with color camera", + "price": 895 + }, + { + "id": "remote-kit", + "name": "Remote Control", + "model": "", + "description": "Remote controls with rolling code technology", + "price": 0 + }, + { + "id": "solar-kit", + "name": "Solar Panel Kit", + "model": "SP-100", + "description": "100W solar panel with charge controller and battery", + "price": 695 + }, + { + "id": "gsm-controller", + "name": "GSM Cellular Controller", + "model": "GSM-4G", + "description": "4G cellular controller with app, no phone line needed", + "price": 595 + }, + { + "id": "battery-backup", + "name": "Battery Backup System", + "model": "BB-12", + "description": "12V battery backup system with 72-hour runtime", + "price": 245 + } ] -} +} \ No newline at end of file diff --git a/server/index.js b/server/index.js index 7229568..f3280cb 100644 --- a/server/index.js +++ b/server/index.js @@ -28,7 +28,7 @@ app.use(session({ }, })); -const pricingData = JSON.parse( +let pricingData = JSON.parse( fs.readFileSync(path.join(__dirname, 'data', 'pricing.json'), 'utf-8') ); @@ -71,6 +71,38 @@ app.get('/api/pricing', requireAuth, (req, res) => { res.json(pricingData); }); +app.post('/api/pricing', requireAuth, (req, res) => { + try { + const newPricing = req.body; + if (!newPricing || typeof newPricing !== 'object') { + return res.status(400).json({ error: 'Invalid pricing data format' }); + } + // Basic validation + const requiredSections = ['operators', 'groundLoopStyles', 'loopDetectors', 'groundLoopSizes', 'groundLoopTypes', 'remoteButtonOptions', 'accessControl']; + for (const section of requiredSections) { + if (!Array.isArray(newPricing[section])) { + return res.status(400).json({ error: `Missing or invalid section: ${section}` }); + } + } + + const pricingPath = path.join(__dirname, 'data', 'pricing.json'); + const backupPath = path.join(__dirname, 'data', 'pricing.backup.json'); + + // Create a backup of the current pricing.json + fs.copyFileSync(pricingPath, backupPath); + + // Save the new pricing data + fs.writeFileSync(pricingPath, JSON.stringify(newPricing, null, 2), 'utf-8'); + + // Update the in-memory data + pricingData = newPricing; + + res.json({ success: true, message: 'Pricing updated successfully', backupCreated: true }); + } catch (err) { + res.status(500).json({ error: 'Failed to save pricing data: ' + err.message }); + } +}); + app.get('/api/pricing/:category', requireAuth, (req, res) => { const { category } = req.params; const validCategories = ['operators', 'groundLoopStyles', 'groundLoopSizes', 'loopDetectors', 'groundLoopTypes', 'accessControl', 'remoteButtonOptions'];