Fixed delete workflow and added additional profile info

This commit is contained in:
Corey Blais
2026-05-20 17:12:15 -04:00
parent 5db30022eb
commit 0db90aab45
9 changed files with 403 additions and 248 deletions
+251 -193
View File
@@ -19,6 +19,9 @@ type Bird = {
name: string;
tagId: string | null;
species: string;
motivators: string | null;
demotivators: string | null;
favoriteSnack: string | null;
gender: BirdGender;
dateOfBirth: string | null;
gotchaDay: string | null;
@@ -179,6 +182,9 @@ type BirdFormState = {
name: string;
tagId: string;
species: string;
motivators: string;
demotivators: string;
favoriteSnack: string;
gender: BirdGender;
dateOfBirth: string;
gotchaDay: string;
@@ -315,6 +321,9 @@ const emptyBirdForm: BirdFormState = {
name: '',
tagId: '',
species: '',
motivators: '',
demotivators: '',
favoriteSnack: '',
gender: 'unknown',
dateOfBirth: '',
gotchaDay: '',
@@ -437,6 +446,9 @@ const toBirdForm = (bird: Bird): BirdFormState => ({
name: bird.name,
tagId: bird.tagId ?? '',
species: bird.species,
motivators: bird.motivators ?? '',
demotivators: bird.demotivators ?? '',
favoriteSnack: bird.favoriteSnack ?? '',
gender: bird.gender,
dateOfBirth: bird.dateOfBirth ?? '',
gotchaDay: bird.gotchaDay ?? '',
@@ -2094,7 +2106,7 @@ function App() {
}
};
const handleWorkspaceSwitch = async (workspaceId: number) => {
const handleWorkspaceSwitch = async (workspaceId: number, nextActivePage: AppPage = 'overview') => {
if (!authToken || workspaceId === workspace?.id) {
return;
}
@@ -2129,7 +2141,7 @@ function App() {
setMedications([]);
setMedicationAdministrations([]);
setAllBirdVetVisits({});
setActivePage('overview');
setActivePage(nextActivePage);
} catch (switchError) {
setError(switchError instanceof Error ? switchError.message : 'Unable to switch flocks.');
} finally {
@@ -2279,6 +2291,7 @@ function App() {
throw new Error(await readErrorMessage(response, 'Unable to create flock.'));
}
const createdData = (await readJsonSafely<{ workspace?: Workspace }>(response)) ?? {};
const workspaceResponse = await apiFetch('/auth/session', authToken);
if (!workspaceResponse.ok) {
throw new Error(await readErrorMessage(workspaceResponse, 'Flock was created but the session could not be refreshed.'));
@@ -2296,6 +2309,11 @@ function App() {
...emptyWorkspaceCreateForm,
billingEmail: data.session.user.email,
});
setExpandedSettingsSection(null);
if (createdData.workspace) {
await handleWorkspaceSwitch(createdData.workspace.id, 'settings');
}
} catch (workspaceError) {
setError(workspaceError instanceof Error ? workspaceError.message : 'Unable to create flock.');
} finally {
@@ -3205,7 +3223,7 @@ function App() {
}
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.`,
`Delete ${workspace.name}?\n\nThis only works when the flock has no birds. Remove or transfer all birds first. If this flock has a Stripe subscription, FlockPal will cancel it before deleting the flock.\n\nYou will be switched to another flock or a new empty flock automatically.`,
);
if (!confirmed) {
@@ -4247,10 +4265,6 @@ function App() {
</section>
<div className="detail-grid">
<article className="detail-card">
<span>Name</span>
<strong>{selectedBird.name}</strong>
</article>
<article className="detail-card">
<span>Band ID</span>
<strong>{selectedBird.tagId || 'Not recorded'}</strong>
@@ -4267,6 +4281,18 @@ function App() {
<span>Species</span>
<strong>{selectedBird.species}</strong>
</article>
<article className="detail-card">
<span>Favorite snack</span>
<strong>{selectedBird.favoriteSnack || 'Not recorded'}</strong>
</article>
<article className="detail-card">
<span>Motivators</span>
<strong>{selectedBird.motivators || 'Not recorded'}</strong>
</article>
<article className="detail-card">
<span>Demotivates</span>
<strong>{selectedBird.demotivators || 'Not recorded'}</strong>
</article>
<article className="detail-card">
<span>Gender</span>
<strong className="detail-gender">
@@ -4559,11 +4585,35 @@ function App() {
<p className="eyebrow">Flock</p>
<h2>Flock profile</h2>
</div>
<button
className="secondary-button"
onClick={() => setExpandedSettingsSection((current) => (current === 'new-workspace' ? null : 'new-workspace'))}
type="button"
aria-expanded={expandedSettingsSection === 'new-workspace'}
>
{expandedSettingsSection === 'new-workspace' ? 'Close' : 'New flock'}
</button>
</div>
<p className="muted">
Manage this flock's name and type. Household billing details live in the Billing info card below.
Choose the flock to manage here. Household billing details live in the Billing info card below.
</p>
<form className="form-panel" onSubmit={handleWorkspaceSubmit}>
{authSession.workspaces.length > 1 ? (
<label>
Flock
<select
value={workspace?.id ?? ''}
onChange={(event) => handleWorkspaceSwitch(Number(event.target.value), 'settings')}
disabled={switchingWorkspaceId !== null || savingWorkspace || deletingWorkspace}
>
{authSession.workspaces.map((entry) => (
<option key={entry.workspace.id} value={entry.workspace.id}>
{entry.workspace.name}
</option>
))}
</select>
</label>
) : null}
<label>
Flock name
<input value={workspaceForm.name} onChange={(event) => setWorkspaceForm({ ...workspaceForm, name: event.target.value })} required />
@@ -4688,6 +4738,175 @@ function App() {
<small className="muted">Delete is only available when this flock has no birds. Collaborators and tokens are removed with it.</small>
) : null}
</form>
{expandedSettingsSection === 'new-workspace' ? (
<section className="settings-subsection">
<div className="settings-nested-header">
<p className="eyebrow">New flock</p>
<h3>Add an additional flock</h3>
</div>
<p className="muted">
Use this only when you need a separate flock. To turn the current household flock into a rescue, update the current flock type above.
</p>
<form className="form-panel" onSubmit={handleCreateWorkspace}>
<label>
Flock name
<input
value={workspaceCreateForm.name}
onChange={(event) => setWorkspaceCreateForm({ ...workspaceCreateForm, name: event.target.value })}
required
/>
</label>
<label>
Flock type
<select
value={workspaceCreateForm.workspaceType}
onChange={(event) => {
const workspaceType = event.target.value as WorkspaceCreateFormState['workspaceType'];
setWorkspaceCreateForm({
...workspaceCreateForm,
workspaceType,
rescueOnboarding:
workspaceType === 'rescue'
? {
...workspaceCreateForm.rescueOnboarding,
name: workspaceCreateForm.rescueOnboarding.name || workspaceCreateForm.name,
}
: workspaceCreateForm.rescueOnboarding,
});
}}
>
<option value="standard">Standard household</option>
<option value="rescue">Rescue</option>
</select>
</label>
{workspaceCreateForm.workspaceType === 'standard' ? (
<>
<label>
Household plan
<select
value={workspaceCreateForm.billingPlan}
onChange={(event) =>
setWorkspaceCreateForm({
...workspaceCreateForm,
billingPlan: event.target.value as WorkspaceCreateFormState['billingPlan'],
})
}
>
<option value="household_basic">Conure (4 birds)</option>
<option value="household_plus">Indian Ringneck (10 birds)</option>
<option value="household_macaw">Macaw (11+)</option>
</select>
</label>
<label>
Billing frequency
<select
value={workspaceCreateForm.billingInterval}
onChange={(event) =>
setWorkspaceCreateForm({
...workspaceCreateForm,
billingInterval: event.target.value as WorkspaceCreateFormState['billingInterval'],
})
}
>
<option value="monthly">{formatBillingIntervalDropdownLabel(workspaceCreateForm.billingPlan, 'monthly')}</option>
<option value="yearly">{formatBillingIntervalDropdownLabel(workspaceCreateForm.billingPlan, 'yearly')}</option>
</select>
</label>
<article className="summary-card">
<strong>
{formatBillingPlanName(workspaceCreateForm.billingPlan)} •{' '}
{formatBillingIntervalName(workspaceCreateForm.billingInterval)}
</strong>
<span>{formatBillingPlanCapacity(workspaceCreateForm.billingPlan)}</span>
</article>
</>
) : (
<>
<section className="settings-nested-card">
<h3>Rescue onboarding</h3>
<label>
Rescue Name
<input
value={workspaceCreateForm.rescueOnboarding.name}
onChange={(event) =>
setWorkspaceCreateForm({
...workspaceCreateForm,
rescueOnboarding: { ...workspaceCreateForm.rescueOnboarding, name: event.target.value },
})
}
/>
</label>
<label>
City
<input
value={workspaceCreateForm.rescueOnboarding.city}
onChange={(event) =>
setWorkspaceCreateForm({
...workspaceCreateForm,
rescueOnboarding: { ...workspaceCreateForm.rescueOnboarding, city: event.target.value },
})
}
/>
</label>
<label>
State
<input
value={workspaceCreateForm.rescueOnboarding.state}
onChange={(event) =>
setWorkspaceCreateForm({
...workspaceCreateForm,
rescueOnboarding: { ...workspaceCreateForm.rescueOnboarding, state: event.target.value },
})
}
/>
</label>
<label>
EIN
<input
value={workspaceCreateForm.rescueOnboarding.ein}
onChange={(event) =>
setWorkspaceCreateForm({
...workspaceCreateForm,
rescueOnboarding: { ...workspaceCreateForm.rescueOnboarding, ein: event.target.value },
})
}
/>
</label>
<label>
Website
<input
type="url"
value={workspaceCreateForm.rescueOnboarding.website}
onChange={(event) =>
setWorkspaceCreateForm({
...workspaceCreateForm,
rescueOnboarding: { ...workspaceCreateForm.rescueOnboarding, website: event.target.value },
})
}
/>
</label>
</section>
<article className="summary-card">
<strong>{formatBillingPlanName('rescue_free')}</strong>
<span>No billing is applied to rescue flocks.</span>
</article>
</>
)}
<label>
Billing contact email
<input
type="email"
value={workspaceCreateForm.billingEmail}
onChange={(event) => setWorkspaceCreateForm({ ...workspaceCreateForm, billingEmail: event.target.value })}
placeholder="Used for household billing and account notices"
/>
</label>
<button className="primary-button" type="submit" disabled={creatingWorkspace}>
{creatingWorkspace ? 'Creating flock...' : 'Create flock'}
</button>
</form>
</section>
) : null}
</article>
<article className="panel form-panel settings-card-billing">
@@ -5021,191 +5240,6 @@ function App() {
) : null}
</article>
<article className="panel form-panel settings-card-separate-flock">
<div className="panel-header">
<div>
<p className="eyebrow">Separate flock</p>
<h2>Add an additional flock</h2>
</div>
<button
className="secondary-button"
onClick={() =>
setExpandedSettingsSection((current) => (current === 'new-workspace' ? null : 'new-workspace'))
}
type="button"
aria-expanded={expandedSettingsSection === 'new-workspace'}
>
{expandedSettingsSection === 'new-workspace' ? 'Close' : 'Open'}
</button>
</div>
{expandedSettingsSection === 'new-workspace' ? (
<>
<p className="muted">
Use this only when you need a separate flock. To turn the current household flock into a rescue, use Flock profile and
billing above instead.
</p>
<form className="form-panel" onSubmit={handleCreateWorkspace}>
<label>
Flock name
<input
value={workspaceCreateForm.name}
onChange={(event) => setWorkspaceCreateForm({ ...workspaceCreateForm, name: event.target.value })}
required
/>
</label>
<label>
Flock type
<select
value={workspaceCreateForm.workspaceType}
onChange={(event) => {
const workspaceType = event.target.value as WorkspaceCreateFormState['workspaceType'];
setWorkspaceCreateForm({
...workspaceCreateForm,
workspaceType,
rescueOnboarding:
workspaceType === 'rescue'
? {
...workspaceCreateForm.rescueOnboarding,
name: workspaceCreateForm.rescueOnboarding.name || workspaceCreateForm.name,
}
: workspaceCreateForm.rescueOnboarding,
});
}}
>
<option value="standard">Standard household</option>
<option value="rescue">Rescue</option>
</select>
</label>
{workspaceCreateForm.workspaceType === 'standard' ? (
<>
<label>
Household plan
<select
value={workspaceCreateForm.billingPlan}
onChange={(event) =>
setWorkspaceCreateForm({
...workspaceCreateForm,
billingPlan: event.target.value as WorkspaceCreateFormState['billingPlan'],
})
}
>
<option value="household_basic">Conure (4 birds)</option>
<option value="household_plus">Indian Ringneck (10 birds)</option>
<option value="household_macaw">Macaw (11+)</option>
</select>
</label>
<label>
Billing frequency
<select
value={workspaceCreateForm.billingInterval}
onChange={(event) =>
setWorkspaceCreateForm({
...workspaceCreateForm,
billingInterval: event.target.value as WorkspaceCreateFormState['billingInterval'],
})
}
>
<option value="monthly">{formatBillingIntervalDropdownLabel(workspaceCreateForm.billingPlan, 'monthly')}</option>
<option value="yearly">{formatBillingIntervalDropdownLabel(workspaceCreateForm.billingPlan, 'yearly')}</option>
</select>
</label>
<article className="summary-card">
<strong>
{formatBillingPlanName(workspaceCreateForm.billingPlan)} {' '}
{formatBillingIntervalName(workspaceCreateForm.billingInterval)}
</strong>
<span>{formatBillingPlanCapacity(workspaceCreateForm.billingPlan)}</span>
</article>
</>
) : (
<>
<section className="settings-nested-card">
<h3>Rescue onboarding</h3>
<label>
Rescue Name
<input
value={workspaceCreateForm.rescueOnboarding.name}
onChange={(event) =>
setWorkspaceCreateForm({
...workspaceCreateForm,
rescueOnboarding: { ...workspaceCreateForm.rescueOnboarding, name: event.target.value },
})
}
/>
</label>
<label>
City
<input
value={workspaceCreateForm.rescueOnboarding.city}
onChange={(event) =>
setWorkspaceCreateForm({
...workspaceCreateForm,
rescueOnboarding: { ...workspaceCreateForm.rescueOnboarding, city: event.target.value },
})
}
/>
</label>
<label>
State
<input
value={workspaceCreateForm.rescueOnboarding.state}
onChange={(event) =>
setWorkspaceCreateForm({
...workspaceCreateForm,
rescueOnboarding: { ...workspaceCreateForm.rescueOnboarding, state: event.target.value },
})
}
/>
</label>
<label>
EIN
<input
value={workspaceCreateForm.rescueOnboarding.ein}
onChange={(event) =>
setWorkspaceCreateForm({
...workspaceCreateForm,
rescueOnboarding: { ...workspaceCreateForm.rescueOnboarding, ein: event.target.value },
})
}
/>
</label>
<label>
Website
<input
type="url"
value={workspaceCreateForm.rescueOnboarding.website}
onChange={(event) =>
setWorkspaceCreateForm({
...workspaceCreateForm,
rescueOnboarding: { ...workspaceCreateForm.rescueOnboarding, website: event.target.value },
})
}
/>
</label>
</section>
<article className="summary-card">
<strong>{formatBillingPlanName('rescue_free')}</strong>
<span>No billing is applied to rescue flocks.</span>
</article>
</>
)}
<label>
Billing contact email
<input
type="email"
value={workspaceCreateForm.billingEmail}
onChange={(event) => setWorkspaceCreateForm({ ...workspaceCreateForm, billingEmail: event.target.value })}
placeholder="Used for household billing and account notices"
/>
</label>
<button className="primary-button" type="submit" disabled={creatingWorkspace}>
{creatingWorkspace ? 'Creating flock...' : 'Create flock'}
</button>
</form>
</>
) : null}
</article>
<article className="panel form-panel settings-card-bird-profiles">
<div className="panel-header">
<div>
@@ -5312,6 +5346,30 @@ function App() {
</div>
<small className="muted">Search or select a species so alerts and chart references stay consistent.</small>
</label>
<label>
Favorite snack
<input
value={birdForm.favoriteSnack}
onChange={(event) => setBirdForm({ ...birdForm, favoriteSnack: event.target.value })}
placeholder="Optional"
/>
</label>
<label className="wide-field">
Motivators
<textarea
value={birdForm.motivators}
onChange={(event) => setBirdForm({ ...birdForm, motivators: event.target.value })}
placeholder="Training rewards, sounds, people, toys, routines"
/>
</label>
<label className="wide-field">
Demotivates
<textarea
value={birdForm.demotivators}
onChange={(event) => setBirdForm({ ...birdForm, demotivators: event.target.value })}
placeholder="Stressors, disliked handling, noises, situations"
/>
</label>
<div className="segmented-field wide-field">
<span>Gender</span>
<div className="segmented-control" role="radiogroup" aria-label="Bird gender">
-4
View File
@@ -513,10 +513,6 @@ textarea {
order: 2;
}
.settings-card-separate-flock {
order: 3;
}
.settings-card-automation {
order: 4;
}