updates
This commit is contained in:
@@ -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) => {
|
||||
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
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
setShowPlanForm(false);
|
||||
fetchApplications();
|
||||
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,39 +327,66 @@ const ApplicationPlanModal = ({ onClose, onSubmit }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPropertyDetails && selectedPropertyDetails.sections && selectedPropertyDetails.sections.length > 0 && (
|
||||
{selectedPropertyDetails && (
|
||||
<div>
|
||||
<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) && (
|
||||
<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>
|
||||
{/* 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">
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user