Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ A web-based behavioral observation application for Board Certified Behavior Anal
### Data Management

- **Auto-Save** - Automatic localStorage persistence with timestamp indicator
- **Offline Mode** - The form remains usable without internet; submissions queue locally and sync automatically when connectivity returns (see [Offline mode](#offline-mode) below)
- **Export to CSV** - Full data export in CSV format
- **Export to Word** - Professional formatted .docx document generation
- **Print Support** - Print-optimized layout
Expand Down Expand Up @@ -91,6 +92,47 @@ src/
5. **Set Recommendations** - Check applicable recommendations and next steps
6. **End & Export** - Click "End Observation" and export to CSV or Word

## Offline mode

The app works offline once it has been loaded online at least once. Behind the scenes
it uses a service worker (PWA) to cache the JavaScript, styles, and fonts needed to
run, and a local sync queue to hold submissions until the network returns.

### How to set up a device for offline use

1. Open the app on the device while connected to the internet.
2. Wait until you see the green **Offline-ready** pill next to the connectivity dot
in the bottom bar. That confirms the service worker has cached everything.
3. (Optional but recommended) **Add to Home Screen** so the app launches like a
native app:
- iOS Safari: Share → Add to Home Screen
- Android Chrome: Menu → Install app
4. After that, the app will launch and the form will work even without internet.

### How offline submissions work

- The form is **always usable**, online or offline. Your draft is auto-saved to
the device on every change.
- When you click **Submit** while offline (the button reads **Save & Queue**), the
full report is added to a local sync queue and the editor is reset so you can
start the next observation.
- The connectivity dot shows a small badge with the number of pending reports.
- When the device reconnects, queued reports sync automatically. You can also use
the **Sync** button at any time to push queued reports manually.
- If a sync fails (e.g. the server returns an error), the failed item is shown
with **Retry** and **Discard** controls. Other queued items continue to sync.

### Limitations and verification

- **First-ever launch must be online.** Web browsers cannot open a page they have
never fetched before. After one online launch the app caches everything and
works offline forever after.
- Viewing previously submitted reports under "My Reports" requires connectivity;
only the create-and-save flow works offline.
- To verify offline behavior in Chrome: open DevTools → Application → Service
Workers → tick **Offline**, then hard-reload. The app shell should render and
Submit should queue.

## Mobile/Tablet Optimization

- Large touch targets (44px minimum)
Expand Down
39 changes: 34 additions & 5 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useState, useCallback, useEffect } from 'react';
import { useObservationStorage } from './hooks/useLocalStorage';
import { useSupabase } from './hooks/useSupabase';
import { useSyncQueue } from './hooks/useSyncQueue';
import { useConnectivity } from './hooks/useConnectivity';
import { usePwaStatus } from './hooks/usePwaStatus';

// Components
import { ObservationHeader } from './components/Header/ObservationHeader';
Expand Down Expand Up @@ -35,11 +38,16 @@ const TABS = [
function App() {
const { data, setData, updateField, resetObservation, loadObservation, lastSaved } = useObservationStorage();
const { submitObservation, submitting, submitError, submitSuccess, submitMode } = useSupabase();
const connectivity = useConnectivity();
const { canSubmit } = connectivity;
const syncQueue = useSyncQueue();
const { offlineReady } = usePwaStatus();
const [activeTab, setActiveTab] = useState('narrative');
const [isObserving, setIsObserving] = useState(false);
const [showAdmin, setShowAdmin] = useState(false);
const [showMyReports, setShowMyReports] = useState(false);
const [actionsOpen, setActionsOpen] = useState(false);
const [queueNotice, setQueueNotice] = useState(null);

useEffect(() => {
setIsObserving(!!data.header.startTime && !data.header.endTime);
Expand Down Expand Up @@ -128,12 +136,28 @@ function App() {
}, [resetObservation]);

const handleSubmit = useCallback(async () => {
const result = await submitObservation(data);
if (result && result.mode === 'insert' && result.id) {
// Stamp the new Supabase row id so subsequent submits UPDATE instead of INSERT.
updateField('remoteId', result.id);
if (canSubmit) {
const result = await submitObservation(data);
if (result && result.mode === 'insert' && result.id) {
// First submit succeeded online: free the editor for a new observation.
resetObservation();
setQueueNotice({ kind: 'submitted', at: Date.now() });
} else if (result && result.mode === 'update' && result.id) {
// Editing a previously-submitted row succeeded — keep the data on screen.
updateField('remoteId', result.id);
} else if (!result) {
// Server-side failure: queue locally so the user does not lose work.
syncQueue.enqueue(data);
resetObservation();
setQueueNotice({ kind: 'queued-after-fail', at: Date.now() });
}
return;
}
}, [submitObservation, data, updateField]);
// Offline: queue immediately and start a fresh observation.
syncQueue.enqueue(data);
resetObservation();
setQueueNotice({ kind: 'queued-offline', at: Date.now() });
}, [canSubmit, submitObservation, data, updateField, resetObservation, syncQueue]);

const handleResumeReport = useCallback((report) => {
loadObservation(report);
Expand Down Expand Up @@ -296,6 +320,11 @@ function App() {
onMyReportsOpen={() => setShowMyReports(true)}
open={actionsOpen}
onClose={() => setActionsOpen(false)}
connectivity={connectivity}
syncQueue={syncQueue}
offlineReady={offlineReady}
queueNotice={queueNotice}
onDismissQueueNotice={() => setQueueNotice(null)}
/>
</div>
);
Expand Down
Loading