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,
onSectionDelete,
onPropertyUpdate,
onSectionClick,
selectedSections = [],
editable = false,
className = "h-96 w-full"
}) => {
@@ -136,7 +138,13 @@ const PropertyMap = ({
// Handle section click
const handleSectionClick = (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
@@ -350,18 +358,20 @@ const PropertyMap = ({
if (!section.polygonData?.coordinates?.[0]) return null;
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 (
<Polygon
key={section.id}
positions={coordinates}
pathOptions={{
fillColor: getSectionColor(section),
fillOpacity: isSelected ? 0.6 : 0.4,
color: getSectionColor(section),
weight: isSelected ? 3 : 2,
opacity: isSelected ? 1 : 0.8,
fillColor: isExternallySelected ? '#10b981' : getSectionColor(section),
fillOpacity: isSelected ? 0.7 : 0.4,
color: isExternallySelected ? '#059669' : getSectionColor(section),
weight: isSelected ? 4 : 2,
opacity: 1,
}}
eventHandlers={{
click: () => handleSectionClick(section)

View File

@@ -6,8 +6,9 @@ import {
WrenchScrewdriverIcon,
CalculatorIcon
} 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 PropertyMap from '../../components/Maps/PropertyMap';
import toast from 'react-hot-toast';
const Applications = () => {
@@ -22,8 +23,8 @@ const Applications = () => {
const fetchApplications = async () => {
try {
setLoading(true);
// TODO: Implement applications API endpoint
setApplications([]);
const response = await applicationsAPI.getPlans();
setApplications(response.data.data.plans || []);
} catch (error) {
console.error('Failed to fetch applications:', error);
toast.error('Failed to load applications');
@@ -77,8 +78,49 @@ const Applications = () => {
<div className="space-y-4">
{applications.map((application) => (
<div key={application.id} className="card">
{/* Application card content will go here */}
<p>Application: {application.id}</p>
<div className="flex justify-between items-start">
<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>
@@ -88,10 +130,34 @@ const Applications = () => {
{showPlanForm && (
<ApplicationPlanModal
onClose={() => setShowPlanForm(false)}
onSubmit={(planData) => {
console.log('Plan submitted:', planData);
onSubmit={async (planData) => {
try {
// Create a plan for each selected area
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>
</div>
{/* Area Selection */}
{/* Area Selection with Map */}
{loadingProperty && (
<div className="flex items-center gap-2 text-gray-600">
<LoadingSpinner size="sm" />
@@ -261,9 +327,36 @@ const ApplicationPlanModal = ({ onClose, onSubmit }) => {
</div>
)}
{selectedPropertyDetails && selectedPropertyDetails.sections && selectedPropertyDetails.sections.length > 0 && (
{selectedPropertyDetails && (
<div>
<label className="label">Application Areas *</label>
{/* Property Map */}
{selectedPropertyDetails.latitude && selectedPropertyDetails.longitude && (
<div className="mb-4">
<div className="relative">
<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">
@@ -287,15 +380,15 @@ const ApplicationPlanModal = ({ onClose, onSubmit }) => {
</p>
)}
</div>
)}
{selectedPropertyDetails && (!selectedPropertyDetails.sections || selectedPropertyDetails.sections.length === 0) && (
) : (
<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>
)}
{/* Product Selection */}
<div>