1. Overview
The Team Audit feature enables collaborative inspection of sites by multiple team members simultaneously. Team audits distribute sections among members, track progress collectively, and finalize reports with team leader approval.
Feature Dependencies:
- Schedule - Parent scheduling system
- Sites - Audit locations and site management
- Users - Team member assignment and roles
- Questionnaires - Audit forms and sections
- Reports - Final report generation
- Authentication - User verification and permissions
Clean Architecture Approach
- Presentation layer: UI components and screens
- Business logic: Controllers and use cases
- Data management: Repositories and services
- Domain models: Entities and data structures
Functional Workflow
flowchart LR A[Team Lobby] --> B[Section Distribution] B --> C[Questionnaire Completion] C --> D[Data Aggregation] D --> E[Final Review] E --> F[Submission & Distribution] style A fill:#f9d5e5,stroke:#333,stroke-width:2px style C fill:#d5e5f9,stroke:#333,stroke-width:2px style E fill:#e5f9d5,stroke:#333,stroke-width:2px style F fill:#f9e5d5,stroke:#333,stroke-width:2px
Each phase uses specialized components that ensure data integrity, user collaboration, and accurate reporting.
2. Technical Architecture
2.1 Architecture Diagram
The Team Audit feature follows clean architecture principles with distinct layers:
flowchart TD A[UI Components] --> B[Controllers] B --> C[Use Cases] C --> D[Repositories] D --> E[API Clients] style A fill:#f9d5e5,stroke:#333,stroke-width:2px style B fill:#d5e5f9,stroke:#333,stroke-width:2px style C fill:#e5f9d5,stroke:#333,stroke-width:2px style D fill:#f9e5d5,stroke:#333,stroke-width:2px style E fill:#e5d5f9,stroke:#333,stroke-width:2px
2.2 Core Principles
- Separation of Concerns: UI, business logic, and data access are clearly separated
- Domain-Driven Design: Organization around business domains with well-defined models
- Unidirectional Data Flow: Data flows from UI through controllers to use cases to repositories
- State Management: Jotai for atomic state, React Query for server state
- Form Handling: React Hook Form with Zod validation
2.3 Project Structure
packages/app/features/team-report/
├── _controllers/ # Business logic controllers
├── _domain/ # Domain models & interfaces
├── _use-cases/ # Business logic use cases
├── lobby/ # Team lobby feature
│ ├── _components/ # UI components
│ ├── _controllers/ # Lobby controllers
│ ├── _repository/ # Data access
├── summary/ # Audit summary feature
│ ├── _components/ # Summary UI components
│ ├── _controllers/ # Summary controllers
│ ├── _repository/ # Data access
├── state.tsx # Global state
2.4 Key Technologies
- React Native + Expo: Cross-platform mobile framework
- Tamagui: UI component library for consistent design
- Jotai: Atomic state management for component state
- React Query: Data fetching, caching, and synchronization
- React Hook Form: Form state management and validation
- Zod: Schema validation for forms and API responses
- TypeScript: Static typing for robust code
3. Key Components and Their Functions
3.1 Team Report Controllers
Controllers manage business logic between UI and data layers.
3.1.1 Team Start Controller
Located at _controllers/team-report-start-controller.ts
// Initializes a new team report
public useStartReport = () => {
const mutation = useMutation({
mutationFn: async (schedule: SelectedTeamSchedule) => {
return teamReportRepository.startTeamReport({
scheduleID: schedule.id,
siteID: schedule.siteID,
});
},
onSuccess: (data) => {
setAtom(this.reportIDAtom, data.reportID);
this.navigateToOverview(data.reportID);
},
});
return {
startReport: mutation.mutate,
mutation,
};
};3.1.2 Team Summary Controller
Located at summary/_controllers/team-summary-controller.ts
// Fetches team audit summary data
public useTeamAuditSummary = () => {
const mongoReportId = this.useMongoReportId();
return useQuery({
queryKey: ['team-audit-summary', mongoReportId],
queryFn: async () => {
return teamSummaryService.getTeamAuditReportSummaryUIData({
reportID: mongoReportId
});
},
refetchOnReconnect: 'always',
});
};
// Handles report form for signature and email config
public useTeamReportForm = () => {
const teamReportForm = useForm<TeamReportFormModel>({
resolver: zodResolver(teamReportFormSchema),
});
const submissionMutation = useMutation({
mutationFn: async (formData: TeamReportFormModel) => {
const result = await submitTeamReportUseCase.execute({
formData,
summaryData,
generateSignaturePromise,
leaderId,
});
return result.payoffData;
},
// Mutation lifecycle handlers
});
return {
form: teamReportForm,
isSubmitting: submissionMutation.isPending,
error: submissionMutation.error,
handleSubmit: teamReportForm.handleSubmit(submissionMutation.mutateAsync),
};
};3.2 Use Cases
Use cases implement specific business logic operations.
3.2.1 Submit Team Report Section Use Case
Located at _use-cases/submit-team-report-section-usecase.ts
// Handles section submission with attachment validation and upload
public execute = async (params: ExecuteParams) => {
// Validate attachments if enabled
if (!params.forceSubmit && this.validateAttachments) {
await this.validateLocalAttachment(params);
}
// Upload attachments with progress tracking
const questions = await this.uploadAllQuestionAttachment(params);
// Submit the complete section data
return this.submit({
report: { ...params.payload, questions },
siteID: params.siteID,
firebaseReportID: params.firebaseReportID,
sectionIndex: params.sectionIndex,
});
};3.2.2 Submit Team Report Use Case
Located at summary/_use-cases/summary-usecase.ts
// Handles final report submission
public execute = async (params: SubmitParams): Promise<SubmitResult> => {
try {
// Prepare submission data and process signatures
const requestBody = await teamSummaryService.prepareSubmissionData(
{
signatures: params.formData.signatures,
emailTargets: params.formData.emailTargets,
},
params.generateSignaturePromise
);
// Submit the report to the API
const response = await teamSummaryRepository.postTeamAuditReport({
reportId: params.summaryData.reportID,
siteId: params.summaryData.siteID,
requestBody,
});
// Process the response for UI payoff
const payoffData = teamSummaryService.parsedSubmitResponseForPayoff(response);
return {
success: true,
payoffData,
};
} catch (error) {
return {
success: false,
error: error.message || 'Failed to submit report',
};
}
};3.3 Domain Models
Domain models define the data structures used in the application.
3.3.1 Team Summary View Model
Located at _domain/team-summary-view-model.ts
// Core data structures for the Team Audit Summary
interface IFlagCount {
red: number;
green: number;
yellow: number;
}
interface ISection {
approvalIndex: number;
sectionIndex: number;
sectionName: string;
isRequired: boolean;
approvalStatus: string;
}
interface ISignature {
isSigned: boolean;
name: string;
path: string;
position: string;
}
interface ITeamAuditSummary {
auditName: string;
siteName: string;
datetimeIn: string;
flagCount: IFlagCount;
sections: ISection[];
signatures: ISignature[];
emailTargets: IEmailTarget[] | null;
reportID: string;
siteID: string;
}3.3.2 Team Summary Request/Response Models
Located at summary/_domain/team-summary-model.ts
// Form data model with Zod validation
export const teamReportFormSchema = z.object({
signatures: z.array(
z.object({
name: z.string().min(1, "Name is required"),
position: z.string().min(1, "Position is required"),
isSigned: z.boolean(),
path: z.string().min(1, "Signature is required"),
})
),
emailTargets: z.array(
z.object({
email: z.string().email("Invalid email"),
enabled: z.boolean(),
setByAdmin: z.boolean().optional(),
status: z.enum(["unsent", "sent", "failed"]).optional(),
})
),
});
// Submit request model
export class TeamAuditRequestModel {
signatures: ReportSignature[];
emailTargets: EmailTarget[];
}
// API response schema
export const teamAuditSubmitResponse = z.object({
data: z.object({
siteName: z.string(),
flagCount: z.object({
red: z.number(),
yellow: z.number(),
green: z.number(),
}),
dateOfVisit: z.string(),
timeOfVisit: z.string(),
duration: z.string(),
}),
message: z.string(),
});3.4 Core UI Components
The Team Audit feature includes several specialized UI components.
3.4.1 Team Audit Summary Component
Located at summary/team-audit-summary.tsx
const TeamAuditSummary = () => {
const { LL } = useLocalization();
const { data: summaryData } = teamSummaryController.useTeamAuditSummary();
const { form } = teamSummaryController.useTeamReportForm();
return (
<YStack flex={1} position="relative">
<FormProvider {...form}>
<ScrollView flex={1}>
{/* Site details showing location and audit info */}
<SiteDetailsCard>
<XStack alignItems="center" gap="$tw3">
<LocationIconContainer>
<Octicons name="location" size={18} color="white" />
</LocationIconContainer>
<YStack flex={1} gap="$tw1">
<Text bold color="white">
{summaryData?.siteName}
</Text>
<Text caption bold color="white">
{summaryData?.auditName}
</Text>
<Text caption color="white">
{`Report Started: ${dayjs(summaryData?.datetimeIn).format(
"hh:mm A Z"
)}`}
</Text>
</YStack>
<Image source={SiteDetails} marginRight={-8} />
</XStack>
</SiteDetailsCard>
{/* Flag summary showing audit health */}
<YStack paddingHorizontal="$tw4">
<Text bold>{LL.questionnaire.summary.reportSummary()}</Text>
<XStack justifyContent="center" paddingVertical="$tw2">
<FlagSummary
totalGreenFlag={summaryData?.flagCount.green}
totalYellowFlag={summaryData?.flagCount.yellow}
totalRedFlag={summaryData?.flagCount.red}
/>
</XStack>
</YStack>
{/* Section preview, signature and email components */}
<SectionPreview />
<TeamLeaderSignature summaryData={summaryData} />
<EmailSection />
<ConfirmSubmit />
</ScrollView>
</FormProvider>
{/* Upload progress indicator */}
<UploadProgress summaryData={summaryData} />
</YStack>
);
};3.4.2 Sections List Component
Located at lobby/_components/sections-list.ts
const QuestionnaireSectionsList = () => {
const { LL } = useLocalization();
const {
data: sectionData,
isLoading,
isRefetching,
} = teamLobbyController.useSectionList();
const { height } = useWindowDimensions();
if (isLoading && !isRefetching) {
return (
<SectionListContainer height={height / 2} alignItems="center">
<Spinner size="large" />
</SectionListContainer>
);
}
if (!sectionData) {
return (
<SectionListContainer height={height / 2} alignItems="center">
<Text>{LL.schedule.schedule.teamAudit.noSections()}</Text>
</SectionListContainer>
);
}
return (
<SectionListScrollView>
{/* Required sections */}
<SectionItemList
title={LL.schedule.schedule.teamAudit.required()}
items={sectionData.filter((item) => item.required === true)}
isLoading={isRefetching}
/>
{/* Optional sections */}
<SectionItemList
title={LL.schedule.schedule.teamAudit.optional()}
items={sectionData.filter((item) => item.required !== true)}
isLoading={isRefetching}
/>
</SectionListScrollView>
);
};4. Detailed Team Audit Workflow
The team audit process follows a specific flow from initialization to completion. This section details each stage of the workflow, the components involved, and the data flow between them.
4.1 Workflow Sequence Diagram
sequenceDiagram participant TL as Team Lobby participant SS as Schedule Selection participant TRI as Team Report Init participant QD as Questionnaire Distribution participant SC as Section Completion participant SUM as Summary Compilation participant FR as Final Review participant TLS as Team Leader Signature participant EC as Email Configuration participant RD as Report Distribution TL->>SS: Select Schedule SS->>TRI: Initialize Team Report TRI->>QD: Distribute Sections QD->>SC: Complete Sections SC->>SUM: Compile Summary SUM->>FR: Review FR->>TLS: Add Signature TLS->>EC: Configure Emails EC->>RD: Distribute Report
4.2 Initialization Phase
4.2.1 Team Lobby Screen
Located at packages/app/features/team-report/lobby/lobby-screen.tsx, this screen serves as the gathering point for team members.
Data Flow:
- Team members access the lobby screen
- Team leader is identified based on role permissions
- Available schedules are fetched and displayed
- Team leader selects a schedule for auditing
API Integration:
-
The lobby screen fetches available schedules using a query like:
const { data: schedules } = useQuery({ queryKey: ["schedules", "active"], queryFn: () => scheduleRepository.getActiveSchedules(), });
4.2.2 Schedule Selection
The schedule selection process is managed by the useSelectSchedule hook from ReportTeamStartController:
Implementation:
// In team-report-start-controller.ts
public useSelectSchedule = () => {
return {
select: (params: SelectedTeamSchedule) => {
// Implementation to select a schedule and update state
setAtom(this.selectedScheduleAtom, params);
return { success: true };
},
};
};Data Flow:
- Team leader selects a schedule from the list
- The
selectfunction is called with the schedule details - The selected schedule is stored in the
selectedScheduleAtom - The UI updates to show schedule details and enable the “Start Audit” button
4.2.3 Team Report Initialization
When the team leader starts the audit, the useStartReport hook is used:
Implementation:
// In team-report-start-controller.ts
public useStartReport = () => {
const mutation = useMutation({
mutationFn: async (selectedSchedule: SelectedTeamSchedule) => {
// Call the API to start a team report
const response = await teamReportRepository.startTeamReport({
scheduleID: selectedSchedule.id,
siteID: selectedSchedule.siteID,
});
return response;
},
onSuccess: (data) => {
// Handle successful initialization
// Store report ID and navigate to first section
},
});
return {
startReport: (selectedSchedule: SelectedTeamSchedule) => {
mutation.mutate(selectedSchedule);
},
mutation,
};
};API Request:
// Sample API request for starting a team report
const startTeamReport = async (params: {
scheduleID: string;
siteID: string;
}) => {
const response = await api.post("/v1.0/team-reports/start", params);
if (response.ok && response.data) {
return response.data;
}
throw new Error("Failed to start team report");
};API Response:
{
"data": {
"reportID": "team-report-123",
"startTime": "2023-04-15T09:30:00Z",
"sections": [
{
"sectionIndex": 0,
"sectionName": "General Information",
"status": "not_started"
},
{
"sectionIndex": 1,
"sectionName": "Safety Protocols",
"status": "not_started"
}
// Additional sections...
]
},
"success": true
}4.3 Questionnaire Completion Phase
4.3.1 Section Distribution
After initialization, team members can access different sections of the questionnaire:
Implementation:
// In team-report-start-controller.ts
public useStartSection = () => {
const mutation = useMutation({
mutationFn: async (params: { sectionIndex: number }) => {
// API call to claim a section for editing
const response = await teamReportRepository.claimSection({
reportID: currentReportID,
sectionIndex: params.sectionIndex,
});
return response;
},
onSuccess: (data, variables) => {
// Navigate to the section questionnaire
router.push(`/questionnaire/team/${currentReportID}/section/${variables.sectionIndex}`);
},
});
return {
start: (params: { sectionIndex: number }) => {
mutation.mutate(params);
},
mutationState: mutation,
};
};Data Flow:
- Team members view available sections on the main screen
- A team member selects a section to work on
- The
startfunction is called with the section index - The API is called to claim the section for that team member
- On success, navigation occurs to the section questionnaire
4.3.2 Question Answering
Team members complete the questionnaire sections by answering questions:
UI Implementation:
// Sample question component
const QuestionItem = ({ question, onChange }) => {
return (
<YStack gap="$2">
<Text bold>{question.text}</Text>
{question.type === "text" && (
<Input
value={question.answer}
onChangeText={(text) => onChange(question.id, text)}
/>
)}
{question.type === "yesno" && (
<XStack gap="$2">
<Button
variant={question.answer === "yes" ? "filled" : "outlined"}
onPress={() => onChange(question.id, "yes")}
>
Yes
</Button>
<Button
variant={question.answer === "no" ? "filled" : "outlined"}
onPress={() => onChange(question.id, "no")}
>
No
</Button>
</XStack>
)}
{/* Other question types */}
</YStack>
);
};Data Management:
- Answers are stored in local state using React Hook Form
- File attachments are handled with a separate component
- Real-time validation provides feedback to users
4.3.3 Section Submission
When a team member completes a section, they submit it using the SubmitTeamReportSectionUsecase:
Implementation Flow:
- User clicks the “Submit Section” button
- The controller calls the use case’s execute method
- The use case validates attachments
- Attachments are uploaded with progress tracking
- Section data is submitted to the backend
- User is redirected to the section selection screen
Sequence Diagram:
┌─────────────┐ ┌─────────────────┐ ┌──────────────────────────┐ ┌───────────────────┐
│ │ │ │ │ │ │ │
│ UI Component│ │ Controller │ │ Submit Section Use Case │ │ Repository │
│ │ │ │ │ │ │ │
└──────┬──────┘ └────────┬────────┘ └───────────┬──────────────┘ └─────────┬─────────┘
│ │ │ │
│ Submit Section │ │ │
│───────────────────▶│ │ │
│ │ Execute Use Case │ │
│ │────────────────────────▶│ │
│ │ │ │
│ │ │ Validate Attachments │
│ │ │◀───────────────────────── │
│ │ │ │
│ │ │ Upload Attachments │
│ │ │────────────────────────── ▶│
│ │ │ │
│ │ │ Submit Section Data │
│ │ │────────────────────────── ▶│
│ │ Return Result │ │
│ │◀────────────────────────│ │
│ Update UI │ │ │
│◀───────────────────│ │ │
│ │ │ │
┌──────┴──────┐ ┌────────┴────────┐ ┌───────────┴──────────────┐ ┌─────────┴─────────┐
│ │ │ Controller │ │ Submit Section Use Case │ │ Repository │
│ UI Component│ │ │ │ │ │ │
└─────────────┘ └─────────────────┘ └──────────────────────────┘ └───────────────────┘
4.4 Summary and Finalization Phase
4.4.1 Summary Compilation
After all sections are completed, the team leader accesses the summary screen:
Implementation:
// In team-summary-controller.ts
public useTeamAuditSummary = () => {
const { reportID } = useReportContext();
return useQuery({
queryKey: ['team-report', reportID, 'summary'],
queryFn: async () => {
const response = await teamReportRepository.getTeamAuditSummary(reportID);
return response.data;
},
});
};Data Flow:
- Team leader navigates to the summary screen
- The controller fetches summary data from the API
- Data is transformed to match the
ITeamAuditSummaryinterface - The UI renders the complete audit summary
API Response Structure:
{
"data": {
"auditName": "Quarterly Safety Audit",
"siteName": "Main Manufacturing Plant",
"datetimeIn": "2023-04-15T09:30:00Z",
"flagCount": {
"red": 3,
"yellow": 5,
"green": 42
},
"sections": [
{
"approvalIndex": 1,
"sectionIndex": 0,
"sectionName": "General Information",
"isRequired": true,
"approvalStatus": "approved"
}
// More sections...
],
"signatures": [
{
"isSigned": false,
"name": "",
"path": "",
"position": "Team Leader"
}
],
"emailTargets": [
{
"email": "manager@example.com",
"enabled": true,
"setByAdmin": true,
"status": "unsent"
}
],
"reportID": "team-report-123",
"siteID": "site-456"
}
}4.4.2 Final Review and Signature
The team leader reviews the complete report and provides a signature:
Signature Component:
// In team-leader-signature.tsx
const TeamLeaderSignature = ({ summaryData }) => {
const { form } = teamSummaryController.useTeamReportForm();
const signature = form.watch("signature");
return (
<YStack gap="$3" padding="$4">
<Text bold>Team Leader Signature</Text>
{!signature ? (
<Button onPress={() => navigation.push("/signature")}>
Add Signature
</Button>
) : (
<XStack>
<Image source={{ uri: signature.path }} width={150} height={80} />
<YStack>
<Text>{signature.name}</Text>
<Text caption>{signature.position}</Text>
</YStack>
</XStack>
)}
</YStack>
);
};Signature Capture Flow:
- Team leader clicks “Add Signature” button
- Navigation to signature capture screen
- Team leader draws signature and enters name/position
- Signature is saved as an image and stored with form data
- UI updates to show the captured signature
4.4.3 Email Configuration
The team leader configures email recipients for report distribution:
Email Section Component:
// In email-section.tsx
const EmailSection = () => {
const { form } = teamSummaryController.useTeamReportForm();
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "emailTargets",
});
return (
<YStack gap="$3" padding="$4">
<Text bold>Email Report To</Text>
{fields.map((field, index) => (
<XStack key={field.id} alignItems="center">
<Controller
control={form.control}
name={`emailTargets.${index}.email`}
render={({ field }) => (
<Input
value={field.value}
onChangeText={(text) =>
field.onChange({ ...field.value, email: text })
}
placeholder="Email address"
flex={1}
/>
)}
/>
<Controller
control={form.control}
name={`emailTargets.${index}.enabled`}
render={({ field }) => (
<Switch checked={field.value} onCheckedChange={field.onChange} />
)}
/>
<Button icon="trash" onPress={() => remove(index)} />
</XStack>
))}
<Button
onPress={() => append({ email: "", enabled: true, setByAdmin: false })}
>
Add Email
</Button>
</YStack>
);
};Email Configuration Flow:
- Pre-configured emails (set by admin) are displayed and can’t be removed
- Team leader can add additional email recipients
- Each recipient can be enabled/disabled
- Email validation ensures correct format
- Configuration is saved with the report data
4.4.4 Report Submission with JSON Export
Finally, the team leader submits the complete report, which now includes automatic JSON export when the feature flag is enabled:
Submit Implementation with Export:
// In team-summary-controller.ts
public useSubmitTeamReport = () => {
const { reportID } = useReportContext();
const { LL } = useLocalization();
const allOrgFeatures = useOrgFeatures();
return useMutation({
mutationFn: async (data: SubmitTeamReportParams) => {
// Check if JSON export is enabled
if (allOrgFeatures?.features?.EXPORT_JSON_REPORT_ON_SUBMIT_SYNC) {
try {
// Export team audit data to JSON
const auditDataExportUseCase = auditDataExportFactory.getInstance(LL);
const exportResult = await auditDataExportUseCase.exportAuditData({
summaryData: data.summaryData,
leaderId: data.leaderId,
});
// Log export result (non-blocking)
if (exportResult.success) {
observabilitySentry.logEvent('Team audit data exported', {
filePath: exportResult.filePath,
fileSize: exportResult.fileSize,
});
}
} catch (error) {
// Export failure doesn't block submission
observabilitySentry.logEvent('Export failed', error);
}
}
// Continue with normal submission
const response = await teamReportRepository.submitTeamReport({
reportID,
signature: data.signature,
emailTargets: data.emailTargets,
});
return response;
},
onSuccess: () => {
// Handle successful submission
// Navigate to confirmation screen
},
});
};Submission Flow with Export:
sequenceDiagram participant TL as Team Leader participant TSC as TeamSummaryController participant Export as AuditDataExportUseCase participant FS as FileSystem participant API as Backend API TL->>TSC: Click Submit TSC->>TSC: Validate Sections TSC->>TSC: Check Feature Flag alt Export Enabled TSC->>Export: exportAuditData(summaryData) Export->>FS: Save JSON File FS-->>TSC: Export Result Note over TSC: Log result (non-blocking) end TSC->>API: Submit Team Report API-->>TSC: Submission Confirmed TSC-->>TL: Navigate to Payoff
- Team leader clicks “Submit Report” button
- Validation ensures all required sections are complete
- Validation checks for team leader signature
- If feature flag enabled: JSON export initiated (non-blocking)
- The controller calls the repository to submit the report
- Success/error state is displayed to the user
- On success, navigation to completion confirmation screen
Exported Team Audit Data Structure:
{
"auditName": "Monthly Team Audit",
"siteName": "Main Branch",
"datetimeIn": "2025-01-19T10:00:00Z",
"flagCount": {
"red": 5,
"green": 20,
"yellow": 3
},
"sections": [
{
"sectionIndex": 0,
"sectionName": "Kitchen",
"isRequired": true,
"approvalStatus": "approved",
"auditor": "user_123"
}
],
"signatures": [...],
"emailTargets": [...],
"reportID": "team_123",
"siteID": "site_456"
}Export Features:
- Automatic Export: Triggered during team report submission
- Complete Summary: Exports the full
ITeamAuditSummaryobject - Non-Blocking: Export failures don’t prevent submission
- Platform Support: Android SAF, iOS sharing, Web download
- File Naming:
team_report_[reportID]_[timestamp].json - Feature Flag: Controlled by
EXPORT_JSON_REPORT_ON_SUBMIT_SYNC
4.5 Report Distribution Phase
After successful submission, the report is distributed according to the configured email settings:
API Implementation:
// In team-report-repository.ts
public sendReportEmails = async (params: { reportID: string }) => {
const response = await this.api.post(`/v1.0/team-reports/${params.reportID}/send-emails`);
if (response.ok && response.data) {
return response.data;
}
throw new Error('Failed to send report emails');
};5. Data Flow and API Integration
5.1 Data Flow Architecture
The Team Audit feature follows a unidirectional data flow pattern:
flowchart TD UI[UI Components] --> CTRL[Controllers] CTRL --> UC[Use Cases] UC --> REPO[Repositories] REPO --> API[API Client] API --> BS[Backend Services] style UI fill:#f9d5e5,stroke:#333,stroke-width:2px style CTRL fill:#d5e5f9,stroke:#333,stroke-width:2px style UC fill:#e5f9d5,stroke:#333,stroke-width:2px style REPO fill:#f9e5d5,stroke:#333,stroke-width:2px style API fill:#e5d5f9,stroke:#333,stroke-width:2px style BS fill:#f5f5f5,stroke:#333,stroke-width:2px
5.2 Key API Endpoints
| Endpoint | Method | Purpose | Link |
|---|---|---|---|
/v1.0/team-[reports](../../Reports/ReportsOverview.md)/start | POST | Initialize team audit | API Definition |
/v1.0/team-[reports](../../Reports/ReportsOverview.md)/{reportID}/sections/{sectionIndex}/claim | POST | Claim section | API Definition |
/v1.0/team-[reports](../../Reports/ReportsOverview.md)/{reportID}/summary | GET | Get audit summary | API Definition |
/v1.0/team-[reports](../../Reports/ReportsOverview.md)/{reportID}/submit | POST | Submit audit | API Definition |
5.3 State Management
The Team Audit feature uses a combination of Jotai atoms for UI state and React Query for server state:
// Core state atoms in state.tsx
export const teamAuditProgressUploadAtom = atom<boolean>(false);
export const mongoReportIdAtom = atom<string>("");
export const auditorColorsAtom = atom<Record<string, string>>({});
// React Query usage for server state
const useTeamAuditSummary = () => {
const mongoReportId = useAtomValue(mongoReportIdAtom);
return useQuery({
queryKey: ["team-audit-summary", mongoReportId],
queryFn: () =>
teamSummaryService.getTeamAuditReportSummaryUIData({
reportID: mongoReportId,
}),
refetchOnReconnect: "always",
});
};5.4 Attachment Handling Process
File attachments follow a specialized workflow:
flowchart LR A[Select Files] --> B[Validate] --> C[Upload] C --> D[Update Progress] --> E[Update References] style A fill:#f9d5e5,stroke:#333,stroke-width:2px style C fill:#d5e5f9,stroke:#333,stroke-width:2px style E fill:#e5f9d5,stroke:#333,stroke-width:2px
Implementation summary:
- Files are selected using device native pickers
- Validation ensures proper format and size
- Upload process shows progress to the user
- Successfully uploaded files receive secure URLs
- References are stored with the question data
6. Form Management and Validation
The Team Audit feature utilizes a comprehensive form management system built on React Hook Form with Zod validation schema.
6.1 Form Architecture
flowchart TD FP[FormProvider] --> HF[React Hook Form] HF --> ZV[Zod Validation] HF --> FC[Form Components] FC --> CMP[Controller Pattern] FC --> FLD[Field Arrays] CMP --> I[Inputs] CMP --> S[Selectors] CMP --> F[File Uploads] CMP --> CB[Custom Buttons] style FP fill:#f9d5e5,stroke:#333,stroke-width:2px style HF fill:#d5e5f9,stroke:#333,stroke-width:2px style ZV fill:#e5f9d5,stroke:#333,stroke-width:2px style FC fill:#f9e5d5,stroke:#333,stroke-width:2px style CMP fill:#e5d5f9,stroke:#333,stroke-width:2px
6.2 Core Form Implementation
Located at packages/app/features/team-report/summary/team-summary-controller.ts:
// Form initialization with validation
public useTeamReportForm = () => {
const { data: summaryData } = this.useTeamAuditSummary();
// Define validation schema using Zod
const validationSchema = z.object({
signature: z.object({
isSigned: z.boolean(),
name: z.string().min(1, { message: "Name is required" }),
path: z.string().min(1, { message: "Signature is required" }),
position: z.string().min(1, { message: "Position is required" }),
}),
emailTargets: z.array(
z.object({
email: z.string().email({ message: "Invalid email address" }),
enabled: z.boolean(),
setByAdmin: z.boolean().optional(),
status: z.enum(['unsent', 'sent', 'failed']).optional(),
})
).refine(
(targets) => targets.some(target => target.enabled),
{ message: "At least one email recipient must be enabled" }
),
});
// Initialize form with default values
const form = useForm({
resolver: zodResolver(validationSchema),
defaultValues: {
signature: summaryData?.signatures[0] || {
isSigned: false,
name: '',
path: '',
position: ''
},
emailTargets: summaryData?.emailTargets || [],
},
});
return { form };
};6.3 Form Component Integration
Forms are used in components via the FormProvider to ensure context is properly passed down:
// Example from TeamAuditSummary component
const TeamAuditSummary = () => {
const { form } = teamSummaryController.useTeamReportForm();
return (
<YStack flex={1}>
<FormProvider {...form}>
<ScrollView>
{/* Form components that access the form context */}
<TeamLeaderSignature />
<EmailSection />
<SubmitButton />
</ScrollView>
</FormProvider>
</YStack>
);
};6.4 Form Field Components
Individual form fields use the Controller component for proper integration:
// Example email field component
const EmailField = ({ index, readOnly }) => {
return (
<Controller
name={`emailTargets.${index}`}
render={({ field, fieldState }) => (
<XStack alignItems="center" gap="$2">
<Input
value={field.value.email}
onChangeText={(text) =>
field.onChange({ ...field.value, email: text })
}
editable={!readOnly && !field.value.setByAdmin}
placeholder="Enter email address"
flex={1}
error={!!fieldState.error}
/>
<Switch
checked={field.value.enabled}
onCheckedChange={(checked) =>
field.onChange({ ...field.value, enabled: checked })
}
disabled={readOnly}
/>
{!field.value.setByAdmin && !readOnly && (
<Button
icon="trash"
variant="ghost"
onPress={() => remove(index)}
/>
)}
{fieldState.error && (
<Text color="$error" fontSize="$xs">
{fieldState.error.message}
</Text>
)}
</XStack>
)}
/>
);
};6.5 Form Validation Strategies
The Team Audit implementation includes several validation strategies:
- Schema Validation: Using Zod for structural typing and schema validation
- Field-Level Validation: Immediate validation of individual fields
- Form-Level Validation: Cross-field validation to ensure consistency
- API Validation: Backend validation for security and ensuring business rules
- Custom Error Handling: Formatted error display with contextual guidance
7. UI Components and User Experience
7.1 Key UI Components
The Team Audit feature includes several specialized UI components that enhance the user experience.
classDiagram class SiteDetailsCard { +location: string +siteName: string +auditName: string +startTime: Date +render() } class FlagSummary { +green: number +yellow: number +red: number +total: number +renderCounts() } class SectionPreview { +sections: Section[] +renderSectionList() +renderSectionStatus(status) } class TeamLeaderSignature { +signature: SignatureData +handleCaptureSignature() +renderSignatureDisplay() } class EmailSection { +emails: EmailTarget[] +addEmail() +removeEmail() +toggleEmail() } class ConfirmSubmit { +isSubmitting: boolean +handleSubmit() +validateBeforeSubmit() } class UploadProgress { +progress: number +isUploading: boolean +renderProgressBar() }
7.2 Site Details Card
Located at packages/app/features/team-report/summary/_components/shared-ui.tsx:
// Site details card implementation
export const SiteDetailsCard = styled(XStack, {
backgroundColor: "$primary",
borderRadius: "$4",
marginHorizontal: "$4",
padding: "$4",
shadowColor: "rgba(0, 0, 0, 0.1)",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 1,
shadowRadius: 4,
elevation: 2,
});
// Usage in components
const SiteInfo = ({ summaryData }) => (
<SiteDetailsCard>
<XStack alignItems="center" gap="$3">
<LocationIconContainer>
<Octicons name="location" size={18} color="white" />
</LocationIconContainer>
<YStack flex={1} gap="$1">
<Text bold color="white">
{summaryData?.siteName}
</Text>
<Text caption bold color="white">
{summaryData?.auditName}
</Text>
<Text caption color="white">
{`Started: ${formatDateTime(summaryData?.datetimeIn)}`}
</Text>
</YStack>
<Image source={SiteDetailsIllustration} marginRight={-8} />
</XStack>
</SiteDetailsCard>
);7.3 Flag Summary Component
Located at packages/app/features/team-report/summary/_components/flag-summary.ts`:
// Flag summary visualizing audit health
const FlagSummary = ({
totalGreenFlag = 0,
totalYellowFlag = 0,
totalRedFlag = 0,
}) => {
const total = totalGreenFlag + totalYellowFlag + totalRedFlag;
return (
<YStack alignItems="center" gap="$2">
<XStack gap="$4">
<FlagCount count={totalGreenFlag} color="green" />
<FlagCount count={totalYellowFlag} color="yellow" />
<FlagCount count={totalRedFlag} color="red" />
</XStack>
<Text caption>Total: {total} questions</Text>
</YStack>
);
};
const FlagCount = ({ count, color }) => (
<YStack alignItems="center">
<Circle size={40} backgroundColor={`$${color}`}>
<Text bold color="white">
{count}
</Text>
</Circle>
<Text caption marginTop="$1">
{color.charAt(0).toUpperCase() + color.slice(1)}
</Text>
</YStack>
);7.4 Navigation Flow
The Team Audit feature implements a structured navigation flow to guide users through the process.
flowchart LR A[Team Lobby] --> B[Audit Overview] B --> C[Section Selection] C --> D[Section Form] D --> C C --> E[Summary Screen] E --> F[Signature Capture] F --> E E --> G[Submission] G --> H[Completion] style A fill:#f9d5e5,stroke:#333,stroke-width:2px style E fill:#d5e5f9,stroke:#333,stroke-width:2px style G fill:#e5f9d5,stroke:#333,stroke-width:2px style H fill:#f9e5d5,stroke:#333,stroke-width:2px
Implementation:
The navigation structure is implemented using Expo Router in apps/expo/app/questionnaire/_layout.tsx:
// Navigation structure definition
export default function QuestionnaireLayout() {
return (
<Stack>
<Stack.Screen
name="team-lobby"
options={{
title: "Team Audit",
headerShown: true,
}}
/>
<Stack.Screen
name="overview"
options={{
title: "Audit Overview",
headerShown: true,
}}
/>
<Stack.Screen
name="index"
options={{
title: "Questionnaire",
headerShown: true,
}}
/>
<Stack.Screen
name="section"
options={{
title: "Section Form",
headerShown: true,
}}
/>
<Stack.Screen
name="team-audit-summary"
options={{
title: "Audit Summary",
headerShown: true,
}}
/>
<Stack.Screen
name="signature"
options={{
title: "Signature",
headerShown: true,
presentation: "modal",
}}
/>
<Stack.Screen
name="payoff"
options={{
title: "Audit Complete",
headerShown: false,
}}
/>
</Stack>
);
}8. Error Handling and Recovery
8.1 Error Handling Strategies
The Team Audit feature implements robust error handling at multiple levels of the application:
flowchart TD A[Error Source] --> B{Error Type} B -->|Network Error| C[Retry Strategy] B -->|Validation Error| D[User Feedback] B -->|Server Error| E[Graceful Degradation] B -->|Unknown Error| F[Fallback UI] C --> G[Exponential Backoff] C --> H[Offline Caching] D --> I[Field-Level Errors] D --> J[Form-Level Errors] D --> K[Toast Notifications] E --> L[Partial Data Display] E --> M[Alternative Workflows] F --> N[Error Boundary] F --> O[Error Logging] style A fill:#f9d5e5,stroke:#333,stroke-width:2px style B fill:#d5e5f9,stroke:#333,stroke-width:2px style C fill:#e5f9d5,stroke:#333,stroke-width:2px style D fill:#f9e5d5,stroke:#333,stroke-width:2px
8.2 Network Error Handling
Located at packages/app/api/api-client.ts:
// API client with retry logic
const client = create({
baseURL: API_BASE_URL,
timeout: 30000,
headers: {
"Content-Type": "application/json",
},
});
// Configure retry logic for network failures
client.addMonitor(
axiosRetry(client.axiosInstance, {
retries: 3,
retryDelay: (retryCount) => {
return Math.min(1000 * Math.pow(2, retryCount), 10000); // Exponential backoff
},
retryCondition: (error) => {
// Only retry on network errors or 5xx server errors
return (
axiosRetry.isNetworkError(error) ||
(error.response && error.response.status >= 500)
);
},
})
);8.3 Form Validation Errors
Form validation errors are presented inline with clear messaging:
// Form error display component
const FormErrors = ({ errors }) => {
if (Object.keys(errors).length === 0) return null;
return (
<YStack
backgroundColor="$errorLight"
padding="$2"
borderRadius="$2"
marginBottom="$2"
>
<Text color="$error" bold>
Please fix the following errors:
</Text>
{Object.entries(errors).map(([key, error]) => (
<Text key={key} color="$error">
• {error.message}
</Text>
))}
</YStack>
);
};8.4 Recovery Mechanisms
8.4.1 Offline Support
Data can be stored locally and synchronized when connectivity is restored:
// Offline-capable repository method
public updateTeamQuestionnaireSection = async (params) => {
try {
// Try online submission first
const response = await this.api.patch(
`/v1.0/team-reports/${params.reportID}/sections/${params.sectionIndex}`,
params
);
if (response.ok) {
// Clear any pending offline data
await this.offlineStorage.removePendingChange(
`team-report-${params.reportID}-section-${params.sectionIndex}`
);
return response.data;
}
throw new Error('Failed to update section');
} catch (error) {
if (isNetworkError(error)) {
// Store data for offline synchronization
await this.offlineStorage.storePendingChange(
`team-report-${params.reportID}-section-${params.sectionIndex}`,
params
);
// Return optimistic result
return {
success: true,
offlineMode: true,
data: {
sectionIndex: params.sectionIndex,
status: 'pending_sync',
},
};
}
// Re-throw other errors
throw error;
}
};8.4.2 Autosave
Form data is automatically saved to prevent data loss:
// Autosave implementation
useEffect(() => {
let autosaveTimeout;
const subscription = form.watch((formValues) => {
// Debounce to prevent too frequent saves
if (autosaveTimeout) {
clearTimeout(autosaveTimeout);
}
autosaveTimeout = setTimeout(() => {
// Save to local storage
saveFormDataToStorage(reportID, sectionIndex, formValues);
}, 2000);
});
return () => {
subscription.unsubscribe();
if (autosaveTimeout) {
clearTimeout(autosaveTimeout);
}
};
}, [form, reportID, sectionIndex]);9. Testing Strategy
The Team Audit implementation follows a comprehensive testing approach to ensure reliability and maintainability.
9.1 Testing Architecture
flowchart TD UT[Unit Tests] --> CT[Component Tests] CT --> IT[Integration Tests] IT --> E2E[End-to-End Tests] UT --> M[Mocking Strategy] CT --> M style UT fill:#f9d5e5,stroke:#333,stroke-width:2px style CT fill:#d5e5f9,stroke:#333,stroke-width:2px style IT fill:#e5f9d5,stroke:#333,stroke-width:2px style E2E fill:#f9e5d5,stroke:#333,stroke-width:2px
9.2 Unit Testing Controllers
Each controller is tested with a corresponding test file that verifies its behavior in isolation:
// Example from team-summary-controller.test.ts
describe("TeamSummaryController", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should fetch team audit summary data", async () => {
// Mock dependencies
const mockSummaryData = {
siteName: "Test Site",
auditName: "Safety Audit",
flagCount: { red: 3, yellow: 2, green: 10 },
};
vi.mocked(
teamSummaryService.getTeamAuditReportSummaryUIData
).mockResolvedValue(mockSummaryData);
// Execute the test
const { result } = renderHook(() =>
teamSummaryController.useTeamAuditSummary()
);
// Wait for the query to complete
await waitFor(() => expect(result.current.isSuccess).toBe(true));
// Verify the data
expect(result.current.data).toEqual(mockSummaryData);
});
it("should handle form submission", async () => {
// Test implementation
});
});9.3 Repository Testing
Repository layers are tested to ensure proper API integration:
// Example from team-summary-repo.test.ts
describe("TeamSummaryRepository", () => {
const mockResponse = {
ok: true,
status: 200,
data: { success: true },
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(apiClient.client.get).mockResolvedValue(mockResponse);
vi.mocked(apiClient.client.post).mockResolvedValue(mockResponse);
});
describe("getTeamAuditReportSummaryData", () => {
it("calls the correct endpoint with the provided reportID", async () => {
const reportID = "test-report-id";
await teamSummaryRepository.getTeamAuditReportSummaryData({ reportID });
expect(reportEndpoint.GET_TEAM_AUDIT_REPORT_SUMMARY).toHaveBeenCalledWith(
{ mongoReportId: reportID }
);
expect(apiClient.client.get).toHaveBeenCalledWith(
`/v1.0/reports/app/${reportID}`
);
});
it("validates the response data using schema", async () => {
const reportID = "test-report-id";
await teamSummaryRepository.getTeamAuditReportSummaryData({ reportID });
expect(summaryPageDataResponseSchema.parse).toHaveBeenCalledWith(
mockResponse.data
);
});
});
});9.4 Component Testing
UI components are tested using React Testing Library:
// Example from team-audit-summary.test.tsx
describe("TeamAuditSummary", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should render correctly with summary data", () => {
render(<MockTeamAuditSummary />);
// Verify main containers
expect(screen.getByTestId("root-container")).toBeTruthy();
expect(screen.getByTestId("form-provider")).toBeTruthy();
expect(screen.getByTestId("scroll-view")).toBeTruthy();
// Verify site information is displayed
expect(screen.getByTestId("site-name")).toBeTruthy();
expect(screen.getByTestId("audit-name")).toBeTruthy();
expect(screen.getByText("Test Site")).toBeTruthy();
expect(screen.getByText("Safety Audit")).toBeTruthy();
// Verify report summary is displayed
expect(screen.getByText("Report Summary")).toBeTruthy();
expect(screen.getByTestId("flag-summary")).toBeTruthy();
// Verify subcomponents are rendered
expect(screen.getByTestId("section-preview")).toBeTruthy();
expect(screen.getByTestId("team-leader-signature")).toBeTruthy();
expect(screen.getByTestId("email-section")).toBeTruthy();
});
});9.5 Mock Implementation
Vitest mocks are used extensively to isolate components during testing:
// Example mock implementation
vi.mock("../_controllers/team-summary-controller", () => ({
teamSummaryController: {
useTeamAuditSummary: vi.fn().mockReturnValue({
data: {
siteName: "Test Site",
auditName: "Safety Audit",
flagCount: { red: 3, yellow: 2, green: 10 },
sections: [],
signatures: [],
emailTargets: [],
},
isLoading: false,
error: null,
}),
useTeamReportForm: vi.fn().mockReturnValue({
form: {
control: {},
handleSubmit: vi.fn(),
watch: vi.fn(),
},
}),
},
}));9.6 State Testing
Atom-based state management is tested using renderHook:
// Example from state.test.tsx
describe("Team Audit State", () => {
it("should initialize teamAuditProgressUploadAtom with false", () => {
const { result } = renderHook(() => useAtom(teamAuditProgressUploadAtom));
expect(result.current[0]).toBe(false);
});
it("should update the teamAuditProgressUploadAtom value when set", () => {
const { result } = renderHook(() => useAtom(teamAuditProgressUploadAtom));
act(() => {
result.current[1](true);
});
expect(result.current[0]).toBe(true);
});
});9.7 Test Coverage Goals
The Team Audit codebase aims for high test coverage across different layers:
| Layer | Coverage Target | Test Focus |
|---|---|---|
| Domain Models | 100% | Type validation, property access |
| Use Cases | 90%+ | Business logic, error handling |
| Controllers | 85%+ | Hook behavior, state management |
| Repositories | 85%+ | API integration, data mapping |
| UI Components | 75%+ | Rendering, user interactions |
10. Security and Permissions
10.1 Role-Based Access Control
The Team Audit feature implements a comprehensive role-based access control (RBAC) system:
flowchart TD R[User Roles] --> P[Permissions] P --> A[Actions] R --> TL[Team Leader] R --> TM[Team Member] R --> AD[Admin] TL --> TLP[Team Leader Permissions] TM --> TMP[Team Member Permissions] AD --> ADP[Admin Permissions] TLP --> TLPA[Start Audit] TLP --> TLPB[Distribute Sections] TLP --> TLPC[Review Sections] TLP --> TLPC[Submit Report] TMP --> TMPA[Complete Sections] TMP --> TMPB[Upload Attachments] ADP --> ADPA[Configure Recipients] ADP --> ADPB[Access All Reports] style R fill:#f9d5e5,stroke:#333,stroke-width:2px style P fill:#d5e5f9,stroke:#333,stroke-width:2px style A fill:#e5f9d5,stroke:#333,stroke-width:2px
Located at packages/app/features/team-report/summary/_controllers/team-summary-controller.ts:
// Role-based permission checks
public usePermissions = () => {
const { user } = useAuth();
return {
canSubmitReport: user.role === 'team_leader' ||
user.permissions.includes('submit_team_report'),
canEditSignature: user.role === 'team_leader' ||
user.permissions.includes('edit_signature'),
canConfigureEmails: user.role === 'team_leader' ||
user.role === 'admin' ||
user.permissions.includes('configure_emails'),
canResetSection: user.role === 'team_leader' ||
user.permissions.includes('reset_section'),
canAccessAllSections: user.role === 'team_leader' ||
user.permissions.includes('access_all_sections'),
};
};
// Permission-controlled component rendering
const TeamLeaderActions = () => {
const { canSubmitReport } = teamSummaryController.usePermissions();
if (!canSubmitReport) {
return null; // Don't render submission controls
}
return (
<YStack gap="$3">
<Button onPress={handleSubmit}>Submit Report</Button>
<Button variant="outlined" onPress={handleSave}>Save Draft</Button>
</YStack>
);
};10.2 Authentication Flow
All API requests require authentication through JWT tokens:
// Auth token interceptor in api-client.ts
api.addAsyncRequestTransform(async (request) => {
const token = await authStateRepository.getToken();
if (token) {
request.headers["Authorization"] = `Bearer ${token}`;
} else {
throw new AuthError("No valid authentication token");
}
});
// Automatic token refresh handling
api.addAsyncResponseTransform(async (response) => {
if (response.status === 401) {
try {
const newToken = await authStateRepository.refreshToken();
if (newToken) {
// Retry the original request with the new token
return api.request({
...response.config,
headers: {
...response.config.headers,
Authorization: `Bearer ${newToken}`,
},
});
}
} catch (error) {
// Force logout on refresh failure
authStateRepository.clearTokens();
navigateToLogin();
}
}
return response;
});10.3 Data Protection
Sensitive audit data is protected through several mechanisms:
10.3.1 Secure Storage
// Encrypted storage for sensitive data
import * as SecureStore from "expo-secure-store";
export class SecureStorageService {
// Store data securely with encryption
static async storeSecureData(key: string, data: any): Promise<void> {
const jsonValue = JSON.stringify(data);
await SecureStore.setItemAsync(key, jsonValue, {
keychainAccessible: SecureStore.WHEN_UNLOCKED,
});
}
// Retrieve securely stored data
static async getSecureData(key: string): Promise<any> {
const jsonValue = await SecureStore.getItemAsync(key);
return jsonValue ? JSON.parse(jsonValue) : null;
}
// Remove secure data
static async removeSecureData(key: string): Promise<void> {
await SecureStore.deleteItemAsync(key);
}
}10.3.2 Data Encryption
Sensitive data is encrypted both in transit and at rest:
// Transport-level encryption
import { encrypt, decrypt } from 'react-native-crypto';
export class DataEncryptionService {
private static readonly ENCRYPTION_KEY = 'SECURE_TEAM_REPORT_KEY';
// Encrypt data before storage
static encryptData(data: string): string {
return encrypt(data, this.ENCRYPTION_KEY);
}
// Decrypt data after retrieval
static decryptData(encryptedData: string): string {
return decrypt(encryptedData, this.ENCRYPTION_KEY);
}
}
// Usage in offline storage
private async storeOfflineData(key: string, data: any) {
try {
const jsonValue = JSON.stringify(data);
const encryptedValue = DataEncryptionService.encryptData(jsonValue);
await SecureStorageService.storeSecureData(key, encryptedValue);
} catch (error) {
console.error('Error storing offline data:', error);
throw new Error('Failed to store offline data securely');
}
}10.3.3 Data Access Policies
Team audit data access is controlled by policy-based restrictions:
// Data access policy enforcement
class TeamReportAccessPolicy {
// Check if user can access a specific report
static canAccessReport(user: User, reportId: string): boolean {
if (user.role === "admin") return true;
// Team leaders can access all reports they created
if (user.role === "team_leader" && user.createdReports.includes(reportId)) {
return true;
}
// Team members can only access reports they're part of
if (
user.role === "team_member" &&
user.participatedReports.includes(reportId)
) {
return true;
}
return false;
}
// Check if user can access a specific section
static canAccessSection(
user: User,
reportId: string,
sectionId: string
): boolean {
if (!this.canAccessReport(user, reportId)) return false;
if (user.role === "admin" || user.role === "team_leader") {
return true;
}
// Team members can only access assigned sections
return user.assignedSections.some(
(s) => s.reportId === reportId && s.sectionId === sectionId
);
}
}10.4 Audit Trails
All significant actions are logged for accountability:
// Audit logging middleware
const auditLogger = async (action: string, params: any, userId: string) => {
try {
await api.post('/v1.0/audit-logs', {
action,
params,
userId,
timestamp: new Date().toISOString(),
userAgent: getUserAgent(),
ipAddress: await getIPAddress(),
});
} catch (error) {
// Log failures shouldn't break functionality
console.error('Failed to log audit trail:', error);
}
};
// Usage in team report submission
public submitTeamReport = async (params: SubmitTeamReportParams): Promise<any> => {
try {
// Log the submission attempt
await auditLogger('TEAM_REPORT_SUBMIT_ATTEMPT', { reportId: params.reportId }, params.userId);
// Perform the submission
const response = await this.api.post(`/v1.0/team-reports/${params.reportId}/submit`, params);
if (response.ok) {
// Log successful submission
await auditLogger('TEAM_REPORT_SUBMIT_SUCCESS', { reportId: params.reportId }, params.userId);
return response.data;
}
// Log submission failure
await auditLogger('TEAM_REPORT_SUBMIT_FAILURE', {
reportId: params.reportId,
error: response.status
}, params.userId);
throw new Error(`Failed to submit report: ${response.status}`);
} catch (error) {
// Log unexpected errors
await auditLogger('TEAM_REPORT_SUBMIT_ERROR', {
reportId: params.reportId,
error: error.message
}, params.userId);
throw error;
}
};11. Offline Capabilities
The Team Audit feature implements a robust offline-first architecture to ensure data integrity and continuity of operations in challenging connectivity environments.
11.1 Offline Architecture
flowchart TD A[User Action] --> B{Connection?} B -->|Yes| C[Direct API] B -->|No| D[Offline Queue] D --> E[Local Storage] C --> F[Server Response] F --> G[Update UI] E --> H{Online?} H -->|Yes| I[Background Sync] I --> J[Conflict Resolution] J --> G H -->|No| K[Retry Later] K --> E style A fill:#f9d5e5,stroke:#333,stroke-width:2px style B fill:#d5e5f9,stroke:#333,stroke-width:2px style D fill:#e5f9d5,stroke:#333,stroke-width:2px style E fill:#f9e5d5,stroke:#333,stroke-width:2px style I fill:#e5d5f9,stroke:#333,stroke-width:2px style J fill:#f5f5f5,stroke:#333,stroke-width:2px
11.2 Local Storage Implementation
Located at packages/app/shared/infrastructure/offline-storage.ts:
// Offline storage service for persistent data
class OfflineStorage {
// Store section data offline
async storeTeamSectionData(
reportId: string,
sectionIndex: number,
data: any
): Promise<void> {
const key = this.generateSectionKey(reportId, sectionIndex);
const timestamp = new Date().toISOString();
await AsyncStorage.setItem(
key,
JSON.stringify({
data,
timestamp,
synced: false,
syncAttempts: 0,
})
);
}
// Retrieve section data
async getTeamSectionData(
reportId: string,
sectionIndex: number
): Promise<any> {
const key = this.generateSectionKey(reportId, sectionIndex);
const storedData = await AsyncStorage.getItem(key);
if (!storedData) return null;
return JSON.parse(storedData);
}
// Store pending API operation
async storePendingOperation(operation: {
endpoint: string;
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
data?: any;
id: string;
}): Promise<void> {
const pendingOps = await this.getPendingOperations();
pendingOps.push({
...operation,
timestamp: Date.now(),
attempts: 0,
});
await AsyncStorage.setItem("pendingOperations", JSON.stringify(pendingOps));
}
// Get all pending operations
async getPendingOperations(): Promise<PendingOperation[]> {
const pending = await AsyncStorage.getItem("pendingOperations");
return pending ? JSON.parse(pending) : [];
}
// Helper methods
private generateSectionKey(reportId: string, sectionIndex: number): string {
return `team_report_${reportId}_section_${sectionIndex}`;
}
}11.3 Synchronization Service
Located at packages/app/shared/services/sync-service.ts:
// Background synchronization service
class SyncService {
private isRunning = false;
private offlineStorage: OfflineStorage;
private api: ApiClient;
constructor(offlineStorage: OfflineStorage, api: ApiClient) {
this.offlineStorage = offlineStorage;
this.api = api;
// Initialize network change listeners
NetInfo.addEventListener(this.handleNetworkChange);
}
// Handle network status changes
private handleNetworkChange = (state: NetInfoState) => {
if (state.isConnected && state.isInternetReachable) {
this.startSync();
}
};
// Start the synchronization process
async startSync(): Promise<void> {
if (this.isRunning) return;
this.isRunning = true;
try {
// Get all pending operations
const operations = await this.offlineStorage.getPendingOperations();
if (operations.length === 0) {
this.isRunning = false;
return;
}
// Sort by timestamp (oldest first)
operations.sort((a, b) => a.timestamp - b.timestamp);
// Process each operation
for (const operation of operations) {
try {
await this.processOperation(operation);
// Remove from pending queue on success
await this.offlineStorage.removePendingOperation(operation.id);
} catch (error) {
// Update attempts counter
await this.offlineStorage.updateOperationAttempts(
operation.id,
operation.attempts + 1
);
// Log the failure
console.error(
`Sync failed for operation ${operation.id}: ${error.message}`
);
}
}
} finally {
this.isRunning = false;
}
}
// Process a single operation
private async processOperation(operation: PendingOperation): Promise<any> {
switch (operation.method) {
case "GET":
return this.api.get(operation.endpoint);
case "POST":
return this.api.post(operation.endpoint, operation.data);
case "PUT":
return this.api.put(operation.endpoint, operation.data);
case "PATCH":
return this.api.patch(operation.endpoint, operation.data);
case "DELETE":
return this.api.delete(operation.endpoint);
default:
throw new Error(`Unsupported method: ${operation.method}`);
}
}
}11.4 Conflict Resolution
The Team Audit feature implements a sophisticated conflict resolution system to handle cases where multiple users modify the same data while offline.
11.4.1 Conflict Detection
// Conflict detection in repository methods
public async submitTeamReportSection(params: SubmitSectionParams): Promise<any> {
try {
// Get latest version from server if online
let serverVersion;
try {
serverVersion = await this.api.get(
`/v1.0/team-reports/${params.reportID}/sections/${params.sectionIndex}`
);
} catch (error) {
// If offline, proceed with local changes
serverVersion = null;
}
// Check for conflicts if we got server data
if (serverVersion && serverVersion.lastModified) {
const localData = await this.offlineStorage.getTeamSectionData(
params.reportID,
params.sectionIndex
);
// Detect conflicts
if (localData && new Date(localData.lastModified) < new Date(serverVersion.lastModified)) {
return this.handleConflict({
local: localData,
server: serverVersion,
params
});
}
}
// No conflict, proceed with submission
// ...implementation continues
} catch (error) {
// Handle errors
}
}11.4.2 Conflict Resolution UI
// Conflict resolution UI component
const ConflictResolutionModal = ({
visible,
localData,
serverData,
onResolve,
onCancel,
}) => {
const [resolvedData, setResolvedData] = useState(null);
// Prepare merged data with local changes preferred
useEffect(() => {
if (visible && localData && serverData) {
const merged = mergeData(localData, serverData);
setResolvedData(merged);
}
}, [visible, localData, serverData]);
if (!visible) return null;
return (
<Modal visible={visible} onRequestClose={onCancel}>
<YStack padding="$4" gap="$4">
<Text fontWeight="bold">Changes Conflict Detected</Text>
<Text>
Your offline changes conflict with updates made by other team members.
Please review the differences and resolve the conflicts.
</Text>
<ConflictList
conflicts={findConflicts(localData, serverData)}
onResolveConflict={(field, value) => {
setResolvedData((prev) => ({
...prev,
[field]: value,
}));
}}
/>
<XStack gap="$4">
<Button variant="outlined" onPress={onCancel}>
Cancel
</Button>
<Button variant="filled" onPress={() => onResolve(resolvedData)}>
Resolve Conflicts
</Button>
</XStack>
</YStack>
</Modal>
);
};11.5 Offline Usage Indicators
The UI provides clear indicators to users when they are working offline:
// Offline indicator component
const OfflineIndicator = () => {
const isConnected = useNetInfo().isConnected;
if (isConnected) return null;
return (
<XStack
backgroundColor="$warning"
padding="$2"
justifyContent="center"
alignItems="center"
>
<Icon name="wifi-off" color="white" size={16} />
<Text color="white" marginLeft="$2">
You are offline. Changes will be saved and synced when connection is
restored.
</Text>
</XStack>
);
};
// Integration in main screens
const TeamAuditScreen = () => {
return (
<YStack flex={1}>
<OfflineIndicator />
<ScrollView flex={1}>{/* Screen content */}</ScrollView>
</YStack>
);
};11.6 Background Synchronization
The app implements background synchronization to ensure data is updated even when the app is not in active use:
// Background sync configuration
import * as BackgroundFetch from "expo-background-fetch";
import * as TaskManager from "expo-task-manager";
const BACKGROUND_SYNC_TASK = "background-sync";
// Define the task
TaskManager.defineTask(BACKGROUND_SYNC_TASK, async () => {
try {
// Get sync service instance
const syncService = getSyncService();
// Run synchronization
await syncService.startSync();
// Return success
return BackgroundFetch.BackgroundFetchResult.NewData;
} catch (error) {
console.error("Background sync failed:", error);
return BackgroundFetch.BackgroundFetchResult.Failed;
}
});
// Register background sync
async function registerBackgroundSync() {
try {
await BackgroundFetch.registerTaskAsync(BACKGROUND_SYNC_TASK, {
minimumInterval: 15 * 60, // 15 minutes
stopOnTerminate: false,
startOnBoot: true,
});
console.log("Background sync registered");
} catch (error) {
console.error("Background sync registration failed:", error);
}
}12. Performance Optimization
The Team Audit feature implements various optimization techniques to ensure smooth operation even on lower-end devices and in challenging network environments.
12.1 Memory Management
flowchart TD A[Memory Management] --> B[Component Virtualization] A --> C[Asset Optimization] A --> D[Memory Leak Prevention] B --> B1[Virtualized Lists] B --> B2[On-demand Loading] C --> C1[Image Resizing] C --> C2[Asset Preloading] D --> D1[Cleanup Effects] D --> D2[Component Unmount] style A fill:#f9d5e5,stroke:#333,stroke-width:2px style B fill:#d5e5f9,stroke:#333,stroke-width:2px style C fill:#e5f9d5,stroke:#333,stroke-width:2px style D fill:#f9e5d5,stroke:#333,stroke-width:2px
12.1.1 Virtualized Lists
For displaying potentially long lists of sections or questions, we use virtualized components:
// Optimized section list with virtualization
const SectionsList = ({ sections }) => {
return (
<FlashList
data={sections}
renderItem={({ item }) => <SectionItem section={item} />}
estimatedItemSize={80}
keyExtractor={(item) => item.sectionIndex.toString()}
onEndReachedThreshold={0.5}
ListEmptyComponent={<EmptyState />}
contentContainerStyle={{ paddingBottom: 20 }}
/>
);
};12.1.2 Memory Leak Prevention
Every component that uses subscriptions implements proper cleanup:
// Proper effect cleanup to prevent memory leaks
useEffect(() => {
const subscription = NetInfo.addEventListener(handleNetworkChange);
return () => {
subscription(); // Cleanup subscription when component unmounts
};
}, []);12.2 Network Optimization
12.2.1 Data Serialization
We optimize payloads for minimal data transfer:
// Optimized data transformation for network transfers
export const transformForUpload = (formData: FormData): UploadPayload => {
// Only include necessary fields
return {
id: formData.id,
questions: formData.questions.map((q) => ({
id: q.id,
answer: q.answer,
// Only include references to attachments, not the full binary data
attachmentRefs: q.attachments?.map((a) => a.id) || [],
})),
metadata: {
version: formData.version,
completedBy: formData.completedBy,
},
};
};12.2.2 Request Batching
For multiple related operations, requests are batched:
// Batch multiple attachment uploads in a single network request
public async batchUploadAttachments(attachments: Attachment[]): Promise<UploadResult[]> {
// Group files by type for optimal encoding
const imageAttachments = attachments.filter(a => a.type.startsWith('image/'));
const documentAttachments = attachments.filter(a => !a.type.startsWith('image/'));
// Create form data for batch upload
const formData = new FormData();
// Append all images
imageAttachments.forEach((attachment, index) => {
formData.append(`images[${index}]`, {
uri: attachment.path,
name: attachment.name,
type: attachment.type
});
});
// Append all documents
documentAttachments.forEach((attachment, index) => {
formData.append(`documents[${index}]`, {
uri: attachment.path,
name: attachment.name,
type: attachment.type
});
});
// Single API call for multiple files
const response = await api.post('/v1.0/attachments/batch-upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: this.onProgress
});
return response.data.results;
}12.2.3 Payload Compression
Large data payloads are compressed before transmission:
// Compression middleware for large payloads
api.addRequestTransform((request) => {
// Only compress large payloads
if (request.data && JSON.stringify(request.data).length > 10000) {
request.headers["Content-Encoding"] = "gzip";
request.data = compressData(request.data);
}
return request;
});
// Compression function
function compressData(data) {
const jsonString = JSON.stringify(data);
return gzip(jsonString);
}12.3 Rendering Optimization
12.3.1 Component Memoization
Heavy components are memoized to prevent unnecessary re-renders:
// Memoized section component
const SectionItem = React.memo(
({ section, onSelect }) => {
return (
<Pressable onPress={() => onSelect(section.sectionIndex)}>
<YStack padding="$3" borderRadius="$2" backgroundColor="$background">
<Text bold>{section.sectionName}</Text>
<SectionStatusBadge status={section.approvalStatus} />
</YStack>
</Pressable>
);
},
(prevProps, nextProps) => {
// Custom comparison logic
return (
prevProps.section.approvalStatus === nextProps.section.approvalStatus
);
}
);12.3.2 Dynamic Imports
Heavy components used infrequently are dynamically imported:
// Dynamic import for signature capture component
const SignatureCapture = React.lazy(() => import("./signature-capture"));
// Usage with suspense
const SignatureModal = ({ visible, onClose, onSave }) => {
if (!visible) return null;
return (
<Modal visible={visible} onRequestClose={onClose}>
<React.Suspense fallback={<Spinner />}>
<SignatureCapture onSave={onSave} onCancel={onClose} />
</React.Suspense>
</Modal>
);
};12.3.3 Render Optimization
Components use optimized rendering techniques:
// Optimized rendering with useMemo and useCallback
const OptimizedQuestionList = ({ questions, onChange }) => {
// Memoize expensive transformations
const processedQuestions = useMemo(() => {
return questions.map((q) => ({
...q,
hasAttachments: q.attachments && q.attachments.length > 0,
isAnswered: !!q.answer,
}));
}, [questions]);
// Memoize callbacks
const handleChange = useCallback(
(id, value) => {
onChange(id, value);
},
[onChange]
);
return (
<YStack>
{processedQuestions.map((question) => (
<QuestionItem
key={question.id}
question={question}
onChange={handleChange}
/>
))}
</YStack>
);
};12.4 Resource Management
12.4.1 Image Optimization
Images are processed for optimal size and quality:
// Image optimization before upload
async function optimizeImage(uri: string, quality: number = 0.8): Promise<string> {
const result = await manipulateAsync(
uri,
[{ resize: { width: 800 } }],
{ compress: quality, format: SaveFormat.JPEG }
);
return result.uri;
}
// Usage in attachment upload
public async uploadImageAttachment(attachment: ImageAttachment): Promise<UploadResult> {
// Optimize the image before upload
const optimizedUri = await optimizeImage(attachment.path);
// Create optimized attachment
const optimizedAttachment = {
...attachment,
path: optimizedUri
};
// Upload the optimized image
return this.uploadFile(optimizedAttachment);
}12.4.2 Asset Preloading
Critical assets are preloaded to prevent UI jank:
// Asset preloading service
class AssetPreloader {
static async preloadTeamAuditAssets() {
// Preload images
const imageAssets = [
require("../assets/images/site-details.png"),
require("../assets/images/flag-green.png"),
require("../assets/images/flag-yellow.png"),
require("../assets/images/flag-red.png"),
require("../assets/images/signature-placeholder.png"),
];
// Start preloading
await Asset.loadAsync(imageAssets);
// Preload fonts
await Font.loadAsync({
"Roboto-Bold": require("../assets/fonts/Roboto-Bold.ttf"),
"Roboto-Regular": require("../assets/fonts/Roboto-Regular.ttf"),
});
}
}
// Used in app initialization
useEffect(() => {
AssetPreloader.preloadTeamAuditAssets();
}, []);12.5 Performance Monitoring
The app includes performance monitoring to identify bottlenecks:
// Performance tracking hooks
export const usePerformanceTracker = (trackingName: string) => {
const startTime = useRef(Date.now());
const events = useRef<Array<{ name: string; timestamp: number }>>([]);
// Record a performance event
const trackEvent = useCallback((eventName: string) => {
events.current.push({
name: eventName,
timestamp: Date.now() - startTime.current,
});
}, []);
// Complete tracking and send metrics
const completeTracking = useCallback(() => {
const totalTime = Date.now() - startTime.current;
// Send metrics to analytics
Analytics.logEvent("performance_tracking", {
tracking_name: trackingName,
total_time: totalTime,
events: events.current,
});
// Log to console in development
if (__DEV__) {
console.log(
`Performance (${trackingName}): ${totalTime}ms`,
events.current
);
}
// Reset for reuse
events.current = [];
startTime.current = Date.now();
}, [trackingName]);
return { trackEvent, completeTracking };
};
// Usage in a component
const TeamAuditSummary = () => {
const { trackEvent, completeTracking } =
usePerformanceTracker("team_audit_summary");
useEffect(() => {
trackEvent("component_mount");
return () => {
trackEvent("component_unmount");
completeTracking();
};
}, []);
// Component implementation
};14. Conclusion
The Team Audit feature in Nimbly 2.0 represents a sophisticated implementation that enables collaborative inspection workflows. By following clean architecture principles and leveraging modern frontend technologies, the system provides a robust, scalable solution for conducting team-based audits across various facilities and scenarios.
14.1 Key Technical Achievements
The implementation successfully delivers:
- Real-time Collaboration: Multiple team members can work concurrently on different sections
- Comprehensive Data Management: Structured handling of complex audit data with proper validation
- Responsive Offline-Capable UI: Functional experience across network conditions
- Robust Attachment Handling: Secure, validated file uploads with progress tracking
- Role-Based Permissions: Granular access control based on team roles
14.2 Architectural Strengths
The codebase demonstrates several architectural strengths:
- Clear separation of concerns following clean architecture principles
- Strong typing with TypeScript and validation with Zod
- Reactive state management with Jotai and React Query
- Consistent use of repository pattern for data access
- Comprehensive error handling across all levels
14.3 Development Practices
The development process followed several best practices:
- Component-based design for UI elements
- Test-driven development with co-located test files
- Feature flags for incremental deployment
- Comprehensive documentation
- Performance optimization for mobile devices
This documentation provides a complete technical reference for developers working with the Team Audit feature, enabling them to understand the architecture, implementation details, and workflows necessary for maintenance and extension of the system.
15. Deployment and Integration
The Team Audit feature is designed with a robust deployment process and integrates with various systems and services.
15.1 Deployment Architecture
flowchart TD A[Code Repository] --> B[CI/CD Pipeline] B --> C{Environment} C -->|Development| D[Dev Environment] C -->|Staging| E[Staging Environment] C -->|Production| F[Production Environment] D --> G[Development Tests] E --> H[Integration Tests] F --> I[Monitoring] style A fill:#f9d5e5,stroke:#333,stroke-width:2px style B fill:#d5e5f9,stroke:#333,stroke-width:2px style C fill:#e5f9d5,stroke:#333,stroke-width:2px style F fill:#f9e5d5,stroke:#333,stroke-width:2px style I fill:#e5d5f9,stroke:#333,stroke-width:2px
15.2 Release Process
The Team Audit feature follows a structured release process:
- Feature Development: Code is written and unit tested in feature branches
- Code Review: All changes undergo peer review through pull requests
- Automated Testing: CI pipeline runs unit and integration tests
- QA Verification: Manual testing in the staging environment
- Feature Flagging: New features are wrapped in feature flags for controlled rollout
- Staged Rollout: Features are released to a percentage of users first
- Monitoring: Performance and error metrics are monitored closely after release
- Documentation: Release notes and documentation are updated
15.3 Feature Flags
The Team Audit feature uses feature flags for controlled deployment:
// Feature flag configuration
const featureFlags = {
teamAudit: {
ENABLED: true,
ATTACHMENT_VALIDATION: true,
REAL_TIME_COLLABORATION: false,
OFFLINE_SYNC: true,
AI_ASSISTANCE: false,
},
};
// Feature flag usage
if (featureFlags.teamAudit.ENABLED) {
// Register team audit routes
router.registerRoute("/team-audit", TeamAuditScreen);
// Only show attachment validation if enabled
if (featureFlags.teamAudit.ATTACHMENT_VALIDATION) {
attachmentValidationEnabled = true;
}
}
// Dynamic feature flag loading
class FeatureFlagService {
private flags: Record<string, any> = {};
async loadFlags(userId: string): Promise<void> {
try {
const response = await api.get("/v1.0/feature-flags", {
params: { userId },
});
if (response.ok) {
this.flags = response.data;
}
} catch (error) {
console.error("Failed to load feature flags:", error);
// Fall back to default flags
this.flags = defaultFeatureFlags;
}
}
isEnabled(flagPath: string): boolean {
const parts = flagPath.split(".");
let current = this.flags;
for (const part of parts) {
if (current[part] === undefined) return false;
current = current[part];
}
return !!current;
}
}15.4 Integration Points
The Team Audit feature integrates with several external systems:
15.4.1 PDF Generation
Reports are converted to PDF for distribution:
// PDF generation service integration
async function generateTeamReportPDF(reportId: string): Promise<string> {
try {
// Call the PDF generation service
const response = await api.post("/v1.0/reports/generate-pdf", {
reportId,
template: "team-audit",
});
if (response.ok) {
return response.data.pdfUrl;
}
throw new Error(`Failed to generate PDF: ${response.status}`);
} catch (error) {
console.error("PDF generation error:", error);
throw error;
}
}15.4.2 Email Integration
The reporting system integrates with email services:
// Email service integration
async function sendReportEmails(
reportId: string,
emailTargets: EmailTarget[]
): Promise<void> {
try {
// Filter enabled email targets
const enabledTargets = emailTargets.filter((target) => target.enabled);
if (enabledTargets.length === 0) {
console.log("No email targets enabled, skipping email send");
return;
}
// Call the email service
const response = await api.post("/v1.0/notifications/email", {
reportId,
recipients: enabledTargets.map((target) => target.email),
templateId: "team-audit-report",
});
if (!response.ok) {
throw new Error(`Failed to send emails: ${response.status}`);
}
console.log("Report emails sent successfully");
} catch (error) {
console.error("Email sending error:", error);
throw error;
}
}15.4.3 Analytics Integration
User actions and report data are fed into analytics systems:
// Analytics integration
class AnalyticsService {
// Track user actions
static trackAction(
action: string,
properties: Record<string, any> = {}
): void {
try {
Analytics.logEvent(action, {
...properties,
timestamp: new Date().toISOString(),
});
} catch (error) {
console.error("Failed to track action:", error);
}
}
// Track team audit funnel
static trackTeamAuditStep(
step: string,
reportId: string,
properties: Record<string, any> = {}
): void {
this.trackAction("team_audit_step", {
step,
reportId,
...properties,
});
}
// Track completion of reports
static trackReportCompletion(reportId: string, stats: ReportStats): void {
this.trackAction("team_audit_completed", {
reportId,
completionTimeMinutes: stats.completionTimeMinutes,
sectionCount: stats.sectionCount,
questionCount: stats.questionCount,
flagCounts: stats.flagCounts,
});
}
}15.5 Versioning and Backward Compatibility
The Team Audit feature implements versioning to ensure backward compatibility:
// API version handling
const API_VERSIONS = {
TEAM_AUDIT_V1: "2023-04-01",
TEAM_AUDIT_V2: "2023-10-15",
CURRENT: "2023-10-15",
};
// API client with version header
api.addRequestTransform((request) => {
request.headers["X-API-Version"] = API_VERSIONS.CURRENT;
return request;
});
// Version-specific data mapping
function mapReportData(data: any, version: string): TeamReportModel {
if (version === API_VERSIONS.TEAM_AUDIT_V1) {
// Map using V1 schema
return {
id: data.id,
sections: data.sections.map(mapV1Section),
// Other V1 mappings...
};
} else {
// Map using current schema
return {
id: data.reportId,
sections: data.sections.map(mapCurrentSection),
// Other current mappings...
};
}
}15.6 Environment Configuration
The Team Audit feature is configured for different environments:
// Environment configuration
const ENV = {
development: {
API_URL: "https://dev-api.nimbly.com/v1.0",
ATTACHMENT_UPLOAD_LIMIT: 10, // MB
POLLING_INTERVAL: 5000, // ms
LOG_LEVEL: "debug",
},
staging: {
API_URL: "https://staging-api.nimbly.com/v1.0",
ATTACHMENT_UPLOAD_LIMIT: 20, // MB
POLLING_INTERVAL: 10000, // ms
LOG_LEVEL: "info",
},
production: {
API_URL: "https://api.nimbly.com/v1.0",
ATTACHMENT_UPLOAD_LIMIT: 50, // MB
POLLING_INTERVAL: 30000, // ms
LOG_LEVEL: "error",
},
};
// Get current environment configuration
const getEnvConfig = () => {
const environment = process.env.ENVIRONMENT || "development";
return ENV[environment];
};
// Usage example
const config = getEnvConfig();
console.log(`API URL: ${config.API_URL}`);15.7 Monitoring and Alerts
The Team Audit feature includes comprehensive monitoring:
// Error monitoring integration
class ErrorMonitor {
static initialize(): void {
// Set up global error handler
ErrorUtils.setGlobalHandler((error, isFatal) => {
this.reportError(error, { isFatal });
// Display user-friendly error
if (isFatal) {
showErrorDialog(
"An unexpected error occurred",
"Please restart the app and try again"
);
}
});
}
static reportError(error: Error, context: Record<string, any> = {}): void {
try {
// Send to error monitoring service
Sentry.captureException(error, {
extra: context,
});
// Log locally in development
if (__DEV__) {
console.error("Error captured:", error, context);
}
} catch (reportingError) {
console.error("Failed to report error:", reportingError);
}
}
}
// Performance monitoring
class PerformanceMonitor {
static trackRenderTime(componentName: string, renderTimeMs: number): void {
if (renderTimeMs > 100) {
// Track slow renders
Analytics.logEvent("slow_render", {
componentName,
renderTimeMs,
});
}
}
static trackNetworkRequest(
endpoint: string,
durationMs: number,
success: boolean
): void {
Analytics.logEvent("network_request", {
endpoint,
durationMs,
success,
});
// Alert on slow network requests
if (durationMs > 5000) {
Sentry.captureMessage("Slow network request", {
extra: {
endpoint,
durationMs,
},
level: "warning",
});
}
}
}16. References
16.1 API Documentation
16.2 Libraries and Dependencies
- React Native - Mobile application framework
- Expo - Development platform for React Native
- Tamagui - UI component library
- React Query - Data fetching and caching library
- Jotai - Atomic state management
- React Hook Form - Form state management
- Zod - Schema validation
16.3 Design Systems
- Nimbly Design System - Component and style guidelines
- Tamagui Theme Documentation - Theming system
16.4 Internal Resources
- Team Audit PRD - Product Requirements Document
- Team Audit Design Specs - Design Specifications
- API Architecture - API Design Guidelines
Last Updated: 08 May 2025 at 01:02 am IST by samir
Last Updated: 08 May 2025 at 12:06 pm IST by apple