Overtime in MySQL timecard over two weeks - mysql

I'm trying to figure out how to calculate the overtime on timecard bills that have been or need to be paid. The problem is that bills cover more than one weeks worth of hours and the query grabs more than one employee's history at a time. Any suggestions on how to do this, perhaps with a case statement of some kind?
For example, suppose in my list of employees one works 39 hours one week and 45 the next. The bill would show 84 hours worked, and would also need to show 5 hours of overtime (not four!). This needs to be done in the context of the below query, which handles multiple bills and multiple employees.
Note the query below shows how this would work if the billing period was only one week.
select
username,
CASE
WHEN paidOn IS NULL THEN 'Unpaid'
ELSE paidOn
END as paid,
round(sum(TIME_TO_SEC(TIMEDIFF(timeOut, timeIn)))/3600,2) AS hours
, CASE
WHEN round(sum(TIME_TO_SEC(TIMEDIFF(timeOut, timeIn)))/3600,2) > 40
THEN round((sum(TIME_TO_SEC(TIMEDIFF(timeOut, timeIn)))/3600 - 40) * payrate + 40 * payrate,2)
ELSE
round(sum(TIME_TO_SEC(TIMEDIFF(timeOut, timeIn)))/3600 * payrate, 2)
END as pay
from
timecard
LEFT JOIN
employees
ON
employees.userID = timecard.userID
WHERE
paid != 'd'
GROUP BY
paidOn, timecard.userID
ORDER BY
paid DESC
LIMIT 30;

The way I understand it is that this works fine if the billing period is only 1 week, but not when you extend it to multiple weeks. Then I would simply use the query you have above as a subquery and then aggregate your weeks together. Here's a short example of what I mean:
SELECT
EmployeeId,
SUM(RegularPayHours) * RegularPayRate,
SUM(OverTimeHours) * OverTimeRate
FROM
(SELECT
EmployeeId,
DATEPART(week,TimeCardDate) AS [WorkWeek],
CASE WHEN SUM(HoursWorked) > 40 THEN 40 ELSE SUM(HoursWorked) END AS [RegularPayHours],
CASE WHEN SUM(HoursWorked) > 40 THEN SUM(HoursWorked) - 40 ELSE 0 END AS [OvertimeHours]
FROM
TimeCard
WHERE
TimeCardDate BETWEEN StartDate AND EndDate
GROUP BY
EmployeeId,
DATEPART(week,TimeCardDate)
) a
WHERE
WorkWeek IN (1,2)
GROUP BY
EmployeeId
This will give you the first two work weeks combined as one result but with the overtime calculations done at the week level. You can essentially create any custom pay period that is X weeks long with this method.
You may need to custom define what a work-week is by setting the DATEFIRST value.

Related

How to query available item leases based on a date range in MySQL?

