Merge pull request #212 from everysingletear/i18n/new-namespaces

i18n: add things, history, projects, labels, filters, timer and points namespaces (Part of #145)
This commit is contained in:
Mohamad Tarbin
2026-08-13 19:39:39 -04:00
committed by GitHub
23 changed files with 580 additions and 258 deletions

View File

@@ -71,5 +71,11 @@
"reset": "Reset Timer", "reset": "Reset Timer",
"delete": "Delete Session" "delete": "Delete Session"
} }
},
"toolbar": {
"defaultProject": "Default Project"
},
"sort": {
"assignedToMe": "Assigned to me"
} }
} }

View File

@@ -0,0 +1,17 @@
{
"builder": {
"errorName": "Please enter a filter name",
"errorConditions": "Please configure at least one filter condition",
"editTitle": "Edit Filter",
"newTitle": "New Filter",
"addConditions": "Add conditions to preview",
"save": "Save Filter",
"name": "Filter Name",
"namePlaceholder": "e.g. Overdue tasks for Alice",
"color": "Color"
},
"nameExists": "A filter with this name already exists",
"tasks_one": "{{count}} task",
"tasks_other": "{{count}} tasks",
"overdue": "{{count}} overdue"
}

View File

@@ -0,0 +1,89 @@
{
"edit": {
"title": "Edit History",
"completedDate": "Completed Date",
"note": "Note",
"additionalNotes": "Additional Notes",
"deleteTitle": "Delete History",
"deleteMessage": "Are you sure you want to delete this history?"
},
"status": {
"inProgress": "In Progress",
"completed": "Completed",
"skipped": "Skipped",
"pendingApproval": "Pending Approval",
"rejected": "Rejected",
"missed": "Missed",
"rescheduled": "Rescheduled",
"scheduled": "Scheduled"
},
"badge": {
"onTime": "On Time",
"onTimeLabel": "On time"
},
"detail": {
"title": "Activity Detail",
"openTask": "Open Task",
"editEntry": "Edit Entry",
"performedBy": "Performed by",
"assignedTo": "Assigned to",
"lastUpdated": "Last updated",
"duration": "Duration",
"pointsEarned": "Points earned",
"points_one": "{{count}} pt",
"points_other": "{{count}} pts",
"note": "Note"
},
"charts": {
"noData": "No data available",
"noLabels": "No Labels",
"unassigned": "Unassigned",
"unknownTask": "Unknown Task",
"late": "Late",
"status": {
"title": "Status",
"description": "Completed tasks status"
},
"due": {
"title": "Due Date",
"description": "Current tasks due date"
},
"priority": {
"title": "Priority",
"description": "Tasks by priority"
},
"labels": {
"title": "Labels",
"description": "Tasks by labels"
},
"labelsDuration": {
"title": "Labels (time)",
"description": "Time spent by labels (hours)"
},
"tasksTime": {
"title": "Tasks (time)",
"description": "Time spent by individual tasks (hours)"
},
"assigneeBreakdown": {
"title": "by Assignee",
"description": "Tasks grouped by assignee"
}
},
"filter": {
"status": "Status",
"pending": "Pending",
"labels": "Labels",
"hasNotes": "Has Notes",
"hasPoints": "Has Points",
"timePeriod": "Time Period",
"user": "User"
},
"period": {
"days_one": "{{count}} Day",
"days_other": "{{count}} Days",
"allTime": "All Time"
},
"activities": {
"title": "Activities"
}
}

View File

@@ -0,0 +1,9 @@
{
"shared": "Shared",
"delete": {
"title": "Delete Label",
"message": "Are you sure you want to delete this label? This will remove the label from all tasks."
},
"loadError": "Failed to load labels. Please try again.",
"blurb": "Manage your labels and organize your tasks effectively. Labels will be automatically shared with your circle if they are used on a shared task."
}

View File

@@ -0,0 +1,74 @@
{
"redeem": "Redeem Points",
"redeemModal": {
"redeemButton": "Redeem",
"pointsAvailable": "{{count}} points available",
"pointsToRedeem": "Points to Redeem",
"placeholder": "Enter points...",
"cannotExceed": "Cannot exceed available points",
"quickAdd": "Quick Add:",
"aboutToRedeem": "You are about to redeem",
"amount": "{{count}} points",
"remaining": "Remaining: {{count}} points"
},
"leaderboard": {
"titlePoints": "Points Leaderboard",
"titleTasks": "Tasks Leaderboard",
"subtitlePoints": "Rankings based on points earned during the selected time period",
"subtitleTasks": "Rankings based on tasks completed during the selected time period",
"modePoints": "Points",
"modeTasks": "Tasks",
"you": "You",
"tasksAndAvg": "{{tasks}} tasks • {{avg}} avg per task",
"available": "{{count}} available",
"points_one": "{{count}} point",
"points_other": "{{count}} points"
},
"tabs": {
"short7d": "7D",
"short6m": "6M",
"shortAll": "All",
"days7": "7 Days",
"months6": "6 Months",
"allTime": "All Time"
},
"filter": {
"analysisTitle": "Filter & Analysis",
"title": "Filter Points",
"showFor": "Show points for:",
"timePeriod": "Time period:"
},
"cards": {
"available": {
"title": "Available",
"subtext": "Ready to redeem"
},
"redeemed": {
"title": "Redeemed",
"subtext": "Previously used"
},
"total": {
"title": "Total",
"subtext": "All time earned"
},
"period": {
"title": "Period Points",
"allTime": "All time",
"last6Months": "Last 6 months",
"lastDays_one": "Last {{count}} day",
"lastDays_other": "Last {{count}} days"
}
},
"summary": {
"showingFor": "Showing points for",
"unknownUser": "Unknown User",
"overThe": "over the",
"allTime": "All Time",
"last6Months": "Last 6 Months",
"lastDays_one": "Last {{count}} Day",
"lastDays_other": "Last {{count}} Days"
},
"trend": {
"title": "Points Trend"
}
}

View File

@@ -0,0 +1,24 @@
{
"iconPicker": {
"chooseIcon": "Choose Project Icon",
"availableIcons": "Available Icons"
},
"defaultChip": "Default",
"tasks_one": "{{count}} task",
"tasks_other": "{{count}} tasks",
"shared": "Shared",
"delete": {
"title": "Delete Project",
"message": "Are you sure you want to delete \"{{name}}\"? This will remove the project but keep all tasks (they'll move to the Default Project)."
},
"loadError": "Failed to load projects. Please try again.",
"blurb": "Organize your tasks into projects. Create custom workspaces to keep your tasks organized and easily accessible.",
"defaultDescription": "All tasks without a specific project",
"selector": {
"title": "Projects",
"createNew": "Create New Project",
"createNewDescription": "Add a custom project workspace",
"manage": "Manage Projects",
"manageDescription": "View, edit, and organize all projects"
}
}

View File

@@ -0,0 +1,18 @@
{
"errName": "Name is required",
"errStateNumber": "State must be a number",
"errStateBool": "State must be true or false",
"errStateRequired": "State is required",
"name": "Name",
"namePlaceholder": "Thing name",
"value": "Value",
"valuePlaceholder": "Thing value",
"updateState": "Update state",
"update": "Update",
"history": {
"overview": "Things Overview",
"visualization": "Data Visualization",
"changeHistory": "Change History",
"updated": "Updated"
}
}

View File

@@ -0,0 +1,29 @@
{
"toast": {
"sessionUpdatedTitle": "Session updated",
"sessionUpdatedMessage": "Timer session has been updated successfully.",
"sessionUpdateFailTitle": "Failed to update session",
"tryAgain": "Please try again.",
"sessionUpdateErrorTitle": "Error updating session",
"sessionDeletedTitle": "Session deleted",
"sessionDeletedMessage": "Timer session has been deleted successfully.",
"sessionDeleteErrorTitle": "Error deleting session",
"deleteConfirmTitle": "Delete Timer Session",
"deleteConfirmMessage": "Are you sure you want to delete this timer session?"
},
"loading": "Loading timer data...",
"noData": "No timer data found for this chore.",
"activeWork": "Active Work",
"breakTime": "Break Time",
"workSessions": "Work Sessions",
"totalTime": "Total Time",
"distribution": "Work vs Break Distribution",
"noActiveTime": "No active time yet",
"sessionBreakdown": "Session Breakdown",
"sessions": "Sessions",
"addSession": "Add Session",
"startTime": "Start Time",
"endTime": "End Time",
"leaveEmpty": "Leave empty if session is ongoing",
"noSessionForChore": "No timer session found for this chore."
}

View File

