Added scheduled end of shift message & cleaned up tv mode layout

This commit is contained in:
2025-09-11 12:27:21 -05:00
parent 52be5c93f4
commit 136047ca25
8 changed files with 124 additions and 33 deletions

View File

@@ -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;

View File

@@ -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
View 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, MondayFriday.
// 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;

View File

@@ -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']),

View File

@@ -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;
}
});

View File

@@ -11,16 +11,24 @@ const Header = (headerProps: ComponentProps<'header'>) => {
if (tvMode)
return (
<div className='absolute top-16 right-20'>
<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,
)}
>

View File

@@ -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>

View File

@@ -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'}`}
/>
<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'}`}
/>
<p className={`${tvMode ? 'text-4xl' : 'text-3xl'}`}>
{s ? formatDate(s.updatedAt) : '--:--'}
</p>
</div>
</div>
</div>