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