auth flow + property switching
This commit is contained in:
@@ -4,6 +4,7 @@ import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-a
|
||||
import { Viewer } from '@pascal-app/viewer'
|
||||
import { useKeyboard } from '@/hooks/use-keyboard'
|
||||
import useEditor from '@/store/use-editor'
|
||||
import { usePropertyScene } from '@/features/cloud-sync/lib/models/hooks'
|
||||
import { ZoneSystem } from '../systems/zone/zone-system'
|
||||
import { ToolManager } from '../tools/tool-manager'
|
||||
import { ActionMenu } from '../ui/action-menu'
|
||||
@@ -16,8 +17,8 @@ import { ExportManager } from './export-manager'
|
||||
import { Grid } from './grid'
|
||||
import { SelectionManager } from './selection-manager'
|
||||
|
||||
// Load default scene initially (will be replaced when property loads)
|
||||
useScene.getState().loadScene()
|
||||
console.log('Loaded scene in editor')
|
||||
initSpatialGridSync()
|
||||
initSpaceDetectionSync(useScene, useEditor)
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ The cloud sync feature provides:
|
||||
|
||||
- **Authentication** - Sign in with magic link via Better Auth
|
||||
- **Property Management** - Create and manage properties with Google Maps address search
|
||||
- **Scene Loading** - Automatically load property scenes from the database when a property is selected
|
||||
- **Auto-Save** - Automatically save scene changes to the database (2-second debounce)
|
||||
- **Database Sync** - Save and load editor state from a PostgreSQL database via Supabase
|
||||
|
||||
## Architecture
|
||||
@@ -51,6 +53,13 @@ features/cloud-sync/
|
||||
3. Properties are associated with the authenticated user
|
||||
4. User can switch between properties
|
||||
|
||||
### Scene Management
|
||||
1. When a property is selected, its scene is loaded from `properties_models` table
|
||||
2. If no scene exists, loads default empty scene
|
||||
3. Scene changes are auto-saved every 2 seconds (debounced)
|
||||
4. Updates existing model (highest version) instead of creating new ones
|
||||
5. Scene graph includes all nodes and hierarchy
|
||||
|
||||
### Database Integration
|
||||
- Uses Supabase (PostgreSQL) for database access
|
||||
- Server actions use service role key to bypass RLS
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { Cloud } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useAuth } from '../lib/auth/hooks'
|
||||
import { usePropertyStore } from '../lib/properties/store'
|
||||
import { ProfileDropdown } from './profile-dropdown'
|
||||
import { PropertyDropdown } from './property-dropdown'
|
||||
import { SignInDialog } from './sign-in-dialog'
|
||||
@@ -16,6 +17,14 @@ import { SignInDialog } from './sign-in-dialog'
|
||||
export function CloudSaveButton() {
|
||||
const { isAuthenticated, isLoading } = useAuth()
|
||||
const [isSignInDialogOpen, setIsSignInDialogOpen] = useState(false)
|
||||
const initialize = usePropertyStore(state => state.initialize)
|
||||
|
||||
// Initialize property store when authenticated
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
initialize()
|
||||
}
|
||||
}, [isAuthenticated, initialize])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,7 @@ import { GoogleAddressSearch } from './google-address-search'
|
||||
interface NewPropertyDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSuccess?: () => void
|
||||
onSuccess?: (propertyId: string) => void
|
||||
}
|
||||
|
||||
interface AddressData {
|
||||
@@ -60,10 +60,10 @@ export function NewPropertyDialog({ open, onOpenChange, onSuccess }: NewProperty
|
||||
country: address.country || 'US',
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
if (result.success && result.data) {
|
||||
onOpenChange(false)
|
||||
setAddress(null)
|
||||
onSuccess?.()
|
||||
onSuccess?.(result.data.id)
|
||||
} else {
|
||||
setError(result.error || 'Failed to create property')
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Check, ChevronDown, Home, Plus } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useActiveProperty, useProperties } from '../lib/properties/hooks'
|
||||
import { usePropertyStore } from '../lib/properties/store'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -11,13 +11,21 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/primitives/dropdown-menu'
|
||||
import { NewPropertyDialog } from './new-property-dialog'
|
||||
import { usePropertyScene } from '../lib/models/hooks'
|
||||
|
||||
/**
|
||||
* PropertyDropdown - Shows active property and allows switching between properties
|
||||
*/
|
||||
export function PropertyDropdown() {
|
||||
const { properties, isLoading: propertiesLoading, refetch } = useProperties()
|
||||
const { activeProperty, setActiveProperty, isPending } = useActiveProperty()
|
||||
usePropertyScene() // Load and auto-save property scenes
|
||||
|
||||
// Use property store
|
||||
const properties = usePropertyStore(state => state.properties)
|
||||
const activeProperty = usePropertyStore(state => state.activeProperty)
|
||||
const isLoading = usePropertyStore(state => state.isLoading)
|
||||
const setActiveProperty = usePropertyStore(state => state.setActiveProperty)
|
||||
const fetchProperties = usePropertyStore(state => state.fetchProperties)
|
||||
|
||||
const [isNewPropertyDialogOpen, setIsNewPropertyDialogOpen] = useState(false)
|
||||
|
||||
const handlePropertySelect = async (propertyId: string) => {
|
||||
@@ -28,8 +36,9 @@ export function PropertyDropdown() {
|
||||
setIsNewPropertyDialogOpen(true)
|
||||
}
|
||||
|
||||
const handlePropertyCreated = () => {
|
||||
refetch()
|
||||
const handlePropertyCreated = async (propertyId: string) => {
|
||||
// Set the newly created property as active (this will also fetch properties)
|
||||
await setActiveProperty(propertyId)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -38,7 +47,7 @@ export function PropertyDropdown() {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className="flex h-9 items-center gap-2 rounded-lg border border-border bg-background/95 px-3 text-sm shadow-lg backdrop-blur-md transition-colors hover:bg-accent hover:text-accent-foreground disabled:opacity-50 focus:outline-none"
|
||||
disabled={propertiesLoading || isPending}
|
||||
disabled={isLoading}
|
||||
type="button"
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Property model actions - Server actions for scene loading/saving
|
||||
* Manages 3D models (scene graphs) stored in properties_models table
|
||||
*/
|
||||
|
||||
'use server'
|
||||
|
||||
import type { AnyNode, AnyNodeId } from '@pascal-app/core'
|
||||
import { createServerSupabaseClient } from '../database/server'
|
||||
import { getSession } from '../auth/server'
|
||||
import { createId } from '../utils/id-generator'
|
||||
import type { ActionResult } from '../properties/actions'
|
||||
|
||||
export interface SceneGraph {
|
||||
nodes: Record<AnyNodeId, AnyNode>
|
||||
rootNodeIds: AnyNodeId[]
|
||||
}
|
||||
|
||||
export interface PropertyModel {
|
||||
id: string
|
||||
name: string
|
||||
version: number
|
||||
draft: boolean
|
||||
property_id: string
|
||||
scene_graph: SceneGraph | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest model for a property (highest version)
|
||||
*/
|
||||
export async function getPropertyModel(propertyId: string): Promise<ActionResult<PropertyModel | null>> {
|
||||
try {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not authenticated',
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
const supabase = await createServerSupabaseClient()
|
||||
|
||||
// Get the property to verify ownership
|
||||
const { data: property, error: propertyError } = await supabase
|
||||
.from('properties')
|
||||
.select('id, owner_id')
|
||||
.eq('id', propertyId)
|
||||
.single()
|
||||
|
||||
if (propertyError || !property) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Property not found',
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
if (property.owner_id !== session.user.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
// Get the latest model (highest version, then most recent)
|
||||
const { data: model, error: modelError } = await supabase
|
||||
.from('properties_models')
|
||||
.select('*')
|
||||
.eq('property_id', propertyId)
|
||||
.is('deleted_at', null)
|
||||
.order('version', { ascending: false })
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.single()
|
||||
|
||||
console.log('[getPropertyModel] Query result:', {
|
||||
propertyId,
|
||||
hasModel: !!model,
|
||||
modelError: modelError?.message,
|
||||
errorCode: modelError?.code,
|
||||
modelKeys: model ? Object.keys(model) : [],
|
||||
})
|
||||
|
||||
if (modelError) {
|
||||
// No model found is not an error - just return null
|
||||
if (modelError.code === 'PGRST116') {
|
||||
console.log('[getPropertyModel] No model found (PGRST116), returning null')
|
||||
return {
|
||||
success: true,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[getPropertyModel] Database error:', modelError)
|
||||
return {
|
||||
success: false,
|
||||
error: modelError.message,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[getPropertyModel] Model found:', {
|
||||
id: model.id,
|
||||
version: model.version,
|
||||
hasSceneGraph: !!model.scene_graph,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: model as PropertyModel,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch property model',
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or update a property model's scene graph
|
||||
* If a model exists, updates it. Otherwise creates a new one.
|
||||
*/
|
||||
export async function savePropertyModel(
|
||||
propertyId: string,
|
||||
sceneGraph: SceneGraph,
|
||||
): Promise<ActionResult<PropertyModel>> {
|
||||
try {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not authenticated',
|
||||
}
|
||||
}
|
||||
|
||||
const supabase = await createServerSupabaseClient()
|
||||
|
||||
// Get the property to verify ownership
|
||||
const { data: property, error: propertyError } = await supabase
|
||||
.from('properties')
|
||||
.select('id, owner_id, name')
|
||||
.eq('id', propertyId)
|
||||
.single()
|
||||
|
||||
if (propertyError || !property) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Property not found',
|
||||
}
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
if (property.owner_id !== session.user.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a model already exists
|
||||
const { data: existingModel } = await supabase
|
||||
.from('properties_models')
|
||||
.select('id, version')
|
||||
.eq('property_id', propertyId)
|
||||
.is('deleted_at', null)
|
||||
.order('version', { ascending: false })
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.single()
|
||||
|
||||
if (existingModel) {
|
||||
// Update existing model
|
||||
const { data: updatedModel, error: updateError } = await supabase
|
||||
.from('properties_models')
|
||||
.update({
|
||||
scene_graph: sceneGraph as any,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', existingModel.id)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (updateError) {
|
||||
return {
|
||||
success: false,
|
||||
error: updateError.message,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedModel as PropertyModel,
|
||||
message: 'Model updated successfully',
|
||||
}
|
||||
} else {
|
||||
// Create new model
|
||||
const modelId = createId('model')
|
||||
|
||||
const { data: newModel, error: createError } = await supabase
|
||||
.from('properties_models')
|
||||
.insert({
|
||||
id: modelId,
|
||||
property_id: propertyId,
|
||||
name: `${property.name} - Editor`,
|
||||
version: 1,
|
||||
draft: true,
|
||||
scene_graph: sceneGraph as any,
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (createError) {
|
||||
return {
|
||||
success: false,
|
||||
error: createError.message,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: newModel as PropertyModel,
|
||||
message: 'Model created successfully',
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to save property model',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Hooks for property model (scene) loading and auto-saving
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { useScene } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import useEditor from '@/store/use-editor'
|
||||
import { usePropertyStore } from '../properties/store'
|
||||
import { getPropertyModel, savePropertyModel } from './actions'
|
||||
|
||||
/**
|
||||
* Load the scene when a property becomes active
|
||||
* Saves changes automatically with debouncing
|
||||
*/
|
||||
export function usePropertyScene() {
|
||||
// Subscribe to property store
|
||||
const activeProperty = usePropertyStore((state) => state.activeProperty)
|
||||
const isLoadingProperty = usePropertyStore((state) => state.isLoading)
|
||||
|
||||
const lastPropertyIdRef = useRef<string | null>(null)
|
||||
const saveTimeoutRef = useRef<NodeJS.Timeout>()
|
||||
const isSavingRef = useRef(false)
|
||||
const currentPropertyIdRef = useRef<string | null>(null)
|
||||
|
||||
// Extract property ID for dependency tracking
|
||||
const propertyId = activeProperty?.id ?? null
|
||||
const propertyName = activeProperty?.name ?? null
|
||||
|
||||
// Load scene when active property changes
|
||||
useEffect(() => {
|
||||
if (isLoadingProperty) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!propertyId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip if same property
|
||||
if (lastPropertyIdRef.current === propertyId) {
|
||||
return
|
||||
}
|
||||
|
||||
lastPropertyIdRef.current = propertyId
|
||||
|
||||
// Load the property's scene
|
||||
async function loadScene() {
|
||||
try {
|
||||
const result = await getPropertyModel(propertyId || '')
|
||||
|
||||
if (result.success && result.data?.scene_graph) {
|
||||
// Load the scene graph into the store
|
||||
const { nodes, rootNodeIds } = result.data.scene_graph
|
||||
useScene.getState().setScene(nodes, rootNodeIds)
|
||||
} else {
|
||||
// No scene found - clear the scene
|
||||
useScene.getState().clearScene()
|
||||
}
|
||||
} catch (error) {
|
||||
// Fall back to clear scene
|
||||
useScene.getState().clearScene()
|
||||
}
|
||||
|
||||
// Reset editor state after loading/clearing scene
|
||||
useEditor.getState().setPhase('site')
|
||||
useViewer.getState().setSelection({
|
||||
buildingId: null,
|
||||
levelId: null,
|
||||
selectedIds: [],
|
||||
zoneId: null,
|
||||
})
|
||||
}
|
||||
|
||||
loadScene()
|
||||
}, [propertyId, isLoadingProperty])
|
||||
|
||||
// Auto-save scene changes with debouncing
|
||||
useEffect(() => {
|
||||
if (!propertyId) {
|
||||
currentPropertyIdRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
currentPropertyIdRef.current = propertyId
|
||||
|
||||
// Subscribe to any scene changes
|
||||
// Use JSON stringification to detect any node changes, not just count
|
||||
let lastNodesSnapshot = JSON.stringify(useScene.getState().nodes)
|
||||
|
||||
const unsubscribe = useScene.subscribe((state) => {
|
||||
const currentNodesSnapshot = JSON.stringify(state.nodes)
|
||||
|
||||
// Only trigger save if nodes actually changed
|
||||
if (currentNodesSnapshot === lastNodesSnapshot) {
|
||||
return
|
||||
}
|
||||
|
||||
lastNodesSnapshot = currentNodesSnapshot
|
||||
const nodes = state.nodes
|
||||
|
||||
// Skip if currently saving
|
||||
if (isSavingRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
// Clear existing timeout
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current)
|
||||
}
|
||||
|
||||
// Debounce save by 2 seconds
|
||||
saveTimeoutRef.current = setTimeout(async () => {
|
||||
// Get the current property ID at save time (not the captured value)
|
||||
const currentPropertyId = currentPropertyIdRef.current
|
||||
if (!currentPropertyId) {
|
||||
return
|
||||
}
|
||||
|
||||
const rootNodeIds = useScene.getState().rootNodeIds
|
||||
const sceneGraph = { nodes, rootNodeIds }
|
||||
|
||||
isSavingRef.current = true
|
||||
|
||||
try {
|
||||
await savePropertyModel(currentPropertyId, sceneGraph)
|
||||
} finally {
|
||||
isSavingRef.current = false
|
||||
}
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
return () => {
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current)
|
||||
}
|
||||
unsubscribe()
|
||||
}
|
||||
}, [propertyId])
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
getActiveProperty,
|
||||
getUserProperties,
|
||||
@@ -116,8 +116,9 @@ export function useActiveProperty() {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const isInitialFetchRef = useRef(true)
|
||||
|
||||
const fetchActiveProperty = useCallback(async () => {
|
||||
const fetchActiveProperty = useCallback(async (allowAutoSelect = false) => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
@@ -128,7 +129,9 @@ export function useActiveProperty() {
|
||||
setActivePropertyState(result.data || null)
|
||||
|
||||
// If no active property is set, automatically set the first property as active
|
||||
if (!result.data) {
|
||||
// Only do this on initial mount to avoid interfering with property selection/creation
|
||||
if (!result.data && allowAutoSelect) {
|
||||
console.log('[useActiveProperty] No active property, checking if we should auto-select')
|
||||
const propertiesResult = await getUserProperties()
|
||||
|
||||
if (
|
||||
@@ -136,15 +139,19 @@ export function useActiveProperty() {
|
||||
propertiesResult.data &&
|
||||
propertiesResult.data.length > 0
|
||||
) {
|
||||
console.log('[useActiveProperty] Found properties, auto-selecting first one')
|
||||
const firstProperty = propertiesResult.data[0]
|
||||
if (firstProperty) {
|
||||
const setActiveResult = await setActivePropertyAction(firstProperty.id)
|
||||
|
||||
if (setActiveResult.success) {
|
||||
console.log('[useActiveProperty] Auto-selected property:', firstProperty.name)
|
||||
setActivePropertyState(firstProperty)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!result.data && !allowAutoSelect) {
|
||||
console.log('[useActiveProperty] No active property but auto-select is disabled')
|
||||
}
|
||||
} else {
|
||||
setError(result.error || 'Failed to fetch active property')
|
||||
@@ -162,20 +169,44 @@ export function useActiveProperty() {
|
||||
async (propertyId: string | null) => {
|
||||
try {
|
||||
setIsPending(true)
|
||||
setIsLoading(true)
|
||||
const result = await setActivePropertyAction(propertyId)
|
||||
|
||||
if (result.success) {
|
||||
// Fetch the updated active property
|
||||
if (propertyId) {
|
||||
await fetchActiveProperty()
|
||||
// Fetch the property data and set it immediately
|
||||
console.log('[useActiveProperty] Fetching property data for ID:', propertyId)
|
||||
const propertiesResult = await getUserProperties()
|
||||
|
||||
if (propertiesResult.success && propertiesResult.data) {
|
||||
const selectedProperty = propertiesResult.data.find(p => p.id === propertyId)
|
||||
if (selectedProperty) {
|
||||
console.log('[useActiveProperty] Found property, setting as active:', selectedProperty.name)
|
||||
console.log('[useActiveProperty] Current isLoading state:', isLoading)
|
||||
setActivePropertyState(selectedProperty)
|
||||
setIsLoading(false)
|
||||
console.log('[useActiveProperty] Set isLoading to false')
|
||||
} else {
|
||||
console.error('[useActiveProperty] Property not found in user properties')
|
||||
// Fall back to refetch
|
||||
await fetchActiveProperty(false)
|
||||
}
|
||||
} else {
|
||||
console.error('[useActiveProperty] Failed to fetch properties')
|
||||
// Fall back to refetch
|
||||
await fetchActiveProperty(false)
|
||||
}
|
||||
} else {
|
||||
setActivePropertyState(null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
} else {
|
||||
console.error(result.error || 'Failed to update active property')
|
||||
setIsLoading(false)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err instanceof Error ? err.message : 'An unexpected error occurred')
|
||||
setIsLoading(false)
|
||||
} finally {
|
||||
setIsPending(false)
|
||||
}
|
||||
@@ -184,7 +215,12 @@ export function useActiveProperty() {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
fetchActiveProperty()
|
||||
// Only allow auto-select on the initial mount
|
||||
const allowAutoSelect = isInitialFetchRef.current
|
||||
if (isInitialFetchRef.current) {
|
||||
isInitialFetchRef.current = false
|
||||
}
|
||||
fetchActiveProperty(allowAutoSelect)
|
||||
}, [fetchActiveProperty])
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Property store - Zustand store for property state management
|
||||
*/
|
||||
|
||||
import { create } from 'zustand'
|
||||
import type { Property } from './types'
|
||||
import {
|
||||
getActiveProperty,
|
||||
getUserProperties,
|
||||
setActiveProperty as setActivePropertyAction,
|
||||
} from './actions'
|
||||
|
||||
interface PropertyStore {
|
||||
// State
|
||||
activeProperty: Property | null
|
||||
properties: Property[]
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
|
||||
// Actions
|
||||
fetchProperties: () => Promise<void>
|
||||
fetchActiveProperty: () => Promise<void>
|
||||
setActiveProperty: (propertyId: string) => Promise<void>
|
||||
initialize: () => Promise<void>
|
||||
}
|
||||
|
||||
export const usePropertyStore = create<PropertyStore>((set, get) => ({
|
||||
// Initial state
|
||||
activeProperty: null,
|
||||
properties: [],
|
||||
isLoading: true,
|
||||
error: null,
|
||||
|
||||
// Fetch all properties
|
||||
fetchProperties: async () => {
|
||||
const result = await getUserProperties()
|
||||
|
||||
if (result.success) {
|
||||
set({ properties: result.data || [], error: null })
|
||||
} else {
|
||||
set({ error: result.error || 'Failed to fetch properties', properties: [] })
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch the active property from database
|
||||
fetchActiveProperty: async () => {
|
||||
set({ isLoading: true })
|
||||
|
||||
const result = await getActiveProperty()
|
||||
|
||||
if (result.success) {
|
||||
set({
|
||||
activeProperty: result.data || null,
|
||||
isLoading: false,
|
||||
error: null
|
||||
})
|
||||
|
||||
// If no active property, auto-select the first one
|
||||
if (!result.data) {
|
||||
const propertiesResult = await getUserProperties()
|
||||
if (propertiesResult.success && propertiesResult.data && propertiesResult.data.length > 0) {
|
||||
const firstProperty = propertiesResult.data[0]
|
||||
if (firstProperty) {
|
||||
await get().setActiveProperty(firstProperty.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
set({
|
||||
error: result.error || 'Failed to fetch active property',
|
||||
activeProperty: null,
|
||||
isLoading: false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Set active property
|
||||
setActiveProperty: async (propertyId: string) => {
|
||||
set({ isLoading: true })
|
||||
|
||||
// Update database
|
||||
const result = await setActivePropertyAction(propertyId)
|
||||
|
||||
if (result.success) {
|
||||
// Fetch properties to get the full property object
|
||||
const propertiesResult = await getUserProperties()
|
||||
|
||||
if (propertiesResult.success && propertiesResult.data) {
|
||||
const selectedProperty = propertiesResult.data.find(p => p.id === propertyId)
|
||||
|
||||
if (selectedProperty) {
|
||||
set({
|
||||
activeProperty: selectedProperty,
|
||||
properties: propertiesResult.data,
|
||||
isLoading: false,
|
||||
error: null
|
||||
})
|
||||
} else {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: 'Property not found'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: 'Failed to fetch properties'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: result.error || 'Failed to set active property'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Initialize - fetch both properties and active property
|
||||
initialize: async () => {
|
||||
set({ isLoading: true })
|
||||
await Promise.all([
|
||||
get().fetchProperties(),
|
||||
get().fetchActiveProperty(),
|
||||
])
|
||||
},
|
||||
}))
|
||||
Reference in New Issue
Block a user