diff --git a/CHANGELOG.md b/CHANGELOG.md index 255175b..b714377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Version 0.2.4 + +### Added + +* admin can select user to view coins for that user + +### Fixed + +* fix disable password in demo instance (#74) + ## Version 0.2.3 ### Fixed diff --git a/app/actions/data.ts b/app/actions/data.ts index 898e9d0..d452175 100644 --- a/app/actions/data.ts +++ b/app/actions/data.ts @@ -185,7 +185,7 @@ export async function loadCoinsData(): Promise { const data = await loadData('coins') return { ...data, - transactions: data.transactions.filter(x => x.userId === user.id) + transactions: user.isAdmin ? data.transactions : data.transactions.filter(x => x.userId === user.id) } } catch { return getDefaultCoinsData() @@ -194,7 +194,7 @@ export async function loadCoinsData(): Promise { export async function saveCoinsData(data: CoinsData): Promise { const user = await getCurrentUser() - + // Create clones of the data const newData = _.cloneDeep(data) newData.transactions = newData.transactions.map(transaction => ({ @@ -219,12 +219,14 @@ export async function addCoins({ type = 'MANUAL_ADJUSTMENT', relatedItemId, note, + userId, }: { amount: number description: string type?: TransactionType relatedItemId?: string note?: string + userId?: string }): Promise { await verifyPermission('coins', type === 'MANUAL_ADJUSTMENT' ? 'write' : 'interact') const data = await loadCoinsData() @@ -235,7 +237,8 @@ export async function addCoins({ description, timestamp: d2t({ dateTime: getNow({}) }), ...(relatedItemId && { relatedItemId }), - ...(note && note.trim() !== '' && { note }) + ...(note && note.trim() !== '' && { note }), + userId: userId || await getCurrentUserId() } const newData: CoinsData = { @@ -270,12 +273,14 @@ export async function removeCoins({ type = 'MANUAL_ADJUSTMENT', relatedItemId, note, + userId, }: { amount: number description: string type?: TransactionType relatedItemId?: string note?: string + userId?: string }): Promise { await verifyPermission('coins', type === 'MANUAL_ADJUSTMENT' ? 'write' : 'interact') const data = await loadCoinsData() @@ -286,7 +291,8 @@ export async function removeCoins({ description, timestamp: d2t({ dateTime: getNow({}) }), ...(relatedItemId && { relatedItemId }), - ...(note && note.trim() !== '' && { note }) + ...(note && note.trim() !== '' && { note }), + userId: userId || await getCurrentUserId() } const newData: CoinsData = { @@ -478,6 +484,6 @@ export async function deleteUser(userId: string): Promise { export async function loadServerSettings(): Promise { return { - isDemo: !!process.env.NEXT_PUBLIC_DEMO, + isDemo: !!process.env.DEMO, } } diff --git a/components/CoinsManager.tsx b/components/CoinsManager.tsx index 3641a7b..09dc10e 100644 --- a/components/CoinsManager.tsx +++ b/components/CoinsManager.tsx @@ -17,6 +17,8 @@ import { TransactionNoteEditor } from './TransactionNoteEditor' import { useHelpers } from '@/lib/client-helpers' export default function CoinsManager() { + const { currentUser } = useHelpers() + const [selectedUser, setSelectedUser] = useState() const { add, remove, @@ -28,14 +30,13 @@ export default function CoinsManager() { totalSpent, coinsSpentToday, transactionsToday - } = useCoins() + } = useCoins({selectedUser}) const [settings] = useAtom(settingsAtom) const [usersData] = useAtom(usersAtom) const DEFAULT_AMOUNT = '0' const [amount, setAmount] = useState(DEFAULT_AMOUNT) const [pageSize, setPageSize] = useState(50) const [currentPage, setCurrentPage] = useState(1) - const { currentUser } = useHelpers() const [note, setNote] = useState('') @@ -62,7 +63,22 @@ export default function CoinsManager() { return (
-

Coins Management

+
+

Coins Management

