Skip to main content

Crews and Scheduling

Operators are usually scheduled as a crew, a named group that works together. Crews belong to a site, not a single line, because a crew covers whichever lines need people that day. When a crew is assigned to a site-level shift, the schedule applies to every line beneath the site, so it lines up with whichever line each operator actually logs into.

Crew membership has start and end dates. When someone moves from Day Crew to Evening Crew, their history on Day Crew is kept, and reports for last month still show who was on which crew at the time.

Create a crew and add members
  1. Navigate to Ops → Scheduling → Crews.
  2. On the Crews tab, create a crew named Day Crew at the Philadelphia site.
  3. On the Crew Members tab, select Day Crew and add johnDoe and marcoDiaz, each with a start date of 12:00 AM today (earlier than the shift assignment you'll create next) and no end date.
Schedule the crew
  1. On the Scheduling tab, assign Day Crew to the Day Shift at Philadelphia, with a start of 7:00 AM today and an end of 3:00 PM today. If you're doing this lesson outside those hours, assign the crew to the shift you're actually working (for example, Evening Shift, 3:00 PM to 11:00 PM) so the next lesson's logins fall inside the scheduled window.
  2. Review the assignments list. You'll see one assignment per crew member, not one for the crew.

The assignment's start and end are the scheduled time OLE is measured against. Kanoa doesn't recalculate them from the shift calendar later, so enter the times you actually want people scheduled for.

Assigning a crew takes a snapshot of who is on the crew on the assignment's start date, so a member whose crew start date is later than the assignment's start isn't included. Adding someone to the crew next week doesn't change shifts that were already assigned, so the schedule stays a true record of what was planned.

Scheduling one person

To cover a gap without changing a crew, assign an individual operator to a shift on the same tab. Crew and individual assignments land in the same schedule, and Kanoa blocks overlapping assignments for the same person on the same shift or asset.

Is scheduling required?

No, but without it, Attendance and Labor Gap have nothing to compare against. An operator who logs in without being scheduled is measured against the whole report window instead, which makes their attendance look low. That's expected, not an error.

Check your work

Copy this script into the Ignition Designer Script Console (Tools → Script Console) and run it. It checks that Day Crew exists at the Philadelphia site, that johnDoe and marcoDiaz are current members, and that each of them has a shift assignment from the crew today with an end time. It also checks that each member is scheduled right now, so the next lesson's logins are counted. A NOTE flags assignments on a different shift or asset. It doesn't change anything.

Show the check script
checkLesson-crews.py
# Kanoa Ops Training - check "Crews and Scheduling" (Operator effectiveness, lesson 1)
#
# Run in the Ignition Designer Script Console (Tools > Script Console), in the
# project that inherits KANOA_SCRIPTS. It only reads; it changes nothing.
# Every check prints PASS or FAIL with what to fix, then a summary.

SITE = 'CoreForge\\Philadelphia'
CREW = 'Day Crew'
MEMBERS = ['johnDoe', 'marcoDiaz']
SHIFT = 'Day Shift'

results = []

def check(ok, what, fix=None):
results.append(bool(ok))
if ok:
print 'PASS %s' % what
else:
print 'FAIL %s' % what
if fix:
print ' -> %s' % fix

def note(what):
print 'NOTE %s' % what

def field(row, *names):
for n in names:
if n in row and row[n] is not None:
return row[n]
return None

def text(v):
return '' if v is None else unicode(v).strip()

def key(v):
return ' '.join(text(v).lower().split())

def truthy(v):
return str(v).lower() in ('true', '1')

def rows(data):
# Kanoa returns a dataset, a list of dictionaries, or None
if data is None:
return []
if hasattr(data, 'getRowCount'):
cols = list(data.getColumnNames())
return [dict((c, data.getValueAt(i, c)) for c in cols) for i in range(data.getRowCount())]
return [dict(r) for r in data]

def fmt(d):
return system.date.format(d, 'yyyy-MM-dd HH:mm') if d is not None else 'none'

print '=' * 70
print 'Kanoa Ops Training - Crews and Scheduling check'
print '=' * 70

now = system.date.now()
midnight = system.date.midnight(now)
siteId = system.kanoa.utilities.getFieldValue('assetId', system.kanoa.asset.getAssets({'assetPath': SITE}))
users = rows(system.kanoa.security.getUsers({}))
userIds = dict((key(field(u, 'userName')), field(u, 'userId')) for u in users)
userNames = dict((text(field(u, 'userId')), text(field(u, 'userName'))) for u in users)

def memberName(row):
return text(field(row, 'userName')) or userNames.get(text(field(row, 'userId')), '')

# 1. The crew
print '\n-- Crew'
crews = rows(system.kanoa.operators.getCrews({}))
exact = [c for c in crews if text(field(c, 'crewName')) == CREW]
close = [c for c in crews if key(field(c, 'crewName')) == key(CREW)]
crew = (exact or close or [None])[0]
crewId = None
if crew is None:
check(False, '%s exists' % CREW, 'In Ops > Scheduling > Crews, on the Crews tab, create a crew named %s at the Philadelphia site.' % CREW)
else:
crewId = field(crew, 'crewId')
actual = text(field(crew, 'crewName'))
check(actual == CREW, '%s exists' % CREW, "Found '%s'. Rename it to %s." % (actual, CREW))
crewAsset = field(crew, 'assetId')
check(text(crewAsset) == text(siteId), '%s belongs to the Philadelphia site' % CREW,
"It's on %s. Edit the crew and set its site to Philadelphia; crews belong to a site, not a line."
% (text(field(crew, 'assetPath', 'assetName')) or 'asset %s' % text(crewAsset)))
if field(crew, 'enabled') is not None:
check(truthy(field(crew, 'enabled')), '%s is enabled' % CREW, 'Edit the crew and tick Enabled.')

# 2. Crew members
memberStart = {}
if crewId is not None:
print '\n-- Crew members'
members = rows(system.kanoa.operators.getCrewMembers({'crewId': crewId}))
for name in MEMBERS:
if key(name) not in userIds:
check(False, '%s is a Kanoa user' % name, 'Create %s in the Managing Users lesson, then log in once as that user.' % name)
continue
mine = [m for m in members if key(memberName(m)) == key(name)]
if not mine:
check(False, '%s is on %s' % (name, CREW), 'On the Crew Members tab, select %s and add %s.' % (CREW, name))
continue
m = mine[-1]
start, end = field(m, 'startDate'), field(m, 'endDate')
memberStart[key(name)] = start
current = start is not None and not system.date.isAfter(start, now) and (end is None or system.date.isAfter(end, now))
check(current, '%s is on %s now (from %s, to %s)' % (name, CREW, fmt(start), fmt(end)),
'Edit the membership: start date 12:00 AM today or earlier, and no end date.')

# 3. Shift assignments
if crewId is not None:
print '\n-- Shift assignments'
dayEnd = system.date.addDays(midnight, 1)
assigned = rows(system.kanoa.operators.getOperatorShiftAssignments({'crewId': crewId, 'startDate': midnight, 'endDate': dayEnd}))
check(assigned, '%s is assigned to a shift today' % CREW,
'On the Scheduling tab, assign %s to the %s at Philadelphia, 7:00 AM to 3:00 PM today.' % (CREW, SHIFT))
for name in MEMBERS:
mine = [a for a in assigned if key(memberName(a)) == key(name)]
if not assigned:
break
if not mine:
start = memberStart.get(key(name))
first = None
for a in assigned:
d = field(a, 'startDate')
if d is not None and (first is None or system.date.isBefore(d, first)):
first = d
late = start is not None and first is not None and system.date.isAfter(start, first)
check(False, '%s has an assignment from the crew' % name,
('Their crew start date (%s) is after the assignment start (%s), so they were left out. ' % (fmt(start), fmt(first)) if late else '')
+ 'Fix the crew member start date to 12:00 AM today, delete the assignments and assign the crew again.')
continue
a = mine[0]
start, end = field(a, 'startDate'), field(a, 'endDate')
check(True, '%s has an assignment from the crew (%s to %s)' % (name, fmt(start), fmt(end)))
check(end is not None, '%s assignment has an end time' % name,
'Edit the assignment and set an end of 3:00 PM today (or the end of the shift you are working).')
shiftName = text(field(a, 'shiftName'))
if shiftName and key(shiftName) != key(SHIFT):
note('%s is assigned to %s, not %s. That is fine if it is the shift you are working.' % (name, shiftName, SHIFT))
asset = field(a, 'assetId')
if asset is not None and text(asset) != text(siteId):
note('%s is assigned on %s rather than the Philadelphia site, so it only covers that asset.'
% (name, text(field(a, 'assetPath', 'assetName')) or 'asset %s' % text(asset)))
live = [x for x in mine if field(x, 'startDate') is not None and not system.date.isAfter(field(x, 'startDate'), now)
and (field(x, 'endDate') is None or system.date.isAfter(field(x, 'endDate'), now))]
windows = ', '.join('%s to %s' % (fmt(field(x, 'startDate')), fmt(field(x, 'endDate'))) for x in mine)
check(live, '%s is scheduled right now' % name,
"Today's assignments: %s. In the next lesson, login time outside an assignment isn't counted and the "
"operator leaderboard comes back empty. Assign %s to the shift you're working now "
"(for example Evening Shift, 3:00 PM to 11:00 PM)." % (windows, CREW))

extra = sorted(set(memberName(a) for a in assigned) - set(MEMBERS))
if extra:
note('Also assigned with %s today: %s' % (CREW, ', '.join(extra)))

passed = results.count(True)
print '\n' + '=' * 70
print 'Result: %d of %d checks passed' % (passed, len(results))
if passed == len(results):
print 'All good. Day Crew is set up and scheduled.'
else:
print 'Fix the FAIL items above, then run this again.'
print '=' * 70