Skip to content

Latest commit

 

History

History
619 lines (523 loc) · 15.5 KB

File metadata and controls

619 lines (523 loc) · 15.5 KB

TypeScript Types Usage Guide

MoveLink Marketplace Database Integration

This guide shows you how to use the TypeScript types with your new database schema.


📦 Import Types

// Import specific types
import type { 
  Mover, 
  Booking, 
  Payment, 
  Rating,
  MoverLocation,
  BookingRequest,
  Notification,
  Database 
} from '@/types/database';

// Import enums
import type {
  VerificationStatus,
  AvailabilityStatus,
  BookingStatus,
  PaymentStatus,
  PaymentMethod,
  VehicleType,
  RatingType
} from '@/types/database';

// Import Supabase client (already typed)
import { supabase } from '@/integrations/supabase/client';

🚀 Usage Examples

1. Query Movers (Type-Safe)

// Get all verified movers
const getVerifiedMovers = async () => {
  const { data, error } = await supabase
  const { data, error } = await supabase
    .from('movers')
    .select('*')
    .eq('verification_status', 'verified')
    .is('deleted_at', null);
  
  if (error) throw error;
  
  // 'data' is automatically typed as Mover[]
  return data;
};

// Get single mover with type safety
const getMover = async (moverId: string): Promise<Mover | null> => {
  const { data, error } = await supabase
    .from('movers')
    .select('*')
    .eq('id', moverId)
    .single();
  
  if (error) throw error;
  return data; // Typed as Mover
};

2. Create Mover Profile

interface CreateMoverInput {
  user_id: string;
  business_name: string;
  phone_primary: string;
  vehicle_types: VehicleType[];
  service_radius_km?: number;
}

const createMoverProfile = async (input: CreateMoverInput) => {
  const { data, error } = await supabase
    .from('movers')
    .insert({
      user_id: input.user_id,
      business_name: input.business_name,
      phone_primary: input.phone_primary,
      vehicle_types: input.vehicle_types,
      service_radius_km: input.service_radius_km || 20,
      // TypeScript will autocomplete all available fields!
      verification_status: 'pending',
      availability_status: 'offline',
      is_accepting_bookings: false,
      has_helpers: false,
      helper_count: 0
    })
    .select()
    .single();
  
  if (error) throw error;
  return data; // Typed as Mover
};

3. Create Booking

interface CreateBookingInput {
  customer_id: string;
  pickup_address: string;
  pickup_latitude: number;
  pickup_longitude: number;
  dropoff_address: string;
  dropoff_latitude: number;
  dropoff_longitude: number;
  scheduled_date: string; // ISO date
  scheduled_time_start: string; // HH:MM
  property_size: string;
  estimated_price: number;
}

const createBooking = async (input: CreateBookingInput) => {
  const { data, error } = await supabase
    .from('bookings')
    .insert({
      customer_id: input.customer_id,
      pickup_address: input.pickup_address,
      pickup_location: `POINT(${input.pickup_longitude} ${input.pickup_latitude})`,
      dropoff_address: input.dropoff_address,
      dropoff_location: `POINT(${input.dropoff_longitude} ${input.dropoff_latitude})`,
      scheduled_date: input.scheduled_date,
      scheduled_time_start: input.scheduled_time_start,
      property_size: input.property_size,
      estimated_price: input.estimated_price,
      status: 'pending' as BookingStatus, // Type-safe enum
      booking_number: '' // Auto-generated by trigger
    })
    .select()
    .single();
  
  if (error) throw error;
  return data; // Typed as Booking
};

4. Find Nearby Movers (Using Helper Function)

interface NearbyMover {
  mover_id: string;
  business_name: string;
  distance_km: number;
  rating: number;
  total_moves: number;
  vehicle_types: VehicleType[];
}

const findNearbyMovers = async (
  latitude: number,
  longitude: number,
  radiusKm: number = 20,
  minRating: number = 3.0
): Promise<NearbyMover[]> => {
  const { data, error } = await supabase.rpc('find_nearby_movers', {
    p_location: `POINT(${longitude} ${latitude})`,
    p_radius_km: radiusKm,
    p_min_rating: minRating
  });
  
  if (error) throw error;
  return data || [];
};

5. Create Payment

interface CreatePaymentInput {
  booking_id: string;
  customer_id: string;
  mover_id: string;
  amount: number;
  payment_method: PaymentMethod;
  commission_rate: number; // e.g., 15.00 for 15%
  mpesa_phone_number?: string;
}

