forked from documenso/documenso
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
824 additions
and
169 deletions.
There are no files selected for viewing
69 changes: 69 additions & 0 deletions
69
apps/web/src/app/(dashboard)/admin/documents/[id]/admin-actions.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
'use client'; | ||
|
||
import Link from 'next/link'; | ||
|
||
import { type Document, DocumentStatus } from '@documenso/prisma/client'; | ||
import { trpc } from '@documenso/trpc/react'; | ||
import { cn } from '@documenso/ui/lib/utils'; | ||
import { Button } from '@documenso/ui/primitives/button'; | ||
import { | ||
Tooltip, | ||
TooltipContent, | ||
TooltipProvider, | ||
TooltipTrigger, | ||
} from '@documenso/ui/primitives/tooltip'; | ||
import { useToast } from '@documenso/ui/primitives/use-toast'; | ||
|
||
export type AdminActionsProps = { | ||
className?: string; | ||
document: Document; | ||
}; | ||
|
||
export const AdminActions = ({ className, document }: AdminActionsProps) => { | ||
const { toast } = useToast(); | ||
|
||
const { mutate: resealDocument, isLoading: isResealDocumentLoading } = | ||
trpc.admin.resealDocument.useMutation({ | ||
onSuccess: () => { | ||
toast({ | ||
title: 'Success', | ||
description: 'Document resealed', | ||
}); | ||
}, | ||
onError: () => { | ||
toast({ | ||
title: 'Error', | ||
description: 'Failed to reseal document', | ||
variant: 'destructive', | ||
}); | ||
}, | ||
}); | ||
|
||
return ( | ||
<div className={cn('flex gap-x-4', className)}> | ||
<TooltipProvider> | ||
<Tooltip> | ||
<TooltipTrigger asChild> | ||
<Button | ||
variant="outline" | ||
loading={isResealDocumentLoading} | ||
disabled={document.status !== DocumentStatus.COMPLETED} | ||
onClick={() => resealDocument({ id: document.id })} | ||
> | ||
Reseal document | ||
</Button> | ||
</TooltipTrigger> | ||
|
||
<TooltipContent className="max-w-[40ch]"> | ||
Attempts sealing the document again, useful for after a code change has occurred to | ||
resolve an erroneous document. | ||
</TooltipContent> | ||
</Tooltip> | ||
</TooltipProvider> | ||
|
||
<Button variant="outline" asChild> | ||
<Link href={`/admin/users/${document.userId}`}>Go to owner</Link> | ||
</Button> | ||
</div> | ||
); | ||
}; |
86 changes: 86 additions & 0 deletions
86
apps/web/src/app/(dashboard)/admin/documents/[id]/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { DateTime } from 'luxon'; | ||
|
||
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document'; | ||
import { | ||
Accordion, | ||
AccordionContent, | ||
AccordionItem, | ||
AccordionTrigger, | ||
} from '@documenso/ui/primitives/accordion'; | ||
import { Badge } from '@documenso/ui/primitives/badge'; | ||
|
||
import { DocumentStatus } from '~/components/formatter/document-status'; | ||
import { LocaleDate } from '~/components/formatter/locale-date'; | ||
|
||
import { AdminActions } from './admin-actions'; | ||
import { RecipientItem } from './recipient-item'; | ||
|
||
type AdminDocumentDetailsPageProps = { | ||
params: { | ||
id: string; | ||
}; | ||
}; | ||
|
||
export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) { | ||
const document = await getEntireDocument({ id: Number(params.id) }); | ||
|
||
return ( | ||
<div> | ||
<div className="flex items-start justify-between"> | ||
<div className="flex items-center gap-x-4"> | ||
<h1 className="text-2xl font-semibold">{document.title}</h1> | ||
<DocumentStatus status={document.status} /> | ||
</div> | ||
|
||
{document.deletedAt && ( | ||
<Badge size="large" variant="destructive"> | ||
Deleted | ||
</Badge> | ||
)} | ||
</div> | ||
|
||
<div className="text-muted-foreground mt-4 text-sm"> | ||
<div> | ||
Created on: <LocaleDate date={document.createdAt} format={DateTime.DATETIME_MED} /> | ||
</div> | ||
<div> | ||
Last updated at: <LocaleDate date={document.updatedAt} format={DateTime.DATETIME_MED} /> | ||
</div> | ||
</div> | ||
|
||
<hr className="my-4" /> | ||
|
||
<h2 className="text-lg font-semibold">Admin Actions</h2> | ||
|
||
<AdminActions className="mt-2" document={document} /> | ||
|
||
<hr className="my-4" /> | ||
<h2 className="text-lg font-semibold">Recipients</h2> | ||
|
||
<div className="mt-4"> | ||
<Accordion type="multiple" className="space-y-4"> | ||
{document.Recipient.map((recipient) => ( | ||
<AccordionItem | ||
key={recipient.id} | ||
value={recipient.id.toString()} | ||
className="rounded-lg border" | ||
> | ||
<AccordionTrigger className="px-4"> | ||
<div className="flex items-center gap-x-4"> | ||
<h4 className="font-semibold">{recipient.name}</h4> | ||
<Badge size="small" variant="neutral"> | ||
{recipient.email} | ||
</Badge> | ||
</div> | ||
</AccordionTrigger> | ||
|
||
<AccordionContent className="border-t px-4 pt-4"> | ||
<RecipientItem recipient={recipient} /> | ||
</AccordionContent> | ||
</AccordionItem> | ||
))} | ||
</Accordion> | ||
</div> | ||
</div> | ||
); | ||
} |
182 changes: 182 additions & 0 deletions
182
apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
'use client'; | ||
|
||
import { useRouter } from 'next/navigation'; | ||
|
||
import { useForm } from 'react-hook-form'; | ||
import { z } from 'zod'; | ||
|
||
import { | ||
type Field, | ||
type Recipient, | ||
type Signature, | ||
SigningStatus, | ||
} from '@documenso/prisma/client'; | ||
import { trpc } from '@documenso/trpc/react'; | ||
import { Button } from '@documenso/ui/primitives/button'; | ||
import { DataTable } from '@documenso/ui/primitives/data-table'; | ||
import { | ||
Form, | ||
FormControl, | ||
FormField, | ||
FormItem, | ||
FormLabel, | ||
FormMessage, | ||
} from '@documenso/ui/primitives/form/form'; | ||
import { Input } from '@documenso/ui/primitives/input'; | ||
import { useToast } from '@documenso/ui/primitives/use-toast'; | ||
|
||
const ZAdminUpdateRecipientFormSchema = z.object({ | ||
name: z.string().min(1), | ||
email: z.string().email(), | ||
}); | ||
|
||
type TAdminUpdateRecipientFormSchema = z.infer<typeof ZAdminUpdateRecipientFormSchema>; | ||
|
||
export type RecipientItemProps = { | ||
recipient: Recipient & { | ||
Field: Array< | ||
Field & { | ||
Signature: Signature | null; | ||
} | ||
>; | ||
}; | ||
}; | ||
|
||
export const RecipientItem = ({ recipient }: RecipientItemProps) => { | ||
const { toast } = useToast(); | ||
const router = useRouter(); | ||
|
||
const form = useForm<TAdminUpdateRecipientFormSchema>({ | ||
defaultValues: { | ||
name: recipient.name, | ||
email: recipient.email, | ||
}, | ||
}); | ||
|
||
const { mutateAsync: updateRecipient } = trpc.admin.updateRecipient.useMutation(); | ||
|
||
const onUpdateRecipientFormSubmit = async ({ name, email }: TAdminUpdateRecipientFormSchema) => { | ||
try { | ||
await updateRecipient({ | ||
id: recipient.id, | ||
name, | ||
email, | ||
}); | ||
|
||
toast({ | ||
title: 'Recipient updated', | ||
description: 'The recipient has been updated successfully', | ||
}); | ||
|
||
router.refresh(); | ||
} catch (error) { | ||
toast({ | ||
title: 'Failed to update recipient', | ||
description: error.message, | ||
variant: 'destructive', | ||
}); | ||
} | ||
}; | ||
|
||
return ( | ||
<div> | ||
<Form {...form}> | ||
<form onSubmit={form.handleSubmit(onUpdateRecipientFormSubmit)}> | ||
<fieldset | ||
className="flex h-full max-w-xl flex-col gap-y-4" | ||
disabled={ | ||
form.formState.isSubmitting || recipient.signingStatus === SigningStatus.SIGNED | ||
} | ||
> | ||
<FormField | ||
control={form.control} | ||
name="name" | ||
render={({ field }) => ( | ||
<FormItem className="flex-1"> | ||
<FormLabel required>Name</FormLabel> | ||
|
||
<FormControl> | ||
<Input {...field} /> | ||
</FormControl> | ||
|
||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
|
||
<FormField | ||
control={form.control} | ||
name="email" | ||
render={({ field }) => ( | ||
<FormItem className="flex-1"> | ||
<FormLabel required>Email</FormLabel> | ||
|
||
<FormControl> | ||
<Input type="email" {...field} /> | ||
</FormControl> | ||
|
||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
|
||
<div> | ||
<Button type="submit" loading={form.formState.isSubmitting}> | ||
Update Recipient | ||
</Button> | ||
</div> | ||
</fieldset> | ||
</form> | ||
</Form> | ||
|
||
<hr className="my-4" /> | ||
|
||
<h2 className="mb-4 text-lg font-semibold">Fields</h2> | ||
|
||
<DataTable | ||
data={recipient.Field} | ||
columns={[ | ||
{ | ||
header: 'ID', | ||
accessorKey: 'id', | ||
cell: ({ row }) => <div>{row.original.id}</div>, | ||
}, | ||
{ | ||
header: 'Type', | ||
accessorKey: 'type', | ||
cell: ({ row }) => <div>{row.original.type}</div>, | ||
}, | ||
{ | ||
header: 'Inserted', | ||
accessorKey: 'inserted', | ||
cell: ({ row }) => <div>{row.original.inserted ? 'True' : 'False'}</div>, | ||
}, | ||
{ | ||
header: 'Value', | ||
accessorKey: 'customText', | ||
cell: ({ row }) => <div>{row.original.customText}</div>, | ||
}, | ||
{ | ||
header: 'Signature', | ||
accessorKey: 'signature', | ||
cell: ({ row }) => ( | ||
<div> | ||
{row.original.Signature?.typedSignature && ( | ||
<span>{row.original.Signature.typedSignature}</span> | ||
)} | ||
|
||
{row.original.Signature?.signatureImageAsBase64 && ( | ||
<img | ||
src={row.original.Signature.signatureImageAsBase64} | ||
alt="Signature" | ||
className="h-12 w-full dark:invert" | ||
/> | ||
)} | ||
</div> | ||
), | ||
}, | ||
]} | ||
/> | ||
</div> | ||
); | ||
}; |
Oops, something went wrong.