Fixed delete workflow and added additional profile info
This commit is contained in:
+251
-193
@@ -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">
|
||||
|
||||
@@ -513,10 +513,6 @@ textarea {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.settings-card-separate-flock {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.settings-card-automation {
|
||||
order: 4;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user