Added memorial settings

This commit is contained in:
Corey Blais
2026-04-22 10:42:43 -04:00
parent 36e074c1fd
commit 646f895ed6
8 changed files with 466 additions and 32 deletions
+154 -3
View File
@@ -26,6 +26,10 @@ type Bird = {
photoDataUrl: string | null;
notifyOnDob: boolean;
notifyOnGotchaDay: boolean;
memorializedAt: string | null;
memorializedOn: string | null;
memorialNote: string | null;
notifyOnMemorialDay: boolean;
createdAt: string;
latestWeightGrams: number | null;
latestRecordedOn: string | null;
@@ -182,6 +186,12 @@ type BirdFormState = {
notifyOnGotchaDay: boolean;
};
type MemorializeBirdFormState = {
memorializedOn: string;
memorialNote: string;
notifyOnMemorialDay: boolean;
};
type WorkspaceFormState = {
name: string;
workspaceType: WorkspaceType;
@@ -297,6 +307,12 @@ const emptyBirdForm: BirdFormState = {
notifyOnGotchaDay: false,
};
const emptyMemorializeBirdForm = (): MemorializeBirdFormState => ({
memorializedOn: new Date().toISOString().slice(0, 10),
memorialNote: '',
notifyOnMemorialDay: true,
});
const emptyWorkspaceForm: WorkspaceFormState = {
name: 'My Flock',
workspaceType: 'standard',
@@ -1068,6 +1084,7 @@ function App() {
const [adminSummary, setAdminSummary] = useState<AdminSummary | null>(null);
const [adminRescueWorkspaces, setAdminRescueWorkspaces] = useState<AdminRescueWorkspace[]>([]);
const [birds, setBirds] = useState<Bird[]>([]);
const [memorializedBirds, setMemorializedBirds] = useState<Bird[]>([]);
const [selectedBirdId, setSelectedBirdId] = useState<string>('');
const [editingBirdId, setEditingBirdId] = useState<string>('');
const [weights, setWeights] = useState<WeightRecord[]>([]);
@@ -1084,6 +1101,7 @@ function App() {
const [workspaceCreateForm, setWorkspaceCreateForm] = useState<WorkspaceCreateFormState>(emptyWorkspaceCreateForm);
const [integrationTokenForm, setIntegrationTokenForm] = useState<IntegrationTokenFormState>(emptyIntegrationTokenForm);
const [birdForm, setBirdForm] = useState<BirdFormState>(emptyBirdForm);
const [memorializeBirdForm, setMemorializeBirdForm] = useState<MemorializeBirdFormState>(emptyMemorializeBirdForm);
const [birdPhotoName, setBirdPhotoName] = useState('');
const [photoCrop, setPhotoCrop] = useState<PhotoCropState | null>(null);
const [photoDrag, setPhotoDrag] = useState<PhotoDragState | null>(null);
@@ -1138,6 +1156,7 @@ function App() {
previewUrl?: string | null;
} | null>(null);
const [deletingBird, setDeletingBird] = useState(false);
const [memorializingBird, setMemorializingBird] = useState(false);
const [editingVetVisitId, setEditingVetVisitId] = useState('');
const [deletingVetVisitId, setDeletingVetVisitId] = useState('');
const [editingMedicationId, setEditingMedicationId] = useState('');
@@ -1583,6 +1602,7 @@ function App() {
setAdminSummary(null);
setAdminRescueWorkspaces([]);
setBirds([]);
setMemorializedBirds([]);
setWeights([]);
setVetVisits([]);
setMedications([]);
@@ -1686,10 +1706,11 @@ function App() {
throw new Error(await readErrorMessage(birdsResponse, 'Unable to load flock members.'));
}
const data = (await readJsonSafely<{ birds?: Bird[] }>(birdsResponse)) ?? {};
const data = (await readJsonSafely<{ birds?: Bird[]; memorializedBirds?: Bird[] }>(birdsResponse)) ?? {};
const nextBirds = data.birds ?? [];
setBirds(nextBirds);
setMemorializedBirds(data.memorializedBirds ?? []);
setSelectedBirdId((current) => (current && nextBirds.some((bird) => bird.id === current) ? current : ''));
if (membersResponse.ok) {
@@ -2845,6 +2866,71 @@ function App() {
}
};
const handleMemorializeBird = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!selectedBird || memorializingBird) {
return;
}
const confirmed = window.confirm(
`Memorialize ${selectedBird.name}?\n\nThis cannot be undone by you. ${selectedBird.name} will become read-only, hidden from the standard flock view, and excluded from the subscription bird count.`,
);
if (!confirmed) {
return;
}
setMemorializingBird(true);
setError('');
try {
const response = await apiFetch(`/birds/${selectedBird.id}/memorialize`, authToken, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(memorializeBirdForm),
});
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Unable to memorialize bird.'));
}
const data = await readJsonSafely<{ bird: Bird }>(response);
if (!data?.bird) {
throw new Error('Unable to memorialize bird.');
}
const memorializedBird = data.bird;
setBirds((current) => current.filter((bird) => bird.id !== memorializedBird.id));
setMemorializedBirds((current) => sortBirdsByName([...current.filter((bird) => bird.id !== memorializedBird.id), memorializedBird]));
setSelectedBirdId('');
setEditingBirdId('');
setBirdForm(emptyBirdForm);
setBirdPhotoName('');
setPhotoCrop(null);
setPhotoDrag(null);
setMemorializeBirdForm(emptyMemorializeBirdForm());
setWeights([]);
setVetVisits([]);
setMedications([]);
setMedicationAdministrations([]);
setAllBirdWeights((current) => {
const next = { ...current };
delete next[memorializedBird.id];
return next;
});
setAllBirdVetVisits((current) => {
const next = { ...current };
delete next[memorializedBird.id];
return next;
});
} catch (memorializeError) {
setError(memorializeError instanceof Error ? memorializeError.message : 'Unable to memorialize bird.');
} finally {
setMemorializingBird(false);
}
};
const handleFlockTransferSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (transferringBird) {
@@ -4878,6 +4964,32 @@ function App() {
))}
</div>
{memorializedBirds.length ? (
<section className="settings-subsection settings-nested-card">
<div className="panel-header">
<div>
<p className="eyebrow">Memorials</p>
<h3>Memorialized birds</h3>
<p className="muted">
These profiles are read-only, hidden from the standard flock view, and excluded from household plan bird counts.
</p>
</div>
</div>
<div className="recent-list">
{memorializedBirds.map((bird) => (
<article className="vet-visit-card" key={bird.id}>
<strong>{bird.name}</strong>
<small>
{bird.species} Memorialized {formatDate(bird.memorializedOn)}
</small>
{bird.notifyOnMemorialDay ? <small>Memorial day reminders enabled.</small> : <small>Memorial day reminders off.</small>}
{bird.memorialNote ? <p className="muted">{bird.memorialNote}</p> : null}
</article>
))}
</div>
</section>
) : null}
<form className="form-panel settings-nested-stack" onSubmit={handleBirdSubmit}>
<section className="settings-nested-card">
<div className="settings-nested-header">
@@ -5269,12 +5381,51 @@ function App() {
<div className="panel-header">
<div>
<p className="eyebrow">Danger zone</p>
<h3>Remove bird profile</h3>
<h3>Destructive profile actions</h3>
<p className="muted">
Remove {selectedBird.name} from this flock. This also removes weight records, vet visits, and medication history for this bird.
Memorializing is not reversible by you. It makes {selectedBird.name} read-only, hides the profile from the standard flock view,
and removes the bird from subscription counting.
</p>
</div>
</div>
<form className="form-panel inline-form care-entry-form" onSubmit={handleMemorializeBird}>
<label>
Memorial date
<input
type="date"
value={memorializeBirdForm.memorializedOn}
onChange={(event) => setMemorializeBirdForm({ ...memorializeBirdForm, memorializedOn: event.target.value })}
required
/>
</label>
<label className="toggle-card">
<span>Send memorial day reminders</span>
<input
type="checkbox"
checked={memorializeBirdForm.notifyOnMemorialDay}
onChange={(event) => setMemorializeBirdForm({ ...memorializeBirdForm, notifyOnMemorialDay: event.target.checked })}
/>
<small className="muted">Send an annual remembrance notification using the same delivery workflow as Hatch Day reminders.</small>
</label>
<label className="wide-field">
Memorial note
<textarea
rows={3}
value={memorializeBirdForm.memorialNote}
onChange={(event) => setMemorializeBirdForm({ ...memorializeBirdForm, memorialNote: event.target.value })}
placeholder="Optional words to include in future memorial reminders."
/>
</label>
<div className="button-row care-form-actions">
<button className="danger-button" type="submit" disabled={memorializingBird || activeMembership?.role !== 'owner'}>
{memorializingBird ? 'Memorializing...' : `Memorialize ${selectedBird.name}`}
</button>
</div>
{activeMembership?.role !== 'owner' ? <p className="muted">Only flock owners can memorialize a bird profile.</p> : null}
</form>
<p className="muted">
Removing is separate from memorializing and permanently deletes weight records, vet visits, and medication history for this bird.
</p>
<div className="button-row">
<button className="danger-button" onClick={handleRemoveBird} type="button" disabled={deletingBird}>
{deletingBird ? 'Removing...' : 'Remove from flock'}
+1 -1
View File
@@ -10,7 +10,7 @@
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
+1 -1
View File
@@ -2,7 +2,7 @@
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]