fix(costs): expenses and payments saved offline wait in the queue and sync once the connection is back

Every Costs write went straight to the API, so offline an expense failed with
"Unknown error" and was lost. Expenses (add, edit, delete, split, paid tick,
reorder) now go through budgetRepo and the mutation queue like places and
packing items: a temporary row offline, held behind an older queued write
online. The replay reads the expense from `item` only, so the split answer
({ members, item }) keeps the local payers and the paid answer ({ member })
never lands in the expense table. An expense linked to a place or booking
created offline is sent with the real id once that one has synced.

Payments are queued without a local row and a hint says they show once
sent; the ledger reads the balances again when the queue is through, and
offline it shows the last balances it read (new Dexie table, v10). Receipts
need an upload, so an offline save with new ones is stopped with a hint.

Refs #2601
This commit is contained in:
Maurice
2026-10-12 18:14:30 +02:00
parent a5bf5e6853
commit cfdbfb49c0
43 changed files with 1006 additions and 35 deletions
+3
View File
@@ -245,5 +245,8 @@ const budget: TranslationStrings = {
'costs.receiptLeftBehind.two': 'فشل الحفظ، وما زال هناك إيصالان مرفوعان. احذفهما من تبويب الملفات.',
'costs.receiptLeftBehind.few': 'فشل الحفظ، وما زال هناك {count} إيصالات مرفوعة. احذفها من تبويب الملفات.',
'costs.receiptLeftBehind.many': 'فشل الحفظ، وما زال هناك {count} إيصالًا مرفوعًا. احذفها من تبويب الملفات.',
'costs.receiptsNeedConnection':
'يحتاج رفع الإيصالات إلى اتصال. أزل الإيصالات الجديدة لحفظ المصروف دون اتصال، أو احفظ عندما تعود متصلاً.',
'costs.paymentQueued': 'أنت غير متصل. تم حفظ الدفعة وستظهر عندما يعود TREK للاتصال.',
};
export default budget;
+3
View File
@@ -229,6 +229,9 @@ const budget: TranslationStrings = {
'Yadda saxlamaq mümkün olmadı və yüklənmiş {count} qəbz hələ də qalır. Onları Fayllar bölməsindən silin.',
'costs.receiptLeftBehind.one':
'Yadda saxlamaq mümkün olmadı və yüklənmiş {count} qəbz hələ də qalır. Onu Fayllar bölməsindən silin.',
'costs.receiptsNeedConnection':
'Qəbzlərin yüklənməsi üçün bağlantı lazımdır. Xərci oflayn saxlamaq üçün yeni qəbzləri silin və ya yenidən onlayn olduqda saxlayın.',
'costs.paymentQueued': 'Siz oflaynsınız. Ödəniş saxlanıldı və TREK yenidən onlayn olduqda görünəcək.',
};
export default budget;
+3
View File
@@ -207,5 +207,8 @@ const budget: TranslationStrings = {
'Falha ao salvar, e {count} comprovantes enviados ainda estão lá. Remova-os na aba Arquivos.',
'costs.receiptLeftBehind.one':
'Falha ao salvar, e {count} comprovante enviado ainda está lá. Remova-o na aba Arquivos.',
'costs.receiptsNeedConnection':
'Os recibos precisam de conexão para serem enviados. Remova os novos para salvar a despesa offline, ou salve quando estiver online de novo.',
'costs.paymentQueued': 'Você está offline. O pagamento foi salvo e aparece quando o TREK voltar a ficar online.',
};
export default budget;
+3
View File
@@ -207,6 +207,9 @@ const budget: TranslationStrings = {
"No s'ha pogut desar i encara hi ha {count} rebuts pujats. Elimina'ls a la pestanya Fitxers.",
'costs.receiptLeftBehind.one':
"No s'ha pogut desar i encara hi ha {count} rebut pujat. Elimina'l a la pestanya Fitxers.",
'costs.receiptsNeedConnection':
'Els rebuts necessiten connexió per pujar-se. Treu els nous per desar la despesa sense connexió, o desa quan tornis a estar en línia.',
'costs.paymentQueued': "No tens connexió. El pagament s'ha desat i apareixerà quan TREK torni a estar en línia.",
};
export default budget;
+3
View File
@@ -217,5 +217,8 @@ const budget: TranslationStrings = {
'Uložení selhalo, {count} nahraná účtenka tam stále je. Odstraňte ji v záložce Soubory.',
'costs.receiptLeftBehind.few':
'Uložení selhalo, {count} nahrané účtenky tam stále jsou. Odstraňte je v záložce Soubory.',
'costs.receiptsNeedConnection':
'Účtenky potřebují k nahrání připojení. Odeberte nové, abyste výdaj uložili offline, nebo uložte, až budete znovu online.',
'costs.paymentQueued': 'Jste offline. Platba je uložená a zobrazí se, jakmile bude TREK znovu online.',
};
export default budget;
+3
View File
@@ -207,5 +207,8 @@ const budget: TranslationStrings = {
'Speichern fehlgeschlagen, {count} hochgeladene Belege liegen noch da. Im Dateien-Tab entfernen.',
'costs.receiptLeftBehind.one':
'Speichern fehlgeschlagen, {count} hochgeladener Beleg liegt noch da. Im Dateien-Tab entfernen.',
'costs.receiptsNeedConnection':
'Belege brauchen zum Hochladen eine Verbindung. Entferne die neuen, um die Ausgabe offline zu speichern, oder speichere, sobald du wieder online bist.',
'costs.paymentQueued': 'Du bist offline. Die Zahlung ist gespeichert und erscheint, sobald TREK wieder online ist.',
};
export default budget;
+3
View File
@@ -207,6 +207,9 @@ const budget: TranslationStrings = {
'Save failed, and {count} uploaded receipts are still there. Remove them in the Files tab.',
'costs.receiptLeftBehind.one':
'Save failed, and {count} uploaded receipt is still there. Remove it in the Files tab.',
'costs.receiptsNeedConnection':
'Receipts need a connection to upload. Remove the new ones to save the expense offline, or save once you are back online.',
'costs.paymentQueued': 'You are offline. The payment is saved and shows up once TREK is back online.',
};
export default budget;
+3
View File
@@ -205,5 +205,8 @@ const budget: TranslationStrings = {
'costs.viewReceipt': 'Ver recibo',
'costs.receiptLeftBehind': 'No se pudo guardar y quedan {count} recibos subidos. Elimínalos en la pestaña Archivos.',
'costs.receiptLeftBehind.one': 'No se pudo guardar y queda {count} recibo subido. Elimínalo en la pestaña Archivos.',
'costs.receiptsNeedConnection':
'Los recibos necesitan conexión para subirse. Quita los nuevos para guardar el gasto sin conexión, o guarda cuando vuelvas a estar en línea.',
'costs.paymentQueued': 'Estás sin conexión. El pago está guardado y aparecerá cuando TREK vuelva a estar en línea.',
};
export default budget;
+3
View File
@@ -207,6 +207,9 @@ const budget: TranslationStrings = {
'Salvestamine ebaõnnestus ja {count} üleslaaditud kviitungit on endiselt alles. Eemalda need vahekaardil Failid.',
'costs.receiptLeftBehind.one':
'Salvestamine ebaõnnestus ja {count} üleslaaditud kviitung on endiselt alles. Eemalda see vahekaardil Failid.',
'costs.receiptsNeedConnection':
'Tšekkide üleslaadimiseks on vaja ühendust. Eemalda uued, et kulu võrguühenduseta salvestada, või salvesta, kui oled taas võrgus.',
'costs.paymentQueued': 'Oled võrguühenduseta. Makse on salvestatud ja ilmub, kui TREK on taas võrgus.',
};
export default budget;
+3
View File
@@ -208,5 +208,8 @@ const budget: TranslationStrings = {
"Échec de l'enregistrement : {count} reçus importés sont toujours là. Supprimez-les dans l'onglet Fichiers.",
'costs.receiptLeftBehind.one':
"Échec de l'enregistrement : {count} reçu importé est toujours là. Supprimez-le dans l'onglet Fichiers.",
'costs.receiptsNeedConnection':
"Les justificatifs ont besoin d'une connexion pour être envoyés. Retirez les nouveaux pour enregistrer la dépense hors ligne, ou enregistrez une fois de retour en ligne.",
'costs.paymentQueued': 'Vous êtes hors ligne. Le paiement est enregistré et apparaîtra dès que TREK sera de nouveau en ligne.',
};
export default budget;
+3
View File
@@ -209,5 +209,8 @@ const budget: TranslationStrings = {
'Η αποθήκευση απέτυχε και {count} ανεβασμένες αποδείξεις παραμένουν. Διαγράψτε τες στην καρτέλα Αρχεία.',
'costs.receiptLeftBehind.one':
'Η αποθήκευση απέτυχε και {count} ανεβασμένη απόδειξη παραμένει. Διαγράψτε τη στην καρτέλα Αρχεία.',
'costs.receiptsNeedConnection':
'Οι αποδείξεις χρειάζονται σύνδεση για να ανέβουν. Αφαίρεσε τις νέες για να αποθηκεύσεις το έξοδο εκτός σύνδεσης ή αποθήκευσε όταν είσαι ξανά online.',
'costs.paymentQueued': 'Είσαι εκτός σύνδεσης. Η πληρωμή αποθηκεύτηκε και θα εμφανιστεί μόλις το TREK συνδεθεί ξανά.',
};
export default budget;
+3
View File
@@ -208,5 +208,8 @@ const budget: TranslationStrings = {
'costs.receiptLeftBehind':
'A mentés nem sikerült, és {count} feltöltött nyugta még ott van. Töröld őket a Fájlok fülön.',
'costs.receiptLeftBehind.one': 'A mentés nem sikerült, és a feltöltött nyugta még ott van. Töröld a Fájlok fülön.',
'costs.receiptsNeedConnection':
'A nyugták feltöltéséhez kapcsolat kell. Távolítsd el az újakat, hogy offline mentsd a kiadást, vagy ments, amikor újra online vagy.',
'costs.paymentQueued': 'Offline vagy. A fizetés el van mentve, és megjelenik, amint a TREK újra online lesz.',
};
export default budget;
+3
View File
@@ -195,5 +195,8 @@ const budget: TranslationStrings = {
'costs.deleteReceipt': 'Hapus kuitansi',
'costs.viewReceipt': 'Lihat kuitansi',
'costs.receiptLeftBehind': 'Gagal menyimpan, tetapi {count} kuitansi yang diunggah masih ada. Hapus di tab File.',
'costs.receiptsNeedConnection':
'Struk perlu koneksi untuk diunggah. Hapus struk baru untuk menyimpan pengeluaran secara offline, atau simpan saat kamu kembali online.',
'costs.paymentQueued': 'Kamu sedang offline. Pembayaran sudah disimpan dan akan muncul begitu TREK kembali online.',
};
export default budget;
+3
View File
@@ -207,5 +207,8 @@ const budget: TranslationStrings = {
'Salvataggio non riuscito e {count} ricevute caricate sono ancora lì. Rimuovile nella scheda File.',
'costs.receiptLeftBehind.one':
'Salvataggio non riuscito e {count} ricevuta caricata è ancora lì. Rimuovila nella scheda File.',
'costs.receiptsNeedConnection':
'Le ricevute hanno bisogno di una connessione per essere caricate. Rimuovi quelle nuove per salvare la spesa offline, oppure salva quando sei di nuovo online.',
'costs.paymentQueued': 'Sei offline. Il pagamento è salvato e comparirà quando TREK tornerà online.',
};
export default budget;
+3
View File
@@ -196,5 +196,8 @@ const budget: TranslationStrings = {
'costs.viewReceipt': 'レシートを表示',
'costs.receiptLeftBehind':
'保存に失敗し、アップロード済みのレシートが{count}件残っています。「ファイル」タブで削除してください。',
'costs.receiptsNeedConnection':
'レシートのアップロードには接続が必要です。オフラインで経費を保存するには新しいレシートを外すか、オンラインに戻ってから保存してください。',
'costs.paymentQueued': 'オフラインです。支払いは保存され、TREK がオンラインに戻ると表示されます。',
};
export default budget;
+3
View File
@@ -195,5 +195,8 @@ const budget: TranslationStrings = {
'costs.deleteReceipt': '영수증 삭제',
'costs.viewReceipt': '영수증 보기',
'costs.receiptLeftBehind': '저장에 실패했고 업로드된 영수증 {count}개가 남아 있습니다. 파일 탭에서 삭제하세요.',
'costs.receiptsNeedConnection':
'영수증을 업로드하려면 연결이 필요합니다. 오프라인으로 지출을 저장하려면 새 영수증을 제거하거나, 다시 온라인이 되면 저장하세요.',
'costs.paymentQueued': '오프라인 상태입니다. 결제가 저장되었으며 TREK가 다시 온라인이 되면 표시됩니다.',
};
export default budget;
+3
View File
@@ -208,5 +208,8 @@ const budget: TranslationStrings = {
'Opslaan mislukt en er staan nog {count} geüploade bonnen. Verwijder ze op het tabblad Bestanden.',
'costs.receiptLeftBehind.one':
'Opslaan mislukt en er staat nog {count} geüploade bon. Verwijder hem op het tabblad Bestanden.',
'costs.receiptsNeedConnection':
'Bonnetjes hebben een verbinding nodig om te uploaden. Verwijder de nieuwe om de uitgave offline op te slaan, of sla op zodra je weer online bent.',
'costs.paymentQueued': 'Je bent offline. De betaling is opgeslagen en verschijnt zodra TREK weer online is.',
};
export default budget;
+3
View File
@@ -228,5 +228,8 @@ const budget: TranslationStrings = {
'Zapis nie powiódł się, a {count} przesłane paragony wciąż tam są. Usuń je w zakładce Pliki.',
'costs.receiptLeftBehind.many':
'Zapis nie powiódł się, a {count} przesłanych paragonów wciąż tam jest. Usuń je w zakładce Pliki.',
'costs.receiptsNeedConnection':
'Paragony wymagają połączenia do przesłania. Usuń nowe, aby zapisać wydatek offline, albo zapisz po powrocie online.',
'costs.paymentQueued': 'Jesteś offline. Płatność została zapisana i pojawi się, gdy TREK znów będzie online.',
};
export default budget;
+3
View File
@@ -228,5 +228,8 @@ const budget: TranslationStrings = {
'Не удалось сохранить, {count} загруженных чека остались. Удалите их на вкладке «Файлы».',
'costs.receiptLeftBehind.many':
'Не удалось сохранить, {count} загруженных чеков остались. Удалите их на вкладке «Файлы».',
'costs.receiptsNeedConnection':
'Для загрузки чеков нужно подключение. Удалите новые, чтобы сохранить расход офлайн, или сохраните, когда снова будете онлайн.',
'costs.paymentQueued': 'Вы офлайн. Платёж сохранён и появится, когда TREK снова будет онлайн.',
};
export default budget;
+3
View File
@@ -218,6 +218,9 @@ const budget: TranslationStrings = {
'Uloženie zlyhalo a {count} nahraná účtenka tam stále je. Odstráňte ju v záložke Súbory.',
'costs.receiptLeftBehind.few':
'Uloženie zlyhalo a {count} nahrané účtenky tam stále sú. Odstráňte ich v záložke Súbory.',
'costs.receiptsNeedConnection':
'Účtenky potrebujú na nahratie pripojenie. Odstráňte nové, aby ste výdavok uložili offline, alebo uložte, keď budete znova online.',
'costs.paymentQueued': 'Ste offline. Platba je uložená a zobrazí sa, keď bude TREK znova online.',
};
export default budget;
+3
View File
@@ -207,5 +207,8 @@ const budget: TranslationStrings = {
'Sparandet misslyckades och {count} uppladdade kvitton finns kvar. Ta bort dem under Filer.',
'costs.receiptLeftBehind.one':
'Sparandet misslyckades och {count} uppladdat kvitto finns kvar. Ta bort det under Filer.',
'costs.receiptsNeedConnection':
'Kvitton behöver en anslutning för att laddas upp. Ta bort de nya för att spara utgiften offline, eller spara när du är online igen.',
'costs.paymentQueued': 'Du är offline. Betalningen är sparad och visas när TREK är online igen.',
};
export default budget;
+3
View File
@@ -195,6 +195,9 @@ const budget: TranslationStrings = {
'costs.deleteReceipt': 'ลบใบเสร็จ',
'costs.viewReceipt': 'ดูใบเสร็จ',
'costs.receiptLeftBehind': 'บันทึกไม่สำเร็จ และยังมีใบเสร็จที่อัปโหลด {count} รายการอยู่ ลบได้ในแท็บไฟล์',
'costs.receiptsNeedConnection':
'การอัปโหลดใบเสร็จต้องใช้การเชื่อมต่อ ลบใบเสร็จใหม่ออกเพื่อบันทึกค่าใช้จ่ายแบบออฟไลน์ หรือบันทึกเมื่อกลับมาออนไลน์',
'costs.paymentQueued': 'คุณออฟไลน์อยู่ การชำระเงินถูกบันทึกแล้วและจะแสดงเมื่อ TREK กลับมาออนไลน์',
};
export default budget;
+3
View File
@@ -208,5 +208,8 @@ const budget: TranslationStrings = {
'Kaydetme başarısız oldu ve yüklenen {count} fiş hâlâ duruyor. Dosyalar sekmesinden silin.',
'costs.receiptLeftBehind.one':
'Kaydetme başarısız oldu ve yüklenen {count} fiş hâlâ duruyor. Dosyalar sekmesinden silin.',
'costs.receiptsNeedConnection':
'Fişlerin yüklenmesi için bağlantı gerekir. Harcamayı çevrimdışı kaydetmek için yenilerini kaldırın ya da tekrar çevrimiçi olduğunuzda kaydedin.',
'costs.paymentQueued': 'Çevrimdışısınız. Ödeme kaydedildi ve TREK yeniden çevrimiçi olduğunda görünecek.',
};
export default budget;
+3
View File
@@ -229,5 +229,8 @@ const budget: TranslationStrings = {
'Не вдалося зберегти, а {count} завантажені чеки залишилися. Видаліть їх на вкладці «Файли».',
'costs.receiptLeftBehind.many':
'Не вдалося зберегти, а {count} завантажених чеків залишилося. Видаліть їх на вкладці «Файли».',
'costs.receiptsNeedConnection':
"Для завантаження чеків потрібне з'єднання. Видаліть нові, щоб зберегти витрату офлайн, або збережіть, коли знову будете онлайн.",
'costs.paymentQueued': "Ви офлайн. Платіж збережено, і він з'явиться, коли TREK знову буде онлайн.",
};
export default budget;
+3
View File
@@ -195,6 +195,9 @@ const budget: TranslationStrings = {
'costs.deleteReceipt': 'Xóa hóa đơn',
'costs.viewReceipt': 'Xem hóa đơn',
'costs.receiptLeftBehind': 'Không lưu được, và {count} hóa đơn đã tải lên vẫn còn đó. Hãy vào tab Tập tin để xóa.',
'costs.receiptsNeedConnection':
'Cần có kết nối để tải hóa đơn lên. Gỡ các hóa đơn mới để lưu khoản chi khi ngoại tuyến, hoặc lưu khi bạn trực tuyến trở lại.',
'costs.paymentQueued': 'Bạn đang ngoại tuyến. Khoản thanh toán đã được lưu và sẽ hiện ra khi TREK trực tuyến trở lại.',
};
export default budget;
+3
View File
@@ -194,5 +194,8 @@ const budget: TranslationStrings = {
'costs.deleteReceipt': '刪除收據',
'costs.viewReceipt': '查看收據',
'costs.receiptLeftBehind': '儲存失敗,已上傳的 {count} 張收據仍留在系統中。請到「檔案」分頁刪除。',
'costs.receiptsNeedConnection':
'上傳收據需要網路連線。移除新收據即可離線儲存這筆支出,或在恢復連線後再儲存。',
'costs.paymentQueued': '你目前處於離線狀態。付款已儲存,TREK 恢復連線後就會顯示。',
};
export default budget;
+3
View File
@@ -194,5 +194,8 @@ const budget: TranslationStrings = {
'costs.deleteReceipt': '删除收据',
'costs.viewReceipt': '查看收据',
'costs.receiptLeftBehind': '保存失败,但已上传的 {count} 张收据仍然保留。请在“文件”标签页中删除。',
'costs.receiptsNeedConnection':
'上传收据需要网络连接。移除新收据即可离线保存这笔支出,或在恢复联网后再保存。',
'costs.paymentQueued': '你当前处于离线状态。付款已保存,TREK 恢复联网后就会显示。',
};
export default budget;