EventContextMenu.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. 'use client'
  2. import type { ReactNode } from 'react'
  3. import { useRouter } from 'next/navigation'
  4. import { useTranslations } from 'next-intl'
  5. import { toast } from 'sonner'
  6. import { Check, ExternalLink, Pencil, Trash2, Undo2, Wrench } from 'lucide-react'
  7. import {
  8. ContextMenu,
  9. ContextMenuContent,
  10. ContextMenuItem,
  11. ContextMenuLabel,
  12. ContextMenuSeparator,
  13. ContextMenuTrigger,
  14. } from '@/components/ui/context-menu'
  15. import { useConfirm } from '@/components/confirm-dialog'
  16. import { deleteReminder, toggleReminder } from '@/features/vehicles/Actions/reminderActions'
  17. import { deleteScheduledMessage } from '@/features/scheduled-messages/Actions/scheduledMessageActions'
  18. import { getEventLink } from './calendar-utils'
  19. import { deleteAppointment } from '../Actions/deleteAppointment'
  20. import { isEditableBooking, useEventPeek } from './EventPeek'
  21. import type { CalendarEvent } from '../Actions/calendarActions'
  22. /**
  23. * Right-click on an event: open it, and for the things the calendar owns
  24. * outright, act on them here. A reminder can be ticked off or deleted; a
  25. * scheduled message can be deleted before it goes out. Work orders and
  26. * quotes carry money and paper, so they are only opened from here.
  27. */
  28. export function EventContextMenu({
  29. event,
  30. children,
  31. }: {
  32. event: CalendarEvent
  33. children: ReactNode
  34. }) {
  35. const t = useTranslations('calendar.eventMenu')
  36. const router = useRouter()
  37. const confirm = useConfirm()
  38. const { refresh, editBooking } = useEventPeek()
  39. const open = () => {
  40. if (event.type === 'external') {
  41. if (event.externalUrl) window.open(event.externalUrl, '_blank', 'noopener')
  42. return
  43. }
  44. router.push(getEventLink(event))
  45. }
  46. const run = async (
  47. action: () => Promise<{ success: boolean; error?: string }>,
  48. doneMessage: string
  49. ) => {
  50. const result = await action()
  51. if (result.success) {
  52. toast.success(doneMessage)
  53. refresh()
  54. } else {
  55. toast.error(result.error || t('failed'))
  56. }
  57. }
  58. const removeReminder = async () => {
  59. const ok = await confirm({
  60. description: t('deleteReminderConfirm', { title: event.title }),
  61. destructive: true,
  62. })
  63. if (ok) await run(() => deleteReminder(event.id), t('deleted'))
  64. }
  65. const removeMessage = async () => {
  66. const ok = await confirm({
  67. description: t('deleteMessageConfirm', { title: event.title }),
  68. destructive: true,
  69. })
  70. if (ok) await run(() => deleteScheduledMessage(event.id), t('deleted'))
  71. }
  72. const removeAppointment = async () => {
  73. const ok = await confirm({
  74. description: t('deleteAppointmentConfirm', { title: event.title }),
  75. destructive: true,
  76. })
  77. if (ok) await run(() => deleteAppointment(event.id), t('deleted'))
  78. }
  79. const reminderDone = event.type === 'reminder' && event.status === 'completed'
  80. const canEdit = Boolean(editBooking) && isEditableBooking(event)
  81. return (
  82. <ContextMenu>
  83. <ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
  84. <ContextMenuContent className="min-w-52">
  85. <ContextMenuLabel className="truncate text-xs font-normal text-muted-foreground">
  86. {event.title}
  87. </ContextMenuLabel>
  88. <ContextMenuSeparator />
  89. {canEdit && (
  90. <ContextMenuItem onClick={() => editBooking?.({ mode: 'edit', event })}>
  91. <Pencil className="mr-2 h-4 w-4" />
  92. {t('edit')}
  93. </ContextMenuItem>
  94. )}
  95. {event.type === 'appointment' ? (
  96. editBooking && (
  97. <>
  98. <ContextMenuItem onClick={() => editBooking({ mode: 'convert', event })}>
  99. <Wrench className="mr-2 h-4 w-4" />
  100. {t('makeWorkOrder')}
  101. </ContextMenuItem>
  102. <ContextMenuSeparator />
  103. <ContextMenuItem variant="destructive" onClick={removeAppointment}>
  104. <Trash2 className="mr-2 h-4 w-4" />
  105. {t('deleteAppointment')}
  106. </ContextMenuItem>
  107. </>
  108. )
  109. ) : (
  110. <ContextMenuItem onClick={open}>
  111. <ExternalLink className="mr-2 h-4 w-4" />
  112. {t('open')}
  113. </ContextMenuItem>
  114. )}
  115. {event.type === 'reminder' && (
  116. <>
  117. <ContextMenuItem onClick={() => run(() => toggleReminder(event.id), t('updated'))}>
  118. {reminderDone ? (
  119. <Undo2 className="mr-2 h-4 w-4" />
  120. ) : (
  121. <Check className="mr-2 h-4 w-4" />
  122. )}
  123. {reminderDone ? t('markNotDone') : t('markDone')}
  124. </ContextMenuItem>
  125. <ContextMenuSeparator />
  126. <ContextMenuItem variant="destructive" onClick={removeReminder}>
  127. <Trash2 className="mr-2 h-4 w-4" />
  128. {t('deleteReminder')}
  129. </ContextMenuItem>
  130. </>
  131. )}
  132. {event.type === 'message' && (
  133. <>
  134. <ContextMenuSeparator />
  135. <ContextMenuItem variant="destructive" onClick={removeMessage}>
  136. <Trash2 className="mr-2 h-4 w-4" />
  137. {t('deleteMessage')}
  138. </ContextMenuItem>
  139. </>
  140. )}
  141. </ContextMenuContent>
  142. </ContextMenu>
  143. )
  144. }