From 03646972f7830f080543727c979b10361536d2e6 Mon Sep 17 00:00:00 2001 From: Kubosaka Date: Sat, 22 Feb 2025 15:11:07 +0900 Subject: [PATCH 1/5] =?UTF-8?q?oas=E5=AE=9A=E7=BE=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/generated/openapi_gen.go | 28 +++++++++ openapi/openapi.yaml | 25 ++++++++ view/next-project/src/generated/hooks.ts | 59 +++++++++++++++++++ .../next-project/src/generated/model/index.ts | 1 + 4 files changed, 113 insertions(+) diff --git a/api/generated/openapi_gen.go b/api/generated/openapi_gen.go index a480cdc08..a003760ee 100644 --- a/api/generated/openapi_gen.go +++ b/api/generated/openapi_gen.go @@ -393,6 +393,12 @@ type GetFinancialRecordsParams struct { Year *int `form:"year,omitempty" json:"year,omitempty"` } +// GetFinancialRecordsCsvDownloadParams defines parameters for GetFinancialRecordsCsvDownload. +type GetFinancialRecordsCsvDownloadParams struct { + // Year year + Year int `form:"year" json:"year"` +} + // PostFundInformationsParams defines parameters for PostFundInformations. type PostFundInformationsParams struct { // UserId user_id @@ -815,6 +821,9 @@ type ServerInterface interface { // (POST /financial_records) PostFinancialRecords(ctx echo.Context) error + // (GET /financial_records/csv/download) + GetFinancialRecordsCsvDownload(ctx echo.Context, params GetFinancialRecordsCsvDownloadParams) error + // (DELETE /financial_records/{id}) DeleteFinancialRecordsId(ctx echo.Context, id int) error @@ -2024,6 +2033,24 @@ func (w *ServerInterfaceWrapper) PostFinancialRecords(ctx echo.Context) error { return err } +// GetFinancialRecordsCsvDownload converts echo context to params. +func (w *ServerInterfaceWrapper) GetFinancialRecordsCsvDownload(ctx echo.Context) error { + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params GetFinancialRecordsCsvDownloadParams + // ------------- Required query parameter "year" ------------- + + err = runtime.BindQueryParameter("form", true, true, "year", ctx.QueryParams(), ¶ms.Year) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter year: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetFinancialRecordsCsvDownload(ctx, params) + return err +} + // DeleteFinancialRecordsId converts echo context to params. func (w *ServerInterfaceWrapper) DeleteFinancialRecordsId(ctx echo.Context) error { var err error @@ -3171,6 +3198,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.PUT(baseURL+"/festival_items/:id", wrapper.PutFestivalItemsId) router.GET(baseURL+"/financial_records", wrapper.GetFinancialRecords) router.POST(baseURL+"/financial_records", wrapper.PostFinancialRecords) + router.GET(baseURL+"/financial_records/csv/download", wrapper.GetFinancialRecordsCsvDownload) router.DELETE(baseURL+"/financial_records/:id", wrapper.DeleteFinancialRecordsId) router.PUT(baseURL+"/financial_records/:id", wrapper.PutFinancialRecordsId) router.GET(baseURL+"/fund_informations", wrapper.GetFundInformations) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index a01939b1b..06a3b4c18 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1335,6 +1335,31 @@ paths: application/json: schema: type: object + /financial_records/csv/download: + get: + tags: + - financial_record + description: financial_recordの年度予算のCSVをダウンロード + parameters: + - name: year + in: query + description: year + schema: + type: integer + required: true + responses: + "200": + description: financial_recordの年度予算のCSV取得 + headers: + Content-Disposition: + schema: + type: string + example: attachment; filename=financial_record.csv + content: + text/csv: + schema: + type: string + format: binary /fund_informations: get: tags: diff --git a/view/next-project/src/generated/hooks.ts b/view/next-project/src/generated/hooks.ts index 01c4469cf..627240622 100644 --- a/view/next-project/src/generated/hooks.ts +++ b/view/next-project/src/generated/hooks.ts @@ -86,6 +86,7 @@ import type { GetExpensesIdDetails200, GetFestivalItemsDetailsUserIdParams, GetFestivalItemsParams, + GetFinancialRecordsCsvDownloadParams, GetFinancialRecordsParams, GetFundInformations200, GetFundInformationsDetails200, @@ -3777,6 +3778,64 @@ export const useDeleteFinancialRecordsId = ( } } +/** + * financial_recordの年度予算のCSVをダウンロード + */ +export type getFinancialRecordsCsvDownloadResponse = { + data: Blob; + status: number; + headers: Headers; +} + +export const getGetFinancialRecordsCsvDownloadUrl = (params: GetFinancialRecordsCsvDownloadParams,) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()) + } + }); + + return normalizedParams.size ? `/financial_records/csv/download?${normalizedParams.toString()}` : `/financial_records/csv/download` +} + +export const getFinancialRecordsCsvDownload = async (params: GetFinancialRecordsCsvDownloadParams, options?: RequestInit): Promise => { + + return customFetch>(getGetFinancialRecordsCsvDownloadUrl(params), + { + ...options, + method: 'GET' + + + } +);} + + + + +export const getGetFinancialRecordsCsvDownloadKey = (params: GetFinancialRecordsCsvDownloadParams,) => [`/financial_records/csv/download`, ...(params ? [params]: [])] as const; + +export type GetFinancialRecordsCsvDownloadQueryResult = NonNullable>> +export type GetFinancialRecordsCsvDownloadQueryError = unknown + +export const useGetFinancialRecordsCsvDownload = ( + params: GetFinancialRecordsCsvDownloadParams, options?: { swr?:SWRConfiguration>, TError> & { swrKey?: Key, enabled?: boolean }, request?: SecondParameter } +) => { + const {swr: swrOptions, request: requestOptions} = options ?? {} + + const isEnabled = swrOptions?.enabled !== false + const swrKey = swrOptions?.swrKey ?? (() => isEnabled ? getGetFinancialRecordsCsvDownloadKey(params) : null); + const swrFn = () => getFinancialRecordsCsvDownload(params, requestOptions) + + const query = useSwr>, TError>(swrKey, swrFn, swrOptions) + + return { + swrKey, + ...query + } +} + /** * fund_informationの一覧を取得 */ diff --git a/view/next-project/src/generated/model/index.ts b/view/next-project/src/generated/model/index.ts index 992020119..2cdef6c6c 100644 --- a/view/next-project/src/generated/model/index.ts +++ b/view/next-project/src/generated/model/index.ts @@ -83,6 +83,7 @@ export * from './getExpensesId200'; export * from './getExpensesIdDetails200'; export * from './getFestivalItemsDetailsUserIdParams'; export * from './getFestivalItemsParams'; +export * from './getFinancialRecordsCsvDownloadParams'; export * from './getFinancialRecordsParams'; export * from './getFundInformations200'; export * from './getFundInformationsDetails200'; From 73875139155fa39a40247a0f5a36c8e7f2d4ace2 Mon Sep 17 00:00:00 2001 From: Kubosaka Date: Sat, 22 Feb 2025 17:02:04 +0900 Subject: [PATCH 2/5] =?UTF-8?q?csv=E3=83=87=E3=83=BC=E3=82=BF=E6=9B=B8?= =?UTF-8?q?=E3=81=8D=E5=87=BA=E3=81=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/financial_record_controller.go | 42 ++++++++++++++ .../repository/financial_record_repository.go | 56 +++++++++++++++++++ api/internals/domain/financial_record.go | 9 +++ .../usecase/financial_record_usecase.go | 50 +++++++++++++++++ api/router/router.go | 1 + 5 files changed, 158 insertions(+) diff --git a/api/externals/controller/financial_record_controller.go b/api/externals/controller/financial_record_controller.go index 17037b01f..1a99cf1f9 100644 --- a/api/externals/controller/financial_record_controller.go +++ b/api/externals/controller/financial_record_controller.go @@ -1,6 +1,8 @@ package controller import ( + "encoding/csv" + "fmt" "net/http" "github.com/NUTFes/FinanSu/api/generated" @@ -17,6 +19,7 @@ type FinancialRecordController interface { CreateFinancialRecord(echo.Context) error UpdateFinancialRecord(echo.Context) error DestroyFinancialRecord(echo.Context) error + DownloadFinancialRecordsCSV(echo.Context) error } func NewFinancialRecordController(u usecase.FinancialRecordUseCase) FinancialRecordController { @@ -81,5 +84,44 @@ func (f *financialRecordController) DestroyFinancialRecord(c echo.Context) error return c.String(http.StatusOK, "Destroy FinancialRecord") } +func (f *financialRecordController) DownloadFinancialRecordsCSV(c echo.Context) error { + year := c.QueryParam("year") + var err error + + records, err := f.u.GetFinancialRecordDetailForCSV(c.Request().Context(), year) + if err != nil { + return err + } + + // ヘッダーの設定 + w := c.Response().Writer + fileName := fmt.Sprintf("予算_%s.csv", year) + attachment := fmt.Sprintf(`attachment; filename="%s"`, fileName) + w.Header().Set("Content-Type", "text/csv") + w.Header().Set("Content-Disposition", attachment) + + if err := makeCSV(w, records); err != nil { + return err + } + + return nil +} + +func makeCSV(writer http.ResponseWriter, records [][]string) error { + csvWriter := csv.NewWriter(writer) + for _, record := range records { + if err := csvWriter.Write(record); err != nil { + http.Error(writer, "CSVの書き込み中にエラーが発生しました", http.StatusInternalServerError) + return err + } + } + csvWriter.Flush() + if err := csvWriter.Error(); err != nil { + http.Error(writer, "CSVのフラッシュ中にエラーが発生しました", http.StatusInternalServerError) + return err + } + return nil +} + type FinancialRecordDetails = generated.FinancialRecordDetails type FinancialRecord = generated.FinancialRecord diff --git a/api/externals/repository/financial_record_repository.go b/api/externals/repository/financial_record_repository.go index 3fb39ab8a..211bd1c4d 100644 --- a/api/externals/repository/financial_record_repository.go +++ b/api/externals/repository/financial_record_repository.go @@ -23,6 +23,7 @@ type FinancialRecordRepository interface { Update(context.Context, string, generated.FinancialRecord) error Delete(context.Context, string) error FindLatestRecord(context.Context) (*sql.Row, error) + AllForCSV(context.Context, string) (*sql.Rows, error) } func NewFinancialRecordRepository(c db.Client, ac abstract.Crud) FinancialRecordRepository { @@ -126,6 +127,27 @@ func (frr *financialRecordRepository) FindLatestRecord(c context.Context) (*sql. return frr.crud.ReadByID(c, query) } +// 年度別に取得 +func (frr *financialRecordRepository) AllForCSV( + c context.Context, + year string, +) (*sql.Rows, error) { + ds := selectFinancialRecordQueryForCSV + dsExceptItem := selectFinancialRecordQueryForCsvExceptItem + if year != "" { + ds = ds.Where(goqu.Ex{"years.year": year}) + dsExceptItem = dsExceptItem.Where(goqu.Ex{"years.year": year}) + } + // 2つのdsをUNION + sql := ds.Union(dsExceptItem).Order(goqu.I("id").Asc()) + query, _, err := sql.ToSQL() + + if err != nil { + return nil, err + } + return frr.crud.Read(c, query) +} + var selectFinancialRecordQuery = dialect.Select( "financial_records.id", "financial_records.name", "years.year", @@ -139,3 +161,37 @@ var selectFinancialRecordQuery = dialect.Select( LeftJoin(goqu.I("item_budgets"), goqu.On(goqu.I("festival_items.id").Eq(goqu.I("item_budgets.festival_item_id")))). LeftJoin(goqu.I("buy_reports"), goqu.On(goqu.I("festival_items.id").Eq(goqu.I("buy_reports.festival_item_id")))). GroupBy("financial_records.id") + +// 予算・部門がないものを取得するds +var selectFinancialRecordQueryForCSV = dialect.Select( + "financial_records.id", + "financial_records.name", + "divisions.name", + "festival_items.name", + goqu.COALESCE(goqu.SUM("item_budgets.amount"), 0).As("budget"), + goqu.COALESCE(goqu.SUM("buy_reports.amount"), 0).As("expense")). + From("festival_items"). + InnerJoin(goqu.I("divisions"), goqu.On(goqu.I("festival_items.division_id").Eq(goqu.I("divisions.id")))). + InnerJoin(goqu.I("financial_records"), goqu.On(goqu.I("divisions.financial_record_id").Eq(goqu.I("financial_records.id")))). + InnerJoin(goqu.I("years"), goqu.On(goqu.I("financial_records.year_id").Eq(goqu.I("years.id")))). + LeftJoin(goqu.I("item_budgets"), goqu.On(goqu.I("festival_items.id").Eq(goqu.I("item_budgets.festival_item_id")))). + LeftJoin(goqu.I("buy_reports"), goqu.On(goqu.I("festival_items.id").Eq(goqu.I("buy_reports.festival_item_id")))).GroupBy("festival_items.id") + +// 予算・部門がないものを取得するds +var selectFinancialRecordQueryForCsvExceptItem = dialect.Select( + "financial_records.id", + "financial_records.name", + goqu.COALESCE(goqu.I("divisions.name"), "").As("divisionName"), + goqu.L("''").As("festivalItemName"), + goqu.L("0").As("budget"), + goqu.L("0").As("expense")). + From("financial_records"). + LeftJoin(goqu.I("divisions"), goqu.On(goqu.I("divisions.financial_record_id").Eq(goqu.I("financial_records.id")))). + InnerJoin(goqu.I("years"), goqu.On(goqu.I("financial_records.year_id").Eq(goqu.I("years.id")))).Where(goqu.Or( + goqu.Ex{ + "divisions.id": goqu.Op{"notIn": dialect.Select("festival_items.division_id").From("festival_items")}, + }, + goqu.Ex{ + "financial_records.id": goqu.Op{"notIn": dialect.Select("divisions.financial_record_id").From("divisions")}, + }, +)) diff --git a/api/internals/domain/financial_record.go b/api/internals/domain/financial_record.go index 9eeb96df4..a66355b3f 100644 --- a/api/internals/domain/financial_record.go +++ b/api/internals/domain/financial_record.go @@ -11,3 +11,12 @@ type FinancialRecord struct { CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } + +type FinancialRecordDetail struct { + FinancialRecordID int `json:"financialRecordId"` + FinancialRecordName string `json:"financialRecordName"` + DivisionName string `json:"divisionName"` + FestivalItemName string `json:"festivalItemName"` + BudgetAmount int `json:"budgetAmount"` + ReportAmount int `json:"reportAmount"` +} diff --git a/api/internals/usecase/financial_record_usecase.go b/api/internals/usecase/financial_record_usecase.go index 0af3eabf9..d99f61720 100644 --- a/api/internals/usecase/financial_record_usecase.go +++ b/api/internals/usecase/financial_record_usecase.go @@ -2,9 +2,11 @@ package usecase import ( "context" + "strconv" rep "github.com/NUTFes/FinanSu/api/externals/repository" "github.com/NUTFes/FinanSu/api/generated" + "github.com/NUTFes/FinanSu/api/internals/domain" "github.com/pkg/errors" ) @@ -25,6 +27,7 @@ type FinancialRecordUseCase interface { FinancialRecord, ) (FinancialRecordWithBalance, error) DestroyFinancialRecord(context.Context, string) error + GetFinancialRecordDetailForCSV(context.Context, string) ([][]string, error) } func NewFinancialRecordUseCase(rep rep.FinancialRecordRepository) FinancialRecordUseCase { @@ -198,7 +201,54 @@ func (fru *financialRecordUseCase) DestroyFinancialRecord(c context.Context, id return err } +func (fru *financialRecordUseCase) GetFinancialRecordDetailForCSV( + c context.Context, + year string, +) ([][]string, error) { + csvData := make([][]string, 0) + HEADER := []string{"局", "部門", "物品", "予算申請金額", "購入金額"} + csvData = append(csvData, HEADER) + var financialRecords []FinancialRecordData + + rows, err := fru.rep.AllForCSV(c, year) + if err != nil { + return csvData, errors.Wrapf(err, "can not connect SQL") + } + + defer rows.Close() + + for rows.Next() { + var financialRecord FinancialRecordData + err := rows.Scan( + &financialRecord.FinancialRecordID, + &financialRecord.FinancialRecordName, + &financialRecord.DivisionName, + &financialRecord.FestivalItemName, + &financialRecord.BudgetAmount, + &financialRecord.ReportAmount, + ) + + if err != nil { + return csvData, errors.Wrapf(err, "scan error") + } + financialRecords = append(financialRecords, financialRecord) + } + + for _, financialRecord := range financialRecords { + csvData = append(csvData, []string{ + financialRecord.FinancialRecordName, + financialRecord.DivisionName, + financialRecord.FestivalItemName, + strconv.Itoa(financialRecord.BudgetAmount), + strconv.Itoa(financialRecord.ReportAmount), + }) + } + + return csvData, err +} + type FinancialRecordDetails = generated.FinancialRecordDetails type FinancialRecord = generated.FinancialRecord type FinancialRecordWithBalance = generated.FinancialRecordWithBalance type Total = generated.Total +type FinancialRecordData = domain.FinancialRecordDetail diff --git a/api/router/router.go b/api/router/router.go index 5463bc29a..95a155f65 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -193,6 +193,7 @@ func (r router) ProvideRouter(e *echo.Echo) { e.POST("/financial_records", r.financialRecordController.CreateFinancialRecord) e.PUT("/financial_records/:id", r.financialRecordController.UpdateFinancialRecord) e.DELETE("/financial_records/:id", r.financialRecordController.DestroyFinancialRecord) + e.GET("/financial_records/csv/download", r.financialRecordController.DownloadFinancialRecordsCSV) // fund informations e.GET("/fund_informations", r.fundInformationController.IndexFundInformation) From 977d77a09662253b8bea5955b4b1da02f8137f22 Mon Sep 17 00:00:00 2001 From: Kubosaka Date: Sat, 22 Feb 2025 17:08:00 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=87=E3=83=B3?= =?UTF-8?q?=E3=83=88=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/financial_record_repository.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/api/externals/repository/financial_record_repository.go b/api/externals/repository/financial_record_repository.go index 211bd1c4d..ae50b096b 100644 --- a/api/externals/repository/financial_record_repository.go +++ b/api/externals/repository/financial_record_repository.go @@ -187,11 +187,12 @@ var selectFinancialRecordQueryForCsvExceptItem = dialect.Select( goqu.L("0").As("expense")). From("financial_records"). LeftJoin(goqu.I("divisions"), goqu.On(goqu.I("divisions.financial_record_id").Eq(goqu.I("financial_records.id")))). - InnerJoin(goqu.I("years"), goqu.On(goqu.I("financial_records.year_id").Eq(goqu.I("years.id")))).Where(goqu.Or( - goqu.Ex{ - "divisions.id": goqu.Op{"notIn": dialect.Select("festival_items.division_id").From("festival_items")}, - }, - goqu.Ex{ - "financial_records.id": goqu.Op{"notIn": dialect.Select("divisions.financial_record_id").From("divisions")}, - }, -)) + InnerJoin(goqu.I("years"), goqu.On(goqu.I("financial_records.year_id").Eq(goqu.I("years.id")))). + Where(goqu.Or( + goqu.Ex{ + "divisions.id": goqu.Op{"notIn": dialect.Select("festival_items.division_id").From("festival_items")}, + }, + goqu.Ex{ + "financial_records.id": goqu.Op{"notIn": dialect.Select("divisions.financial_record_id").From("divisions")}, + }, + )) From dd9187f3b571bcadc250923d81eddf961a55ae35 Mon Sep 17 00:00:00 2001 From: Kubosaka Date: Mon, 24 Feb 2025 15:39:47 +0900 Subject: [PATCH 4/5] =?UTF-8?q?csv=E3=81=AE=E4=BA=8C=E6=AC=A1=E5=85=83?= =?UTF-8?q?=E3=82=B9=E3=83=A9=E3=82=A4=E3=82=B9=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/internals/usecase/financial_record_usecase.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/api/internals/usecase/financial_record_usecase.go b/api/internals/usecase/financial_record_usecase.go index d99f61720..f9faabb5c 100644 --- a/api/internals/usecase/financial_record_usecase.go +++ b/api/internals/usecase/financial_record_usecase.go @@ -205,9 +205,8 @@ func (fru *financialRecordUseCase) GetFinancialRecordDetailForCSV( c context.Context, year string, ) ([][]string, error) { - csvData := make([][]string, 0) - HEADER := []string{"局", "部門", "物品", "予算申請金額", "購入金額"} - csvData = append(csvData, HEADER) + header := []string{"局", "部門", "物品", "予算申請金額", "購入金額"} + csvData := [][]string{header} var financialRecords []FinancialRecordData rows, err := fru.rep.AllForCSV(c, year) From a602a6a5246c52f20866d85ef84dc790ce3328e3 Mon Sep 17 00:00:00 2001 From: Kubosaka Date: Mon, 24 Feb 2025 15:40:52 +0900 Subject: [PATCH 5/5] =?UTF-8?q?type=E5=90=8D=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/internals/domain/financial_record.go | 2 +- api/internals/usecase/financial_record_usecase.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/internals/domain/financial_record.go b/api/internals/domain/financial_record.go index a66355b3f..0b04f2214 100644 --- a/api/internals/domain/financial_record.go +++ b/api/internals/domain/financial_record.go @@ -12,7 +12,7 @@ type FinancialRecord struct { UpdatedAt time.Time `json:"updatedAt"` } -type FinancialRecordDetail struct { +type FinancialRecordData struct { FinancialRecordID int `json:"financialRecordId"` FinancialRecordName string `json:"financialRecordName"` DivisionName string `json:"divisionName"` diff --git a/api/internals/usecase/financial_record_usecase.go b/api/internals/usecase/financial_record_usecase.go index f9faabb5c..69931e64f 100644 --- a/api/internals/usecase/financial_record_usecase.go +++ b/api/internals/usecase/financial_record_usecase.go @@ -250,4 +250,4 @@ type FinancialRecordDetails = generated.FinancialRecordDetails type FinancialRecord = generated.FinancialRecord type FinancialRecordWithBalance = generated.FinancialRecordWithBalance type Total = generated.Total -type FinancialRecordData = domain.FinancialRecordDetail +type FinancialRecordData = domain.FinancialRecordData