Added memorial settings
This commit is contained in:
+154
-3
@@ -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'}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
|
||||
Reference in New Issue
Block a user