feat: google my business
This commit is contained in:
parent
4eac6b9ff8
commit
69e8944437
|
|
@ -563,7 +563,7 @@ export class IntegrationsController {
|
|||
@Post('/gmb/:id')
|
||||
async saveGmb(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { id: string; accountId: string; locationName: string },
|
||||
@Body() body: { id: string; accountName: string; locationName: string },
|
||||
@GetOrgFromRequest() org: Organization
|
||||
) {
|
||||
return this._integrationService.saveGmb(org.id, id, body);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const GmbContinue: FC<{
|
|||
const { integration } = useIntegration();
|
||||
const [location, setSelectedLocation] = useState<null | {
|
||||
id: string;
|
||||
accountId: string;
|
||||
accountName: string;
|
||||
locationName: string;
|
||||
}>(null);
|
||||
const fetch = useFetch();
|
||||
|
|
@ -34,7 +34,7 @@ export const GmbContinue: FC<{
|
|||
}, []);
|
||||
|
||||
const setLocation = useCallback(
|
||||
(param: { id: string; accountId: string; locationName: string }) => () => {
|
||||
(param: { id: string; accountName: string; locationName: string }) => () => {
|
||||
setSelectedLocation(param);
|
||||
},
|
||||
[]
|
||||
|
|
@ -95,7 +95,7 @@ export const GmbContinue: FC<{
|
|||
(p: {
|
||||
id: string;
|
||||
name: string;
|
||||
accountId: string;
|
||||
accountName: string;
|
||||
locationName: string;
|
||||
picture: {
|
||||
data: {
|
||||
|
|
@ -111,7 +111,7 @@ export const GmbContinue: FC<{
|
|||
)}
|
||||
onClick={setLocation({
|
||||
id: p.id,
|
||||
accountId: p.accountId,
|
||||
accountName: p.accountName,
|
||||
locationName: p.locationName,
|
||||
})}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const topicTypes = [
|
|||
const callToActionTypes = [
|
||||
{
|
||||
label: 'None',
|
||||
value: '',
|
||||
value: 'NONE',
|
||||
},
|
||||
{
|
||||
label: 'Book',
|
||||
|
|
@ -84,7 +84,7 @@ const GmbSettings: FC = () => {
|
|||
<Select
|
||||
label="Call to Action"
|
||||
{...register('callToActionType', {
|
||||
value: '',
|
||||
value: 'NONE',
|
||||
})}
|
||||
>
|
||||
{callToActionTypes.map((t) => (
|
||||
|
|
@ -94,13 +94,15 @@ const GmbSettings: FC = () => {
|
|||
))}
|
||||
</Select>
|
||||
|
||||
{callToActionType && callToActionType !== 'CALL' && (
|
||||
<Input
|
||||
label="Call to Action URL"
|
||||
placeholder="https://example.com"
|
||||
{...register('callToActionUrl')}
|
||||
/>
|
||||
)}
|
||||
{callToActionType &&
|
||||
callToActionType !== 'NONE' &&
|
||||
callToActionType !== 'CALL' && (
|
||||
<Input
|
||||
label="Call to Action URL"
|
||||
placeholder="https://example.com"
|
||||
{...register('callToActionUrl')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{topicType === 'EVENT' && (
|
||||
<div className="flex flex-col gap-[10px] mt-[10px] p-[15px] border border-input rounded-[8px]">
|
||||
|
|
@ -116,11 +118,7 @@ const GmbSettings: FC = () => {
|
|||
type="date"
|
||||
{...register('eventStartDate')}
|
||||
/>
|
||||
<Input
|
||||
label="End Date"
|
||||
type="date"
|
||||
{...register('eventEndDate')}
|
||||
/>
|
||||
<Input label="End Date" type="date" {...register('eventEndDate')} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-[10px]">
|
||||
<Input
|
||||
|
|
@ -172,7 +170,7 @@ export default withProvider({
|
|||
if (items.length > 0 && items[0].length > 1) {
|
||||
return 'Google My Business posts can only have one image';
|
||||
}
|
||||
|
||||
|
||||
// Check for video - GMB doesn't support video in local posts
|
||||
if (items.length > 0 && items[0].length > 0) {
|
||||
const media = items[0][0];
|
||||
|
|
@ -190,4 +188,3 @@ export default withProvider({
|
|||
},
|
||||
maximumCharacters: 1500,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -371,7 +371,7 @@ export class IntegrationService {
|
|||
async saveGmb(
|
||||
org: string,
|
||||
id: string,
|
||||
data: { id: string; accountId: string; locationName: string }
|
||||
data: { id: string; accountName: string; locationName: string }
|
||||
) {
|
||||
const getIntegration = await this._integrationRepository.getIntegrationById(
|
||||
org,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export class GmbSettingsDto {
|
|||
|
||||
@IsOptional()
|
||||
@IsIn([
|
||||
'NONE',
|
||||
'BOOK',
|
||||
'ORDER',
|
||||
'SHOP',
|
||||
|
|
@ -16,6 +17,7 @@ export class GmbSettingsDto {
|
|||
'CALL',
|
||||
])
|
||||
callToActionType?:
|
||||
| 'NONE'
|
||||
| 'BOOK'
|
||||
| 'ORDER'
|
||||
| 'SHOP'
|
||||
|
|
@ -65,4 +67,3 @@ export class GmbSettingsDto {
|
|||
@IsString()
|
||||
offerTerms?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ export class GmbProvider extends SocialAbstract implements SocialProvider {
|
|||
id: string;
|
||||
name: string;
|
||||
picture: { data: { url: string } };
|
||||
accountId: string;
|
||||
accountName: string;
|
||||
locationName: string;
|
||||
}> = [];
|
||||
|
||||
|
|
@ -206,6 +206,11 @@ export class GmbProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
if (locationsData.locations) {
|
||||
for (const location of locationsData.locations) {
|
||||
// location.name is in format: locations/{locationId}
|
||||
// We need the full path: accounts/{accountId}/locations/{locationId}
|
||||
const locationId = location.name.replace('locations/', '');
|
||||
const fullResourceName = `${accountName}/locations/${locationId}`;
|
||||
|
||||
// Get profile photo if available
|
||||
let photoUrl = '';
|
||||
try {
|
||||
|
|
@ -235,17 +240,21 @@ export class GmbProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
|
||||
allLocations.push({
|
||||
id: location.name, // format: locations/{locationId}
|
||||
// id is the full resource path for the v4 API: accounts/{accountId}/locations/{locationId}
|
||||
id: fullResourceName,
|
||||
name: location.title || 'Unnamed Location',
|
||||
picture: { data: { url: photoUrl } },
|
||||
accountId: accountName,
|
||||
accountName: accountName,
|
||||
locationName: location.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue with other accounts if one fails
|
||||
console.error(`Failed to fetch locations for account ${accountName}:`, error);
|
||||
console.error(
|
||||
`Failed to fetch locations for account ${accountName}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -254,11 +263,13 @@ export class GmbProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
async fetchPageInformation(
|
||||
accessToken: string,
|
||||
data: { id: string; accountId: string; locationName: string }
|
||||
data: { id: string; accountName: string; locationName: string }
|
||||
) {
|
||||
// Fetch location details
|
||||
// data.id is the full resource path: accounts/{accountId}/locations/{locationId}
|
||||
// data.locationName is the v1 API format: locations/{locationId}
|
||||
// Fetch location details using the v1 API format
|
||||
const locationResponse = await fetch(
|
||||
`https://mybusinessbusinessinformation.googleapis.com/v1/${data.id}?readMask=name,title,storefrontAddress,metadata`,
|
||||
`https://mybusinessbusinessinformation.googleapis.com/v1/${data.locationName}?readMask=name,title,storefrontAddress,metadata`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
|
|
@ -271,7 +282,7 @@ export class GmbProvider extends SocialAbstract implements SocialProvider {
|
|||
let photoUrl = '';
|
||||
try {
|
||||
const mediaResponse = await fetch(
|
||||
`https://mybusinessbusinessinformation.googleapis.com/v1/${data.id}/media`,
|
||||
`https://mybusinessbusinessinformation.googleapis.com/v1/${data.locationName}/media`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
|
|
@ -296,6 +307,7 @@ export class GmbProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
|
||||
return {
|
||||
// Return the full resource path as id (for v4 Local Posts API)
|
||||
id: data.id,
|
||||
name: locationData.title || 'Unnamed Location',
|
||||
access_token: accessToken,
|
||||
|
|
@ -318,7 +330,7 @@ export class GmbProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
const information = await this.fetchPageInformation(accessToken, {
|
||||
id: requiredId,
|
||||
accountId: findPage.accountId,
|
||||
accountName: findPage.accountName,
|
||||
locationName: findPage.locationName,
|
||||
});
|
||||
|
||||
|
|
@ -348,8 +360,12 @@ export class GmbProvider extends SocialAbstract implements SocialProvider {
|
|||
topicType: settings?.topicType || 'STANDARD',
|
||||
};
|
||||
|
||||
// Add call to action if provided
|
||||
if (settings?.callToActionType && settings?.callToActionUrl) {
|
||||
// Add call to action if provided (and not NONE)
|
||||
if (
|
||||
settings?.callToActionType &&
|
||||
settings.callToActionType !== 'NONE' &&
|
||||
settings?.callToActionUrl
|
||||
) {
|
||||
postBody.callToAction = {
|
||||
actionType: settings.callToActionType,
|
||||
url: settings.callToActionUrl,
|
||||
|
|
@ -466,7 +482,17 @@ export class GmbProvider extends SocialAbstract implements SocialProvider {
|
|||
|
||||
// Use the Business Profile Performance API
|
||||
const response = await fetch(
|
||||
`https://businessprofileperformance.googleapis.com/v1/${id}:getDailyMetricsTimeSeries?dailyMetric=WEBSITE_CLICKS&dailyMetric=CALL_CLICKS&dailyMetric=BUSINESS_DIRECTION_REQUESTS&dailyMetric=BUSINESS_IMPRESSIONS_DESKTOP_MAPS&dailyMetric=BUSINESS_IMPRESSIONS_MOBILE_MAPS&dailyRange.startDate.year=${dayjs(startDate).year()}&dailyRange.startDate.month=${dayjs(startDate).month() + 1}&dailyRange.startDate.day=${dayjs(startDate).date()}&dailyRange.endDate.year=${dayjs(endDate).year()}&dailyRange.endDate.month=${dayjs(endDate).month() + 1}&dailyRange.endDate.day=${dayjs(endDate).date()}`,
|
||||
`https://businessprofileperformance.googleapis.com/v1/${id}:getDailyMetricsTimeSeries?dailyMetric=WEBSITE_CLICKS&dailyMetric=CALL_CLICKS&dailyMetric=BUSINESS_DIRECTION_REQUESTS&dailyMetric=BUSINESS_IMPRESSIONS_DESKTOP_MAPS&dailyMetric=BUSINESS_IMPRESSIONS_MOBILE_MAPS&dailyRange.startDate.year=${dayjs(
|
||||
startDate
|
||||
).year()}&dailyRange.startDate.month=${
|
||||
dayjs(startDate).month() + 1
|
||||
}&dailyRange.startDate.day=${dayjs(
|
||||
startDate
|
||||
).date()}&dailyRange.endDate.year=${dayjs(
|
||||
endDate
|
||||
).year()}&dailyRange.endDate.month=${
|
||||
dayjs(endDate).month() + 1
|
||||
}&dailyRange.endDate.day=${dayjs(endDate).date()}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
|
|
@ -497,7 +523,10 @@ export class GmbProvider extends SocialAbstract implements SocialProvider {
|
|||
const dataPoints =
|
||||
series.timeSeries?.datedValues?.map((dv: any) => ({
|
||||
total: dv.value || 0,
|
||||
date: `${dv.date.year}-${String(dv.date.month).padStart(2, '0')}-${String(dv.date.day).padStart(2, '0')}`,
|
||||
date: `${dv.date.year}-${String(dv.date.month).padStart(
|
||||
2,
|
||||
'0'
|
||||
)}-${String(dv.date.day).padStart(2, '0')}`,
|
||||
})) || [];
|
||||
|
||||
if (dataPoints.length > 0) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue