Improve delivery workflows and add versioned guided tours (#1)
Reviewed-on: #1 Co-authored-by: Egor Pozharov <chedius232@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
+33
-131
@@ -1,162 +1,64 @@
|
||||
import { useState, useEffect, lazy, Suspense } from 'react';
|
||||
import { Truck, Loader2, LogOut } from 'lucide-react';
|
||||
import { Truck, Loader2, LogOut, CircleHelp } from 'lucide-react';
|
||||
import { DeliveryForm } from './components/delivery/DeliveryForm';
|
||||
import { LoginForm } from './components/auth/LoginForm';
|
||||
import { ToastContainer } from './components/ui/Toast';
|
||||
import { Button } from './components/ui/Button';
|
||||
import { UpdatePrompt } from './components/ui/UpdatePrompt';
|
||||
import { GuidedTour } from './components/ui/GuidedTour';
|
||||
import { useDeliveryStore } from './stores/deliveryStore';
|
||||
import { useAuthStore } from './stores/authStore';
|
||||
import { useToastStore } from './stores/toastStore';
|
||||
import { useWebSocket } from './hooks/useWebSocket';
|
||||
import { getTodayFrontend } from './utils/date';
|
||||
|
||||
// Lazy load pages for code splitting
|
||||
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
||||
const DeliveryListPage = lazy(() => import('./pages/DeliveryListPage'));
|
||||
|
||||
// Fallback loading component
|
||||
const PageLoader = () => (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-[#1B263B]" />
|
||||
</div>
|
||||
);
|
||||
const PageLoader = () => <div role="status" className="flex items-center justify-center gap-3 py-20 text-[#666870]"><Loader2 size={24} className="animate-spin" />Загрузка…</div>;
|
||||
|
||||
function App() {
|
||||
const [view, setView] = useState<'dashboard' | 'delivery-list'>('dashboard');
|
||||
const [selectedDate, setSelectedDate] = useState<string>('');
|
||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||
const [formDate, setFormDate] = useState<string>('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const { isAuthenticated, isAuthChecking, restoreAuth, logout } = useAuthStore();
|
||||
const addDelivery = useDeliveryStore(state => state.addDelivery);
|
||||
const fetchDeliveryCounts = useDeliveryStore(state => state.fetchDeliveryCounts);
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState(getTodayFrontend);
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [tourReplay, setTourReplay] = useState(0);
|
||||
const { isAuthenticated, isAuthChecking, user, restoreAuth, logout } = useAuthStore();
|
||||
const { addDelivery, isLoading, isLoadingCounts, error, countsError, currentDate } = useDeliveryStore();
|
||||
useWebSocket();
|
||||
|
||||
// Restore auth on mount
|
||||
useEffect(() => {
|
||||
restoreAuth();
|
||||
}, [restoreAuth]);
|
||||
|
||||
// Refresh counts when form closes (only when authenticated)
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && !isFormOpen) {
|
||||
fetchDeliveryCounts();
|
||||
}
|
||||
}, [isAuthenticated, isFormOpen, fetchDeliveryCounts]);
|
||||
|
||||
const handleDateSelect = (date: string) => {
|
||||
setSelectedDate(date);
|
||||
setView('delivery-list');
|
||||
useEffect(() => { restoreAuth(); }, [restoreAuth]);
|
||||
const selectDate = (date: string) => { setSelectedDate(date); setView('delivery-list'); };
|
||||
const submitDelivery = async (data: Parameters<typeof addDelivery>[0]) => {
|
||||
await addDelivery(data);
|
||||
useToastStore.getState().addToast('Доставка создана', 'success');
|
||||
selectDate(data.date);
|
||||
};
|
||||
|
||||
const handleBackToDashboard = () => {
|
||||
setView('dashboard');
|
||||
setSelectedDate('');
|
||||
};
|
||||
|
||||
const handleAddDelivery = () => {
|
||||
const today = new Date().toLocaleDateString('ru-RU').split('.').join('-');
|
||||
setFormDate(today);
|
||||
setIsFormOpen(true);
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (data: Parameters<typeof addDelivery>[0]) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await addDelivery(data);
|
||||
setIsFormOpen(false);
|
||||
|
||||
// If created for different date, navigate to that date
|
||||
const today = new Date().toLocaleDateString('ru-RU').split('.').join('-');
|
||||
if (data.date !== today) {
|
||||
setSelectedDate(data.date);
|
||||
setView('delivery-list');
|
||||
}
|
||||
} catch {
|
||||
// Error is handled by store
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading while checking auth
|
||||
if (isAuthChecking) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[#fbf8fb]">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-[#1B263B]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show login form if not authenticated
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<>
|
||||
<UpdatePrompt />
|
||||
<LoginForm />
|
||||
<ToastContainer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAuthChecking) return <PageLoader />;
|
||||
if (!isAuthenticated) return <><UpdatePrompt /><LoginForm /><ToastContainer /></>;
|
||||
return (
|
||||
<div className="min-h-screen bg-[#fbf8fb]">
|
||||
<a href="#main-content" className="skip-link">Перейти к содержимому</a>
|
||||
<UpdatePrompt />
|
||||
<header className="sticky top-0 z-40 bg-[#1B263B] text-white shadow-md">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-14">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-white/10 rounded-lg">
|
||||
<Truck size={24} className="text-white" />
|
||||
</div>
|
||||
<h1 className="text-lg font-semibold hidden sm:block">Delivery Tracker</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-sm text-white/70">
|
||||
{view === 'dashboard' ? 'Панель управления' : `Доставки на ${selectedDate}`}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={logout}
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
<LogOut size={18} className="mr-1" />
|
||||
Выйти
|
||||
</Button>
|
||||
</div>
|
||||
<header className="sticky top-0 z-40 bg-[#1B263B] text-white">
|
||||
<div className="mx-auto flex min-h-16 max-w-7xl items-center justify-between gap-2 px-4 sm:px-6 lg:px-8">
|
||||
<button type="button" onClick={() => setView('dashboard')} className="flex min-h-11 items-center gap-2 text-left" aria-label="Delivery Tracker — открыть план доставок">
|
||||
<Truck size={25} className="shrink-0" /><span className="text-sm font-semibold sm:text-lg">Delivery Tracker</span>
|
||||
</button>
|
||||
<div className="flex items-center gap-1 sm:gap-3">
|
||||
<Button variant="ghost" size="sm" data-tour="tour-help" onClick={() => setTourReplay(value => value + 1)} className="text-white hover:bg-white/10" aria-label="Помощь: обзор текущего экрана"><CircleHelp size={19} /><span className="ml-2 hidden sm:inline">Помощь</span></Button>
|
||||
<Button variant="ghost" size="sm" onClick={logout} className="text-white hover:bg-white/10" aria-label="Выйти из системы"><LogOut size={18} /><span className="ml-2 hidden sm:inline">Выйти</span></Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<main id="main-content" tabIndex={-1} className="mx-auto max-w-7xl px-4 py-5 sm:px-6 sm:py-7 lg:px-8">
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
{view === 'dashboard' ? (
|
||||
<Dashboard
|
||||
onDateSelect={handleDateSelect}
|
||||
onAddDelivery={handleAddDelivery}
|
||||
/>
|
||||
) : (
|
||||
<DeliveryListPage
|
||||
selectedDate={selectedDate}
|
||||
onBack={handleBackToDashboard}
|
||||
/>
|
||||
)}
|
||||
{view === 'dashboard' ? <Dashboard onDateSelect={selectDate} onAddDelivery={() => setFormOpen(true)} />
|
||||
: <DeliveryListPage selectedDate={selectedDate} onDateSelect={selectDate} onBack={() => setView('dashboard')} />}
|
||||
</Suspense>
|
||||
</main>
|
||||
|
||||
<DeliveryForm
|
||||
isOpen={isFormOpen}
|
||||
onClose={() => setIsFormOpen(false)}
|
||||
onSubmit={handleFormSubmit}
|
||||
defaultDate={formDate}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
|
||||
<DeliveryForm isOpen={formOpen} onClose={() => setFormOpen(false)} onSubmit={submitDelivery} defaultDate={getTodayFrontend()} />
|
||||
<GuidedTour key={`${user?.id || user?.username}:${view}`} scope={view} userKey={user?.id || user?.username || 'user'} replay={tourReplay}
|
||||
ready={!formOpen && (view === 'dashboard' ? !isLoadingCounts && !countsError : !isLoading && !error && currentDate === selectedDate)} />
|
||||
<ToastContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
Reference in New Issue
Block a user