Création d'une page tableau en lecture seule et possibilité d'exporter en PDF via wkhtmltopdf
This commit is contained in:
parent
3c21412344
commit
6ebe4c90d7
|
@ -1,13 +1,13 @@
|
|||
declare namespace StyleModuleCssModule {
|
||||
declare namespace StyleModuleCssNamespace {
|
||||
export interface IStyleModuleCss {
|
||||
editIcon: string;
|
||||
editableText: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare const StyleModuleCssModule: StyleModuleCssModule.IStyleModuleCss & {
|
||||
declare const StyleModuleCssModule: StyleModuleCssNamespace.IStyleModuleCss & {
|
||||
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
|
||||
locals: StyleModuleCssModule.IStyleModuleCss;
|
||||
locals: StyleModuleCssNamespace.IStyleModuleCss;
|
||||
};
|
||||
|
||||
export = StyleModuleCssModule;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
declare namespace StyleModuleCssModule {
|
||||
declare namespace StyleModuleCssNamespace {
|
||||
export interface IStyleModuleCss {
|
||||
footer: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare const StyleModuleCssModule: StyleModuleCssModule.IStyleModuleCss & {
|
||||
declare const StyleModuleCssModule: StyleModuleCssNamespace.IStyleModuleCss & {
|
||||
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
|
||||
locals: StyleModuleCssModule.IStyleModuleCss;
|
||||
locals: StyleModuleCssNamespace.IStyleModuleCss;
|
||||
};
|
||||
|
||||
export = StyleModuleCssModule;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
declare namespace StyleModuleCssModule {
|
||||
declare namespace StyleModuleCssNamespace {
|
||||
export interface IStyleModuleCss {
|
||||
header: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare const StyleModuleCssModule: StyleModuleCssModule.IStyleModuleCss & {
|
||||
declare const StyleModuleCssModule: StyleModuleCssNamespace.IStyleModuleCss & {
|
||||
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
|
||||
locals: StyleModuleCssModule.IStyleModuleCss;
|
||||
locals: StyleModuleCssNamespace.IStyleModuleCss;
|
||||
};
|
||||
|
||||
export = StyleModuleCssModule;
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
declare namespace StyleModuleCssModule {
|
||||
declare namespace StyleModuleCssNamespace {
|
||||
export interface IStyleModuleCss {
|
||||
tabContent: string;
|
||||
tabs: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare const StyleModuleCssModule: StyleModuleCssModule.IStyleModuleCss & {
|
||||
declare const StyleModuleCssModule: StyleModuleCssNamespace.IStyleModuleCss & {
|
||||
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
|
||||
locals: StyleModuleCssModule.IStyleModuleCss;
|
||||
locals: StyleModuleCssNamespace.IStyleModuleCss;
|
||||
};
|
||||
|
||||
export = StyleModuleCssModule;
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
declare namespace StyleModuleCssModule {
|
||||
declare namespace StyleModuleCssNamespace {
|
||||
export interface IStyleModuleCss {
|
||||
home: string;
|
||||
noProjects: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare const StyleModuleCssModule: StyleModuleCssModule.IStyleModuleCss & {
|
||||
declare const StyleModuleCssModule: StyleModuleCssNamespace.IStyleModuleCss & {
|
||||
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
|
||||
locals: StyleModuleCssModule.IStyleModuleCss;
|
||||
locals: StyleModuleCssNamespace.IStyleModuleCss;
|
||||
};
|
||||
|
||||
export = StyleModuleCssModule;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
declare namespace StyleModuleCssModule {
|
||||
declare namespace StyleModuleCssNamespace {
|
||||
export interface IStyleModuleCss {
|
||||
notFound: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare const StyleModuleCssModule: StyleModuleCssModule.IStyleModuleCss & {
|
||||
declare const StyleModuleCssModule: StyleModuleCssNamespace.IStyleModuleCss & {
|
||||
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
|
||||
locals: StyleModuleCssModule.IStyleModuleCss;
|
||||
locals: StyleModuleCssNamespace.IStyleModuleCss;
|
||||
};
|
||||
|
||||
export = StyleModuleCssModule;
|
||||
|
|
|
@ -8,6 +8,8 @@ import { useProjectReducer, addTask, updateTaskEstimation, removeTask, updateTas
|
|||
import { Task, TaskID, EstimationConfidence } from "../../models/task";
|
||||
import TimePreview from "../project/time-preview";
|
||||
import RepartitionPreview from "../project/repartition-preview";
|
||||
import FinancialPreview from "../project/financial-preview";
|
||||
import { getHideFinancialPreviewOnPrint } from "../../models/params";
|
||||
|
||||
export interface PdfProps {
|
||||
projectId: string
|
||||
|
@ -20,14 +22,21 @@ const Pdf: FunctionalComponent<PdfProps> = ({ projectId }) => {
|
|||
|
||||
return (
|
||||
<div class={`container ${style.pdf}`}>
|
||||
<div class="column is-9">
|
||||
<TaskTable
|
||||
project={project}
|
||||
readonly={true} />
|
||||
<div class="columns">
|
||||
<div class="column is-9">
|
||||
<TaskTable
|
||||
project={project}
|
||||
readonly={true} />
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<TimePreview project={project} />
|
||||
<RepartitionPreview project={project} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<TimePreview project={project} />
|
||||
<RepartitionPreview project={project} />
|
||||
<div class="columns">
|
||||
<div class={`column ${getHideFinancialPreviewOnPrint(project) ? 'noPrint': ''}`}>
|
||||
<FinancialPreview project={project} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
declare namespace StyleModuleCssModule {
|
||||
declare namespace StyleModuleCssNamespace {
|
||||
export interface IStyleModuleCss {
|
||||
estimation: string;
|
||||
mainColumn: string;
|
||||
|
@ -9,9 +9,9 @@ declare namespace StyleModuleCssModule {
|
|||
}
|
||||
}
|
||||
|
||||
declare const StyleModuleCssModule: StyleModuleCssModule.IStyleModuleCss & {
|
||||
declare const StyleModuleCssModule: StyleModuleCssNamespace.IStyleModuleCss & {
|
||||
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
|
||||
locals: StyleModuleCssModule.IStyleModuleCss;
|
||||
locals: StyleModuleCssNamespace.IStyleModuleCss;
|
||||
};
|
||||
|
||||
export = StyleModuleCssModule;
|
||||
|
|
|
@ -91,7 +91,10 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
<table class={`table is-bordered is-striped is-hoverable is-fullwidth ${style.middleTable}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
{
|
||||
readonly ? '' :
|
||||
<th class={`${style.noBorder} noPrint`} rowSpan={2}></th>
|
||||
}
|
||||
<th class={style.mainColumn} rowSpan={2}>Tâche</th>
|
||||
<th rowSpan={2}>Catégorie</th>
|
||||
<th colSpan={3}>Estimation (en <ProjectTimeUnit project={project} />)</th>
|
||||
|
@ -109,6 +112,8 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
const categoryLabel = category ? category.label : '???';
|
||||
return (
|
||||
<tr key={`taks-${t.id}`}>
|
||||
{
|
||||
readonly ? '' :
|
||||
<td class={`is-narrow noPrint`}>
|
||||
<button
|
||||
onClick={onTaskRemoveClick.bind(null, t.id)}
|
||||
|
@ -116,6 +121,7 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
🗑️
|
||||
</button>
|
||||
</td>
|
||||
}
|
||||
<td class={style.mainColumn}>
|
||||
<EditableText
|
||||
render={(value) => (<span>{value}</span>)}
|
||||
|
@ -166,7 +172,9 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
<tfoot>
|
||||
<tr>
|
||||
<td class={`${style.noBorder} noPrint`}></td>
|
||||
<td colSpan={2} class={readonly ? style.noBorder : ''}>
|
||||
<td colSpan={readonly ? 1 : 2} class={readonly ? style.noBorder : ''}>
|
||||
{
|
||||
readonly ? '' :
|
||||
<div class="field has-addons noPrint">
|
||||
<p class="control is-expanded">
|
||||
<input class="input" type="text" placeholder="Nouvelle tâche"
|
||||
|
@ -191,6 +199,7 @@ const TaskTable: FunctionalComponent<TaskTableProps> = ({ project, onTaskAdd, on
|
|||
</a>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
<th colSpan={3}>Total</th>
|
||||
</tr>
|
||||
|
|
|
@ -27,6 +27,9 @@ module.exports = {
|
|||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8081',
|
||||
},
|
||||
'/export': {
|
||||
target: 'http://127.0.0.1:8081',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -3,6 +3,7 @@ module forge.cadoles.com/wpetit/guesstimate
|
|||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/SebastiaanKlippert/go-wkhtmltopdf v1.5.0
|
||||
github.com/asdine/storm/v3 v3.1.1
|
||||
github.com/caarlos0/env/v6 v6.2.1
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
|
|
|
@ -19,6 +19,8 @@ github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
|
|||
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||
github.com/SebastiaanKlippert/go-wkhtmltopdf v1.5.0 h1:KNMkSqhko6c7eVncl/laVCS95jRryULVhVwwL0VynnU=
|
||||
github.com/SebastiaanKlippert/go-wkhtmltopdf v1.5.0/go.mod h1:zRBvVJtIfhaWvQ9lPIkXrtona4qqSmjZ1HfKvq4dQzI=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
|
|
|
@ -10,16 +10,27 @@ import (
|
|||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Config is the configuration struct
|
||||
type Config struct {
|
||||
HTTP HTTPConfig `yaml:"http"`
|
||||
Data DataConfig `ymal:"data"`
|
||||
HTTP HTTPConfig `yaml:"http"`
|
||||
Client ClientConfig `yaml:"client"`
|
||||
Data DataConfig `ymal:"data"`
|
||||
}
|
||||
|
||||
// HTTPConfig is the configuration part which defines HTTP related params
|
||||
type HTTPConfig struct {
|
||||
BaseURL string `yaml:"baseurl" env:"GUESSTIMATE_BASE_URL"`
|
||||
Address string `yaml:"address" env:"GUESSTIMATE_HTTP_ADDRESS"`
|
||||
PublicDir string `yaml:"publicDir" env:"GUESSTIMATE_PUBLIC_DIR"`
|
||||
}
|
||||
|
||||
// ClientConfig is the configuration part which defines the client app related params
|
||||
type ClientConfig struct {
|
||||
BaseURL string `yaml:"baseurl" env:"GUESSTIMATE_CLIENT_BASE_URL"`
|
||||
Address string `yaml:"address" env:"GUESSTIMATE_CLIENT_HTTP_ADDRESS"`
|
||||
}
|
||||
|
||||
// DataConfig is the configuration part which defines data related params
|
||||
type DataConfig struct {
|
||||
Path string `yaml:"path" env:"GUESSTIMATE_DATA_PATH"`
|
||||
}
|
||||
|
@ -40,6 +51,7 @@ func NewFromFile(filepath string) (*Config, error) {
|
|||
return config, nil
|
||||
}
|
||||
|
||||
// WithEnvironment retrieves the configuration from env vars
|
||||
func WithEnvironment(conf *Config) error {
|
||||
if err := env.Parse(conf); err != nil {
|
||||
return err
|
||||
|
@ -48,23 +60,31 @@ func WithEnvironment(conf *Config) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// NewDumpDefault retrieves the default configuration
|
||||
func NewDumpDefault() *Config {
|
||||
config := NewDefault()
|
||||
return config
|
||||
}
|
||||
|
||||
// NewDefault creates and returns a new default configuration
|
||||
func NewDefault() *Config {
|
||||
return &Config{
|
||||
HTTP: HTTPConfig{
|
||||
BaseURL: "localhost",
|
||||
Address: ":8081",
|
||||
PublicDir: "public",
|
||||
},
|
||||
Client: ClientConfig{
|
||||
BaseURL: "localhost",
|
||||
Address: ":8080",
|
||||
},
|
||||
Data: DataConfig{
|
||||
Path: "guesstimate.db",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Dump writes a given config to a config file
|
||||
func Dump(config *Config, w io.Writer) error {
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
|
|
|
@ -2,6 +2,7 @@ package config
|
|||
|
||||
import "gitlab.com/wpetit/goweb/service"
|
||||
|
||||
// ServiceProvider returns the current config service
|
||||
func ServiceProvider(config *Config) service.Provider {
|
||||
return func(ctn *service.Container) (interface{}, error) {
|
||||
return config, nil
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
// ServiceName defines the project's service
|
||||
const ServiceName service.Name = "config"
|
||||
|
||||
// From retrieves the config service in the given container
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"gitlab.com/wpetit/goweb/static"
|
||||
)
|
||||
|
||||
// Mount endoints for server app
|
||||
func Mount(r *chi.Mux, config *config.Config) error {
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
r.Get("/projects/{projectID}", handleGetProject)
|
||||
|
@ -17,6 +18,10 @@ func Mount(r *chi.Mux, config *config.Config) error {
|
|||
r.Delete("/projects/{projectID}", handleDeleteProject)
|
||||
})
|
||||
|
||||
r.Route("/export", func(r chi.Router) {
|
||||
r.Get("/projects/{projectID}", handleExportProject)
|
||||
})
|
||||
|
||||
clientIndex := path.Join(config.HTTP.PublicDir, "index.html")
|
||||
|
||||
serveClientIndex := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -24,6 +29,7 @@ func Mount(r *chi.Mux, config *config.Config) error {
|
|||
}
|
||||
|
||||
r.Get("/p/*", serveClientIndex)
|
||||
r.Get("/pdf/*", serveClientIndex)
|
||||
|
||||
notFoundHandler := r.NotFoundHandler()
|
||||
r.Get("/*", static.Dir(config.HTTP.PublicDir, "", notFoundHandler))
|
||||
|
|
|
@ -2,14 +2,17 @@ package route
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v4"
|
||||
|
||||
"forge.cadoles.com/wpetit/guesstimate/internal/config"
|
||||
"forge.cadoles.com/wpetit/guesstimate/internal/model"
|
||||
"forge.cadoles.com/wpetit/guesstimate/internal/storm"
|
||||
"github.com/SebastiaanKlippert/go-wkhtmltopdf"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
|
@ -74,6 +77,62 @@ func handleGetProject(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func handleExportProject(w http.ResponseWriter, r *http.Request) {
|
||||
ctn := container.Must(r.Context())
|
||||
cfg := config.Must(ctn)
|
||||
|
||||
projectID := getProjectID(r)
|
||||
|
||||
var (
|
||||
err error
|
||||
url string
|
||||
)
|
||||
|
||||
url = "http://" + string(cfg.Client.BaseURL) + string(cfg.Client.Address) + "/pdf/" + string(projectID)
|
||||
fmt.Println(url)
|
||||
|
||||
// Create new PDF generator
|
||||
pdfg, err := wkhtmltopdf.NewPDFGenerator()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Set global options
|
||||
pdfg.Dpi.Set(300)
|
||||
pdfg.Orientation.Set(wkhtmltopdf.OrientationPortrait)
|
||||
pdfg.Grayscale.Set(true)
|
||||
|
||||
// Create a new input page from an URL
|
||||
page := wkhtmltopdf.NewPage(url)
|
||||
|
||||
// Set options for this page
|
||||
page.FooterRight.Set("[page]")
|
||||
page.FooterFontSize.Set(10)
|
||||
page.Zoom.Set(0.95)
|
||||
|
||||
// Add to document
|
||||
pdfg.AddPage(page)
|
||||
|
||||
// Create PDF document in internal buffer
|
||||
err = pdfg.Create()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Write buffer contents to file on disk
|
||||
err = pdfg.WriteFile("./sample.pdf")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
rsp, err := writePDF(w, http.StatusOK, pdfg.Bytes())
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not write pdf response"))
|
||||
}
|
||||
|
||||
fmt.Println(rsp)
|
||||
}
|
||||
|
||||
type createRequest struct {
|
||||
Project *model.Project `json:"project"`
|
||||
}
|
||||
|
@ -246,3 +305,12 @@ func writeJSON(w http.ResponseWriter, statusCode int, data interface{}) error {
|
|||
|
||||
return encoder.Encode(data)
|
||||
}
|
||||
|
||||
func writePDF(w http.ResponseWriter, statusCode int, data []byte) (int, error) {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=foo.pdf")
|
||||
w.Header().Set("Content-Type", "application/pdf")
|
||||
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
return w.Write(data)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue