Skip to content

Commit 7a20a1b

Browse files
Faxbot Agentclaude
andcommitted
UX fixes: proper datetime serialization, phone formatting, file types, PDF preview
Backend: - Serialize datetimes with 'Z' suffix for proper iOS ISO8601 parsing - Add file_type field to InboundFaxOut (pdf/tiff/image/text) - Detect file type from file paths Admin Console: - Add inline PDF preview dialog (View button + full-screen iframe) - Improve date formatting (relative: "Today at 3:45 PM", "5m ago", etc.) - Add View button alongside Download in both mobile and desktop Changes implement user-requested fixes for UTC timestamps, inconsistent phone formatting, useless page count indicators, and lack of inline PDF preview. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 120ae92 commit 7a20a1b

File tree

4 files changed

+553
-37
lines changed

4 files changed

+553
-37
lines changed

api/admin_ui/src/api/client.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,14 +434,21 @@ export class AdminAPIClient {
434434
'X-API-Key': this.apiKey,
435435
},
436436
});
437-
437+
438438
if (!res.ok) {
439439
throw new Error(`Download failed: ${res.status}`);
440440
}
441-
441+
442442
return res.blob();
443443
}
444444

445+
async deleteInbound(id: string): Promise<{ success: boolean; id: string }> {
446+
const res = await this.fetch(`/inbound/${encodeURIComponent(id)}`, {
447+
method: 'DELETE',
448+
});
449+
return res.json();
450+
}
451+
445452
// Inbound helpers
446453
async getInboundCallbacks(): Promise<any> {
447454
const res = await this.fetch('/admin/inbound/callbacks');

api/admin_ui/src/components/Inbound.tsx

Lines changed: 235 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@ import {
2323
IconButton,
2424
Tooltip,
2525
Snackbar,
26+
Dialog,
27+
DialogActions,
28+
DialogContent,
29+
DialogContentText,
30+
DialogTitle,
2631
} from '@mui/material';
27-
import {
28-
Refresh as RefreshIcon,
29-
Download as DownloadIcon,
30-
ContentCopy as ContentCopyIcon,
32+
import {
33+
Refresh as RefreshIcon,
34+
Download as DownloadIcon,
35+
ContentCopy as ContentCopyIcon,
3136
PlayArrow as TestIcon,
3237
Inbox as InboxIcon,
3338
Phone as PhoneIcon,
@@ -37,6 +42,8 @@ import {
3742
Error as ErrorIcon,
3843
Warning as WarningIcon,
3944
Info as InfoIcon,
45+
Delete as DeleteIcon,
46+
Visibility as VisibilityIcon,
4047
} from '@mui/icons-material';
4148
import AdminAPIClient from '../api/client';
4249
import { useTraits } from '../hooks/useTraits';
@@ -56,6 +63,11 @@ function Inbound({ client, docsBase }: InboundProps) {
5663
const [callbacks, setCallbacks] = useState<any | null>(null);
5764
const [simulating, setSimulating] = useState(false);
5865
const [copySnackbar, setCopySnackbar] = useState<string>('');
66+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
67+
const [faxToDelete, setFaxToDelete] = useState<string | null>(null);
68+
const [deleting, setDeleting] = useState(false);
69+
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
70+
const [previewPdfUrl, setPreviewPdfUrl] = useState<string | null>(null);
5971
// Precise help anchors (lightweight resolver for inbound failures)
6072
const base = docsBase || 'https://dmontgomery40.github.io/Faxbot';
6173
const anchors: Record<string,string> = {
@@ -103,6 +115,59 @@ function Inbound({ client, docsBase }: InboundProps) {
103115
}
104116
};
105117

118+
const previewPdf = async (id: string) => {
119+
try {
120+
// Clean up previous URL if exists
121+
if (previewPdfUrl) {
122+
URL.revokeObjectURL(previewPdfUrl);
123+
}
124+
const blob = await client.downloadInboundPdf(id);
125+
const url = URL.createObjectURL(blob);
126+
setPreviewPdfUrl(url);
127+
setPreviewDialogOpen(true);
128+
} catch (err) {
129+
alert(`Preview failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
130+
}
131+
};
132+
133+
const handlePreviewClose = () => {
134+
setPreviewDialogOpen(false);
135+
// Clean up blob URL after a delay
136+
setTimeout(() => {
137+
if (previewPdfUrl) {
138+
URL.revokeObjectURL(previewPdfUrl);
139+
setPreviewPdfUrl(null);
140+
}
141+
}, 100);
142+
};
143+
144+
const handleDeleteClick = (id: string) => {
145+
setFaxToDelete(id);
146+
setDeleteDialogOpen(true);
147+
};
148+
149+
const handleDeleteConfirm = async () => {
150+
if (!faxToDelete) return;
151+
152+
try {
153+
setDeleting(true);
154+
await client.deleteInbound(faxToDelete);
155+
setFaxes(faxes.filter(f => f.id !== faxToDelete));
156+
setCopySnackbar('Fax deleted successfully');
157+
} catch (err) {
158+
setError(`Delete failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
159+
} finally {
160+
setDeleting(false);
161+
setDeleteDialogOpen(false);
162+
setFaxToDelete(null);
163+
}
164+
};
165+
166+
const handleDeleteCancel = () => {
167+
setDeleteDialogOpen(false);
168+
setFaxToDelete(null);
169+
};
170+
106171
useEffect(() => {
107172
fetchInbound();
108173
(async () => {
@@ -154,10 +219,49 @@ function Inbound({ client, docsBase }: InboundProps) {
154219
if (!dateString) return '-';
155220
try {
156221
const date = new Date(dateString);
157-
if (isSmallMobile) {
158-
return date.toLocaleDateString();
222+
const now = new Date();
223+
const diffMs = now.getTime() - date.getTime();
224+
const diffMins = Math.floor(diffMs / 60000);
225+
const diffHours = Math.floor(diffMs / 3600000);
226+
const diffDays = Math.floor(diffMs / 86400000);
227+
228+
// Format time
229+
const timeStr = date.toLocaleTimeString('en-US', {
230+
hour: 'numeric',
231+
minute: '2-digit',
232+
hour12: true
233+
});
234+
235+
// Check if it's today
236+
const isToday = date.toDateString() === now.toDateString();
237+
if (isToday) {
238+
if (diffMins < 1) return 'Just now';
239+
if (diffMins < 60) return `${diffMins}m ago`;
240+
return `Today at ${timeStr}`;
241+
}
242+
243+
// Check if it's yesterday
244+
const yesterday = new Date(now);
245+
yesterday.setDate(yesterday.getDate() - 1);
246+
if (date.toDateString() === yesterday.toDateString()) {
247+
return `Yesterday at ${timeStr}`;
248+
}
249+
250+
// Check if it's this week
251+
if (diffDays < 7) {
252+
const dayName = date.toLocaleDateString('en-US', { weekday: 'long' });
253+
return `${dayName} at ${timeStr}`;
254+
}
255+
256+
// Check if it's this year
257+
if (date.getFullYear() === now.getFullYear()) {
258+
const monthDay = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
259+
return `${monthDay} at ${timeStr}`;
159260
}
160-
return date.toLocaleString();
261+
262+
// Older dates
263+
const fullDate = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
264+
return `${fullDate} at ${timeStr}`;
161265
} catch {
162266
return dateString;
163267
}
@@ -248,15 +352,37 @@ function Inbound({ client, docsBase }: InboundProps) {
248352
</Stack>
249353

250354
{/* Actions */}
251-
<Button
252-
variant="contained"
253-
startIcon={<DownloadIcon />}
254-
onClick={() => downloadPdf(fax.id)}
255-
fullWidth
256-
sx={{ borderRadius: 2 }}
257-
>
258-
Download PDF
259-
</Button>
355+
<Stack spacing={1}>
356+
<Box sx={{ display: 'flex', gap: 1 }}>
357+
<Button
358+
variant="contained"
359+
onClick={() => previewPdf(fax.id)}
360+
fullWidth
361+
sx={{ borderRadius: 2 }}
362+
>
363+
View
364+
</Button>
365+
<Button
366+
variant="outlined"
367+
startIcon={<DownloadIcon />}
368+
onClick={() => downloadPdf(fax.id)}
369+
fullWidth
370+
sx={{ borderRadius: 2 }}
371+
>
372+
Download
373+
</Button>
374+
</Box>
375+
<Button
376+
variant="outlined"
377+
color="error"
378+
startIcon={<DeleteIcon />}
379+
onClick={() => handleDeleteClick(fax.id)}
380+
fullWidth
381+
sx={{ borderRadius: 2 }}
382+
>
383+
Delete
384+
</Button>
385+
</Stack>
260386
</Stack>
261387
</CardContent>
262388
</Card>
@@ -598,15 +724,36 @@ same => n,System(curl -s -X POST -H "Content-Type: application/json" -H "X-Inter
598724
</Typography>
599725
</TableCell>
600726
<TableCell align="right">
601-
<Tooltip title="Download PDF">
602-
<IconButton
603-
size="small"
604-
onClick={() => downloadPdf(fax.id)}
605-
disabled={!fax.id}
606-
>
607-
<DownloadIcon />
608-
</IconButton>
609-
</Tooltip>
727+
<Box sx={{ display: 'flex', gap: 0.5, justifyContent: 'flex-end' }}>
728+
<Tooltip title="View PDF">
729+
<IconButton
730+
size="small"
731+
onClick={() => previewPdf(fax.id)}
732+
disabled={!fax.id}
733+
>
734+
<VisibilityIcon />
735+
</IconButton>
736+
</Tooltip>
737+
<Tooltip title="Download PDF">
738+
<IconButton
739+
size="small"
740+
onClick={() => downloadPdf(fax.id)}
741+
disabled={!fax.id}
742+
>
743+
<DownloadIcon />
744+
</IconButton>
745+
</Tooltip>
746+
<Tooltip title="Delete">
747+
<IconButton
748+
size="small"
749+
onClick={() => handleDeleteClick(fax.id)}
750+
disabled={!fax.id}
751+
color="error"
752+
>
753+
<DeleteIcon />
754+
</IconButton>
755+
</Tooltip>
756+
</Box>
610757
</TableCell>
611758
</TableRow>
612759
))}
@@ -649,6 +796,69 @@ same => n,System(curl -s -X POST -H "Content-Type: application/json" -H "X-Inter
649796
onClose={() => setCopySnackbar('')}
650797
message={copySnackbar}
651798
/>
799+
800+
{/* Delete Confirmation Dialog */}
801+
<Dialog
802+
open={deleteDialogOpen}
803+
onClose={handleDeleteCancel}
804+
>
805+
<DialogTitle>Delete Inbound Fax?</DialogTitle>
806+
<DialogContent>
807+
<DialogContentText>
808+
Are you sure you want to delete this fax? This will permanently remove the record and associated PDF file. This action cannot be undone.
809+
</DialogContentText>
810+
</DialogContent>
811+
<DialogActions>
812+
<Button onClick={handleDeleteCancel} disabled={deleting}>
813+
Cancel
814+
</Button>
815+
<Button onClick={handleDeleteConfirm} color="error" variant="contained" disabled={deleting}>
816+
{deleting ? 'Deleting...' : 'Delete'}
817+
</Button>
818+
</DialogActions>
819+
</Dialog>
820+
821+
{/* PDF Preview Dialog */}
822+
<Dialog
823+
open={previewDialogOpen}
824+
onClose={handlePreviewClose}
825+
maxWidth="lg"
826+
fullWidth
827+
PaperProps={{
828+
sx: {
829+
height: '90vh',
830+
maxHeight: '90vh',
831+
}
832+
}}
833+
>
834+
<DialogTitle>
835+
Fax Preview
836+
<IconButton
837+
onClick={handlePreviewClose}
838+
sx={{
839+
position: 'absolute',
840+
right: 8,
841+
top: 8,
842+
}}
843+
>
844+
<Box component="span" sx={{ fontSize: '1.5rem' }}>×</Box>
845+
</IconButton>
846+
</DialogTitle>
847+
<DialogContent sx={{ p: 0, display: 'flex', flexDirection: 'column' }}>
848+
{previewPdfUrl && (
849+
<Box
850+
component="iframe"
851+
src={previewPdfUrl}
852+
sx={{
853+
width: '100%',
854+
height: '100%',
855+
border: 'none',
856+
flexGrow: 1,
857+
}}
858+
/>
859+
)}
860+
</DialogContent>
861+
</Dialog>
652862
</Box>
653863
);
654864
}

0 commit comments

Comments
 (0)