From 87f1ec34b98288c520998f7d878a2504de0b5bd3 Mon Sep 17 00:00:00 2001 From: jnisbet Date: Wed, 10 Apr 2024 10:37:17 +0100 Subject: [PATCH] Initial Commit --- departments.csv | 8 ++ employee_manifest.py | 125 ++++++++++++++++++++++++++ employeeregister.csv | 1 + home.py | 16 ++++ index.py | 86 ++++++++++++++++++ lib.py | 65 ++++++++++++++ sign_in.py | 156 +++++++++++++++++++++++++++++++++ sign_in_logs/2024-03-25.log | 1 + sign_ins/signin-2024-03-25.csv | 2 + signinregister.pdf | 68 ++++++++++++++ 10 files changed, 528 insertions(+) create mode 100644 departments.csv create mode 100644 employee_manifest.py create mode 100644 employeeregister.csv create mode 100644 home.py create mode 100644 index.py create mode 100644 lib.py create mode 100644 sign_in.py create mode 100644 sign_in_logs/2024-03-25.log create mode 100644 sign_ins/signin-2024-03-25.csv create mode 100644 signinregister.pdf diff --git a/departments.csv b/departments.csv new file mode 100644 index 0000000..c96f539 --- /dev/null +++ b/departments.csv @@ -0,0 +1,8 @@ +Department +Audit +IT +Partners +Payroll +Reception +Secretarys +Tax \ No newline at end of file diff --git a/employee_manifest.py b/employee_manifest.py new file mode 100644 index 0000000..9776a2e --- /dev/null +++ b/employee_manifest.py @@ -0,0 +1,125 @@ +from tkinter import * +from tkinter import ttk +import pandas as pd +from lib import treerow_add, csvrow_add, treerow_del, csvrow_del, row_tracker +from tkinter import messagebox + +class EmployeeManifest(Frame): + def __init__(self, parent, controller): + Frame.__init__(self, parent) + self.employeereg_columns = ['department','fname','sname','cardID'] + + #Data frames for list of departments and database of users + self.employee_df = pd.read_csv('employeeregister.csv', converters={'cardID': str}) + self.department_df = pd.read_csv('departments.csv') + + self.row_tracker = row_tracker(self.department_df, 0) + + self.input_frame = Frame(self) + self.input_frame.grid(row=0, column=0, sticky='w') + + self.firstname_label = Label(self.input_frame, text='First Name') + self.firstname_label.grid(row=0, column=0, padx=10, sticky="w") + self.firstname_text = Entry(self.input_frame) + self.firstname_text.grid(row=0, column=1, pady=5) + self.firstname_text.focus() + + self.surname_label = Label(self.input_frame, text='Surname') + self.surname_label.grid(row=1, column=0, padx=10, sticky="w") + self.surname_text = Entry(self.input_frame) + self.surname_text.grid(row=1, column=1, pady=5) + + self.card_label = Label(self.input_frame, text='Scan Card') + self.card_label.grid(row=2, column=0, padx=10, sticky="w") + self.card_text = Entry(self.input_frame) + self.card_text.grid(row=2, column=1, pady=5) + + self.enter_btn = Button(self.input_frame, text='Enter Employee Info', command= lambda: self.write_line_csv(self.get_inputs())) + self.enter_btn.grid(row=3, column=0, pady=5, padx=10) + + self.del_row_btn = Button(self.input_frame, text='Delete Employee(s)', command=self.delete_employees) + self.del_row_btn.grid(row=3, column=1, padx=10) + + #create and store options for the department option menu + self.department_options = [] + + for rownum, row in self.department_df.iterrows(): + self.department_options.append(row.Department) + + #Create department option menu + self.value_inside = StringVar(self.input_frame) + self.value_inside.set("") + self.options_label = Label(self.input_frame, text='Department') + self.options_label.grid(row=0, column=2, padx=10) + self.department_options = OptionMenu(self.input_frame, self.value_inside, *self.department_options) + self.department_options.config(width=14) + self.department_options.grid(row=0, column=3, padx=(0, 10)) + + self.employee_register_tree = ttk.Treeview(self) + self.employee_register_tree.grid(column=0, row=4, padx=(10, 0), sticky="we") + self.employee_register_tree['columns'] = ('First Name', 'Surname', 'Card Code') + self.employee_register_tree.column('#0', width=120) + self.employee_register_tree.column('First Name', anchor=CENTER, width=110) + self.employee_register_tree.column('Surname', anchor=CENTER, width=110) + self.employee_register_tree.column('Card Code', anchor=CENTER, width=110) + + #Define headings for each column + self.employee_register_tree.heading('#0', text='Department') + self.employee_register_tree.heading('First Name', text='First Name') + self.employee_register_tree.heading('Surname', text='Surname') + self.employee_register_tree.heading('Card Code', text='Card Code') + + self.tree_scrollbar = Scrollbar(self, orient="vertical", command=self.employee_register_tree.yview) + self.employee_register_tree.configure(yscrollcommand=self.tree_scrollbar.set) + self.tree_scrollbar.grid(column=1,row=4, sticky='ns', pady=(0, 10)) + + for rownum, row in self.employee_df.iterrows(): + treerow_add(self.row_tracker, self.employee_register_tree, row) + + def delete_employees(self): + employees = self.employee_register_tree.selection() + + #Prevent deletion of department parent row + for employee in employees: + if 'D' in employee: + return + + delete_prompt = messagebox.askokcancel('Delete Users', 'Are you sure you wish to delete user(s)') + if delete_prompt == False: + return + + #Delete user from tree and CSV + self.employee_df = csvrow_del('employeeregister.csv', self.employee_df, employees) + treerow_del(self.row_tracker, self.employee_register_tree, employees) + + #Clear inputs of all fields + def clear_inputs(self, input_arr): + self.value_inside.set("") + + for input in input_arr: + input.delete('0', END) + + self.firstname_text.focus() + + #Return array of inputs in all fields + def get_inputs(self): + inputs = [self.value_inside.get(), self.firstname_text.get(), self.surname_text.get(), self.card_text.get()] + return inputs + + def write_line_csv(self, inputs): + #Field validation + for input in inputs: + if input == '': + messagebox.showerror('Validation Error', 'Please make sure all fields are completed') + return + + for x in range(len(self.employee_df)): + if self.employee_df.iloc[x]['cardID'] == inputs[3]: + messagebox.showerror('Validation Error', 'Card ID must be unique for each employee') + return + + self.clear_inputs([self.firstname_text, self.surname_text, self.card_text]) + + #Add user to tree and csv + treerow_add(self.row_tracker, self.employee_register_tree, inputs) + self.employee_df = csvrow_add('employeeregister.csv', self.employee_df, self.employeereg_columns, inputs) \ No newline at end of file diff --git a/employeeregister.csv b/employeeregister.csv new file mode 100644 index 0000000..83ad549 --- /dev/null +++ b/employeeregister.csv @@ -0,0 +1 @@ +department,fname,sname,cardID diff --git a/home.py b/home.py new file mode 100644 index 0000000..85f3280 --- /dev/null +++ b/home.py @@ -0,0 +1,16 @@ +from tkinter import * +from tkinter import ttk + +class Home(Frame): + def __init__(self, parent, controller): + super().__init__(parent) + + btn_frame = Frame(self) + btn_frame.pack() + + employee_management_btn = Button(btn_frame, text='Employess Management', command=lambda: controller.frame_update('employee_manifest')) + employee_management_btn.pack(pady = (0,20)) + + signin_btn = Button(btn_frame, text='Sign in Register', command=lambda: controller.frame_update('sign_in')) + signin_btn.pack() + diff --git a/index.py b/index.py new file mode 100644 index 0000000..d08aafa --- /dev/null +++ b/index.py @@ -0,0 +1,86 @@ +from tkinter import * +from tkinter import ttk +import pandas as pd +from employee_manifest import EmployeeManifest +from home import Home +from sign_in import SignIn +import sched +import time +import os +from datetime import datetime, date , timedelta +from threading import Thread +from queue import Queue +#Special mention to James Haywood for imaging the PI $$ <3 <3! +class Main(Tk): + def __init__(self): + Tk.__init__(self) + + if os.name == 'nt': + self.geometry('520x450') + else: + self.geometry('720x450') + + self.winfo_toplevel().title("Sign in App") + self.main_frame = Frame(self) + self.main_frame.pack(side='top', fill=BOTH, expand=1, pady=10) + + #Thread safe queue and event to check queue for scheduled sign out at midnight + #Required because scheduler is run in seperate thread which causes tkinter to break without + self.sched_queue = Queue() + self.bind('<>', self.run_signout) + + #List of GUI's used for mounting on button click + class FramesList(): + home = Home + employee_manifest = EmployeeManifest + sign_in = SignIn + + self.frames = FramesList() + + #Mount home frame when class envoked (program started) + self.current_frame = self.frames.home(self.main_frame, self) + self.current_frame.place(anchor="c", relx=.5, rely=.5) + + self.home_btn = Button(text = 'Home', command=lambda: Main.frame_update(self, 'home')) + self.home_btn.place(anchor='w', x=5, y=20) + + self.sched_thread = Thread(target=self.run_sched, daemon=True) + self.sched_thread.start() + + #Destroy the current GUI frame and mount a requested GUI + def frame_update(self, requested_frame): + self.current_frame.destroy() + next_frame = getattr(self.frames, requested_frame) + self.current_frame = next_frame(self.main_frame, self) + self.current_frame.place(anchor="c", relx=.5, rely=.5) + + #Add function to the thread safe queue and then generate an event that causes the queue to be checked + def update_queue(event, self): + self.sched_queue.put(self.clear_all_signin) + self.event_generate('<>') + + #When CheckQueue event is fired this function is run + #Gets function from queue and then runs it + def run_signout(self, event): + msg = self.sched_queue.get() + msg() + + #Clears sign ins (scheduled to run at the start of next day) by just destroying and re-mounting the sign in frame + def clear_all_signin(self): + print('clear sign in executed') + if type(app.current_frame).__name__ == 'SignIn': + print('p0ng') + self.frame_update('sign_in') + self.run_sched() + + #Rune scheduled task at 12am each day + def run_sched(self): + scheduler = sched.scheduler(time.time, time.sleep) + tomorrows_date = date.today() + timedelta(1) + #todays_date = date.today() + scheduled_time = datetime(tomorrows_date.year, tomorrows_date.month, tomorrows_date.day, 00, 00, 00) + scheduler.enterabs(scheduled_time.timestamp(), 1, self.clear_all_signin) + scheduler.run() + +app = Main() +app.mainloop() diff --git a/lib.py b/lib.py new file mode 100644 index 0000000..75b7b5f --- /dev/null +++ b/lib.py @@ -0,0 +1,65 @@ +from tkinter import * +from tkinter import ttk +import pandas as pd + +#Uses a counter to ensure unique iid for each row +#keeps track of parent row iids +#returns parent iids incrementing the counter each time +def row_tracker(department_frame, iid_count): + class Row_Tracker: + def __init__(self, iid_count): + self.iid_count = iid_count + + def departiid(self, department): + self.iid_count += 1 + setattr(self, department, 'D' + str(self.iid_count)) + return getattr(self, department) + + row_tracker = Row_Tracker(iid_count) + + for rownum, row in department_frame.iterrows(): + setattr(row_tracker, row['Department'], False) + + return row_tracker + +#add a row to the tree +def treerow_add(row_tracker, tree, inputs): + row_iid = inputs[3] + department = inputs[0] + department_iid = getattr(row_tracker, department) + if not getattr(row_tracker, department): + department_iid = row_tracker.departiid(department) + tree.insert(parent='', index='end', iid=department_iid, text=department, values=('', '', '')) + + tree.insert(parent=department_iid, index='end', iid=row_iid, text='', values=(inputs[1], inputs[2], row_iid)) + +#add a row to the csv +# return frame because variable for that frame needs to be updated +def csvrow_add(file_path, frame, columns, inputs): + df_addition = {} + for i, column in enumerate(columns): + df_addition[column] = inputs[i] + frame = frame._append(df_addition, ignore_index = True) + frame.to_csv(file_path, index=False) + + return frame + +#Delete employess from the tree and csv +#If parent row has no remaining children, delete it row from the tree and reset tracking for it's iid +def treerow_del(row_tracker, tree, iids): + for iid in iids: + parent_row_iid = tree.parent(iid) + tree.delete(iid) + + if not tree.get_children(parent_row_iid): + parent_row = tree.item(parent_row_iid) + setattr(row_tracker, parent_row['text'], False) + tree.delete(parent_row_iid) + +#Delete employee from data frame and csv +def csvrow_del(file_path, frame, iids): + for iid in iids: + frame = frame.drop(frame[frame['cardID'] == iid].index) + + frame.to_csv(file_path, index=False) + return frame diff --git a/sign_in.py b/sign_in.py new file mode 100644 index 0000000..c7f43af --- /dev/null +++ b/sign_in.py @@ -0,0 +1,156 @@ +from tkinter import * +from tkinter import ttk +import pandas as pd +import os +import re +import logging +from datetime import datetime, timedelta +from lib import row_tracker, treerow_add, csvrow_add, csvrow_del, treerow_del +from reportlab.pdfgen import canvas +from reportlab.lib.pagesizes import A4 + +class SignIn(Frame): + def __init__(self, parent, controller): + Frame.__init__(self, parent) + print('Signin refresh') + self.columns_signin = ['department', 'fname', 'sname', 'cardID', 'signin'] + self.current_date = str(datetime.now().date()) + + #File path generation for logs and csv of sign ins + self.todays_signins_path = 'sign_ins/signin-'+ self.current_date + '.csv' + self.todays_log_path = 'sign_in_logs/' + self.current_date + '.log' + + self.recent_signin = [False, timedelta(0)] + logging.basicConfig(level=logging.INFO, filename=self.todays_log_path, filemode="a") + + #Data frames for list of departments and database of users + self.employee_df = pd.read_csv('employeeregister.csv', converters={'cardID': str}) + self.department_df = pd.read_csv('departments.csv', converters={'cardID': str}) + + self.row_trackerobj = row_tracker(self.department_df, 0) + + self.signin_df = self.load_signin_df() + + self.input_frame = Frame(self) + self.input_frame.grid(column=0, row=0, columnspan=2, padx=10, pady=(0,30)) + + self.scan_label = Label(self.input_frame, text='Scan Card') + self.scan_label.grid(column=0, row=0, pady=10) + + self.border_color = Frame(self.input_frame) + self.scan_text = Entry(self.border_color) + self.scan_text.focus() + self.scan_text.bind("", self.focus_color) + self.scan_text.bind("", self.unfocus_color) + self.scan_text.bind('', self.register_scan) + self.scan_text.pack(padx=3, pady=3) + self.border_color.grid(column=0, row=1) + + self.tree_frame = Frame(self) + self.tree_frame.grid(column=0, row=2) + + self.fire_register_tree = ttk.Treeview(self.tree_frame) + self.fire_register_tree.grid(column=0, row=0, padx=10) + self.fire_register_tree['columns'] = ('First Name', 'Surname', 'Sign in Time') + self.fire_register_tree.column('#0', width=120) + self.fire_register_tree.column('First Name', anchor=CENTER, width=110) + self.fire_register_tree.column('Surname', anchor=CENTER, width=110) + self.fire_register_tree.column('Sign in Time', anchor=CENTER, width=110) + + #Define headings for each column + self.fire_register_tree.heading('#0', text='Department') + self.fire_register_tree.heading('First Name', text='First Name') + self.fire_register_tree.heading('Surname', text='Surname') + self.fire_register_tree.heading('Sign in Time', text='Sign in Time') + + self.pdf_btn = Button(self, text='Signin Report', command=self.generate_pdf) + self.pdf_btn.grid(column=0, row=3, pady=(10,0)) + + #Build tree rows for existing sign ins (needed if program restarted after users have already signed in for the day) + for rownum, row in self.signin_df.iterrows(): + treerow_add(self.row_trackerobj, self.fire_register_tree, row) + + def load_signin_df(self): + sign_ins = os.listdir('sign_ins') + + for file in sign_ins: + if re.findall(self.current_date, file): + return pd.read_csv(self.todays_signins_path, converters={'cardID': str, 'iid': str}) + + df = pd.DataFrame(columns=self.columns_signin) + df.to_csv(self.todays_signins_path, index=False) + return df + + #Clear input of text box + def input_clear(self): + self.scan_text.delete('0', END) + self.scan_text.focus() + + #change border colour of text box when focused/unfocused + def focus_color(self, event): + self.border_color.configure(background='green') + + def unfocus_color(self, event): + self.border_color.configure(background='red') + + def register_scan(self, event): + card_code = self.scan_text.get() + signin_datetime = datetime.now() + signin_time = signin_datetime.strftime("%H:%M:%S") + signin_datetime_future = signin_datetime + timedelta(seconds = 10) + + self.input_clear() + + # If employee does not exist exit + if not self.employee_df.loc[self.employee_df['cardID'] == card_code].values.size: + return + + #Prevent double tap of card + if card_code == self.recent_signin[0] and self.recent_signin[1] > signin_datetime: + return + + self.recent_signin = [card_code, signin_datetime_future] + code_linked_employee = self.employee_df.loc[self.employee_df['cardID'] == card_code].values[0].tolist() + + #If employee has already signed in sign out. remove from tree, csv and create a log + if self.signin_df.loc[self.signin_df['cardID'] == card_code].values.size: + self.signin_df = csvrow_del(self.todays_signins_path, self.signin_df, [card_code]) + treerow_del( self.row_trackerobj, self.fire_register_tree, [card_code]) + logging.info('SIGN OUT: ' + code_linked_employee[1] + ' ' + code_linked_employee[2] + ' : ' + signin_time) + return + + code_linked_employee.append(signin_time) + tree_row_toadd = [code_linked_employee[0], code_linked_employee[1], code_linked_employee[2], code_linked_employee[3]] + + #Add employee to tree and sign in csv, also create log + treerow_add( self.row_trackerobj, self.fire_register_tree, tree_row_toadd) + self.signin_df = csvrow_add(self.todays_signins_path, self.signin_df, self.columns_signin, code_linked_employee) + logging.info('SIGN IN: ' + code_linked_employee[1] + ' ' + code_linked_employee[2] + ' : ' + signin_time) + + #Populate PDF and send to default printer + def generate_pdf(self): + w, h = A4 + current_row = 150 + register_filename = "signinregister.pdf" + + c = canvas.Canvas(register_filename, pagesize=A4) + c.drawString(30, h - 100, "Department") + c.drawString(150, h - 100, "First Name") + c.drawString(270, h - 100, "Surname") + c.drawString(390, h - 100, "Sign in Time") + + for rownnum, row in self.signin_df.iterrows(): + current_row += 20 + + c.drawString(30, h - current_row, row[0]) + c.drawString(150, h - current_row, row[1]) + c.drawString(270, h - current_row, row[2]) + c.drawString(390, h - current_row, row[4]) + + c.showPage() + c.save() + + if os.name == 'nt': + os.startfile(register_filename, "print") + else: + os.system(f"lp {register_filename}") diff --git a/sign_in_logs/2024-03-25.log b/sign_in_logs/2024-03-25.log new file mode 100644 index 0000000..2de683e --- /dev/null +++ b/sign_in_logs/2024-03-25.log @@ -0,0 +1 @@ +INFO:root:SIGN IN: Joe Bloggs : 15:35:48 diff --git a/sign_ins/signin-2024-03-25.csv b/sign_ins/signin-2024-03-25.csv new file mode 100644 index 0000000..d542a9b --- /dev/null +++ b/sign_ins/signin-2024-03-25.csv @@ -0,0 +1,2 @@ +department,fname,sname,cardID,signin +,,,, diff --git a/signinregister.pdf b/signinregister.pdf new file mode 100644 index 0000000..b89ce4e --- /dev/null +++ b/signinregister.pdf @@ -0,0 +1,68 @@ +%PDF-1.3 +%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com +1 0 obj +<< +/F1 2 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/Contents 7 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 6 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +4 0 obj +<< +/PageMode /UseNone /Pages 6 0 R /Type /Catalog +>> +endobj +5 0 obj +<< +/Author (anonymous) /CreationDate (D:20240402090541+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240402090541+00'00') /Producer (ReportLab PDF Library - www.reportlab.com) + /Subject (unspecified) /Title (untitled) /Trapped /False +>> +endobj +6 0 obj +<< +/Count 1 /Kids [ 3 0 R ] /Type /Pages +>> +endobj +7 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 202 +>> +stream +Gas3-_$YcZ'SYMZ:N;]N_RqqlkYb#$U.t@c,]oDe`__$^,[0EY`5#Xeh[bJR9ohL4Aq6&LXU#Vn>EB*opl#:[dm&7p&g7o&8sr>V_.qjil[C[M*S942GiO$@SY4F00]H!"k'%AL:2kXSeuP"ieQQ*h$eF/[_Ch;m8+nTC^f0u?nj8Y':8u(EG_%:qP-bd@`Y#iaE'RX]~>endstream +endobj +xref +0 8 +0000000000 65535 f +0000000073 00000 n +0000000104 00000 n +0000000211 00000 n +0000000414 00000 n +0000000482 00000 n +0000000778 00000 n +0000000837 00000 n +trailer +<< +/ID +[<325c24b56bce97acfa22c1fe23eb8ce5><325c24b56bce97acfa22c1fe23eb8ce5>] +% ReportLab generated PDF document -- digest (http://www.reportlab.com) + +/Info 5 0 R +/Root 4 0 R +/Size 8 +>> +startxref +1129 +%%EOF