Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/EdgarJr30/proyecto-de-grado-cms/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The service layer (src/services/) provides a clean abstraction over Supabase interactions. All database queries, RPC calls, and storage operations go through services.
Key Principle: Components should never directly call supabase. All data access must go through service functions.

Architecture

Component → Service Function → Supabase Client → Database

Benefits

  • Separation of concerns: Business logic lives in services, not components
  • Type safety: Services enforce TypeScript types
  • Error handling: Centralized error mapping and validation
  • Testability: Services can be mocked for testing
  • Cache invalidation: Services trigger data invalidation events

Supabase Client

The authenticated client is initialized in lib/supabaseClient.ts:
src/lib/supabaseClient.ts
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;

if (!supabaseUrl || !supabaseAnonKey) {
  throw new Error('Missing Supabase URL or anon key');
}

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  auth: {
    persistSession: true,
    autoRefreshToken: true,
    detectSessionInUrl: true,
  },
});
The frontend client uses only the anon key. Never expose the service role key in the frontend.

Service Structure

Core Services

ticketService.ts handles all ticket/work order operations:
src/services/ticketService.ts
import { supabase } from '../lib/supabaseClient';
import { invalidateData } from '../lib/dataInvalidation';
import type { Ticket, WorkOrder } from '../types/Ticket';

const PAGE_SIZE = 20;

export async function getAllTickets(page: number): Promise<Ticket[]> {
  const from = page * PAGE_SIZE;
  const to = from + PAGE_SIZE - 1;

  const { data, error } = await supabase
    .from('v_tickets_compat')
    .select('*')
    .eq('is_archived', false)
    .order('id', { ascending: false })
    .range(from, to);

  if (error) {
    console.error('Error al obtener tickets:', error.message);
    return [];
  }

  return data ?? [];
}

export async function createTicket(
  ticket: Omit<Ticket, 'id' | 'status' | 'created_by'>
) {
  const { data: { user }, error: userErr } = await supabase.auth.getUser();
  if (userErr) throw userErr;
  if (!user) throw new Error('No hay sesión activa.');

  const { data, error } = await supabase
    .from('tickets')
    .insert([{ ...ticket, status: 'Pendiente', created_by: user.id }])
    .select('id, title')
    .single();

  if (error) throw new Error(error.message);
  invalidateData('tickets');  // Trigger cache invalidation
  return data;
}

Inventory Services

Inventory operations are organized in services/inventory/:
services/inventory/
├── index.ts                      # Re-exports all inventory services
├── inventoryClient.ts            # Shared inventory client
├── partsService.ts               # Parts catalog
├── warehousesService.ts          # Warehouse management
├── stockService.ts               # Stock queries
├── docsService.ts                # Inventory documents
├── ledgerService.ts              # Transaction ledger
├── kardexService.ts              # Movement history
├── uomsService.ts                # Units of measure
├── vendorsService.ts             # Vendor management
├── reorderPoliciesService.ts     # Reorder policies
└── reorderSuggestionsService.ts  # Reorder suggestions

Example: Parts Service

src/services/inventory/partsService.ts
import { supabase } from '../../lib/supabaseClient';
import type { Part } from '../../types/inventory/master';

export async function getParts(): Promise<Part[]> {
  const { data, error } = await supabase
    .from('parts')
    .select('*')
    .order('part_code');
  
  if (error) throw error;
  return data ?? [];
}

export async function createPart(part: Omit<Part, 'id'>) {
  const { data, error } = await supabase
    .from('parts')
    .insert([part])
    .select()
    .single();
  
  if (error) throw error;
  return data;
}

Service Patterns

Pagination

export async function getTicketsPaginated(
  page: number,
  pageSize: number
): Promise<{ data: Ticket[]; count: number }> {
  const from = page * pageSize;
  const to = from + pageSize - 1;
  
  const { data, error, count } = await supabase
    .from('tickets')
    .select('*', { count: 'exact' })
    .range(from, to);
  
  if (error) throw error;
  return { data: data ?? [], count: count ?? 0 };
}

Filtering

export async function getFilteredTickets(
  filters: TicketFilters
): Promise<Ticket[]> {
  let query = supabase
    .from('tickets')
    .select('*')
    .eq('is_archived', false);
  
  if (filters.status) {
    query = query.eq('status', filters.status);
  }
  
  if (filters.location_id) {
    query = query.eq('location_id', filters.location_id);
  }
  
  if (filters.search) {
    query = query.or(`title.ilike.%${filters.search}%,requester.ilike.%${filters.search}%`);
  }
  
  const { data, error } = await query.order('id', { ascending: false });
  
  if (error) throw error;
  return data ?? [];
}

