Integrate Web Pricing Editor and update configurations

This commit is contained in:
2026-06-30 15:02:13 -04:00
parent 7de185cc45
commit 3095844c15
4 changed files with 619 additions and 50 deletions

View File

@ -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 (
<div className="min-h-screen bg-gray-50">
<AdminPricing
pricing={pricing}
onSave={(newPricing) => setPricing(newPricing)}
onClose={() => setIsAdminMode(false)}
/>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
<div className="min-h-screen bg-gray-50 relative">
<div className="absolute top-4 left-4 z-10">
<button
onClick={() => setIsAdminMode(true)}
className="text-xs text-gray-500 hover:text-gray-700 transition-colors flex items-center gap-1 bg-white px-3 py-1.5 rounded-lg border border-gray-200 shadow-sm"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Edit Pricing
</button>
</div>
<Wizard pricing={pricing} onLogout={handleLogout} />
</div>
);

View File

@ -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 (
<div className="max-w-6xl mx-auto px-4 py-8">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Edit Pricing & Choices</h1>
<p className="text-gray-500 mt-1">Adjust base prices, required/optional parts, and options</p>
</div>
<div className="flex gap-3">
<button
onClick={onClose}
className="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg font-medium transition-colors"
>
Back to Estimator
</button>
<button
onClick={handleSave}
disabled={saving}
className="px-5 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save Changes'}
</button>
</div>
</div>
{message && (
<div className={`p-4 mb-6 rounded-lg font-medium ${message.type === 'success' ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-red-50 text-red-700 border border-red-200'}`}>
{message.text}
</div>
)}
{/* Tabs */}
<div className="flex border-b border-gray-200 mb-6 overflow-x-auto whitespace-nowrap">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`py-3 px-4 font-medium text-sm border-b-2 transition-all ${
activeTab === tab.id
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab.label}
</button>
))}
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
{activeTab === 'operators' && (
<div className="space-y-8">
{data.operators.map((op, opIdx) => (
<div key={op.id} className="border border-gray-100 rounded-xl p-5 bg-gray-50/50">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Name</label>
<input
type="text"
value={op.name}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Model</label>
<input
type="text"
value={op.model}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Base Price ($)</label>
<input
type="number"
step="0.01"
value={op.basePrice}
onChange={(e) => 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"
/>
</div>
</div>
{/* Required Parts */}
{op.requiredParts && op.requiredParts.length > 0 && (
<div className="mt-4">
<h4 className="text-sm font-semibold text-gray-700 mb-2">Required Parts & Fees Included</h4>
<div className="space-y-2">
{op.requiredParts.map((part, partIdx) => (
<div key={part.id} className="grid grid-cols-1 md:grid-cols-3 gap-3 bg-white p-3 rounded-lg border border-gray-200">
<div className="flex items-center text-sm font-medium text-gray-700">{part.name}</div>
<div>
<label className="block text-[10px] text-gray-400 uppercase">Qty</label>
<input
type="number"
value={part.qty}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-[10px] text-gray-400 uppercase">Unit Price ($)</label>
<input
type="number"
step="0.01"
value={part.unitPrice}
onChange={(e) => 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"
/>
</div>
</div>
))}
</div>
</div>
)}
{/* Optional Parts */}
{op.optionalParts && op.optionalParts.length > 0 && (
<div className="mt-4">
<h4 className="text-sm font-semibold text-gray-700 mb-2">Optional Parts</h4>
<div className="space-y-2">
{op.optionalParts.map((part, partIdx) => (
<div key={part.id} className="grid grid-cols-1 md:grid-cols-3 gap-3 bg-white p-3 rounded-lg border border-gray-200">
<div className="flex items-center text-sm font-medium text-gray-700">{part.name}</div>
<div>
<label className="block text-[10px] text-gray-400 uppercase">Qty</label>
<input
type="number"
value={part.qty}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-[10px] text-gray-400 uppercase">Unit Price ($)</label>
<input
type="number"
step="0.01"
value={part.unitPrice}
onChange={(e) => 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"
/>
</div>
</div>
))}
</div>
</div>
)}
{/* Arm Options for barrier */}
{op.armOptions && op.armOptions.length > 0 && (
<div className="mt-4">
<h4 className="text-sm font-semibold text-gray-700 mb-2">Arm Options</h4>
<div className="space-y-2">
{op.armOptions.map((arm, armIdx) => (
<div key={arm.id} className="grid grid-cols-1 md:grid-cols-2 gap-3 bg-white p-3 rounded-lg border border-gray-200">
<div className="flex items-center text-sm font-medium text-gray-700">{arm.name}</div>
<div>
<label className="block text-[10px] text-gray-400 uppercase">Price ($)</label>
<input
type="number"
step="0.01"
value={arm.price}
onChange={(e) => 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"
/>
</div>
</div>
))}
</div>
</div>
)}
</div>
))}
</div>
)}
{activeTab !== 'operators' && (
<div className="space-y-4">
{data[activeTab].map((item, idx) => (
<div key={item.id} className="grid grid-cols-1 md:grid-cols-3 gap-4 bg-gray-50 p-4 rounded-xl border border-gray-150">
<div>
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Name</label>
<div className="font-semibold text-gray-800 text-sm mt-2">{item.name}</div>
</div>
{item.description !== undefined && (
<div>
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Description</label>
<input
type="text"
value={item.description}
onChange={(e) => 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"
/>
</div>
)}
{item.price !== undefined && (
<div>
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Price ($)</label>
<input
type="number"
step="0.01"
value={item.price}
onChange={(e) => 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"
/>
</div>
)}
{item.additionalCost !== undefined && (
<div>
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Additional Cost ($)</label>
<input
type="number"
step="0.01"
value={item.additionalCost}
onChange={(e) => 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"
/>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@ -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
}
]
}

View File

@ -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'];