first commit
This commit is contained in:
319
app/api/v1/work_shifts.py
Normal file
319
app/api/v1/work_shifts.py
Normal file
@ -0,0 +1,319 @@
|
||||
import math
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
from time import time
|
||||
|
||||
import qrcode
|
||||
from fastapi import APIRouter
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfgen import canvas
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
from app import mongo
|
||||
from app.utils.response_util import response
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/get-shifts/{active}", tags=[""])
|
||||
async def get_shifts(active: bool, page: int = 1, items_per_page: int = 10):
|
||||
start_time = time()
|
||||
|
||||
total_count = await mongo.work_shifts_collection.count_documents({"active": active})
|
||||
total_pages = math.ceil(total_count / items_per_page) if total_count else 1
|
||||
|
||||
skip = (page - 1) * items_per_page
|
||||
raw_shifts = await mongo.work_shifts_collection.find(
|
||||
{"active": active},
|
||||
{"_id": False}
|
||||
).skip(skip).limit(items_per_page).to_list(length=items_per_page)
|
||||
|
||||
shifts = []
|
||||
for shift in raw_shifts:
|
||||
user = await mongo.users_collection.find_one({"id": shift["user_id"]}, {"_id": False})
|
||||
|
||||
started_at = shift.get("start_time")
|
||||
finished_at = shift.get("end_time")
|
||||
total_hours = 0
|
||||
pause_hours = 0
|
||||
|
||||
if started_at and finished_at:
|
||||
total_hours = round((finished_at - started_at) / 1000, 2)
|
||||
|
||||
for pause in shift.get("pause", []):
|
||||
if pause.get("start") and pause.get("end"):
|
||||
pause_hours += (pause["end"] - pause["start"]) / 1000
|
||||
|
||||
pause_hours = round(pause_hours, 2)
|
||||
shifts.append({
|
||||
"workShift": {
|
||||
"id": shift["id"],
|
||||
"user": user,
|
||||
"startedAt": started_at,
|
||||
"finishedAt": finished_at,
|
||||
},
|
||||
"totalHours": total_hours,
|
||||
"pauseHours": pause_hours,
|
||||
})
|
||||
|
||||
return response({
|
||||
"shifts": shifts,
|
||||
"paginationInfo": {
|
||||
"totalPages": total_pages
|
||||
},
|
||||
"ok": True
|
||||
}, start_time=start_time)
|
||||
|
||||
|
||||
@router.get("/generate-qr-code/{user_id}", tags=[""])
|
||||
async def generate_qr_code(user_id: int):
|
||||
user = await mongo.users_collection.find_one({"id": user_id}, {"_id": False})
|
||||
full_name = f'{user["secondName"]} {user["firstName"]}'
|
||||
position = user.get("position", {}).get("name", "")
|
||||
|
||||
width_mm, height_mm = 58, 40
|
||||
width_pt, height_pt = width_mm * mm, height_mm * mm
|
||||
|
||||
buffer = BytesIO()
|
||||
pdf = canvas.Canvas(buffer, pagesize=(width_pt, height_pt))
|
||||
pdf.setTitle("User QR")
|
||||
|
||||
pdfmetrics.registerFont(TTFont('Arial', 'assets/arial.ttf'))
|
||||
pdfmetrics.registerFont(TTFont('Arial Bold', 'assets/arial_bold.ttf'))
|
||||
|
||||
qr_img = qrcode.make(str(user_id)).convert("RGB")
|
||||
|
||||
qr_width_mm = 30
|
||||
qr_height_mm = 30
|
||||
qr_x = (width_mm - qr_width_mm) / 2 * mm
|
||||
qr_y = 2 * mm
|
||||
|
||||
pdf.drawImage(ImageReader(qr_img), qr_x, qr_y, qr_width_mm * mm, qr_height_mm * mm)
|
||||
text_y = qr_y + qr_height_mm * mm + 4 * mm
|
||||
|
||||
pdf.setFont("Arial Bold", 9)
|
||||
pdf.drawCentredString(width_pt / 2, text_y, full_name)
|
||||
|
||||
if position:
|
||||
text_y -= 4 * mm
|
||||
pdf.setFont("Arial", 8)
|
||||
pdf.drawCentredString(width_pt / 2, text_y, position)
|
||||
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
|
||||
buffer.seek(0)
|
||||
return StreamingResponse(buffer, media_type="application/pdf")
|
||||
|
||||
|
||||
@router.post("/start-shift/{user_id}", tags=[""])
|
||||
async def start_shift(user_id: int):
|
||||
start_time = time()
|
||||
new_id = await mongo.get_next_id(mongo.work_shifts_collection)
|
||||
|
||||
shift = {
|
||||
"id": new_id,
|
||||
"user_id": user_id,
|
||||
"active": True,
|
||||
"start_time": get_time_millis(),
|
||||
"pause": [],
|
||||
"end_time": None
|
||||
}
|
||||
|
||||
await mongo.work_shifts_collection.insert_one(shift)
|
||||
return response({
|
||||
"message": "Смена начата", "ok": True
|
||||
}, start_time=start_time)
|
||||
|
||||
|
||||
@router.post("/finish-shift/{user_id}", tags=[""])
|
||||
async def finish_shift(user_id: int):
|
||||
start_time = time()
|
||||
|
||||
work_shift = await mongo.work_shifts_collection.find_one({
|
||||
"user_id": user_id,
|
||||
"active": True
|
||||
}, {"_id": False})
|
||||
|
||||
if not work_shift:
|
||||
return response({
|
||||
"message": "Активная смена не найдена",
|
||||
"ok": False
|
||||
}, start_time=start_time)
|
||||
|
||||
end_time = get_time_millis()
|
||||
await mongo.work_shifts_collection.update_one(
|
||||
{"id": work_shift["id"]},
|
||||
{"$set": {
|
||||
"active": False,
|
||||
"end_time": end_time
|
||||
}}
|
||||
)
|
||||
|
||||
await add_time_tracking_record(work_shift, end_time)
|
||||
return response({
|
||||
"message": "Смена завершена",
|
||||
"ok": True
|
||||
}, start_time=start_time)
|
||||
|
||||
|
||||
@router.post("/finish-shift-by-id/{shift_id}", tags=[""])
|
||||
async def finish_shift_by_id(shift_id: int):
|
||||
start_time = time()
|
||||
|
||||
work_shift = await mongo.work_shifts_collection.find_one({"id": shift_id}, {"_id": False})
|
||||
if not work_shift:
|
||||
return response({
|
||||
"message": "Активная смена не найдена",
|
||||
"ok": False
|
||||
}, start_time=start_time)
|
||||
|
||||
end_time = get_time_millis()
|
||||
await mongo.work_shifts_collection.update_one(
|
||||
{"id": work_shift["id"]},
|
||||
{"$set": {
|
||||
"active": False,
|
||||
"end_time": end_time
|
||||
}}
|
||||
)
|
||||
|
||||
await add_time_tracking_record(work_shift, end_time)
|
||||
return response({
|
||||
"message": "Смена завершена",
|
||||
"ok": True
|
||||
}, start_time=start_time)
|
||||
|
||||
|
||||
@router.delete("/delete-shift/{shift_id}", tags=[""])
|
||||
async def delete_shift(shift_id: int):
|
||||
start_time = time()
|
||||
await mongo.work_shifts_collection.delete_one({"id": shift_id})
|
||||
|
||||
return response({
|
||||
"message": "Смена удалена",
|
||||
"ok": True
|
||||
}, start_time=start_time)
|
||||
|
||||
|
||||
@router.post("/pause/start/{shift_id}", tags=[""])
|
||||
async def start_pause_by_shift_id(shift_id: int):
|
||||
start_time = time()
|
||||
|
||||
await mongo.work_shifts_collection.update_one(
|
||||
{"id": shift_id},
|
||||
{"$push": {"pause": {"start": get_time_millis(), "end": None}}}
|
||||
)
|
||||
|
||||
return response({
|
||||
"message": "Пауза начата",
|
||||
"ok": True
|
||||
}, start_time=start_time)
|
||||
|
||||
|
||||
@router.post("/pause/start/for-user/{user_id}", tags=[""])
|
||||
async def start_pause_by_user_id(user_id: int):
|
||||
start_time = time()
|
||||
|
||||
await mongo.work_shifts_collection.update_one(
|
||||
{"user_id": user_id, "active": True},
|
||||
{"$push": {"pause": {"start": get_time_millis(), "end": None}}}
|
||||
)
|
||||
|
||||
return response({
|
||||
"message": "Пауза начата для пользователя",
|
||||
"ok": True
|
||||
}, start_time=start_time)
|
||||
|
||||
|
||||
@router.post("/pause/finish/{shift_id}", tags=[""])
|
||||
async def finish_pause_by_shift_id(shift_id: int):
|
||||
start_time = time()
|
||||
|
||||
await mongo.work_shifts_collection.update_one(
|
||||
{"id": shift_id, "pause.end": None},
|
||||
{"$set": {"pause.$.end": get_time_millis()}}
|
||||
)
|
||||
|
||||
return response({
|
||||
"message": "Пауза завершена",
|
||||
"ok": True
|
||||
}, start_time=start_time)
|
||||
|
||||
|
||||
@router.post("/pause/finish/for-user/{shift_id}", tags=[""])
|
||||
async def finish_pause_by_user_id(user_id: int):
|
||||
start_time = time()
|
||||
|
||||
await mongo.work_shifts_collection.update_one(
|
||||
{"user_id": user_id, "active": True, "pause.end": None},
|
||||
{"$set": {"pause.$.end": get_time_millis()}}
|
||||
)
|
||||
|
||||
return response({
|
||||
"message": "Пауза завершена для пользователя",
|
||||
"ok": True
|
||||
}, start_time=start_time)
|
||||
|
||||
|
||||
async def add_time_tracking_record(work_shift: dict, end_time: int):
|
||||
user_id = work_shift["user_id"]
|
||||
start_time = work_shift["start_time"]
|
||||
|
||||
user = await mongo.users_collection.find_one({"id": user_id}, {"_id": False})
|
||||
pay_rate = await mongo.pay_rates_collection.find_one(
|
||||
{"id": user.get("payRate", {}).get("id")},
|
||||
{"_id": False}
|
||||
)
|
||||
|
||||
pauses = work_shift.get("pauses", [])
|
||||
total_pause_time = sum(
|
||||
pause["end"] - pause["start"]
|
||||
for pause in pauses
|
||||
if "start" in pause and "end" in pause
|
||||
)
|
||||
|
||||
worked_time = max(end_time - start_time - total_pause_time, 0)
|
||||
total_hours = round(worked_time / 3_600_000, 2)
|
||||
total_amount = 0.0
|
||||
|
||||
if pay_rate and pay_rate["payrollScheme"]["key"] == "hourly":
|
||||
overtime_threshold = pay_rate.get("overtimeThreshold", 0)
|
||||
base_hours = min(total_hours, overtime_threshold)
|
||||
overtime_hours = max(total_hours - overtime_threshold, 0)
|
||||
|
||||
base_rate = pay_rate.get("baseRate", 0)
|
||||
overtime_rate = pay_rate.get("overtimeRate", base_rate)
|
||||
|
||||
total_amount = round(base_hours * base_rate + overtime_hours * overtime_rate, 2)
|
||||
|
||||
today_date = datetime.now().strftime("%Y-%m-%dT00:00:00.000")
|
||||
existing = await mongo.time_tracking_collection.find_one({
|
||||
"userId": user_id,
|
||||
"date": today_date
|
||||
})
|
||||
|
||||
if existing:
|
||||
await mongo.time_tracking_collection.update_one(
|
||||
{"_id": existing["_id"]},
|
||||
{
|
||||
"$inc": {
|
||||
"hours": total_hours,
|
||||
"amount": total_amount
|
||||
}
|
||||
}
|
||||
)
|
||||
else:
|
||||
await mongo.time_tracking_collection.insert_one({
|
||||
"id": await mongo.get_next_id(mongo.time_tracking_collection),
|
||||
"date": today_date,
|
||||
"userId": user_id,
|
||||
"hours": total_hours,
|
||||
"amount": total_amount
|
||||
})
|
||||
|
||||
|
||||
def get_time_millis():
|
||||
return int(time() * 1000)
|
||||
Reference in New Issue
Block a user