fixed transfer process
This commit is contained in:
+72
-120
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user