import { useState } from "react"; import { useUpdate, useNotify, useCreate, useGetIdentity, useLocaleState, useTranslate, } from "ra-core"; import { Check, Pencil, Clock } from "lucide-react"; import { DataTable } from "@/components/ds/admin/data-table"; import { ReferenceField } from "@/components/ds/admin/reference-field"; import { TextField } from "@/components/ds/admin/text-field"; import { Button } from "@/components/ds/ui/button"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ds/ui/tooltip"; import { getRelativeDueDate, parseLocalDate } from "@/lib/date-utils"; import type { Task, TaskSummary } from "../types"; import { TaskPriorityBadge } from "./TaskPriorityBadge"; import { TaskStatusBadge } from "./TaskStatusBadge"; import { TaskEdit } from "./TaskEdit"; import { TaskTypeIcon } from "./TaskTypeIcon"; const RelatedEntityField = ({ record }: { record: Task | TaskSummary }) => { const translate = useTranslate(); const getEntityName = () => { if (record.contact_id) { // For contacts, the ReferenceField will show first_name + last_name return ( ); } if (record.company_id) { return ( ); } if (record.deal_id) { return ( ); } return ; }; const title = record.contact_id ? translate("crm.filter.contact") : record.company_id ? translate("crm.filter.company") : record.deal_id ? translate("crm.filter.deal") : ""; return (
{getEntityName()}
); }; const DueDateField = ({ record, translate, locale, }: { record: Task | TaskSummary; translate: ReturnType; locale?: string; }) => { const isCompleted = record.status === "done" || record.status === "cancelled"; const { text, isOverdue } = getRelativeDueDate(record.due_date, isCompleted, { translate, locale, }); // Parse date as local date to avoid timezone shift (same logic as getRelativeDueDate) const dueDate = record.due_date ? parseLocalDate(record.due_date) : null; const formattedDate = dueDate ? dueDate.toLocaleDateString(locale) : ""; return (
{text} {dueDate && !isCompleted && ( {formattedDate} )}
); }; const TaskActions = ({ record, onEdit, }: { record: Task; onEdit: (taskId: number) => void; }) => { const [update] = useUpdate(); const [create] = useCreate(); const notify = useNotify(); const { identity } = useGetIdentity(); const translate = useTranslate(); const [locale] = useLocaleState(); // Create a task note for audit trail const createTaskNote = (text: string) => { if (!identity?.id) return; // Use server-based UTC timestamp (single source of truth) // Avoids client-side time issues (wrong machine time, timezone errors) const date = new Date().toISOString(); create( "taskNotes", { data: { task_id: record.id, text, date, sales_id: identity.id, status: "cold", }, }, { onError: (error) => { console.error("Failed to create task note:", error); }, }, ); }; const handleMarkComplete = (e: React.MouseEvent) => { e.stopPropagation(); update( "tasks", { id: record.id, data: { status: "done", done_date: new Date().toISOString(), updated_at: new Date().toISOString(), }, previousData: record, }, { onSuccess: () => { notify(translate("crm.task.notification.marked_complete"), { type: "success", }); createTaskNote(translate("crm.task.note.marked_complete_quick")); }, }, ); }; const handleEdit = (e: React.MouseEvent) => { e.stopPropagation(); onEdit(record.id as number); }; const handleSnooze = (e: React.MouseEvent) => { e.stopPropagation(); const today = new Date(); today.setHours(0, 0, 0, 0); const tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate() + 1); let newDueDateString: string; let noteText: string; let notificationText: string; if (!record.due_date) { // No due date, set to tomorrow newDueDateString = tomorrow.toISOString().slice(0, 10); noteText = translate("crm.task.note.snoozed_to_date", { date: tomorrow.toLocaleDateString(locale), }); notificationText = translate("crm.task.notification.snoozed_tomorrow"); } else { const dueDate = parseLocalDate(record.due_date); if (!dueDate) { newDueDateString = tomorrow.toISOString().slice(0, 10); noteText = translate("crm.task.note.snoozed_to_date", { date: tomorrow.toLocaleDateString(locale), }); notificationText = translate("crm.task.notification.snoozed_tomorrow"); } else { const isOverdueOrDueToday = dueDate <= today; if (isOverdueOrDueToday) { newDueDateString = tomorrow.toISOString().slice(0, 10); noteText = translate("crm.task.note.snoozed_to_date", { date: tomorrow.toLocaleDateString(locale), }); notificationText = translate( "crm.task.notification.snoozed_tomorrow", ); } else { // Add 1 day to the due date const newDueDate = new Date(dueDate); newDueDate.setDate(newDueDate.getDate() + 1); newDueDateString = newDueDate.toISOString().slice(0, 10); noteText = translate("crm.task.note.postponed_to_date", { date: newDueDate.toLocaleDateString(locale), }); notificationText = translate("crm.task.notification.postponed_day"); } } } update( "tasks", { id: record.id, data: { due_date: newDueDateString, updated_at: new Date().toISOString(), }, previousData: record, }, { onSuccess: () => { notify(notificationText, { type: "success" }); createTaskNote(noteText); }, }, ); }; const isCompleted = record.status === "done" || record.status === "cancelled"; // Determine smart button label const today = new Date(); today.setHours(0, 0, 0, 0); let isOverdueOrDueToday = true; if (record.due_date) { const labelDueDate = parseLocalDate(record.due_date); if (labelDueDate) { isOverdueOrDueToday = labelDueDate <= today; } } const snoozeLabel = isOverdueOrDueToday ? translate("crm.task.action.snooze_tomorrow") : translate("crm.task.action.postpone_day"); return (
{!isCompleted && (

{translate("crm.task.action.mark_complete")}

)}

{translate("crm.task.action.edit")}

{!isCompleted && (

{snoozeLabel}

)}
); }; export const TaskListTable = () => { const [editingTaskId, setEditingTaskId] = useState(null); const translate = useTranslate(); const [locale] = useLocaleState(); const getRowClassName = (record: Task) => { const isCompleted = record.status === "done" || record.status === "cancelled"; const { isOverdue } = getRelativeDueDate(record.due_date, isCompleted, { translate, locale, }); // Add 'group' class for hover actions, plus overdue styling const baseClass = "group"; if (isCompleted) return baseClass; return isOverdue ? `${baseClass} bg-destructive/10 hover:bg-destructive/20` : baseClass; }; return ( <> (
{record.text}
)} /> } /> ( )} /> ( )} /> } /> ( )} />
{editingTaskId && ( setEditingTaskId(null)} /> )} ); };