Added gender
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user