mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
fee41ab394
Collab, packing bags, packing templates and imports, the category a place form creates and the place imports are not kept on the device, so offline their writes cannot wait in the queue. They failed with a bare "Error" or the generic save message. While TREK is offline the failure now reads "This needs a connection. Try again once TREK is back online.", on desktop and on the phone's Collab notes; online nothing changes. Refs #2601
125 lines
4.4 KiB
TypeScript
125 lines
4.4 KiB
TypeScript
import type { PackingUpdateBagRequest } from '@trek/shared';
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
|
|
import { packingApi } from '../../api/client';
|
|
import { useNetworkMode } from '../../hooks/useNetworkMode';
|
|
import type { PackingBag } from '../../types';
|
|
import { offlineOr } from '../../utils/needsConnection';
|
|
import { BAG_COLORS } from './packingListPanel.constants';
|
|
import { useBagTotalsPing } from './useBagTotalsPing';
|
|
|
|
type Translate = (key: string, params?: Record<string, string | number>) => string;
|
|
interface Toaster {
|
|
error: (message: string) => void;
|
|
}
|
|
|
|
/**
|
|
* The trip's bags behind both packing lists: the bags with their server-summed
|
|
* weights, kept fresh by the room's totals ping, and the bag writes. The desktop
|
|
* panel and the phone tab draw their own bag sidebar, modal or sheet over it.
|
|
*/
|
|
export function usePackingBags({
|
|
tripId,
|
|
bagTrackingEnabled,
|
|
t,
|
|
toast,
|
|
}: {
|
|
tripId: number;
|
|
bagTrackingEnabled: boolean;
|
|
t: Translate;
|
|
toast: Toaster;
|
|
}) {
|
|
const [bags, setBags] = useState<PackingBag[]>([]);
|
|
/** Server-summed weight of everything in no bag (#2191); null until the first load. */
|
|
const [unassignedWeightGrams, setUnassignedWeightGrams] = useState<number | null>(null);
|
|
|
|
const reloadBags = useCallback(async () => {
|
|
if (!bagTrackingEnabled) return;
|
|
try {
|
|
const r = await packingApi.listBags(tripId);
|
|
setBags(r.bags || []);
|
|
setUnassignedWeightGrams(r.unassigned_weight_grams ?? null);
|
|
} catch {
|
|
// Offline or a failed read: the surfaces fall back to the local sum
|
|
// (see `serverWeightsFresh` below), so there is nothing to roll back.
|
|
}
|
|
}, [tripId, bagTrackingEnabled]);
|
|
|
|
useEffect(() => {
|
|
void reloadBags();
|
|
}, [reloadBags]);
|
|
|
|
// Bag weights are summed server-side across every member (#2191), so an item
|
|
// this viewer may not even see still moves them. The item events cannot carry
|
|
// that (a private item is delivered only to its owner, which is the very rule
|
|
// that made the totals wrong), so the server pings the room content-free and
|
|
// we re-read the numbers.
|
|
useBagTotalsPing(bagTrackingEnabled, reloadBags);
|
|
|
|
// Bags are not part of the offline cache (no repo, no Dexie table), so while
|
|
// offline the server totals are frozen at the last online read and cannot see
|
|
// the optimistic item writes the mutation queue is holding. A stale absolute
|
|
// number measured against an airline limit is worse than an honest partial
|
|
// one, so offline the surfaces sum what they can see instead (#2191).
|
|
const { offline } = useNetworkMode();
|
|
const serverWeightsFresh = !offline;
|
|
|
|
/**
|
|
* Creates a bag in the next colour. Resolves to the server's answer once the
|
|
* write went through, even when that answer carries no bag, or to null once
|
|
* the failure is reported.
|
|
*/
|
|
const tryCreateBag = async (name: string): Promise<{ bag: PackingBag } | null> => {
|
|
try {
|
|
const data = await packingApi.createBag(tripId, { name, color: BAG_COLORS[bags.length % BAG_COLORS.length] });
|
|
setBags((prev) => [...prev, data.bag]);
|
|
return { bag: data.bag };
|
|
} catch {
|
|
toast.error(offlineOr(t, t('packing.toast.saveError')));
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/** Creates a bag in the next colour; the new bag, or undefined once the failure is reported. */
|
|
const createBag = async (name: string): Promise<PackingBag | undefined> => (await tryCreateBag(name))?.bag;
|
|
|
|
const updateBag = async (bagId: number, data: PackingUpdateBagRequest) => {
|
|
try {
|
|
const result = await packingApi.updateBag(tripId, bagId, data);
|
|
setBags((prev) => prev.map((b) => (b.id === bagId ? { ...b, ...result.bag } : b)));
|
|
} catch {
|
|
toast.error(offlineOr(t, t('common.error')));
|
|
}
|
|
};
|
|
|
|
const deleteBag = async (bagId: number) => {
|
|
try {
|
|
await packingApi.deleteBag(tripId, bagId);
|
|
setBags((prev) => prev.filter((b) => b.id !== bagId));
|
|
} catch {
|
|
toast.error(offlineOr(t, t('packing.toast.deleteError')));
|
|
}
|
|
};
|
|
|
|
const setBagMembers = async (bagId: number, userIds: number[]) => {
|
|
try {
|
|
const result = await packingApi.setBagMembers(tripId, bagId, userIds);
|
|
setBags((prev) => prev.map((b) => (b.id === bagId ? { ...b, members: result.members } : b)));
|
|
} catch {
|
|
toast.error(offlineOr(t, t('common.error')));
|
|
}
|
|
};
|
|
|
|
return {
|
|
bags,
|
|
unassignedWeightGrams,
|
|
serverWeightsFresh,
|
|
reloadBags,
|
|
tryCreateBag,
|
|
createBag,
|
|
updateBag,
|
|
deleteBag,
|
|
setBagMembers,
|
|
};
|
|
}
|