Skip to content

Commit dc5bb51

Browse files
authored
Merge pull request #232 from Resgrid/develop
RU-T49 Adding routes and more langs
2 parents a063f07 + 23ad2dd commit dc5bb51

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+11816
-118
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ Gemfile
3737

3838
expo-env.d.ts
3939
# @end expo-cli
40+
.dual-graph/

CLAUDE.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<!-- dgc-policy-v10 -->
2+
# Dual-Graph Context Policy
3+
4+
This project uses a local dual-graph MCP server for efficient context retrieval.
5+
6+
## MANDATORY: Always follow this order
7+
8+
1. **Call `graph_continue` first** — before any file exploration, grep, or code reading.
9+
10+
2. **If `graph_continue` returns `needs_project=true`**: call `graph_scan` with the
11+
current project directory (`pwd`). Do NOT ask the user.
12+
13+
3. **If `graph_continue` returns `skip=true`**: project has fewer than 5 files.
14+
Do NOT do broad or recursive exploration. Read only specific files if their names
15+
are mentioned, or ask the user what to work on.
16+
17+
4. **Read `recommended_files`** using `graph_read`**one call per file**.
18+
- `graph_read` accepts a single `file` parameter (string). Call it separately for each
19+
recommended file. Do NOT pass an array or batch multiple files into one call.
20+
- `recommended_files` may contain `file::symbol` entries (e.g. `src/auth.ts::handleLogin`).
21+
Pass them verbatim to `graph_read(file: "src/auth.ts::handleLogin")` — it reads only
22+
that symbol's lines, not the full file.
23+
- Example: if `recommended_files` is `["src/auth.ts::handleLogin", "src/db.ts"]`,
24+
call `graph_read(file: "src/auth.ts::handleLogin")` and `graph_read(file: "src/db.ts")`
25+
as two separate calls (they can be parallel).
26+
27+
5. **Check `confidence` and obey the caps strictly:**
28+
- `confidence=high` -> Stop. Do NOT grep or explore further.
29+
- `confidence=medium` -> If recommended files are insufficient, call `fallback_rg`
30+
at most `max_supplementary_greps` time(s) with specific terms, then `graph_read`
31+
at most `max_supplementary_files` additional file(s). Then stop.
32+
- `confidence=low` -> Call `fallback_rg` at most `max_supplementary_greps` time(s),
33+
then `graph_read` at most `max_supplementary_files` file(s). Then stop.
34+
35+
## Token Usage
36+
37+
A `token-counter` MCP is available for tracking live token usage.
38+
39+
- To check how many tokens a large file or text will cost **before** reading it:
40+
`count_tokens({text: "<content>"})`
41+
- To log actual usage after a task completes (if the user asks):
42+
`log_usage({input_tokens: <est>, output_tokens: <est>, description: "<task>"})`
43+
- To show the user their running session cost:
44+
`get_session_stats()`
45+
46+
Live dashboard URL is printed at startup next to "Token usage".
47+
48+
## Rules
49+
50+
- Do NOT use `rg`, `grep`, or bash file exploration before calling `graph_continue`.
51+
- Do NOT do broad/recursive exploration at any confidence level.
52+
- `max_supplementary_greps` and `max_supplementary_files` are hard caps - never exceed them.
53+
- Do NOT dump full chat history.
54+
- Do NOT call `graph_retrieve` more than once per turn.
55+
- After edits, call `graph_register_edit` with the changed files. Use `file::symbol` notation (e.g. `src/auth.ts::handleLogin`) when the edit targets a specific function, class, or hook.
56+
57+
## Context Store
58+
59+
Whenever you make a decision, identify a task, note a next step, fact, or blocker during a conversation, append it to `.dual-graph/context-store.json`.
60+
61+
**Entry format:**
62+
```json
63+
{"type": "decision|task|next|fact|blocker", "content": "one sentence max 15 words", "tags": ["topic"], "files": ["relevant/file.ts"], "date": "YYYY-MM-DD"}
64+
```
65+
66+
**To append:** Read the file → add the new entry to the array → Write it back → call `graph_register_edit` on `.dual-graph/context-store.json`.
67+
68+
**Rules:**
69+
- Only log things worth remembering across sessions (not every minor detail)
70+
- `content` must be under 15 words
71+
- `files` lists the files this decision/task relates to (can be empty)
72+
- Log immediately when the item arises — not at session end
73+
74+
## Session End
75+
76+
When the user signals they are done (e.g. "bye", "done", "wrap up", "end session"), proactively update `CONTEXT.md` in the project root with:
77+
- **Current Task**: one sentence on what was being worked on
78+
- **Key Decisions**: bullet list, max 3 items
79+
- **Next Steps**: bullet list, max 3 items
80+
81+
Keep `CONTEXT.md` under 20 lines total. Do NOT summarize the full conversation — only what's needed to resume next session.
82+
83+
---
84+
85+
# Project: Resgrid Unit (React Native / Expo)
86+
87+
## Tech Stack
88+
89+
TypeScript · React Native · Expo (managed, prebuild) · Zustand · React Query · React Hook Form · react-i18next · react-native-mmkv · Axios · @rnmapbox/maps · gluestack-ui · lucide-react-native
90+
91+
## Code Style
92+
93+
- Write concise, type-safe TypeScript. Avoid `any`; use precise types and interfaces for props/state.
94+
- Use functional components and hooks; never class components. Use `React.FC` for typed components.
95+
- Enable strict mode in `tsconfig.json`.
96+
- Organize files by feature, grouping related components, hooks, and styles.
97+
- All components must be mobile-friendly and responsive, supporting both iOS and Android.
98+
- This is an Expo managed project using prebuild — **do not make native code changes** outside Expo prebuild capabilities.
99+
100+
## Naming Conventions
101+
102+
- Variables and functions: `camelCase` (e.g., `isFetchingData`, `handleUserInput`)
103+
- Components: `PascalCase` (e.g., `UserProfile`, `ChatScreen`)
104+
- Files and directories: `lowercase-hyphenated` (e.g., `user-profile.tsx`, `chat-screen/`)
105+
106+
## Styling
107+
108+
- Use `gluestack-ui` components from `components/ui` when available.
109+
- For anything without a Gluestack component, use `StyleSheet.create()` or Styled Components.
110+
- Support both **dark mode and light mode**.
111+
- Follow WCAG accessibility guidelines for mobile.
112+
113+
## Performance
114+
115+
- Minimize `useEffect`, `useState`, and heavy computation inside render methods.
116+
- Use `React.memo()` for components with static props.
117+
- Optimize `FlatList` with `removeClippedSubviews`, `maxToRenderPerBatch`, `windowSize`, and `getItemLayout` when items have a consistent size.
118+
- Avoid anonymous functions in `renderItem` or event handlers.
119+
120+
## Internationalization
121+
122+
- All user-visible text **must** be wrapped in `t()` from `react-i18next`.
123+
- Translation dictionary files live in `src/translations/`.
124+
125+
## Libraries — use these, not alternatives
126+
127+
| Purpose | Library |
128+
|---|---|
129+
| Package manager | `yarn` |
130+
| State management | `zustand` |
131+
| Data fetching | `react-query` |
132+
| Forms | `react-hook-form` |
133+
| i18n | `react-i18next` |
134+
| Local storage | `react-native-mmkv` |
135+
| Secure storage | Expo SecureStore |
136+
| HTTP | `axios` |
137+
| Maps / navigation | `@rnmapbox/maps` |
138+
| Icons | `lucide-react-native` (use directly in markup, not via gluestack Icon wrapper) |
139+
140+
## Conditional Rendering
141+
142+
Use ternary `? :` for conditional rendering — **never `&&`**.
143+
144+
## Testing
145+
146+
- Use Jest. Generate tests for all new components, services, and logic.
147+
- Ensure tests run without errors before considering a task done.
148+
149+
## Best Practices
150+
151+
- Follow React Native's threading model for smooth UI performance.
152+
- Use React Navigation for navigation and deep linking.
153+
- Handle errors gracefully and provide user feedback.
154+
- Implement proper offline support.
155+
- Optimize for low-end devices.

