i18n: extract things, history, projects, labels, filters, timer and points
Part of #145. Fourteen files across seven feature areas that had no namespace yet: the things create/edit modals and their history, the chore history detail and edit modals, the activity feed, the points view and its redemption modal, the project view with its selector and icon picker, the label view, the advanced filter builder and the timer edit modal. Seven new namespaces registered in `src/i18n/config.js` in one change rather than one per PR, so the `ns:` array is touched once and my other extraction PRs cannot conflict with this one. Namespaces stay feature-scoped as described in #145; if you'd rather fold any of these into `common` or `chores`, say which and I'll rework it. Dictionaries: `history` 57 keys, `points` 50, `timer` 25, `projects` 16, `things` 14, `filters` 13, `labels` 5. English only — no translations, no behaviour change. Every t() value is checked against this branch's base: the string must appear character-for-character in the code it replaces (226 call sites). Three values in `UserPoints` are matched loosely and worth naming. The base builds the leaderboard heading and subtitle around a ternary — `{mode === 'points' ? 'Points' : 'Tasks'} Leaderboard` and `Rankings based on {…} during the selected time period` — so neither full sentence exists contiguously in the source. Each key holds exactly what one branch renders. The sentences are kept whole rather than split around the ternary, since a sentence assembled from fragments cannot be reordered by a translator.
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
public/locales/en/filters.json
Normal file
17
public/locales/en/filters.json
Normal 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"
|
||||||
|
}
|
||||||
89
public/locales/en/history.json
Normal file
89
public/locales/en/history.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
public/locales/en/labels.json
Normal file
9
public/locales/en/labels.json
Normal 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."
|
||||||
|
}
|
||||||
74
public/locales/en/points.json
Normal file
74
public/locales/en/points.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
public/locales/en/projects.json
Normal file
24
public/locales/en/projects.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
public/locales/en/things.json
Normal file
18
public/locales/en/things.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
public/locales/en/timer.json
Normal file
29
public/locales/en/timer.json
Normal 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."
|
||||||
|
}
|
||||||
@@ -22,7 +22,18 @@ i18n
|
|||||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||||
},
|
},
|
||||||
|
|
||||||
ns: ['common', 'settings', 'chores'],
|
ns: [
|
||||||
|
'common',
|
||||||
|
'settings',
|
||||||
|
'chores',
|
||||||
|
'labels',
|
||||||
|
'history',
|
||||||
|
'filters',
|
||||||
|
'things',
|
||||||
|
'projects',
|
||||||
|
'timer',
|
||||||
|
'points',
|
||||||
|
],
|
||||||
defaultNS: 'common',
|
defaultNS: 'common',
|
||||||
|
|
||||||
detection: {
|
detection: {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user