diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index ea7f6a1..c076b6b 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -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; diff --git a/convex/auth.ts b/convex/auth.ts index 23e1e0b..17076f8 100644 --- a/convex/auth.ts +++ b/convex/auth.ts @@ -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(), diff --git a/convex/crons.ts b/convex/crons.ts new file mode 100644 index 0000000..84f5839 --- /dev/null +++ b/convex/crons.ts @@ -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; diff --git a/convex/schema.ts b/convex/schema.ts index fb6ae8f..4b37535 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -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']), diff --git a/convex/statuses.ts b/convex/statuses.ts index c25e40b..7be89bc 100644 --- a/convex/statuses.ts +++ b/convex/statuses.ts @@ -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; + } +}); diff --git a/src/components/layout/header/index.tsx b/src/components/layout/header/index.tsx index 26fc21b..f0bd02e 100644 --- a/src/components/layout/header/index.tsx +++ b/src/components/layout/header/index.tsx @@ -11,16 +11,24 @@ const Header = (headerProps: ComponentProps<'header'>) => { if (tvMode) return ( -
- -
+
+
+ +
+
); return (
diff --git a/src/components/layout/status/list/index.tsx b/src/components/layout/status/list/index.tsx index ed91c1d..ef0d456 100644 --- a/src/components/layout/status/list/index.tsx +++ b/src/components/layout/status/list/index.tsx @@ -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 = ({

{u.name ?? u.email ?? 'User'} @@ -232,15 +232,15 @@ export const StatusList = ({ {isUpdatedByOther && s?.updatedBy && (
- + via - + {s.updatedBy.name ?? s.updatedBy.email ?? 'another user'} @@ -252,7 +252,7 @@ export const StatusList = ({
@@ -262,25 +262,25 @@ export const StatusList = ({ {/* Time Info */}
- - + + {s ? formatTime(s.updatedAt) : '--:--'}
- + {s ? formatDate(s.updatedAt) : '--/--'}
{s && (
- + {getStatusAge(s.updatedAt)}
diff --git a/src/components/layout/status/table/index.tsx b/src/components/layout/status/table/index.tsx index 4c4dfe7..3e43e8d 100644 --- a/src/components/layout/status/table/index.tsx +++ b/src/components/layout/status/table/index.tsx @@ -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'} />
-

- {u.name ?? 'Technician #' + (i + 1)} -

+

{u.name ?? 'Technician #' + (i + 1)}

{s?.updatedBy && s.updatedBy.id !== u.id && (
- {s ? formatTime(s.updatedAt) : '--:--'} +

+ {s ? formatTime(s.updatedAt) : '--:--'} +

- {s ? formatDate(s.updatedAt) : '--:--'} +

+ {s ? formatDate(s.updatedAt) : '--:--'} +