Untitled

 avatar
Yash
plain_text
a year ago
24 kB
14
Indexable
import { toaster } from '@/components/ui/toaster';
import { useGetAllAssignedVehicle, useGetRoutes } from '@/modules/routes/hooks';
import { useAuthContext } from '@/providers/authProvider';
import { SCHOOLLOG_NODE_API_URL } from '@/services/requestUris';
import {
  Badge,
  Box,
  Button,
  Card,
  Flex,
  Grid,
  GridItem,
  HStack,
  Icon,
  Input,
  Menu,
  Spinner,
  Text,
  VStack,
} from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import dayjs from 'dayjs';
import { find } from 'lodash';
import type { LucideIcon } from 'lucide-react';
import {
  AlertCircle,
  Bus,
  CheckCircle,
  ChevronDownIcon,
  Clock,
  Eye,
  Home,
  Play,
  School,
  XCircle,
} from 'lucide-react';
import type { Dispatch, JSX, SetStateAction } from 'react';
import React, { useMemo, useState } from 'react';
import {
  useCreateTrip,
  useGetTripRouteStudentCount,
  useGetTrips,
} from '../hooks';

// API Response types
type ApiTrip = {
  id: string;
  t_route_id: string;
  school_id: string;
  start_time: string;
  end_time: string;
  date: string;
  type: string;
  created_by_user_id: string | null;
  pickup_completed: boolean;
  drop_completed: boolean;
  pickedup_count?: number;
  dropped_count?: number;
  pickup_attendance?: boolean;
  drop_attendance?: boolean;
};

type VehicleRouteData = {
  id: string;
  vehicle_id: string;
  route_id: string;
  school_id: string;
  order: string | number;
  type: string;
  active: boolean;
  vehicle: {
    id: string;
    name: string;
    number: string;
    capacity: number;
    type: string;
    active: boolean;
    school_id: string;
    created_at: string;
    updated_at: string;
    deleted_at: string | null;
  };
  routes: {
    id: string;
    name: string;
    active: boolean;
    school_id: string;
    created_at: string | null;
    updated_at: string | null;
    deleted_at: string | null;
  };
};

type TripStatus = 'not_started' | 'ongoing' | 'completed';
type TripDirection = 'to_school' | 'to_home';

type StudentCounts = {
  total: number;
  picked: number;
  dropped: number;
  available?: number;
};

type TripCardData = {
  id: string | null; // null when no trip data exists
  routeId: string;
  routeName: string;
  vehicleName: string;
  vehicleNumber: string;
  direction: TripDirection;
  status: TripStatus | null;
  startTime?: string;
  endTime?: string;
  totalStudents: number;
  students: StudentCounts;
  schoolAttendance?: number;
  gatePassed?: number;
  pickup_attendance?: boolean;
  drop_attendance?: boolean;
  progressSteps?: {
    current: number;
    total: number;
  };
  hasData: boolean;
};

type RouteRowData = {
  routeId: string;
  routeName: string;
  vehicleName: string;
  vehicleNumber: string;
  totalStudents: number;
  toSchoolTrip: TripCardData;
  toHomeTrip: TripCardData;
};

const getStatusColor = (status: TripStatus | null): string => {
  if (!status) return 'gray';
  switch (status) {
    case 'not_started':
      return 'orange';
    case 'ongoing':
      return 'blue';
    case 'completed':
      return 'green';
    default:
      return 'gray';
  }
};

const getStatusIcon = (status: TripStatus | null): LucideIcon => {
  if (!status) return XCircle;
  switch (status) {
    case 'not_started':
      return AlertCircle;
    case 'ongoing':
      return Clock;
    case 'completed':
      return CheckCircle;
    default:
      return XCircle;
  }
};

const getDirectionText = (direction: TripDirection): string => {
  return direction === 'to_school' ? 'To School' : 'To Home';
};

const getDirectionIcon = (direction: TripDirection): LucideIcon => {
  return direction === 'to_school' ? School : Home;
};

const getTripStatus = (trip: ApiTrip): TripStatus => {
  if (trip.pickup_attendance && trip.drop_attendance) {
    return 'completed';
  }

  if (trip.start_time && !trip.end_time) {
    return 'ongoing';
  }

  if (!trip) {
    return 'not_started';
  }

  return 'completed';
};

const formatTime = (dateString: string): string => {
  const date = new Date(dateString);
  return date.toLocaleTimeString('en-US', {
    hour: '2-digit',
    minute: '2-digit',
    hour12: true,
  });
};

const convertDirection = (apiType: string): TripDirection => {
  if (apiType === 'to_school') return 'to_school';
  if (apiType === 'to_home') return 'to_home';
  return 'to_school';
};