jest-setup.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
import '@testing-library/react-native/extend-expect';
22

3+
// Mock @sentry/react-native — native module (RNSentry) is unavailable in Jest
4+
jest.mock('@sentry/react-native', () => ({
5+
captureException: jest.fn(),
6+
captureMessage: jest.fn(),
7+
init: jest.fn(),
8+
wrap: jest.fn((fn: any) => fn),
9+
withScope: jest.fn((cb: any) => cb({ setExtra: jest.fn(), setTag: jest.fn() })),
10+
setUser: jest.fn(),
11+
setTag: jest.fn(),
12+
setExtra: jest.fn(),
13+
addBreadcrumb: jest.fn(),
14+
configureScope: jest.fn(),
15+
ReactNavigationInstrumentation: jest.fn(),
16+
ReactNativeTracing: jest.fn(),
17+
}));
18+
319
// react-hook form setup for testing
420
// @ts-ignore
521
global.window = {};

src/api/mapping/mapping.ts

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,59 @@
1+
import { type FeatureCollection } from 'geojson';
2+
3+
import { getBaseApiUrl } from '@/lib/storage/app';
14
import { type GetMapDataAndMarkersResult } from '@/models/v4/mapping/getMapDataAndMarkersResult';
25
import { type GetMapLayersResult } from '@/models/v4/mapping/getMapLayersResult';
6+
import {
7+
type GetAllActiveLayersResult,
8+
type GetCustomMapLayerResult,
9+
type GetCustomMapResult,
10+
type GetCustomMapsResult,
11+
type GetGeoJSONResult,
12+
type GetIndoorMapFloorResult,
13+
type GetIndoorMapResult,
14+
type GetIndoorMapsResult,
15+
type SearchAllMapFeaturesResult,
16+
type SearchCustomMapRegionsResult,
17+
type SearchIndoorLocationsResult,
18+
} from '@/models/v4/mapping/mappingResults';
319

