-
Notifications
You must be signed in to change notification settings - Fork 77
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
1 parent
ff3a78a
commit fb0e29d
Showing
14 changed files
with
328 additions
and
12 deletions.
There are no files selected for viewing
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
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
242 changes: 242 additions & 0 deletions
242
app/javascript/src/components/Invoices/List/SendInvoice/index.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,242 @@ | ||
import React, { | ||
FormEvent, | ||
KeyboardEvent, | ||
useEffect, | ||
useRef, | ||
useState | ||
} from "react"; | ||
|
||
import invoicesApi from "apis/invoices"; | ||
import cn from "classnames"; | ||
import Toastr from "common/Toastr"; | ||
import useOutsideClick from "helpers/outsideClick"; | ||
import { X } from "phosphor-react"; | ||
|
||
import { | ||
isEmailValid, | ||
emailSubject, | ||
emailBody, | ||
isDisabled, | ||
buttonText | ||
} from "./utils"; | ||
|
||
import { ApiStatus as InvoiceStatus } from "../../../../constants"; | ||
|
||
interface InvoiceEmail { | ||
subject: string; | ||
message: string; | ||
recipients: string[]; | ||
} | ||
|
||
const Recipient: React.FC<{ email: string; handleClick: any }> = ({ | ||
email, | ||
handleClick | ||
}) => ( | ||
<div className="flex items-center px-2 py-1 m-0.5 space-x-2 border rounded-full bg-miru-gray-400 w-fit"> | ||
<p>{email}</p> | ||
|
||
<button | ||
type="button" | ||
className="text-miru-black-1000 hover:text-miru-red-400" | ||
onClick={handleClick} | ||
> | ||
<X size={14} weight="bold" /> | ||
</button> | ||
</div> | ||
); | ||
|
||
const SendInvoice: React.FC<any> = ({ invoice, setIsSending, isSending }) => { | ||
const [status, setStatus] = useState<InvoiceStatus>(InvoiceStatus.IDLE); | ||
const [invoiceEmail, setInvoiceEmail] = useState<InvoiceEmail>({ | ||
subject: emailSubject(invoice), | ||
message: emailBody(invoice), | ||
recipients: [invoice.client.email] | ||
}); | ||
const [newRecipient, setNewRecipient] = useState<string>(""); | ||
const [width, setWidth] = useState<string>("10ch"); | ||
|
||
const modal = useRef(); | ||
const input: React.RefObject<HTMLInputElement> = useRef(); | ||
|
||
useOutsideClick(modal, () => setIsSending(false), isSending); | ||
useEffect(() => { | ||
const length = newRecipient.length; | ||
|
||
setWidth(`${length > 10 ? length : 10}ch`); | ||
}, [newRecipient]); | ||
|
||
const handleSubmit = async (event: FormEvent) => { | ||
try { | ||
event.preventDefault(); | ||
setStatus(InvoiceStatus.LOADING); | ||
|
||
const payload = { invoice_email: invoiceEmail }; | ||
const { | ||
data: { notice } | ||
} = await invoicesApi.sendInvoice(invoice.id, payload); | ||
|
||
Toastr.success(notice); | ||
setStatus(InvoiceStatus.SUCCESS); | ||
} catch (error) { | ||
setStatus(InvoiceStatus.ERROR); | ||
} | ||
}; | ||
|
||
const handleRemove = (recipient: string) => { | ||
const recipients = invoiceEmail.recipients.filter((r) => r !== recipient); | ||
|
||
setInvoiceEmail({ | ||
...invoiceEmail, | ||
recipients | ||
}); | ||
}; | ||
|
||
const handleInput = (event: KeyboardEvent) => { | ||
const recipients = invoiceEmail.recipients; | ||
|
||
if (isEmailValid(newRecipient) && event.key === "Enter") { | ||
setInvoiceEmail({ | ||
...invoiceEmail, | ||
recipients: recipients.concat(newRecipient) | ||
}); | ||
setNewRecipient(""); | ||
} | ||
}; | ||
|
||
return ( | ||
<div | ||
className="fixed inset-0 z-10 overflow-y-auto" | ||
aria-labelledby="modal-title" | ||
role="dialog" | ||
aria-modal="true" | ||
> | ||
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0"> | ||
<div | ||
className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" | ||
aria-hidden="true" | ||
></div> | ||
|
||
<span | ||
className="hidden sm:inline-block sm:align-middle sm:h-screen" | ||
aria-hidden="true" | ||
> | ||
​ | ||
</span> | ||
|
||
<div | ||
ref={modal} | ||
className="relative inline-block overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full" | ||
> | ||
<div className="px-4 pt-5 pb-4 bg-white sm:p-6 sm:pb-4"> | ||
<div className="flex items-center justify-between mt-2 mb-6"> | ||
<h6 className="form__title"> | ||
Send Invoice #{invoice.invoiceNumber} | ||
</h6> | ||
<button | ||
type="button" | ||
className="text-miru-gray-1000" | ||
onClick={() => setIsSending(false)} | ||
> | ||
<X size={16} weight="bold" /> | ||
</button> | ||
</div> | ||
|
||
<form className="space-y-4"> | ||
<fieldset className="flex flex-col field_with_errors"> | ||
<label htmlFor="to" className="mb-2 form__label"> | ||
To | ||
</label> | ||
|
||
<div | ||
onClick={() => input.current.focus()} | ||
className={cn( | ||
"p-1.5 rounded bg-miru-gray-100 flex flex-wrap", | ||
{ "h-9": !invoiceEmail.recipients } | ||
)} | ||
> | ||
{invoiceEmail.recipients.map((recipient) => ( | ||
<Recipient | ||
key={recipient} | ||
email={recipient} | ||
handleClick={() => handleRemove(recipient)} | ||
/> | ||
))} | ||
|
||
<input | ||
ref={input} | ||
style={{ width }} | ||
className={cn( | ||
"py-2 mx-1.5 rounded bg-miru-gray-100 w-fit cursor-text focus:outline-none", | ||
{ | ||
"text-miru-red-400": !isEmailValid(newRecipient) | ||
} | ||
)} | ||
type="email" | ||
name="to" | ||
value={newRecipient} | ||
onChange={(e) => setNewRecipient(e.target.value.trim())} | ||
onKeyDown={handleInput} | ||
/> | ||
</div> | ||
</fieldset> | ||
|
||
<fieldset className="flex flex-col field_with_errors"> | ||
<label htmlFor="subject" className="mb-2 form__label"> | ||
Subject | ||
</label> | ||
|
||
<input | ||
className="p-1.5 rounded bg-miru-gray-100" | ||
type="text" | ||
name="subject" | ||
value={invoiceEmail.subject} | ||
onChange={(e) => | ||
setInvoiceEmail({ | ||
...invoiceEmail, | ||
subject: e.target.value | ||
}) | ||
} | ||
/> | ||
</fieldset> | ||
|
||
<fieldset className="flex flex-col field_with_errors"> | ||
<label htmlFor="body" className="mb-2 form__label"> | ||
Message | ||
</label> | ||
|
||
<textarea | ||
name="body" | ||
className="p-1.5 rounded bg-miru-gray-100" | ||
value={invoiceEmail.message} | ||
onChange={(e) => | ||
setInvoiceEmail({ ...invoiceEmail, message: e.target.value }) | ||
} | ||
rows={5} | ||
/> | ||
</fieldset> | ||
|
||
<div> | ||
<button | ||
type="button" | ||
onClick={handleSubmit} | ||
className={cn( | ||
"flex justify-center w-full p-3 mt-6 text-lg font-bold text-white uppercase border border-transparent rounded-md shadow-sm bg-miru-han-purple-1000 hover:bg-miru-han-purple-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-miru-han-purple-600", | ||
{ | ||
"hover:bg-miru-chart-green-400 bg-miru-chart-green-600": | ||
status === InvoiceStatus.SUCCESS | ||
} | ||
)} | ||
disabled={isDisabled(status)} | ||
> | ||
{buttonText(status)} | ||
</button> | ||
</div> | ||
</form> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default SendInvoice; |
29 changes: 29 additions & 0 deletions
29
app/javascript/src/components/Invoices/List/SendInvoice/utils.ts
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,29 @@ | ||
import dayjs from "dayjs"; | ||
import { currencyFormat } from "helpers/currency"; | ||
import * as Yup from "yup"; | ||
import { ApiStatus as InvoiceStatus } from "../../../../constants"; | ||
|
||
export const isEmailValid = (email: string): boolean => { | ||
const schema = Yup.string().email(); | ||
|
||
return schema.isValidSync(email); | ||
}; | ||
|
||
export const emailSubject = (invoice: any): string => | ||
`${invoice.company.name} sent you an invoice (${invoice.invoiceNumber})`; | ||
|
||
export const emailBody = (invoice: any): string => { | ||
const formattedAmount = currencyFormat({ | ||
baseCurrency: invoice.company.baseCurrency, | ||
amount: invoice.amount | ||
}); | ||
const dueDate = dayjs(invoice.dueDate).format(invoice.company.dateFormat); | ||
|
||
return `${invoice.company.name} has sent you an invoice (${invoice.invoiceNumber}) for ${formattedAmount} that's due on ${dueDate}.`; | ||
}; | ||
|
||
export const isDisabled = (status: string): boolean => | ||
status === InvoiceStatus.LOADING || status === InvoiceStatus.SUCCESS; | ||
|
||
export const buttonText = (status: string): string => | ||
status === InvoiceStatus.SUCCESS ? "🎉 Invoice Sent!" : "Send Invoice"; |
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
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
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
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
Oops, something went wrong.