Changeset 9af201e for frontend/src/pages/billing
- Timestamp:
- 09/04/26 19:08:08 (2 weeks ago)
- Branches:
- master
- Children:
- cdcff72
- Parents:
- e1f74f6
- Location:
- frontend/src/pages/billing
- Files:
-
- 2 edited
-
BillingDetail.js (modified) (5 diffs)
-
BillingList.js (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
frontend/src/pages/billing/BillingDetail.js
re1f74f6 r9af201e 22 22 23 23 useEffect(() => { 24 fetchBilling(); 25 }, [id]); 26 27 const fetchBilling = async () => { 28 try { 29 setLoading(true); 30 const response = await billingService.getBillingById(id); 24 const fetchBilling = async () => { 25 try { 26 setLoading(true); 27 const response = await billingService.getBillingById(id); 31 28 setBilling(response.data); 32 29 setPaymentStatus(response.data.paymentStatus); … … 39 36 console.error('Could not fetch billing details:', err); 40 37 } 41 } catch (err) { 42 setError('Failed to fetch billing record'); 43 console.error(err); 44 } finally { 45 setLoading(false); 46 } 47 }; 38 } catch (err) { 39 setError('Failed to fetch billing record'); 40 console.error(err); 41 } finally { 42 setLoading(false); 43 } 44 }; 45 46 fetchBilling(); 47 }, [id]); 48 48 49 49 const handleUpdatePaymentStatus = async () => { … … 81 81 if (user.role === 'DOCTOR' || user.role === 'LAB_TECHNICIAN') { 82 82 return ( 83 <div style={{ padding: '20px', textAlign: 'center' }}>84 <h1 className="text-2xl font-bold" style={{ color: '#7c3aed', marginBottom: '10px' }}>Access Denied</h1>85 <p style={{ color: 'var(--color-neutral-600)' }}>You do not have permission to access billing records.</p>86 </div>83 <div style={{ padding: '20px', textAlign: 'center' }}> 84 <h1 className="text-2xl font-bold" style={{ color: '#7c3aed', marginBottom: '10px' }}>Access Denied</h1> 85 <p style={{ color: 'var(--color-neutral-600)' }}>You do not have permission to access billing records.</p> 86 </div> 87 87 ); 88 88 } … … 90 90 if (!billing) { 91 91 return ( 92 <div>93 <ErrorAlert message="Billing record not found" onClose={() => navigate('/billing')} />94 </div>92 <div> 93 <ErrorAlert message="Billing record not found" onClose={() => navigate('/billing')} /> 94 </div> 95 95 ); 96 96 } 97 97 98 98 return ( 99 <div> 100 <div className="flex justify-between items-center mb-6"> 101 <h1 className="text-3xl font-bold" style={{ color: '#7c3aed' }}>Billing Record #{billing.billId}</h1> 102 <div className="space-x-2"> 103 <button 104 onClick={handleDownloadInvoice} 105 disabled={downloading} 106 className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 disabled:opacity-50" 107 > 108 {downloading ? 'Downloading...' : 'Download Invoice (PDF)'} 109 </button> 110 <button onClick={() => navigate('/billing')} className="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400"> 111 Back 112 </button> 113 </div> 99 <div> 100 <div className="flex justify-between items-center mb-6"> 101 <h1 className="text-3xl font-bold" style={{ color: '#7c3aed' }}>Billing Record #{billing.billId}</h1> 102 <div className="space-x-2"> 103 <button 104 onClick={handleDownloadInvoice} 105 disabled={downloading} 106 className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 disabled:opacity-50" 107 > 108 {downloading ? 'Downloading...' : 'Download Invoice (PDF)'} 109 </button> 110 <button onClick={() => navigate('/billing')} className="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400"> 111 Back 112 </button> 114 113 </div> 115 116 {error && <ErrorAlert message={error} onClose={() => setError(null)} />} 117 {success && <SuccessAlert message={success} onClose={() => setSuccess(null)} />} 118 119 <div className={`grid gap-6 mb-6 ${isPatient ? 'grid-cols-1 lg:grid-cols-2' : 'grid-cols-1 lg:grid-cols-3'}`}> 114 </div> 115 116 {error && <ErrorAlert message={error} onClose={() => setError(null)} />} 117 {success && <SuccessAlert message={success} onClose={() => setSuccess(null)} />} 118 119 <div className={`grid gap-6 mb-6 ${isPatient ? 'grid-cols-1 lg:grid-cols-2' : 'grid-cols-1 lg:grid-cols-3'}`}> 120 <div className="bg-white rounded-lg shadow p-6"> 121 <h2 className="text-xl font-bold mb-4">Billing Information</h2> 122 <div className="space-y-3"> 123 <InfoRow label="Patient" value={billing.patientName} /> 124 <InfoRow label="Total Cost" value={`$${billing.totalCost}`} /> 125 <InfoRow label="Current Status" value={billing.paymentStatus} /> 126 <InfoRow label="Payment Date" value={billing.paymentDate || 'Not paid'} /> 127 </div> 128 </div> 129 130 {!isPatient && ( 120 131 <div className="bg-white rounded-lg shadow p-6"> 121 <h2 className="text-xl font-bold mb-4">Billing Information</h2> 122 <div className="space-y-3"> 123 <InfoRow label="Patient" value={billing.patientName} /> 124 <InfoRow label="Total Cost" value={`$${billing.totalCost}`} /> 125 <InfoRow label="Current Status" value={billing.paymentStatus} /> 126 <InfoRow label="Payment Date" value={billing.paymentDate || 'Not paid'} /> 132 <h2 className="text-xl font-bold mb-4">Update Payment Status</h2> 133 <div className="space-y-4"> 134 <div> 135 <label className="block text-sm font-semibold mb-2">Payment Status</label> 136 <select 137 value={paymentStatus} 138 onChange={(e) => setPaymentStatus(e.target.value)} 139 className="w-full px-4 py-2 border rounded-lg" 140 > 141 <option value="PENDING">Pending</option> 142 <option value="PAID">Paid</option> 143 <option value="CANCELLED">Cancelled</option> 144 </select> 145 </div> 146 <button 147 onClick={handleUpdatePaymentStatus} 148 disabled={updating || paymentStatus === billing.paymentStatus} 149 className="w-full bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700 disabled:opacity-50" 150 > 151 {updating ? 'Updating...' : 'Update Status'} 152 </button> 127 153 </div> 128 154 </div> 129 130 {!isPatient && ( 131 <div className="bg-white rounded-lg shadow p-6"> 132 <h2 className="text-xl font-bold mb-4">Update Payment Status</h2> 133 <div className="space-y-4"> 134 <div> 135 <label className="block text-sm font-semibold mb-2">Payment Status</label> 136 <select 137 value={paymentStatus} 138 onChange={(e) => setPaymentStatus(e.target.value)} 139 className="w-full px-4 py-2 border rounded-lg" 140 > 141 <option value="PENDING">Pending</option> 142 <option value="PAID">Paid</option> 143 <option value="CANCELLED">Cancelled</option> 144 </select> 145 </div> 146 <button 147 onClick={handleUpdatePaymentStatus} 148 disabled={updating || paymentStatus === billing.paymentStatus} 149 className="w-full bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700 disabled:opacity-50" 150 > 151 {updating ? 'Updating...' : 'Update Status'} 152 </button> 153 </div> 154 </div> 155 )} 156 157 <div className="bg-white rounded-lg shadow p-6"> 158 <h2 className="text-xl font-bold mb-4">Patient Details</h2> 159 <div className="space-y-3"> 160 {billingDetail && ( 161 <> 162 <InfoRow label="EMBG" value={billingDetail.patientEmbg} /> 163 <InfoRow label="Phone" value={billingDetail.patientPhone} /> 164 <InfoRow label="Bill Date" value={billingDetail.billDate} /> 165 </> 166 )} 155 )} 156 157 <div className="bg-white rounded-lg shadow p-6"> 158 <h2 className="text-xl font-bold mb-4">Patient Details</h2> 159 <div className="space-y-3"> 160 {billingDetail && ( 161 <> 162 <InfoRow label="EMBG" value={billingDetail.patientEmbg} /> 163 <InfoRow label="Phone" value={billingDetail.patientPhone} /> 164 <InfoRow label="Bill Date" value={billingDetail.billDate} /> 165 </> 166 )} 167 </div> 168 </div> 169 </div> 170 171 <div className="bg-white rounded-lg shadow p-6"> 172 <h2 className="text-xl font-bold mb-4">Itemized Services</h2> 173 174 {billingDetail && (billingDetail.procedures.length > 0 || billingDetail.labTests.length > 0) ? ( 175 <div className="space-y-4"> 176 {billingDetail.procedures.length > 0 && ( 177 <div> 178 <h3 className="font-semibold text-lg mb-2">Procedures</h3> 179 <table className="w-full"> 180 <thead className="bg-gray-100"> 181 <tr> 182 <th className="px-4 py-2 text-left">Description</th> 183 <th className="px-4 py-2 text-right">Cost</th> 184 </tr> 185 </thead> 186 <tbody> 187 {billingDetail.procedures.map((proc, idx) => ( 188 <tr key={idx} className="border-t"> 189 <td className="px-4 py-2">{proc.description}</td> 190 <td className="px-4 py-2 text-right">${proc.cost}</td> 191 </tr> 192 ))} 193 </tbody> 194 </table> 195 </div> 196 )} 197 198 {billingDetail.labTests.length > 0 && ( 199 <div> 200 <h3 className="font-semibold text-lg mb-2">Lab Tests</h3> 201 <table className="w-full"> 202 <thead className="bg-gray-100"> 203 <tr> 204 <th className="px-4 py-2 text-left">Description</th> 205 <th className="px-4 py-2 text-right">Cost</th> 206 </tr> 207 </thead> 208 <tbody> 209 {billingDetail.labTests.map((test, idx) => ( 210 <tr key={idx} className="border-t"> 211 <td className="px-4 py-2">{test.description}</td> 212 <td className="px-4 py-2 text-right">${test.cost}</td> 213 </tr> 214 ))} 215 </tbody> 216 </table> 217 </div> 218 )} 219 220 <div className="border-t-2 pt-4 mt-4 flex justify-end"> 221 <div className="text-right"> 222 <p className="text-gray-600">Total Amount:</p> 223 <p className="text-2xl font-bold text-purple-600">${billing.totalCost}</p> 224 </div> 167 225 </div> 168 226 </div> 169 </div> 170 171 <div className="bg-white rounded-lg shadow p-6"> 172 <h2 className="text-xl font-bold mb-4">Itemized Services</h2> 173 174 {billingDetail && (billingDetail.procedures.length > 0 || billingDetail.labTests.length > 0) ? ( 175 <div className="space-y-4"> 176 {billingDetail.procedures.length > 0 && ( 177 <div> 178 <h3 className="font-semibold text-lg mb-2">Procedures</h3> 179 <table className="w-full"> 180 <thead className="bg-gray-100"> 181 <tr> 182 <th className="px-4 py-2 text-left">Description</th> 183 <th className="px-4 py-2 text-right">Cost</th> 184 </tr> 185 </thead> 186 <tbody> 187 {billingDetail.procedures.map((proc, idx) => ( 188 <tr key={idx} className="border-t"> 189 <td className="px-4 py-2">{proc.description}</td> 190 <td className="px-4 py-2 text-right">${proc.cost}</td> 191 </tr> 192 ))} 193 </tbody> 194 </table> 195 </div> 196 )} 197 198 {billingDetail.labTests.length > 0 && ( 199 <div> 200 <h3 className="font-semibold text-lg mb-2">Lab Tests</h3> 201 <table className="w-full"> 202 <thead className="bg-gray-100"> 203 <tr> 204 <th className="px-4 py-2 text-left">Description</th> 205 <th className="px-4 py-2 text-right">Cost</th> 206 </tr> 207 </thead> 208 <tbody> 209 {billingDetail.labTests.map((test, idx) => ( 210 <tr key={idx} className="border-t"> 211 <td className="px-4 py-2">{test.description}</td> 212 <td className="px-4 py-2 text-right">${test.cost}</td> 213 </tr> 214 ))} 215 </tbody> 216 </table> 217 </div> 218 )} 219 220 <div className="border-t-2 pt-4 mt-4 flex justify-end"> 221 <div className="text-right"> 222 <p className="text-gray-600">Total Amount:</p> 223 <p className="text-2xl font-bold text-purple-600">${billing.totalCost}</p> 224 </div> 225 </div> 226 </div> 227 ) : ( 228 <p className="text-gray-500">No services itemized for this billing record.</p> 229 )} 230 </div> 231 </div> 227 ) : ( 228 <p className="text-gray-500">No services itemized for this billing record.</p> 229 )} 230 </div> 231 </div> 232 232 ); 233 233 } … … 235 235 function InfoRow({ label, value }) { 236 236 return ( 237 <div className="flex justify-between">238 <span className="text-gray-800">{label}:</span>239 <span className="text-gray-800">{value || 'N/A'}</span>240 </div>237 <div className="flex justify-between"> 238 <span className="text-gray-800">{label}:</span> 239 <span className="text-gray-800">{value || 'N/A'}</span> 240 </div> 241 241 ); 242 242 } -
frontend/src/pages/billing/BillingList.js
re1f74f6 r9af201e 89 89 // Refresh the list 90 90 const response = user.role === 'BILLING_ADMIN' 91 ? await billingService.getAllBillings()92 : await billingService.getBillingHistoryForPatient(patientId || user.patientId);91 ? await billingService.getAllBillings() 92 : await billingService.getBillingHistoryForPatient(patientId || user.patientId); 93 93 setBillings(response.data || []); 94 94 } catch (err) { … … 101 101 if (user.role === 'DOCTOR' || user.role === 'LAB_TECHNICIAN') { 102 102 return ( 103 <div style={{ padding: '20px', textAlign: 'center' }}>104 <h1 className="text-2xl font-bold" style={{ color: '#7c3aed', marginBottom: '10px' }}>Access Denied</h1>105 <p style={{ color: 'var(--color-neutral-600)' }}>You do not have permission to access billing records.</p>106 </div>103 <div style={{ padding: '20px', textAlign: 'center' }}> 104 <h1 className="text-2xl font-bold" style={{ color: '#7c3aed', marginBottom: '10px' }}>Access Denied</h1> 105 <p style={{ color: 'var(--color-neutral-600)' }}>You do not have permission to access billing records.</p> 106 </div> 107 107 ); 108 108 } … … 111 111 112 112 return ( 113 <div> 114 <h1 className="text-3xl font-bold mb-6" style={{ color: '#7c3aed' }}>{patientId ? 'Patient Billing History' : 'Billing Records'}</h1> 115 116 {error && <ErrorAlert message={error} onClose={() => setError(null)} />} 117 {success && <SuccessAlert message={success} onClose={() => setSuccess(null)} />} 118 119 {/* Filters for Billing Admin */} 120 {user.role === 'BILLING_ADMIN' && ( 121 <div className="bg-white rounded-lg shadow p-6 mb-6"> 122 <h2 className="text-lg font-bold mb-4">Filters</h2> 123 <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> 113 <div> 114 <h1 className="text-3xl font-bold mb-6" style={{ color: '#7c3aed' }}>{patientId ? 'Patient Billing History' : 'Billing Records'}</h1> 115 116 {error && <ErrorAlert message={error} onClose={() => setError(null)} />} 117 {success && <SuccessAlert message={success} onClose={() => setSuccess(null)} />} 118 119 {/* Filters for Billing Admin */} 120 {user.role === 'BILLING_ADMIN' && ( 121 <div className="bg-white rounded-lg shadow p-6 mb-6"> 122 <h2 className="text-lg font-bold mb-4">Filters</h2> 123 <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> 124 <div> 125 <label className="block text-sm font-semibold mb-2">Status</label> 126 <select 127 value={statusFilter} 128 onChange={(e) => setStatusFilter(e.target.value)} 129 className="w-full px-4 py-2 border rounded-lg" 130 > 131 <option value="">All Statuses</option> 132 <option value="PENDING">Pending</option> 133 <option value="PAID">Paid</option> 134 <option value="PROCESSING">Processing</option> 135 </select> 136 </div> 137 <div> 138 <label className="block text-sm font-semibold mb-2">Patient</label> 139 <select 140 value={patientFilter} 141 onChange={(e) => setPatientFilter(e.target.value)} 142 className="w-full px-4 py-2 border rounded-lg" 143 > 144 <option value="">All Patients</option> 145 {patients.map(p => ( 146 <option key={p.patientId} value={`${p.firstName} ${p.lastName}`}> 147 {p.firstName} {p.lastName} 148 </option> 149 ))} 150 </select> 151 </div> 152 </div> 153 </div> 154 )} 155 156 {/* Pending Billing Records Section (for Billing Admin) */} 157 {user.role === 'BILLING_ADMIN' && pendingBillings.length > 0 && ( 158 <div className="bg-yellow-50 rounded-lg shadow p-6 mb-6 border-l-4 border-yellow-500"> 159 <h2 className="text-lg font-bold mb-4">Pending Billing Records ({pendingBillings.length})</h2> 160 <div className="space-y-3"> 161 {pendingBillings.map(billing => ( 162 <div key={billing.billId} className="bg-white rounded p-4 flex justify-between items-center"> 124 163 <div> 125 <label className="block text-sm font-semibold mb-2">Status</label> 126 <select 127 value={statusFilter} 128 onChange={(e) => setStatusFilter(e.target.value)} 129 className="w-full px-4 py-2 border rounded-lg" 130 > 131 <option value="">All Statuses</option> 132 <option value="PENDING">Pending</option> 133 <option value="PAID">Paid</option> 134 <option value="PROCESSING">Processing</option> 135 </select> 164 <p className="font-semibold">{billing.patientName}</p> 165 <p className="text-sm text-gray-600">${billing.totalCost}</p> 136 166 </div> 137 <div> 138 <label className="block text-sm font-semibold mb-2">Patient</label> 139 <select 140 value={patientFilter} 141 onChange={(e) => setPatientFilter(e.target.value)} 142 className="w-full px-4 py-2 border rounded-lg" 143 > 144 <option value="">All Patients</option> 145 {patients.map(p => ( 146 <option key={p.patientId} value={`${p.firstName} ${p.lastName}`}> 147 {p.firstName} {p.lastName} 148 </option> 149 ))} 150 </select> 151 </div> 167 <button 168 onClick={() => handleUpdateStatus(billing.billId, 'PAID')} 169 className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 text-sm" 170 > 171 Mark Paid 172 </button> 152 173 </div> 153 </div> 154 )} 155 156 {/* Pending Billing Records Section (for Billing Admin) */} 157 {user.role === 'BILLING_ADMIN' && pendingBillings.length > 0 && ( 158 <div className="bg-yellow-50 rounded-lg shadow p-6 mb-6 border-l-4 border-yellow-500"> 159 <h2 className="text-lg font-bold mb-4">Pending Billing Records ({pendingBillings.length})</h2> 160 <div className="space-y-3"> 161 {pendingBillings.map(billing => ( 162 <div key={billing.billId} className="bg-white rounded p-4 flex justify-between items-center"> 163 <div> 164 <p className="font-semibold">{billing.patientName}</p> 165 <p className="text-sm text-gray-600">${billing.totalCost}</p> 166 </div> 167 <button 168 onClick={() => handleUpdateStatus(billing.billId, 'PAID')} 169 className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 text-sm" 170 > 171 Mark Paid 172 </button> 173 </div> 174 ))} 175 </div> 176 </div> 177 )} 178 179 <div className="bg-white rounded-lg shadow overflow-hidden"> 180 <table className="w-full"> 181 <thead className="bg-gray-100"> 174 ))} 175 </div> 176 </div> 177 )} 178 179 <div className="bg-white rounded-lg shadow overflow-hidden"> 180 <table className="w-full"> 181 <thead className="bg-gray-100"> 182 182 <tr> 183 183 <th className="px-6 py-3 text-left text-sm font-semibold">Patient</th> … … 187 187 <th className="px-6 py-3 text-left text-sm font-semibold">Actions</th> 188 188 </tr> 189 </thead>190 <tbody>189 </thead> 190 <tbody> 191 191 {filteredBillings.map(billing => ( 192 <tr key={billing.billId} className="border-t hover:bg-gray-50">193 <td className="px-6 py-3">{billing.patientName}</td>194 <td className="px-6 py-3">${billing.totalCost}</td>195 <td className="px-6 py-3">192 <tr key={billing.billId} className="border-t hover:bg-gray-50"> 193 <td className="px-6 py-3">{billing.patientName}</td> 194 <td className="px-6 py-3">${billing.totalCost}</td> 195 <td className="px-6 py-3"> 196 196 <span className={`px-3 py-1 rounded text-sm font-semibold ${ 197 billing.paymentStatus === 'PENDING' ? 'bg-yellow-100 text-yellow-800' :198 billing.paymentStatus === 'PAID' ? 'bg-green-100 text-green-800' :199 'bg-red-100 text-red-800'197 billing.paymentStatus === 'PENDING' ? 'bg-yellow-100 text-yellow-800' : 198 billing.paymentStatus === 'PAID' ? 'bg-green-100 text-green-800' : 199 'bg-red-100 text-red-800' 200 200 }`}> 201 201 {billing.paymentStatus} 202 202 </span> 203 </td>204 <td className="px-6 py-3">{billing.paymentDate || 'Not paid'}</td>205 <td className="px-6 py-3">206 <Link to={`/billing/${billing.billId}`} style={{ color: '#7c3aed', textDecoration: 'none', fontWeight: '400' }} onMouseEnter={(e) => e.currentTarget.style.textDecoration = 'underline'} onMouseLeave={(e) => e.currentTarget.style.textDecoration = 'none'}>207 View208 </Link>209 </td>210 </tr>203 </td> 204 <td className="px-6 py-3">{billing.paymentDate || 'Not paid'}</td> 205 <td className="px-6 py-3"> 206 <Link to={`/billing/${billing.billId}`} style={{ color: '#7c3aed', textDecoration: 'none', fontWeight: '400' }} onMouseEnter={(e) => e.currentTarget.style.textDecoration = 'underline'} onMouseLeave={(e) => e.currentTarget.style.textDecoration = 'none'}> 207 View 208 </Link> 209 </td> 210 </tr> 211 211 ))} 212 </tbody> 213 </table> 214 215 {filteredBillings.length === 0 && ( 216 <div className="p-6 text-center text-gray-500"> 217 {billings.length === 0 ? 'No billing records found' : 'No records match the selected filters'} 218 </div> 219 )} 220 </div> 212 </tbody> 213 </table> 214 215 {filteredBillings.length === 0 && ( 216 <div className="p-6 text-center text-gray-500"> 217 {billings.length === 0 ? 'No billing records found' : 'No records match the selected filters'} 218 </div> 219 )} 221 220 </div> 221 </div> 222 222 ); 223 223 }
Note:
See TracChangeset
for help on using the changeset viewer.
