diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0ab1438..9929016 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,10 +1,20 @@ -import { useState, useEffect } from 'react'; -import { Truck } from 'lucide-react'; -import { Dashboard } from './pages/Dashboard'; -import { DeliveryListPage } from './pages/DeliveryListPage'; +import { useState, useEffect, lazy, Suspense } from 'react'; +import { Truck, Loader2 } from 'lucide-react'; import { DeliveryForm } from './components/delivery/DeliveryForm'; +import { ToastContainer } from './components/ui/Toast'; import { useDeliveryStore } from './stores/deliveryStore'; +// Lazy load pages for code splitting +const Dashboard = lazy(() => import('./pages/Dashboard')); +const DeliveryListPage = lazy(() => import('./pages/DeliveryListPage')); + +// Fallback loading component +const PageLoader = () => ( +
+ +
+); + function App() { const [view, setView] = useState<'dashboard' | 'delivery-list'>('dashboard'); const [selectedDate, setSelectedDate] = useState(''); @@ -76,17 +86,19 @@ function App() {
- {view === 'dashboard' ? ( - - ) : ( - - )} + }> + {view === 'dashboard' ? ( + + ) : ( + + )} +
+ + ); } diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 6cfc25f..06f6242 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,5 +1,10 @@ const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080'; +// Request deduplication cache +const pendingRequests = new Map>(); +// Abort controllers for cancelling requests +const abortControllers = new Map(); + export class ApiError extends Error { status: number; details?: unknown; @@ -12,34 +17,72 @@ export class ApiError extends Error { } } +function getRequestKey(endpoint: string, method: string, body?: unknown): string { + return `${method}:${endpoint}:${body ? JSON.stringify(body) : ''}`; +} + async function fetchApi( endpoint: string, - options?: RequestInit + options?: RequestInit & { deduplicate?: boolean } ): Promise { const url = `${API_BASE_URL}${endpoint}`; + const method = options?.method || 'GET'; + const requestKey = getRequestKey(endpoint, method, options?.body); - const response = await fetch(url, { - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers, - }, - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => null); - throw new ApiError( - errorData?.error || `HTTP ${response.status}`, - response.status, - errorData?.details - ); + // Cancel previous request with same key (for non-GET requests or explicit override) + const shouldCancelPrevious = method !== 'GET' || options?.deduplicate === false; + if (shouldCancelPrevious && abortControllers.has(requestKey)) { + abortControllers.get(requestKey)?.abort(); } - return response.json(); + // Deduplicate GET requests + if (method === 'GET' && options?.deduplicate !== false) { + if (pendingRequests.has(requestKey)) { + return pendingRequests.get(requestKey) as Promise; + } + } + + // Create new abort controller + const controller = new AbortController(); + abortControllers.set(requestKey, controller); + + const requestPromise = (async (): Promise => { + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new ApiError( + errorData?.error || `HTTP ${response.status}`, + response.status, + errorData?.details + ); + } + + return response.json(); + } finally { + pendingRequests.delete(requestKey); + abortControllers.delete(requestKey); + } + })(); + + if (method === 'GET' && options?.deduplicate !== false) { + pendingRequests.set(requestKey, requestPromise); + } + + return requestPromise; } export const api = { - get: (endpoint: string) => fetchApi(endpoint, { method: 'GET' }), + get: (endpoint: string, options?: { deduplicate?: boolean }) => + fetchApi(endpoint, { method: 'GET', ...options }), post: (endpoint: string, data: unknown) => fetchApi(endpoint, { @@ -56,3 +99,10 @@ export const api = { delete: (endpoint: string) => fetchApi(endpoint, { method: 'DELETE' }), }; + +// Utility to cancel all pending requests (useful on unmount) +export function cancelAllRequests(): void { + abortControllers.forEach(controller => controller.abort()); + abortControllers.clear(); + pendingRequests.clear(); +} diff --git a/frontend/src/api/deliveries.ts b/frontend/src/api/deliveries.ts index bbc32d5..b996956 100644 --- a/frontend/src/api/deliveries.ts +++ b/frontend/src/api/deliveries.ts @@ -1,4 +1,5 @@ import { api } from './client'; +import { backendDateToFrontend } from '../utils/date'; import type { Delivery, PickupLocation, DeliveryStatus } from '../types'; // Types matching backend responses @@ -44,18 +45,6 @@ interface UpdateDeliveryResponse { message: string; } -// Convert backend date format (YYYY-MM-DD) to frontend format (DD-MM-YYYY) -function backendDateToFrontend(dateStr: string): string { - const [year, month, day] = dateStr.split('-'); - return `${day}-${month}-${year}`; -} - -// Convert frontend date format (DD-MM-YYYY) to backend format (YYYY-MM-DD) -export function frontendDateToBackend(dateStr: string): string { - const [day, month, year] = dateStr.split('-'); - return `${year}-${month}-${day}`; -} - // Map backend delivery to frontend delivery function mapBackendToFrontend(backend: BackendDelivery): Delivery { return { diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 6c887d1..6f4b478 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,2 +1,3 @@ -export { api, ApiError } from './client'; -export { deliveriesApi, frontendDateToBackend } from './deliveries'; +export { api, ApiError, cancelAllRequests } from './client'; +export { deliveriesApi } from './deliveries'; +export { frontendDateToBackend } from '../utils/date'; diff --git a/frontend/src/components/delivery/DeliveryCard.tsx b/frontend/src/components/delivery/DeliveryCard.tsx index 4783dd4..d772fbb 100644 --- a/frontend/src/components/delivery/DeliveryCard.tsx +++ b/frontend/src/components/delivery/DeliveryCard.tsx @@ -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 ); -}; +}); + +DeliveryCard.displayName = 'DeliveryCard'; diff --git a/frontend/src/components/delivery/DeliveryForm.tsx b/frontend/src/components/delivery/DeliveryForm.tsx index 2c97d9c..b9ad7c2 100644 --- a/frontend/src/components/delivery/DeliveryForm.tsx +++ b/frontend/src/components/delivery/DeliveryForm.tsx @@ -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 ( Отмена - @@ -103,7 +105,7 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa 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 && ( +