const createPayment = async (input: CreatePaymentInput) => {
  // Validate commission rate is within acceptable range
  if (input.commission_rate < 0 || input.commission_rate > 100) {
    throw new Error('Commission rate must be between 0 and 100');
  }
  
  const commissionAmount = (input.amount * input.commission_rate) / 100;
  const moverPayoutAmount = input.amount - commissionAmount;
  
  const { data, error } = await supabase
    .from('payments')
    .insert({
      booking_id: input.booking_id,
      customer_id: input.customer_id,
      mover_id: input.mover_id,
      amount: input.amount,
      payment_method: input.payment_method,
      payment_status: 'pending' as PaymentStatus,
      commission_rate: input.commission_rate,
      commission_amount: commissionAmount,
      mover_payout_amount: moverPayoutAmount,
      mpesa_phone_number: input.mpesa_phone_number,
      payment_reference: '' // Auto-generated
    })
    .select()
    .single();
  
  if (error) throw error;
  return data; // Typed as Payment
};

6. Create Rating

interface CreateRatingInput {
  booking_id: string;
  rater_id: string;
  rated_id: string;
  rating_type: RatingType;
  rating: number; // 1-5
  review_text?: string;
  punctuality_rating?: number;
  professionalism_rating?: number;
  care_of_items_rating?: number;
}

const createRating = async (input: CreateRatingInput) => {
  const { data, error } = await supabase
    .from('ratings')
    .insert({
      booking_id: input.booking_id,
      rater_id: input.rater_id,
      rated_id: input.rated_id,
      rating_type: input.rating_type,
      rating: input.rating,
      review_text: input.review_text,
      punctuality_rating: input.punctuality_rating,
      professionalism_rating: input.professionalism_rating,
      care_of_items_rating: input.care_of_items_rating
    })
    .select()
    .single();
  
  if (error) throw error;
  return data; // Typed as Rating
};

7. Track Mover Location

interface TrackLocationInput {
  mover_id: string;
  booking_id?: string;
  latitude: number;
  longitude: number;
  heading?: number;
  speed?: number;
  accuracy?: number;
}

const trackMoverLocation = async (input: TrackLocationInput) => {
  const { data, error } = await supabase
    .from('mover_locations')
    .insert({
      mover_id: input.mover_id,
      booking_id: input.booking_id,
      location: `POINT(${input.longitude} ${input.latitude})`,
      latitude: input.latitude,
      longitude: input.longitude,
      heading: input.heading,
      speed: input.speed,
      accuracy: input.accuracy,
      location_source: 'gps'
    })
    .select()
    .single();
  
  if (error) throw error;
  return data; // Typed as MoverLocation
};

8. Get Customer's Bookings

const getCustomerBookings = async (
  customerId: string,
  status?: BookingStatus
) => {
  let query = supabase
    .from('bookings')
    .select('*')
    .eq('customer_id', customerId)
    .order('created_at', { ascending: false });
  
  if (status) {
    query = query.eq('status', status);
  }
  
  const { data, error } = await query;
  
  if (error) throw error;
  return data; // Typed as Booking[]
};

9. Get Mover's Assigned Bookings

const getMoverBookings = async (
  moverId: string,
  statuses: BookingStatus[] = ['pending', 'accepted', 'in_progress']
) => {
  const { data, error } = await supabase
    .from('bookings')
    .select('*')
    .eq('mover_id', moverId)
    .in('status', statuses)
    .order('scheduled_date', { ascending: true });
  
  if (error) throw error;
  return data; // Typed as Booking[]
};

10. Get Mover Stats (Using Helper Function)

interface MoverStats {
  total_bookings: number;
  completed_bookings: number;
  cancelled_bookings: number;
  total_earnings: number;
  average_rating: number;
  acceptance_rate: number;
  completion_rate: number;
  this_month_earnings: number;
  this_month_bookings: number;
}

const getMoverStats = async (moverId: string): Promise<MoverStats> => {
  const { data, error } = await supabase.rpc('get_mover_stats', {
    p_mover_id: moverId
  });
  
  if (error) throw error;
  return data;
};

🎨 React Component Examples

Mover List Component

import { useEffect, useState } from 'react';
import { supabase } from '@/integrations/supabase/client';
import type { Mover } from '@/types/database';

export const MoverList = () => {
  const [movers, setMovers] = useState<Mover[]>([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    loadMovers();
  }, []);
  
  const loadMovers = async () => {
    try {
      const { data, error } = await supabase
        .from('movers')
        .select('*')
        .eq('verification_status', 'verified')
        .order('rating', { ascending: false });
      
      if (error) throw error;
      setMovers(data || []);
    } catch (error) {
      console.error('Error loading movers:', error);
    } finally {
      setLoading(false);
    }
  };
  
  if (loading) return <div>Loading...</div>;
  
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
      {movers.map((mover) => (
        <div key={mover.id} className="border rounded-lg p-4">
          <h3 className="font-bold">{mover.business_name}</h3>
          <p>Rating: {mover.rating} </p>
          <p>Total Moves: {mover.total_moves}</p>
          <p>Phone: {mover.phone_primary}</p>
          <p>Vehicles: {mover.vehicle_types.join(', ')}</p>
        </div>
      ))}
    </div>
  );
};

Booking Form Component

