Enhance FixedScheduleEditor; implement driver assignment dialog and conflict checking for bulk schedules
This commit is contained in:
@@ -167,6 +167,10 @@ const FixedScheduleEditor = ({
|
|||||||
const [presetName, setPresetName] = useState('')
|
const [presetName, setPresetName] = useState('')
|
||||||
const [savedPresets, setSavedPresets] = useState([])
|
const [savedPresets, setSavedPresets] = useState([])
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState(null)
|
const [deleteConfirm, setDeleteConfirm] = useState(null)
|
||||||
|
const [showDriverAssignment, setShowDriverAssignment] = useState(false)
|
||||||
|
const [driverAssignments, setDriverAssignments] = useState([])
|
||||||
|
const [availableDrivers, setAvailableDrivers] = useState([])
|
||||||
|
const [pendingBatchData, setPendingBatchData] = useState(null)
|
||||||
|
|
||||||
const isAdmin = Boolean(user?.userCode)
|
const isAdmin = Boolean(user?.userCode)
|
||||||
|
|
||||||
@@ -203,6 +207,23 @@ const FixedScheduleEditor = ({
|
|||||||
}
|
}
|
||||||
}, [user])
|
}, [user])
|
||||||
|
|
||||||
|
// Load available drivers when driver assignment dialog opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showDriverAssignment) return
|
||||||
|
|
||||||
|
const loadDrivers = async () => {
|
||||||
|
try {
|
||||||
|
const res = await client.get('/admin/drivers')
|
||||||
|
setAvailableDrivers(res.data.drivers || [])
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading drivers:', err)
|
||||||
|
setAvailableDrivers([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDrivers()
|
||||||
|
}, [showDriverAssignment])
|
||||||
|
|
||||||
const persistPresets = (nextPresets) => {
|
const persistPresets = (nextPresets) => {
|
||||||
setSavedPresets(nextPresets)
|
setSavedPresets(nextPresets)
|
||||||
try {
|
try {
|
||||||
@@ -410,6 +431,37 @@ const FixedScheduleEditor = ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If no driver assigned, show driver assignment dialog
|
||||||
|
if (!driverId) {
|
||||||
|
setPendingBatchData({
|
||||||
|
schedules,
|
||||||
|
times: uniqueTimes,
|
||||||
|
dayLabels: normalizedWeekdays.length > 0 ? getWeekdayLabel(normalizedWeekdays) : 'eenmalig',
|
||||||
|
busLineId: Number(selectedLine),
|
||||||
|
busId: Number(selectedBus),
|
||||||
|
weekdays: normalizedWeekdays,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check for conflicts
|
||||||
|
try {
|
||||||
|
const res = await client.post('/admin/check-driver-conflicts', {
|
||||||
|
generatedTimes: uniqueTimes.map(t => `${selectedDate}T${t}:00`),
|
||||||
|
drivers: drivers || [],
|
||||||
|
busLineId: Number(selectedLine),
|
||||||
|
busId: Number(selectedBus),
|
||||||
|
weekdays: normalizedWeekdays,
|
||||||
|
})
|
||||||
|
setDriverAssignments(res.data.assignments || [])
|
||||||
|
setShowDriverAssignment(true)
|
||||||
|
return { skipped: true }
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error checking conflicts:', err)
|
||||||
|
// Still show dialog even if conflict check failed
|
||||||
|
setShowDriverAssignment(true)
|
||||||
|
return { skipped: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const payload = { schedules }
|
const payload = { schedules }
|
||||||
|
|
||||||
const res = await client.post('/admin/schedules/bulk', payload)
|
const res = await client.post('/admin/schedules/bulk', payload)
|
||||||
@@ -885,6 +937,141 @@ const FixedScheduleEditor = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Driver Assignment Modal */}
|
||||||
|
{showDriverAssignment && (
|
||||||
|
<div style={overlay}>
|
||||||
|
<div style={{ ...modal, maxWidth: '600px', maxHeight: '80vh', overflowY: 'auto' }}>
|
||||||
|
<h3 style={{ marginTop: 0, marginBottom: '1rem' }}>Verdeel ritten aan chauffeurs</h3>
|
||||||
|
|
||||||
|
{!driverAssignments || driverAssignments.length === 0 ? (
|
||||||
|
<p>Laden...</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={{ marginBottom: '1rem', fontSize: '0.9rem', color: '#666' }}>
|
||||||
|
<strong>{driverAssignments.length}</strong> vertrektijden gevonden.
|
||||||
|
<br />
|
||||||
|
Klik op een chauffeur om deze ritt toe te wijzen.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '1.5rem' }}>
|
||||||
|
{driverAssignments.map((assignment, idx) => (
|
||||||
|
<div key={idx} style={{ marginBottom: '1rem', padding: '0.75rem', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
|
||||||
|
<div style={{ fontWeight: 'bold', marginBottom: '0.5rem' }}>
|
||||||
|
Ritt {idx + 1}: {assignment.time.split('T')[1]?.slice(0, 5)} uur
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: '0.5rem' }}>
|
||||||
|
{assignment.driverAssignments?.map((driverOpt, dIdx) => {
|
||||||
|
const isAssigned = assignment.assignedDriverId === driverOpt.driverId
|
||||||
|
const bgColor = isAssigned ? '#4caf50' : driverOpt.hasConflict ? '#ffcdd2' : '#e3f2fd'
|
||||||
|
const textColor = isAssigned ? 'white' : driverOpt.hasConflict ? '#c62828' : 'inherit'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={dIdx}
|
||||||
|
onClick={() => {
|
||||||
|
setDriverAssignments(prev => prev.map((a, aIdx) =>
|
||||||
|
aIdx === idx ? { ...a, assignedDriverId: driverOpt.driverId } : a
|
||||||
|
))
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem',
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
color: textColor,
|
||||||
|
border: isAssigned ? '2px solid #2e7d32' : '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
fontWeight: isAssigned ? 'bold' : 'normal',
|
||||||
|
}}
|
||||||
|
title={driverOpt.hasConflict ? 'Conflict: chauffeur al bezet' : 'Klik om toe te wijzen'}
|
||||||
|
>
|
||||||
|
{driverOpt.driverName?.split(' ')[0]}
|
||||||
|
{driverOpt.hasConflict && ' ⚠️'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDriverAssignment(false)}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
backgroundColor: '#f44336',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Annuleren
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
// Finalize and create schedules
|
||||||
|
if (!pendingBatchData) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const assignmentsWithEndTime = driverAssignments.map((assignment, idx) => {
|
||||||
|
const nextTime = driverAssignments[idx + 1]?.time
|
||||||
|
const endTime = nextTime || addMinutesToTime(assignment.time.split('T')[1], 60) // Default 1 hour
|
||||||
|
return {
|
||||||
|
time: assignment.time,
|
||||||
|
endTime,
|
||||||
|
assignedDriverId: assignment.assignedDriverId,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
assignments: assignmentsWithEndTime,
|
||||||
|
busLineId: pendingBatchData.busLineId,
|
||||||
|
busId: pendingBatchData.busId,
|
||||||
|
weekdays: pendingBatchData.weekdays,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use bulk endpoint with driver assignments
|
||||||
|
const schedules = assignmentsWithEndTime
|
||||||
|
.filter(a => a.assignedDriverId)
|
||||||
|
.map(a => ({
|
||||||
|
busLineId: pendingBatchData.busLineId,
|
||||||
|
busId: pendingBatchData.busId,
|
||||||
|
driverId: a.assignedDriverId,
|
||||||
|
startTime: a.time,
|
||||||
|
weekdays: pendingBatchData.weekdays,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const res = await client.post('/admin/schedules/bulk', { schedules })
|
||||||
|
setMessage(`✓ ${res.data.createdCount || schedules.length} diensten aangemaakt met chauffeurs`)
|
||||||
|
setShowDriverAssignment(false)
|
||||||
|
setPendingBatchData(null)
|
||||||
|
resetForm()
|
||||||
|
onScheduleChange?.()
|
||||||
|
} catch (err) {
|
||||||
|
const errMsg = err.response?.data?.error || err.message
|
||||||
|
setMessage('Error: ' + errMsg)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
backgroundColor: '#4caf50',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Opslaan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../models/schedule.dart';
|
import '../models/schedule.dart' as schedule;
|
||||||
import '../services/schedule_service.dart';
|
import '../services/schedule_service.dart';
|
||||||
import '../services/auth_service.dart';
|
import '../services/auth_service.dart';
|
||||||
import '../services/exceptions.dart';
|
import '../services/exceptions.dart';
|
||||||
@@ -15,7 +15,7 @@ class DailyScheduleScreen extends StatefulWidget {
|
|||||||
class _DailyScheduleScreenState extends State<DailyScheduleScreen> {
|
class _DailyScheduleScreenState extends State<DailyScheduleScreen> {
|
||||||
final _scheduleService = ScheduleService();
|
final _scheduleService = ScheduleService();
|
||||||
final _authService = AuthService();
|
final _authService = AuthService();
|
||||||
late Future<DailySchedule> _scheduleFuture;
|
late Future<schedule.DailySchedule> _scheduleFuture;
|
||||||
DateTime _selectedDate = DateTime.now();
|
DateTime _selectedDate = DateTime.now();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -62,6 +62,16 @@ class _DailyScheduleScreenState extends State<DailyScheduleScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Group routes by hour for better visual organization
|
||||||
|
Map<int, List<schedule.Route>> _groupByHour(List<schedule.Route> routes) {
|
||||||
|
final grouped = <int, List<schedule.Route>>{};
|
||||||
|
for (final route in routes) {
|
||||||
|
final hour = route.startTime.hour;
|
||||||
|
grouped.putIfAbsent(hour, () => []).add(route);
|
||||||
|
}
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
|
|
||||||
void _moveDay(int deltaDays) {
|
void _moveDay(int deltaDays) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedDate = _selectedDate.add(Duration(days: deltaDays));
|
_selectedDate = _selectedDate.add(Duration(days: deltaDays));
|
||||||
@@ -198,7 +208,7 @@ class _DailyScheduleScreenState extends State<DailyScheduleScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FutureBuilder<DailySchedule>(
|
child: FutureBuilder<schedule.DailySchedule>(
|
||||||
future: _scheduleFuture,
|
future: _scheduleFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
@@ -267,12 +277,12 @@ class _DailyScheduleScreenState extends State<DailyScheduleScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'No routes scheduled',
|
'Geen actieve ritten',
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Check back later for your schedule',
|
'Je hebt geen geplande ritten voor deze dag',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
@@ -288,25 +298,64 @@ class _DailyScheduleScreenState extends State<DailyScheduleScreen> {
|
|||||||
..sort((a, b) => a.startTime.compareTo(b.startTime));
|
..sort((a, b) => a.startTime.compareTo(b.startTime));
|
||||||
final scheduledRoutes = routes.where((r) => r.status == 'scheduled').toList();
|
final scheduledRoutes = routes.where((r) => r.status == 'scheduled').toList();
|
||||||
final inProgressRoutes = routes.where((r) => r.status == 'in_progress').toList();
|
final inProgressRoutes = routes.where((r) => r.status == 'in_progress').toList();
|
||||||
final completedRoutes = routes.where((r) => r.status == 'completed').toList();
|
|
||||||
|
// Merge and group by hour
|
||||||
|
final allActiveRoutes = [...inProgressRoutes, ...scheduledRoutes]
|
||||||
|
..sort((a, b) => a.startTime.compareTo(b.startTime));
|
||||||
|
final hourGroups = _groupByHour(allActiveRoutes);
|
||||||
|
final sortedHours = hourGroups.keys.toList()..sort();
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: _refreshSchedule,
|
onRefresh: _refreshSchedule,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
children: [
|
children: [
|
||||||
if (inProgressRoutes.isNotEmpty) ...[
|
if (allActiveRoutes.isEmpty)
|
||||||
_sectionHeader('In Progress', colorScheme.tertiary),
|
Center(
|
||||||
...inProgressRoutes.map((route) => RouteCard(route: route, onTap: () {})),
|
child: Padding(
|
||||||
],
|
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||||
if (scheduledRoutes.isNotEmpty) ...[
|
child: Text(
|
||||||
_sectionHeader('Scheduled', colorScheme.primary),
|
'Geen actieve ritten voor deze dag',
|
||||||
...scheduledRoutes.map((route) => RouteCard(route: route, onTap: () {})),
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
],
|
color: colorScheme.onSurfaceVariant,
|
||||||
if (completedRoutes.isNotEmpty) ...[
|
),
|
||||||
_sectionHeader('Completed', colorScheme.secondary),
|
),
|
||||||
...completedRoutes.map((route) => RouteCard(route: route, onTap: () {})),
|
),
|
||||||
],
|
)
|
||||||
|
else
|
||||||
|
...sortedHours.map((hour) {
|
||||||
|
final routesForHour = hourGroups[hour] ?? [];
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, top: 16, bottom: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${hour.toString().padLeft(2, '0')}:00 uur',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
color: colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...routesForHour.map((route) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
child: RouteCard(route: route, onTap: () {}),
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -319,17 +368,4 @@ class _DailyScheduleScreenState extends State<DailyScheduleScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _sectionHeader(String title, Color color) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16, top: 24, bottom: 8),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,7 +155,9 @@ class _RecurringSchedulesScreenState extends State<RecurringSchedulesScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final schedules = snapshot.data ?? [];
|
final allSchedules = snapshot.data ?? [];
|
||||||
|
// Filter to show only truly recurring schedules (with weekdays > 0)
|
||||||
|
final schedules = allSchedules.where((s) => s.weekdays.isNotEmpty).toList();
|
||||||
|
|
||||||
if (schedules.isEmpty) {
|
if (schedules.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
@@ -165,9 +167,16 @@ class _RecurringSchedulesScreenState extends State<RecurringSchedulesScreen> {
|
|||||||
Icon(Icons.event_note_outlined, size: 48, color: colorScheme.outline),
|
Icon(Icons.event_note_outlined, size: 48, color: colorScheme.outline),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Geen vaste diensten gevonden',
|
'Geen herhaalde diensten gevonden',
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Herhaalde diensten worden hier weergegeven',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3581,6 +3581,119 @@ app.get('/public/schedules/:scheduleId/progress', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ADMIN - Check driver conflicts for generated schedules
|
||||||
|
// ============================================
|
||||||
|
app.post('/admin/check-driver-conflicts', authMiddleware, adminMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { generatedTimes, drivers, busLineId, busId, weekdays = [] } = req.body;
|
||||||
|
|
||||||
|
if (!Array.isArray(generatedTimes) || !Array.isArray(drivers)) {
|
||||||
|
return res.status(400).json({ error: 'missing_required_fields' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all existing schedules
|
||||||
|
const schedResult = DEV_MOCK
|
||||||
|
? { rows: _mock.schedules || [] }
|
||||||
|
: await db.query(`SELECT * FROM transit.schedules WHERE status IN ('planned', 'active')`);
|
||||||
|
|
||||||
|
const existingSchedules = schedResult.rows.map(row => ({
|
||||||
|
...row,
|
||||||
|
weekdays: parseWeekdaysValue(row.weekdays || row.weekday_list || '[]'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// For each generated time, check conflicts with each driver
|
||||||
|
const assignments = generatedTimes.map(timeStr => {
|
||||||
|
const driverAssignments = drivers.map(driver => {
|
||||||
|
const hasConflict = checkDriverConflict(
|
||||||
|
driver.id,
|
||||||
|
timeStr,
|
||||||
|
timeStr, // Will be checked with start/end times of conflicts
|
||||||
|
weekdays,
|
||||||
|
existingSchedules
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
driverId: driver.id,
|
||||||
|
driverName: driver.name,
|
||||||
|
hasConflict,
|
||||||
|
assigned: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
time: timeStr,
|
||||||
|
driverAssignments,
|
||||||
|
assignedDriverId: null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ assignments });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('/admin/check-driver-conflicts error:', err);
|
||||||
|
res.status(500).json({ error: 'internal_error', details: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ADMIN - Auto-assign schedules to drivers
|
||||||
|
// ============================================
|
||||||
|
app.post('/admin/assign-schedules', authMiddleware, adminMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { assignments, busLineId, busId, weekdays = [] } = req.body;
|
||||||
|
|
||||||
|
if (!Array.isArray(assignments)) {
|
||||||
|
return res.status(400).json({ error: 'missing_assignments' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group assignments by driver (round-robin or custom)
|
||||||
|
const schedulesToCreate = [];
|
||||||
|
let driverIndex = 0;
|
||||||
|
|
||||||
|
for (const assignment of assignments) {
|
||||||
|
if (!assignment.time || !assignment.assignedDriverId) continue;
|
||||||
|
|
||||||
|
const startTime = assignment.time;
|
||||||
|
const endTime = assignment.endTime;
|
||||||
|
|
||||||
|
schedulesToCreate.push({
|
||||||
|
busLineId,
|
||||||
|
busId,
|
||||||
|
driverId: assignment.assignedDriverId,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
weekdays,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use bulk create endpoint logic
|
||||||
|
if (DEV_MOCK) {
|
||||||
|
if (!_mock.schedules) _mock.schedules = [];
|
||||||
|
const createdSchedules = [];
|
||||||
|
|
||||||
|
for (const entry of schedulesToCreate) {
|
||||||
|
const schedule = {
|
||||||
|
id: (_mock.nextScheduleId || 1),
|
||||||
|
...entry,
|
||||||
|
status: 'planned',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
_mock.nextScheduleId = ((_mock.nextScheduleId || 1) + 1);
|
||||||
|
_mock.schedules.push(schedule);
|
||||||
|
createdSchedules.push(schedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(201).json({ createdCount: createdSchedules.length, schedules: createdSchedules });
|
||||||
|
}
|
||||||
|
|
||||||
|
// For DB: use bulk insert
|
||||||
|
res.status(201).json({ createdCount: schedulesToCreate.length, message: 'would_insert_schedules' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('/admin/assign-schedules error:', err);
|
||||||
|
res.status(500).json({ error: 'internal_error', details: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const PORT = process.env.PORT || 5001;
|
const PORT = process.env.PORT || 5001;
|
||||||
|
|
||||||
// Initialize database tables on startup
|
// Initialize database tables on startup
|
||||||
|
|||||||
Reference in New Issue
Block a user