const getVehicleInfoByRouteId = (
  routeId: string,
  vehicleRoutesData: VehicleRouteData[],
): { vehicleName: string; vehicleNumber: string } => {
  const vehicleRoute = vehicleRoutesData?.find(
    (vr) => vr.route_id === routeId && vr.active,
  );

  if (vehicleRoute && vehicleRoute.vehicle) {
    return {
      vehicleName: vehicleRoute.vehicle.name,
      vehicleNumber: vehicleRoute.vehicle.number,
    };
  }

  return {
    vehicleName: 'N/A',
    vehicleNumber: 'N/A',
  };
};

const TripCard = ({
  trip,
  selectedDate,
}: {
  trip: TripCardData;
  selectedDate: string;
}): JSX.Element => {
  const statusToShow = trip.hasData ? trip.status : 'not_started';
  const StatusIconComponent = getStatusIcon(statusToShow);
  const DirectionIconComponent = getDirectionIcon(trip.direction);
  const client = useQueryClient();

  const { mutate: createTrip, isPending: isTripCreating } = useCreateTrip({
    onSuccess: () => {
      toaster.create({
        title: `Trip Started Successfully`,
        type: 'success',
      });
      client.invalidateQueries({
        queryKey: [SCHOOLLOG_NODE_API_URL.getAllTripRouteStudentCount],
      });
      client.invalidateQueries({
        queryKey: [SCHOOLLOG_NODE_API_URL.getAllTrips],
      });
    },
    onError: (e) => {
      toaster.create({
        type: 'error',
        title: e.message ?? 'Something went wrong',
      });
    },
  });

  const handleViewDetailsClick = (): void => {
    if (trip.hasData && trip.id) {
      window.location.href = `/v2/transport-attendance/${trip.routeId}?date=${selectedDate}`;
    }
  };

  const handleStartTripClick = (): void => {
    createTrip({
      t_route_id: trip.routeId,
      type: trip.direction,
      start_time: dayjs(selectedDate)
        .hour(dayjs(trip.startTime).hour())
        .minute(dayjs(trip.startTime).minute())
        .second(0)
        .millisecond(0)
        .toDate(),
      date: selectedDate,
    });
  };

  const getProgressPercentage = (): number => {
    if (!trip.progressSteps) return 0;
    return (trip.progressSteps.current / trip.progressSteps.total) * 100;
  };

  return (
    <Card.Root variant="elevated" size="md" bg="white" shadow="sm">
      <Card.Body p={4}>
        <VStack align="stretch" gap={3}>
          <Flex justify="space-between" align="center">
            <Text fontSize="lg" fontWeight="bold" color="gray.800">
              {trip.routeName}
            </Text>
            <HStack gap={2}>
              <Badge colorPalette={getStatusColor(statusToShow)} size="sm">
                <HStack gap={1}>
                  <Icon>
                    <StatusIconComponent size={12} />
                  </Icon>
                  <Text>
                    {trip.hasData && trip.status === 'completed'
                      ? 'Completed'
                      : trip.hasData && trip.status === 'ongoing'
                        ? 'In Progress'
                        : 'Not Started'}
                  </Text>
                </HStack>
              </Badge>
            </HStack>
          </Flex>

          {/* Direction */}
          <HStack gap={2}>
            <Icon color="gray.600">
              <DirectionIconComponent size={16} />
            </Icon>
            <Text fontSize="md" color="gray.700" fontWeight="medium">
              {getDirectionText(trip.direction)}
            </Text>
          </HStack>

          {/* Vehicle Info */}
          <HStack gap={4}>
            <HStack gap={2}>
              <Icon color="gray.500">
                <Bus size={16} />
              </Icon>
              <Text fontSize="sm" color="gray.800" fontWeight="bold">
                {trip.vehicleName}
              </Text>
            </HStack>
          </HStack>

          {/* Student Stats */}
          {trip.hasData ? (
            <Grid templateColumns="repeat(4, 1fr)" gap={4} py={2}>
              {trip.direction === 'to_school' ? (
                <>
                  <VStack gap={1}>
                    <Text fontSize="2xl" fontWeight="bold" color="blue.600">
                      {trip.totalStudents}
                    </Text>
                    <Text fontSize="xs" color="gray.600" textAlign="center">
                      Assigned
                    </Text>
                  </VStack>
                  <VStack gap={1}>
                    <Text fontSize="2xl" fontWeight="bold" color="green.600">
                      {trip.students.picked}
                    </Text>
                    <Text fontSize="xs" color="gray.600" textAlign="center">
                      Picked Up
                    </Text>
                  </VStack>
                  <VStack gap={1}>
                    <Text fontSize="2xl" fontWeight="bold" color="purple.600">
                      {trip.students.dropped}
                    </Text>
                    <Text fontSize="xs" color="gray.600" textAlign="center">
                      Dropped Off
                    </Text>
                  </VStack>
                  <VStack gap={1}>
                    <Text fontSize="2xl" fontWeight="bold" color="orange.600">
                      {trip.gatePassed || 0}
                    </Text>
                    <Text fontSize="xs" color="gray.600" textAlign="center">
                      At School
                    </Text>
                  </VStack>
                </>
              ) : (
                <>
                  <VStack gap={1}>
                    <Text fontSize="2xl" fontWeight="bold" color="blue.600">
                      {trip.students.available || 0}
                    </Text>
                    <Text fontSize="xs" color="gray.600" textAlign="center">
                      Available
                    </Text>
                  </VStack>
                  <VStack gap={1}>
                    <Text fontSize="2xl" fontWeight="bold" color="green.600">
                      {trip.students.picked}
                    </Text>
                    <Text fontSize="xs" color="gray.600" textAlign="center">
                      Onboard
                    </Text>
                  </VStack>
                  <VStack gap={1}>
                    <Text fontSize="2xl" fontWeight="bold" color="red.600">
                      {trip.students.dropped}
                    </Text>
                    <Text fontSize="xs" color="gray.600" textAlign="center">
                      Dropped Off
                    </Text>
                  </VStack>
                </>
              )}
            </Grid>
          ) : (
            <Box py={4} textAlign="center">
              <VStack gap={2}>
                <Icon color="orange.500" size="lg">
                  <AlertCircle size={24} />
                </Icon>
                <Text fontSize="sm" color="gray.500" fontWeight="medium">
                  Trip not started yet
                </Text>
                <Text fontSize="xs" color="gray.400">
                  Click "Start Trip" to begin
                </Text>
              </VStack>
            </Box>
          )}

          {/* Progress Bar */}
          {trip.hasData && trip.progressSteps && (
            <VStack gap={2}>
              <Box
                w="100%"
                bg="gray.200"
                borderRadius="md"
                overflow="hidden"
                h="2"
              >
                <Box
                  h="100%"
                  bg="green.500"
                  transition="width 0.3s ease"
                  w={`${getProgressPercentage()}%`}
                />
              </Box>
              <Text fontSize="sm" color="gray.600">
                Trip Progress ({trip.progressSteps.current} /{' '}
                {trip.progressSteps.total} stops)
              </Text>
            </VStack>
          )}

          {/* Action Button */}
          {trip.hasData ? (
            <Button
              variant="solid"
              size="sm"
              onClick={handleViewDetailsClick}
              w="100%"
            >
              <Eye size={14} />
              View Details
            </Button>
          ) : (
            <Button
              variant="solid"
              colorPalette="blue"
              size="sm"
              onClick={handleStartTripClick}
              w="100%"
              loading={isTripCreating}
            >
              <Play size={14} />
              Start Trip
            </Button>
          )}
        </VStack>
      </Card.Body>
    </Card.Root>
  );
};

