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 (
+
+
+
+
+
);
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) => (
+
+
+
+ {/* 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) => (
+
+ ))}
+
+ )}
+
+
+ );
+}
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'];