fixing rescue settings
This commit is contained in:
+166
-9
@@ -548,7 +548,7 @@ const formatSubscriptionStatus = (status: SubscriptionStatus) => {
|
||||
|
||||
const formatRescueVerificationStatus = (status: RescueVerificationStatus) => {
|
||||
if (status === 'approved') {
|
||||
return 'Approved';
|
||||
return 'Active';
|
||||
}
|
||||
if (status === 'rejected') {
|
||||
return 'Rejected';
|
||||
@@ -837,6 +837,7 @@ function App() {
|
||||
const [cancelingRescueRequest, setCancelingRescueRequest] = useState(false);
|
||||
const [savingWorkspaceMember, setSavingWorkspaceMember] = useState(false);
|
||||
const [creatingWorkspace, setCreatingWorkspace] = useState(false);
|
||||
const [deletingWorkspace, setDeletingWorkspace] = useState(false);
|
||||
const [creatingIntegrationToken, setCreatingIntegrationToken] = useState(false);
|
||||
const [revokingIntegrationTokenId, setRevokingIntegrationTokenId] = useState('');
|
||||
const [newIntegrationTokenSecret, setNewIntegrationTokenSecret] = useState('');
|
||||
@@ -864,6 +865,10 @@ function App() {
|
||||
destinationOwnerEmail: '',
|
||||
notes: '',
|
||||
});
|
||||
const [flockTransferForm, setFlockTransferForm] = useState({
|
||||
birdId: '',
|
||||
targetWorkspaceId: '',
|
||||
});
|
||||
const [deletingBird, setDeletingBird] = useState(false);
|
||||
const [editingVetVisitId, setEditingVetVisitId] = useState('');
|
||||
const [deletingVetVisitId, setDeletingVetVisitId] = useState('');
|
||||
@@ -874,6 +879,10 @@ 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],
|
||||
@@ -2161,6 +2170,53 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFlockTransferSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const response = await apiFetch(`/birds/${flockTransferForm.birdId}/transfer`, authToken, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
targetWorkspaceId: Number(flockTransferForm.targetWorkspaceId),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await readErrorMessage(response, 'Unable to transfer bird to another flock.'));
|
||||
}
|
||||
|
||||
const data = (await readJsonSafely<{ bird?: Bird }>(response)) ?? {};
|
||||
const transferredBirdName = data.bird?.name || birds.find((bird) => bird.id === flockTransferForm.birdId)?.name || 'Bird';
|
||||
|
||||
setBirds((current) => current.filter((bird) => bird.id !== flockTransferForm.birdId));
|
||||
setAllBirdWeights((current) => {
|
||||
const next = { ...current };
|
||||
delete next[flockTransferForm.birdId];
|
||||
return next;
|
||||
});
|
||||
setWeights((current) => (selectedBird?.id === flockTransferForm.birdId ? [] : current));
|
||||
setVetVisits((current) => (selectedBird?.id === flockTransferForm.birdId ? [] : current));
|
||||
if (selectedBird?.id === flockTransferForm.birdId) {
|
||||
setSelectedBirdId('');
|
||||
}
|
||||
if (editingBirdId === flockTransferForm.birdId) {
|
||||
setEditingBirdId('');
|
||||
setBirdForm(emptyBirdForm);
|
||||
setBirdPhotoName('');
|
||||
}
|
||||
setFlockTransferForm({
|
||||
birdId: '',
|
||||
targetWorkspaceId: '',
|
||||
});
|
||||
|
||||
window.alert(`${transferredBirdName} was moved to the selected flock.`);
|
||||
} catch (submitError) {
|
||||
setError(submitError instanceof Error ? submitError.message : 'Unable to transfer bird to another flock.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleWorkspaceSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setError('');
|
||||
@@ -2213,6 +2269,47 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteWorkspace = async () => {
|
||||
if (!workspace || !authToken || deletingWorkspace || activeMembership?.role !== 'owner') {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = window.confirm(
|
||||
`Delete ${workspace.name}?\n\nThis only works when the flock has no birds. Remove or transfer all birds first.\n\nYou will be switched to another flock or a new personal flock automatically.`,
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
setDeletingWorkspace(true);
|
||||
|
||||
try {
|
||||
const response = await apiFetch('/workspace', authToken, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await readErrorMessage(response, 'Unable to delete flock.'));
|
||||
}
|
||||
|
||||
const data = (await readJsonSafely<{ token?: string; session?: AuthSessionPayload }>(response)) ?? {};
|
||||
|
||||
if (!data.session) {
|
||||
throw new Error('Flock was deleted but the session could not be refreshed.');
|
||||
}
|
||||
|
||||
const nextToken = data.token || authToken;
|
||||
persistSessionToken(nextToken);
|
||||
applySession(data.session, nextToken);
|
||||
} catch (workspaceError) {
|
||||
setError(workspaceError instanceof Error ? workspaceError.message : 'Unable to delete flock.');
|
||||
} finally {
|
||||
setDeletingWorkspace(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelRescueRequest = async () => {
|
||||
if (!authToken) {
|
||||
return;
|
||||
@@ -3226,6 +3323,14 @@ function App() {
|
||||
? 'Convert current flock to rescue'
|
||||
: 'Save flock settings'}
|
||||
</button>
|
||||
{activeMembership?.role === 'owner' ? (
|
||||
<button className="danger-button" onClick={handleDeleteWorkspace} type="button" disabled={deletingWorkspace}>
|
||||
{deletingWorkspace ? 'Deleting flock...' : 'Delete flock'}
|
||||
</button>
|
||||
) : null}
|
||||
{activeMembership?.role === 'owner' ? (
|
||||
<small className="muted">Delete is only available when this flock has no birds. Collaborators and tokens are removed with it.</small>
|
||||
) : null}
|
||||
</form>
|
||||
</article>
|
||||
|
||||
@@ -3241,14 +3346,22 @@ function App() {
|
||||
<strong>{workspace ? formatBillingPlanName(workspace.billingPlan) : 'No plan yet'}</strong>
|
||||
<span>{workspace ? formatBillingPlanCapacity(workspace.billingPlan) : 'Pick a flock plan to see bird capacity.'}</span>
|
||||
</article>
|
||||
<article className="summary-card">
|
||||
<strong>{workspace ? formatSubscriptionStatus(workspace.subscriptionStatus) : 'Unknown'}</strong>
|
||||
<span>Flock write access will follow subscription health once billing is connected.</span>
|
||||
</article>
|
||||
{workspace?.workspaceType !== 'rescue' ? (
|
||||
<article className="summary-card">
|
||||
<strong>{workspace ? formatSubscriptionStatus(workspace.subscriptionStatus) : 'Unknown'}</strong>
|
||||
<span>Flock write access will follow subscription health once billing is connected.</span>
|
||||
</article>
|
||||
) : null}
|
||||
{workspace?.workspaceType === 'rescue' ? (
|
||||
<article className="summary-card">
|
||||
<strong>{formatRescueVerificationStatus(workspace.rescueVerificationStatus)}</strong>
|
||||
<span>Rescue flocks are read-only until an admin approves their verification.</span>
|
||||
<span>
|
||||
{workspace.rescueVerificationStatus === 'approved'
|
||||
? 'Rescue verification is approved and this flock is fully active.'
|
||||
: workspace.rescueVerificationStatus === 'rejected'
|
||||
? 'This rescue request was rejected. Update the flock or contact support before trying again.'
|
||||
: 'Rescue flocks are read-only until an admin approves their verification.'}
|
||||
</span>
|
||||
{workspace.rescueVerificationStatus === 'pending' &&
|
||||
(activeMembership?.role === 'owner' || activeMembership?.role === 'assistant') ? (
|
||||
<button
|
||||
@@ -3858,12 +3971,56 @@ function App() {
|
||||
{expandedSettingsSection === 'transfer' ? (
|
||||
<>
|
||||
<p className="muted">
|
||||
This is the first step toward rescue handoffs and owner-to-owner transfers. For now it captures the matching details we would
|
||||
later use to safely move a bird record between accounts.
|
||||
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.
|
||||
</p>
|
||||
<form className="form-panel" onSubmit={handleFlockTransferSubmit}>
|
||||
<label>
|
||||
Bird to move
|
||||
<select
|
||||
value={flockTransferForm.birdId}
|
||||
onChange={(event) => setFlockTransferForm({ ...flockTransferForm, 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 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 to transfer
|
||||
Bird for external transfer prep
|
||||
<select
|
||||
value={mergeForm.birdId}
|
||||
onChange={(event) => setMergeForm({ ...mergeForm, birdId: event.target.value })}
|
||||
|
||||
@@ -815,6 +815,12 @@ textarea {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.form-divider {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(53, 129, 98, 0.32), transparent);
|
||||
}
|
||||
|
||||
.inline-form {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user