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..ae50b096b 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,38 @@ 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/generated/openapi_gen.go b/api/generated/openapi_gen.go index 59c608795..debf5d191 100644 --- a/api/generated/openapi_gen.go +++ b/api/generated/openapi_gen.go @@ -423,6 +423,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 @@ -851,6 +857,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 @@ -2110,6 +2119,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 @@ -3259,6 +3286,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/api/internals/domain/financial_record.go b/api/internals/domain/financial_record.go index 9eeb96df4..0b04f2214 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 FinancialRecordData 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..69931e64f 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,53 @@ func (fru *financialRecordUseCase) DestroyFinancialRecord(c context.Context, id return err } +func (fru *financialRecordUseCase) GetFinancialRecordDetailForCSV( + c context.Context, + year string, +) ([][]string, error) { + header := []string{"局", "部門", "物品", "予算申請金額", "購入金額"} + csvData := [][]string{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.FinancialRecordData diff --git a/api/router/router.go b/api/router/router.go index 70dd8534a..f2c554318 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -195,6 +195,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) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 1b84e8a95..171c011eb 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1387,6 +1387,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 30c53b5e9..f2ea8dacd 100644 --- a/view/next-project/src/generated/hooks.ts +++ b/view/next-project/src/generated/hooks.ts @@ -90,6 +90,7 @@ import type { GetFestivalItemsDetailsUserIdParams, GetFestivalItemsParams, GetFestivalItemsUsersParams, + GetFinancialRecordsCsvDownloadParams, GetFinancialRecordsParams, GetFundInformations200, GetFundInformationsDetails200, @@ -3897,6 +3898,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 5260024ac..9a8f11407 100644 --- a/view/next-project/src/generated/model/index.ts +++ b/view/next-project/src/generated/model/index.ts @@ -87,6 +87,7 @@ export * from './getExpensesIdDetails200'; export * from './getFestivalItemsDetailsUserIdParams'; export * from './getFestivalItemsParams'; export * from './getFestivalItemsUsersParams'; +export * from './getFinancialRecordsCsvDownloadParams'; export * from './getFinancialRecordsParams'; export * from './getFundInformations200'; export * from './getFundInformationsDetails200';