Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 104 additions & 32 deletions fdm-app/app/components/blocks/helpdesk/message-composer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { useId } from "react"
import type {
SavedReplyContext,
SavedReplySummary,
} from "@nmi-agro/fdm-helpdesk"
import { useEffect, useId } from "react"
import { Controller, useWatch } from "react-hook-form"
import { useFetcher } from "react-router"
import { RemixFormProvider, useRemixForm } from "remix-hook-form"
Expand All @@ -25,7 +29,7 @@ import { Switch } from "~/components/ui/switch"
import { Textarea } from "~/components/ui/textarea"
import { Message } from "./message"
import { MessageSchema } from "./message-schema"
import type { HelpdeskUser } from "./types"
import type { HelpdeskUser } from "./types.d"

const formDefaultValues = {
body: "",
Expand All @@ -37,16 +41,21 @@ export function MessageComposer({
intent,
principal,
showAgentControls,
savedReplies,
savedReplyContext,
defaultValues = { ...formDefaultValues },
className,
}: {
intent: string
principal: HelpdeskUser | null
showAgentControls?: boolean
savedReplies?: SavedReplySummary[]
savedReplyContext?: SavedReplyContext
defaultValues?: z.infer<typeof MessageSchema>
className?: string
}) {
const fetcher = useFetcher()
const savedReplyFetcher = useFetcher()

const form = useRemixForm({
mode: "onTouched",
Expand All @@ -58,11 +67,28 @@ export function MessageComposer({
...defaultValues,
},
})

const sender_role = useWatch({ name: "sender_role", control: form.control })
const is_internal = useWatch({ name: "is_internal", control: form.control })
const messageInputId = useId()

useEffect(() => {
if (typeof savedReplyFetcher.data?.body === "string") {
const current = form.getValues("body")
if (current) {
form.setValue(
"body",
`${current}\n\n${savedReplyFetcher.data.body}`,
)
} else {
form.setValue("body", savedReplyFetcher.data.body)
}
}
}, [form.getValues, form.setValue, savedReplyFetcher.data])

const isSubmitting = fetcher.state !== "idle"
const isFetchingSavedReply = savedReplyFetcher.state !== "idle"
const isDisabled = isSubmitting || isFetchingSavedReply

return (
<RemixFormProvider {...form}>
Expand All @@ -77,7 +103,7 @@ export function MessageComposer({
isInternal={is_internal}
title={
<fieldset
disabled={isSubmitting}
disabled={isDisabled}
className="w-full flex flex-row items-center gap-2 md:gap-4"
>
<span className="min-w-0 flex-1">
Expand Down Expand Up @@ -118,7 +144,7 @@ export function MessageComposer({
field.onChange
}
className="mt-1"
disabled={isSubmitting}
disabled={isDisabled}
/>
</div>
<FieldError
Expand Down Expand Up @@ -176,41 +202,87 @@ export function MessageComposer({
</fieldset>
}
>
<fieldset disabled={isSubmitting}>
<fieldset disabled={isDisabled}>
{savedReplies && savedReplies.length > 0 ? (
<Field orientation="horizontal">
<FieldLabel>Gebruik een sjabloon: </FieldLabel>
<Select
onValueChange={(reply_id) => {
const formData = new FormData()
formData.append(
"intent",
"apply_saved_reply",
)
formData.append("reply_id", reply_id)
for (const [k, v] of Object.entries(
savedReplyContext ?? {},
)) {
if (typeof v === "string")
formData.append(k, v)
}
savedReplyFetcher.submit(formData, {
method: "post",
})
}}
>
<SelectTrigger>
<SelectValue />
<Spinner
className={cn(
!isFetchingSavedReply &&
"invisible",
)}
/>
</SelectTrigger>
<SelectContent>
{savedReplies.map((reply) => (
<SelectItem
key={reply.reply_id}
value={reply.reply_id}
>
{reply.title}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
) : undefined}
<Controller
name="body"
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldContent>
<Textarea
{...field}
className="bg-card"
id={messageInputId}
placeholder={
"Schrijf uw bericht hier..."
}
/>
<FieldDescription>
{showAgentControls
? sender_role === "agent"
? is_internal
? "Intern bericht — alleen zichtbaar voor medewerkers."
: "Dit bericht wordt als medewerker verstuurd en is zichtbaar voor de gebruiker."
: "Dit bericht wordt verstuurd als de gebruiker."
: "Voeg aanvullende informatie toe of stel een vervolgvraag. U ontvangt een kopie per e-mail."}
</FieldDescription>
<FieldError
errors={[fieldState.error]}
/>
</FieldContent>
</Field>
)}
render={({ field, fieldState }) => {
return (
<Field data-invalid={fieldState.invalid}>
<FieldContent>
<Textarea
{...field}
className="bg-card"
id={messageInputId}
placeholder={
"Schrijf uw bericht hier..."
}
/>
<FieldDescription>
{showAgentControls
? sender_role === "agent"
? is_internal
? "Intern bericht — alleen zichtbaar voor medewerkers."
: "Dit bericht wordt als medewerker verstuurd en is zichtbaar voor de gebruiker."
: "Dit bericht wordt verstuurd als de gebruiker."
: "Voeg aanvullende informatie toe of stel een vervolgvraag. U ontvangt een kopie per e-mail."}
</FieldDescription>
<FieldError
errors={[fieldState.error]}
/>
</FieldContent>
</Field>
)
}}
/>
</fieldset>
<Button
type="submit"
className="block ms-auto min-w-0"
disabled={isSubmitting}
disabled={isDisabled}
>
Versturen
{isSubmitting && <Spinner />}
Expand Down
21 changes: 19 additions & 2 deletions fdm-app/app/components/blocks/helpdesk/message.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { Message as MessageT } from "@nmi-agro/fdm-helpdesk"
import type {
Message as MessageT,
SavedReplyContext,
} from "@nmi-agro/fdm-helpdesk"
import { formatDate, formatDistanceToNow } from "date-fns"
import { nl } from "date-fns/locale"
import type { ReactNode } from "react"
import { cn } from "@/app/lib/utils"
import { Card } from "~/components/ui/card"
import { HelpdeskUserAvatar } from "./helpdesk-user"
import { CreateSavedReplyDialog } from "./saved-reply-dialog"
import type { HelpdeskUser } from "./types"

export type MessageExtended = MessageT & { principal: HelpdeskUser | null }
Expand All @@ -17,6 +21,9 @@ export function Message({
isInternal = false,
className,
children,
canSaveReply = false,
replyContext,
replyBody,
}: {
title?: ReactNode
principal: HelpdeskUser | null
Expand All @@ -26,6 +33,9 @@ export function Message({
isInternal?: boolean
className?: string
children: ReactNode
canSaveReply?: boolean
replyContext?: SavedReplyContext
replyBody?: string
}) {
const formattedDateTooltip = date
? formatDate(date, "PP HH:mm", { locale: nl })
Expand All @@ -37,7 +47,7 @@ export function Message({
return (
<Card
className={cn(
"md:ms-10 px-2 py-4",
"group/message md:ms-10 px-2 py-4",
isInternal && "border-amber-100 bg-amber-100/35",
className,
)}
Expand All @@ -57,6 +67,13 @@ export function Message({
</span>
)}
<div className="ms-auto">
{canSaveReply && replyBody && (
<CreateSavedReplyDialog
body={replyBody}
context={replyContext}
isInternal={isInternal}
/>
)}
{formattedDateTooltip && (
<span
className="text-sm"
Expand Down
Loading
Loading