Add automatic lunch feature. Clean up some code.
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
import { type ChangeEvent, useRef, useState } from 'react';
|
import { type ChangeEvent, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
type Preloaded,
|
type Preloaded,
|
||||||
@@ -49,7 +48,7 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
|||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const generateUploadUrl = useMutation(api.files.generateUploadUrl);
|
const generateUploadUrl = useMutation(api.files.generateUploadUrl);
|
||||||
const updateUserImage = useMutation(api.auth.updateUserImage);
|
const updateUser = useMutation(api.auth.updateUser);
|
||||||
|
|
||||||
const currentImageUrl = useQuery(
|
const currentImageUrl = useQuery(
|
||||||
api.files.getImageUrl,
|
api.files.getImageUrl,
|
||||||
@@ -98,7 +97,7 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
|||||||
storageId: Id<'_storage'>;
|
storageId: Id<'_storage'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
await updateUserImage({ storageId: uploadResponse.storageId });
|
await updateUser({ image: uploadResponse.storageId });
|
||||||
|
|
||||||
toast.success('Profile picture updated.');
|
toast.success('Profile picture updated.');
|
||||||
handleReset();
|
handleReset();
|
||||||
|
|||||||
@@ -51,12 +51,7 @@ export const UserInfoForm = ({ preloadedUser }: UserInfoFormProps) => {
|
|||||||
const user = usePreloadedQuery(preloadedUser);
|
const user = usePreloadedQuery(preloadedUser);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const updateUserName = useMutation(api.auth.updateUserName);
|
const updateUser = useMutation(api.auth.updateUser);
|
||||||
const updateUserEmail = useMutation(api.auth.updateUserEmail);
|
|
||||||
const updateUserLunchtime = useMutation(api.auth.updateUserLunchtime);
|
|
||||||
const updateUserAutomaticLunch = useMutation(
|
|
||||||
api.auth.updateUserAutomaticLunch,
|
|
||||||
);
|
|
||||||
|
|
||||||
const initialValues = useMemo<z.infer<typeof formSchema>>(
|
const initialValues = useMemo<z.infer<typeof formSchema>>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -74,22 +69,29 @@ export const UserInfoForm = ({ preloadedUser }: UserInfoFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
|
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
const ops: Promise<unknown>[] = [];
|
|
||||||
const name = values.name.trim();
|
const name = values.name.trim();
|
||||||
const email = values.email.trim().toLowerCase();
|
const email = values.email.trim().toLowerCase();
|
||||||
const lunchTime = values.lunchTime.trim();
|
const lunchTime = values.lunchTime.trim();
|
||||||
const automaticLunch = values.automaticLunch;
|
const automaticLunch = values.automaticLunch;
|
||||||
if (name !== (user?.name ?? '')) ops.push(updateUserName({ name }));
|
const patch: Partial<{
|
||||||
if (email !== (user?.email ?? '')) ops.push(updateUserEmail({ email }));
|
name: string;
|
||||||
if (lunchTime !== (user?.lunchTime ?? ''))
|
email: string;
|
||||||
ops.push(updateUserLunchtime({ lunchTime }));
|
lunchTime: string;
|
||||||
if (automaticLunch !== user?.automaticLunch)
|
automaticLunch: boolean;
|
||||||
ops.push(updateUserAutomaticLunch({ automaticLunch }));
|
}> = {};
|
||||||
if (ops.length === 0) return;
|
if (name !== (user?.name ?? '') && name !== undefined)
|
||||||
|
patch.name = name;
|
||||||
|
if (email !== (user?.email ?? '') && email !== undefined)
|
||||||
|
patch.email = email;
|
||||||
|
if (lunchTime !== (user?.lunchTime && '') && lunchTime !== undefined)
|
||||||
|
patch.lunchTime = lunchTime;
|
||||||
|
if (automaticLunch !== user?.automaticLunch && automaticLunch !== undefined)
|
||||||
|
patch.automaticLunch = automaticLunch;
|
||||||
|
if (Object.keys(patch).length === 0) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await Promise.all(ops);
|
await updateUser(patch);
|
||||||
form.reset({ name, email, lunchTime });
|
form.reset(patch);
|
||||||
toast.success('Profile updated successfully.');
|
toast.success('Profile updated successfully.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -16,12 +16,19 @@ const nextOccurrenceMs = (hhmm: string, from = new Date()): number => {
|
|||||||
export const LunchReminder = () => {
|
export const LunchReminder = () => {
|
||||||
const setStatus = useMutation(api.statuses.createLunchStatus);
|
const setStatus = useMutation(api.statuses.createLunchStatus);
|
||||||
const timeoutRef = useRef<number | null>(null);
|
const timeoutRef = useRef<number | null>(null);
|
||||||
const user = useQuery(api.auth.getUser);
|
const user = useQuery(api.auth.getUser, {});
|
||||||
const lunchTime = user?.lunchTime ?? '';
|
const lunchTime = user?.lunchTime ?? '';
|
||||||
|
const automaticLunch = user?.automaticLunch ?? false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
timeoutRef.current = null;
|
||||||
|
}
|
||||||
|
if (!lunchTime || automaticLunch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const schedule = () => {
|
const schedule = () => {
|
||||||
if (!lunchTime) return;
|
|
||||||
const ms = nextOccurrenceMs(lunchTime);
|
const ms = nextOccurrenceMs(lunchTime);
|
||||||
console.log('Ms = ', ms);
|
console.log('Ms = ', ms);
|
||||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
@@ -44,7 +51,7 @@ export const LunchReminder = () => {
|
|||||||
description: 'Would you like to set your status to "At lunch"?',
|
description: 'Would you like to set your status to "At lunch"?',
|
||||||
action: {
|
action: {
|
||||||
label: 'Set to lunch',
|
label: 'Set to lunch',
|
||||||
onClick: () => void setStatus(),
|
onClick: () => void setStatus({}),
|
||||||
},
|
},
|
||||||
cancel: {
|
cancel: {
|
||||||
label: 'Not now',
|
label: 'Not now',
|
||||||
@@ -59,9 +66,11 @@ export const LunchReminder = () => {
|
|||||||
|
|
||||||
schedule();
|
schedule();
|
||||||
return () => {
|
return () => {
|
||||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
timeoutRef.current = null
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [lunchTime, setStatus]);
|
}, [automaticLunch, lunchTime, setStatus]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { permission } from 'process';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ import {
|
|||||||
} from '@convex-dev/auth/server';
|
} from '@convex-dev/auth/server';
|
||||||
import { api } from './_generated/api';
|
import { api } from './_generated/api';
|
||||||
import { type Id } from './_generated/dataModel';
|
import { type Id } from './_generated/dataModel';
|
||||||
import { action, mutation, query } from './_generated/server';
|
import {
|
||||||
|
action,
|
||||||
|
mutation,
|
||||||
|
query,
|
||||||
|
type MutationCtx,
|
||||||
|
type QueryCtx,
|
||||||
|
} from './_generated/server';
|
||||||
import Authentik from '@auth/core/providers/authentik';
|
import Authentik from '@auth/core/providers/authentik';
|
||||||
import { Entra, Password, validatePassword } from './custom/auth';
|
import { Entra, Password, validatePassword } from './custom/auth';
|
||||||
|
|
||||||
@@ -24,127 +30,116 @@ export const PASSWORD_MAX = 100;
|
|||||||
export const PASSWORD_REGEX =
|
export const PASSWORD_REGEX =
|
||||||
/^(?=.{8,100}$)(?!.*\s)(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\p{P}\p{S}]).*$/u;
|
/^(?=.{8,100}$)(?!.*\s)(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\p{P}\p{S}]).*$/u;
|
||||||
|
|
||||||
export const getUser = query(async (ctx) => {
|
type RWCtx = MutationCtx | QueryCtx;
|
||||||
const userId = await getAuthUserId(ctx);
|
type User = {
|
||||||
if (!userId) return null;
|
id: Id<'users'>;
|
||||||
|
email: string | null;
|
||||||
|
name: string | null;
|
||||||
|
image: Id<'_storage'> | null;
|
||||||
|
lunchTime: string | null;
|
||||||
|
automaticLunch: boolean;
|
||||||
|
provider: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserData = async (ctx: RWCtx, userId: Id<'users'>): Promise<User> => {
|
||||||
const user = await ctx.db.get(userId);
|
const user = await ctx.db.get(userId);
|
||||||
if (!user) throw new ConvexError('User not found.');
|
if (!user) throw new ConvexError('User not found.');
|
||||||
|
|
||||||
const image: Id<'_storage'> | null =
|
const image: Id<'_storage'> | null =
|
||||||
typeof user.image === 'string' && user.image.length > 0
|
typeof user.image === 'string' && user.image.length > 0
|
||||||
? (user.image as Id<'_storage'>)
|
? (user.image as Id<'_storage'>)
|
||||||
: null;
|
: null;
|
||||||
const authAccount = await ctx.db
|
|
||||||
|
const authAccount = await getUserAuthAccountData(ctx, userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user._id,
|
||||||
|
email: user.email ?? null,
|
||||||
|
name: user.name ?? null,
|
||||||
|
image,
|
||||||
|
lunchTime: user.lunchTime ?? null,
|
||||||
|
automaticLunch: user.automaticLunch ?? false,
|
||||||
|
provider: authAccount?.provider ?? null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserAuthAccountData = async (ctx: RWCtx, userId: Id<'users'>) => {
|
||||||
|
const user = await ctx.db.get(userId);
|
||||||
|
if (!user) throw new ConvexError('User not found.');
|
||||||
|
const authAccountData = await ctx.db
|
||||||
.query('authAccounts')
|
.query('authAccounts')
|
||||||
.withIndex('userIdAndProvider', (q) => q.eq('userId', userId))
|
.withIndex('userIdAndProvider', (q) => q.eq('userId', userId))
|
||||||
.first();
|
.first();
|
||||||
return {
|
return authAccountData;
|
||||||
id: user._id,
|
|
||||||
email: user?.email,
|
|
||||||
name: user?.name,
|
|
||||||
image,
|
|
||||||
lunchTime: user?.lunchTime,
|
|
||||||
automaticLunch: user.automaticLunch,
|
|
||||||
provider: authAccount?.provider,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUser = query({
|
||||||
|
args: { userId: v.optional(v.id('users')) },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const userId = args.userId ?? await getAuthUserId(ctx);
|
||||||
|
if (!userId) return null;
|
||||||
|
return getUserData(ctx, userId);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getUserAuthAccount = query(async (ctx) => {
|
export const getUserAuthAccount = query(async (ctx) => {
|
||||||
const userId = await getAuthUserId(ctx);
|
const userId = await getAuthUserId(ctx);
|
||||||
if (!userId) return null;
|
if (!userId) return null;
|
||||||
const authAccount = await ctx.db
|
return getUserAuthAccountData(ctx, userId);
|
||||||
.query('authAccounts')
|
|
||||||
.withIndex('userIdAndProvider', (q) => q.eq('userId', userId))
|
|
||||||
.first();
|
|
||||||
return authAccount;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getAllUsers = query(async (ctx) => {
|
export const getAllUsers = query(async (ctx) => {
|
||||||
const users = await ctx.db.query('users').collect();
|
const users = await ctx.db.query('users').collect();
|
||||||
return users.map((u) => ({
|
return Promise.all(users.map((u) => getUserData(ctx, u._id)));
|
||||||
id: u._id,
|
|
||||||
email: u.email ?? null,
|
|
||||||
name: u.name ?? null,
|
|
||||||
image: u.image ?? null,
|
|
||||||
lunchTime: u.lunchTime ?? null,
|
|
||||||
automaticLUnch: u.automaticLunch ?? (false as boolean),
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getAllUserIds = query(async (ctx) => {
|
export const getAllUserIds = query(async (ctx) => {
|
||||||
const users = await ctx.db.query('users').collect();
|
const users = await ctx.db.query('users').collect();
|
||||||
const userIds = users.map((u) => u._id);
|
return users.map((u) => u._id);
|
||||||
return userIds;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateUserName = mutation({
|
export const updateUser = mutation({
|
||||||
args: {
|
args: {
|
||||||
name: v.string(),
|
name: v.optional(v.string()),
|
||||||
|
email: v.optional(v.string()),
|
||||||
|
image: v.optional(v.id('_storage')),
|
||||||
|
lunchTime: v.optional(v.string()),
|
||||||
|
automaticLunch: v.optional(v.boolean()),
|
||||||
},
|
},
|
||||||
handler: async (ctx, { name }) => {
|
handler: async (ctx, args) => {
|
||||||
const userId = await getAuthUserId(ctx);
|
const userId = await getAuthUserId(ctx);
|
||||||
if (!userId) throw new ConvexError('Not authenticated.');
|
if (!userId) throw new ConvexError('Not authenticated.');
|
||||||
const user = await ctx.db.get(userId);
|
const user = await ctx.db.get(userId);
|
||||||
if (!user) throw new ConvexError('User not found.');
|
if (!user) throw new ConvexError('User not found.');
|
||||||
await ctx.db.patch(userId, { name });
|
if (args.lunchTime !== undefined && !args.lunchTime.includes(':')) {
|
||||||
return { success: true };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateUserEmail = mutation({
|
|
||||||
args: {
|
|
||||||
email: v.string(),
|
|
||||||
},
|
|
||||||
handler: async (ctx, { email }) => {
|
|
||||||
const userId = await getAuthUserId(ctx);
|
|
||||||
if (!userId) throw new ConvexError('Not authenticated.');
|
|
||||||
const user = await ctx.db.get(userId);
|
|
||||||
if (!user) throw new ConvexError('User not found.');
|
|
||||||
await ctx.db.patch(userId, { email });
|
|
||||||
return { success: true };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateUserImage = mutation({
|
|
||||||
args: {
|
|
||||||
storageId: v.id('_storage'),
|
|
||||||
},
|
|
||||||
handler: async (ctx, { storageId }) => {
|
|
||||||
const userId = await getAuthUserId(ctx);
|
|
||||||
if (!userId) throw new ConvexError('Not authenticated.');
|
|
||||||
const user = await ctx.db.get(userId);
|
|
||||||
if (!user) throw new ConvexError('User not found.');
|
|
||||||
const oldImage = user.image as Id<'_storage'> | undefined;
|
|
||||||
await ctx.db.patch(userId, { image: storageId });
|
|
||||||
if (oldImage && oldImage !== storageId) await ctx.storage.delete(oldImage);
|
|
||||||
return { success: true };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateUserLunchtime = mutation({
|
|
||||||
args: {
|
|
||||||
lunchTime: v.string(),
|
|
||||||
},
|
|
||||||
handler: async (ctx, { lunchTime }) => {
|
|
||||||
const userId = await getAuthUserId(ctx);
|
|
||||||
if (!userId) throw new ConvexError('Not authenticated.');
|
|
||||||
const user = await ctx.db.get(userId);
|
|
||||||
if (!user) throw new ConvexError('User not found.');
|
|
||||||
if (!lunchTime.includes(':'))
|
|
||||||
throw new ConvexError('Lunch time is invalid.');
|
throw new ConvexError('Lunch time is invalid.');
|
||||||
await ctx.db.patch(userId, { lunchTime });
|
}
|
||||||
return { success: true };
|
const patch: Partial<{
|
||||||
},
|
name: string;
|
||||||
});
|
email: string;
|
||||||
|
image: Id<'_storage'>;
|
||||||
|
lunchTime: string;
|
||||||
|
automaticLunch: boolean;
|
||||||
|
}> = {};
|
||||||
|
|
||||||
|
if (args.name !== undefined) patch.name = args.name;
|
||||||
|
if (args.email !== undefined) patch.email = args.email;
|
||||||
|
if (args.lunchTime !== undefined) patch.lunchTime = args.lunchTime;
|
||||||
|
if (args.automaticLunch !== undefined)
|
||||||
|
patch.automaticLunch = args.automaticLunch;
|
||||||
|
|
||||||
|
if (args.image !== undefined) {
|
||||||
|
const oldImage = user.image as Id<'_storage'> | undefined;
|
||||||
|
patch.image = args.image;
|
||||||
|
if (oldImage && oldImage !== args.image) {
|
||||||
|
await ctx.storage.delete(oldImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(patch).length > 0) {
|
||||||
|
await ctx.db.patch(userId, patch);
|
||||||
|
}
|
||||||
|
|
||||||
export const updateUserAutomaticLunch = mutation({
|
|
||||||
args: { automaticLunch: v.boolean() },
|
|
||||||
handler: async (ctx, { automaticLunch }) => {
|
|
||||||
const userId = await getAuthUserId(ctx);
|
|
||||||
if (!userId) throw new ConvexError('Not authenticated.');
|
|
||||||
const user = await ctx.db.get(userId);
|
|
||||||
if (!user) throw new ConvexError('User not found.');
|
|
||||||
if (user.automaticLunch === automaticLunch) return { success: true };
|
|
||||||
await ctx.db.patch(userId, { automaticLunch });
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -157,7 +152,7 @@ export const updateUserPassword = action({
|
|||||||
handler: async (ctx, { currentPassword, newPassword }) => {
|
handler: async (ctx, { currentPassword, newPassword }) => {
|
||||||
const userId = await getAuthUserId(ctx);
|
const userId = await getAuthUserId(ctx);
|
||||||
if (!userId) throw new ConvexError('Not authenticated.');
|
if (!userId) throw new ConvexError('Not authenticated.');
|
||||||
const user = await ctx.runQuery(api.auth.getUser);
|
const user = await ctx.runQuery(api.auth.getUser, { userId });
|
||||||
if (!user?.email) throw new ConvexError('User not found.');
|
if (!user?.email) throw new ConvexError('User not found.');
|
||||||
const verified = await retrieveAccount(ctx, {
|
const verified = await retrieveAccount(ctx, {
|
||||||
provider: 'password',
|
provider: 'password',
|
||||||
|
|||||||
@@ -4,10 +4,26 @@ import { api } from './_generated/api';
|
|||||||
|
|
||||||
const crons = cronJobs();
|
const crons = cronJobs();
|
||||||
|
|
||||||
// Runs at 5:00 PM America/Chicago, Monday–Friday.
|
crons.cron(
|
||||||
// Convex will handle DST if your project version supports `timeZone`.
|
// Run at 7:00 AM CST / 8:00 AM CDT
|
||||||
|
// Only on weekdays
|
||||||
|
'Schedule Automatic Lunches',
|
||||||
|
'0 13 * * 1-5',
|
||||||
|
api.statuses.automaticLunch,
|
||||||
|
);
|
||||||
|
|
||||||
|
crons.cron(
|
||||||
|
// Run at 7:00 AM CST / 8:00 AM CDT
|
||||||
|
// Only on weekdays
|
||||||
|
'Schedule Automatic Lunches Test',
|
||||||
|
'25 18 * * 1-5',
|
||||||
|
api.statuses.automaticLunch,
|
||||||
|
);
|
||||||
|
|
||||||
crons.cron(
|
crons.cron(
|
||||||
'End of shift (weekdays 5pm CT)',
|
'End of shift (weekdays 5pm CT)',
|
||||||
|
// Run at 4:00 PM CST / 5:00 PM CDT
|
||||||
|
// Only on weekdays
|
||||||
'0 22 * * 1-5',
|
'0 22 * * 1-5',
|
||||||
api.statuses.endOfShiftUpdate,
|
api.statuses.endOfShiftUpdate,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -156,23 +156,41 @@ export const updateAllStatuses = mutation({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const createLunchStatus = mutation({
|
export const createLunchStatus = mutation({
|
||||||
args: {},
|
args: { userId: v.optional(v.id('users'))},
|
||||||
handler: async (ctx) => {
|
handler: async (ctx, args) => {
|
||||||
const authUserId = await getAuthUserId(ctx);
|
const authUserId = await getAuthUserId(ctx);
|
||||||
if (!authUserId) throw new ConvexError('Not authenticated.');
|
const lunchUserId = args.userId ?? authUserId
|
||||||
|
if (!lunchUserId) throw new ConvexError('Not authenticated.');
|
||||||
await ctx.runMutation(api.statuses.create, {
|
await ctx.runMutation(api.statuses.create, {
|
||||||
message: 'At lunch',
|
message: 'At lunch',
|
||||||
userId: authUserId,
|
userId: lunchUserId,
|
||||||
});
|
});
|
||||||
const oneHour = 60 * 60 * 1000;
|
const oneHour = 60 * 60 * 1000;
|
||||||
await ctx.scheduler.runAfter(oneHour, api.statuses.create, {
|
await ctx.scheduler.runAfter(oneHour, api.statuses.backFromLunchStatus, {
|
||||||
message: 'At desk',
|
userId: lunchUserId,
|
||||||
userId: authUserId,
|
|
||||||
});
|
});
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const backFromLunchStatus = mutation({
|
||||||
|
args: { userId: v.optional(v.id('users')) },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const authUserId = await getAuthUserId(ctx);
|
||||||
|
const lunchUserId = args.userId ?? authUserId
|
||||||
|
if (!lunchUserId) throw new ConvexError('Not authenticated.');
|
||||||
|
const user = await ensureUser(ctx, lunchUserId);
|
||||||
|
if (!user.currentStatusId) throw new ConvexError('User has no current status.');
|
||||||
|
const currentStatus = await ctx.db.get(user.currentStatusId);
|
||||||
|
if (currentStatus?.message === 'At lunch') {
|
||||||
|
await ctx.runMutation(api.statuses.create, {
|
||||||
|
message: 'At desk',
|
||||||
|
userId: lunchUserId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const getCurrentForUser = query({
|
export const getCurrentForUser = query({
|
||||||
args: { userId: v.id('users') },
|
args: { userId: v.id('users') },
|
||||||
handler: async (ctx, { userId }) => {
|
handler: async (ctx, { userId }) => {
|
||||||
@@ -331,3 +349,36 @@ export const endOfShiftUpdate = action({
|
|||||||
} else return;
|
} else return;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const automaticLunch = action({
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const now = new Date(
|
||||||
|
new Date().toLocaleString('en-US', {
|
||||||
|
timeZone: 'America/Chicago',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const users = await ctx.runQuery(api.auth.getAllUsers);
|
||||||
|
await Promise.all(
|
||||||
|
users.map(async (user) => {
|
||||||
|
if (user.automaticLunch && user.lunchTime) {
|
||||||
|
const [hours, minutes] = user.lunchTime.split(':').map(Number);
|
||||||
|
const userLunchTime = new Date(now);
|
||||||
|
userLunchTime.setHours(hours, minutes, 0, 0);
|
||||||
|
const diffInMs = userLunchTime.getTime() - now.getTime();
|
||||||
|
// Only schedule if lunch is in the future today
|
||||||
|
if (diffInMs > 0) {
|
||||||
|
await ctx.scheduler.runAfter(
|
||||||
|
diffInMs,
|
||||||
|
api.statuses.createLunchStatus,
|
||||||
|
{ userId: user.id },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`Skipped ${user.name} - lunch time ${user.lunchTime} already passed.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user