Improve delivery workflows and add versioned guided tours

This commit is contained in:
2026-10-08 01:35:24 +06:00
parent 1860e52b88
commit 55b5ca49e2
25 changed files with 933 additions and 1431 deletions
@@ -1,5 +1,5 @@
import { memo } from 'react';
import { MapPin, Phone } from 'lucide-react';
import { MapPin, Pencil, Trash2 } from 'lucide-react';
import type { Delivery } from '../../types';
import { formatPickupLocation } from '../../types';
import { StatusBadge } from './StatusBadge';
@@ -7,108 +7,34 @@ import { PaymentBadge } from './PaymentBadge';
import { WhatsAppIcon } from '../ui/WhatsAppIcon';
import { getWhatsAppUrl } from '../../utils/phone';
const CITY = 'kokshetau';
interface DeliveryRowProps {
delivery: Delivery;
onStatusChange: (id: string) => void;
onEdit: (delivery: Delivery) => void;
onDelete: (id: string) => void;
pending?: boolean;
}
export const DeliveryRow = memo(({ delivery, onStatusChange, onEdit, onDelete }: DeliveryRowProps) => {
const handleAddressClick = (e: React.MouseEvent) => {
e.stopPropagation();
const encodedAddress = encodeURIComponent(delivery.address);
window.open(`https://2gis.kz/${CITY}/search/${encodedAddress}`, '_blank');
};
const handlePhoneClick = (e: React.MouseEvent) => {
e.stopPropagation();
window.location.href = `tel:${delivery.phone}`;
};
const handleWhatsAppClick = (e: React.MouseEvent) => {
e.stopPropagation();
window.location.href = getWhatsAppUrl(delivery.phone);
};
return (
<tr className="hover:bg-[#f5f3f5] transition-colors border-b border-[#e4e2e4] last:border-b-0">
<td className="px-4 py-3">
<StatusBadge
status={delivery.status}
onClick={() => onStatusChange(delivery.id)}
size="sm"
/>
</td>
<td className="px-4 py-3 text-sm text-[#1b1b1d]">{delivery.date}</td>
<td className="px-4 py-3 text-sm text-[#1b1b1d]">
{delivery.pickupLocation2
? `${formatPickupLocation(delivery.pickupLocation, delivery.warehouseRequestSource)} + ${formatPickupLocation(delivery.pickupLocation2, delivery.warehouseRequestSource2)}`
: formatPickupLocation(delivery.pickupLocation, delivery.warehouseRequestSource)}
</td>
<td className="px-4 py-3 text-sm text-[#1b1b1d]">
{delivery.productName}
{delivery.productName2 && <span className="block text-xs text-[#75777d]">+ {delivery.productName2}</span>}
</td>
<td className="px-4 py-3 text-sm text-[#1b1b1d]">{delivery.customerName}</td>
<td className="px-4 py-3">
<button
onClick={handleAddressClick}
className="flex items-center gap-1.5 text-sm text-[#1B263B] hover:text-[#F28C28] transition-colors text-left"
>
<MapPin size={14} />
<span className="max-w-[200px] truncate">
ул. {delivery.street}, д. {delivery.house}{delivery.apartment ? `, кв. ${delivery.apartment}` : ''}
</span>
</button>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<button
onClick={handlePhoneClick}
className="flex items-center gap-1.5 text-sm text-[#16a34a] hover:underline transition-colors"
>
<Phone size={14} />
{delivery.phone}
</button>
<button
onClick={handleWhatsAppClick}
className="p-1.5 rounded-md text-[#16a34a] hover:bg-[#f5f3f5] transition-colors"
title="Открыть WhatsApp"
aria-label={`Открыть WhatsApp для номера ${delivery.phone}`}
>
<WhatsAppIcon className="h-5 w-5" />
</button>
</div>
</td>
<td className="px-4 py-3 text-sm text-[#45474d] max-w-[200px] truncate">
<PaymentBadge status={delivery.paymentStatus} size="sm" />
</td>
<td className="px-4 py-3 text-sm text-[#45474d] max-w-[200px] truncate">
{delivery.comment || '-'}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1">
<button
onClick={() => onEdit(delivery)}
className="p-1.5 rounded-md hover:bg-[#e4e2e4] text-[#75777d] transition-colors"
title="Редактировать"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>
</button>
<button
onClick={() => onDelete(delivery.id)}
className="p-1.5 rounded-md hover:bg-red-50 text-red-500 transition-colors"
title="Удалить"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
</button>
</div>
</td>
</tr>
);
});
export const DeliveryRow = memo(({ delivery: d, onStatusChange, onEdit, onDelete, pending }: DeliveryRowProps) => (
<tr data-tour="delivery-card" aria-busy={pending} className="align-top transition-colors hover:bg-[#f5f3f5]">
<td className="px-3 py-3"><StatusBadge status={d.status} onClick={() => onStatusChange(d.id)} disabled={pending} size="sm" /></td>
<td className="min-w-[140px] max-w-[220px] px-3 py-3 text-sm">
<p className="break-words font-medium">{d.productName}</p><p className="mt-1 text-[#666870]">{formatPickupLocation(d.pickupLocation, d.warehouseRequestSource)}</p>
{d.pickupLocation2 && <div className="mt-2"><p className="break-words font-medium">{d.productName2 || 'Вторая загрузка'}</p><p className="mt-1 text-[#666870]">{formatPickupLocation(d.pickupLocation2, d.warehouseRequestSource2)}</p></div>}
</td>
<td className="min-w-[190px] px-3 py-3 text-sm"><p className="font-medium">{d.customerName}</p>
<div className="flex items-center gap-1"><a href={`tel:${d.phone}`} className="flex min-h-11 items-center whitespace-nowrap text-[#166534] hover:underline">{d.phone}</a><a href={getWhatsAppUrl(d.phone)} className="icon-button text-[#166534]" aria-label={`WhatsApp: ${d.phone}`}><WhatsAppIcon className="h-5 w-5" /></a></div>
{d.additionalPhone && <div className="flex items-center gap-1"><a href={`tel:${d.additionalPhone}`} className="flex min-h-11 items-center whitespace-nowrap text-[#666870]">{d.additionalPhone}</a><a href={getWhatsAppUrl(d.additionalPhone)} className="icon-button" aria-label={`WhatsApp: ${d.additionalPhone}`}><WhatsAppIcon className="h-5 w-5" /></a></div>}
</td>
<td className="min-w-[160px] max-w-[220px] px-3 py-3 text-sm">
<a href={`https://2gis.kz/kokshetau/search/${encodeURIComponent(d.address)}`} target="_blank" rel="noopener noreferrer" className="flex min-h-11 items-start gap-1.5 text-[#1B263B] underline-offset-4 hover:underline"><MapPin size={16} className="mt-0.5 shrink-0" /><span className="break-words">ул. {d.street}, д. {d.house}{d.apartment ? `, кв. ${d.apartment}` : ''}</span></a>
<p className="mt-1 text-[#666870]">{[d.entrance && `Подъезд ${d.entrance}`, d.floor && `этаж ${d.floor}`, d.hasElevator ? 'есть лифт' : 'нет лифта'].filter(Boolean).join(' · ')}</p>
</td>
<td className="px-3 py-3"><PaymentBadge status={d.paymentStatus} size="sm" /></td>
<td className="min-w-[130px] max-w-[210px] break-words px-3 py-3 text-sm text-[#45474d]">{d.serviceInfo && <p className="mb-2 font-medium">{d.serviceInfo}</p>}{d.comment || '—'}</td>
<td className="px-3 py-3"><div className="flex gap-1">
<button type="button" disabled={pending} onClick={() => onEdit(d)} className="icon-button" title="Редактировать" aria-label={`Редактировать доставку для ${d.customerName}`}><Pencil size={18} /></button>
<button type="button" disabled={pending} onClick={() => onDelete(d.id)} className="icon-button text-red-700" title="Удалить" aria-label={`Удалить доставку для ${d.customerName}`}><Trash2 size={18} /></button>
</div></td>
</tr>
));
DeliveryRow.displayName = 'DeliveryRow';