+ {currentUser?.isAdmin && ( + + )} +
diff --git a/components/UserForm.tsx b/components/UserForm.tsx index 5f563a7..06c0f65 100644 --- a/components/UserForm.tsx +++ b/components/UserForm.tsx @@ -251,6 +251,7 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps) id="disable-password" checked={disablePassword} onCheckedChange={setDisablePassword} + disabled={serverSettings.isDemo} />
diff --git a/hooks/useCoins.tsx b/hooks/useCoins.tsx index d559633..fd22816 100644 --- a/hooks/useCoins.tsx +++ b/hooks/useCoins.tsx @@ -1,21 +1,23 @@ import { useAtom } from 'jotai' -import { checkPermission } from '@/lib/utils' +import { calculateCoinsEarnedToday, calculateCoinsSpentToday, calculateTotalEarned, calculateTotalSpent, calculateTransactionsToday, checkPermission } from '@/lib/utils' import { coinsAtom, - coinsEarnedTodayAtom, - totalEarnedAtom, - totalSpentAtom, - coinsSpentTodayAtom, - transactionsTodayAtom, - coinsBalanceAtom + // coinsEarnedTodayAtom, + // totalEarnedAtom, + // totalSpentAtom, + // coinsSpentTodayAtom, + // transactionsTodayAtom, + // coinsBalanceAtom, + settingsAtom, + usersAtom } from '@/lib/atoms' import { addCoins, removeCoins, saveCoinsData } from '@/app/actions/data' -import { CoinsData } from '@/lib/types' +import { CoinsData, User } from '@/lib/types' import { toast } from '@/hooks/use-toast' import { useHelpers } from '@/lib/client-helpers' function handlePermissionCheck( - user: any, + user: User | undefined, resource: 'habit' | 'wishlist' | 'coins', action: 'write' | 'interact' ): boolean { @@ -40,18 +42,30 @@ function handlePermissionCheck( return true } -export function useCoins() { - const { currentUser: user } = useHelpers() +export function useCoins(options?: { selectedUser?: string }) { const [coins, setCoins] = useAtom(coinsAtom) - const [coinsEarnedToday] = useAtom(coinsEarnedTodayAtom) - const [totalEarned] = useAtom(totalEarnedAtom) - const [totalSpent] = useAtom(totalSpentAtom) - const [coinsSpentToday] = useAtom(coinsSpentTodayAtom) - const [transactionsToday] = useAtom(transactionsTodayAtom) - const [balance] = useAtom(coinsBalanceAtom) + const [settings] = useAtom(settingsAtom) + const [users] = useAtom(usersAtom) + const { currentUser } = useHelpers() + let user: User | undefined; + if (!options?.selectedUser) { + user = currentUser; + } else { + user = users.users.find(u => u.id === options.selectedUser) + } + + // Filter transactions for the selectd user + const transactions = coins.transactions.filter(t => t.userId === user?.id) + + const balance = transactions.reduce((sum, t) => sum + t.amount, 0) + const coinsEarnedToday = calculateCoinsEarnedToday(transactions, settings.system.timezone) + const totalEarned = calculateTotalEarned(transactions) + const totalSpent = calculateTotalSpent(transactions) + const coinsSpentToday = calculateCoinsSpentToday(transactions, settings.system.timezone) + const transactionsToday = calculateTransactionsToday(transactions, settings.system.timezone) const add = async (amount: number, description: string, note?: string) => { - if (!handlePermissionCheck(user, 'coins', 'write')) return null + if (!handlePermissionCheck(currentUser, 'coins', 'write')) return null if (isNaN(amount) || amount <= 0) { toast({ title: "Invalid amount", @@ -64,7 +78,8 @@ export function useCoins() { amount, description, type: 'MANUAL_ADJUSTMENT', - note + note, + userId: user?.id }) setCoins(data) toast({ title: "Success", description: `Added ${amount} coins` }) @@ -72,7 +87,7 @@ export function useCoins() { } const remove = async (amount: number, description: string, note?: string) => { - if (!handlePermissionCheck(user, 'coins', 'write')) return null + if (!handlePermissionCheck(currentUser, 'coins', 'write')) return null const numAmount = Math.abs(amount) if (isNaN(numAmount) || numAmount <= 0) { toast({ @@ -86,7 +101,8 @@ export function useCoins() { amount: numAmount, description, type: 'MANUAL_ADJUSTMENT', - note + note, + userId: user?.id }) setCoins(data) toast({ title: "Success", description: `Removed ${numAmount} coins` }) @@ -94,7 +110,7 @@ export function useCoins() { } const updateNote = async (transactionId: string, note: string) => { - if (!handlePermissionCheck(user, 'coins', 'write')) return null + if (!handlePermissionCheck(currentUser, 'coins', 'write')) return null const transaction = coins.transactions.find(t => t.id === transactionId) if (!transaction) { toast({ @@ -128,7 +144,7 @@ export function useCoins() { remove, updateNote, balance, - transactions: coins.transactions, + transactions: transactions, coinsEarnedToday, totalEarned, totalSpent, diff --git a/hooks/useWishlist.tsx b/hooks/useWishlist.tsx index 2504ade..2364120 100644 --- a/hooks/useWishlist.tsx +++ b/hooks/useWishlist.tsx @@ -1,11 +1,12 @@ import { useAtom } from 'jotai' -import { wishlistAtom, coinsAtom, coinsBalanceAtom } from '@/lib/atoms' +import { wishlistAtom, coinsAtom } from '@/lib/atoms' import { saveWishlistItems, removeCoins } from '@/app/actions/data' import { toast } from '@/hooks/use-toast' import { WishlistItemType } from '@/lib/types' import { celebrations } from '@/utils/celebrations' import { checkPermission } from '@/lib/utils' import { useHelpers } from '@/lib/client-helpers' +import { useCoins } from './useCoins' function handlePermissionCheck( user: any, @@ -37,7 +38,7 @@ export function useWishlist() { const { currentUser: user } = useHelpers() const [wishlist, setWishlist] = useAtom(wishlistAtom) const [coins, setCoins] = useAtom(coinsAtom) - const [balance] = useAtom(coinsBalanceAtom) + const { balance } = useCoins() const addWishlistItem = async (item: Omit) => { if (!handlePermissionCheck(user, 'wishlist', 'write')) return diff --git a/lib/atoms.ts b/lib/atoms.ts index 3f78d71..77fff5f 100644 --- a/lib/atoms.ts +++ b/lib/atoms.ts @@ -49,44 +49,44 @@ export const coinsAtom = atom(getDefaultCoinsData()); export const wishlistAtom = atom(getDefaultWishlistData()); export const serverSettingsAtom = atom(getDefaultServerSettings()); -// Derived atom for coins earned today -export const coinsEarnedTodayAtom = atom((get) => { - const coins = get(coinsAtom); - const settings = get(settingsAtom); - return calculateCoinsEarnedToday(coins.transactions, settings.system.timezone); -}); +// // Derived atom for coins earned today +// export const coinsEarnedTodayAtom = atom((get) => { +// const coins = get(coinsAtom); +// const settings = get(settingsAtom); +// return calculateCoinsEarnedToday(coins.transactions, settings.system.timezone); +// }); -// Derived atom for total earned -export const totalEarnedAtom = atom((get) => { - const coins = get(coinsAtom); - return calculateTotalEarned(coins.transactions); -}); +// // Derived atom for total earned +// export const totalEarnedAtom = atom((get) => { +// const coins = get(coinsAtom); +// return calculateTotalEarned(coins.transactions); +// }); -// Derived atom for total spent -export const totalSpentAtom = atom((get) => { - const coins = get(coinsAtom); - return calculateTotalSpent(coins.transactions); -}); +// // Derived atom for total spent +// export const totalSpentAtom = atom((get) => { +// const coins = get(coinsAtom); +// return calculateTotalSpent(coins.transactions); +// }); -// Derived atom for coins spent today -export const coinsSpentTodayAtom = atom((get) => { - const coins = get(coinsAtom); - const settings = get(settingsAtom); - return calculateCoinsSpentToday(coins.transactions, settings.system.timezone); -}); +// // Derived atom for coins spent today +// export const coinsSpentTodayAtom = atom((get) => { +// const coins = get(coinsAtom); +// const settings = get(settingsAtom); +// return calculateCoinsSpentToday(coins.transactions, settings.system.timezone); +// }); -// Derived atom for transactions today -export const transactionsTodayAtom = atom((get) => { - const coins = get(coinsAtom); - const settings = get(settingsAtom); - return calculateTransactionsToday(coins.transactions, settings.system.timezone); -}); +// // Derived atom for transactions today +// export const transactionsTodayAtom = atom((get) => { +// const coins = get(coinsAtom); +// const settings = get(settingsAtom); +// return calculateTransactionsToday(coins.transactions, settings.system.timezone); +// }); -// Derived atom for current balance from all transactions -export const coinsBalanceAtom = atom((get) => { - const coins = get(coinsAtom); - return coins.transactions.reduce((sum, transaction) => sum + transaction.amount, 0); -}); +// // Derived atom for current balance from all transactions +// export const coinsBalanceAtom = atom((get) => { +// const coins = get(coinsAtom); +// return coins.transactions.reduce((sum, transaction) => sum + transaction.amount, 0); +// }); /* transient atoms */ interface PomodoroAtom { diff --git a/lib/env.server.ts b/lib/env.server.ts index 54ba4ce..bb1e9e4 100644 --- a/lib/env.server.ts +++ b/lib/env.server.ts @@ -2,13 +2,13 @@ import { z } from "zod" const zodEnv = z.object({ AUTH_SECRET: z.string(), - NEXT_PUBLIC_DEMO: z.string().optional(), + DEMO: z.string().optional(), }) declare global { interface ProcessEnv extends z.TypeOf { AUTH_SECRET: string; - NEXT_PUBLIC_DEMO?: string; + DEMO?: string; } } diff --git a/package.json b/package.json index 404d9e9..c010d77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "habittrove", - "version": "0.2.3", + "version": "0.2.4", "private": true, "scripts": { "dev": "next dev --turbopack",