Added timeline feature
This commit is contained in:
@@ -5,6 +5,8 @@ import type {
|
||||
BirdMilestoneReminderDeliveryRow,
|
||||
BirdMilestoneReminderType,
|
||||
BirdRow,
|
||||
BirdTimelineEventRow,
|
||||
BirdTimelineEventType,
|
||||
BirdTransferCodeRow,
|
||||
LostBirdMatchRow,
|
||||
MedicationAdministrationRow,
|
||||
@@ -26,6 +28,7 @@ const birdSelectFields = `
|
||||
birds.motivators,
|
||||
birds.demotivators,
|
||||
birds.favorite_snack,
|
||||
birds.location_label,
|
||||
birds.vet_clinic_name,
|
||||
birds.vet_clinic_address,
|
||||
birds.vet_account_number,
|
||||
@@ -51,6 +54,34 @@ const birdSelectFields = `
|
||||
latest.recorded_on::text AS latest_recorded_on
|
||||
`;
|
||||
|
||||
type WorkspaceTimelineSnapshot = {
|
||||
workspace_id: number;
|
||||
workspace_name: string;
|
||||
owner_email: string | null;
|
||||
};
|
||||
|
||||
const getWorkspaceTimelineSnapshot = async (workspaceId: number) => {
|
||||
const result = await db.query<WorkspaceTimelineSnapshot>(
|
||||
`SELECT
|
||||
workspaces.id AS workspace_id,
|
||||
workspaces.name AS workspace_name,
|
||||
COALESCE(workspaces.billing_email, owner_member.invite_email, owner_member.email) AS owner_email
|
||||
FROM workspaces
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT invite_email, email
|
||||
FROM workspace_members
|
||||
WHERE workspace_members.workspace_id = workspaces.id
|
||||
AND workspace_members.role = 'owner'
|
||||
ORDER BY accepted_at DESC NULLS LAST, created_at ASC
|
||||
LIMIT 1
|
||||
) owner_member ON TRUE
|
||||
WHERE workspaces.id = $1`,
|
||||
[workspaceId],
|
||||
);
|
||||
|
||||
return result.rows[0] ?? null;
|
||||
};
|
||||
|
||||
export const getBirdById = async (birdId: string, workspaceId: number) => {
|
||||
const result = await db.query<BirdRow>(
|
||||
`SELECT
|
||||
@@ -134,6 +165,84 @@ export const listMemorializedBirds = async (workspaceId: number) => {
|
||||
return result.rows;
|
||||
};
|
||||
|
||||
export const createBirdTimelineEvent = async ({
|
||||
birdId,
|
||||
eventType,
|
||||
fromWorkspaceId,
|
||||
toWorkspaceId,
|
||||
locationLabel,
|
||||
note,
|
||||
eventDate,
|
||||
createdByUserId,
|
||||
}: {
|
||||
birdId: string;
|
||||
eventType: BirdTimelineEventType;
|
||||
fromWorkspaceId?: number | null;
|
||||
toWorkspaceId?: number | null;
|
||||
locationLabel?: string | null;
|
||||
note?: string | null;
|
||||
eventDate?: string | null;
|
||||
createdByUserId?: string | null;
|
||||
}) => {
|
||||
const [fromWorkspace, toWorkspace] = await Promise.all([
|
||||
fromWorkspaceId ? getWorkspaceTimelineSnapshot(fromWorkspaceId) : Promise.resolve(null),
|
||||
toWorkspaceId ? getWorkspaceTimelineSnapshot(toWorkspaceId) : Promise.resolve(null),
|
||||
]);
|
||||
|
||||
const result = await db.query<BirdTimelineEventRow>(
|
||||
`INSERT INTO bird_timeline_events (
|
||||
bird_id,
|
||||
event_type,
|
||||
from_workspace_id,
|
||||
to_workspace_id,
|
||||
from_workspace_name,
|
||||
to_workspace_name,
|
||||
from_owner_email,
|
||||
to_owner_email,
|
||||
location_label,
|
||||
note,
|
||||
event_date,
|
||||
created_by_user_id
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, COALESCE($9, $6, $5), $10, COALESCE($11::date, CURRENT_DATE), $12)
|
||||
RETURNING id, bird_id, event_type, from_workspace_id, to_workspace_id, from_workspace_name, to_workspace_name, from_owner_email, to_owner_email, location_label, note, event_date::text, created_by_user_id, created_at`,
|
||||
[
|
||||
birdId,
|
||||
eventType,
|
||||
fromWorkspaceId ?? null,
|
||||
toWorkspaceId ?? null,
|
||||
fromWorkspace?.workspace_name ?? null,
|
||||
toWorkspace?.workspace_name ?? null,
|
||||
fromWorkspace?.owner_email ?? null,
|
||||
toWorkspace?.owner_email ?? null,
|
||||
locationLabel ?? null,
|
||||
note ?? null,
|
||||
eventDate ?? null,
|
||||
createdByUserId ?? null,
|
||||
],
|
||||
);
|
||||
|
||||
return result.rows[0] ?? null;
|
||||
};
|
||||
|
||||
export const listBirdTimelineEvents = async (birdId: string, workspaceId: number) => {
|
||||
const result = await db.query<BirdTimelineEventRow>(
|
||||
`SELECT id, bird_id, event_type, from_workspace_id, to_workspace_id, from_workspace_name, to_workspace_name, from_owner_email, to_owner_email, location_label, note, event_date::text, created_by_user_id, created_at
|
||||
FROM bird_timeline_events
|
||||
WHERE bird_id = $1
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM birds
|
||||
WHERE birds.id = bird_timeline_events.bird_id
|
||||
AND birds.workspace_id = $2
|
||||
)
|
||||
ORDER BY event_date DESC, created_at DESC`,
|
||||
[birdId, workspaceId],
|
||||
);
|
||||
|
||||
return result.rows;
|
||||
};
|
||||
|
||||
export const findBirdsByBandId = async (tagId: string) => {
|
||||
const result = await db.query<LostBirdMatchRow>(
|
||||
`SELECT
|
||||
@@ -367,6 +476,7 @@ export const createBird = async ({
|
||||
motivators,
|
||||
demotivators,
|
||||
favoriteSnack,
|
||||
locationLabel = null,
|
||||
vetClinicName = null,
|
||||
vetClinicAddress = null,
|
||||
vetAccountNumber = null,
|
||||
@@ -392,6 +502,7 @@ export const createBird = async ({
|
||||
motivators: string | null;
|
||||
demotivators: string | null;
|
||||
favoriteSnack: string | null;
|
||||
locationLabel?: string | null;
|
||||
vetClinicName?: string | null;
|
||||
vetClinicAddress?: string | null;
|
||||
vetAccountNumber?: string | null;
|
||||
@@ -410,9 +521,9 @@ export const createBird = async ({
|
||||
publicProfileEnabled?: boolean;
|
||||
}) => {
|
||||
const result = await db.query<BirdRow>(
|
||||
`INSERT INTO birds (id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled)
|
||||
VALUES (COALESCE($1::uuid, gen_random_uuid()), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)
|
||||
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
|
||||
`INSERT INTO birds (id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, location_label, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled)
|
||||
VALUES (COALESCE($1::uuid, gen_random_uuid()), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)
|
||||
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, location_label, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
|
||||
[
|
||||
birdId ?? null,
|
||||
workspaceId,
|
||||
@@ -422,6 +533,7 @@ export const createBird = async ({
|
||||
motivators,
|
||||
demotivators,
|
||||
favoriteSnack,
|
||||
locationLabel,
|
||||
vetClinicName,
|
||||
vetClinicAddress,
|
||||
vetAccountNumber,
|
||||
@@ -453,6 +565,7 @@ export const updateBird = async ({
|
||||
motivators,
|
||||
demotivators,
|
||||
favoriteSnack,
|
||||
locationLabel,
|
||||
vetClinicName,
|
||||
vetClinicAddress,
|
||||
vetAccountNumber,
|
||||
@@ -478,6 +591,7 @@ export const updateBird = async ({
|
||||
motivators: string | null;
|
||||
demotivators: string | null;
|
||||
favoriteSnack: string | null;
|
||||
locationLabel: string | null;
|
||||
vetClinicName: string | null;
|
||||
vetClinicAddress: string | null;
|
||||
vetAccountNumber: string | null;
|
||||
@@ -503,26 +617,27 @@ export const updateBird = async ({
|
||||
motivators = $5,
|
||||
demotivators = $6,
|
||||
favorite_snack = $7,
|
||||
vet_clinic_name = $8,
|
||||
vet_clinic_address = $9,
|
||||
vet_account_number = $10,
|
||||
vet_doctor_name = $11,
|
||||
gender = $12,
|
||||
date_of_birth = $13,
|
||||
gotcha_day = $14,
|
||||
chart_color = $15,
|
||||
photo_data_url = $16,
|
||||
photo_object_key = $17,
|
||||
photo_content_type = $18,
|
||||
photo_updated_at = $19,
|
||||
notify_on_dob = $20,
|
||||
notify_on_gotcha_day = $21,
|
||||
public_profile_code = $22,
|
||||
public_profile_enabled = $23
|
||||
location_label = $8,
|
||||
vet_clinic_name = $9,
|
||||
vet_clinic_address = $10,
|
||||
vet_account_number = $11,
|
||||
vet_doctor_name = $12,
|
||||
gender = $13,
|
||||
date_of_birth = $14,
|
||||
gotcha_day = $15,
|
||||
chart_color = $16,
|
||||
photo_data_url = $17,
|
||||
photo_object_key = $18,
|
||||
photo_content_type = $19,
|
||||
photo_updated_at = $20,
|
||||
notify_on_dob = $21,
|
||||
notify_on_gotcha_day = $22,
|
||||
public_profile_code = $23,
|
||||
public_profile_enabled = $24
|
||||
WHERE id = $1
|
||||
AND workspace_id = $24
|
||||
AND workspace_id = $25
|
||||
AND memorialized_at IS NULL
|
||||
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
||||
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, location_label, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
||||
(
|
||||
SELECT weight_grams::text
|
||||
FROM weight_records
|
||||
@@ -545,6 +660,7 @@ export const updateBird = async ({
|
||||
motivators,
|
||||
demotivators,
|
||||
favoriteSnack,
|
||||
locationLabel,
|
||||
vetClinicName,
|
||||
vetClinicAddress,
|
||||
vetAccountNumber,
|
||||
@@ -590,7 +706,7 @@ export const memorializeBird = async ({
|
||||
WHERE id = $1
|
||||
AND workspace_id = $2
|
||||
AND memorialized_at IS NULL
|
||||
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
||||
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, location_label, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
||||
(
|
||||
SELECT weight_grams::text
|
||||
FROM weight_records
|
||||
@@ -626,7 +742,7 @@ export const updateMemorialReminderPreference = async ({
|
||||
WHERE id = $1
|
||||
AND workspace_id = $2
|
||||
AND memorialized_at IS NOT NULL
|
||||
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
||||
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, location_label, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
||||
(
|
||||
SELECT weight_grams::text
|
||||
FROM weight_records
|
||||
@@ -666,7 +782,7 @@ export const transferBirdToWorkspace = async (birdId: string, sourceWorkspaceId:
|
||||
WHERE id = $1
|
||||
AND workspace_id = $2
|
||||
AND memorialized_at IS NULL
|
||||
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
||||
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, location_label, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
||||
(
|
||||
SELECT weight_grams::text
|
||||
FROM weight_records
|
||||
@@ -762,6 +878,17 @@ export const completePendingBirdTransfersForOwner = async (ownerEmail: string, t
|
||||
}
|
||||
|
||||
await markPendingBirdTransferCompleted(transfer.id, targetWorkspaceId);
|
||||
try {
|
||||
await createBirdTimelineEvent({
|
||||
birdId: bird.id,
|
||||
eventType: 'transferred',
|
||||
fromWorkspaceId: transfer.source_workspace_id,
|
||||
toWorkspaceId: targetWorkspaceId,
|
||||
createdByUserId: transfer.requested_by_user_id,
|
||||
});
|
||||
} catch (timelineError) {
|
||||
console.error('Unable to write bird timeline event', timelineError);
|
||||
}
|
||||
completed += 1;
|
||||
} catch (error) {
|
||||
failed += 1;
|
||||
|
||||
Reference in New Issue
Block a user