Merge pull request #210 from everysingletear/i18n/chores
i18n: extract the chore list and its modals (Part of #145)
This commit is contained in:
@@ -100,11 +100,24 @@
|
|||||||
"saveFilter": "Save Filter",
|
"saveFilter": "Save Filter",
|
||||||
"saveAsNew": "Save as New Filter",
|
"saveAsNew": "Save as New Filter",
|
||||||
"groupBy": "Group by",
|
"groupBy": "Group by",
|
||||||
"showTasksFor": "Show tasks for"
|
"showTasksFor": "Show tasks for",
|
||||||
|
"type": {
|
||||||
|
"assignee": "Assignee"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sort": {
|
"sort": {
|
||||||
"assignedToMe": "Assigned to me",
|
"assignedToMe": "Assigned to me",
|
||||||
"smart": "Smart"
|
"smart": "Smart",
|
||||||
|
"createdDate": "Created Date",
|
||||||
|
"updatedDate": "Updated Date",
|
||||||
|
"sortAndGroup": "Sort and Group (Ctrl+G)",
|
||||||
|
"groupBy": "Group By",
|
||||||
|
"quickFilters": "Quick Filters",
|
||||||
|
"assignedTo": "Assigned to:",
|
||||||
|
"availableForMe": "Available for me",
|
||||||
|
"assignedToOthers": "Assigned to others",
|
||||||
|
"createFilter": "Create Filter",
|
||||||
|
"createFilterHint": "Build advanced filter rules"
|
||||||
},
|
},
|
||||||
"thing": {
|
"thing": {
|
||||||
"triggerHint": "Trigger a task when a thing state changes to a desired state",
|
"triggerHint": "Trigger a task when a thing state changes to a desired state",
|
||||||
@@ -124,7 +137,9 @@
|
|||||||
"dueToday": "Due Today",
|
"dueToday": "Due Today",
|
||||||
"pendingApproval": "Pending Approval",
|
"pendingApproval": "Pending Approval",
|
||||||
"dueThisWeek": "Due This Week",
|
"dueThisWeek": "Due This Week",
|
||||||
"noDueDate": "No Due Date"
|
"noDueDate": "No Due Date",
|
||||||
|
"dueDate": "Due Date",
|
||||||
|
"dueLater": "Due Later"
|
||||||
},
|
},
|
||||||
"insights": {
|
"insights": {
|
||||||
"highPriority": "High Priority"
|
"highPriority": "High Priority"
|
||||||
@@ -134,7 +149,16 @@
|
|||||||
},
|
},
|
||||||
"actionMenu": {
|
"actionMenu": {
|
||||||
"view": "View",
|
"view": "View",
|
||||||
"archive": "Archive"
|
"archive": "Archive",
|
||||||
|
"writeNFC": "Write to NFC",
|
||||||
|
"completeWithNote": "Complete with note",
|
||||||
|
"completeInPast": "Complete in past",
|
||||||
|
"skipToNext": "Skip to next due date",
|
||||||
|
"sendNudge": "Send nudge",
|
||||||
|
"history": "History",
|
||||||
|
"clone": "Clone",
|
||||||
|
"moveToProject": "Move to project",
|
||||||
|
"removeDueDate": "Remove due date"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"rescheduledTitle": "Task Rescheduled",
|
"rescheduledTitle": "Task Rescheduled",
|
||||||
@@ -187,18 +211,195 @@
|
|||||||
"someFailedTitle": "Some Tasks Failed",
|
"someFailedTitle": "Some Tasks Failed",
|
||||||
"unexpectedError": "An unexpected error occurred. Please try again.",
|
"unexpectedError": "An unexpected error occurred. Please try again.",
|
||||||
"deletedBulkTitle": "🗑️ Tasks Deleted",
|
"deletedBulkTitle": "🗑️ Tasks Deleted",
|
||||||
"bulkDeleteFailTitle": "Bulk Delete Failed"
|
"bulkDeleteFailTitle": "Bulk Delete Failed",
|
||||||
|
"archivedDate": "Archived Date",
|
||||||
|
"loadFailTitle": "Failed to load archived tasks",
|
||||||
|
"loadFailMsg": "Please try again later.",
|
||||||
|
"deletedMsg": "The archived task has been permanently deleted.",
|
||||||
|
"restoreTasksTitle": "Restore Tasks",
|
||||||
|
"restore": "Restore",
|
||||||
|
"restoreConfirm_one": "Restore {{count}} task to active list?",
|
||||||
|
"restoreConfirm_other": "Restore {{count}} tasks to active list?",
|
||||||
|
"restoredBulkTitle": "📤 Tasks Restored",
|
||||||
|
"restoredCount_one": "Restored {{count}} task{{offline}}.",
|
||||||
|
"restoredCount_other": "Restored {{count}} tasks{{offline}}.",
|
||||||
|
"restoreFailCount_one": "{{count}} task could not be restored.",
|
||||||
|
"restoreFailCount_other": "{{count}} tasks could not be restored.",
|
||||||
|
"bulkRestoreFailTitle": "Bulk Restore Failed",
|
||||||
|
"deleteTasksTitle": "Delete Archived Tasks",
|
||||||
|
"deleteConfirm_one": "Permanently delete {{count}} archived task?\n\nThis action cannot be undone.",
|
||||||
|
"deleteConfirm_other": "Permanently delete {{count}} archived tasks?\n\nThis action cannot be undone.",
|
||||||
|
"deletedCount_one": "Successfully deleted {{count}} task.",
|
||||||
|
"deletedCount_other": "Successfully deleted {{count}} tasks.",
|
||||||
|
"deleteFailCount_one": "{{count}} task could not be deleted.",
|
||||||
|
"deleteFailCount_other": "{{count}} tasks could not be deleted.",
|
||||||
|
"title": "Archived Tasks",
|
||||||
|
"subtitle": "View and manage tasks that have been archived or completed.",
|
||||||
|
"heading": "Archived Tasks",
|
||||||
|
"searchPlaceholder": "Search archived tasks",
|
||||||
|
"switchToCompact": "Switch to Compact View",
|
||||||
|
"switchToCard": "Switch to Card View",
|
||||||
|
"exitMultiSelect": "Exit Multi-select Mode (Ctrl+S)",
|
||||||
|
"enableMultiSelect": "Enable Multi-select Mode (Ctrl+S)",
|
||||||
|
"selected_one": "{{count}} task selected",
|
||||||
|
"selected_other": "{{count}} tasks selected",
|
||||||
|
"selectAllTitle": "Select all visible tasks (Ctrl+A)",
|
||||||
|
"all": "All",
|
||||||
|
"closeMultiSelect": "Close multi-select (Esc)",
|
||||||
|
"clearMultiSelect": "Clear multi-select (Esc)",
|
||||||
|
"close": "Close",
|
||||||
|
"clear": "Clear",
|
||||||
|
"restoreSelectedTitle": "Restore selected tasks (R)",
|
||||||
|
"deleteSelectedTitle": "Delete selected tasks (E)",
|
||||||
|
"clearSearch": "Clear search",
|
||||||
|
"clearFilters": "Clear filters",
|
||||||
|
"count_one": "{{count}} archived task",
|
||||||
|
"count_other": "{{count}} archived tasks",
|
||||||
|
"matching": " matching \"{{term}}\""
|
||||||
},
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"deleteConfirm": "Are you sure you want to delete this chore?"
|
"deleteConfirm": "Are you sure you want to delete this chore?"
|
||||||
},
|
},
|
||||||
"list": {
|
"list": {
|
||||||
"complete": "Complete"
|
"complete": "Complete",
|
||||||
|
"filterSaved": "Filter Saved",
|
||||||
|
"filterSavedMsg": "\"{{name}}\" has been saved",
|
||||||
|
"nothingScheduled": "Nothing scheduled",
|
||||||
|
"createChore": "Create new chore (Cmd+C)",
|
||||||
|
"filterUpdated": "Filter Updated",
|
||||||
|
"filterUpdatedMsg": "\"{{name}}\" has been updated successfully",
|
||||||
|
"advancedFilterCreated": "Advanced Filter Created",
|
||||||
|
"advancedFilterCreatedMsg": "\"{{name}}\" has been created successfully"
|
||||||
},
|
},
|
||||||
"multiToolbar": {
|
"multiToolbar": {
|
||||||
"skip": "Skip"
|
"skip": "Skip",
|
||||||
|
"completeTitle": "Complete selected tasks (Enter)",
|
||||||
|
"complete": "Complete",
|
||||||
|
"skipTitle": "Skip selected tasks (/)",
|
||||||
|
"archiveTitle": "Archive selected tasks (X)",
|
||||||
|
"archive": "Archive",
|
||||||
|
"deleteTitle": "Delete selected tasks (Shift+X)"
|
||||||
},
|
},
|
||||||
"remind": {
|
"remind": {
|
||||||
"title": "Reminders"
|
"title": "Reminders"
|
||||||
|
},
|
||||||
|
"assignee": {
|
||||||
|
"anyone": "Anyone"
|
||||||
|
},
|
||||||
|
"filterChip": {
|
||||||
|
"cancelAll": "Cancel All Filters",
|
||||||
|
"unpin": "Unpin filter",
|
||||||
|
"pin": "Pin filter",
|
||||||
|
"edit": "Edit filter",
|
||||||
|
"delete": "Delete filter"
|
||||||
|
},
|
||||||
|
"multiSelect": {
|
||||||
|
"showShortcuts": "Show keyboard shortcuts",
|
||||||
|
"title": "Multi-select Mode",
|
||||||
|
"selection": "Selection",
|
||||||
|
"selectAll": "Select all visible tasks",
|
||||||
|
"clearOrExit": "Clear selection or exit multi-select mode",
|
||||||
|
"actions": "Actions",
|
||||||
|
"markCompleted": "Mark selected tasks as completed",
|
||||||
|
"deleteSelected": "Delete selected tasks",
|
||||||
|
"interface": "Interface",
|
||||||
|
"quickAdd": "Quick add new task"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"needTitle": "Need Notification?",
|
||||||
|
"needBody": "You need to enable permission to receive notifications, do you want to enable it?",
|
||||||
|
"keepDisabled": "No, Keep it Disabled"
|
||||||
|
},
|
||||||
|
"assigneeCard": {
|
||||||
|
"loading": "Loading tasks by assignee...",
|
||||||
|
"title": "Tasks by Assignee",
|
||||||
|
"scheduled": "Scheduled",
|
||||||
|
"pendingReview": "Pending Review"
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"status": {
|
||||||
|
"inProgress": "In Progress"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"impersonate": {
|
||||||
|
"viewAs": "View tasks as",
|
||||||
|
"switchTitle": "Switch to user view",
|
||||||
|
"switchHint": "Tasks will be filtered to show only assignments for selected user",
|
||||||
|
"chooseUser": "Choose User",
|
||||||
|
"changeUser": "Change User"
|
||||||
|
},
|
||||||
|
"modals": {
|
||||||
|
"changeDueDate": "Change due date",
|
||||||
|
"completePast": "Save Chore that you completed in the past",
|
||||||
|
"delegate": "Delegate to someone else",
|
||||||
|
"selectPerformer": "Select a performer",
|
||||||
|
"addNote": "Add note to attach to this completion:",
|
||||||
|
"complete": "Complete"
|
||||||
|
},
|
||||||
|
"shortcuts": {
|
||||||
|
"allSelectedTitle": "✅ All Tasks Selected",
|
||||||
|
"someSelectedTitle": "🎯 Tasks Selected",
|
||||||
|
"allSelectedAltTitle": "🎯 All Tasks Selected"
|
||||||
|
},
|
||||||
|
"overview": {
|
||||||
|
"title": "Chores Overviews",
|
||||||
|
"search": "Search",
|
||||||
|
"newChore": "New Chore",
|
||||||
|
"unassigned": "Unassigned",
|
||||||
|
"changeDueDate": "Change due date"
|
||||||
|
},
|
||||||
|
"dataError": {
|
||||||
|
"attachmentsFailed": "Failed to fetch attachments"
|
||||||
|
},
|
||||||
|
"nudge": {
|
||||||
|
"title": "Send Nudge",
|
||||||
|
"customMessage": "Custom Message (optional)",
|
||||||
|
"messagePlaceholder": "Add a personal message with your nudge...",
|
||||||
|
"notifyAll": "Notify All Assignees",
|
||||||
|
"notifyAllHint": "If enabled, all members who can see this task will be notified. Otherwise, only the assigned person will receive the nudge."
|
||||||
|
},
|
||||||
|
"nfc": {
|
||||||
|
"errWrite": "Error writing to NFC tag. Please try again.",
|
||||||
|
"errUnsupported": "NFC is not supported by this browser. Copy the URL and write it to an NFC tag using a compatible device.",
|
||||||
|
"titleSuccess": "Tag written!",
|
||||||
|
"titleError": "Something went wrong",
|
||||||
|
"titleWaiting": "Hold near NFC tag",
|
||||||
|
"subSuccess": "Your NFC tag is ready to use.",
|
||||||
|
"subWaiting": "Keep your device near the tag until complete.",
|
||||||
|
"subIdle": "Encode this task link onto any NFC tag.",
|
||||||
|
"tagUrl": "Tag URL",
|
||||||
|
"copyUrl": "Copy URL",
|
||||||
|
"autoComplete": "Auto-complete on scan",
|
||||||
|
"autoCompleteHint": "Mark task done when tag is tapped"
|
||||||
|
},
|
||||||
|
"duePicker": {
|
||||||
|
"today": "Today",
|
||||||
|
"tomorrow": "Tomorrow",
|
||||||
|
"weekend": "Weekend",
|
||||||
|
"nextWeek": "Next week"
|
||||||
|
},
|
||||||
|
"photoTask": {
|
||||||
|
"title": "Scan photo to create task",
|
||||||
|
"cameraUnavailable": "Camera not available",
|
||||||
|
"uploadPhoto": "Upload Photo",
|
||||||
|
"scanDocument": "Scan Document",
|
||||||
|
"capture": "Capture",
|
||||||
|
"processNatively": "Process Natively",
|
||||||
|
"processImage": "Process Image",
|
||||||
|
"createTask": "Create Task",
|
||||||
|
"failedScan": "Failed scan"
|
||||||
|
},
|
||||||
|
"descriptionPlaceholder": "Enter description...",
|
||||||
|
"notifTemplate": {
|
||||||
|
"onDueDate": "On due date",
|
||||||
|
"beforeDue": "{{count}} {{unit}} before due",
|
||||||
|
"afterDue": "{{count}} {{unit}} after due",
|
||||||
|
"errDuplicate": "This notification setting already exists. Please use a different timing.",
|
||||||
|
"errOneDue": "Only one \"Due Alert\" notification is allowed.",
|
||||||
|
"errAllConfigured": "All common {{type}} times are already configured.",
|
||||||
|
"reminder": "Reminder",
|
||||||
|
"dueAlert": "Due Alert",
|
||||||
|
"followUp": "Follow-up",
|
||||||
|
"rememberFuture": "Remember for Future Tasks"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,14 @@
|
|||||||
"tooLargeTitle": "File Too Large",
|
"tooLargeTitle": "File Too Large",
|
||||||
"tooLargeMessage": "The file you are trying to upload is too large.",
|
"tooLargeMessage": "The file you are trying to upload is too large.",
|
||||||
"deniedTitle": "Permission Denied",
|
"deniedTitle": "Permission Denied",
|
||||||
"deniedMessage": "You do not have permission to upload files."
|
"deniedMessage": "You do not have permission to upload files.",
|
||||||
|
"plusFeatureTitle": "Plus Feature",
|
||||||
|
"plusFeatureMessage": "Image uploads are not available in the Basic plan. Upgrade to Plus to add images to your content.",
|
||||||
|
"upgradeTitle": "Upgrade Required",
|
||||||
|
"upgradeMessage": "Image uploads are only available for Plus accounts.",
|
||||||
|
"failedTitle": "Upload Failed",
|
||||||
|
"failedMessage": "Failed to upload image.",
|
||||||
|
"processingMessage": "An error occurred while processing the image."
|
||||||
},
|
},
|
||||||
"getStarted": "Get Started!",
|
"getStarted": "Get Started!",
|
||||||
"imageLoadFailed": "Failed to load image.",
|
"imageLoadFailed": "Failed to load image.",
|
||||||
@@ -191,5 +198,7 @@
|
|||||||
"allSynced": "All changes synced",
|
"allSynced": "All changes synced",
|
||||||
"willSync": "Will sync when back online",
|
"willSync": "Will sync when back online",
|
||||||
"cancelAll": "Cancel All"
|
"cancelAll": "Cancel All"
|
||||||
}
|
},
|
||||||
|
"attachments": "Attachments",
|
||||||
|
"fileNumbered": "File {{index}}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,26 +13,29 @@ import Option from '@mui/joy/Option'
|
|||||||
import Select from '@mui/joy/Select'
|
import Select from '@mui/joy/Select'
|
||||||
import Typography from '@mui/joy/Typography'
|
import Typography from '@mui/joy/Typography'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { NOTIFICATION_TYPE, TASK_COLOR } from '../utils/Colors'
|
import { NOTIFICATION_TYPE, TASK_COLOR } from '../utils/Colors'
|
||||||
import { TIME_UNITS } from '../utils/DurationUtils'
|
import { TIME_UNITS } from '../utils/DurationUtils'
|
||||||
|
|
||||||
const timeUnits = TIME_UNITS
|
const timeUnits = TIME_UNITS
|
||||||
|
|
||||||
const timingOptions = [
|
const timingOptions = [
|
||||||
{ label: 'Before', value: 'before' },
|
{ value: 'before' },
|
||||||
{ label: 'Due', value: 'ondue' },
|
{ value: 'ondue' },
|
||||||
{ label: 'After', value: 'after' },
|
{ value: 'after' },
|
||||||
]
|
]
|
||||||
|
|
||||||
function getRelativeLabel(notification) {
|
function getRelativeLabel(notification, t) {
|
||||||
const { value, unit } = notification
|
const { value, unit } = notification
|
||||||
const numericValue = Number(value)
|
const numericValue = Number(value)
|
||||||
if (numericValue === 0) {
|
if (numericValue === 0) {
|
||||||
return 'On due date'
|
return t('notifTemplate.onDueDate')
|
||||||
}
|
}
|
||||||
const unitName = unit === 'm' ? 'minutes' : unit === 'h' ? 'hours' : 'days'
|
const unitName = t(`notifTemplate.unitName.${unit}`)
|
||||||
const absValue = Math.abs(numericValue)
|
const absValue = Math.abs(numericValue)
|
||||||
return `${absValue} ${unitName} ${numericValue < 0 ? 'before' : 'after'} due`
|
return numericValue < 0
|
||||||
|
? t('notifTemplate.beforeDue', { count: absValue, unit: unitName })
|
||||||
|
: t('notifTemplate.afterDue', { count: absValue, unit: unitName })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions to convert between internal value and UI representation
|
// Helper functions to convert between internal value and UI representation
|
||||||
@@ -71,6 +74,7 @@ const NotificationTemplate = ({
|
|||||||
value,
|
value,
|
||||||
showTimeline = true,
|
showTimeline = true,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const [notifications, setNotifications] = useState(
|
const [notifications, setNotifications] = useState(
|
||||||
value?.templates ||
|
value?.templates ||
|
||||||
JSON.parse(localStorage.getItem('defaultNotificationTemplate')) ||
|
JSON.parse(localStorage.getItem('defaultNotificationTemplate')) ||
|
||||||
@@ -218,9 +222,7 @@ const NotificationTemplate = ({
|
|||||||
if (!currentNotification) return
|
if (!currentNotification) return
|
||||||
|
|
||||||
if (isDuplicate(currentNotification, idx, currentList)) {
|
if (isDuplicate(currentNotification, idx, currentList)) {
|
||||||
setError(
|
setError(t('notifTemplate.errDuplicate'))
|
||||||
'This notification setting already exists. Please use a different timing.',
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,14 +234,14 @@ const NotificationTemplate = ({
|
|||||||
|
|
||||||
if (type === 'due') {
|
if (type === 'due') {
|
||||||
if (notificationsRef.current.some(n => Number(n.value) === 0)) {
|
if (notificationsRef.current.some(n => Number(n.value) === 0)) {
|
||||||
setError('Only one "Due Alert" notification is allowed.')
|
setError(t('notifTemplate.errOneDue'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
newNotification = { value: 0, unit: 'm' }
|
newNotification = { value: 0, unit: 'm' }
|
||||||
} else {
|
} else {
|
||||||
newNotification = getSmartSuggestion(type)
|
newNotification = getSmartSuggestion(type)
|
||||||
if (!newNotification) {
|
if (!newNotification) {
|
||||||
setError(`All common ${type} times are already configured.`)
|
setError(t('notifTemplate.errAllConfigured', { type }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,7 +366,7 @@ const NotificationTemplate = ({
|
|||||||
fontSize: '0.6rem',
|
fontSize: '0.6rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Due Date
|
{t('group.dueDate')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -398,7 +400,7 @@ const NotificationTemplate = ({
|
|||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
title={getRelativeLabel(n)}
|
title={getRelativeLabel(n, t)}
|
||||||
>
|
>
|
||||||
<Badge
|
<Badge
|
||||||
badgeContent={
|
badgeContent={
|
||||||
@@ -550,7 +552,7 @@ const NotificationTemplate = ({
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{getRelativeLabel(n)}
|
{getRelativeLabel(n, t)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -575,7 +577,7 @@ const NotificationTemplate = ({
|
|||||||
value={opt.value}
|
value={opt.value}
|
||||||
disabled={opt.value === 'ondue' && hasOnDueElsewhere}
|
disabled={opt.value === 'ondue' && hasOnDueElsewhere}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{t(`notifTemplate.timing.${opt.value}`)}
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -638,7 +640,7 @@ const NotificationTemplate = ({
|
|||||||
>
|
>
|
||||||
{timeUnits.map(opt => (
|
{timeUnits.map(opt => (
|
||||||
<Option key={opt.value} value={opt.value}>
|
<Option key={opt.value} value={opt.value}>
|
||||||
{opt.label}
|
{t(`notifTemplate.unitShort.${opt.value}`)}
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -688,7 +690,7 @@ const NotificationTemplate = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reminder
|
{t('notifTemplate.reminder')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => addSmartNotification('due')}
|
onClick={() => addSmartNotification('due')}
|
||||||
@@ -710,7 +712,7 @@ const NotificationTemplate = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Due Alert
|
{t('notifTemplate.dueAlert')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => addSmartNotification('followup')}
|
onClick={() => addSmartNotification('followup')}
|
||||||
@@ -729,7 +731,7 @@ const NotificationTemplate = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Follow-up
|
{t('notifTemplate.followUp')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
{showSaveDefault && (
|
{showSaveDefault && (
|
||||||
@@ -762,7 +764,7 @@ const NotificationTemplate = ({
|
|||||||
setShowSaveDefault(false)
|
setShowSaveDefault(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Remember for Future Tasks
|
{t('notifTemplate.rememberFuture')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
import EmptyState from '../../components/common/EmptyState'
|
import EmptyState from '../../components/common/EmptyState'
|
||||||
@@ -92,6 +93,7 @@ const applyPendingArchivedState = async chores => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ArchivedTasks = () => {
|
const ArchivedTasks = () => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const { data: userProfile, isLoading: isUserProfileLoading } =
|
const { data: userProfile, isLoading: isUserProfileLoading } =
|
||||||
useUserProfile()
|
useUserProfile()
|
||||||
const { showError, showSuccess } = useNotification()
|
const { showError, showSuccess } = useNotification()
|
||||||
@@ -132,7 +134,7 @@ const ArchivedTasks = () => {
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
id: 'assignee',
|
id: 'assignee',
|
||||||
label: 'Assignee',
|
label: t('toolbar.type.assignee'),
|
||||||
type: 'multi-select',
|
type: 'multi-select',
|
||||||
icon: <Person />,
|
icon: <Person />,
|
||||||
options: performers.map(p => ({
|
options: performers.map(p => ({
|
||||||
@@ -144,7 +146,7 @@ const ArchivedTasks = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'priority',
|
id: 'priority',
|
||||||
label: 'Priority',
|
label: t('priority'),
|
||||||
type: 'multi-select',
|
type: 'multi-select',
|
||||||
icon: <PriorityHigh />,
|
icon: <PriorityHigh />,
|
||||||
options: Priorities.map(p => ({
|
options: Priorities.map(p => ({
|
||||||
@@ -159,7 +161,7 @@ const ArchivedTasks = () => {
|
|||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
id: 'label',
|
id: 'label',
|
||||||
label: 'Labels',
|
label: t('labels.label'),
|
||||||
type: 'multi-select',
|
type: 'multi-select',
|
||||||
icon: <Label />,
|
icon: <Label />,
|
||||||
options: availableLabels.map(l => ({
|
options: availableLabels.map(l => ({
|
||||||
@@ -186,7 +188,7 @@ const ArchivedTasks = () => {
|
|||||||
: []),
|
: []),
|
||||||
{
|
{
|
||||||
id: 'archivedAt',
|
id: 'archivedAt',
|
||||||
label: 'Archived Date',
|
label: t('archived.archivedDate'),
|
||||||
type: 'date-range',
|
type: 'date-range',
|
||||||
icon: <Archive />,
|
icon: <Archive />,
|
||||||
filterFn: (item, value) => {
|
filterFn: (item, value) => {
|
||||||
@@ -197,7 +199,7 @@ const ArchivedTasks = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[performers, availableLabels],
|
[performers, availableLabels, t],
|
||||||
)
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -233,8 +235,8 @@ const ArchivedTasks = () => {
|
|||||||
setFilteredChores(sortedChores)
|
setFilteredChores(sortedChores)
|
||||||
} catch {
|
} catch {
|
||||||
showError({
|
showError({
|
||||||
title: 'Failed to load archived tasks',
|
title: t('archived.loadFailTitle'),
|
||||||
message: 'Please try again later.',
|
message: t('archived.loadFailMsg'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -386,8 +388,8 @@ const ArchivedTasks = () => {
|
|||||||
setFilteredChores(newFilteredChores)
|
setFilteredChores(newFilteredChores)
|
||||||
|
|
||||||
showSuccess({
|
showSuccess({
|
||||||
title: 'Task Restored',
|
title: t('archived.restoredTitle'),
|
||||||
message: 'The task has been restored and is now active.',
|
message: t('archived.restoredMsg'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,8 +405,8 @@ const ArchivedTasks = () => {
|
|||||||
setFilteredChores(newFilteredChores)
|
setFilteredChores(newFilteredChores)
|
||||||
|
|
||||||
showSuccess({
|
showSuccess({
|
||||||
title: 'Task Deleted',
|
title: t('archived.deletedTitle'),
|
||||||
message: 'The archived task has been permanently deleted.',
|
message: t('archived.deletedMsg'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,10 +467,10 @@ const ArchivedTasks = () => {
|
|||||||
|
|
||||||
setConfirmModelConfig({
|
setConfirmModelConfig({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
title: 'Restore Tasks',
|
title: t('archived.restoreTasksTitle'),
|
||||||
confirmText: 'Restore',
|
confirmText: t('archived.restore'),
|
||||||
cancelText: 'Cancel',
|
cancelText: t('common:cancel'),
|
||||||
message: `Restore ${selectedData.length} task${selectedData.length > 1 ? 's' : ''} to active list?`,
|
message: t('archived.restoreConfirm', { count: selectedData.length }),
|
||||||
onClose: async isConfirmed => {
|
onClose: async isConfirmed => {
|
||||||
if (isConfirmed === true) {
|
if (isConfirmed === true) {
|
||||||
try {
|
try {
|
||||||
@@ -533,23 +535,28 @@ const ArchivedTasks = () => {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['pendingCommands'] })
|
queryClient.invalidateQueries({ queryKey: ['pendingCommands'] })
|
||||||
}
|
}
|
||||||
showSuccess({
|
showSuccess({
|
||||||
title: '📤 Tasks Restored',
|
title: t('archived.restoredBulkTitle'),
|
||||||
message: `Restored ${allRestored.length} task${allRestored.length > 1 ? 's' : ''}${offlineNote}.`,
|
message: t('archived.restoredCount', {
|
||||||
|
count: allRestored.length,
|
||||||
|
offline: offlineNote,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failedTasks.length > 0) {
|
if (failedTasks.length > 0) {
|
||||||
showError({
|
showError({
|
||||||
title: 'Some Tasks Failed',
|
title: t('archived.someFailedTitle'),
|
||||||
message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be restored.`,
|
message: t('archived.restoreFailCount', {
|
||||||
|
count: failedTasks.length,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSelection()
|
clearSelection()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError({
|
showError({
|
||||||
title: 'Bulk Restore Failed',
|
title: t('archived.bulkRestoreFailTitle'),
|
||||||
message: 'An unexpected error occurred. Please try again.',
|
message: t('archived.unexpectedError'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -564,10 +571,10 @@ const ArchivedTasks = () => {
|
|||||||
|
|
||||||
setConfirmModelConfig({
|
setConfirmModelConfig({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
title: 'Delete Archived Tasks',
|
title: t('archived.deleteTasksTitle'),
|
||||||
confirmText: 'Delete',
|
confirmText: t('archived.delete'),
|
||||||
cancelText: 'Cancel',
|
cancelText: t('common:cancel'),
|
||||||
message: `Permanently delete ${selectedData.length} archived task${selectedData.length > 1 ? 's' : ''}?\n\nThis action cannot be undone.`,
|
message: t('archived.deleteConfirm', { count: selectedData.length }),
|
||||||
onClose: async isConfirmed => {
|
onClose: async isConfirmed => {
|
||||||
if (isConfirmed === true) {
|
if (isConfirmed === true) {
|
||||||
try {
|
try {
|
||||||
@@ -585,8 +592,10 @@ const ArchivedTasks = () => {
|
|||||||
|
|
||||||
if (deletedTasks.length > 0) {
|
if (deletedTasks.length > 0) {
|
||||||
showSuccess({
|
showSuccess({
|
||||||
title: '🗑️ Tasks Deleted',
|
title: t('archived.deletedBulkTitle'),
|
||||||
message: `Successfully deleted ${deletedTasks.length} task${deletedTasks.length > 1 ? 's' : ''}.`,
|
message: t('archived.deletedCount', {
|
||||||
|
count: deletedTasks.length,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const deletedIds = new Set(deletedTasks.map(c => c.id))
|
const deletedIds = new Set(deletedTasks.map(c => c.id))
|
||||||
@@ -602,16 +611,18 @@ const ArchivedTasks = () => {
|
|||||||
|
|
||||||
if (failedTasks.length > 0) {
|
if (failedTasks.length > 0) {
|
||||||
showError({
|
showError({
|
||||||
title: 'Some Tasks Failed',
|
title: t('archived.someFailedTitle'),
|
||||||
message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be deleted.`,
|
message: t('archived.deleteFailCount', {
|
||||||
|
count: failedTasks.length,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSelection()
|
clearSelection()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError({
|
showError({
|
||||||
title: 'Bulk Delete Failed',
|
title: t('archived.bulkDeleteFailTitle'),
|
||||||
message: 'An unexpected error occurred. Please try again.',
|
message: t('archived.unexpectedError'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -653,10 +664,10 @@ const ArchivedTasks = () => {
|
|||||||
level='h3'
|
level='h3'
|
||||||
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
||||||
>
|
>
|
||||||
Archived Tasks
|
{t('archived.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||||
View and manage tasks that have been archived or completed.
|
{t('archived.subtitle')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -675,7 +686,7 @@ const ArchivedTasks = () => {
|
|||||||
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
|
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
|
||||||
>
|
>
|
||||||
<Archive />
|
<Archive />
|
||||||
Archived Tasks
|
{t('archived.heading')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
@@ -701,7 +712,7 @@ const ArchivedTasks = () => {
|
|||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
slotProps={{ input: { ref: searchInputRef } }}
|
slotProps={{ input: { ref: searchInputRef } }}
|
||||||
placeholder='Search archived tasks'
|
placeholder={t('archived.searchPlaceholder')}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{
|
sx={{
|
||||||
@@ -747,8 +758,8 @@ const ArchivedTasks = () => {
|
|||||||
onClick={toggleViewMode}
|
onClick={toggleViewMode}
|
||||||
title={
|
title={
|
||||||
viewMode === 'default'
|
viewMode === 'default'
|
||||||
? 'Switch to Compact View'
|
? t('archived.switchToCompact')
|
||||||
: 'Switch to Card View'
|
: t('archived.switchToCard')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{viewMode === 'default' ? <ViewAgenda /> : <ViewModule />}
|
{viewMode === 'default' ? <ViewAgenda /> : <ViewModule />}
|
||||||
@@ -768,8 +779,8 @@ const ArchivedTasks = () => {
|
|||||||
onClick={toggleMultiSelectMode}
|
onClick={toggleMultiSelectMode}
|
||||||
title={
|
title={
|
||||||
isMultiSelectMode
|
isMultiSelectMode
|
||||||
? 'Exit Multi-select Mode (Ctrl+S)'
|
? t('archived.exitMultiSelect')
|
||||||
: 'Enable Multi-select Mode (Ctrl+S)'
|
: t('archived.enableMultiSelect')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isMultiSelectMode ? <CheckBox /> : <CheckBoxOutlineBlank />}
|
{isMultiSelectMode ? <CheckBox /> : <CheckBoxOutlineBlank />}
|
||||||
@@ -850,8 +861,7 @@ const ArchivedTasks = () => {
|
|||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<CheckBox sx={{ color: 'primary.500' }} />
|
<CheckBox sx={{ color: 'primary.500' }} />
|
||||||
<Typography level='body-sm' fontWeight='md'>
|
<Typography level='body-sm' fontWeight='md'>
|
||||||
{selectedChores.size} task
|
{t('archived.selected', { count: selectedChores.size })}
|
||||||
{selectedChores.size !== 1 ? 's' : ''} selected
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -874,9 +884,9 @@ const ArchivedTasks = () => {
|
|||||||
'--Button-paddingInline': '0.75rem',
|
'--Button-paddingInline': '0.75rem',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
title='Select all visible tasks (Ctrl+A)'
|
title={t('archived.selectAllTitle')}
|
||||||
>
|
>
|
||||||
All
|
{t('archived.all')}
|
||||||
{showKeyboardShortcuts && (
|
{showKeyboardShortcuts && (
|
||||||
<KeyboardShortcutHint
|
<KeyboardShortcutHint
|
||||||
shortcut='A'
|
shortcut='A'
|
||||||
@@ -905,9 +915,15 @@ const ArchivedTasks = () => {
|
|||||||
'--Button-paddingInline': '0.75rem',
|
'--Button-paddingInline': '0.75rem',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
title={`${selectedChores.size === 0 ? 'Close' : 'Clear'} multi-select (Esc)`}
|
title={
|
||||||
|
selectedChores.size === 0
|
||||||
|
? t('archived.closeMultiSelect')
|
||||||
|
: t('archived.clearMultiSelect')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{selectedChores.size === 0 ? 'Close' : 'Clear'}
|
{selectedChores.size === 0
|
||||||
|
? t('archived.close')
|
||||||
|
: t('archived.clear')}
|
||||||
{showKeyboardShortcuts && (
|
{showKeyboardShortcuts && (
|
||||||
<KeyboardShortcutHint
|
<KeyboardShortcutHint
|
||||||
withCtrl={false}
|
withCtrl={false}
|
||||||
@@ -951,9 +967,9 @@ const ArchivedTasks = () => {
|
|||||||
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
title='Restore selected tasks (R)'
|
title={t('archived.restoreSelectedTitle')}
|
||||||
>
|
>
|
||||||
Restore
|
{t('archived.restore')}
|
||||||
{showKeyboardShortcuts && selectedChores.size > 0 && (
|
{showKeyboardShortcuts && selectedChores.size > 0 && (
|
||||||
<KeyboardShortcutHint
|
<KeyboardShortcutHint
|
||||||
shortcut='R'
|
shortcut='R'
|
||||||
@@ -978,9 +994,9 @@ const ArchivedTasks = () => {
|
|||||||
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
title='Delete selected tasks (E)'
|
title={t('archived.deleteSelectedTitle')}
|
||||||
>
|
>
|
||||||
Delete
|
{t('archived.delete')}
|
||||||
{showKeyboardShortcuts && selectedChores.size > 0 && (
|
{showKeyboardShortcuts && selectedChores.size > 0 && (
|
||||||
<KeyboardShortcutHint
|
<KeyboardShortcutHint
|
||||||
shortcut='E'
|
shortcut='E'
|
||||||
@@ -1013,12 +1029,12 @@ const ArchivedTasks = () => {
|
|||||||
}
|
}
|
||||||
primaryAction={
|
primaryAction={
|
||||||
searchTerm
|
searchTerm
|
||||||
? { label: 'Clear search', onClick: handleSearchClose }
|
? { label: t('archived.clearSearch'), onClick: handleSearchClose }
|
||||||
: { label: 'Clear filters', onClick: clearAll }
|
: { label: t('archived.clearFilters'), onClick: clearAll }
|
||||||
}
|
}
|
||||||
secondaryAction={
|
secondaryAction={
|
||||||
searchTerm && hasActiveFilters
|
searchTerm && hasActiveFilters
|
||||||
? { label: 'Clear filters', onClick: clearAll }
|
? { label: t('archived.clearFilters'), onClick: clearAll }
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -1034,9 +1050,8 @@ const ArchivedTasks = () => {
|
|||||||
) : (
|
) : (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography level='body-sm' color='text.secondary' sx={{ mb: 2 }}>
|
<Typography level='body-sm' color='text.secondary' sx={{ mb: 2 }}>
|
||||||
{finalChores.length} archived task
|
{t('archived.count', { count: finalChores.length })}
|
||||||
{finalChores.length !== 1 ? 's' : ''}
|
{searchTerm && t('archived.matching', { term: searchTerm })}
|
||||||
{searchTerm && ` matching "${searchTerm}"`}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<List sx={{ gap: viewMode === 'compact' ? 0 : 1 }}>
|
<List sx={{ gap: viewMode === 'compact' ? 0 : 1 }}>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
ThumbDown,
|
ThumbDown,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import { Box, Typography } from '@mui/joy'
|
import { Box, Typography } from '@mui/joy'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useLongPress } from '../../hooks/useLongPress'
|
import { useLongPress } from '../../hooks/useLongPress'
|
||||||
import ChoreCard from './ChoreCard'
|
import ChoreCard from './ChoreCard'
|
||||||
@@ -89,6 +90,7 @@ const ChoreListView = ({
|
|||||||
showActions = true,
|
showActions = true,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const renderChoreCard = (chore, key) => {
|
const renderChoreCard = (chore, key) => {
|
||||||
const CardComponent = viewMode === 'compact' ? CompactChoreCard : ChoreCard
|
const CardComponent = viewMode === 'compact' ? CompactChoreCard : ChoreCard
|
||||||
return (
|
return (
|
||||||
@@ -202,7 +204,7 @@ const ChoreListView = ({
|
|||||||
<Check sx={{ fontSize: 20 }} />
|
<Check sx={{ fontSize: 20 }} />
|
||||||
)}
|
)}
|
||||||
<Typography level='body-xs' sx={{ mt: 0.5 }}>
|
<Typography level='body-xs' sx={{ mt: 0.5 }}>
|
||||||
{chore.status !== 1 ? 'Start' : 'Complete'}
|
{chore.status !== 1 ? t('choreView.start') : t('list.complete')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</SwipeAction>
|
</SwipeAction>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Webhook,
|
Webhook,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import { Box, Checkbox, Chip, IconButton, Typography } from '@mui/joy'
|
import { Box, Checkbox, Chip, IconButton, Typography } from '@mui/joy'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
@@ -42,6 +43,7 @@ const CompactChoreCard = ({
|
|||||||
onlyClickable = false,
|
onlyClickable = false,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
|
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
const { timeFormat } = useLocalization()
|
const { timeFormat } = useLocalization()
|
||||||
@@ -96,7 +98,7 @@ const CompactChoreCard = ({
|
|||||||
if (assignee) parts.push(assignee)
|
if (assignee) parts.push(assignee)
|
||||||
}
|
}
|
||||||
if (chore.assignedTo === null) {
|
if (chore.assignedTo === null) {
|
||||||
parts.push('Anyone')
|
parts.push(t('assignee.anyone'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Points
|
// Points
|
||||||
@@ -404,7 +406,7 @@ const CompactChoreCard = ({
|
|||||||
ml: 1,
|
ml: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{getDueDateChipText(chore.nextDueDate, chore, timeFormat)}
|
{getDueDateChipText(chore.nextDueDate, chore, timeFormat, t)}
|
||||||
</Chip>
|
</Chip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Button, Chip, Menu, MenuItem, Typography } from '@mui/joy'
|
import { Button, Chip, Menu, MenuItem, Typography } from '@mui/joy'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import IconButton from '@mui/joy/IconButton'
|
import IconButton from '@mui/joy/IconButton'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||||
@@ -15,6 +16,7 @@ const IconButtonWithMenu = ({
|
|||||||
useChips,
|
useChips,
|
||||||
title,
|
title,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const [anchorEl, setAnchorEl] = useState(null)
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
const menuRef = useRef(null)
|
const menuRef = useRef(null)
|
||||||
const menuOptions = Array.isArray(options) ? options : []
|
const menuOptions = Array.isArray(options) ? options : []
|
||||||
@@ -120,7 +122,7 @@ const IconButtonWithMenu = ({
|
|||||||
setSelectedItem?.setSelectedItem('All')
|
setSelectedItem?.setSelectedItem('All')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel All Filters
|
{t('filterChip.cancelAll')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)} */}
|
)} */}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { Box, Card, IconButton, 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'
|
||||||
|
|
||||||
const MultiSelectHelp = ({ isVisible = true }) => {
|
const MultiSelectHelp = ({ isVisible = true }) => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const { ResponsiveModal } = useResponsiveModal()
|
const { ResponsiveModal } = useResponsiveModal()
|
||||||
|
|
||||||
const [isHelpOpen, setIsHelpOpen] = useState(false)
|
const [isHelpOpen, setIsHelpOpen] = useState(false)
|
||||||
@@ -29,8 +31,8 @@ const MultiSelectHelp = ({ isVisible = true }) => {
|
|||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
boxShadow: 'lg',
|
boxShadow: 'lg',
|
||||||
}}
|
}}
|
||||||
aria-label='Show keyboard shortcuts'
|
aria-label={t('multiSelect.showShortcuts')}
|
||||||
title='Show keyboard shortcuts'
|
title={t('multiSelect.showShortcuts')}
|
||||||
>
|
>
|
||||||
<HelpOutline />
|
<HelpOutline />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -39,7 +41,7 @@ const MultiSelectHelp = ({ isVisible = true }) => {
|
|||||||
<ResponsiveModal
|
<ResponsiveModal
|
||||||
open={isHelpOpen}
|
open={isHelpOpen}
|
||||||
onClose={() => setIsHelpOpen(false)}
|
onClose={() => setIsHelpOpen(false)}
|
||||||
title='Multi-select Mode'
|
title={t('multiSelect.title')}
|
||||||
description='Use these keyboard shortcuts to work more efficiently.'
|
description='Use these keyboard shortcuts to work more efficiently.'
|
||||||
footer={
|
footer={
|
||||||
<ModalActions
|
<ModalActions
|
||||||
@@ -51,16 +53,16 @@ const MultiSelectHelp = ({ isVisible = true }) => {
|
|||||||
{/* Selection shortcuts */}
|
{/* Selection shortcuts */}
|
||||||
<Card variant='soft' sx={{ p: 2 }}>
|
<Card variant='soft' sx={{ p: 2 }}>
|
||||||
<Typography level='title-sm' sx={{ mb: 1.5, color: 'primary.600' }}>
|
<Typography level='title-sm' sx={{ mb: 1.5, color: 'primary.600' }}>
|
||||||
Selection
|
{t('multiSelect.selection')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
<ShortcutItem
|
<ShortcutItem
|
||||||
keys={['Ctrl', 'A']}
|
keys={['Ctrl', 'A']}
|
||||||
description='Select all visible tasks'
|
description={t('multiSelect.selectAll')}
|
||||||
/>
|
/>
|
||||||
<ShortcutItem
|
<ShortcutItem
|
||||||
keys={['Esc']}
|
keys={['Esc']}
|
||||||
description='Clear selection or exit multi-select mode'
|
description={t('multiSelect.clearOrExit')}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -68,16 +70,16 @@ const MultiSelectHelp = ({ isVisible = true }) => {
|
|||||||
{/* Action shortcuts */}
|
{/* Action shortcuts */}
|
||||||
<Card variant='soft' sx={{ p: 2 }}>
|
<Card variant='soft' sx={{ p: 2 }}>
|
||||||
<Typography level='title-sm' sx={{ mb: 1.5, color: 'success.600' }}>
|
<Typography level='title-sm' sx={{ mb: 1.5, color: 'success.600' }}>
|
||||||
Actions
|
{t('multiSelect.actions')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
<ShortcutItem
|
<ShortcutItem
|
||||||
keys={['Enter']}
|
keys={['Enter']}
|
||||||
description='Mark selected tasks as completed'
|
description={t('multiSelect.markCompleted')}
|
||||||
/>
|
/>
|
||||||
<ShortcutItem
|
<ShortcutItem
|
||||||
keys={['Del', '⌫']}
|
keys={['Del', '⌫']}
|
||||||
description='Delete selected tasks'
|
description={t('multiSelect.deleteSelected')}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -85,12 +87,12 @@ const MultiSelectHelp = ({ isVisible = true }) => {
|
|||||||
{/* Interface shortcuts */}
|
{/* Interface shortcuts */}
|
||||||
<Card variant='soft' sx={{ p: 2 }}>
|
<Card variant='soft' sx={{ p: 2 }}>
|
||||||
<Typography level='title-sm' sx={{ mb: 1.5, color: 'warning.600' }}>
|
<Typography level='title-sm' sx={{ mb: 1.5, color: 'warning.600' }}>
|
||||||
Interface
|
{t('multiSelect.interface')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
<ShortcutItem
|
<ShortcutItem
|
||||||
keys={['Ctrl', 'K']}
|
keys={['Ctrl', 'K']}
|
||||||
description='Quick add new task'
|
description={t('multiSelect.quickAdd')}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -73,10 +73,12 @@ import {
|
|||||||
import NotificationAccessSnackbar from './NotificationAccessSnackbar'
|
import NotificationAccessSnackbar from './NotificationAccessSnackbar'
|
||||||
import Sidepanel from './Sidepanel'
|
import Sidepanel from './Sidepanel'
|
||||||
import { INSIGHT_FILTER_DEFS } from './SmartInsightsCard'
|
import { INSIGHT_FILTER_DEFS } from './SmartInsightsCard'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const MyChores = () => {
|
const MyChores = () => {
|
||||||
const { data: userProfile, isLoading: isUserProfileLoading } =
|
const { data: userProfile, isLoading: isUserProfileLoading } =
|
||||||
useUserProfile()
|
useUserProfile()
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md'))
|
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md'))
|
||||||
const { showError, showSuccess, showUndo, showWarning } = useNotification()
|
const { showError, showSuccess, showUndo, showWarning } = useNotification()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -192,16 +194,16 @@ const MyChores = () => {
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
id: 'status',
|
id: 'status',
|
||||||
label: 'Due Date',
|
label: t('group.dueDate'),
|
||||||
type: 'single-select',
|
type: 'single-select',
|
||||||
icon: <CalendarMonth />,
|
icon: <CalendarMonth />,
|
||||||
options: [
|
options: [
|
||||||
{ value: 'Overdue', label: 'Overdue', color: 'danger' },
|
{ value: 'Overdue', label: t('group.overdue'), color: 'danger' },
|
||||||
{ value: 'Due today', label: 'Due Today', color: 'warning' },
|
{ value: 'Due today', label: t('group.dueToday'), color: 'warning' },
|
||||||
{ value: 'Due in week', label: 'Due This Week' },
|
{ value: 'Due in week', label: t('group.dueThisWeek') },
|
||||||
{ value: 'Due Later', label: 'Due Later' },
|
{ value: 'Due Later', label: t('group.dueLater') },
|
||||||
{ value: 'No Due Date', label: 'No Due Date' },
|
{ value: 'No Due Date', label: t('group.noDueDate') },
|
||||||
{ value: 'Pending Approval', label: 'Pending Approval' },
|
{ value: 'Pending Approval', label: t('group.pendingApproval') },
|
||||||
],
|
],
|
||||||
filterFn: (item, value) => {
|
filterFn: (item, value) => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@@ -232,7 +234,7 @@ const MyChores = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'priority',
|
id: 'priority',
|
||||||
label: 'Priority',
|
label: t('priority'),
|
||||||
type: 'multi-select',
|
type: 'multi-select',
|
||||||
icon: <PriorityHigh />,
|
icon: <PriorityHigh />,
|
||||||
options: Priorities.map(p => ({
|
options: Priorities.map(p => ({
|
||||||
@@ -247,7 +249,7 @@ const MyChores = () => {
|
|||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
id: 'label',
|
id: 'label',
|
||||||
label: 'Labels',
|
label: t('labels.label'),
|
||||||
type: 'multi-select',
|
type: 'multi-select',
|
||||||
icon: <Style />,
|
icon: <Style />,
|
||||||
options: userLabels.map(l => ({
|
options: userLabels.map(l => ({
|
||||||
@@ -977,8 +979,8 @@ const MyChores = () => {
|
|||||||
updateFilter={updateFilter}
|
updateFilter={updateFilter}
|
||||||
onFilterSaved={name =>
|
onFilterSaved={name =>
|
||||||
showSuccess({
|
showSuccess({
|
||||||
title: 'Filter Saved',
|
title: t('list.filterSaved'),
|
||||||
message: `"${name}" has been saved`,
|
message: t('list.filterSavedMsg', { name }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onClearAllFilters={() => {
|
onClearAllFilters={() => {
|
||||||
@@ -1329,7 +1331,7 @@ const MyChores = () => {
|
|||||||
<EmptyState
|
<EmptyState
|
||||||
size='sm'
|
size='sm'
|
||||||
icon={<EditCalendar />}
|
icon={<EditCalendar />}
|
||||||
title='Nothing scheduled'
|
title={t('list.nothingScheduled')}
|
||||||
description='This day is free. Add a task if you want something to land here.'
|
description='This day is free. Add a task if you want something to land here.'
|
||||||
primaryAction={{
|
primaryAction={{
|
||||||
label: 'Add task',
|
label: 'Add task',
|
||||||
@@ -1471,7 +1473,7 @@ const MyChores = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
Navigate(`/chores/create`)
|
Navigate(`/chores/create`)
|
||||||
}}
|
}}
|
||||||
title='Create new chore (Cmd+C)'
|
title={t('list.createChore')}
|
||||||
>
|
>
|
||||||
<Add />
|
<Add />
|
||||||
<KeyboardShortcutHint
|
<KeyboardShortcutHint
|
||||||
@@ -1581,15 +1583,15 @@ const MyChores = () => {
|
|||||||
operator: filter.operator,
|
operator: filter.operator,
|
||||||
})
|
})
|
||||||
showSuccess({
|
showSuccess({
|
||||||
title: 'Filter Updated',
|
title: t('list.filterUpdated'),
|
||||||
message: `"${filter.name}" has been updated successfully`,
|
message: t('list.filterUpdatedMsg', { name: filter.name }),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Create new filter
|
// Create new filter
|
||||||
saveFilter(filter)
|
saveFilter(filter)
|
||||||
showSuccess({
|
showSuccess({
|
||||||
title: 'Advanced Filter Created',
|
title: t('list.advancedFilterCreated'),
|
||||||
message: `"${filter.name}" has been created successfully`,
|
message: t('list.advancedFilterCreatedMsg', { name: filter.name }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
setShowAdvancedFilterBuilder(false)
|
setShowAdvancedFilterBuilder(false)
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { LocalNotifications } from '@capacitor/local-notifications'
|
|||||||
import { Preferences } from '@capacitor/preferences'
|
import { Preferences } from '@capacitor/preferences'
|
||||||
import { Button, Snackbar, Stack, Typography } from '@mui/joy'
|
import { Button, Snackbar, Stack, Typography } from '@mui/joy'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { registerPushNotifications } from '../../CapacitorListener'
|
import { registerPushNotifications } from '../../CapacitorListener'
|
||||||
|
|
||||||
const NotificationAccessSnackbar = () => {
|
const NotificationAccessSnackbar = () => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
// Define the function outside of useEffect
|
// Define the function outside of useEffect
|
||||||
@@ -55,10 +57,9 @@ const NotificationAccessSnackbar = () => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Typography level='title-lg'>Need Notification?</Typography>
|
<Typography level='title-lg'>{t('notifications.needTitle')}</Typography>
|
||||||
<Typography sx={{ mt: 1, mb: 2 }}>
|
<Typography sx={{ mt: 1, mb: 2 }}>
|
||||||
You need to enable permission to receive notifications, do you want to
|
{t('notifications.needBody')}
|
||||||
enable it?
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack direction='row' spacing={1}>
|
<Stack direction='row' spacing={1}>
|
||||||
<Button
|
<Button
|
||||||
@@ -84,7 +85,7 @@ const NotificationAccessSnackbar = () => {
|
|||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Yes
|
{t('common:yes')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
@@ -98,7 +99,7 @@ const NotificationAccessSnackbar = () => {
|
|||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
No, Keep it Disabled
|
{t('notifications.keepDisabled')}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import IconButton from '@mui/joy/IconButton'
|
import IconButton from '@mui/joy/IconButton'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
||||||
|
|
||||||
const SortAndGrouping = ({
|
const SortAndGrouping = ({
|
||||||
@@ -28,6 +29,7 @@ const SortAndGrouping = ({
|
|||||||
title,
|
title,
|
||||||
onCreateNewFilter,
|
onCreateNewFilter,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const [anchorEl, setAnchorEl] = useState(null)
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||||
const [isKeyboardNavigating, setIsKeyboardNavigating] = useState(false)
|
const [isKeyboardNavigating, setIsKeyboardNavigating] = useState(false)
|
||||||
@@ -79,12 +81,12 @@ const SortAndGrouping = ({
|
|||||||
if (!anchorEl) return
|
if (!anchorEl) return
|
||||||
|
|
||||||
const groupByItems = [
|
const groupByItems = [
|
||||||
{ name: 'Smart', value: 'default' },
|
{ name: t('sort.smart'), value: 'default' },
|
||||||
{ name: 'Due Date', value: 'due_date' },
|
{ name: t('group.dueDate'), value: 'due_date' },
|
||||||
{ name: 'Priority', value: 'priority' },
|
{ name: t('priority'), value: 'priority' },
|
||||||
{ name: 'Labels', value: 'labels' },
|
{ name: t('labels.label'), value: 'labels' },
|
||||||
{ name: 'Created Date', value: 'created_date' },
|
{ name: t('sort.createdDate'), value: 'created_date' },
|
||||||
{ name: 'Updated Date', value: 'updated_date' },
|
{ name: t('sort.updatedDate'), value: 'updated_date' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const filterItems = [
|
const filterItems = [
|
||||||
@@ -249,7 +251,7 @@ const SortAndGrouping = ({
|
|||||||
height: 24,
|
height: 24,
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
}}
|
}}
|
||||||
title='Sort and Group (Ctrl+G)'
|
title={t('sort.sortAndGroup')}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
{label ? label : null}
|
{label ? label : null}
|
||||||
@@ -279,7 +281,7 @@ const SortAndGrouping = ({
|
|||||||
height: 24,
|
height: 24,
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
}}
|
}}
|
||||||
title='Sort and Group (Ctrl+G)'
|
title={t('sort.sortAndGroup')}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -322,7 +324,7 @@ const SortAndGrouping = ({
|
|||||||
>
|
>
|
||||||
<ListItemContent>
|
<ListItemContent>
|
||||||
<Typography level='title-sm' sx={{ fontWeight: 600 }}>
|
<Typography level='title-sm' sx={{ fontWeight: 600 }}>
|
||||||
{title || 'Group By'}
|
{title || t('sort.groupBy')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</ListItemContent>
|
</ListItemContent>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -330,12 +332,12 @@ const SortAndGrouping = ({
|
|||||||
<Divider sx={{ my: 1 }} />
|
<Divider sx={{ my: 1 }} />
|
||||||
|
|
||||||
{[
|
{[
|
||||||
{ name: 'Smart', value: 'default' },
|
{ name: t('sort.smart'), value: 'default' },
|
||||||
{ name: 'Due Date', value: 'due_date' },
|
{ name: t('group.dueDate'), value: 'due_date' },
|
||||||
{ name: 'Priority', value: 'priority' },
|
{ name: t('priority'), value: 'priority' },
|
||||||
{ name: 'Labels', value: 'labels' },
|
{ name: t('labels.label'), value: 'labels' },
|
||||||
{ name: 'Created Date', value: 'created_date' },
|
{ name: t('sort.createdDate'), value: 'created_date' },
|
||||||
{ name: 'Updated Date', value: 'updated_date' },
|
{ name: t('sort.updatedDate'), value: 'updated_date' },
|
||||||
].map((item, index) => (
|
].map((item, index) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={`${k}-${item?.value}`}
|
key={`${k}-${item?.value}`}
|
||||||
@@ -406,7 +408,7 @@ const SortAndGrouping = ({
|
|||||||
>
|
>
|
||||||
<ListItemContent>
|
<ListItemContent>
|
||||||
<Typography level='title-sm' sx={{ fontWeight: 600 }}>
|
<Typography level='title-sm' sx={{ fontWeight: 600 }}>
|
||||||
Quick Filters
|
{t('sort.quickFilters')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</ListItemContent>
|
</ListItemContent>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -422,7 +424,7 @@ const SortAndGrouping = ({
|
|||||||
>
|
>
|
||||||
<ListItemContent>
|
<ListItemContent>
|
||||||
<Typography level='body-xs' sx={{ fontWeight: 600 }}>
|
<Typography level='body-xs' sx={{ fontWeight: 600 }}>
|
||||||
Assigned to:
|
{t('sort.assignedTo')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</ListItemContent>
|
</ListItemContent>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -431,28 +433,28 @@ const SortAndGrouping = ({
|
|||||||
key={`${k}-assignee-anyone`}
|
key={`${k}-assignee-anyone`}
|
||||||
index={4}
|
index={4}
|
||||||
filterKey='anyone'
|
filterKey='anyone'
|
||||||
label='Anyone'
|
label={t('assignee.anyone')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MenuItem_QuickFilter
|
<MenuItem_QuickFilter
|
||||||
key={`${k}-assignee-assigned-to-me`}
|
key={`${k}-assignee-assigned-to-me`}
|
||||||
index={5}
|
index={5}
|
||||||
filterKey='assigned_to_me'
|
filterKey='assigned_to_me'
|
||||||
label='Assigned to me'
|
label={t('sort.assignedToMe')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MenuItem_QuickFilter
|
<MenuItem_QuickFilter
|
||||||
key={`${k}-assignee-available-for-me`}
|
key={`${k}-assignee-available-for-me`}
|
||||||
index={6}
|
index={6}
|
||||||
filterKey='available_for_me'
|
filterKey='available_for_me'
|
||||||
label='Available for me'
|
label={t('sort.availableForMe')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MenuItem_QuickFilter
|
<MenuItem_QuickFilter
|
||||||
key={`${k}-assignee-assigned-to-others`}
|
key={`${k}-assignee-assigned-to-others`}
|
||||||
index={7}
|
index={7}
|
||||||
filterKey='assigned_to_others'
|
filterKey='assigned_to_others'
|
||||||
label='Assigned to others'
|
label={t('sort.assignedToOthers')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Divider sx={{ my: 1 }} />
|
<Divider sx={{ my: 1 }} />
|
||||||
@@ -485,13 +487,13 @@ const SortAndGrouping = ({
|
|||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Create Filter
|
{t('sort.createFilter')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
level='body-xs'
|
level='body-xs'
|
||||||
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
||||||
>
|
>
|
||||||
Build advanced filter rules
|
{t('sort.createFilterHint')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</ListItemContent>
|
</ListItemContent>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import EmptyState from '../../components/common/EmptyState'
|
|||||||
import { useCircleMembers } from '../../queries/UserQueries'
|
import { useCircleMembers } from '../../queries/UserQueries'
|
||||||
import { TASK_COLOR } from '../../utils/Colors'
|
import { TASK_COLOR } from '../../utils/Colors'
|
||||||
import { resolvePhotoURL } from '../../utils/Helpers'
|
import { resolvePhotoURL } from '../../utils/Helpers'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const TasksByAssigneeCard = ({ chores = [] }) => {
|
const TasksByAssigneeCard = ({ chores = [] }) => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const [assigneeData, setAssigneeData] = useState([])
|
const [assigneeData, setAssigneeData] = useState([])
|
||||||
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
|
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
|
||||||
useCircleMembers()
|
useCircleMembers()
|
||||||
@@ -105,7 +107,7 @@ const TasksByAssigneeCard = ({ chores = [] }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography level='body-sm' color='neutral'>
|
<Typography level='body-sm' color='neutral'>
|
||||||
Loading tasks by assignee...
|
{t('assigneeCard.loading')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
)
|
)
|
||||||
@@ -164,7 +166,7 @@ const TasksByAssigneeCard = ({ chores = [] }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BarChart color='' />
|
<BarChart color='' />
|
||||||
<Typography level='title-md'>Tasks by Assignee</Typography>
|
<Typography level='title-md'>{t('assigneeCard.title')}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -181,22 +183,22 @@ const TasksByAssigneeCard = ({ chores = [] }) => {
|
|||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
key: 'inProgress',
|
key: 'inProgress',
|
||||||
label: 'In Progress',
|
label: t('filter.status.inProgress'),
|
||||||
color: getStatusColor('inProgress'),
|
color: getStatusColor('inProgress'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'overdue',
|
key: 'overdue',
|
||||||
label: 'Overdue',
|
label: t('group.overdue'),
|
||||||
color: getStatusColor('overdue'),
|
color: getStatusColor('overdue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'scheduled',
|
key: 'scheduled',
|
||||||
label: 'Scheduled',
|
label: t('assigneeCard.scheduled'),
|
||||||
color: getStatusColor('scheduled'),
|
color: getStatusColor('scheduled'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'pendingReview',
|
key: 'pendingReview',
|
||||||
label: 'Pending Review',
|
label: t('assigneeCard.pendingReview'),
|
||||||
color: getStatusColor('pendingReview'),
|
color: getStatusColor('pendingReview'),
|
||||||
},
|
},
|
||||||
].map(status => (
|
].map(status => (
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { SupervisorAccount } from '@mui/icons-material'
|
|||||||
import { Avatar, Box, Button, Sheet, Typography } from '@mui/joy'
|
import { Avatar, Box, Button, Sheet, Typography } from '@mui/joy'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext'
|
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext'
|
||||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||||
import UserModal from '../Modals/Inputs/UserModal'
|
import UserModal from '../Modals/Inputs/UserModal'
|
||||||
const UserSwitcher = () => {
|
const UserSwitcher = () => {
|
||||||
const {
|
const { t } = useTranslation('chores')
|
||||||
|
const {
|
||||||
impersonatedUser,
|
impersonatedUser,
|
||||||
isImpersonating,
|
isImpersonating,
|
||||||
startImpersonation,
|
startImpersonation,
|
||||||
@@ -52,15 +54,15 @@ const UserSwitcher = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SupervisorAccount color='' />
|
<SupervisorAccount color='' />
|
||||||
<Typography level='title-md'>View tasks as</Typography>
|
<Typography level='title-md'>{t('impersonate.viewAs')}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography level='title-md' sx={{ mb: 0.5 }}>
|
<Typography level='title-md' sx={{ mb: 0.5 }}>
|
||||||
Switch to user view
|
{t('impersonate.switchTitle')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography level='body-sm' sx={{ mb: 1, color: 'text.secondary' }}>
|
<Typography level='body-sm' sx={{ mb: 1, color: 'text.secondary' }}>
|
||||||
Tasks will be filtered to show only assignments for selected user
|
{t('impersonate.switchHint')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Button
|
<Button
|
||||||
@@ -69,7 +71,7 @@ const UserSwitcher = () => {
|
|||||||
onClick={() => setIsModalOpen(true)}
|
onClick={() => setIsModalOpen(true)}
|
||||||
size='sm'
|
size='sm'
|
||||||
>
|
>
|
||||||
Choose User
|
{t('impersonate.chooseUser')}
|
||||||
</Button>
|
</Button>
|
||||||
<UserModal
|
<UserModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
@@ -120,7 +122,7 @@ const UserSwitcher = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SupervisorAccount color='' />
|
<SupervisorAccount color='' />
|
||||||
<Typography level='title-md'>View tasks as</Typography>
|
<Typography level='title-md'>{t('impersonate.viewAs')}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -153,7 +155,7 @@ const UserSwitcher = () => {
|
|||||||
setIsModalOpen(true)
|
setIsModalOpen(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Change User
|
{t('impersonate.changeUser')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='plain'
|
variant='plain'
|
||||||
@@ -164,7 +166,7 @@ const UserSwitcher = () => {
|
|||||||
stopImpersonation()
|
stopImpersonation()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
{t('common:cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Capacitor } from '@capacitor/core'
|
import { Capacitor } from '@capacitor/core'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import DateModal from '../../Modals/Inputs/DateModal'
|
import DateModal from '../../Modals/Inputs/DateModal'
|
||||||
import DueDatePickerModal, {
|
import DueDatePickerModal, {
|
||||||
combineDueDate,
|
combineDueDate,
|
||||||
@@ -25,13 +26,14 @@ const ChoreModals = ({
|
|||||||
onNudge,
|
onNudge,
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{activeModal === 'changeDueDate' && modalChore && (
|
{activeModal === 'changeDueDate' && modalChore && (
|
||||||
<DueDatePickerModal
|
<DueDatePickerModal
|
||||||
open={true}
|
open={true}
|
||||||
key={'changeDueDate' + modalChore.id}
|
key={'changeDueDate' + modalChore.id}
|
||||||
title='Change due date'
|
title={t('modals.changeDueDate')}
|
||||||
{...splitDueDate(modalChore.nextDueDate)}
|
{...splitDueDate(modalChore.nextDueDate)}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onApply={parts =>
|
onApply={parts =>
|
||||||
@@ -46,7 +48,7 @@ const ChoreModals = ({
|
|||||||
isOpen={true}
|
isOpen={true}
|
||||||
key={'completedInPast' + modalChore.id}
|
key={'completedInPast' + modalChore.id}
|
||||||
current={modalChore.nextDueDate}
|
current={modalChore.nextDueDate}
|
||||||
title='Save Chore that you completed in the past'
|
title={t('modals.completePast')}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSave={onCompleteWithPastDate}
|
onSave={onCompleteWithPastDate}
|
||||||
/>
|
/>
|
||||||
@@ -57,8 +59,8 @@ const ChoreModals = ({
|
|||||||
isOpen={true}
|
isOpen={true}
|
||||||
options={membersData?.res || []}
|
options={membersData?.res || []}
|
||||||
displayKey='displayName'
|
displayKey='displayName'
|
||||||
title='Delegate to someone else'
|
title={t('modals.delegate')}
|
||||||
placeholder='Select a performer'
|
placeholder={t('modals.selectPerformer')}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSave={selected => onAssigneeChange(selected.id)}
|
onSave={selected => onAssigneeChange(selected.id)}
|
||||||
/>
|
/>
|
||||||
@@ -67,9 +69,9 @@ const ChoreModals = ({
|
|||||||
{activeModal === 'completeWithNote' && modalChore && (
|
{activeModal === 'completeWithNote' && modalChore && (
|
||||||
<TextModal
|
<TextModal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
title='Add note to attach to this completion:'
|
title={t('modals.addNote')}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
okText='Complete'
|
okText={t('modals.complete')}
|
||||||
onSave={onCompleteWithNote}
|
onSave={onCompleteWithNote}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Star,
|
Star,
|
||||||
StarBorder,
|
StarBorder,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Chip,
|
Chip,
|
||||||
@@ -28,6 +29,7 @@ const CustomFilterChips = ({
|
|||||||
onFilterPin,
|
onFilterPin,
|
||||||
onFilterEdit,
|
onFilterEdit,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [menuAnchor, setMenuAnchor] = useState(null)
|
const [menuAnchor, setMenuAnchor] = useState(null)
|
||||||
const [selectedFilter, setSelectedFilter] = useState(null)
|
const [selectedFilter, setSelectedFilter] = useState(null)
|
||||||
@@ -237,24 +239,24 @@ const CustomFilterChips = ({
|
|||||||
{selectedFilter.isPinned ? (
|
{selectedFilter.isPinned ? (
|
||||||
<>
|
<>
|
||||||
<StarBorder sx={{ mr: 1 }} />
|
<StarBorder sx={{ mr: 1 }} />
|
||||||
Unpin filter
|
{t('filterChip.unpin')}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Star sx={{ mr: 1 }} />
|
<Star sx={{ mr: 1 }} />
|
||||||
Pin filter
|
{t('filterChip.pin')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{onFilterEdit && (
|
{onFilterEdit && (
|
||||||
<MenuItem onClick={handleEdit}>
|
<MenuItem onClick={handleEdit}>
|
||||||
<Edit sx={{ mr: 1 }} />
|
<Edit sx={{ mr: 1 }} />
|
||||||
Edit filter
|
{t('filterChip.edit')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem onClick={handleDelete} color='danger'>
|
<MenuItem onClick={handleDelete} color='danger'>
|
||||||
<Delete sx={{ mr: 1 }} />
|
<Delete sx={{ mr: 1 }} />
|
||||||
Delete filter
|
{t('filterChip.delete')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
|
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
|
||||||
import LABEL_COLORS, {
|
import LABEL_COLORS, {
|
||||||
getTextColorFromBackgroundColor,
|
getTextColorFromBackgroundColor,
|
||||||
@@ -53,6 +54,7 @@ const MultiSelectToolbar = ({
|
|||||||
showKeyboardShortcuts,
|
showKeyboardShortcuts,
|
||||||
selectAllDisabled,
|
selectAllDisabled,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const [projectMenuAnchor, setProjectMenuAnchor] = useState(null)
|
const [projectMenuAnchor, setProjectMenuAnchor] = useState(null)
|
||||||
const projectMenuRef = useRef(null)
|
const projectMenuRef = useRef(null)
|
||||||
|
|
||||||
@@ -120,7 +122,7 @@ const MultiSelectToolbar = ({
|
|||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<CheckBox sx={{ color: 'primary.500' }} />
|
<CheckBox sx={{ color: 'primary.500' }} />
|
||||||
<Typography level='body-sm' fontWeight='md'>
|
<Typography level='body-sm' fontWeight='md'>
|
||||||
{selectedCount} task{selectedCount !== 1 ? 's' : ''} selected
|
{t('archived.selected', { count: selectedCount })}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -143,9 +145,9 @@ const MultiSelectToolbar = ({
|
|||||||
'--Button-paddingInline': '0.75rem',
|
'--Button-paddingInline': '0.75rem',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
title='Select all visible tasks (Ctrl+A)'
|
title={t('archived.selectAllTitle')}
|
||||||
>
|
>
|
||||||
All
|
{t('archived.all')}
|
||||||
{showKeyboardShortcuts && (
|
{showKeyboardShortcuts && (
|
||||||
<KeyboardShortcutHint
|
<KeyboardShortcutHint
|
||||||
shortcut='A'
|
shortcut='A'
|
||||||
@@ -170,9 +172,13 @@ const MultiSelectToolbar = ({
|
|||||||
'--Button-paddingInline': '0.75rem',
|
'--Button-paddingInline': '0.75rem',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
title={`${selectedCount === 0 ? 'Close' : 'Clear'} multi-select (Esc)`}
|
title={
|
||||||
|
selectedCount === 0
|
||||||
|
? t('archived.closeMultiSelect')
|
||||||
|
: t('archived.clearMultiSelect')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{selectedCount === 0 ? 'Close' : 'Clear'}
|
{selectedCount === 0 ? t('archived.close') : t('archived.clear')}
|
||||||
{showKeyboardShortcuts && (
|
{showKeyboardShortcuts && (
|
||||||
<KeyboardShortcutHint
|
<KeyboardShortcutHint
|
||||||
withCtrl={false}
|
withCtrl={false}
|
||||||
@@ -215,9 +221,9 @@ const MultiSelectToolbar = ({
|
|||||||
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
title='Complete selected tasks (Enter)'
|
title={t('multiToolbar.completeTitle')}
|
||||||
>
|
>
|
||||||
Complete
|
{t('multiToolbar.complete')}
|
||||||
{showKeyboardShortcuts && selectedCount > 0 && (
|
{showKeyboardShortcuts && selectedCount > 0 && (
|
||||||
<KeyboardShortcutHint
|
<KeyboardShortcutHint
|
||||||
shortcut='Enter'
|
shortcut='Enter'
|
||||||
@@ -241,9 +247,9 @@ const MultiSelectToolbar = ({
|
|||||||
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
title='Skip selected tasks (/)'
|
title={t('multiToolbar.skipTitle')}
|
||||||
>
|
>
|
||||||
Skip
|
{t('multiToolbar.skip')}
|
||||||
{showKeyboardShortcuts && selectedCount > 0 && (
|
{showKeyboardShortcuts && selectedCount > 0 && (
|
||||||
<KeyboardShortcutHint
|
<KeyboardShortcutHint
|
||||||
shortcut='/'
|
shortcut='/'
|
||||||
@@ -324,9 +330,9 @@ const MultiSelectToolbar = ({
|
|||||||
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
title='Archive selected tasks (X)'
|
title={t('multiToolbar.archiveTitle')}
|
||||||
>
|
>
|
||||||
Archive
|
{t('multiToolbar.archive')}
|
||||||
{showKeyboardShortcuts && selectedCount > 0 && (
|
{showKeyboardShortcuts && selectedCount > 0 && (
|
||||||
<KeyboardShortcutHint
|
<KeyboardShortcutHint
|
||||||
shortcut='X'
|
shortcut='X'
|
||||||
@@ -351,9 +357,9 @@ const MultiSelectToolbar = ({
|
|||||||
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
title='Delete selected tasks (Shift+X)'
|
title={t('multiToolbar.deleteTitle')}
|
||||||
>
|
>
|
||||||
Delete
|
{t('archived.delete')}
|
||||||
{showKeyboardShortcuts && selectedCount > 0 && (
|
{showKeyboardShortcuts && selectedCount > 0 && (
|
||||||
<KeyboardShortcutHint
|
<KeyboardShortcutHint
|
||||||
shortcut='E'
|
shortcut='E'
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export const useKeyboardShortcuts = ({
|
export const useKeyboardShortcuts = ({
|
||||||
addTaskModalOpen,
|
addTaskModalOpen,
|
||||||
@@ -11,6 +12,7 @@ export const useKeyboardShortcuts = ({
|
|||||||
searchTerm,
|
searchTerm,
|
||||||
selectedChores,
|
selectedChores,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -64,13 +66,13 @@ export const useKeyboardShortcuts = ({
|
|||||||
|
|
||||||
if (allVisibleSelected) {
|
if (allVisibleSelected) {
|
||||||
handlers.onShowMessage({
|
handlers.onShowMessage({
|
||||||
title: '✅ All Tasks Selected',
|
title: t('shortcuts.allSelectedTitle'),
|
||||||
message: `All ${visibleChores.length} filtered task${visibleChores.length !== 1 ? 's are' : ' is'} already selected.`,
|
message: `All ${visibleChores.length} filtered task${visibleChores.length !== 1 ? 's are' : ' is'} already selected.`,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
handlers.onSelectAll()
|
handlers.onSelectAll()
|
||||||
handlers.onShowMessage({
|
handlers.onShowMessage({
|
||||||
title: '🎯 Tasks Selected',
|
title: t('shortcuts.someSelectedTitle'),
|
||||||
message: `Selected ${visibleChores.length} filtered task${visibleChores.length !== 1 ? 's' : ''}.`,
|
message: `Selected ${visibleChores.length} filtered task${visibleChores.length !== 1 ? 's' : ''}.`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -92,20 +94,20 @@ export const useKeyboardShortcuts = ({
|
|||||||
|
|
||||||
if (allChoresSelected) {
|
if (allChoresSelected) {
|
||||||
handlers.onShowMessage({
|
handlers.onShowMessage({
|
||||||
title: '✅ All Tasks Selected',
|
title: t('shortcuts.allSelectedTitle'),
|
||||||
message: `All ${allChores.length} task${allChores.length !== 1 ? 's are' : ' is'} already selected (including collapsed sections).`,
|
message: `All ${allChores.length} task${allChores.length !== 1 ? 's are' : ' is'} already selected (including collapsed sections).`,
|
||||||
})
|
})
|
||||||
} else if (allExpandedSelected) {
|
} else if (allExpandedSelected) {
|
||||||
handlers.onSelectAll()
|
handlers.onSelectAll()
|
||||||
const collapsedCount = allChores.length - expandedChores.length
|
const collapsedCount = allChores.length - expandedChores.length
|
||||||
handlers.onShowMessage({
|
handlers.onShowMessage({
|
||||||
title: '🎯 All Tasks Selected',
|
title: t('shortcuts.allSelectedAltTitle'),
|
||||||
message: `Selected all ${allChores.length} tasks (including ${collapsedCount} from collapsed sections).`,
|
message: `Selected all ${allChores.length} tasks (including ${collapsedCount} from collapsed sections).`,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
handlers.onSelectAll()
|
handlers.onSelectAll()
|
||||||
handlers.onShowMessage({
|
handlers.onShowMessage({
|
||||||
title: '🎯 Tasks Selected',
|
title: t('shortcuts.someSelectedTitle'),
|
||||||
message: `Selected ${expandedChores.length} task${expandedChores.length !== 1 ? 's' : ''} from expanded sections.`,
|
message: `Selected ${expandedChores.length} task${expandedChores.length !== 1 ? 's' : ''} from expanded sections.`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -185,7 +187,7 @@ export const useKeyboardShortcuts = ({
|
|||||||
document.removeEventListener('keydown', handleKeyDown)
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
document.removeEventListener('keyup', handleKeyUp)
|
document.removeEventListener('keyup', handleKeyUp)
|
||||||
}
|
}
|
||||||
}, [isMultiSelectMode, selectedChores.size, addTaskModalOpen])
|
}, [isMultiSelectMode, selectedChores.size, addTaskModalOpen, t])
|
||||||
|
|
||||||
return { showKeyboardShortcuts }
|
return { showKeyboardShortcuts }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
SearchRounded,
|
SearchRounded,
|
||||||
Warning,
|
Warning,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
@@ -32,6 +33,7 @@ import DateModal from './Modals/Inputs/DateModal'
|
|||||||
|
|
||||||
// enum for chore status:
|
// enum for chore status:
|
||||||
const CHORE_STATUS = {
|
const CHORE_STATUS = {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
NO_DUE_DATE: 'No due date',
|
NO_DUE_DATE: 'No due date',
|
||||||
DUE_SOON: 'Soon',
|
DUE_SOON: 'Soon',
|
||||||
DUE_NOW: 'Due',
|
DUE_NOW: 'Due',
|
||||||
@@ -119,7 +121,7 @@ const ChoresOverview = () => {
|
|||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Typography level='h4' mb={1.5}>
|
<Typography level='h4' mb={1.5}>
|
||||||
Chores Overviews
|
{t('overview.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
{/* <SummaryCard /> */}
|
{/* <SummaryCard /> */}
|
||||||
<Grid container>
|
<Grid container>
|
||||||
@@ -132,7 +134,7 @@ const ChoresOverview = () => {
|
|||||||
gap={2}
|
gap={2}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder='Search'
|
placeholder={t('overview.search')}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
if (e.target.value === '') {
|
if (e.target.value === '') {
|
||||||
@@ -169,7 +171,7 @@ const ChoresOverview = () => {
|
|||||||
Navigate(`/chores/create`)
|
Navigate(`/chores/create`)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
New Chore
|
{t('overview.newChore')}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -228,7 +230,7 @@ const ChoresOverview = () => {
|
|||||||
color='warning'
|
color='warning'
|
||||||
startDecorator={<Avatar color='primary'>?</Avatar>}
|
startDecorator={<Avatar color='primary'>?</Avatar>}
|
||||||
>
|
>
|
||||||
Unassigned
|
{t('overview.unassigned')}
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
@@ -315,7 +317,7 @@ const ChoresOverview = () => {
|
|||||||
<DateModal
|
<DateModal
|
||||||
isOpen={isDateModalOpen}
|
isOpen={isDateModalOpen}
|
||||||
key={choreId}
|
key={choreId}
|
||||||
title={`Change due date`}
|
title={t('overview.changeDueDate')}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsDateModalOpen(false)
|
setIsDateModalOpen(false)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { GetChoreAttachments } from '../../../utils/Fetcher'
|
|||||||
import { resolvePhotoURL } from '../../../utils/Helpers'
|
import { resolvePhotoURL } from '../../../utils/Helpers'
|
||||||
import { cacheChoreImages, getImageSrc } from '../../../utils/ImageCache'
|
import { cacheChoreImages, getImageSrc } from '../../../utils/ImageCache'
|
||||||
import AttachmentViewerModal from './AttachmentViewerModal'
|
import AttachmentViewerModal from './AttachmentViewerModal'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']
|
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ const downloadFile = (url, fileName) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AttachmentBrowserModal({ choreId, isOpen, onClose }) {
|
function AttachmentBrowserModal({ choreId, isOpen, onClose }) {
|
||||||
|
const { t } = useTranslation('common')
|
||||||
const { ResponsiveModal } = useResponsiveModal()
|
const { ResponsiveModal } = useResponsiveModal()
|
||||||
const [attachments, setAttachments] = useState([])
|
const [attachments, setAttachments] = useState([])
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
@@ -45,7 +47,7 @@ function AttachmentBrowserModal({ choreId, isOpen, onClose }) {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
GetChoreAttachments(choreId)
|
GetChoreAttachments(choreId)
|
||||||
.then(async res => {
|
.then(async res => {
|
||||||
if (!res.ok) throw new Error('Failed to fetch attachments')
|
if (!res.ok) throw new Error(t('chores:dataError.attachmentsFailed'))
|
||||||
return res.json()
|
return res.json()
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -56,7 +58,7 @@ function AttachmentBrowserModal({ choreId, isOpen, onClose }) {
|
|||||||
})
|
})
|
||||||
.catch(() => setAttachments([]))
|
.catch(() => setAttachments([]))
|
||||||
.finally(() => setIsLoading(false))
|
.finally(() => setIsLoading(false))
|
||||||
}, [isOpen, choreId])
|
}, [isOpen, choreId, t])
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setAttachments([])
|
setAttachments([])
|
||||||
@@ -86,7 +88,7 @@ function AttachmentBrowserModal({ choreId, isOpen, onClose }) {
|
|||||||
<ResponsiveModal
|
<ResponsiveModal
|
||||||
open={!!isOpen}
|
open={!!isOpen}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
title='Attachments'
|
title={t('attachments')}
|
||||||
footer={
|
footer={
|
||||||
<ModalActions primary={{ label: 'Done', onClick: handleClose }} />
|
<ModalActions primary={{ label: 'Done', onClick: handleClose }} />
|
||||||
}
|
}
|
||||||
@@ -126,7 +128,7 @@ function AttachmentBrowserModal({ choreId, isOpen, onClose }) {
|
|||||||
)}
|
)}
|
||||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||||
<Typography level='body-sm' noWrap>
|
<Typography level='body-sm' noWrap>
|
||||||
{attachment.file_name || `File ${index + 1}`}
|
{attachment.file_name || t('fileNumbered', { index: index + 1 })}
|
||||||
</Typography>
|
</Typography>
|
||||||
{attachment.size_bytes > 0 && (
|
{attachment.size_bytes > 0 && (
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
|
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
|
||||||
import ModalActions from '../../../components/common/ModalActions'
|
import ModalActions from '../../../components/common/ModalActions'
|
||||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||||
import { isOfficialDonetickInstanceSync } from '../../../utils/FeatureToggle'
|
import { isOfficialDonetickInstanceSync } from '../../../utils/FeatureToggle'
|
||||||
|
|
||||||
function NudgeModal({ config }) {
|
function NudgeModal({ config }) {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const { ResponsiveModal } = useResponsiveModal()
|
const { ResponsiveModal } = useResponsiveModal()
|
||||||
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
@@ -107,19 +109,19 @@ function NudgeModal({ config }) {
|
|||||||
size='lg'
|
size='lg'
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
unmountDelay={250}
|
unmountDelay={250}
|
||||||
title='Send Nudge'
|
title={t('nudge.title')}
|
||||||
description='Send a gentle reminder to the people assigned to this task.'
|
description='Send a gentle reminder to the people assigned to this task.'
|
||||||
footer={
|
footer={
|
||||||
<ModalActions
|
<ModalActions
|
||||||
secondary={{
|
secondary={{
|
||||||
label: 'Cancel',
|
label: t('choreView.cancel'),
|
||||||
onClick: () => handleAction(false),
|
onClick: () => handleAction(false),
|
||||||
endDecorator: showKeyboardShortcuts ? (
|
endDecorator: showKeyboardShortcuts ? (
|
||||||
<KeyboardShortcutHint shortcut='X' />
|
<KeyboardShortcutHint shortcut='X' />
|
||||||
) : undefined,
|
) : undefined,
|
||||||
}}
|
}}
|
||||||
primary={{
|
primary={{
|
||||||
label: 'Send Nudge',
|
label: t('nudge.title'),
|
||||||
onClick: () => handleAction(true),
|
onClick: () => handleAction(true),
|
||||||
disabled: !isOfficialInstance,
|
disabled: !isOfficialInstance,
|
||||||
endDecorator: showKeyboardShortcuts ? (
|
endDecorator: showKeyboardShortcuts ? (
|
||||||
@@ -144,9 +146,9 @@ function NudgeModal({ config }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<FormControl mb={2}>
|
<FormControl mb={2}>
|
||||||
<FormLabel>Custom Message (optional)</FormLabel>
|
<FormLabel>{t('nudge.customMessage')}</FormLabel>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder='Add a personal message with your nudge...'
|
placeholder={t('nudge.messagePlaceholder')}
|
||||||
value={message}
|
value={message}
|
||||||
onChange={e => setMessage(e.target.value)}
|
onChange={e => setMessage(e.target.value)}
|
||||||
minRows={3}
|
minRows={3}
|
||||||
@@ -156,10 +158,9 @@ function NudgeModal({ config }) {
|
|||||||
|
|
||||||
<FormControl orientation='horizontal' sx={{ mb: 3 }}>
|
<FormControl orientation='horizontal' sx={{ mb: 3 }}>
|
||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<FormLabel>Notify All Assignees</FormLabel>
|
<FormLabel>{t('nudge.notifyAll')}</FormLabel>
|
||||||
<Typography level='body-sm' color='text.secondary'>
|
<Typography level='body-sm' color='text.secondary'>
|
||||||
If enabled, all members who can see this task will be notified.
|
{t('nudge.notifyAllHint')}
|
||||||
Otherwise, only the assigned person will receive the nudge.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useRef, 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 { startNativeNFCWrite } from '../../../service/NFCWriter'
|
import { startNativeNFCWrite } from '../../../service/NFCWriter'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const pulseKeyframes = `
|
const pulseKeyframes = `
|
||||||
@keyframes nfc-pulse {
|
@keyframes nfc-pulse {
|
||||||
@@ -105,6 +106,7 @@ function NFCIcon({ status }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function WriteNFCModal({ config }) {
|
function WriteNFCModal({ config }) {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const { ResponsiveModal } = useResponsiveModal()
|
const { ResponsiveModal } = useResponsiveModal()
|
||||||
const [nfcStatus, setNfcStatus] = useState('idle')
|
const [nfcStatus, setNfcStatus] = useState('idle')
|
||||||
const [errorMessage, setErrorMessage] = useState('')
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
@@ -171,13 +173,11 @@ function WriteNFCModal({ config }) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error writing to NFC tag:', error)
|
console.error('Error writing to NFC tag:', error)
|
||||||
setNfcStatus('error')
|
setNfcStatus('error')
|
||||||
setErrorMessage('Error writing to NFC tag. Please try again.')
|
setErrorMessage(t('nfc.errWrite'))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setNfcStatus('error')
|
setNfcStatus('error')
|
||||||
setErrorMessage(
|
setErrorMessage(t('nfc.errUnsupported'))
|
||||||
'NFC is not supported by this browser. Copy the URL and write it to an NFC tag using a compatible device.',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,20 +187,20 @@ function WriteNFCModal({ config }) {
|
|||||||
const isError = nfcStatus === 'error'
|
const isError = nfcStatus === 'error'
|
||||||
|
|
||||||
const title = isSuccess
|
const title = isSuccess
|
||||||
? 'Tag written!'
|
? t('nfc.titleSuccess')
|
||||||
: isError
|
: isError
|
||||||
? 'Something went wrong'
|
? t('nfc.titleError')
|
||||||
: isWaiting
|
: isWaiting
|
||||||
? 'Hold near NFC tag'
|
? t('nfc.titleWaiting')
|
||||||
: 'Write to NFC'
|
: t('actionMenu.writeNFC')
|
||||||
|
|
||||||
const subtitle = isSuccess
|
const subtitle = isSuccess
|
||||||
? 'Your NFC tag is ready to use.'
|
? t('nfc.subSuccess')
|
||||||
: isError
|
: isError
|
||||||
? errorMessage
|
? errorMessage
|
||||||
: isWaiting
|
: isWaiting
|
||||||
? 'Keep your device near the tag until complete.'
|
? t('nfc.subWaiting')
|
||||||
: 'Encode this task link onto any NFC tag.'
|
: t('nfc.subIdle')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -214,14 +214,14 @@ function WriteNFCModal({ config }) {
|
|||||||
closeOnEscape={!isWaiting}
|
closeOnEscape={!isWaiting}
|
||||||
footer={
|
footer={
|
||||||
isSuccess ? (
|
isSuccess ? (
|
||||||
<ModalActions primary={{ label: 'Done', onClick: handleClose }} />
|
<ModalActions primary={{ label: t('activity.status.done'), onClick: handleClose }} />
|
||||||
) : isWaiting ? (
|
) : isWaiting ? (
|
||||||
<ModalActions
|
<ModalActions
|
||||||
secondary={{ label: 'Cancel', onClick: handleCancel }}
|
secondary={{ label: t('choreView.cancel'), onClick: handleCancel }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ModalActions
|
<ModalActions
|
||||||
secondary={{ label: 'Cancel', onClick: handleClose }}
|
secondary={{ label: t('choreView.cancel'), onClick: handleClose }}
|
||||||
primary={{
|
primary={{
|
||||||
label: nfcStatus === 'writing' ? 'Starting…' : 'Write tag',
|
label: nfcStatus === 'writing' ? 'Starting…' : 'Write tag',
|
||||||
onClick: writeToNFC,
|
onClick: writeToNFC,
|
||||||
@@ -250,7 +250,7 @@ function WriteNFCModal({ config }) {
|
|||||||
color: 'text.tertiary',
|
color: 'text.tertiary',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Tag URL
|
{t('nfc.tagUrl')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
value={getURL()}
|
value={getURL()}
|
||||||
@@ -269,7 +269,7 @@ function WriteNFCModal({ config }) {
|
|||||||
variant='plain'
|
variant='plain'
|
||||||
color={copied ? 'success' : 'neutral'}
|
color={copied ? 'success' : 'neutral'}
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
title='Copy URL'
|
title={t('nfc.copyUrl')}
|
||||||
>
|
>
|
||||||
<ContentCopy sx={{ fontSize: 16 }} />
|
<ContentCopy sx={{ fontSize: 16 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -280,7 +280,7 @@ function WriteNFCModal({ config }) {
|
|||||||
level='body-xs'
|
level='body-xs'
|
||||||
sx={{ color: 'success.500', mt: 0.5, textAlign: 'right' }}
|
sx={{ color: 'success.500', mt: 0.5, textAlign: 'right' }}
|
||||||
>
|
>
|
||||||
Copied!
|
{t('common:copied')}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -299,10 +299,10 @@ function WriteNFCModal({ config }) {
|
|||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography level='body-sm' fontWeight={500}>
|
<Typography level='body-sm' fontWeight={500}>
|
||||||
Auto-complete on scan
|
{t('nfc.autoComplete')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography level='body-xs' sx={{ color: 'text.tertiary' }}>
|
<Typography level='body-xs' sx={{ color: 'text.tertiary' }}>
|
||||||
Mark task done when tag is tapped
|
{t('nfc.autoCompleteHint')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { useMediaQuery } from '@mui/material'
|
import { useMediaQuery } from '@mui/material'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import AppModal from '../../components/common/AppModal'
|
import AppModal from '../../components/common/AppModal'
|
||||||
import LABEL_COLORS, {
|
import LABEL_COLORS, {
|
||||||
@@ -63,6 +64,7 @@ const ChoreActionMenu = ({
|
|||||||
sx = {},
|
sx = {},
|
||||||
variant = 'soft',
|
variant = 'soft',
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const [anchorEl, setAnchorEl] = React.useState(null)
|
const [anchorEl, setAnchorEl] = React.useState(null)
|
||||||
const [isOfficialInstance, setIsOfficialInstance] = useState(false)
|
const [isOfficialInstance, setIsOfficialInstance] = useState(false)
|
||||||
const [showProjectPicker, setShowProjectPicker] = useState(false)
|
const [showProjectPicker, setShowProjectPicker] = useState(false)
|
||||||
@@ -250,7 +252,7 @@ const ChoreActionMenu = ({
|
|||||||
{
|
{
|
||||||
key: 'completeNote',
|
key: 'completeNote',
|
||||||
icon: <NoteAdd />,
|
icon: <NoteAdd />,
|
||||||
label: 'Complete with note',
|
label: t('actionMenu.completeWithNote'),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
onCompleteWithNote?.()
|
onCompleteWithNote?.()
|
||||||
handleMenuClose()
|
handleMenuClose()
|
||||||
@@ -259,7 +261,7 @@ const ChoreActionMenu = ({
|
|||||||
{
|
{
|
||||||
key: 'completePast',
|
key: 'completePast',
|
||||||
icon: <Update />,
|
icon: <Update />,
|
||||||
label: 'Complete in past',
|
label: t('actionMenu.completeInPast'),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
onCompleteWithPastDate?.()
|
onCompleteWithPastDate?.()
|
||||||
handleMenuClose()
|
handleMenuClose()
|
||||||
@@ -268,13 +270,13 @@ const ChoreActionMenu = ({
|
|||||||
{
|
{
|
||||||
key: 'skip',
|
key: 'skip',
|
||||||
icon: <SwitchAccessShortcut />,
|
icon: <SwitchAccessShortcut />,
|
||||||
label: 'Skip to next due date',
|
label: t('actionMenu.skipToNext'),
|
||||||
onClick: handleSkip,
|
onClick: handleSkip,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'delegate',
|
key: 'delegate',
|
||||||
icon: <RecordVoiceOver />,
|
icon: <RecordVoiceOver />,
|
||||||
label: 'Delegate to someone else',
|
label: t('modals.delegate'),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
onChangeAssignee?.()
|
onChangeAssignee?.()
|
||||||
handleMenuClose()
|
handleMenuClose()
|
||||||
@@ -283,7 +285,7 @@ const ChoreActionMenu = ({
|
|||||||
isOfficialInstance && {
|
isOfficialInstance && {
|
||||||
key: 'nudge',
|
key: 'nudge',
|
||||||
icon: <Notifications />,
|
icon: <Notifications />,
|
||||||
label: 'Send nudge',
|
label: t('actionMenu.sendNudge'),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
onNudge?.()
|
onNudge?.()
|
||||||
handleMenuClose()
|
handleMenuClose()
|
||||||
@@ -293,7 +295,7 @@ const ChoreActionMenu = ({
|
|||||||
{
|
{
|
||||||
key: 'history',
|
key: 'history',
|
||||||
icon: <ManageSearch />,
|
icon: <ManageSearch />,
|
||||||
label: 'History',
|
label: t('actionMenu.history'),
|
||||||
onClick: handleHistory,
|
onClick: handleHistory,
|
||||||
},
|
},
|
||||||
{ key: 'divider-2', type: 'divider' },
|
{ key: 'divider-2', type: 'divider' },
|
||||||
@@ -302,7 +304,7 @@ const ChoreActionMenu = ({
|
|||||||
{
|
{
|
||||||
key: 'changeDueDate',
|
key: 'changeDueDate',
|
||||||
icon: <MoreTime />,
|
icon: <MoreTime />,
|
||||||
label: 'Change due date',
|
label: t('modals.changeDueDate'),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
onChangeDueDate?.()
|
onChangeDueDate?.()
|
||||||
handleMenuClose()
|
handleMenuClose()
|
||||||
@@ -311,15 +313,15 @@ const ChoreActionMenu = ({
|
|||||||
{
|
{
|
||||||
key: 'writeNfc',
|
key: 'writeNfc',
|
||||||
icon: <Nfc />,
|
icon: <Nfc />,
|
||||||
label: 'Write to NFC',
|
label: t('actionMenu.writeNFC'),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
onWriteNFC?.()
|
onWriteNFC?.()
|
||||||
handleMenuClose()
|
handleMenuClose()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ key: 'edit', icon: <Edit />, label: 'Edit', onClick: handleEdit },
|
{ key: 'edit', icon: <Edit />, label: t('choreView.edit'), onClick: handleEdit },
|
||||||
{ key: 'clone', icon: <CopyAll />, label: 'Clone', onClick: handleClone },
|
{ key: 'clone', icon: <CopyAll />, label: t('actionMenu.clone'), onClick: handleClone },
|
||||||
{ key: 'view', icon: <ViewCarousel />, label: 'View', onClick: handleView },
|
{ key: 'view', icon: <ViewCarousel />, label: t('actionMenu.view'), onClick: handleView },
|
||||||
{
|
{
|
||||||
key: 'archive',
|
key: 'archive',
|
||||||
icon: chore.isActive ? <Archive /> : <Unarchive />,
|
icon: chore.isActive ? <Archive /> : <Unarchive />,
|
||||||
@@ -330,14 +332,14 @@ const ChoreActionMenu = ({
|
|||||||
projects.length > 0 && {
|
projects.length > 0 && {
|
||||||
key: 'moveToProject',
|
key: 'moveToProject',
|
||||||
icon: <DriveFileMove />,
|
icon: <DriveFileMove />,
|
||||||
label: 'Move to project',
|
label: t('actionMenu.moveToProject'),
|
||||||
onClick: () => setShowProjectPicker(true),
|
onClick: () => setShowProjectPicker(true),
|
||||||
},
|
},
|
||||||
{ key: 'divider-4', type: 'divider' },
|
{ key: 'divider-4', type: 'divider' },
|
||||||
{
|
{
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
icon: <Delete />,
|
icon: <Delete />,
|
||||||
label: 'Delete',
|
label: t('archived.delete'),
|
||||||
onClick: handleDelete,
|
onClick: handleDelete,
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
@@ -345,7 +347,7 @@ const ChoreActionMenu = ({
|
|||||||
|
|
||||||
const quickScheduleButtons = (
|
const quickScheduleButtons = (
|
||||||
<>
|
<>
|
||||||
<Tooltip title='Today' placement='top'>
|
<Tooltip title={t('duePicker.today')} placement='top'>
|
||||||
<IconButton
|
<IconButton
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
@@ -356,7 +358,7 @@ const ChoreActionMenu = ({
|
|||||||
<Today />
|
<Today />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title='Tomorrow' placement='top'>
|
<Tooltip title={t('duePicker.tomorrow')} placement='top'>
|
||||||
<IconButton
|
<IconButton
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
@@ -367,7 +369,7 @@ const ChoreActionMenu = ({
|
|||||||
<WbSunny />
|
<WbSunny />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title='Weekend' placement='top'>
|
<Tooltip title={t('duePicker.weekend')} placement='top'>
|
||||||
<IconButton
|
<IconButton
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
@@ -378,7 +380,7 @@ const ChoreActionMenu = ({
|
|||||||
<Weekend />
|
<Weekend />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title='Next week' placement='top'>
|
<Tooltip title={t('duePicker.nextWeek')} placement='top'>
|
||||||
<IconButton
|
<IconButton
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
@@ -389,7 +391,7 @@ const ChoreActionMenu = ({
|
|||||||
<NextWeek />
|
<NextWeek />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title='Remove due date' placement='top'>
|
<Tooltip title={t('actionMenu.removeDueDate')} placement='top'>
|
||||||
<IconButton
|
<IconButton
|
||||||
size='sm'
|
size='sm'
|
||||||
color='neutral'
|
color='neutral'
|
||||||
@@ -529,7 +531,7 @@ const ChoreActionMenu = ({
|
|||||||
>
|
>
|
||||||
<ArrowBack fontSize='small' />
|
<ArrowBack fontSize='small' />
|
||||||
<Typography level='body-sm' fontWeight={600}>
|
<Typography level='body-sm' fontWeight={600}>
|
||||||
Back
|
{t('common:back')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Replay,
|
Replay,
|
||||||
TextSnippet,
|
TextSnippet,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -121,6 +122,7 @@ async function extractTaskFromOCR(ocrText) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PhotoTaskModal = ({ open, onClose, onTaskExtracted }) => {
|
const PhotoTaskModal = ({ open, onClose, onTaskExtracted }) => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const { ResponsiveModal } = useResponsiveModal()
|
const { ResponsiveModal } = useResponsiveModal()
|
||||||
const { isNativeScanner, scanDocument } = useDocumentScanner()
|
const { isNativeScanner, scanDocument } = useDocumentScanner()
|
||||||
const videoRef = useRef(null)
|
const videoRef = useRef(null)
|
||||||
@@ -302,7 +304,7 @@ const PhotoTaskModal = ({ open, onClose, onTaskExtracted }) => {
|
|||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
size='md'
|
size='md'
|
||||||
fullWidth
|
fullWidth
|
||||||
title='Scan photo to create task'
|
title={t('photoTask.title')}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
{(phase === 'capture' || phase === 'preview') && (
|
{(phase === 'capture' || phase === 'preview') && (
|
||||||
@@ -340,7 +342,7 @@ const PhotoTaskModal = ({ open, onClose, onTaskExtracted }) => {
|
|||||||
<Box sx={{ textAlign: 'center', p: 3 }}>
|
<Box sx={{ textAlign: 'center', p: 3 }}>
|
||||||
<CameraAlt sx={{ fontSize: 48, opacity: 0.5, mb: 1 }} />
|
<CameraAlt sx={{ fontSize: 48, opacity: 0.5, mb: 1 }} />
|
||||||
<Typography level='body-sm' sx={{ opacity: 0.7 }}>
|
<Typography level='body-sm' sx={{ opacity: 0.7 }}>
|
||||||
Camera not available
|
{t('photoTask.cameraUnavailable')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -487,7 +489,7 @@ const PhotoTaskModal = ({ open, onClose, onTaskExtracted }) => {
|
|||||||
startDecorator={<PhotoCamera />}
|
startDecorator={<PhotoCamera />}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
>
|
>
|
||||||
Upload Photo
|
{t('photoTask.uploadPhoto')}
|
||||||
</Button>
|
</Button>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
@@ -503,7 +505,7 @@ const PhotoTaskModal = ({ open, onClose, onTaskExtracted }) => {
|
|||||||
startDecorator={<DocumentScanner />}
|
startDecorator={<DocumentScanner />}
|
||||||
onClick={handleNativeScan}
|
onClick={handleNativeScan}
|
||||||
>
|
>
|
||||||
Scan Document
|
{t('photoTask.scanDocument')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
cameraAvailable && (
|
cameraAvailable && (
|
||||||
@@ -513,7 +515,7 @@ const PhotoTaskModal = ({ open, onClose, onTaskExtracted }) => {
|
|||||||
startDecorator={<CameraAlt />}
|
startDecorator={<CameraAlt />}
|
||||||
onClick={handleCapture}
|
onClick={handleCapture}
|
||||||
>
|
>
|
||||||
Capture
|
{t('photoTask.capture')}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -538,7 +540,7 @@ const PhotoTaskModal = ({ open, onClose, onTaskExtracted }) => {
|
|||||||
color='primary'
|
color='primary'
|
||||||
onClick={() => handleProcess('native')}
|
onClick={() => handleProcess('native')}
|
||||||
>
|
>
|
||||||
Process Natively
|
{t('photoTask.processNatively')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@@ -546,7 +548,7 @@ const PhotoTaskModal = ({ open, onClose, onTaskExtracted }) => {
|
|||||||
color='primary'
|
color='primary'
|
||||||
onClick={() => handleProcess('tesseract')}
|
onClick={() => handleProcess('tesseract')}
|
||||||
>
|
>
|
||||||
Process Image
|
{t('photoTask.processImage')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -559,7 +561,7 @@ const PhotoTaskModal = ({ open, onClose, onTaskExtracted }) => {
|
|||||||
startDecorator={<ArrowBack />}
|
startDecorator={<ArrowBack />}
|
||||||
onClick={handleBackToPreview}
|
onClick={handleBackToPreview}
|
||||||
>
|
>
|
||||||
Back
|
{t('common:back')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
@@ -580,7 +582,7 @@ const PhotoTaskModal = ({ open, onClose, onTaskExtracted }) => {
|
|||||||
startDecorator={<ArrowBack />}
|
startDecorator={<ArrowBack />}
|
||||||
onClick={handleBackToPreview}
|
onClick={handleBackToPreview}
|
||||||
>
|
>
|
||||||
Back
|
{t('common:back')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
@@ -591,7 +593,7 @@ const PhotoTaskModal = ({ open, onClose, onTaskExtracted }) => {
|
|||||||
Retake
|
Retake
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='solid' color='primary' onClick={handleConfirm}>
|
<Button variant='solid' color='primary' onClick={handleConfirm}>
|
||||||
Create Task
|
{t('photoTask.createTask')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { apiClient } from '../../utils/ApiClient'
|
|||||||
import { isPlusAccount, resolvePhotoURL } from '../../utils/Helpers'
|
import { isPlusAccount, resolvePhotoURL } from '../../utils/Helpers'
|
||||||
import { patchDescriptionHtml } from '../../utils/ImageCache'
|
import { patchDescriptionHtml } from '../../utils/ImageCache'
|
||||||
import './RichTextEditor.css'
|
import './RichTextEditor.css'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const RichTextEditor = forwardRef(
|
const RichTextEditor = forwardRef(
|
||||||
(
|
(
|
||||||
@@ -53,7 +54,7 @@ const RichTextEditor = forwardRef(
|
|||||||
value = '',
|
value = '',
|
||||||
onChange,
|
onChange,
|
||||||
isEditable = true,
|
isEditable = true,
|
||||||
placeholder = 'Enter description...',
|
placeholder = null,
|
||||||
variant = 'outlined',
|
variant = 'outlined',
|
||||||
entityId,
|
entityId,
|
||||||
entityType,
|
entityType,
|
||||||
@@ -61,6 +62,7 @@ const RichTextEditor = forwardRef(
|
|||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const { showError } = useNotification()
|
const { showError } = useNotification()
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
// Display-only HTML with expired image srcs swapped for cached/re-signed ones
|
// Display-only HTML with expired image srcs swapped for cached/re-signed ones
|
||||||
@@ -92,9 +94,9 @@ const RichTextEditor = forwardRef(
|
|||||||
// Check if user has plus account
|
// Check if user has plus account
|
||||||
if (!isPlusAccount(userProfile)) {
|
if (!isPlusAccount(userProfile)) {
|
||||||
showError({
|
showError({
|
||||||
title: 'Plus Feature',
|
title: t('common:upload.plusFeatureTitle'),
|
||||||
message:
|
message:
|
||||||
'Image uploads are not available in the Basic plan. Upgrade to Plus to add images to your content.',
|
t('common:upload.plusFeatureMessage'),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -155,33 +157,33 @@ const RichTextEditor = forwardRef(
|
|||||||
|
|
||||||
if (response.status === 507) {
|
if (response.status === 507) {
|
||||||
showError({
|
showError({
|
||||||
title: 'Storage Quota Exceeded',
|
title: t('common:upload.quotaTitle'),
|
||||||
message: 'You have exceeded your quota for uploading files.',
|
message: t('common:upload.quotaMessage'),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
} else if (response.status === 413) {
|
} else if (response.status === 413) {
|
||||||
showError({
|
showError({
|
||||||
title: 'File Too Large',
|
title: t('common:upload.tooLargeTitle'),
|
||||||
message: 'The file you are trying to upload is too large.',
|
message: t('common:upload.tooLargeMessage'),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
} else if (response.status === 403 && !isPlusAccount(userProfile)) {
|
} else if (response.status === 403 && !isPlusAccount(userProfile)) {
|
||||||
showError({
|
showError({
|
||||||
title: 'Upgrade Required',
|
title: t('common:upload.upgradeTitle'),
|
||||||
message:
|
message:
|
||||||
'Image uploads are only available for Plus accounts. Please ',
|
t('common:upload.upgradeMessage'),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
} else if (response.status === 403) {
|
} else if (response.status === 403) {
|
||||||
showError({
|
showError({
|
||||||
title: 'Permission Denied',
|
title: t('common:upload.deniedTitle'),
|
||||||
message: 'You do not have permission to upload files.',
|
message: t('common:upload.deniedMessage'),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
} else if (!response.ok) {
|
} else if (!response.ok) {
|
||||||
showError({
|
showError({
|
||||||
title: 'Upload Failed',
|
title: t('common:upload.failedTitle'),
|
||||||
message: 'Failed to upload image.',
|
message: t('common:upload.failedMessage'),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -199,8 +201,8 @@ const RichTextEditor = forwardRef(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during image processing or upload:', error)
|
console.error('Error during image processing or upload:', error)
|
||||||
showError({
|
showError({
|
||||||
title: 'Upload Failed',
|
title: t('common:upload.failedTitle'),
|
||||||
message: 'An error occurred while processing the image.',
|
message: t('common:upload.processingMessage'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +228,7 @@ const RichTextEditor = forwardRef(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
placeholder: placeholder,
|
placeholder: placeholder ?? t('descriptionPlaceholder'),
|
||||||
})
|
})
|
||||||
new QuillMarkdown(editorRef.current, {})
|
new QuillMarkdown(editorRef.current, {})
|
||||||
editorRef.current.root.innerHTML = value
|
editorRef.current.root.innerHTML = value
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Replay,
|
Replay,
|
||||||
WarningAmber,
|
WarningAmber,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -37,6 +38,7 @@ const ScanPanel = ({
|
|||||||
onTaskExtracted,
|
onTaskExtracted,
|
||||||
open,
|
open,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation('chores')
|
||||||
const {
|
const {
|
||||||
activate,
|
activate,
|
||||||
cameraAvailable,
|
cameraAvailable,
|
||||||
@@ -109,18 +111,18 @@ const ScanPanel = ({
|
|||||||
if (phase === 'capture') {
|
if (phase === 'capture') {
|
||||||
if (isNativeScanner) {
|
if (isNativeScanner) {
|
||||||
return {
|
return {
|
||||||
label: 'Scan Document',
|
label: t('photoTask.scanDocument'),
|
||||||
icon: <DocumentScanner />,
|
icon: <DocumentScanner />,
|
||||||
onClick: handleNativeScan,
|
onClick: handleNativeScan,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cameraAvailable) {
|
if (cameraAvailable) {
|
||||||
return { label: 'Capture', icon: <CameraAlt />, onClick: capture }
|
return { label: t('photoTask.capture'), icon: <CameraAlt />, onClick: capture }
|
||||||
}
|
}
|
||||||
// No camera on this device — Upload is the only way forward, so it
|
// No camera on this device — Upload is the only way forward, so it
|
||||||
// graduates from the inline secondary to the footer's primary
|
// graduates from the inline secondary to the footer's primary
|
||||||
return {
|
return {
|
||||||
label: 'Upload Photo',
|
label: t('photoTask.uploadPhoto'),
|
||||||
icon: <PhotoCamera />,
|
icon: <PhotoCamera />,
|
||||||
onClick: openFilePicker,
|
onClick: openFilePicker,
|
||||||
}
|
}
|
||||||
@@ -322,7 +324,7 @@ const ScanPanel = ({
|
|||||||
{capturedImage && (
|
{capturedImage && (
|
||||||
<img
|
<img
|
||||||
src={capturedImage}
|
src={capturedImage}
|
||||||
alt='Failed scan'
|
alt={t('photoTask.failedScan')}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
display: 'block',
|
display: 'block',
|
||||||
|
|||||||
Reference in New Issue
Block a user