import { useState } from 'react';
import { supabase } from '@/integrations/supabase/client';
import type { BookingStatus } from '@/types/database';
import { useAuth } from '@/hooks/useAuth';

interface BookingFormData {
  pickup_address: string;
  dropoff_address: string;
  scheduled_date: string;
  scheduled_time_start: string;
  property_size: string;
}

export const BookingForm = () => {
  const { user } = useAuth();
  const [formData, setFormData] = useState<BookingFormData>({
    pickup_address: '',
    dropoff_address: '',
    scheduled_date: '',
    scheduled_time_start: '',
    property_size: '1BR'
  });
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (!user) return;
    
    try {
      const { data, error } = await supabase
        .from('bookings')
        .insert({
          customer_id: user.id,
          pickup_address: formData.pickup_address,
          pickup_location: 'POINT(36.8219 -1.2921)', // Get from geocoding
          dropoff_address: formData.dropoff_address,
          dropoff_location: 'POINT(36.8219 -1.2921)', // Get from geocoding
          scheduled_date: formData.scheduled_date,
          scheduled_time_start: formData.scheduled_time_start,
          property_size: formData.property_size,
          estimated_price: 5000, // Calculate from quote
          status: 'pending' as BookingStatus,
          booking_number: ''
        })
        .select()
        .single();
      
      if (error) throw error;
      
      alert('Booking created: ' + data.booking_number);
    } catch (error) {
      console.error('Error creating booking:', error);
    }
  };
  
  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <input
        type="text"
        placeholder="Pickup Address"
        value={formData.pickup_address}
        onChange={(e) => setFormData({ ...formData, pickup_address: e.target.value })}
        className="w-full p-2 border rounded"
      />
      {/* Add more form fields */}
      <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
        Create Booking
      </button>
    </form>
  );
};

🔔 Real-Time Subscriptions

Subscribe to Booking Updates

import { useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import type { Booking } from '@/types/database';

export const useBookingSubscription = (customerId: string) => {
  useEffect(() => {
    const channel = supabase
      .channel('booking-updates')
      .on(
        'postgres_changes',
        {
          event: '*',
          schema: 'public',
          table: 'bookings',
          filter: `customer_id=eq.${customerId}`
        },
        (payload) => {
          console.log('Booking updated:', payload.new as Booking);
          // Update your state here
        }
      )
      .subscribe((status, err) => {
        // Handle channel lifecycle status
        if (status === 'SUBSCRIBED') {
          console.log('✅ Successfully subscribed to booking updates');
        } else if (status === 'CLOSED') {
          console.log('⚠️ Channel closed');
          // NOTE: Implement reconnection logic here (e.g., exponential backoff retry)
          // Consider using a reconnection strategy to re-establish the subscription
        } else if (status === 'CHANNEL_ERROR') {
          console.error('❌ Channel error:', err);
          // NOTE: Implement error recovery and backoff logic
          // Common causes: network issues, auth token expiration, rate limits
          // Recommended: Use exponential backoff (1s, 2s, 4s, 8s, etc.) before retrying
        } else {
          console.log('Channel status:', status);
        }
      });
    
    return () => {
      supabase.removeChannel(channel);
    };
  }, [customerId]);
};

✅ Type-Safe Query Helpers

Create reusable helper functions:

// src/services/movers.ts
import { supabase } from '@/integrations/supabase/client';
import type { Mover, VerificationStatus } from '@/types/database';

export const moverService = {
  getAll: async (status?: VerificationStatus): Promise<Mover[]> => {
    let query = supabase.from('movers').select('*');
    
    if (status) {
      query = query.eq('verification_status', status);
    }
    
    const { data, error } = await query;
    if (error) throw error;
    return data || [];
  },
  
  getById: async (id: string): Promise<Mover | null> => {
    const { data, error } = await supabase
      .from('movers')
      .select('*')
      .eq('id', id)
      .single();
    
    if (error) throw error;
    return data;
  },
  
  updateAvailability: async (id: string, isAvailable: boolean) => {
    const { data, error } = await supabase
      .from('movers')
      .update({
        is_accepting_bookings: isAvailable,
        availability_status: isAvailable ? 'online' : 'offline'
      })
      .eq('id', id)
      .select()
      .single();
    
    if (error) throw error;
    return data;
  }
};

🎯 Best Practices

  1. Always use types for function parameters and return values
  2. Create service layers for database operations
  3. Use React hooks for data fetching with types
  4. Leverage TypeScript's autocomplete - it knows all table columns!
  5. Handle errors properly with typed error responses
  6. Use enums for status values to prevent typos

📚 Next Steps

  1. ✅ Import types in your components
  2. ✅ Update existing API calls to use new tables
  3. ✅ Create service files for each table (movers, bookings, payments, etc.)
  4. ✅ Build React hooks for common queries
  5. ✅ Implement real-time subscriptions for live updates

Your types are fully integrated with Supabase and will give you amazing autocomplete and type safety! 🎉