Added gender

This commit is contained in:
blaisadmin
2026-04-14 23:34:15 -04:00
parent 40900a0968
commit 43c32a5efc
8 changed files with 237 additions and 16 deletions
+6
View File
@@ -54,6 +54,7 @@ import {
import type {
AuthContext,
BillingPlan,
BirdGender,
BirdRow,
IntegrationTokenRow,
ProviderKey,
@@ -116,6 +117,7 @@ const workspaceTypeSchema = z.enum(['standard', 'rescue']);
const workspaceRoleSchema = z.enum(['owner', 'manager', 'staff', 'viewer']);
const billingPlanSchema = z.enum(['household_basic', 'household_plus', 'household_macaw']);
const integrationTokenScopeSchema = z.enum(['read_only', 'read_write']);
const birdGenderSchema = z.enum(['unknown', 'male', 'female']);
const workspaceSchema = z.object({
name: z.string().trim().min(1).max(160),
@@ -147,6 +149,7 @@ const birdSchema = z.object({
name: z.string().trim().min(1).max(120),
tagId: z.string().trim().min(1).max(80),
species: z.string().trim().min(1).max(120),
gender: birdGenderSchema.optional(),
dateOfBirth: dateStringSchema.optional().or(z.literal('')),
gotchaDay: dateStringSchema.optional().or(z.literal('')),
chartColor: chartColorSchema.optional(),
@@ -263,6 +266,7 @@ const normalizeBird = (row: BirdRow) => ({
name: row.name,
tagId: row.tag_id,
species: row.species,
gender: row.gender,
dateOfBirth: row.date_of_birth,
gotchaDay: row.gotcha_day,
chartColor: row.chart_color,
@@ -1076,6 +1080,7 @@ app.post('/api/birds', requireAuth, requireWriteAccess, requireWorkspaceRole(['o
name: parsed.data.name,
tagId: parsed.data.tagId,
species: parsed.data.species,
gender: (parsed.data.gender ?? 'unknown') as BirdGender,
dateOfBirth: emptyToNull(parsed.data.dateOfBirth),
gotchaDay: emptyToNull(parsed.data.gotchaDay),
chartColor: parsed.data.chartColor ?? '#cb3a35',
@@ -1110,6 +1115,7 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
name: parsed.data.name,
tagId: parsed.data.tagId,
species: parsed.data.species,
gender: (parsed.data.gender ?? 'unknown') as BirdGender,
dateOfBirth: emptyToNull(parsed.data.dateOfBirth),
gotchaDay: emptyToNull(parsed.data.gotchaDay),
chartColor: parsed.data.chartColor ?? '#cb3a35',
+20
View File
@@ -164,6 +164,7 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
name VARCHAR(120) NOT NULL,
tag_id VARCHAR(80) NOT NULL,
species VARCHAR(120) NOT NULL,
gender VARCHAR(16) NOT NULL DEFAULT 'unknown',
date_of_birth DATE,
gotcha_day DATE,
chart_color VARCHAR(7) NOT NULL DEFAULT '#cb3a35',
@@ -175,6 +176,7 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
ALTER TABLE birds
ADD COLUMN IF NOT EXISTS workspace_id INTEGER NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS gender VARCHAR(16) NOT NULL DEFAULT 'unknown',
ADD COLUMN IF NOT EXISTS date_of_birth DATE,
ADD COLUMN IF NOT EXISTS gotcha_day DATE,
ADD COLUMN IF NOT EXISTS chart_color VARCHAR(7) NOT NULL DEFAULT '#cb3a35',
@@ -222,5 +224,23 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
CREATE INDEX IF NOT EXISTS idx_vet_visits_bird_visited_on
ON vet_visits (bird_id, visited_on DESC);
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'birds'
AND column_name = 'is_female'
) THEN
UPDATE birds
SET gender = CASE
WHEN is_female IS TRUE THEN 'female'
WHEN is_female IS FALSE THEN 'male'
ELSE gender
END
WHERE gender = 'unknown';
END IF;
END $$;
`);
};
@@ -24,6 +24,7 @@ test('createBird returns the inserted bird row', async () => {
name: 'Kiwi',
tag_id: 'A-1',
species: 'Cockatiel',
gender: 'female',
date_of_birth: null,
gotcha_day: null,
chart_color: '#cb3a35',
@@ -42,6 +43,7 @@ test('createBird returns the inserted bird row', async () => {
name: 'Kiwi',
tagId: 'A-1',
species: 'Cockatiel',
gender: 'female',
dateOfBirth: null,
gotchaDay: null,
chartColor: '#cb3a35',
@@ -52,6 +54,7 @@ test('createBird returns the inserted bird row', async () => {
assert.equal(bird?.name, 'Kiwi');
assert.equal(bird?.workspace_id, 10);
assert.equal(bird?.gender, 'female');
});
test('listWeightsForBird scopes by bird, workspace, and day window', async () => {
+20 -14
View File
@@ -1,5 +1,5 @@
import { db } from '../db/client.js';
import type { BirdRow, VetVisitRow, WeightRow } from '../types.js';
import type { BirdGender, BirdRow, VetVisitRow, WeightRow } from '../types.js';
const birdSelectFields = `
birds.id,
@@ -7,6 +7,7 @@ const birdSelectFields = `
birds.name,
birds.tag_id,
birds.species,
birds.gender,
birds.date_of_birth::text,
birds.gotcha_day::text,
birds.chart_color,
@@ -63,6 +64,7 @@ export const createBird = async ({
name,
tagId,
species,
gender,
dateOfBirth,
gotchaDay,
chartColor,
@@ -74,6 +76,7 @@ export const createBird = async ({
name: string;
tagId: string;
species: string;
gender: BirdGender;
dateOfBirth: string | null;
gotchaDay: string | null;
chartColor: string;
@@ -82,10 +85,10 @@ export const createBird = async ({
notifyOnGotchaDay: boolean;
}) => {
const result = await db.query<BirdRow>(
`INSERT INTO birds (workspace_id, name, tag_id, species, date_of_birth, gotcha_day, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id, workspace_id, name, tag_id, species, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
[workspaceId, name, tagId, species, dateOfBirth, gotchaDay, chartColor, photoDataUrl, notifyOnDob, notifyOnGotchaDay],
`INSERT INTO birds (workspace_id, name, tag_id, species, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
[workspaceId, name, tagId, species, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, notifyOnDob, notifyOnGotchaDay],
);
return result.rows[0] ?? null;
@@ -97,6 +100,7 @@ export const updateBird = async ({
name,
tagId,
species,
gender,
dateOfBirth,
gotchaDay,
chartColor,
@@ -109,6 +113,7 @@ export const updateBird = async ({
name: string;
tagId: string;
species: string;
gender: BirdGender;
dateOfBirth: string | null;
gotchaDay: string | null;
chartColor: string;
@@ -121,15 +126,16 @@ export const updateBird = async ({
SET name = $2,
tag_id = $3,
species = $4,
date_of_birth = $5,
gotcha_day = $6,
chart_color = $7,
photo_data_url = $8,
notify_on_dob = $9,
notify_on_gotcha_day = $10
gender = $5,
date_of_birth = $6,
gotcha_day = $7,
chart_color = $8,
photo_data_url = $9,
notify_on_dob = $10,
notify_on_gotcha_day = $11
WHERE id = $1
AND workspace_id = $11
RETURNING id, workspace_id, name, tag_id, species, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, created_at,
AND workspace_id = $12
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, created_at,
(
SELECT weight_grams::text
FROM weight_records
@@ -144,7 +150,7 @@ export const updateBird = async ({
ORDER BY recorded_on DESC
LIMIT 1
) AS latest_recorded_on`,
[birdId, name, tagId, species, dateOfBirth, gotchaDay, chartColor, photoDataUrl, notifyOnDob, notifyOnGotchaDay, workspaceId],
[birdId, name, tagId, species, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, notifyOnDob, notifyOnGotchaDay, workspaceId],
);
return result.rows[0] ?? null;
+2
View File
@@ -3,6 +3,7 @@ export type WorkspaceRole = 'owner' | 'manager' | 'staff' | 'viewer';
export type BillingPlan = 'rescue_free' | 'household_basic' | 'household_plus' | 'household_macaw';
export type ProviderKey = 'google' | 'microsoft' | 'apple';
export type IntegrationTokenScope = 'read_only' | 'read_write';
export type BirdGender = 'unknown' | 'male' | 'female';
export type UserRow = {
id: string;
@@ -89,6 +90,7 @@ export type BirdRow = {
name: string;
tag_id: string;
species: string;
gender: BirdGender;
date_of_birth: string | null;
gotcha_day: string | null;
chart_color: string;