frontend refactor
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { memo } from 'react';
|
||||
import { MapPin, Phone, Package, Store, Calendar, MessageSquare, CheckCircle2, Circle, CheckSquare } from 'lucide-react';
|
||||
import type { Delivery } from '../../types';
|
||||
import { pickupLocationLabels } from '../../types';
|
||||
@@ -11,7 +12,7 @@ interface DeliveryCardProps {
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export const DeliveryCard = ({ delivery, onStatusChange, onEdit, onDelete }: DeliveryCardProps) => {
|
||||
export const DeliveryCard = memo(({ delivery, onStatusChange, onEdit, onDelete }: DeliveryCardProps) => {
|
||||
const handleAddressClick = () => {
|
||||
const encodedAddress = encodeURIComponent(delivery.address);
|
||||
window.open(`https://maps.google.com/?q=${encodedAddress}`, '_blank');
|
||||
@@ -132,4 +133,6 @@ export const DeliveryCard = ({ delivery, onStatusChange, onEdit, onDelete }: Del
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
DeliveryCard.displayName = 'DeliveryCard';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Button, Input, Select, Modal } from '../ui';
|
||||
import { pickupOptions } from '../../constants/pickup';
|
||||
import { formatDateForInput, parseDateFromInput, getTodayFrontend } from '../../utils/date';
|
||||
import type { Delivery, PickupLocation, DeliveryStatus } from '../../types';
|
||||
import { pickupLocationLabels } from '../../types';
|
||||
|
||||
interface DeliveryFormProps {
|
||||
isOpen: boolean;
|
||||
@@ -12,16 +13,12 @@ interface DeliveryFormProps {
|
||||
isSubmitting?: boolean;
|
||||
}
|
||||
|
||||
const pickupOptions: { value: PickupLocation; label: string }[] = [
|
||||
{ value: 'warehouse', label: pickupLocationLabels.warehouse },
|
||||
{ value: 'symbat', label: pickupLocationLabels.symbat },
|
||||
{ value: 'nursaya', label: pickupLocationLabels.nursaya },
|
||||
{ value: 'galaktika', label: pickupLocationLabels.galaktika },
|
||||
];
|
||||
// Phone validation regex for Kazakhstan numbers
|
||||
const PHONE_REGEX = /^\+7\s?\(?\d{3}\)?\s?\d{3}[\s-]?\d{2}[\s-]?\d{2}$/;
|
||||
|
||||
export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDate, isSubmitting }: DeliveryFormProps) => {
|
||||
const [formData, setFormData] = useState({
|
||||
date: defaultDate || new Date().toLocaleDateString('ru-RU').split('.').join('-'),
|
||||
date: defaultDate || getTodayFrontend(),
|
||||
pickupLocation: 'warehouse' as PickupLocation,
|
||||
productName: '',
|
||||
address: '',
|
||||
@@ -50,34 +47,39 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa
|
||||
}
|
||||
}, [initialData, defaultDate, isOpen]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
const validatePhone = useCallback((phone: string): boolean => {
|
||||
if (!phone) return false;
|
||||
return PHONE_REGEX.test(phone);
|
||||
}, []);
|
||||
|
||||
const isPhoneValid = !formData.phone || validatePhone(formData.phone);
|
||||
const isAdditionalPhoneValid = !formData.additionalPhone || validatePhone(formData.additionalPhone);
|
||||
const isFormValid = formData.productName && formData.address && formData.phone && isPhoneValid;
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit(formData);
|
||||
if (!initialData) {
|
||||
setFormData({
|
||||
date: defaultDate || new Date().toLocaleDateString('ru-RU').split('.').join('-'),
|
||||
pickupLocation: 'warehouse',
|
||||
productName: '',
|
||||
address: '',
|
||||
phone: '',
|
||||
additionalPhone: '',
|
||||
hasElevator: false,
|
||||
comment: '',
|
||||
status: 'new',
|
||||
});
|
||||
if (!isFormValid) return;
|
||||
try {
|
||||
await onSubmit(formData);
|
||||
if (!initialData) {
|
||||
setFormData({
|
||||
date: defaultDate || getTodayFrontend(),
|
||||
pickupLocation: 'warehouse',
|
||||
productName: '',
|
||||
address: '',
|
||||
phone: '',
|
||||
additionalPhone: '',
|
||||
hasElevator: false,
|
||||
comment: '',
|
||||
status: 'new',
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
} catch {
|
||||
// Error is handled by parent, keep form open
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const formatDateForInput = (dateStr: string) => {
|
||||
const [day, month, year] = dateStr.split('-');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
const formatDateFromInput = (dateStr: string) => {
|
||||
const [year, month, day] = dateStr.split('-');
|
||||
return `${day}-${month}-${year}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -89,7 +91,7 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa
|
||||
<Button variant="ghost" onClick={onClose} disabled={isSubmitting}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button type="submit" form="delivery-form" disabled={isSubmitting}>
|
||||
<Button type="submit" form="delivery-form" disabled={isSubmitting || !isFormValid}>
|
||||
{isSubmitting ? 'Сохранение...' : initialData ? 'Сохранить' : 'Создать'}
|
||||
</Button>
|
||||
</>
|
||||
@@ -103,7 +105,7 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa
|
||||
<input
|
||||
type="date"
|
||||
value={formatDateForInput(formData.date)}
|
||||
onChange={(e) => setFormData({ ...formData, date: formatDateFromInput(e.target.value) })}
|
||||
onChange={(e) => setFormData({ ...formData, date: parseDateFromInput(e.target.value) })}
|
||||
className="w-full px-3 py-2 bg-[#f5f3f5] border border-[#c5c6cd] rounded-md text-[#1b1b1d] focus:outline-none focus:ring-2 focus:ring-[#1B263B] focus:border-transparent transition-colors"
|
||||
required
|
||||
/>
|
||||
@@ -144,7 +146,14 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa
|
||||
}}
|
||||
placeholder="+7 (776)-567-89-01"
|
||||
required
|
||||
aria-invalid={!isPhoneValid}
|
||||
aria-describedby={!isPhoneValid ? 'phone-error' : undefined}
|
||||
/>
|
||||
{!isPhoneValid && formData.phone && (
|
||||
<p id="phone-error" className="text-sm text-red-500 mt-1">
|
||||
Введите корректный номер: +7 (XXX) XXX-XX-XX
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Input
|
||||
label="Дополнительный номер телефона"
|
||||
@@ -157,7 +166,14 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa
|
||||
}
|
||||
}}
|
||||
placeholder="+7 (776)-567-89-01"
|
||||
aria-invalid={!isAdditionalPhoneValid}
|
||||
aria-describedby={!isAdditionalPhoneValid ? 'additional-phone-error' : undefined}
|
||||
/>
|
||||
{!isAdditionalPhoneValid && formData.additionalPhone && (
|
||||
<p id="additional-phone-error" className="text-sm text-red-500 mt-1">
|
||||
Введите корректный номер: +7 (XXX) XXX-XX-XX
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Plus, LayoutGrid, Table as TableIcon } from 'lucide-react';
|
||||
import { DeliveryCard } from './DeliveryCard';
|
||||
import { DeliveryRow } from './DeliveryRow';
|
||||
@@ -17,8 +17,8 @@ interface DeliveryListProps {
|
||||
export const DeliveryList = ({ deliveries, onStatusChange, onEdit, onDelete, onAdd, date }: DeliveryListProps) => {
|
||||
const [viewMode, setViewMode] = useState<'kanban' | 'table'>('kanban');
|
||||
|
||||
const newDeliveries = deliveries.filter(d => d.status === 'new');
|
||||
const deliveredDeliveries = deliveries.filter(d => d.status === 'delivered');
|
||||
const newDeliveries = useMemo(() => deliveries.filter(d => d.status === 'new'), [deliveries]);
|
||||
const deliveredDeliveries = useMemo(() => deliveries.filter(d => d.status === 'delivered'), [deliveries]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { memo } from 'react';
|
||||
import { MapPin, Phone } from 'lucide-react';
|
||||
import type { Delivery } from '../../types';
|
||||
import { pickupLocationLabels } from '../../types';
|
||||
@@ -10,7 +11,7 @@ interface DeliveryRowProps {
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export const DeliveryRow = ({ delivery, onStatusChange, onEdit, onDelete }: DeliveryRowProps) => {
|
||||
export const DeliveryRow = memo(({ delivery, onStatusChange, onEdit, onDelete }: DeliveryRowProps) => {
|
||||
const handleAddressClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const encodedAddress = encodeURIComponent(delivery.address);
|
||||
@@ -75,4 +76,6 @@ export const DeliveryRow = ({ delivery, onStatusChange, onEdit, onDelete }: Deli
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
DeliveryRow.displayName = 'DeliveryRow';
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useToastStore } from '../../stores/toastStore';
|
||||
import { X, CheckCircle, AlertCircle, Info } from 'lucide-react';
|
||||
|
||||
const icons = {
|
||||
success: CheckCircle,
|
||||
error: AlertCircle,
|
||||
info: Info,
|
||||
};
|
||||
|
||||
const styles = {
|
||||
success: 'bg-green-50 border-green-200 text-green-800',
|
||||
error: 'bg-red-50 border-red-200 text-red-800',
|
||||
info: 'bg-blue-50 border-blue-200 text-blue-800',
|
||||
};
|
||||
|
||||
export const ToastContainer = () => {
|
||||
const { toasts, removeToast } = useToastStore();
|
||||
|
||||
if (toasts.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
||||
{toasts.map((toast) => {
|
||||
const Icon = icons[toast.type];
|
||||
return (
|
||||
<div
|
||||
key={toast.id}
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg border shadow-lg min-w-[300px] animate-in slide-in-from-right ${styles[toast.type]}`}
|
||||
role="alert"
|
||||
>
|
||||
<Icon size={20} />
|
||||
<p className="flex-1 text-sm">{toast.message}</p>
|
||||
<button
|
||||
onClick={() => removeToast(toast.id)}
|
||||
className="p-1 hover:bg-black/5 rounded transition-colors"
|
||||
aria-label="Закрыть"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user