20+
import { createCachedApiEndpoint } from '../common/cached-client';
421
import { createApiEndpoint } from '../common/client';
522

623
const getMayLayersApi = createApiEndpoint('/Mapping/GetMayLayers');
7-
824
const getMapDataAndMarkersApi = createApiEndpoint('/Mapping/GetMapDataAndMarkers');
925

26+
// Indoor map endpoints
27+
const getIndoorMapsApi = createCachedApiEndpoint('/Mapping/GetIndoorMaps', {
28+
ttl: 5 * 60 * 1000,
29+
enabled: true,
30+
});
31+
const getIndoorMapApi = createApiEndpoint('/Mapping/GetIndoorMap');
32+
const getIndoorMapFloorApi = createApiEndpoint('/Mapping/GetIndoorMapFloor');
33+
const getIndoorMapZonesGeoJSONApi = createApiEndpoint('/Mapping/GetIndoorMapZonesGeoJSON');
34+
const searchIndoorLocationsApi = createApiEndpoint('/Mapping/SearchIndoorLocations');
35+
const getNearbyIndoorMapsApi = createApiEndpoint('/Mapping/GetNearbyIndoorMaps');
36+
37+
// Custom map endpoints
38+
const getCustomMapsApi = createCachedApiEndpoint('/Mapping/GetCustomMaps', {
39+
ttl: 5 * 60 * 1000,
40+
enabled: true,
41+
});
42+
const getCustomMapApi = createApiEndpoint('/Mapping/GetCustomMap');
43+
const getCustomMapLayerApi = createApiEndpoint('/Mapping/GetCustomMapLayer');
44+
const getMapLayerGeoJSONApi = createApiEndpoint('/Mapping/GetMapLayerGeoJSON');
45+
const getCustomMapRegionsGeoJSONApi = createApiEndpoint('/Mapping/GetCustomMapRegionsGeoJSON');
46+
const searchCustomMapRegionsApi = createApiEndpoint('/Mapping/SearchCustomMapRegions');
47+
48+
// Discovery endpoints
49+
const getAllActiveLayersApi = createCachedApiEndpoint('/Mapping/GetAllActiveLayers', {
50+
ttl: 5 * 60 * 1000,
51+
enabled: true,
52+
});
53+
const searchAllMapFeaturesApi = createApiEndpoint('/Mapping/SearchAllMapFeatures');
54+
55+
// --- Existing Endpoints ---
56+
1057
export const getMapDataAndMarkers = async (signal?: AbortSignal) => {
1158
const response = await getMapDataAndMarkersApi.get<GetMapDataAndMarkersResult>(undefined, signal);
1259
return response.data;
@@ -21,3 +68,127 @@ export const getMayLayers = async (type: number, signal?: AbortSignal) => {
2168
);
2269
return response.data;
2370
};
71+
72+
// --- Indoor Maps ---
73+
74+
export const getIndoorMaps = async () => {
75+
const response = await getIndoorMapsApi.get<GetIndoorMapsResult>();
76+
return response.data;
77+
};
78+
79+
export const getIndoorMap = async (mapId: string) => {
80+
const response = await getIndoorMapApi.get<GetIndoorMapResult>({
81+
id: encodeURIComponent(mapId),
82+
});
83+
return response.data;
84+
};
85+
86+
export const getIndoorMapFloor = async (floorId: string) => {
87+
const response = await getIndoorMapFloorApi.get<GetIndoorMapFloorResult>({
88+
floorId: encodeURIComponent(floorId),
89+
});
90+
return response.data;
91+
};
92+
93+
export const getIndoorMapZonesGeoJSON = async (floorId: string) => {
94+
const response = await getIndoorMapZonesGeoJSONApi.get<GetGeoJSONResult>({
95+
floorId: encodeURIComponent(floorId),
96+
});
97+
return response.data;
98+
};
99+
100+
export const searchIndoorLocations = async (term: string, mapId?: string) => {
101+
const params: Record<string, unknown> = { term: encodeURIComponent(term) };
102+
if (mapId) {
103+
params.mapId = encodeURIComponent(mapId);
104+
}
105+
const response = await searchIndoorLocationsApi.get<SearchIndoorLocationsResult>(params);
106+
return response.data;
107+
};
108+
109+
export const getNearbyIndoorMaps = async (lat: number, lon: number, radiusMeters: number) => {
110+
const response = await getNearbyIndoorMapsApi.get<GetIndoorMapsResult>({
111+
lat,
112+
lon,
113+
radiusMeters,
114+
});
115+
return response.data;
116+
};
117+
118+
// --- Custom Maps ---
119+
120+
export const getCustomMaps = async (type?: number) => {
121+
const params: Record<string, unknown> = {};
122+
if (type !== undefined) {
123+
params.type = encodeURIComponent(type);
124+
}
125+
const response = await getCustomMapsApi.get<GetCustomMapsResult>(params);
126+
return response.data;
127+
};
128+
129+
export const getCustomMap = async (mapId: string) => {
130+
const response = await getCustomMapApi.get<GetCustomMapResult>({
131+
id: encodeURIComponent(mapId),
132+
});
133+
return response.data;
134+
};
135+
136+
export const getCustomMapLayer = async (layerId: string) => {
137+
const response = await getCustomMapLayerApi.get<GetCustomMapLayerResult>({
138+
layerId: encodeURIComponent(layerId),
139+
});
140+
return response.data;
141+
};
142+
143+
export const getMapLayerGeoJSON = async (layerId: string) => {
144+
const response = await getMapLayerGeoJSONApi.get<GetGeoJSONResult>({
145+
layerId: encodeURIComponent(layerId),
146+
});
147+
return response.data;
148+
};
149+
150+
export const getCustomMapRegionsGeoJSON = async (layerId: string) => {
151+
const response = await getCustomMapRegionsGeoJSONApi.get<GetGeoJSONResult>({
152+
layerId: encodeURIComponent(layerId),
153+
});
154+
return response.data;
155+
};
156+
157+
export const searchCustomMapRegions = async (term: string, layerId?: string) => {
158+
const params: Record<string, unknown> = { term: encodeURIComponent(term) };
159+
if (layerId) {
160+
params.layerId = encodeURIComponent(layerId);
161+
}
162+
const response = await searchCustomMapRegionsApi.get<SearchCustomMapRegionsResult>(params);
163+
return response.data;
164+
};
165+
166+
// --- Discovery & Search ---
167+
168+
export const getAllActiveLayers = async () => {
169+
const response = await getAllActiveLayersApi.get<GetAllActiveLayersResult>();
170+
return response.data;
171+
};
172+
173+
export const searchAllMapFeatures = async (term: string, type?: 'all' | 'indoor' | 'custom') => {
174+
const params: Record<string, unknown> = { term: encodeURIComponent(term) };
175+
if (type) {
176+
params.type = type;
177+
}
178+
const response = await searchAllMapFeaturesApi.get<SearchAllMapFeaturesResult>(params);
179+
return response.data;
180+
};
181+
182+
// --- URL Helpers (no fetch needed, constructs URLs for components) ---
183+
184+
export const getFloorImageUrl = (floorId: string): string => {
185+
return `${getBaseApiUrl()}/Mapping/GetIndoorMapFloorImage/${encodeURIComponent(floorId)}`;
186+
};
187+
188+
export const getCustomMapLayerImageUrl = (layerId: string): string => {
189+
return `${getBaseApiUrl()}/Mapping/GetCustomMapLayerImage/${encodeURIComponent(layerId)}`;
190+
};
191+
192+
export const getCustomMapTileUrl = (layerId: string): string => {
193+
return `${getBaseApiUrl()}/Mapping/GetCustomMapTile/${encodeURIComponent(layerId)}/{z}/{x}/{y}`;
194+
};

0 commit comments

Comments
 (0)