feat: map shapes, image persistence, grab tool, layer rename/delete, i18n, page metadata
This commit is contained in:
252
src/lib/api/map.ts
Normal file
252
src/lib/api/map.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user