const RouteRow = ({
  routeRow,
  selectedDate,
}: {
  routeRow: RouteRowData;
  selectedDate: string;
}): JSX.Element => {
  return (
    <Grid templateColumns="repeat(2, 1fr)" gap={4} mb={6}>
      <GridItem>
        <TripCard trip={routeRow.toSchoolTrip} selectedDate={selectedDate} />
      </GridItem>
      <GridItem>
        <TripCard trip={routeRow.toHomeTrip} selectedDate={selectedDate} />
      </GridItem>
    </Grid>
  );
};

const RouteFilter = ({
  selectedRoute,
  setSelectedRoute,
  filterOptions,
}: {
  selectedRoute: string;
  setSelectedRoute: Dispatch<SetStateAction<string>>;
  filterOptions: { value: string; label: string }[];
}): JSX.Element => {
  const currentSelection = filterOptions.find(
    (option) => option.value === selectedRoute,
  ) || { value: 'all', label: 'All Routes' };

  const handleSelectFilter = (newRouteId: string): void => {
    setSelectedRoute(newRouteId);
  };

  const allOptions = [{ value: 'all', label: 'All Routes' }, ...filterOptions];

  return (
    <Menu.Root>
      <Menu.Trigger>
        <Button
          minW={150}
          justifyContent="space-between"
          variant="outline"
          size="md"
          colorPalette="gray"
          bg="white"
          borderColor="gray.300"
          _focus={{
            borderColor: 'blue.500',
            boxShadow: '0 0 0 1px var(--chakra-colors-blue-500)',
          }}
        >
          <Text color="gray.700">{currentSelection.label}</Text>
          <ChevronDownIcon size={16} />
        </Button>
      </Menu.Trigger>
      <Menu.Positioner>
        <Menu.Content>
          {allOptions.map((option) => (
            <Menu.Item
              onClick={() => handleSelectFilter(option.value)}
              key={option.value}
              value={option.value}
            >
              {option.label}
            </Menu.Item>
          ))}
        </Menu.Content>
      </Menu.Positioner>
    </Menu.Root>
  );
};

