mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 06:11:45 +00:00
Merge remote-tracking branch 'origin/dev' into naver-list-import
This commit is contained in:
@@ -14,6 +14,8 @@ import {
|
||||
toggleMemberPaid,
|
||||
getPerPersonSummary,
|
||||
calculateSettlement,
|
||||
reorderBudgetItems,
|
||||
reorderBudgetCategories,
|
||||
} from '../services/budgetService';
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
@@ -56,6 +58,38 @@ router.post('/', authenticate, (req: Request, res: Response) => {
|
||||
broadcast(tripId, 'budget:created', { item }, req.headers['x-socket-id'] as string);
|
||||
});
|
||||
|
||||
router.put('/reorder/items', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
const { orderedIds } = req.body;
|
||||
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
|
||||
if (!checkPermission('budget_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id))
|
||||
return res.status(403).json({ error: 'No permission' });
|
||||
|
||||
reorderBudgetItems(tripId, orderedIds);
|
||||
res.json({ success: true });
|
||||
broadcast(tripId, 'budget:reordered', { orderedIds }, req.headers['x-socket-id'] as string);
|
||||
});
|
||||
|
||||
router.put('/reorder/categories', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
const { orderedCategories } = req.body;
|
||||
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
|
||||
if (!checkPermission('budget_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id))
|
||||
return res.status(403).json({ error: 'No permission' });
|
||||
|
||||
reorderBudgetCategories(tripId, orderedCategories);
|
||||
res.json({ success: true });
|
||||
broadcast(tripId, 'budget:reordered', { orderedCategories }, req.headers['x-socket-id'] as string);
|
||||
});
|
||||
|
||||
router.put('/:id', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, id } = req.params;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
updateReservation,
|
||||
deleteReservation,
|
||||
} from '../services/reservationService';
|
||||
import { createBudgetItem, updateBudgetItem, deleteBudgetItem } from '../services/budgetService';
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
@@ -53,7 +54,6 @@ router.post('/', authenticate, (req: Request, res: Response) => {
|
||||
// Auto-create budget entry if price was provided
|
||||
if (create_budget_entry && create_budget_entry.total_price > 0) {
|
||||
try {
|
||||
const { createBudgetItem } = require('../services/budgetService');
|
||||
const budgetItem = createBudgetItem(tripId, {
|
||||
name: title,
|
||||
category: create_budget_entry.category || type || 'Other',
|
||||
@@ -126,7 +126,6 @@ router.put('/:id', authenticate, (req: Request, res: Response) => {
|
||||
if (!create_budget_entry || !create_budget_entry.total_price) {
|
||||
const linked = db.prepare('SELECT id FROM budget_items WHERE trip_id = ? AND reservation_id = ?').get(tripId, id) as { id: number } | undefined;
|
||||
if (linked) {
|
||||
const { deleteBudgetItem } = require('../services/budgetService');
|
||||
deleteBudgetItem(linked.id, tripId);
|
||||
broadcast(tripId, 'budget:deleted', { id: linked.id }, req.headers['x-socket-id'] as string);
|
||||
}
|
||||
@@ -135,7 +134,6 @@ router.put('/:id', authenticate, (req: Request, res: Response) => {
|
||||
// Auto-create or update budget entry if price was provided
|
||||
if (create_budget_entry && create_budget_entry.total_price > 0) {
|
||||
try {
|
||||
const { createBudgetItem, updateBudgetItem } = require('../services/budgetService');
|
||||
const itemName = title || current.title;
|
||||
const existing = db.prepare('SELECT id FROM budget_items WHERE trip_id = ? AND reservation_id = ?').get(tripId, id) as { id: number } | undefined;
|
||||
if (existing) {
|
||||
|
||||
+3
-153
@@ -23,6 +23,7 @@ import {
|
||||
addMember,
|
||||
removeMember,
|
||||
exportICS,
|
||||
copyTripById,
|
||||
verifyTripAccess,
|
||||
NotFoundError,
|
||||
ValidationError,
|
||||
@@ -199,160 +200,9 @@ router.post('/:id/copy', authenticate, (req: Request, res: Response) => {
|
||||
if (!canAccessTrip(req.params.id, authReq.user.id))
|
||||
return res.status(404).json({ error: 'Trip not found' });
|
||||
|
||||
const src = db.prepare('SELECT * FROM trips WHERE id = ?').get(req.params.id) as Trip | undefined;
|
||||
if (!src) return res.status(404).json({ error: 'Trip not found' });
|
||||
|
||||
const title = req.body.title || src.title;
|
||||
|
||||
const copyTrip = db.transaction(() => {
|
||||
// 1. Create new trip
|
||||
const tripResult = db.prepare(`
|
||||
INSERT INTO trips (user_id, title, description, start_date, end_date, currency, cover_image, is_archived, reminder_days)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)
|
||||
`).run(authReq.user.id, title, src.description, src.start_date, src.end_date, src.currency, src.cover_image, src.reminder_days ?? 3);
|
||||
const newTripId = tripResult.lastInsertRowid;
|
||||
|
||||
// 2. Copy days → build ID map
|
||||
const oldDays = db.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(req.params.id) as any[];
|
||||
const dayMap = new Map<number, number | bigint>();
|
||||
const insertDay = db.prepare('INSERT INTO days (trip_id, day_number, date, notes, title) VALUES (?, ?, ?, ?, ?)');
|
||||
for (const d of oldDays) {
|
||||
const r = insertDay.run(newTripId, d.day_number, d.date, d.notes, d.title);
|
||||
dayMap.set(d.id, r.lastInsertRowid);
|
||||
}
|
||||
|
||||
// 3. Copy places → build ID map
|
||||
const oldPlaces = db.prepare('SELECT * FROM places WHERE trip_id = ?').all(req.params.id) as any[];
|
||||
const placeMap = new Map<number, number | bigint>();
|
||||
const insertPlace = db.prepare(`
|
||||
INSERT INTO places (trip_id, name, description, lat, lng, address, category_id, price, currency,
|
||||
reservation_status, reservation_notes, reservation_datetime, place_time, end_time,
|
||||
duration_minutes, notes, image_url, google_place_id, website, phone, transport_mode, osm_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
for (const p of oldPlaces) {
|
||||
const r = insertPlace.run(newTripId, p.name, p.description, p.lat, p.lng, p.address, p.category_id,
|
||||
p.price, p.currency, p.reservation_status, p.reservation_notes, p.reservation_datetime,
|
||||
p.place_time, p.end_time, p.duration_minutes, p.notes, p.image_url, p.google_place_id,
|
||||
p.website, p.phone, p.transport_mode, p.osm_id);
|
||||
placeMap.set(p.id, r.lastInsertRowid);
|
||||
}
|
||||
|
||||
// 4. Copy place_tags
|
||||
const oldTags = db.prepare(`
|
||||
SELECT pt.* FROM place_tags pt JOIN places p ON p.id = pt.place_id WHERE p.trip_id = ?
|
||||
`).all(req.params.id) as any[];
|
||||
const insertTag = db.prepare('INSERT OR IGNORE INTO place_tags (place_id, tag_id) VALUES (?, ?)');
|
||||
for (const t of oldTags) {
|
||||
const newPlaceId = placeMap.get(t.place_id);
|
||||
if (newPlaceId) insertTag.run(newPlaceId, t.tag_id);
|
||||
}
|
||||
|
||||
// 5. Copy day_assignments → build ID map
|
||||
const oldAssignments = db.prepare(`
|
||||
SELECT da.* FROM day_assignments da JOIN days d ON d.id = da.day_id WHERE d.trip_id = ?
|
||||
`).all(req.params.id) as any[];
|
||||
const assignmentMap = new Map<number, number | bigint>();
|
||||
const insertAssignment = db.prepare(`
|
||||
INSERT INTO day_assignments (day_id, place_id, order_index, notes, reservation_status, reservation_notes, reservation_datetime, assignment_time, assignment_end_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
for (const a of oldAssignments) {
|
||||
const newDayId = dayMap.get(a.day_id);
|
||||
const newPlaceId = placeMap.get(a.place_id);
|
||||
if (newDayId && newPlaceId) {
|
||||
const r = insertAssignment.run(newDayId, newPlaceId, a.order_index, a.notes,
|
||||
a.reservation_status, a.reservation_notes, a.reservation_datetime,
|
||||
a.assignment_time, a.assignment_end_time);
|
||||
assignmentMap.set(a.id, r.lastInsertRowid);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Copy day_accommodations → build ID map (before reservations, which reference them)
|
||||
const oldAccom = db.prepare('SELECT * FROM day_accommodations WHERE trip_id = ?').all(req.params.id) as any[];
|
||||
const accomMap = new Map<number, number | bigint>();
|
||||
const insertAccom = db.prepare(`
|
||||
INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_out, confirmation, notes)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
for (const a of oldAccom) {
|
||||
const newPlaceId = placeMap.get(a.place_id);
|
||||
const newStartDay = dayMap.get(a.start_day_id);
|
||||
const newEndDay = dayMap.get(a.end_day_id);
|
||||
if (newPlaceId && newStartDay && newEndDay) {
|
||||
const r = insertAccom.run(newTripId, newPlaceId, newStartDay, newEndDay, a.check_in, a.check_out, a.confirmation, a.notes);
|
||||
accomMap.set(a.id, r.lastInsertRowid);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Copy reservations
|
||||
const oldReservations = db.prepare('SELECT * FROM reservations WHERE trip_id = ?').all(req.params.id) as any[];
|
||||
const insertReservation = db.prepare(`
|
||||
INSERT INTO reservations (trip_id, day_id, place_id, assignment_id, accommodation_id, title, reservation_time, reservation_end_time,
|
||||
location, confirmation_number, notes, status, type, metadata, day_plan_position)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
for (const r of oldReservations) {
|
||||
insertReservation.run(newTripId,
|
||||
r.day_id ? (dayMap.get(r.day_id) ?? null) : null,
|
||||
r.place_id ? (placeMap.get(r.place_id) ?? null) : null,
|
||||
r.assignment_id ? (assignmentMap.get(r.assignment_id) ?? null) : null,
|
||||
r.accommodation_id ? (accomMap.get(r.accommodation_id) ?? null) : null,
|
||||
r.title, r.reservation_time, r.reservation_end_time,
|
||||
r.location, r.confirmation_number, r.notes, r.status, r.type,
|
||||
r.metadata, r.day_plan_position);
|
||||
}
|
||||
|
||||
// 8. Copy budget_items (paid_by_user_id reset to null)
|
||||
const oldBudget = db.prepare('SELECT * FROM budget_items WHERE trip_id = ?').all(req.params.id) as any[];
|
||||
const insertBudget = db.prepare(`
|
||||
INSERT INTO budget_items (trip_id, category, name, total_price, persons, days, note, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
for (const b of oldBudget) {
|
||||
insertBudget.run(newTripId, b.category, b.name, b.total_price, b.persons, b.days, b.note, b.sort_order);
|
||||
}
|
||||
|
||||
// 9. Copy packing_bags → build ID map
|
||||
const oldBags = db.prepare('SELECT * FROM packing_bags WHERE trip_id = ?').all(req.params.id) as any[];
|
||||
const bagMap = new Map<number, number | bigint>();
|
||||
const insertBag = db.prepare(`
|
||||
INSERT INTO packing_bags (trip_id, name, color, weight_limit_grams, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
for (const bag of oldBags) {
|
||||
const r = insertBag.run(newTripId, bag.name, bag.color, bag.weight_limit_grams, bag.sort_order);
|
||||
bagMap.set(bag.id, r.lastInsertRowid);
|
||||
}
|
||||
|
||||
// 10. Copy packing_items (checked reset to 0)
|
||||
const oldPacking = db.prepare('SELECT * FROM packing_items WHERE trip_id = ?').all(req.params.id) as any[];
|
||||
const insertPacking = db.prepare(`
|
||||
INSERT INTO packing_items (trip_id, name, checked, category, sort_order, weight_grams, bag_id)
|
||||
VALUES (?, ?, 0, ?, ?, ?, ?)
|
||||
`);
|
||||
for (const p of oldPacking) {
|
||||
insertPacking.run(newTripId, p.name, p.category, p.sort_order, p.weight_grams,
|
||||
p.bag_id ? (bagMap.get(p.bag_id) ?? null) : null);
|
||||
}
|
||||
|
||||
// 11. Copy day_notes
|
||||
const oldNotes = db.prepare('SELECT * FROM day_notes WHERE trip_id = ?').all(req.params.id) as any[];
|
||||
const insertNote = db.prepare(`
|
||||
INSERT INTO day_notes (day_id, trip_id, text, time, icon, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
for (const n of oldNotes) {
|
||||
const newDayId = dayMap.get(n.day_id);
|
||||
if (newDayId) insertNote.run(newDayId, newTripId, n.text, n.time, n.icon, n.sort_order);
|
||||
}
|
||||
|
||||
return newTripId;
|
||||
});
|
||||
|
||||
try {
|
||||
const newTripId = copyTrip();
|
||||
writeAudit({ userId: authReq.user.id, action: 'trip.copy', ip: getClientIp(req), details: { sourceTripId: Number(req.params.id), newTripId: Number(newTripId), title } });
|
||||
const newTripId = copyTripById(req.params.id, authReq.user.id, req.body.title);
|
||||
writeAudit({ userId: authReq.user.id, action: 'trip.copy', ip: getClientIp(req), details: { sourceTripId: Number(req.params.id), newTripId, title: req.body.title } });
|
||||
const trip = db.prepare(`${TRIP_SELECT} WHERE t.id = :tripId`).get({ userId: authReq.user.id, tripId: newTripId });
|
||||
res.status(201).json({ trip });
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user