fixed transfer process

This commit is contained in:
blaisadmin
2026-04-15 23:39:10 -04:00
parent 765d6c61db
commit 3a0e30085c
9 changed files with 480 additions and 231 deletions
+72 -120
View File
@@ -860,15 +860,16 @@ function App() {
reason: '',
notes: '',
});
const [mergeForm, setMergeForm] = useState({
birdId: '',
destinationOwnerEmail: '',
notes: '',
});
const [flockTransferForm, setFlockTransferForm] = useState({
birdId: '',
targetWorkspaceId: '',
destinationOwnerEmail: '',
});
const [transferringBird, setTransferringBird] = useState(false);
const [transferError, setTransferError] = useState('');
const [transferNotice, setTransferNotice] = useState<{
message: string;
previewUrl?: string | null;
} | null>(null);
const [deletingBird, setDeletingBird] = useState(false);
const [editingVetVisitId, setEditingVetVisitId] = useState('');
const [deletingVetVisitId, setDeletingVetVisitId] = useState('');
@@ -879,10 +880,6 @@ function App() {
() => birds.find((bird) => bird.id === selectedBirdId) ?? null,
[birds, selectedBirdId],
);
const transferableWorkspaces = useMemo(
() => authSession?.workspaces.filter((entry) => entry.workspace.id !== workspace?.id) ?? [],
[authSession, workspace?.id],
);
const editingBird = useMemo(
() => birds.find((bird) => bird.id === editingBirdId) ?? null,
[birds, editingBirdId],
@@ -2127,59 +2124,22 @@ function App() {
}
};
const handleMergeSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setError('');
try {
const response = await apiFetch('/transfers/draft', authToken, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(mergeForm),
});
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Unable to save transfer draft.'));
}
const data =
(await readJsonSafely<{
bird?: Bird;
destinationOwnerExists?: boolean;
inviteSent?: boolean;
}>(response)) ?? {};
const transferBirdName = data.bird?.name || birds.find((bird) => bird.id === mergeForm.birdId)?.name || 'bird';
const inviteCopy = data.inviteSent
? `\n\nA FlockPal invite was also sent to ${mergeForm.destinationOwnerEmail} because that email does not have an account yet.`
: data.destinationOwnerExists
? `\n\n${mergeForm.destinationOwnerEmail} already has a FlockPal account, so no invite was needed.`
: '';
window.alert(
`Transfer prep saved for ${transferBirdName}.${inviteCopy}\n\nThis is currently a planning workflow only. Later this page can turn into a real account-to-account transfer flow using verified bird identity and ownership checks.`,
);
setMergeForm({
birdId: '',
destinationOwnerEmail: '',
notes: '',
});
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : 'Unable to save transfer draft.');
}
};
const handleFlockTransferSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (transferringBird) {
return;
}
setError('');
setTransferError('');
setTransferNotice(null);
setTransferringBird(true);
try {
const response = await apiFetch(`/birds/${flockTransferForm.birdId}/transfer`, authToken, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
targetWorkspaceId: Number(flockTransferForm.targetWorkspaceId),
destinationOwnerEmail: flockTransferForm.destinationOwnerEmail,
}),
});
@@ -2187,9 +2147,29 @@ function App() {
throw new Error(await readErrorMessage(response, 'Unable to transfer bird to another flock.'));
}
const data = (await readJsonSafely<{ bird?: Bird }>(response)) ?? {};
const data =
(await readJsonSafely<{
bird?: Bird;
inviteSent?: boolean;
invitePreviewUrl?: string | null;
message?: string;
}>(response)) ?? {};
const transferredBirdName = data.bird?.name || birds.find((bird) => bird.id === flockTransferForm.birdId)?.name || 'Bird';
if (data.inviteSent) {
setTransferNotice({
message:
data.message ??
`A bird transfer invite was sent to ${flockTransferForm.destinationOwnerEmail}. The transfer will complete automatically after they sign in.`,
previewUrl: data.invitePreviewUrl,
});
setFlockTransferForm({
birdId: '',
destinationOwnerEmail: '',
});
return;
}
setBirds((current) => current.filter((bird) => bird.id !== flockTransferForm.birdId));
setAllBirdWeights((current) => {
const next = { ...current };
@@ -2208,12 +2188,16 @@ function App() {
}
setFlockTransferForm({
birdId: '',
targetWorkspaceId: '',
destinationOwnerEmail: '',
});
window.alert(`${transferredBirdName} was moved to the selected flock.`);
window.alert(`${transferredBirdName} was transferred to ${flockTransferForm.destinationOwnerEmail}.`);
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : 'Unable to transfer bird to another flock.');
const message = submitError instanceof Error ? submitError.message : 'Unable to transfer bird to another flock.';
setTransferError(message);
setError(message);
} finally {
setTransferringBird(false);
}
};
@@ -3955,7 +3939,7 @@ function App() {
<div className="panel-header">
<div>
<p className="eyebrow">Settings</p>
<h2>Bird transfer prep</h2>
<h2>Bird transfer</h2>
</div>
<button
className="secondary-button"
@@ -3971,15 +3955,19 @@ function App() {
{expandedSettingsSection === 'transfer' ? (
<>
<p className="muted">
Move a bird to another flock you already belong to. This keeps the bird record, weight history, and vet visits attached while
changing which flock owns it.
Transfer a bird to another flock by entering the receiving flock owner's email. This keeps the bird record, weight history, and
vet visits attached while changing which flock owns it.
</p>
<form className="form-panel" onSubmit={handleFlockTransferSubmit}>
<label>
Bird to move
<select
value={flockTransferForm.birdId}
onChange={(event) => setFlockTransferForm({ ...flockTransferForm, birdId: event.target.value })}
onChange={(event) => {
setFlockTransferForm({ ...flockTransferForm, birdId: event.target.value });
setTransferError('');
setTransferNotice(null);
}}
required
>
<option value="">Select a bird from this flock</option>
@@ -3991,70 +3979,34 @@ function App() {
</select>
</label>
<label>
Destination flock
<select
value={flockTransferForm.targetWorkspaceId}
onChange={(event) => setFlockTransferForm({ ...flockTransferForm, targetWorkspaceId: event.target.value })}
required
>
<option value="">Select another flock you can access</option>
{transferableWorkspaces.map((entry) => (
<option key={entry.workspace.id} value={String(entry.workspace.id)}>
{entry.workspace.name} {formatWorkspaceRole(entry.membership.role)}
</option>
))}
</select>
</label>
<button className="primary-button" type="submit" disabled={!transferableWorkspaces.length}>
Move bird to another flock
</button>
{!transferableWorkspaces.length ? (
<small className="muted">Create or join another flock first to use in-app bird transfers.</small>
) : null}
</form>
<div className="form-divider" />
<p className="muted">
For future owner-to-owner handoffs outside your own flocks, save a transfer draft below.
</p>
<form className="form-panel" onSubmit={handleMergeSubmit}>
<label>
Bird for external transfer prep
<select
value={mergeForm.birdId}
onChange={(event) => setMergeForm({ ...mergeForm, birdId: event.target.value })}
required
>
<option value="">Select a bird from this flock</option>
{birds.map((bird) => (
<option key={bird.id} value={bird.id}>
{bird.name} {bird.species} Band {bird.tagId}
</option>
))}
</select>
</label>
<label>
Destination owner email
Receiving flock owner email
<input
type="email"
value={mergeForm.destinationOwnerEmail}
onChange={(event) => setMergeForm({ ...mergeForm, destinationOwnerEmail: event.target.value })}
value={flockTransferForm.destinationOwnerEmail}
onChange={(event) => {
setFlockTransferForm({ ...flockTransferForm, destinationOwnerEmail: event.target.value });
setTransferError('');
setTransferNotice(null);
}}
placeholder="owner@example.com"
required
/>
</label>
<label>
Transfer notes
<textarea
rows={4}
value={mergeForm.notes}
onChange={(event) => setMergeForm({ ...mergeForm, notes: event.target.value })}
placeholder="Optional context for rescue release, adoption, or household transfer"
/>
</label>
<button className="primary-button" type="submit">
Save transfer draft
<button className="primary-button" type="submit" disabled={transferringBird}>
{transferringBird ? 'Transferring bird...' : 'Transfer bird'}
</button>
{transferError ? (
<p className="error-banner" role="alert">
{transferError}
</p>
) : null}
{transferNotice ? (
<article className="summary-card" role="status">
<strong>Pending transfer invite sent</strong>
<span>{transferNotice.message}</span>
{transferNotice.previewUrl ? <a href={transferNotice.previewUrl}>Open invite link</a> : null}
</article>
) : null}
</form>
</>
) : null}