I have a custom object "Task Tracking" with 3 custom fields:
1) Lookup to user object.
2) Number of open task (number field)
3) Number if closed task (number field)
When a task is created by user A with let's stay status is IN PROGRESS, I need to create record in Task Tracking object with these details :
1) Lookup field = user A
2) Number of open task = 1
3) Number of closed task = 0;
Now next time, when same user A creates another task, the new Task Tracking record shouldn't be created but it should only update Number of task field.
I have tried this much. I was able to create Task Tracking record whenever a Task is created but It was creating new Task Tracking Object for every Task that I am creating
trigger TrackTask2 on Task (before insert) {
List<sujya__Task_Tracking__c> li = new List<sujya__Task_Tracking__c>();
sujya__Task_Tracking__c s = new sujya__Task_Tracking__c();
if(Trigger.isBefore && Trigger.isInsert){
for(Task t:Trigger.new){
s.sujya__User__c = t.CreatedById;
li.add(s);
}
insert li;
}
}
You should query the system first to see if one exists. If it does, then update it, if it does not then create one.
trigger TrackTask2 on Task (before insert) {
Set<Id> users = new Set<Id>();
for (Task t : Trigger.new) {
users.add(t.ownerId); //might be t.whoId;
}
List<sujya__Task_Tracking__c> existingTrackers = [SELECT Id, sujya__User__c, number_open_task__c, number_closed_task__c FROM sujya__Task_Tracking__c WHERE sujya__User__c IN :users];
Map<Id, sujya__Task_Tracking__c> userTrackerMap = new Map<Id, sujya__Task_Tracking__c>();
for (sujya__Task_Tracking__c tracker : existingTrackers) {
userTrackerMap.put(tracker.sujya__User__c, tracker);
}
for (Task t : Trigger.new) {
sujya__Task_Tracking__c userTracker = existingTrackers.get(t.ownerId);
if (userTracker == null) {
userTracker = new sujya__Task_Tracking__c();
userTracker.number_open_task__c = 1;
userTracker.sujya__User__c = t.ownerId;
} else {
userTracker.number_open_task__c += 1;
}
userTrackerMap.put(userTracker.sujya__User__c, userTracker);
}
upsert userTrackerMap.getValues();
}
This will get you most of the way, but you're going to have to add logic for when a task is closed to decrement the number of open tasks and the number of close tasks.
And please follow trigger best practices by using a handler class
Related
I have an Apex class whose purpose it is to retrieve and delete overdue tasks on the contact role (related to an account) that the user just called. I need to modify it so that it queries for all overdue tasks assigned to the user on ALL contact roles on the account but am struggling to get the right query.
Here is a portion of the code in question, the part I think is most relevant:
/***************************************************
Brief Description: Deletes overdue Tasks or Events.
****************************************************/
public class DSDenali_DeleteOverDueActivities {
private static List<Task> followUpTasksToDelete = new List<Task>();
private static List<Event> followUpEventsToDelete = new List<Event>();
private static Map<Id, Set<String>> ownerOfActivities = new Map<Id, Set<String>>();
#InvocableMethod (label = 'DialSource Denali Delete Overdue Activities')
public static void gatherData(List<Data> requests)
{
Map<Id, String> results = new Map<Id, String>();
for (Data request : requests)
results.put(request.contactRoleID, request.assignedTo);
for (Id key : results.keySet())
{
Set<String> assignedToValues = parseAssignedTo(results.get(key));
System.debug('assignedToValues: ' + assignedToValues);
ownerOfActivities.put(key, assignedToValues);
System.debug(ownerOfActivities);
}
queryAndFilterData();
deleteOverdueActivities();
}
//Query for the Tasks and Events and filter the ones to delete
private static void queryAndFilterData()
{
List<Contact_Role__c> contactRoles = [SELECT Id,
(SELECT Id, Owner.UserRole.Name, OwnerId FROM Tasks WHERE status != 'Completed' AND ActivityDate <= :system.TODAY() AND Type__c = 'Outbound Call'),
(SELECT Id, Owner.UserRole.Name, OwnerId, Description FROM Events WHERE EndDateTime <= :system.NOW())
FROM Contact_Role__c
WHERE Id IN :ownerOfActivities.keySet()];
for (Contact_Role__c contactRole : contactRoles)
{
for (Task currentTask : contactRole.Tasks)
{
if (ownerOfActivities.get(contactRole.Id).contains(currentTask.OwnerId))
{
if (currentTask.OwnerId != '0050B000006ET37' && currentTask.Owner.UserRole != NULL && Pattern.matches('.*Altair.*', currentTask.Owner.UserRole.Name))
followUpTasksToDelete.add(currentTask);
else if (currentTask.OwnerId == '0050B000006ET37')
followUpTasksToDelete.add(currentTask);
else
continue;
}
else if (ownerOfActivities.get(contactRole.Id).contains('ALL'))
{
if (currentTask.Owner.UserRole != NULL && Pattern.matches('.*Altair.*', currentTask.Owner.UserRole.Name))
followUpTasksToDelete.add(currentTask);
else
continue;
}
}
for (Event currentEvent : contactRole.Events)
{
if (ownerOfActivities.get(contactRole.Id).contains(currentEvent.OwnerId) && currentEvent.Description == NULL)
{
if (currentEvent.OwnerId != '0050B000006ET37' && currentEvent.Owner.UserRole != NULL && Pattern.matches('.*Altair.*', currentEvent.Owner.UserRole.Name))
followUpEventsToDelete.add(currentEvent);
else if (currentEvent.OwnerId == '0050B000006ET37')
followUpEventsToDelete.add(currentEvent);
else
continue;
}
else if (ownerOfActivities.get(contactRole.Id).contains('ALL') && currentEvent.Description == NULL)
{
if (currentEvent.Owner.UserRole != NULL && Pattern.matches('.*Altair.*', currentEvent.Owner.UserRole.Name))
followUpEventsToDelete.add(currentEvent);
else
continue;
}
}
}
}
//Delete overdue Events/Tasks
private static void deleteOverdueActivities()
{
try{
delete followUpTasksToDelete;
}
catch (DmlException e){
System.debug('The following error occured (DSDenali_DeleteOverDueActivities): ' + e);
}
try{
delete followUpEventsToDelete;
}
catch (DmlException e){
System.debug('The following error occured (DSDenali_DeleteOverDueActivities): ' + e);
}
}
//Parse the CSVs of possible owners
private static Set<String> parseAssignedTo(String assignedTo)
{
Set<String> assignedToValues = new Set<String>();
assignedToValues.addAll(assignedTo.deleteWhitespace().split(','));
return assignedToValues;
}
public class Data
{
#InvocableVariable (required=true)
public String assignedTo;
#InvocableVariable (required=false)
public Id contactRoleID;
}
}
(updated after OP posted more code and asked for code review)
It's not bad code, could use some comments. Consider posting it in https://codereview.stackexchange.com/ (although not many SF-related posts end up there) or on https://salesforce.stackexchange.com
gatherData()
Your input variable (after some parsing) is Map<Id, Set<String>> where key is contact role's id. That set of strings for users (owners) is bit misleading. At a glance you immediately ask yourself why it can't be Set<Id>. Only deep down in the code you see that apparently "All" is one of allowed values. This is... not great. I'd be tempted to make 2 methods here, one taking legit Map<Id, Set<Id>> and other taking simply Set<Id> if you know that you're effectively skipping the second parameter.
queryAndFilterData()
You have only one query and it's not in a loop, very good. My trick (from before edit) won't work for you (you don't really have the Account__c or however is the field called anywere in the input, you have only record ids. If you want to check / delete all tasks for roles under this account cleanest might be to use two queries
// 1st the helper to collect all accounts...
Set<Id> allAccounts = new Map<Id, Account>([SELECT Id
FROM Account
WHERE Id IN (SELECT Account__c FROM Contact_Role__c WHERE Id IN :ownerOfActivities.keyset()]).keyset();
// then the outline of the main query would be bit like this
SELECT Id,
(SELECT ... FROM Tasks WHERE ...),
(SELECT ... FROM Events WHERE ...)
FROM Contact_Role__c
WHERE Account__c IN :allAccounts
AND ...
I'd check how much of this filtering logic could be pushed to the query itself rather than manually inspecting each returned row. I mean look at that:
Imagine we're going the simple route (ignoring concept of "All" users) and let's say you have another Set<Id> allUsers; variable (composed out of all ids mentioned in all your data pieces)
The query for Tasks could become something as simple as
(SELECT Id
FROM Tasks
WHERE Status != 'Completed'
AND ActivityDate <= TODAY
AND Type__c = 'Outbound Call'
AND OwnerId IN :allUsers
AND (OwnerId = '0050B000006ET37' OR Owner.UserRole.Name LIKE '%.Altair.%')
)
You still have to loop through them to verify whether each can really really be deleted (it's not enough to match out of all users, it has to check also if it's OK for user on this particular Contact_Role__c, right?) but something like that should return less rows and no more regular expression matching... should be bit faster.
I wouldn't have a magic variable for that one special owner's id like that. Ideally there would be something else that describes this special user (role? profile? custom field on User record? permission to "Author Apex" in profile?). At the very least move it to helper Id variable at top of the file so it's not copy-pasted all over the place. And ask your business user what should happen if that guy (default task owner? some integration account?) ever leaves the company because poo poo will hit the propeller hard.
If you'll get comfortable with this version of the query the "ALL" version would become even simpler? No "all users", no "magic id" & job done?
(SELECT Id
FROM Tasks
WHERE Status != 'Completed'
AND ActivityDate <= TODAY
AND Type__c = 'Outbound Call'
Owner.UserRole.Name LIKE '%.Altair.%'
)
Don't trust random guy on internet who doesn't know your business process but yeah, there's some room for improvement. Just test thoroughly :)
deleteOverdueActivities()
This is not great try-catch pattern. You just raise it in debug log but silently swallow the error. Make it fail hard (let the error bubble up to user) or do some proper error handling like inserting something to helper Log__c object maybe or sending an email / chatter post to admin...
parseAssignedTo()
Not bad. I expect it explodes horribly when you pass a null to it. You're kind of protected from it by marking the variable required in last lines but I think this annotation applies only to Flows. Calling it from other Apex code isn't protected enough.
I am trying to write a trigger to update the 'Name' field (so WhoId in the API) to the 'Name' (ContactId) of the custom Primary Contact field of the Account the Task is related to.
trigger updateNameToPrimary on Task (after insert, after update) {
for(Task t : Trigger.new) {
t.WhoID = [SELECT Account.Id
FROM Account
WHERE Id = :t.Id].Custom_Primary_Contact__c;
}
}
I have been doing some testing and don't think it's working and cannot figure out why. Just looking for a point in the right direction as I still am in the learning process.
Change Trigger from "after insert after update" To "before insert before update".
Something like this
trigger updateNameToPrimary on Task (before insert, before update) {
set<Id> accIdSet = new set<Id>();
for(Task t : Trigger.new) {
if(t.AccountId!=null)
accIdSet.add(t.AccountId);
}
map<Id,Account> accMap = new map<Id,Account>([select Custom_Primary_Contact__c from Account where Id in:accIdSet]);
for(Task t : Trigger.new) {
if(t.AccountId!=null && accMap.containsKey(t.AccountId))
t.WhoID = accMap.get(t.AccountId).Custom_Primary_Contact__c;
}
}
i have a code segment in my DataBase.Batchable
if(pageToken!=null)
deleteEvents(accessToken , pageToken,c.CalendarId__c);
}
and delete event function code is
public static void deleteEvents(String accessToken,String pageToken,String CalendarId){
while(pageToken != null)
{ String re = GCalendarUtil.doApiCall(null,'GET','https://www.googleapis.com/calendar/v3/calendars/'+CalendarId+'/events?pageToken='+pageToken,accessToken);
System.debug('next page response is'+ re);
re = re.replaceAll('"end":','"end1":');
re = re.replaceAll('"dateTime":','"dateTime1":');
JSON2Apex1 aa = JSON2Apex1.parse(re);
List<JSON2Apex1.Items> ii = aa.items ;
System.debug('size of ii'+ii.size());
pageToken = aa.nextPageToken ;
List<String> event_id =new List<String>();
if(ii!= null){
for(JSON2Apex1.Items i: ii){
event_id.add(i.id);
}
}
for(String ml: event_id)
System.debug('Hello Worlds'+ml);
for(String s:event_id)
{
GCalendarUtil.doApiCall(null,'DELETE','https://www.googleapis.com/calendar/v3/calendars/'+CalendarId+'/events/'+s,accessToken);
}
}
}
because there were nearly about 180 event thats why i am getting this error but for deleting them i have an option that i create a custom object and a text field (id of event)in it and then create one more Database.Batchable class for deleting them and pass it as a batch of 9 or any other method so that i am able to do more than 100 callouts with in an Database.Batchable interface??
10 is the max. And it doesn't seem like Google's calendar API supports mass delete.
You could try chunking your batch job (the size of list of records passed to each execute() call). For example something like this:
global Database.QueryLocator start(Database.BatchableContext bc){
return Database.getQueryLocator([SELECT Id FROM Event]); // anything that loops through Events and not say Accounts
}
global void execute(Database.BatchableContext BC, List<Account> scope){
// your deletes here
}
Database.executeBatch(new MyBatchClass(), 10); // this will make sure to work on 10 events (or less) at a time
Another option would be to read up about daisy-chaining of batches. In execute you'd be deleting up to 10, in finish() method you check again if there's still more work to do and you can fire a batch again.
P.S. Don't use hardcoded "10". Use Limits.getCallouts() and Limits.getLimitCallouts() so it will automatically update if Salesforce ever increases the limit.
In a trigger you could for example set up 10 #future methods, each with fresh limit of 10 callouts... Still batch sounds like a better idea.
If possible, could anyone please direct me towards the right direction? The second code (Batch Apex) is just not compiling. Currently the error is,
Error: Compile Error: Invalid type: updateContactOnEmailOptOutChangeScheduler
at line 63 column 73
But I think there are other issues to that I can't seem to make it right.
On a contact update, the trigger updates all duplicate contacts with new value of "Email Opt Out" if this field has been updated. Also, the trigger only updates duplicate contacts that have a HasOptedOutOfEmail value different from one being updated. Now my task is to convert this requirement from trigger (that was written and tested by my colleague) to Batch Apex. First is the original trigger. Second is the code I just wrote in the format of batch apex.
Original Trigger Code
trigger updateContactOnEmailOptOutChange on Contact (after update) {
//Initialize lists and maps
List<Contact> duplicateContacts = new List<Contact>();
Map<String, Contact> contactEmailMap = new Map<String, Contact>();
Map<Id, Contact> contactIdMap = new Map<Id, Contact>();
//Build a map with contacts to update. Only select the ones that have a different "Email Opt Out" value from the contact being updated.
for (Integer i = 0; i < Trigger.new.size(); i++) {
if (Trigger.old[i].HasOptedOutOfEmail != Trigger.new[i].HasOptedOutOfEmail) {
contactEmailMap.put(Trigger.old[i].email, Trigger.new[i]);
contactIdMap.put(Trigger.old[i].id, Trigger.new[i]);
}
}
//Only go through this process if "Email Opt Out" (HasOptedOutofEmail) was updated.
If (contactIdMap.size()>0) {
//Query the database and look for all contacts with a duplicate email address (same email as the contact currently being updated).
for (Contact dupContact : [SELECT Id, Name, Email, HasOptedOutOfEmail
FROM Contact
WHERE Email IN : contactEmailMap.KeySet()
AND Id NOT IN : contactIdMap.KeySet()]) {
Contact contact = contactEmailMap.get(dupContact.Email);
If (dupContact.HasOptedOutOfEmail <> contact.HasOptedOutOfEmail) {
dupContact.HasOptedOutOfEmail = contact.HasOptedOutOfEmail;
duplicateContacts.add(dupContact);
}
}
//If any duplicate contacts were found, update all duplicate contacts with the new HasOptedOutOfEmail value.
If (duplicateContacts.size()>0) update duplicateContacts;
}
}
BATCH APEX
global class updateContactOnEmailOptOutChange implements Database.Batchable<sObject>
{
global string query;
global updateContactOnEmailOptOutChange()
{
query = 'SELECT id,Name, Email, HasOptedOutofEmail from Contact where HasOptedOutofEmail=true';
}
global Database.QueryLocator start(Database.BatchableContext BC)
{
return Database.getQueryLocator(query);
}
global void execute(Database.BatchableContext BC, List <sObject> duplicateContacts)
{
Map<String, Contact> contactEmailMap = new Map<String, Contact>();
Map <Id, Contact> contactIdMap = new Map<Id, Contact>();
// Build a map with contacts to update. Only select the ones that have a different "Email Opt Out" value from the contact being updated.
if(trigger.isUpdate){
for(Integer i=0; i<Trigger.new.size();i++)
{
if(Trigger.old[i].HasOptedOutOfEmail != Trigger.new[i].HasOptedOutOfEmail)
{
contactEmailMap.put(Trigger.old[i].email, Trigger.new[i]);
contactIdMap.put(Trigger.old[i].id, Trigger.new[i]);
}
}
if(contactidMap.size()>0)
{
//Query the database and look for all contacts with a duplicate email address(same email as the contact currently being updated)
for (Contact dupContact: [SELECT Id, Name, Email, HasOptedOutofEmail
FROM Contact
WHERE Email IN: contactEmailMap.KeySet()
AND Id NOT IN: contactIdMap.KeySet()])
{
Contact contact=contactEmailMap.get(dupContact.Email);
If(dupContact.HasOptedOutOfEmail <> contact.HasOptedOutOfEmail)
{
dupContact.HasOptedOutOfEmail = contact.HasOptedOutOfEmail;
duplicateContacts.add(dupContact);
}
}
// if any duplicate contacts were found, update all duplicate contacts with the new HasOptedOutOFEmail value.
If(duplicateContacts.size<>0) update duplicateContacts;
}
}
}
//The batch process has completed successfully. Schedule next batch.
global void finish(Database.BatchableContext BC){
// //Build the system time of now + 300 seconds to schedule the batch apex.
Datetime sysTime = System.now();
sysTime = sysTime.addSeconds(300);
String chron_exp=''+sysTime.second()+''+sysTime.minute()+''+sysTime.hour()+''+sysTime.day()+''+sysTime.month()+'?'+sysTime.year();
system.debug(chron_exp);
updateContactOnEmailOptOutChangeScheduler scheduleFieldUpdate = new updateContactOnEmailOptOutChangeScheduler();
//Schedule the next job, and give it the system time so name is unique
System.schedule('New Email Update Job'+sysTime.getTime(),chron_exp,scheduleFieldUpdate);
}
}
Your batch apex is including references to the Trigger class that is only valid inside a trigger. The compile error message says there is an invalid type on line 63. Check the line numbers but that likely points to the reference to Trigger
I wrote a trigger that places the Account Owner Name on a Case created for that Account. It works and performs also in my bulk test of 200. Here is the code:
trigger CaseBeforeInsertUpdate on Case (before insert, before update) {
Set<Id> accountIds = new Set<Id>();
Set<Id> accountOwnerIds = new Set<Id>();
for (Case c : Trigger.new) {
if(c.AccountId != null) {
accountIds.add(c.AccountId);
}
}
Map<Id,Account> accountMap = new Map<Id, Account>([select Id, OwnerId from Account where Id IN :accountIds]);
for (Account a : accountMap.values()) {
if(a.OwnerId != null) {
accountOwnerIds.add(a.OwnerId);
}
}
Map<Id, User> userMap = new Map<Id,User>([select Name from User where Id IN :accountOwnerIds]);
if(userMap.size() > 0) {
for(Case c: Trigger.new) {
c.MerchantOwner__c = userMap.get(accountMap.get(c.AccountId).OwnerId).name;
}
}
}
By accident I discovered a bug and I can't figure out what is going wrong. If I go to a Case listview (i.e. My open cases) and select multiple Cases and Close them, I get an error: System.NullPointerException: Attempt to de-reference a null object for the row that updates the MerchantOwner field. When I mass close cases in my testclass everything works fine..
My best guess is I'm trying to do this for a Case that has no Account attached to it but as far as I see I try to not have these Cases updated by not adding them to the accountIds Set in the first place.
Does anyone know what I am doing wrong? Any help would be greatly appreciated.
I would change the following For loop from:
for(Case c: Trigger.new) {
c.MerchantOwner__c = userMap.get(accountMap.get(c.AccountId).OwnerId).name;
}
To
for(Case c: Trigger.new) {
if (c.AccountId != null // Make sure there is an Account linked to the Case
&& accountMap.ContainsKey(c.AccountId) // Make sure our Account query captured it
&& accountMap.get(c.AccountId).OwnerId != null // Make sure that account has an owner
&& usermap.ContainsKey(accountMap.get(c.AccountId).Ownerid) // Finally make sure our User query found the owner
){
c.MerchantOwner__c = userMap.get(accountMap.get(c.AccountId).OwnerId).name;
}
}