Skip to content

Commit c3941ad

Browse files
author
Steve Hansen
committed
feat(addin): add UI to link driver submissions to manual report requests
- Add DriverSubmission type to report.ts - Add loadSubmissions and loadUnmergedSubmissions to reportService - Update useRequests hook to accept linkedSubmissionId parameter - Update NewRequestModal with dropdown to select unmerged driver submissions - Auto-fill device, time range, and force report when submission is selected - Build dist/ with new changes
1 parent b71f83b commit c3941ad

9 files changed

Lines changed: 208 additions & 13 deletions

File tree

src/FleetClaim.AddIn.React/app/components/NewRequestModal.tsx

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
} from '@geotab/zenith';
99
import { useGeotab } from '@/contexts';
1010
import { useRequests } from '@/hooks';
11+
import { loadUnmergedSubmissions, SubmissionRecord } from '@/services';
12+
import { DriverSubmission } from '@/types';
1113

1214
interface NewRequestModalProps {
1315
isOpen: boolean;
@@ -112,7 +114,7 @@ export const NewRequestModal: React.FC<NewRequestModalProps> = ({
112114
onSubmit,
113115
toast
114116
}) => {
115-
const { devices, loadDevices } = useGeotab();
117+
const { api, devices, loadDevices } = useGeotab();
116118
const { submit } = useRequests();
117119

118120
const [selectedDeviceId, setSelectedDeviceId] = useState<string>('');
@@ -125,13 +127,32 @@ export const NewRequestModal: React.FC<NewRequestModalProps> = ({
125127
const [forceReport, setForceReport] = useState(false);
126128
const [isSubmitting, setIsSubmitting] = useState(false);
127129
const [activePreset, setActivePreset] = useState<'hour' | '24h' | null>('hour');
130+
131+
// Driver submissions state
132+
const [submissions, setSubmissions] = useState<SubmissionRecord[]>([]);
133+
const [selectedSubmissionId, setSelectedSubmissionId] = useState<string>('');
134+
const [loadingSubmissions, setLoadingSubmissions] = useState(false);
128135

129136
// Load devices when modal opens
130137
useEffect(() => {
131138
if (isOpen && devices.length === 0) {
132139
loadDevices();
133140
}
134141
}, [isOpen, devices.length, loadDevices]);
142+
143+
// Load unmerged submissions when modal opens
144+
useEffect(() => {
145+
if (isOpen && api) {
146+
setLoadingSubmissions(true);
147+
loadUnmergedSubmissions(api)
148+
.then(setSubmissions)
149+
.catch(err => {
150+
console.warn('Failed to load submissions:', err);
151+
setSubmissions([]);
152+
})
153+
.finally(() => setLoadingSubmissions(false));
154+
}
155+
}, [isOpen, api]);
135156

136157
const deviceOptions = useMemo(() =>
137158
devices.map(d => ({
@@ -145,6 +166,32 @@ export const NewRequestModal: React.FC<NewRequestModalProps> = ({
145166
devices.find(d => d.id === selectedDeviceId),
146167
[devices, selectedDeviceId]
147168
);
169+
170+
// Filter submissions for selected device
171+
const deviceSubmissions = useMemo(() => {
172+
if (!selectedDeviceId) return submissions;
173+
return submissions.filter(s => s.submission.deviceId === selectedDeviceId);
174+
}, [submissions, selectedDeviceId]);
175+
176+
// Format submission for dropdown
177+
const submissionOptions = useMemo(() =>
178+
deviceSubmissions.map(s => {
179+
const date = new Date(s.submission.incidentTimestamp);
180+
const dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
181+
const driver = s.submission.driverName || 'Unknown driver';
182+
const vehicle = s.submission.deviceName || 'Unknown vehicle';
183+
return {
184+
id: s.submission.id,
185+
name: `${dateStr} - ${driver} (${vehicle})`
186+
};
187+
}),
188+
[deviceSubmissions]
189+
);
190+
191+
const selectedSubmission = useMemo(() =>
192+
submissions.find(s => s.submission.id === selectedSubmissionId)?.submission,
193+
[submissions, selectedSubmissionId]
194+
);
148195

149196
const handleSubmit = useCallback(async () => {
150197
if (!selectedDeviceId) {
@@ -165,15 +212,16 @@ export const NewRequestModal: React.FC<NewRequestModalProps> = ({
165212
selectedDevice?.name || 'Unknown',
166213
rangeStart,
167214
rangeEnd,
168-
forceReport
215+
forceReport,
216+
selectedSubmissionId || undefined
169217
);
170218
onSubmit();
171219
} catch (err) {
172220
toast.error(err instanceof Error ? err.message : 'Failed to submit request');
173221
} finally {
174222
setIsSubmitting(false);
175223
}
176-
}, [selectedDeviceId, selectedDevice, rangeStart, rangeEnd, forceReport, submit, onSubmit, toast]);
224+
}, [selectedDeviceId, selectedDevice, rangeStart, rangeEnd, forceReport, selectedSubmissionId, submit, onSubmit, toast]);
177225

178226
const handleSetLastHour = useCallback(() => {
179227
const end = new Date();
@@ -194,6 +242,25 @@ export const NewRequestModal: React.FC<NewRequestModalProps> = ({
194242
const handleCustomDateChange = useCallback(() => {
195243
setActivePreset(null);
196244
}, []);
245+
246+
// When a submission is selected, auto-fill device and time range
247+
const handleSubmissionSelect = useCallback((submissionId: string) => {
248+
setSelectedSubmissionId(submissionId);
249+
const submission = submissions.find(s => s.submission.id === submissionId)?.submission;
250+
if (submission) {
251+
// Set device
252+
setSelectedDeviceId(submission.deviceId);
253+
// Set time range: 30 min before to 30 min after incident
254+
const incidentTime = new Date(submission.incidentTimestamp);
255+
const start = new Date(incidentTime.getTime() - 30 * 60 * 1000);
256+
const end = new Date(incidentTime.getTime() + 30 * 60 * 1000);
257+
setRangeStart(start);
258+
setRangeEnd(end);
259+
setActivePreset(null);
260+
// Force report since we're linking to a submission
261+
setForceReport(true);
262+
}
263+
}, [submissions]);
197264

198265
if (!isOpen) return null;
199266

@@ -204,6 +271,48 @@ export const NewRequestModal: React.FC<NewRequestModalProps> = ({
204271
title="New Report Request"
205272
>
206273
<Modal.Content>
274+
{/* Driver Submission Section - shown if there are unmerged submissions */}
275+
{submissions.length > 0 && (
276+
<div style={styles.formField}>
277+
<label style={styles.label}>Link to Driver Submission (Optional)</label>
278+
<Dropdown
279+
dataItems={submissionOptions}
280+
value={selectedSubmissionId ? [selectedSubmissionId] : []}
281+
onChange={(selected: any[]) => {
282+
const id = selected?.[0]?.id || '';
283+
if (id) {
284+
handleSubmissionSelect(id);
285+
} else {
286+
setSelectedSubmissionId('');
287+
}
288+
}}
289+
placeholder={loadingSubmissions ? "Loading submissions..." : "Select a driver submission to link..."}
290+
searchField={true}
291+
multiselect={false}
292+
errorHandler={(e) => console.error('Dropdown error:', e)}
293+
/>
294+
{selectedSubmission && (
295+
<div style={{
296+
marginTop: '8px',
297+
padding: '12px',
298+
backgroundColor: 'var(--zen-color-background-secondary, #f0f9ff)',
299+
borderRadius: '6px',
300+
border: '1px solid var(--zen-color-info, #0ea5e9)',
301+
fontSize: '13px'
302+
}}>
303+
<strong>Driver submission selected</strong>
304+
<div style={{ marginTop: '4px', color: 'var(--zen-color-text-secondary, #6b7280)' }}>
305+
{selectedSubmission.description || 'No description'}
306+
{selectedSubmission.driverName && ` — ${selectedSubmission.driverName}`}
307+
</div>
308+
<div style={{ marginTop: '4px', fontSize: '12px', color: 'var(--zen-color-text-tertiary, #9ca3af)' }}>
309+
The vehicle, time range, and force report options have been auto-filled.
310+
</div>
311+
</div>
312+
)}
313+
</div>
314+
)}
315+
207316
<div style={styles.formField}>
208317
<label style={styles.label}>Vehicle</label>
209318
<Dropdown
@@ -212,6 +321,10 @@ export const NewRequestModal: React.FC<NewRequestModalProps> = ({
212321
onChange={(selected: any[]) => {
213322
const id = selected?.[0]?.id || '';
214323
setSelectedDeviceId(id);
324+
// Clear submission if device changes
325+
if (selectedSubmissionId && id !== selectedSubmission?.deviceId) {
326+
setSelectedSubmissionId('');
327+
}
215328
}}
216329
placeholder="Select a vehicle..."
217330
searchField={true}

src/FleetClaim.AddIn.React/app/hooks/useRequests.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,29 @@ export function useRequests() {
3838
deviceName: string,
3939
rangeStart: Date,
4040
rangeEnd: Date,
41-
forceReport = false
41+
forceReport = false,
42+
linkedSubmissionId?: string
4243
) => {
4344
if (!api) return;
4445

4546
// Get username - prefer credentials.userName (email) over currentUser.name (short name)
4647
// credentials comes from getSession() and has the full email
4748
const userName = credentials?.userName || session?.userName || currentUser?.name || 'Unknown User';
4849

49-
const request = {
50+
const request: any = {
5051
deviceId,
5152
deviceName,
5253
requestedBy: userName,
5354
fromDate: rangeStart.toISOString(),
5455
toDate: rangeEnd.toISOString(),
55-
forceReport
56+
forceReport: forceReport || !!linkedSubmissionId // Force report if linking to submission
5657
};
5758

59+
// Add linkedSubmissionId if provided
60+
if (linkedSubmissionId) {
61+
request.linkedSubmissionId = linkedSubmissionId;
62+
}
63+
5864
const addInDataId = await submitReportRequest(api, request);
5965

6066
// Audit the request (best-effort, don't block)

src/FleetClaim.AddIn.React/app/services/reportService.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Handles loading, saving, and managing reports via Geotab AddInData
55
*/
66

7-
import { GeotabApi, AddInData, IncidentReport, ReportRequest, AddInDataWrapper } from '@/types';
7+
import { GeotabApi, AddInData, IncidentReport, ReportRequest, DriverSubmission, AddInDataWrapper } from '@/types';
88

99
const ADDIN_ID = 'aji_jHQGE8k2TDodR8tZrpw';
1010
const API_BASE_URL = 'https://fleetclaim-api-589116575765.us-central1.run.app';
@@ -19,6 +19,11 @@ export interface RequestRecord {
1919
addInDataId: string;
2020
}
2121

22+
export interface SubmissionRecord {
23+
submission: DriverSubmission;
24+
addInDataId: string;
25+
}
26+
2227
/**
2328
* Load all reports from AddInData
2429
*/
@@ -83,6 +88,46 @@ export async function loadRequests(api: GeotabApi): Promise<RequestRecord[]> {
8388
return requests;
8489
}
8590

91+
/**
92+
* Load all driver submissions from AddInData
93+
*/
94+
export async function loadSubmissions(api: GeotabApi): Promise<SubmissionRecord[]> {
95+
const result = await apiCall<AddInData[]>(api, 'Get', {
96+
typeName: 'AddInData',
97+
search: { addInId: ADDIN_ID }
98+
});
99+
const addInData = result || [];
100+
101+
const submissions: SubmissionRecord[] = [];
102+
103+
for (const item of addInData) {
104+
try {
105+
const raw = item.details;
106+
const wrapper = typeof raw === 'string' ? JSON.parse(raw) : raw;
107+
108+
if (wrapper && (wrapper as AddInDataWrapper<DriverSubmission>).type === 'driverSubmission') {
109+
const submission = (wrapper as AddInDataWrapper<DriverSubmission>).payload || wrapper;
110+
submissions.push({
111+
submission: submission as DriverSubmission,
112+
addInDataId: item.id
113+
});
114+
}
115+
} catch (e) {
116+
console.warn('Error parsing submission:', e);
117+
}
118+
}
119+
120+
return submissions;
121+
}
122+
123+
/**
124+
* Load unmerged driver submissions (status = 'synced')
125+
*/
126+
export async function loadUnmergedSubmissions(api: GeotabApi): Promise<SubmissionRecord[]> {
127+
const all = await loadSubmissions(api);
128+
return all.filter(s => s.submission.status === 'synced');
129+
}
130+
86131
/**
87132
* Update a report in AddInData
88133
*/

src/FleetClaim.AddIn.React/app/types/report.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,43 @@ export interface ReportRequest {
152152
toDate: string;
153153
status: RequestStatus;
154154
forceReport?: boolean;
155+
linkedSubmissionId?: string;
155156
error?: string;
156157
completedAt?: string;
157158
}
158159

160+
export interface DriverSubmission {
161+
id: string;
162+
deviceId: string;
163+
deviceName?: string;
164+
driverId?: string;
165+
driverName?: string;
166+
incidentTimestamp: string;
167+
latitude?: number;
168+
longitude?: number;
169+
locationAddress?: string;
170+
description?: string;
171+
severity?: Severity;
172+
damageDescription?: string;
173+
damageLevel?: 'none' | 'minor' | 'moderate' | 'severe' | 'totalLoss';
174+
vehicleDriveable?: boolean;
175+
policeReportNumber?: string;
176+
policeAgency?: string;
177+
injuriesReported?: boolean;
178+
injuryDescription?: string;
179+
notes?: string;
180+
photos?: Photo[];
181+
status: 'synced' | 'merged' | 'converted' | 'standalone';
182+
createdAt: string;
183+
updatedAt: string;
184+
submittedAt?: string;
185+
mergedIntoReportId?: string;
186+
}
187+
159188
export interface AddInDataWrapper<T> {
160-
type: 'report' | 'reportRequest' | 'config' | 'driverSubmission';
189+
type: 'report' | 'reportRequest' | 'config' | 'driverSubmission' | 'workerState';
161190
payload: T;
162191
version?: number;
163192
}
193+
194+
export type SubmissionStatus = 'synced' | 'merged' | 'converted' | 'standalone';
2.55 KB
Binary file not shown.

src/FleetClaim.AddIn.React/dist/fleetclaim.079b19b9.js renamed to src/FleetClaim.AddIn.React/dist/fleetclaim.a8d874fc.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/FleetClaim.AddIn.React/dist/fleetclaim.079b19b9.js.LICENSE.txt renamed to src/FleetClaim.AddIn.React/dist/fleetclaim.a8d874fc.js.LICENSE.txt

File renamed without changes.

src/FleetClaim.AddIn.React/dist/fleetclaim.079b19b9.js.map renamed to src/FleetClaim.AddIn.React/dist/fleetclaim.a8d874fc.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>FleetClaim</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap" rel="stylesheet"><script defer="defer" src="fleetclaim.079b19b9.js"></script><link href="fleetclaim.fb977bff.css" rel="stylesheet"></head><body><div id="fleetclaim"></div></body></html>
1+
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>FleetClaim</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap" rel="stylesheet"><script defer="defer" src="fleetclaim.a8d874fc.js"></script><link href="fleetclaim.fb977bff.css" rel="stylesheet"></head><body><div id="fleetclaim"></div></body></html>

0 commit comments

Comments
 (0)