feat: Sprachnotiz in der Durchsicht, Info-Seite und Zähler (T7 L5 Tasks 20, 22)
Task 20: /inspections/new?voice=<id> zeigt die Sprachnotiz über dem Formular (Abgeschriebenes, Unklares, „Alle übernehmen“ mit Rückfrage) und Vorschläge unter den Feldern mit wörtlichem Zitat; nichts wird ohne Tipp übernommen, die Volkskarte wird nie angehakt. Bei ausgeschalteter Auswertung ist der Text offen zum Abtippen. Speichern schickt voiceCaptureId; bei 409 VOICE_CAPTURE_GONE genau ein neuer Versuch ohne Notiz mit demselben Idempotency-Key (online und aus der Warteschlange); VOICE_CAPTURE_COLONY_MISMATCH nie automatisch, sondern „Ohne Sprachnotiz speichern/senden“. Skalen aus lib/inspections/scales.ts mit Paritätstest zum Backend. Task 22: /settings/voice mit Modell und Fristen (bei ausgeschalteter Auswertung ausdrücklich: nichts geht an einen KI-Dienst), „Alle meine Sprachnotizen löschen“ auf Server und Gerät mit Anzahl; Systemkarte „Sprachnotizen“ (nur system:manage) mit Zählern und Kosten der letzten 7 Tage (Ruling 15); Service Worker cacht /api/v1/voice-captures nie, /voice und /voice/new offline erreichbar. Gates: Frontend lint, typecheck, 3106 Tests, build. Refs #27 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
51206bea4f
commit
ce922c8569
40 files changed
+3347
-45
No files matched your search
@@ -14,6 +14,7 @@ import {
|
||||
listQueue,
|
||||
retryBulkWithout,
|
||||
retryInspectionWithoutHiveChange,
|
||||
retryInspectionWithoutVoice,
|
||||
type QueueError,
|
||||
type QueueItem,
|
||||
} from "@/lib/offline/db";
|
||||
@@ -289,6 +290,24 @@ export function PendingQueueSheet({
|
||||
onError: () => toast.error("Eintrag konnte nicht erneut gesendet werden."),
|
||||
});
|
||||
|
||||
// „Ohne Sprachnotiz senden“: only after the server said the note is another colony's.
|
||||
const resendWithoutVoice = useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
await retryInspectionWithoutVoice(id);
|
||||
return syncPendingEntries(userId, { afterChange: true });
|
||||
},
|
||||
networkMode: "always",
|
||||
onSuccess: (result) => {
|
||||
if (result.synced > 0) {
|
||||
toast.success(entriesSynced(result.synced));
|
||||
void queryClient.invalidateQueries();
|
||||
return;
|
||||
}
|
||||
void queryClient.invalidateQueries({ queryKey: OFFLINE_QUEUE_QUERY_KEY });
|
||||
},
|
||||
onError: () => toast.error("Eintrag konnte nicht erneut gesendet werden."),
|
||||
});
|
||||
|
||||
const records = items.filter((item) => item.store !== VOICE_STORE);
|
||||
const voiceNotes = items.filter((item) => item.store === VOICE_STORE);
|
||||
const names = entryNames(records);
|
||||
@@ -354,6 +373,19 @@ export function PendingQueueSheet({
|
||||
Ohne Beute-Änderung senden
|
||||
</Button>
|
||||
) : null}
|
||||
{item.canResendWithoutVoice && typeof item.id === "number" ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="touch"
|
||||
className="self-start"
|
||||
disabled={resendWithoutVoice.isPending}
|
||||
onClick={() => resendWithoutVoice.mutate(item.id as number)}
|
||||
>
|
||||
<RotateCw aria-hidden />
|
||||
Ohne Sprachnotiz senden
|
||||
</Button>
|
||||
) : null}
|
||||
{item.resendWithout && typeof item.id === "number" ? (
|
||||
<ResendWithoutButton
|
||||
resend={item.resendWithout}
|
||||
|
||||
Reference in new issue
Block a user