export default function TripDashboard(): JSX.Element {
  const [selectedDate, setSelectedDate] = useState<string>(() => {
    const today = new Date();
    return today.toISOString().split('T')[0];
  });
  const [selectedRoute, setSelectedRoute] = useState<string>('all');
  const [appliedDate, setAppliedDate] = useState<Date>(() => new Date());

  const { school } = useAuthContext();

  const { data: tripRouteStudentCountData, isLoading: isLoadingStudentCount } =
    useGetTripRouteStudentCount({
      date: appliedDate,
    });

  const { data: routesData, isLoading: isLoadingRoutes } = useGetRoutes({
    status: 'active',
    ...(selectedRoute && selectedRoute !== 'all'
      ? { routeId: selectedRoute }
      : {}),
  });

  const { data: allRoutes, isLoading: isLoadingAllRoutes } = useGetRoutes({
    status: 'active',
  });

  const { data: vehicleRoutesResponse, isLoading: isLoadingVehicleRoutes } =
    useGetAllAssignedVehicle({
      schoolId: school.id,
      status: 'active',
    });

  const { data: tripsData, isLoading: isLoadingTrips } = useGetTrips({
    date: appliedDate,
  });

  const isLoading =
    isLoadingRoutes ||
    isLoadingVehicleRoutes ||
    isLoadingTrips ||
    isLoadingStudentCount ||
    isLoadingAllRoutes;

  const routeOptions =
    allRoutes?.data?.map((rd) => ({
      value: rd.id,
      label: rd.name,
    })) || [];

  const routeRows = useMemo(() => {
    if (!routesData?.data || !vehicleRoutesResponse) {
      return [];
    }

    const vehicleRoutesData: VehicleRouteData[] = Array.isArray(
      vehicleRoutesResponse,
    )
      ? vehicleRoutesResponse
      : vehicleRoutesResponse || [];

    const routeRowsData: RouteRowData[] = routesData.data.map((route) => {
      const vehicleInfo = getVehicleInfoByRouteId(route.id, vehicleRoutesData);
      const studentCountData = find(
        tripRouteStudentCountData,
        (d) => d.route_id === route.id,
      );
      const totalStudents = studentCountData?.total_students || 0;

      // Find trips for this route
      const routeTrips =
        tripsData?.filter((trip: ApiTrip) => trip.t_route_id === route.id) ||
        [];

      const toSchoolTrip = routeTrips.find(
        (trip: ApiTrip) => trip.type === 'to_school',
      );
      const toHomeTrip = routeTrips.find(
        (trip: ApiTrip) => trip.type === 'to_home',
      );

      const attendance = studentCountData?.attendance || 0;
      const gatepassCount = studentCountData?.gatepass_count || 0;
      const availableCount = attendance - gatepassCount;

      // Create to_school trip card data
      const toSchoolTripData: TripCardData = toSchoolTrip
        ? {
            id: toSchoolTrip.id,
            routeId: route.id,
            routeName: route.name,
            vehicleName: vehicleInfo.vehicleName,
            vehicleNumber: vehicleInfo.vehicleNumber,
            direction: 'to_school',
            status: getTripStatus(toSchoolTrip),
            startTime: formatTime(toSchoolTrip.start_time),
            endTime: formatTime(toSchoolTrip.end_time),
            totalStudents,
            students: {
              total: totalStudents,
              picked: toSchoolTrip.pickedup_count || 0,
              dropped: toSchoolTrip.dropped_count || 0,
            },
            schoolAttendance: attendance,
            gatePassed: gatepassCount,
            pickup_attendance: toSchoolTrip.pickup_attendance,
            drop_attendance: toSchoolTrip.drop_attendance,
            progressSteps: {
              current: toSchoolTrip.pickedup_count || 0,
              total: 6,
            },
            hasData: true,
          }
        : {
            id: null,
            routeId: route.id,
            routeName: route.name,
            vehicleName: vehicleInfo.vehicleName,
            vehicleNumber: vehicleInfo.vehicleNumber,
            direction: 'to_school',
            status: null,
            totalStudents,
            students: {
              total: 0,
              picked: 0,
              dropped: 0,
            },
            hasData: false,
          };

      // Create to_home trip card data
      const toHomeTripData: TripCardData = toHomeTrip
        ? {
            id: toHomeTrip.id,
            routeId: route.id,
            routeName: route.name,
            vehicleName: vehicleInfo.vehicleName,
            vehicleNumber: vehicleInfo.vehicleNumber,
            direction: 'to_home',
            status: getTripStatus(toHomeTrip),
            startTime: formatTime(toHomeTrip.start_time),
            endTime: formatTime(toHomeTrip.end_time),
            totalStudents,
            students: {
              total: totalStudents,
              picked: toHomeTrip.pickedup_count || 0,
              dropped: toHomeTrip.dropped_count || 0,
              available: availableCount >= 0 ? availableCount : 0,
            },
            pickup_attendance: toHomeTrip.pickup_attendance,
            drop_attendance: toHomeTrip.drop_attendance,
            progressSteps: {
              current: toHomeTrip.dropped_count || 0,
              total: 6,
            },
            hasData: true,
          }
        : {
            id: null,
            routeId: route.id,
            routeName: route.name,
            vehicleName: vehicleInfo.vehicleName,
            vehicleNumber: vehicleInfo.vehicleNumber,
            direction: 'to_home',
            status: null,
            totalStudents,
            students: {
              total: 0,
              picked: 0,
              dropped: 0,
              available: 0,
            },
            hasData: false,
          };

      return {
        routeId: route.id,
        routeName: route.name,
        vehicleName: vehicleInfo.vehicleName,
        vehicleNumber: vehicleInfo.vehicleNumber,
        totalStudents,
        toSchoolTrip: toSchoolTripData,
        toHomeTrip: toHomeTripData,
      };
    });

    return routeRowsData;
  }, [routesData, tripsData, vehicleRoutesResponse, tripRouteStudentCountData]);

  const handleDateChange = (
    event: React.ChangeEvent<HTMLInputElement>,
  ): void => {
    setSelectedDate(event.target.value);
  };

  const handleGetData = (): void => {
    if (selectedDate) {
      const newDate = new Date(selectedDate);
      setAppliedDate(newDate);
    }
  };

  const hasRoutesData = !!(routesData?.data && vehicleRoutesResponse);

  return (
    <Box p={4} bg="gray.50" minH="100vh">
      <VStack gap={4} align="stretch">
        {/* Date Filter Section */}
        <VStack gap={3} align="start">
          <HStack gap={3} align="end">
            <VStack align="flex-start" gap={2} flex={1}>
              <Text fontSize="sm" color="gray.600">
                Select Route
              </Text>
              <RouteFilter
                selectedRoute={selectedRoute}
                setSelectedRoute={setSelectedRoute}
                filterOptions={routeOptions}
              />
            </VStack>
            <VStack align="flex-start" gap={2} flex={1}>
              <Text fontSize="sm" color="gray.600">
                Select Date
              </Text>
              <Input
                type="date"
                value={selectedDate}
                onChange={handleDateChange}
                size="md"
                bg="white"
                borderColor="gray.300"
                _focus={{
                  borderColor: 'blue.500',
                  boxShadow: '0 0 0 1px var(--chakra-colors-blue-500)',
                }}
              />
            </VStack>
            <Button
              colorPalette="blue"
              size="md"
              onClick={handleGetData}
              px={6}
              variant="outline"
              loading={isLoading}
              loadingText="Getting..."
            >
              Get
            </Button>
          </HStack>
        </VStack>

        {isLoading && (
          <Text textAlign="center" color="gray.500" py={8}>
            <Spinner mr={2} />
            Loading trips...
          </Text>
        )}

        {!isLoading && !hasRoutesData && (
          <Text textAlign="center" color="gray.500" py={8}>
            Unable to load route data. Please try again.
          </Text>
        )}

        {!isLoading && hasRoutesData && (
          <>
            {routeRows.length > 0 ? (
              <VStack gap={0} align="stretch">
                {routeRows.map((routeRow) => (
                  <RouteRow
                    key={routeRow.routeId}
                    routeRow={routeRow}
                    selectedDate={selectedDate}
                  />
                ))}
              </VStack>
            ) : (
              <Text textAlign="center" color="gray.500" py={8}>
                No routes found for the selected filter.
              </Text>
            )}
          </>
        )}
      </VStack>
    </Box>
  );
}
Editor is loading...
Leave a Comment