working on timeline locations
Deploy / deploy-dev (push) Successful in 2m21s
Deploy / deploy-prod (push) Has been skipped

This commit is contained in:
blaisadmin
2026-06-28 22:57:22 -04:00
parent a988d9662b
commit 9ddd85b5c4
8 changed files with 430 additions and 41 deletions
+78 -5
View File
@@ -286,6 +286,19 @@ const birdProfileListSchema = z
.optional()
.or(z.literal(''));
const verifiedLocationDetailsSchema = z
.object({
city: z.string().trim().max(120).optional().or(z.literal('')),
region: z.string().trim().max(120).optional().or(z.literal('')),
country: z.string().trim().max(120).optional().or(z.literal('')),
countryCode: z.string().trim().max(2).optional().or(z.literal('')),
latitude: z.coerce.number().min(-90).max(90).optional().nullable().or(z.literal('')),
longitude: z.coerce.number().min(-180).max(180).optional().nullable().or(z.literal('')),
precision: z.enum(['city', 'region', 'country']).optional(),
})
.optional()
.nullable();
const birdSchema = z.object({
name: z.string().trim().min(1).max(120),
tagId: z.string().trim().max(80).optional().or(z.literal('')),
@@ -294,6 +307,7 @@ const birdSchema = z.object({
demotivators: birdProfileListSchema,
favoriteSnack: z.string().trim().max(160).optional().or(z.literal('')),
locationLabel: z.string().trim().max(160).optional().or(z.literal('')),
locationDetails: verifiedLocationDetailsSchema,
vetClinicName: z.string().trim().max(160).optional().or(z.literal('')),
vetClinicAddress: z.string().trim().max(500).optional().or(z.literal('')),
vetAccountNumber: z.string().trim().max(120).optional().or(z.literal('')),
@@ -323,13 +337,56 @@ const birdTimelineEventSchema = z
eventType: z.enum(['location_updated', 'owner_changed', 'manual_note']),
eventDate: dateStringSchema.optional().or(z.literal('')),
locationLabel: z.string().trim().max(160).optional().or(z.literal('')),
locationDetails: verifiedLocationDetailsSchema,
note: z.string().trim().max(500).optional().or(z.literal('')),
})
.refine(
(value) => value.eventType === 'owner_changed' || Boolean(value.locationLabel?.trim() || value.note?.trim()),
(value) =>
value.eventType === 'owner_changed' ||
Boolean(
value.locationLabel?.trim() ||
value.note?.trim() ||
value.locationDetails?.city?.trim() ||
value.locationDetails?.region?.trim() ||
value.locationDetails?.country?.trim(),
),
'Add a location or note for this timeline item.',
);
type VerifiedLocationDetailsInput = z.infer<typeof verifiedLocationDetailsSchema>;
const normalizeVerifiedLocationDetails = (value: VerifiedLocationDetailsInput) => {
if (!value) {
return null;
}
const city = value.city?.trim() || null;
const region = value.region?.trim() || null;
const country = value.country?.trim() || null;
const countryCode = value.countryCode?.trim().toUpperCase() || null;
const latitude = typeof value.latitude === 'number' ? Number(value.latitude.toFixed(4)) : null;
const longitude = typeof value.longitude === 'number' ? Number(value.longitude.toFixed(4)) : null;
const precision = value.precision ?? (city ? 'city' : region ? 'region' : country ? 'country' : null);
if (!city && !region && !country && !countryCode && latitude === null && longitude === null) {
return null;
}
return {
city,
region,
country,
countryCode,
latitude,
longitude,
precision,
verifiedAt: new Date().toISOString(),
};
};
const formatVerifiedLocationLabel = (details: ReturnType<typeof normalizeVerifiedLocationDetails>) =>
details ? [details.city, details.region, details.country].filter(Boolean).join(', ') || null : null;
const weightSchema = z.object({
weightGrams: z.coerce.number().positive().max(10000),
recordedOn: dateStringSchema,
@@ -707,6 +764,7 @@ const normalizeBird = (row: BirdRow) => ({
demotivators: row.demotivators,
favoriteSnack: row.favorite_snack,
locationLabel: row.location_label,
locationDetails: row.location_details,
vetClinicName: row.vet_clinic_name,
vetClinicAddress: row.vet_clinic_address,
vetAccountNumber: row.vet_account_number,
@@ -825,6 +883,7 @@ const normalizeBirdTimelineEvent = (row: BirdTimelineEventRow) => ({
fromOwnerEmail: row.from_owner_email,
toOwnerEmail: row.to_owner_email,
locationLabel: row.location_label,
locationDetails: row.location_details,
note: row.note,
eventDate: row.event_date,
createdByUserId: row.created_by_user_id,
@@ -2367,6 +2426,7 @@ const writeBirdTimelineEvent = async ({
fromWorkspaceId,
toWorkspaceId,
locationLabel,
locationDetails,
note,
eventDate,
createdByUserId,
@@ -2376,6 +2436,7 @@ const writeBirdTimelineEvent = async ({
fromWorkspaceId?: number | null;
toWorkspaceId?: number | null;
locationLabel?: string | null;
locationDetails?: Record<string, unknown> | null;
note?: string | null;
eventDate?: string | null;
createdByUserId?: string | null;
@@ -2387,6 +2448,7 @@ const writeBirdTimelineEvent = async ({
fromWorkspaceId,
toWorkspaceId,
locationLabel,
locationDetails,
note,
eventDate,
createdByUserId,
@@ -3644,11 +3706,13 @@ app.post(
return;
}
const locationDetails = normalizeVerifiedLocationDetails(parsed.data.locationDetails);
const event = await createBirdTimelineEvent({
birdId: bird.id,
eventType: parsed.data.eventType as BirdTimelineEventType,
toWorkspaceId: req.auth!.workspace.id,
locationLabel: emptyToNull(parsed.data.locationLabel),
locationLabel: formatVerifiedLocationLabel(locationDetails) ?? emptyToNull(parsed.data.locationLabel),
locationDetails,
note: emptyToNull(parsed.data.note),
eventDate: emptyToNull(parsed.data.eventDate),
createdByUserId: req.auth!.user.id,
@@ -3742,6 +3806,7 @@ app.post('/api/birds', requireAuth, requireWriteAccess, requireWorkspaceRole(['o
workspaceId: req.auth!.workspace.id,
photoDataUrl: emptyToNull(parsed.data.photoDataUrl),
});
const locationDetails = normalizeVerifiedLocationDetails(parsed.data.locationDetails);
uploadedObjectKeyToCleanup = photoStorage.photoObjectKey;
const bird = await createBird({
birdId,
@@ -3752,7 +3817,8 @@ app.post('/api/birds', requireAuth, requireWriteAccess, requireWorkspaceRole(['o
motivators: emptyToNull(parsed.data.motivators),
demotivators: emptyToNull(parsed.data.demotivators),
favoriteSnack: emptyToNull(parsed.data.favoriteSnack),
locationLabel: emptyToNull(parsed.data.locationLabel),
locationLabel: formatVerifiedLocationLabel(locationDetails) ?? emptyToNull(parsed.data.locationLabel),
locationDetails,
vetClinicName: emptyToNull(parsed.data.vetClinicName),
vetClinicAddress: emptyToNull(parsed.data.vetClinicAddress),
vetAccountNumber: emptyToNull(parsed.data.vetAccountNumber),
@@ -3781,6 +3847,7 @@ app.post('/api/birds', requireAuth, requireWriteAccess, requireWorkspaceRole(['o
eventType: 'profile_created',
toWorkspaceId: req.auth!.workspace.id,
locationLabel: bird!.location_label,
locationDetails: bird!.location_details,
createdByUserId: req.auth!.user.id,
});
res.status(201).json({ bird: normalizeBird(bird!) });
@@ -4109,6 +4176,7 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
});
uploadedObjectKeyToCleanup =
photoStorage.photoObjectKey && photoStorage.photoObjectKey !== existingBird.photo_object_key ? photoStorage.photoObjectKey : null;
const locationDetails = normalizeVerifiedLocationDetails(parsed.data.locationDetails);
const bird = await updateBird({
birdId: req.params.birdId,
workspaceId: req.auth!.workspace.id,
@@ -4118,7 +4186,8 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
motivators: emptyToNull(parsed.data.motivators),
demotivators: emptyToNull(parsed.data.demotivators),
favoriteSnack: emptyToNull(parsed.data.favoriteSnack),
locationLabel: emptyToNull(parsed.data.locationLabel),
locationLabel: formatVerifiedLocationLabel(locationDetails) ?? emptyToNull(parsed.data.locationLabel),
locationDetails,
vetClinicName: emptyToNull(parsed.data.vetClinicName),
vetClinicAddress: emptyToNull(parsed.data.vetClinicAddress),
vetAccountNumber: emptyToNull(parsed.data.vetAccountNumber),
@@ -4148,12 +4217,16 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
previousName: existingBird.name,
species: bird.species,
});
if ((existingBird.location_label ?? '') !== (bird.location_label ?? '')) {
if (
(existingBird.location_label ?? '') !== (bird.location_label ?? '') ||
JSON.stringify(existingBird.location_details ?? null) !== JSON.stringify(bird.location_details ?? null)
) {
await writeBirdTimelineEvent({
birdId: bird.id,
eventType: 'location_updated',
toWorkspaceId: req.auth!.workspace.id,
locationLabel: bird.location_label,
locationDetails: bird.location_details,
createdByUserId: req.auth!.user.id,
});
}