updates
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user