stc

a simple time card webapp
git clone _git@git.brennen.work:stc.git
Log | Files | Refs | README

commit e60128477e36bf3cef1c7c73d84e6e45879d3be1
parent 8a4eeacab5cc2f9def6ddd6894ce5a77308f3f77
Author: Youth Employment Program Production <youthemployment22@gmail.com>
Date:   Sat,  1 Jul 2023 01:12:29 -0600

Long time without changes shared

Diffstat:
Mapp/forms.py | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mapp/routes.py | 823++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mapp/static/css/main.css | 120++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Aapp/templates/admin/agreements/index.html | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/templates/admin/agreements/newagreement.html | 7-------
Aapp/templates/admin/agreements/projects/index.html | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/templates/admin/agreements/projects/newproject.html | 5++++-
Mapp/templates/admin/employee_report/index.html | 5++++-
Mapp/templates/admin/employee_report/widget.html | 30++++++++++++++++++++++++++++++
Mapp/templates/admin/reports/employee_report.html | 2+-
Mapp/templates/admin/reports/pay_period_report.html | 13+++++++++++++
Aapp/templates/admin/reports/project.html | 27+++++++++++++++++++++++++++
Aapp/templates/admin/reports/rangeSel.html | 28++++++++++++++++++++++++++++
Aapp/templates/admin/reports/total_timedata_report.html | 14++++++++++++++
Aapp/templates/admin/total_timedata_report/index.html | 26++++++++++++++++++++++++++
Aapp/templates/admin/total_timedata_report/widget.html | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/templates/admin/users/active.html | 1+
Mapp/templates/admin/users/inactive.html | 19++++++++++++++++---
Aapp/templates/admin/users/newpass.html | 28++++++++++++++++++++++++++++
Mapp/templates/dashboard/activeusers/widget.html | 3++-
Mapp/templates/dashboard/punchclock/index.html | 56+++++++++++++++++++++++++++++++++++++++++++++-----------
Aapp/templates/dashboard/punchclock/otheruser.html | 18++++++++++++++++++
Aapp/templates/dashboard/punchclock/update/date.html | 27+++++++++++++++++++++++++++
Aapp/templates/dashboard/punchclock/update/endTime.html | 27+++++++++++++++++++++++++++
Aapp/templates/dashboard/punchclock/update/note.html | 27+++++++++++++++++++++++++++
Aapp/templates/dashboard/punchclock/update/startTime.html | 27+++++++++++++++++++++++++++
Mapp/templates/dashboard/punchclock/widget.html | 4++--
Mapp/templates/knowlegebase/index.html | 5+++++
Mapp/templates/login.html | 4++--
Mdatabase.ini | 5++---
30 files changed, 1516 insertions(+), 111 deletions(-)

