added vehicle creation functionality
This commit is contained in:
@@ -128,7 +128,8 @@ class VehicleSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Vehicle
|
model = Vehicle
|
||||||
fields = ['id', 'vehicle_number', 'tare', 'tare_date', 'tare_user', 'tare_user_name',
|
fields = ['id', 'vehicle_number', 'trailer1_number', 'trailer2_number', 'driver_pid',
|
||||||
|
'tare', 'tare_date', 'tare_user', 'tare_user_name',
|
||||||
'gross', 'gross_date', 'gross_user', 'gross_user_name', 'extra']
|
'gross', 'gross_date', 'gross_user', 'gross_user_name', 'extra']
|
||||||
read_only_fields = ['id', 'tare_date', 'tare_user', 'tare_user_name',
|
read_only_fields = ['id', 'tare_date', 'tare_user', 'tare_user_name',
|
||||||
'gross_date', 'gross_user', 'gross_user_name']
|
'gross_date', 'gross_user', 'gross_user_name']
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ class Vehicle(models.Model):
|
|||||||
gross_date = models.DateTimeField(null=True, blank=True)
|
gross_date = models.DateTimeField(null=True, blank=True)
|
||||||
gross_user = models.ForeignKey(User, null=True, blank=True, related_name='user_vehicle_gross', on_delete=models.SET_NULL)
|
gross_user = models.ForeignKey(User, null=True, blank=True, related_name='user_vehicle_gross', on_delete=models.SET_NULL)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.vehicle_number}"
|
return f"{self.vehicle_number}"
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ from .models import Vehicle
|
|||||||
class VehicleSerializer(serializers.ModelSerializer):
|
class VehicleSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Vehicle
|
model = Vehicle
|
||||||
fields = ['id', 'vehicle_number']
|
fields = ['id', 'vehicle_number', 'trailer1_number', 'trailer2_number', 'driver_pid', 'tare', 'tare_date', 'tare_user', 'gross', 'gross_date', 'gross_user']
|
||||||
@@ -51,56 +51,6 @@
|
|||||||
border-color: #667eea;
|
border-color: #667eea;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* New vehicle form */
|
|
||||||
.vehicle-new-form {
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
background: #f0f0ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vehicle-new-form input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 7px 10px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vehicle-new-form input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vehicle-new-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vehicle-new-actions button {
|
|
||||||
flex: 1;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background: white;
|
|
||||||
color: #333;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vehicle-new-actions button:first-child {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
border-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vehicle-new-actions button:first-child:hover {
|
|
||||||
background: #5568d3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Vehicle list */
|
/* Vehicle list */
|
||||||
.vehicle-list {
|
.vehicle-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -367,6 +317,41 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Vehicle form sections */
|
||||||
|
.vehicle-form-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-form-cancel {
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: white;
|
||||||
|
color: #666;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-form-cancel:hover:not(:disabled) {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-form-cancel:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.main {
|
.main {
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ export default function Main() {
|
|||||||
|
|
||||||
const [selectedVehicleId, setSelectedVehicleId] = useState(null);
|
const [selectedVehicleId, setSelectedVehicleId] = useState(null);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [newVehicleNumber, setNewVehicleNumber] = useState('');
|
|
||||||
const [showNewForm, setShowNewForm] = useState(false);
|
const [showNewForm, setShowNewForm] = useState(false);
|
||||||
|
const [newForm, setNewForm] = useState({ vehicle_number: '', trailer1_number: '', trailer2_number: '', driver_pid: '' });
|
||||||
|
const [newExtraData, setNewExtraData] = useState({});
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
@@ -64,18 +65,48 @@ export default function Main() {
|
|||||||
}
|
}
|
||||||
}, [selectedVehicle]);
|
}, [selectedVehicle]);
|
||||||
|
|
||||||
|
// Open new vehicle form
|
||||||
|
const handleNewVehicle = () => {
|
||||||
|
setSelectedVehicleId(null);
|
||||||
|
setShowNewForm(true);
|
||||||
|
setNewForm({ vehicle_number: '', trailer1_number: '', trailer2_number: '', driver_pid: '' });
|
||||||
|
setNewExtraData({});
|
||||||
|
setError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cancel new vehicle form
|
||||||
|
const handleCancelNew = () => {
|
||||||
|
setShowNewForm(false);
|
||||||
|
setNewForm({ vehicle_number: '', trailer1_number: '', trailer2_number: '', driver_pid: '' });
|
||||||
|
setNewExtraData({});
|
||||||
|
setError('');
|
||||||
|
};
|
||||||
|
|
||||||
// Create new vehicle
|
// Create new vehicle
|
||||||
const handleCreateVehicle = async () => {
|
const handleCreateVehicle = async () => {
|
||||||
if (!newVehicleNumber.trim()) return;
|
if (!newForm.vehicle_number.trim()) return;
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const res = await api.post('/api/vehicles/', { vehicle_number: newVehicleNumber.trim() });
|
const payload = {
|
||||||
|
vehicle_number: newForm.vehicle_number.trim(),
|
||||||
|
trailer1_number: newForm.trailer1_number.trim() || null,
|
||||||
|
trailer2_number: newForm.trailer2_number.trim() || null,
|
||||||
|
driver_pid: newForm.driver_pid.trim() || null,
|
||||||
|
};
|
||||||
|
// Include extra data if any values set
|
||||||
|
const hasExtra = Object.values(newExtraData).some(v => v !== undefined && v !== null && v !== '');
|
||||||
|
if (hasExtra) {
|
||||||
|
payload.extra = { data: newExtraData };
|
||||||
|
}
|
||||||
|
const res = await api.post('/api/vehicles/', payload);
|
||||||
setSelectedVehicleId(res.data.id);
|
setSelectedVehicleId(res.data.id);
|
||||||
setNewVehicleNumber('');
|
|
||||||
setShowNewForm(false);
|
setShowNewForm(false);
|
||||||
|
setNewForm({ vehicle_number: '', trailer1_number: '', trailer2_number: '', driver_pid: '' });
|
||||||
|
setNewExtraData({});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err.response?.data?.vehicle_number?.[0] || err.response?.data?.detail || 'Failed to create vehicle';
|
const data = err.response?.data;
|
||||||
|
const msg = data?.vehicle_number?.[0] || data?.detail || (typeof data === 'string' ? data : JSON.stringify(data)) || 'Failed to create vehicle';
|
||||||
setError(msg);
|
setError(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
@@ -142,7 +173,7 @@ export default function Main() {
|
|||||||
<div className="vehicle-list-header">
|
<div className="vehicle-list-header">
|
||||||
<button
|
<button
|
||||||
className="vehicle-add-btn"
|
className="vehicle-add-btn"
|
||||||
onClick={() => { setShowNewForm(true); setError(''); }}
|
onClick={handleNewVehicle}
|
||||||
>
|
>
|
||||||
+ New Vehicle
|
+ New Vehicle
|
||||||
</button>
|
</button>
|
||||||
@@ -155,34 +186,12 @@ export default function Main() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showNewForm && (
|
|
||||||
<div className="vehicle-new-form">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Vehicle number"
|
|
||||||
value={newVehicleNumber}
|
|
||||||
onChange={e => setNewVehicleNumber(e.target.value)}
|
|
||||||
onKeyDown={e => e.key === 'Enter' && handleCreateVehicle()}
|
|
||||||
disabled={isSaving}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<div className="vehicle-new-actions">
|
|
||||||
<button onClick={handleCreateVehicle} disabled={isSaving}>
|
|
||||||
{isSaving ? '...' : 'Create'}
|
|
||||||
</button>
|
|
||||||
<button onClick={() => { setShowNewForm(false); setNewVehicleNumber(''); setError(''); }}>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="vehicle-list">
|
<div className="vehicle-list">
|
||||||
{filteredVehicles.map(v => (
|
{filteredVehicles.map(v => (
|
||||||
<div
|
<div
|
||||||
key={v.id}
|
key={v.id}
|
||||||
className={`vehicle-list-item ${selectedVehicleId === v.id ? 'vehicle-list-item--active' : ''}`}
|
className={`vehicle-list-item ${selectedVehicleId === v.id ? 'vehicle-list-item--active' : ''}`}
|
||||||
onClick={() => { setSelectedVehicleId(v.id); setError(''); }}
|
onClick={() => { setSelectedVehicleId(v.id); setShowNewForm(false); setError(''); }}
|
||||||
>
|
>
|
||||||
<span className="vehicle-list-number">{v.vehicle_number}</span>
|
<span className="vehicle-list-number">{v.vehicle_number}</span>
|
||||||
{v.tare != null && (
|
{v.tare != null && (
|
||||||
@@ -196,9 +205,95 @@ export default function Main() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right panel - Vehicle detail */}
|
{/* Right panel - Vehicle detail or New form */}
|
||||||
<div className="main-right">
|
<div className="main-right">
|
||||||
{!selectedVehicle ? (
|
{showNewForm ? (
|
||||||
|
<div className="vehicle-detail">
|
||||||
|
{error && <div className="vehicle-error">{error}</div>}
|
||||||
|
|
||||||
|
<h2 className="vehicle-detail-title">New Vehicle</h2>
|
||||||
|
|
||||||
|
<div className="vehicle-form-section">
|
||||||
|
<div className="extra-fields">
|
||||||
|
<div className="extra-field">
|
||||||
|
<label>Vehicle Number *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newForm.vehicle_number}
|
||||||
|
onChange={e => setNewForm(prev => ({ ...prev, vehicle_number: e.target.value }))}
|
||||||
|
disabled={isSaving}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="extra-field">
|
||||||
|
<label>Trailer 1</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newForm.trailer1_number}
|
||||||
|
onChange={e => setNewForm(prev => ({ ...prev, trailer1_number: e.target.value }))}
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="extra-field">
|
||||||
|
<label>Trailer 2</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newForm.trailer2_number}
|
||||||
|
onChange={e => setNewForm(prev => ({ ...prev, trailer2_number: e.target.value }))}
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="extra-field">
|
||||||
|
<label>Driver PID</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newForm.driver_pid}
|
||||||
|
onChange={e => setNewForm(prev => ({ ...prev, driver_pid: e.target.value }))}
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{vehicleNomenclatures.length > 0 && (
|
||||||
|
<div className="extra-section">
|
||||||
|
<h3>Additional Data</h3>
|
||||||
|
<div className="extra-fields">
|
||||||
|
{vehicleNomenclatures.map(nom => (
|
||||||
|
<div key={nom.code} className="extra-field">
|
||||||
|
<NomenclatureDropdown
|
||||||
|
nomenclatureCode={nom.code}
|
||||||
|
value={newExtraData[nom.code]}
|
||||||
|
onChange={val => setNewExtraData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[nom.code]: val,
|
||||||
|
}))}
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="vehicle-form-actions">
|
||||||
|
<button
|
||||||
|
className="vehicle-form-cancel"
|
||||||
|
onClick={handleCancelNew}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="extra-save-btn"
|
||||||
|
onClick={handleCreateVehicle}
|
||||||
|
disabled={isSaving || !newForm.vehicle_number.trim()}
|
||||||
|
>
|
||||||
|
{isSaving ? 'Creating...' : 'Create Vehicle'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : !selectedVehicle ? (
|
||||||
<div className="main-placeholder">
|
<div className="main-placeholder">
|
||||||
Select a vehicle or create a new one
|
Select a vehicle or create a new one
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user