From 52504aeb21282065b7f5e1637a051f796c0595d3 Mon Sep 17 00:00:00 2001 From: chark1es Date: Wed, 28 May 2025 09:26:51 -0700 Subject: [PATCH] improve reimbursement submissions --- .../dashboard/reimbursement/ReceiptForm.tsx | 551 +++++++++++++----- .../dashboard/universal/ZoomablePreview.tsx | 156 +++++ 2 files changed, 555 insertions(+), 152 deletions(-) create mode 100644 src/components/dashboard/universal/ZoomablePreview.tsx diff --git a/src/components/dashboard/reimbursement/ReceiptForm.tsx b/src/components/dashboard/reimbursement/ReceiptForm.tsx index 39bddef..26628b8 100644 --- a/src/components/dashboard/reimbursement/ReceiptForm.tsx +++ b/src/components/dashboard/reimbursement/ReceiptForm.tsx @@ -4,6 +4,7 @@ import FilePreview from '../universal/FilePreview'; import { toast } from 'react-hot-toast'; import { motion, AnimatePresence } from 'framer-motion'; import type { ItemizedExpense } from '../../../schemas/pocketbase'; +// import ZoomablePreview from '../universal/ZoomablePreview'; interface ReceiptFormData { file: File; @@ -66,6 +67,35 @@ export default function ReceiptForm({ onSubmit, onCancel }: ReceiptFormProps) { const [locationAddress, setLocationAddress] = useState(''); const [notes, setNotes] = useState(''); const [error, setError] = useState(''); + const [jsonInput, setJsonInput] = useState(''); + const [showJsonInput, setShowJsonInput] = useState(false); + const [zoomLevel, setZoomLevel] = useState(1); + + // Sample JSON data for users to copy + const sampleJsonData = { + itemized_expenses: [ + { + description: "Presentation supplies for IEEE workshop", + category: "Supplies", + amount: 45.99 + }, + { + description: "Team lunch during planning meeting", + category: "Meals", + amount: 82.50 + }, + { + description: "Transportation to conference venue", + category: "Travel", + amount: 28.75 + } + ], + tax: 12.65, + date: "2024-01-15", + location_name: "Office Depot & Local Restaurant", + location_address: "1234 Campus Drive, San Diego, CA 92093", + notes: "Expenses for January IEEE workshop preparation and team coordination meeting" + }; const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { @@ -144,6 +174,69 @@ export default function ReceiptForm({ onSubmit, onCancel }: ReceiptFormProps) { }); }; + const parseJsonData = () => { + try { + if (!jsonInput.trim()) { + toast.error('Please enter JSON data to parse'); + return; + } + + const parsed = JSON.parse(jsonInput); + + // Validate the structure + if (!parsed.itemized_expenses || !Array.isArray(parsed.itemized_expenses)) { + throw new Error('itemized_expenses must be an array'); + } + + // Validate each expense item + for (const item of parsed.itemized_expenses) { + if (!item.description || !item.category || typeof item.amount !== 'number') { + throw new Error('Each expense item must have description, category, and amount'); + } + if (!EXPENSE_CATEGORIES.includes(item.category)) { + throw new Error(`Invalid category: ${item.category}. Must be one of: ${EXPENSE_CATEGORIES.join(', ')}`); + } + } + + // Populate the form fields + setItemizedExpenses(parsed.itemized_expenses); + if (parsed.tax !== undefined) setTax(Number(parsed.tax) || 0); + if (parsed.date) setDate(parsed.date); + if (parsed.location_name) setLocationName(parsed.location_name); + if (parsed.location_address) setLocationAddress(parsed.location_address); + if (parsed.notes) setNotes(parsed.notes); + + setError(''); + toast.success(`Successfully imported ${parsed.itemized_expenses.length} expense items`); + setShowJsonInput(false); + setJsonInput(''); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Invalid JSON format'; + setError(`JSON Parse Error: ${errorMessage}`); + toast.error(`Failed to parse JSON: ${errorMessage}`); + } + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text).then(() => { + toast.success('Sample data copied to clipboard!'); + }).catch(() => { + toast.error('Failed to copy to clipboard'); + }); + }; + + const zoomIn = () => { + setZoomLevel(prev => Math.min(prev + 0.25, 3)); + }; + + const zoomOut = () => { + setZoomLevel(prev => Math.max(prev - 0.25, 0.5)); + }; + + const resetZoom = () => { + setZoomLevel(1); + }; + return ( - {/* Date */} - - - setDate(e.target.value)} - required - /> + {/* Date and Location in Grid */} + +
+ + setDate(e.target.value)} + required + /> +
+ +
+ + setTax(Number(e.target.value))} + min="0" + step="0.01" + placeholder="0.00" + /> +
- {/* Location Name */} - - - setLocationName(e.target.value)} - required - /> + {/* Location Fields */} + +
+ + setLocationName(e.target.value)} + placeholder="Store/vendor name" + required + /> +
+ +
+ + setLocationAddress(e.target.value)} + placeholder="Full address" + required + /> +
- {/* Location Address */} - - - setLocationAddress(e.target.value)} - required - /> - - - {/* Notes */} + {/* Notes - Reduced height */}