commit 1366076ae035da4cb779c5ae50881ebd5523b97b parent f37a573f9377a4426c530e479d36543e8ed75f9a Author: Brennen T. Mazur <dev@brennen.work> Date: Tue, 11 Jun 2024 15:40:19 -0600 Merge branch 'prod' Diffstat:
139 files changed, 9541 insertions(+), 5 deletions(-)
diff --git a/.flaskenv b/.flaskenv @@ -0,0 +1 @@ +FLASK_APP=stc.py diff --git a/.gitignore b/.gitignore @@ -0,0 +1,9 @@ +config.py +db.py +seeds.py +newtimes.py +certs/ +.env/ +__pycache__/* +*/__pycache__/* +*.swp diff --git a/.old.app.py b/.old.app.py @@ -0,0 +1,83 @@ +import datetime +import json +from models import Users +from flask import Flask, render_template, abort, redirect, url_for, request, session, jsonify; +from flask_login import UserMixin, login_user, LoginManager, login_required, logout_user, current_user; +from pymongo import MongoClient; +from flask_bcrypt import Bcrypt; +# import routes + +OrganizationName = "Youth Employment Program" + +app = Flask(__name__) + + +# Mongo setup +client = MongoClient('localhost', 27017) +bcrypt = Bcrypt(app) +db = client['simple_timecard_database'] +app.config['SECRET_KEY'] = 'secretkey' +# Mongo setup + + +# Database collections/documents +users_col = db.users_collection # Make aditional user info single document/array + +time_col = db.time_data_collection + +fleet_col = db.fleet_collection + +agreement_col = db.agreement_collection +# Database collections/documents + + +# Page Routes +@app.route('/') #main route should check if user is logged in, then redirect to /dashboard else redirect to /login +def hello(): + return render_template('index.html',ORGNAME = OrganizationName) #This implimentation is messy, maybe abstract to a defPage()? + +@app.route("/login") +def login(): + return render_template('login.html',ORGNAME = OrganizationName) + +@app.route("/dashboard") +def dashboard(): + return render_template('dashboard/layout.html',currenttime=datetime.datetime.utcnow(),ORGNAME=OrganizationName) + +@app.route("/admin") +def admin(): + return render_template ('admin/layout.html',ORGNAME=OrganizationName) + +@app.route("/hours")#modify to take userid ex. /hours<userid> for "admin" +def hours():#userid goes into call to db to get user[] -> then returns formatted table (punchclock/index.html + return render_template ('dashboard/punchclock/index.html',ORGNAME=OrganizationName) + +@app.route("/fleet") +def fleet(): + return render_template('dashboard/fleet/index.html',ORGNAME=OrganizationName) + +@app.route("/admin/roles") +def roles(): + return render_template('admin/roles/index.html',ORGNAME=OrganizationName) + +@app.route("/admin/agreement") +def agreement(): + return render_template('admin/agreement/index.html',ORGNAME=OrganizationName) + +@app.route("/docs") +def knowlegebase(): + return render_template('knowlegebase/index.html',ORGNAME=OrganizationName) + +# Page Routes + + +# Model Routes + +@app.route('/user/signup', methods=['GET']) +def signup(): + return Users().signup() + +# Model Routes + +if __name__ == '__main__': + app.run(debug=True) diff --git a/.old.models.py b/.old.models.py @@ -0,0 +1,146 @@ +# Test enviroment added validators username +# removed BaseModel +# added jsonify +# fixed datetime + +import datetime +import uuid +from flask import Flask, jsonify, request, redirect, session +from fastapi.encoders import jsonable_encoder +from passlib.hash import pbkdf2_sha256 # Replace with Brennen's hash when he finds it +from typing import List, Optional +from pydantic import Field, ValidationError, validator +from app import db + +class Users: + + def start_session(self, user): + del user['password'] + session['logged_in'] = True + session['user'] = user + return jsonify(user), 200 + + def signup(self): + print(request.form) + users = { + '_id': uuid.uuid4().hex, + 'username': request.form.get('Username'), + 'password': request.form.get('Password'), + 'role': request.form.get('Role'), + 'location': request.form.get('Primary Location'), + 'phone': request.form.get('Phone Number'), + 'email': request.form.get('Email'), + 'pay_period': request.form.get('Pay Period'), + 'pay_value': request.form.get('Pay Value'), + } + + # users['password'] = pbkdf2_sha256.encrypt(users['password']) + + return jsonify(users), 200 + + def signout(self): + session.clear() + return redirect('/login') + + # def login(self): + # users = db.users.find_one({ + # 'email': request.form.get('email') + # }) + + # if users and pbkdf2_sha256.verify(request.form.get('password'), users['password']): + # return self.start_session(users) + + # return jsonify({ 'error': 'Invalid login' }), 401 + + @validator('username') + def username_alphanumeric(cls, v): + assert v.isalnum(), 'Username must be alphanumeric' + return v + +class Time: + + def clockin(self): + clockin = { + '_id': int, + 'clock_in': Optional[datetime.datetime.utcnow], + 'date': Optional[datetime.date], + 'project': str, + 'note': str, + } + + return jsonify(clockin), 200 + + def clockout(self): + clockout = { + '_id': int, + 'clock_out': Optional[datetime.datetime.utcnow], + 'date': Optional[datetime.date], + 'project': str, + 'note': Optional[str], + 'total_time': int + } + + return jsonify(clockout), 200 + + _id: int + # forign key + clock_in: Optional[datetime.datetime.utcnow] #System time + modified_by: str #link to _id of user + date: Optional [datetime.date] + project: str + clock_out: Optional[datetime.datetime.utcnow] #System time + note: str + perdium: bool + total_time: int #clock_out - clock_in + + def to_json(self): + return jsonable_encoder(self, exclude_none=True) + + def to_bson(self): + data = self.dict(by_alias=True, exclude_none=True) + + if data["_id"] is None: + data.pop("_id") + return data + + +class Fleet: + _id: int + date: Optional[datetime.datetime.utcnow] + operator: int #forign key to userID + safety_checks: bool #array for different safety checks + additional_notes: str + vehicle: int #vehicleID + incident_report: str + mileage: int + + def to_json(self): + return jsonable_encoder(self, exclude_none=True) + + def to_bson(self): + data = self.dict(by_alias=True, exclude_none=True) + + if data["_id"] is None: + data.pop("_id") + return data + + +class Agreement: + _id: int #forign key to user + start_date: int + end_date: int + bid_document: str #Filepath to document + budget: float + cost: int + +class Projects:#Projects references agreement + project_id: int + project_name: str + project_budget: List[float] = [ + # labor_budget: float | Indexed 0 + # travel_budget: float | Indexed 1 + # supplies_budget: float | Indexed 2 + # contact_budget: float | Indexed 3 + # equipment_budget: float | Indexed 4 + # other: float | Indexed 5 + ] +\ No newline at end of file diff --git a/README b/README @@ -1,12 +1,13 @@ stc is a simple time card implementation for personal use or small to medium size businesses. -The goal of this project is to create an extensible tool for time and other asset management. +The goal of this project is to create an extensible tool for time and asset management. -Current plans are to use mongoDB as the backend to a simplistic dashboard for accessing management interfaces, overseeing and managing lower role's clocked in status, and requesting reports for not only current/past pay periods sorted by employee, but also by project code +Current plans are to use mongoDB as the backend to a simplistic dashboard for accessing management interfaces, overseeing and managing role's clocked in status, and requesting reports for not only current/past pay periods sorted by employee, but also by project code Project has potential to be renamed to scms (simple content management system) based on potential end-goals of multiple clients. -An option would be to create the base structure for the cms (as human resource management [ex. time tracking]) and allow patches to include additional functionality like equipment tracking etc. Allows no-nonsence approach to anti-bloat management software. -Likely will be lisencing under some copyleft lisence, GPLv3 or LGPLv3. Also mulling over options for BSD lisences. +An option would be to create the base structure for the cms (as human resource management [ex. time tracking]) and allow patches to include additional functionality like equipment tracking etc. -Work is currently ongoing on the wireframe for the landing and signin page. +templates holds each "component" either a standalone file, or a directory for a index.html(full page route) and widget.html(dashboard interactable view) + +For organization specifics... it might be usefull to create diff files for specific additions etc? Possibly too costly to maintain over master version changes. This needs to be determined and refined. diff --git a/StructureProposals/ROLEDEFAULTS.md b/StructureProposals/ROLEDEFAULTS.md @@ -0,0 +1,27 @@ +ROLES + + Accountant { + PermissiveModules['clockin','fleet','agreements','reports','roles'] + BasePay=$17.00/hr + BaseSalary=$xxxxx + }, + Project Manager { + PermissiveModules['clockin','fleet','activeusers','agreements','reports','roles'] + BasePay=$17.00/hr + }, + Crew Lead { + PermissiveModules['clockin','fleet','activeusers'] + BasePay=$17.00/hr + }, + Assistant Lead { + PermissiveModules['clockin','fleet'] + BasePay=$13.00/hr + }, + Crew Level 2 { + PermissiveModules['clockin'] + BasePay=$11.00/hr + }, + Crew Level 1 { + PermissiveModules['clockin'] + BasePay=$9.25/hr + } diff --git a/TODO.md b/TODO.md @@ -0,0 +1,54 @@ +# MAJOR General Functionality +- [ ] Agreement and Project Management Refining +- [ ] Clean up interface +- [ ] Functional Fleet management and checkin/checkout +- [ ] Navigation tweaks +- [ ] Active(clocked in) Users[widget] should be refined to Active Projects for scalability, selecting a clocked in project shows crew members clocked into said project. +- [ ] Progressive Web App Implementation +- [ ] css sidebar for admin and accessable toolbar +- [ ] fleet entries must function + +# MINOR Functional tasks +- [ ] users page change fname, mname, lname links +- [ ] Consolidate active and inactive users page. +- [ ] Consolidate update and new form pages + - [ ] users + - [ ] project + - [ ] agreement +- [ ] When users are set to inactive or active (probably just inactive) should remove all current/existing sessions +- [ ] executive director sets default agreement wages per role, on end of fiscal year(Nolan says March 1st) set confirmed:False and prompt Executive director for new values (might as well offer fringe update) +- [ ] Change branch for project must allow for Global change(check if form.projSel.data == "Global") +- [X] newproject should take values for FE#, Name, and optional description? + - [ ] Should add check to enforce unique`project_name... Easy to do if check value of FE integer after submission of form.` +- [ ] add remove agreement + WARNING CONFIRM form + - [ ] must move or remove child projects (NOLAN SAYS JUST WARN NEEDS TO BE ADDRESSED AND REDIRECT DON'T REMOVE) + - [ ] remove times if project is deleted? Move to YEP General to continue to pay employees? MUST ADDRESS ENTRIES BEFORE PROJECT IS ACTUALLY REMOVED +- [X] add change agreement name +- [>] add remove project + WARNING CONFIRM form + - [ ] must offer to change all time entries under project to another project CURRENTLY ORPHANS ENTRIES + - [X] ''' + db.agreements_collection.updateOne({agreement_name:project_name},{$pull:{projects:{$in:[project_id]}}}) + ''' +- [X] add change project name +- [X] add move project + - [X] ''' + db.agreements_collection.updateOne({agreement_name:project_current_agreement_name},{$pull:{projects:{$in:[project_id]}}}) + ''' + - [X] ''' + db.agreements_collection.updateOne({agreement_name:project_new_agreement_name},{$push:{projects:project_id}}) + ''' + - [X] `db.projects_collection.updateOne({_id:project_id},{agreement:new_agreement_id})` +- [X] hours page change project linking +- [X] hours page swap to aggregate and allow sort accending recent +- [>] time entries must enforce positive time values + - [x] form requires positive values through validation + - [ ] update forms must check present values (must check after submit) +- [X] rework admin page flow to show agency for projects?... wanted to sort and display by agency, but is many to one relationship... +### Unclear on how to do the below, doesn't appear to be able to remove session from client or make invalid session... +### Will likely have to overload the @login_required fn to check for user is_active... will have to on route check for updates, not sure if user object is refreshing every page... likely just being set on login. +- [X] Change username to firstname lastname for today +- [X] change all match from feb 1 to today for nolan to tnc management + +If genrate spreadsheet fleet is billable calculate fleet, else return 0 on sheet. diff --git a/app/__init__.py b/app/__init__.py @@ -0,0 +1,19 @@ +from flask import Flask +from config import Config + +app = Flask(__name__) +app.config.from_object(Config) + +from app.meetings import bp as meetings_bp +app.register_blueprint(meetings_bp) #app.register_blueprint(meetings_bp, url_prefix='/meeting') + +from app.fleet import bp as fleet_bp +app.register_blueprint(fleet_bp) #app.register_blueprint(fleet_bp, url_prefix='/fleet') + +from app.equipment import bp as equipment_bp +app.register_blueprint(equipment_bp) #app.register_blueprint(equipment_bp, url_prefix='/equipment') + +from app.branches import bp as branch_bp +app.register_blueprint(branch_bp) #app.register_blueprint(equipment_bp, url_prefix='/equipment') + +from app import routes, models diff --git a/app/branches/__init__.py b/app/branches/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('branches',__name__, template_folder='templates') + +from app.branches import routes diff --git a/app/branches/__pycache__/__init__.cpython-310.pyc b/app/branches/__pycache__/__init__.cpython-310.pyc Binary files differ. diff --git a/app/branches/__pycache__/forms.cpython-310.pyc b/app/branches/__pycache__/forms.cpython-310.pyc Binary files differ. diff --git a/app/branches/__pycache__/routes.cpython-310.pyc b/app/branches/__pycache__/routes.cpython-310.pyc Binary files differ. diff --git a/app/branches/forms.py b/app/branches/forms.py @@ -0,0 +1,19 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, PasswordField, BooleanField, SelectField, TimeField, DateField, IntegerField +from wtforms.validators import DataRequired, optional, length, InputRequired, EqualTo + +class NewBranch(FlaskForm): + branch_name = StringField('Branch Name', validators=[DataRequired()]) + address = StringField('Address',validators=[DataRequired()]) + city = StringField('City',validators=[DataRequired()]) + state = StringField('State',validators=[DataRequired()]) + zipcode = StringField('Zipcode', validators=[DataRequired()]) + submit_branch = SubmitField('Add Branch') + +class UpdateBranch(FlaskForm): + branch_name = StringField('Branch Name') + address = StringField('Address') + city = StringField('City') + state = StringField('State') + zipcode = StringField('Zipcode') + update_branch= SubmitField('Update Branch') diff --git a/app/branches/routes.py b/app/branches/routes.py @@ -0,0 +1,166 @@ +from app import app +from flask_pymongo import PyMongo +from flask import render_template, redirect, url_for, flash, request +from flask_login import login_required +from app.branches import bp +from bson.objectid import ObjectId +import datetime, hashlib +import os +from app.branches.forms import NewBranch, UpdateBranch +#from app.meetings.update import +#from app.meetings.meeting import + +mongo = PyMongo(app) + +### Define fetch_meeting ### +#TRY TO MOVE TO app.meetings.meeting.py +class BranchNotFoundError(Exception): + pass + +def fetch_branch(branch_id): + branch = mongo.db.branch_collection.find_one({"_id":ObjectId(branch_id)}) + + if branch == None: + raise BranchNotFoundError(f'Branch Id {branch_id} returned None') + else: + return branch + +def get_available_branches(): + availableBranches = [] + for branch in mongo.db.branch_collection.find(): + availableBranches.append((branch['_id'],branch['branch_name'])) + return availableBranches + +### BEGIN DEV ROUTES ### + +@bp.route('/branches/seed',methods=["GET","PUT"]) +@login_required +def branchesSeed(): + seeds = [ + { + "branch_name": "Dillon", + "address":"730 North Montana St", + "city":"Dillon", + "state":"MT", + "zipcode": "88888" + }, + { + "branch_name": "Salmon", + "address":"601 Lena St", + "city":"Salmon", + "state":"ID", + "zipcode": "99999" + }, + { + "branch_name":"BLM Internship", + "address":"730 North Montana St", + "city":"Dillon", + "state":"MT", + "zipcode": "777777" + } + ] + mongo.db.branch_collection.delete_many({}) + mongo.db.branch_collection.insert_many(seeds) + dev_branches = mongo.db.branch_collection.find() + return render_template('dev_branches.html',dev_branches=dev_branches) + +@bp.route('/branches/dev',methods=["GET"]) +@login_required +def allBranches(): + dev_branches = mongo.db.branch_collection.find({}) + return render_template('dev_branches.html',dev_branches=dev_branches) + +### END DEV ROUTES ### + +#### BEGIN ROUTES #### + +@bp.route('/branches',methods=["GET"]) +@bp.route('/branches/',methods=["GET"]) +@login_required +def branches(): + branches = mongo.db.branch_collection.find({}) + return render_template('branches.html',branches=branches) + +@bp.route('/branch/<branch_id>',methods=["GET"]) +@bp.route('/branch/<branch_id>/',methods=["GET"]) +def branch(branch_id): + try: + branch = fetch_branch(branch_id) + except BranchNotFoundError as e: + return render_template('error.html',error=e) + else: + return render_template('branch.html',branch=branch) + +@bp.route('/branch/new',methods=["GET","POST"]) +@login_required +def new_branch(): + form = NewBranch() + if form.validate_on_submit(): + try: + new_branch = {'branch_name':form.branch_name.data,'address':form.address.data,'city':form.city.data,'state':form.state.data,'zipcode':form.zipcode.data} + if form.branch_name.data == "": + new_branch['branch_name']=form.city.data + except Exception: + return "Error assigning form data" + else: + mongo.db.branch_collection.insert_one(new_branch) + flash("Created new branch!") + return redirect(url_for('branches.branches')) + +# if form.name.data != "": +# new_meeting['meeting_name']=form.name.data + return render_template('new_branch.html',form=form) + +@bp.route('/branch/<branch_id>/update',methods=["GET","POST"]) +@login_required +def update_branch(branch_id): + try: + branch = fetch_branch(branch_id) + except BranchNotFoundError as e: + return render_template('error.html',error=e) + else: + return render_template('update_branch.html',branch=branch) + +@bp.route('/branch/<branch_id>/<update>',methods=["GET","POST"]) +@login_required +def change_branch(branch_id,update): + form = UpdateBranch() + try: + branch = fetch_branch(branch_id) + except BranchNotFoundError as e: + return render_template('error.html',error=e) + else: + if form.validate_on_submit(): + if update == "branch_name": + mongo.db.branch_collection.update_one({'_id':branch['_id']},{'$set':{'branch_name':form.branch_name.data}}) + flash("Updated branch name from {} to {}".format(branch['branch_name'],form.branch_name.data)) + return redirect(url_for('branches.update_branch',branch_id=branch['_id'])) + if update == "address": + mongo.db.branch_collection.update_one({'_id':branch['_id']},{'$set':{'address':form.address.data}}) + flash("Updated address from {} to {}".format(branch['address'],form.address.data)) + return redirect(url_for('branches.update_branch',branch_id=branch['_id'])) + if update == "city": + mongo.db.branch_collection.update_one({'_id':branch['_id']},{'$set':{'city':form.city.data}}) + flash("Updated city from {} to {}".format(branch['city'],form.city.data)) + return redirect(url_for('branches.update_branch',branch_id=branch['_id'])) + if update == "state": + mongo.db.branch_collection.update_one({'_id':branch['_id']},{'$set':{'state':form.state.data}}) + flash("Updated state to {}".format(form.state.data)) + return redirect(url_for('branches.update_branch',branch_id=branch['_id'])) + if update == "zipcode": + mongo.db.branch_collection.update_one({'_id':branch['_id']},{'$set':{'zipcode':form.zipcode.data}}) + flash("Updated zipcode to {}".format(form.zipcode.data)) + return redirect(url_for('branches.update_branch',branch_id=branch['_id'])) + return render_template('form_branches.html',branch=branch,update=update,form=form) + +@bp.route('/branch/<branch_id>/remove',methods=["GET","POST"]) +@login_required +def remove_branch(branch_id): + try: + branch = fetch_branch(branch_id) + except BranchNotFoundError as e: + return render_template('error.html',error=e) + else: + mongo.db.branch_collection.delete_one(branch) + flash("Deleted branch {}".format(branch['branch_name'])) + return redirect(url_for('branches.branches')) diff --git a/app/branches/templates/branch.html b/app/branches/templates/branch.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block title %}{{ branch['branch_name'] }} Branch{% endblock %} + +{% block content %} + {% if branch %} + <section class="hours-grid"> {# class="meeting">#} + <a href="{{ url_for('branches.branches') }}" class="action-button">back to branches</a> + <h2 style="color:red;align:left">{{ branch['branch_name'] }}</h2> + <div><p>{{ branch['address'] }}</p></div> + <a class="action-button" href="{{ url_for('branches.update_branch',branch_id=branch['_id']) }}">modify</a> + </section> + {% endif %} +{% endblock %} diff --git a/app/branches/templates/branches.html b/app/branches/templates/branches.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% block title %}Branches{% 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> + {% if branches %} + <section class="branches"> + <h2 style="color:red">Branches</h2> + {% for branch in branches %} + <a href="{{ url_for('branches.branch',branch_id=branch['_id']) }}"> + <h3>{{ branch.branch_name }}</h3> + <div>{{ branch.address }}</div> + <div>{{ branch['city'] }}</div> + <div>{{ branch['state'] }}</div> + <div>{{ branch['zipcode'] }}</div> + </a> + {% endfor %} + </section> + {% endif %} + <a href="{{ url_for('branches.new_branch') }}" class="action-button">Add new Branch</a> + </section> +{% endblock %} diff --git a/app/branches/templates/dev_branches.html b/app/branches/templates/dev_branches.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block title %}DEV Seed Branches{% endblock %} + +{% block content %} + + {%- for x in dev_branches %} + {%- print(x) %} + </br> + </br> + {%- endfor %} + +{% endblock %} diff --git a/app/branches/templates/error.html b/app/branches/templates/error.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block title %}Error{% endblock %} + +{% block content %} + +<div style="text-align:center;"> + <h3>{{ error }}</h3> + <a href="{{ url_for('branches.branches') }}">back to branches</a> +</div> + +{% endblock %} diff --git a/app/branches/templates/form_branches.html b/app/branches/templates/form_branches.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} + +{% block title %}Update Branch{% endblock %} + +{% block content %} +<section> + <a href="{{ url_for('branches.update_branch',branch_id=branch['branch_id']) }}">back to branch</a> + <h3>Update Branch</h3> +<form action="" method="POST" novalidate> + {{ form.hidden_tag() }} + {% for error in form.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {% if update == "branch_name" %}{{ form.branch_name.label }}{{ form.branch_name() }}{% endif %} + {% if update == "address" %}{{ form.address.label }}{{ form.address() }}{% endif %} + {% if update == "city" %}{{ form.city.label }}{{ form.city() }}{% endif %} + {% if update == "state" %}{{ form.state.label }}{{ form.state() }}{% endif %} + {% if update == "zipcode" %}{{ form.zipcode.label }}{{ form.zipcode() }}{% endif %} + {{ form.update_branch() }} +</form> +</section> +{% endblock %} diff --git a/app/branches/templates/new_branch.html b/app/branches/templates/new_branch.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} + +{% block title %}New Branch{% endblock %} + +{% block content %} +<section> + <a href="{{ url_for('branches.branches') }}">back to branches</a> +<h3>Add new Branch</h3> +<form action="" method="POST" novalidate> + {{ form.hidden_tag() }} + {% for error in form.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {{ form.branch_name.label }}{{ form.branch_name() }} + {{ form.address.label }}{{ form.address() }} + {{ form.city.label }}{{ form.city() }} + {{ form.state.label }}{{ form.state() }} + {{ form.zipcode.label }}{{ form.zipcode() }} + {{ form.submit_branch() }} +</form> +</section> +{% endblock %} diff --git a/app/branches/templates/update_branch.html b/app/branches/templates/update_branch.html @@ -0,0 +1,25 @@ +{% extends 'base.html' %} + +{% block title %}Update {{ branch.branch_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 %} + {% if branch %} + <section class="branch"> + <a href="{{ url_for('branches.branch',branch_id=branch['_id']) }}">back to branch</a> + <div style="display:flex"><h2>Date: {{ branch.branch_name }}</h2><a href="{{url_for('branches.change_branch',branch_id=branch["_id"],update="branch_name")}}"style="color:red">change</a></div> + <div style="display:flex"><h3>Address: {{ branch.address }}</h3><a href="{{url_for('branches.change_branch',branch_id=branch['_id'],update="address")}}"style="color:red">change</a></div> + <div style="display:flex"><h3>City: {{ branch.city }}</h3><a href="{{url_for('branches.change_branch',branch_id=branch['_id'],update="city")}}"style="color:red">change</a></div> + <div style="display:flex"><h3>State: {{ branch.state }}</h3><a href="{{url_for('branches.change_branch',branch_id=branch['_id'],update="state")}}"style="color:red">change</a></div> + <div style="display:flex"><h4 href="color:red">Zipcode: {{ branch.zipcode }}</h4><a href="{{ url_for('branches.change_branch',branch_id=branch['_id'],update="zipcode")}}" style="color:red">change</a></div> + <a href="{{ url_for('branches.branch',branch_id=branch['_id']) }}">back to branch</a> + <a href="{{ url_for('branches.remove_branch',branch_id=branch['_id']) }}">remove branch</a> + </section> + {% endif %} +{% endblock %} diff --git a/app/equipment/__init__.py b/app/equipment/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('equipment',__name__,template_folder='templates') + +from app.equipment import routes diff --git a/app/equipment/__pycache__/__init__.cpython-310.pyc b/app/equipment/__pycache__/__init__.cpython-310.pyc Binary files differ. diff --git a/app/equipment/__pycache__/equipment.cpython-310.pyc b/app/equipment/__pycache__/equipment.cpython-310.pyc Binary files differ. diff --git a/app/equipment/__pycache__/forms.cpython-310.pyc b/app/equipment/__pycache__/forms.cpython-310.pyc Binary files differ. diff --git a/app/equipment/__pycache__/routes.cpython-310.pyc b/app/equipment/__pycache__/routes.cpython-310.pyc Binary files differ. diff --git a/app/equipment/equipment.py b/app/equipment/equipment.py @@ -0,0 +1,84 @@ +from app import app +from flask_pymongo import PyMongo +from bson.objectid import ObjectId + +mongo = PyMongo(app) + +class EquipmentNotFoundError(Exception): + pass + +def fetch_equipment(equipment_id): + equipment = mongo.db.equipment_collection.find_one({"_id":ObjectId(equipment_id)}) + + if equipment == None: + raise EquipmentNotFoundError(f'Equipment Id {equipment_id} returned None') + else: + return equipment + +# return all past and present equipment +def get_all_equipment(): + equipment = mongo.db.equipment_collection.find() + return equipment + +# return all equipment not damaged, in use, and not in use presently +def get_usable_equipment(): + equipment = mongo.db.equipment_collection.find({'retired':{'$exists':False}}) + return equipment + +# return equipment not damaged, and ready to use, does not return equipment in use presently +def get_available_equipment(): + equipment = mongo.db.equipment_collection.find({'retired':{'$exists':False},'checked_out':{'$exists':False}}) + return equipment + +# return available equipment by type +def get_available_equipment_type(equipment_type,branch=None): + matchdoc = {"equipment_type":equipment_type,'checked_out':{'$exists':False},'retired':{'$exists':False}} + if branch != None: + matchdoc["branch"] = branch + equipment = mongo.db.equipment_collection.aggregate( [ + { + "$match": matchdoc + }, + { + "$sort": { + "equipment_type":1, + "purchase_timestamp":1 + } + } + ] ) + return equipment + +# return document of equipment type (_id) and count(number of occurances) +def get_equipment_types(): + types = mongo.db.equipment_collection.aggregate( [ + { + '$group':{ + '_id':'$equipment_type', + 'count': {'$count':{} } + } + }, + { '$sort':{'equipment_type':1} } + ] ) + return types + +# return tuple list ('type','type (count)') +def count_equipment_types(): + counts = [] + for tool in get_equipment_types(): + label = "{} ({})".format(tool['_id'],tool['count']) + counts.append((tool['_id'],label)) + return counts + +# update db and returns equipment with checked_out attribute set to True by id +def checkout_equipment_id(equipment_id): + equipment = fetch_equpiment(equipment_id) + equipment['checked_out'] = True + mongo.db.equipment_collection.update({'_id':equipment_id},{'$set':{'checked_out':True}}) + return equipment + +# update db and returns equipment with popped checked_out attribute by id +def checkin_equipment_id(equipment_id): + equipment = fetch_equipment(equipment_id) + equipment.pop('checked_out') + mongo.db.equipment_collection.update({'_id':equipment_id},{'$unset':{'checked_out':""}}) + return equipment diff --git a/app/equipment/forms.py b/app/equipment/forms.py @@ -0,0 +1,23 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, SelectField, DateField, FloatField, IntegerField +from wtforms.validators import DataRequired, optional, length, InputRequired, EqualTo + +class NewEquipment(FlaskForm): + equipment_type = SelectField("Type",validators=[DataRequired()]) + equipment_type_number = IntegerField("Equipment Number",validators=[optional()]) + branch = SelectField("Branch", validators=[DataRequired()]) + purchase_timestamp = DateField("Purchase Date",validators=[DataRequired()]) + purchase_price = FloatField("Purchase Price", validators=[optional()]) + match_percentage = FloatField("Percentage Match",validators=[optional()]) + purchasing_project = SelectField("Purchasing Project",validators=[optional()]) + submit_equipment = SubmitField("Submit Equipment") + +class UpdateEquipment(FlaskForm): + equipment_type = SelectField("Type",validators=[optional()]) + equipment_type_number = IntegerField("Equipment Number",validators=[optional()]) + branch = SelectField("Branch", validators=[optional()]) + purchase_timestamp = DateField("Purchase Date",validators=[optional()]) + purchase_price = FloatField("Purchase Price", validators=[optional()]) + match_percentage = FloatField("Percentage Match",validators=[optional()]) + purchasing_project = SelectField("Purchasing Project",validators=[optional()]) + submit_equipment = SubmitField("Submit Equipment") diff --git a/app/equipment/routes.py b/app/equipment/routes.py @@ -0,0 +1,216 @@ +from app import app +from flask_pymongo import PyMongo +from flask import render_template, redirect, url_for, flash, request +from flask_login import login_required +from bson.objectid import ObjectId +from app.routes import get_available_projects +from app.branches.routes import get_available_branches +from app.equipment import bp +from app.equipment.forms import NewEquipment, UpdateEquipment +from app.equipment.equipment import fetch_equipment, EquipmentNotFoundError, get_all_equipment, get_usable_equipment, get_available_equipment, get_available_equipment_type, get_equipment_types, count_equipment_types +import datetime + +mongo = PyMongo(app) + +#class EquipmentNotFoundError(Exception): +# pass +# +#def fetch_equipment(equipment_id): +# equipment = mongo.db.equipment_collection.find_one({"_id":ObjectId(equipment_id)}) +# +# if equipment == None: +# raise EquipmentNotFoundError(f'Equipment Id {equipment_id} returned None') +# else: +# return equipment +# +## return all past and present equipment +#def get_all_equipment(): +# equipment = mongo.db.equipment_collection.find() +# return equipment +# +## return all equipment not damaged, in use, and not in use presently +#def get_usable_equipment(): +# equipment = mongo.db.equipment_collection.find({'retired':{'$exists':False}}) +# return equipment +# +## return equipment not damaged, and ready to use, does not return equipment in use presently +#def get_available_equipment(): +# equipment = mongo.db.equipment_collection.find({'retired':{'$exists':False},'checked_out':{'$exists':False}}) +# return equipment +# +## return document of equipment type (_id) and count(number of occurances) +#def get_equipment_types(): +# types = mongo.db.equipment_collection.aggregate( [ +# { +# '$group':{ +# '_id':'$equipment_type', +# 'count': {'$count':{} } +# } +# }, +# { '$sort':{'equipment_type':1} } +# ] ) +# return types +# +## return tuple list ('type','type (count)') +#def count_equipment_types(): +# counts = [] +# for tool in get_equipment_types(): +# label = "{} ({})".format(tool['_id'],tool['count']) +# counts.append((tool['_id'],label)) +# return counts + +## DEV and SEED Routes ## + +@bp.route("/dev/equipment",methods=["GET"]) +@bp.route("/dev/equipment/",methods=["GET"]) +@login_required +def devEquipment(): + equipment = mongo.db.equipment_collection.find({}) + flash("dev=True") + return render_template("equipment.html",equipment=equipment,dev=True) + +@bp.route("/dev/seed/equipment",methods=["GET","PUT"]) +@login_required +def seedEquipment(): + seeds = [ + { # If equipment_type == "vehicle" pass data as params to NewVehicle(FlaskForm) to add additional details like vehicle_number + "equipment_type":"vehicle", + "branch":ObjectId("6627f69645530484ae5a4ade"), #TODO Change all references to branches to branch_id for scaling via a branch_collection + "purchase_timestamp": datetime.datetime.now(), + "purchase_price": 26500.68, + "match_percentage": 10.25, + "purchasing_project": ObjectId("647a3d95ab70a58f2a44a886") + }, + { + "equipment_type":"shovel", + "branch":ObjectId("6627f69645530484ae5a4adf"), + "purchase_timestamp": datetime.datetime.now(), + "purchase_price": 16.68, + "match_percentage": 2.5, + "purchasing_project": ObjectId("647a3d95ab70a58f2a44a886") + }, + { + "equipment_type":"vehicle", + "branch":ObjectId("6627f69645530484ae5a4adf"), + "purchase_timestamp": datetime.datetime.now(), + "purchase_price": 65238.2, + "match_percentage": 12.5, + "purchasing_project": ObjectId("647a3d95ab70a58f2a44a886"), + "retired":True + }, + { + "equipment_type":"chainsaw", + "branch":ObjectId("6627f69645530484ae5a4ade"), + "purchase_timestamp": datetime.datetime.now(), + "purchase_price": 16.68, + "match_percentage": 2.5, + "purchasing_project": ObjectId("647a3d95ab70a58f2a44a886") + }, + { + "equipment_type":"chainsaw", + "branch":ObjectId("6627f69645530484ae5a4ade"), + "purchase_timestamp": datetime.datetime.now(), + "purchase_price": 16.68, + "match_percentage": 2.5, + "purchasing_project": ObjectId("647a3d95ab70a58f2a44a886") + } + ] + mongo.db.equipment_collection.delete_many({}) + mongo.db.equipment_collection.insert_many(seeds) + return redirect(url_for("equipment.devEquipment")) + +## END ## + +@bp.route("/equipment") +@bp.route("/equipment/") +@login_required +def all_equipment(): + equipment = mongo.db.equipment_collection.find({}) + return render_template("equipment.html",equipment=equipment) + +@bp.route("/equipment/<equipment_id>") +@login_required +def equipment(equipment_id): + equipment = mongo.db.equipment_collection.find({'_id':ObjectId(equipment_id)}) + return render_template("equipment.html",equipment_id=equipment_id,equipment=equipment) + +@bp.route("/equipment/t/<equipmentType>",methods=["GET"]) +@login_required +def EquipmentType(equipmentType): + equipment = get_available_equipment_type(equipmentType) + return render_template("equipment.html",equipment=equipment) + +@bp.route("/newequipment") +@bp.route("/newequipment/") +@login_required +def new_equipment(): + form = NewEquipment() + + types = [('',"Select Type")] + for t in count_equipment_types(): + types.append(t) + types.append(('Create New Type','Create New Type')) + form.equipment_type.choices = types + form.purchasing_project.choices = get_available_projects() + + return render_template("form_equipment.html",form=form,new=True) + +# TODO change to retire? How should this work? +@bp.route("/equipment/<equipment_id>/remove",methods=["GET","POST"]) +@login_required +def remove_equipment(equipment_id): + form = RemoveEquipment() + return render_template("form_equipment.html") + +@bp.route("/equipment/<equipment_id>/<update>",methods=["GET","POST"]) +@login_required +def update_equipment(equipment_id,update): + form = UpdateEquipment() + + types = [('',"Select Type")] + for t in count_equipment_types(): + types.append(t) + types.append(('Create New Type','Create New Type')) + form.equipment_type.choices = types + form.purchasing_project.choices = get_available_projects() + form.branch.choices = get_available_branches() + + try: + equipment = fetch_equipment(equipment_id) + except EquipmentNotFoundError as e: + return render_template('error.html',error=e) + else: + if form.validate_on_submit(): + match update: + case "equipment_type": + mongo.db.equipment_collection.update_one({'_id':equipment['_id']},{'$set':{'equipment_type':form.equipment_type.data}}) + flash("Updated Equipment Type to {}".format(form.equipment_type.data)) + return redirect(url_for('equipment.equipment',equipment_id=equipment_id)) + case "equipment_type_number": + mongo.db.equipment_collection.update_one({'_id':equipment['_id']},{'$set':{'equipment_type_number':form.equipment_type_number.data}}) + flash("Updated Equipment Number for {} to {}".format(equipment['equipment_type'],form.equipment_type_number.data)) + return redirect(url_for('equipment.equipment',equipment_id=equipment_id)) + case "purchase_timestamp": + timestamp = datetime.datetime.combine(form.date.data,datetime.min.time()) + mongo.db.equipment_collection.update_one({'_id':equipment['_id']},{'$set':{'purchase_timestamp':form.purchase_timestamp.data}}) + flash("Updated Purchase Timestamp to {}".format(timestamp)) + return redirect(url_for('equipment.equipment',equipment_id=equipment_id)) + case "purchase_price": + mongo.db.equipment_collection.update_one({'_id':equipment['_id']},{'$set':{'purchase_price':form.purchase_price.data}}) + flash("Updated Purchase Price to {}".format(form.purchase_price.data)) + return redirect(url_for('equipment.equipment',equipment_id=equipment_id)) + case "match_percentage": + mongo.db.equipment_collection.update_one({'_id':equipment['_id']},{'$set':{'match_percentage':form.match_percentage.data}}) + flash("Updated Match Percentage to {}".format(form.match_percentage.data)) + return redirect(url_for('equipment.equipment',equipment_id=equipment_id)) + case "purchasing_project": + mongo.db.equipment_collection.update_one({'_id':equipment['_id']},{'$set':{'purchasing_project':form.purchasing_project.data}}) + flash("Updated Purchasing Project to {}".format(form.purchasing_project.data)) + return redirect(url_for('equipment.equipment',equipment_id=equipment_id)) + case "branch": + mongo.db.equipment_collection.update_one({'_id':equipment['_id']},{'$set':{'branch':form.branch.data}}) + flash("Updated Branch to {}".format(form.branch.data)) + return redirect(url_for('equipment.equipment',equipment_id=equipment_id)) + + return render_template("form_equipment.html",update=update,form=form,equipment=equipment) + diff --git a/app/equipment/templates/dev_equipment.html b/app/equipment/templates/dev_equipment.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% block title %}DEV Equipment{% endblock %} +{% block content %} + + {%- for x in dev_equipment %} + {%- print(x) %} + </br> + </br> + {%- endfor %} + +{% endblock %} diff --git a/app/equipment/templates/equipment.html b/app/equipment/templates/equipment.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block title %}Equipment{% endblock %} + +{% block content %} + + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + <div id="messagebanner"><p>{{ message }} </p></div> + {% endfor %} + {% endif %} + {% endwith %} + + {% if equipment_id or dev %} + <a href="{{url_for("equipment.all_equipment")}}"><div>back to equipment</div></a> + {% endif %} + + <section> + {% for tool in equipment %} + {% if dev %} + <article>{{ tool }}</article> + {% else %} + {% if not equipment_id %}<a href="{{url_for("equipment.equipment",equipment_id=tool['_id'])}}">{%else%}<a href="{{url_for("equipment.remove_equipment",equipment_id=tool['_id'])}}"style="color:red">remove</a>{% endif %} + <article style="padding-bottom:1em;padding-top:1em;margin:0;"> + {% for key,value in tool.items() %} + <div style="display:grid;grid-gap:2em; grid-auto-flow:column; grid-template-columns:min-content"> + <div>{{ key }}</div> + <div>{{ value }}</div> + {% if equipment_id %}<a href="{{ url_for("equipment.update_equipment",equipment_id=tool['_id'],update=key ) }}"style="color:red;">change</a>{% endif %} + </div> + {% endfor %} + </article> + {% if not equipment_id %}</a>{% endif %} + {% endif %} + {% endfor %} + {% if not equipment_id %} + <a href="{{url_for("equipment.new_equipment")}}"><div>new equipment</div></a> + {% endif %} + </section> +{% endblock %} diff --git a/app/equipment/templates/error.html b/app/equipment/templates/error.html diff --git a/app/equipment/templates/form_equipment.html b/app/equipment/templates/form_equipment.html @@ -0,0 +1,35 @@ +{% extends 'base.html' %} +{% block title %}Equipment{% endblock %} +{% block content %} + +<a href="{{url_for('equipment.equipment',equipment_id=equipment['_id'])}}">back to {{equipment['equipment_type']}}</a> +<section> + <form action="" method="POST" novalidate> + {{ form.hidden_tag() }} + {% for error in form.errors %} + <span style="color:red;">[ {{ error }} ]</span> + {% endfor %} + {% if new %} + <div>NEW Equipment Block </div> + {{ form.equipment_type.label }}{{ form.equipment_type() }} + {{ form.equipment_type_number.label }}{{ form.equipment_type_number() }} + {{ form.branch.label }}{{ form.branch() }} + {{ form.purchase_timestamp.label }}{{ form.purchase_timestamp() }} + {{ form.purchase_price.label }}{{ form.purchase_price() }} + {{ form.match_percentage.label }}{{ form.match_percentage() }} + {{ form.purchasing_project.label }}{{ form.purchasing_project() }} + {% else %} + <div>Update Equipment Block </div> + {% if update == 'equipment_type' %}{{ form.equipment_type.label }}{{ form.equipment_type() }}{% endif %} + {% if update == 'equipment_type_number' %}{{ form.equipment_type_number.label }}{{ form.equipment_type_number() }}{% endif %} + {% if update == 'branch' %}{{ form.branch.label }}{{ form.branch() }}{% endif %} + {% if update == 'purchase_timestamp' %}{{ form.purchase_timestamp.label }}{{ form.purchase_timestamp() }}{% endif %} + {% if update == 'purchase_price' %}{{ form.purchase_price.label }}{{ form.purchase_price() }}{% endif %} + {% if update == 'match_percentage' %}{{ form.match_percentage.label }}{{ form.match_percentage() }}{% endif %} + {% if update == 'purchasing_project' %}{{ form.purchasing_project.label }}{{ form.purchasing_project() }}{% endif %} + {% endif %} + {{ form.submit_equipment() }} + </form> +</section> + +{% endblock %} diff --git a/app/equipment/update.py b/app/equipment/update.py diff --git a/app/fleet/__init__.py b/app/fleet/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('fleet',__name__, template_folder='templates') + +from app.fleet import routes diff --git a/app/fleet/fleet.py b/app/fleet/fleet.py diff --git a/app/fleet/forms.py b/app/fleet/forms.py @@ -0,0 +1,42 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, PasswordField, BooleanField, SelectField, TimeField, DateField, IntegerField +from flask_wtf.file import FileField +from wtforms.validators import DataRequired, optional, length, InputRequired, EqualTo + +#TODO +class NewFleet(FlaskForm): + location = StringField('Meeting Link', validators=[DataRequired()]) + expected_end = IntegerField('Expected Duration',default=60) + meeting_description = StringField('Meeting Description') + schedule_meeting = SubmitField('Schedule Meeting') + +#TODO +class UpdateFleet(FlaskForm): + date = DateField('Meeting Date') + time = TimeField('Meeting Time') + location = StringField('Meeting Link') + expected_end = IntegerField('Expected Duration', default=60) + meeting_description = StringField('Meeting Description') + update_meeting = SubmitField('Update Meeting') + +class FleetCheckoutForm(FlaskForm): + vehicle = SelectField('Vehicle', validators = [DataRequired()]) + start_mileage = IntegerField('Starting Mileage', validators=[DataRequired()]) #Require some sort of validator for check... + horn = BooleanField('Horn') + signals = BooleanField('Signals') + tires = BooleanField('Tires') + mirrors = BooleanField('Mirrors') + enginefluid = BooleanField('Engine Fluids') + steeringfluid = BooleanField('Steering Fluid') + brakefluid = BooleanField('Brake Fluid') + transmissionfluid = BooleanField('Transmission Fluid') + windshield = BooleanField('Windshield') + wipers = BooleanField('Windshield Wipers') + towingequipment = BooleanField('Towing Equipment') + additionalnotes = StringField('Additional Notes', validators=[optional()]) #May not need this at all? + checkout = SubmitField('Checkout Vehicle') #Update to take role name for pass to write fn + +class FleetCheckinForm(FlaskForm): + end_mileage = IntegerField('Ending Mileage',validators=[DataRequired()]) #Validation for end check + incident_notes = StringField('Incident Notes',validators=[optional()]) #May not need this at all? + checkin = SubmitField('Checkin Vehicle') diff --git a/app/fleet/routes.py b/app/fleet/routes.py @@ -0,0 +1,247 @@ +from app import app +from flask_pymongo import PyMongo +from flask import render_template, redirect, url_for, flash, request +from flask_login import login_required +from app.fleet import bp +from bson.objectid import ObjectId +import datetime, hashlib +import os +from app.fleet.forms import NewFleet, UpdateFleet, FleetCheckoutForm, FleetCheckinForm +#from app.meetings.update import +#from app.meetings.meeting import + +mongo = PyMongo(app) + +### Define fetch_meeting ### +#TRY TO MOVE TO app.meetings.meeting.py +class FleetNotFoundError(Exception): + pass + +def fetch_fleet(fleet_id): + fleet = mongo.db.fleet_collection.find({"fleet_id":fleet_id}) + + if fleet == None: + raise FleetNotFoundError(f'Fleet Id {fleet_id} returned None') + else: + return fleet + +### BEGIN DEV ROUTES ### + +@bp.route('/fleet/seed',methods=["GET","PUT"]) +@login_required +def fleetSeed(): + pj1 = "647a3d95ab70a58f2a44a886" + pj2 = "647a455f92e1af234bfb91b2" + seeds = [ + { + "_id": "pool", + "available":[3,4,5], # list of _id where equipment_collection.find({}) type: is "vehicle" + "in_use":[(1,"brennentmazur"),(2,"nikolasmmazur")] #replace int with _id of equipment where "type": "vehicle" + }, + { + "fleet_id": ObjectId(), + "branch":'Salmon', + "vehicle_number": 5, + "vehicle_name": 'Green Machine', + "mileage_start": 2, + "mileage_end": 6, + "project": ObjectId(pj1), + "operator":"brennentmazur", + "failed_checks":[] #appended from form where check is false on_validate + }, + { + 'fleet_id': ObjectId(), + 'branch':'Dillon', + "vehicle_number": 4, + "vehicle_name": 'Ram Machine', + "mileage_start": 622, + "mileage_end": 6999, + "project": ObjectId(pj1), + "operator":"brennentmazur", + "failed_checks":[] #appended from form where check is false on_validate + } + ] + mongo.db.fleet_collection.delete_many({}) + mongo.db.fleet_collection.insert_many(seeds) + dev_fleet = mongo.db.fleet_collection.find() + return render_template('dev.html',dev_fleet=dev_fleet) + +@bp.route('/fleet/dev',methods=["GET"]) +@login_required +def allFleet(): + dev_fleet = mongo.db.fleet_collection.find({}) + return render_template('dev.html',dev_fleet=dev_fleet) + +### END DEV ROUTES ### + +#### BEGIN ROUTES #### + +#TODO +@bp.route('/fleet',methods=["GET"]) +@bp.route('/fleet/',methods=["GET"]) +@login_required +def vehicles(): + ongoingFleet = mongo.db.fleet_collection.aggregate( [ + { + '$match':{ + 'timestamp':{ + '$lt':datetime.datetime.now() + }, + 'expected_end':{ + '$gt':datetime.datetime.now() + } + } + }, + { '$sort':{'timestamp':1}} + ] ) + + upcomingFleet = mongo.db.fleet_collection.aggregate( [ + { + '$match':{'timestamp':{'$gt': datetime.datetime.now() }} + }, + { + '$sort':{'timestamp':1} + } + ] ) + + pastFleet = mongo.db.fleet_collection.aggregate( [ + { + '$match':{ 'timestamp': {"$lt": datetime.datetime.now()},'expected_end':{'$lt': datetime.datetime.now()} } + }, + { + '$sort':{ 'timestamp':-1 } + }, + { + '$limit':7 + } + ]) + return render_template('fleet.html',ongoingFleet=ongoingFleet,upcomingFleet=upcomingFleet,pastFleet=pastFleet) + +#TODO +@bp.route('/fleet/past',methods=["GET"]) +@login_required +def past(): + allOldFleet = True + pastFleet = mongo.db.fleet_collection.aggregate( [ + { + '$match':{'expected_end': {"$lt": datetime.datetime.now()} } + }, + { + '$sort':{ 'timestamp':-1 } + } + ] ) + return render_template('fleet.html',pastFleet=pastFleet, allOldFleet=allOldFleet) + +@bp.route('/fleet/<fleet_id>',methods=["GET"]) +@bp.route('/fleet/<fleet_id>/',methods=["GET"]) +def fleet(fleet_id): + try: + fleet = fetch_fleet(fleet_id) + except FleetNotFoundError as e: + return render_template('error.html',error=e) + else: + return render_template('fleet.html',fleet=fleet) + +#TODO +@bp.route('/fleet/new',methods=["GET","POST"]) +@login_required +def new(): + form = NewFleet() + if form.validate_on_submit(): + #timestp = datetime.datetime.combine(form.date.data,form.time.data) +# "fleet_id": new ObjectId(), +# "branch":'Salmon', +# "vehicle_number": 5, +# "vehicle_name": 'Green Machine', +# "mileage_start": 2, +# "mileage_end": 6, +# "project": ObjectId(pj1), +# "operator":"brennentmazur", +# "failed_checks":[] #appended from form where check is false on_validate + #expectedtime = timestp + datetime.timedelta(minutes=form.expected_end.data) + try: + new_fleet = {'branch':form.branch.data,'vehicle_number':form.vehicle_number.data} + if form.vehicle_name.data != "": + new_fleet['vehicle_name']=form.vehicle_name.data + except Exception: + return "Error assigning form data" + else: + mongo.db.fleet_collection.insert_one(new_fleet) + flash("Created new fleet vehicle!") + return redirect(url_for('fleet.vehicles')) + +# if form.name.data != "": +# new_meeting['meeting_name']=form.name.data + return render_template('new.html',form=form) + +#TODO DOES THIS NEED TO BE HERE? GAS RECIPTS? +@bp.route('/fleet/<fleet_id>/upload',methods=["GET","POST"]) +@login_required +def upload(fleet_id): + try: + fleet = fetch_fleet(fleet_id) + except FleetNotFoundError as e: + return render_template('error.html',error=e) + else: + form = NewFileUpload() + upload = datetime.datetime.utcnow() + if form.validate_on_submit(): + flash("submitted") + mongo.db.fleet_collection.update_one({'_id':fleet['_id']},{'$push':{'documents':'brennen'}}) + return redirect(url_for('fleet.upload',fleet_id=fleet['fleet_id'])) + return render_template('upload.html',fleet=fleet,form=form,upload=upload) + +#TODO +@bp.route('/fleet/<fleet_id>/update',methods=["GET","POST"]) +@login_required +def update(fleet_id): + try: + fleet = fetch_fleet(fleet_id) + except FleetNotFoundError as e: + return render_template('error.html',error=e) + else: + return render_template('update.html',fleet=fleet) + +#TODO +@bp.route('/fleet/<fleet_id>/<update>',methods=["GET","POST"]) +@login_required +def change(fleet_id,update): + form = UpdateFleet() + try: + fleet = fetch_fleet(fleet_id) + except FleetNotFoundError as e: + return render_template('error.html',error=e) + else: + if form.validate_on_submit(): + if update == "date": + timestamp = datetime.datetime.combine(form.date.data,fleet['timestamp'].time()) + mongo.db.fleet_collection.update_one({'fleet_id':fleet['fleet_id']},{'$set':{'timestamp':timestamp}}) + flash("Updated Date from {} to {}".format(fleet['timestamp'],timestamp)) + return redirect(url_for('fleet.update',fleet_id=fleet['fleet_id'])) + if update == "time": + timestamp = datetime.datetime.combine(fleet['timestamp'].date(),form.time.data) + mongo.db.fleet_collection.update_one({'fleet_id':fleet['fleet_id']},{'$set':{'timestamp':timestamp}}) + flash("Updated Time from {} to {}".format(fleet['timestamp'],timestamp)) + return redirect(url_for('fleet.update',fleet_id=fleet['fleet_id'])) + if update == "location": + mongo.db.fleet_collection.update_one({'fleet_id':fleet['fleet_id']},{'$set':{'location':form.location.data}}) + flash("Updated location from {} to {}".format(fleet['location'],form.location.data)) + return redirect(url_for('fleet.update',fleet_id=fleet['fleet_id'])) + if update == "description": + mongo.db.fleet_collection.update_one({'fleet_id':fleet['fleet_id']},{'$set':{'fleet_description':form.fleet_description.data}}) + flash("Updated description to {}".format(form.fleet_description.data)) + return redirect(url_for('fleet.update',fleet_id=fleet['fleet_id'])) + return render_template('form.html',fleet=fleet,update=update,form=form) + +#TODO +@bp.route('/fleet/<fleet_id>/remove',methods=["GET","POST"]) +@login_required +def remove(fleet_id): + try: + fleet = fetch_fleet(fleet_id) + except FleetNotFoundError as e: + return render_template('error.html',error=e) + else: + mongo.db.fleet_collection.delete_one(fleet) + flash("Deleted fleet {} {}".format(fleet['fleet_number'],fleet['vehicle_name'])) + return redirect(url_for('fleet.vehicles')) diff --git a/app/fleet/templates/dev.html b/app/fleet/templates/dev.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block title %}DEV Seed Meeting{% endblock %} + +{% block content %} + + {%- for x in dev_meetings %} + {%- print(x) %} + </br> + </br> + {%- endfor %} + +{% endblock %} diff --git a/app/fleet/templates/error.html b/app/fleet/templates/error.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block title %}Error{% endblock %} + +{% block content %} + +<div style="text-align:center;"> + <h3>{{ error }}</h3> + <a href="{{ url_for('meetings.meetings') }}">back to meetings</a> +</div> + +{% endblock %} diff --git a/app/fleet/templates/fleet.html b/app/fleet/templates/fleet.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block title %}Fleet{% 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> + Woo Hoo, Fleet Page! + This page will be for checking out a vehicle + display the vehicle checks + </section> +{% endblock %} diff --git a/app/fleet/templates/form.html b/app/fleet/templates/form.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} + +{% block title %}Update Meeting{% endblock %} + +{% block content %} +<section> + <a href="{{ url_for('meetings.update',meeting_id=meeting['meeting_id']) }}">back to meetings</a> + <h3>Update {{meeting['timestamp'].date() }} Meeting</h3> +<form action="" method="POST" novalidate> + {{ form.hidden_tag() }} + {% for error in form.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {% if update == "date" %}{{ form.date.label }}{{ form.date() }}{% endif %} + {% if update == "time" %}{{ form.time.label }}{{ form.time() }}{% endif %} + {% if update == "end" %}{{ form.expected_end.label }}{{ form.expected_end() }}{% endif %} + {% if update == "location" %}{{ form.location.label }}{{ form.location() }}{% endif %} + {% if update == "description" %}{{ form.meeting_description.label }}{{ form.meeting_description() }}{% endif %} + {{ form.update_meeting() }} +</form> +</section> +{% endblock %} diff --git a/app/fleet/templates/meeting.html b/app/fleet/templates/meeting.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block title %}{{ meeting.timestamp.date() }} Meeting{% endblock %} + +{% block content %} + {% if meeting %} + <section class="hours-grid"> {# class="meeting">#} + <a href="{{ url_for('meetings.meetings') }}" class="action-button">back to meetings</a> + <h2 style="color:red;align:left">{{ meeting.timestamp.date() }}</h2><h3 style="align:right">{{ meeting.timestamp.time().isoformat(timespec='minutes') }}</h3> + <a href="{{ meeting.location }}">@ {{ meeting.location }}</a> + <div><p>{{ meeting.meeting_description }}</p></div> + <div><h4 href="color:red">Documents:</h4><a href="{{ url_for('meetings.upload',meeting_id=meeting['meeting_id']) }}">[upload]</a></div> + {% if meeting.documents %} + {% for document in meeting['documents'] %} + <div> + <a href="">{{ document }}</a> + </div> + {% endfor %} + {% endif %} + <a class="action-button" href="{{ url_for('meetings.update',meeting_id=meeting['meeting_id']) }}">modify</a> + </section> + {% endif %} +{% endblock %} diff --git a/app/fleet/templates/new.html b/app/fleet/templates/new.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} + +{% block title %}New Meeting{% endblock %} + +{% block content %} +<section> + <a href="{{ url_for('meetings.meetings') }}">back to meetings</a> +<h3>Schedule new Meeting</h3> +<form action="" method="POST" novalidate> + {{ form.hidden_tag() }} + {% for error in form.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {{ form.date.label }}{{ form.date() }} + {{ form.time.label }}{{ form.time() }} + {{ form.expected_end.label }}{{ form.expected_end() }} + {{ form.location.label }}{{ form.location() }} + {{ form.meeting_description.label }}{{ form.meeting_description() }} + {{ form.schedule_meeting() }} +</form> +</section> +{% endblock %} diff --git a/app/fleet/templates/update.html b/app/fleet/templates/update.html @@ -0,0 +1,32 @@ +{% extends 'base.html' %} + +{% block title %}{{ meeting.date }}Update Meeting{% endblock %} + +{% block content %} + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + <div id="messagebanner"><p>{{ message }}</p></div> + {% endfor %} + {% endif %} + {% endwith %} + {% if meeting %} + <section class="meeting"> + <a href="{{ url_for('meetings.meeting',meeting_id=meeting['meeting_id']) }}">back to meeting</a> + <div style="display:flex"><h2>Date: {{ meeting.timestamp.date() }}</h2><a href="{{url_for('meetings.change',meeting_id=meeting["meeting_id"],update="date")}}"style="color:red">change</a></div> + <div style="display:flex"><h3>Time: {{ meeting.timestamp.time().isoformat(timespec="minutes") }}</h3><a href="{{url_for('meetings.change',meeting_id=meeting['meeting_id'],update="time")}}"style="color:red">change</a></div> + <div style="display:flex"><h3>Location link: {{ meeting.location }}</h3><a href="{{url_for('meetings.change',meeting_id=meeting['meeting_id'],update="location")}}"style="color:red">change</a></div> + <div style="display:flex"><h3>Description: {{ meeting.meeting_destription }}</h3><a href="{{url_for('meetings.change',meeting_id=meeting['meeting_id'],update="description")}}"style="color:red">change</a></div> + <div style="display:flex"><h4 href="color:red">Documents:</h4><a href="{{ url_for('meetings.upload',meeting_id=meeting['meeting_id']) }}" style="color:red">[upload]</a></div> + {% if meeting.document %} + {% for document in meeting.document %} + <div> + <a href="">{{ document }}</a><a href=""style="color:red">remove</a> + </div> + {% endfor %} + {% endif %} + <a href="{{ url_for('meetings.meeting',meeting_id=meeting['meeting_id']) }}">back to meeting</a> + <a href="{{ url_for('meetings.remove',meeting_id=meeting['meeting_id']) }}">remove meeting</a> + </section> + {% endif %} +{% endblock %} diff --git a/app/fleet/templates/upload.html b/app/fleet/templates/upload.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block title %}Upload{% endblock %} + +{% block content %} +{% with messages = get_flashed_messages() %} +{% if messages %} + {% for message in messages %} + <div id="messagebanner"><p>{{ message }}</p></div> + {% endfor %} +{% endif %} +{% endwith %} + <a href="{{ url_for('meetings.meeting',meeting_id=meeting['meeting_id']) }}">back to meeting</a> + <section class="hours-grid">{#class="meeting">#} + <h2>{{ upload.date() }} {{ upload.time().isoformat(timespec='minutes') }}</h2> + <p>Upload document to {{ meeting['timestamp'].date() }} Meeting for {{ meeting['meeting_description'] }}</p> + <form action="" method="POST" novalidate enctype="multipart/form-data"> + {{ form.hidden_tag() }} + {{ form.document() }} + {{ form.submit_file() }} + </form> + {% if meeting.documents %} + <h2>existing documents</h2> + {% for doc in meeting['documents'] %} + <p>{{ doc }}</p> + {% endfor %} + {% endif %} + </section> +{% endblock %} diff --git a/app/fleet/update.py b/app/fleet/update.py diff --git a/app/forms.py b/app/forms.py @@ -0,0 +1,281 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, PasswordField, BooleanField, SelectField, DateField, TelField, EmailField, FloatField, IntegerField, TimeField, DecimalField +from wtforms.validators import DataRequired, optional, length, InputRequired, EqualTo, NumberRange, ValidationError + +class GreaterThan: + """ + Compares the values of two fields. + + :param fieldname: + The name of the compared field. + :param message: + Error message to raise in case of validation error. + """ + def __init__(self, fieldname, message=None): + self.fieldname = fieldname + self.message = message + + def __call__(self, form, field): + try: + other = form[self.fieldname] + except KeyError as exc: + raise ValidationError( + field.gettext("Invalid fieldname '$s'.") % self.fieldname + ) from exc + if field.data > other.data: + return + + d = { + "other_label":hasattr(other,"label") + and other.label.text + or self.fieldname, + "other_name": self.fieldname, + } + message = self.message + if message is None: + message = field.gettext("Field must be Greater than %(other_name)s.") + + raise ValidationError(message % d) + +class LessThan: + """ + Compares the values of two fields + + :param fieldname: + The name of the field to compare to. + :param message: + Error message to raise in case of validation error. + """ + def __init__(self, fieldname, message=None): + self.fieldname = fieldname + self.message = message + + def __call__(self, form, field): + try: + other = form[self.fieldname] + except KeyError as exc: + raise ValidationError( + field.gettext("Invalid field name '%s'.") % self.fieldname + ) from exc + if field.data < other.data: + return + + d = { + "other_label":hasattr(other, "label") + and other.label.text + or self.fieldname, + "other_name": self.fieldname, + } + message = self.message + if message is None: + message = field.gettext("Field must be less than %(other_label)s.") + + raise ValidationError(message % d) + +# Login class currently assumes mongodb collection(Users) with structure +# { +# Name: [username], +# Password: [hashed_password] +# } +##### ##### +### Sub Forms ### +##### ##### +#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=[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 ### +##### ##### +class LoginForm(FlaskForm): + username = StringField('Username', validators=[DataRequired()]) + password = PasswordField('Password', validators=[DataRequired()]) + login = SubmitField('Login') + +#ATTEMPT AT SETTING A JOB APPLICATION FORM +#class NewUserApplicationForm(NewUserForm): +# paymentAddress = BooleanField('Address-is this the address you want your check sent to?',validators=[DataRequired()] +# reference1 = StringField('Please include name, phone number, and Title/Relationship.',validators=[optional()] +# reference2 = StringField('Please include name',validators=[optional()] +# reference3 = StringField('',validators=[optional()] +# priorEmployer = StringField('Please include Company name, phone number, and supervisor.',validators=[optional()] + + +class NewUserForm(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... + 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') + +class ChangeBranchForm(FlaskForm): + branch = SelectField('Branch',validators=[DataRequired()]) + changeBranch = SubmitField('Change Branch') + +class NewRoleForm(FlaskForm): + rolename = StringField('Role Name', validators=[DataRequired()]) + +class ConfirmRemove(FlaskForm): + confirm = SubmitField('YES REMOVE') + +class NewProjectForm(BudgetForm): + fenumber = IntegerField('FE #', validators=[DataRequired()]) + projectName = StringField('Project Name', validators=[DataRequired()]) + agreement = SelectField('Part of Agreement', validators=[DataRequired()]) + branch = SelectField('Organization Branch', validators=[DataRequired()]) + createNewProject = SubmitField('Create New Project') + +class MoveProjectForm(FlaskForm): + newAgreement = SelectField('Parent Agreement', validators=[DataRequired()]) + moveProject = SubmitField('Update Project') + +class RenameProjectForm(FlaskForm): + fenumber = IntegerField('FE #', validators=[DataRequired()]) + newName = StringField('Project Name', validators=[DataRequired()]) + renameProject = SubmitField('Update Project') + +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()]) + endDate = DateField('End Date') + createNewAgreement = SubmitField('Create New Agreement') + +class RenameAgreementForm(FlaskForm): + newName = StringField('Agreement Name', validators=[DataRequired()]) + renameAgreement = SubmitField('Update Agreement') + +class PunchclockoutWidget(FlaskForm): + #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 + clockout = SubmitField('Clock Out') + +class PunchclockinWidget(FlaskForm): + projectsSel = SelectField('Project', validators=[DataRequired()]) + #clockin = currenttime + # IFF user.role is_in(trusted_role[]) then allow lunch minute definition + clockin = SubmitField('Clock In') + +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=[InputRequired(),LessThan('endTime')]) + #endTime = TimeField('End',validators=[DataRequired(),GreaterThan('startTime')]) + 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=[optional()]) + submitEntr = SubmitField('Submit') + +class FleetCheckoutForm(FlaskForm): + vehicle = SelectField('Vehicle', validators = [DataRequired()]) + start_mileage = IntegerField('Starting Mileage', validators=[DataRequired()])# Require some sort of validator for check... + horn = BooleanField('Horn') + signals = BooleanField('Signals') + tires = BooleanField('Tires') + mirrors = BooleanField('Mirrors') + enginefluid = BooleanField('Engine Fluids') + steeringfluid = BooleanField('Steering Fluid') + brakefluid = BooleanField('Brake Fluid') + transmissionfluid = BooleanField('Transmission Fluid') + windshield = BooleanField('Windshield') + wipers = BooleanField('Windshield Wipers') + towingequipment = BooleanField('Towing Equipment') + additionalnotes = StringField('Additional Notes',validators=[optional()])# May not need this at all? + checkout = SubmitField('Checkout Vehicle')#Update to take role name for pass to write fn + +class FleetCheckinForm(FlaskForm): + end_mileage = IntegerField('Ending Mileage',validators=[DataRequired()])# Require some sort of validator for check... + 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 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=[optional()]) + mname = StringField('Middle Initial', validators=[optional(),length(max=1)]) + lname = StringField('Last Name', validators=[optional()]) + birthday = DateField('Birthday',validators=[optional()])# Ought to change this to some validation for age range accepted + role = SelectField('Role',validators=[optional()]) + address = StringField('Address',validators=[optional()])# Require some sort of validator for check... + branch = SelectField('Branch',validators=[optional(),length(max=200)]) + phonenumber = TelField('Phone Number',validators=[optional(),length(max=12)])# Require some sort of validator for check... + email = EmailField('Email',validators=[optional()])# 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... + modUser = 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") + activecrew = BooleanField('Active Crew List') + fleet = BooleanField('Fleet') + ###### End Modules ##### + updaterole = SubmitField('Update')#Update to take role name for pass to write fn + +class AdmnPermissionsForm(FlaskForm):# for each module make Boolean field. Gets passed to fn writing to permissions_collection SET MANUALLY CURRENTLY + agreements = BooleanField('Agreements') + updateEntr = SubmitField('Update') + reports = BooleanField('Reports') + manageusers = BooleanField('Manage Users') + ###### End Modules ##### + updaterole = SubmitField('Update')#Update to take role name for pass to write fn + + diff --git a/app/meetings/__init__.py b/app/meetings/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('meetings',__name__, template_folder='templates') + +from app.meetings import routes diff --git a/app/meetings/__pycache__/__init__.cpython-310.pyc b/app/meetings/__pycache__/__init__.cpython-310.pyc Binary files differ. diff --git a/app/meetings/__pycache__/forms.cpython-310.pyc b/app/meetings/__pycache__/forms.cpython-310.pyc Binary files differ. diff --git a/app/meetings/__pycache__/meeting.cpython-310.pyc b/app/meetings/__pycache__/meeting.cpython-310.pyc Binary files differ. diff --git a/app/meetings/__pycache__/routes.cpython-310.pyc b/app/meetings/__pycache__/routes.cpython-310.pyc Binary files differ. diff --git a/app/meetings/__pycache__/update.cpython-310.pyc b/app/meetings/__pycache__/update.cpython-310.pyc Binary files differ. diff --git a/app/meetings/forms.py b/app/meetings/forms.py @@ -0,0 +1,24 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, PasswordField, BooleanField, SelectField, TimeField, DateField, IntegerField +from flask_wtf.file import FileField +from wtforms.validators import DataRequired, optional, length, InputRequired, EqualTo + +class NewMeeting(FlaskForm): + date = DateField('Meeting Date', validators=[DataRequired()]) + time = TimeField('Meeting Time', validators=[DataRequired()]) + location = StringField('Meeting Link', validators=[DataRequired()]) + expected_end = IntegerField('Expected Duration',default=60) + meeting_description = StringField('Meeting Description') + schedule_meeting = SubmitField('Schedule Meeting') + +class UpdateMeeting(FlaskForm): + date = DateField('Meeting Date') + time = TimeField('Meeting Time') + location = StringField('Meeting Link') + expected_end = IntegerField('Expected Duration', default=60) + meeting_description = StringField('Meeting Description') + update_meeting = SubmitField('Update Meeting') + +class NewFileUpload(FlaskForm): + document = FileField('file') + submit_file = SubmitField('Submit File') diff --git a/app/meetings/meeting.py b/app/meetings/meeting.py diff --git a/app/meetings/routes.py b/app/meetings/routes.py @@ -0,0 +1,289 @@ +from app import app +from flask_pymongo import PyMongo +from flask import render_template, redirect, url_for, flash, request +from flask_login import login_required +from app.meetings import bp +from bson.objectid import ObjectId +import datetime, hashlib +import os +from gridfs import GridFS +from app.meetings.forms import NewMeeting, UpdateMeeting, NewFileUpload +#from app.meetings.update import +#from app.meetings.meeting import + +mongo = PyMongo(app) +fs = GridFS(mongo.db, 'meeting') + +# REMOVE THIS WHEN UPLOAD AND READ ARE RESOLVED +#a=fs.put(b'test data') +#item = fs.get(a) +# END REMOVE + +### Define fetch_meeting ### +#TRY TO MOVE TO app.meetings.meeting.py +class MeetingNotFoundError(Exception): + pass + +def fetch_meeting(meeting_id): + meeting = mongo.db.meeting_collection.find_one({"meeting_id":meeting_id}) + + if meeting == None: + raise MeetingNotFoundError(f'Meeting Id {meeting_id} returned None') + else: + return meeting + +### BEGIN DEV ROUTES ### + +@bp.route('/meetings/seed',methods=["GET","PUT"]) +@login_required +def meetingSeed(): + expectedtime = datetime.datetime.now() + expectedtime1 = expectedtime + datetime.timedelta(minutes=60) + expectedtime2 = expectedtime - datetime.timedelta(minutes=60) + r1 = hashlib.sha256(os.urandom(16)).hexdigest() + r2 = hashlib.sha256(os.urandom(16)).hexdigest() + seeds = [ + { + "meeting_id": r1, + "timestamp": datetime.datetime.now(), + "expected_end": expectedtime1, + "location": "https://www.brennen.work", + "documents":["/uploads/test.txt","/uploads/test2.pdf"], + "meeting_description":"Design Meeting" + }, + { + "meeting_id": r2, + "timestamp": datetime.datetime.now(), + "expected_end": expectedtime2, + "location": "https://www.brennen.work", + "documents":["/uploads/test2.doc"], + "minutes_file":"minutes.doc", + "meeting_description":"Design Meet" + } + ] + mongo.db.meeting_collection.delete_many({}) + mongo.db.meeting_collection.insert_many(seeds) + dev_meetings = mongo.db.meeting_collection.find() + return render_template('dev_meetings.html',dev_meetings=dev_meetings) + +@bp.route('/meetings/dev',methods=["GET"]) +@login_required +def allMeetings(): + dev_meetings = mongo.db.meeting_collection.find({}) + return render_template('dev_meetings.html',dev_meetings=dev_meetings) + +### END DEV ROUTES ### + +#### BEGIN ROUTES #### + +@bp.route('/meetings',methods=["GET"]) +@bp.route('/meetings/',methods=["GET"]) +@login_required +def meetings(): + ongoingMeetings = mongo.db.meeting_collection.aggregate( [ + { + '$match':{ + 'timestamp':{ + '$lt':datetime.datetime.now() + }, + 'expected_end':{ + '$gt':datetime.datetime.now() + } + } + }, + { '$sort':{'timestamp':1}} + ] ) + + upcomingMeetings = mongo.db.meeting_collection.aggregate( [ + { + '$match':{'timestamp':{'$gt': datetime.datetime.now() }} + }, + { + '$sort':{'timestamp':1} + } + ] ) + + pastMeetings = mongo.db.meeting_collection.aggregate( [ + { + '$match':{ 'timestamp': {"$lt": datetime.datetime.now()},'expected_end':{'$lt': datetime.datetime.now()} } + }, + { + '$sort':{ 'timestamp':-1 } + }, + { + '$limit':7 + } + ]) + + return render_template('meetings.html',ongoingMeetings=ongoingMeetings,upcomingMeetings=upcomingMeetings,pastMeetings=pastMeetings) + +@bp.route('/meetings/past',methods=["GET"]) +@login_required +def past_meetings(): + allOldMeetings = True + pastMeetings = mongo.db.meeting_collection.aggregate( [ + { + '$match':{'expected_end': {"$lt": datetime.datetime.now()} } + }, + { + '$sort':{ 'timestamp':-1 } + } + ] ) + return render_template('meetings.html',pastMeetings=pastMeetings, allOldMeetings=allOldMeetings) + +@bp.route('/meeting/<meeting_id>',methods=["GET"]) +@bp.route('/meeting/<meeting_id>/',methods=["GET"]) +def meeting(meeting_id): + try: + meeting = fetch_meeting(meeting_id) + except MeetingNotFoundError as e: + return render_template('error.html',error=e) + else: + return render_template('meeting.html',meeting=meeting) + +@bp.route('/meeting/new',methods=["GET","POST"]) +@login_required +def new_meeting(): + form = NewMeeting() + if form.validate_on_submit(): + m_id = hashlib.sha256(os.urandom(16)).hexdigest() + timestp = datetime.datetime.combine(form.date.data,form.time.data) + expectedtime = timestp + datetime.timedelta(minutes=form.expected_end.data) + try: + new_meeting = {'meeting_id':m_id,'timestamp':timestp,'location':form.location.data,'expected_end':expectedtime} + if form.meeting_description.data != "": + new_meeting['meeting_description']=form.meeting_description.data + except Exception: + return "Error assigning form data" + else: + mongo.db.meeting_collection.insert_one(new_meeting) + flash("Created new meeting!") + return redirect(url_for('meetings.meetings')) + +# if form.name.data != "": +# new_meeting['meeting_name']=form.name.data + return render_template('new_meeting.html',form=form) + +@bp.route('/meeting/<meeting_id>/upload',methods=["GET","POST","PUT"]) +@login_required +def upload_meeting_document(meeting_id): + try: + meeting = fetch_meeting(meeting_id) + except MeetingNotFoundError as e: + return render_template('error.html',error=e) + else: + form = NewFileUpload() + if form.validate_on_submit(): + fs = GridFS(mongo.db, 'meeting') + #fs = GridFS(mongo.db) + #upload = request.files['file'] + upload = request.files[form.document.name].read() + uploadfn = request.files[form.document.name].filename + if uploadfn != '': + fid = fs.put(upload,filename=uploadfn,meeting=ObjectId(meeting['_id'])) + flash("submitted {}".format(uploadfn)) + mongo.db.meeting_collection.update_one({'_id':meeting['_id']},{'$push':{'documents':{"doc_id":ObjectId(fid),"doc_filename":uploadfn}}}) + return redirect(url_for('meetings.upload_meeting_document',meeting_id=meeting['meeting_id'])) +# app_root = os.path.dirname(os.path.abspath(__file__)) +# target = os.path.join(app_root, 'static/meeting-docs') +# if not os.path.isdir(target): +# os.mkdir(target) +# if request.method == "POST": +# upload = request.files['file'] +# if upload.filename != '': +# upload.save(os.path.join('static/meeting-docs',upload.filename)) +# flash("submitted {}".format(upload.filename)) +# mongo.db.meeting_collection.update_one({'_id':meeting['_id']},{'$push':{'documents':upload.filename}}) +# return redirect(url_for('meetings.upload_meeting_document',meeting_id=meeting['meeting_id'])) + return render_template('upload_meeting_document.html',meeting=meeting,form=form) + +@bp.route('/meetings/upload',methods=["GET","POST"]) +@login_required +def upload_doc(): + form = NewFileUpload() + if form.validate_on_submit(): + fs = GridFS(mongo.db, 'meeting') + upload = request.files[form.document.name].read() + if request.files[form.document.name].filename != '': + fid = fs.put(upload,filename=request.files[form.document.name].filename,meeting="meeting id") + flash("submitted {}".format(fid)) + return redirect(url_for('meetings.upload_doc')) + return render_template('upload_meeting_doc_test.html',form=form) + +@bp.route('/meeting/get-file/<fid>') +@bp.route('/meeting/get-file') +def get_meeting_file(fid=None): + fs = GridFS(mongo.db, 'meeting') + if fid is not None: + file = fs.get(ObjectId(fid)) + rfile = app.response_class(file, direct_passthrough=True, mimetype='application/octet-stream') + rfile.headers.set('Content-Disposition','attachment',filename=file.filename) #maybe change to actually return real filename instead of file_id + return rfile + return render_template('get_meeting_file.html', files=fs.find()) #find() was list() + +@bp.route('/meeting/delete-file/<fid>') +@bp.route('/meeting/delete-file/<fid>/<previous>') +def delete_meeting_file(fid=None,previous=None): + fs = GridFS(mongo.db, 'meeting') + if fid is not None and previous is None: + fs.delete(ObjectId(fid)) + return redirect(url_for('meetings.get_meeting_file')) + elif fid is not None and previous is not None: + meeting = fetch_meeting(previous) + fs.delete(ObjectId(fid)) + mongo.db.meeting_collection.update_one({"_id":meeting["_id"]},{"$pull":{'documents':{'doc_id':ObjectId(fid)}}}) + return redirect(url_for('meetings.update_meeting',meeting_id=previous)) + else: + return render_template('get_meeting_file.html', files=fs.find())#find() was list() + +@bp.route('/meeting/<meeting_id>/update',methods=["GET","POST"]) +@login_required +def update_meeting(meeting_id): + try: + meeting = fetch_meeting(meeting_id) + except MeetingNotFoundError as e: + return render_template('error.html',error=e) + else: + return render_template('update_meeting.html',meeting=meeting) + +@bp.route('/meeting/<meeting_id>/<update>',methods=["GET","POST"]) +@login_required +def change_meeting(meeting_id,update): + form = UpdateMeeting() + try: + meeting = fetch_meeting(meeting_id) + except MeetingNotFoundError as e: + return render_template('error.html',error=e) + else: + if form.validate_on_submit(): + if update == "date": + timestamp = datetime.datetime.combine(form.date.data,meeting['timestamp'].time()) + mongo.db.meeting_collection.update_one({'meeting_id':meeting['meeting_id']},{'$set':{'timestamp':timestamp}}) + flash("Updated Date from {} to {}".format(meeting['timestamp'],timestamp)) + return redirect(url_for('meetings.update_meeting',meeting_id=meeting['meeting_id'])) + if update == "time": + timestamp = datetime.datetime.combine(meeting['timestamp'].date(),form.time.data) + mongo.db.meeting_collection.update_one({'meeting_id':meeting['meeting_id']},{'$set':{'timestamp':timestamp}}) + flash("Updated Time from {} to {}".format(meeting['timestamp'],timestamp)) + return redirect(url_for('meetings.update_meeting',meeting_id=meeting['meeting_id'])) + if update == "location": + mongo.db.meeting_collection.update_one({'meeting_id':meeting['meeting_id']},{'$set':{'location':form.location.data}}) + flash("Updated location from {} to {}".format(meeting['location'],form.location.data)) + return redirect(url_for('meetings.update_meeting',meeting_id=meeting['meeting_id'])) + if update == "description": + mongo.db.meeting_collection.update_one({'meeting_id':meeting['meeting_id']},{'$set':{'meeting_description':form.meeting_description.data}}) + flash("Updated description to {}".format(form.meeting_description.data)) + return redirect(url_for('meetings.update_meeting',meeting_id=meeting['meeting_id'])) + return render_template('form_meetings.html',meeting=meeting,update=update,form=form) + +@bp.route('/meeting/<meeting_id>/remove',methods=["GET","POST"]) +@login_required +def remove_meeting(meeting_id): + try: + meeting = fetch_meeting(meeting_id) + except MeetingNotFoundError as e: + return render_template('error.html',error=e) + else: + mongo.db.meeting_collection.delete_one(meeting) + flash("Deleted meeting {}".format(meeting['meeting_description'])) + return redirect(url_for('meetings.meetings')) diff --git a/app/meetings/templates/dev_meetings.html b/app/meetings/templates/dev_meetings.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block title %}DEV Seed Meeting{% endblock %} + +{% block content %} + + {%- for x in dev_meetings %} + {%- print(x) %} + </br> + </br> + {%- endfor %} + +{% endblock %} diff --git a/app/meetings/templates/error.html b/app/meetings/templates/error.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block title %}Error{% endblock %} + +{% block content %} + +<div style="text-align:center;"> + <h3>{{ error }}</h3> + <a href="{{ url_for('meetings.meetings') }}">back to meetings</a> +</div> + +{% endblock %} diff --git a/app/meetings/templates/form_meetings.html b/app/meetings/templates/form_meetings.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} + +{% block title %}Update Meeting{% endblock %} + +{% block content %} +<section> + <a href="{{ url_for('meetings.update_meeting',meeting_id=meeting['meeting_id']) }}">back to meetings</a> + <h3>Update {{meeting['timestamp'].date() }} Meeting</h3> +<form action="" method="POST" novalidate> + {{ form.hidden_tag() }} + {% for error in form.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {% if update == "date" %}{{ form.date.label }}{{ form.date() }}{% endif %} + {% if update == "time" %}{{ form.time.label }}{{ form.time() }}{% endif %} + {% if update == "end" %}{{ form.expected_end.label }}{{ form.expected_end() }}{% endif %} + {% if update == "location" %}{{ form.location.label }}{{ form.location() }}{% endif %} + {% if update == "description" %}{{ form.meeting_description.label }}{{ form.meeting_description() }}{% endif %} + {{ form.update_meeting() }} +</form> +</section> +{% endblock %} diff --git a/app/meetings/templates/get_meeting_file.html b/app/meetings/templates/get_meeting_file.html @@ -0,0 +1,25 @@ +{% extends 'base.html' %} + +{% block title %}Get File{% 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="hours-grid">{#class="meeting">#} + {# {{ item.read() }} #} + <ul>{% for file in files %} + <li> + <a href="{{ url_for('meetings.get_meeting_file', fid=file._id) }}"> + {{ file.filename }} + {{ file }} + </a> + <a href="{{url_for('meetings.delete_meeting_file',fid=file._id)}}">remove</a> + </li> + {% endfor %}</ul> + </section> +{% endblock %} diff --git a/app/meetings/templates/meeting.html b/app/meetings/templates/meeting.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block title %}{{ meeting.timestamp.date() }} Meeting{% endblock %} + +{% block content %} + {% if meeting %} + <section class="hours-grid"> {# class="meeting">#} + <a href="{{ url_for('meetings.meetings') }}" class="action-button">back to meetings</a> + <h2 style="color:red;align:left">{{ meeting.timestamp.date() }}</h2><h3 style="align:right">{{ meeting.timestamp.time().isoformat(timespec='minutes') }}</h3> + <a href="{{ meeting.location }}">@ {{ meeting.location }}</a> + <div><p>{{ meeting.meeting_description }}</p></div> + <div><h4 href="color:red">Documents:</h4><a href="{{ url_for('meetings.upload_meeting_document',meeting_id=meeting['meeting_id']) }}">[upload]</a></div> + {% if meeting.documents %} + {% for document in meeting['documents'] %} + <div> + <a href="{{url_for('meetings.get_meeting_file',fid=document['doc_id'])}}">{{ document['doc_filename'] }}</a> + </div> + {% endfor %} + {% endif %} + <a class="action-button" href="{{ url_for('meetings.update_meeting',meeting_id=meeting['meeting_id']) }}">modify</a> + </section> + {% endif %} +{% endblock %} diff --git a/app/meetings/templates/meetings.html b/app/meetings/templates/meetings.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} + +{% block title %}Meetings{% 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="hours-grid"> + {% if ongoingMeetings is defined %} + <section class="ongoing"> + <h2 style="color:red">Ongoing</h2> + {% for ongoing in ongoingMeetings %} + <a href="{{ url_for('meetings.meeting',meeting_id=ongoing['meeting_id']) }}"><div class="action-button">{{ ongoing.timestamp.date().isoformat() }} <a style="color:red" href="{{ ongoing['location'] }}" target="_blank"><div class="action-button">Join</div></a></div> + <p>{{ ongoing['meeting_description'] }} </p> + <p>{{ ongoing['expected_end'].time().isoformat(timespec='minutes') }}</p> + </a> + {% endfor %} + </section> + {% endif %} + {% if upcomingMeetings %} + <section class="upcoming"> + <h2>Upcoming:</h2> + {%- for upcoming in upcomingMeetings %} + <a href="{{ url_for('meetings.meeting',meeting_id=upcoming['meeting_id']) }}"><div>{{ upcoming.timestamp.date().isoformat() }} @ {{ upcoming.timestamp.time().isoformat(timespec='minutes') }}</div> + <p>{{ upcoming['meeting_description'] }}</p> + </a> + {%- endfor %} + </section> + {% endif %} + {% if pastMeetings %} + <section style="margin-bottom:1em;"> + <h2>Past Meetings:</h2> + {%- for meeting in pastMeetings %} + <div style="padding-bottom:1em;"> + <a href="{{ url_for('meetings.meeting',meeting_id=meeting['meeting_id']) }}"><div>{{ meeting.timestamp.date().isoformat() }} @ {{ meeting.timestamp.time().isoformat(timespec='minutes') }}</div>{% if allOldMeetings %}<div>{{meeting['meeting_description']}}</div>{% endif %}</a> + </div> + {%- endfor %} + {% if not allOldMeetings %}<a href="{{ url_for('meetings.past_meetings') }}" class="action-button">View older meetings</a>{% endif %} + {% if allOldMeetings %}<a href="{{ url_for('meetings.meetings') }}"class="action-button">Back to meetings</a>{% endif %} + </section> + {% endif %} + <a href="{{ url_for('meetings.new_meeting') }}" class="action-button">Schedule Meeting</a> + </section> +{% endblock %} diff --git a/app/meetings/templates/new_meeting.html b/app/meetings/templates/new_meeting.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} + +{% block title %}New Meeting{% endblock %} + +{% block content %} +<section> + <a href="{{ url_for('meetings.meetings') }}">back to meetings</a> +<h3>Schedule new Meeting</h3> +<form action="" method="POST" novalidate> + {{ form.hidden_tag() }} + {% for error in form.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {{ form.date.label }}{{ form.date() }} + {{ form.time.label }}{{ form.time() }} + {{ form.expected_end.label }}{{ form.expected_end() }} + {{ form.location.label }}{{ form.location() }} + {{ form.meeting_description.label }}{{ form.meeting_description() }} + {{ form.schedule_meeting() }} +</form> +</section> +{% endblock %} diff --git a/app/meetings/templates/update_meeting.html b/app/meetings/templates/update_meeting.html @@ -0,0 +1,32 @@ +{% extends 'base.html' %} + +{% block title %}{{ meeting.date }}Update Meeting{% endblock %} + +{% block content %} + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + <div id="messagebanner"><p>{{ message }}</p></div> + {% endfor %} + {% endif %} + {% endwith %} + {% if meeting %} + <section class="meeting"> + <a href="{{ url_for('meetings.meeting',meeting_id=meeting['meeting_id']) }}">back to meeting</a> + <div style="display:flex"><h2>Date: {{ meeting.timestamp.date() }}</h2><a href="{{url_for('meetings.change_meeting',meeting_id=meeting["meeting_id"],update="date")}}"style="color:red">change</a></div> + <div style="display:flex"><h3>Time: {{ meeting.timestamp.time().isoformat(timespec="minutes") }}</h3><a href="{{url_for('meetings.change_meeting',meeting_id=meeting['meeting_id'],update="time")}}"style="color:red">change</a></div> + <div style="display:flex"><h3>Location link: {{ meeting.location }}</h3><a href="{{url_for('meetings.change_meeting',meeting_id=meeting['meeting_id'],update="location")}}"style="color:red">change</a></div> + <div style="display:flex"><h3>Description: {{ meeting.meeting_destription }}</h3><a href="{{url_for('meetings.change_meeting',meeting_id=meeting['meeting_id'],update="description")}}"style="color:red">change</a></div> + <div style="display:flex"><h4 href="color:red">Documents:</h4><a href="{{ url_for('meetings.upload_meeting_document',meeting_id=meeting['meeting_id']) }}" style="color:red">[upload]</a></div> + {% if meeting.documents %} + {% for document in meeting.documents %} + <div> + <a href="">{{ document["doc_filename"] }}</a><a href="{{url_for('meetings.delete_meeting_file',fid=document["doc_id"],previous=meeting["meeting_id"])}}"style="color:red">remove</a> + </div> + {% endfor %} + {% endif %} + <a href="{{ url_for('meetings.meeting',meeting_id=meeting['meeting_id']) }}">back to meeting</a> + <a href="{{ url_for('meetings.remove_meeting',meeting_id=meeting['meeting_id']) }}">remove meeting</a> + </section> + {% endif %} +{% endblock %} diff --git a/app/meetings/templates/upload_meeting_doc_test.html b/app/meetings/templates/upload_meeting_doc_test.html @@ -0,0 +1,21 @@ +{% extends 'base.html' %} + +{% block title %}Upload{% endblock %} + +{% block content %} +{% with messages = get_flashed_messages() %} +{% if messages %} + {% for message in messages %} + <div id="messagebanner"><p>{{ message }}</p></div> + {% endfor %} +{% endif %} +{% endwith %} + <a href="{{ url_for('meetings.meetings') }}">back to meeting</a> + <section class="hours-grid">{#class="meeting">#} + <form action="" method="POST" novalidate enctype="multipart/form-data"> + {{ form.hidden_tag() }} + {{ form.document() }} + {{ form.submit_file() }} + </form> + </section> +{% endblock %} diff --git a/app/meetings/templates/upload_meeting_document.html b/app/meetings/templates/upload_meeting_document.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block title %}Upload{% endblock %} + +{% block content %} +{% with messages = get_flashed_messages() %} +{% if messages %} + {% for message in messages %} + <div id="messagebanner"><p>{{ message }}</p></div> + {% endfor %} +{% endif %} +{% endwith %} + <a href="{{ url_for('meetings.meeting',meeting_id=meeting['meeting_id']) }}">back to meeting</a> + <section class="hours-grid">{#class="meeting">#} + {# <h2>{{ upload.date() }} {{ upload.time().isoformat(timespec='minutes') }}</h2> #} + <p>Upload document to {{ meeting['timestamp'].date() }} Meeting for {{ meeting['meeting_description'] }}</p> + <form action="" method="POST" novalidate enctype="multipart/form-data"> + {{ form.hidden_tag() }} + {{ form.document() }} + {{ form.submit_file() }} + </form> + {% if meeting.documents %} + <h2>existing documents</h2> + {% for doc in meeting['documents'] %} + <p>{{ doc['doc_filename'] }}</p> + {% endfor %} + {% endif %} + </section> +{% endblock %} diff --git a/app/meetings/update.py b/app/meetings/update.py diff --git a/app/models.py b/app/models.py @@ -0,0 +1,217 @@ +# Test enviroment added validators username +# removed BaseModel +# added jsonify +# fixed datetime + +import datetime +import uuid +from flask import jsonify, request, redirect, session +from fastapi.encoders import jsonable_encoder +from typing import List, Optional +from pydantic import Field, ValidationError, validator +from werkzeug.security import generate_password_hash, check_password_hash +#from app import db + +class User(): + + def __init__(self, fname, mname, lname, email, phonenumber, branch, address, birthday, role): + self.fname = str(fname) + self.mname = str(mname) + self.lname = str(lname) + self.username = self.fname.lower() + self.mname.lower() + self.lname.lower() + self.email = email + self.phonenumber = phonenumber + self.branch = str(branch) + self.address = address + self.birthday = birthday + self.role = role + self.password_hash = None + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self['password_hash'], password) + + @staticmethod + def is_authenticated(): + return True + + @staticmethod + def is_active(): + return True + + @staticmethod + def is_anonymous(): + return False + + def get_id(self): + return self.username + + + + #def start_session(self, user): + # del user['password'] + # session['logged_in'] = True + # session['user'] = user + #return jsonify(user), 200 + +#ought to update this fn + def signup(self): + print(request.form) + user = { + '_id': uuid.uuid4().hex, + 'username': request.form.get('username'), + 'password_hash': request.form.get('password'), + 'role': request.form.get('Role'), + 'location': request.form.get('Primary Location'), + 'phone': request.form.get('Phone Number'), + 'email': request.form.get('Email'), + 'pay_period': request.form.get('Pay Period'), + 'pay_value': request.form.get('Pay Value'), + } + + # user['password'] = generate_password_hash(user['password']) + + return jsonify(user), 200 + +#ought to update this fn + def signout(self): + session.clear() + return redirect('/login') + + # def loginModel(self): + # user = db.user.find_one({ + # 'email': request.form.get('email') + # }) + + # if user and password_verify(request.form.get('password'), user['password']): + # return self.start_session(user) + + # return jsonify({ 'error': 'Invalid login' }), 401 + +#ought to update this fn + @validator('username') + def username_alphanumeric(cls, v): + assert v.isalnum(), 'Username must be alphanumeric' + return v + + +class Time: + + def clockin(self, clockin): + clockin = { + '_id': int, + 'clock_in': Optional[datetime.datetime.utcnow], + 'date': Optional[datetime.datetime.today], + 'project': str, + 'note': str, + } + + return jsonify(clockin), 200 + + def clockout(self, clockout): + clockout = { + '_id': int, + 'clock_out': Optional[datetime.datetime.utcnow], + 'date': Optional[datetime.datetime.today], + 'project': str, + 'note': Optional[str], + 'total_time': int + } + + return jsonify(clockout), 200 + + _id: int + # forign key + clock_in: Optional[datetime.datetime.utcnow] #System time Clock_out should be optional, but can clock_in? + modified_by: str #link to _id of user + date: Optional [datetime.datetime.today] + project: str + clock_out: Optional[datetime.datetime.utcnow] #System time + note: str + perdium: bool + total_time: int #clock_out - clock_in OR if ! clock_out utcnow - clock_in + + def to_json(self): + return jsonable_encoder(self, exclude_none=True) + + def to_bson(self): + data = self.dict(by_alias=True, exclude_none=True) + + if data["_id"] is None: + data.pop("_id") + return data + + +class Fleet: + + def vehicle_repair(self, vehicle_repair): + vehicle_repair = { + '_id': int, + 'date': Optional[datetime.datetime.utcnow], + 'operator': int, #User ID forign key + 'safety_checks': bool, + 'additional_notes': str, + 'vehicle': int, + 'incident_report': str, + 'mileage': int + } + + return jsonify(vehicle_repair), 200 + + _id: int + date: Optional[datetime.datetime.utcnow] + operator: int #forign key to userID + safety_checks: bool #array for different safety checks + additional_notes: str + vehicle: int #vehicleID + incident_report: str + mileage: int + + +class Agreement: + + def document(self, document): + document = { + '_id': int, #forign key to user + 'start_date': int, + 'end_date': int, + 'bid_document': str, #Filepath to document + 'budget': float, + 'cost': int, + } + + return jsonify(document), 200 + + _id: int #forign key to user + start_date: int + end_date: int + bid_document: str #Filepath to document + budget: float + cost: int + +class Projects:#Projects references agreement + + def project(self, project): + project = { + 'project_id': int, + 'project_name': str, + # 'project_budget': List[float] = [] + } + + return jsonify(project), 200 + + project_id: int + project_name: str + project_role: List[str] = [ + + ] + project_budget: List[float] = [ + # labor_budget: float | Indexed 0 + # travel_budget: float | Indexed 1 + # supplies_budget: float | Indexed 2 + # contact_budget: float | Indexed 3 + # equipment_budget: float | Indexed 4 + # other: float | Indexed 5 + ] diff --git a/app/routes.py b/app/routes.py @@ -0,0 +1,2287 @@ +import datetime +import random, string +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, NewHoursForm, ConfirmRemove, NewAgreementForm, RenameAgreementForm, NewProjectForm, MoveProjectForm, RenameProjectForm, CrewClockinWidget, NewUserHourForm, ChangePasswordForm, ChangeUserForm, upDate, updateTime, updateProject, newNote, dateRange, ChangeBranchForm +from flask import request +from werkzeug.urls import url_parse +from werkzeug.security import generate_password_hash, check_password_hash +from flask_login import current_user, login_user, logout_user, login_required +from app.branches.routes import get_available_branches +from app.equipment.equipment import get_available_equipment_type +from app.models import User, Time, Fleet, Agreement, Projects +from bson.objectid import ObjectId +import bson.json_util as json_util +#from .config import EXEMPT_METHODS +import functools + +OrganizationName = 'Youth Employment Program' # Maybe pass this as a value though the object for relevant pages??? + +#### #### +####### Application Modules Init ####### +#### #### +# DB Connection & LoginManager init +mongo = PyMongo(app) +login_manager = LoginManager(app) +login_manager.login_view = 'login' + +class UserNotFoundError(Exception): + pass + +def fetch_user(user_id): + user = mongo.db.user_collection.find_one({"_id":ObjectId(user_id)}) + if user == None: + raise UserNotFoundError(f"User id {user_id} returned none") + else: + return user + +def fetch_user_from_username(username): + user = mongo.db.user_collection.find_one({"username":username}) + if user == None: + raise UserNotFoundError(f"User name {username} returned none") + else: + return user + +# Add ability to get users by role type... case match statement inside if for default no arguments? +# TODO currently doesn't care about active state. FIX +def get_available_users(active=True,role=None,branch=None): + availableUsers = [("","Select User")] + for user in mongo.db.users_collection.find(): + if branch: + if 'branch' in user: + if user['branch'] == 'Global' or user['branch'] == branch: + availableUsers.append((user['_id'],user['username'])) + else: + availableUsers.append((user['_id'],user['username'])) + return availableUsers + +def get_available_projects(branch=None): + availableProjects = [("","Select Project")] + for project in mongo.db.projects_collection.find(): + if branch: + if 'branch' in project: + if project['branch'] == 'Global' or project['branch'] == ObjectId(branch): + availableProjects.append((project['_id'],project['project_name'])) + else: + availableProjects.append((project['_id'],project['project_name'])) + return availableProjects + +#### #### +####### User Routes/Queries ####### +#### #### +#@app.route('/user/signup', methods=['GET']) +#def signup(): +# return User().signup() + +#@app.route('/user/loginModel', methods=['GET']) +#def loginModel(): +# return User().loginModel() + +#@app.route('/user/signout') +#def signout(): +# return User().signout() + +#### #### +####### Time Routes/Queries ####### +#### #### +#@app.route('/time/clockin', methods=['GET', 'POST']) +#def clockin(): +# return Time().clockin() + +#@app.route('/time/clockout', methods=['GET', 'POST']) +#def clockout(): +# return Time().clockout() + +#### #### +####### Vehicle Routes/Queries ####### +#### #### +#@app.route('/fleet/vehicle_repair', methods=['GET', 'POST']) +#def vehicle_repair(): +# return Fleet().vehicle_repair() + +#### #### +####### Agreement Routes/Queries ####### +#### #### +#@app.route('/agreement/document', methods=['GET', 'POST']) +#def document(): +# return Agreement().document() + +#### #### +####### Project Routes/Queries ####### +#### #### +#@app.route('/projects/project', methods=['GET', 'POST', 'PUT']) + +##{{{{{{{{{{{{{{{{{{{{{ Decorator Functions }}}}}}}}}}}}}}}}}}}}}## +#@login_required +#def admin_required(func): +# """Ensure logged in user has admin permissions""" +# @functools.wraps(func) +# def decorated_view(*args, **kwargs): +# if current_user.role in : +# pass +# elif not current_user.role in : +# return current_app.login_manager.unauthorized() +# +# #if not has_permission: +# return func(*args, **kwargs) +# +# return decorated_view +##{{{{{{{{{{{{{{{{{{{{{ END Decorator Functions }}}}}}}}}}}}}}}}}}}}}## + + +#================================# +########### ############ +#### PAGE ROUTING #### +########### ############ +#================================# + +#### #### +####### Logout User Route ####### +#### #### +@app.route("/logout", methods=['GET']) +def logout(): + logout_user() + return redirect(url_for('login')) + +#### #### +####### Login/Root Route ####### +#### #### +@app.route('/', methods=['GET', 'POST']) +@app.route("/login", methods=['GET', 'POST']) +def login(): + if current_user.is_authenticated: + return redirect(url_for('dashboard')) + form = LoginForm() + if form.validate_on_submit(): + # check form value for identity in db, if found AND form password matches stored hash, create User object + try: + u = fetch_user_from_username(form.username.data) + except: + flash("Invalid username or password") + return redirect(url_for('login')) + else: + if u['is_active'] and User.check_password(u, form.password.data): + user_obj = User(fname=u['fname'],mname=u['mname'],lname=u['lname'],email=u['email'],branch=u['branch'],address=u['address'],birthday=u['birthday'],role=u['role'],phonenumber=u['phonenumber']) + #login with new user object + login_user(user_obj) + # check next redirect to stop cross-site-redirects, another example here : http://flask.pocoo.org/snippets/62/ + next = request.args.get('next') + if not next or url_parse(next).netloc != '': + next = url_for('dashboard') + return redirect(next) + else: + flash("Invalid username or password") + return redirect(url_for('login')) + return render_template('login.html',form=form,ORGNAME = OrganizationName) + +#### #### +####### Load User Data ####### +#### #### +#load user_data into User object iff username == stored username +@login_manager.user_loader +def load_user(username): + u = mongo.db.user_collection.find_one({"username": username}) + if not u: + return None + user_obj = User(fname=u['fname'],mname=u['mname'],lname=u['lname'],email=u['email'],branch=u['branch'],address=u['address'],birthday=u['birthday'],role=u['role'],phonenumber=u['phonenumber']) + user_obj.password_hash = u['password_hash'] + return user_obj + +@app.route('/update/<update_t>/<username>',methods=['GET','POST']) +@login_required +def update_user(update_t,username): #username=current_user): + form = ChangeUserForm() + form.role.choices = mongo.db.permissions_collection.find({},{'label':1})#get_available_roles() + + form.branch.choices = [("",'Select Branch'),("Global","Global")] + for branch in get_available_branches(): + form.branch.choices.append(branch) + + user = fetch_user_from_username(username) + match update_t: + case "fname": + if form.validate_on_submit(): + mongo.db.user_collection.update_one({'username':user['username']},{'$set':{'fname':form.fname.data,'username':form.fname.data.lower()+user['mname'].lower()+user['lname'].lower()}}) + flash("change {}.".format(update_t)) + return redirect(url_for('activeusers')) + case "mname": + if form.validate_on_submit(): + mongo.db.user_collection.update_one({'username':user['username']},{'$set':{'mname':form.mname.data,'username':user['fname'].lower()+form.mname.data.lower()+user['lname'].lower()}}) + flash("change {}.".format(update_t)) + return redirect(url_for('activeusers')) + case "lname": + if form.validate_on_submit(): + mongo.db.user_collection.update_one({'username':user['username']},{'$set':{'lname':form.lname.data,'username':user['fname'].lower()+user['mname'].lower()+form.lname.data.lower()}}) + flash("change {}.".format(update_t)) + return redirect(url_for('activeusers')) + case "birthday": + if form.validate_on_submit(): + mongo.db.user_collection.update_one({'username':user['username']},{'$set':{'birthday':form.birthday.data.strftime('%Y-%m-%d')}}) + flash("change {}.".format(update_t)) + return redirect(url_for('activeusers')) + case "role": + if form.validate_on_submit(): + mongo.db.user_collection.update_one({'username':user['username']},{'$set':{'role':form.role.data}}) + flash("change {}.".format(update_t)) + return redirect(url_for('activeusers')) + case "address": + if form.validate_on_submit(): + mongo.db.user_collection.update_one({'username':user['username']},{'$set':{'address':form.address.data}}) + flash("change {}.".format(update_t)) + return redirect(url_for('activeusers')) + case "branch": + if form.validate_on_submit(): + mongo.db.user_collection.update_one({'username':user['username']},{'$set':{'branch':form.branch.data}}) + flash("change {}.".format(update_t)) + return redirect(url_for('activeusers')) + case "phonenumber": + if form.validate_on_submit(): + mongo.db.user_collection.update_one({'username':user['username']},{'$set':{'phonenumber':form.phonenumber.data}}) + flash("change {}.".format(update_t)) + return redirect(url_for('activeusers')) + case "email": + if form.validate_on_submit(): + mongo.db.user_collection.update_one({'username':user['username']},{'$set':{'email':form.email.data}}) + flash("change {}.".format(update_t)) + return redirect(url_for('activeusers')) + case "payPeriod": + if form.validate_on_submit(): + mongo.db.user_collection.update_one({'username':user['username']},{'$set':{'pay_period':form.payPeriod.data}}) + flash("change {}.".format(update_t)) + return redirect(url_for('activeusers')) + case "payValue": + if form.validate_on_submit(): + mongo.db.user_collection.update_one({'username':user['username']},{'$set':{'pay_value':form.payValue.data}}) + flash("change {}.".format(update_t)) + return redirect(url_for('activeusers')) + + return render_template("admin/users/update_user.html",user=user,form=form,update_t=update_t) + +@app.route('/newpass',methods=['GET','POST']) +@login_required +def chgpass(): + form = ChangePasswordForm() + if form.validate_on_submit(): + 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')) + + return render_template('admin/users/newpass.html',form=form,ORGNAME=OrganizationName) + +@app.route('/newpass/<uid>',methods=['GET','POST']) +@login_required +def chgpass_by_uid(uid): + try: + user = fetch_user(uid) + except: + return "User not Found" + else: + form = ChangePasswordForm() + if form.validate_on_submit(): + mongo.db.user_collection.update_one({'username':user['username']},{'$set':{'password_hash':generate_password_hash(form.newpass.data)}}) + flash("Changed password for {} to {}".format(user['username'],form.newpass.data)) #Will need to sendmail password to form.email.data later + + return redirect(url_for('dashboard')) + + return render_template('admin/users/newpass.html',form=form,ORGNAME=OrganizationName) +#### #### +####### Dashboard Route ####### +#### #### + +@app.route('/index') +@app.route("/dashboard", methods=['GET', 'POST']) +@login_required +def dashboard(): + + currentdate = datetime.datetime.now() + + dashperms=mongo.db.permissions_collection.find_one({'label': current_user.role},{'dashboard':1,'_id':0}) + dashperms=dashperms['dashboard'] + + clocked_in_users = mongo.db.time_collection.find({'clock_out': {'$exists':False}}) + def clock_user_out(time_id,notes='',lunch=False,perdiem=False): + if notes != '' and notes != None: + 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: + flash('No time entry found, or user has checked out already') + return redirect(url_for('dashboard')) + else: + 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 + time_id = mongo.db.time_collection.find_one({'modified_by.0': current_user.username,'clock_out':{'$exists':False}})["_id"] + else: + clocked_out = True + #End isUserClockedIn() + #Start fleetCheckedOut() + if mongo.db.fleet_collection.find_one({'operator': current_user.username,'end_mileage':{'$exists':False}}): + fleetCheckedOut = True + fleet_id = mongo.db.fleet_collection.find_one({'operator': current_user.username,'end_mileage':{'$exists':False}})["_id"] + vehicle_name = mongo.db.fleet_collection.find_one({'operator': current_user.username,'end_mileage':{'$exists':False}})["vehicle"] + start_mileage = mongo.db.fleet_collection.find_one({'operator': current_user.username,'end_mileage':{'$exists':False}})["start_mileage"] + else: + fleetCheckedOut = False + vehicle_name = '' + #End fleetCheckedOut() + def fleet_check_in(fleet_id,start_mileage,end_mileage,notes=''): + 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 != '' 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}}) + else: + flash('uncaught logic updating {} with {}'.format(fleet_id,end_mileage)) + mongo.db.fleet_collection.update_one({'_id':fleet_id},{'$set':{'end_mileage':end_mileage}}) + return redirect(url_for('dashboard')) + else: + flash('Unable to check out vehicle') + return redirect(url_for('dashboard')) + + #def newfleet_check_out(vehicle_id,start_milage,additional_notes=None,safety_checks=None) + #availableVehicles = mongo.db.fleet_collection.find_one({'_id':'Fleet Pool'},{'available':1})['available'] + availableVehicles = [("","Select Vehicle")] + for vehicle in get_available_equipment_type('vehicle',branch=current_user.branch): + availableVehicles.append((vehicle['_id'],"Vehicle {}".format(vehicle['vehicle_id']))) + +#currently gets ALL projects TODO make filter by available agreements/projects + availableProjects = get_available_projects(current_user.branch) +# 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)) + + #reset clocked_in_users + clocked_in_users = mongo.db.time_collection.find({'clock_out': {'$exists':False}}) + +# END default form values + + clockinform=PunchclockinWidget() + clockoutform=PunchclockoutWidget() + fleetoutform=FleetCheckoutForm() + fleetinform=FleetCheckinForm() + crewform=CrewClockinWidget() + + clockinform.projectsSel.choices = availableProjects + #clockoutform.projectsSel.choices = availableProjects + fleetoutform.vehicle.choices = availableVehicles + #fleetoutform.vehicle.default = availableVehicles[0]# Doesn't function + #fleetoutform.start_mileage.data = lastMileage + crewform.time.data = datetime.datetime.now() + crewform.projectSel.choices = availableProjects + crewform.projectSel.data = availableProjects[0] + crewform.userSel.choices = clocked_out_active_users + crewform.userSel.data = clocked_out_active_users[0] + +# Currently broken... appears to call constructor(wanted for setting default values) but regenerates csrf_token... clone fn without this call? or overload fn? Haven't looked at documentation yet +# See: https://wtforms.readthedocs.io/en/3.0.x/forms/#wtforms.form.BaseForm.__init__ + #clockinform.process() + #clockoutform.process() + #fleetoutform.process() + + #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.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.now()], + 'modified_by' : [current_user.username], + 'date' : datetime.datetime.today(), + 'project' : ObjectId(clockinform.projectsSel.data)}) + return redirect(url_for('dashboard')) + +# Can still clock in without checking current vehicle status!!! TODO FIX ME !!! + #if fleetoutform.validate_on_submit(): + if not fleetCheckedOut and request.method == 'POST' and fleetoutform.validate(): + mongo.db.fleet_collection.insert_one({'date':datetime.datetime.today(), # NEED to work on modular way of storing safety checks... might condence to single true if all checked. else returns false and records false datavalue.label in incident_report[] If incident report, remove vehicle from available pool and display widget in admin layout + 'vehicle':fleetoutform.vehicle.data, + 'start_mileage':fleetoutform.start_mileage.data, + 'operator':current_user.username, + 'additional_notes':fleetoutform.additionalnotes.data}) + #TODO mongo.db.fleet_collection.update_one + return redirect(url_for('dashboard')) + + if fleetCheckedOut and request.method == 'POST' and fleetinform.validate(): + fleet_check_in(fleet_id,start_mileage,fleetinform.end_mileage.data,fleetinform.incident_notes.data) + return redirect(url_for('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/project/<mod_username>/<timeid>",methods=['GET','POST']) +@login_required +def updateProjectTime(mod_username,timeid): + timeid = ObjectId(timeid) + availableProjects = get_available_projects(current_user.branch)#[] #change to get_available_projects() -> projects where user branch == project['branch'] + form = updateProject() + +# for project in mongo.db.projects_collection.find(): +# availableProjects.append((project['_id'],project['project_name'])) + + form.projectSel.choices = availableProjects + + if form.validate_on_submit(): + app.logger.info('update project route') + try: + entry = mongo.db.time_collection.find_one({'_id':timeid}) + except: + flash("Issue finding/assigning time_id: {}".format(timeid)) + else: + try: + mongo.db.time_collection.update_one({'_id':timeid},{'$set':{'project':ObjectId(form.projectSel.data)}}) + except: + flash("unable to set project for {} to {}".format(timeid, form.projectSel.data)) + else: + flash("Updated project") + finally: + return redirect(url_for('hours',username=entry['modified_by'][0]))#change to hours and mod_username redirect + return render_template('dashboard/punchclock/update/project.html',form=form, 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: + if form.note.data != None and form.note.data != '': + 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! + else: + try: + mongo.db.time_collection.update_one({'_id':timeid},{'$unset':{"note":""},'$push':{'modified_by':mod_username}}) + except: + flash("Unable to remove note") + else: + flash('Removed note') + finally: + return redirect(url_for('dashboard')) + + 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): + timeid = ObjectId(timeid) + + if mongo.db.time_collection.find_one({'_id': timeid, 'lunch':{'$exists':False}}): + mongo.db.time_collection.update_one({'_id':timeid},{'$set':{'lunch':True}}) + if mongo.db.time_collection.find_one({'_id': timeid})['lunch'] == True: + mongo.db.time_collection.update_one({'_id':timeid},{'$set':{'lunch':False}}) + else: + mongo.db.time_collection.update_one({'_id':timeid},{'$set':{'lunch':True}}) + + return redirect(url_for('dashboard')) + +@app.route("/toggle-per-diem/<timeid>",methods=['GET','POST']) +@login_required +def toggle_per_diem(timeid): + timeid = ObjectId(timeid) + + if mongo.db.time_collection.find_one({'_id': timeid, 'per_diem':{'$exists':False}}): + mongo.db.time_collection.update_one({'_id':timeid},{'$set':{'per_diem':True}}) + if mongo.db.time_collection.find_one({'_id': timeid})['per_diem'] == True: + mongo.db.time_collection.update_one({'_id':timeid},{'$set':{'per_diem':False}}) + else: + 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 = get_available_projects(current_user.branch)#[] + #TODO Might be helpful to make available projects search for projects available to the selected user... +# 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' : ObjectId(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): + user = mongo.db.user_collection.find_one({"username": usernm}) + availableProjects = get_available_projects(current_user.branch) #[] +# for project in mongo.db.projects_collection.find(): +# availableProjects.append((project['_id'],project['project_name'])) +# availableProjects = [("","Select Project")] +# for project in mongo.db.projects_collection.find(): +# if 'branch' in project: +# if project['branch'] == 'Global' or project['branch'] == user['branch']: +# availableProjects.append((project['_id'],project['project_name'])) + + 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 + # if form.projectSel.data == "" or form.projectSel.data == "Select Project": + # flash("You must Select a Project")# This part doesn't seem to function? + # return redirect(url_for('new_time',usernm=usernm)) + 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) + entryevent = { + 'date' : dateentry, + 'clock_in' : [starttime], + 'clock_out' : [endtime], + 'project' : ObjectId(form.projectSel.data), + 'lunch': form.lunchSel.data, + 'per_diem':form.perDiemSel.data + } + if usernm is current_user.username: + entryevent['modified_by'] = [usernm] + else: + entryevent['modified_by'] = [usernm,current_user.username] + if form.note.data != '': + entryevent['note'] = form.note.data + mongo.db.time_collection.insert_one(entryevent) + + return redirect(url_for('hours',username=user['username'])) + + return render_template('dashboard/punchclock/otheruser.html',user=user,form=form,ORGNAME=OrganizationName) + +@app.route("/newusertime",methods=['GET','POST']) +@login_required +def new_user_time(): + clocked_in_users = mongo.db.time_collection.find({'clock_out': {'$exists':False}}) + availableProjects = get_available_projects(current_user.branch) +# 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) + entryevent = { + 'date' : dateentry, + 'clock_in' : [starttime], + 'clock_out' : [endtime], + 'project' : ObjectId(form.projectSel.data), + 'lunch': form.lunchSel.data, + 'per_diem':form.perDiemSel.data + } + if form.userSel.data is current_user.username: + entryevent['modified_by'] = [form.userSel.data] + else: + entryevent['modified_by'] = [form.userSel.data,current_user.username] + if form.note.data != '' and form.note.data != None: + entryevent['note'] = form.note.data + try: + mongo.db.time_collection.insert_one(entryevent) + except: + flash("Unhandled error occured") + else: + flash("Created time entry for {}".format(userSel.data)) + finally: + return redirect(url_for('new_user_time')) + + return render_template('dashboard/punchclock/otheruser.html',form=form,ORGNAME=OrganizationName) + +@app.route("/clockinuser/<modusernm>/<usertoinid>/<project>/<intime>", methods=['GET','POST']) +@login_required +def clockin_by_id(modusernm,usertoinid,project,intime): + timeid = ObjectId() + user2 = eval(usertoinid) + project = eval(project) + + mongo.db.time_collection.insert_one({'_id':timeid, + 'modified_by' :[user2[0], modusernm], + 'clock_in' : [datetime.datetime.strptime(intime, '%Y-%m-%d %H:%M:%S.%f')], + 'project' : ObjectId(project[0])}) + + 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.now()]}}) + mongo.db.time_collection.update_one({'_id':time_id},{'$push':{'modified_by':mod_username}}) + flash('Clocked out') + else: + flash('No time entry found, or user has checked out already') + + clock_otheruser_out(timeid,modusernm) + + return redirect(url_for('dashboard')) + +#### #### +####### Admin Route ####### +#### #### +@app.route("/admin") +@login_required +#@admin_required +def admin(): + adminperms=mongo.db.permissions_collection.find_one({'label': current_user.role},{'admin':1,'_id':0}) + adminperms=adminperms['admin'] + #AgreementsData + allagreements=mongo.db.agreements_collection.find()#All agreements, including outside endDate... filter to active agreements? + agreements = [] + for agreement in allagreements: + agreement['total_budget']=0 + agreement['total_cost']=0 + for i in range(len(agreement['projects'])): + try: + if mongo.db.projects_collection.find_one({'_id': agreement['projects'][i]}) is None: + flash("could not find project {} in agreement {}".format(agreement['projects'][i],agreement['_id'])) + return redirect(url_for('dashboard')) + else: + agreement['projects'][i]= mongo.db.projects_collection.find_one({'_id': agreement['projects'][i]}) + except: + flash("could not find project {} in agreement {}".format(agreement['projects'][i],agreement['_id'])) + return redirect(url_for('dashboard')) + else: + try: + if agreement['projects'][i]:#['budget'][0] + agreement['projects'][i]['total_budget'] = sum(agreement['projects'][i]['budget'][0].values()) + else: + agreement['projects'][i]['total_budget'] = 1 + except: + flash("could not assign agreement {} total budget") + agreement['projects'][i]['total_budget'] = 1 + finally: + agreement['projects'][i]['total_cost'] = sum(agreement['projects'][i]['costs'][0].values()) + agreement['total_budget']=agreement['total_budget']+agreement['projects'][i]['total_budget'] + agreement['total_cost']=agreement['total_cost']+agreement['projects'][i]['total_cost'] + agreements.append(agreement) + + #all_permissions=mongo.db.permissions_collection.find_one({"label":current_user.role}) + #admnperms=all_permissions.admin + return render_template ('admin/layout.html',agreements=agreements,permissions=adminperms,ORGNAME=OrganizationName) + +#### #### +####### Agreement Report Route ####### +#### #### +# Report Routes + +#@app.route('/admin/agreement') +#@login_required +#def agreement_report(): +# return render_template ('admin/reports/agreement_report.html', ORGNAME=OrganizationName) + +#### #### +####### Employee Report Route ####### +#### #### +#@app.route('/admin/employee') +#@login_required +#def employee_report(): +# return render_template ('admin/reports/employee_report.html', ORGNAME=OrganizationName) + +#### #### +####### Pay Period Report Route ####### +#### #### +#@app.route('/admin/pay-period') +#@login_required +#def pay_period_report(): +# return render_template ('admin/reports/pay_period_report.html', ORGNAME=OrganizationName) + +#### #### +####### Vehicle Report Route ####### +#### #### +#@app.route('/admin/vehicle') +#@login_required +#def vehicle_report(): +# return render_template ('admin/reports/vehicle_report.html', ORGNAME=OrganizationName) + +############################# +###### DEV HOURS ROUTE ###### +############################# +@app.route('/hoursd/<username>', methods=['GET','POST']) +@login_required +def hoursd(username):#userid goes into call to db to get user[] -> then returns formatted table (punchclock/index.html + + aghours = mongo.db.time_collection.find({'modified_by.0':username}) + dbhours = mongo.db.time_collection.aggregate( [ + { + "$match":{'modified_by.0':username} + }, + { + '$lookup': { + 'from': 'projects_collection', + 'localField': 'project', + 'foreignField': '_id', + 'as': 'project' + } + }, + { + "$sort":{'clock_in':-1} + }, + { + "$limit":10 #change to ~ 30 days OR to prior 1st or 15th of month + } + + ] ) + + tspp = mongo.db.time_collection.aggregate( [ + { + "$lookup":{ + 'from':'projects_collection', + 'localField':'project', + 'foreignField':'_id', + 'as':'project_data' + } + }, + { + "$group": { + "_id":"$project_data", + "laborHoursWorked": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }, + "lunchCount": { "$sum" : { '$cond':["$lunch",1,0] } }, + "perdiemCount": { "$sum" :{'$cond':["$per_diem",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) + + #Banner, TODO create a MOTD framework, for admin messages + currentdate = datetime.datetime.now() + currentdate = currentdate.strftime('%Y-%m-%d') + currentdate = currentdate.split('-') + birthday = current_user.birthday.split("-") + if currentdate[1] == birthday[1] and currentdate[2] == birthday[2]: + flash("Happy Birthday {}!".format(current_user.fname)) + ## END Banner + + return render_template ('dashboard/punchclock/index.dev.html',cd=currentdate, bd=birthday,hours=dbhours,tspp=tspp,ORGNAME=OrganizationName) + + + +#### #### +####### Hours Route ####### +#### #### +@app.route('/hours/<username>', methods=['GET','POST']) +@login_required +def hours(username): + #query time collection for all time entries + #for entry in entries: + # time = entry.get('clock_out'[0],datetime.datetime.now()) - entry['clock_in'][0] + # if entry['modified_by'][0] in user_hours: + # user_hours[entry['modified_by'][0]] =+ time + # else: + # user_hours.append({entry['modified_by'][0],time}) + + # user = load_user(username) + #dashperms=mongo.db.permissions_collection.find_one({'label': current_user.role},{'dashboard':1,'_id':0}) + #dashperms=dashperms['dashboard'] + #total_hours=0 + user = mongo.db.user_collection.find_one({"username": username}) + availableProjects = get_available_projects(current_user.branch) +# 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.aggregate( [ + { + "$match":{'modified_by.0':username} + }, + { + '$lookup': { + 'from': 'projects_collection', + 'localField': 'project', + 'foreignField': '_id', + 'as': 'project' + } + }, + { + "$sort":{'clock_in':-1} + }, + { + "$limit":10 #change to ~ 30 days OR to prior 1st or 15th of month + } + + ] ) + #hours = [] + #deltas=[] + #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.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' : ObjectId(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' : ObjectId(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}) + + + tspp = mongo.db.time_collection.aggregate( [ + { + "$lookup":{ + 'from':'projects_collection', + 'localField':'project', + 'foreignField':'_id', + 'as':'project_data' + } + }, + { + "$group": { + "_id":"$project_data", + "laborHoursWorked": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }, + "lunchCount": { "$sum" : { '$cond':["$lunch",1,0] } }, + "perdiemCount": { "$sum" :{'$cond':["$per_diem",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 ('dashboard/punchclock/index.html',form=form,hours=dbhours,user=user,tspp=tspp,ORGNAME=OrganizationName) + +@app.route('/allhours/<username>', methods=['GET','POST']) +@login_required +def all_user_hours(username): + user = mongo.db.user_collection.find_one({"username": username}) + #need a function which checks for dated(archived) time collections, and appends them to list. Might look like below + # hours = [ {'current_year()":{'current pay period':{mongo_aggregate_lookup(returned doc)}},(each pay period in current year)},{'last_year().date':[{data},{data}]},etc... + # This setup might benefit from time entries being in their own db? + dbhours = mongo.db.time_collection.aggregate( [ + { + "$match":{'modified_by.0':username} + }, + { + '$lookup': { + 'from': 'projects_collection', + 'localField': 'project', + 'foreignField': '_id', + 'as': 'project' + } + }, + { + "$sort":{'clock_in':-1} + } + ] ) + + return render_template ('dashboard/punchclock/all.html',hours=dbhours,user=user,ORGNAME=OrganizationName) + +# Don't really need this until additional functionality is added, not in current project scope +#@app.route("/fleet") +#def fleet(): +# return render_template('dashboard/fleet/index.html',ORGNAME=OrganizationName) + +#### #### +####### Roles Admin Route ####### +#### #### +@app.route("/admin/roles") +@login_required +def roles(): + admnform = AdmnPermissionsForm() + dashform = DashPermissionsForm() + return render_template('admin/roles/updateroles.html',dashform=dashform,admnform=admnform,ORGNAME=OrganizationName) + +#### #### +####### Active Users Admin Route ####### +#### #### +@app.route("/admin/users/active") +@login_required +def activeusers(): + active = mongo.db.user_collection.find({'is_active':True}) + return render_template('admin/users/active.html',activeusers=active,ORGNAME=OrganizationName) + +@app.route("/user/<user_id>") +@login_required +def user(user_id): + usr = mongo.db.user_collection.find({"_id":ObjectId(user_id)}) + return render_template('admin/users/active.html',activeusers=usr,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 ####### +#### #### +@app.route("/admin/users/inactive") +@login_required +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) + try: + mongo.db.time_collection.delete_one({'_id':timeid}) + except: + flash("An unhandled error occured removing time") + else: + flash("Removed Time") + finally: + 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: + availableRoles.append((perm['_id'],perm['label'])) + + defaultBranch = 'Dillon' + defaultRole = 'Crew' +# END TMP Values + + form = ChangeUserForm() + + form.branch.choices = [("",'Select Branch'),("Global","Global")] + for branch in get_available_branches(): + form.branch.choices.append(branch) + + 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 = [] + for perm in mongo.db.permissions_collection.find(): + availableRoles.append((perm['label'])) + + defaultBranch = 'Dillon' +# END TMP Values + + form = NewUserForm() + + #form.branch.choices = availableBranches + form.branch.choices = [("",'Select Branch'),("Global","Global")] + for branch in get_available_branches(): + form.branch.choices.append(branch) + + form.role.choices = availableRoles + #form.process() + + if form.validate_on_submit(): + genpasswd = ''.join(random.choice(string.ascii_letters) for _ in range(14)) + 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, + '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'), + 'password_hash':generate_password_hash(genpasswd), + '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':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 + return redirect(url_for('newuser')) + + return render_template('admin/users/newuser.html',form=form,ORGNAME=OrganizationName) + +#### #### +####### Agreement Admin Route ####### +#### #### +@app.route("/admin/agreement/<agreement_id>",methods=["GET"]) +@login_required +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 +def newagreement(): +# Temp values, change to modular db dependent values +# END TMP Values + + form = NewAgreementForm() + + if form.validate_on_submit(): + # create deterministic agreement unique _id? Example being genpasswd in new user validate on submit + + mongo.db.agreements_collection.insert_one({ + 'agreement_name':form.agreementName.data, + 'agency':[form.agency.data], + 'projects':[], + 'start_date':form.startDate.data.strftime('%Y-%m-%d'), + 'end_date':form.endDate.data.strftime('%Y-%m-%d'), + # 'budget':[{ + # 'labor':form.laborBudget.data, + # 'travel':form.travelBudget.data, + # 'supplies':form.suppliesBudget.data, + # 'contact':form.contactBudget.data, + # 'equipment':form.equipmentBudget.data, + # 'other':form.otherBudget.data + # }] # most recent labor budget accessed via budget.0.labor + }) + flash("{} with {} added, please create at least one project.".format(form.agreementName.data, form.agency.data )) #Will need to sendmail password to form.email.data later + return redirect(url_for('newproject')) + + return render_template('admin/agreements/newagreement.html',form=form,ORGNAME=OrganizationName) + +#### #### +####### 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/projects/new/<agreementid>", methods=["GET","POST"]) +@app.route("/admin/projects/new", methods=["GET","POST"]) +@login_required +def newproject(agreementid=None): +# Available Agreements. Move to fn() + availableAgreements = [] + for agreement in mongo.db.agreements_collection.find(): + availableAgreements.append((agreement['_id'],agreement['agreement_name'])) +# END Available Agreements + + form = NewProjectForm() + # TODO If statement for optional newproject('argument') if new or none return all choices, else return (agreement_id, agreement_name) for new agreement ID passed <-- What did I mean by this? OH It's a conditional to redirect to create a new agreement... this would require passing the form data to the next route as params? + form.agreement.choices = availableAgreements + #form.branch.choices = ['Dillon','Salmon'] + form.branch.choices = [("",'Select Branch'),("Global","Global")] + for branch in get_available_branches(): + form.branch.choices.append(branch) + #TODO MAKE BELOW WORK!!! Apply to other branch dependent areas + #branches = [] + #for branch in mongo.db.branch_collection.find(): + # branches.append((branch['_id'],branch['location'])) + #form.branch.choices = branches + + 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 + + pjname = "FE " + str(form.fenumber.data) + ": " + form.projectName.data + + mongo.db.projects_collection.insert_one({ + 'project_name':pjname, + 'agreement':ObjectId(form.agreement.data), + 'branch':form.branch.data, + #'branch':ObjectId(form.branch.data), + 'budget':[{ + 'labor':form.laborBudget.data, + 'travel':form.travelBudget.data, + 'supplies':form.suppliesBudget.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, + 'perdiem':0, + 'equipment':0, + 'indirect':0, + 'contracting':0, + 'lodging':0, + 'other':0 + }] + }) + pj_id = mongo.db.projects_collection.find_one({'project_name':pjname})['_id'] + mongo.db.agreements_collection.update_one({ '_id':ObjectId(form.agreement.data) },{ '$push':{ 'projects':pj_id }}) + + flash("{} part of {} added".format(form.projectName.data, form.agreement.data )) #Will need to sendmail password to form.email.data later + return redirect(url_for('admin')) + + return render_template('admin/agreements/projects/newproject.html',form=form,ORGNAME=OrganizationName) + +@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':probject_id}) + if tme is None: + flash('No Current Hours submitted for project {}'.format(probject_id)) + except: + flash("Issue assigning time data for project id {}".format(probject_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/rename/project/<project_id>",methods=["GET","POST"]) +@login_required +def rename_project(project_id): + form = RenameProjectForm() + if form.validate_on_submit(): + pjname = "FE " + str(form.fenumber.data) + ": " + form.newName.data + try: + project = mongo.db.projects_collection.find_one({'_id':project_id}) + except: + flash("Issue finding Project with id {}".format(project_id)) + else: + try: + mongo.db.projects_collection.update_one({'_id':ObjectId(project_id)},{'$set':{'project_name':pjname}}) + except: + flash("Issue setting {} as project name for {}".format(pjname,project_id)) + finally: + return redirect(url_for('project', project_id=project_id)) + return render_template('admin/agreements/projects/update/rename.html',form=form,ORGNAME=OrganizationName) + +@app.route("/admin/update/branch/<collection>/<document_id>",methods=["GET","POST"]) +@login_required +def change_branch(collection,document_id): + form = ChangeBranchForm() + + form.branch.choices = [("",'Select Branch'),("Global","Global")] + for branch in get_available_branches(): + form.branch.choices.append(branch) + + if form.validate_on_submit(): + match collection: + case "project": + mongo.db.projects_collection.update_one({"_id":ObjectId(document_id)},{'$set':{'branch':ObjectId(form.branch.data)}}) + flash("Changed Branch for Project {} to {}".format(document_id,form.branch.data)) + return redirect(url_for('project',project_id=document_id)) + case "user": + mongo.db.user_collection.update_one({"_id":ObjectId(document_id)},{'$set':{'branch':ObjectId(form.branch.data)}}) + flash("Changed Branch for User {} to {}".format(document_id,form.branch.data)) + return redirect(url_for('user',user_id=document_id)) + + return render_template('admin/update/branch.html',form=form,ORGNAME=OrganizationName) + +@app.route("/admin/move/project/<project_id>",methods=["GET","POST"]) +@login_required +def move_project(project_id): + #TODO replace w/ get agreement(s) fn + availableAgreements = [] + for agreement in mongo.db.agreements_collection.find(): + availableAgreements.append((agreement['_id'],agreement['agreement_name'])) + # END + form = MoveProjectForm() + form.newAgreement.choices = availableAgreements # assign the fn as above + if form.validate_on_submit(): + try: + #TODO replace w/ get project(s) fn + project = mongo.db.projects_collection.find_one({'_id':ObjectId(project_id)}) + #END + except: + flash('Issue finding project with id {}'.format(project_id)) + else: + try: + mongo.db.agreements_collection.find_one({'_id':ObjectId(project['agreement'])}) + except: + flash('Issue finding agreement {} for project {}'.format(project['agreement'],project_id)) + else: + # NOTE To refactor the below into a moveProject(s) fn I can pass a list of projects as a variable for the $pull and $push as {'$pull':{'projects'{'$in':varOfProjects}}} etcv This will be clean when I need to move all the projects from an agreement that's in queue for deletement + try: + mongo.db.agreements_collection.update_one({'_id':ObjectId(project['agreement'])},{'$pull':{'projects':ObjectId(project_id)}}) + except: + flash('Issue removing project {} from agreement {}'.format(project_id,project['agreement'])) + else: + try: + mongo.db.agreements_collection.update_one({'_id':ObjectId(form.newAgreement.data)},{'$push':{'projects':ObjectId(project_id)}}) + except: + flash('Issue adding project {} to agreement {}'.format(project_id,form.newAgreement.data)) + else: + mongo.db.projects_collection.update_one({'_id':ObjectId(project_id)},{'$set':{'agreement':form.newAgreement.data}}) + finally: + return redirect(url_for('project', project_id=project_id)) + return render_template('admin/agreements/projects/update/move.html',form=form,ORGNAME=OrganizationName) + +@app.route("/admin/remove/project/<project_id>",methods=["GET","POST"]) +@login_required +def remove_project(project_id): + form = ConfirmRemove() + if form.validate_on_submit(): + try: + #TODO replace w/ get project(s) fn + project = mongo.db.projects_collection.find_one({'_id':ObjectId(project_id)}) + #END + except: + flash('Issue finding project with id {}'.format(project_id)) + else: + try: + mongo.db.agreements_collection.find_one({'_id':ObjectId(project['agreement'])}) + except: + flash('Issue finding agreement for project {}'.format(project_id)) + else: + #NOTE abstract to removeProject(s) fn for mass deletion. Ex {'$pull':{'projects':{'$in':passedListOfDeletableProjects}}} + if mongo.db.agreements_collection.update_one({'_id':ObjectId(project['agreement'])},{'$pull':{'projects':ObjectId(project_id)}}): + mongo.db.projects_collection.delete_one({'_id':ObjectId(project_id)}) + + return redirect(url_for('agreement',agreement_id=project['agreement'])) + + return render_template('admin/confirm_remove.html',form=form,ORGNAME=OrganizationName) +#### #### +####### Knowlegebase Route ####### +#### #### +@app.route("/docs") +@login_required +def knowlegebase(): + return render_template('knowlegebase/index.html',ORGNAME=OrganizationName) + + +# Report Routes +# Agreement Routes +@app.route('/admin/reports/agreement') +@login_required +def agreement_report(): + return render_template('admin/reports/agreement_report.html', ORGNAME=OrganizationName) + +@app.route('/admin/reports/agreements') +@login_required +def project_report(): + 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) + 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' + } + }, + { + "$addFields": { + "totalTime": { "$cond":{ "$if":"$clock_out","$then":{"$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }},"$else":{'Clocked in'}}, + "totalTime": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }, + "lunchCount": { "$sum" : { '$cond':["$lunch",1,0] } }, + "perdiemCount": { "$sum" :{'$cond':["$per_diem",1,0] } } + } + }, + { + "$addFields": { + "totalHoursWorked": {"$subtract":[ "$totalTime", {"$multiply":["$lunchCount", 30 * 60 * 1000]}] } + } + }, + { + "$sort": {'date': -1} + } + ] ) + + by_project = tspp +# by_project = {} +# for project in tspp: +# by_project[project['_id'][0]['project_name']]=project['_id'][0] +# by_project[project['_id'][0]['project_name']]['totalHoursWorked']=project['totalHoursWorked'] +# by_project[project['_id'][0]['project_name']]['lunchCount']=project['lunchCount'] +# by_project[project['_id'][0]['project_name']]['perdiemCount']=project['perdiemCount'] + +# for time in ptl: +# for user in by_user: +# if time['modified_by'][0] in user: +# if by_user[user].get('times'): +# by_user[user]['times'].append(time) +# else: +# by_user[user].update({'times':[time]}) +# for project in by_project: +# if time['project_data'][0]['project_name'] in project: +# if by_project[project].get('times'): +# by_project[project]['times'].append(time) +# else: +# by_project[project].update({'times':[time]}) + + return render_template('admin/reports/project.html',by_project=by_project,tspp=tspp,projectlookup=ptl,ORGNAME=OrganizationName) + +@app.route('/admin/rename/agreement/<agreement_id>',methods=["GET","POST"]) +@login_required +def rename_agreement(agreement_id): + form = RenameAgreementForm() + if form.validate_on_submit(): + try: + agreement = mongo.db.agreements_collection.find_one({'_id':agreement_id}) + except: + flash("Issue finding Agreement with id {}".format(agreement_id)) + return redirect(url_for('rename_agreement',agreement_id=agreement_id)) + else: + try: + mongo.db.agreements_collection.update_one({'_id':ObjectId(agreement_id)},{'$set':{'agreement_name':form.newName.data}}) + except: + flash("Issue setting {} as agreement name for {}".format(form.newName.data,agreement_id)) + return redirect(url_for('rename_agreement',agreement_id=agreement_id)) + else: + return redirect(url_for('agreement', agreement_id=agreement_id)) + return render_template('admin/agreements/update/rename.html',form=form,ORGNAME=OrganizationName) + +@app.route("/admin/remove/agreement/<agreement_id>",methods=["GET","POST"]) +@login_required +def remove_agreement(agreement_id): + #TODO + form = ConfirmRemove() + if form.validate_on_submit(): + try: + #TODO replace with get project(s) fn + agreement = mongo.db.agreements_collection.find_one({'_id':ObjectId(agreement_id)}) + #END + except: + flash('Issue finding agreement id {}'.format(agreement_id)) + else: + #for each project either remove or move + flash('WARNING: This action removes all projects currently associated with this agreement, AND removes all time entries tied to the projects.') +# try: + # for project in agreement['projects'] + + return render_template('admin/agreements/projects/update/move.html',form=form,ORGNAME=OrganizationName)#TODO FIX + +@app.route('/admin/change/agreement/dates',methods=["GET","POST"]) +@login_required +def change_agreement_dates(): + #TODO + return render_template('admin/agreements/projects/update/move.html',form=form,ORGNAME=OrganizationName)#TODO FIX + +# 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) + + everyhours_by_user = mongo.db.time_collection.aggregate([ {'$sort':{'modified_by.0':1,'date':1}} ]) + + everyhours_by_project = mongo.db.time_collection.aggregate([ {'$sort':{'project':1,'modified_by.0':1}} ]) + + return render_template ('admin/reports/total_timedata_report.html', by_user = everyhours_by_user, by_project = everyhours_by_project, 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') + + usertimes = mongo.db.time_collection.aggregate([ + { + "$match": { + "$and":[{"date":{"$gte":begin}},{"date":{"$lt":end}}] + } + }, + { + "$lookup":{ + 'from':'user_collection', + 'localField':'modified_by.0', + 'foreignField':"username", + 'as':'userinfo' + } + }, + { + "$sort":{"userinfo.username":1} + } + ]) + + 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':["$per_diem",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':["$per_diem",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}}] + } + }, + { + "$lookup":{ + 'from':'projects_collection', + 'localField':'project', + 'foreignField':'_id', + 'as':'project_data' + } + }, + { + "$group": { + "_id":"$project_data", + "laborHoursWorked": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }, + "lunchCount": { "$sum" : { '$cond':["$lunch",1,0] } }, + "perdiemCount": { "$sum" :{'$cond':["$per_diem",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( [ + { + "$match": { + "$and":[{"date":{"$gte":begin}},{"date":{"$lt":end}}] + } + }, + { + "$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' + } + }, + { + "$addFields": { + "totalTime": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }, + "lunchCount": { "$sum" : { '$cond':["$lunch",1,0] } }, + "perdiemCount": { "$sum" :{'$cond':["$per_diem",1,0] } } + } + }, + { + "$addFields": { + "totalHoursWorked": {"$subtract":[ "$totalTime", {"$multiply":["$lunchCount", 30 * 60 * 1000]}] } + } + }, + { + "$sort": {'date': -1} + } + ] ) + + by_project = {} + for project in tspp: + by_project[project['_id'][0]['project_name']]=project['_id'][0] + by_project[project['_id'][0]['project_name']]['totalHoursWorked']=project['totalHoursWorked'] + by_project[project['_id'][0]['project_name']]['lunchCount']=project['lunchCount'] + by_project[project['_id'][0]['project_name']]['perdiemCount']=project['perdiemCount'] + + by_user ={} + for user in hours: + by_user[user['_id']]=user + + for time in ptl: + for user in by_user: + if time['modified_by'][0] in user: + if by_user[user].get('times'): + by_user[user]['times'].append(time) + else: + by_user[user].update({'times':[time]}) + for project in by_project: + if time['project_data'][0]['project_name'] in project: + if by_project[project].get('times'): + by_project[project]['times'].append(time) + else: + by_project[project].update({'times':[time]}) + +# for time in ptl: +# if time['modified_by'][0] not in by_user: +# by_user[time['modified_by'][0]]=[time] +# else: +# by_user[time['modified_by'][0]].append(time) +# if time['project_data'][0]['project_name'] not in by_project: +# by_project[time['project_data'][0]['project_name']]=[time] +# else: +# by_project[time['project_data'][0]['project_name']].append(time) + + #return json_util.dumps(by_user) + #return json_util.dumps(by_project) + return render_template ('admin/reports/bound_timedata_report.html', by_project=by_project, by_user=by_user, usertimes=usertimes, allhours=allhours, hours=hours, tspp=tspp, projectlookup=ptl, ORGNAME=OrganizationName) +## Refactoring fn(s) START ## +def get_all_user_times(): + usertimes = mongo.db.time_collection.aggregate([ + { + "$lookup":{ + 'from':'user_collection', + 'localField':'modified_by.0', + 'foreignField':"username", + 'as':'userinfo' + } + }, + { + "$sort":{"userinfo.username":1} + } + ]) + + allhours = mongo.db.time_collection.aggregate( [ + { + "$group": { + "_id":{"_id":'$_id', + "project":"$project"}, + "totalTime": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }, + "lunchCount":{ "$sum":{'$cond':["$lunch",1,0] } }, + "perdiemCount":{ "$sum":{'$cond':["$per_diem",1,0] } } + } + }, + { + "$addFields": { + "totalHoursWorked": {"$subtract":[ "$totalTime", {"$multiply":["$lunchCount", 30 * 60 * 1000]}] } + } + }, + { + "$sort": { "_id": 1 } + } + ] )# Total hours worked + + + hours = mongo.db.time_collection.aggregate( [ + { + "$group": { + "_id": { + "$first":"$modified_by", + }, + "totalTime": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }, + "lunchCount":{ "$sum":{'$cond':["$lunch",1,0] } }, + "perdiemCount":{ "$sum":{'$cond':["$per_diem",1,0] } } + } + }, + { + "$addFields": { + "totalHoursWorked": {"$subtract":[ "$totalTime", {"$multiply":["$lunchCount", 30 * 60 * 1000]}] } + } + }, + { + "$sort": { "_id": 1 } + } + ] )# Total hours worked + tspp = mongo.db.time_collection.aggregate( [ + { + "$lookup":{ + 'from':'projects_collection', + 'localField':'project', + 'foreignField':'_id', + 'as':'project_data' + } + }, + { + "$group": { + "_id":"$project_data", + "laborHoursWorked": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }, + "lunchCount": { "$sum" : { '$cond':["$lunch",1,0] } }, + "perdiemCount": { "$sum" :{'$cond':["$per_diem",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' + } + }, + { + "$addFields": { + "totalTime": { "$sum": { "$subtract": [{"$last":"$clock_out"}, {"$last":"$clock_in"}] } }, + "lunchCount": { "$sum" : { '$cond':["$lunch",1,0] } }, + "perdiemCount": { "$sum" :{'$cond':["$per_diem",1,0] } } + } + }, + { + "$addFields": { + "totalHoursWorked": {"$subtract":[ "$totalTime", {"$multiply":["$lunchCount", 30 * 60 * 1000]}] } + } + }, + { + "$sort": {'date': -1} + } + ] ) + + by_project = {} + for project in tspp: + by_project[project['_id'][0]['project_name']]=project['_id'][0] + by_project[project['_id'][0]['project_name']]['totalHoursWorked']=project['totalHoursWorked'] + by_project[project['_id'][0]['project_name']]['lunchCount']=project['lunchCount'] + by_project[project['_id'][0]['project_name']]['perdiemCount']=project['perdiemCount'] + + by_user ={} + for user in hours: + by_user[user['_id']]=user + + for time in ptl: + for user in by_user: + if time['modified_by'][0] in user: + if by_user[user].get('times'): + by_user[user]['times'].append(time) + else: + by_user[user].update({'times':[time]}) + for project in by_project: + if time['project_data'][0]['project_name'] in project: + if by_project[project].get('times'): + by_project[project]['times'].append(time) + else: + by_project[project].update({'times':[time]}) + return by_user + +## Refartoring fn(s) END ## + +####### 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)] + users.append(user) + hours = {} + deltas=[] + 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)] + 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.now()] + t = time['clock_out'][0] - time['clock_in'][0] + hours['total_time'] = t + times_by_user[time['modified_by'][0]].append(t) + deltas.append(time) + + for user in users: + 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', 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(): + by_user = get_all_user_times() + 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', by_user=by_user, 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.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') +@login_required +def vehicle_report(): + return render_template ('admin/reports/vehicle_report.html', ORGNAME=OrganizationName) + +@app.route('/admin/reports/vehicles/<vehicle>') +@login_required +def vehicle_indepth(): + return render_template ('admin/reports/vehicle_report.html', ORGNAME=OrganizationName) +# Report Routes End + +#====================================# +############# ############## +#### DEVELOPMENT ROUTES #### +############# ############## +#====================================# + +# DEVELOPMENT ROUTES, remove/modify permissions before production + +#### #### +####### Fleet Data Route ####### +#### #### +@app.route("/dev/fleetdata") +@login_required +def fleetdatalist(): + allfleetdata = mongo.db.fleet_collection.find() + return render_template('dev/fleetdata.html', allfleetdata=allfleetdata) + +#### #### +####### User Data Route ####### +#### #### +@app.route("/dev/usrs") +@login_required +def userslist(): + allusers = mongo.db.user_collection.find() + return render_template('dev/usrs.html', allusers=allusers) + +#### #### +####### Time Data Route ####### +#### #### +@app.route("/dev/timedata", methods=['GET']) +@login_required +def timedata(): + alltimedata = mongo.db.time_collection.find() + return render_template('dev/timedata.html', alltimedata=alltimedata) + +#### #### +####### Agreement Data Route ####### +#### #### +@app.route("/dev/agreementdata", methods=['GET']) +@login_required +def agreementdata(): + allagreementdata = mongo.db.agreements_collection.find() + return render_template('dev/agreementdata.html', allagreementdata=allagreementdata) + +#### #### +####### Project Data Route ####### +#### #### +@app.route("/dev/projectdata", methods=['GET']) +@login_required +def projectdata(): + allprojectsdata = mongo.db.projects_collection.find() + return render_template('dev/projectdata.html', allprojectsdata=allprojectsdata) diff --git a/app/static/css/main.css b/app/static/css/main.css @@ -0,0 +1,438 @@ +:root { + --zoning:#ff8b00; + --maincolor:#00b6a6; + --accent:#5a180c; + --widgetbg:#f0f0f0; + --rootbg:#fff; + --progressfill:var(--maincolor); + --totalprogressfill:var(--accent); + --progressbg:var(--zoning); +/* --zoning:#dcd; + --maincolor:#dff; + --accent:#f3f; + --widgetbg:#f0f0f0/*#4f4f4f*/; +/* --rootbg:#dbcfff;/*#aaeeaa*/ +/* --progressfill:var(--maincolor); + --totalprogressfill:var(--accent); + --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(-2px); + 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;} +/* min-height: 100vh; /**/ +a,a.visited,a.hover { + text-decoration:none; + color:#000; +} + +.user-summary{ + padding-top:2em; +} +dl { + text-align:justify; +} + +::selection{ + background: var(--zoning) +} +/********** GLOBAL SET **********/ + /***Printing***/ +@media print { + .pagebreak { + clear:both; + break-after:always !important; + } + header, footer, #doc, .reportswidget, .permissions, .activeusers { + display:none !important; + } + .admin-grid { + display:block !important; + } + .agreements,.admin-content { + padding: 0px !important; + margin: 0px !important; + background-color: #fff !important; + width:100vw !important; + } + @media (max-width:720px),@media (min-width:720px),@media (max-width: 414px),@media (min-width: 415px) { + .pagebreak { + clear:both; + break-after:always !important; + } + header, footer, #doc, .reportswidget, .permissions, .activeusers { + display:none !important; + } + .admin-grid { + display:block !important; + } + .agreements,.admin-content { + padding: 0px !important; + margin: 0px !important; + background-color: #fff !important; + width:100vw !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(-5px); box-shadow: 0 6px 6px 0 rgba(0,0,0,0.2); };*/ +} +.action-button { + border: none; + background: var(--accent); + cursor: pointer; + border-radius: 3px; + padding: 6px; + width:150px; + height:1.2em; + color: white; + box-shadow: 0 3px 6px 0 rgba(0,0,0,0.2); +} +/********** NAVIGATION **********/ +header { + font-size:1.5em; + align-items:center; + justify-items:center; + display:grid; + gap:0.5rem; + padding:0rem 1.5rem 0rem 1.5rem; + margin:1rem 0rem 1rem 0rem; + width:auto; + height:3em; + grid-template-columns: min-content auto min-content; + grid-template-areas: "logo navi logout"; + align-self:center; +} +header #logo { + grid-area:logo; + align-self:center; + } +header #navi { + grid-area:navi; + justify-content:center; + height:100%; + display:grid; + + grid-auto-flow:column; + grid-auto-columns:max-content; + gap:0.5rem; + place-self:center; + + } +header #logout { + grid-area:logout; + justify-self:end; + align-self:center; + } +/*}*/ + +#doc { + position:fixed; + z-index:2; + bottom:1rem; + 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 { + display: grid; + align-items: center; + justify-items: center; + grid-row-start: 1; + grid-row-end: 2; + grid-column-start: 2; + grid-column-end: 3; + 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, .agreement-grid, .project-grid { + padding:5rem; + margin:1rem; + display: grid; + align-items: center; + justify-items: center; + text-align:center; + background-color: var(--widgetbg); + border-radius:.5em; +} +.hours-grid table tr:nth-child(even) { + background-color:var(--maincolor); +} +.hours-grid table,.hours-grid tr,.hours-grid td{ + border:1px; + border-color:var(--accent); + 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) */ +.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{ + border:1px; + 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; + padding:0px; + margin:0px; +} +.activeusers-grid table,.activeusers-grid tr,.activeusers-grid td{ + padding:1rem; +} +.activeusers-grid table tr:nth-child(even) { + background-color:var(--maincolor); +} +.activeusers-grid table td:nth-child(odd) { + text-align:left; +} +.activeusers-grid table td:nth-child(even) { + text-align:right; +} +/********** WIDGETS **********/ +/*section h3 { + max-height: max-content; +}*/ +.permissions, .reportswidget, .activeusers, .agreements, .fleet, .punchclock { + height:max-content; + display: grid; + /*align-items: center;*/ + justify-items: center; + text-align:center; + background-color: var(--widgetbg); + border-radius:.5em; +} +@media (min-width: 415px) { + .punchclock { + height:-webkit-fill-available; + height:-moz-available; + width:-webkit-fill-available; + width:-moz-available; + } + .permissions, .reportswidget, .activeusers, .agreements, .fleet { + height:-webkit-fill-available; + height:-moz-available; + width:-webkit-fill-available; + width:-moz-available; + min-height:40vh; + } +} +@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; + } + .permissions, .reportswidget, .activeusers, .agreements, .fleet { + padding: 2em 5em 3em 5em; + } +} +.project-overlook table tr:nth-child(even), .employee-overlook table tr:nth-child(even) { + background-color:var(--maincolor); +} +.employee-overlook table td:nth-child(odd) { + text-align:left; +} +.employee-overlook table td:nth-child(even) { + text-align:right; +} +/***####### FLEET WIDGET #######***/ +.safetychecks{ + display:block; + text-align:center; +} +/*.safetychecks table tr:nth-child(even) { + background-color:var(--maincolor); +}*/ +.safetychecks table td:nth-child(even) { + text-align:left; +} +.safetychecks table td:nth-child(odd) { + text-align:right; +} +.fleet #notes { + width:100%; +} +/********** END WIDGETS **********/ +/********** PRIMARY LAYOUTS **********/ +.base-grid,.admin-grid { + display: grid; + grid-gap:1em; + margin:1em; +/* place-items:center; + * */ +} +@media (min-width:720px){ + .base-grid { + grid-template-columns: repeat(2, 1fr); + grid-auto-columns: minmax(100px, 600px); + } + .admin-grid { + grid-template-columns:repeat(5,1fr); + grid-auto-rows: auto; + } + + .agreements,.admin-content { + grid-row:1/4; + grid-column:1/ span 4; + } +} +@media (max-width:720px){ + .base-grid { + } + .admin-grid { + } + .agreements,.admin-content { + } +} +/********** Progress display **********/ +.agreements a { + width:100%; +} +.progress { + margin:.5em 1em; + padding:1em; + border-radius:.5em; + background-color:var(--progressbg); + text-align:center; +} +.total-progress { + 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; +} + +/********** DOCUMENTATION **********/ +.documentation-container { + text-align: center; + margin-left: 10vw; + margin-right: 10vw; +} +.documentation-header { + padding-top: 5vh; +} +.intro-documentation { + padding-top: 2vh; +} +.input-example { + width: 50px; +} + +/********** Footer **********/ +footer { + margin-top:4em; + text-align:center; +} +footer img { + width:100%; + z-index:-1; + position:static; +} diff --git a/app/static/imgs/logo.svg b/app/static/imgs/logo.svg @@ -0,0 +1,1575 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + version="1.1" + id="svg1593" + width="5em" + height="5em" + viewBox="0 0 1159.6801 1132.8"> + <metadata + id="metadata1599"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs1597" /> + <g + id="g1601"> + <image + width="100%" + height="100%" + preserveAspectRatio="none" + style="image-rendering:optimizeQuality" + xlink:href="data:image/png;base64,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 +WI9qv6G5AAAAAElFTkSuQmCC +" + id="image1603" /> + </g> +</svg> diff --git a/app/static/imgs/modlogo-for-foot.svg b/app/static/imgs/modlogo-for-foot.svg @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="svg48" + width="120rem" + height="60rem" + viewBox="0 0 183.73836 90.295418" + sodipodi:docname="modlogo-for-foot.svg" + inkscape:version="1.0.2 (e86c870879, 2021-01-15)"> + <metadata + id="metadata1531"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs52"> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath1535"> + <rect + style="opacity:0.87;fill:#000000" + id="rect1537" + width="120rem" + height="60rem" + x="58.525616" + y="137.42026" /> + </clipPath> + </defs> + <sodipodi:namedview + id="namedview50" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="1.9990747" + inkscape:cx="127.20718" + inkscape:cy="31.826676" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="g54" + inkscape:document-rotation="0" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" /> + <g + inkscape:groupmode="layer" + inkscape:label="Image" + id="g54" + transform="translate(-58.525616,-137.42026)"> + <image + width="300" + height="300" + preserveAspectRatio="none" + style="image-rendering:optimizeQuality" + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAS4AAAEnCAYAAAAXT66LAAAABmJLR0QAWgAYAAyQY9L2AAAgAElE QVR42uydd3hU1dbG373PmZJJD6SQEHrNUIaWYEAlIhKxYEOBiBRFKfZ79XqxfNdyEbuoFEURhGDB Qq6KQcQgSmBAIBhmCJ0QSEISQsokmXbO/v6YmWQIAUGKlPV7nnmmZHJmn13es9baa+8DEARBEARB EARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARB EARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARB EARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARB EARBEARBEARBnEMYVQHRmC7LpjPuqAAUByBUWNPeFVQrBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQ BEEQBEEQxDEIgAlKSCYuEqijXq5CJU7S9gxgAGXLExcsMlUBidUxlzGSK4IsLuKCF6vGokXWFkHC RfxtggUwnxCdsgyRaBEkXMQFZ235WVU+dfP/BokWcbHAqQou4asSg0e6mJ9AecXLmAWkej8zZpFo EWRxERcoCVme9o5P8bxfQWJFEARBEARBEARxubIMYMsoLEBcQlBnvgxImABmnd90PCth9lSGoFpA qwcQAKgGCL0W2297juJfBAkXcYGI2K9GhoJ2DEosg6YZIOlFka65sOmjAITAdd11JFgECRdxAYjV j0MYbO0YnNFM1cnYr20luD4WHFxoZT3Krx5IYkWQcBEXgFh98RCDrY4hJBI2TTQOaFuCc4MAVOjl ENQOvorEiiDhIi4Qwfr6QQaHjPKQDiiRY6DlBthVFVBrgevvJLEiSLiIC4vYb2ewQrkTZFkL95Ab SaQIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAI giAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAI giAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAI 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 fgPxCQit2Zx+8QAAAABJRU5ErkJggg== " + id="image56" + clip-path="url(#clipPath1535)" /> + </g> +</svg> diff --git a/app/static/imgs/question-mark-inside-a-circle-svgrepo-com.svg b/app/static/imgs/question-mark-inside-a-circle-svgrepo-com.svg @@ -0,0 +1,9 @@ +<svg fill="#000000" height="3rem" width="3rem" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 29.536 29.536" xml:space="preserve"> + +<g id="SVGRepo_bgCarrier" stroke-width="0"/> + +<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/> + +<g id="SVGRepo_iconCarrier"> <path d="M14.768,0C6.611,0,0,6.609,0,14.768c0,8.155,6.611,14.767,14.768,14.767s14.768-6.612,14.768-14.767 C29.535,6.609,22.924,0,14.768,0z M14.768,27.126c-6.828,0-12.361-5.532-12.361-12.359c0-6.828,5.533-12.362,12.361-12.362 c6.826,0,12.359,5.535,12.359,12.362C27.127,21.594,21.594,27.126,14.768,27.126z"/> <path d="M14.385,19.337c-1.338,0-2.289,0.951-2.289,2.34c0,1.336,0.926,2.339,2.289,2.339c1.414,0,2.314-1.003,2.314-2.339 C16.672,20.288,15.771,19.337,14.385,19.337z"/> <path d="M14.742,6.092c-1.824,0-3.34,0.513-4.293,1.053l0.875,2.804c0.668-0.462,1.697-0.772,2.545-0.772 c1.285,0.027,1.879,0.644,1.879,1.543c0,0.85-0.67,1.697-1.494,2.701c-1.156,1.364-1.594,2.701-1.516,4.012l0.025,0.669h3.42 v-0.463c-0.025-1.158,0.387-2.162,1.311-3.215c0.979-1.08,2.211-2.366,2.211-4.321C19.705,7.968,18.139,6.092,14.742,6.092z"/> </g></g></g> + +</svg> diff --git a/app/templates/admin/agreements/index.html b/app/templates/admin/agreements/index.html @@ -0,0 +1,58 @@ +{% 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"> + <dl> + <dt>{{ agreement['agreement_name'] }}<a href="{{url_for('rename_agreement',agreement_id=agreement['_id'])}}" style="color:var(--accent)"> change</a></dt> + <dd> + {%- for element in agreement['agency'] %} + {{ agreement['agency'] }} + {%- endfor %} + </dd> + <dd>{{ agreement['start_date'] }} - {{ agreement['end_date'] }}<a href="{{url_for('change_agreement_dates')}}" style="color:var(--accent);"> change</a></dd> + <dd> + {% if agreement['contacts'] %} + {% for contact in agreement['contacts'] %} + {{ contact }} + {% endfor %} + {% endif %} + </dd> + <dd><a href="{{url_for('remove_agreement',agreement_id=agreement['_id'])}}" style="color:var(--accent)">remove</a></dd> + </dl> + + + <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 @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% block title %}Add New Agreement{% endblock %} + +{% block content %} +<section class="new-agreement-grid"> + <h3>Add New Agreement</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.agreementName.label }}{{ form.agreementName() }}<br> + {{ form.agency.label }}{{ form.agency() }}<br> + {{ form.startDate.label }}{{ form.startDate() }}<br> + {{ form.endDate.label }}{{ form.endDate() }}<br> + {{ form.createNewAgreement() }} + </form> + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + <p style='color:green'>{{ message }}</p> + {% endfor %} + {% endif %} + {% endwith %} +</section> +{% endblock %} diff --git a/app/templates/admin/agreements/projects/index.html b/app/templates/admin/agreements/projects/index.html @@ -0,0 +1,56 @@ +{% 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'] }}<a href="{{url_for('rename_project', project_id=project['_id'])}}"style="color:var(--accent);"> change</a></dt> + <dd>ID: {{ project['_id'] }}</dd> + <dd>A_ID:{{ project['agreement']}}<a href="{{url_for('move_project',project_id=project['_id'])}}"style="color:var(--accent);"> change</a></dd> + <dd>Branch:{{ project['branch']}}<a href="{{url_for('change_branch',collection='project',document_id=project['_id'])}}"style="color:var(--accent);"> change</a></dd> + <dd>Budget:{{ project['budget']}}</dd> + <dd>Costs:{{ project['costs']}}</dd> + <dd><a href="{{url_for('remove_project',project_id=project['_id'])}}" style="color:var(--accent);">REMOVE</a></dd> + </dl> + + <div class="current-pay-period"> + <table> + <tr> + <th>Date</th> + <th>Employee</th> + <th>Time</th> + <th>In</th> + <th>Out</th> + <th>Lunch</th> + <th>Perdiem</th> + <th>Note</th> + </tr> + {% for entry in payperiod_times %} + <tr> + <td><a href="{{url_for('updateDate',mod_username=current_user.username,timeid=entry._id)}}">{{entry['date'].date().isoformat()}}</a></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><a href="{{url_for('updateStartTime',mod_username=current_user.username,timeid=entry._id)}}">{{entry['clock_in'][-1].time().isoformat(timespec='minutes')}}</a></td> + {% if entry['clock_out'] %}<td><a href="{{url_for('updateEndTime',mod_username=current_user.username,timeid=entry._id)}}">{{entry['clock_out'][-1].time().isoformat(timespec='minutes')}}</a></td>{% else %}<td><a href="{{url_for('updateEndTime',mod_username=current_user.username,timeid=entry._id)}}">Clocked In</a></td>{% endif %} + {% if entry['lunch'] %}<td><a href="{{url_for('toggle_lunch',timeid=entry._id)}}">{{entry['lunch']}}</a></td>{% else %}<td><a href="{{url_for('toggle_lunch',timeid=entry._id)}}"> </a></td>{% endif %}<!-- make this an image of checkbox so can link to toggle route... --> + {% if entry['perdiem'] %}<td><a href="{{url_for('toggle_per_diem',timeid=entry._id)}}">{{entry['perdiem']}}</a></td>{% else %}<td><a href="{{url_for('toggle_per_diem',timeid=entry._id)}}"> </a></td>{% endif %}<!-- make this an image of checkbox so can link to toggle route... --> + <td>{{entry['note']}}</td> + </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 @@ -0,0 +1,40 @@ +{% extends 'base.html' %} + +{% block title %}Add New Project{% endblock %} + +{% block content %} +<section class="new-project-grid"> + <h3>Add New Project</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.fenumber.label }}{{ form.fenumber() }}<br> + {{ form.projectName.label }}{{ form.projectName() }}<br> + {{ form.agreement.label }}{{ form.agreement() }}<br> + {{ form.branch.label }}{{ form.branch() }}<br> + <h4>Budget</h4> + {{ form.laborBudget.label }}{{ form.laborBudget() }}<br> + {{ form.travelBudget.label }}{{ form.travelBudget() }}<br> + {{ form.suppliesBudget.label }}{{ form.suppliesBudget() }}<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> + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + <p style='color:green'>{{ message }}</p> + {% endfor %} + {% endif %} + {% endwith %} +</section> +{% endblock %} diff --git a/app/templates/admin/agreements/projects/update/move.html b/app/templates/admin/agreements/projects/update/move.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block title %}Update Project Parent{% endblock %} + +{% block content %} +<section class="new-agreement-grid"> + <h3>Update Project Parent</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.newAgreement.label }}{{ form.newAgreement() }}<br> + {{ form.moveProject() }} + </form> +</section> +{% endblock %} diff --git a/app/templates/admin/agreements/projects/update/rename.html b/app/templates/admin/agreements/projects/update/rename.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block title %}Update Project Name {% endblock %} + +{% block content %} +<section class="new-agreement-grid"> + <h3>Update Project Name</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.fenumber.label }}{{ form.fenumber() }}<br> + {{ form.newName.label }}{{ form.newName() }}<br> + {{ form.renameProject() }} + </form> +</section> +{% endblock %} diff --git a/app/templates/admin/agreements/update/rename.html b/app/templates/admin/agreements/update/rename.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block title %}Update Agreement Name {% endblock %} + +{% block content %} +<section class="new-agreement-grid"> + <h3>Update Agreement Name</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.newName.label }}{{ form.newName() }}<br> + {{ form.renameAgreement() }} + </form> +</section> +{% endblock %} diff --git a/app/templates/admin/agreements/widget.html b/app/templates/admin/agreements/widget.html @@ -0,0 +1,25 @@ +<section class="agreements"> + <h3>Agreements & Funding</h3> + {% for agreement in agreements %} + <a href="/admin/agreement/{{ agreement._id }}"><div class="progress">{{ agreement.agreement_name }} + <div class="total-progress" style="width:{{ (agreement.total_cost)*100 }}%;">{# {{ agreement.total_cost|round(2,'ceil') }}/{{ agreement.total_budget|round(2, 'ceil') }} | {{ ((agreement.total_cost/agreement.total_budget)*100)|round|int }}% #}</div> + {% for project in agreement['projects'] %} + <div class="progress-bar" style="width:{{ (project.total_cost)*100 }}%;">{{ project['project_name'] }}: {# {{ project.total_cost|round(2,'ceil') }}/{{ project.total_budget|round(2, 'ceil') }} | {{ ((project.total_cost/project.total_budget)*100)|round|int }} #}%</div> + {% endfor %} + </div></a> + {% endfor %} + {#{% for agreement in agreements %} + {{ agreement.agreement_name }}<br> + {{ agreement['budget'] }}<br> + {% for project in agreement['projects'] %} + {{ project['budget'] }}<br> + {{ project['total_budget'] }}<br> + {{ project['costs'] }}<br> + {{ project['total_cost'] }}<br> + {% endfor %} + {{ agreement['costs'] }} + <br><br> + {% endfor %}#} + <a href="{{ url_for('newagreement') }}"><input type="submit" value="Add New Agreement"></a> + <a href="{{ url_for('newproject') }}"><input type="submit" value="Add New Project"></a> +</section> diff --git a/app/templates/admin/bound_timedata_report/widget.html b/app/templates/admin/bound_timedata_report/widget.html @@ -0,0 +1,159 @@ +<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"> + <!--{#<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 Div</br> + {% for elmnt in usors %} + {{elmnt}}</br> + {% endfor %} + + </div>#}--> + <!--{#<div>Projours Div</br> + {% for elmnt in projours %} + {{elmnt}}</br> + {% endfor %} + </div>#}--> + <!--{#<div>Project Lookup Div</br> + {% for elmnt in projectlookup %} + {{elmnt['modified_by'][0]}} + {{elmnt['date']}} + {{elmnt['project_data'][0]['project_name']}} + {{elmnt}}</br></br> + {% endfor %} + </div>#}--> + <div class="pagebreak"><h2>Reports by User</h2></div> + <!-- <div class="pagebreak"></div> --> + {% for user, times in by_user.items() %} + <div class="pagebreak"> + <dl class="user-summary"> + <dt><h3>{{ user }}</h3></dt> + <dd>Total Hours(hrs):<a href="{{url_for('hours',username=user)}}">{{ (times['totalHoursWorked']/(1000*60*60))|round(2) }}</a></dd> + <dd>Total Lunches:<a href="{{url_for('hours',username=user)}}">{{ times['lunchCount'] }}</a></dd> + <dd>Total Per Diem:<a href="{{url_for('hours',username=user)}}">{{ times['perdiemCount'] }}</a></dd> + </dl> + <table> + <tr> + <th>Date</th> + <th>Project</th> + <th>Clocked In</th> + <th>Clocked Out</th> + <th>Lunch</th> + <th>Per Diem</th> + <th>Time(hrs)</th> + </tr> + {% for time in times['times'] %} + <!-- {# <a href="{{url_for('hours',username=time['modified_by'][0])}}#{{time['_id']}}"> #} --> + <tr> + <td><a href="{{url_for('updateDate',mod_username=current_user.username,timeid=time._id)}}">{{time['date'].date().isoformat()}}</a></td> + <td><a href="{{url_for('updateProjectTime',mod_username=current_user.username,timeid=time._id)}}">{{time['project_data'][0]['project_name']}}</a></td> + <td><a href="{{url_for('updateStartTime',mod_username=current_user.username,timeid=time._id)}}">{{time['clock_in'][-1].time().isoformat(timespec="minutes")}}</a></td> + {% if time['clock_out'] is defined %} + <td><a href="{{url_for('updateEndTime',mod_username=current_user.username,timeid=time._id)}}">{{time['clock_out'][-1].time().isoformat(timespec="minutes")}}</a></td> + {% else %} + <td><a href="{{url_for('updateEndTime',mod_username=current_user.username,timeid=time._id)}}">Clocked In</a></td> + {% endif %} + <td><a href="{{url_for('toggle_lunch',timeid=time._id)}}">{{time['lunchCount']}}</a></td> + <td><a href="{{url_for('toggle_per_diem',timeid=time._id)}}">{{time['perdiemCount']}}</a></td> + <td><a href="{{url_for('hours',username=time['modified_by'][0])}}">{{ (time['totalHoursWorked']/(1000*60*60))|round(2) }}</a></td> + </tr> + {% endfor %} + </table></div> + <table> + <tr> + <th>Date</th> + <th>Note</th> + </tr> + {% for item in times['times'] %} + {% if item['note'] %} + <tr> + <td>{{item['date'].date().isoformat()}}</td> + <td>{{item['note']}}</td> + </tr> + {% endif %} + {% endfor %} + </table> + {% endfor %} + + <!-- </section> + <section class="project-overlook"> --> + <div class="pagebreak"><h2>Reports by Project</h2></div> + <!-- <div class="pagebreak"></div> --> + {% for project, times in by_project.items() %} + <div class="pagebreak"> + <dl class="user-summary"> + <dt><h3>{{ project }}</h3></dt> + <dd>Total Hours(hrs):<a href="{{url_for('hours',username='brennentmazur')}}">{{ (times['totalHoursWorked']/(1000*60*60))|round(2) }}</a></dd> + <dd>Total Lunches:<a href="{{url_for('hours',username='brennentmazur')}}">{{ times['lunchCount'] }}</a></dd> + <dd>Total Per Diem:<a href="{{url_for('hours',username='brennentmazur')}}">{{ times['perdiemCount'] }}</a></dd> + </dl> + <table> + <tr> + <th>Date</th> + <th>Employee</th> + <th>Clocked In</th> + <th>Clocked Out</th> + <th>Lunch</th> + <th>Per Diem</th> + <th>Time(hrs)</th> + </tr> + {% for time in times['times'] %} + <tr> + <td>{{time['date'].date().isoformat()}}</td> + <td>{{time['modified_by'][0]}}</td> + <td>{{time['clock_in'][-1].time().isoformat(timespec="minutes")}}</td> + {% if time['clock_out'] is defined %} + <td>{{time['clock_out'][-1].time().isoformat(timespec="minutes")}}</td> + {% else %} + <td>Clocked In</td> + {% endif %} + <td>{{time['lunchCount']}}</td> + <td>{{time['perdiemCount']}}</td> + <td><a href="{{url_for('hours',username=time['modified_by'][0])}}">{{ (time['totalHoursWorked']/(1000*60*60))|round(2) }}</a></td> + </tr> + {% endfor %} + </table></div> + <table> + <tr> + <th>Date</th> + <th>Employee</th> + <th>Note</th> + </tr> + {% for item in times['times'] %} + {% if item['note'] %} + <tr> + <td>{{item['date'].date().isoformat()}}</td> + <td>{{item['modified_by'][0]}}</td> + <td>{{item['note']}}</td> + </tr> + {% endif %} + {% endfor %} + </table> + {% endfor %} + + </section> +</section> diff --git a/app/templates/admin/confirm_remove.html b/app/templates/admin/confirm_remove.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} + +{% block title %}Confirm{% endblock %} + +{% block content %} +<section class="new-agreement-grid"> + <h3>WARNING:Are you sure you want to remove this asset?</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.confirm() }} + </form> +</section> +{% endblock %} diff --git a/app/templates/admin/employee_report/index.html b/app/templates/admin/employee_report/index.html @@ -0,0 +1,29 @@ +{% 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> + {% for user in by_user %} + {{ user }} + {% endfor %} + <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/employee_report/widget.html b/app/templates/admin/employee_report/widget.html @@ -0,0 +1,53 @@ +<section class="agreements"> + <section class="employee-overlook"> + <h3>Employee Overlook</h3> + {% for user in by_user %} + {{ user }} + {% endfor %} + <div class="usercard"> + <h4>{{ 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> + <tr><td>Pay Period</td><td>{{ user.pay_period }}</td></tr> + <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> </td>{% endif %} + {% if entry['perdiem'] %}<td>{{entry['perdiem']}}</td>{% else %}<td> </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/layout.html b/app/templates/admin/layout.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block title %}Admin{% 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="admin-grid"> + <!-- returned values from admin check is array of permissive ACCESS else return 'missing permissions response' --> + {%- for x in permissions %} + {% include 'admin/'~x~'/widget.html' %} + {%- else-%} + {{ 'You do not have permission to access this page' }} + {%- endfor %} + </section> +{% endblock %} diff --git a/app/templates/admin/pay_period_report/widget.html b/app/templates/admin/pay_period_report/widget.html @@ -0,0 +1,32 @@ +<section class="agreements"> + <h3>Pay-Period Overlook</h3> + {% for user in users %} + {% if user.pay_period == 'hourly' %} + <!-- Hourly --> + <a href="{{ url_for('employee_report',username=user.username) }}"> + <div class="usercard"> + <h4>{{ user.fname }} {{ user.mname }}. {{ user.lname }}</h3> + <table> + <tr><td>Estimated Payment:</td><td>${{ ((user.pay_value)*(user.total_hours.0 +((user.total_hours.1 /60)|round(2) )))|round(2) }}</td></tr> <!-- Multiply hours by pay-value --> + <tr><td>Total Time:</td><td>{{user.total_hours.0}}hrs {{user.total_hours.1}}min.</td></tr> + <tr><td>Pay Rate:</td><td>${{ user.pay_value }}</td></tr> + <tr><td>Pay Period</td><td>Hourly</td></tr> + </table> + </div> + </a> + {% else %} + <!-- Salaried --> + <a href="{{ url_for('employee_report',username=user.username) }}"> + <div class="usercard"> + <h4>{{ user.fname }} {{ user.mname }}. {{ user.lname }}</h3> + <table> + <tr><td>Estimated Payment:</td><td>${{ (user.pay_value / 24)|round(2) }}</td></tr> <!-- Multiply hours by pay-value --> + <!-- <tr><td>Total Time:</td><td>{{user.total_hours}}.</td></tr> --> + <tr><td>Pay Rate:</td><td>${{ user.pay_value }}</td></tr> + <tr><td>Pay Period</td><td>Salaried</td></tr> + </table> + </div> + </a> + {% endif %} + {% endfor %} +</section> diff --git a/app/templates/admin/reports/agreement_report.html b/app/templates/admin/reports/agreement_report.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block title %}Agreement 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','agreement_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/reports/bound_timedata_report.html b/app/templates/admin/reports/bound_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','bound_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/reports/employee_report.html b/app/templates/admin/reports/employee_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','employee_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/reports/pay_period_report.html b/app/templates/admin/reports/pay_period_report.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% 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'] %} + {% 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/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/reports/vehicle_report.html b/app/templates/admin/reports/vehicle_report.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block title %}Vehicle 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','employee_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/reports/widget.html b/app/templates/admin/reports/widget.html @@ -0,0 +1,6 @@ +<section class="reportswidget"> + <h3>Reports</h3> + <a href="{{ url_for('select_date_range') }}"><input type="submit" value="Generate"></a> + <a href="{{ url_for('project_report') }}"><input type="submit" value="Agreements"></a> + <a href="{{ url_for('report_employees') }}"><input type="submit" value="Employees"></a> +</section> diff --git a/app/templates/admin/roles/index.html b/app/templates/admin/roles/index.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block title %}Employees/Crew/Roles{% endblock %} + +{% block content %} +<table> <tr><th>Name</th><th>Role</th><th>Active Employee</th></tr> + <tr> + <td><input type="button" onClick="alert('route to /user<userid>')" value="Brennen T. Mazur"></td><td><select><option value="Crew Lead" selected>Crew Lead</option><option value="Crew">Crew</option><option value="Accounting">Accounting</option><option value="Admin">Admin</option></select></td><td><input type="checkbox" name="activeEmployee" checked></td> + </tr> + <tr> + <td><input type="button" onClick="alert('route to /user<userid>')" value="Nikolas Mazur"></td><td><select><option value="Crew" selected>Crew</option><option value="Crew Lead">Crew Lead</option><option value="Accounting">Accounting</option><option value="Admin">Admin</option></select></td><td><input type="checkbox" name="activeEmployee"></td> + </tr> + <tr><td><input type="button" onClick="alert('route to /newuser')" value="[new Employee]"</td></tr> + +{% endblock %} diff --git a/app/templates/admin/roles/updateroles.html b/app/templates/admin/roles/updateroles.html @@ -0,0 +1,51 @@ +{% extends 'base.html' %} + +{% block title %}Current Activated Users{% endblock %} + +{% block content %} +<section class="role-permissions"> +<table> + <tr> + <th>User Role</th> + {% for field in dashform %} + <th>{{ field.label }}</th> + {% endfor %} + </tr> + <form action="" method="post" novalidate> + {{ dashform.hidden_tag() }} + {% for error in dashform.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {% for role in ['Crew','Crew Lead','Project Manager','Developer','Accountant'] %} + <tr><td>{{ role }}</td> + {% for field in dashform %} + <td>{{ field }}</td> + {% endfor %} + </tr> + {% endfor %} + </form> +</table> + +<table> + <tr> + <th>Admin Roles</th> + {% for field in admnform %} + <th>{{ field.label }}</th> + {% endfor %} + </tr> + <form action="" method="post" novalidate> + {{ admnform.hidden_tag() }} + {% for error in admnform.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {% for role in ['Crew','Crew Lead','Project Manager','Developer','Accountant'] %} + <tr><td>{{ role }}</td> + {% for field in admnform %} + <td>{{ field }}</td> + {% endfor %} + </tr> + {% endfor %} + </form> +</table> +</section> +{% endblock %} diff --git a/app/templates/admin/roles/widget.html b/app/templates/admin/roles/widget.html @@ -0,0 +1,6 @@ +<section class="permissions"><!-- did not change css tag yet... --> + <h3>Permissions by</h3> + <input type="submit" value="Roles"> + <input type="submit" value="Employee"> + <!-- <input type="submit" value="Page"> --> +</section> 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,140 @@ +<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"> + <!--{#<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 Div</br> + {% for elmnt in usors %} + {{elmnt}}</br> + {% endfor %} + + </div>#}--> + <!--{#<div>Projours Div</br> + {% for elmnt in projours %} + {{elmnt}}</br> + {% endfor %} + </div>#}--> + <!--{#<div>Project Lookup Div</br> + {% for elmnt in projectlookup %} + {{elmnt['modified_by'][0]}} + {{elmnt['date']}} + {{elmnt['project_data'][0]['project_name']}} + {{elmnt}}</br></br> + {% endfor %} + </div>#}--> + <div class="pagebreak"><h2>Reports by User</h2></div> + <!-- <div class="pagebreak"></div> --> + {% for user in by_user %} + {{ user }} + {% endfor %} + + {# + {% for user, times in by_user %} + <div class="pagebreak"> + <dl class="user-summary"> + <dt><h3>{{ user }}</h3></dt> + <dd>Total Hours(hrs):<a href="{{url_for('hours',username=user)}}">{{ (times['totalHoursWorked']/(1000*60*60))|round(2) }}</a></dd> + <dd>Total Lunches:<a href="{{url_for('hours',username=user)}}">{{ times['lunchCount'] }}</a></dd> + <dd>Total Per Diem:<a href="{{url_for('hours',username=user)}}">{{ times['perdiemCount'] }}</a></dd> + </dl> + <table> + <tr> + <th>Date</th> + <th>Project</th> + <th>Clocked In</th> + <th>Clocked Out</th> + <th>Lunch</th> + <th>Per Diem</th> + <th>Time(hrs)</th> + </tr> + {% for time in times['times'] %} + <tr> + <td>{{time['date'].date().isoformat()}}</td> + <td>{{time['project_data'][0]['project_name']}}</td> + <td>{{time['clock_in'][-1].time().isoformat(timespec="minutes")}}</td> + {% if time['clock_out'] is defined %} + <td>{{time['clock_out'][-1].time().isoformat(timespec="minutes")}}</td> + {% else %} + <td>Clocked In</td> + {% endif %} + <td>{{time['lunchCount']}}</td> + <td>{{time['perdiemCount']}}</td> + <td><a href="{{url_for('hours',username=time['modified_by'][0])}}">{{ (time['totalHoursWorked']/(1000*60*60))|round(2) }}</a></td> + </tr> + {% endfor %} + </table></div> + {% endfor %} + #} + + <!-- </section> + <section class="project-overlook"> --> + <div class="pagebreak"><h2>Reports by Project</h2></div> + <!-- <div class="pagebreak"></div> --> + {% for project in by_project %} + {{ project }} + {% endfor %} + + {# + {% for project, times in by_project.items() %} + <div class="pagebreak"> + <dl class="user-summary"> + <dt><h3>{{ project }}</h3></dt> + <dd>Total Hours(hrs):<a href="{{url_for('hours',username='brennentmazur')}}">{{ (times['totalHoursWorked']/(1000*60*60))|round(2) }}</a></dd> + <dd>Total Lunches:<a href="{{url_for('hours',username='brennentmazur')}}">{{ times['lunchCount'] }}</a></dd> + <dd>Total Per Diem:<a href="{{url_for('hours',username='brennentmazur')}}">{{ times['perdiemCount'] }}</a></dd> + </dl> + <table> + <tr> + <th>Date</th> + <th>Employee</th> + <th>Clocked In</th> + <th>Clocked Out</th> + <th>Lunch</th> + <th>Per Diem</th> + <th>Time(hrs)</th> + </tr> + {% for time in times['times'] %} + <tr> + <td>{{time['date'].date().isoformat()}}</td> + <td>{{time['modified_by'][0]}}</td> + <td>{{time['clock_in'][-1].time().isoformat(timespec="minutes")}}</td> + {% if time['clock_out'] is defined %} + <td>{{time['clock_out'][-1].time().isoformat(timespec="minutes")}}</td> + {% else %} + <td>Clocked In</td> + {% endif %} + <td>{{time['lunchCount']}}</td> + <td>{{time['perdiemCount']}}</td> + <td><a href="{{url_for('hours',username=time['modified_by'][0])}}">{{ (time['totalHoursWorked']/(1000*60*60))|round(2) }}</a></td> + </tr> + {% endfor %} + </table></div> + {% endfor %} + #} + + </section> +</section> diff --git a/app/templates/admin/update/branch.html b/app/templates/admin/update/branch.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block title %}Update Branch{% endblock %} + +{% block content %} +<section class="new-agreement-grid"> + <h3>Update Branch</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.branch.label }}{{ form.branch() }}<br> + {{ form.changeBranch() }} + </form> +</section> +{% endblock %} diff --git a/app/templates/admin/users/active.html b/app/templates/admin/users/active.html @@ -0,0 +1,33 @@ +{% extends 'base.html' %} + +{% block title %}All Active Users{% 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="activeusers-grid"> + {%- for user in activeusers %} + <div class="usercard"> + <h3>{{ user.fname }} {{ user.mname }} {{ user.lname }}</h3> + <table> + <tr><td>Username</td><td>{{ user.username }}</td></tr> + <tr><td>Password</td><td><a href="{{url_for('chgpass_by_uid',uid=user._id)}}">Reset</a></td></tr> + <tr><td>Birthday</td><td><a href="{{url_for('update_user',update_t='birthday',username=user.username)}}">{{ user.birthday }}</a></td></tr> + <tr><td>Role</td><td><a href="{{url_for('update_user',update_t='role',username=user.username)}}">{{ user.role }}</a></td></tr> + <tr><td>Branch</td><td><a href="{{url_for('update_user',update_t='branch',username=user.username)}}">{{ user.branch }}</a></td></tr> + <tr><td>Phone Number</td><td><a href="{{url_for('update_user',update_t='phonenumber',username=user.username)}}">{{ user.phonenumber }}</a></td></tr> + <tr><td>Address</td><td><a href="{{url_for('update_user',update_t='address',username=user.username)}}">{{ user.address }}</a></td></tr> + <tr><td>Email</td><td><a href="{{url_for('update_user',update_t='email',username=user.username)}}">{{ user.email }}</a></td></tr> + <tr><td>Pay Value</td><td><a href="{{url_for('update_user',update_t='payValue',username=user.username)}}">{{ user.pay_value }}</a></td></tr> + <button><a href="{{ url_for('deactivate_user',userid=user._id) }}">Deactivate User</a></button> + </table> + </div> + {%- endfor %} +</section> +{% endblock %} diff --git a/app/templates/admin/users/inactive.html b/app/templates/admin/users/inactive.html @@ -0,0 +1,32 @@ +{% extends 'base.html' %} + +{% block title %}All Inactive Users{% 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="activeusers-grid"> + {%- for user in inactiveusers %} + <div class="usercard"> + <h3>{{ user.fname }} {{ user.mname }} {{ user.lname }}</h3> + <table> + <tr><td>Username</td><td>{{user.username}}</td></tr> + <tr><td>Password</td><td><a href="{{url_for('chgpass_by_uid',uid=user._id)}}">Reset</a></td></tr> + <tr><td>Birthday</td><td><a href="{{url_for('update_user',update_t='birthday',username=user.username)}}">{{ user.birthday }}</a></td></tr> + <tr><td>Role</td><td><a href="{{url_for('update_user',update_t='role',username=user.username)}}">{{ user.role }}</a></td></tr> + <tr><td>Branch</td><td><a href="{{url_for('update_user',update_t='branch',username=user.username)}}">{{ user.branch }}</a></td></tr> + <tr><td>Phone Number</td><td><a href="{{url_for('update_user',update_t='phonenumber',username=user.username)}}">{{ user.phonenumber }}</a></td></tr> + <tr><td>Address</td><td><a href="{{url_for('update_user',update_t='address',username=user.username)}}">{{ user.address }}</a></td></tr> + <tr><td>Email</td><td><a href="{{url_for('update_user',update_t='email',username=user.username)}}">{{ user.email }}</a></td></tr> + <tr><td>Pay Value</td><td><a href="{{url_for('update_user',update_t='payValue',username=user.username)}}">{{ user.pay_value }}</a></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/admin/users/newuser.html b/app/templates/admin/users/newuser.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} + +{% block title %}Add New Employee{% endblock %} + +{% block content %} +<section class="new-user-grid"> + <h3>Add New Employee</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.fname.label }}{{ form.fname() }}<br> + {{ form.mname.label }}{{ form.mname(size=1) }}<br> + {{ form.lname.label }}{{ form.lname() }}<br> + {{ form.birthday.label }}{{ form.birthday() }}<br> + {{ form.role.label }}{{ form.role() }}<br> + {{ form.address.label }}{{ form.address() }}<br> + {{ form.branch.label }}{{ form.branch() }}<br> + {{ form.phonenumber.label }}{{ form.phonenumber() }}<br> + {{ form.email.label }}{{ form.email() }}<br> + {{ form.payPeriod.label }}{{ form.payPeriod() }}<br> + {{ form.payValue.label }}{{ form.payValue() }}<br> + {{ form.setActive() }}{{ form.setActive.label }}<br> + {{ form.createNewUser() }} + </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/admin/users/update_user.html b/app/templates/admin/users/update_user.html @@ -0,0 +1,37 @@ +{% extends 'base.html' %} + +{% block title %}Update Employee{% endblock %} + +{% block content %} +<section class="new-user-grid"> + <h3>Update {{user.fname}}'s {{update_t}}</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 %} + {% if update_t == "fname" %}{{ form.fname.label }}{{ form.fname() }}{% endif %} + {% if update_t == "mname" %}{{ form.mname.label }}{{ form.mname(size=1) }}{% endif %} + {% if update_t == "lname" %}{{ form.lname.label }}{{ form.lname() }}{% endif %} + {% if update_t == "birthday" %}{{ form.birthday.label }}{{ form.birthday() }}{% endif %} + {% if update_t == "role" %}{{ form.role.label }}{{ form.role() }}{% endif %} + {% if update_t == "address" %}{{ form.address.label }}{{ form.address() }}{% endif %} + {% if update_t == "branch" %}{{ form.branch.label }}{{ form.branch() }}{% endif %} + {% if update_t == "phonenumber" %}{{ form.phonenumber.label }}{{ form.phonenumber() }}{% endif %} + {% if update_t == "email" %}{{ form.email.label }}{{ form.email() }}{% endif %} + {% if update_t == "payPeriod" %}{{ form.payPeriod.label }}{{ form.payPeriod() }}{% endif %} + {% if update_t == "payValue" %}{{ form.payValue.label }}{{ form.payValue() }}{% endif %} + {{ form.modUser() }} + </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/admin/users/widget.html b/app/templates/admin/users/widget.html @@ -0,0 +1,6 @@ +<section class="activeusers"> + <h3>Users</h3> + <a href="{{ url_for('activeusers') }}"><input type="submit" value="Active"></a> <!--sends active tag to route changes available users in dropdown --> + <a href="{{ url_for('inactiveusers') }}"><input type="submit" value="Inactive"></a> <!-- sends inactive tag to route for available users dropdown --> + <a href="{{ url_for('newuser') }}"><input type="submit" value="New"></a> <!-- separate newuser.html pg? --> +</section> diff --git a/app/templates/admin/vehicle_report/widget.html b/app/templates/admin/vehicle_report/widget.html @@ -0,0 +1,14 @@ +<section class="agreements"> + <h3>Vehicle Overlook</h3> + <a href="/admin/agreement" onClick="alert('route to /admin/agreement<id>')"><div class="progress">Agreement Bid + <div class="total-progress" style="width:51.71%;">10342/20000</div> + <div class="progress-bar" style="width:5.7%;">Project1:342/6000</div> + <div class="progress-bar" style="width:85.71%;">Project2:6000/7000</div> + <div class="progress-bar" style="width:57.14%;">Project3:4000/7000</div> + </div></a> + <a href="/admin/agreement" onClick="alert('route to /admin/agreement<id>')"><div class="progress">Agreement Bid 2 + <div class="total-progress" style="width:30%;">10000/30000</div> + <div class="progress-bar" style="width:28.94%;">Project1:4342/15000</div> + <div class="progress-bar" style="width:37.72%;">Project3:5658/15000</div> + </div></a> +</section> diff --git a/app/templates/base.html b/app/templates/base.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + {% if title %} + <title>{% block title %} {% endblock %} - {{ ORGNAME }}</title> + {% else %} + <title>{{ ORGNAME }}</title> + {% endif %} + <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}"> + <meta name="viewport" content="width=device-width,initial-scale=1" /> +</head> +<body onload="startTime()"> +<!-- BEGIN NAVIGATION BAR --> + <header> + <!-- BEGIN logo --> + <a id="logo" href="{{ url_for('dashboard') }}"><img src="{{ url_for('static', filename='imgs/logo.svg') }}" /></a> + <!-- END logo --> + <!-- BEGIN navigation/navi logic --> + {% if current_user.is_authenticated %} + <!-- if userHasPermission(current_user.role) [admin] | if route is admin [dashboard] else [empty space]-->{% if adminNav == True %} + <a href="{{ url_for('admin') }}"><div id="navi">Admin</div></a> + {% else %} + <a href="{{ url_for('dashboard') }}"><div id="navi">Dashboard</div></a> + {% endif %} + {% else %} + <div id="navi"></div> + {% endif %} + <!-- END navigation/navi logic --> + <!-- BEGIN login/logout logic --> + {% if current_user.is_anonymous == True %} + <a href="{{ url_for('login') }}"><div id="logout">Login</div></a> + {% else %} + <a href="{{ url_for('logout') }}"><div id="logout">Logout</div></a> + {% endif %} + <!-- END login/logout logic --> + </header> +<!-- END NAVIGATION BAR --> +<!-- BEGIN DOCUMENTATION BUTTON --> + <div id="doc"><a href="/docs"><svg fill="var(--accent)" height="2rem" width="2rem" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 29.536 29.536" xml:space="preserve"><g stroke-linecap="round" stroke-linejoin="round"/><path d="M14.768,0C6.611,0,0,6.609,0,14.768c0,8.155,6.611,14.767,14.768,14.767s14.768-6.612,14.768-14.767 C29.535,6.609,22.924,0,14.768,0z M14.768,27.126c-6.828,0-12.361-5.532-12.361-12.359c0-6.828,5.533-12.362,12.361-12.362 c6.826,0,12.359,5.535,12.359,12.362C27.127,21.594,21.594,27.126,14.768,27.126z"/> <path d="M14.385,19.337c-1.338,0-2.289,0.951-2.289,2.34c0,1.336,0.926,2.339,2.289,2.339c1.414,0,2.314-1.003,2.314-2.339 C16.672,20.288,15.771,19.337,14.385,19.337z"/> <path d="M14.742,6.092c-1.824,0-3.34,0.513-4.293,1.053l0.875,2.804c0.668-0.462,1.697-0.772,2.545-0.772 c1.285,0.027,1.879,0.644,1.879,1.543c0,0.85-0.67,1.697-1.494,2.701c-1.156,1.364-1.594,2.701-1.516,4.012l0.025,0.669h3.42 v-0.463c-0.025-1.158,0.387-2.162,1.311-3.215c0.979-1.08,2.211-2.366,2.211-4.321C19.705,7.968,18.139,6.092,14.742,6.092z"/></svg></a></div> +<!-- END DOCUMENTATION BUTTON --> + <section class="appview"> + {% block content %} {% endblock %} + </section> + + <footer> + Engineered with ❤ for {{ ORGNAME }} with Sawtooth Systems + </footer> +{% if current_user.is_authenticated %} +<script> +function startTime() { + const today = new Date(); + let h = today.getHours(); + let m = today.getMinutes(); + let s = today.getSeconds(); + m = checkTime(m); + s = checkTime(s); + document.getElementById('clock').innerHTML = h + ":" + m + ":" + s; + setTimeout(startTime, 1000); +} + +function checkTime(i) { + if (i < 10) {i = "0" + i}; // add zero in front of numbers < 10 + return i; + } +</script> +{% endif %} +</body> +</html> + diff --git a/app/templates/dashboard/activeusers/widget.html b/app/templates/dashboard/activeusers/widget.html @@ -0,0 +1,48 @@ +<section class="activeusers"> + <h3>Clocked in Crew List</h3> + {# + {% for user in clocked_in_users %} + {%- print(user.modified_by.0) %} + {% endfor %} + #} + <form> + <!-- <input type="submit" value="Clock Crew Out"> --> + <table><!-- replace w/ function getUserHours(username) --> + {% for user in clocked_in_users %} + <tr> + <td><a href="{{ url_for('hours',username=user.modified_by.0) }}" class="action-button">{{ user.modified_by.0 }}</a></td> + <td><a href="{{ url_for('toggle_lunch',timeid=user._id) }}"> + {% if user.lunch %} + <input type="checkbox" name="lunch" checked><label for="lunch">Lunch</label> + {% else %} + <input type="checkbox" name="lunch"><label for="lunch">Lunch</label> + {% endif %} + </a></td> + <td><a href="{{ url_for('toggle_per_diem',timeid=user._id) }}"> + {% if user.per_diem %} + <input type="checkbox" name="per_diem" checked><label for="per_diem">Per Diem</label> + {% else %} + <input type="checkbox" name="per_diem"><label for="per_diem">Per Diem</label> + {% endif %} + </a></td> + <td><a href="{{ url_for('hours',username=user.modified_by.0) }}" class="action-button">{{ user.clock_in.0.isoformat(timespec='minutes') }}</a></td><!-- can format/display non-military time with format %I:%M%p removed.time() after index to try and show date value--> + <td><a href="{{ url_for('clockout_by_id',modusernm=current_user.username,timeid=user._id) }}" class="action-button">Clock Out</a></td> + </tr> + {% endfor %} + </table> + </form> + <a href="{{ url_for('clockin_new_user') }}" class="action-button">Clock in User</a> + <a href="{{ url_for('new_user_time') }}" class="action-button">New User Entry</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> + <tr> + <td>{{ crewform.userSel.label }} {{ crewform.userSel() }}</td> + <td>{{ crewform.projectSel.label }} {{ crewform.projectSel() }}</td> + <td>{{ crewform.time.label }} {{ crewform.time() }}</td> + <td><button type="submit"><a href="{{ url_for('clockin_by_id', modusernm=current_user.username, intime=crewform.time.data, project=crewform.projectSel.data, usertoinid=crewform.userSel.data) }}">Clock In</a></button></td> + </tr> + </table> + </form> +--> +</section> diff --git a/app/templates/dashboard/fleet/widget.html b/app/templates/dashboard/fleet/widget.html @@ -0,0 +1,23 @@ +<section class="fleet"> + <h3>Fleet</h3> + <form class="widget-form" action="" method="POST" novalidate> + {% for error in fleetoutform.errors or fleetinform.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {% if not fleetCheckedOut %} + <div class="safetychecks"> + <table> + {% for field in fleetoutform %} + <tr><td>{{ field }}</td><td>{% if field.widget.input_type != 'submit' and field.widget.input_type != 'hidden' %}{{ field.label }}</td></tr>{% endif %} + {% endfor %} + </table> + </div> + {% elif fleetCheckedOut %} + <h5> You have {{ vehicle_name }} checked out</h5> + {% for field in fleetinform %} + {{ field }}{% if field.widget.input_type != 'submit' and field.widget.input_type != 'hidden' %}{{ field.label }}{% endif %}<br> + {% endfor %} + </div> + {% endif %} + </form> +</section> diff --git a/app/templates/dashboard/layout.html b/app/templates/dashboard/layout.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} + +{% block title %}Dashboard{% 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="base-grid"> + <!-- for loop takes current user's dashboard permissions and iterates through to include Accessable widgets, else reports lack of permissions --> + {%- for x in permissions %} + {% include '/dashboard/'~x~'/widget.html' %} + {%- else %} + {{ 'You do not have permission to access this page' }} + {%- endfor %} + + </section> +{% endblock %} diff --git a/app/templates/dashboard/punchclock/all.html b/app/templates/dashboard/punchclock/all.html @@ -0,0 +1,42 @@ +{% extends 'base.html' %} + +{% block title %}Hours{% endblock %} + +{% block content %} + <section class="hours-grid"> + <h3>{{ user.fname }} {{ user.lname }}</h3><!-- IF logged in user has permission allow this username section to be a dropdown for modifying user time sheets. --> + <h1 id="clock"></h1> + <table> + {% for event in hours %} + <tr> + {% for entry in event %} + <td>{{ event[entry] }}</br></td> + {% endfor %} + </tr> + {# <td><a href="{{url_for('updateDate',mod_username=current_user.username,timeid=entry._id)}}">{{ entry.date.date().isoformat() }}</a></td> + <td><a href="{{url_for('updateProjectTime',mod_username=current_user.username,timeid=entry._id)}}">{{ entry.project[0]['project_name'] }}</a></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><a href="{{url_for( 'removetime',timeid=entry._id) }}" class="action-button">Remove</a></td> + #} + </tr> + {% endfor %} + </table> + </form> + </section> +{% endblock %} diff --git a/app/templates/dashboard/punchclock/index.dev.html b/app/templates/dashboard/punchclock/index.dev.html @@ -0,0 +1,67 @@ +{% extends 'base.html' %} + +{% block title %}Hours{% 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="hours-grid"> + {{ cd[2] }} + {{ cd[1] }} + {{ bd[2] }} + {{ bd[1] }} + {{ current_user.birthday }} + <h1 id="clock"></h1> + <div><!-- abstract to payPeriod() --> + <h6>$payperiod range</h6> + <!--<h5>Total:{# {{ statement_hours }} #}</h5> --> + </div> + <form action="" method="POST" novalidate> + <table><tr> + + {% for entry in hours %} + <tr> + <td><a href="{{url_for('updateDate',mod_username=current_user.username,timeid=entry._id)}}">{{ entry.date.date().isoformat() }}</a></td> + {% if entry.project[0] %} + <td><a href="{{url_for('updateProjectTime',mod_username=current_user.username,timeid=entry._id)}}">{{ entry.project[0]['project_name'] }}</a></td> + {% else %} + <td><a href="{{url_for('updateProjectTime',mod_username=current_user.username,timeid=entry._id)}}">Project not Found</a></td> + {% endif %} + <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><a href="{{url_for( 'removetime',timeid=entry._id) }}" class="action-button">Remove</a></td> + {#{{ form.hidden_tag() }}#} + </tr> + {# {{entry}} #} + {% endfor %} + </table> + </form> + </section> + {# + {% for time in tspp %} + {{ time }} + </br> + {% endfor %} + #} +{% endblock %} diff --git a/app/templates/dashboard/punchclock/index.html b/app/templates/dashboard/punchclock/index.html @@ -0,0 +1,84 @@ +{% extends 'base.html' %} + +{% block title %}Hours{% endblock %} + +{% block content %}{# change table to div structure https://css-tricks.com/complete-guide-table-element/ #} + <section class="hours-grid"> + <h3>{{ user.fname }} {{ user.lname }}</h3><!-- IF logged in user has permission allow this username section to be a dropdown for modifying user time sheets. --> + <h1 id="clock"></h1> + <div><!-- abstract to payPeriod() --> + <h6>$payperiod range</h6> + <!--<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' 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> + <td><a href="{{url_for('updateDate',mod_username=current_user.username,timeid=entry._id)}}">{{ entry.date.date().isoformat() }}</a></td> + {% if entry.project[0] %} + <td><a href="{{url_for('updateProjectTime',mod_username=current_user.username,timeid=entry._id)}}">{{ entry.project[0]['project_name'] }}</a></td> + {% else %} + <td><a href="{{url_for('updateProjectTime',mod_username=current_user.username,timeid=entry._id)}}">Project not Found</a></td> + {% endif %} + <td><a href="{{url_for('updateStartTime',mod_username=current_user.username,timeid=entry._id)}}">{{ entry.clock_in[-1].time().isoformat(timespec='minutes') }}</a></td> + {% if entry.clock_out %} + <td><a href="{{url_for('updateEndTime',mod_username=current_user.username,timeid=entry._id)}}">{{ entry.clock_out[-1].time().isoformat(timespec='minutes') }}</a></td> + {% else %} + <td><a href="{{url_for('updateEndTime',mod_username=current_user.username,timeid=entry._id)}}">Clocked In</a></td> + {% endif %} + {% 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> + <td><a href="{{url_for( 'removetime',timeid=entry._id) }}" class="action-button">Remove</a></td> + </tr> + <tr> + {% endif %} + {% if entry.note %} + <td colspan='6'><a href="{{url_for('updateNote',mod_username=current_user.username,timeid=entry._id)}}">{{ entry.note }}</a></td> + {% else %} + <td colspan='6'><a href="{{url_for('updateNote',mod_username=current_user.username,timeid=entry._id)}}">Add Note</a></td> + {% endif %} + </tr> + {# {{entry}} #} + {% endfor %} + </table> + </form> + <a href="{{url_for('all_user_hours',username=user.username)}}"class="action-button">Past Hours</a> + </section> + {# + {% for time in tspp %} + {{ time }} + </br> + {% endfor %} + #} +{% endblock %} diff --git a/app/templates/dashboard/punchclock/otheruser.html b/app/templates/dashboard/punchclock/otheruser.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block title %}Clock In User{% 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="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' %} + {% for error in field.errors %} + <tr><td><span style="color:red;">{{ field.label }}</span></td><td><span style="color:red;">[{{error}}]</span></td></tr> + {% endfor %} + <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/project.html b/app/templates/dashboard/punchclock/update/project.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block title %}Update Project{% endblock %} + +{% block content %} +<section class="new-agreement-grid"> + <h3>Update Project</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.projectSel.label }}{{ form.projectSel() }}<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 @@ -0,0 +1,32 @@ +<section class="punchclock"> + <h2 id="clock"></h2> + {% for error in clockoutform.errors or clockinform.errors %} + <span style="color:red;">[{{ error }}]</span> + {% endfor %} + {% if clocked_out %} + <form class="widget-form" action="" method="POST" novalidate> + <div> + {{ clockinform.hidden_tag() }} + <p>{{ clockinform.projectsSel.label }}<br>{{ clockinform.projectsSel() }}</p> + <p>{{ clockinform.clockin() }}</p> + </div> + </form> + {% elif not clocked_out %} + <form class="widget-form" action="" method="POST" novalidate> + <div> + {{ clockoutform.hidden_tag() }} + <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> + {% else %} + <p>Neither clocked in or out! Something is wrong!</p> + {% endif %} + <!-- Add iff satement for clocked_in==True + <p> form.projects(choices=projects,default=(if(project[0]==0){return project[0].label })</p> + --> + <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/dev/agreementdata.html b/app/templates/dev/agreementdata.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block title %}agreement data in db{% endblock %} + +{% block content %} + {%- for x in allagreementdata %} + {%- print(x) %} + </br> + </br> + {%- endfor %} +{% endblock %} diff --git a/app/templates/dev/fleetdata.html b/app/templates/dev/fleetdata.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block title %}fleet data in db{% endblock %} + +{% block content %} + {%- for x in allfleetdata %} + {%- print(x) %} + </br> + </br> + {%- endfor %} +{% endblock %} diff --git a/app/templates/dev/projectdata.html b/app/templates/dev/projectdata.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block title %}project data in db{% endblock %} + +{% block content %} + {%- for x in allprojectsdata %} + {%- print(x) %} + </br> + </br> + {%- endfor %} +{% endblock %} diff --git a/app/templates/dev/timedata.html b/app/templates/dev/timedata.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block title %}time data in db{% endblock %} + +{% block content %} + {%- for x in alltimedata %} + {%- print(x) %} + </br> + </br> + {%- endfor %} +{% endblock %} diff --git a/app/templates/dev/usrs.html b/app/templates/dev/usrs.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block title %}users in db{% endblock %} + +{% block content %} + {%- for x in allusers %} + {%- print(x) %} + </br> + </br> + {%- endfor %} +{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} + +{% block title %} Resource Management System {% endblock %} + +{% block content %} + <section class="singlepage"> + <h2>Welcome to YEP!</h2><h6>Call to action! Motto or Mission Statement</h6> + <div class="button"><a href="/login">Clock In!</a></div> + </section> +{% endblock %} diff --git a/app/templates/knowlegebase/index.html b/app/templates/knowlegebase/index.html @@ -0,0 +1,43 @@ +{% extends 'base.html' %} + +{% 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> + <p class="intro-documentation"> + Hello! Welcome to the <span class="italics">Simple Time Card</span> web application. + </p> + <p class="crew-login-documentation"> + The usage for crew memebers is meant to be as simple as possible. After logging in you'll + have access to the "Dashboard" where you can clock in to the "Project" that you're currently + working on. + <!-- Should you forget to clock in, or out, you can change your hours by using the hour:minute AM/PM + and select either the update or delete button. --> + </p> + <p class="crew-vehicle-documentation"> + Additionally you can checkout a vehicle by selecting said vehicle, inputting the + starting mileage, selecting the checks for components that are functional, and any other + pertient information. You may only have one vehicle checked out at a time. When done you can + check the vehicle back in by inserting the ending mileage and any potential incidents. + <br /> + For example: + <select> + <option>Vehicle 1</option> + <option>Vehicle 2</option> + <option>Vehicle 3</option> + </select> + <input class="input-example" name="start_mileage" type="number" value=""> + </p> + <p class="end-documentation"> + For best security practices users should logout whenever not interacting with the application. + </p> + </section> + </section> +{% endblock %} diff --git a/app/templates/login.html b/app/templates/login.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} + +{% block title %}Login{% 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="login-grid"> + <form class="login" action="" method="post" novalidate> + {{ form.hidden_tag() }} + <p> + {{ form.username.label }}<br> + {{ form.username(size=32) }} + {% for error in form.username.errors %} + <br><span style="color:red;">[{{ error }}]</span> + {% endfor %} + </p> + <p> + {{ form.password.label }}<br> + {{ form.password(size=32) }} <!-- is it necessary to limit password length? --> + {% for error in form.password.errors %} + <br><span style="color:red;">[{{ error }}]</span> + {% endfor %} + </p> + <p>{{ form.login() }}</p> + </form> +</section> +<!-- old version before structure change --> +<!-- <section class="login-grid"> + <form class="login"> + <label for="username">Login: </label> + <input type="text" id="username" name="username"><br> + <label for="password">Password: </label> + <input type="password" id="password" name="password"><br> + <input type="submit" value="Login"> + </form> + </section> +--> +{% endblock %} diff --git a/config.py b/config.py @@ -0,0 +1,11 @@ +import os +import urllib +from dotenv import load_dotenv + +basedir = os.path.abspath(os.path.dirname(__file__)) +load_dotenv(os.path.join(basedir, '.env')) + +class Config(object): + SECRET_KEY = os.environ.get('SECRET_KEY') or 'temporary-dev-key-here-change-prior-to-deployment' + MONGO_URI = "mongodb://localhost:27017/simple_resource_management_software" + diff --git a/database.ini b/database.ini @@ -0,0 +1,5 @@ +# [PROD] +# DB_URI = mongodb+srv://<username>:<password>@localhost:27017 + +#[TEST] +#DB_URI = mongodb+srv://nikolas:password@localhost:27017 diff --git a/db.py b/db.py @@ -0,0 +1,18 @@ +import bson +import pymongo + +from flask import current_app, g +from werkzeug.local import LocalProxy +from pymongo import MongoClient + +from pymongo.errors import DuplicateKeyError, OperationFailure +from bson.objectid import ObjectId +from bson.errors import InvalidId + + +def get_database(): + client = pymongo.MongoClient('localhost', 27017) + return client['simple_resource_management_software'] + +if __name__ == '__main__': + dbname = get_database() +\ No newline at end of file diff --git a/model.py b/model.py @@ -0,0 +1,92 @@ +import datetime + +from fastapi.encoders import jsonable_encoder +from typing import List, Optional, Union +from pydantic import BaseModel, Field + +class Users(BaseModel): + _id: Optional[PydanticObjectId] = Field(None, alias="_id") + slug: str + username: str + role: str + location: str + phone: int + email: str + active: bool + pay_period: str + pay_value: float + + def to_json(self): + return jsonable_encoder(self, exclude_none=True) + + def to_bson(self): + data = self.dict(by_alias=True, exclude_none=True) + + if data["_id"] is None: + data.pop("_id") + return data + + +class Time(BaseModel): + _id: str + # forign key + clock_in: datetime.utcnow #System time + modified_by: str #link to _id of user + date: datetime.date + project: str + clock_out: datetime.utcnow #System time + note: str + perdium: bool + total_time: int #clock_out - clock_in + + def to_json(self): + return jsonable_encoder(self, exclude_none=True) + + def to_bson(self): + data = self.dict(by_alias=True, exclude_none=True) + + if data["_id"] is None: + data.pop("_id") + return data + + +class Fleet(BaseModel): + _id: int + date: datetime.date + operator: int #forign key to userID + safety_checks: bool #array for different safety checks + additional_notes: str + vehicle: int #vehicleID + incident_report: str + mileage: int + + def to_json(self): + return jsonable_encoder(self, exclude_none=True) + + def to_bson(self): + data = self.dict(by_alias=True, exclude_none=True) + + if data["_id"] is None: + data.pop("_id") + return data + + +class Agreement(BaseModel): + _id: int #forign key to user + start_date: int + end_date: int + bid_document: str #Filepath to document + budget: float + cost: int + +class Projects(BaseModel):#Projects references agreement + project_id: int + project_name: str + project_budget: List[float] = [ + # labor_budget: float | Indexed 0 + # travel_budget: float | Indexed 1 + # supplies_budget: float | Indexed 2 + # contact_budget: float | Indexed 3 + # equipment_budget: float | Indexed 4 + # other: float | Indexed 5 + ] +\ No newline at end of file diff --git a/requirements.txt b/requirements.txt @@ -0,0 +1,23 @@ +anyio==3.6.2 +bcrypt==4.0.1 +click==8.1.3 +dnspython==2.2.1 +fastapi==0.89.1 +Flask==2.2.2 +Flask-Bcrypt==1.0.1 +Flask-Login==0.6.2 +Flask-PyMongo==2.3.0 +Flask-WTF==1.1.1 +idna==3.4 +importlib-metadata==5.1.0 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.1 +pydantic==1.10.4 +pymongo==4.3.3 +sniffio==1.3.0 +starlette==0.22.0 +typing-extensions==4.4.0 +Werkzeug==2.2.2 +WTForms==3.0.1 +zipp==3.11.0 diff --git a/seeds.py b/seeds.py @@ -0,0 +1,451 @@ +import pymongo +import datetime + +client = pymongo.MongoClient('mongodb://localhost:27017/simple_resource_management_software') +db = client.get_database('simple_resource_management_software') + +# Create collections +user_collection = db.get_collection('user_collection') +time_collection = db.get_collection('time_collection') +fleet_collection = db.get_collection('fleet_collection') +agreements_collection = db.get_collection('agreements_collection') +projects_collection = db.get_collection('projects_collection') +permissions_collection = db.get_collection('permissions_collection') + +# Clean previous seeding +x = user_collection.delete_many({}) +x = time_collection.delete_many({}) +x = fleet_collection.delete_many({}) +x = agreements_collection.delete_many({}) +x = projects_collection.delete_many({}) +x = permissions_collection.delete_many({}) + +# Database collections/documents +# Example create user via User constructor(all fields required) +#u2 = User(fname='hiccup',mname='',lname='crawford',email='hiccup@example.com',branch='Dillon', address='705 N Railroad Ave, Dillon MT, 59725',birthday='2022-11-17',role='Dog',phonenumber='3074380491') +# create password +#u2.set_password('hiccupmonster') + +# User Documents +#document/json user_data, will have User.__init__() create userobj (routes.py) +#pw is 'password' +user1 = { + 'fname': 'Nikolas', + 'mname': 'M', + 'lname': 'Mazur', + 'username': 'nikolasmmazur', + 'birthday': '1999-03-26', + 'password_hash': 'pbkdf2:sha256:260000$DBIF9Dfq1OcsYwSk$37f5cc231ff2c97cc7a6b60f25c767380574f1c01cc17069da4f3e7e25ba3370', + 'role': 'Web Developer', + 'address': '275 DuPont Dr, Lander Wy 82520', + 'branch': 'Lander', + 'phonenumber': '3074380460', + 'email': 'kolemazur@gmail.com', + 'pay_period': 'hourly', + 'pay_value': 15.12, + 'is_active':False + } + +#pw is 'password2' +user2 = { + 'fname': 'Brennen', + 'mname': 'T', + 'lname': 'Mazur', + 'username': 'brennentmazur', + 'birthday': '1997-04-28', + 'password_hash': 'pbkdf2:sha256:260000$ukazhSEG3m9xH2oL$5cc00ff3411f614720287c18f615d71578face70abc990ea5def89f520b0ac2c', + 'role': 'Crew Lead', + 'branch': 'Dillon', + 'phonenumber': 3074380491, + 'address': '705 N Railroad Ave, Dillon MT, 59725', + 'email': 'brennen.mazur@gmail.com', + 'pay_period': 'salaried', + 'pay_value': 43000, + 'is_active':True + } + +user3 = { + 'fname': 'Hiccup', + 'mname': 'C', + 'lname': 'Mazur', + 'username': 'hiccupcmazur', + 'birthday': '2022-09-14', + 'password_hash': 'pbkdf2:sha256:260000$ukazhSEG3m9xH2oL$5cc00ff3411f614720287c18f615d71578face70abc990ea5def89f520b0ac2c', + 'role': 'Crew', + 'branch': 'Dillon', + 'phonenumber': 3074380491, + 'address': '705 N Railroad Ave, Dillon MT, 59725', + 'email': 'hiccup.mazur@fake.com', + 'pay_period': 'salaried', + 'pay_value': 43000, + 'is_active':True + } + +# Time documents +time1 = { + 'clock_in': [datetime.datetime.utcnow()], #System time Clock_out should be optional, but can clock_in? + 'modified_by': ['nikolasmmazur'], #link to _id of user + 'project': 'Project 2', #Probably link with projects foreign key + 'clock_out': [datetime.datetime.utcnow()], #System time + 'note': 'Changed due to clocking out early', + 'per_diem': False, +# 'total_time': +} + +time2 = { + 'clock_in': [datetime.datetime.utcnow()], #System time Clock_out should be optional, but can clock_in? + 'modified_by': ['nikolasmmazur','brennentmazur'], #link to _id of user + 'project': 'Project 2', #Probably link with projects foreign key + 'clock_out': [datetime.datetime.utcnow()], #System time + 'note': 'Changed due to clocking out early', + 'per_diem': False, +# 'total_time': +} + +time3 = { + 'clock_in': [datetime.datetime.utcnow()], + 'modified_by': ['hiccupcmazur'], + 'project': 'Project 5' + } + +# Fleet documents +fleetpool = { + '_id':'Fleet Pool', + 'available':['Vehicle1','Vehicle2','Vehicle3','Vehicle5','Vehicle6'], + 'unavailable':[('Vehicle4','brennentmazur')] # Tuple for who checked vehicle lastOR set to 'REPAIRS' for obvious reasons + } + +fleet1 = { + 'date': datetime.datetime.today(), + 'operator': 'brennentmazur', #forign key to userID + 'start_mileage': 33, + 'safety_checks': [True,True,True,True,True],#array for different safety checks + 'additional_notes': 'Oil needs checked', + 'vehicle': 'Vehicle4', #vehicleID + 'incident_report': '' +} + +fleet2 = { + 'date': datetime.datetime.today(), + 'operator': 'nikolasmmazur', #forign key to userID + 'start_mileage': 33, + 'end_mileage': 33, +# 'safety_checks': True, #array for different safety checks + 'additional_notes': 'Tires need replaced', + 'vehicle': 'The Small Truck', #vehicleID + 'incident_report': 'I dunno what goes in incident reports.' +} + +# Agreement documents +agreement1 = { + 'agreement_name': 'Agreement 1', + 'agency': ['USFS'], + 'projects': [2,3,4], + 'start_date': '2023-12-5', + 'end_date': '2023-8-12', + #'bid_document': '/asset/document/agreements/New-Agreement.pdf', #Filepath to document +# 'budget': [{ +# 'labor':1259.40, +# 'travel':220.00, +# 'supplies':320.00, +# 'contact':420.00, +# 'equipment':620.00, +# 'other':20.00 +# }], +# 'costs': [{ +# 'labor':159.40, +# 'travel':20.00, +# 'supplies':30.00, +# 'contact':40.00, +# 'equipment':60.00, +# 'other':2.00 +# }] +} + +agreement2 = { + 'agreement_name': 'Agreement 2', + 'agency': ['SMSP'], + 'projects': [1,5], + 'start_date': '2021-2-21', + 'end_date': '2022-12-13', + #'bid_document': '/asset/document/agreements/Old-Agreement.pdf', #Filepath to document +# 'budget': [{ +# 'labor':259.40, +# 'travel':220.00, +# 'supplies':320.00, +# 'contact':420.69, +# 'equipment':20.00, +# 'other':20.00 +# }], +# 'costs': [{ +# 'labor':159.40, +# 'travel':20.00, +# 'supplies':30.00, +# 'contact':40.00, +# 'equipment':60.00, +# 'other':2.00 +# }] +} + +# Projects documents +projects1 = { + '_id':1, + 'project_name': 'Project 1', + 'agreement': 2, + 'budget': [{ + 'labor':159.40, + 'travel':420.00, + 'supplies':20.00, + 'contact':40.00, + 'equipment':60.00, + 'other':2.00 + }], + 'costs': [{ + 'labor':159.40, + 'travel':20.00, + 'supplies':30.00, + 'contact':40.00, + 'equipment':60.00, + 'other':2.00 + }] + #'project_budget': [13.20, 0, 20, 300, 50, 0 + # labor_budget: float | Indexed 0 + # travel_budget: float | Indexed 1 + # supplies_budget: float | Indexed 2 + # contact_budget: float | Indexed 3 + # equipment_budget: float | Indexed 4 + # other: float | Indexed 5 + #], +} + +projects2 = { + '_id':2, + 'project_name': 'Project 2', + 'agreement': 1, + 'budget': [{ + 'labor':159.40, + 'travel':820.00, + 'supplies':1120.00, + 'contact':42.00, + 'equipment':75.00, + 'other':12.00 + }], + 'costs': [{ + 'labor':159.40, + 'travel':20.00, + 'supplies':30.00, + 'contact':40.00, + 'equipment':60.00, + 'other':2.00 + }] + #'project_budget': [13.20, 0, 20, 300, 50, 0 + # labor_budget: float | Indexed 0 + # travel_budget: float | Indexed 1 + # supplies_budget: float | Indexed 2 + # contact_budget: float | Indexed 3 + # equipment_budget: float | Indexed 4 + # other: float | Indexed 5 + #], +} + +projects3 = { + '_id':3, + 'project_name': 'Project 3', + 'agreement': 1, + 'budget': [{ + 'labor':459.40, + 'travel':510.00, + 'supplies':80.00, + 'contact':94.20, + 'equipment':9.00, + 'other':27.00 + }], + 'costs': [{ + 'labor':159.40, + 'travel':20.00, + 'supplies':30.00, + 'contact':40.00, + 'equipment':60.00, + 'other':2.00 + }] + #'project_budget': [13.20, 0, 20, 300, 50, 0 + # labor_budget: float | Indexed 0 + # travel_budget: float | Indexed 1 + # supplies_budget: float | Indexed 2 + # contact_budget: float | Indexed 3 + # equipment_budget: float | Indexed 4 + # other: float | Indexed 5 + #], +} +projects4 = { + 'project_name': 'Project 4', + '_id':4, + 'agreement': 1, + 'budget': [{ + 'labor':258.40, + 'travel':71.00, + 'supplies':20.00, + 'contact':4.00, + 'equipment':66.00, + 'other':59.00 + }], + 'costs': [{ + 'labor':159.40, + 'travel':20.00, + 'supplies':30.00, + 'contact':40.00, + 'equipment':60.00, + 'other':2.00 + }] + #'project_budget': [13.20, 0, 20, 300, 50, 0 + # labor_budget: float | Indexed 0 + # travel_budget: float | Indexed 1 + # supplies_budget: float | Indexed 2 + # contact_budget: float | Indexed 3 + # equipment_budget: float | Indexed 4 + # other: float | Indexed 5 + #], +} +projects5 = { + '_id':5, + 'project_name': 'Project 5', + 'agreement': 2, + 'budget': [{ + 'labor':1259.40, + 'travel':220.00, + 'supplies':320.00, + 'contact':420.00, + 'equipment':620.00, + 'other':20.00 + }], + 'costs': [{ + 'labor':159.40, + 'travel':20.00, + 'supplies':30.00, + 'contact':40.00, + 'equipment':60.00, + 'other':2.00 + }] + #'project_budget': [13.20, 0, 20, 300, 50, 0 + # labor_budget: float | Indexed 0 + # travel_budget: float | Indexed 1 + # supplies_budget: float | Indexed 2 + # contact_budget: float | Indexed 3 + # equipment_budget: float | Indexed 4 + # other: float | Indexed 5 + #], +} +# Permissions documents (only needs array list of str for each 'service') +crew = { + '_id':1, + 'label': 'Crew L1', + 'dashboard': ['punchclock'], + 'admin': [], + 'base_pay_value':10.00 + } + +crew2 = { + '_id':2, + 'label': 'Crew L2', + 'dashboard': ['punchclock'], + 'admin': [], + 'base_pay_value':10.00 + } + +alead = { + '_id':3, + 'label': 'Assistant Lead', + 'dashboard': ['punchclock','fleet'], + 'admin': [], + 'base_pay_value':13.00 + } + +lead = { + '_id':4, + 'label': 'Crew Lead', + 'dashboard': ['punchclock','fleet','activeusers'], + 'admin': [], + 'base_pay_value':15.00 + } + +personelDirector = { + '_id':5, + 'label': 'Personel Director', + 'dashboard': ['punchclock','fleet','activeusers'], + 'admin': ['agreements','reports','users','roles'], + 'base_pay_value':10.00 + } + +developer = { + '_id':6, + 'label': 'Web Developer', + 'dashboard': ['punchclock','fleet','activeusers'], + 'admin': ['agreements','reports','users','roles'], + 'base_pay_value':32.00 + } + +ed = { + '_id':7, + 'label': 'Executive Director', + 'dashboard': ['punchclock','fleet','activeusers'], + 'admin': ['agreements','reports','users','roles'], + 'base_pay_value':10.00 + } + +manager = { + '_id':8, + 'label': 'Project Manager', + 'dashboard': ['punchclock','fleet','activeusers'], + 'admin': ['agreements','reports','users','roles'], + 'base_pay_value':25.00 + } + +accountant = { + '_id':9, + 'label': 'Accountant', + 'dashboard': ['punchclock','fleet','activeusers'], + 'admin': ['agreements','reports','users'], + 'base_pay_value':23.00 + } + + +# Insert documents +user_collection.insert_many([user1, user2, user3]) +time_collection.insert_many([time1, time2,time3]) +fleet_collection.insert_many([fleetpool, fleet1, fleet2]) +agreements_collection.insert_many([agreement1, agreement2]) +projects_collection.insert_many([projects1, projects2, projects3, projects4, projects5]) +permissions_collection.insert_many([crew,crew2,alead,lead,personelDirecton,developer,ed,manager,accountant]) + +# Print seeded data +for x in user_collection.find(): + print(x) + +for x in time_collection.find(): + print(x) + +for x in fleet_collection.find(): + print(x) + +for x in agreements_collection.find(): + print(x) + +for x in projects_collection.find(): + print(x) + +for x in permissions_collection.find(): + print(x) + +#print('making query on agreements and attempting to inject relavant projects') +#test_agreements = agreements_collection.find() +#fagreements=[] +#for agreement in test_agreements: +# for i in range(len(agreement['projects'])): +# print(agreement['projects'][i]) +# agreement['projects'][i] = projects_collection.find_one({'_id': agreement['projects'][i]}) +# # print(agreement['projects'][i]) +# #for a in agreement: +# #print(a) +# fagreements.append(agreement) +#for agree in fagreements: +# print(agree) diff --git a/stc.py b/stc.py @@ -0,0 +1,2 @@ +# Pull application from app package (app/) and import the Flask application object defined in app/__init__.py and associated files +from app import app