mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 17:23:25 +00:00
ADD - build Housing phone app
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useHousingStore } from '@/stores/housing'
|
||||
import type { HousingOverview } from '@/types/housing'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
|
||||
vi.mock('@/utils/nui', () => ({ nuiCall: vi.fn() }))
|
||||
|
||||
const mockNuiCall = vi.mocked(nuiCall)
|
||||
const overview: HousingOverview = {
|
||||
available: true,
|
||||
provider: 'esx_property',
|
||||
properties: [
|
||||
{
|
||||
access: 'owner',
|
||||
capabilities: {
|
||||
cctv: true,
|
||||
garageStatus: true,
|
||||
keys: true,
|
||||
lock: true,
|
||||
waypoint: true,
|
||||
},
|
||||
cctv: { enabled: true },
|
||||
entrance: { x: 1, y: 2, z: 3 },
|
||||
garage: { enabled: true, storedVehicles: 2 },
|
||||
id: 'esx_property:1',
|
||||
keys: [],
|
||||
locked: true,
|
||||
name: 'Alta Street 3',
|
||||
providerId: '1',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe('housing store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
mockNuiCall.mockReset()
|
||||
})
|
||||
|
||||
it('loads a provider-backed housing overview', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({ data: overview, success: true })
|
||||
const housing = useHousingStore()
|
||||
|
||||
expect(await housing.load()).toBe(true)
|
||||
expect(housing.overview).toEqual(overview)
|
||||
expect(mockNuiCall).toHaveBeenCalledWith('housing:overview')
|
||||
})
|
||||
|
||||
it('represents a missing provider as an available response with offline data', async () => {
|
||||
const offline = { available: false, properties: [], provider: null }
|
||||
mockNuiCall.mockResolvedValueOnce({ data: offline, success: true })
|
||||
const housing = useHousingStore()
|
||||
|
||||
expect(await housing.load()).toBe(true)
|
||||
expect(housing.overview).toEqual(offline)
|
||||
})
|
||||
|
||||
it('loads only provider-authorized key candidates', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
data: { candidates: [{ id: 42, name: 'Alex Morgan' }] },
|
||||
success: true,
|
||||
})
|
||||
const housing = useHousingStore()
|
||||
|
||||
expect(await housing.loadKeyCandidates('esx_property:1')).toBe(true)
|
||||
expect(housing.candidates).toEqual([{ id: 42, name: 'Alex Morgan' }])
|
||||
})
|
||||
|
||||
it('refreshes the overview after a successful state mutation', async () => {
|
||||
mockNuiCall
|
||||
.mockResolvedValueOnce({ success: true })
|
||||
.mockResolvedValueOnce({ data: overview, success: true })
|
||||
const housing = useHousingStore()
|
||||
|
||||
expect(await housing.command('toggle_lock', 'esx_property:1')).toBe(true)
|
||||
expect(mockNuiCall).toHaveBeenNthCalledWith(1, 'housing:command', {
|
||||
action: 'toggle_lock',
|
||||
propertyId: 'esx_property:1',
|
||||
})
|
||||
expect(mockNuiCall).toHaveBeenNthCalledWith(2, 'housing:overview')
|
||||
})
|
||||
|
||||
it('keeps the overview and exposes a rejected action', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
error: 'property_access_denied',
|
||||
success: false,
|
||||
})
|
||||
const housing = useHousingStore()
|
||||
housing.overview = overview
|
||||
|
||||
expect(await housing.command('toggle_lock', 'esx_property:1')).toBe(false)
|
||||
expect(housing.overview).toEqual(overview)
|
||||
expect(housing.error).toBe('property_access_denied')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import type {
|
||||
HousingCommand,
|
||||
HousingKeyCandidate,
|
||||
HousingOverview,
|
||||
} from '@/types/housing'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
|
||||
export const useHousingStore = defineStore('housing', {
|
||||
state: () => ({
|
||||
candidates: [] as HousingKeyCandidate[],
|
||||
error: '',
|
||||
isLoading: false,
|
||||
isLoadingCandidates: false,
|
||||
overview: null as HousingOverview | null,
|
||||
pendingAction: '',
|
||||
}),
|
||||
actions: {
|
||||
async load(): Promise<boolean> {
|
||||
this.isLoading = true
|
||||
const response = await nuiCall<HousingOverview>('housing:overview')
|
||||
this.isLoading = false
|
||||
if (response.success && response.data) {
|
||||
this.overview = response.data
|
||||
this.error = ''
|
||||
return true
|
||||
}
|
||||
this.error = response.error ?? 'request_failed'
|
||||
return false
|
||||
},
|
||||
async loadKeyCandidates(propertyId: string): Promise<boolean> {
|
||||
this.isLoadingCandidates = true
|
||||
this.candidates = []
|
||||
const response = await nuiCall<{ candidates: HousingKeyCandidate[] }>(
|
||||
'housing:key-candidates',
|
||||
{ propertyId },
|
||||
)
|
||||
this.isLoadingCandidates = false
|
||||
if (response.success && response.data) {
|
||||
this.candidates = response.data.candidates
|
||||
this.error = ''
|
||||
return true
|
||||
}
|
||||
this.error = response.error ?? 'request_failed'
|
||||
return false
|
||||
},
|
||||
async command(
|
||||
action: HousingCommand,
|
||||
propertyId: string,
|
||||
payload: Record<string, unknown> = {},
|
||||
): Promise<boolean> {
|
||||
this.pendingAction = `${action}:${propertyId}`
|
||||
const response = await nuiCall('housing:command', {
|
||||
action,
|
||||
propertyId,
|
||||
...payload,
|
||||
})
|
||||
this.pendingAction = ''
|
||||
if (!response.success) {
|
||||
this.error = response.error ?? 'request_failed'
|
||||
return false
|
||||
}
|
||||
this.error = ''
|
||||
if (
|
||||
action === 'toggle_lock' ||
|
||||
action === 'grant_key' ||
|
||||
action === 'revoke_key'
|
||||
) {
|
||||
await this.load()
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1233,6 +1233,88 @@ const defaultLocales: LocaleTree = {
|
||||
default: 'SkyRide could not complete the request.',
|
||||
},
|
||||
},
|
||||
house: {
|
||||
name: 'House',
|
||||
subtitle: 'Homes and access',
|
||||
myHomes: 'My Homes',
|
||||
properties: 'Properties',
|
||||
owned: 'Owned',
|
||||
shared: 'Shared with you',
|
||||
locked: 'Locked',
|
||||
unlocked: 'Unlocked',
|
||||
owner: 'Owner',
|
||||
keyholder: 'Key holder',
|
||||
openDetails: 'Open property details',
|
||||
tryAgain: 'Try Again',
|
||||
refresh: 'Refresh',
|
||||
offline: 'House is offline',
|
||||
offlineBody:
|
||||
'Start a supported housing resource or review Config.Housing.',
|
||||
unavailable: 'Homes unavailable',
|
||||
empty: 'No Homes',
|
||||
emptyBody: 'Owned homes and homes shared with you will appear here.',
|
||||
provider: 'Housing data · {system}',
|
||||
access: 'Access',
|
||||
status: 'Status',
|
||||
actions: 'Controls',
|
||||
setWaypoint: 'Directions',
|
||||
viewCamera: 'View Camera',
|
||||
garage: 'Garage',
|
||||
garageEnabled: 'Enabled',
|
||||
garageDisabled: 'Unavailable',
|
||||
storedVehicles: '{count} stored',
|
||||
camera: 'Camera',
|
||||
cameraAvailable: 'Ready',
|
||||
cameraUnavailable: 'Unavailable',
|
||||
keys: 'Keys',
|
||||
manageKeys: 'Manage Keys',
|
||||
addKey: 'Add Key',
|
||||
revokeKey: 'Revoke',
|
||||
noKeys: 'No shared keys',
|
||||
noKeysBody: 'Only you currently have access to this home.',
|
||||
chooseResident: 'Give a Key',
|
||||
chooseResidentBody: 'Choose a person currently inside this property.',
|
||||
noCandidates: 'Nobody is waiting inside',
|
||||
noCandidatesBody:
|
||||
'A person must be inside the property before you can give them a key.',
|
||||
revokeTitle: 'Revoke Key?',
|
||||
revokeBody: '{name} will lose access to this property.',
|
||||
offlineKey: 'Offline',
|
||||
onlineKey: 'Online',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
lockSuccess: 'Door status updated.',
|
||||
waypointSuccess: 'Route set on your map.',
|
||||
keyGranted: 'Key granted.',
|
||||
keyRevoked: 'Key revoked.',
|
||||
cameraStarting: 'Opening camera…',
|
||||
errors: {
|
||||
provider_unavailable: 'The housing provider is unavailable.',
|
||||
provider_error: 'The housing provider could not be reached.',
|
||||
housing_unavailable: 'Housing data is unavailable for this character.',
|
||||
property_not_found: 'This property no longer exists.',
|
||||
property_access_denied: 'You no longer have access to this property.',
|
||||
owner_required: 'Only the property owner can do that.',
|
||||
invalid_property: 'Select a valid property.',
|
||||
invalid_action: 'This housing action is unavailable.',
|
||||
capability_unavailable:
|
||||
'This property provider does not support that control.',
|
||||
invalid_target: 'Select a valid resident.',
|
||||
target_not_in_property: 'That person is no longer inside the property.',
|
||||
key_not_found: 'That key no longer exists.',
|
||||
key_already_exists: 'That person already has access to this property.',
|
||||
keyholder_offline:
|
||||
'Offline keys must be revoked through the property menu.',
|
||||
cctv_unavailable: 'The camera is unavailable for this property.',
|
||||
provider_rejected: 'The housing provider rejected the request.',
|
||||
action_failed: 'The property change could not be saved.',
|
||||
rate_limited: 'Please wait before using another housing control.',
|
||||
device_not_open: 'Open the phone again to manage your homes.',
|
||||
device_locked: 'Unlock the phone to manage your homes.',
|
||||
request_failed: 'The housing request failed.',
|
||||
default: 'The housing request failed.',
|
||||
},
|
||||
},
|
||||
calculator: { name: 'Calculator' },
|
||||
snake: {
|
||||
name: 'Snake',
|
||||
|
||||
Reference in New Issue
Block a user