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";
|
} from "convex/server";
|
||||||
import type * as CustomPassword from "../CustomPassword.js";
|
import type * as CustomPassword from "../CustomPassword.js";
|
||||||
import type * as auth from "../auth.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 files from "../files.js";
|
||||||
import type * as http from "../http.js";
|
import type * as http from "../http.js";
|
||||||
import type * as statuses from "../statuses.js";
|
import type * as statuses from "../statuses.js";
|
||||||
@@ -30,6 +31,7 @@ import type * as statuses from "../statuses.js";
|
|||||||
declare const fullApi: ApiFromModules<{
|
declare const fullApi: ApiFromModules<{
|
||||||
CustomPassword: typeof CustomPassword;
|
CustomPassword: typeof CustomPassword;
|
||||||
auth: typeof auth;
|
auth: typeof auth;
|
||||||
|
crons: typeof crons;
|
||||||
files: typeof files;
|
files: typeof files;
|
||||||
http: typeof http;
|
http: typeof http;
|
||||||
statuses: typeof statuses;
|
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({
|
export const updateUserName = mutation({
|
||||||
args: {
|
args: {
|
||||||
name: v.string(),
|
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'),
|
userId: v.id('users'),
|
||||||
message: v.string(),
|
message: v.string(),
|
||||||
updatedAt: v.number(),
|
updatedAt: v.number(),
|
||||||
updatedBy: v.id('users'),
|
updatedBy: v.optional(v.id('users')),
|
||||||
})
|
})
|
||||||
.index('by_user', ['userId'])
|
.index('by_user', ['userId'])
|
||||||
.index('by_user_updatedAt', ['userId', 'updatedAt']),
|
.index('by_user_updatedAt', ['userId', 'updatedAt']),
|
||||||
|
@@ -3,9 +3,12 @@ import { getAuthUserId } from '@convex-dev/auth/server';
|
|||||||
import {
|
import {
|
||||||
type MutationCtx,
|
type MutationCtx,
|
||||||
type QueryCtx,
|
type QueryCtx,
|
||||||
|
action,
|
||||||
|
internalMutation,
|
||||||
mutation,
|
mutation,
|
||||||
query,
|
query,
|
||||||
} from './_generated/server';
|
} from './_generated/server';
|
||||||
|
import { api } from './_generated/api';
|
||||||
import type { Doc, Id } from './_generated/dataModel';
|
import type { Doc, Id } from './_generated/dataModel';
|
||||||
import { paginationOptsValidator } from 'convex/server';
|
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.
|
* Current status for a specific user.
|
||||||
* - Uses users.currentStatusId if present,
|
* - Uses users.currentStatusId if present,
|
||||||
@@ -199,7 +225,7 @@ export const getCurrentForAll = query({
|
|||||||
|
|
||||||
// Updated by (if different) + URL
|
// Updated by (if different) + URL
|
||||||
let updatedByUser: StatusRow['user'] | null = null;
|
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);
|
const updater = await ctx.db.get(curStatus.updatedBy);
|
||||||
if (!updater) throw new ConvexError('Updater not found.');
|
if (!updater) throw new ConvexError('Updater not found.');
|
||||||
const updaterImageId = getImageId(updater);
|
const updaterImageId = getImageId(updater);
|
||||||
@@ -286,7 +312,9 @@ export const listHistory = query({
|
|||||||
for (const s of result.page) {
|
for (const s of result.page) {
|
||||||
const owner = await getDisplay(s.userId);
|
const owner = await getDisplay(s.userId);
|
||||||
const updatedBy =
|
const updatedBy =
|
||||||
s.updatedBy !== s.userId ? await getDisplay(s.updatedBy) : null;
|
(s.updatedBy && s.updatedBy !== s.userId)
|
||||||
|
? await getDisplay(s.updatedBy)
|
||||||
|
: null;
|
||||||
|
|
||||||
statuses.push({
|
statuses.push({
|
||||||
user: owner,
|
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)
|
if (tvMode)
|
||||||
return (
|
return (
|
||||||
<div className='absolute top-16 right-20'>
|
<header
|
||||||
<Controls />
|
{...headerProps}
|
||||||
</div>
|
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 (
|
return (
|
||||||
<header
|
<header
|
||||||
{...headerProps}
|
{...headerProps}
|
||||||
className={cn(
|
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,
|
headerProps?.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@@ -118,9 +118,9 @@ export const StatusList = ({
|
|||||||
|
|
||||||
const containerCn = ccn({
|
const containerCn = ccn({
|
||||||
context: tvMode,
|
context: tvMode,
|
||||||
className: 'w-full max-w-6xl mx-auto',
|
className: 'w-full max-w-4xl mx-auto',
|
||||||
on: 'p-8',
|
on: 'px-12 max-w-3xl',
|
||||||
off: 'px-6 py-4',
|
off: 'px-6',
|
||||||
});
|
});
|
||||||
|
|
||||||
const tabsCn = ccn({
|
const tabsCn = ccn({
|
||||||
@@ -212,7 +212,7 @@ export const StatusList = ({
|
|||||||
fullName={u.name ?? 'User'}
|
fullName={u.name ?? 'User'}
|
||||||
className={`
|
className={`
|
||||||
transition-all duration-500 ring-2 ring-transparent
|
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' : ''}
|
${isAnimating ? 'ring-primary/30 ring-4' : ''}
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
@@ -223,8 +223,8 @@ export const StatusList = ({
|
|||||||
<div className='flex items-center gap-3 mb-2'>
|
<div className='flex items-center gap-3 mb-2'>
|
||||||
<h3
|
<h3
|
||||||
className={`
|
className={`
|
||||||
font-semibold truncate
|
font-bold truncate
|
||||||
${tvMode ? 'text-2xl' : 'text-lg'}
|
${tvMode ? 'text-3xl' : 'text-2xl'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{u.name ?? u.email ?? 'User'}
|
{u.name ?? u.email ?? 'User'}
|
||||||
@@ -232,15 +232,15 @@ export const StatusList = ({
|
|||||||
|
|
||||||
{isUpdatedByOther && s?.updatedBy && (
|
{isUpdatedByOther && s?.updatedBy && (
|
||||||
<div className='flex items-center gap-2 text-muted-foreground'>
|
<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
|
via
|
||||||
</span>
|
</span>
|
||||||
<BasedAvatar
|
<BasedAvatar
|
||||||
src={s.updatedBy.imageUrl}
|
src={s.updatedBy.imageUrl}
|
||||||
fullName={s.updatedBy.name ?? 'User'}
|
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.name ??
|
||||||
s.updatedBy.email ??
|
s.updatedBy.email ??
|
||||||
'another user'}
|
'another user'}
|
||||||
@@ -252,7 +252,7 @@ export const StatusList = ({
|
|||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
mb-3 leading-relaxed
|
mb-3 leading-relaxed
|
||||||
${tvMode ? 'text-xl' : 'text-base'}
|
${tvMode ? 'text-2xl' : 'text-xl'}
|
||||||
${s ? 'text-foreground' : 'text-muted-foreground italic'}
|
${s ? 'text-foreground' : 'text-muted-foreground italic'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
@@ -262,25 +262,25 @@ export const StatusList = ({
|
|||||||
{/* Time Info */}
|
{/* Time Info */}
|
||||||
<div className='flex items-center gap-4 text-muted-foreground'>
|
<div className='flex items-center gap-4 text-muted-foreground'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Clock className={tvMode ? 'w-4 h-4' : 'w-3 h-3'} />
|
<Clock 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 ? formatTime(s.updatedAt) : '--:--'}
|
{s ? formatTime(s.updatedAt) : '--:--'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Calendar
|
<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) : '--/--'}
|
{s ? formatDate(s.updatedAt) : '--/--'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{s && (
|
{s && (
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Activity
|
<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)}
|
{getStatusAge(s.updatedAt)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -83,7 +83,7 @@ export const StatusTable = ({
|
|||||||
const headerCn = ccn({
|
const headerCn = ccn({
|
||||||
context: tvMode,
|
context: tvMode,
|
||||||
className: 'w-full mb-2 flex justify-between',
|
className: 'w-full mb-2 flex justify-between',
|
||||||
on: 'mt-25',
|
on: '',
|
||||||
off: 'mb-2',
|
off: 'mb-2',
|
||||||
});
|
});
|
||||||
const thCn = ccn({
|
const thCn = ccn({
|
||||||
@@ -95,8 +95,8 @@ export const StatusTable = ({
|
|||||||
const tdCn = ccn({
|
const tdCn = ccn({
|
||||||
context: tvMode,
|
context: tvMode,
|
||||||
className: 'py-2 px-2 border',
|
className: 'py-2 px-2 border',
|
||||||
on: 'lg:text-4xl',
|
on: 'lg:text-5xl',
|
||||||
off: 'lg:text-3xl',
|
off: 'lg:text-4xl',
|
||||||
});
|
});
|
||||||
const tCheckboxCn = `py-3 px-4 border`;
|
const tCheckboxCn = `py-3 px-4 border`;
|
||||||
const checkBoxCn = `lg:scale-200 cursor-pointer`;
|
const checkBoxCn = `lg:scale-200 cursor-pointer`;
|
||||||
@@ -174,11 +174,7 @@ export const StatusTable = ({
|
|||||||
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
|
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p> {u.name ?? 'Technician #' + (i + 1)} </p>
|
||||||
className={`font-semibold ${tvMode ? 'text-5xl' : 'text-4xl'}`}
|
|
||||||
>
|
|
||||||
{u.name ?? 'Technician #' + (i + 1)}
|
|
||||||
</p>
|
|
||||||
{s?.updatedBy && s.updatedBy.id !== u.id && (
|
{s?.updatedBy && s.updatedBy.id !== u.id && (
|
||||||
<div className='flex items-center gap-1 text-muted-foreground'>
|
<div className='flex items-center gap-1 text-muted-foreground'>
|
||||||
<BasedAvatar
|
<BasedAvatar
|
||||||
@@ -209,13 +205,17 @@ export const StatusTable = ({
|
|||||||
<Clock
|
<Clock
|
||||||
className={`${tvMode ? 'lg:w-11 lg:h-11' : 'lg:w-9 lg:h-9'}`}
|
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>
|
||||||
<div className='flex gap-4 my-1'>
|
<div className='flex gap-4 my-1'>
|
||||||
<Calendar
|
<Calendar
|
||||||
className={`${tvMode ? 'lg:w-11 lg:h-11' : 'lg:w-9 lg:h-9'}`}
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user