diff --git a/app/forms.py b/app/forms.py @@ -1,5 +1,5 @@ from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField, PasswordField, BooleanField, SelectField, DateField, TelField, EmailField, FloatField, IntegerField, TimeField +from wtforms import StringField, SubmitField, PasswordField, BooleanField, SelectField, DateField, TelField, EmailField, FloatField, IntegerField, TimeField, DecimalField from wtforms.validators import DataRequired, optional, length, InputRequired, EqualTo # Login class currently assumes mongodb collection(Users) with structure @@ -12,12 +12,15 @@ from wtforms.validators import DataRequired, optional, length, InputRequired, Eq ##### ##### #class ContactForm(FlaskForm): #Fill out, can apply to volunteers as well as agreement/project contacts etc... class BudgetForm(FlaskForm): #Iff ContactForm is completed change to be subform of ContactForm - laborBudget = FloatField('Labor', validators=[DataRequired()]) - travelBudget = FloatField('Travel', validators=[DataRequired()]) - suppliesBudget = FloatField('Supplies', validators=[DataRequired()]) - contactBudget = FloatField('Contact', validators=[DataRequired()]) - equipmentBudget = FloatField('Equipment', validators=[DataRequired()]) - otherBudget = FloatField('Other',validators=[optional()]) + laborBudget = FloatField('Labor', validators=[optional()]) + travelBudget = FloatField('Travel', validators=[optional()]) + suppliesBudget = FloatField('Supplies', validators=[optional()]) + perdiemBudget = FloatField('Per Diem', validators=[optional()]) + equipmentBudget = FloatField('Equipment', validators=[optional()]) + indirectBudget = FloatField('Indirect', validators=[optional()]) + contractingBudget = FloatField('Contracting', validators=[optional()]) + lodgingBudget = FloatField('Lodging', validators=[optional()]) + otherBudget = FloatField('Other', validators=[optional()]) ##### ##### ### Main Forms ### @@ -43,9 +46,9 @@ class NewUserForm(FlaskForm): createNewUser = SubmitField('Create New User') class ChangePasswordForm(FlaskForm): - newpass = PasswordField('Password',[InputRequired(),EqualTo('confpass',message='Passwords must match')]) - confpass = PasswordField('Confirm',validators=[DataRequired()]) - changePassword = SubmitField('Change Password') + newpass = PasswordField('Password',[InputRequired(),EqualTo('confpass',message='Passwords must match')]) + confpass = PasswordField('Confirm',validators=[DataRequired()]) + changePassword = SubmitField('Change Password') class NewRoleForm(FlaskForm): rolename = StringField('Role Name', validators=[DataRequired()]) @@ -55,7 +58,10 @@ class NewProjectForm(BudgetForm): agreement = SelectField('Part of Agreement', validators=[DataRequired()]) createNewProject = SubmitField('Create New Project') -class NewAgreementForm(BudgetForm): +class NewProjectNote(FlaskForm): + projectNote = StringField('Project Note', validators=[DataRequired()]) + +class NewAgreementForm(FlaskForm): agreementName = StringField('Agreement Name', validators=[DataRequired()]) agency = StringField('Signing Agency', validators=[DataRequired()]) startDate = DateField('Start Date',validators=[DataRequired()]) @@ -63,8 +69,9 @@ class NewAgreementForm(BudgetForm): createNewAgreement = SubmitField('Create New Agreement') class PunchclockoutWidget(FlaskForm): - projectsSel = SelectField('Project', validators=[DataRequired()]) + #projectsSel = SelectField('Project', validators=[DataRequired()]) #clockout = currenttime + recapOrNote = StringField('Notes or Recap') lunchBox = BooleanField('Lunch') per_diemBox = BooleanField('Per Diem') # IFF user.role is_in(trusted_role[]) then allow lunch minute definition @@ -80,6 +87,37 @@ class CrewClockinWidget(FlaskForm): userSel = SelectField('User:', validators=[DataRequired()]) projectSel = SelectField('Project:', validators=[DataRequired()]) time = TimeField('Started:') + submitEntr = SubmitField('Clock In') + #clockin = SubmitField('Clock In') + +class NewHoursForm(FlaskForm): + dateSel = DateField('Date',validators=[DataRequired()]) + projectSel = SelectField('Project',validators=[DataRequired()]) + startTime = TimeField('Start',validators=[DataRequired()]) + endTime = TimeField('End',validators=[DataRequired()]) + lunchSel = BooleanField('Lunch') + perDiemSel = BooleanField('Per Diem') + note = StringField('Notes') + submitEntr = SubmitField('Submit') + +class NewUserHourForm(NewHoursForm): + userSel = SelectField('User',validators=[DataRequired()]) + +class upDate(FlaskForm): + dateSel = DateField('Date', validators=[DataRequired()]) + submitEntr = SubmitField('Submit') + +class updateTime(FlaskForm): + timeSel = TimeField('Time', validators=[DataRequired()]) + submitEntr = SubmitField('Submit') + +class updateProject(FlaskForm): + projectSel = SelectField('Project', validators=[DataRequired()]) + submitEntr = SubmitField('Submit') + +class newNote(FlaskForm): + note = StringField('Replace Note', validators=[DataRequired()]) + submitEntr = SubmitField('Submit') class FleetCheckoutForm(FlaskForm): vehicle = SelectField('Vehicle', validators = [DataRequired()]) @@ -103,14 +141,25 @@ class FleetCheckinForm(FlaskForm): incident_notes = StringField('Incident Notes',validators=[optional()])# May not need this at all? checkin = SubmitField('Checkin Vehicle')#Update to take role name for pass to write fn -class ChangeHoursForm(FlaskForm): - projectChg = SelectField('Project') - startTiChg = TimeField('Start') - endTimeChg = TimeField('End') - lunchBxChg = BooleanField('Lunch') - perDiemChg = BooleanField('Per Diem') - updateEntr = SubmitField('Update') - removeEntr = SubmitField('Remove') +class dateRange(FlaskForm): + lowerBound = DateField('Start Date',validators=[DataRequired()]) + upperBound = DateField('End Date',validators=[DataRequired()]) + submitEntr = SubmitField('Submit') + +class ChangeUserForm(FlaskForm): + fname = StringField('First Name', validators=[DataRequired()]) + mname = StringField('Middle Initial', validators=[DataRequired(),length(max=1)]) + lname = StringField('Last Name', validators=[DataRequired()]) + birthday = DateField('Birthday',validators=[DataRequired()])# Ought to change this to some validation for age range accepted + role = SelectField('Role',validators=[DataRequired()]) + address = StringField('Address',validators=[DataRequired()])# Require some sort of validator for check... + branch = SelectField('Branch',validators=[DataRequired(),length(max=200)]) + phonenumber = TelField('Phone Number',validators=[DataRequired(),length(max=12)])# Require some sort of validator for check... + email = EmailField('Email',validators=[DataRequired()])# Require some sort of validator for check... + payPeriod = StringField('Pay Period Override',validators=[optional()])# May not need this at all? + payValue = FloatField('Pay Value Override',validators=[optional()])# Require some sort of validator for check... + setActive = BooleanField('Active',default="checked")# Require some sort of validator for check... + modNewUser = SubmitField('Update User') class DashPermissionsForm(FlaskForm):# for each module make Boolean field. Gets passed to fn writing to permissions_collection SET MANUALLY CURRENTLY punchclock = BooleanField('Punch Clock',default="checked") diff --git a/app/routes.py b/app/routes.py @@ -4,7 +4,7 @@ from app import app from flask_pymongo import PyMongo from flask_login import LoginManager from flask import render_template, url_for, request, flash, redirect -from app.forms import LoginForm, PunchclockinWidget, PunchclockoutWidget, FleetCheckoutForm, FleetCheckinForm, NewUserForm, AdmnPermissionsForm, DashPermissionsForm, ChangeHoursForm, NewAgreementForm, NewProjectForm, CrewClockinWidget, ChangePasswordForm +from app.forms import LoginForm, PunchclockinWidget, PunchclockoutWidget, FleetCheckoutForm, FleetCheckinForm, NewUserForm, AdmnPermissionsForm, DashPermissionsForm, NewHoursForm, NewAgreementForm, NewProjectForm, CrewClockinWidget, NewUserHourForm, ChangePasswordForm, ChangeUserForm, upDate, updateTime, updateProject, newNote, dateRange from flask import request from werkzeug.urls import url_parse from werkzeug.security import generate_password_hash, check_password_hash @@ -145,8 +145,8 @@ def load_user(username): def chgpass(): form = ChangePasswordForm() if form.validate_on_submit(): - mongo.db.users_collection.update_one({'username':current_user.username},{'$set':{'password_hash':generate_password_hash(form.confpass)}}) - flash("Changed password for {}".format(current_user.username)) #Will need to sendmail password to form.email.data later + mongo.db.user_collection.update_one({'username':current_user.username},{'$set':{'password_hash':generate_password_hash(form.newpass.data)}}) + flash("Changed password for {} to {}".format(current_user.username,form.newpass.data)) #Will need to sendmail password to form.email.data later return redirect(url_for('dashboard')) @@ -172,20 +172,35 @@ def dashboard(): dashperms=dashperms['dashboard'] clocked_in_users = mongo.db.time_collection.find({'clock_out': {'$exists':False}}) - def clock_user_out(time_id,lunch=False,perdiem=False): - if mongo.db.time_collection.find({'_id': time_id}, {'clock_out':{'$exists':False}}): - if lunch==True and perdiem==True: - mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.utcnow()],'lunch':True,'per_diem':True}}) - elif lunch==True and perdiem==False: - mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.utcnow()],'lunch':True}}) - elif lunch==False and perdiem==True: - mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.utcnow()],'per_diem':True}}) + def clock_user_out(time_id,notes='',lunch=False,perdiem=False): + if notes != '': + if mongo.db.time_collection.find({'_id': time_id}, {'clock_out':{'$exists':False}}): + if lunch==True and perdiem==True: + mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.now()],'note':notes,'lunch':True,'per_diem':True}}) + elif lunch==True and perdiem==False: + mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.now()],'note':notes,'lunch':True}}) + elif lunch==False and perdiem==True: + mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.now()],'note':notes,'per_diem':True}}) + else: + mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.now()],'note':notes}}) + return redirect(url_for('dashboard')) else: - mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.utcnow()]}}) - return redirect(url_for('dashboard')) + flash('No time entry found, or user has checked out already') + return redirect(url_for('dashboard')) else: - flash('No time entry found, or user has checked out already') - return redirect(url_for('dashboard')) + if mongo.db.time_collection.find({'_id': time_id}, {'clock_out':{'$exists':False}}): + if lunch==True and perdiem==True: + mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.now()],'lunch':True,'per_diem':True}}) + elif lunch==True and perdiem==False: + mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.now()],'lunch':True}}) + elif lunch==False and perdiem==True: + mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.now()],'per_diem':True}}) + else: + mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.now()]}}) + return redirect(url_for('dashboard')) + else: + flash('No time entry found, or user has checked out already') + # Move to a isUserClockedIn(default: username=current_user) if mongo.db.time_collection.find_one({'modified_by.0': current_user.username,'clock_out':{'$exists':False}}): clocked_out = False @@ -207,7 +222,7 @@ def dashboard(): if mongo.db.fleet_collection.find({'_id': fleet_id}, {'end_mileage':{'$exists':False}}): if end_mileage <= start_mileage: flash('end mileage less than starting mileage') - elif notes is not '' and end_mileage >= start_mileage: + elif notes != '' and end_mileage >= start_mileage: mongo.db.fleet_collection.update_one({'_id':fleet_id},{'$set':{'end_mileage':end_mileage,'incident_notes':{notes:False}}})#incident_note dict isResolved and string value to display on admin page elif notes == '' and end_mileage >= start_mileage: mongo.db.fleet_collection.update_one({'_id':fleet_id},{'$set':{'end_mileage':end_mileage}}) @@ -252,7 +267,7 @@ def dashboard(): crewform=CrewClockinWidget() clockinform.projectsSel.choices = availableProjects - clockoutform.projectsSel.choices = availableProjects + #clockoutform.projectsSel.choices = availableProjects fleetoutform.vehicle.choices = availableVehicles #fleetoutform.vehicle.default = availableVehicles[0]# Doesn't function #fleetoutform.start_mileage.data = lastMileage @@ -270,11 +285,11 @@ def dashboard(): #if clockinform.validate_on_submit(): Currently will submit all present forms... replaced w/ below if not clocked_out and request.method == 'POST' and clockoutform.validate(): - clock_user_out(time_id,clockoutform.lunchBox.data,clockoutform.per_diemBox.data) + clock_user_out(time_id,clockoutform.recapOrNote.data,clockoutform.lunchBox.data,clockoutform.per_diemBox.data)#add clockoutform.recapOrNote.data (will take some thought for clock_user_out fn potentially?) return redirect(url_for('dashboard')) if clocked_out and request.method == 'POST' and clockinform.validate(): - mongo.db.time_collection.insert_one({'clock_in' : [datetime.datetime.utcnow()], + mongo.db.time_collection.insert_one({'clock_in' : [datetime.datetime.now()], 'modified_by' : [current_user.username], 'date' : datetime.datetime.today(), 'project' : clockinform.projectsSel.data}) @@ -297,6 +312,138 @@ def dashboard(): return render_template('dashboard/layout.html',permissions=dashperms,clocked_out=clocked_out,clockoutform=clockoutform,clockinform=clockinform,fleetCheckedOut=fleetCheckedOut,crewform=crewform,vehicle_name=vehicle_name,fleetinform=fleetinform,fleetoutform=fleetoutform,clocked_in_users=clocked_in_users,ORGNAME=OrganizationName) +@app.route("/update/start/<mod_username>/<timeid>",methods=['GET','POST']) #TODO MAKE OTHER VALUE FOR LAST PAGE, RETURNS LAST PAGE ELSE DASHBOARD +@login_required +def updateStartTime(mod_username,timeid): +# projects = [] +# agreement_id = ObjectId(agreement_id) +# agreement = {'_id': 'defaultagreement', 'agreement_name': 'Default Agreement', 'agency': ['YEP'], 'projects': ['no projects'], 'start_date': 'No Start Date', 'end_date': 'No End Date'} +# try: +# agreement = mongo.db.agreements_collection.find_one({'_id':agreement_id}) +# #mongo.db.agreements_collection.find_one({'_id':agreement_id}) +# except: +# flash("Issue assigning Agreement data for agreement id {}".format(agreement_id)) +# else: +# for project in agreement['projects']: +# try: +# pj = mongo.db.projects_collection.find_one({'_id':project}) +# except: +# flash("Issue assigning project data for id {}".format(project)) +# else: +# projects.append(pj) +# finally: +# return render_template('admin/agreements/index.html',projects=projects,agreement=agreement,ORGNAME=OrganizationName) + timeid = ObjectId(timeid) + form = updateTime() + + if form.validate_on_submit(): # Possible bug iff user clocks in between page load and form submit... will create additional time_collection entry + try: + entry = mongo.db.time_collection.find_one({'_id': timeid}) + except: + flash("Issue finding/assigning time_id: {}".format(timeid)) + else: + day = entry['date'].date() + newtime = datetime.datetime.combine(day, form.timeSel.data)#TODO FINISH Creating variable for datetime.combine(date,timeSel.data) + try: + mongo.db.time_collection.update_one({'_id':timeid},{'$push':{'modified_by':mod_username,'clock_in':newtime}}) + except: + flash("Unable to push mod_username {}".format(mod_username)) + else: + flash('Updated time to {}'.format(form.timeSel.data)) + finally: + return redirect(url_for('dashboard'))#TODO RETURN LAST PAGE HERE! + + return render_template('dashboard/punchclock/update/startTime.html',form=form, ORGNAME=OrganizationName) + +@app.route("/update/end/<mod_username>/<timeid>",methods=['GET','POST']) #TODO MAKE OTHER VALUE FOR LAST PAGE, RETURNS LAST PAGE ELSE DASHBOARD +@login_required +def updateEndTime(mod_username,timeid): + timeid = ObjectId(timeid) + form = updateTime() + + if form.validate_on_submit(): + try: + entry = mongo.db.time_collection.find_one({'_id': timeid}) + except: + flash("Issue finding/assigning time_id: {}".format(timeid)) + else: + day = entry['date'].date() + newtime = datetime.datetime.combine(day, form.timeSel.data) + try: + mongo.db.time_collection.update_one({'_id':timeid},{'$push':{'modified_by':mod_username,'clock_out':newtime}}) + except: + flash("Unable to push mod_username {}".format(mod_username)) + else: + flash('Updated time to {}'.format(form.timeSel.data)) + finally: + return redirect(url_for('dashboard'))#TODO RETURN LAST PAGE HERE! + + return render_template('dashboard/punchclock/update/endTime.html',form=form, ORGNAME=OrganizationName) + +@app.route("/update/day/<mod_username>/<timeid>",methods=['GET','POST']) #TODO MAKE OTHER VALUE FOR LAST PAGE, RETURNS LAST PAGE ELSE DASHBOARD +@login_required +def updateDate(mod_username,timeid): + timeid = ObjectId(timeid) + form = upDate() + + if form.validate_on_submit(): + try: + entry = mongo.db.time_collection.find_one({'_id': timeid}) + except: + flash("Issue finding/assigning time_id: {}".format(timeid)) + else: + fdate = datetime.datetime.combine(form.dateSel.data,datetime.time()) + newstart = entry['clock_in'][-1].replace(year = fdate.year, month = fdate.month, day = fdate.day) + try: + newend = entry['clock_out'][-1].replace(year = fdate.year, month = fdate.month, day = fdate.day) + except: + flash("User currently clocked in") # TODO FIGURE OUT WHAT TO DO IF USER IS CLOCKED IN AND ATTEMPTING TO CHANGE ENTRY DATE... POSSIBLY SHOULDN'T UPDATE JUST THE CLOCKIN TIME AND LEAVE CLOCK OUT... ALSO WOULD BE ODD TO CLOCK OUT USER... + else: + try: + mongo.db.time_collection.update_one({'_id':timeid},{'$set':{'date':fdate},'$push':{'modified_by':mod_username,'clock_in':newstart,'clock_out':newend}}) + except: + flash("Unable to push mod_username {} date {} clockin {} and clockout {}".format(mod_username,fdate,newstart,newend)) + try: + mongo.db.time_collection.update_one({'_id':timeid},{'$set':{'date':fdate}}) + except: + flash("Unable to set date {}".format(fdate)) + finally: + try: + mongo.db.time_collection.update_one({'_id':timeid},{'$push':{'modified_by':mod_username,'clock_in':newstart,'clock_out':newend}}) + except: + flash("Unable to push mod_username {} date {} clockin {} and clockout {}".format(mod_username,fdate,newstart,newend)) + else: + flash('Updated date to {}'.format(form.dateSel.data)) + finally: + return redirect(url_for('dashboard'))#TODO RETURN LAST PAGE HERE! + + return render_template('dashboard/punchclock/update/date.html',form=form, ORGNAME=OrganizationName) + +@app.route("/update/note/<mod_username>/<timeid>",methods=['GET','POST']) #TODO MAKE OTHER VALUE FOR LAST PAGE, RETURNS LAST PAGE ELSE DASHBOARD +@login_required +def updateNote(mod_username,timeid): + timeid = ObjectId(timeid) + form = newNote() + + if form.validate_on_submit(): + try: + entry = mongo.db.time_collection.find_one({'_id': timeid}) + except: + flash("Issue finding/assigning time_id: {}".format(timeid)) + else: + newNoted = form.note.data + try: + mongo.db.time_collection.update_one({'_id':timeid},{'$set':{'note':newNoted},'$push':{'modified_by':mod_username}}) + except: + flash("{}: Unable to set note to {}".format(mod_username,newNoted)) + else: + flash('Updated note') + finally: + return redirect(url_for('dashboard'))#TODO RETURN LAST PAGE HERE! + + return render_template('dashboard/punchclock/update/note.html',form=form, ORGNAME=OrganizationName) + +#TODO FIGURE OUT WHY IT TAKES TWO CLICKS TO SET VALUES TO TRUE ON HOURS PAGE AND ALSO CREW CLOCKED IN LIST @app.route("/toggle-lunch/<timeid>",methods=['GET','POST']) @login_required def toggle_lunch(timeid): @@ -324,6 +471,138 @@ def toggle_per_diem(timeid): mongo.db.time_collection.update_one({'_id':timeid},{'$set':{'per_diem':True}}) return redirect(url_for('dashboard')) +#TODO +@app.route("/clockinuser",methods=['GET','POST']) +@login_required +def clockin_new_user(): + clocked_in_users = mongo.db.time_collection.find({'clock_out': {'$exists':False}}) + availableProjects = [] + for project in mongo.db.projects_collection.find(): + availableProjects.append((project['_id'],project['project_name'])) +# GET_CLOCKED out users + clocked_out_active_users=[] + clocked_in_active_users=[] + for active in mongo.db.user_collection.find({'is_active':True},{'username':1,'fname':1,'mname':1,'lname':1}): + funame = active['fname']+' '+active['lname'] + alreadyin = [] + for user in clocked_in_users: + alreadyin.append(user['modified_by'][0]) + if any(element in active['username'] for element in alreadyin): + clocked_in_active_users.append((active['_id'],funame)) + else: + clocked_out_active_users.append((active['username'],funame)) + + form=CrewClockinWidget() + form.time.data = datetime.datetime.now() + form.projectSel.choices = availableProjects + #form.projectSel.data = availableProjects[0] + form.userSel.choices = clocked_out_active_users + #form.userSel.data = clocked_out_active_users[0] + + if form.validate_on_submit(): # Possible bug iff user clocks in between page load and form submit... will create additional time_collection entry + mongo.db.time_collection.insert_one({'clock_in' : [form.time.data], + 'modified_by' : [form.userSel.data, current_user.username], + 'date' : datetime.datetime.today(), + 'project' : form.projectSel.data}) + return redirect(url_for('dashboard')) + + return render_template('dashboard/punchclock/otheruser.html',form=form,ORGNAME=OrganizationName) + +@app.route("/newtime/<usernm>",methods=['GET','POST']) +@login_required +def new_time(usernm): + clocked_in_users = mongo.db.time_collection.find({'clock_out': {'$exists':False}}) + availableProjects = [] + for project in mongo.db.projects_collection.find(): + availableProjects.append((project['_id'],project['project_name'])) +# GET_CLOCKED out users + clocked_out_active_users=[] + clocked_in_active_users=[] + for active in mongo.db.user_collection.find({'is_active':True},{'username':1,'fname':1,'mname':1,'lname':1}): + funame = active['fname']+' '+active['lname'] + alreadyin = [] + for user in clocked_in_users: + alreadyin.append(user['modified_by'][0]) + if any(element in active['username'] for element in alreadyin): + clocked_in_active_users.append((active['_id'],funame)) + else: + clocked_out_active_users.append((active['username'],funame)) + + form=NewHoursForm() + form.projectSel.choices = availableProjects + + if form.validate_on_submit(): # Possible bug iff user clocks in between page load and form submit... will create additional time_collection entry + dateentry = datetime.datetime.combine(form.dateSel.data,datetime.time()) + starttime = datetime.datetime.combine(form.dateSel.data,form.startTime.data) + endtime = datetime.datetime.combine(form.dateSel.data,form.endTime.data) + if usernm is current_user.username: + mongo.db.time_collection.insert_one({ + 'modified_by' : [usernm], + 'date' : dateentry, + 'clock_in' : [starttime], + 'clock_out' : [endtime], + 'project' : form.projectSel.data}) + else: + mongo.db.time_collection.insert_one({ + 'modified_by' : [usernm, current_user.username], + 'date' : dateentry, + 'clock_in' : [starttime], + 'clock_out' : [endtime], + 'project' : form.projectSel.data}) + + return redirect(url_for('dashboard')) + + return render_template('dashboard/punchclock/otheruser.html',form=form,ORGNAME=OrganizationName) + +@app.route("/newusertime/<usernm>",methods=['GET','POST']) +@login_required +def new_user_time(usernm): + clocked_in_users = mongo.db.time_collection.find({'clock_out': {'$exists':False}}) + availableProjects = [] + for project in mongo.db.projects_collection.find(): + availableProjects.append((project['_id'],project['project_name'])) +# GET_CLOCKED out users + clocked_out_active_users=[] + clocked_in_active_users=[] + for active in mongo.db.user_collection.find({'is_active':True},{'username':1,'fname':1,'mname':1,'lname':1}): + funame = active['fname']+' '+active['lname'] + alreadyin = [] + for user in clocked_in_users: + alreadyin.append(user['modified_by'][0]) + if any(element in active['username'] for element in alreadyin): + clocked_in_active_users.append((active['_id'],funame)) + else: + clocked_out_active_users.append((active['username'],funame)) + + form=NewUserHourForm() + #form.startTime.data = datetime.datetime.now() + form.projectSel.choices = availableProjects + #form.projectSel.data = availableProjects[0] + form.userSel.choices = clocked_out_active_users + #form.userSel.data = clocked_out_active_users[0] + + if form.validate_on_submit(): # Possible bug iff user clocks in between page load and form submit... will create additional time_collection entry + dateentry = datetime.datetime.combine(form.dateSel.data,datetime.time()) + starttime = datetime.datetime.combine(form.dateSel.data,form.startTime.data) + endtime = datetime.datetime.combine(form.dateSel.data,form.endTime.data) + if form.userSel.data is current_user.username: + mongo.db.time_collection.insert_one({ + 'modified_by' : [form.userSel.data], + 'date' : dateentry, + 'clock_in' : [starttime], + 'clock_out' : [endtime], + 'project' : form.projectSel.data}) + else: + mongo.db.time_collection.insert_one({ + 'modified_by' : [form.userSel.data, current_user.username], + 'date' : dateentry, + 'clock_in' : [starttime], + 'clock_out' : [endtime], + 'project' : form.projectSel.data}) + + return redirect(url_for('dashboard')) + + return render_template('dashboard/punchclock/otheruser.html',form=form,ORGNAME=OrganizationName) @app.route("/clockinuser/<modusernm>/<usertoinid>/<project>/<intime>", methods=['GET','POST']) @login_required @@ -348,7 +627,7 @@ def clockout_by_id(modusernm,timeid): def clock_otheruser_out(time_id,mod_username): if mongo.db.time_collection.find({'_id': time_id}, {'clock_out':{'$exists':False}}): - mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.utcnow()]}}) + mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.now()]}}) mongo.db.time_collection.update_one({'_id':time_id},{'$push':{'modified_by':mod_username}}) flash('Clocked out') else: @@ -423,7 +702,7 @@ def admin(): #### #### ####### Hours Route ####### #### #### -@app.route('/hours/<username>')#modify to take userid ex. /hours<userid> for "admin" currently pulls from current_user... simply always pass username to hours(if possible set a default to current_user) +@app.route('/hours/<username>', methods=['GET','POST'])#modify to take userid ex. /hours<userid> for "admin" currently pulls from current_user... simply always pass username to hours(if possible set a default to current_user) @login_required def hours(username):#userid goes into call to db to get user[] -> then returns formatted table (punchclock/index.html #set var user_hours = [] @@ -439,25 +718,54 @@ def hours(username):#userid goes into call to db to get user[] -> then returns f #dashperms=mongo.db.permissions_collection.find_one({'label': current_user.role},{'dashboard':1,'_id':0}) #dashperms=dashperms['dashboard'] #total_hours=0 - form = ChangeHoursForm() - projectChoices = mongo.db.project_collection.find({'completed':{'$exists':False}}) user = mongo.db.user_collection.find_one({"username": username}) + availableProjects = [] + for project in mongo.db.projects_collection.find(): #TODO TO RESOLVE BELOW ISSUE, JUST USE aggregator QUERY, WILL ALLOW SORTING AS WELL AS PROPER SUMMATION OF HOURS + availableProjects.append((project['_id'],project['project_name']))#TODO FIND OUT WHY THIS RETURNS THE OBJECTID VALUE .str THROWS AttributeError: 'ObjectId' object has no attribute 'str' + #'completed':{'$exists':False} dbhours = mongo.db.time_collection.find({'modified_by.0':user['username']}) hours = [] deltas=[] - for hour in dbhours: + for hour in dbhours: # Currenty acts wrong with longer than 1 day + for x, y in availableProjects: + if x is ObjectId(hour['project']): + hour['projectName'] = y if 'clock_out' not in hour: - hour['clock_out']=[datetime.datetime.utcnow()] - time = hour['clock_out'][0] - hour['clock_in'][0] + hour['clock_out']=[datetime.datetime.now()] + time = hour['clock_out'][-1] - hour['clock_in'][-1] hour['total_time'] = time hours.append(hour) deltas.append(time) + form = NewHoursForm() + form.dateSel.data = datetime.datetime.today() + form.projectSel.choices = availableProjects + form.startTime.data = datetime.datetime.now() total_hours = sum(deltas,datetime.timedelta()) statement_hours = "{} Hours, {} Minutes".format(total_hours.seconds//3600,(total_hours.seconds//60)%60) + + if form.validate_on_submit(): # Possible bug iff user clocks in between page load and form submit... will create additional time_collection entry + indt = datetime.combine(form.dateSel.data,form.startTime.data) + outdt = datetime.combine(form.dateSel.data,form.endTime.data) + if user['username'] is current_user.username and not form.perDiemSel.data and not form.lunchSel.data: + mongo.db.time_collection.insert_one({'clock_in' : [indt], + 'modified_by' : [current_user.username], + 'date' : datetime.datetime.today(), + 'project' : form.projectSel.data, + 'clock_out':[outdt]}) + return redirect(url_for('hours',username=user['username'])) + + if user['username'] is not current_user.username and not form.perDiemSel.data and not form.lunchSel.data: + mongo.db.time_collection.insert_one({'clock_in' : [indt], + 'modified_by' : [user['username'], current_user.username], + 'date' : datetime.datetime.today(), + 'project' : form.projectSel.data, + 'clock_out':[outdt]}) + + return redirect(url_for('hours',username=user['username'])) #hours = mongo.db.time_collection.find({'modified_by.0':user.username}) - return render_template ('dashboard/punchclock/index.html',form=form,hours=hours,total_hours=total_hours,statement_hours=statement_hours,user=user,ORGNAME=OrganizationName) + return render_template ('dashboard/punchclock/index.html',form=form,hours=hours,availableProjects=availableProjects,total_hours=total_hours,statement_hours=statement_hours,user=user,ORGNAME=OrganizationName) # Don't really need this until additional functionality is added, not in current project scope #@app.route("/fleet") @@ -483,6 +791,16 @@ def activeusers(): active = mongo.db.user_collection.find({'is_active':True}) return render_template('admin/users/active.html',activeusers=active,ORGNAME=OrganizationName) +@app.route("/admin/deactivate/<userid>",methods=['GET','POST']) +@login_required +def deactivate_user(userid): + userid = ObjectId(userid) + + if mongo.db.user_collection.find_one({'_id': userid})['is_active'] == True: + mongo.db.user_collection.update_one({'_id':userid},{'$set':{'is_active':False}}) + + return redirect(url_for('activeusers')) + #### #### ####### Inactive Users Admin Route ####### #### #### @@ -492,33 +810,116 @@ def inactiveusers(): inactive = mongo.db.user_collection.find({'is_active':False}) return render_template('admin/users/inactive.html',inactiveusers=inactive,ORGNAME=OrganizationName) +@app.route("/admin/activate/<userid>",methods=['GET','POST']) +@login_required +def activate_user(userid): + userid = ObjectId(userid) + + if mongo.db.user_collection.find_one({'_id': userid})['is_active'] == False: + mongo.db.user_collection.update_one({'_id':userid},{'$set':{'is_active':True}}) + + return redirect(url_for('inactiveusers')) + #### #### ####### New User Admin Route ####### #### #### +@app.route("/removetime/<timeid>",methods=['GET','POST']) +@login_required +def removetime(timeid): + timeid = ObjectId(timeid) + mongo.db.time_collection.delete_one({'_id':timeid}) + return redirect(url_for('dashboard')) +#@app.route("/clockoutuser/<modusernm>/<timeid>", methods=['GET','POST']) +#@login_required +#def clockout_by_id(modusernm,timeid): +# # if modified_by.last != modusernm: modified_by.append(modusernm) +# +# timeid = ObjectId(timeid) +# +# def clock_otheruser_out(time_id,mod_username): +# if mongo.db.time_collection.find({'_id': time_id}, {'clock_out':{'$exists':False}}): +# mongo.db.time_collection.update_one({'_id':time_id},{'$set':{'clock_out':[datetime.datetime.utcnow()]}}) +# mongo.db.time_collection.update_one({'_id':time_id},{'$push':{'modified_by':mod_username}}) +# flash('Clocked user out') +# else: +# flash('No time entry found, or user has checked out already') +# +# clock_otheruser_out(timeid,modusernm) +# +# return redirect(url_for('dashboard')) +#### #### +@app.route("/admin/users/modify/<uid>", methods=["GET","POST"]) +@login_required +def moduser(uid): +# Temp values, change to modular db dependent values + availableBranches = ['Dillon','Salmon'] + allRoles=mongo.db.permissions_collection.find({},{'label':1}) + availableRoles = [] + + dashperms=mongo.db.permissions_collection.find_one({'label': current_user.role},{'dashboard':1,'_id':0}) + dashperms=dashperms['dashboard'] + + for perm in dashperms: + #availableProjects.append((project['_id'],project['project_name'])) + availableRoles.append((perm['_id'],perm['label'])) + + defaultBranch = 'Dillon' + defaultRole = 'Crew' +# END TMP Values + + form = ChangeUserForm() + form.branch.choices = availableBranches + form.branch.default = defaultBranch + form.role.choices = availableRoles + form.role.default = defaultRole + + if form.validate_on_submit(): + mongo.db.user_collection.update_one({"_id":ObjectId(uid)},{ '$set': { + 'fname':form.fname.data, + 'mname':form.mname.data, + 'lname':form.lname.data, + 'username':form.fname.data.lower()+form.mname.data.lower()+form.lname.data.lower(), + 'birthday':form.birthday.data.strftime('%Y-%m-%d'), + 'role':form.role.data, + 'branch':form.branch.data, + 'phonenumber':form.phonenumber.data, + 'address':form.address.data, + 'email':form.email.data, + 'pay_period':form.payPeriod.data, + 'pay_value':form.role.data, + 'is_active':form.setActive.data + }}) + flash("Updated user information for {} {}".format(form.fname.data, form.lname.data)) + return redirect(url_for('activeusers')) + + return render_template('admin/users/moduser.html',form=form,ORGNAME=OrganizationName) + @app.route("/admin/users/new", methods=["GET","POST"]) @login_required def newuser(): # Temp values, change to modular db dependent values availableBranches = ['Dillon','Salmon'] - availableRoles = ['Crew', 'Assistant Crew Lead', 'Crew Lead', 'Project Manager', 'Accounting'] + + availableRoles = [] + for perm in mongo.db.permissions_collection.find(): + availableRoles.append((perm['label'])) + defaultBranch = 'Dillon' - defaultRole = 'Crew' # END TMP Values form = NewUserForm() form.branch.choices = availableBranches form.branch.default = defaultBranch form.role.choices = availableRoles - form.role.default = defaultRole #form.process() if form.validate_on_submit(): genpasswd = ''.join(random.choice(string.ascii_letters) for _ in range(14)) - # if form.payValue.data: - # value = form.payValue.data - # else: - # defined = mongo.db.permissions_collection.find_one({'label':form.role.data}) - # value = defined.base_pay_value + if form.payValue.data is not None: + rolePayValue = form.payValue.data + else: + roleValue = mongo.db.permissions_collection.find_one({'label':form.role.data}) + rolePayValue = roleValue['base_pay_value'] mongo.db.user_collection.insert_one({ 'fname':form.fname.data, @@ -533,7 +934,7 @@ def newuser(): 'address':form.address.data, 'email':form.email.data, 'pay_period':form.payPeriod.data, - 'pay_value':form.role.data, + 'pay_value':rolePayValue, 'is_active':form.setActive.data }) flash("New user for {} {} added with a password of {}".format(form.fname.data, form.lname.data, genpasswd)) #Will need to sendmail password to form.email.data later @@ -544,10 +945,27 @@ def newuser(): #### #### ####### Agreement Admin Route ####### #### #### -@app.route("/admin/agreement") +@app.route("/admin/agreement/<agreement_id>",methods=["GET"]) @login_required -def agreement(): - return render_template('admin/agreement/index.html',ORGNAME=OrganizationName) +def agreement(agreement_id): + projects = [] + agreement_id = ObjectId(agreement_id) + agreement = {'_id': 'defaultagreement', 'agreement_name': 'Default Agreement', 'agency': ['YEP'], 'projects': ['no projects'], 'start_date': 'No Start Date', 'end_date': 'No End Date'} + try: + agreement = mongo.db.agreements_collection.find_one({'_id':agreement_id}) + #mongo.db.agreements_collection.find_one({'_id':agreement_id}) + except: + flash("Issue assigning Agreement data for agreement id {}".format(agreement_id)) + else: + for project in agreement['projects']: + try: + pj = mongo.db.projects_collection.find_one({'_id':project}) + except: + flash("Issue assigning project data for id {}".format(project)) + else: + projects.append(pj) + finally: + return render_template('admin/agreements/index.html',projects=projects,agreement=agreement,ORGNAME=OrganizationName) @app.route("/admin/agreements/new", methods=["GET","POST"]) @login_required @@ -584,6 +1002,32 @@ def newagreement(): ####### Project Admin Route ####### #### #### ####### TODO Need to filter out available agrements key=agreement_name:value=_id(agreement) Assign _id to agreement, and write _id(project) to agreement.projects[] + +@app.route("/admin/project/<project_id>",methods=["GET"]) +@login_required +def project(project_id): + payperiod_times = [] + project_id = project_id + probject_id = ObjectId(project_id) + project = {'_id': 'defaultproject', 'project_name': 'Template Project', 'agreement': 'YEP General', 'budget': [{'labor':2, 'No Start Date':2, 'travel':2, 'end_date':2, 'supplies':2, 'No End Date':2}],'cost':[{'labor':1, 'No Start Date':1, 'travel':1, 'end_date':1, 'supplies':1, 'No End Date':1}]} + try: + project = mongo.db.projects_collection.find_one({'_id':probject_id}) + except: + flash("Issue assigning Project data for project id {}".format(probject_id)) + else: + #for project in agreement['projects']: + try: + tme = mongo.db.time_collection.find({'project':project_id}) + if tme is None: + flash('No Current Hours submitted for project {}'.format(project_id)) + except: + flash("Issue assigning time data for project id {}".format(project_id)) + else: + for time in tme: + payperiod_times.append(time) + finally: + return render_template('admin/agreements/projects/index.html',project=project,payperiod_times=payperiod_times,ORGNAME=OrganizationName) + @app.route("/admin/projects/new", methods=["GET","POST"]) @login_required def newproject(): @@ -595,11 +1039,38 @@ def newproject(): form = NewProjectForm() form.agreement.choices = availableAgreements - form.otherBudget.data = 0 + # form.laborBudget.data = 2 + # form.travelBudget.data = 0 + # form.suppliesBudget.data = 0 + # form.perdiemBudget.data = 0 + # form.equipmentBudget.data = 0 + # form.indirectBudget.data = 0 + # form.contractingBudget.data = 0 + # form.lodgingBudget.data = 0 + # form.otherBudget.data = 0 if form.validate_on_submit(): # create deterministic agreement unique _id? Example being genpasswd in new user validate on submit - + ### TODO MAKE THIS A FOR submit IN form IF submit.data == None, 'submit.label':0, else 'submit.label':submit.data ## probably can use f-strings to accomplish the 'submit.label' part + if form.laborBudget.data is None: + form.laborBudget.data = 0.0 + if form.travelBudget.data is None: + form.travelBudget.data = 0.0 + if form.suppliesBudget.data is None: + form.suppliesBudget.data = 0.0 + if form.perdiemBudget.data is None: + form.perdiemBudget.data = 0.0 + if form.equipmentBudget.data is None: + form.equipmentBudget.data = 0.0 + if form.indirectBudget.data is None: + form.indirectBudget.data = 0.0 + if form.contractingBudget.data is None: + form.contractingBudget.data = 0.0 + if form.lodgingBudget.data is None: + form.lodgingBudget.data = 0.0 + if form.otherBudget.data is None: + form.otherBudget.data = 0.0 + mongo.db.projects_collection.insert_one({ 'project_name':form.projectName.data, 'agreement':ObjectId(form.agreement.data), @@ -607,16 +1078,22 @@ def newproject(): 'labor':form.laborBudget.data, 'travel':form.travelBudget.data, 'supplies':form.suppliesBudget.data, - 'contact':form.contactBudget.data, + 'perdiem':form.perdiemBudget.data, 'equipment':form.equipmentBudget.data, + 'indirect':form.indirectBudget.data, + 'contracting':form.contractingBudget.data, + 'lodging':form.lodgingBudget.data, 'other':form.otherBudget.data }], # most recent labor budget accessed via budget.0.labor 'costs':[{ 'labor':0, 'travel':0, 'supplies':0, - 'contact':0, + 'perdiem':0, 'equipment':0, + 'indirect':0, + 'contracting':0, + 'lodging':0, 'other':0 }] }) @@ -650,12 +1127,188 @@ def project_report(): return render_template('admin/reports/project.html') # Payperiod Routes +#TODO marked 06.29,23 +###### TESTING START ####### +@app.route('/dev/time-data-total-report') +@login_required +def time_data_total_report(): + hours = mongo.db.time_collection.aggregate( [ +# { +# "$project":{"modified_by":1} +# }, +# { +# "$unwind":"$modified_by" +# }, +## ADD MATCH TO LIMIT BY DATE FOR REPORTING DATE SELECTION ## + { + "$group": { + "_id": { + "$first":"$modified_by", + }, + "totalTime": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }, + "lunchCount":{ "$sum":{'$cond':["$lunch",1,0] } }, + "perdiemCount":{ "$sum":{'$cond':["$perdiem",1,0] } } + } + }, + { + "$addFields": { + "totalHoursWorked": {"$subtract":[ "$totalTime", {"$multiply":["$lunchCount", 30 * 60 * 1000]}] } + } + }, + { + "$sort": { "_id": 1 } + } + ] )# Total hours worked + tspp = mongo.db.time_collection.aggregate( [ + { + "$group": { + "_id":{"projectId": "$project"}, + "laborHoursWorked": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }, + "lunchCount": { "$sum" : { '$cond':["$lunch",1,0] } }, + "perdiemCount": { "$sum" :{'$cond':["$perdiem",1,0] } } + } + }, + { + "$addFields": { + "totalHoursWorked": {"$subtract":[ "$laborHoursWorked", {"$multiply":["$lunchCount", 30 * 60 * 1000]}] } + } + }, + { + "$sort": { "_id": -1 } + } + ] )# Time Spent Per Project (filter entries by username, then group and sum hours by project) + + return render_template ('admin/reports/total_timedata_report.html', hours=hours, tspp=tspp, ORGNAME=OrganizationName) +####### TESTING END ####### +###### TESTING Period selection START ####### +@app.route('/dev/select-date-range', methods=["GET","POST"]) +@login_required +def select_date_range(): + form = dateRange() + + if form.validate_on_submit(): + + try: + begin = form.lowerBound.data + end = form.upperBound.data + #begin = datetime.datetime.strptime(form.lowerBound.data,'%F-%m-%d') + #end = datetime.datetime.strptime(form.upperBound.data, '%Y-%m-%d') + #begin = datetime.datetime.combine(form.lowerBound.data,datetime.time()) + #end = datetime.datetime.combine(form.upperBound.data,datetime.time()) + except: + flash("Error Reporting for time entries between {} and {}.".format(begin,end)) + else: + flash("Report for time entries between {} and {}.".format(form.lowerBound.data, form.upperBound.data )) + return redirect(url_for('time_bound_report',startday=begin,endday=end)) + + return render_template('admin/reports/rangeSel.html',form=form,ORGNAME=OrganizationName) + +@app.route('/dev/report-range/<startday>/<endday>') +@login_required +def time_bound_report(startday,endday): + begin = datetime.datetime.strptime(startday,'%Y-%m-%d') + end = datetime.datetime.strptime(endday, '%Y-%m-%d') + + allhours = mongo.db.time_collection.aggregate( [ + { + "$match": { + "$and":[{"date":{"$gte":begin}},{"date":{"$lt":end}}] + } + }, + { + "$group": { + "_id":{"_id":'$_id', + "project":"$project"}, + "totalTime": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }, + "lunchCount":{ "$sum":{'$cond':["$lunch",1,0] } }, + "perdiemCount":{ "$sum":{'$cond':["$perdiem",1,0] } } + } + }, + { + "$addFields": { + "totalHoursWorked": {"$subtract":[ "$totalTime", {"$multiply":["$lunchCount", 30 * 60 * 1000]}] } + } + }, + { + "$sort": { "_id": 1 } + } + ] )# Total hours worked + + + hours = mongo.db.time_collection.aggregate( [ + { + "$match": { + "$and":[{"date":{"$gte":begin}},{"date":{"$lt":end}}] + } + }, + { + "$group": { + "_id": { + "$first":"$modified_by", + }, + "totalTime": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }, + "lunchCount":{ "$sum":{'$cond':["$lunch",1,0] } }, + "perdiemCount":{ "$sum":{'$cond':["$perdiem",1,0] } } + } + }, + { + "$addFields": { + "totalHoursWorked": {"$subtract":[ "$totalTime", {"$multiply":["$lunchCount", 30 * 60 * 1000]}] } + } + }, + { + "$sort": { "_id": 1 } + } + ] )# Total hours worked + tspp = mongo.db.time_collection.aggregate( [ + { + "$match": { + "$and":[{"date":{"$gte":begin}},{"date":{"$lt":end}}] + } + }, + { + "$group": { + "_id":{"projectId": "$project"}, + "laborHoursWorked": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }, + "lunchCount": { "$sum" : { '$cond':["$lunch",1,0] } }, + "perdiemCount": { "$sum" :{'$cond':["$perdiem",1,0] } } + } + }, + { + "$addFields": { + "totalHoursWorked": {"$subtract":[ "$laborHoursWorked", {"$multiply":["$lunchCount", 30 * 60 * 1000]}] } + } + }, + { + "$sort": { "_id": -1 } + } + ] )# Time Spent Per Project (filter entries by username, then group and sum hours by project) + + ptl = mongo.db.time_collection.aggregate( [ + { + "$lookup":{ # TODO TODO TODO THIS WILL REQUIRE CHANGING ALL DB WRITES TO time_collection['project'] TO BECOME ObjectId() OBJECTS THIS WILL LIKELY BREAK THINGS!!!! NEED TO ITERATE THROUGH DB ENTRIES AS WELL AS ENSURE READ OPERATIONS STILL GUNCTION PROPERLY AFTERWARDS + 'from':'projects_collection', + 'localField':'project', + 'foreignField':'_id', + 'as':'project_data' + } + }, + { + "$sort": {'date': -1} + } + ] ) + + + return render_template ('admin/reports/total_timedata_report.html', allhours=allhours, hours=hours, tspp=tspp, projectlookup=ptl, ORGNAME=OrganizationName) +####### TESTING END ####### + @app.route('/admin/reports/pay-period', methods=['GET']) @login_required def pay_period_report(): pay = mongo.db.time_collection.find({}) dbactiveusers = mongo.db.user_collection.find({'is_active':True}) users=[] + nouser=[] times_by_user={} for user in dbactiveusers: times_by_user[user['username']]=[datetime.timedelta(seconds=0)] @@ -665,10 +1318,14 @@ def pay_period_report(): for time in pay: if time['modified_by'][0] not in times_by_user: times_by_user[time['modified_by'][0]]=[datetime.timedelta(seconds=0)] - user_lookup = mongo.db.user_collection.find_one({'username':time['modified_by'][0]}) - users.append(user_lookup) + try: + user_lookup = mongo.db.user_collection.find_one({'username':time['modified_by'][0]}) + except: + nouser.append(time['_id']) + else: + users.append(user_lookup) if 'clock_out' not in time: - time['clock_out']=[datetime.datetime.utcnow()] + time['clock_out']=[datetime.datetime.now()] t = time['clock_out'][0] - time['clock_in'][0] hours['total_time'] = t times_by_user[time['modified_by'][0]].append(t) @@ -678,27 +1335,95 @@ def pay_period_report(): total_hours = sum(times_by_user[user['username']],datetime.timedelta()) statement_hours = (total_hours.seconds//3600,(total_hours.seconds//60)%60) user['total_hours']=statement_hours - return render_template('admin/reports/pay_period_report.html', users=users, pay=pay, ORGNAME=OrganizationName) + return render_template('admin/reports/pay_period_report.html', nouser=nouser, users=users, pay=pay, ORGNAME=OrganizationName) # @app.route("/dev/fleetdata") # @login_required # def fleetdatalist(): # allfleetdata = mongo.db.fleet_collection.find() # return render_template('dev/fleetdata.html', allfleetdata=allfleetdata) +def calculateHours(username=current_user): + if not mongo.db.time_collection.find({"modified_by.0":username}): + return 0 + else: + times = mongo.db.time_collection.find({"modified_by.0":username}) + deltas=[] + + for time in times: + if 'clock_out' not in time: + time['clock_out']=[datetime.datetime.now()] + t = time['clock_out'][0] - time['clock_in'][0] + deltas.append(t) + + total_calculated_hours = sum(deltas,datetime.timedelta()) + + return total_calculated_hours @app.route('/admin/reports/employees') @login_required def report_employees(): users = mongo.db.user_collection.find() hours = mongo.db.time_collection.find() + for user in users: + user['total_hours']=calculateHours(user['username']) return render_template ('admin/employee_report/index.html', hours=hours, users=users, ORGNAME=OrganizationName) @app.route('/admin/reports/employee/<username>') @login_required def employee_report(username): user = mongo.db.user_collection.find_one({"username": username}) - hours = mongo.db.time_collection.find({'modified_by.0':user['username']}) - return render_template ('admin/reports/employee_report.html', hours=hours, user=user, ORGNAME=OrganizationName) + hours = mongo.db.time_collection.aggregate( [ + { + "$match": { + "modified_by.0":username + } + }, + { + "$sort": { "date": -1 } + } + ] )# Total hours worked + #hours = mongo.db.time_collection.find({'modified_by.0':user['username']}) + # hours = mongo.db.time_collection.aggregate( + # { + # "$match": {'modified_by.0':user['username'] } + # }, + # { + # "$lookup": { "from":"projects_collection", "localField":"project", "foreignField":"project_name", "as":"project_data"} + # }) + thw = mongo.db.time_collection.aggregate( [ + { + "$match": { + "modified_by.0":username + } + }, + { + "$group": { + "_id":"$modified_by.0", + "totalHoursWorked": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } } + } + }, + { + "$sort": { "totalHoursWorked": -1 } + } + ] )# Total hours worked + tspp = mongo.db.time_collection.aggregate( [ + { + "$match": { + "modified_by.0":username + } + }, + { + "$group": { + "_id":{"projectId": "$project"}, + "totalHoursWorked": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } } + } + }, + { + "$sort": { "totalHoursWorked": -1 } + } + ] )# Time Spent Per Project (filter entries by username, then group and sum hours by project) + + return render_template ('admin/reports/employee_report.html', hours=hours, user=user, thw=thw, tspp=tspp, ORGNAME=OrganizationName) # Vehicle Routes @app.route('/admin/reports/vehicles') diff --git a/app/static/css/main.css b/app/static/css/main.css @@ -17,6 +17,51 @@ --progressbg:var(--zoning); */ } +/*testinput stylestart*/ +input:not([type="submit"]):not([type="checkbox"])/*[type="text"]*/ { + width: 200px; + display: block; + border: none; + padding: 10px 0; + border-bottom: solid 1px var(--accent); + transition: all 0.3s cubic-bezier(.64,.09,.08,1); + background: linear-gradient(to bottom, rgba(255,255,255,0) 90%, var(--accent) 10%); + background-position: -200px 0; + background-size: 200px 100%; + background-repeat: no-repeat; + color: darken(var(--accent), 20%); + &:focus, &:valid { + box-shadow: none; + outline: none; + background-position: 0 0; + &::-webkit-input-placeholder { + color: var(--accent); + font-size: 11px; + transform: translateY(-20px); + visibility: visible !important; + } + } +} + +button a { + color:white; +} +button, input[type="submit"], input[type="checkbox"] { + border: none; + background: var(--accent); + cursor: pointer; + border-radius: 3px; + padding: 6px; + /*width: 200px;*/ + color: white; + /*margin-left: 25px;*/ + box-shadow: 0 3px 6px 0 rgba(0,0,0,0.2); + &:hover { + transform: translateY(-10px); + box-shadow: 0 6px 6px 0 rgba(0,0,0,0.2); + } +} +/*testend*/ html,body {margin:0;padding:0;background-color:var(--rootbg);} .appview {margin:0;padding:0;} @@ -29,6 +74,38 @@ a,a.visited,a.hover { ::selection{ background: var(--zoning) } +/********** GLOBAL SET **********/ + /***Printing***/ +@media print { + .pagebreak { + clear:both; + page-break-after:always; + } + header, #doc, .reportswidget, .permissions, .activeusers { + display:none !important; + } +} + /***EndingPr***/ +#messagebanner p { + text-align: center; + color: var(--accent); + background-color: var(--zoning); + padding: 1em; + margin: 0 1em; +} + +#button a { + border: none; + background: var(--accent); + cursor: pointer; + border-radius: 3px; + padding: 6px; + width: 200px; + color: white; + margin-left: 25px; + box-shadow: 0 3px 6px 0 rgba(0,0,0,0.2); +/* &:hover { transform: translateY(-10px); box-shadow: 0 6px 6px 0 rgba(0,0,0,0.2); };*/ +} /********** NAVIGATION **********/ header { font-size:1.5em; @@ -37,7 +114,7 @@ header { display:grid; gap:0.5rem; padding:0rem 1.5rem 0rem 1.5rem; - margin:1rem 0rem 0rem 0rem; + margin:1rem 0rem 1rem 0rem; width:auto; height:3em; grid-template-columns: min-content auto min-content; @@ -74,10 +151,28 @@ header #logout { right:1.5rem; align-self:center; } + +.user-settings { + font-size:1.5em; + align-items:center; + justify-items:center; + display:grid; + gap:0.5rem; + width:auto; + height:3em; + padding:0rem 1.5rem 0rem 1.5rem; + margin:1rem 0rem 1rem 0rem; +/* grid-template-columns: min-content auto min-content; + grid-template-areas: "logo navi logout"; +*/ align-self:center; +} + /********** LOGIN PAGE **********/ .login-grid { display: grid; grid-template-columns: repeat(3, 1fr); + margin-top: 1em; + margin-bottom: 1em; } .login { @@ -88,13 +183,14 @@ header #logout { grid-row-end: 2; grid-column-start: 2; grid-column-end: 3; - padding-top:5rem; - padding-bottom:4rem; + padding:4rem; +/* padding-bottom:4rem;*/ background-color:var(--maincolor); + border-radius:.5em; /* box-shadow: 0px 0px .1em .1em var(--accent);/* probably shouldn't have box-shadow for clean ui at intermediate page sizes (between laptop and phone off ratio) */ } /********** FULL PAGE **********/ -.hours-grid, .new-user-grid, .new-agreement-grid, .new-project-grid, .role-permissions, .activeusers-grid { +.hours-grid, .new-user-grid, .new-agreement-grid, .new-project-grid, .role-permissions, .activeusers-grid, .agreement-grid, .project-grid { padding:5rem; margin:1rem; display: grid; @@ -113,7 +209,7 @@ header #logout { padding:1rem; } /*agreements here is actually for the tables in the admin pages. initially used for the Hours by Employee widget(/admin/employee_report/index.html) */ -.agreements table tr:nth-child(even) { +.project-table-grid table tr:nth-child(even), .current-pay-period table tr:nth-child(even) .agreements table tr:nth-child(even) { background-color:var(--maincolor); } .agreements table,.hours-grid tr,.hours-grid td{ @@ -121,11 +217,13 @@ header #logout { border-color:var(--accent); padding:1rem; } + /********** (In)Active Users PAGE **********/ .activeusers-grid { grid-auto-columns:auto; } .usercard { + width:fit-content; border:1px solid; border-color:var(--accent); border-radius:.5em; @@ -172,10 +270,9 @@ header #logout { min-height:40vh; } } -@media (max-width: 414px) { - body { - background-color:#000; - } +@media (max-width: 414px) {/* try 240px width standard? https://dabblet.com/result/gist/1576044 */ + table,tr,td {display:block;}/* not sure if works, intended for table flow over rows */ + tr:nth-child(2n) {background:var(--maincolor);}/* not sure if works, intended for table flow over rows */ .punchclock { padding: 5em 8em; } @@ -215,7 +312,8 @@ header #logout { display: grid; grid-gap:1em; margin:1em; - place-items:center; +/* place-items:center; + * */ } @media (min-width:720px){ .base-grid { @@ -255,12 +353,14 @@ header #logout { color:#fff; margin:1em; height:2em; + min-width:fit-content; background-color:var(--totalprogressfill); text-align:center; } .progress-bar { margin:1em; height:2em; + min-width:fit-content; background-color:var(--progressfill); text-align:left; } diff --git a/app/templates/admin/agreements/index.html b/app/templates/admin/agreements/index.html @@ -0,0 +1,52 @@ +{% extends 'base.html' %} + +{% block title %}{{agreement['agreement_name']}}{% endblock %} + +{% block content %} +{% with messages = get_flashed_messages() %} +{% if messages %} + {% for message in messages %} + <div id="messagebanner"><p>{{ message }}</p></div> + {% endfor %} +{% endif %} +{% endwith %} + <section class="agreement-grid"> + <h3>{{ agreement['agreement_name'] }}</h3> + + {%- for element in agreement['signing_agency'] %} + {{ agreement[element] }} + {%- endfor %} + {{ agreement['start_date'] }} - {{ agreement['end_date'] }} + {% if agreement['contacts'] %} + {% for contact in agreement['contacts'] %} + {{ contact }} + {% endfor %} + {% endif %} + + + <h5>Projects</h5> + <div class="project-table-grid"> + {%- for project in projects %} + <a href="{{url_for('project',project_id=project['_id'])}}"><table> + <caption>{{ project.project_name }}</caption> + <tr> + <th>Line Item</th> + <th>Budget</th> + <th>Cost</th> + <th>Use(%)</th> + </tr> + <!-- for each tuple in budget iterate through line item data --> + {% for lineitem in project['budget'][0] %} + <tr> + <td>{{ lineitem }}</td><!-- Line item label --> + <td>{{ project['budget'][0][lineitem] }}</td> + <td>{{ project['costs'][0][lineitem] }}</td> + {# <td>{{ (project['costs'][0][lineitem]/project['budget'][0][lineitem])*100 }}%</td> #} + </tr> + {% endfor %} + </table> + </a> + {%- endfor %} + </div> <!-- END Project table grid --> + </section> +{% endblock %} diff --git a/app/templates/admin/agreements/newagreement.html b/app/templates/admin/agreements/newagreement.html @@ -17,13 +17,6 @@ {{ form.agency.label }}{{ form.agency() }}<br> {{ form.startDate.label }}{{ form.startDate() }}<br> {{ form.endDate.label }}{{ form.endDate() }}<br> - <h4>Budget</h4> - {{ form.laborBudget.label }}{{ form.laborBudget() }}<br> - {{ form.travelBudget.label }}{{ form.travelBudget() }}<br> - {{ form.suppliesBudget.label }}{{ form.suppliesBudget() }}<br> - {{ form.contactBudget.label }}{{ form.contactBudget() }}<br> - {{ form.equipmentBudget.label }}{{ form.equipmentBudget() }}<br> - {{ form.otherBudget.label }}{{ form.otherBudget() }}<br><br> {{ form.createNewAgreement() }} </form> {% with messages = get_flashed_messages() %} diff --git a/app/templates/admin/agreements/projects/index.html b/app/templates/admin/agreements/projects/index.html @@ -0,0 +1,54 @@ +{% extends 'base.html' %} + +{% block title %}{{ project['project_name'] }}{% endblock %} +{% block content %} + +{% with messages = get_flashed_messages() %} +{% if messages %} + {% for message in messages %} + <div id="messagebanner"><p>{{ message }}</p></div> + {% endfor %} +{% endif %} +{% endwith %} + +<section class="project-grid"> + <dl> + <dt>{{ project['project_name'] }}</dt> + <dd>ID: {{ project['_id'] }}</dd> + <dd>A_ID:{{ project['agreement']}}</dd> + <dd>Budget:{{ project['budget']}}</dd> + <dd>Costs:{{ project['costs']}}</dd> + </dl> + + <div class="current-pay-period"> + <table> + <tr> + <th>Entry ID</th> + <th>Date</th> + <th>Employee</th> + <th>Time</th> + <th>In</th> + <th>Out</th> + <th>Lunch</th> + <th>Perdiem</th> + </tr> + {% for entry in payperiod_times %} + <tr> + <td>{{entry['_id']}}</td> + <td>{{entry['date'].date().isoformat()}}</td> + <td>{{entry.modified_by.0}}</td> + {% if entry['clock_out'] %}<td>{{(entry['clock_out'][-1]-entry['clock_in'][-1])}}</td>{% else %}<td>Clocked In</td>{% endif %} + <td>{{entry['clock_in'][-1].time().isoformat(timespec='minutes')}}</td> + {% if entry['clock_out'] %}<td>{{entry['clock_out'][-1].time().isoformat(timespec='minutes')}}</td>{% else %}<td>Clocked In</td>{% endif %} + {% if entry['lunch'] %}<td>{{entry['lunch']}}</td>{% else %}<td>&nbsp;</td>{% endif %}<!-- make this an image of checkbox so can link to toggle route... --> + {% if entry['perdiem'] %}<td>{{entry['perdiem']}}</td>{% else %}<td>&nbsp;</td>{% endif %}<!-- make this an image of checkbox so can link to toggle route... --> + </tr> + {% endfor %} + </table> + </div> + + <div class="non-pay-period"> + </div> +</section> + +{% endblock %} diff --git a/app/templates/admin/agreements/projects/newproject.html b/app/templates/admin/agreements/projects/newproject.html @@ -19,8 +19,11 @@ {{ form.laborBudget.label }}{{ form.laborBudget() }}<br> {{ form.travelBudget.label }}{{ form.travelBudget() }}<br> {{ form.suppliesBudget.label }}{{ form.suppliesBudget() }}<br> - {{ form.contactBudget.label }}{{ form.contactBudget() }}<br> + {{ form.perdiemBudget.label }}{{ form.perdiemBudget() }}<br> {{ form.equipmentBudget.label }}{{ form.equipmentBudget() }}<br> + {{ form.indirectBudget.label }}{{ form.indirectBudget() }}<br> + {{ form.contractingBudget.label }}{{ form.contractingBudget() }}<br> + {{ form.lodgingBudget.label }}{{ form.lodgingBudget() }}<br> {{ form.otherBudget.label }}{{ form.otherBudget() }}<br><br> {{ form.createNewProject() }} </form> diff --git a/app/templates/admin/employee_report/index.html b/app/templates/admin/employee_report/index.html @@ -10,7 +10,10 @@ <table> <tr><th>First</th><th>Middle</th><th>Last</th><th>Pay</th><th>Hours</th></tr> {%- for user in users %} - <tr><td>{{user.fname}}</td><td>{{user.mname}}</td><td>{{user.lname}}</td><td>{{user.pay_value}}</td><td>{{user.total_hours}}</td></tr> + {# + <tr><td>{{user.fname}}</td><td>{{user.mname}}</td><td>{{user.lname}}</td><td>{{user.pay_value}}</td><td>{{user.total_hours}}</td><td><a href="{{url_for('employee_report',username=user.username)}}"><button>More</button></a></td></tr> + #} + {{ user }} {% endfor %} </table> </section> diff --git a/app/templates/admin/employee_report/widget.html b/app/templates/admin/employee_report/widget.html @@ -16,5 +16,35 @@ <tr><td>Active</td><td>{% if user.is_active %} Employee is active {% else %} Employee is inactive {% endif %}</td></tr> </table> </div> + {% for project in thw %} + {{project['totalHoursWorked']/(1000*60*60)}} Total Hours worked this payperiod + {% endfor %} + <div class="employee-hours"> + <table> + <tr> + <th>Date</th> + <th>Project</th> + <th>Total Time</th> + <th>Clocked In</th> + <th>Clocked Out</th> + <th>Lunch</th> + <th>Per Diem</th> + </tr> + {% for entry in hours %} + <tr> + <td>{{ entry['date'].date().isoformat() }}</td> + {% if entry['project_data'] %}<td>{{ entry['project_data']['project_name'] }}</td>{% else %}<td>{{entry['project']}}</td>{% endif %} + {% if entry['clock_out'] %}<td>{{entry['clock_out'][-1]-entry['clock_in'][-1]}}</td>{% else %}<td>Clocked In</td>{% endif %} + <td>{{ entry['clock_in'][-1].time().isoformat(timespec='minutes')}}</td> + {% if entry['clock_out'] %}<td>{{entry['clock_out'][-1].time().isoformat(timespec='minutes')}}</td>{% else %}<td>Clocked In</td>{% endif %} + {% if entry['lunch'] %}<td>{{entry['lunch']}}</td>{% else %}<td>&nbsp;</td>{% endif %} + {% if entry['perdiem'] %}<td>{{entry['perdiem']}}</td>{% else %}<td>&nbsp;</td>{% endif %} + </tr> + {% endfor %} + </table> + </div> + {% for project in tspp %} + {{project['totalHoursWorked']/(1000*60*60)}} Hours on {{project['_id']}}</br> + {% endfor %} </section> </section> diff --git a/app/templates/admin/reports/employee_report.html b/app/templates/admin/reports/employee_report.html @@ -5,7 +5,7 @@ {% block content %} <section class="admin-grid"> <!-- returned values from admin check is array of permissive ACCESS else return 'missing permissions response' --> - {%- for x in ['reports','employee_report','roles','users'] %} + {%- for x in ['reports','total_timedata_report','roles','users'] %} {% include 'admin/'~x~'/widget.html' %} {%- else-%} {{ 'You do not have permission to access this page' }} diff --git a/app/templates/admin/reports/pay_period_report.html b/app/templates/admin/reports/pay_period_report.html @@ -3,6 +3,19 @@ {% block title %}Payment Reports{% endblock %} {% block content %} + +{% with messages = get_flashed_messages() %} +{% if messages %} + {% for message in messages %} + <div id="messagebanner"><p>{{message}}</p></div> + {% endfor %} +{% endif %} +{% endwith %} + +{% for user in nouser %} + <div id="messagebanner"><p>No user for {{ user }}</p></div> +{% endfor %} + <section class="admin-grid"> <!-- returned values from admin check is array of permissive ACCESS else return 'missing permissions response' --> {%- for x in ['pay_period_report','reports','roles','users'] %} diff --git a/app/templates/admin/reports/project.html b/app/templates/admin/reports/project.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block title %}Agreement Reports{% endblock %} + +{% block content %} + +{% with messages = get_flashed_messages() %} +{% if messages %} + {% for message in messages %} + <div id="messagebanner"><p>{{message}}</p></div> + {% endfor %} +{% endif %} +{% endwith %} + +{% for user in nouser %} + <div id="messagebanner"><p>No user for {{ user }}</p></div> +{% endfor %} + + <section class="admin-grid"> + <!-- returned values from admin check is array of permissive ACCESS else return 'missing permissions response' --> + {%- for x in ['pay_period_report','reports','roles','users'] %} + {% include 'admin/'~x~'/widget.html' %} + {%- else-%} + {{ 'You do not have permission to access this page' }} + {%- endfor %} + </section> +{% endblock %} diff --git a/app/templates/admin/reports/rangeSel.html b/app/templates/admin/reports/rangeSel.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block title %}Select Report Range{% endblock %} + +{% block content %} + <section class="hours-grid"> + <h1 id="clock"></h1> + {% for error in form.errors %} + <span style="color:red;">{{error}}</span> + {% endfor %} + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + <p style='color:green'>{{message}}</p> + {% endfor %} + {% endif %} + {% endwith %} + <form action="" method="POST" novalidate> + <table> + {% for field in form %}{% if field.widget.input_type != 'hidden' and field.widget.input_type != 'submit' %} + <tr><td>{{ field.label }}</td><td>{{ field }}</td></tr> + {% endif %}{% endfor %} + </table> + {{ form.submitEntr() }} + {{ form.hidden_tag() }} + </form> + </section> +{% endblock %} diff --git a/app/templates/admin/reports/total_timedata_report.html b/app/templates/admin/reports/total_timedata_report.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block title %}Employee Reports{% endblock %} + +{% block content %} + <section class="admin-grid"> + <!-- returned values from admin check is array of permissive ACCESS else return 'missing permissions response' --> + {%- for x in ['reports','total_timedata_report','roles','users'] %} + {% include 'admin/'~x~'/widget.html' %} + {%- else-%} + {{ 'You do not have permission to access this page' }} + {%- endfor %} + </section> +{% endblock %} diff --git a/app/templates/admin/total_timedata_report/index.html b/app/templates/admin/total_timedata_report/index.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} + +{% block title %}Employee Reports{% endblock %} + +{% block content %} + <section class="admin-grid"> + <!-- returned values from admin check is array of permissive ACCESS else return 'missing permissions response' --> + <section class="agreements"> + <h3>Hours by Employee</h3> + <table> + <tr><th>First</th><th>Middle</th><th>Last</th><th>Pay</th><th>Hours</th></tr> + {%- for user in users %} + {# + <tr><td>{{user.fname}}</td><td>{{user.mname}}</td><td>{{user.lname}}</td><td>{{user.pay_value}}</td><td>{{user.total_hours}}</td><td><a href="{{url_for('employee_report',username=user.username)}}"><button>More</button></a></td></tr> + #} + {{ user }} + {% endfor %} + </table> + </section> + {%- for x in ['reports','roles','users'] %} + {% include 'admin/'~x~'/widget.html' %} + {%- else-%} + {{ 'You do not have permission to access this page' }} + {%- endfor %} + </section> +{% endblock %} diff --git a/app/templates/admin/total_timedata_report/widget.html b/app/templates/admin/total_timedata_report/widget.html @@ -0,0 +1,81 @@ +<section class="agreements"> + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + <div id='messagebanner'><p>{{ message }}</p></div> + {% endfor %} + {% endif %} + {% endwith %} + <section class="employee-overlook"> + <h3>Employee Overlook</h3> + <div class="employee-hours"> + <table> + <tr> + <th>Employee</th> + <th>Total(hrs)</th> + <th>Lunch(#)</th> + <th>Per Diem(#)</th> + <th>Billable(hrs)</th> + </tr> + {% for entry in hours %} + <tr> + <td><a href="{{url_for('hours',username=entry._id)}}">{{ entry['_id'] }}</a></td> + <td><a href="{{url_for('hours',username=entry._id)}}">{{ (entry['totalTime']/(1000*60*60))|round(2) }}</a></td> + <td><a href="{{url_for('hours',username=entry._id)}}">{{ entry['lunchCount'] }}</a></td> + <td><a href="{{url_for('hours',username=entry._id)}}">{{ entry['perdiemCount'] }}</a></td> + <td><a href="{{url_for('hours',username=entry._id)}}">{{ (entry['totalHoursWorked']/(1000*60*60))|round(2) }}</a></td> + </tr> + {% endfor %} + </table> + </div> + <div class="employee-hours"> + <table> + <tr> + <th>Project</th> + <th>Total(hrs)</th> + <th>Lunch(#)</th> + <th>Per Diem(#)</th> + <th>Billable(hrs)</th> + </tr> + {% for project in tspp %} + <tr> + <td>{{project['_id']}}</td> + <td>{{(project['laborHoursWorked']/(1000*60*60))|round(2)}}</td> + <td>{{project['lunchCount']}}</td> + <td>{{project['perdiemCount']}}</td> + <td>{{(project['totalHoursWorked']/(1000*60*60))|round(2)}}</td> + </tr> + {% endfor %} + </table> + </div> + <div class="employee-hours"> + <table> + <tr> + <th>Employee</th> + <th>Total(hrs)</th> + <th>Lunch(#)</th> + <th>Per Diem(#)</th> + <th>Billable(hrs)</th> + </tr> + {% for entry in allhours %} + <tr> + <td><a href="{{url_for('hours',username=entry._id)}}">{{ entry['_id'] }}</a></td> + <td><a href="{{url_for('hours',username=entry._id)}}">{{ (entry['totalTime']/(1000*60*60))|round(2) }}</a></td> + <td><a href="{{url_for('hours',username=entry._id)}}">{{ entry['lunchCount'] }}</a></td> + <td><a href="{{url_for('hours',username=entry._id)}}">{{ entry['perdiemCount'] }}</a></td> + <td><a href="{{url_for('hours',username=entry._id)}}">{{ (entry['totalHoursWorked']/(1000*60*60))|round(2) }}</a></td> + </tr> + {% endfor %} + </table> + </div> + <div>Test section + {% for elmnt in projectlookup %} + {{elmnt}}</br> + {% endfor %} + + </div> + </section> + <section id="data-by-project"> + + </section> +</section> diff --git a/app/templates/admin/users/active.html b/app/templates/admin/users/active.html @@ -16,6 +16,7 @@ <tr><td>Address</td><td>{{ user.address }}</td></tr> <tr><td>Email</td><td>{{ user.email }}</td></tr> <tr><td>Pay Value</td><td>{{ user.pay_value }}</td></tr> + <button><a href="{{ url_for('deactivate_user',userid=user._id) }}">Deactivate User</a></button> </table> </div> {%- endfor %} diff --git a/app/templates/admin/users/inactive.html b/app/templates/admin/users/inactive.html @@ -3,9 +3,22 @@ {% block title %}All Inactive Users{% endblock %} {% block content %} +<section class="activeusers-grid"> {%- for user in inactiveusers %} - {%- print(user) %} - </br> - </br> + <div class="usercard"> + <h3>{{ user.fname }} {{ user.mname }} {{ user.lname }}</h3> + <table> + <tr><td>Username</td><td>{{user.username}}</td></tr> + <tr><td>Birthday</td><td>{{user.birthday}}</td></tr> + <tr><td>Role</td><td>{{user.role}}</td></tr> + <tr><td>Branch</td><td>{{user.branch}}</td></tr> + <tr><td>Phone Number</td><td>{{user.phonenumber}}</td></tr> + <tr><td>Address</td><td>{{user.address}}</td></tr> + <tr><td>Email</td><td>{{user.email}}</td></tr> + <tr><td>Pay Value</td><td>{{user.pay_value }}</td></tr> + <button><a href="{{ url_for('activate_user',userid=user._id) }}">Activate User</a></button> + </table> + </div> {%- endfor %} +</section> {% endblock %} diff --git a/app/templates/admin/users/newpass.html b/app/templates/admin/users/newpass.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block title %}New Password{% endblock %} + +{% block content %} +<section class="new-user-grid"> + <h3>New Password</h3> + <form action="" method="POST" novalidate> + {{ form.hidden_tag() }} + {% for error in form.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {% for ferror in form.form_errors %} + <span style="color:yellow;">[{{ ferror }}]</span> + {% endfor %} + {{ form.newpass.label }}{{ form.newpass() }}<br> + {{ form.confpass.label }}{{ form.confpass() }}<br> + {{ form.changePassword() }} + </form> + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + <p>{{ message }}</p> + {% endfor %} + {% endif %} + {% endwith %} +</section> +{% endblock %} diff --git a/app/templates/dashboard/activeusers/widget.html b/app/templates/dashboard/activeusers/widget.html @@ -25,12 +25,13 @@ <input type="checkbox" name="per_diem"><label for="per_diem">Per Diem</label> {% endif %} </a></td> - <td><button><a href="{{ url_for('hours',username=user.modified_by.0) }}">{{ user.clock_in.0.time().isoformat(timespec='minutes') }}</a></button></td><!-- can format/display non-military time with format %I:%M%p --> + <td><button><a href="{{ url_for('hours',username=user.modified_by.0) }}">{{ user.clock_in.0.isoformat(timespec='minutes') }}</a></button></td><!-- can format/display non-military time with format %I:%M%p removed.time() after index to try and show date value--> <td><button><a href="{{ url_for('clockout_by_id',modusernm=current_user.username,timeid=user._id) }}">Clock Out</a></button></td> </tr> {% endfor %} </table> </form> + <a href="{{ url_for('clockin_new_user') }}"><button>Clock in User</button></a> <!-- clock in clocked out user MIGHT NEED TO COMMENT OUT... DOES NOT GET DATA FROM FORMS ON SUBMIT... ONLY USES DEFAULT.data --> <!-- <form> <table> diff --git a/app/templates/dashboard/punchclock/index.html b/app/templates/dashboard/punchclock/index.html @@ -8,25 +8,59 @@ <h1 id="clock"></h1> <div><!-- abstract to payPeriod() --> <h6>$payperiod range</h6> - <h5>Total: {{ statement_hours }}</h5> + <!--<h5>Total:{# {{ statement_hours }} #}</h5> --> + {{availableProjects}} </div> + <a href="{{url_for('new_time',usernm=user.username)}}" style=" border: none; + background: var(--accent); + cursor: pointer; + border-radius: 3px; + padding: 6px; + width: 200px; + color: white; + margin-left: 25px; + box-shadow: 0 3px 6px 0 rgba(0,0,0,0.2);">New Time</a> <form action="" method="POST" novalidate> <table><tr> - {% for field in form %}{% if field.widget.input_type != 'hidden' %} + {% for field in form %}{% if field.widget.input_type != 'hidden' and field.widget.input_type != 'submit' %} <th>{{ field.label }}</th> {% endif %}{% endfor %}</tr> + <!--{# <tr> + <td>{{ form.dateSel() }}</td> + <td>{{ form.projectSel() }}</td> + <td>{{ form.startTime() }}</td> + <td>{{ form.endTime() }}</td> + <td>{{ form.lunchSel() }}</td> + <td>{{ form.perDiemSel() }}</td> + <td>{{ form.note() }}</td> + <td>{{ form.submitEntr() }}</td> + </tr> #}--> {% for entry in hours %} - <tr><!-- remove the forms and create each tr as a link to a modify entry['_id'] route(redirects back to hours) --> - <td>{{ entry.project }}{{ form.projectChg(default=entry.project) }}</td> - <td>{{ entry.clock_in.0.time().isoformat(timespec='minutes') }}{{ form.startTiChg() }}</td> - <td>{{ entry.clock_out.0.time().isoformat(timespec='minutes') }}{{ form.endTimeChg() }}</td> - <td>{{ entry.lunch }}{% if entry.lunch %}{{ form.lunchBxChg(default="checked")}}{%else%}{{ form.lunchBxChg() }}{% endif %} </td> - <td>{{ entry.per_diem }}{{ form.perDiemChg(default='checked') }}</td> - <td>{{ form.updateEntr() }}</td><!-- remove? --> - <td>{{ form.removeEntr() }}</td><!-- change to input type=submit like activecrewlist index pg --> - {{ form.hidden_tag() }} + <tr> + <td><a href="{{url_for('updateDate',mod_username=current_user.username,timeid=entry._id)}}">{{ entry.date.date().isoformat() }}</a></td> + <td>{{ entry.projectName }}</td> + <td><a href="{{url_for('updateStartTime',mod_username=current_user.username,timeid=entry._id)}}">{{ entry.clock_in[-1].time().isoformat(timespec='minutes') }}</a></td> + <td><a href="{{url_for('updateEndTime',mod_username=current_user.username,timeid=entry._id)}}">{{ entry.clock_out[-1].time().isoformat(timespec='minutes') }}</a></td> + {% if entry.lunch %} + <td><a href="{{url_for('toggle_lunch',timeid=entry._id)}}">Yes</a></td> + {% else %} + <td><a href="{{url_for('toggle_lunch',timeid=entry._id)}}">No</a></td> + {% endif %} + {% if entry.per_diem %} + <td><a href="{{url_for('toggle_per_diem',timeid=entry._id)}}">Yes</a></td> + {% else %} + <td><a href="{{url_for('toggle_per_diem',timeid=entry._id)}}">No</a></td> + {% endif %} + {% if entry.note %} + <td><a href="{{url_for('updateNote',mod_username=current_user.username,timeid=entry._id)}}">{{ entry.note }}</a></td> + {% else %} + <td><a href="{{url_for('updateNote',mod_username=current_user.username,timeid=entry._id)}}">Add Note</a></td> + {% endif %} + <td><button><a href="{{url_for( 'removetime',timeid=entry._id) }}">Remove</a></button></td> + {#{{ form.hidden_tag() }}#} </tr> + {# {{entry}} #} {% endfor %} </table> </form> diff --git a/app/templates/dashboard/punchclock/otheruser.html b/app/templates/dashboard/punchclock/otheruser.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block title %}Clock In User{% endblock %} + +{% block content %} + <section class="hours-grid"> + <h1 id="clock"></h1> + <form action="" method="POST" novalidate> + <table> + {% for field in form %}{% if field.widget.input_type != 'hidden' and field.widget.input_type != 'submit' %} + <tr><td>{{ field.label }}</td><td>{{ field }}</td></tr> + {% endif %}{% endfor %} + </table> + {{ form.submitEntr() }} + {{ form.hidden_tag() }} + </form> + </section> +{% endblock %} diff --git a/app/templates/dashboard/punchclock/update/date.html b/app/templates/dashboard/punchclock/update/date.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block title %}Update Date{% endblock %} + +{% block content %} +<section class="new-agreement-grid"> + <h3>Update Date</h3> + <form action="" method="POST" novalidate> + {{ form.hidden_tag() }} + {% for error in form.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {% for ferror in form.form_errors %} + <span style="color:yellow;">[{{ ferror }}]</span> + {% endfor %} + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + <p style='color:red'>{{ message }}</p> + {% endfor %} + {% endif %} + {% endwith %} + {{ form.dateSel.label }}{{ form.dateSel() }}<br> + {{ form.submitEntr() }} + </form> +</section> +{% endblock %} diff --git a/app/templates/dashboard/punchclock/update/endTime.html b/app/templates/dashboard/punchclock/update/endTime.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block title %}Update End Time{% endblock %} + +{% block content %} +<section class="new-agreement-grid"> + <h3>Update Ending Time</h3> + <form action="" method="POST" novalidate> + {{ form.hidden_tag() }} + {% for error in form.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {% for ferror in form.form_errors %} + <span style="color:yellow;">[{{ ferror }}]</span> + {% endfor %} + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + <p style='color:green'>{{ message }}</p> + {% endfor %} + {% endif %} + {% endwith %} + {{ form.timeSel.label }}{{ form.timeSel() }}<br> + {{ form.submitEntr() }} + </form> +</section> +{% endblock %} diff --git a/app/templates/dashboard/punchclock/update/note.html b/app/templates/dashboard/punchclock/update/note.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block title %}Update Note{% endblock %} + +{% block content %} +<section class="new-agreement-grid"> + <h3>Update Note</h3> + <form action="" method="POST" novalidate> + {{ form.hidden_tag() }} + {% for error in form.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {% for ferror in form.form_errors %} + <span style="color:yellow;">[{{ ferror }}]</span> + {% endfor %} + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + <p style='color:red'>{{ message }}</p> + {% endfor %} + {% endif %} + {% endwith %} + {{ form.note.label }}{{ form.note() }}<br> + {{ form.submitEntr() }} + </form> +</section> +{% endblock %} diff --git a/app/templates/dashboard/punchclock/update/startTime.html b/app/templates/dashboard/punchclock/update/startTime.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block title %}Update Start Time{% endblock %} + +{% block content %} +<section class="new-agreement-grid"> + <h3>Update Starting Time</h3> + <form action="" method="POST" novalidate> + {{ form.hidden_tag() }} + {% for error in form.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {% for ferror in form.form_errors %} + <span style="color:yellow;">[{{ ferror }}]</span> + {% endfor %} + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + <p style='color:green'>{{ message }}</p> + {% endfor %} + {% endif %} + {% endwith %} + {{ form.timeSel.label }}{{ form.timeSel() }}<br> + {{ form.submitEntr() }} + </form> +</section> +{% endblock %} diff --git a/app/templates/dashboard/punchclock/widget.html b/app/templates/dashboard/punchclock/widget.html @@ -15,9 +15,9 @@ <form class="widget-form" action="" method="POST" novalidate> <div> {{ clockoutform.hidden_tag() }} - <p>{{ clockoutform.projectsSel.label }}<br>{{ clockoutform.projectsSel() }}</p> <p>{{ clockoutform.lunchBox() }} {{ clockoutform.lunchBox.label }} {{ clockoutform.per_diemBox() }} {{ clockoutform.per_diemBox.label }}</p> + <p>{{ clockoutform.recapOrNote.label }} {{ clockoutform.recapOrNote() }}</p> <p>{{ clockoutform.clockout() }}</p> </div> </form> @@ -27,6 +27,6 @@ <!-- Add iff satement for clocked_in==True <p> form.projects(choices=projects,default=(if(project[0]==0){return project[0].label })</p> --> - <button><a href="{{ url_for('hours',username=current_user.username) }}">My Hours</a></button> + <a href="{{ url_for('hours',username=current_user.username) }}"><button>My Hours</button></a> <h2>{{ current_user.fname }} {{ current_user.lname }}</h2> </section> diff --git a/app/templates/knowlegebase/index.html b/app/templates/knowlegebase/index.html @@ -3,6 +3,11 @@ {% block title %}User Documentation{% endblock %} {% block content %} + <section class="user-settings"> + <a href="{{url_for('chgpass')}}"><button>New Password</button></a> + + </section> + <section class="documentation-container"> <section class=""> <h3 class="documentation-header">User Documentation</h3> diff --git a/app/templates/login.html b/app/templates/login.html @@ -3,14 +3,14 @@ {% block title %}Login{% endblock %} {% block content %} -<section class="login-grid"> {% with messages = get_flashed_messages() %} {% if messages %} {% for message in messages %} - <p>{{ message }}</p> + <div id="messagebanner"><p>{{ message }}</p></div> {% endfor %} {% endif %} {% endwith %} +<section class="login-grid"> <form class="login" action="" method="post" novalidate> {{ form.hidden_tag() }} <p> diff --git a/database.ini b/database.ini @@ -1,5 +1,5 @@ # [PROD] # DB_URI = mongodb+srv://<username>:<password>@localhost:27017 -[TEST] -DB_URI = mongodb+srv://nikolas:password@localhost:27017 -\ No newline at end of file +#[TEST] +#DB_URI = mongodb+srv://nikolas:password@localhost:27017