@open-mercato/core 0.7.1-develop.7173.1.a901520d0f → 0.7.1-develop.7176.1.d229567a50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/phone_call/index.js +53 -0
- package/dist/generated/entities/phone_call/index.js.map +7 -0
- package/dist/generated/entities/phone_call_participant/index.js +27 -0
- package/dist/generated/entities/phone_call_participant/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +5 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +41 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +20 -0
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/entities/cli.js +46 -3
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/entities/migrations/Migration20260822120000.js +44 -0
- package/dist/modules/entities/migrations/Migration20260822120000.js.map +7 -0
- package/dist/modules/phone_calls/acl.js +15 -0
- package/dist/modules/phone_calls/acl.js.map +7 -0
- package/dist/modules/phone_calls/api/calls/route.js +132 -0
- package/dist/modules/phone_calls/api/calls/route.js.map +7 -0
- package/dist/modules/phone_calls/api/openapi.js +23 -0
- package/dist/modules/phone_calls/api/openapi.js.map +7 -0
- package/dist/modules/phone_calls/backend/page.js +261 -0
- package/dist/modules/phone_calls/backend/page.js.map +7 -0
- package/dist/modules/phone_calls/backend/page.meta.js +16 -0
- package/dist/modules/phone_calls/backend/page.meta.js.map +7 -0
- package/dist/modules/phone_calls/commands/calls.js +125 -0
- package/dist/modules/phone_calls/commands/calls.js.map +7 -0
- package/dist/modules/phone_calls/commands/index.js +2 -0
- package/dist/modules/phone_calls/commands/index.js.map +7 -0
- package/dist/modules/phone_calls/data/entities.js +162 -0
- package/dist/modules/phone_calls/data/entities.js.map +7 -0
- package/dist/modules/phone_calls/data/validators.js +68 -0
- package/dist/modules/phone_calls/data/validators.js.map +7 -0
- package/dist/modules/phone_calls/di.js +12 -0
- package/dist/modules/phone_calls/di.js.map +7 -0
- package/dist/modules/phone_calls/encryption.js +28 -0
- package/dist/modules/phone_calls/encryption.js.map +7 -0
- package/dist/modules/phone_calls/events.js +18 -0
- package/dist/modules/phone_calls/events.js.map +7 -0
- package/dist/modules/phone_calls/index.js +15 -0
- package/dist/modules/phone_calls/index.js.map +7 -0
- package/dist/modules/phone_calls/migrations/Migration20260706124120_phone_calls.js +22 -0
- package/dist/modules/phone_calls/migrations/Migration20260706124120_phone_calls.js.map +7 -0
- package/dist/modules/phone_calls/migrations/Migration20260814124333_phone_calls.js +13 -0
- package/dist/modules/phone_calls/migrations/Migration20260814124333_phone_calls.js.map +7 -0
- package/dist/modules/phone_calls/setup.js +16 -0
- package/dist/modules/phone_calls/setup.js.map +7 -0
- package/generated/entities/phone_call/index.ts +25 -0
- package/generated/entities/phone_call_participant/index.ts +12 -0
- package/generated/entities.ids.generated.ts +5 -0
- package/generated/entity-fields-registry.ts +41 -0
- package/package.json +7 -7
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/configs/lib/upgrade-actions.ts +20 -0
- package/src/modules/entities/cli.ts +61 -1
- package/src/modules/entities/migrations/Migration20260822120000.ts +56 -0
- package/src/modules/phone_calls/acl.ts +11 -0
- package/src/modules/phone_calls/api/calls/route.ts +150 -0
- package/src/modules/phone_calls/api/openapi.ts +25 -0
- package/src/modules/phone_calls/backend/page.meta.ts +12 -0
- package/src/modules/phone_calls/backend/page.tsx +303 -0
- package/src/modules/phone_calls/commands/calls.ts +156 -0
- package/src/modules/phone_calls/commands/index.ts +1 -0
- package/src/modules/phone_calls/data/entities.ts +144 -0
- package/src/modules/phone_calls/data/validators.ts +69 -0
- package/src/modules/phone_calls/di.ts +10 -0
- package/src/modules/phone_calls/encryption.ts +26 -0
- package/src/modules/phone_calls/events.ts +25 -0
- package/src/modules/phone_calls/i18n/de.json +42 -0
- package/src/modules/phone_calls/i18n/en.json +42 -0
- package/src/modules/phone_calls/i18n/es.json +42 -0
- package/src/modules/phone_calls/i18n/ko.json +42 -0
- package/src/modules/phone_calls/i18n/pl.json +42 -0
- package/src/modules/phone_calls/index.ts +13 -0
- package/src/modules/phone_calls/migrations/.snapshot-open-mercato.json +772 -0
- package/src/modules/phone_calls/migrations/Migration20260706124120_phone_calls.ts +23 -0
- package/src/modules/phone_calls/migrations/Migration20260814124333_phone_calls.ts +13 -0
- package/src/modules/phone_calls/setup.ts +14 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260822120000 extends Migration {
|
|
3
|
+
async up() {
|
|
4
|
+
this.addSql(`
|
|
5
|
+
insert into "encryption_maps" ("id", "entity_id", "tenant_id", "organization_id", "fields_json", "is_active", "created_at", "updated_at")
|
|
6
|
+
select gen_random_uuid(), 'phone_calls:phone_call', src."tenant_id", src."organization_id", '[{"field":"raw_snapshot"},{"field":"provider_facts"},{"field":"recording_url"}]'::jsonb, true, now(), now()
|
|
7
|
+
from (
|
|
8
|
+
select distinct "tenant_id", "organization_id"
|
|
9
|
+
from "encryption_maps"
|
|
10
|
+
where "is_active" = true and "deleted_at" is null
|
|
11
|
+
) src
|
|
12
|
+
where not exists (
|
|
13
|
+
select 1 from "encryption_maps" existing
|
|
14
|
+
where existing."entity_id" = 'phone_calls:phone_call'
|
|
15
|
+
and existing."tenant_id" is not distinct from src."tenant_id"
|
|
16
|
+
and existing."organization_id" is not distinct from src."organization_id"
|
|
17
|
+
and existing."deleted_at" is null
|
|
18
|
+
);
|
|
19
|
+
`);
|
|
20
|
+
this.addSql(`
|
|
21
|
+
insert into "encryption_maps" ("id", "entity_id", "tenant_id", "organization_id", "fields_json", "is_active", "created_at", "updated_at")
|
|
22
|
+
select gen_random_uuid(), 'phone_calls:phone_call_participant', src."tenant_id", src."organization_id", '[{"field":"phone_number"},{"field":"display_name"},{"field":"email"}]'::jsonb, true, now(), now()
|
|
23
|
+
from (
|
|
24
|
+
select distinct "tenant_id", "organization_id"
|
|
25
|
+
from "encryption_maps"
|
|
26
|
+
where "is_active" = true and "deleted_at" is null
|
|
27
|
+
) src
|
|
28
|
+
where not exists (
|
|
29
|
+
select 1 from "encryption_maps" existing
|
|
30
|
+
where existing."entity_id" = 'phone_calls:phone_call_participant'
|
|
31
|
+
and existing."tenant_id" is not distinct from src."tenant_id"
|
|
32
|
+
and existing."organization_id" is not distinct from src."organization_id"
|
|
33
|
+
and existing."deleted_at" is null
|
|
34
|
+
);
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
async down() {
|
|
38
|
+
this.addSql(`delete from "encryption_maps" where "entity_id" in ('phone_calls:phone_call', 'phone_calls:phone_call_participant');`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
Migration20260822120000
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=Migration20260822120000.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/entities/migrations/Migration20260822120000.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\n// Backfill the phone_calls encryption maps for every pre-existing (tenant, org) scope that already\n// has active encryption maps. Encryption maps are seeded only at tenant creation (`entities\n// seed-encryption`), so a tenant that predates the phone_calls module has no map for either of its\n// entities, `encryptEntityPayload` no-ops, and caller/destination numbers plus the untouched Tillio\n// payload are written as PLAINTEXT, both in the base tables and in the query index doc. Restricting\n// the insert to scopes that ALREADY have maps mirrors what seed-encryption does and correctly skips\n// tenants with encryption disabled (which have no maps at all). Idempotent via the NOT EXISTS guard,\n// so re-runs are a no-op and it stays consistent with the runtime `upsertEncryptionMapSpecs` helper.\n// Same shape as the `devices:user_device` backfill in Migration20260722120000.\nexport class Migration20260822120000 extends Migration {\n\n override async up(): Promise<void> {\n this.addSql(`\n insert into \"encryption_maps\" (\"id\", \"entity_id\", \"tenant_id\", \"organization_id\", \"fields_json\", \"is_active\", \"created_at\", \"updated_at\")\n select gen_random_uuid(), 'phone_calls:phone_call', src.\"tenant_id\", src.\"organization_id\", '[{\"field\":\"raw_snapshot\"},{\"field\":\"provider_facts\"},{\"field\":\"recording_url\"}]'::jsonb, true, now(), now()\n from (\n select distinct \"tenant_id\", \"organization_id\"\n from \"encryption_maps\"\n where \"is_active\" = true and \"deleted_at\" is null\n ) src\n where not exists (\n select 1 from \"encryption_maps\" existing\n where existing.\"entity_id\" = 'phone_calls:phone_call'\n and existing.\"tenant_id\" is not distinct from src.\"tenant_id\"\n and existing.\"organization_id\" is not distinct from src.\"organization_id\"\n and existing.\"deleted_at\" is null\n );\n `);\n\n this.addSql(`\n insert into \"encryption_maps\" (\"id\", \"entity_id\", \"tenant_id\", \"organization_id\", \"fields_json\", \"is_active\", \"created_at\", \"updated_at\")\n select gen_random_uuid(), 'phone_calls:phone_call_participant', src.\"tenant_id\", src.\"organization_id\", '[{\"field\":\"phone_number\"},{\"field\":\"display_name\"},{\"field\":\"email\"}]'::jsonb, true, now(), now()\n from (\n select distinct \"tenant_id\", \"organization_id\"\n from \"encryption_maps\"\n where \"is_active\" = true and \"deleted_at\" is null\n ) src\n where not exists (\n select 1 from \"encryption_maps\" existing\n where existing.\"entity_id\" = 'phone_calls:phone_call_participant'\n and existing.\"tenant_id\" is not distinct from src.\"tenant_id\"\n and existing.\"organization_id\" is not distinct from src.\"organization_id\"\n and existing.\"deleted_at\" is null\n );\n `);\n }\n\n override async down(): Promise<void> {\n // Reverts the feature's encryption maps. phone_calls maps have no source other than this module,\n // so deleting them all returns the schema to its pre-feature state.\n this.addSql(`delete from \"encryption_maps\" where \"entity_id\" in ('phone_calls:phone_call', 'phone_calls:phone_call_participant');`);\n }\n\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAWnB,MAAM,gCAAgC,UAAU;AAAA,EAErD,MAAe,KAAoB;AACjC,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAeX;AAED,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAeX;AAAA,EACH;AAAA,EAEA,MAAe,OAAsB;AAGnC,SAAK,OAAO,sHAAsH;AAAA,EACpI;AAEF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const features = [
|
|
2
|
+
{ id: "phone_calls.view", title: "View phone calls", module: "phone_calls" },
|
|
3
|
+
{
|
|
4
|
+
id: "phone_calls.manage",
|
|
5
|
+
title: "Manage phone calls (pull, raw payload)",
|
|
6
|
+
module: "phone_calls",
|
|
7
|
+
dependsOn: ["phone_calls.view"]
|
|
8
|
+
}
|
|
9
|
+
];
|
|
10
|
+
var acl_default = features;
|
|
11
|
+
export {
|
|
12
|
+
acl_default as default,
|
|
13
|
+
features
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=acl.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/phone_calls/acl.ts"],
|
|
4
|
+
"sourcesContent": ["export const features = [\n { id: 'phone_calls.view', title: 'View phone calls', module: 'phone_calls' },\n {\n id: 'phone_calls.manage',\n title: 'Manage phone calls (pull, raw payload)',\n module: 'phone_calls',\n dependsOn: ['phone_calls.view'],\n },\n]\n\nexport default features\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,oBAAoB,OAAO,oBAAoB,QAAQ,cAAc;AAAA,EAC3E;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,kBAAkB;AAAA,EAChC;AACF;AAEA,IAAO,cAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
|
|
3
|
+
import { buildIlikeTerm } from "@open-mercato/shared/lib/db/buildIlikeTerm";
|
|
4
|
+
import { E } from "../../../../generated/entities.ids.generated.js";
|
|
5
|
+
import { PhoneCall } from "../../data/entities.js";
|
|
6
|
+
import { createPhoneCallsCrudOpenApi, createPagedListResponseSchema } from "../openapi.js";
|
|
7
|
+
const filterDateSchema = z.string().trim().min(1).refine((value) => !Number.isNaN(new Date(value).getTime()), { message: "Expected a parsable date" });
|
|
8
|
+
const listSchema = z.object({
|
|
9
|
+
page: z.coerce.number().min(1).default(1),
|
|
10
|
+
pageSize: z.coerce.number().min(1).max(100).default(50),
|
|
11
|
+
q: z.string().optional(),
|
|
12
|
+
providerKey: z.string().optional(),
|
|
13
|
+
status: z.string().optional(),
|
|
14
|
+
direction: z.string().optional(),
|
|
15
|
+
// Validated rather than parsed later: an unparsable value used to be dropped, so a typo in a
|
|
16
|
+
// filter silently returned the unfiltered page.
|
|
17
|
+
startedFrom: filterDateSchema.optional(),
|
|
18
|
+
startedTo: filterDateSchema.optional(),
|
|
19
|
+
sortField: z.string().optional(),
|
|
20
|
+
sortDir: z.enum(["asc", "desc"]).optional(),
|
|
21
|
+
id: z.string().uuid().optional()
|
|
22
|
+
}).passthrough();
|
|
23
|
+
const dayOnlySchema = z.iso.date();
|
|
24
|
+
function rangeBoundary(value, edge) {
|
|
25
|
+
if (!dayOnlySchema.safeParse(value).success) return new Date(value);
|
|
26
|
+
return /* @__PURE__ */ new Date(`${value}T${edge === "start" ? "00:00:00.000" : "23:59:59.999"}Z`);
|
|
27
|
+
}
|
|
28
|
+
const routeMetadata = {
|
|
29
|
+
GET: { requireAuth: true, requireFeatures: ["phone_calls.view"] }
|
|
30
|
+
};
|
|
31
|
+
const metadata = routeMetadata;
|
|
32
|
+
const listFields = [
|
|
33
|
+
"id",
|
|
34
|
+
"organization_id",
|
|
35
|
+
"tenant_id",
|
|
36
|
+
"provider_key",
|
|
37
|
+
"integration_id",
|
|
38
|
+
"external_call_id",
|
|
39
|
+
"external_conversation_id",
|
|
40
|
+
"direction",
|
|
41
|
+
"status",
|
|
42
|
+
"started_at",
|
|
43
|
+
"answered_at",
|
|
44
|
+
"ended_at",
|
|
45
|
+
"duration_seconds",
|
|
46
|
+
"ingest_status",
|
|
47
|
+
"last_ingested_at",
|
|
48
|
+
"created_at",
|
|
49
|
+
"updated_at"
|
|
50
|
+
];
|
|
51
|
+
const crud = makeCrudRoute({
|
|
52
|
+
metadata: routeMetadata,
|
|
53
|
+
orm: {
|
|
54
|
+
entity: PhoneCall,
|
|
55
|
+
idField: "id",
|
|
56
|
+
orgField: "organizationId",
|
|
57
|
+
tenantField: "tenantId",
|
|
58
|
+
softDeleteField: "deletedAt"
|
|
59
|
+
},
|
|
60
|
+
indexer: { entityType: E.phone_calls.phone_call },
|
|
61
|
+
list: {
|
|
62
|
+
schema: listSchema,
|
|
63
|
+
entityId: E.phone_calls.phone_call,
|
|
64
|
+
fields: listFields,
|
|
65
|
+
transformItem: (item) => ({ ...item, recording_url: null }),
|
|
66
|
+
sortFieldMap: {
|
|
67
|
+
externalCallId: "external_call_id",
|
|
68
|
+
startedAt: "started_at",
|
|
69
|
+
endedAt: "ended_at",
|
|
70
|
+
durationSeconds: "duration_seconds",
|
|
71
|
+
status: "status",
|
|
72
|
+
direction: "direction",
|
|
73
|
+
providerKey: "provider_key",
|
|
74
|
+
ingestStatus: "ingest_status",
|
|
75
|
+
createdAt: "created_at",
|
|
76
|
+
updatedAt: "updated_at"
|
|
77
|
+
},
|
|
78
|
+
buildFilters: (query) => {
|
|
79
|
+
const filters = {};
|
|
80
|
+
if (query.id) filters.id = { $eq: query.id };
|
|
81
|
+
if (query.providerKey) filters.provider_key = { $eq: query.providerKey };
|
|
82
|
+
if (query.status) filters.status = { $eq: query.status };
|
|
83
|
+
if (query.direction) filters.direction = { $eq: query.direction };
|
|
84
|
+
if (query.q) {
|
|
85
|
+
const term = buildIlikeTerm(query.q);
|
|
86
|
+
filters.$or = [
|
|
87
|
+
{ external_call_id: { $ilike: term } },
|
|
88
|
+
{ external_conversation_id: { $ilike: term } },
|
|
89
|
+
{ provider_key: { $ilike: term } }
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
const startedRange = {};
|
|
93
|
+
if (query.startedFrom) startedRange.$gte = rangeBoundary(query.startedFrom, "start");
|
|
94
|
+
if (query.startedTo) startedRange.$lte = rangeBoundary(query.startedTo, "end");
|
|
95
|
+
if (Object.keys(startedRange).length) filters.started_at = startedRange;
|
|
96
|
+
return filters;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
const GET = crud.GET;
|
|
101
|
+
const phoneCallListItemSchema = z.object({
|
|
102
|
+
id: z.string().uuid(),
|
|
103
|
+
organization_id: z.string().uuid().nullable().optional(),
|
|
104
|
+
tenant_id: z.string().uuid().nullable().optional(),
|
|
105
|
+
provider_key: z.string().nullable().optional(),
|
|
106
|
+
integration_id: z.string().nullable().optional(),
|
|
107
|
+
external_call_id: z.string().nullable().optional(),
|
|
108
|
+
external_conversation_id: z.string().nullable().optional(),
|
|
109
|
+
direction: z.string().nullable().optional(),
|
|
110
|
+
status: z.string().nullable().optional(),
|
|
111
|
+
started_at: z.string().nullable().optional(),
|
|
112
|
+
answered_at: z.string().nullable().optional(),
|
|
113
|
+
ended_at: z.string().nullable().optional(),
|
|
114
|
+
duration_seconds: z.number().nullable().optional(),
|
|
115
|
+
recording_url: z.string().nullable().optional(),
|
|
116
|
+
ingest_status: z.string().nullable().optional(),
|
|
117
|
+
last_ingested_at: z.string().nullable().optional(),
|
|
118
|
+
created_at: z.string().nullable().optional(),
|
|
119
|
+
updated_at: z.string().nullable().optional()
|
|
120
|
+
});
|
|
121
|
+
const openApi = createPhoneCallsCrudOpenApi({
|
|
122
|
+
resourceName: "PhoneCall",
|
|
123
|
+
pluralName: "PhoneCalls",
|
|
124
|
+
querySchema: listSchema,
|
|
125
|
+
listResponseSchema: createPagedListResponseSchema(phoneCallListItemSchema)
|
|
126
|
+
});
|
|
127
|
+
export {
|
|
128
|
+
GET,
|
|
129
|
+
metadata,
|
|
130
|
+
openApi
|
|
131
|
+
};
|
|
132
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/phone_calls/api/calls/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport type { Where } from '@open-mercato/shared/lib/query/types'\nimport { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'\nimport { E } from '#generated/entities.ids.generated'\nimport { PhoneCall } from '../../data/entities'\nimport { createPhoneCallsCrudOpenApi, createPagedListResponseSchema } from '../openapi'\n\nconst filterDateSchema = z\n .string()\n .trim()\n .min(1)\n .refine((value) => !Number.isNaN(new Date(value).getTime()), { message: 'Expected a parsable date' })\n\nconst listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n q: z.string().optional(),\n providerKey: z.string().optional(),\n status: z.string().optional(),\n direction: z.string().optional(),\n // Validated rather than parsed later: an unparsable value used to be dropped, so a typo in a\n // filter silently returned the unfiltered page.\n startedFrom: filterDateSchema.optional(),\n startedTo: filterDateSchema.optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n id: z.string().uuid().optional(),\n })\n .passthrough()\n\nconst dayOnlySchema = z.iso.date()\n\n// A day is a range, not an instant. `new Date('2026-08-20')` is midnight UTC, so using it as\n// the upper bound drops every call on the day the user picked. The filter bar sends bare\n// `yyyy-MM-dd` for both ends, so each is widened to the day it names; a caller that sends a\n// full timestamp keeps the instant it asked for.\nfunction rangeBoundary(value: string, edge: 'start' | 'end'): Date {\n if (!dayOnlySchema.safeParse(value).success) return new Date(value)\n return new Date(`${value}T${edge === 'start' ? '00:00:00.000' : '23:59:59.999'}Z`)\n}\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['phone_calls.view'] },\n}\n\nexport const metadata = routeMetadata\n\n// No `recording_url`: provider recording links carry their own access token, and this route\n// is gated on `phone_calls.view`. The key still ships as null so the response shape holds.\nconst listFields = [\n 'id',\n 'organization_id',\n 'tenant_id',\n 'provider_key',\n 'integration_id',\n 'external_call_id',\n 'external_conversation_id',\n 'direction',\n 'status',\n 'started_at',\n 'answered_at',\n 'ended_at',\n 'duration_seconds',\n 'ingest_status',\n 'last_ingested_at',\n 'created_at',\n 'updated_at',\n]\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: PhoneCall,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.phone_calls.phone_call },\n list: {\n schema: listSchema,\n entityId: E.phone_calls.phone_call,\n fields: listFields,\n transformItem: (item: Record<string, unknown>) => ({ ...item, recording_url: null }),\n sortFieldMap: {\n externalCallId: 'external_call_id',\n startedAt: 'started_at',\n endedAt: 'ended_at',\n durationSeconds: 'duration_seconds',\n status: 'status',\n direction: 'direction',\n providerKey: 'provider_key',\n ingestStatus: 'ingest_status',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n buildFilters: (query): Where<Record<string, unknown>> => {\n const filters: Record<string, unknown> = {}\n if (query.id) filters.id = { $eq: query.id }\n if (query.providerKey) filters.provider_key = { $eq: query.providerKey }\n if (query.status) filters.status = { $eq: query.status }\n if (query.direction) filters.direction = { $eq: query.direction }\n if (query.q) {\n const term = buildIlikeTerm(query.q)\n filters.$or = [\n { external_call_id: { $ilike: term } },\n { external_conversation_id: { $ilike: term } },\n { provider_key: { $ilike: term } },\n ]\n }\n const startedRange: Record<string, Date> = {}\n if (query.startedFrom) startedRange.$gte = rangeBoundary(query.startedFrom, 'start')\n if (query.startedTo) startedRange.$lte = rangeBoundary(query.startedTo, 'end')\n if (Object.keys(startedRange).length) filters.started_at = startedRange\n return filters\n },\n },\n})\n\nexport const GET = crud.GET\n\nconst phoneCallListItemSchema = z.object({\n id: z.string().uuid(),\n organization_id: z.string().uuid().nullable().optional(),\n tenant_id: z.string().uuid().nullable().optional(),\n provider_key: z.string().nullable().optional(),\n integration_id: z.string().nullable().optional(),\n external_call_id: z.string().nullable().optional(),\n external_conversation_id: z.string().nullable().optional(),\n direction: z.string().nullable().optional(),\n status: z.string().nullable().optional(),\n started_at: z.string().nullable().optional(),\n answered_at: z.string().nullable().optional(),\n ended_at: z.string().nullable().optional(),\n duration_seconds: z.number().nullable().optional(),\n recording_url: z.string().nullable().optional(),\n ingest_status: z.string().nullable().optional(),\n last_ingested_at: z.string().nullable().optional(),\n created_at: z.string().nullable().optional(),\n updated_at: z.string().nullable().optional(),\n})\n\nexport const openApi = createPhoneCallsCrudOpenApi({\n resourceName: 'PhoneCall',\n pluralName: 'PhoneCalls',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(phoneCallListItemSchema),\n})\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,qBAAqB;AAE9B,SAAS,sBAAsB;AAC/B,SAAS,SAAS;AAClB,SAAS,iBAAiB;AAC1B,SAAS,6BAA6B,qCAAqC;AAE3E,MAAM,mBAAmB,EACtB,OAAO,EACP,KAAK,EACL,IAAI,CAAC,EACL,OAAO,CAAC,UAAU,CAAC,OAAO,MAAM,IAAI,KAAK,KAAK,EAAE,QAAQ,CAAC,GAAG,EAAE,SAAS,2BAA2B,CAAC;AAEtG,MAAM,aAAa,EAChB,OAAO;AAAA,EACN,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,EACtD,GAAG,EAAE,OAAO,EAAE,SAAS;AAAA,EACvB,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA;AAAA;AAAA,EAG/B,aAAa,iBAAiB,SAAS;AAAA,EACvC,WAAW,iBAAiB,SAAS;AAAA,EACrC,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,SAAS,EAAE,KAAK,CAAC,OAAO,MAAM,CAAC,EAAE,SAAS;AAAA,EAC1C,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACjC,CAAC,EACA,YAAY;AAEf,MAAM,gBAAgB,EAAE,IAAI,KAAK;AAMjC,SAAS,cAAc,OAAe,MAA6B;AACjE,MAAI,CAAC,cAAc,UAAU,KAAK,EAAE,QAAS,QAAO,IAAI,KAAK,KAAK;AAClE,SAAO,oBAAI,KAAK,GAAG,KAAK,IAAI,SAAS,UAAU,iBAAiB,cAAc,GAAG;AACnF;AAEA,MAAM,gBAAgB;AAAA,EACpB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,kBAAkB,EAAE;AAClE;AAEO,MAAM,WAAW;AAIxB,MAAM,aAAa;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,OAAO,cAAc;AAAA,EACzB,UAAU;AAAA,EACV,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB;AAAA,EACA,SAAS,EAAE,YAAY,EAAE,YAAY,WAAW;AAAA,EAChD,MAAM;AAAA,IACJ,QAAQ;AAAA,IACR,UAAU,EAAE,YAAY;AAAA,IACxB,QAAQ;AAAA,IACR,eAAe,CAAC,UAAmC,EAAE,GAAG,MAAM,eAAe,KAAK;AAAA,IAClF,cAAc;AAAA,MACZ,gBAAgB;AAAA,MAChB,WAAW;AAAA,MACX,SAAS;AAAA,MACT,iBAAiB;AAAA,MACjB,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,aAAa;AAAA,MACb,cAAc;AAAA,MACd,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AAAA,IACA,cAAc,CAAC,UAA0C;AACvD,YAAM,UAAmC,CAAC;AAC1C,UAAI,MAAM,GAAI,SAAQ,KAAK,EAAE,KAAK,MAAM,GAAG;AAC3C,UAAI,MAAM,YAAa,SAAQ,eAAe,EAAE,KAAK,MAAM,YAAY;AACvE,UAAI,MAAM,OAAQ,SAAQ,SAAS,EAAE,KAAK,MAAM,OAAO;AACvD,UAAI,MAAM,UAAW,SAAQ,YAAY,EAAE,KAAK,MAAM,UAAU;AAChE,UAAI,MAAM,GAAG;AACX,cAAM,OAAO,eAAe,MAAM,CAAC;AACnC,gBAAQ,MAAM;AAAA,UACZ,EAAE,kBAAkB,EAAE,QAAQ,KAAK,EAAE;AAAA,UACrC,EAAE,0BAA0B,EAAE,QAAQ,KAAK,EAAE;AAAA,UAC7C,EAAE,cAAc,EAAE,QAAQ,KAAK,EAAE;AAAA,QACnC;AAAA,MACF;AACA,YAAM,eAAqC,CAAC;AAC5C,UAAI,MAAM,YAAa,cAAa,OAAO,cAAc,MAAM,aAAa,OAAO;AACnF,UAAI,MAAM,UAAW,cAAa,OAAO,cAAc,MAAM,WAAW,KAAK;AAC7E,UAAI,OAAO,KAAK,YAAY,EAAE,OAAQ,SAAQ,aAAa;AAC3D,aAAO;AAAA,IACT;AAAA,EACF;AACF,CAAC;AAEM,MAAM,MAAM,KAAK;AAExB,MAAM,0BAA0B,EAAE,OAAO;AAAA,EACvC,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,iBAAiB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EACvD,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EACjD,cAAc,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC7C,gBAAgB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC/C,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACjD,0BAA0B,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACzD,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC1C,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACvC,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC3C,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC5C,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACzC,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACjD,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC9C,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC9C,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACjD,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC3C,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAC7C,CAAC;AAEM,MAAM,UAAU,4BAA4B;AAAA,EACjD,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,oBAAoB,8BAA8B,uBAAuB;AAC3E,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCrudOpenApiFactory,
|
|
3
|
+
createPagedListResponseSchema as createSharedPagedListResponseSchema,
|
|
4
|
+
defaultOkResponseSchema as sharedDefaultOkResponseSchema
|
|
5
|
+
} from "@open-mercato/shared/lib/openapi/crud";
|
|
6
|
+
const defaultOkResponseSchema = sharedDefaultOkResponseSchema;
|
|
7
|
+
function createPagedListResponseSchema(itemSchema) {
|
|
8
|
+
return createSharedPagedListResponseSchema(itemSchema, { paginationMetaOptional: true });
|
|
9
|
+
}
|
|
10
|
+
const buildPhoneCallsCrudOpenApi = createCrudOpenApiFactory({
|
|
11
|
+
defaultTag: "PhoneCalls",
|
|
12
|
+
defaultOkResponseSchema,
|
|
13
|
+
makeListDescription: ({ pluralLower }) => `Returns a paginated collection of ${pluralLower} scoped to the authenticated organization.`
|
|
14
|
+
});
|
|
15
|
+
function createPhoneCallsCrudOpenApi(options) {
|
|
16
|
+
return buildPhoneCallsCrudOpenApi(options);
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
createPagedListResponseSchema,
|
|
20
|
+
createPhoneCallsCrudOpenApi,
|
|
21
|
+
defaultOkResponseSchema
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=openapi.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/phone_calls/api/openapi.ts"],
|
|
4
|
+
"sourcesContent": ["import { type ZodTypeAny } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n createCrudOpenApiFactory,\n createPagedListResponseSchema as createSharedPagedListResponseSchema,\n defaultOkResponseSchema as sharedDefaultOkResponseSchema,\n type CrudOpenApiOptions,\n} from '@open-mercato/shared/lib/openapi/crud'\n\nexport const defaultOkResponseSchema = sharedDefaultOkResponseSchema\n\nexport function createPagedListResponseSchema(itemSchema: ZodTypeAny) {\n return createSharedPagedListResponseSchema(itemSchema, { paginationMetaOptional: true })\n}\n\nconst buildPhoneCallsCrudOpenApi = createCrudOpenApiFactory({\n defaultTag: 'PhoneCalls',\n defaultOkResponseSchema,\n makeListDescription: ({ pluralLower }) =>\n `Returns a paginated collection of ${pluralLower} scoped to the authenticated organization.`,\n})\n\nexport function createPhoneCallsCrudOpenApi(options: CrudOpenApiOptions): OpenApiRouteDoc {\n return buildPhoneCallsCrudOpenApi(options)\n}\n"],
|
|
5
|
+
"mappings": "AAEA;AAAA,EACE;AAAA,EACA,iCAAiC;AAAA,EACjC,2BAA2B;AAAA,OAEtB;AAEA,MAAM,0BAA0B;AAEhC,SAAS,8BAA8B,YAAwB;AACpE,SAAO,oCAAoC,YAAY,EAAE,wBAAwB,KAAK,CAAC;AACzF;AAEA,MAAM,6BAA6B,yBAAyB;AAAA,EAC1D,YAAY;AAAA,EACZ;AAAA,EACA,qBAAqB,CAAC,EAAE,YAAY,MAClC,qCAAqC,WAAW;AACpD,CAAC;AAEM,SAAS,4BAA4B,SAA8C;AACxF,SAAO,2BAA2B,OAAO;AAC3C;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
5
|
+
import { DataTable } from "@open-mercato/ui/backend/DataTable";
|
|
6
|
+
import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
|
|
7
|
+
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
8
|
+
import { useAppEvent } from "@open-mercato/ui/backend/injection/useAppEvent";
|
|
9
|
+
import { useProgressPoll } from "@open-mercato/ui/backend/progress/useProgressPoll";
|
|
10
|
+
import { formatDateTime } from "@open-mercato/shared/lib/time";
|
|
11
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
12
|
+
import { PHONE_CALL_RESOURCE_KIND } from "@open-mercato/shared/modules/phone_calls/types";
|
|
13
|
+
const DIRECTION_VALUES = ["inbound", "outbound", "internal", "unknown"];
|
|
14
|
+
const STATUS_VALUES = ["new", "ringing", "answered", "missed", "failed", "completed", "unknown"];
|
|
15
|
+
function isPhoneCallStatus(value) {
|
|
16
|
+
return STATUS_VALUES.includes(value);
|
|
17
|
+
}
|
|
18
|
+
const SORT_FIELDS = {
|
|
19
|
+
external_call_id: "externalCallId",
|
|
20
|
+
direction: "direction",
|
|
21
|
+
status: "status",
|
|
22
|
+
provider_key: "providerKey",
|
|
23
|
+
started_at: "startedAt",
|
|
24
|
+
duration_seconds: "durationSeconds",
|
|
25
|
+
ingest_status: "ingestStatus"
|
|
26
|
+
};
|
|
27
|
+
const STATUS_TONE = {
|
|
28
|
+
completed: "success",
|
|
29
|
+
answered: "success",
|
|
30
|
+
ringing: "warning",
|
|
31
|
+
new: "neutral",
|
|
32
|
+
missed: "error",
|
|
33
|
+
failed: "error",
|
|
34
|
+
unknown: "neutral"
|
|
35
|
+
};
|
|
36
|
+
function formatDuration(seconds) {
|
|
37
|
+
if (seconds == null || Number.isNaN(seconds)) return "\u2014";
|
|
38
|
+
const total = Math.max(0, Math.floor(seconds));
|
|
39
|
+
const mins = Math.floor(total / 60);
|
|
40
|
+
const secs = total % 60;
|
|
41
|
+
return `${mins}:${String(secs).padStart(2, "0")}`;
|
|
42
|
+
}
|
|
43
|
+
function PhoneCallsPage() {
|
|
44
|
+
const t = useT();
|
|
45
|
+
const [rows, setRows] = React.useState([]);
|
|
46
|
+
const [total, setTotal] = React.useState(0);
|
|
47
|
+
const [totalPages, setTotalPages] = React.useState(1);
|
|
48
|
+
const [totalIsCapped, setTotalIsCapped] = React.useState(false);
|
|
49
|
+
const [page, setPage] = React.useState(1);
|
|
50
|
+
const [pageSize, setPageSize] = React.useState(50);
|
|
51
|
+
const [search, setSearch] = React.useState("");
|
|
52
|
+
const [sorting, setSorting] = React.useState([{ id: "started_at", desc: true }]);
|
|
53
|
+
const [filterValues, setFilterValues] = React.useState({});
|
|
54
|
+
const handleSortingChange = React.useCallback((next) => {
|
|
55
|
+
setSorting(next);
|
|
56
|
+
setPage(1);
|
|
57
|
+
}, []);
|
|
58
|
+
const [isLoading, setIsLoading] = React.useState(true);
|
|
59
|
+
const [error, setError] = React.useState(null);
|
|
60
|
+
const [reloadToken, setReloadToken] = React.useState(0);
|
|
61
|
+
useAppEvent("progress.job.completed", (event) => {
|
|
62
|
+
const meta = event.payload?.meta;
|
|
63
|
+
if (meta?.resourceKind !== PHONE_CALL_RESOURCE_KIND) return;
|
|
64
|
+
setReloadToken((token) => token + 1);
|
|
65
|
+
}, []);
|
|
66
|
+
const { recentlyCompleted } = useProgressPoll();
|
|
67
|
+
const seenJobIds = React.useRef(null);
|
|
68
|
+
React.useEffect(() => {
|
|
69
|
+
const ingestJobs = recentlyCompleted.filter(
|
|
70
|
+
(job) => job.meta?.resourceKind === PHONE_CALL_RESOURCE_KIND
|
|
71
|
+
);
|
|
72
|
+
if (seenJobIds.current === null) {
|
|
73
|
+
seenJobIds.current = new Set(ingestJobs.map((job) => job.id));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const unseen = ingestJobs.filter((job) => !seenJobIds.current.has(job.id));
|
|
77
|
+
if (!unseen.length) return;
|
|
78
|
+
for (const job of unseen) seenJobIds.current.add(job.id);
|
|
79
|
+
setReloadToken((token) => token + 1);
|
|
80
|
+
}, [recentlyCompleted]);
|
|
81
|
+
const queryString = React.useMemo(() => {
|
|
82
|
+
const params = new URLSearchParams();
|
|
83
|
+
params.set("page", String(page));
|
|
84
|
+
params.set("pageSize", String(pageSize));
|
|
85
|
+
if (search.trim()) params.set("q", search.trim());
|
|
86
|
+
if (typeof filterValues.direction === "string" && filterValues.direction) params.set("direction", filterValues.direction);
|
|
87
|
+
if (typeof filterValues.status === "string" && filterValues.status) params.set("status", filterValues.status);
|
|
88
|
+
if (typeof filterValues.providerKey === "string" && filterValues.providerKey.trim()) params.set("providerKey", filterValues.providerKey.trim());
|
|
89
|
+
const started = filterValues.started;
|
|
90
|
+
if (started?.from) params.set("startedFrom", started.from);
|
|
91
|
+
if (started?.to) params.set("startedTo", started.to);
|
|
92
|
+
const activeSort = sorting[0];
|
|
93
|
+
if (activeSort) {
|
|
94
|
+
const sortField = SORT_FIELDS[activeSort.id];
|
|
95
|
+
if (sortField) {
|
|
96
|
+
params.set("sortField", sortField);
|
|
97
|
+
params.set("sortDir", activeSort.desc ? "desc" : "asc");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return params.toString();
|
|
101
|
+
}, [page, pageSize, search, filterValues, sorting]);
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
let cancelled = false;
|
|
104
|
+
async function load() {
|
|
105
|
+
setIsLoading(true);
|
|
106
|
+
setError(null);
|
|
107
|
+
const response = await apiCall(`/api/phone_calls/calls?${queryString}`).catch((err) => ({
|
|
108
|
+
ok: false,
|
|
109
|
+
result: { error: err instanceof Error ? err.message : void 0 }
|
|
110
|
+
}));
|
|
111
|
+
if (cancelled) return;
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
const errBody = response.result;
|
|
114
|
+
setError(errBody?.error ?? t("phone_calls.calls.list.error", "Failed to load phone calls"));
|
|
115
|
+
setRows([]);
|
|
116
|
+
setTotal(0);
|
|
117
|
+
setTotalPages(1);
|
|
118
|
+
setTotalIsCapped(false);
|
|
119
|
+
} else {
|
|
120
|
+
const data = response.result ?? {};
|
|
121
|
+
setRows(Array.isArray(data.items) ? data.items : []);
|
|
122
|
+
setTotal(typeof data.total === "number" ? data.total : 0);
|
|
123
|
+
setTotalPages(typeof data.totalPages === "number" ? data.totalPages : 1);
|
|
124
|
+
setTotalIsCapped(data.totalIsCapped === true);
|
|
125
|
+
}
|
|
126
|
+
setIsLoading(false);
|
|
127
|
+
}
|
|
128
|
+
void load();
|
|
129
|
+
return () => {
|
|
130
|
+
cancelled = true;
|
|
131
|
+
};
|
|
132
|
+
}, [queryString, reloadToken, t]);
|
|
133
|
+
const filters = React.useMemo(() => [
|
|
134
|
+
{
|
|
135
|
+
id: "direction",
|
|
136
|
+
label: t("phone_calls.calls.filters.direction", "Direction"),
|
|
137
|
+
type: "select",
|
|
138
|
+
options: DIRECTION_VALUES.map((value) => ({
|
|
139
|
+
value,
|
|
140
|
+
label: t(`phone_calls.calls.direction.${value}`, value)
|
|
141
|
+
}))
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "status",
|
|
145
|
+
label: t("phone_calls.calls.filters.status", "Status"),
|
|
146
|
+
type: "select",
|
|
147
|
+
options: STATUS_VALUES.map((value) => ({
|
|
148
|
+
value,
|
|
149
|
+
label: t(`phone_calls.calls.status.${value}`, value)
|
|
150
|
+
}))
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "providerKey",
|
|
154
|
+
label: t("phone_calls.calls.filters.provider", "Provider"),
|
|
155
|
+
type: "text",
|
|
156
|
+
placeholder: t("phone_calls.calls.filters.providerPlaceholder", "Provider key")
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "started",
|
|
160
|
+
label: t("phone_calls.calls.filters.started", "Started"),
|
|
161
|
+
type: "dateRange"
|
|
162
|
+
}
|
|
163
|
+
], [t]);
|
|
164
|
+
const columns = React.useMemo(() => [
|
|
165
|
+
{
|
|
166
|
+
accessorKey: "external_call_id",
|
|
167
|
+
header: t("phone_calls.calls.columns.callId", "Call ID"),
|
|
168
|
+
meta: { truncate: true, maxWidth: "240px" },
|
|
169
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: row.original.external_call_id || "\u2014" })
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
accessorKey: "direction",
|
|
173
|
+
header: t("phone_calls.calls.columns.direction", "Direction"),
|
|
174
|
+
cell: ({ row }) => {
|
|
175
|
+
const value = row.original.direction;
|
|
176
|
+
return /* @__PURE__ */ jsx("span", { className: "text-sm", children: value ? t(`phone_calls.calls.direction.${value}`, value) : "\u2014" });
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
accessorKey: "status",
|
|
181
|
+
header: t("phone_calls.calls.columns.status", "Status"),
|
|
182
|
+
cell: ({ row }) => {
|
|
183
|
+
const value = row.original.status;
|
|
184
|
+
if (!value) return /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: "\u2014" });
|
|
185
|
+
return /* @__PURE__ */ jsx(StatusBadge, { variant: STATUS_TONE[isPhoneCallStatus(value) ? value : "unknown"], dot: true, children: t(`phone_calls.calls.status.${value}`, value) });
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
accessorKey: "provider_key",
|
|
190
|
+
header: t("phone_calls.calls.columns.provider", "Provider"),
|
|
191
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-sm", children: row.original.provider_key || "\u2014" })
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
accessorKey: "started_at",
|
|
195
|
+
header: t("phone_calls.calls.columns.started", "Started"),
|
|
196
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: formatDateTime(row.original.started_at) ?? "\u2014" })
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
accessorKey: "duration_seconds",
|
|
200
|
+
header: t("phone_calls.calls.columns.duration", "Duration"),
|
|
201
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-sm tabular-nums", children: formatDuration(row.original.duration_seconds) })
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
accessorKey: "ingest_status",
|
|
205
|
+
header: t("phone_calls.calls.columns.ingestStatus", "Ingest"),
|
|
206
|
+
cell: ({ row }) => {
|
|
207
|
+
const value = row.original.ingest_status;
|
|
208
|
+
return /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: value ? t(`phone_calls.calls.ingestStatus.${value}`, value) : "\u2014" });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
], [t]);
|
|
212
|
+
return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(
|
|
213
|
+
DataTable,
|
|
214
|
+
{
|
|
215
|
+
title: t("phone_calls.calls.list.title", "Phone Calls"),
|
|
216
|
+
extensionTableId: "phone_calls.calls",
|
|
217
|
+
columns,
|
|
218
|
+
data: rows,
|
|
219
|
+
isLoading,
|
|
220
|
+
error,
|
|
221
|
+
sortable: true,
|
|
222
|
+
manualSorting: true,
|
|
223
|
+
sorting,
|
|
224
|
+
onSortingChange: handleSortingChange,
|
|
225
|
+
searchValue: search,
|
|
226
|
+
onSearchChange: (value) => {
|
|
227
|
+
setSearch(value);
|
|
228
|
+
setPage(1);
|
|
229
|
+
},
|
|
230
|
+
searchPlaceholder: t("phone_calls.calls.list.searchPlaceholder", "Search by call, conversation or provider"),
|
|
231
|
+
filters,
|
|
232
|
+
filterValues,
|
|
233
|
+
onFiltersApply: (values) => {
|
|
234
|
+
setFilterValues(values);
|
|
235
|
+
setPage(1);
|
|
236
|
+
},
|
|
237
|
+
onFiltersClear: () => {
|
|
238
|
+
setFilterValues({});
|
|
239
|
+
setPage(1);
|
|
240
|
+
},
|
|
241
|
+
emptyState: t("phone_calls.calls.list.empty", "No phone calls yet. Calls appear here once they are ingested from a provider."),
|
|
242
|
+
pagination: {
|
|
243
|
+
page,
|
|
244
|
+
pageSize,
|
|
245
|
+
total,
|
|
246
|
+
totalPages,
|
|
247
|
+
totalIsCapped,
|
|
248
|
+
onPageChange: setPage,
|
|
249
|
+
pageSizeOptions: [10, 25, 50, 100],
|
|
250
|
+
onPageSizeChange: (size) => {
|
|
251
|
+
setPageSize(size);
|
|
252
|
+
setPage(1);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
) }) });
|
|
257
|
+
}
|
|
258
|
+
export {
|
|
259
|
+
PhoneCallsPage as default
|
|
260
|
+
};
|
|
261
|
+
//# sourceMappingURL=page.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/phone_calls/backend/page.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { SortingState } from '@tanstack/react-table'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { StatusBadge, type StatusMap } from '@open-mercato/ui/primitives/status-badge'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'\nimport { useProgressPoll } from '@open-mercato/ui/backend/progress/useProgressPoll'\nimport { formatDateTime } from '@open-mercato/shared/lib/time'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { PHONE_CALL_RESOURCE_KIND } from '@open-mercato/shared/modules/phone_calls/types'\n\ntype PhoneCallRow = {\n id: string\n provider_key: string | null\n external_call_id: string | null\n direction: string | null\n status: string | null\n started_at: string | null\n duration_seconds: number | null\n ingest_status: string | null\n}\n\ntype PhoneCallListResponse = {\n items?: PhoneCallRow[]\n total?: number\n page?: number\n totalPages?: number\n totalIsCapped?: boolean\n}\n\nconst DIRECTION_VALUES = ['inbound', 'outbound', 'internal', 'unknown'] as const\nconst STATUS_VALUES = ['new', 'ringing', 'answered', 'missed', 'failed', 'completed', 'unknown'] as const\n\ntype PhoneCallStatusValue = typeof STATUS_VALUES[number]\n\nfunction isPhoneCallStatus(value: string): value is PhoneCallStatusValue {\n return (STATUS_VALUES as readonly string[]).includes(value)\n}\n\n// Column id (snake_case accessorKey) -> API sortField key (camelCase, matches\n// the route's sortFieldMap). Columns absent from this map are not sortable.\nconst SORT_FIELDS: Record<string, string> = {\n external_call_id: 'externalCallId',\n direction: 'direction',\n status: 'status',\n provider_key: 'providerKey',\n started_at: 'startedAt',\n duration_seconds: 'durationSeconds',\n ingest_status: 'ingestStatus',\n}\n\nconst STATUS_TONE: StatusMap<PhoneCallStatusValue> = {\n completed: 'success',\n answered: 'success',\n ringing: 'warning',\n new: 'neutral',\n missed: 'error',\n failed: 'error',\n unknown: 'neutral',\n}\n\nfunction formatDuration(seconds: number | null): string {\n if (seconds == null || Number.isNaN(seconds)) return '\u2014'\n const total = Math.max(0, Math.floor(seconds))\n const mins = Math.floor(total / 60)\n const secs = total % 60\n return `${mins}:${String(secs).padStart(2, '0')}`\n}\n\nexport default function PhoneCallsPage() {\n const t = useT()\n const [rows, setRows] = React.useState<PhoneCallRow[]>([])\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [totalIsCapped, setTotalIsCapped] = React.useState(false)\n const [page, setPage] = React.useState(1)\n const [pageSize, setPageSize] = React.useState(50)\n const [search, setSearch] = React.useState('')\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'started_at', desc: true }])\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n\n const handleSortingChange = React.useCallback((next: SortingState) => {\n setSorting(next)\n setPage(1)\n }, [])\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [reloadToken, setReloadToken] = React.useState(0)\n\n // Ingest runs in a provider worker, so the rows land after the page did. Any job that\n // declares this resource kind wrote calls, whichever provider queued it.\n useAppEvent('progress.job.completed', (event) => {\n const meta = (event.payload as { meta?: Record<string, unknown> | null } | undefined)?.meta\n if (meta?.resourceKind !== PHONE_CALL_RESOURCE_KIND) return\n setReloadToken((token) => token + 1)\n }, [])\n\n // The event above is a live subscription with no replay: one emitted while the stream is\n // reconnecting is gone, and the grid keeps showing a range the ingest already filled. Polling\n // asks for state instead of waiting to be told, keyed by job id so nothing reloads twice.\n const { recentlyCompleted } = useProgressPoll()\n const seenJobIds = React.useRef<Set<string> | null>(null)\n React.useEffect(() => {\n const ingestJobs = recentlyCompleted.filter(\n (job) => (job.meta as { resourceKind?: string } | null | undefined)?.resourceKind === PHONE_CALL_RESOURCE_KIND,\n )\n // Jobs that finished before this page mounted are recorded without a reload; the initial\n // load already covers them.\n if (seenJobIds.current === null) {\n seenJobIds.current = new Set(ingestJobs.map((job) => job.id))\n return\n }\n const unseen = ingestJobs.filter((job) => !seenJobIds.current!.has(job.id))\n if (!unseen.length) return\n for (const job of unseen) seenJobIds.current.add(job.id)\n setReloadToken((token) => token + 1)\n }, [recentlyCompleted])\n\n const queryString = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (search.trim()) params.set('q', search.trim())\n if (typeof filterValues.direction === 'string' && filterValues.direction) params.set('direction', filterValues.direction)\n if (typeof filterValues.status === 'string' && filterValues.status) params.set('status', filterValues.status)\n if (typeof filterValues.providerKey === 'string' && filterValues.providerKey.trim()) params.set('providerKey', filterValues.providerKey.trim())\n const started = filterValues.started as { from?: string; to?: string } | undefined\n if (started?.from) params.set('startedFrom', started.from)\n if (started?.to) params.set('startedTo', started.to)\n const activeSort = sorting[0]\n if (activeSort) {\n const sortField = SORT_FIELDS[activeSort.id]\n if (sortField) {\n params.set('sortField', sortField)\n params.set('sortDir', activeSort.desc ? 'desc' : 'asc')\n }\n }\n return params.toString()\n }, [page, pageSize, search, filterValues, sorting])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setError(null)\n const response = await apiCall<PhoneCallListResponse>(`/api/phone_calls/calls?${queryString}`).catch((err: unknown) => ({\n ok: false as const,\n result: { error: err instanceof Error ? err.message : undefined },\n }))\n if (cancelled) return\n if (!response.ok) {\n const errBody = response.result as { error?: string } | undefined\n setError(errBody?.error ?? t('phone_calls.calls.list.error', 'Failed to load phone calls'))\n setRows([])\n setTotal(0)\n setTotalPages(1)\n setTotalIsCapped(false)\n } else {\n const data = (response.result ?? {}) as PhoneCallListResponse\n setRows(Array.isArray(data.items) ? data.items : [])\n setTotal(typeof data.total === 'number' ? data.total : 0)\n setTotalPages(typeof data.totalPages === 'number' ? data.totalPages : 1)\n setTotalIsCapped(data.totalIsCapped === true)\n }\n setIsLoading(false)\n }\n void load()\n return () => { cancelled = true }\n }, [queryString, reloadToken, t])\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'direction',\n label: t('phone_calls.calls.filters.direction', 'Direction'),\n type: 'select',\n options: DIRECTION_VALUES.map((value) => ({\n value,\n label: t(`phone_calls.calls.direction.${value}`, value),\n })),\n },\n {\n id: 'status',\n label: t('phone_calls.calls.filters.status', 'Status'),\n type: 'select',\n options: STATUS_VALUES.map((value) => ({\n value,\n label: t(`phone_calls.calls.status.${value}`, value),\n })),\n },\n {\n id: 'providerKey',\n label: t('phone_calls.calls.filters.provider', 'Provider'),\n type: 'text',\n placeholder: t('phone_calls.calls.filters.providerPlaceholder', 'Provider key'),\n },\n {\n id: 'started',\n label: t('phone_calls.calls.filters.started', 'Started'),\n type: 'dateRange',\n },\n ], [t])\n\n const columns = React.useMemo<ColumnDef<PhoneCallRow>[]>(() => [\n {\n accessorKey: 'external_call_id',\n header: t('phone_calls.calls.columns.callId', 'Call ID'),\n meta: { truncate: true, maxWidth: '240px' },\n cell: ({ row }) => (\n <span className=\"text-sm font-medium\">{row.original.external_call_id || '\u2014'}</span>\n ),\n },\n {\n accessorKey: 'direction',\n header: t('phone_calls.calls.columns.direction', 'Direction'),\n cell: ({ row }) => {\n const value = row.original.direction\n return <span className=\"text-sm\">{value ? t(`phone_calls.calls.direction.${value}`, value) : '\u2014'}</span>\n },\n },\n {\n accessorKey: 'status',\n header: t('phone_calls.calls.columns.status', 'Status'),\n cell: ({ row }) => {\n const value = row.original.status\n if (!value) return <span className=\"text-sm text-muted-foreground\">\u2014</span>\n return (\n <StatusBadge variant={STATUS_TONE[isPhoneCallStatus(value) ? value : 'unknown']} dot>\n {t(`phone_calls.calls.status.${value}`, value)}\n </StatusBadge>\n )\n },\n },\n {\n accessorKey: 'provider_key',\n header: t('phone_calls.calls.columns.provider', 'Provider'),\n cell: ({ row }) => <span className=\"text-sm\">{row.original.provider_key || '\u2014'}</span>,\n },\n {\n accessorKey: 'started_at',\n header: t('phone_calls.calls.columns.started', 'Started'),\n cell: ({ row }) => <span className=\"text-sm text-muted-foreground\">{formatDateTime(row.original.started_at) ?? '\u2014'}</span>,\n },\n {\n accessorKey: 'duration_seconds',\n header: t('phone_calls.calls.columns.duration', 'Duration'),\n cell: ({ row }) => <span className=\"text-sm tabular-nums\">{formatDuration(row.original.duration_seconds)}</span>,\n },\n {\n accessorKey: 'ingest_status',\n header: t('phone_calls.calls.columns.ingestStatus', 'Ingest'),\n cell: ({ row }) => {\n const value = row.original.ingest_status\n return (\n <span className=\"text-sm text-muted-foreground\">\n {value ? t(`phone_calls.calls.ingestStatus.${value}`, value) : '\u2014'}\n </span>\n )\n },\n },\n ], [t])\n\n return (\n <Page>\n <PageBody>\n <DataTable<PhoneCallRow>\n title={t('phone_calls.calls.list.title', 'Phone Calls')}\n extensionTableId=\"phone_calls.calls\"\n columns={columns}\n data={rows}\n isLoading={isLoading}\n error={error}\n sortable\n manualSorting\n sorting={sorting}\n onSortingChange={handleSortingChange}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('phone_calls.calls.list.searchPlaceholder', 'Search by call, conversation or provider')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={(values) => { setFilterValues(values); setPage(1) }}\n onFiltersClear={() => { setFilterValues({}); setPage(1) }}\n emptyState={t('phone_calls.calls.list.empty', 'No phone calls yet. Calls appear here once they are ingested from a provider.')}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n totalIsCapped,\n onPageChange: setPage,\n pageSizeOptions: [10, 25, 50, 100],\n onPageSizeChange: (size) => { setPageSize(size); setPage(1) },\n }}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const metadata = {
|
|
2
|
+
requireAuth: true,
|
|
3
|
+
requireFeatures: ["phone_calls.view"],
|
|
4
|
+
pageTitle: "Phone Calls",
|
|
5
|
+
pageTitleKey: "phone_calls.nav.calls",
|
|
6
|
+
pageGroup: "Communication",
|
|
7
|
+
pageGroupKey: "phone_calls.nav.group",
|
|
8
|
+
pagePriority: 10,
|
|
9
|
+
pageOrder: 100,
|
|
10
|
+
icon: "phone-call",
|
|
11
|
+
breadcrumb: [{ label: "Phone Calls", labelKey: "phone_calls.nav.calls" }]
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
metadata
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=page.meta.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/phone_calls/backend/page.meta.ts"],
|
|
4
|
+
"sourcesContent": ["export const metadata = {\n requireAuth: true,\n requireFeatures: ['phone_calls.view'],\n pageTitle: 'Phone Calls',\n pageTitleKey: 'phone_calls.nav.calls',\n pageGroup: 'Communication',\n pageGroupKey: 'phone_calls.nav.group',\n pagePriority: 10,\n pageOrder: 100,\n icon: 'phone-call',\n breadcrumb: [{ label: 'Phone Calls', labelKey: 'phone_calls.nav.calls' }],\n}\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,aAAa;AAAA,EACb,iBAAiB,CAAC,kBAAkB;AAAA,EACpC,WAAW;AAAA,EACX,cAAc;AAAA,EACd,WAAW;AAAA,EACX,cAAc;AAAA,EACd,cAAc;AAAA,EACd,WAAW;AAAA,EACX,MAAM;AAAA,EACN,YAAY,CAAC,EAAE,OAAO,eAAe,UAAU,wBAAwB,CAAC;AAC1E;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|