+ Введите корректный номер: +7 (XXX) XXX-XX-XX +

+ )} + {!isAdditionalPhoneValid && formData.additionalPhone && ( +

+ Введите корректный номер: +7 (XXX) XXX-XX-XX +

+ )}
{ 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 (
diff --git a/frontend/src/components/delivery/DeliveryRow.tsx b/frontend/src/components/delivery/DeliveryRow.tsx index 263920c..3d38ceb 100644 --- a/frontend/src/components/delivery/DeliveryRow.tsx +++ b/frontend/src/components/delivery/DeliveryRow.tsx @@ -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 ); -}; +}); + +DeliveryRow.displayName = 'DeliveryRow'; diff --git a/frontend/src/components/ui/Toast.tsx b/frontend/src/components/ui/Toast.tsx new file mode 100644 index 0000000..9771a1e --- /dev/null +++ b/frontend/src/components/ui/Toast.tsx @@ -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 ( +
+ {toasts.map((toast) => { + const Icon = icons[toast.type]; + return ( +
+ +

{toast.message}

+ +
+ ); + })} +
+ ); +}; diff --git a/frontend/src/constants/pickup.ts b/frontend/src/constants/pickup.ts new file mode 100644 index 0000000..2226b12 --- /dev/null +++ b/frontend/src/constants/pickup.ts @@ -0,0 +1,14 @@ +import type { PickupLocation } from '../types'; +import { pickupLocationLabels } from '../types'; + +export 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 }, +]; + +export const pickupFilterOptions: { value: PickupLocation | 'all'; label: string }[] = [ + { value: 'all', label: 'Все места загрузки' }, + ...pickupOptions, +]; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 49af7b7..c6de4d2 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,9 +1,10 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { Plus, Printer, ChevronRight, CalendarDays } from 'lucide-react'; import { format, startOfMonth, endOfMonth, eachDayOfInterval, isToday } from 'date-fns'; import { ru } from 'date-fns/locale'; import { useDeliveryStore } from '../stores/deliveryStore'; import type { Delivery } from '../types'; +import { pickupLocationLabels } from '../types'; import { Button } from '../components/ui/Button'; import { Card } from '../components/ui/Card'; @@ -12,7 +13,7 @@ interface DashboardProps { onAddDelivery: () => void; } -export const Dashboard = ({ onDateSelect, onAddDelivery }: DashboardProps) => { +const Dashboard = ({ onDateSelect, onAddDelivery }: DashboardProps) => { const deliveryCounts = useDeliveryStore(state => state.deliveryCounts); const fetchDeliveryCounts = useDeliveryStore(state => state.fetchDeliveryCounts); const [currentMonth, setCurrentMonth] = useState(new Date()); @@ -22,9 +23,11 @@ export const Dashboard = ({ onDateSelect, onAddDelivery }: DashboardProps) => { fetchDeliveryCounts(); }, [fetchDeliveryCounts]); - const monthStart = startOfMonth(currentMonth); - const monthEnd = endOfMonth(currentMonth); - const days = eachDayOfInterval({ start: monthStart, end: monthEnd }); + const days = useMemo(() => { + const monthStart = startOfMonth(currentMonth); + const monthEnd = endOfMonth(currentMonth); + return eachDayOfInterval({ start: monthStart, end: monthEnd }); + }, [currentMonth]); const getCountForDate = (date: Date) => { const dateStr = format(date, 'dd-MM-yyyy'); @@ -76,7 +79,7 @@ export const Dashboard = ({ onDateSelect, onAddDelivery }: DashboardProps) => { ${dayDeliveries.map((d: Delivery) => ` ${d.status === 'new' ? 'Новое' : 'Доставлено'} - ${d.pickupLocation === 'warehouse' ? 'Склад' : d.pickupLocation === 'symbat' ? 'Сымбат' : d.pickupLocation === 'nursaya' ? 'Нурсая' : 'Галактика'} + ${pickupLocationLabels[d.pickupLocation]} ${d.productName} ${d.address} ${d.phone} @@ -118,7 +121,7 @@ export const Dashboard = ({ onDateSelect, onAddDelivery }: DashboardProps) => {

- {format(currentMonth, 'MMMM yyyy', { locale: ru })} + {format(currentMonth, 'LLLL yyyy', { locale: ru })}

); }; + +export default Dashboard; diff --git a/frontend/src/pages/DeliveryListPage.tsx b/frontend/src/pages/DeliveryListPage.tsx index e076360..27e69e0 100644 --- a/frontend/src/pages/DeliveryListPage.tsx +++ b/frontend/src/pages/DeliveryListPage.tsx @@ -1,28 +1,30 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { ArrowLeft, Filter, Loader2, AlertCircle } from 'lucide-react'; import { useDeliveryStore } from '../stores/deliveryStore'; import { DeliveryList as DeliveryListComponent } from '../components/delivery/DeliveryList'; import { DeliveryForm } from '../components/delivery/DeliveryForm'; import { Button } from '../components/ui/Button'; import { Select } from '../components/ui/Select'; +import { pickupFilterOptions } from '../constants/pickup'; import type { Delivery, PickupLocation } from '../types'; -import { pickupLocationLabels } from '../types'; interface DeliveryListPageProps { selectedDate: string; onBack: () => void; } -export const DeliveryListPage = ({ selectedDate, onBack }: DeliveryListPageProps) => { - const deliveries = useDeliveryStore(state => state.deliveries); - const isLoading = useDeliveryStore(state => state.isLoading); - const error = useDeliveryStore(state => state.error); - const fetchDeliveriesByDate = useDeliveryStore(state => state.fetchDeliveriesByDate); - const toggleStatus = useDeliveryStore(state => state.toggleStatus); - const deleteDelivery = useDeliveryStore(state => state.deleteDelivery); - const updateDelivery = useDeliveryStore(state => state.updateDelivery); - const addDelivery = useDeliveryStore(state => state.addDelivery); - const clearError = useDeliveryStore(state => state.clearError); +const DeliveryListPage = ({ selectedDate, onBack }: DeliveryListPageProps) => { + const { + deliveries, + isLoading, + error, + fetchDeliveriesByDate, + toggleStatus, + deleteDelivery, + updateDelivery, + addDelivery, + clearError, + } = useDeliveryStore(); // Fetch deliveries when date changes useEffect(() => { @@ -34,18 +36,10 @@ export const DeliveryListPage = ({ selectedDate, onBack }: DeliveryListPageProps const [pickupFilter, setPickupFilter] = useState('all'); // Use all deliveries from store (already filtered by API) - const dayDeliveries = deliveries; - const filteredDeliveries = pickupFilter === 'all' - ? dayDeliveries - : dayDeliveries.filter(d => d.pickupLocation === pickupFilter); - - const pickupOptions: { value: PickupLocation | 'all'; label: string }[] = [ - { value: 'all', label: 'Все места загрузки' }, - { value: 'warehouse', label: pickupLocationLabels.warehouse }, - { value: 'symbat', label: pickupLocationLabels.symbat }, - { value: 'nursaya', label: pickupLocationLabels.nursaya }, - { value: 'galaktika', label: pickupLocationLabels.galaktika }, - ]; + const filteredDeliveries = useMemo(() => { + if (pickupFilter === 'all') return deliveries; + return deliveries.filter(d => d.pickupLocation === pickupFilter); + }, [deliveries, pickupFilter]); const handleStatusChange = async (id: string) => { const delivery = deliveries.find(d => d.id === id); @@ -110,7 +104,7 @@ export const DeliveryListPage = ({ selectedDate, onBack }: DeliveryListPageProps label="" value={pickupFilter} onChange={(e) => setPickupFilter(e.target.value as PickupLocation | 'all')} - options={pickupOptions} + options={pickupFilterOptions} />
@@ -150,3 +144,5 @@ export const DeliveryListPage = ({ selectedDate, onBack }: DeliveryListPageProps
); }; + +export default DeliveryListPage; diff --git a/frontend/src/stores/deliveryStore.ts b/frontend/src/stores/deliveryStore.ts index 70c9b01..5161fec 100644 --- a/frontend/src/stores/deliveryStore.ts +++ b/frontend/src/stores/deliveryStore.ts @@ -1,5 +1,6 @@ import { create } from 'zustand'; import { deliveriesApi } from '../api'; +import { useToastStore } from './toastStore'; import type { Delivery, DeliveryStatus } from '../types'; interface DeliveryState { @@ -40,10 +41,12 @@ export const useDeliveryStore = create()((set, get) => ({ const deliveries = await deliveriesApi.getByDate(date); set({ deliveries, isLoading: false }); } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to fetch deliveries'; set({ - error: err instanceof Error ? err.message : 'Failed to fetch deliveries', + error: message, isLoading: false, }); + useToastStore.getState().addToast(message, 'error'); } }, @@ -54,10 +57,12 @@ export const useDeliveryStore = create()((set, get) => ({ const counts = await deliveriesApi.getCounts(); set({ deliveryCounts: counts, isLoadingCounts: false }); } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to fetch counts'; set({ - error: err instanceof Error ? err.message : 'Failed to fetch counts', + error: message, isLoadingCounts: false, }); + useToastStore.getState().addToast(message, 'error'); } }, @@ -72,10 +77,12 @@ export const useDeliveryStore = create()((set, get) => ({ await get().fetchDeliveryCounts(); set({ isLoading: false }); } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to create delivery'; set({ - error: err instanceof Error ? err.message : 'Failed to create delivery', + error: message, isLoading: false, }); + useToastStore.getState().addToast(message, 'error'); throw err; } }, @@ -91,10 +98,12 @@ export const useDeliveryStore = create()((set, get) => ({ await get().fetchDeliveryCounts(); set({ isLoading: false }); } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to update delivery'; set({ - error: err instanceof Error ? err.message : 'Failed to update delivery', + error: message, isLoading: false, }); + useToastStore.getState().addToast(message, 'error'); throw err; } }, @@ -112,10 +121,12 @@ export const useDeliveryStore = create()((set, get) => ({ // Refresh counts await get().fetchDeliveryCounts(); } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to delete delivery'; set({ - error: err instanceof Error ? err.message : 'Failed to delete delivery', + error: message, isLoading: false, }); + useToastStore.getState().addToast(message, 'error'); throw err; } }, @@ -136,10 +147,12 @@ export const useDeliveryStore = create()((set, get) => ({ isLoading: false, })); } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to update status'; set({ - error: err instanceof Error ? err.message : 'Failed to update status', + error: message, isLoading: false, }); + useToastStore.getState().addToast(message, 'error'); throw err; } }, diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts new file mode 100644 index 0000000..ffdc131 --- /dev/null +++ b/frontend/src/utils/date.ts @@ -0,0 +1,52 @@ +import { format, parse, type Locale } from 'date-fns'; + +/** + * Convert backend date format (YYYY-MM-DD) to frontend format (DD-MM-YYYY) + */ +export function backendDateToFrontend(dateStr: string): string { + const [year, month, day] = dateStr.split('-'); + return `${day}-${month}-${year}`; +} + +/** + * Convert frontend date format (DD-MM-YYYY) to backend format (YYYY-MM-DD) + */ +export function frontendDateToBackend(dateStr: string): string { + const [day, month, year] = dateStr.split('-'); + return `${year}-${month}-${day}`; +} + +/** + * Format frontend date for HTML input type="date" (YYYY-MM-DD) + */ +export function formatDateForInput(dateStr: string): string { + const [day, month, year] = dateStr.split('-'); + return `${year}-${month}-${day}`; +} + +/** + * Parse date from HTML input type="date" to frontend format (DD-MM-YYYY) + */ +export function parseDateFromInput(dateStr: string): string { + const [year, month, day] = dateStr.split('-'); + return `${day}-${month}-${year}`; +} + +/** + * Get today's date in frontend format + */ +export function getTodayFrontend(): string { + return format(new Date(), 'dd-MM-yyyy'); +} + +/** + * Format frontend date for display with date-fns + */ +export function formatFrontendDate( + dateStr: string, + formatStr: string, + options?: { locale?: Locale } +): string { + const date = parse(dateStr, 'dd-MM-yyyy', new Date()); + return format(date, formatStr, options); +}