feat: configure page and form from CRM UI

This commit is contained in:
Hussain Nagaria 2025-10-02 21:57:24 +05:30
parent 6e1dd04819
commit cd366839fc
3 changed files with 253 additions and 174 deletions

View File

@ -0,0 +1,143 @@
import frappe
from frappe.integrations.utils import make_get_request
FB_GRAPH_API_BASE = "https://graph.facebook.com"
FB_GRAPH_API_VERSION = "v23.0"
def get_fb_graph_api_url(endpoint: str) -> str:
if endpoint.startswith("/"):
endpoint = endpoint[1:]
return f"{FB_GRAPH_API_BASE}/{FB_GRAPH_API_VERSION}/{endpoint}"
def sync_leads_from_facebook(access_token: str, lead_form_id: str) -> None:
url = get_fb_graph_api_url(f"/{lead_form_id}/leads")
last_synced_at = frappe.db.get_value(
"Lead Sync Source", {"facebook_lead_form": lead_form_id}, "last_synced_at"
)
timestamp = frappe.utils.data.get_timestamp(last_synced_at)
filtering = f"filtering=[{{'field':'time_created','operator':'GREATER_THAN','value':{timestamp}}}]"
if last_synced_at:
url = f"{url}?{filtering}"
leads = make_get_request(
url,
params={
"access_token": access_token,
"fields": "id,created_time,field_data",
"limit": 15000,
},
).get("data", [])
form_questions = frappe.db.get_all(
"Facebook Lead Form Question", filters={"parent": lead_form_id}, fields=["key", "mapped_to_crm_field"]
)
# Map form questions to CRM Lead fields
question_to_field_map = {
q["key"]: q["mapped_to_crm_field"] for q in form_questions if q["mapped_to_crm_field"]
}
for lead in leads:
lead_data = {item["name"]: item["values"][0] for item in lead["field_data"]}
crm_lead_data = {
question_to_field_map.get(k): v for k, v in lead_data.items() if k in question_to_field_map
}
crm_lead_data["source"] = "Facebook"
crm_lead_data["facebook_lead_id"] = lead["id"]
try:
frappe.get_doc(
{
"doctype": "CRM Lead",
**crm_lead_data,
}
).insert(ignore_permissions=True)
except frappe.UniqueValidationError:
# Skip duplicate leads based on facebook_lead_id
frappe.log_error("Duplicate lead skipped")
continue
frappe.db.set_value(
"Lead Sync Source", {"facebook_lead_form": lead_form_id}, "last_synced_at", frappe.utils.now()
)
@frappe.whitelist()
def fetch_and_store_pages_from_facebook(access_token: str) -> list[dict]:
if not access_token:
frappe.throw(frappe._("Access token is required"))
account_details = get_fb_account_details(access_token)
if not account_details.get("id"):
frappe.throw(frappe._("Invalid access token provided for Facebook."))
url = get_fb_graph_api_url("/me/accounts")
pages = make_get_request(url, params={"access_token": access_token}).get("data", [])
for page in pages:
page_id = page["id"]
already_synced = frappe.db.exists("Facebook Page", page_id)
if not already_synced:
create_facebook_page_in_db(page, account_details)
forms = fetch_and_store_leadgen_forms_from_facebook(page_id, page["access_token"])
page["forms"] = forms
return pages
def get_fb_account_details(access_token: str) -> dict:
url = get_fb_graph_api_url("me")
try:
response = make_get_request(url, params={"access_token": access_token})
except Exception as _:
frappe.throw(frappe._("Please check your access token"))
return response
def create_facebook_page_in_db(page: dict, account_details: dict) -> None:
frappe.get_doc(
{
"doctype": "Facebook Page",
"page_name": page["name"],
"id": page["id"],
"category": page["category"],
"access_token": page["access_token"],
"account_id": account_details["id"],
}
).insert(ignore_permissions=True)
def fetch_and_store_leadgen_forms_from_facebook(page_id: str, page_access_token: str) -> list[dict]:
fields = "id,name,questions"
url = get_fb_graph_api_url(f"/{page_id}/leadgen_forms")
forms = make_get_request(
url,
params={
"access_token": page_access_token,
"fields": fields,
"limit": 15000,
},
).get("data", [])
for form in forms:
form_id = form["id"]
already_synced = frappe.db.exists("Facebook Lead Form", form_id)
if already_synced:
continue
create_facebook_lead_form_in_db(form, page_id)
return forms
def create_facebook_lead_form_in_db(form: dict, page_id: str) -> None:
form_doc = frappe.get_doc(
{
"doctype": "Facebook Lead Form",
"form_name": form["name"],
"id": form["id"],
"page": page_id,
"questions": form["questions"],
}
)
frappe.errprint(form_doc.as_dict())
form_doc.insert(ignore_permissions=True)

View File

