Added scheduled end of shift message & cleaned up tv mode layout
This commit is contained in:
2
convex/_generated/api.d.ts
vendored
2
convex/_generated/api.d.ts
vendored
@@ -15,6 +15,7 @@ import type {
|
||||
} from "convex/server";
|
||||
import type * as CustomPassword from "../CustomPassword.js";
|
||||
import type * as auth from "../auth.js";
|
||||
import type * as crons from "../crons.js";
|
||||
import type * as files from "../files.js";
|
||||
import type * as http from "../http.js";
|
||||
import type * as statuses from "../statuses.js";
|
||||
@@ -30,6 +31,7 @@ import type * as statuses from "../statuses.js";
|
||||
declare const fullApi: ApiFromModules<{
|
||||
CustomPassword: typeof CustomPassword;
|
||||
auth: typeof auth;
|
||||
crons: typeof crons;
|
||||
files: typeof files;
|
||||
http: typeof http;
|
||||
statuses: typeof statuses;
|
||||
|
@@ -36,6 +36,22 @@ export const getUser = query(async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
export const getAllUsers = query(async (ctx) => {
|
||||
const users = await ctx.db.query('users').collect();
|
||||
return users.map((u) => ({
|
||||
id: u._id,
|
||||
email: u.email ?? null,
|
||||
name: u.name ?? null,
|
||||
image: u.image ?? null,
|
||||
}));
|
||||
});
|
||||
|
||||
export const getAllUserIds = query(async (ctx) => {
|
||||
const users = await ctx.db.query('users').collect();
|
||||
const userIds = users.map((u) => u._id);
|
||||
return userIds;
|
||||
});
|
||||
|
||||
export const updateUserName = mutation({
|
||||
args: {
|
||||
name: v.string(),
|
||||
|
15
convex/crons.ts
Normal file
15
convex/crons.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// convex/crons.ts
|
||||
import { cronJobs } from 'convex/server';
|
||||
import { api } from './_generated/api';
|
||||
|
||||
const crons = cronJobs();
|
||||
|
||||
// Runs at 5:00 PM America/Chicago, Monday–Friday.
|
||||
// Convex will handle DST if your project version supports `timeZone`.
|
||||
crons.cron(
|
||||
'End of shift (weekdays 5pm CT)',
|
||||
'0 22 * * 1-5',
|
||||
api.statuses.endOfShiftUpdate,
|
||||
);
|
||||
|
||||
export default crons;
|
@@ -23,7 +23,7 @@ export default defineSchema({
|
||||
userId: v.id('users'),
|
||||
message: v.string(),
|
||||
updatedAt: v.number(),
|
||||
updatedBy: v.id('users'),
|
||||
updatedBy: v.optional(v.id('users')),
|
||||
})
|
||||
.index('by_user', ['userId'])
|
||||
.index('by_user_updatedAt', ['userId', 'updatedAt']),
|
||||
|
@@ -3,9 +3,12 @@ import { getAuthUserId } from '@convex-dev/auth/server';
|
||||
import {
|
||||
type MutationCtx,
|
||||
type QueryCtx,
|
||||
action,
|
||||
internalMutation,
|
||||
mutation,
|
||||
query,
|
||||
} from './_generated/server';
|
||||
import { api } from './_generated/api';
|
||||
import type { Doc, Id } from './_generated/dataModel';
|
||||
import { paginationOptsValidator } from 'convex/server';
|
||||
|
||||
@@ -135,6 +138,29 @@ export const bulkCreate = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Update all statuses for all users.
|
||||
*/
|
||||
export const updateAllStatuses = mutation({
|
||||
args: { message: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const userIds = await ctx.runQuery(api.auth.getAllUserIds);
|
||||
const updatedAt = Date.now();
|
||||
const statusIds: Id<'statuses'>[] = [];
|
||||
for (const userId of userIds) {
|
||||
await ensureUser(ctx, userId);
|
||||
const statusId = await ctx.db.insert('statuses', {
|
||||
message: args.message,
|
||||
userId,
|
||||
updatedAt,
|
||||
});
|
||||
await ctx.db.patch(userId, { currentStatusId: statusId });
|
||||
statusIds.push(statusId);
|
||||
}
|
||||
return { statusIds };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Current status for a specific user.
|
||||
* - Uses users.currentStatusId if present,
|
||||
@@ -199,7 +225,7 @@ export const getCurrentForAll = query({
|
||||
|
||||
// Updated by (if different) + URL
|
||||
let updatedByUser: StatusRow['user'] | null = null;
|
||||
if (curStatus && curStatus.updatedBy !== u._id) {
|
||||
if (curStatus && curStatus.updatedBy && curStatus.updatedBy !== u._id) {
|
||||
const updater = await ctx.db.get(curStatus.updatedBy);
|
||||
if (!updater) throw new ConvexError('Updater not found.');
|
||||
const updaterImageId = getImageId(updater);
|
||||
@@ -286,7 +312,9 @@ export const listHistory = query({
|
||||
for (const s of result.page) {
|
||||
const owner = await getDisplay(s.userId);
|
||||
const updatedBy =
|
||||
s.updatedBy !== s.userId ? await getDisplay(s.updatedBy) : null;
|
||||
(s.updatedBy && s.updatedBy !== s.userId)
|
||||
? await getDisplay(s.updatedBy)
|
||||
: null;
|
||||
|
||||
statuses.push({
|
||||
user: owner,
|
||||
@@ -309,3 +337,25 @@ export const listHistory = query({
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const endOfShiftUpdate = action({
|
||||
handler: async (ctx) => {
|
||||
const now = new Date(
|
||||
new Date().toLocaleString('en-US', {
|
||||
timeZone: 'America/Chicago',
|
||||
}),
|
||||
);
|
||||
const day = now.getDay();
|
||||
const hour = now.getHours();
|
||||
const minute = now.getMinutes();
|
||||
if (day == 0 || day === 6) return;
|
||||
if (hour === 12) {
|
||||
await ctx.runMutation(api.statuses.updateAllStatuses, {
|
||||
message: 'End of shift',
|
||||
});
|
||||
} else if (hour === 11) {
|
||||
const ms = ((60-minute) % 60) * 60 * 1000;
|
||||
await ctx.scheduler.runAfter(ms, api.statuses.endOfShiftUpdate);
|
||||
} else return;
|
||||
}
|
||||
});
|
||||
|
@@ -11,16 +11,24 @@ const Header = (headerProps: ComponentProps<'header'>) => {
|
||||
|
||||
if (tvMode)
|
||||
return (
|
||||
<div className='absolute top-16 right-20'>
|
||||
<Controls />
|
||||
</div>
|
||||
<header
|
||||
{...headerProps}
|
||||
className={cn(
|
||||
'w-full px-4 md:px-6 lg:px-20 my-8',
|
||||
headerProps?.className,
|
||||
)}
|
||||
>
|
||||
<div className='flex-1 flex justify-end mt-5'>
|
||||
<Controls />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
return (
|
||||
<header
|
||||
{...headerProps}
|
||||
className={cn(
|
||||
'w-full min-h-[10vh] px-4 md:px-6 lg:px-20 my-8',
|
||||
'w-full px-4 md:px-6 lg:px-20 my-8',
|
||||
headerProps?.className,
|
||||
)}
|
||||
>
|
||||
|
@@ -118,9 +118,9 @@ export const StatusList = ({
|
||||
|
||||
const containerCn = ccn({
|
||||
context: tvMode,
|
||||
className: 'w-full max-w-6xl mx-auto',
|
||||
on: 'p-8',
|
||||
off: 'px-6 py-4',
|
||||
className: 'w-full max-w-4xl mx-auto',
|
||||
on: 'px-12 max-w-3xl',
|
||||
off: 'px-6',
|
||||
});
|
||||
|
||||
const tabsCn = ccn({
|
||||
@@ -212,7 +212,7 @@ export const StatusList = ({
|
||||
fullName={u.name ?? 'User'}
|
||||
className={`
|
||||
transition-all duration-500 ring-2 ring-transparent
|
||||
${tvMode ? 'w-16 h-16' : 'w-12 h-12'}
|
||||
${tvMode ? 'w-18 h-18' : 'w-15 h-15'}
|
||||
${isAnimating ? 'ring-primary/30 ring-4' : ''}
|
||||
`}
|
||||
/>
|
||||
@@ -223,8 +223,8 @@ export const StatusList = ({
|
||||
<div className='flex items-center gap-3 mb-2'>
|
||||
<h3
|
||||
className={`
|
||||
font-semibold truncate
|
||||
${tvMode ? 'text-2xl' : 'text-lg'}
|
||||
font-bold truncate
|
||||
${tvMode ? 'text-3xl' : 'text-2xl'}
|
||||
`}
|
||||
>
|
||||
{u.name ?? u.email ?? 'User'}
|
||||
@@ -232,15 +232,15 @@ export const StatusList = ({
|
||||
|
||||
{isUpdatedByOther && s?.updatedBy && (
|
||||
<div className='flex items-center gap-2 text-muted-foreground'>
|
||||
<span className={tvMode ? 'text-sm' : 'text-xs'}>
|
||||
<span className={tvMode ? 'text-lg' : 'text-base'}>
|
||||
via
|
||||
</span>
|
||||
<BasedAvatar
|
||||
src={s.updatedBy.imageUrl}
|
||||
fullName={s.updatedBy.name ?? 'User'}
|
||||
className={tvMode ? 'w-5 h-5' : 'w-4 h-4'}
|
||||
className={tvMode ? 'w-6 h-6' : 'w-4 h-4'}
|
||||
/>
|
||||
<span className={tvMode ? 'text-sm' : 'text-xs'}>
|
||||
<span className={tvMode ? 'text-lg font-semibold' : 'text-base'}>
|
||||
{s.updatedBy.name ??
|
||||
s.updatedBy.email ??
|
||||
'another user'}
|
||||
@@ -252,7 +252,7 @@ export const StatusList = ({
|
||||
<div
|
||||
className={`
|
||||
mb-3 leading-relaxed
|
||||
${tvMode ? 'text-xl' : 'text-base'}
|
||||
${tvMode ? 'text-2xl' : 'text-xl'}
|
||||
${s ? 'text-foreground' : 'text-muted-foreground italic'}
|
||||
`}
|
||||
>
|
||||
@@ -262,25 +262,25 @@ export const StatusList = ({
|
||||
{/* Time Info */}
|
||||
<div className='flex items-center gap-4 text-muted-foreground'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Clock className={tvMode ? 'w-4 h-4' : 'w-3 h-3'} />
|
||||
<span className={tvMode ? 'text-base' : 'text-sm'}>
|
||||
<Clock className={tvMode ? 'w-5 h-5' : 'w-4 h-4'} />
|
||||
<span className={tvMode ? 'text-lg' : 'text-base'}>
|
||||
{s ? formatTime(s.updatedAt) : '--:--'}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Calendar
|
||||
className={tvMode ? 'w-4 h-4' : 'w-3 h-3'}
|
||||
className={tvMode ? 'w-5 h-5' : 'w-4 h-4'}
|
||||
/>
|
||||
<span className={tvMode ? 'text-base' : 'text-sm'}>
|
||||
<span className={tvMode ? 'text-lg' : 'text-base'}>
|
||||
{s ? formatDate(s.updatedAt) : '--/--'}
|
||||
</span>
|
||||
</div>
|
||||
{s && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<Activity
|
||||
className={tvMode ? 'w-4 h-4' : 'w-3 h-3'}
|
||||
className={tvMode ? 'w-5 h-5' : 'w-4 h-4'}
|
||||
/>
|
||||
<span className={tvMode ? 'text-base' : 'text-sm'}>
|
||||
<span className={tvMode ? 'text-lg' : 'text-base'}>
|
||||
{getStatusAge(s.updatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
@@ -83,7 +83,7 @@ export const StatusTable = ({
|
||||
const headerCn = ccn({
|
||||
context: tvMode,
|
||||
className: 'w-full mb-2 flex justify-between',
|
||||
on: 'mt-25',
|
||||
on: '',
|
||||
off: 'mb-2',
|
||||
});
|
||||
const thCn = ccn({
|
||||
@@ -95,8 +95,8 @@ export const StatusTable = ({
|
||||
const tdCn = ccn({
|
||||
context: tvMode,
|
||||
className: 'py-2 px-2 border',
|
||||
on: 'lg:text-4xl',
|
||||
off: 'lg:text-3xl',
|
||||
on: 'lg:text-5xl',
|
||||
off: 'lg:text-4xl',
|
||||
});
|
||||
const tCheckboxCn = `py-3 px-4 border`;
|
||||
const checkBoxCn = `lg:scale-200 cursor-pointer`;
|
||||
@@ -174,11 +174,7 @@ export const StatusTable = ({
|
||||
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
|
||||
/>
|
||||
<div>
|
||||
<p
|
||||
className={`font-semibold ${tvMode ? 'text-5xl' : 'text-4xl'}`}
|
||||
>
|
||||
{u.name ?? 'Technician #' + (i + 1)}
|
||||
</p>
|
||||
<p> {u.name ?? 'Technician #' + (i + 1)} </p>
|
||||
{s?.updatedBy && s.updatedBy.id !== u.id && (
|
||||
<div className='flex items-center gap-1 text-muted-foreground'>
|
||||
<BasedAvatar
|
||||
@@ -209,13 +205,17 @@ export const StatusTable = ({
|
||||
<Clock
|
||||
className={`${tvMode ? 'lg:w-11 lg:h-11' : 'lg:w-9 lg:h-9'}`}
|
||||
/>
|
||||
{s ? formatTime(s.updatedAt) : '--:--'}
|
||||
<p className={`${tvMode ? 'text-4xl' : 'text-3xl'}`}>
|
||||
{s ? formatTime(s.updatedAt) : '--:--'}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex gap-4 my-1'>
|
||||
<Calendar
|
||||
className={`${tvMode ? 'lg:w-11 lg:h-11' : 'lg:w-9 lg:h-9'}`}
|
||||
/>
|
||||
{s ? formatDate(s.updatedAt) : '--:--'}
|
||||
<p className={`${tvMode ? 'text-4xl' : 'text-3xl'}`}>
|
||||
{s ? formatDate(s.updatedAt) : '--:--'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user