ADD - build Housing phone app

This commit is contained in:
Type
2026-08-09 22:12:38 +02:00
parent 6702cd8933
commit 2e4843cfbe
14 changed files with 1490 additions and 26 deletions
+97
View File
@@ -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')
})
})
+75
View File
@@ -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
},
},
})
+82
View File
@@ -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',