This commit is contained in:
Jake Kasper
2025-08-22 15:09:42 -04:00
parent 5afa1ee5a9
commit 89ee666858
2 changed files with 149 additions and 46 deletions

View File

@@ -61,6 +61,8 @@ const PropertyMap = ({
onSectionUpdate, onSectionUpdate,
onSectionDelete, onSectionDelete,
onPropertyUpdate, onPropertyUpdate,
onSectionClick,
selectedSections = [],
editable = false, editable = false,
className = "h-96 w-full" className = "h-96 w-full"
}) => { }) => {
@@ -136,7 +138,13 @@ const PropertyMap = ({
// Handle section click // Handle section click
const handleSectionClick = (section) => { const handleSectionClick = (section) => {
setSelectedSection(selectedSection?.id === section.id ? null : section); if (onSectionClick) {
// For application planning mode - use the provided callback
onSectionClick(section);
} else {
// For editing mode - use the internal state
setSelectedSection(selectedSection?.id === section.id ? null : section);
}
}; };
// Delete selected section // Delete selected section
@@ -350,18 +358,20 @@ const PropertyMap = ({
if (!section.polygonData?.coordinates?.[0]) return null; if (!section.polygonData?.coordinates?.[0]) return null;
const coordinates = section.polygonData.coordinates[0].map(([lng, lat]) => [lat, lng]); const coordinates = section.polygonData.coordinates[0].map(([lng, lat]) => [lat, lng]);
const isSelected = selectedSection?.id === section.id; const isInternallySelected = selectedSection?.id === section.id;
const isExternallySelected = selectedSections.includes(section.id);
const isSelected = isInternallySelected || isExternallySelected;
return ( return (
<Polygon <Polygon
key={section.id} key={section.id}
positions={coordinates} positions={coordinates}
pathOptions={{ pathOptions={{
fillColor: getSectionColor(section), fillColor: isExternallySelected ? '#10b981' : getSectionColor(section),
fillOpacity: isSelected ? 0.6 : 0.4, fillOpacity: isSelected ? 0.7 : 0.4,
color: getSectionColor(section), color: isExternallySelected ? '#059669' : getSectionColor(section),
weight: isSelected ? 3 : 2, weight: isSelected ? 4 : 2,
opacity: isSelected ? 1 : 0.8, opacity: 1,
}} }}
eventHandlers={{ eventHandlers={{
click: () => handleSectionClick(section) click: () => handleSectionClick(section)

View File

@@ -6,8 +6,9 @@ import {
WrenchScrewdriverIcon, WrenchScrewdriverIcon,
CalculatorIcon CalculatorIcon
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { propertiesAPI, productsAPI, equipmentAPI } from '../../services/api'; import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI } from '../../services/api';
import LoadingSpinner from '../../components/UI/LoadingSpinner'; import LoadingSpinner from '../../components/UI/LoadingSpinner';
import PropertyMap from '../../components/Maps/PropertyMap';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
const Applications = () => { const Applications = () => {
@@ -22,8 +23,8 @@ const Applications = () => {
const fetchApplications = async () => { const fetchApplications = async () => {
try { try {
setLoading(true); setLoading(true);
// TODO: Implement applications API endpoint const response = await applicationsAPI.getPlans();
setApplications([]); setApplications(response.data.data.plans || []);
} catch (error) { } catch (error) {
console.error('Failed to fetch applications:', error); console.error('Failed to fetch applications:', error);
toast.error('Failed to load applications'); toast.error('Failed to load applications');
@@ -77,8 +78,49 @@ const Applications = () => {
<div className="space-y-4"> <div className="space-y-4">
{applications.map((application) => ( {applications.map((application) => (
<div key={application.id} className="card"> <div key={application.id} className="card">
{/* Application card content will go here */} <div className="flex justify-between items-start">
<p>Application: {application.id}</p> <div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="font-semibold text-gray-900">
{application.propertyName} - {application.sectionName}
</h3>
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
application.status === 'planned' ? 'bg-blue-100 text-blue-800' :
application.status === 'completed' ? 'bg-green-100 text-green-800' :
application.status === 'in_progress' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{application.status}
</span>
</div>
<p className="text-sm text-gray-600 mb-1">
<MapPinIcon className="h-4 w-4 inline mr-1" />
{application.propertyAddress}
</p>
<p className="text-sm text-gray-600 mb-1">
Area: {Math.round(application.sectionArea).toLocaleString()} sq ft
</p>
<p className="text-sm text-gray-600 mb-1">
Equipment: {application.equipmentName}
</p>
<p className="text-sm text-gray-600">
Products: {application.productCount}
</p>
{application.notes && (
<p className="text-sm text-gray-500 mt-2 italic">
"{application.notes}"
</p>
)}
</div>
<div className="text-right">
<p className="text-sm font-medium text-gray-900">
{application.plannedDate ? new Date(application.plannedDate).toLocaleDateString() : 'No date set'}
</p>
<p className="text-xs text-gray-500">
Created {new Date(application.createdAt).toLocaleDateString()}
</p>
</div>
</div>
</div> </div>
))} ))}
</div> </div>
@@ -88,10 +130,34 @@ const Applications = () => {
{showPlanForm && ( {showPlanForm && (
<ApplicationPlanModal <ApplicationPlanModal
onClose={() => setShowPlanForm(false)} onClose={() => setShowPlanForm(false)}
onSubmit={(planData) => { onSubmit={async (planData) => {
console.log('Plan submitted:', planData); try {
setShowPlanForm(false); // Create a plan for each selected area
fetchApplications(); const planPromises = planData.selectedAreas.map(async (areaId) => {
const planPayload = {
lawnSectionId: parseInt(areaId),
equipmentId: parseInt(planData.equipmentId),
plannedDate: new Date().toISOString().split('T')[0], // Default to today
notes: planData.notes,
products: [{
productId: planData.selectedProduct?.isShared ? parseInt(planData.selectedProduct.id) : null,
userProductId: !planData.selectedProduct?.isShared ? parseInt(planData.selectedProduct.id) : null,
rateAmount: parseFloat(planData.selectedProduct?.customRateAmount || planData.selectedProduct?.rateAmount || 1),
rateUnit: planData.selectedProduct?.customRateUnit || planData.selectedProduct?.rateUnit || 'per 1000sqft'
}]
};
return applicationsAPI.createPlan(planPayload);
});
await Promise.all(planPromises);
toast.success(`Created ${planData.selectedAreas.length} application plan(s) successfully`);
setShowPlanForm(false);
fetchApplications();
} catch (error) {
console.error('Failed to create application plans:', error);
toast.error('Failed to create application plans');
}
}} }}
/> />
)} )}
@@ -253,7 +319,7 @@ const ApplicationPlanModal = ({ onClose, onSubmit }) => {
</select> </select>
</div> </div>
{/* Area Selection */} {/* Area Selection with Map */}
{loadingProperty && ( {loadingProperty && (
<div className="flex items-center gap-2 text-gray-600"> <div className="flex items-center gap-2 text-gray-600">
<LoadingSpinner size="sm" /> <LoadingSpinner size="sm" />
@@ -261,39 +327,66 @@ const ApplicationPlanModal = ({ onClose, onSubmit }) => {
</div> </div>
)} )}
{selectedPropertyDetails && selectedPropertyDetails.sections && selectedPropertyDetails.sections.length > 0 && ( {selectedPropertyDetails && (
<div> <div>
<label className="label">Application Areas *</label> <label className="label">Application Areas *</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{selectedPropertyDetails.sections.map((section) => (
<label key={section.id} className="flex items-center">
<input
type="checkbox"
checked={planData.selectedAreas.includes(section.id)}
onChange={() => handleAreaToggle(section.id)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2 text-sm">
{section.name} ({section.area ? `${Math.round(section.area)} sq ft` : 'No size'})
</span>
</label>
))}
</div>
{planData.selectedAreas.length > 0 && (
<p className="text-sm text-gray-600 mt-2">
Total area: {selectedPropertyDetails.sections
.filter(s => planData.selectedAreas.includes(s.id))
.reduce((total, s) => total + (s.area || 0), 0).toFixed(0)} sq ft
</p>
)}
</div>
)}
{selectedPropertyDetails && (!selectedPropertyDetails.sections || selectedPropertyDetails.sections.length === 0) && ( {/* Property Map */}
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg"> {selectedPropertyDetails.latitude && selectedPropertyDetails.longitude && (
<p className="text-yellow-800 text-sm"> <div className="mb-4">
This property has no lawn sections defined. Please add lawn sections to the property first. <div className="relative">
</p> <PropertyMap
center={[selectedPropertyDetails.latitude, selectedPropertyDetails.longitude]}
zoom={18}
property={selectedPropertyDetails}
sections={selectedPropertyDetails.sections || []}
editable={false}
className="h-64 w-full"
selectedSections={planData.selectedAreas}
onSectionClick={(section) => handleAreaToggle(section.id)}
/>
<div className="absolute top-2 left-2 bg-white rounded-lg shadow-lg p-2 text-xs">
<p className="font-medium text-gray-700">Click sections to select</p>
{planData.selectedAreas.length > 0 && (
<p className="text-blue-600">{planData.selectedAreas.length} selected</p>
)}
</div>
</div>
</div>
)}
{selectedPropertyDetails.sections && selectedPropertyDetails.sections.length > 0 ? (
<div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{selectedPropertyDetails.sections.map((section) => (
<label key={section.id} className="flex items-center">
<input
type="checkbox"
checked={planData.selectedAreas.includes(section.id)}
onChange={() => handleAreaToggle(section.id)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2 text-sm">
{section.name} ({section.area ? `${Math.round(section.area)} sq ft` : 'No size'})
</span>
</label>
))}
</div>
{planData.selectedAreas.length > 0 && (
<p className="text-sm text-gray-600 mt-2">
Total area: {selectedPropertyDetails.sections
.filter(s => planData.selectedAreas.includes(s.id))
.reduce((total, s) => total + (s.area || 0), 0).toFixed(0)} sq ft
</p>
)}
</div>
) : (
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-yellow-800 text-sm">
This property has no lawn sections defined. Please add lawn sections to the property first.
</p>
</div>
)}
</div> </div>
)} )}