@@ -22,7 +22,19 @@ i18n
loadPath: '/locales/{{lng}}/{{ns}}.json', loadPath: '/locales/{{lng}}/{{ns}}.json',
}, },
ns: ['common', 'settings', 'chores', 'auth'], ns: [
'auth',
'common',
'settings',
'chores',
'labels',
'history',
'filters',
'things',
'projects',
'timer',
'points',
],
defaultNS: 'common', defaultNS: 'common',
detection: { detection: {

View File

@@ -11,6 +11,7 @@ import {
Typography, Typography,
} from '@mui/joy' } from '@mui/joy'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import LabelModal from '../Modals/Inputs/LabelModal' import LabelModal from '../Modals/Inputs/LabelModal'
import { import {
@@ -32,6 +33,7 @@ import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import { useLabels } from './LabelQueries' import { useLabels } from './LabelQueries'
const LabelCardContent = ({ label, currentUserId, onToggleActions }) => { const LabelCardContent = ({ label, currentUserId, onToggleActions }) => {
const { t } = useTranslation('labels')
// Check if current user owns this label // Check if current user owns this label
const isOwnedByCurrentUser = label.created_by === currentUserId const isOwnedByCurrentUser = label.created_by === currentUserId
@@ -124,7 +126,7 @@ const LabelCardContent = ({ label, currentUserId, onToggleActions }) => {
fontWeight: 'md', fontWeight: 'md',
}} }}
> >
Shared {t('shared')}
</Chip> </Chip>
)} )}
</Box> </Box>
@@ -149,6 +151,7 @@ const LabelCardContent = ({ label, currentUserId, onToggleActions }) => {
} }
const LabelView = () => { const LabelView = () => {
const { t } = useTranslation('labels')
const { data: labels, isLabelsLoading, isError } = useLabels() const { data: labels, isLabelsLoading, isError } = useLabels()
const { data: userProfile } = useUserProfile() const { data: userProfile } = useUserProfile()
@@ -173,14 +176,11 @@ const LabelView = () => {
const handleDeleteClicked = id => { const handleDeleteClicked = id => {
setConfirmationModel({ setConfirmationModel({
isOpen: true, isOpen: true,
title: 'Delete Label', title: t('delete.title'),
message: t('delete.message'),
message: confirmText: t('common:delete'),
'Are you sure you want to delete this label? This will remove the label from all tasks.',
confirmText: 'Delete',
color: 'danger', color: 'danger',
cancelText: 'Cancel', cancelText: t('common:cancel'),
onClose: confirmed => { onClose: confirmed => {
if (confirmed === true) { if (confirmed === true) {
handleDeleteLabel(id) handleDeleteLabel(id)
@@ -230,7 +230,7 @@ const LabelView = () => {
if (isError) { if (isError) {
return ( return (
<Typography color='danger' textAlign='center'> <Typography color='danger' textAlign='center'>
Failed to load labels. Please try again. {t('loadError')}
</Typography> </Typography>
) )
} }
@@ -244,12 +244,10 @@ const LabelView = () => {
level='h3' level='h3'
sx={{ fontWeight: 'lg', color: 'text.primary' }} sx={{ fontWeight: 'lg', color: 'text.primary' }}
> >
Labels {t('common:navigation.labels')}
</Typography> </Typography>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}> <Typography level='body-sm' sx={{ color: 'text.secondary' }}>
Manage your labels and organize your tasks effectively. Labels will {t('blurb')}
be automatically shared with your circle if they are used on a
shared task.
</Typography> </Typography>
</Stack> </Stack>
</Box> </Box>
@@ -300,7 +298,7 @@ const LabelView = () => {
> >
<EditIcon sx={{ fontSize: 20 }} /> <EditIcon sx={{ fontSize: 20 }} />
<Typography level='body-xs' sx={{ mt: 0.5 }}> <Typography level='body-xs' sx={{ mt: 0.5 }}>
Edit {t('common:edit')}
</Typography> </Typography>
</Box> </Box>
</SwipeAction> </SwipeAction>
@@ -319,7 +317,7 @@ const LabelView = () => {
> >
<DeleteIcon sx={{ fontSize: 20 }} /> <DeleteIcon sx={{ fontSize: 20 }} />
<Typography level='body-xs' sx={{ mt: 0.5 }}> <Typography level='body-xs' sx={{ mt: 0.5 }}>
Delete {t('common:delete')}
</Typography> </Typography>
</Box> </Box>
</SwipeAction> </SwipeAction>

View File

@@ -1,12 +1,14 @@
import { FormLabel, Input } from '@mui/joy' import { FormLabel, Input } from '@mui/joy'
import moment from 'moment' import moment from 'moment'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ModalActions from '../../components/common/ModalActions' import ModalActions from '../../components/common/ModalActions'
import { useResponsiveModal } from '../../hooks/useResponsiveModal' import { useResponsiveModal } from '../../hooks/useResponsiveModal'
import ConfirmationModal from './Inputs/ConfirmationModal' import ConfirmationModal from './Inputs/ConfirmationModal'
function EditHistoryModal({ config, historyRecord }) { function EditHistoryModal({ config, historyRecord }) {
const { t } = useTranslation('history')
const { ResponsiveModal } = useResponsiveModal() const { ResponsiveModal } = useResponsiveModal()
const [completedDate, setCompletedDate] = useState('') const [completedDate, setCompletedDate] = useState('')
@@ -40,12 +42,12 @@ function EditHistoryModal({ config, historyRecord }) {
onClose={config?.onClose} onClose={config?.onClose}
size='lg' size='lg'
// fullWidth={true} // fullWidth={true}
title='Edit History' title={t('edit.title')}
footer={ footer={
<ModalActions <ModalActions
secondary={{ label: 'Cancel', onClick: config.onClose }} secondary={{ label: t('common:cancel'), onClick: config.onClose }}
primary={{ primary={{
label: 'Save', label: t('common:save'),
onClick: () => onClick: () =>
config.onSave({ config.onSave({
id: historyRecord.id, id: historyRecord.id,
@@ -57,7 +59,7 @@ function EditHistoryModal({ config, historyRecord }) {
/> />
} }
> >
<FormLabel>Due Date</FormLabel> <FormLabel>{t('chores:dueDate')}</FormLabel>
<Input <Input
type='datetime-local' type='datetime-local'
value={dueDate} value={dueDate}
@@ -66,7 +68,7 @@ function EditHistoryModal({ config, historyRecord }) {
}} }}
sx={{ mb: 2 }} sx={{ mb: 2 }}
/> />
<FormLabel>Completed Date</FormLabel> <FormLabel>{t('edit.completedDate')}</FormLabel>
<Input <Input
type='datetime-local' type='datetime-local'
value={completedDate} value={completedDate}
@@ -75,12 +77,12 @@ function EditHistoryModal({ config, historyRecord }) {
}} }}
sx={{ mb: 2 }} sx={{ mb: 2 }}
/> />
<FormLabel>Note</FormLabel> <FormLabel>{t('edit.note')}</FormLabel>
<Input <Input
fullWidth fullWidth
multiline multiline
label='Additional Notes' label={t('edit.additionalNotes')}
placeholder='Additional Notes' placeholder={t('edit.additionalNotes')}
value={notes} value={notes}
onChange={e => { onChange={e => {
if (e.target.value.trim() === '') { if (e.target.value.trim() === '') {
@@ -104,10 +106,10 @@ function EditHistoryModal({ config, historyRecord }) {
} }
setIsDeleteModalOpen(false) setIsDeleteModalOpen(false)
}, },
title: 'Delete History', title: t('edit.deleteTitle'),
message: 'Are you sure you want to delete this history?', message: t('edit.deleteMessage'),
confirmText: 'Delete', confirmText: t('common:delete'),
cancelText: 'Cancel', cancelText: t('common:cancel'),
color: 'danger', color: 'danger',
}} }}
/> />

View File

@@ -14,6 +14,7 @@ import {
} from '@mui/icons-material' } from '@mui/icons-material'
import { Avatar, Box, Button, Chip, Divider, Stack, Typography } from '@mui/joy' import { Avatar, Box, Button, Chip, Divider, Stack, Typography } from '@mui/joy'
import moment from 'moment' import moment from 'moment'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import ModalActions from '../../components/common/ModalActions' import ModalActions from '../../components/common/ModalActions'
import { useLocalization } from '../../contexts/LocalizationContext' import { useLocalization } from '../../contexts/LocalizationContext'
@@ -22,13 +23,13 @@ import { TASK_COLOR } from '../../utils/Colors.jsx'
import RichTextEditor from '../components/RichTextEditor.jsx' import RichTextEditor from '../components/RichTextEditor.jsx'
const STATUS_CONFIG = { const STATUS_CONFIG = {
0: { label: 'In Progress', color: 'primary', icon: <AccessTime /> }, 0: { label: t('status.inProgress'), color: 'primary', icon: <AccessTime /> },
1: { label: 'Completed', color: 'success', icon: <Check /> }, 1: { label: t('status.completed'), color: 'success', icon: <Check /> },
2: { label: 'Skipped', color: 'warning', icon: <Redo /> }, 2: { label: t('status.skipped'), color: 'warning', icon: <Redo /> },
3: { label: 'Pending Approval', color: 'neutral', icon: <HourglassEmpty /> }, 3: { label: t('status.pendingApproval'), color: 'neutral', icon: <HourglassEmpty /> },
4: { label: 'Rejected', color: 'danger', icon: <ThumbDown /> }, 4: { label: t('status.rejected'), color: 'danger', icon: <ThumbDown /> },
5: { label: 'Missed', color: 'danger', icon: <RunningWithErrors /> }, 5: { label: t('status.missed'), color: 'danger', icon: <RunningWithErrors /> },
6: { label: 'Rescheduled', color: 'warning', icon: <Schedule /> }, 6: { label: t('status.rescheduled'), color: 'warning', icon: <Schedule /> },
} }
const DetailRow = ({ icon, label, value, children }) => ( const DetailRow = ({ icon, label, value, children }) => (
@@ -55,6 +56,8 @@ const DetailRow = ({ icon, label, value, children }) => (
) )
const TimingBadge = ({ historyEntry }) => { const TimingBadge = ({ historyEntry }) => {
const { t } = useTranslation('history')
if (!historyEntry.dueDate || !historyEntry.performedAt) return null if (!historyEntry.dueDate || !historyEntry.performedAt) return null
if ([0, 5, 6].includes(historyEntry.status)) return null if ([0, 5, 6].includes(historyEntry.status)) return null
@@ -71,7 +74,7 @@ const TimingBadge = ({ historyEntry }) => {
sx={{ backgroundColor: TASK_COLOR.COMPLETED, color: 'white' }} sx={{ backgroundColor: TASK_COLOR.COMPLETED, color: 'white' }}
startDecorator={<Check />} startDecorator={<Check />}
> >
On Time {t('badge.onTime')}
</Chip> </Chip>
) )
} else if (performedAt.isBefore(dueDate)) { } else if (performedAt.isBefore(dueDate)) {
@@ -103,6 +106,7 @@ const TimingBadge = ({ historyEntry }) => {
} }
function HistoryDetailModal({ config }) { function HistoryDetailModal({ config }) {
const { t } = useTranslation('history')
const { ResponsiveModal } = useResponsiveModal() const { ResponsiveModal } = useResponsiveModal()
const { fmt } = useLocalization() const { fmt } = useLocalization()
const navigate = useNavigate() const navigate = useNavigate()
@@ -114,7 +118,9 @@ function HistoryDetailModal({ config }) {
const statusCfg = STATUS_CONFIG[entry.status] ?? STATUS_CONFIG[1] const statusCfg = STATUS_CONFIG[entry.status] ?? STATUS_CONFIG[1]
const isFirstSchedule = entry.status === 6 && !entry.dueDate const isFirstSchedule = entry.status === 6 && !entry.dueDate
const statusLabel = isFirstSchedule ? 'Scheduled' : statusCfg.label const statusLabel = isFirstSchedule
? t('status.scheduled')
: t(statusCfg.labelKey)
const performer = performers.find(p => p.userId === entry.completedBy) const performer = performers.find(p => p.userId === entry.completedBy)
const assignedTo = performers.find(p => p.userId === entry.assignedTo) const assignedTo = performers.find(p => p.userId === entry.assignedTo)
const isDifferentAssignee = const isDifferentAssignee =
@@ -138,7 +144,7 @@ function HistoryDetailModal({ config }) {
<ResponsiveModal <ResponsiveModal
open={config?.isOpen} open={config?.isOpen}
onClose={config?.onClose} onClose={config?.onClose}
title='Activity Detail' title={t('detail.title')}
footer={ footer={
<ModalActions> <ModalActions>
{entry.choreId && ( {entry.choreId && (
@@ -151,7 +157,7 @@ function HistoryDetailModal({ config }) {
navigate(`/chores/${entry.choreId}`) navigate(`/chores/${entry.choreId}`)
}} }}
> >
Open Task {t('detail.openTask')}
</Button> </Button>
)} )}
{config?.onEdit && ( {config?.onEdit && (
@@ -159,7 +165,7 @@ function HistoryDetailModal({ config }) {
startDecorator={<Edit sx={{ fontSize: 16 }} />} startDecorator={<Edit sx={{ fontSize: 16 }} />}
onClick={() => config.onEdit(entry)} onClick={() => config.onEdit(entry)}
> >
Edit Entry {t('detail.editEntry')}
</Button> </Button>
)} )}
</ModalActions> </ModalActions>
@@ -196,7 +202,7 @@ function HistoryDetailModal({ config }) {
{performer && ( {performer && (
<DetailRow <DetailRow
icon={<Check sx={{ fontSize: 16 }} />} icon={<Check sx={{ fontSize: 16 }} />}
label='Performed by' label={t('detail.performedBy')}
> >
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<Avatar <Avatar
@@ -216,7 +222,7 @@ function HistoryDetailModal({ config }) {
{isDifferentAssignee && assignedTo && ( {isDifferentAssignee && assignedTo && (
<DetailRow <DetailRow
icon={<Person sx={{ fontSize: 16 }} />} icon={<Person sx={{ fontSize: 16 }} />}
label='Assigned to' label={t('detail.assignedTo')}
value={assignedTo.displayName} value={assignedTo.displayName}
/> />
)} )}
@@ -259,7 +265,7 @@ function HistoryDetailModal({ config }) {
{showUpdatedAt && ( {showUpdatedAt && (
<DetailRow <DetailRow
icon={<Update sx={{ fontSize: 16 }} />} icon={<Update sx={{ fontSize: 16 }} />}
label='Last updated' label={t('detail.lastUpdated')}
value={fmt.dateTime(entry.updatedAt)} value={fmt.dateTime(entry.updatedAt)}
/> />
)} )}
@@ -268,7 +274,7 @@ function HistoryDetailModal({ config }) {
{entry.duration > 0 && ( {entry.duration > 0 && (
<DetailRow <DetailRow
icon={<Schedule sx={{ fontSize: 16 }} />} icon={<Schedule sx={{ fontSize: 16 }} />}
label='Duration' label={t('detail.duration')}
value={formatDuration(entry.duration)} value={formatDuration(entry.duration)}
/> />
)} )}
@@ -277,8 +283,8 @@ function HistoryDetailModal({ config }) {
{entry.points > 0 && ( {entry.points > 0 && (
<DetailRow <DetailRow
icon={<Typography sx={{ fontSize: 14 }}></Typography>} icon={<Typography sx={{ fontSize: 14 }}></Typography>}
label='Points earned' label={t('detail.pointsEarned')}
value={`${entry.points} pt${entry.points > 1 ? 's' : ''}`} value={t('detail.points', { count: entry.points })}
/> />
)} )}

View File

@@ -11,6 +11,7 @@ import FilterBuilderContent, {
import { FILTER_COLORS } from '../../../utils/Colors' import { FILTER_COLORS } from '../../../utils/Colors'
import { applyFilter } from '../../../utils/FilterEngine' import { applyFilter } from '../../../utils/FilterEngine'
import { useFilters } from '../../Filters/FilterQueries' import { useFilters } from '../../Filters/FilterQueries'
import { useTranslation } from 'react-i18next'
const EMPTY_FILTERS = [] const EMPTY_FILTERS = []
@@ -25,6 +26,7 @@ const AdvancedFilterBuilder = ({
userProfile = null, userProfile = null,
editingFilter = null, editingFilter = null,
}) => { }) => {
const { t } = useTranslation('filters')
const [filterName, setFilterName] = useState('') const [filterName, setFilterName] = useState('')
const [filterColor, setFilterColor] = useState(FILTER_COLORS[0].value) const [filterColor, setFilterColor] = useState(FILTER_COLORS[0].value)
const [selections, setSelections] = useState(defaultSelections()) const [selections, setSelections] = useState(defaultSelections())
@@ -76,15 +78,15 @@ const AdvancedFilterBuilder = ({
const handleSave = () => { const handleSave = () => {
if (!filterName.trim()) { if (!filterName.trim()) {
setError('Please enter a filter name') setError(t('builder.errorName'))
return return
} }
if (filterNameExists(filterName.trim(), editingFilter?.id)) { if (filterNameExists(filterName.trim(), editingFilter?.id)) {
setError('A filter with this name already exists') setError(t('nameExists'))
return return
} }
if (conditions.length === 0) { if (conditions.length === 0) {
setError('Please configure at least one filter condition') setError(t('builder.errorConditions'))
return return
} }
onSave({ onSave({
@@ -106,7 +108,7 @@ const AdvancedFilterBuilder = ({
maxHeight='92vh' maxHeight='92vh'
title={ title={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{editingFilter ? 'Edit Filter' : 'New Filter'} {editingFilter ? t('builder.editTitle') : t('builder.newTitle')}
{activeConditionCount > 0 && ( {activeConditionCount > 0 && (
<Chip size='sm' variant='solid' color='primary'> <Chip size='sm' variant='solid' color='primary'>
{activeConditionCount} condition {activeConditionCount} condition
@@ -129,17 +131,17 @@ const AdvancedFilterBuilder = ({
{conditions.length > 0 ? ( {conditions.length > 0 ? (
<> <>
<Chip size='sm' variant='soft' color='neutral'> <Chip size='sm' variant='soft' color='neutral'>
{previewCount} task{previewCount !== 1 ? 's' : ''} {t('tasks', { count: previewCount })}
</Chip> </Chip>
{previewOverdueCount > 0 && ( {previewOverdueCount > 0 && (
<Chip size='sm' variant='solid' color='danger'> <Chip size='sm' variant='solid' color='danger'>
{previewOverdueCount} overdue {t('overdue', { count: previewOverdueCount })}
</Chip> </Chip>
)} )}
</> </>
) : ( ) : (
<Typography level='body-xs' sx={{ color: 'text.tertiary' }}> <Typography level='body-xs' sx={{ color: 'text.tertiary' }}>
Add conditions to preview {t('builder.addConditions')}
</Typography> </Typography>
)} )}
</Box> </Box>
@@ -147,7 +149,7 @@ const AdvancedFilterBuilder = ({
{/* Actions */} {/* Actions */}
<ModalActions> <ModalActions>
<Button variant='outlined' color='neutral' onClick={onClose}> <Button variant='outlined' color='neutral' onClick={onClose}>
Cancel {t('common:cancel')}
</Button> </Button>
<Button <Button
variant='solid' variant='solid'
@@ -155,7 +157,7 @@ const AdvancedFilterBuilder = ({
startDecorator={<Save sx={{ fontSize: 16 }} />} startDecorator={<Save sx={{ fontSize: 16 }} />}
onClick={handleSave} onClick={handleSave}
> >
Save Filter {t('builder.save')}
</Button> </Button>
</ModalActions> </ModalActions>
</Box> </Box>
@@ -168,10 +170,10 @@ const AdvancedFilterBuilder = ({
level='body-xs' level='body-xs'
sx={{ mb: 0.75, color: 'text.secondary', fontWeight: 600 }} sx={{ mb: 0.75, color: 'text.secondary', fontWeight: 600 }}
> >
Filter Name {t('builder.name')}
</Typography> </Typography>
<Input <Input
placeholder='e.g. Overdue tasks for Alice' placeholder={t('builder.namePlaceholder')}
value={filterName} value={filterName}
onChange={e => { onChange={e => {
setFilterName(e.target.value) setFilterName(e.target.value)
@@ -193,13 +195,13 @@ const AdvancedFilterBuilder = ({
level='body-xs' level='body-xs'
sx={{ mb: 0.75, color: 'text.secondary', fontWeight: 600 }} sx={{ mb: 0.75, color: 'text.secondary', fontWeight: 600 }}
> >
Color {t('builder.color')}
</Typography> </Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{FILTER_COLORS.map(c => ( {FILTER_COLORS.map(c => (
<Box <Box
key={c.value} key={c.value}
title={c.name} title={t(`common:colors.${c.key}`)}
onClick={() => setFilterColor(c.value)} onClick={() => setFilterColor(c.value)}
sx={{ sx={{
width: 26, width: 26,

View File

@@ -10,8 +10,10 @@ import {
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import ModalActions from '../../../components/common/ModalActions' import ModalActions from '../../../components/common/ModalActions'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal' import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { useTranslation } from 'react-i18next'
function CreateThingModal({ isOpen, onClose, onSave, currentThing }) { function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
const { t } = useTranslation('things')
const { ResponsiveModal } = useResponsiveModal() const { ResponsiveModal } = useResponsiveModal()
const [name, setName] = useState(currentThing?.name || '') const [name, setName] = useState(currentThing?.name || '')
@@ -33,17 +35,17 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
const isValid = () => { const isValid = () => {
const newErrors = {} const newErrors = {}
if (!name || name.trim() === '') { if (!name || name.trim() === '') {
newErrors.name = 'Name is required' newErrors.name = t('errName')
} }
if (type === 'number' && isNaN(state)) { if (type === 'number' && isNaN(state)) {
newErrors.state = 'State must be a number' newErrors.state = t('errStateNumber')
} }
if (type === 'boolean' && !['true', 'false'].includes(state)) { if (type === 'boolean' && !['true', 'false'].includes(state)) {
newErrors.state = 'State must be true or false' newErrors.state = t('errStateBool')
} }
if ((type === 'text' && !state) || state.trim() === '') { if ((type === 'text' && !state) || state.trim() === '') {
newErrors.state = 'State is required' newErrors.state = t('errStateRequired')
} }
setErrors(newErrors) setErrors(newErrors)
@@ -67,7 +69,7 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
footer={ footer={
<ModalActions <ModalActions
secondary={{ secondary={{
label: 'Cancel', label: t('common:cancel'),
onClick: onClose, onClick: onClose,
}} }}
primary={{ primary={{
@@ -78,9 +80,9 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
} }
> >
<FormControl> <FormControl>
<Typography>Name</Typography> <Typography>{t('name')}</Typography>
<Textarea <Textarea
placeholder='Thing name' placeholder={t('namePlaceholder')}
value={name} value={name}
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
sx={{ minWidth: 300 }} sx={{ minWidth: 300 }}
@@ -101,9 +103,9 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
</FormControl> </FormControl>
{type === 'text' && ( {type === 'text' && (
<FormControl> <FormControl>
<Typography>Value</Typography> <Typography>{t('value')}</Typography>
<Input <Input
placeholder='Thing value' placeholder={t('valuePlaceholder')}
value={state || ''} value={state || ''}
onChange={e => setState(e.target.value)} onChange={e => setState(e.target.value)}
sx={{ minWidth: 300 }} sx={{ minWidth: 300 }}
@@ -113,9 +115,9 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
)} )}
{type === 'number' && ( {type === 'number' && (
<FormControl> <FormControl>
<Typography>Value</Typography> <Typography>{t('value')}</Typography>
<Input <Input
placeholder='Thing value' placeholder={t('valuePlaceholder')}
type='number' type='number'
value={state || ''} value={state || ''}
onChange={e => { onChange={e => {

View File

@@ -2,8 +2,10 @@ import { FormControl, FormHelperText, Input, Typography } from '@mui/joy'
import { useState } from 'react' import { useState } from 'react'
import ModalActions from '../../../components/common/ModalActions' import ModalActions from '../../../components/common/ModalActions'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal' import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { useTranslation } from 'react-i18next'
function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) { function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
const { t } = useTranslation('things')
const { ResponsiveModal } = useResponsiveModal() const { ResponsiveModal } = useResponsiveModal()
const [state, setState] = useState(currentThing?.state || '') const [state, setState] = useState(currentThing?.state || '')
@@ -13,7 +15,7 @@ function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
const newErrors = {} const newErrors = {}
if (state.trim() === '') { if (state.trim() === '') {
newErrors.state = 'State is required' newErrors.state = t('errStateRequired')
} }
setErrors(newErrors) setErrors(newErrors)
@@ -38,18 +40,18 @@ function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
open={isOpen} open={isOpen}
onClose={onClose} onClose={onClose}
size='sm' size='sm'
title='Update state' title={t('updateState')}
footer={ footer={
<ModalActions <ModalActions
secondary={{ label: 'Cancel', onClick: onClose }} secondary={{ label: t('common:cancel'), onClick: onClose }}
primary={{ label: 'Update', onClick: handleSave }} primary={{ label: t('update'), onClick: handleSave }}
/> />
} }
> >
<FormControl> <FormControl>
<Typography>Value</Typography> <Typography>{t('value')}</Typography>
<Input <Input
placeholder='Thing value' placeholder={t('valuePlaceholder')}
value={state || ''} value={state || ''}
onChange={e => setState(e.target.value)} onChange={e => setState(e.target.value)}
sx={{ minWidth: 300 }} sx={{ minWidth: 300 }}

View File

@@ -3,6 +3,7 @@ import ModalActions from '../../../components/common/ModalActions'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal' import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { getTextColorFromBackgroundColor } from '../../../utils/Colors' import { getTextColorFromBackgroundColor } from '../../../utils/Colors'
import PROJECT_ICONS from '../../../utils/ProjectIcons' import PROJECT_ICONS from '../../../utils/ProjectIcons'
import { useTranslation } from 'react-i18next'
const IconPickerModal = ({ const IconPickerModal = ({
isOpen, isOpen,
@@ -11,6 +12,7 @@ const IconPickerModal = ({
currentIcon, currentIcon,
projectColor, projectColor,
}) => { }) => {
const { t } = useTranslation('projects')
const { ResponsiveModal } = useResponsiveModal() const { ResponsiveModal } = useResponsiveModal()
const handleIconClick = iconValue => { const handleIconClick = iconValue => {
@@ -25,13 +27,13 @@ const IconPickerModal = ({
size='lg' size='lg'
fullWidth={true} fullWidth={true}
unmountDelay={250} unmountDelay={250}
title='Choose Project Icon' title={t('iconPicker.chooseIcon')}
footer={ footer={
<ModalActions secondary={{ label: 'Cancel', onClick: onClose }} /> <ModalActions secondary={{ label: t('common:cancel'), onClick: onClose }} />
} }
> >
<FormControl> <FormControl>
<FormLabel>Available Icons</FormLabel> <FormLabel>{t('iconPicker.availableIcons')}</FormLabel>
<Grid <Grid
container container
spacing={1} spacing={1}
@@ -85,7 +87,7 @@ const IconPickerModal = ({
lineHeight: 1.2, lineHeight: 1.2,
}} }}
> >
{iconData.name} {t(`icons.${iconData.key}`)}
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>

View File

@@ -12,6 +12,7 @@ import {
} from '@mui/joy' } from '@mui/joy'
import moment from 'moment' import moment from 'moment'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocalization } from '../../../contexts/LocalizationContext' import { useLocalization } from '../../../contexts/LocalizationContext'
import ModalActions from '../../../components/common/ModalActions' import ModalActions from '../../../components/common/ModalActions'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal' import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
@@ -24,6 +25,7 @@ import {
import ConfirmationModal from './ConfirmationModal' import ConfirmationModal from './ConfirmationModal'
const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => { const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
const { t } = useTranslation('timer')
const { ResponsiveModal } = useResponsiveModal() const { ResponsiveModal } = useResponsiveModal()
const { fmt } = useLocalization() const { fmt } = useLocalization()
@@ -181,8 +183,8 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
{ {
onSuccess: () => { onSuccess: () => {
showSuccess({ showSuccess({
title: 'Session updated', title: t('toast.sessionUpdatedTitle'),
message: 'Timer session has been updated successfully.', message: t('toast.sessionUpdatedMessage'),
}) })
refetchTimer() refetchTimer()
cancelEditingSession(sessionId) cancelEditingSession(sessionId)
@@ -190,15 +192,15 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
}, },
onError: () => { onError: () => {
showError({ showError({
title: 'Failed to update session', title: t('toast.sessionUpdateFailTitle'),
message: 'Please try again.', message: t('toast.tryAgain'),
}) })
}, },
}, },
) )
} catch (error) { } catch (error) {
showError({ showError({
title: 'Error updating session', title: t('toast.sessionUpdateErrorTitle'),
message: error.message, message: error.message,
}) })
} finally { } finally {
@@ -213,15 +215,15 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
{ {
onSuccess: () => { onSuccess: () => {
showSuccess({ showSuccess({
title: 'Session deleted', title: t('toast.sessionDeletedTitle'),
message: 'Timer session has been deleted successfully.', message: t('toast.sessionDeletedMessage'),
}) })
refetchTimer() refetchTimer()
onTimerUpdate?.() onTimerUpdate?.()
}, },
onError: error => { onError: error => {
showError({ showError({
title: 'Error deleting session', title: t('toast.sessionDeleteErrorTitle'),
message: error.message, message: error.message,
}) })
}, },
@@ -235,10 +237,10 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
const confirmDeleteSession = sessionId => { const confirmDeleteSession = sessionId => {
setConfirmDeleteConfig({ setConfirmDeleteConfig({
isOpen: true, isOpen: true,
title: 'Delete Timer Session', title: t('toast.deleteConfirmTitle'),
message: 'Are you sure you want to delete this timer session?', message: t('toast.deleteConfirmMessage'),
confirmText: 'Delete', confirmText: t('common:delete'),
cancelText: 'Cancel', cancelText: t('common:cancel'),
color: 'danger', color: 'danger',
onClose: isConfirmed => { onClose: isConfirmed => {
if (isConfirmed) { if (isConfirmed) {
@@ -310,23 +312,23 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
tertiary={ tertiary={
!loading && timerData && !editingSessions[timerData.id] !loading && timerData && !editingSessions[timerData.id]
? { ? {
label: 'Delete', label: t('common:delete'),
color: 'danger', color: 'danger',
onClick: () => confirmDeleteSession(timerData.id), onClick: () => confirmDeleteSession(timerData.id),
} }
: undefined : undefined
} }
secondary={{ label: 'Close', onClick: handleClose }} secondary={{ label: t('common:close'), onClick: handleClose }}
primary={ primary={
!loading && timerData !loading && timerData
? editingSessions[timerData.id] ? editingSessions[timerData.id]
? { ? {
label: 'Save', label: t('common:save'),
onClick: () => saveSession(timerData.id), onClick: () => saveSession(timerData.id),
loading, loading,
} }
: { : {
label: 'Edit', label: t('common:edit'),
startDecorator: <Edit />, startDecorator: <Edit />,
onClick: () => startEditingSession(), onClick: () => startEditingSession(),
} }
@@ -337,13 +339,13 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
> >
{loading && ( {loading && (
<Alert color='neutral' sx={{ mb: 2 }}> <Alert color='neutral' sx={{ mb: 2 }}>
Loading timer data... {t('loading')}
</Alert> </Alert>
)} )}
{!loading && !timerData && ( {!loading && !timerData && (
<Alert color='warning' sx={{ mb: 2 }}> <Alert color='warning' sx={{ mb: 2 }}>
No timer data found for this chore. {t('noData')}
</Alert> </Alert>
)} )}
@@ -403,7 +405,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
color: 'text.primary', color: 'text.primary',
}} }}
> >
Active Work {t('activeWork')}
</Typography> </Typography>
</Box> </Box>
<Box> <Box>
@@ -457,7 +459,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
color: 'text.primary', color: 'text.primary',
}} }}
> >
Break Time {t('breakTime')}
</Typography> </Typography>
</Box> </Box>
<Box> <Box>
@@ -511,7 +513,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
color: 'text.primary', color: 'text.primary',
}} }}
> >
Work Sessions {t('workSessions')}
</Typography> </Typography>
</Box> </Box>
<Box> <Box>
@@ -565,7 +567,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
color: 'text.primary', color: 'text.primary',
}} }}
> >
Total Time {t('totalTime')}
</Typography> </Typography>
</Box> </Box>
<Box> <Box>
@@ -597,12 +599,12 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
level='body-xs' level='body-xs'
sx={{ color: 'text.secondary', fontWeight: 'medium' }} sx={{ color: 'text.secondary', fontWeight: 'medium' }}
> >
Work vs Break Distribution {t('distribution')}
</Typography> </Typography>
<Typography level='body-xs' sx={{ color: 'text.tertiary' }}> <Typography level='body-xs' sx={{ color: 'text.tertiary' }}>
{calculateCurrentActiveDuration() > 0 {calculateCurrentActiveDuration() > 0
? `${Math.round((calculateCurrentActiveDuration() / calculateTotalDuration()) * 100)}% active` ? `${Math.round((calculateCurrentActiveDuration() / calculateTotalDuration()) * 100)}% active`
: 'No active time yet'} : t('noActiveTime')}
</Typography> </Typography>
</Box> </Box>
<Box <Box
@@ -630,7 +632,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
{/* Time Session */} {/* Time Session */}
<Box> <Box>
<Typography level='h4' sx={{ mb: 2 }}> <Typography level='h4' sx={{ mb: 2 }}>
Session Breakdown {t('sessionBreakdown')}
</Typography> </Typography>
<Box> <Box>
@@ -644,7 +646,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
level='body-sm' level='body-sm'
sx={{ fontWeight: 'bold', mb: 2 }} sx={{ fontWeight: 'bold', mb: 2 }}
> >
Work Sessions ({timerData.pauseLog.length}) {t('workSessions')} ({timerData.pauseLog.length})
</Typography> </Typography>
<Box <Box
@@ -795,7 +797,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
level='body-sm' level='body-sm'
sx={{ fontWeight: 'bold' }} sx={{ fontWeight: 'bold' }}
> >
Sessions {t('sessions')}
</Typography> </Typography>
<Button <Button
size='sm' size='sm'
@@ -803,7 +805,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
startDecorator={<Add />} startDecorator={<Add />}
onClick={() => addPauseLogEntry(timerData.id)} onClick={() => addPauseLogEntry(timerData.id)}
> >
Add Session {t('addSession')}
</Button> </Button>
</Box> </Box>
@@ -855,7 +857,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
level='body-xs' level='body-xs'
sx={{ fontWeight: 'bold' }} sx={{ fontWeight: 'bold' }}
> >
Start Time {t('startTime')}
</Typography> </Typography>
<Input <Input
type='datetime-local' type='datetime-local'
@@ -878,7 +880,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
level='body-xs' level='body-xs'
sx={{ fontWeight: 'bold' }} sx={{ fontWeight: 'bold' }}
> >
End Time {t('endTime')}
</Typography> </Typography>
<Input <Input
type='datetime-local' type='datetime-local'
@@ -903,7 +905,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
} }
/> />
<FormHelperText> <FormHelperText>
Leave empty if session is ongoing {t('leaveEmpty')}
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
@@ -941,7 +943,7 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
{!timerData && ( {!timerData && (
<Alert color='neutral' sx={{ mt: 2 }}> <Alert color='neutral' sx={{ mt: 2 }}>
No timer session found for this chore. {t('noSessionForChore')}
</Alert> </Alert>
)} )}
</Box> </Box>

View File

@@ -11,6 +11,7 @@ import {
Stack, Stack,
Typography, Typography,
} from '@mui/joy' } from '@mui/joy'
import { useTranslation } from 'react-i18next'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import ModalActions from '../../components/common/ModalActions.jsx' import ModalActions from '../../components/common/ModalActions.jsx'
@@ -18,6 +19,7 @@ import { useResponsiveModal } from '../../hooks/useResponsiveModal.js'
import { resolvePhotoURL } from '../../utils/Helpers.jsx' import { resolvePhotoURL } from '../../utils/Helpers.jsx'
function RedeemPointsModal({ config }) { function RedeemPointsModal({ config }) {
const { t } = useTranslation('points')
const { ResponsiveModal } = useResponsiveModal() const { ResponsiveModal } = useResponsiveModal()
const [points, setPoints] = useState(0) const [points, setPoints] = useState(0)
@@ -56,12 +58,12 @@ function RedeemPointsModal({ config }) {
open={config?.isOpen} open={config?.isOpen}
onClose={config?.onClose} onClose={config?.onClose}
size='md' size='md'
title='Redeem Points' title={t('redeem')}
footer={ footer={
<ModalActions <ModalActions
secondary={{ label: 'Cancel', onClick: config?.onClose }} secondary={{ label: t('common:cancel'), onClick: config?.onClose }}
primary={{ primary={{
label: 'Redeem', label: t('redeemModal.redeemButton'),
startDecorator: <CreditCard />, startDecorator: <CreditCard />,
disabled: !canRedeem, disabled: !canRedeem,
onClick: () => onClick: () =>
@@ -103,7 +105,7 @@ function RedeemPointsModal({ config }) {
startDecorator={<Toll />} startDecorator={<Toll />}
sx={{ mt: 0.5 }} sx={{ mt: 0.5 }}
> >
{config?.available || 0} points available {t('redeemModal.pointsAvailable', { count: config?.available || 0 })}
</Chip> </Chip>
</Box> </Box>
</Stack> </Stack>
@@ -112,7 +114,7 @@ function RedeemPointsModal({ config }) {
{/* Points Input Section */} {/* Points Input Section */}
<FormControl> <FormControl>
<FormLabel sx={{ fontWeight: 600, mb: 1 }}> <FormLabel sx={{ fontWeight: 600, mb: 1 }}>
Points to Redeem {t('redeemModal.pointsToRedeem')}
</FormLabel> </FormLabel>
<Input <Input
type='number' type='number'
@@ -124,7 +126,7 @@ function RedeemPointsModal({ config }) {
input: { input: {
min: 0, min: 0,
max: config?.available || 0, max: config?.available || 0,
placeholder: 'Enter points...', placeholder: t('redeemModal.placeholder'),
}, },
}} }}
onChange={e => handlePointsChange(e.target.value)} onChange={e => handlePointsChange(e.target.value)}
@@ -140,7 +142,7 @@ function RedeemPointsModal({ config }) {
/> />
{points > config?.available && ( {points > config?.available && (
<Typography level='body-xs' sx={{ color: 'danger.500', mt: 0.5 }}> <Typography level='body-xs' sx={{ color: 'danger.500', mt: 0.5 }}>
Cannot exceed available points {t('redeemModal.cannotExceed')}
</Typography> </Typography>
)} )}
</FormControl> </FormControl>
@@ -148,7 +150,7 @@ function RedeemPointsModal({ config }) {
{/* Quick Selection Buttons */} {/* Quick Selection Buttons */}
<Box> <Box>
<Typography level='body-sm' sx={{ fontWeight: 600, mb: 1.5 }}> <Typography level='body-sm' sx={{ fontWeight: 600, mb: 1.5 }}>
Quick Add: {t('redeemModal.quickAdd')}
</Typography> </Typography>
<Stack <Stack
direction='row' direction='row'
@@ -199,19 +201,21 @@ function RedeemPointsModal({ config }) {
}} }}
> >
<Typography level='body-sm' sx={{ color: 'text.secondary' }}> <Typography level='body-sm' sx={{ color: 'text.secondary' }}>
You are about to redeem {t('redeemModal.aboutToRedeem')}
</Typography> </Typography>
<Typography <Typography
level='h4' level='h4'
sx={{ color: 'primary.600', fontWeight: 700 }} sx={{ color: 'primary.600', fontWeight: 700 }}
> >
{points} points {t('redeemModal.amount', { count: points })}
</Typography> </Typography>
<Typography <Typography
level='body-xs' level='body-xs'
sx={{ color: 'text.secondary', mt: 0.5 }} sx={{ color: 'text.secondary', mt: 0.5 }}
> >
Remaining: {(config?.available || 0) - points} points {t('redeemModal.remaining', {
count: (config?.available || 0) - points,
})}
</Typography> </Typography>
</Card> </Card>
)} )}

View File

@@ -11,6 +11,7 @@ import {
Typography, Typography,
} from '@mui/joy' } from '@mui/joy'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import ProjectModal from '../Modals/Inputs/ProjectModal' import ProjectModal from '../Modals/Inputs/ProjectModal'
@@ -40,6 +41,7 @@ const ProjectCardContent = ({
onCardClick, onCardClick,
onToggleActions, onToggleActions,
}) => { }) => {
const { t } = useTranslation('projects')
// Check if current user owns this project // Check if current user owns this project
const isOwnedByCurrentUser = project.created_by === currentUserId const isOwnedByCurrentUser = project.created_by === currentUserId
const isDefaultProject = project.id === 'default' const isDefaultProject = project.id === 'default'
@@ -144,7 +146,7 @@ const ProjectCardContent = ({
fontWeight: 'md', fontWeight: 'md',
}} }}
> >
Default {t('defaultChip')}
</Chip> </Chip>
)} )}
</Typography> </Typography>
@@ -178,7 +180,7 @@ const ProjectCardContent = ({
color: 'primary.500', color: 'primary.500',
}} }}
> >
{taskCount} tasks {t('tasks', { count: taskCount })}
</Chip> </Chip>
{!isOwnedByCurrentUser && !isDefaultProject && ( {!isOwnedByCurrentUser && !isDefaultProject && (
@@ -193,7 +195,7 @@ const ProjectCardContent = ({
fontWeight: 'md', fontWeight: 'md',
}} }}
> >
Shared {t('shared')}
</Chip> </Chip>
)} )}
</Box> </Box>
@@ -218,6 +220,7 @@ const ProjectCardContent = ({
} }
const ProjectView = () => { const ProjectView = () => {
const { t } = useTranslation('projects')
const { data: projects, isProjectsLoading, isError } = useProjects() const { data: projects, isProjectsLoading, isError } = useProjects()
const { data: userProfile } = useUserProfile() const { data: userProfile } = useUserProfile()
const { data: chores = { res: [] } } = useChores(false) // false to exclude archived const { data: chores = { res: [] } } = useChores(false) // false to exclude archived
@@ -250,11 +253,11 @@ const ProjectView = () => {
const project = userProjects.find(p => p.id === id) const project = userProjects.find(p => p.id === id)
setConfirmationModel({ setConfirmationModel({
isOpen: true, isOpen: true,
title: 'Delete Project', title: t('delete.title'),
message: `Are you sure you want to delete "${project?.name}"? This will remove the project but keep all tasks (they'll move to the Default Project).`, message: t('delete.message', { name: project?.name }),
confirmText: 'Delete', confirmText: t('common:delete'),
color: 'danger', color: 'danger',
cancelText: 'Cancel', cancelText: t('common:cancel'),
onClose: confirmed => { onClose: confirmed => {
if (confirmed === true) { if (confirmed === true) {
handleDeleteProject(id) handleDeleteProject(id)
@@ -347,7 +350,7 @@ const ProjectView = () => {
if (isError) { if (isError) {
return ( return (
<Typography color='danger' textAlign='center'> <Typography color='danger' textAlign='center'>
Failed to load projects. Please try again. {t('loadError')}
</Typography> </Typography>
) )
} }
@@ -360,11 +363,10 @@ const ProjectView = () => {
level='h3' level='h3'
sx={{ fontWeight: 'lg', color: 'text.primary' }} sx={{ fontWeight: 'lg', color: 'text.primary' }}
> >
Projects {t('common:navigation.projects')}
</Typography> </Typography>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}> <Typography level='body-sm' sx={{ color: 'text.secondary' }}>
Organize your tasks into projects. Create custom workspaces to keep {t('blurb')}
your tasks organized and easily accessible.
</Typography> </Typography>
</Stack> </Stack>
</Box> </Box>
@@ -378,8 +380,8 @@ const ProjectView = () => {
<ProjectCardContent <ProjectCardContent
project={{ project={{
id: 'default', id: 'default',
name: 'Default Project', name: t('chores:toolbar.defaultProject'),
description: 'All tasks without a specific project', description: t('defaultDescription'),
icon: 'FolderOpen', icon: 'FolderOpen',
color: '#1976d2', color: '#1976d2',
created_by: userProfile?.id, created_by: userProfile?.id,
@@ -389,7 +391,7 @@ const ProjectView = () => {
onCardClick={() => onCardClick={() =>
handleCardClick({ handleCardClick({
id: 'default', id: 'default',
name: 'Default Project', name: t('chores:toolbar.defaultProject'),
icon: 'FolderOpen', icon: 'FolderOpen',
color: '#1976d2', color: '#1976d2',
}) })
@@ -429,7 +431,7 @@ const ProjectView = () => {
> >
<EditIcon sx={{ fontSize: 20 }} /> <EditIcon sx={{ fontSize: 20 }} />
<Typography level='body-xs' sx={{ mt: 0.5 }}> <Typography level='body-xs' sx={{ mt: 0.5 }}>
Edit {t('common:edit')}
</Typography> </Typography>
</Box> </Box>
</SwipeAction> </SwipeAction>
@@ -450,7 +452,7 @@ const ProjectView = () => {
> >
<DeleteIcon sx={{ fontSize: 20 }} /> <DeleteIcon sx={{ fontSize: 20 }} />
<Typography level='body-xs' sx={{ mt: 0.5 }}> <Typography level='body-xs' sx={{ mt: 0.5 }}>
Delete {t('common:delete')}
</Typography> </Typography>
</Box> </Box>
</SwipeAction> </SwipeAction>

View File

@@ -10,6 +10,7 @@ import {
TrendingUp, TrendingUp,
Update, Update,
} from '@mui/icons-material' } from '@mui/icons-material'
import { useTranslation } from 'react-i18next'
import { import {
Avatar, Avatar,
Box, Box,
@@ -41,6 +42,7 @@ import { useThingHistory } from '../../queries/ThingQueries'
import LoadingComponent from '../components/Loading' import LoadingComponent from '../components/Loading'
const ThingsHistory = () => { const ThingsHistory = () => {
const { t } = useTranslation('things')
const { id } = useParams() const { id } = useParams()
const theme = useTheme() const theme = useTheme()
const { fmt } = useLocalization() const { fmt } = useLocalization()
@@ -186,7 +188,7 @@ const ThingsHistory = () => {
level='title-md' level='title-md'
sx={{ fontWeight: 'lg', color: 'text.primary' }} sx={{ fontWeight: 'lg', color: 'text.primary' }}
> >
Things Overview {t('history.overview')}
</Typography> </Typography>
</Box> </Box>
@@ -266,7 +268,7 @@ const ThingsHistory = () => {
level='title-md' level='title-md'
sx={{ fontWeight: 'lg', color: 'text.primary' }} sx={{ fontWeight: 'lg', color: 'text.primary' }}
> >
Data Visualization {t('history.visualization')}
</Typography> </Typography>
</Box> </Box>
)} )}
@@ -328,7 +330,7 @@ const ThingsHistory = () => {
level='title-md' level='title-md'
sx={{ fontWeight: 'lg', color: 'text.primary' }} sx={{ fontWeight: 'lg', color: 'text.primary' }}
> >
Change History {t('history.changeHistory')}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ borderRadius: 'sm', p: 1, boxShadow: 'md' }}> <Box sx={{ borderRadius: 'sm', p: 1, boxShadow: 'md' }}>
@@ -379,7 +381,7 @@ const ThingsHistory = () => {
display: { xs: 'none', sm: 'block' }, display: { xs: 'none', sm: 'block' },
}} }}
> >
Updated {t('history.updated')}
</Typography> </Typography>
<Chip <Chip

View File

@@ -48,14 +48,21 @@ import { useLabels } from '../Labels/LabelQueries'
import { ChoresGrouper } from '../../utils/Chores' import { ChoresGrouper } from '../../utils/Chores'
import { COLORS, TASK_COLOR } from '../../utils/Colors.jsx' import { COLORS, TASK_COLOR } from '../../utils/Colors.jsx'
import LoadingComponent from '../components/Loading' import LoadingComponent from '../components/Loading'
import { useTranslation } from 'react-i18next'
const groupByDate = history => { const groupByDate = history => {
const aggregated = {} const aggregated = {}
for (let i = 0; i < history.length; i++) { for (let i = 0; i < history.length; i++) {
const item = history[i] const item = history[i]
const date = new Date( // Key by a stable local ISO day (YYYY-MM-DD) so the render-time
item.performedAt || item.updatedAt, // formatter (fmt.date) receives a parseable date instead of a
).toLocaleDateString() // locale-formatted string, which produced "Invalid date".
const d = new Date(item.performedAt || item.updatedAt || item.createdAt)
const date = isNaN(d.getTime())
? 'unknown'
: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
d.getDate(),
).padStart(2, '0')}`
if (!aggregated[date]) { if (!aggregated[date]) {
aggregated[date] = [] aggregated[date] = []
} }
@@ -83,6 +90,7 @@ const ChoreHistoryItem = ({
onViewNote, onViewNote,
onViewDetails, onViewDetails,
}) => { }) => {
const { t } = useTranslation('history')
const cfg = statusConfig[status] ?? statusConfig[1] const cfg = statusConfig[status] ?? statusConfig[1]
return ( return (
@@ -136,7 +144,7 @@ const ChoreHistoryItem = ({
</Typography> </Typography>
{points && ( {points && (
<Chip size='sm' color='success' startDecorator={<Toll />}> <Chip size='sm' color='success' startDecorator={<Toll />}>
{`${points} points`} {t('detail.points', { count: points })}
</Chip> </Chip>
)} )}
{notes && ( {notes && (
@@ -151,7 +159,7 @@ const ChoreHistoryItem = ({
onViewNote?.(notes) onViewNote?.(notes)
}} }}
> >
Note {t('detail.note')}
</Chip> </Chip>
)} )}
</Box> </Box>
@@ -174,7 +182,7 @@ const ChoreHistoryTimeline = ({
{Object.entries(groupedHistory).map(([date, items]) => ( {Object.entries(groupedHistory).map(([date, items]) => (
<Box key={date} sx={{ mb: 4 }}> <Box key={date} sx={{ mb: 4 }}>
<Typography level='title-sm' sx={{ mb: 0.5 }}> <Typography level='title-sm' sx={{ mb: 0.5 }}>
{fmt.date(date)} {date === 'unknown' ? '—' : fmt.date(date)}
</Typography> </Typography>
<Divider /> <Divider />
<Stack spacing={1}> <Stack spacing={1}>
@@ -197,7 +205,7 @@ const ChoreHistoryTimeline = ({
) )
} }
const renderPieChart = (data, size, isPrimary, chartType = null) => { const renderPieChart = (t, data, size, isPrimary, chartType = null) => {
// Filter out items with zero or negative values // Filter out items with zero or negative values
const validData = data.filter(item => item.value > 0) const validData = data.filter(item => item.value > 0)
@@ -216,7 +224,7 @@ const renderPieChart = (data, size, isPrimary, chartType = null) => {
}} }}
> >
<Typography level='body-sm' color='neutral'> <Typography level='body-sm' color='neutral'>
No data available {t('charts.noData')}
</Typography> </Typography>
</Box> </Box>
) )
@@ -400,6 +408,7 @@ const USER_FILTER = (history, userId) => {
} }
const UserActivites = () => { const UserActivites = () => {
const { t } = useTranslation('history')
const { data: userProfile } = useUserProfile() const { data: userProfile } = useUserProfile()
const [tabValue, setTabValue] = React.useState(7) const [tabValue, setTabValue] = React.useState(7)
@@ -448,16 +457,16 @@ const UserActivites = () => {
() => [ () => [
{ {
id: 'status', id: 'status',
label: 'Status', label: t('filter.status'),
type: 'multi-select', type: 'multi-select',
icon: <Checklist />, icon: <Checklist />,
options: [ options: [
{ value: 1, label: 'Completed', color: 'success', icon: <Check sx={{ fontSize: 14 }} /> }, { value: 1, label: t('status.completed'), color: 'success', icon: <Check sx={{ fontSize: 14 }} /> },
{ value: 2, label: 'Skipped', color: 'warning', icon: <Redo sx={{ fontSize: 14 }} /> }, { value: 2, label: t('status.skipped'), color: 'warning', icon: <Redo sx={{ fontSize: 14 }} /> },
{ value: 3, label: 'Pending', color: 'neutral', icon: <HourglassEmpty sx={{ fontSize: 14 }} /> }, { value: 3, label: t('filter.pending'), color: 'neutral', icon: <HourglassEmpty sx={{ fontSize: 14 }} /> },
{ value: 4, label: 'Rejected', color: 'danger', icon: <ThumbDown sx={{ fontSize: 14 }} /> }, { value: 4, label: t('status.rejected'), color: 'danger', icon: <ThumbDown sx={{ fontSize: 14 }} /> },
{ value: 5, label: 'Missed', color: 'danger', icon: <RunningWithErrors sx={{ fontSize: 14 }} /> }, { value: 5, label: t('status.missed'), color: 'danger', icon: <RunningWithErrors sx={{ fontSize: 14 }} /> },
{ value: 6, label: 'Rescheduled', color: 'warning', icon: <Schedule sx={{ fontSize: 14 }} /> }, { value: 6, label: t('status.rescheduled'), color: 'warning', icon: <Schedule sx={{ fontSize: 14 }} /> },
], ],
filterFn: (item, values) => values.includes(item.status), filterFn: (item, values) => values.includes(item.status),
}, },
@@ -465,7 +474,7 @@ const UserActivites = () => {
? [ ? [
{ {
id: 'label', id: 'label',
label: 'Labels', label: t('filter.labels'),
type: 'multi-select', type: 'multi-select',
icon: <Style />, icon: <Style />,
options: userLabels.map(l => ({ options: userLabels.map(l => ({
@@ -492,20 +501,20 @@ const UserActivites = () => {
: []), : []),
{ {
id: 'hasNotes', id: 'hasNotes',
label: 'Has Notes', label: t('filter.hasNotes'),
type: 'boolean', type: 'boolean',
icon: <EventNote />, icon: <EventNote />,
filterFn: item => !!item.notes, filterFn: item => !!item.notes,
}, },
{ {
id: 'hasPoints', id: 'hasPoints',
label: 'Has Points', label: t('filter.hasPoints'),
type: 'boolean', type: 'boolean',
icon: <Toll />, icon: <Toll />,
filterFn: item => (item.points ?? 0) > 0, filterFn: item => (item.points ?? 0) > 0,
}, },
], ],
[userLabels], [userLabels, t],
) )
const { const {
@@ -520,20 +529,20 @@ const UserActivites = () => {
() => [ () => [
{ {
id: 'timePeriod', id: 'timePeriod',
label: 'Time Period', label: t('filter.timePeriod'),
type: 'single-select', type: 'single-select',
icon: <CalendarMonth />, icon: <CalendarMonth />,
defaultValue: 7, defaultValue: 7,
options: [ options: [
{ value: 7, label: '7 Days' }, { value: 7, label: t('period.days', { count: 7 }) },
{ value: 30, label: '30 Days' }, { value: 30, label: t('period.days', { count: 30 }) },
{ value: 90, label: '90 Days' }, { value: 90, label: t('period.days', { count: 90 }) },
{ value: 365, label: 'All Time' }, { value: 365, label: t('period.allTime') },
], ],
}, },
{ {
id: 'completedBy', id: 'completedBy',
label: 'User', label: t('filter.user'),
type: 'single-select', type: 'single-select',
icon: <Person />, icon: <Person />,
options: circleUsers.map(u => ({ options: circleUsers.map(u => ({
@@ -544,7 +553,7 @@ const UserActivites = () => {
}, },
...clientFilterDefs, ...clientFilterDefs,
], ],
[circleUsers, clientFilterDefs], [circleUsers, clientFilterDefs, t],
) )
// Merge server-driven and client-driven active filter states for the bar // Merge server-driven and client-driven active filter states for the bar
@@ -686,7 +695,7 @@ const UserActivites = () => {
// Add unlabeled tasks if there are any // Add unlabeled tasks if there are any
if (unlabeledCount > 0) { if (unlabeledCount > 0) {
result.push({ result.push({
label: 'No Labels', label: t('charts.noLabels'),
value: unlabeledCount, value: unlabeledCount,
color: TASK_COLOR.ANYTIME, color: TASK_COLOR.ANYTIME,
id: 'unlabeled', id: 'unlabeled',
@@ -709,7 +718,9 @@ const UserActivites = () => {
const assignee = circleUsers.find( const assignee = circleUsers.find(
user => user.userId === chore.assignedTo, user => user.userId === chore.assignedTo,
) )
const assigneeName = assignee ? assignee.displayName : 'Unassigned' const assigneeName = assignee
? assignee.displayName
: t('charts.unassigned')
const assigneeId = chore.assignedTo || 'unassigned' const assigneeId = chore.assignedTo || 'unassigned'
if (assigneeCounts[assigneeId]) { if (assigneeCounts[assigneeId]) {
@@ -802,7 +813,7 @@ const UserActivites = () => {
// Add unlabeled tasks duration if there is any // Add unlabeled tasks duration if there is any
if (unlabeledDuration > 0) { if (unlabeledDuration > 0) {
result.push({ result.push({
label: 'No Labels', label: t('charts.noLabels'),
value: Math.round((unlabeledDuration / 3600) * 10) / 10, // Convert to hours and round to 1 decimal value: Math.round((unlabeledDuration / 3600) * 10) / 10, // Convert to hours and round to 1 decimal
color: TASK_COLOR.ANYTIME, color: TASK_COLOR.ANYTIME,
id: 'unlabeled', id: 'unlabeled',
@@ -823,7 +834,7 @@ const UserActivites = () => {
// Iterate through ChoreHistory to get actual time spent per task // Iterate through ChoreHistory to get actual time spent per task
history.forEach(historyItem => { history.forEach(historyItem => {
const duration = historyItem.duration || 0 // duration in seconds from ChoreHistory const duration = historyItem.duration || 0 // duration in seconds from ChoreHistory
const taskName = historyItem.choreName || 'Unknown Task' const taskName = historyItem.choreName || t('charts.unknownTask')
if (taskDurations[taskName]) { if (taskDurations[taskName]) {
taskDurations[taskName].duration += duration taskDurations[taskName].duration += duration
@@ -886,7 +897,7 @@ const UserActivites = () => {
if (totalCompleted > 0) { if (totalCompleted > 0) {
result.push({ result.push({
label: `On time`, label: t('badge.onTimeLabel'),
value: totalCompleted, value: totalCompleted,
color: TASK_COLOR.COMPLETED, color: TASK_COLOR.COMPLETED,
id: 1, id: 1,
@@ -895,7 +906,7 @@ const UserActivites = () => {
if (totalLate > 0) { if (totalLate > 0) {
result.push({ result.push({
label: `Late`, label: t('charts.late'),
value: totalLate, value: totalLate,
color: TASK_COLOR.LATE, color: TASK_COLOR.LATE,
id: 2, id: 2,
@@ -904,7 +915,7 @@ const UserActivites = () => {
if (totalNoDueDate > 0) { if (totalNoDueDate > 0) {
result.push({ result.push({
label: `Completed`, label: t('status.completed'),
value: totalNoDueDate, value: totalNoDueDate,
color: TASK_COLOR.ANYTIME, color: TASK_COLOR.ANYTIME,
id: 3, id: 3,
@@ -919,43 +930,43 @@ const UserActivites = () => {
const chartData = { const chartData = {
history: { history: {
data: historyPieChartData || [], data: historyPieChartData || [],
title: 'Status', title: t('charts.status.title'),
description: 'Completed tasks status', description: t('charts.status.description'),
}, },
due: { due: {
data: choreDuePieChartData || [], data: choreDuePieChartData || [],
title: 'Due Date', title: t('charts.due.title'),
description: 'Current tasks due date', description: t('charts.due.description'),
}, },
// assigned: { // assigned: {
// data: choresAssignedChartData, // data: choresAssignedChartData,
// title: 'Assigned to me', // title: t('chores:sort.assignedToMe'),
// description: 'Tasks assigned to you vs others', // description: 'Tasks assigned to you vs others',
// }, // },
priority: { priority: {
data: choresPriorityChartData || [], data: choresPriorityChartData || [],
title: 'Priority', title: t('charts.priority.title'),
description: 'Tasks by priority', description: t('charts.priority.description'),
}, },
labels: { labels: {
data: choresLabelsChartData || [], data: choresLabelsChartData || [],
title: 'Labels', title: t('charts.labels.title'),
description: 'Tasks by labels', description: t('charts.labels.description'),
}, },
labelsDuration: { labelsDuration: {
data: choresLabelsDurationChartData || [], data: choresLabelsDurationChartData || [],
title: 'Labels (time)', title: t('charts.labelsDuration.title'),
description: 'Time spent by labels (hours)', description: t('charts.labelsDuration.description'),
}, },
tasksTime: { tasksTime: {
data: tasksTimeChartData || [], data: tasksTimeChartData || [],
title: 'Tasks (time)', title: t('charts.tasksTime.title'),
description: 'Time spent by individual tasks (hours)', description: t('charts.tasksTime.description'),
}, },
assigneeBreakdown: { assigneeBreakdown: {
data: choresAssigneeBreakdownChartData || [], data: choresAssigneeBreakdownChartData || [],
title: 'by Assignee', title: t('charts.assigneeBreakdown.title'),
description: 'Tasks grouped by assignee', description: t('charts.assigneeBreakdown.description'),
}, },
} }
if (!userProfile) { if (!userProfile) {
@@ -976,7 +987,7 @@ const UserActivites = () => {
level='title-md' level='title-md'
sx={{ fontWeight: 'lg', color: 'text.primary' }} sx={{ fontWeight: 'lg', color: 'text.primary' }}
> >
Activities {t('activities.title')}
</Typography> </Typography>
</Box> </Box>
@@ -1025,7 +1036,7 @@ const UserActivites = () => {
onViewNote={notes => { onViewNote={notes => {
setNoteViewerConfig({ setNoteViewerConfig({
isOpen: true, isOpen: true,
title: 'Note', title: t('detail.note'),
content: notes, content: notes,
onClose: () => setNoteViewerConfig({ isOpen: false }), onClose: () => setNoteViewerConfig({ isOpen: false }),
}) })
@@ -1153,6 +1164,7 @@ const UserActivites = () => {
}} }}
> >
{renderPieChart( {renderPieChart(
t,
chartData[selectedChart].data, chartData[selectedChart].data,
300, // Increased size for better chart container 300, // Increased size for better chart container
true, true,
@@ -1216,7 +1228,7 @@ const UserActivites = () => {
alignItems: 'center', alignItems: 'center',
}} }}
> >
{renderPieChart(data, 70, false)} {renderPieChart(t, data, 70, false)}
</Box> </Box>
</Card> </Card>
</Grid> </Grid>

View File

@@ -40,6 +40,8 @@ import {
Typography, Typography,
} from '@mui/joy' } from '@mui/joy'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import moment from 'moment'
import { useTranslation } from 'react-i18next'
import LoadingComponent from '../components/Loading.jsx' import LoadingComponent from '../components/Loading.jsx'
import { useChoresHistory } from '../../queries/ChoreQueries.jsx' import { useChoresHistory } from '../../queries/ChoreQueries.jsx'
@@ -48,6 +50,7 @@ import { RedeemPoints } from '../../utils/Fetcher.jsx'
import { resolvePhotoURL } from '../../utils/Helpers.jsx' import { resolvePhotoURL } from '../../utils/Helpers.jsx'
import RedeemPointsModal from '../Modals/RedeemPointsModal' import RedeemPointsModal from '../Modals/RedeemPointsModal'
const UserPoints = () => { const UserPoints = () => {
const { t } = useTranslation('points')
const [tabValue, setTabValue] = useState(7) const [tabValue, setTabValue] = useState(7)
const [isRedeemModalOpen, setIsRedeemModalOpen] = useState(false) const [isRedeemModalOpen, setIsRedeemModalOpen] = useState(false)
const [leaderboardMode, setLeaderboardMode] = useState('points') // 'points' or 'tasks' const [leaderboardMode, setLeaderboardMode] = useState('points') // 'points' or 'tasks'
@@ -104,15 +107,13 @@ const UserPoints = () => {
const currentDate = new Date() const currentDate = new Date()
currentDate.setDate(currentDate.getDate() - i) currentDate.setDate(currentDate.getDate() - i)
daysAggregated.push({ daysAggregated.push({
label: currentDate.toLocaleString('en-US', { weekday: 'short' }), label: moment(currentDate).format('ddd'),
points: 0, points: 0,
tasks: 0, tasks: 0,
}) })
} }
history.forEach(chore => { history.forEach(chore => {
const dayName = new Date(chore.performedAt).toLocaleString('en-US', { const dayName = moment(chore.performedAt).format('ddd')
weekday: 'short',
})
const dayIndex = daysAggregated.findIndex(dayData => { const dayIndex = daysAggregated.findIndex(dayData => {
if (userId) if (userId)
@@ -133,15 +134,13 @@ const UserPoints = () => {
const currentDate = new Date() const currentDate = new Date()
currentDate.setDate(currentDate.getDate() - i) currentDate.setDate(currentDate.getDate() - i)
daysAggregated.push({ daysAggregated.push({
label: currentDate.toLocaleString('en-US', { day: 'numeric' }), label: moment(currentDate).format('D'),
points: 0, points: 0,
tasks: 0, tasks: 0,
}) })
} }
history.forEach(chore => { history.forEach(chore => {
const dayName = new Date(chore.performedAt).toLocaleString('en-US', { const dayName = moment(chore.performedAt).format('D')
day: 'numeric',
})
const dayIndex = daysAggregated.findIndex(dayData => { const dayIndex = daysAggregated.findIndex(dayData => {
if (userId) if (userId)
@@ -164,15 +163,13 @@ const UserPoints = () => {
const currentMonth = new Date() const currentMonth = new Date()
currentMonth.setMonth(currentMonth.getMonth() - i) currentMonth.setMonth(currentMonth.getMonth() - i)
monthlyAggregated.push({ monthlyAggregated.push({
label: currentMonth.toLocaleString('en-US', { month: 'short' }), label: moment(currentMonth).format('MMM'),
points: 0, points: 0,
tasks: 0, tasks: 0,
}) })
} }
history.forEach(chore => { history.forEach(chore => {
const monthName = new Date(chore.performedAt).toLocaleString('en-US', { const monthName = moment(chore.performedAt).format('MMM')
month: 'short',
})
const monthIndex = monthlyAggregated.findIndex(monthData => { const monthIndex = monthlyAggregated.findIndex(monthData => {
if (userId) if (userId)
@@ -195,15 +192,13 @@ const UserPoints = () => {
const currentYear = new Date() const currentYear = new Date()
currentYear.setFullYear(currentYear.getFullYear() - i) currentYear.setFullYear(currentYear.getFullYear() - i)
yearlyAggregated.push({ yearlyAggregated.push({
label: currentYear.toLocaleString('en-US', { year: 'numeric' }), label: moment(currentYear).format('YYYY'),
points: 0, points: 0,
tasks: 0, tasks: 0,
}) })
} }
history.forEach(chore => { history.forEach(chore => {
const yearName = new Date(chore.performedAt).toLocaleString('en-US', { const yearName = moment(chore.performedAt).format('YYYY')
year: 'numeric',
})
const yearIndex = yearlyAggregated.findIndex(yearData => { const yearIndex = yearlyAggregated.findIndex(yearData => {
if (userId) if (userId)
@@ -305,14 +300,14 @@ const UserPoints = () => {
level='h3' level='h3'
sx={{ fontWeight: 'lg', color: 'text.primary' }} sx={{ fontWeight: 'lg', color: 'text.primary' }}
> >
{leaderboardMode === 'points' ? 'Points' : 'Tasks'} Leaderboard {leaderboardMode === 'points'
? t('leaderboard.titlePoints')
: t('leaderboard.titleTasks')}
</Typography> </Typography>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}> <Typography level='body-sm' sx={{ color: 'text.secondary' }}>
Rankings based on{' '}
{leaderboardMode === 'points' {leaderboardMode === 'points'
? 'points earned' ? t('leaderboard.subtitlePoints')
: 'tasks completed'}{' '} : t('leaderboard.subtitleTasks')}
during the selected time period
</Typography> </Typography>
</Stack> </Stack>
</Box> </Box>
@@ -358,9 +353,9 @@ const UserPoints = () => {
}} }}
> >
{[ {[
{ label: '7D', value: 7 }, { label: t('tabs.short7d'), value: 7 },
{ label: '6M', value: 6 * 30 }, { label: t('tabs.short6m'), value: 6 * 30 },
{ label: 'All', value: 24 * 30 }, { label: t('tabs.shortAll'), value: 24 * 30 },
].map((tab, index) => ( ].map((tab, index) => (
<Tab <Tab
key={index} key={index}
@@ -408,7 +403,7 @@ const UserPoints = () => {
sx={{ cursor: 'pointer' }} sx={{ cursor: 'pointer' }}
onClick={() => setLeaderboardMode('points')} onClick={() => setLeaderboardMode('points')}
> >
Points {t('leaderboard.modePoints')}
</Chip> </Chip>
<SwapHoriz <SwapHoriz
sx={{ fontSize: '0.875rem', color: 'text.tertiary' }} sx={{ fontSize: '0.875rem', color: 'text.tertiary' }}
@@ -420,7 +415,7 @@ const UserPoints = () => {
sx={{ cursor: 'pointer' }} sx={{ cursor: 'pointer' }}
onClick={() => setLeaderboardMode('tasks')} onClick={() => setLeaderboardMode('tasks')}
> >
Tasks {t('leaderboard.modeTasks')}
</Chip> </Chip>
</Box> </Box>
</Box> </Box>
@@ -512,7 +507,7 @@ const UserPoints = () => {
color='primary' color='primary'
sx={{ ml: 1 }} sx={{ ml: 1 }}
> >
You {t('leaderboard.you')}
</Chip> </Chip>
)} )}
</Typography> </Typography>
@@ -520,8 +515,10 @@ const UserPoints = () => {
level='body-xs' level='body-xs'
sx={{ color: 'text.secondary' }} sx={{ color: 'text.secondary' }}
> >
{user.periodTasks} tasks {user.avgPointsPerTask} avg {t('leaderboard.tasksAndAvg', {
per task tasks: user.periodTasks,
avg: user.avgPointsPerTask,
})}
</Typography> </Typography>
</Stack> </Stack>
</Box> </Box>
@@ -552,8 +549,12 @@ const UserPoints = () => {
</Box> </Box>
<Typography level='body-xs' sx={{ color: 'text.tertiary' }}> <Typography level='body-xs' sx={{ color: 'text.tertiary' }}>
{leaderboardMode === 'points' {leaderboardMode === 'points'
? `${user.availablePoints} available` ? t('leaderboard.available', {
: `${user.periodPoints} points`} count: user.availablePoints,
})
: t('leaderboard.points', {
count: user.periodPoints,
})}
</Typography> </Typography>
</Stack> </Stack>
@@ -591,7 +592,7 @@ const UserPoints = () => {
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Analytics sx={{ fontSize: '1.5rem', color: 'primary.500' }} /> <Analytics sx={{ fontSize: '1.5rem', color: 'primary.500' }} />
<Typography level='h4' sx={{ fontWeight: 'lg', color: 'text.primary' }}> <Typography level='h4' sx={{ fontWeight: 'lg', color: 'text.primary' }}>
Filter & Analysis {t('filter.analysisTitle')}
</Typography> </Typography>
</Box> </Box>
@@ -610,7 +611,7 @@ const UserPoints = () => {
> >
<Stack spacing={2}> <Stack spacing={2}>
<Typography level='title-sm' sx={{ color: 'text.secondary' }}> <Typography level='title-sm' sx={{ color: 'text.secondary' }}>
Filter Points {t('filter.title')}
</Typography> </Typography>
<Stack <Stack
@@ -621,7 +622,7 @@ const UserPoints = () => {
{/* User Filter */} {/* User Filter */}
<Box sx={{ flex: 1, minWidth: 200 }}> <Box sx={{ flex: 1, minWidth: 200 }}>
<Typography level='body-sm' sx={{ mb: 1, fontWeight: 500 }}> <Typography level='body-sm' sx={{ mb: 1, fontWeight: 500 }}>
Show points for: {t('filter.showFor')}
</Typography> </Typography>
<Select <Select
sx={{ sx={{
@@ -688,7 +689,7 @@ const UserPoints = () => {
{/* Time Period Filter */} {/* Time Period Filter */}
<Box sx={{ flex: 1, minWidth: 200 }}> <Box sx={{ flex: 1, minWidth: 200 }}>
<Typography level='body-sm' sx={{ mb: 1, fontWeight: 500 }}> <Typography level='body-sm' sx={{ mb: 1, fontWeight: 500 }}>
Time period: {t('filter.timePeriod')}
</Typography> </Typography>
<Tabs <Tabs
onChange={(e, tabValue) => { onChange={(e, tabValue) => {
@@ -713,9 +714,9 @@ const UserPoints = () => {
}} }}
> >
{[ {[
{ label: '7 Days', value: 7 }, { label: t('tabs.days7'), value: 7 },
{ label: '6 Months', value: 6 * 30 }, { label: t('tabs.months6'), value: 6 * 30 },
{ label: 'All Time', value: 24 * 30 }, { label: t('tabs.allTime'), value: 24 * 30 },
].map((tab, index) => ( ].map((tab, index) => (
<Tab <Tab
key={index} key={index}
@@ -759,7 +760,7 @@ const UserPoints = () => {
}} }}
sx={{ mt: 'auto' }} sx={{ mt: 'auto' }}
> >
Redeem Points {t('redeem')}
</Button> </Button>
</Box> </Box>
)} )}
@@ -788,27 +789,32 @@ const UserPoints = () => {
const pointsCards = [ const pointsCards = [
{ {
icon: <Toll />, icon: <Toll />,
title: 'Available', title: t('cards.available.title'),
text: `${availablePoints} points`, text: t('leaderboard.points', { count: availablePoints }),
subtext: 'Ready to redeem', subtext: t('cards.available.subtext'),
}, },
{ {
icon: <Redeem />, icon: <Redeem />,
title: 'Redeemed', title: t('cards.redeemed.title'),
text: `${redeemedPoints} points`, text: t('leaderboard.points', { count: redeemedPoints }),
subtext: 'Previously used', subtext: t('cards.redeemed.subtext'),
}, },
{ {
icon: <AccountBalanceWallet />, icon: <AccountBalanceWallet />,
title: 'Total', title: t('cards.total.title'),
text: `${totalPoints} points`, text: t('leaderboard.points', { count: totalPoints }),
subtext: 'All time earned', subtext: t('cards.total.subtext'),
}, },
{ {
icon: <TrendingUp />, icon: <TrendingUp />,
title: 'Period Points', title: t('cards.period.title'),
text: `${periodPoints} points`, text: t('leaderboard.points', { count: periodPoints }),
subtext: `${tabValue === 24 * 30 ? 'All time' : tabValue === 6 * 30 ? 'Last 6 months' : `Last ${tabValue} days`}`, subtext:
tabValue === 24 * 30
? t('cards.period.allTime')
: tabValue === 6 * 30
? t('cards.period.last6Months')
: t('cards.period.lastDays', { count: tabValue }),
}, },
] ]
@@ -871,24 +877,24 @@ const UserPoints = () => {
{/* Current Filter Summary */} {/* Current Filter Summary */}
<Box sx={{ mb: 3, textAlign: 'center' }}> <Box sx={{ mb: 3, textAlign: 'center' }}>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}> <Typography level='body-sm' sx={{ color: 'text.secondary' }}>
Showing points for{' '} {t('summary.showingFor')}{' '}
<Typography <Typography
component='span' component='span'
sx={{ fontWeight: 600, color: 'primary.500' }} sx={{ fontWeight: 600, color: 'primary.500' }}
> >
{circleUsers.find(user => user.userId === selectedUser) {circleUsers.find(user => user.userId === selectedUser)
?.displayName || 'Unknown User'} ?.displayName || t('summary.unknownUser')}
</Typography>{' '} </Typography>{' '}
over the{' '} {t('summary.overThe')}{' '}
<Typography <Typography
component='span' component='span'
sx={{ fontWeight: 600, color: 'primary.500' }} sx={{ fontWeight: 600, color: 'primary.500' }}
> >
{tabValue === 24 * 30 {tabValue === 24 * 30
? 'All Time' ? t('summary.allTime')
: tabValue === 6 * 30 : tabValue === 6 * 30
? 'Last 6 Months' ? t('summary.last6Months')
: `Last ${tabValue} Days`} : t('summary.lastDays', { count: tabValue })}
</Typography> </Typography>
</Typography> </Typography>
</Box> </Box>
@@ -908,7 +914,7 @@ const UserPoints = () => {
level='h4' level='h4'
sx={{ fontWeight: 'lg', color: 'text.primary' }} sx={{ fontWeight: 'lg', color: 'text.primary' }}
> >
Points Trend {t('trend.title')}
</Typography> </Typography>
</Box> </Box>

View File

@@ -10,6 +10,7 @@ import {
MenuItem, MenuItem,
Typography, Typography,
} from '@mui/joy' } from '@mui/joy'
import { useTranslation } from 'react-i18next'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint' import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
@@ -25,6 +26,7 @@ const ProjectSelector = ({
onProjectSelect, onProjectSelect,
showKeyboardShortcuts = false, showKeyboardShortcuts = false,
}) => { }) => {
const { t } = useTranslation('projects')
const { data: projects = [], isLoading } = useProjects() const { data: projects = [], isLoading } = useProjects()
const navigate = useNavigate() const navigate = useNavigate()
@@ -265,7 +267,7 @@ const ProjectSelector = ({
</ListItemDecorator> </ListItemDecorator>
<ListItemContent> <ListItemContent>
<Typography level='title-sm' sx={{ fontWeight: 600 }}> <Typography level='title-sm' sx={{ fontWeight: 600 }}>
Projects {t('selector.title')}
</Typography> </Typography>
</ListItemContent> </ListItemContent>
</MenuItem> </MenuItem>
@@ -339,7 +341,7 @@ const ProjectSelector = ({
: 'var(--joy-palette-text-primary)', : 'var(--joy-palette-text-primary)',
}} }}
> >
Default Project {t('chores:toolbar.defaultProject')}
</Typography> </Typography>
{effectiveSelectedProject === 'Default Project' && ( {effectiveSelectedProject === 'Default Project' && (
<Check <Check
@@ -477,13 +479,13 @@ const ProjectSelector = ({
fontWeight: 500, fontWeight: 500,
}} }}
> >
Create New Project {t('selector.createNew')}
</Typography> </Typography>
<Typography <Typography
level='body-xs' level='body-xs'
sx={{ color: 'var(--joy-palette-text-tertiary)' }} sx={{ color: 'var(--joy-palette-text-tertiary)' }}
> >
Add a custom project workspace {t('selector.createNewDescription')}
</Typography> </Typography>
</ListItemContent> </ListItemContent>
</MenuItem> </MenuItem>
@@ -512,13 +514,13 @@ const ProjectSelector = ({
fontWeight: 500, fontWeight: 500,
}} }}
> >
Manage Projects {t('selector.manage')}
</Typography> </Typography>
<Typography <Typography
level='body-xs' level='body-xs'
sx={{ color: 'var(--joy-palette-text-tertiary)' }} sx={{ color: 'var(--joy-palette-text-tertiary)' }}
> >
View, edit, and organize all projects {t('selector.manageDescription')}
</Typography> </Typography>
</ListItemContent> </ListItemContent>
</MenuItem> </MenuItem>