177 lines
5.8 KiB
TypeScript
177 lines
5.8 KiB
TypeScript
import React from 'react';
|
||
import { Scissors, Edit, Trash2, Calendar, Camera, Square, Clock, AlertTriangle } from './Icons';
|
||
import { Zone } from '../types/zone';
|
||
|
||
interface ZoneCardProps {
|
||
zone: Zone;
|
||
onMarkAsMowed: (zone: Zone) => void;
|
||
onEdit: (zone: Zone) => void;
|
||
onDelete: (id: number) => void;
|
||
}
|
||
|
||
const ZoneCard: React.FC<ZoneCardProps> = ({
|
||
zone,
|
||
onMarkAsMowed,
|
||
onEdit,
|
||
onDelete,
|
||
}) => {
|
||
const getStatusColor = (status: string) => {
|
||
switch (status) {
|
||
case 'overdue':
|
||
return 'border-red-500 bg-red-50';
|
||
case 'due':
|
||
return 'border-orange-500 bg-orange-50';
|
||
case 'new':
|
||
return 'border-blue-500 bg-blue-50';
|
||
default:
|
||
return 'border-green-500 bg-green-50';
|
||
}
|
||
};
|
||
|
||
const getStatusText = (zone: Zone) => {
|
||
if (zone.isNew) {
|
||
return 'Еще не косилась';
|
||
} else if (zone.isOverdue) {
|
||
return `${Math.abs(zone.daysUntilNext!)} дней посроченно`;
|
||
} else if (zone.isDueToday) {
|
||
return 'Срок - сегодня';
|
||
} else {
|
||
return `${zone.daysUntilNext} дней осталось`;
|
||
}
|
||
};
|
||
|
||
const getStatusTextColor = (status: string) => {
|
||
switch (status) {
|
||
case 'overdue':
|
||
return 'text-red-700';
|
||
case 'due':
|
||
return 'text-orange-700';
|
||
case 'new':
|
||
return 'text-blue-700';
|
||
default:
|
||
return 'text-green-700';
|
||
}
|
||
};
|
||
|
||
const getStatusIcon = (status: string) => {
|
||
switch (status) {
|
||
case 'overdue':
|
||
return <AlertTriangle className="h-4 w-4 text-red-600" />;
|
||
case 'due':
|
||
return <Clock className="h-4 w-4 text-orange-600" />;
|
||
case 'new':
|
||
return <Calendar className="h-4 w-4 text-blue-600" />;
|
||
default:
|
||
return <Scissors className="h-4 w-4 text-green-600" />;
|
||
}
|
||
};
|
||
|
||
const formatDate = (dateString: string) => {
|
||
if (!dateString) return 'Никогда';
|
||
return new Date(dateString).toLocaleDateString('ru-RU', {
|
||
month: 'long',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
});
|
||
};
|
||
|
||
const formatArea = (area: number) => {
|
||
if (area === 0) return 'Not specified';
|
||
return `${area.toLocaleString()} м2`;
|
||
};
|
||
|
||
const getScheduleText = (zone: Zone) => {
|
||
if (zone.scheduleType === 'specific') {
|
||
if (zone.nextMowDate) {
|
||
return `Next: ${formatDate(zone.nextMowDate)}`;
|
||
}
|
||
return 'Specific date scheduling';
|
||
} else {
|
||
return zone.intervalDays ? `Каждые ${zone.intervalDays} дней` : 'Не запланированно';
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className={`bg-white rounded-lg shadow-md border-l-4 ${getStatusColor(zone.status)} hover:shadow-lg transition-shadow duration-200`}>
|
||
{/* Zone Image */}
|
||
{zone.imagePath ? (
|
||
<div className="relative h-48 overflow-hidden rounded-t-lg">
|
||
<img
|
||
src={`http://localhost:3001${zone.imagePath}`}
|
||
alt={zone.name}
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
<div className="absolute inset-0 bg-black bg-opacity-20"></div>
|
||
</div>
|
||
) : (
|
||
<div className="h-48 bg-gray-100 rounded-t-lg flex items-center justify-center">
|
||
<div className="text-center text-gray-400">
|
||
<Camera className="h-12 w-12 mx-auto mb-2" />
|
||
<p className="text-sm">Нет изображения</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="p-6">
|
||
{/* Zone Name and Status */}
|
||
<div className="mb-4">
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-2">{zone.name}</h3>
|
||
<div className={`flex items-center gap-2 text-sm font-medium ${getStatusTextColor(zone.status)}`}>
|
||
{getStatusIcon(zone.status)}
|
||
<span>{getStatusText(zone)}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Zone Details */}
|
||
<div className="space-y-2 mb-4 text-sm text-gray-600">
|
||
<div className="flex items-center gap-2">
|
||
<Calendar className="h-4 w-4" />
|
||
<span>Была скошена: {zone.lastMowedDate ? formatDate(zone.lastMowedDate) : '—'}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Clock className="h-4 w-4" />
|
||
<span>{getScheduleText(zone)}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Square className="h-4 w-4" />
|
||
<span>Площадь: {formatArea(zone.area)}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Action Buttons */}
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={() => onMarkAsMowed(zone)}
|
||
disabled={zone.status === 'ok' && zone.daysUntilNext! > 1}
|
||
className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-colors duration-200 ${
|
||
zone.status === 'ok' && zone.daysUntilNext! > 1
|
||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||
: zone.isNew
|
||
? 'bg-blue-600 hover:bg-blue-700 text-white shadow-sm hover:shadow-md'
|
||
: 'bg-green-600 hover:bg-green-700 text-white shadow-sm hover:shadow-md'
|
||
}`}
|
||
>
|
||
<Scissors className="h-4 w-4 inline mr-1" />
|
||
{zone.isNew ? 'Первый покос' : 'Скошенно'}
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => onEdit(zone)}
|
||
className="p-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-md transition-colors duration-200"
|
||
>
|
||
<Edit className="h-4 w-4" />
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => onDelete(zone.id)}
|
||
className="p-2 text-gray-600 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors duration-200"
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ZoneCard; |