We have a business that rents out international phone numbers to customers when traveling. When a customer makes an order We want to display to the customer the available phone numbers for his booking dates based on his start_date and end_date and numbers which is not occupied yet.
Since these phone numbers are rented out, I need to select from the table ONLY those numbers that are not rented out yet for dates that would interfere with the current customers dates.
I also don't want to rent out any phone number prior to 7 days after its end date. Meaning, If a customer booked a phone number for 1-1-2020 through 1-20-2020, I don't want this phone number to be booked by another customer before 1-27-2020. I want the phone number to have a 7 day window of being clear.
I have a table with the phone numbers and a table with the orders that is related to the phone numbers table via phone_number_id. The orders table has the current customers start_date and end_date for travel without the phone number id saved yet to it. The orders table also has the start_date and end_date for all other customers dates of travel as well as which phone_number_id was assigned/booked up for their travel dates.
How would the MySQL query look like when trying to select the phone numbers that are available for the current customers dates?
I build below query at the moment
SELECT x.id
, x.area_code
, x.phone_number
, y.start_date
, y.end_date
FROM vir_num_table x
LEFT
JOIN orderitemsdetail_table y
ON y.vn_id = x.id
WHERE y.start_date BETWEEN '2020-01-11' AND '2020-01-18'
OR y.start_date IS NULL
I've build this query but stuck here how can I add end_date logic.
Any help would be appreciated! Thanks in advance.
The way I'd approach the problem would be to look at conceptually, is as a cross product of the set of all phone numbers, along with the reservation timeframe, and then exclude those where there's a conflicting reservation.
A conflict would be an overlap, existing reservation that has a start_date before the end of the proposed reservation AND has an end_date on or after the start of the proposed reservation.
I'd do an anti-join pattern, something like this:
SELECT pn.phone_number
FROM phone_number pn
LEFT
JOIN reservation rs
ON rs.phone_number = pn.phone_number
AND rs.start_dt <= '2019-12-27' + INTERVAL +7 DAY
AND rs.end_dt > '2019-12-20' + INTERVAL -7 DAY
WHERE rs.phone_number IS NULL
That essentially says get all rows from phone number, along with matching rows from reservations (rows that overlap), but then exclude all the rows that had a match, leaving just phone_number rows that did not have a match.
We can make the < test a <= or , subtract 8 days, to tailor the "7 day" window before; we can tweak as we run the query through the test cases,
We can achieve an equivalent result using a NOT EXISTS and a correlated subquery. Some people find this easier to comprehend than the ant-join, but its essentially the same query, doing the same thing, get all rows from phone_number but exclude the rows where there is a matching (overlapping) row in reservation
SELECT pn.phone_number
FROM phone_number pn
WHERE NOT EXISTS
( SELECT 1
FROM reservation rs
WHERE rs.phone_number = pn.phone_number
AND rs.start_dt <= '2019-12-27' + INTERVAL +7 DAY
AND rs.end_dt > '2019-12-20' + INTERVAL -7 DAY
)
There are several questions on StackOverflow about checking for overlap, or no overlap, of date ranges.
See e.g.
How to check if two date ranges overlap in mysql?
PHP/SQL - How can I check if a date user input is in between an existing date range in my database?
MySQL query to select distinct rows based on date range overlapping
EDIT
Based on the SQL added as an edit to the question, I'd do the query like this:
SELECT pn.`id`
, pn.`area_code`
, pn.`phone_number`
FROM `vir_num_table` pn
LEFT
JOIN `orderitemsdetail_table` rs
ON rs.vn_id = pn.id
AND rs.start_date <= '2020-01-18' + INTERVAL +7 DAY
AND rs.end_date > '2020-01-11' + INTERVAL -7 DAY
WHERE rs.vn_id IS NULL
The two "tricky" parts. First is the anti-join, understanding how that works. (An outer join, to return all rows from vir_num_table but exclude any rows that have a matching row in reservations. The second tricky part is checking for the overlap, coming up with the conditions: r.start <= p.end AND r.end >= p.start, then tweaking whether we want to include the equals as an overlap, and tweaking the extra seven days (easiest to me to just subtract the 7 days from the beginning of the proposed reservation)
... now occurs to me like we need to add a guard period of 7 days on the end of the reservation period as well, doh!
Here's a query plus sorting algo to choose the optimal phone number selection for maximum utilization efficiency (i.e. getting as close as possible to exactly 7 days before and after each use).
I set it to give open ends a weight of 9, so that "near perfect" fits (7-8 days before or after) would be selected ahead of open-ended numbers. This will yield a slight efficiency improvement, as open numbers can accommodate any reservation. You can adjust this for your needs. If you set this to 0, for example, it would always select open numbers first.
SELECT ph.phone_number,
COALESCE(
MIN(
IF(res.end_date > res.start_date > '2020-01-18',
NULL, -- ignore before-comparison for reservations starting and ending after date range
DATEDIFF('2020-01-11', res.end_date)
), 9) AS open_days_before,
COALESCE(
MIN(
IF(res.start_date < res.end_date < '2020-01-11',
NULL, -- ignore after-comparison for reservations starting and ending before date range
DATEDIFF(res.start_date, '2020-01-18')
), 9) AS open_days_after
FROM phone_number ph
LEFT JOIN reservation res
ON res.phone_number = ph.phone_number
AND res.end_date >= CURRENT_DATE() - INTERVAL 6 DAY
GROUP BY ph.phone_number
HAVING open_days_before >= 7
AND open_days_after >= 7
ORDER BY open_days_before + open_days_after
LIMIT 1
Edit: updated to add grouping, because I realize this is an aggregate problem.
Edit 2: bug fix, changed MAX to MIN
Edit 3: added res.end_date >= CURRENT_DATE - INTERVAL 6 DAY to ignore past reservations, limiting aggregate data and treating phone number with no reservations between 6 days ago and the beginning of the new order as "open on the front-end"
Edit 4: added IF conditions to eliminate reservations outside the given before-or-after comparison ranges (e.g. comparing reservations after the selected range from influencing the "open days before" number), to prevent negative numbers, except when there's overlap with the selected range.
Based on the info you've added then you shouldn't need to check the start date of phone numbers which have been booked out.
You customer provides you with a start date and an end date.
You only rent out phone numbers 7 days after their last lease ended
All you need to do is fetch back phone numbers which either:
- Are not rented out and therefor aren't in the orderitems table
- OR have an end_date which is 7 days before the new customer's start date.
Here you go:
SELECT
`main_table`.`id`,
`main_table`.`area_code`,
`main_table`.`phone_number`,
`orderitemsdetail_table`.`start_date`,
`orderitemsdetail_table`.`end_date`
FROM
`vir_num_table` AS `main_table`
LEFT JOIN
`orderitemsdetail_table` AS `orderitemsdetail_table` ON main_table.id = orderitemsdetail_table.vn_id
WHERE
(DATE_ADD(orderitemsdetail_table.end_date, INTERVAL 7 DAY) < '<CUSTOMER START DATE>'
AND orderitemsdetail_table.start_date > '<CUSTOMER END DATE>')
OR orderitemsdetail_table.id IS NULL

create a ranking and statistics with repeated database records

Today I want to get a help in creating scores per user in my database. I have this query:
SELECT
r1.id,
r1.nickname,
r1.fecha,
r1.bestia1,
r1.bestia2,
r1.bestia3,
r1.bestia4
r1.bestia5
FROM
reporte AS r1
INNER JOIN
( SELECT
nickname, MAX(fecha) AS max_date
FROM
reporte
GROUP BY
nickname ) AS latests_reports
ON latests_reports.nickname = r1.nickname
AND latests_reports.max_date = r1.fecha
ORDER BY
r1.fecha DESC
that's from a friend from this site who helped me in get "the last record per user in each day", based on this I am looking how to count the results in a ranking daily, weekly or monthly, in order to use statistics charts or google datastudio, I've tried the next:
select id, nickname, sum(bestia1), sum(bestia2), etc...
But its not giving the complete result which I want. That's why I am looking for help. Additionally I know datastudio filters where I can show many charts but still I can count completely.
for example, one player in the last 30 days reported 265 monsters killed, but when I use in datastudio my query it counts only the latest value (it can be 12). so I want to count correctly in order to use with charts
SQL records filtered with my query:
One general approach for get the total monsters killed by each user on the latest X days and make a score calculation like the one you propose on the commentaries can be like this:
SET #daysOnHistory = X; -- Where X should be an integer positive number (like 10).
SELECT
nickname,
SUM(bestia1) AS total_bestia1_killed,
SUM(bestia2) AS total_bestia2_killed,
SUM(bestia3) AS total_bestia3_killed,
SUM(bestia4) AS total_bestia4_killed,
SUM(bestia5) AS total_bestia5_killed,
SUM(bestia1 + bestia2 + bestia3 + bestia4 + bestia5) AS total_monsters_killed,
SUM(bestia1 + 2 * bestia2 + 3 * bestia3 + 4 * bestia4 + 5 * bestia5) AS total_score
FROM
reporte
WHERE
fecha >= DATE_ADD(DATE(NOW()), INTERVAL -#daysOnHistory DAY)
GROUP BY
nickname
ORDER BY
total_score DESC
Now, if you want the same calculation but only taking into account the days of the current week (assuming a week starts on Monday), you need to replace the previous WHERE clause by next one:
WHERE
fecha >= DATE_ADD(DATE(NOW()), INTERVAL -WEEKDAY(NOW()) DAY)
Even more, if you want all the same, but only taking into account the days of the current month, you need to replace the WHERE clause by:
WHERE
MONTH(fecha) = MONTH(NOW())
For evaluate the statistics on the days of the current year, you need to replace the WHERE clause by:
WHERE
YEAR(fecha) = YEAR(NOW())
And finally, for evaluation on a specific range of days you can use, for example:
WHERE
DATE(fecha) BETWEEN CAST("2018-10-15" AS DATE) AND CAST('2018-11-10' AS DATE)
I hope this guide will help you and clarify your outlook.
This will give you number of monster killed in the last 30 days per user :
SELECT
nickname,
sum(bestia1) as bestia1,
sum(bestia2) as bestia2,
sum(bestia3) as bestia3,
sum(bestia4) as bestia4,
sum(bestia5) as bestia5
FROM
reporte
WHERE fecha >= DATE_ADD(curdate(), interval -30 day)
GROUP BY nickName
ORDER BY

TSQL - Calculate time between punches but exclude break time

Not sure if this is a unique question or not.
I'm needing to get calculated punch in and punch out times for a labor tracking system. Our crew has breaks from 10-10:20 AM and from 1-1:20 PM.
What I need to figure out is how to subtract this time from a total if they are still punched in during these breaks.
For example, if Joe punches in to a job at 09:53 and punches out at 10:23, I want it to show 10 minutes instead of 30.
How could I do this for few "blackout" times of 10-10:20, 1-1:20, 11-11:20, and 5-5:20?
This return the total of work minutes. This check if the worker time overlap with each break time and then change it to indicate how much overlap was.
Then calculate the total of minutes in break and finally subtract for the total of time worker punch in_out.
SQL DEMO
WITH time_off as (
SELECT * ,
CASE WHEN w.in_w < b.out_b AND w.out_w > b.in_b
THEN 'overlap'
END as overlap,
CASE WHEN w.in_w < b.in_b
THEN b.in_b
ELSE w.in_w
END as break_start,
CASE WHEN w.out_w > b.out_b
THEN b.out_b
ELSE w.out_w
END as break_end
FROM workers w
CROSS JOIN breaks b
), break_total as (
SELECT worker_id, in_w, out_w, SUM (CASE WHEN overlap = 'overlap'
THEN datediff(minute, break_start,break_end)
ELSE 0
END) as break_total
FROM time_off
GROUP BY worker_id, in_w, out_w
)
SELECT worker_id,
datediff(minute, in_w, out_w) - break_total as total_minutes
FROM break_total
For some debug do:
SELECT * FROM time_off;
SELECT * FROM break_total;

Calculating patient census by hour

I am trying to build a query that calculates number of patients in the emergency room by hour. I have each patients arrival and departure times. I tried building a boolean style query but all it did was give me the arrivals by hour using this logic
SELECT MRN,
,CASE WHEN CAST(EDArrival AS TIME) between '00:00:00.000' and '00:59:59.000' then 1 else 0 end as Hour0
,CASE WHEN CAST(EDArrival AS TIME) between '01:00:00.000' and '01:59:59.000' then 1 else 0 end as Hour1
,CASE WHEN CAST(EDArrival AS TIME) between '02:00:00.000' and '02:59:59.000' then 1 else 0 end as Hour2
FROM EDArrivals
WHERE EDArrival between '2012-06-01' and '2013-07-01'
I was thinking maybe the query could place a column for each hour with a 1 or 0 in they were in the ED during those hours. What I ultimately want to get to is average patients in the ED by hour over the course of a year. If anyone can think of an easier method I would greatly appreciate the help.
Thank you
This probably won't perform great, but it will give the average for each hour over the time span you specify. The perf issue will be because of the function in the JOIN criteria in the CTE. If you need to do this for a very large number of rows it probably makes sense to break that out to another table and populate a column with the hour.
DECLARE #Hours TABLE (Hr smallint)
INSERT INTO #Hours
(Hr)
VALUES
(0)
,(1)
,(2)
,(3)
,(4)
,(5)
,(6)
,(7)
,(8)
,(9)
,(10)
,(11)
,(12)
,(13)
,(14)
,(15)
,(16)
,(17)
,(18)
,(19)
,(20)
,(21)
,(22)
,(23)
WITH ByDate
AS
(
SELECT
CAST(ED.EDArrival AS date) AS 'Dt',h.Hr, COUNT(*) AS 'PatientCount'
FROM
EDArrivals ED
JOIN
#Hours AS h
ON DATEPART(HOUR, ED.EDArrival) = h.Hr
WHERE
ED.EDArrival BETWEEN '2012-06-01' AND '2013-07-01'
GROUP BY
CAST(ED.EDArrival AS date)
,h.Hr
)
SELECT
Hr, AVG(PatientCount)
FROM
ByDate
GROUP BY
hr
ORDER BY
hr
I should also note that though you don't list it in your requirements, it probably makes more sense to also filter on the departure time is >= the given hour. You likely need to know not just how many patients show up but how many are sticking around at any given time.
I managed to create an example of my comment in SQLFiddle.
http://sqlfiddle.com/#!6/5234e/6
It's similar to JNK answer (hey, I commented first!)
By the way, creating that table variable will not be great, consider keeping a domain table with the hours.
If do you need performance consider also persisting the date part values. Evaluating them for each row is a performance killer.
Also take care with null departures date times and patients staying at midnight.
Have you tried using DateDiff:
SELECT DateDiff(n, startdate, enddate) FROM MyTable
SELECT COUNT(*) [TotalArrivals]
, DATEPART(hh, [EDArrival]) [Hour]
FROM [EDArrivals]
GROUP BY DATEPART(hh, [EDArrival])
This will get you the total arrivals grouped by hour. You can then use this to do your averages per hour / whatever other calculations you need. This wont give you the hours with no arrivals, but that should be easy to fit in to your calculations at the end.

Group by date from multiple columns?

first of all sorry for that title, but I have no idea how to describe it:
I'm saving sessions in my table and I would like to get the count of sessions per hour to know how many sessions were active over the day. The sessions are specified by two timestamps: start and end.
Hopefully you can help me.
Here we go:
http://sqlfiddle.com/#!2/bfb62/2/0
While I'm still not sure how you'd like to compare the start and end dates, looks like using COUNT, YEAR, MONTH, DAY, and HOUR, you could come up with your desired results.
Possibly something similar to this:
SELECT COUNT(ID), YEAR(Start), HOUR(Start), DAY(Start), MONTH(Start)
FROM Sessions
GROUP BY YEAR(Start), HOUR(Start), DAY(Start), MONTH(Start)
And the SQL Fiddle.
What you want to do is rather hard in MySQL. You can, however, get an approximation without too much difficulty. The following counts up users who start and stop within one day:
select date(start), hour,
sum(case when hours.hour between hour(start) and hours.hour then 1 else 0
end) as GoodEstimate
from sessions s cross join
(select 0 as hour union all
select 1 union all
. . .
select 23
) hours
group by date(start), hour
When a user spans multiple days, the query is harder. Here is one approach, that assumes that there exists a user who starts during every hour:
select thehour, count(*)
from (select distinct date(start), hour(start),
(cast(date(start) as datetime) + interval hour(start) hour as thehour
from sessions
) dh left outer join
sessions s
on s.start <= thehour + interval 1 hour and
s.end >= thehour
group by thehour
Note: these are untested so might have syntax errors.
OK, this is another problem where the index table comes to the rescue.
An index table is something that everyone should have in their toolkit, preferably in the master database. It is a table with a single id int primary key indexed column containing sequential numbers from 0 to n where n is a number big enough to do what you need, 100,000 is good, 1,000,000 is better. You only need to create this table once but once you do you will find it has all kinds of applications.
For your problem you need to consider each hour and, if I understand your problem you need to count every session that started before the end of the hour and hasn't ended before that hour starts.
Here is the SQL fiddle for the solution.
What it does is use a known sequential number from the indextable (only 0 to 100 for this fiddle - just over 4 days - you can see why you need a big n) to link with your data at the top and bottom of the hour.