Routing calls based on office hours and holidays - help

Milos Boksan

Gold Partner
Advanced Certified
Joined
Jun 26, 2024
Messages
42
Reaction score
17
Hello everyone,
I am trying to set up routing in V20 to fit this general dialplan:
dailplan.png
There are sometimes up to 50 users in the 3cx system, and it would be nice if this can be set up without using CFD if possible.

Any help is welcome
Thanks
 
You will need to use a script to do this for any DIDs.

We can help with that.
 
Can you provide me of any sort of example script that I can build upon?
Would it be easier to do this through the Call Flow Designer, and just import the script into the 3CX client after it is generated?
 
Can you provide me of any sort of example script that I can build upon?
Would it be easier to do this through the Call Flow Designer, and just import the script into the 3CX client after it is generated?
The CFDesigner currently doesn't support the latest v20 stuff like departments and all. It would need to be a manual script.
 
Hello everyone,
I am trying to set up routing in V20 to fit this general dialplan:
View attachment 45263
There are sometimes up to 50 users in the 3cx system, and it would be nice if this can be set up without using CFD if possible.

Any help is welcome
Thanks
This is exactly what I'm looking for as well. The default holiday script and timebasedcallscript don't cover this very basic scenario. Has anybody put together a working script? I guess it has to be a combination of the 2 earlier mentioned scripts?
 
"ANY DID": does it stand for "any external call" ? Then the holiday.cs is the correct starting point.
A command "return true;" stops the further processing of the call. If you add a check for "out of office", you should be done.

Modified holiday.cs (not tested!), it needs the sound files to be uploaded along with the script file:

C#:
#nullable disable
using CallFlow;
using System;
using System.Threading;
using System.Linq;
using System.Threading.Tasks;
using TCX.Configuration;
using TCX.PBXAPI;
using System.Collections.Generic;

namespace dummy
{
    //this handler plays holiday prompt as specified for destination
    public class PlayDestinationHolidayPromptBeforeRouting : ScriptBase<PlayDestinationHolidayPromptBeforeRouting>
    {
        //reference implementation of trunk routing.
        DestinationStruct FindDefaultDestination(ExternalLine trunk, string callerID, string DID)
        {
            DestinationStruct retval = new();
            string[] range;
            foreach (var a in trunk.RoutingRules)
            {
                bool match = (a.Conditions.Condition.Type == RuleConditionType.BasedOnDID &&
                (
                    a.Data == DID
                    || (a.Data.StartsWith('*') && DID.EndsWith(a.Data[1..]))
                ))
                ||
                (
                    a.Conditions.Condition.Type == RuleConditionType.BasedOnCallerID &&
                    a.Data.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                    .Any
                    (x =>
                        x == "*"
                        || x == callerID
                        || x.StartsWith('*') && callerID.EndsWith(x[1..])
                        || x.EndsWith('*') && callerID.StartsWith(x[..^1])
                        || x.StartsWith('*') && x.EndsWith('*') && callerID.Contains(x[1..^1])
                        || ((range = x.Split("-", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)).Length == 2 ?
                        (range[0].Length == callerID.Length && range[1].Length == callerID.Length) && (range[0].CompareTo(callerID) <= 0 && range[1].CompareTo(callerID) >= 0) : false)
                    )
                )
                ||
                (a.Conditions.Condition.Type == RuleConditionType.ForwardAll);

                if (match)
                {
                    retval.CopyFrom(a.ForwardDestinations.OfficeHoursDestination);
                    break;
                }
            }
            return retval;
        }

        public override async Task<bool> StartAsync()
        {
            if(MyCall.Caller.DN is ExternalLine externalLine && MyCall.IsInbound) //only from trunk
            {
                //get the inbound target
                var defaultDestination = FindDefaultDestination(externalLine, MyCall.Caller.CallerID, MyCall.Caller.CalledNumber).Internal;
                //if the destination is Internal
                if (defaultDestination != null)
                {
                    var timeBasedroute = defaultDestination.GetTimeBasedRoutingInfo();

                    //if destination is currently "on holiday" and holiday prompt is defined, we play it
                    //if (timeBasedroute.reason == CallControlAPI.DivertReason.Holiday && !string.IsNullOrWhiteSpace(timeBasedroute.holiday?.HolidayPrompt))
                        
                    // MODIFIED - REQUIRES UPLOAD OF "VACATION.wav"
                    if (timeBasedroute.reason == CallControlAPI.DivertReason.Holiday)
                    {
                        await MyCall.AssureMedia()
                            .ContinueWith(x => MyCall.PlayPrompt(null, new[] {"VACATION.wav"}, PlayPromptOptions.Blocked))
                            .Unwrap();
                        return true;  // <<<=== STOP default routing procedure !!
                    }
                    
                    var currentTime = externalLine.Now(out var utc, out var timezone, out var groupmode);
                    var g = ps.GetAll<TCX.Configuration.Group>().FirstOrDefault(grp => grp.Number == defaultDestination.Number);

                    // Intercept calls during the specified schedule
                    if (CommonRoutingExtensions.IsOutOfOffice(g,currentTime))
                    {
                        await MyCall.AssureMedia()
                            .ContinueWith(x => MyCall.PlayPrompt(null, new[] {"OFFICE_CLOSED.wav"}, PlayPromptOptions.Blocked))
                            .Unwrap();
                        return true;  // <<<=== STOP default routing procedure !!
                    }
        
                }
            }
            return false;//we always return false to continue default trunk routing procedure.
        }
    }
}
 