RPC Calls

export async function getTicketCounts(): Promise<TicketCounts> {
  const { data, error } = await supabase.rpc('ticket_counts', {
    p_location: null,
    p_term: null,
  });
  
  if (error) {
    console.error('RPC ticket_counts error:', error.message);
  }
  
  const counts: TicketCounts = {
    Pendiente: 0,
    'En Ejecución': 0,
    Finalizadas: 0,
  };
  
  (data ?? []).forEach((row: { status: string; total: number }) => {
    if (row.status) {
      counts[row.status] = row.total;
    }
  });
  
  return counts;
}

Mutations with Invalidation

import { invalidateData } from '../lib/dataInvalidation';

export async function updateTicket(
  id: number,
  updates: Partial<Ticket>
) {
  const { error } = await supabase
    .from('tickets')
    .update(updates)
    .eq('id', id);
  
  if (error) throw new Error(`Error al actualizar: ${error.message}`);
  
  // Invalidate cache to trigger refetch
  invalidateData('tickets');
}
Always call invalidateData() after mutations to ensure UI consistency.

Data Invalidation

The invalidateData function triggers cache invalidation for dependent contexts:
import { invalidateData } from '../lib/dataInvalidation';

// After creating a ticket
invalidateData('tickets');

// After updating user
invalidateData('users');

// After inventory mutation
invalidateData('inventory');

Listening for Invalidations

import { onDataInvalidated } from '../lib/dataInvalidation';

useEffect(() => {
  const unsubscribe = onDataInvalidated('tickets', () => {
    // Refetch tickets
    refetch();
  });
  
  return () => unsubscribe();
}, []);

Error Handling

Standard Error Pattern

export async function fetchData() {
  const { data, error } = await supabase
    .from('table')
    .select('*');
  
  if (error) {
    console.error('Error fetching data:', error.message);
    throw new Error(`Failed to fetch: ${error.message}`);
  }
  
  return data ?? [];
}

Session Validation

function isSessionMissingError(error: unknown): boolean {
  const msg = getErrorMessage(error).toLowerCase();
  return (
    msg.includes('auth session missing') ||
    msg.includes('session missing') ||
    msg.includes('invalid refresh token')
  );
}

Type Safety

Services enforce strict TypeScript types:
import type { Ticket, WorkOrder } from '../types/Ticket';

// Input validation
export async function createTicket(
  ticket: Omit<Ticket, 'id' | 'status' | 'created_by'>
): Promise<Pick<Ticket, 'id' | 'title'>> {
  // Implementation
}

// Return type enforcement
export async function getTicketById(id: number): Promise<WorkOrder | null> {
  // Implementation
}

Real-time Subscriptions

Services can set up real-time subscriptions:
export function subscribeToTicketChanges(
  ticketId: number,
  callback: (ticket: Ticket) => void
) {
  const channel = supabase
    .channel(`ticket_${ticketId}`)
    .on(
      'postgres_changes',
      {
        event: '*',
        schema: 'public',
        table: 'tickets',
        filter: `id=eq.${ticketId}`,
      },
      (payload) => {
        callback(payload.new as Ticket);
      }
    )
    .subscribe();
  
  return () => {
    supabase.removeChannel(channel);
  };
}

Best Practices

Only use the anon key in frontend code. Service role operations must happen in Edge Functions or server-side code.
if (!Number.isInteger(id) || id <= 0) {
  throw new Error('Invalid ID');
}
Prefer database views (like v_tickets_compat) over complex joins in the frontend:
// Good: Use a view
const { data } = await supabase.from('v_tickets_compat').select('*');

// Avoid: Complex joins in frontend
const { data } = await supabase
  .from('tickets')
  .select('*, users(*), locations(*)');
When new RPCs are introduced, provide fallbacks for environments that may not have them yet:
try {
  const { data } = await supabase.rpc('new_function');
  return data;
} catch (error) {
  // Fallback to old approach
  return await legacyFetch();
}

Next Steps

Components

Learn about the UI component library

RBAC

Understand permissions and access control

Build docs developers (and LLMs) love