Add coverage to data management tab
This commit is contained in:
@@ -383,18 +383,25 @@ export class ContactService {
|
|||||||
*/
|
*/
|
||||||
public static async getAvailableFields(
|
public static async getAvailableFields(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
): Promise<Array<{field: string; type: 'string' | 'number' | 'boolean' | 'date'}>> {
|
): Promise<Array<{field: string; type: 'string' | 'number' | 'boolean' | 'date'; coverage: number}>> {
|
||||||
// Standard fields with known types
|
// Get total contact count for coverage calculation
|
||||||
|
const totalContacts = await prisma.contact.count({
|
||||||
|
where: {projectId},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Standard fields with known types (always 100% coverage)
|
||||||
const standardFields = [
|
const standardFields = [
|
||||||
{field: 'email', type: 'string' as const},
|
{field: 'email', type: 'string' as const, coverage: 100},
|
||||||
{field: 'subscribed', type: 'boolean' as const},
|
{field: 'subscribed', type: 'boolean' as const, coverage: 100},
|
||||||
{field: 'createdAt', type: 'date' as const},
|
{field: 'createdAt', type: 'date' as const, coverage: 100},
|
||||||
{field: 'updatedAt', type: 'date' as const},
|
{field: 'updatedAt', type: 'date' as const, coverage: 100},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Get custom fields from the data JSON column with type inference
|
// Get custom fields from the data JSON column with type inference and coverage
|
||||||
// Use raw SQL to extract all keys and sample values from the JSON data column
|
// Use raw SQL to extract all keys, sample values, and contact counts from the JSON data column
|
||||||
const result = await prisma.$queryRaw<Array<{key: string; sample_value: string; json_type: string}>>`
|
const result = await prisma.$queryRaw<
|
||||||
|
Array<{key: string; sample_value: string; json_type: string; contact_count: bigint}>
|
||||||
|
>`
|
||||||
WITH field_keys AS (
|
WITH field_keys AS (
|
||||||
SELECT DISTINCT jsonb_object_keys(data) as key
|
SELECT DISTINCT jsonb_object_keys(data) as key
|
||||||
FROM contacts
|
FROM contacts
|
||||||
@@ -418,15 +425,27 @@ export class ContactService {
|
|||||||
AND data->fk.key IS NOT NULL
|
AND data->fk.key IS NOT NULL
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
) c
|
) c
|
||||||
|
),
|
||||||
|
field_counts AS (
|
||||||
|
SELECT
|
||||||
|
fk.key,
|
||||||
|
COUNT(*) as contact_count
|
||||||
|
FROM field_keys fk
|
||||||
|
JOIN contacts c ON c."projectId" = ${projectId}
|
||||||
|
AND c.data ? fk.key
|
||||||
|
AND c.data->fk.key IS NOT NULL
|
||||||
|
GROUP BY fk.key
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
key,
|
fs.key,
|
||||||
sample_value,
|
fs.sample_value,
|
||||||
json_type
|
fs.json_type,
|
||||||
FROM field_samples
|
fc.contact_count
|
||||||
|
FROM field_samples fs
|
||||||
|
JOIN field_counts fc ON fc.key = fs.key
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Infer types from JSON types and sample values
|
// Infer types from JSON types and sample values, calculate coverage
|
||||||
const customFields = result.map(row => {
|
const customFields = result.map(row => {
|
||||||
let type: 'string' | 'number' | 'boolean' | 'date' = 'string';
|
let type: 'string' | 'number' | 'boolean' | 'date' = 'string';
|
||||||
|
|
||||||
@@ -443,9 +462,14 @@ export class ContactService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate coverage percentage
|
||||||
|
const contactCount = Number(row.contact_count);
|
||||||
|
const coverage = totalContacts > 0 ? Math.round((contactCount / totalContacts) * 100) : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
field: `data.${row.key}`,
|
field: `data.${row.key}`,
|
||||||
type,
|
type,
|
||||||
|
coverage,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {network} from '../lib/network';
|
|||||||
interface FieldData {
|
interface FieldData {
|
||||||
field: string;
|
field: string;
|
||||||
type: 'string' | 'number' | 'boolean' | 'date';
|
type: 'string' | 'number' | 'boolean' | 'date';
|
||||||
|
coverage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FieldUsage {
|
interface FieldUsage {
|
||||||
@@ -158,6 +159,7 @@ export function DataManagementSettings() {
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Field Name</TableHead>
|
<TableHead>Field Name</TableHead>
|
||||||
<TableHead>Type</TableHead>
|
<TableHead>Type</TableHead>
|
||||||
|
<TableHead>Coverage</TableHead>
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -170,6 +172,9 @@ export function DataManagementSettings() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="secondary">{field.type}</Badge>
|
<Badge variant="secondary">{field.type}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-sm text-muted-foreground">{field.coverage}%</span>
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
Reference in New Issue
Block a user