@ -2,18 +2,9 @@
# For license information, please see license.txt
import frappe
from frappe.integrations.utils import make_get_request
from frappe.model.document import Document
FB_GRAPH_API_BASE = "https://graph.facebook.com"
FB_GRAPH_API_VERSION = "v23.0"
def get_fb_graph_api_url(endpoint: str) -> str:
if endpoint.startswith("/"):
endpoint = endpoint[1:]
return f"{FB_GRAPH_API_BASE}/{FB_GRAPH_API_VERSION}/{endpoint}"
from crm.lead_syncing.doctype.lead_sync_source.facebook import sync_leads_from_facebook
class LeadSyncSource(Document):
@ -41,125 +32,6 @@ class LeadSyncSource(Document):
@frappe.whitelist()
def sync_leads(self):
if self.type == "Facebook" and self.access_token:
if not self.facebook_lead_form:
frappe.throw(frappe._("Please select a lead gen form before syncing!"))
sync_leads_from_facebook(self.get_password("access_token"), self.facebook_lead_form)
def sync_leads_from_facebook(access_token: str, lead_form_id: str) -> None:
url = get_fb_graph_api_url(f"/{lead_form_id}/leads")
last_synced_at = frappe.db.get_value(
"Lead Sync Source", {"facebook_lead_form": lead_form_id}, "last_synced_at"
)
timestamp = frappe.utils.data.get_timestamp(last_synced_at)
filtering = f"filtering=[{{'field':'time_created','operator':'GREATER_THAN','value':{timestamp}}}]"
if last_synced_at:
url = f"{url}?{filtering}"
leads = make_get_request(
url,
params={
"access_token": access_token,
"fields": "id,created_time,field_data",
"limit": 15000,
},
).get("data", [])
form_questions = frappe.db.get_all(
"Facebook Lead Form Question", filters={"parent": lead_form_id}, fields=["key", "mapped_to_crm_field"]
)
# Map form questions to CRM Lead fields
question_to_field_map = {
q["key"]: q["mapped_to_crm_field"] for q in form_questions if q["mapped_to_crm_field"]
}
for lead in leads:
lead_data = {item["name"]: item["values"][0] for item in lead["field_data"]}
crm_lead_data = {
question_to_field_map.get(k): v for k, v in lead_data.items() if k in question_to_field_map
}
crm_lead_data["source"] = "Facebook"
crm_lead_data["facebook_lead_id"] = lead["id"]
try:
frappe.get_doc(
{
"doctype": "CRM Lead",
**crm_lead_data,
}
).insert(ignore_permissions=True)
except frappe.UniqueValidationError:
# Skip duplicate leads based on facebook_lead_id
frappe.log_error("Duplicate lead skipped")
continue
frappe.db.set_value(
"Lead Sync Source", {"facebook_lead_form": lead_form_id}, "last_synced_at", frappe.utils.now()
)
def fetch_and_store_pages_from_facebook(access_token: str) -> None:
account_details = get_fb_account_details(access_token)
if not account_details.get("id"):
frappe.log_error("Invalid access token provided for Facebook.", "Lead Sync Source")
return
url = get_fb_graph_api_url("/me/accounts")
pages = make_get_request(url, params={"access_token": access_token}).get("data", [])
for page in pages:
page_id = page["id"]
already_synced = frappe.db.exists("Facebook Page", page_id)
if not already_synced:
create_facebook_page_in_db(page, account_details)
fetch_and_store_leadgen_forms_from_facebook(page_id, page["access_token"])
def get_fb_account_details(access_token: str) -> dict:
url = get_fb_graph_api_url("me")
return make_get_request(url, params={"access_token": access_token})
def create_facebook_page_in_db(page: dict, account_details: dict) -> None:
frappe.get_doc(
{
"doctype": "Facebook Page",
"page_name": page["name"],
"id": page["id"],
"category": page["category"],
"access_token": page["access_token"],
"account_id": account_details["id"],
}
).insert(ignore_permissions=True)
def fetch_and_store_leadgen_forms_from_facebook(page_id: str, page_access_token: str) -> None:
fields = "id,name,questions"
url = get_fb_graph_api_url(f"/{page_id}/leadgen_forms")
forms = make_get_request(
url,
params={
"access_token": page_access_token,
"fields": fields,
"limit": 15000,
},
).get("data", [])
for form in forms:
form_id = form["id"]
already_synced = frappe.db.exists("Facebook Lead Form", form_id)
if already_synced:
continue
create_facebook_lead_form_in_db(form, page_id)
def create_facebook_lead_form_in_db(form: dict, page_id: str) -> None:
form_doc = frappe.get_doc(
{
"doctype": "Facebook Lead Form",
"form_name": form["name"],
"id": form["id"],
"page": page_id,
"questions": form["questions"],
}
)
frappe.errprint(form_doc.as_dict())
form_doc.insert(ignore_permissions=True)

View File

