Merge pull request #212 from everysingletear/i18n/new-namespaces
i18n: add things, history, projects, labels, filters, timer and points namespaces (Part of #145)
This commit is contained in:
@@ -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,19 @@ i18n
|
|||||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||||
},
|
},
|
||||||
|
|
||||||
ns: ['common', 'settings', 'chores', 'auth'],
|
ns: [
|
||||||
|
'auth',
|
||||||
|
'common',
|
||||||
|
'settings',
|
||||||
|
'chores',
|
||||||
|
'labels',
|
||||||
|
'history',
|
||||||
|
'filters',
|
||||||
|
'things',
|
||||||
|
'projects',
|
||||||
|
'timer',
|
||||||
|
'points',
|
||||||
|
],
|
||||||
defaultNS: 'common',
|
defaultNS: 'common',
|
||||||
|
|
||||||
detection: {
|
detection: {
|
||||||
|
|||||||
@@ -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