Add country mapping support for imports and geographic map
- Add normalizeCountryToCode utility to convert country names to ISO-2 codes - Support English, French and common alternate spellings - Update Typeform import to support country field mapping - Update Notion import to support country field mapping - Allow project.update to set/update country with automatic normalization - Fix geographic distribution map showing empty when country data exists Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
getNotionDatabaseSchema,
|
||||
queryNotionDatabase,
|
||||
} from '@/lib/notion'
|
||||
import { normalizeCountryToCode } from '@/lib/countries'
|
||||
|
||||
export const notionImportRouter = router({
|
||||
/**
|
||||
@@ -91,6 +92,7 @@ export const notionImportRouter = router({
|
||||
teamName: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
tags: z.string().optional(), // Multi-select property
|
||||
country: z.string().optional(), // Country name or ISO code
|
||||
}),
|
||||
// Store unmapped columns in metadataJson
|
||||
includeUnmappedInMetadata: z.boolean().default(true),
|
||||
@@ -148,6 +150,15 @@ export const notionImportRouter = router({
|
||||
}
|
||||
}
|
||||
|
||||
// Get country and normalize to ISO code
|
||||
let country: string | null = null
|
||||
if (input.mappings.country) {
|
||||
const countryValue = getPropertyValue(record.properties, input.mappings.country)
|
||||
if (typeof countryValue === 'string') {
|
||||
country = normalizeCountryToCode(countryValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Build metadata from unmapped columns
|
||||
let metadataJson: Record<string, unknown> | null = null
|
||||
if (input.includeUnmappedInMetadata) {
|
||||
@@ -156,6 +167,7 @@ export const notionImportRouter = router({
|
||||
input.mappings.teamName,
|
||||
input.mappings.description,
|
||||
input.mappings.tags,
|
||||
input.mappings.country,
|
||||
].filter(Boolean))
|
||||
|
||||
metadataJson = {}
|
||||
@@ -179,6 +191,7 @@ export const notionImportRouter = router({
|
||||
teamName: typeof teamName === 'string' ? teamName.trim() : null,
|
||||
description: typeof description === 'string' ? description : null,
|
||||
tags,
|
||||
country,
|
||||
metadataJson: metadataJson as Prisma.InputJsonValue ?? undefined,
|
||||
externalIdsJson: {
|
||||
notionPageId: record.id,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
notifyProjectTeam,
|
||||
NotificationTypes,
|
||||
} from '../services/in-app-notification'
|
||||
import { normalizeCountryToCode } from '@/lib/countries'
|
||||
|
||||
export const projectRouter = router({
|
||||
/**
|
||||
@@ -324,6 +325,7 @@ export const projectRouter = router({
|
||||
title: z.string().min(1).max(500).optional(),
|
||||
teamName: z.string().optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
country: z.string().optional().nullable(), // ISO-2 code or country name (will be normalized)
|
||||
// Status update requires roundId
|
||||
roundId: z.string().optional(),
|
||||
status: z
|
||||
@@ -341,13 +343,19 @@ export const projectRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id, metadataJson, status, roundId, ...data } = input
|
||||
const { id, metadataJson, status, roundId, country, ...data } = input
|
||||
|
||||
// Normalize country to ISO-2 code if provided
|
||||
const normalizedCountry = country !== undefined
|
||||
? (country === null ? null : normalizeCountryToCode(country))
|
||||
: undefined
|
||||
|
||||
const project = await ctx.prisma.project.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...data,
|
||||
...(status && { status }),
|
||||
...(normalizedCountry !== undefined && { country: normalizedCountry }),
|
||||
metadataJson: metadataJson as Prisma.InputJsonValue ?? undefined,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getAllTypeformResponses,
|
||||
responseToObject,
|
||||
} from '@/lib/typeform'
|
||||
import { normalizeCountryToCode } from '@/lib/countries'
|
||||
|
||||
export const typeformImportRouter = router({
|
||||
/**
|
||||
@@ -102,6 +103,7 @@ export const typeformImportRouter = router({
|
||||
description: z.string().optional(),
|
||||
tags: z.string().optional(), // Multi-select or text field
|
||||
email: z.string().optional(), // For tracking submission email
|
||||
country: z.string().optional(), // Country name or ISO code
|
||||
}),
|
||||
// Store unmapped columns in metadataJson
|
||||
includeUnmappedInMetadata: z.boolean().default(true),
|
||||
@@ -162,6 +164,15 @@ export const typeformImportRouter = router({
|
||||
}
|
||||
}
|
||||
|
||||
// Get country and normalize to ISO code
|
||||
let country: string | null = null
|
||||
if (input.mappings.country) {
|
||||
const countryValue = record[input.mappings.country]
|
||||
if (typeof countryValue === 'string') {
|
||||
country = normalizeCountryToCode(countryValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Build metadata from unmapped columns
|
||||
let metadataJson: Record<string, unknown> | null = null
|
||||
if (input.includeUnmappedInMetadata) {
|
||||
@@ -171,6 +182,7 @@ export const typeformImportRouter = router({
|
||||
input.mappings.description,
|
||||
input.mappings.tags,
|
||||
input.mappings.email,
|
||||
input.mappings.country,
|
||||
'_response_id',
|
||||
'_submitted_at',
|
||||
].filter(Boolean))
|
||||
@@ -207,6 +219,7 @@ export const typeformImportRouter = router({
|
||||
teamName: typeof teamName === 'string' ? teamName.trim() : null,
|
||||
description: typeof description === 'string' ? description : null,
|
||||
tags,
|
||||
country,
|
||||
metadataJson: metadataJson as Prisma.InputJsonValue ?? undefined,
|
||||
externalIdsJson: {
|
||||
typeformResponseId: response.response_id,
|
||||
|
||||
Reference in New Issue
Block a user