diff --git a/go.mod b/go.mod index 9d557637..b44bb011 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( require ( ariga.io/atlas v0.36.2-0.20250806044935-5bb51a0a956e // indirect + github.com/SebastiaanKlippert/go-wkhtmltopdf v1.9.3 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-redis/redis v6.15.9+incompatible // indirect diff --git a/go.sum b/go.sum index 9681a749..da70ac51 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/SebastiaanKlippert/go-wkhtmltopdf v1.9.3 h1:vrA6+R1BMLKMTbos8jAeuBrImHPGtY4gTlcue3OIej8= +github.com/SebastiaanKlippert/go-wkhtmltopdf v1.9.3/go.mod h1:SQq4xfIdvf6WYKSDxAJc+xOJdolt+/bc1jnQKMtPMvQ= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/internal/interface/main-handler/generate-file/handler.go b/internal/interface/main-handler/generate-file/handler.go new file mode 100644 index 00000000..c6d6c67d --- /dev/null +++ b/internal/interface/main-handler/generate-file/handler.go @@ -0,0 +1,18 @@ +package generatefile + +import ( + "net/http" + + rw "github.com/karincake/risoles" + + u "simrs-vx/internal/use-case/main-use-case/generate-file" +) + +func Generate(w http.ResponseWriter, r *http.Request) { + dto := u.GenerateDto{} + if res := rw.ValidateStructByIOR(w, r.Body, &dto); !res { + return + } + res, err := u.Generate(dto) + rw.DataResponse(w, res, err) +} diff --git a/internal/interface/main-handler/main-handler.go b/internal/interface/main-handler/main-handler.go index 8ce9fadf..4a49c022 100644 --- a/internal/interface/main-handler/main-handler.go +++ b/internal/interface/main-handler/main-handler.go @@ -18,6 +18,7 @@ import ( encounter "simrs-vx/internal/interface/main-handler/encounter" encounterdocument "simrs-vx/internal/interface/main-handler/encounter-document" generalconsent "simrs-vx/internal/interface/main-handler/general-consent" + generatefile "simrs-vx/internal/interface/main-handler/generate-file" internalreference "simrs-vx/internal/interface/main-handler/internal-reference" materialorder "simrs-vx/internal/interface/main-handler/material-order" materialorderitem "simrs-vx/internal/interface/main-handler/material-order-item" @@ -280,6 +281,7 @@ func SetRoutes() http.Handler { hc.RegCrud(r, "/v1/upload-file", uploadfile.O) hc.RegCrud(r, "/v1/encounter-document", encounterdocument.O) hc.RegCrud(r, "/v1/general-consent", generalconsent.O) + r.HandleFunc("POST /v1/generate-file", generatefile.Generate) /******************** actor ********************/ hc.RegCrud(r, "/v1/person", person.O) diff --git a/internal/use-case/main-use-case/generate-file/case.go b/internal/use-case/main-use-case/generate-file/case.go index 72182513..c39a7edc 100644 --- a/internal/use-case/main-use-case/generate-file/case.go +++ b/internal/use-case/main-use-case/generate-file/case.go @@ -1,15 +1,18 @@ package generatefile import ( - // "encoding/json" + "encoding/json" + "errors" - // egc "simrs-vx/internal/domain/main-entities/general-consent" - // ugc "simrs-vx/internal/use-case/main-use-case/general-consent" + egc "simrs-vx/internal/domain/main-entities/general-consent" + ugc "simrs-vx/internal/use-case/main-use-case/general-consent" pl "simrs-vx/pkg/logger" + dg "github.com/karincake/apem/db-gorm-pg" d "github.com/karincake/dodol" + erc "simrs-vx/internal/domain/references/common" ere "simrs-vx/internal/domain/references/encounter" ) @@ -28,18 +31,55 @@ func Generate(input GenerateDto) (*d.Data, error) { // general-consent case ere.DTCGC: // get value from general consent by ref_id - // gc, err := ugc.ReadDetailData(ugc.ReadDetailDto{Ref_Id: input.Ref_Id}, &event, nil) - // if err != nil { - // return nil, err - // } + gc, err := ugc.ReadDetailData(egc.ReadDetailDto{Id: *input.Ref_Id}, &event, nil) + if err != nil { + return nil, err + } // map template data - // gc - // templateData := GeneralConsentPDF{} - // gcUnmarshalled := json.Unmarshal(g) + templateData := GeneralConsentPDF{} + if gc.Value != nil { + err := json.Unmarshal([]byte(*gc.Value), &templateData) + if err != nil { + event.ErrInfo = pl.ErrorInfo{ + Code: "data-unmarshal-fail", + Detail: err.Error(), + Raw: err, + } + return nil, err + } + } else { + return nil, errors.New("no value in this general consent") + } - return nil, nil + input.FormatType = erc.DFTCPDF + input.TemplateName = TDNGC + + // generate file + urlPub, err := generateFile(input, templateData) + if err != nil { + return nil, err + } + + gc.FileUrl = &urlPub + if err := dg.I.Save(&gc).Error; err != nil { + return nil, err + } + + response := ResponseDto{ + FileUrl: urlPub, + } + + return &d.Data{ + Meta: d.II{ + "source": source, + "structure": "single-data", + "status": "created", + }, + Data: response, + }, nil + default: + return nil, errors.New("invalid type code") } - return nil, nil } diff --git a/internal/use-case/main-use-case/generate-file/helper.go b/internal/use-case/main-use-case/generate-file/helper.go new file mode 100644 index 00000000..ec377dc2 --- /dev/null +++ b/internal/use-case/main-use-case/generate-file/helper.go @@ -0,0 +1,83 @@ +package generatefile + +import ( + "errors" + "fmt" + "mime" + "path/filepath" + "time" + + erc "simrs-vx/internal/domain/references/common" + docscfg "simrs-vx/internal/infra/docs-cfg" + pf "simrs-vx/pkg/file-helper" + pm "simrs-vx/pkg/minio-helper" + pp "simrs-vx/pkg/pdf-helper" +) + +// generate temporary file, upload to minio, generate public url, delete temporary file +func generateFile(input GenerateDto, templateData any) (string, error) { + newPath, err := pf.PathToSaveFile(fmt.Sprintf("./public/%s/%d/%s", input.EntityType_Code, *input.Ref_Id, input.Type_Code)) + if err != nil { + return "", err + } + + fPath := fmt.Sprintf("%s/%s_%s.%s", newPath, input.Type_Code, time.Now().Format("20060102150405"), input.FormatType) + + templatePath := docscfg.O.GetPath() + string(input.TemplateName) + + switch input.FormatType { + case erc.DFTCPDF: + if err := generatePDF(GeneratePDFdto{ + TemplatePath: templatePath, + TemplateData: templateData, + PdfPath: fPath, + }); err != nil { + return "", err + } + case erc.DFTCTXLSX: + // TODO: generate xlsx + case erc.DFTCTCSV: + // TODO: generate csv + default: + return "", errors.New("invalid format type") + } + + bucketName := input.EntityType_Code + + objectName := fmt.Sprintf("%v/%s-%d", *input.Ref_Id, input.Type_Code, time.Now().UnixNano()) + pdfUpload := pm.UploadPathInput{ + BucketName: string(bucketName), + Name: objectName, + Path: fPath, + ContentType: mime.TypeByExtension(filepath.Ext(fPath)), + } + + // create bucket if not exist, create object in bucket + info, err := pm.I.FPutObject(pdfUpload) + if err != nil { + return "", err + } + + // generate public url + urlPub := pm.I.GenerateUrl(info.Bucket, info.Key) + if err := pf.DeleteFolder(fPath); err != nil { + return "", err + } + + return urlPub, nil +} + +func generatePDF(input GeneratePDFdto) error { + // parse template data into html template + r := pp.NewRequestPdf("") + if err := r.ParseTemplate(input.TemplatePath, input.TemplateData); err == nil { + _, err := r.GenerateByCommand(input.PdfPath, input.TemplatePath) + if err != nil { + return errors.New("generate pdf by command error : " + err.Error()) + } + } else { + return errors.New("parse template error : " + err.Error()) + } + + return nil +} diff --git a/internal/use-case/main-use-case/generate-file/tycovar.go b/internal/use-case/main-use-case/generate-file/tycovar.go index 716e277d..17c5abc1 100644 --- a/internal/use-case/main-use-case/generate-file/tycovar.go +++ b/internal/use-case/main-use-case/generate-file/tycovar.go @@ -14,12 +14,26 @@ type GeneralConsentPDF struct { } type GenerateDto struct { - EntityType_Code ere.EntityTypeCode `form:"entityType_code"` - Ref_Id *uint `form:"ref_id"` - Type_Code ere.DocTypeCode `form:"type_code"` - FormatType erc.DocFormatTypeCode `form:"formatType"` + EntityType_Code ere.EntityTypeCode `json:"entityType_code"` + Ref_Id *uint `json:"ref_id"` + Type_Code ere.DocTypeCode `json:"type_code"` + FormatType erc.DocFormatTypeCode `json:"formatType"` + TemplateName TemplateDocsName `json:"-"` +} + +type GeneratePDFdto struct { + TemplatePath string + TemplateData any + PdfPath string } type ResponseDto struct { FileUrl string `json:"fileUrl"` } + +type TemplateDocsName string + +// TemplateDocsName is the name of the template file in the assets/docs folder +const ( + TDNGC TemplateDocsName = "general-consent.html" +) diff --git a/pkg/file-helper/file-helper.go b/pkg/file-helper/file-helper.go new file mode 100644 index 00000000..bd0849c7 --- /dev/null +++ b/pkg/file-helper/file-helper.go @@ -0,0 +1,33 @@ +package filehelper + +import ( + "fmt" + "os" + "path/filepath" +) + +// const DEFAULT_EXPIRY_FILES = time.Hour * 24 * 7 + +func PathAgreement(medicalNumber string) (string, error) { + outputPath := fmt.Sprintf("./public/patient/%s", medicalNumber) + err := os.MkdirAll(outputPath, os.ModePerm) + return outputPath, err +} + +func PathToSaveFile(outputPath string) (string, error) { + err := os.MkdirAll(outputPath, os.ModePerm) + return outputPath, err +} + +func RenameFile(srcPath, dstPath string) error { + return os.Rename(srcPath, dstPath) +} + +func DeleteFolder(path string) error { + return os.RemoveAll(path) +} + +func PathToUrl(fileName string) *string { + fileUrl := filepath.ToSlash(fmt.Sprintf("%c%s", os.PathSeparator, fileName)) + return &fileUrl +} diff --git a/pkg/pdf-helper/pdf-helper.go b/pkg/pdf-helper/pdf-helper.go new file mode 100644 index 00000000..597052eb --- /dev/null +++ b/pkg/pdf-helper/pdf-helper.go @@ -0,0 +1,131 @@ +package pdfhelper + +import ( + "bytes" + "html/template" + "log" + "os" + "os/exec" + "strconv" + "strings" + "time" + + "github.com/SebastiaanKlippert/go-wkhtmltopdf" +) + +// pdf requestpdf struct +type RequestPdf struct { + body string +} + +// new request to pdf function +func NewRequestPdf(body string) *RequestPdf { + return &RequestPdf{ + body: body, + } +} + +// parsing template function +func (r *RequestPdf) ParseTemplate(templatePath string, data interface{}) error { + f := strings.Split(templatePath, "/") + fileName := f[len(f)-1] + funcs := template.FuncMap{ + "nl2br": func(text string) template.HTML { + return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "
", -1)) + }, + } + t, err := template.New(fileName).Funcs(funcs).ParseFiles(templatePath) + if err != nil { + return err + } + buf := new(bytes.Buffer) + if err = t.Execute(buf, data); err != nil { + return err + } + r.body = buf.String() + + return nil +} + +func (r *RequestPdf) GenerateByCommand(pdfPath string, templatePath string) (bool, error) { + // wkhtmltopdf -L 0 -R 0 -B 0 -s Legal --enable-local-file-access resultAntigen2.html out.pdf + t := time.Now().Unix() + + if _, err := os.Stat("cloneTemplate/"); os.IsNotExist(err) { + errDir := os.Mkdir("cloneTemplate/", 0777) + if errDir != nil { + log.Fatal(errDir) + } + } + htmlName := strconv.FormatInt(int64(t), 10) + ".html" + err := os.WriteFile("cloneTemplate/"+htmlName, []byte(r.body), 0644) + if err != nil { + panic(err) + } + + cmd := exec.Command("wkhtmltopdf", "--enable-local-file-access", "-L", "0", "-R", "0", "-B", "0", "-s", "A4", "cloneTemplate/"+htmlName, pdfPath) + + if err := cmd.Run(); err != nil { + return false, err + } + dir, err := os.Getwd() + if err != nil { + panic(err) + } + defer os.RemoveAll(dir + "/cloneTemplate") + return true, nil +} + +// generate pdf function +func (r *RequestPdf) GeneratePDF(pdfPath string) (bool, error) { + t := time.Now().Unix() + + if _, err := os.Stat("cloneTemplate/"); os.IsNotExist(err) { + errDir := os.Mkdir("cloneTemplate/", 0777) + if errDir != nil { + log.Fatal(errDir) + } + } + err1 := os.WriteFile("cloneTemplate/"+strconv.FormatInt(int64(t), 10)+".html", []byte(r.body), 0644) + if err1 != nil { + panic(err1) + } + + f, err := os.Open("cloneTemplate/" + strconv.FormatInt(int64(t), 10) + ".html") + if f != nil { + defer f.Close() + } + if err != nil { + log.Fatal(err) + } + + pdfg, err := wkhtmltopdf.NewPDFGenerator() + if err != nil { + log.Fatal(err) + } + + pdfg.AddPage(wkhtmltopdf.NewPageReader(f)) + + pdfg.PageSize.Set(wkhtmltopdf.PageSizeA4) + + pdfg.Dpi.Set(300) + + err = pdfg.Create() + if err != nil { + log.Fatal(err) + } + + err = pdfg.WriteFile(pdfPath) + if err != nil { + log.Fatal(err) + } + + dir, err := os.Getwd() + if err != nil { + panic(err) + } + + defer os.RemoveAll(dir + "/cloneTemplate") + + return true, nil +}