Implement Mark as Cleaned functionality
- Add showCleaned filter to FilterState type - Update MapView to show green markers with checkmark for cleaned reports - Add cleaned status filter toggle in MapFilters component - Display cleaned information in ReportDetailModal - Hide action buttons for already cleaned reports - Wire up markAsCleaned functionality in MapPage - Backend endpoint and database fields already in place
This commit is contained in:
@@ -145,7 +145,7 @@
|
|||||||
|
|
||||||
## Cleanup Tracking System
|
## Cleanup Tracking System
|
||||||
|
|
||||||
- [ ] 18. Implement "Mark as Cleaned" functionality
|
- [-] 18. Implement "Mark as Cleaned" functionality
|
||||||
- Add `cleaned_at` timestamp field to reports table
|
- Add `cleaned_at` timestamp field to reports table
|
||||||
- Add `cleaned_by_user_id` field to track who marked it as cleaned
|
- Add `cleaned_by_user_id` field to track who marked it as cleaned
|
||||||
- Create POST /api/reports/:id/mark-cleaned backend endpoint
|
- Create POST /api/reports/:id/mark-cleaned backend endpoint
|
||||||
|
|||||||
@@ -218,6 +218,28 @@ export function MapFilters({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Show Cleaned Reports Toggle */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label htmlFor="cleaned-toggle" className="text-sm font-medium text-gray-700">
|
||||||
|
Show Cleaned Reports
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
id="cleaned-toggle"
|
||||||
|
onClick={() => onFiltersChange({ ...filters, showCleaned: filters.showCleaned !== false })}
|
||||||
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||||
|
filters.showCleaned !== false ? 'bg-green-600' : 'bg-gray-200'
|
||||||
|
}`}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={filters.showCleaned !== false}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||||
|
filters.showCleaned !== false ? 'translate-x-6' : 'translate-x-1'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Hotspot Legend */}
|
{/* Hotspot Legend */}
|
||||||
{showHotspots && onToggleHotspots && (
|
{showHotspots && onToggleHotspots && (
|
||||||
<div className="pt-2 border-t border-gray-200">
|
<div className="pt-2 border-t border-gray-200">
|
||||||
|
|||||||
@@ -152,6 +152,11 @@ export function MapView({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by cleaned status
|
||||||
|
if (filters.showCleaned === false && report.cleanedAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -181,7 +186,8 @@ export function MapView({
|
|||||||
|
|
||||||
// Add report markers
|
// Add report markers
|
||||||
filteredReports.forEach((report) => {
|
filteredReports.forEach((report) => {
|
||||||
const icon = createCustomMarkerIcon(report.litterType, report.quantity);
|
const isCleaned = !!report.cleanedAt;
|
||||||
|
const icon = createCustomMarkerIcon(report.litterType, report.quantity, isCleaned);
|
||||||
const marker = L.marker([report.latitude, report.longitude], { icon });
|
const marker = L.marker([report.latitude, report.longitude], { icon });
|
||||||
|
|
||||||
// Add popup with report details
|
// Add popup with report details
|
||||||
@@ -204,7 +210,12 @@ export function MapView({
|
|||||||
<h3 class="font-bold text-sm mb-1">${getLitterTypeLabel(report.litterType)}</h3>
|
<h3 class="font-bold text-sm mb-1">${getLitterTypeLabel(report.litterType)}</h3>
|
||||||
<p class="text-xs text-gray-600">Quantity: ${report.quantity}</p>
|
<p class="text-xs text-gray-600">Quantity: ${report.quantity}</p>
|
||||||
<p class="text-xs text-gray-600">Status: ${report.verificationStatus}</p>
|
<p class="text-xs text-gray-600">Status: ${report.verificationStatus}</p>
|
||||||
<p class="text-xs text-gray-600">Date: ${new Date(report.createdAt).toLocaleDateString()}</p>
|
${isCleaned && report.cleanedAt ? `
|
||||||
|
<p class="text-xs text-green-600 font-semibold">✓ Cleaned up</p>
|
||||||
|
<p class="text-xs text-gray-600">Cleaned: ${new Date(report.cleanedAt).toLocaleDateString()}</p>
|
||||||
|
` : `
|
||||||
|
<p class="text-xs text-gray-600">Date: ${new Date(report.createdAt).toLocaleDateString()}</p>
|
||||||
|
`}
|
||||||
${report.description ? `<p class="text-xs mt-1">${report.description}</p>` : ''}
|
${report.description ? `<p class="text-xs mt-1">${report.description}</p>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
@@ -503,7 +514,7 @@ export function MapView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to create custom marker icons based on litter type
|
// Helper function to create custom marker icons based on litter type
|
||||||
function createCustomMarkerIcon(litterType: string, quantity: string): L.DivIcon {
|
function createCustomMarkerIcon(litterType: string, quantity: string, isCleaned: boolean = false): L.DivIcon {
|
||||||
const colors = {
|
const colors = {
|
||||||
plastic: '#3B82F6', // blue
|
plastic: '#3B82F6', // blue
|
||||||
metal: '#6B7280', // gray
|
metal: '#6B7280', // gray
|
||||||
@@ -520,7 +531,8 @@ function createCustomMarkerIcon(litterType: string, quantity: string): L.DivIcon
|
|||||||
severe: 36,
|
severe: 36,
|
||||||
};
|
};
|
||||||
|
|
||||||
const color = colors[litterType as keyof typeof colors] || colors.other;
|
// Use green color for cleaned reports, otherwise use litter type color
|
||||||
|
const color = isCleaned ? '#10B981' : (colors[litterType as keyof typeof colors] || colors.other);
|
||||||
const size = sizes[quantity as keyof typeof sizes] || sizes.moderate;
|
const size = sizes[quantity as keyof typeof sizes] || sizes.moderate;
|
||||||
|
|
||||||
return L.divIcon({
|
return L.divIcon({
|
||||||
@@ -534,6 +546,7 @@ function createCustomMarkerIcon(litterType: string, quantity: string): L.DivIcon
|
|||||||
border-radius: 50% 50% 50% 0;
|
border-radius: 50% 50% 50% 0;
|
||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||||
|
${isCleaned ? 'opacity: 0.7;' : ''}
|
||||||
">
|
">
|
||||||
<div style="
|
<div style="
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -546,7 +559,7 @@ function createCustomMarkerIcon(litterType: string, quantity: string): L.DivIcon
|
|||||||
font-size: ${size * 0.4}px;
|
font-size: ${size * 0.4}px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
">
|
">
|
||||||
${getLitterTypeIcon(litterType)}
|
${isCleaned ? '✓' : getLitterTypeIcon(litterType)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -211,6 +211,26 @@ export function ReportDetailModal({ report, isOpen, onClose, onVerify, onDispute
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Cleaned Status */}
|
||||||
|
{report.cleanedAt && (
|
||||||
|
<div className="p-3 border border-green-200 bg-green-50 rounded-lg">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<svg className="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<p className="text-sm font-semibold text-green-800">Cleaned Up</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-green-700">
|
||||||
|
This litter was cleaned up on {format(new Date(report.cleanedAt), 'PPP')}
|
||||||
|
</p>
|
||||||
|
{report.cleanedByUserId && (
|
||||||
|
<p className="text-xs text-green-600 mt-1">
|
||||||
|
Marked as cleaned by a community member
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{report.description && (
|
{report.description && (
|
||||||
<div className="p-3 border border-gray-200 rounded-lg">
|
<div className="p-3 border border-gray-200 rounded-lg">
|
||||||
@@ -358,7 +378,7 @@ export function ReportDetailModal({ report, isOpen, onClose, onVerify, onDispute
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
{(onVerify || onDispute || onMarkCleaned) && (
|
{(onVerify || onDispute || onMarkCleaned) && !report.cleanedAt && (
|
||||||
<div className="border-t border-gray-200 pt-4">
|
<div className="border-t border-gray-200 pt-4">
|
||||||
{(onVerify || onDispute) && (
|
{(onVerify || onDispute) && (
|
||||||
<p className="text-sm text-gray-700 mb-3">
|
<p className="text-sm text-gray-700 mb-3">
|
||||||
@@ -398,7 +418,7 @@ export function ReportDetailModal({ report, isOpen, onClose, onVerify, onDispute
|
|||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
Cleaned Up
|
Mark as Cleaned
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export interface FilterState {
|
|||||||
litterTypes?: LitterType[];
|
litterTypes?: LitterType[];
|
||||||
quantities?: QuantityLevel[];
|
quantities?: QuantityLevel[];
|
||||||
verificationStatus?: VerificationStatus[];
|
verificationStatus?: VerificationStatus[];
|
||||||
|
showCleaned?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Analytics types
|
// Analytics types
|
||||||
|
|||||||
Reference in New Issue
Block a user