mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-25 12:01:04 +02:00
* ViciDial Working * feat(telephony): add FreeSWITCH provider to upstream-PBX seam Generalize the upstream-PBX capture and control paths to dispatch by provider. FreeSWITCH bridges in with X-PBX-* headers and is driven over the Event Socket Library (uuid_kill / uuid_transfer) by the channel UUID, alongside the existing VICIdial ra_call_control path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * add vici specific configs * feat(telephony): env-based VICIdial config + address3 post-call routing Move VICIdial agent/non-agent API and FreeSWITCH ESL connection settings out of hardcoded POC values into environment variables so the same image works against a PBX on another server. Add VICIdial update_lead forwarding: X-VICI-UPDATE-LEAD_* extracted variables are mapped to lead columns and pushed via the non-agent API before a transfer, with reserved API-control params dropped. Add hardcoded address3 disposition routing (Y/N -> in-group transfer, else hang up) and a synchronous final variable extraction so the transfer path sees the freshest conversation state. Skip upstream_pbx capture on non-PJSIP channels to avoid Asterisk 500s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: run formatter * feat: make vici configurable from UI * chore: clean up PR * fix: incorporate review comments * chore: incorporate review comments * chore: generate client * chore: incporporate review comments * chore: incorporate review comments --------- Co-authored-by: Dograh POC <payment@dograh.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
179 lines
6.3 KiB
TypeScript
179 lines
6.3 KiB
TypeScript
'use client';
|
|
|
|
import { createContext, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
|
|
import { client } from '@/client/client.gen';
|
|
import { getCurrentOrganizationContextApiV1OrganizationsContextGet, getPreferencesApiV1OrganizationsPreferencesGet, getUserConfigurationsApiV1UserConfigurationsUserGet } from '@/client/sdk.gen';
|
|
import type { OrganizationContextResponse, OrganizationPreferences, UserConfigurationRequestResponseSchema } from '@/client/types.gen';
|
|
import { setupAuthInterceptor } from '@/lib/apiClient';
|
|
import { detailFromError } from '@/lib/apiError';
|
|
import type { AuthUser } from '@/lib/auth';
|
|
import { useAuth } from '@/lib/auth';
|
|
|
|
interface TeamPermission {
|
|
id: string;
|
|
}
|
|
|
|
interface OrganizationPricing {
|
|
price_per_second_usd: number | null;
|
|
currency: string;
|
|
billing_enabled: boolean;
|
|
}
|
|
|
|
interface OrgConfigContextType {
|
|
orgContext: OrganizationContextResponse | null;
|
|
userConfig: UserConfigurationRequestResponseSchema | null;
|
|
loading: boolean;
|
|
error: Error | null;
|
|
refreshConfig: () => Promise<void>;
|
|
permissions: TeamPermission[];
|
|
user: AuthUser | null;
|
|
organizationPricing: OrganizationPricing | null;
|
|
organizationPreferences: OrganizationPreferences | null;
|
|
externalPbxIntegrationsEnabled: boolean;
|
|
}
|
|
|
|
const OrgConfigContext = createContext<OrgConfigContextType | null>(null);
|
|
|
|
const pricingFromUserConfig = (
|
|
userConfig: UserConfigurationRequestResponseSchema,
|
|
): OrganizationPricing | null => {
|
|
if (!userConfig.organization_pricing) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
price_per_second_usd: userConfig.organization_pricing.price_per_second_usd as number | null,
|
|
currency: (userConfig.organization_pricing.currency as string) || 'USD',
|
|
billing_enabled: (userConfig.organization_pricing.billing_enabled as boolean) || false,
|
|
};
|
|
};
|
|
|
|
export function OrgConfigProvider({ children }: { children: ReactNode }) {
|
|
const [orgContext, setOrgContext] = useState<OrganizationContextResponse | null>(null);
|
|
const [userConfig, setUserConfig] = useState<UserConfigurationRequestResponseSchema | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [organizationPricing, setOrganizationPricing] = useState<OrganizationPricing | null>(null);
|
|
const [organizationPreferences, setOrganizationPreferences] = useState<OrganizationPreferences | null>(null);
|
|
const [permissions, setPermissions] = useState<TeamPermission[]>([]);
|
|
|
|
const auth = useAuth();
|
|
|
|
const authRef = useRef(auth);
|
|
authRef.current = auth;
|
|
|
|
const hasFetchedConfig = useRef(false);
|
|
const hasFetchedPermissions = useRef(false);
|
|
|
|
if (!auth.loading && auth.isAuthenticated) {
|
|
setupAuthInterceptor(client, auth.getAccessToken);
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (auth.loading || hasFetchedPermissions.current) {
|
|
return;
|
|
}
|
|
hasFetchedPermissions.current = true;
|
|
|
|
const fetchPermissions = async () => {
|
|
const currentAuth = authRef.current;
|
|
if (currentAuth.provider === 'stack' && currentAuth.getSelectedTeam && currentAuth.listPermissions) {
|
|
const selectedTeam = currentAuth.getSelectedTeam();
|
|
if (selectedTeam) {
|
|
try {
|
|
const perms = await currentAuth.listPermissions(selectedTeam);
|
|
setPermissions(Array.isArray(perms) ? perms : []);
|
|
} catch {
|
|
setPermissions([]);
|
|
}
|
|
} else {
|
|
setPermissions([]);
|
|
}
|
|
} else {
|
|
setPermissions([{ id: 'admin' }]);
|
|
}
|
|
};
|
|
|
|
fetchPermissions();
|
|
}, [auth.loading, auth.provider]);
|
|
|
|
const fetchConfig = useCallback(async () => {
|
|
const currentAuth = authRef.current;
|
|
if (!currentAuth.isAuthenticated) {
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const [orgContextResponse, userConfigResponse, preferencesResponse] = await Promise.all([
|
|
getCurrentOrganizationContextApiV1OrganizationsContextGet(),
|
|
getUserConfigurationsApiV1UserConfigurationsUserGet(),
|
|
getPreferencesApiV1OrganizationsPreferencesGet(),
|
|
]);
|
|
|
|
if (preferencesResponse.error) {
|
|
throw new Error(detailFromError(preferencesResponse.error, 'Failed to load organization preferences'));
|
|
}
|
|
|
|
if (orgContextResponse.data) {
|
|
setOrgContext(orgContextResponse.data);
|
|
}
|
|
|
|
if (userConfigResponse.data) {
|
|
setUserConfig(userConfigResponse.data);
|
|
setOrganizationPricing(pricingFromUserConfig(userConfigResponse.data));
|
|
}
|
|
|
|
if (preferencesResponse.data) {
|
|
setOrganizationPreferences(preferencesResponse.data);
|
|
}
|
|
|
|
setError(null);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err : new Error('Failed to fetch organization configuration'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (auth.loading || !auth.isAuthenticated || hasFetchedConfig.current) {
|
|
return;
|
|
}
|
|
hasFetchedConfig.current = true;
|
|
fetchConfig();
|
|
}, [auth.loading, auth.isAuthenticated, fetchConfig]);
|
|
|
|
const refreshConfig = useCallback(async () => {
|
|
await fetchConfig();
|
|
}, [fetchConfig]);
|
|
|
|
return (
|
|
<OrgConfigContext.Provider
|
|
value={{
|
|
orgContext,
|
|
userConfig,
|
|
loading,
|
|
error,
|
|
refreshConfig,
|
|
permissions,
|
|
user: auth.user,
|
|
organizationPricing,
|
|
organizationPreferences,
|
|
externalPbxIntegrationsEnabled:
|
|
organizationPreferences?.external_pbx_integrations_enabled ?? false,
|
|
}}
|
|
>
|
|
{children}
|
|
</OrgConfigContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useOrgConfig() {
|
|
const context = useContext(OrgConfigContext);
|
|
if (!context) {
|
|
throw new Error('useOrgConfig must be used within an OrgConfigProvider');
|
|
}
|
|
return context;
|
|
}
|