Files
root-org/src/lib/api/map.ts
AlacrisDevs 4ee2c0ac07 Map push
2026-02-08 23:51:49 +02:00

301 lines
7.3 KiB
TypeScript

import type { SupabaseClient } from '@supabase/supabase-js';
export interface MapLayer {
id: string;
department_id: string;
name: string;
layer_type: 'osm' | 'image';
image_url: string | null;
image_width: number | null;
image_height: number | null;
center_lat: number;
center_lng: number;
zoom_level: number;
sort_order: number;
created_by: string | null;
created_at: string;
updated_at: string;
}
export interface MapPin {
id: string;
layer_id: string;
label: string;
description: string;
color: string;
lat: number;
lng: number;
bounds_north: number | null;
bounds_south: number | null;
bounds_east: number | null;
bounds_west: number | null;
sort_order: number;
created_by: string | null;
created_at: string;
updated_at: string;
}
export interface MapShape {
id: string;
layer_id: string;
shape_type: 'polygon' | 'rectangle';
label: string;
color: string;
fill_opacity: number;
stroke_width: number;
vertices: [number, number][];
rotation: number;
sort_order: number;
created_by: string | null;
created_at: string;
updated_at: string;
}
export interface MapLayerWithPins extends MapLayer {
pins: MapPin[];
shapes: MapShape[];
}
// ── Layers ──
export async function fetchMapLayers(supabase: SupabaseClient, departmentId: string): Promise<MapLayerWithPins[]> {
const { data: layers, error: layerErr } = await (supabase as any)
.from('map_layers')
.select('*')
.eq('department_id', departmentId)
.order('sort_order');
if (layerErr) throw layerErr;
if (!layers || layers.length === 0) return [];
const layerIds = layers.map((l: any) => l.id);
const [pinResult, shapeResult] = await Promise.all([
(supabase as any).from('map_pins').select('*').in('layer_id', layerIds).order('sort_order'),
(supabase as any).from('map_shapes').select('*').in('layer_id', layerIds).order('sort_order'),
]);
if (pinResult.error) throw pinResult.error;
if (shapeResult.error) throw shapeResult.error;
const pins = pinResult.data ?? [];
const shapes = shapeResult.data ?? [];
return layers.map((layer: any) => ({
...layer,
pins: pins.filter((p: any) => p.layer_id === layer.id),
shapes: shapes.filter((s: any) => s.layer_id === layer.id),
}));
}
export async function createMapLayer(
supabase: SupabaseClient,
departmentId: string,
data: {
name: string;
layer_type: 'osm' | 'image';
image_url?: string;
image_width?: number;
image_height?: number;
center_lat?: number;
center_lng?: number;
zoom_level?: number;
sort_order?: number;
},
): Promise<MapLayer> {
const { data: layer, error } = await (supabase as any)
.from('map_layers')
.insert({
department_id: departmentId,
...data,
})
.select()
.single();
if (error) throw error;
return layer;
}
export async function updateMapLayer(
supabase: SupabaseClient,
layerId: string,
data: Partial<Pick<MapLayer, 'name' | 'layer_type' | 'image_url' | 'image_width' | 'image_height' | 'center_lat' | 'center_lng' | 'zoom_level' | 'sort_order'>>,
): Promise<MapLayer> {
const { data: layer, error } = await (supabase as any)
.from('map_layers')
.update({ ...data, updated_at: new Date().toISOString() })
.eq('id', layerId)
.select()
.single();
if (error) throw error;
return layer;
}
export async function deleteMapLayer(supabase: SupabaseClient, layerId: string): Promise<void> {
const { error } = await (supabase as any)
.from('map_layers')
.delete()
.eq('id', layerId);
if (error) throw error;
}
// ── Pins ──
export async function createMapPin(
supabase: SupabaseClient,
layerId: string,
data: {
label: string;
description?: string;
color?: string;
lat: number;
lng: number;
bounds_north?: number;
bounds_south?: number;
bounds_east?: number;
bounds_west?: number;
sort_order?: number;
},
): Promise<MapPin> {
const { data: pin, error } = await (supabase as any)
.from('map_pins')
.insert({
layer_id: layerId,
...data,
})
.select()
.single();
if (error) throw error;
return pin;
}
export async function updateMapPin(
supabase: SupabaseClient,
pinId: string,
data: Partial<Pick<MapPin, 'label' | 'description' | 'color' | 'lat' | 'lng' | 'bounds_north' | 'bounds_south' | 'bounds_east' | 'bounds_west' | 'sort_order'>>,
): Promise<MapPin> {
const { data: pin, error } = await (supabase as any)
.from('map_pins')
.update({ ...data, updated_at: new Date().toISOString() })
.eq('id', pinId)
.select()
.single();
if (error) throw error;
return pin;
}
export async function deleteMapPin(supabase: SupabaseClient, pinId: string): Promise<void> {
const { error } = await (supabase as any)
.from('map_pins')
.delete()
.eq('id', pinId);
if (error) throw error;
}
// ── Shapes ──
export async function createMapShape(
supabase: SupabaseClient,
layerId: string,
data: {
shape_type: 'polygon' | 'rectangle';
label?: string;
color?: string;
fill_opacity?: number;
stroke_width?: number;
vertices: [number, number][];
rotation?: number;
sort_order?: number;
},
): Promise<MapShape> {
const { data: shape, error } = await (supabase as any)
.from('map_shapes')
.insert({
layer_id: layerId,
...data,
})
.select()
.single();
if (error) throw error;
return shape;
}
export async function updateMapShape(
supabase: SupabaseClient,
shapeId: string,
data: Partial<Pick<MapShape, 'label' | 'color' | 'fill_opacity' | 'stroke_width' | 'vertices' | 'rotation' | 'sort_order'>>,
): Promise<MapShape> {
const { data: shape, error } = await (supabase as any)
.from('map_shapes')
.update({ ...data, updated_at: new Date().toISOString() })
.eq('id', shapeId)
.select()
.single();
if (error) throw error;
return shape;
}
export async function deleteMapShape(supabase: SupabaseClient, shapeId: string): Promise<void> {
const { error } = await (supabase as any)
.from('map_shapes')
.delete()
.eq('id', shapeId);
if (error) throw error;
}
// ── Realtime ──
export interface RealtimeMapPayload<T> {
event: 'INSERT' | 'UPDATE' | 'DELETE';
new: T;
old: Partial<T>;
}
export function subscribeToMapLayers(
supabase: SupabaseClient,
layerIds: string[],
onPinChange: (payload: RealtimeMapPayload<MapPin>) => void,
onShapeChange: (payload: RealtimeMapPayload<MapShape>) => void,
) {
const layerIdSet = new Set(layerIds);
const channelName = `map:${layerIds[0]?.slice(0, 8) ?? 'x'}-${Date.now()}`;
const channel = supabase.channel(channelName);
channel
.on('postgres_changes', { event: '*', schema: 'public', table: 'map_pins' },
(payload) => {
const pin = (payload.new ?? payload.old) as Partial<MapPin>;
const lid = pin.layer_id ?? (payload.old as Partial<MapPin>)?.layer_id;
if (lid && !layerIdSet.has(lid)) return;
onPinChange({
event: payload.eventType as 'INSERT' | 'UPDATE' | 'DELETE',
new: payload.new as MapPin,
old: payload.old as Partial<MapPin>,
});
}
)
.on('postgres_changes', { event: '*', schema: 'public', table: 'map_shapes' },
(payload) => {
const shape = (payload.new ?? payload.old) as Partial<MapShape>;
const lid = shape.layer_id ?? (payload.old as Partial<MapShape>)?.layer_id;
if (lid && !layerIdSet.has(lid)) return;
onShapeChange({
event: payload.eventType as 'INSERT' | 'UPDATE' | 'DELETE',
new: payload.new as MapShape,
old: payload.old as Partial<MapShape>,
});
}
)
.subscribe();
return channel;
}