Sorry for the short description in the diagram fields. I was trying to keep it "to the point"
What I meant by with "ANY DID" was that whichever DID recieved the call it would behave in this way as described in the diagram. So I wanted to put behavior on the DID instead of the object its assigned to.

Imagine you have a scenario where you have 100 DIDs, most of them assigned directly to users (this is the scenario I had). How do you get 3CX to play a holiday (or out of office) message if the DID on which the call is incoming is assigned to a user? But to let the call go through if it is working hours on a non holiday?

One way I've found to do this is (but not completely) is as follows:
1. System -> Options -> General -> Change the "Custom Status 2" name to something relevant to this scenario like "Closed_Holiday_Prompt" for example
2. System -> Options -> Scheduler -> Set "When office is closed switch to" to "Closed_Holiday_Prompt"
3. Users -> *go into any user settings* -> Call Forwarding -> Closed_Holiday_Prompt -> Set "Forward External Calls" to a digital receptionist -> There you can set the Digital Receptionist to have a condition what to do when its a holiday, and what to do when its just out of office hours.

This method requires you to set up EVERY USER INDIVIDUALLY, and also the users themsleves need to set this status manually when its the holiday (since the scheduler wont do this on a holiday), and also because the Call Forwarding options in the Users menu do not have a Holiday condition.

Another method I have found, to play a message when its just Out Of Office Hours, is the "Exceptions" tab in User settings (Users -> AnyUser -> CallForwarding -> Exceptions). Set up to forward all calls to a Digital Receptionist when its Out Of Office, and let the DR handle the conditions.

So far this is what I've found without needing to learn how to code. :)

Hope this helps
 
You found a way to achieve your goal, and that's perfectly fine with me.

Little precision on the script I posted above:
1) If you want to use the script only for some DIDs, you can change the association in the general tab for the script and choose the required DIDs.
2) To check whether the destination of the call is out of office, the script looks up the office hours for "first or default" department of the internal destination - so it is not appropriate if you use individual office hours on users! (but can be adapted...)
 
@GMark
Additional check for "out of office" is looking redundant because timeBasedroute.reason should be already provided according to the destination settings. I think it will be enough just to change
Code:
if (timeBasedroute.reason == CallControlAPI.DivertReason.Holiday)
to
Code:
if (timeBasedroute.reason == CallControlAPI.DivertReason.Holiday || timeBasedroute.reason == CallControlAPI.DivertReason.OutOfOfficeHours)
 
Last edited:
  • Like
Reactions: Evolute IT
@SY: I'm not entirely sure about this part (new[] { "VACATION.wav" }). Was your comment meant to suggest that I should make corrections?
 
@GMark
The original message was posted because I accidentally pressed Ctrl-Enter. I have fixed it,
 
  • Like
Reactions: GMark
My bad! (brain stuck in my former code where I had to look-up opening hours of another department :rolleyes:)
So if you want the two different soundfiles it will be just (
C#:
(...)
                    // MODIFIED - REQUIRES UPLOAD OF "VACATION.wav"
                    if (timeBasedroute.reason == CallControlAPI.DivertReason.Holiday)
                    {
                        await MyCall.AssureMedia()
                            .ContinueWith(x => MyCall.PlayPrompt(null, new[] {"VACATION.wav"}, PlayPromptOptions.Blocked))
                            .Unwrap();
                        return true;  // <<<=== STOP default routing procedure !!
                    }
                  
                    // Intercept calls during the specified schedule
                    if (timeBasedroute.reason == CallControlAPI.DivertReason.OutOfOfficeHours)
                    {
                        await MyCall.AssureMedia()
                            .ContinueWith(x => MyCall.PlayPrompt(null, new[] {"OFFICE_CLOSED.wav"}, PlayPromptOptions.Blocked))
                            .Unwrap();
                        return true;  // <<<=== STOP default routing procedure !!
                    }
(...)
 

Members Online Now

Forum statistics

Threads
111,832
Messages
589,283
Members
164,662
Latest member
DejanMDS