Compare commits

...

1 Commits

Author SHA1 Message Date
Doh
9052c9f37a per-user coins data for admin (#82)
* admin user can see per-user coins data

* fixes

* fix
2025-02-28 17:07:44 -05:00
9 changed files with 119 additions and 69 deletions

View File

@@ -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

View File

@@ -185,7 +185,7 @@ export async function loadCoinsData(): Promise<CoinsData> {
const data = await loadData<CoinsData>('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()
@@ -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<CoinsData> {
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<CoinsData> {
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<void> {
export async function loadServerSettings(): Promise<ServerSettings> {
return {
isDemo: !!process.env.NEXT_PUBLIC_DEMO,
isDemo: !!process.env.DEMO,
}
}

View File

@@ -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<string>()
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 (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Coins Management</h1>
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold mr-6">Coins Management</h1>
{currentUser?.isAdmin && (
<select
className="border rounded p-2"
value={selectedUser}
onChange={(e) => setSelectedUser(e.target.value)}
>
{usersData.users.map(user => (
<option key={user.id} value={user.id}>
{user.username}
</option>
))}
</select>
)}
</div>
<div className="grid gap-6 md:grid-cols-2">
<Card>

View File

@@ -251,6 +251,7 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
id="disable-password"
checked={disablePassword}
onCheckedChange={setDisablePassword}
disabled={serverSettings.isDemo}
/>
<Label htmlFor="disable-password">Disable password</Label>
</div>

View File

@@ -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,

View File

@@ -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<WishlistItemType, 'id'>) => {
if (!handlePermissionCheck(user, 'wishlist', 'write')) return

View File

@@ -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 {

View File

@@ -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<typeof zodEnv> {
AUTH_SECRET: string;
NEXT_PUBLIC_DEMO?: string;
DEMO?: string;
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "habittrove",
"version": "0.2.3",
"version": "0.2.4",
"private": true,
"scripts": {
"dev": "next dev --turbopack",