@ -37,6 +37,18 @@
:type="field.type" :placeholder="field.placeholder" />
</div>
</div>
<div v-if="state.fbAccountPages.length">
<FormControl type="autocomplete" :placeholder="__('Select an account page')"
:options="state.fbAccountPages" :label="__('Facebook Page')"
v-model="syncSource.facebook_page" />
</div>
<div v-if="syncSource.facebook_page">
<FormControl type="autocomplete" :placeholder="__('Select a lead gen form')"
:options="leadFormsForSelectedPage" :label="__('Lead Form')"
v-model="syncSource.facebook_lead_form" />
</div>
</div>
</div>
@ -44,79 +56,131 @@
<div v-if="selectedSourceType" class="flex justify-between mt-auto">
<Button :label="__('Back')" variant="outline" :disabled="sources.insert.loading"
@click="emit('updateStep', 'source-list')" />
<Button :label="__('Create')" variant="solid" :loading="sources.insert.loading"
<Button v-if="state.fbPagesFetched" :label="__('Create')" variant="solid" :loading="sources.insert.loading"
@click="createLeadSyncSource" />
<Button v-else="state.fbPagesFetched" :label="__('Fetch Account Pages')" variant="solid"
:loading="state.fbPagesFetching" @click="getAccountPages(syncSource.access_token)" />
</div>
</div>
</template>
<script setup>
import { ref, inject, onMounted } from "vue";
import { FormControl, toast } from "frappe-ui";
import { ref, inject, onMounted, reactive, watch, computed } from "vue";
import { FormControl, toast, call } from "frappe-ui";
import CircleAlert from "~icons/lucide/circle-alert";
import { supportedSourceTypes } from "./leadSyncSourceConfig";
import EmailProviderIcon from "../EmailProviderIcon.vue";
const syncSource = ref({
name: "",
type: "",
access_token: "",
name: "",
type: "",
access_token: "",
facebook_page: "",
facebook_lead_form: "",
});
const emit = defineEmits()
const state = reactive({
fbPagesFetched: false,
fbPagesFetching: false,
fbAccountPages: [],
});
const emit = defineEmits();
const props = defineProps({
sourceData: {
type: Object,
default: () => ({}),
},
})
sourceData: {
type: Object,
default: () => ({}),
},
});
const selectedSourceType = ref(supportedSourceTypes[0]);
syncSource.value.type = selectedSourceType.value.name;
const sources = inject("sources");
const fbSourceFields = [
{
name: "name",
label: __("Name"),
type: "text",
placeholder: __("Add a name for your source"),
},
{
name: "access_token",
label: __("Access Token"),
type: "password",
placeholder: __("Enter your Facebook Access Token"),
},
{
name: "name",
label: __("Name"),
type: "text",
placeholder: __("Add a name for your source"),
},
{
name: "access_token",
label: __("Access Token"),
type: "password",
placeholder: __("Enter your Facebook Access Token"),
},
];
function handleSelect(sourceType) {
selectedSourceType.value = sourceType;
syncSource.value.type = sourceType.name;
selectedSourceType.value = sourceType;
syncSource.value.type = sourceType.name;
}
function createLeadSyncSource() {
sources.insert.submit({
...syncSource.value
}, {
onSuccess: () => {
toast.success(__('New Lead Syncing Source created successfully'))
emit('updateStep', 'edit-source', { ...syncSource.value })
},
onError: (error) => {
toast.error(error.messages[0] || __('Failed to create source'))
},
})
sources.insert.submit(
{
...syncSource.value,
facebook_page: syncSource.value.facebook_page.id,
facebook_lead_form: syncSource.value.facebook_lead_form.id,
},
{
onSuccess: () => {
toast.success(__("New Lead Syncing Source created successfully"));
emit("updateStep", "edit-source", { ...syncSource.value });
},
onError: (error) => {
toast.error(error.messages[0] || __("Failed to create source"));
},
},
);
}
const getAccountPages = (access_token) => {
state.fbPagesFetching = true;
call(
"crm.lead_syncing.doctype.lead_sync_source.facebook.fetch_and_store_pages_from_facebook",
{ access_token },
)
.then((data) => {
state.fbPagesFetched = true;
state.fbAccountPages = data.map((page) => ({
label: page.name,
value: page.id,
...page,
}));
})
.catch((error) => {
toast.error(error.messages[0] || __("Failed to fetch pages"));
})
.finally(() => {
state.fbPagesFetching = false;
});
};
const leadFormsForSelectedPage = computed(() => {
if (!state.fbAccountPages || !syncSource.value.facebook_page) {
return [];
}
const selectedPage = state.fbAccountPages.find(
(page) => page.id === syncSource.value.facebook_page.id,
);
return selectedPage.forms.map((form) => ({
label: form.name,
value: form.id,
...form,
}));
});
onMounted(() => {
if (props.sourceData?.name) {
Object.assign(syncSource.value, props.sourceData)
syncSource.value.name = `${syncSource.value.name} - Copy`
syncSource.value.enabled = true // Default to enabled
}
})
if (props.sourceData?.name) {
Object.assign(syncSource.value, props.sourceData);
syncSource.value.name = `${syncSource.value.name} - Copy`;
syncSource.value.enabled = true; // Default to enabled
}
});
</script>