feat: add manual BMS data refresh functionality and expand battery purchase form with accounting fields
This commit is contained in:
@@ -4,7 +4,8 @@ import { useState, use } from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import {
|
import {
|
||||||
Battery, ArrowLeft, X, BatteryCharging, Activity, Gauge, MapPin, Bike, User, History,
|
Battery, ArrowLeft, X, BatteryCharging, Activity, Gauge, MapPin, Bike, User, History,
|
||||||
Calendar, DollarSign, CheckCircle, Clock, ArrowRightLeft, Handshake, TrendingUp, Edit
|
Calendar, DollarSign, CheckCircle, Clock, ArrowRightLeft, Handshake, TrendingUp, Edit,
|
||||||
|
RefreshCw
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface BMSData {
|
interface BMSData {
|
||||||
@@ -127,12 +128,37 @@ export default function BatteryDetailPage({ params }: { params: Promise<{ id: st
|
|||||||
const [battery, setBattery] = useState<Battery>(mockBattery);
|
const [battery, setBattery] = useState<Battery>(mockBattery);
|
||||||
const [activeTab, setActiveTab] = useState<'info' | 'bms' | 'history' | 'rent'>('info');
|
const [activeTab, setActiveTab] = useState<'info' | 'bms' | 'history' | 'rent'>('info');
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
const handleSaveEdit = (updatedBattery: Battery) => {
|
const handleSaveEdit = (updatedBattery: Battery) => {
|
||||||
setBattery(updatedBattery);
|
setBattery(updatedBattery);
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRefreshBMS = () => {
|
||||||
|
if (!battery.bmsData) return;
|
||||||
|
setRefreshing(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
const bms = battery.bmsData!;
|
||||||
|
setBattery({
|
||||||
|
...battery,
|
||||||
|
bmsData: {
|
||||||
|
...bms,
|
||||||
|
voltage: Math.round((60 + Math.random() * 10) * 10) / 10,
|
||||||
|
current: Math.round((-3 + Math.random() * 4) * 10) / 10,
|
||||||
|
soc: Math.floor(Math.random() * 40) + 60,
|
||||||
|
temperature: 25 + Math.floor(Math.random() * 15),
|
||||||
|
cycles: bms.cycles + 1,
|
||||||
|
health: Math.max(70, Math.min(100, bms.health + (Math.random() > 0.7 ? 1 : 0))),
|
||||||
|
timestamp: new Date().toISOString().replace('T', ' ').substring(0, 19)
|
||||||
|
},
|
||||||
|
currentSoc: Math.floor(Math.random() * 40) + 60,
|
||||||
|
health: Math.max(70, Math.min(100, battery.health + (Math.random() > 0.7 ? 1 : 0)))
|
||||||
|
});
|
||||||
|
setRefreshing(false);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 lg:p-6">
|
<div className="p-4 lg:p-6">
|
||||||
<div className="flex items-center gap-4 mb-6">
|
<div className="flex items-center gap-4 mb-6">
|
||||||
@@ -192,11 +218,18 @@ export default function BatteryDetailPage({ params }: { params: Promise<{ id: st
|
|||||||
|
|
||||||
{battery.bmsData && (
|
{battery.bmsData && (
|
||||||
<div className="p-5 border-b border-slate-100 bg-gradient-to-r from-green-50 to-emerald-50">
|
<div className="p-5 border-b border-slate-100 bg-gradient-to-r from-green-50 to-emerald-50">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<Activity className="w-4 h-4 text-green-600" />
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium text-green-800">Live BMS Data</span>
|
<Activity className="w-4 h-4 text-green-600" />
|
||||||
<span className="text-xs text-green-600 bg-green-100 px-2 py-0.5 rounded-full">Real-time</span>
|
<span className="font-medium text-green-800">Live BMS Data</span>
|
||||||
|
<span className="text-xs text-green-600 bg-green-100 px-2 py-0.5 rounded-full">Real-time</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-green-600">Updated: {battery.bmsData.timestamp}</span>
|
||||||
|
<button onClick={handleRefreshBMS} disabled={refreshing} className="p-1.5 text-green-600 hover:bg-green-100 rounded-lg">
|
||||||
|
<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 md:grid-cols-6 gap-2">
|
<div className="grid grid-cols-3 md:grid-cols-6 gap-2">
|
||||||
<div className="bg-white rounded-lg p-2 text-center">
|
<div className="bg-white rounded-lg p-2 text-center">
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ interface Battery {
|
|||||||
voltage: number;
|
voltage: number;
|
||||||
purchaseDate: string;
|
purchaseDate: string;
|
||||||
purchasePrice: number;
|
purchasePrice: number;
|
||||||
|
transactionMethod?: 'cash' | 'bank_transfer' | 'mobile_banking' | 'cheque' | 'credit' | 'other';
|
||||||
|
autoJournal?: boolean;
|
||||||
|
autoJournalSource?: 'supplier' | 'import' | 'internal' | 'transfer' | 'other';
|
||||||
warrantyExpiry: string;
|
warrantyExpiry: string;
|
||||||
status: 'available' | 'in-use' | 'maintenance' | 'retired' | 'charging';
|
status: 'available' | 'in-use' | 'maintenance' | 'retired' | 'charging';
|
||||||
currentSoc: number;
|
currentSoc: number;
|
||||||
@@ -879,6 +882,48 @@ function BatteryForm({ battery, onSave, onCancel }: { battery: Battery | null; o
|
|||||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Transaction Method</label>
|
||||||
|
<select
|
||||||
|
value={formData.transactionMethod || 'cash'}
|
||||||
|
onChange={(e) => handleChange('transactionMethod', e.target.value as any)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
|
||||||
|
>
|
||||||
|
<option value="cash">Cash</option>
|
||||||
|
<option value="bank_transfer">Bank Transfer</option>
|
||||||
|
<option value="mobile_banking">Mobile Banking</option>
|
||||||
|
<option value="cheque">Cheque</option>
|
||||||
|
<option value="credit">Credit</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.autoJournal || false}
|
||||||
|
onChange={(e) => handleChange('autoJournal', e.target.checked)}
|
||||||
|
className="w-4 h-4 text-accent rounded border-slate-300 focus:ring-accent"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-slate-700">Auto-Journal Entry</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{formData.autoJournal && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">Auto-Journal Source</label>
|
||||||
|
<select
|
||||||
|
value={formData.autoJournalSource || 'supplier'}
|
||||||
|
onChange={(e) => handleChange('autoJournalSource', e.target.value as any)}
|
||||||
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent"
|
||||||
|
>
|
||||||
|
<option value="supplier">Supplier Purchase</option>
|
||||||
|
<option value="import">Import</option>
|
||||||
|
<option value="internal">Internal Transfer</option>
|
||||||
|
<option value="transfer">Transfer</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 mb-1">Warranty Expiry</label>
|
<label className="block text-sm font-medium text-slate-700 mb-1">Warranty Expiry</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
Reference in New Issue
Block a user