First naive implementation (~ PHP version copy)
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
/node_modules/
|
||||
/lib/
|
||||
yarn-error.log
|
||||
.DS_Store
|
||||
.project
|
||||
.vscode
|
||||
*.log
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"transform": {
|
||||
"^.+\\.ts$": "ts-jest"
|
||||
},
|
||||
"testRegex": ".+\\.spec\\.ts$"
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "gotenberg-js-client",
|
||||
"version": "0.1.0",
|
||||
"description": "A simple JS/TS for interacting with a Gotenberg API",
|
||||
"keywords": [
|
||||
"gotenberg",
|
||||
"pdf",
|
||||
"puppeteer"
|
||||
],
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"author": "Victor Didenko <yumaa.verdin@gmail.com>",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/yumauri/gotenberg-js-client"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/yumauri/gotenberg-js-client/issues"
|
||||
},
|
||||
"homepage": "https://github.com/yumauri/gotenberg-js-client#readme",
|
||||
"scripts": {
|
||||
"dev": "ts-node src/index.ts",
|
||||
"test": "jest --config jestconfig.json",
|
||||
"build": "npm run clean && tsc",
|
||||
"clean": "rimraf lib",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"lint": "tslint -p tsconfig.json",
|
||||
"prepare": "npm run build",
|
||||
"prepublishOnly": "npm test && npm run lint",
|
||||
"preversion": "npm run lint",
|
||||
"version": "npm run format && git add -A src",
|
||||
"postversion": "git push && git push --tags"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^12.7.12",
|
||||
"form-data": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^24.0.19",
|
||||
"jest": "^24.9.0",
|
||||
"prettier": "^1.18.2",
|
||||
"rimraf": "^3.0.0",
|
||||
"ts-jest": "^24.1.0",
|
||||
"ts-node": "^8.4.1",
|
||||
"tslint": "^5.20.0",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"tslint-config-security": "^1.16.0",
|
||||
"tslint-config-standard-plus": "^2.2.0",
|
||||
"typescript": "^3.6.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
printWidth: 80,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
quoteProps: 'as-needed',
|
||||
jsxSingleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
bracketSpacing: true,
|
||||
jsxBracketSameLine: false,
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as FromData from 'form-data'
|
||||
import { HttpAdapter } from '../http/_Adapter'
|
||||
import { NativeAdapter } from '../http/NativeAdapter'
|
||||
import { Request } from '../request/_Request'
|
||||
|
||||
/**
|
||||
* Helper function to convert Request to FormData
|
||||
* https://github.com/form-data/form-data
|
||||
*/
|
||||
const formdata = (request: Request) => {
|
||||
const data = new FromData()
|
||||
|
||||
// append all form values
|
||||
const values = request.getFormValues()
|
||||
for (const field in values) {
|
||||
if (values.hasOwnProperty(field)) {
|
||||
const value = values[field]
|
||||
if (value !== undefined) {
|
||||
data.append(field, String(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// append all form files
|
||||
const files = request.getFormFiles()
|
||||
for (const filename in files) {
|
||||
if (files.hasOwnProperty(filename)) {
|
||||
const document = files[filename]
|
||||
if (document !== undefined) {
|
||||
data.append(filename, document.getStream(), { filename })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Gotenberg client
|
||||
*/
|
||||
export class Client {
|
||||
constructor(
|
||||
private readonly url: string,
|
||||
private readonly client: HttpAdapter = new NativeAdapter()
|
||||
) {}
|
||||
|
||||
public do(request: Request): Promise<NodeJS.ReadableStream> {
|
||||
return this.client.post(this.url + request.getPostURL(), formdata(request))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { Client } from './Client'
|
||||
@@ -0,0 +1,17 @@
|
||||
import { resolve } from 'path'
|
||||
import { createReadStream } from 'fs'
|
||||
import { GotenbergDocument } from './_GotenbergDocument'
|
||||
import { Document } from './_Document'
|
||||
|
||||
/**
|
||||
* Document from file, for gotenberg conversions
|
||||
*/
|
||||
export class FileDocument extends Document implements GotenbergDocument {
|
||||
constructor(fileName: string, private readonly path: string) {
|
||||
super(fileName)
|
||||
}
|
||||
|
||||
public getStream(): NodeJS.ReadableStream {
|
||||
return createReadStream(resolve(__dirname, this.path)) // FIXME: I guess __dirname will not work as I want
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Readable } from 'stream'
|
||||
import { GotenbergDocument } from './_GotenbergDocument'
|
||||
import { Document } from './_Document'
|
||||
|
||||
/**
|
||||
* Readable stream from string
|
||||
* https://medium.com/@dupski/nodejs-creating-a-readable-stream-from-a-string-e0568597387f
|
||||
*/
|
||||
class ReadableBuffer extends Readable {
|
||||
private sent = false
|
||||
private readonly body: Buffer
|
||||
|
||||
constructor(body: string | Buffer) {
|
||||
super()
|
||||
this.body = body instanceof Buffer ? body : Buffer.from(body)
|
||||
}
|
||||
|
||||
_read() {
|
||||
if (!this.sent) {
|
||||
this.push(this.body)
|
||||
this.sent = true
|
||||
} else {
|
||||
this.push(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Document from string or Buffer, for gotenberg conversions
|
||||
*/
|
||||
export class InplaceDocument<T extends string | Buffer> extends Document
|
||||
implements GotenbergDocument {
|
||||
constructor(fileName: string, private readonly body: T) {
|
||||
super(fileName)
|
||||
}
|
||||
|
||||
public getStream(): NodeJS.ReadableStream {
|
||||
return new ReadableBuffer(this.body)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aliase for Document from string, for gotenberg conversions
|
||||
* Use class/extends instead of type alias to preserve this identifier after compilation
|
||||
*/
|
||||
export class StringDocument extends InplaceDocument<string> {}
|
||||
|
||||
/**
|
||||
* Aliase for Document from Buffer, for gotenberg conversions
|
||||
* Use class/extends instead of type alias to preserve this identifier after compilation
|
||||
*/
|
||||
export class BufferDocument extends InplaceDocument<Buffer> {}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { GotenbergDocument } from './_GotenbergDocument'
|
||||
import { Document } from './_Document'
|
||||
|
||||
/**
|
||||
* Document from stream.Readable, for gotenberg conversions
|
||||
*/
|
||||
export class StreamDocument extends Document implements GotenbergDocument {
|
||||
constructor(
|
||||
fileName: string,
|
||||
private readonly stream: NodeJS.ReadableStream
|
||||
) {
|
||||
super(fileName)
|
||||
}
|
||||
|
||||
public getStream(): NodeJS.ReadableStream {
|
||||
return this.stream
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { GotenbergDocument } from './_GotenbergDocument'
|
||||
|
||||
/**
|
||||
* Abstract Document for gotenberg conversions
|
||||
*/
|
||||
export abstract class Document implements GotenbergDocument {
|
||||
constructor(private readonly fileName: string) {}
|
||||
|
||||
public getFileName(): string {
|
||||
return this.fileName
|
||||
}
|
||||
|
||||
abstract getStream(): NodeJS.ReadableStream
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce function, used in `reduce` header
|
||||
*/
|
||||
const reduceFn = (
|
||||
map: { [filename: string]: Document },
|
||||
document: Document
|
||||
) => {
|
||||
map[document.getFileName()] = document
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to reduce array of documents to map
|
||||
*/
|
||||
export const reduce = (documents: Document[]) =>
|
||||
documents.reduce(reduceFn, Object.create(null))
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface GotenbergDocument {
|
||||
getFileName(): string
|
||||
getStream(): NodeJS.ReadableStream
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export { StreamDocument } from './StreamDocument'
|
||||
export { FileDocument } from './FileDocument'
|
||||
export {
|
||||
BufferDocument,
|
||||
InplaceDocument,
|
||||
StringDocument,
|
||||
} from './InplaceDocument'
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as http from 'http'
|
||||
import * as https from 'https'
|
||||
import * as FormData from 'form-data'
|
||||
import { HttpAdapter } from './_Adapter'
|
||||
|
||||
export class NativeAdapter extends HttpAdapter {
|
||||
public post(
|
||||
url: URL | string,
|
||||
data: FormData
|
||||
): Promise<NodeJS.ReadableStream> {
|
||||
const _url = url instanceof URL ? url : new URL(url)
|
||||
const request = _url.protocol === 'http:' ? http.request : https.request
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = request(_url, {
|
||||
method: 'POST',
|
||||
headers: data.getHeaders(),
|
||||
// auth <string> Basic authentication i.e. 'user:password' to compute an Authorization header.
|
||||
// timeout <number>: A number specifying the socket timeout in milliseconds. This will set the timeout before the socket is connected.
|
||||
})
|
||||
|
||||
req.on('error', reject)
|
||||
req.on('response', res => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res)
|
||||
} else {
|
||||
res.resume() // ignore response body
|
||||
reject(new Error(res.statusCode + ' ' + res.statusMessage))
|
||||
}
|
||||
})
|
||||
|
||||
data.pipe(req) // pipe Form data to request
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as FormData from 'form-data'
|
||||
|
||||
export abstract class HttpAdapter {
|
||||
abstract post(
|
||||
url: URL | string,
|
||||
data: FormData
|
||||
): Promise<NodeJS.ReadableStream>
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// import { Greeter } from './index'
|
||||
//
|
||||
// test('My Greeter', () => {
|
||||
// expect(Greeter('Carl')).toBe('Hello Carl')
|
||||
// })
|
||||
@@ -0,0 +1,35 @@
|
||||
export * from './client'
|
||||
export * from './request'
|
||||
export * from './document'
|
||||
export * from './page'
|
||||
|
||||
// import { Client } from './client'
|
||||
// import { FileDocument } from './document'
|
||||
// import { HTMLRequest } from './request'
|
||||
// import { A4 } from './page'
|
||||
// import { createWriteStream } from 'fs'
|
||||
|
||||
//
|
||||
|
||||
//
|
||||
/*
|
||||
const client = new Client('http://localhost:3000')
|
||||
|
||||
client
|
||||
.do(
|
||||
new HTMLRequest(new FileDocument('index.html', '../../test/index.html'))
|
||||
.margins({
|
||||
top: 0,
|
||||
right: 0.2, // ~5mm
|
||||
bottom: 0,
|
||||
left: 0.2, // ~5mm
|
||||
})
|
||||
.paperSize(A4)
|
||||
)
|
||||
.then(res => {
|
||||
res.pipe(createWriteStream('test/invoice_statement.pdf'))
|
||||
})
|
||||
.catch(e => {
|
||||
console.log('Error:', e)
|
||||
})
|
||||
*/
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
export type PageSize = [number, number] | { width?: number; height?: number }
|
||||
export type Margins =
|
||||
| [number, number, number, number]
|
||||
| { top?: number; right?: number; bottom?: number; left?: number }
|
||||
|
||||
// TODO: more sizes
|
||||
// https://papersizes.io/
|
||||
|
||||
export const A3: PageSize = [11.7, 16.5]
|
||||
export const A4: PageSize = [8.27, 11.7]
|
||||
export const A5: PageSize = [5.8, 8.3]
|
||||
export const A6: PageSize = [4.1, 5.8]
|
||||
export const LETTER: PageSize = [8.5, 11]
|
||||
export const LEGAL: PageSize = [8.5, 14]
|
||||
export const TABLOID: PageSize = [11, 17]
|
||||
|
||||
export const NO_MARGINS: Margins = [0, 0, 0, 0]
|
||||
export const NORMAL_MARGINS: Margins = [1, 1, 1, 1]
|
||||
export const LARGE_MARGINS: Margins = [2, 2, 2, 2]
|
||||
@@ -0,0 +1,43 @@
|
||||
import { FormFiles, GotenbergRequest } from './_GotenbergRequest'
|
||||
import { Document, reduce } from '../document/_Document'
|
||||
import { ChromeRequest } from './_ChromeRequest'
|
||||
|
||||
const INDEX_TEMPLATE = 'index.html'
|
||||
|
||||
/**
|
||||
* Request builder for HTML conversions
|
||||
* https://thecodingmachine.github.io/gotenberg/#html
|
||||
*/
|
||||
export class HTMLRequest extends ChromeRequest implements GotenbergRequest {
|
||||
private _assets: Document[]
|
||||
|
||||
constructor(private readonly index: Document, ...assets: Document[]) {
|
||||
super()
|
||||
this._assets = assets || []
|
||||
}
|
||||
|
||||
//
|
||||
// Methods from GotenbergRequest
|
||||
//
|
||||
|
||||
public getPostURL() {
|
||||
return '/convert/html'
|
||||
}
|
||||
|
||||
public getFormFiles(): FormFiles {
|
||||
return {
|
||||
...super.getFormFiles(),
|
||||
...reduce(this._assets),
|
||||
[INDEX_TEMPLATE]: this.index,
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Setters for private fields
|
||||
//
|
||||
|
||||
public assets(...assets: Document[]): HTMLRequest {
|
||||
this._assets = assets
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { FormFiles, GotenbergRequest } from './_GotenbergRequest'
|
||||
import { Document, reduce } from '../document/_Document'
|
||||
import { HTMLRequest } from './HTMLRequest'
|
||||
|
||||
/**
|
||||
* Request builder for Markdown conversions
|
||||
* https://thecodingmachine.github.io/gotenberg/#markdown
|
||||
*/
|
||||
export class MarkdownRequest extends HTMLRequest implements GotenbergRequest {
|
||||
private readonly _markdowns: Document[]
|
||||
|
||||
constructor(index: Document, ...markdowns: Document[]) {
|
||||
super(index)
|
||||
this._markdowns = markdowns
|
||||
}
|
||||
|
||||
//
|
||||
// Methods from GotenbergRequest
|
||||
//
|
||||
|
||||
public getPostURL() {
|
||||
return '/convert/markdown'
|
||||
}
|
||||
|
||||
public getFormFiles(): FormFiles {
|
||||
return {
|
||||
...super.getFormFiles(),
|
||||
...reduce(this._markdowns),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { FormFiles, GotenbergRequest } from './_GotenbergRequest'
|
||||
import { Request } from './_Request'
|
||||
import { Document, reduce } from '../document/_Document'
|
||||
|
||||
/**
|
||||
* Request builder for merging PDFs
|
||||
* https://thecodingmachine.github.io/gotenberg/#merge
|
||||
*/
|
||||
export class MergeRequest extends Request implements GotenbergRequest {
|
||||
private readonly _files: Document[]
|
||||
|
||||
constructor(...files: Document[]) {
|
||||
super()
|
||||
this._files = files
|
||||
}
|
||||
|
||||
//
|
||||
// Methods from GotenbergRequest
|
||||
//
|
||||
|
||||
public getPostURL(): string {
|
||||
return '/merge'
|
||||
}
|
||||
|
||||
public getFormFiles(): FormFiles {
|
||||
return {
|
||||
...super.getFormFiles(),
|
||||
...reduce(this._files),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { FormFiles, FormValues, GotenbergRequest } from './_GotenbergRequest'
|
||||
import { Request } from './_Request'
|
||||
import { Document, reduce } from '../document/_Document'
|
||||
|
||||
const LANDSCAPE = 'landscape'
|
||||
|
||||
/**
|
||||
* Request builder for Office document conversions
|
||||
* https://thecodingmachine.github.io/gotenberg/#office
|
||||
*/
|
||||
export class OfficeRequest extends Request implements GotenbergRequest {
|
||||
private readonly _files: Document[]
|
||||
private _landscape? = false
|
||||
|
||||
constructor(...files: Document[]) {
|
||||
super()
|
||||
this._files = files || []
|
||||
}
|
||||
|
||||
//
|
||||
// Methods from GotenbergRequest
|
||||
//
|
||||
|
||||
public getPostURL(): string {
|
||||
return '/convert/office'
|
||||
}
|
||||
|
||||
public getFormValues(): FormValues {
|
||||
return {
|
||||
...super.getFormValues(),
|
||||
[LANDSCAPE]: this._landscape,
|
||||
}
|
||||
}
|
||||
|
||||
public getFormFiles(): FormFiles {
|
||||
return {
|
||||
...super.getFormFiles(),
|
||||
...reduce(this._files),
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Setters for private fields
|
||||
//
|
||||
|
||||
public landscape(value: boolean): OfficeRequest {
|
||||
this._landscape = value
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { FormValues, GotenbergRequest } from './_GotenbergRequest'
|
||||
import { Request } from './_Request'
|
||||
|
||||
const REMOTE_URL = 'remoteURL'
|
||||
|
||||
/**
|
||||
* Request builder for remote URL conversions
|
||||
* https://thecodingmachine.github.io/gotenberg/#url
|
||||
*/
|
||||
export class URLRequest extends Request implements GotenbergRequest {
|
||||
constructor(private readonly url: string) {
|
||||
super()
|
||||
}
|
||||
|
||||
//
|
||||
// Methods from GotenbergRequest
|
||||
//
|
||||
|
||||
public getPostURL(): string {
|
||||
return '/convert/url'
|
||||
}
|
||||
|
||||
public getFormValues(): FormValues {
|
||||
return {
|
||||
...super.getFormValues(),
|
||||
[REMOTE_URL]: this.url,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { FormFiles, FormValues, GotenbergRequest } from './_GotenbergRequest'
|
||||
import { Request } from './_Request'
|
||||
import { Document } from '../document/_Document'
|
||||
import { Margins, PageSize } from '../page'
|
||||
|
||||
const HEADER_TEMPLATE = 'header.html'
|
||||
const FOOTER_TEMPLATE = 'footer.html'
|
||||
|
||||
const WAIT_DELAY = 'waitDelay'
|
||||
const PAPER_WIDTH = 'paperWidth'
|
||||
const PAPER_HEIGHT = 'paperHeight'
|
||||
const MARGIN_TOP = 'marginTop'
|
||||
const MARGIN_BOTTOM = 'marginBottom'
|
||||
const MARGIN_LEFT = 'marginLeft'
|
||||
const MARGIN_RIGHT = 'marginRight'
|
||||
const LANDSCAPE = 'landscape'
|
||||
const GOOGLE_CHROME_RPCC_BUFFER_SIZE = 'googleChromeRpccBufferSize'
|
||||
|
||||
/**
|
||||
* Abstract ChromeRequest builder for gotenberg headless chrome (HTML and Markdown) conversions
|
||||
*/
|
||||
export abstract class ChromeRequest extends Request
|
||||
implements GotenbergRequest {
|
||||
private _header?: Document
|
||||
private _footer?: Document
|
||||
private _waitDelay?: number
|
||||
private _paperWidth?: number
|
||||
private _paperHeight?: number
|
||||
private _marginTop?: number
|
||||
private _marginBottom?: number
|
||||
private _marginLeft?: number
|
||||
private _marginRight?: number
|
||||
private _landscape? = false
|
||||
private _googleChromeRpccBufferSize?: number
|
||||
|
||||
//
|
||||
// Methods from GotenbergRequest
|
||||
//
|
||||
|
||||
public getFormValues(): FormValues {
|
||||
return {
|
||||
...super.getFormValues(),
|
||||
[WAIT_DELAY]: this._waitDelay,
|
||||
[PAPER_WIDTH]: this._paperWidth,
|
||||
[PAPER_HEIGHT]: this._paperHeight,
|
||||
[MARGIN_TOP]: this._marginTop,
|
||||
[MARGIN_BOTTOM]: this._marginBottom,
|
||||
[MARGIN_LEFT]: this._marginLeft,
|
||||
[MARGIN_RIGHT]: this._marginRight,
|
||||
[GOOGLE_CHROME_RPCC_BUFFER_SIZE]: this._googleChromeRpccBufferSize,
|
||||
[LANDSCAPE]: this._landscape,
|
||||
}
|
||||
}
|
||||
|
||||
public getFormFiles(): FormFiles {
|
||||
return {
|
||||
...super.getFormFiles(),
|
||||
[HEADER_TEMPLATE]: this._header,
|
||||
[FOOTER_TEMPLATE]: this._footer,
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Setters for private fields
|
||||
//
|
||||
|
||||
public header(header: Document): ChromeRequest {
|
||||
this._header = header
|
||||
return this
|
||||
}
|
||||
|
||||
public footer(footer: Document): ChromeRequest {
|
||||
this._footer = footer
|
||||
return this
|
||||
}
|
||||
|
||||
public waitDelay(value: number): ChromeRequest {
|
||||
this._waitDelay = value
|
||||
return this
|
||||
}
|
||||
|
||||
public paperSize(size: PageSize): ChromeRequest {
|
||||
if (Array.isArray(size)) {
|
||||
;[this._paperWidth, this._paperHeight] = size
|
||||
} else {
|
||||
;({ width: this._paperWidth, height: this._paperHeight } = size)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
public paperWidth(value: number): ChromeRequest {
|
||||
this._paperWidth = value
|
||||
return this
|
||||
}
|
||||
|
||||
public paperHeight(value: number): ChromeRequest {
|
||||
this._paperHeight = value
|
||||
return this
|
||||
}
|
||||
|
||||
public margins(margins: Margins): ChromeRequest {
|
||||
if (Array.isArray(margins)) {
|
||||
;[
|
||||
this._marginTop,
|
||||
this._marginRight,
|
||||
this._marginBottom,
|
||||
this._marginLeft,
|
||||
] = margins
|
||||
} else {
|
||||
;({
|
||||
top: this._marginTop,
|
||||
right: this._marginRight,
|
||||
bottom: this._marginBottom,
|
||||
left: this._marginLeft,
|
||||
} = margins)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
public marginTop(value: number): ChromeRequest {
|
||||
this._marginTop = value
|
||||
return this
|
||||
}
|
||||
|
||||
public marginRight(value: number): ChromeRequest {
|
||||
this._marginRight = value
|
||||
return this
|
||||
}
|
||||
|
||||
public marginBottom(value: number): ChromeRequest {
|
||||
this._marginBottom = value
|
||||
return this
|
||||
}
|
||||
|
||||
public marginLeft(value: number): ChromeRequest {
|
||||
this._marginLeft = value
|
||||
return this
|
||||
}
|
||||
|
||||
public landscape(value: boolean): ChromeRequest {
|
||||
this._landscape = value
|
||||
return this
|
||||
}
|
||||
|
||||
public googleChromeRpccBufferSize(value: number): ChromeRequest {
|
||||
this._googleChromeRpccBufferSize = value
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Document } from '../document/_Document'
|
||||
|
||||
export type FormValues = {
|
||||
[field: string]: number | string | boolean | undefined
|
||||
}
|
||||
|
||||
export type FormFiles = {
|
||||
[field: string]: Document | undefined
|
||||
}
|
||||
|
||||
export interface GotenbergRequest {
|
||||
getPostURL(): string
|
||||
getFormValues(): FormValues | void
|
||||
getFormFiles(): FormFiles | void
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { FormFiles, FormValues, GotenbergRequest } from './_GotenbergRequest'
|
||||
|
||||
const RESULT_FILENAME = 'resultFilename'
|
||||
const WAIT_TIMEOUT = 'waitTimeout'
|
||||
const WEBHOOK_URL = 'webhookURL'
|
||||
const WEBHOOK_URL_TIMEOUT = 'webhookURLTimeout'
|
||||
|
||||
/**
|
||||
* Abstract Request builder for gotenberg conversions
|
||||
*/
|
||||
export abstract class Request implements GotenbergRequest {
|
||||
private _resultFilename?: string
|
||||
private _waitTimeout?: number
|
||||
private _webhookURL?: string
|
||||
private _webhookURLTimeout?: number
|
||||
|
||||
//
|
||||
// Methods from GotenbergRequest
|
||||
//
|
||||
|
||||
abstract getPostURL(): string
|
||||
|
||||
public getFormValues(): FormValues {
|
||||
return {
|
||||
[RESULT_FILENAME]: this._resultFilename,
|
||||
[WAIT_TIMEOUT]: this._waitTimeout,
|
||||
[WEBHOOK_URL]: this._webhookURL,
|
||||
[WEBHOOK_URL_TIMEOUT]: this._webhookURLTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
public getFormFiles(): FormFiles {
|
||||
return {}
|
||||
}
|
||||
|
||||
//
|
||||
// Setters for private fields
|
||||
//
|
||||
|
||||
public resultFilename(value: string): Request {
|
||||
this._resultFilename = value
|
||||
return this
|
||||
}
|
||||
|
||||
public waitTimeout(value: number): Request {
|
||||
this._waitTimeout = value
|
||||
return this
|
||||
}
|
||||
|
||||
public webhookURL(value: string): Request {
|
||||
this._webhookURL = value
|
||||
return this
|
||||
}
|
||||
|
||||
public webhookURLTimeout(value: number): Request {
|
||||
this._webhookURLTimeout = value
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { HTMLRequest } from './HTMLRequest'
|
||||
export { MarkdownRequest } from './MarkdownRequest'
|
||||
export { MergeRequest } from './MergeRequest'
|
||||
export { OfficeRequest } from './OfficeRequest'
|
||||
export { URLRequest } from './URLRequest'
|
||||
+406
@@ -0,0 +1,406 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Invoice Statement</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gray {
|
||||
color: #707070;
|
||||
}
|
||||
|
||||
.noBorder {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.noPadding {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.alignTop {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 80px;
|
||||
padding: 0 40px;
|
||||
border-bottom: 1px solid #707070;
|
||||
}
|
||||
|
||||
.logo .setplexlogo {
|
||||
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI1MCA1OSIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMjUwIDU5IiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxnIGNsaXAtcnVsZT0iZXZlbm9kZCIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJtMjE2LjI5IDM2LjQ0M2wtNy4xNDUgNS4zOTNjLTAuMzYzIDAuMzUzLTAuODA4IDAuNTM2LTEuMzE3IDAuNTM2LTAuNjYzIDAtMS4xOTctMC4zMjItMS42MDgtMC44My0wLjQwNS0wLjUwMi0wLjYzOS0xLjA3OS0wLjYzOS0xLjcyNyAwLTAuNjM5IDAuMzE5LTEuMTUgMC44MTQtMS41NDYgMi42NjEtMi4wNjEgNS40MjUtNC4wMTQgNy45NzktNi4yMTMgMi4xNjgtMS44NjggMi40MDYtMi4xNzEtMWUtMyAtMy43MjRsLTcuOTc1LTYuMDc5Yy0wLjYwMi0wLjQxNC0wLjk5Ni0wLjk1MS0wLjk5Ni0xLjY5OCAwLTAuNjcyIDAuMjI3LTEuMjY1IDAuNzIyLTEuNzI0IDAuNDY1LTAuNDMyIDEuMDI0LTAuNjU3IDEuNjU4LTAuNjU3IDAuMjQ4IDAgMC41MDcgMC4wNDQgMC43NDkgMC4wOTggMC4zMTcgMC4wNzEgMC42MDIgMC4yMTMgMC44MzUgMC40MzlsNi4xMiA0LjY4NC0wLjAxNCAwLjAxOWMxLjY5OSAxLjI5MSAzLjM5OSAyLjU4MiA1LjA5OCAzLjg3MyAwLjAxOCAwLjAxMyAwLjAzNiAwLjAyNyAwLjA1NCAwLjA0MSAzLjIzMiAyLjE4IDIuNDE5IDQuMTE4LTAuMDY1IDUuOTU1LTEuNDM2IDEuMDYyLTIuODQ2IDIuMTM0LTQuMjY5IDMuMTZ6IiBmaWxsPSIjMDA3MEJBIi8+PHBhdGggZD0ibTIyNy4zOCAzMi40MzFsNy41NzIgNS44NDFjMC40OSAwLjM5MiAwLjgxIDAuOTA0IDAuODEgMS41NDIgMCAwLjY2My0wLjIyNCAxLjI1Ni0wLjYzOSAxLjc3MS0wLjQxMSAwLjUwOS0wLjk0NSAwLjgzMS0xLjYwOSAwLjgzMS0wLjQ5MSAwLTAuOTM4LTAuMjY0LTEuMzA0LTAuNTcxbC04LjY3Mi02LjQ5NiAzLjg0Mi0yLjkxOHoiIGZpbGw9IiMxQjFCMUIiLz48cGF0aCBkPSJtMjIzLjUyIDI1LjExOGw4LjQxLTYuNDEyYzAuNDQ1LTAuNDA1IDAuOTc1LTAuNjIyIDEuNTc4LTAuNjIyIDAuNjkzIDAgMS4yODggMC4yNTUgMS43NSAwLjc3MSAwLjQzMiAwLjQ4MiAwLjY3NSAxLjA0OSAwLjY3NSAxLjY5OHYwLjAyM2wtM2UtMyAwLjAyNGMtMC4wMzcgMC4yOTItMC4xMjggMC41NzItMC4yNiAwLjgzNC0wLjE2NiAwLjMzLTAuNDM5IDAuNjA5LTAuNzMyIDAuODQ0bC03LjU4IDUuNzU0LTMuODM4LTIuOTE0eiIgZmlsbD0iIzFCMUIxQiIvPjxwYXRoIGQ9Im0xNzEuODMgMzIuNjMxdjIuNzU0aDAuMDE2YzAuMjggMi4zNTEgNC4zMTIgMi40NDcgNi4wMTUgMi40NDdoMTguMjA4YzEuMzczIDAgMi4xNTkgMC43MjYgMi4xNTkgMi4xMTUgMCAxLjM5Ni0wLjc2MyAyLjE1OS0yLjE1OSAyLjE1OWgtMTcuMTAxYy01LjEyOCAwLTEyLjA3OC0xLTEyLjA3OC03LjUxN3YtOC44NTVjMC02LjQ0MiA2Ljg3OC03LjQyOCAxMS45NDQtNy40MjhoOS40MzFjNC40NzUgMCAxMC4xMjkgMC45NjIgMTAuMTI5IDYuNTQzdjUuNDljMCAxLjQ4NC0wLjgwNyAyLjI5Mi0yLjI5MiAyLjI5MmgtMjEuNDc5di00LjM2M2gxOC45MjF2LTMuMDIyYzAtMi40MDEtMi41NS0yLjcxMS00LjQzOC0yLjcxMWgtMTEuMjQ3Yy0xLjk0NCAwLTYuMDMyIDAuMTU3LTYuMDMyIDIuOTMzdjcuMTYzaDNlLTN6IiBmaWxsPSIjMUIxQjFCIi8+PHBhdGggZD0ibTE1Ny44IDQwLjA4YzAgMS41NTgtMC45ODIgMi4yNDctMi40NjkgMi4yNDctMS40OTIgMC0yLjUxMy0wLjY3LTIuNTEzLTIuMjQ3di0yNi44MzJjMC0xLjU1OCAwLjk4Mi0yLjI0OCAyLjQ2OS0yLjI0OCAxLjQ5MiAwIDIuNTEzIDAuNjcgMi41MTMgMi4yNDh2MjYuODMyeiIgZmlsbD0iIzFCMUIxQiIvPjxwYXRoIGQ9Im0xMTcuMjEgNDIuMTA3djQuNzkyYzAgMS41OTctMC45NzIgMi4zMzYtMi41MTMgMi4zMzYtMS41MjggMC0yLjQyNS0wLjc4LTIuNDI1LTIuMzM2di0yMS41NjNjMC02LjExOCA2LjYyNS03LjAzIDExLjQxNC03LjAzaDguMDU4YzQuNjkzIDAgMTEuOTg5IDAuNzU0IDExLjk4OSA2Ljk0MXY5LjUyYzAgNi4xMzItNS43OTcgNy4zNC0xMC44MzggNy4zNGgtMTIuODg5di00LjI3NGgxMy41OThjMi4wNTMgMCA1LjEwMy0wLjMwMSA1LjEwMy0yLjk3OHYtOS41MTljMC0yLjctNC40OC0yLjgtNi4yOTgtMi44aC05LjIxYy0xLjk0NyAwLTUuOTg4IDAuMTYyLTUuOTg4IDIuOTMzdjE2LjYzOGgtMWUtM3oiIGZpbGw9IiMxQjFCMUIiLz48cGF0aCBkPSJtNTQuMjU3IDMyLjYzMXYyLjQ5YzAgMi42MDkgNC4yNjcgMi43MTEgNi4wMzIgMi43MTFoMTguMjA4YzEuMzc0IDAgMi4xNTkgMC43MjYgMi4xNTkgMi4xMTUgMCAxLjM5Ni0wLjc2MyAyLjE1OS0yLjE1OSAyLjE1OWgtMTcuMWMtNS4xMjggMC0xMi4wNzctMS0xMi4wNzctNy41MTd2LTguODU1YzAtNi40NDIgNi44NzctNy40MjggMTEuOTQ0LTcuNDI4aDkuNDMxYzQuNDc1IDAgMTAuMTI5IDAuOTYyIDEwLjEyOSA2LjU0M3Y1LjQ5YzAgMS40ODQtMC44MDcgMi4yOTItMi4yOTIgMi4yOTJoLTIxLjQ4di00LjM2M2gxOC45MjJ2LTMuMDIyYzAtMi40MDEtMi41NS0yLjcxMS00LjQzOC0yLjcxMWgtMTEuMjQ3Yy0xLjk0NCAwLTYuMDMyIDAuMTU3LTYuMDMyIDIuOTMzdjcuMTYzeiIgZmlsbD0iIzFCMUIxQiIvPjxwYXRoIGQ9Im05MS4xMSAzMy41MzhsMWUtMyAtMTEuMDAzaC0xLjY0OWMtMS4zOTcgMC0yLjE1OS0wLjc2My0yLjE1OS0yLjE1OSAwLTEuMzggMC44MDgtMi4wNyAyLjE1OS0yLjA3aDEuNjQ5di01LjA1OGMwLTEuNTk2IDEuMDYyLTIuMjQ4IDIuNTU4LTIuMjQ4IDEuNDc2IDAgMi4zOCAwLjcyOSAyLjM4IDIuMjQ4djUuMDU4aDcuOTgxYzEuMzY3IDAgMi4xMTUgMC43NDggMi4xMTUgMi4xMTUgMCAxLjM2Ni0wLjc0OCAyLjExNS0yLjExNSAyLjExNWgtNy45ODF2MTIuNTg2YzAgMi42MDkgNC4yNjcgMi43MTEgNi4wMzIgMi43MTFoMS40NjFjMS4zNzQgMCAyLjE1OSAwLjcyNiAyLjE1OSAyLjExNSAwIDEuMzk2LTAuNzYzIDIuMTU5LTIuMTU5IDIuMTU5aC0wLjM1NGMtNS4xMjggMC0xMi4wNzgtMS0xMi4wNzgtNy41MTd2LTEuMDUyeiIgZmlsbD0iIzFCMUIxQiIvPjxwYXRoIGQ9Im00MS4yMDMgMzUuNjA4YzAgNS4zMDMtNC42ODYgNi40OTktOS4xNTQgNi40OTloLTE3LjhjLTEuMzYzIDAtMi4yNDgtMC42NTEtMi4yNDgtMi4wNzEgMC0xLjQ0NyAwLjgxNS0yLjIwMyAyLjI0OC0yLjIwM2gxOC41MDhjMS41NzMgMCAzLjM3NS0wLjMwNiAzLjM3NS0yLjIyNHYtMS4zNzNjMC0xLjk4Ny0yLjM2MS0yLjE3OS0zLjg2Mi0yLjE3OWgtOS44NzRjLTQuMjQxIDAtMTAuMTI5LTAuODA5LTEwLjEyOS02LjIzM3YtMS41MDZjMC01LjIwMyA1LjU0Ni02LjAxMSA5LjY0Mi02LjAxMWgxNi40NzFjMS4zOCAwIDIuMjAzIDAuNzA1IDIuMjAzIDIuMTE1IDAgMS40MDktMC44MjQgMi4xMTUtMi4yMDMgMi4xMTVoLTE3LjQ0NmMtMS4xOCAwLTMuNzMgMC4wODUtMy43MyAxLjc4MXYxLjQxOGMwIDIuMDMzIDMuMDcxIDIuMTM1IDQuNDgyIDIuMTM1aDEwLjA5NWM0LjMyNiAwIDkuNDIxIDEuMDI1IDkuNDIxIDYuMzY1djEuMzcyaDFlLTN6IiBmaWxsPSIjMUIxQjFCIi8+PC9nPjwvc3ZnPg==);
|
||||
background-repeat: no-repeat;
|
||||
width: 170px;
|
||||
height: 35px;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
|
||||
.contactsHeader {
|
||||
float: right;
|
||||
font-size: 14px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.contactsHeader p {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.tableInvoices:before {
|
||||
content: 'PAID';
|
||||
position: absolute;
|
||||
font-size: 110px;
|
||||
letter-spacing: 15px;
|
||||
font-weight: bold;
|
||||
color: #daf1ce;
|
||||
top: 150px;
|
||||
left: 50%;
|
||||
z-index: -1;
|
||||
transform: translateX(-50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
.invoiceData {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.invoiceData table tr td {
|
||||
padding-right: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.invoiceData tr td:first-child {
|
||||
color: #707070;
|
||||
}
|
||||
|
||||
.innerDiv {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.companyData {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.companyData h4 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.companyData p {
|
||||
font-size: 14px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.companyData p:nth-child(4) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tableInvoices {
|
||||
position: relative;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.tableInvoices td {
|
||||
padding: 20px 10px;
|
||||
border: 1px solid #707070;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.tableInvoices th {
|
||||
padding: 10px;
|
||||
border: 1px solid #707070;
|
||||
background-color: #e4f9ed;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.tableInvoices tr:first-child th {
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
/*remove border from first td from pre-last tr*/
|
||||
.tableInvoices tr:nth-last-child(2) td:first-child {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.tableInvoices tr:nth-last-child(2) td:first-child p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tableInvoices tr:nth-last-child(2) h4 {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.tableInvoices tr:nth-last-child(2) p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.dashed p {
|
||||
padding: 15px 10px;
|
||||
margin: 0 !important;
|
||||
border-bottom: 1px dashed #707070;
|
||||
}
|
||||
|
||||
.dashed p:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.innerTable {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.innerTable tr:first-child th {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.innerTable tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.innerTable tr th:first-child, .innerTable tr td:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.innerTable tr th:last-child, .innerTable tr td:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.innerTable tr:first-child td {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.haveAnyQuestions {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
height: 141px;
|
||||
padding: 0 0 0 40px;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid #707070;
|
||||
}
|
||||
|
||||
.contactsWrap {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.contactsWrap div {
|
||||
float: left;
|
||||
margin: 30px 10px;
|
||||
}
|
||||
|
||||
.contactsWrap div h4, .contactsWrap div p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.contactsWrap div p {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.header:before, .footer:before {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.verticAlign {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.gorizontAlign {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lowercase {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
@media print {
|
||||
@page {
|
||||
size: A4;
|
||||
}
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
.main {
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
.gray {
|
||||
color: #707070;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
.invoiceData tr td:first-child {
|
||||
color: #707070;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
|
||||
.tableInvoices th {
|
||||
background-color: #e4f9ed;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.footer .logo {
|
||||
margin-right: 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="logo verticAlign">
|
||||
<div class="setplexlogo"></div>
|
||||
</div>
|
||||
<div class="contactsHeader">
|
||||
<p>sales@setplex.com<br/>
|
||||
tel.:+1-855-738-7539</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="invoiceData">
|
||||
<div class="innerDiv">
|
||||
<h2>INVOICE</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<td>INVOICE NUMBER</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>INVOICE DATE</td>
|
||||
<td>17.07.2019</td>
|
||||
</tr>
|
||||
<!-- <tr>
|
||||
<td>PAGE</td>
|
||||
<td>1 FROM 1</td>
|
||||
</tr> -->
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="companyData">
|
||||
<h2>BILL TO</h2>
|
||||
<h4>IIvanov</h4>
|
||||
<p>Ivan Ivanov</p>
|
||||
<p class="gray">Ohio / Columbus</p>
|
||||
<p class="gray">926 Steensland Trail / United States of America</p>
|
||||
<p class="gray"><b>ivan.ivanov99@gmail.com</b></p>
|
||||
</div>
|
||||
<div class="tableInvoicesWrap">
|
||||
<table class="tableInvoices gray">
|
||||
<tr>
|
||||
<th>№</th>
|
||||
<th>PROCESS DATE</th>
|
||||
<td rowspan="2" class="noPadding alignTop">
|
||||
<table class="innerTable">
|
||||
<tr>
|
||||
<th>SERVICE</th>
|
||||
<th>AMOUNT</th>
|
||||
<th>PRICE, USD</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Nora & Apps</td>
|
||||
<td class="gorizontAlign">100 seats</td>
|
||||
<td class="gorizontAlign">100</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CDN Streaming</td>
|
||||
<td class="gorizontAlign">100 seats</td>
|
||||
<td class="gorizontAlign">250</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VOD Storage</td>
|
||||
<td class="gorizontAlign">150 GBs</td>
|
||||
<td class="gorizontAlign">30</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Catchup Channels 7D</td>
|
||||
<td class="gorizontAlign">10 channels</td>
|
||||
<td class="gorizontAlign">350</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<th>TOTAL AMOUNT, USD</th>
|
||||
<th>PAYMENT TYPE</th>
|
||||
<th>TRANSACTION ID</th>
|
||||
<th>STATUS</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="gorizontAlign">1</td>
|
||||
<td class="gorizontAlign">17.07.2019</td>
|
||||
<td class="gorizontAlign">730</td>
|
||||
<td class="gorizontAlign">
|
||||
Credit Card - XXXX7654
|
||||
|
||||
</td>
|
||||
<td class="gorizontAlign">60124519509</td>
|
||||
<td class="gorizontAlign">PAID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2" colspan="5" class="noBorder">
|
||||
<h4>COMMENTS</h4>
|
||||
<p>1. Purpose of this invoice is <b class="lowercase">REGISTRATION</b> of services</p>
|
||||
<p></p>
|
||||
</td>
|
||||
<td class="bold noPadding dashed"><p>SUBTOTAL</p>
|
||||
<p>TAX RATE</p>
|
||||
<p>TAX</p></td>
|
||||
<td class="gorizontAlign bold noPadding dashed"><p>730</p>
|
||||
<p>0%</p>
|
||||
<p>0</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>TOTAL, USD</th>
|
||||
<th>730</th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="haveAnyQuestions gray">
|
||||
If you have any questions about this invoice, please contact us<br/>
|
||||
tel.: +1-855-738-7539<br/>
|
||||
sales@setplex.com
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="logo verticAlign">
|
||||
<div class="setplexlogo"></div>
|
||||
</div>
|
||||
<div class="contactsWrap verticAlign">
|
||||
<div>
|
||||
<h4>ADDRESS</h4>
|
||||
<p>Setplex LLC<br/>
|
||||
231 Central Ave 4th Floor<br/>
|
||||
White Plains, NY 10606</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4>CONTACTS</h4>
|
||||
<p>sales@setplex.com<br/>
|
||||
tel.: +1-855-738-7539<br/>
|
||||
fax.: +1-718-701-4407</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"removeComments": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"outDir": "./lib",
|
||||
"baseUrl": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": [
|
||||
"tslint-config-standard-plus",
|
||||
"tslint-config-security",
|
||||
"tslint-config-prettier"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user