Compare commits

..

1 Commits

Author SHA1 Message Date
700fa23787 fix: document creation timezone 2024-07-23 15:15:29 +03:00
9 changed files with 39 additions and 169 deletions

View File

@ -89,7 +89,7 @@ Please note that you must provide environment variables for connecting to the da
### Option 1: Production Docker Compose Setup
This setup includes a PostgreSQL database and the Documenso application.
This setup includes a PostgreSQL database and the Documenso application. You will need to provide your own SMTP details using environment variables.
<Steps>
@ -103,14 +103,12 @@ Once downloaded, navigate to the directory containing the `compose.yml` file.
### Set Up Environment Variables
Create a `.env` file in the same directory as the `compose.yml` file and fill in the following environment variables:
Create a `.env` file in the same directory as the `compose.yml` file.
Then add your SMTP details as well as the following environment variables:
```bash
POSTGRES_USER="user"
POSTGRES_PASSWORD="changeme"
POSTGRES_DB=documenso
NEXTAUTH_SECRET="<your-secret>"
NEXT_PRIVATE_DATABASE_URL="postgres://<user>:<password>@<docker-network-or-ip:5432>/<db-name>"
NEXT_PRIVATE_ENCRYPTION_KEY="<your-key>"
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="<your-secondary-key>"
NEXT_PUBLIC_WEBAPP_URL="<your-url>"
@ -128,21 +126,12 @@ The `cert.p12` file is required to sign and encrypt documents, so you must provi
```yaml
volumes:
- /path/to/your/keyfile.p12:/opt/documenso/cert.p12
# example
volumes:
- ../../apps/web/example/cert.p12:/opt/documenso/cert.p12
```
<Callout type="info">
Follow the instructions from the ["Signing Certificate"
section](developers/local-development/signing-certificate) to generate the `cert.p12` file.
</Callout>
After updating the volume binding, save the `compose.yml` file and run the following command to start the containers:
```bash
docker-compose up -d
docker-compose --env-file ./.env -d up
```
The command will start the PostgreSQL database and the Documenso application containers.

View File

@ -10,8 +10,6 @@ import { Callout } from 'nextra/components';
signatures to ensure their authenticity, integrity, and confidentiality in the pharmaceutical, medical
device, and other FDA-regulated industries.
> Read more about 21 CFR Part 11 with Documenso here: https://documen.so/21-CFR-Part-11
### Main Requirements
- [x] Strong Identity Checks for each Signature

View File

@ -1,96 +0,0 @@
---
title: Creating an Efficient Statement of Work Approval Process with Documenso
description: Submitting statements of work can be a drag on morale and project efficiency. Let's look at how to create a modern, low-friction workflow for this.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2024-07-23
tags:
- Freelancer
- Statement of Work
- Productivity
---
<figure>
<MdxNextImage
src="/blog/sov.webp"
width="1400"
height="884"
alt="Working papers image"
/>
<figcaption className="text-center">Fine-tune your process using custom role for everyone involved.</figcaption>
</figure>
> TLDR; Statements of Work detail what needs to be done. Automate sending and approving them using Documenso and Zapier.
## What is a Statement of Work
A statement of work is a detailed document that outlines what needs to be done in a project. It covers the project's scope, objectives, and deliverables, laying out all the tasks, deadlines, and milestones. The statement of work also spells out whos responsible for what, ensuring everyones on the same page. Its a roadmap that keeps both clients and service providers aligned and ensures the project stays on track from start to finish.
In the context of freelance work, the statement of work is a document that outlines the details of a project between a freelancer and their client. It's a concrete work to be agreed upon and tracked after completion. The statement of work is created after the [proposal is accepted](https://documen.so/freelance-proposal) and the [contract signed](https://documen.so/freelance-contract).
## What does a good workflow look like?
### 1. Create the statement of work
The team at Zapier created a [excellent guide](https://zapier.com/blog/statement-of-work-template/), which goes into the statement of work. There is a short checklist:
- Project Context/ Current Scope
- Objectives for this piece of work
- Scope (tasks, activities, and limits)
- Requirements (e.g., technical and regulatory)
- Deliverables to be created
- Roles and Responsibilities
### 2. Get approval from subject matter experts (optional)
Since a statements of work can be very technical, having professionals from either side approve it first can be sensible. This can avoid double or unnecessary work and minimize the chances of misunderstandings. If this makes sense, it depends heavily on the scale of the project and the level of insight of the professional providing it.
### 3. Let the client sign off
Having the client sign off on the concrete work is the central step of the statement of work workflow. Assuming the documents content is correct, getting the go-ahead ensures everyone is aligned and clear on what should and will be worked on.
### 4. Inform other Stakeholders (optional)
Depending on the scale of the organizations working together, other people may need to be kept in the loop. This could be accounting on either side, project managers, or other interested parties.
## Fine-Tuning the Flow with Custom Roles
<figure>
<MdxNextImage src="/blog/roles.webp" width="1400" height="884" alt="Documenso Roles UI" />
<figcaption className="text-center">
Let's take a look at what it would look like with Documenso.
</figcaption>
</figure>
### 1. Creating the Document: Templates vs. Custom Document
[Creating a template](https://docs.documenso.com/users/templates) can make sense if you submit statements of work regularly. If you create a Documenso Template, you can add [dynamic text and number fields](https://docs.documenso.com/users/signing-documents/fields) to be filled out when using the template. Another approach to this is creating a template on a document service like Google Docs, filling out a new copy, and uploading the custom-created document to Documenso.
Different parts of this process can be automated using the [Zapier Documenso Integration](https://documen.so/zapier) as desired:
- Automatically sending out a template to be filled out and signed
- Creating a document in Documenso from a newly created document in Google Docs
- Triggering sending a document created from either template or automation
### 2. Approvals
Looping in subject matter experts can easily be done using the approver role. This role allows you to complete a document without blocking the signing. This means the client can sign a document, even if the approval is not yet in place, removing friction. However, having the approval denied will stop the document from being completed and let everyone know there are corrections to be made. A software version of this is possible, having the expert in a viewer role and marking the document as seen without the option to block.
### 3. Signers
Looping in the client is done by adding one or more signer roles. Signer roles require recipients to place at least one signature to fulfill their part in the flow.roles
### 4. BCC
You can add one or several BCC roles to inform interested parties, e.g., accounting or project managers. As the process finishes, BCC recipients receive a copy of the completed document (assuming it completes and is not blocked). Using BCC roles automates filling in everyone who is only interested in the outcome and wants to avoid involvement in the steps leading up to it.
### Conclusion
Streamlining your statement of work approval process with Documenso can significantly improve productivity and ease. Using templates, dynamic fields, and Zapier integrations, you can create a smooth, efficient workflow from start to finish. Adding roles for experts, signers, and BCC recipients tailors the process to fit your project's needs, ensuring everyone stays on the same page.
An efficient SOW process saves time and improves communication, letting you focus on delivering great work. We're excited to hear your thoughts and experiences—reach out on [Twitter / X](https://twitter.com/eltimuro) (DMs are open) or [Discord](https://documen.so/discord) if you have any questions, ideas, or comments.
Best from Hamburg\
Timur

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@ -128,7 +128,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
<div className="mt-8 flex w-full flex-col divide-y">
<p className="text-foreground py-4">Unlimited Documents per Month</p>
<p className="text-foreground py-4">API Access</p>
<p className="text-foreground py-4">API Accesss</p>
<p className="text-foreground py-4">Email and Discord Support</p>
<p className="text-foreground py-4">Premium Profile Name</p>
</div>
@ -162,7 +162,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
<div className="mt-8 flex w-full flex-col divide-y">
<p className="text-foreground py-4">Unlimited Documents per Month</p>
<p className="text-foreground py-4">API Access</p>
<p className="text-foreground py-4">API Accesss</p>
<p className="text-foreground py-4">Email and Discord Support</p>
<p className="text-foreground py-4 font-medium">Team Inbox</p>
<p className="text-foreground py-4">5 Users Included</p>

View File

@ -32,7 +32,7 @@ export default async function ApiTokensPage() {
<hr className="my-4" />
<ApiTokenForm className="max-w-xl" tokens={tokens} />
<ApiTokenForm className="max-w-xl" />
<hr className="mb-4 mt-8" />

View File

@ -26,7 +26,7 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
const team = await getTeamByUrl({ userId: user.id, teamUrl });
let tokens: GetTeamTokensResponse | undefined = undefined;
let tokens: GetTeamTokensResponse | null = null;
try {
tokens = await getTeamTokens({ userId: user.id, teamId: team.id });
@ -63,7 +63,7 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
<hr className="my-4" />
<ApiTokenForm className="max-w-xl" teamId={team.id} tokens={tokens} />
<ApiTokenForm className="max-w-xl" teamId={team.id} />
<hr className="mb-4 mt-8" />

View File

@ -1,16 +1,14 @@
'use client';
import { useState, useTransition } from 'react';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import type { ApiToken } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import type { TCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
@ -46,37 +44,23 @@ const ZCreateTokenFormSchema = ZCreateTokenMutationSchema.extend({
type TCreateTokenFormSchema = z.infer<typeof ZCreateTokenFormSchema>;
type NewlyCreatedToken = {
id: number;
token: string;
};
export type ApiTokenFormProps = {
className?: string;
teamId?: number;
tokens?: Pick<ApiToken, 'id'>[];
};
export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) => {
export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
const router = useRouter();
const [isTransitionPending, startTransition] = useTransition();
const [, copy] = useCopyToClipboard();
const { toast } = useToast();
const [newlyCreatedToken, setNewlyCreatedToken] = useState<NewlyCreatedToken | null>();
const [newlyCreatedToken, setNewlyCreatedToken] = useState('');
const [noExpirationDate, setNoExpirationDate] = useState(false);
// This lets us hide the token from being copied if it has been deleted without
// resorting to a useEffect or any other fanciness. This comes at the cost of it
// taking slighly longer to appear since it will need to wait for the router.refresh()
// to finish updating.
const hasNewlyCreatedToken =
tokens?.find((token) => token.id === newlyCreatedToken?.id) !== undefined;
const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({
onSuccess(data) {
setNewlyCreatedToken(data);
setNewlyCreatedToken(data.token);
},
});
@ -126,7 +110,7 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
form.reset();
startTransition(() => router.refresh());
router.refresh();
} catch (error) {
if (error instanceof TRPCClientError && error.data?.code === 'BAD_REQUEST') {
toast({
@ -232,7 +216,7 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
type="submit"
className="hidden md:inline-flex"
disabled={!form.formState.isDirty}
loading={form.formState.isSubmitting || isTransitionPending}
loading={form.formState.isSubmitting}
>
Create token
</Button>
@ -241,7 +225,7 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
<Button
type="submit"
disabled={!form.formState.isDirty}
loading={form.formState.isSubmitting || isTransitionPending}
loading={form.formState.isSubmitting}
>
Create token
</Button>
@ -250,33 +234,24 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
</form>
</Form>
<AnimatePresence initial={!hasNewlyCreatedToken}>
{newlyCreatedToken && hasNewlyCreatedToken && (
<motion.div
className="mt-8"
initial={{ opacity: 0, y: -40 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 40 }}
>
<Card gradient>
<CardContent className="p-4">
<p className="text-muted-foreground mt-2 text-sm">
Your token was created successfully! Make sure to copy it because you won't be
able to see it again!
</p>
{newlyCreatedToken && (
<Card className="mt-8" gradient>
<CardContent className="p-4">
<p className="text-muted-foreground mt-2 text-sm">
Your token was created successfully! Make sure to copy it because you won't be able to
see it again!
</p>
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
{newlyCreatedToken.token}
</p>
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
{newlyCreatedToken}
</p>
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken.token)}>
Copy token
</Button>
</CardContent>
</Card>
</motion.div>
)}
</AnimatePresence>
<Button variant="outline" onClick={() => void copyToken(newlyCreatedToken)}>
Copy token
</Button>
</CardContent>
</Card>
)}
</div>
);
};

View File

@ -98,8 +98,12 @@ export const AddSettingsFormPartial = ({
// We almost always want to set the timezone to the user's local timezone to avoid confusion
// when the document is signed.
useEffect(() => {
if (!form.formState.touchedFields.meta?.timezone && !documentHasBeenSent) {
form.setValue('meta.timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
// Check if the timezone field has not been modified and the document hasn't been sent
if (!form.formState.dirtyFields.meta?.timezone && !documentHasBeenSent) {
// If the timezone is the default timezone, set it to the user's local timezone automatically
if (form.getValues('meta.timezone') === DEFAULT_DOCUMENT_TIME_ZONE) {
form.setValue('meta.timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
}
}
}, [documentHasBeenSent, form, form.setValue, form.formState.touchedFields.meta?.timezone]);