From e5f403ff7d2ae26773abf9dda367640e03470a5f Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 29 Jul 2026 16:10:45 +0800 Subject: [PATCH] feat(t242): delete unused freight orders --- .../sqlite/freight_delete_repository_test.go | 209 ++++++++++++++++++ .../repository/sqlite/freight_repository.go | 106 +++++++++ .../transport/httpapi/admin_handlers.go | 1 + .../transport/httpapi/admin_handlers_test.go | 106 +++++++++ .../transport/httpapi/freight_handlers.go | 17 ++ .../internal/transport/webui/handler.go | 60 +++++ .../internal/transport/webui/handler_test.go | 106 +++++++++ .../internal/transport/webui/static/admin.js | 19 ++ .../webui/templates/freight-detail.gohtml | 38 +++- backend-api/internal/transport/webui/types.go | 7 + .../transport/webui/usecase_adapter.go | 25 +++ backend-api/internal/usecase/errors.go | 8 + .../internal/usecase/freight_image_ports.go | 1 + .../internal/usecase/freight_image_service.go | 14 ++ .../usecase/freight_image_service_test.go | 24 ++ backend-api/internal/usecase/freight_ports.go | 5 + .../internal/usecase/freight_service.go | 44 ++++ .../internal/usecase/freight_service_test.go | 93 +++++++- docs/current-state.md | 23 +- docs/tasks/T-242.md | 36 ++- 20 files changed, 915 insertions(+), 27 deletions(-) create mode 100644 backend-api/internal/repository/sqlite/freight_delete_repository_test.go diff --git a/backend-api/internal/repository/sqlite/freight_delete_repository_test.go b/backend-api/internal/repository/sqlite/freight_delete_repository_test.go new file mode 100644 index 0000000..3c8cc69 --- /dev/null +++ b/backend-api/internal/repository/sqlite/freight_delete_repository_test.go @@ -0,0 +1,209 @@ +package sqlite_test + +import ( + "context" + "errors" + "testing" + "time" + + "cmroubao/backend-api/internal/domain" + repository "cmroubao/backend-api/internal/repository/sqlite" + "cmroubao/backend-api/internal/usecase" +) + +func TestDeleteFreightOrderIsScopedIdempotentAndReimportable(t *testing.T) { + db := openDatabase(t) + store, err := repository.New(db) + if err != nil { + t.Fatalf("repository.New() error = %v", err) + } + ctx := context.Background() + now := time.Date(2026, 7, 29, 8, 0, 0, 0, time.UTC) + userID := uuid(2420) + seedFreightUser(t, db, userID, now) + + first := freightRun(2421, userID, now) + createAndStartFreightRun(t, store, first, "delete-source-1", "1") + batch := freightBatch(2422, "a", "b") + thumb := "190846474" + batch.Orders[0].Items[0].ProductThumbRef = &thumb + if err := store.CompleteFreightSync( + ctx, + first, + batch, + now.Add(time.Second), + ); err != nil { + t.Fatalf("CompleteFreightSync() error = %v", err) + } + orders, err := store.ListFreightOrders(ctx, "local-admin", 10) + if err != nil || len(orders) != 1 { + t.Fatalf("orders = %+v, %v", orders, err) + } + orderID := orders[0].ID + detail, err := store.GetFreightOrder(ctx, "local-admin", orderID) + if err != nil || len(detail.Items) != 2 { + t.Fatalf("detail = %+v, %v", detail, err) + } + readyImage := domain.FreightItemImage{ + CreatorSubject: "local-admin", + FreightOrderItemID: detail.Items[0].ID, + ProductThumbRef: thumb, + Status: domain.FreightItemImageReady, + MediaType: "image/jpeg", + SizeBytes: 100, + SHA256: repeatHex("c"), + StorageKey: "freight/delete-ready.jpg", + UpdatedAt: now.Add(2 * time.Second), + } + if _, err := store.SaveFreightItemImage(ctx, readyImage); err != nil { + t.Fatalf("SaveFreightItemImage() error = %v", err) + } + + watermarkThrough := now.Add(30 * time.Minute) + dateRun := freightDateRun( + 2423, + userID, + now.Add(time.Minute), + "2026-07-29", + "2026-07-29", + watermarkThrough, + ) + createAndStartFreightRun(t, store, dateRun, "delete-watermark", "2") + if err := store.CompleteFreightDateSync( + ctx, + dateRun, + domain.FreightImportBatch{}, + watermarkThrough, + now.Add(2*time.Minute), + ); err != nil { + t.Fatalf("CompleteFreightDateSync() error = %v", err) + } + + keys, err := store.DeleteFreightOrder( + ctx, + "another-admin", + orderID, + ) + if err != nil || len(keys) != 0 { + t.Fatalf("cross-subject delete = %#v, %v", keys, err) + } + if _, err := store.GetFreightOrder( + ctx, + "local-admin", + orderID, + ); err != nil { + t.Fatalf("cross-subject delete changed order: %v", err) + } + + keys, err = store.DeleteFreightOrder(ctx, "local-admin", orderID) + if err != nil || len(keys) != 1 || + keys[0] != readyImage.StorageKey { + t.Fatalf("DeleteFreightOrder() = %#v, %v", keys, err) + } + for table, want := range map[string]int{ + "freight_orders": 0, + "freight_order_items": 0, + "freight_item_images": 0, + "erp_sync_runs": 2, + } { + var count int + if err := db.QueryRow("SELECT count(*) FROM " + table).Scan( + &count, + ); err != nil || count != want { + t.Fatalf("%s count = %d, %v", table, count, err) + } + } + watermark, err := store.GetFreightSyncWatermark(ctx, "local-admin") + if err != nil || watermark == nil || + !watermark.LastSuccessfulTo.Equal(watermarkThrough) { + t.Fatalf("watermark after delete = %+v, %v", watermark, err) + } + keys, err = store.DeleteFreightOrder(ctx, "local-admin", orderID) + if err != nil || len(keys) != 0 { + t.Fatalf("repeated delete = %#v, %v", keys, err) + } + + reimport := freightRun(2424, userID, now.Add(3*time.Minute)) + createAndStartFreightRun(t, store, reimport, "delete-reimport", "3") + if err := store.CompleteFreightSync( + ctx, + reimport, + freightBatch(2425, "a", "b"), + now.Add(4*time.Minute), + ); err != nil { + t.Fatalf("reimport error = %v", err) + } + orders, err = store.ListFreightOrders(ctx, "local-admin", 10) + if err != nil || len(orders) != 1 || orders[0].ID == orderID || + orders[0].Revision != 1 { + t.Fatalf("reimported orders = %+v, %v", orders, err) + } +} + +func TestDeleteFreightOrderRejectsAnyProcurementReferenceAtomically( + t *testing.T, +) { + db := openDatabase(t) + store, _ := repository.New(db) + ctx := context.Background() + now := time.Date(2026, 7, 29, 9, 0, 0, 0, time.UTC) + userID := uuid(2430) + seedFreightUser(t, db, userID, now) + + run := freightRun(2431, userID, now) + createAndStartFreightRun(t, store, run, "delete-conflict", "4") + if err := store.CompleteFreightSync( + ctx, + run, + freightBatch(2432, "a", "b"), + now.Add(time.Second), + ); err != nil { + t.Fatalf("CompleteFreightSync() error = %v", err) + } + orders, _ := store.ListFreightOrders(ctx, "local-admin", 10) + detail, _ := store.GetFreightOrder( + ctx, + "local-admin", + orders[0].ID, + ) + procurement, err := usecase.NewProcurementService( + store, + procurementClock{now: now.Add(2 * time.Second)}, + &procurementIDs{next: 2440}, + ) + if err != nil { + t.Fatalf("NewProcurementService() error = %v", err) + } + if _, err := procurement.CreateRequest( + ctx, + usecase.CreateProcurementRequestCommand{ + CreatorSubject: "local-admin", + ActorUserID: userID, + FreightOrderItemID: detail.Items[0].ID, + ConfirmProcurementNeeded: true, + }, + ); err != nil { + t.Fatalf("CreateRequest() error = %v", err) + } + + keys, err := store.DeleteFreightOrder( + ctx, + "local-admin", + orders[0].ID, + ) + if !errors.Is(err, usecase.ErrFreightOrderInUse) || len(keys) != 0 { + t.Fatalf("conflicting delete = %#v, %v", keys, err) + } + for table, want := range map[string]int{ + "freight_orders": 1, + "freight_order_items": 2, + "procurement_requests": 1, + } { + var count int + if err := db.QueryRow("SELECT count(*) FROM " + table).Scan( + &count, + ); err != nil || count != want { + t.Fatalf("%s count = %d, %v", table, count, err) + } + } +} diff --git a/backend-api/internal/repository/sqlite/freight_repository.go b/backend-api/internal/repository/sqlite/freight_repository.go index d73e0f4..6b20172 100644 --- a/backend-api/internal/repository/sqlite/freight_repository.go +++ b/backend-api/internal/repository/sqlite/freight_repository.go @@ -617,6 +617,112 @@ func (store *Store) GetFreightOrder( return domain.FreightOrderDetail{Order: order, Items: items}, nil } +func (store *Store) DeleteFreightOrder( + ctx context.Context, + creatorSubject, orderID string, +) ([]string, error) { + tx, err := store.db.BeginTx(ctx, nil) + if err != nil { + return nil, repositoryFailure(err) + } + defer tx.Rollback() + + var storedOrderID string + err = tx.QueryRowContext( + ctx, + `SELECT id FROM freight_orders + WHERE creator_subject = ? AND id = ?`, + creatorSubject, + orderID, + ).Scan(&storedOrderID) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, repositoryFailure(err) + } + + var inUse int + err = tx.QueryRowContext( + ctx, + `SELECT EXISTS ( + SELECT 1 + FROM freight_order_items AS item + JOIN procurement_requests AS request + ON request.freight_order_item_id = item.id + WHERE item.freight_order_id = ? + )`, + storedOrderID, + ).Scan(&inUse) + if err != nil { + return nil, repositoryFailure(err) + } + if inUse != 0 { + return nil, usecase.ErrFreightOrderInUse + } + + rows, err := tx.QueryContext( + ctx, + `SELECT image.storage_key + FROM freight_item_images AS image + JOIN freight_order_items AS item + ON item.id = image.freight_order_item_id + WHERE item.freight_order_id = ? + AND image.status = 'READY' + AND image.storage_key IS NOT NULL + ORDER BY image.storage_key`, + storedOrderID, + ) + if err != nil { + return nil, repositoryFailure(err) + } + storageKeys := make([]string, 0) + for rows.Next() { + var storageKey string + if err := rows.Scan(&storageKey); err != nil { + rows.Close() + return nil, repositoryFailure(err) + } + storageKeys = append(storageKeys, storageKey) + } + if err := rows.Err(); err != nil { + rows.Close() + return nil, repositoryFailure(err) + } + if err := rows.Close(); err != nil { + return nil, repositoryFailure(err) + } + + if _, err := tx.ExecContext( + ctx, + `DELETE FROM freight_order_items WHERE freight_order_id = ?`, + storedOrderID, + ); err != nil { + return nil, repositoryFailure(err) + } + result, err := tx.ExecContext( + ctx, + `DELETE FROM freight_orders + WHERE creator_subject = ? AND id = ?`, + creatorSubject, + storedOrderID, + ) + if err != nil { + return nil, repositoryFailure(err) + } + deleted, err := result.RowsAffected() + if err != nil { + return nil, repositoryFailure(err) + } + if deleted != 1 { + return nil, repositoryFailure(usecase.ErrRepositoryInvariant) + } + if err := tx.Commit(); err != nil { + return nil, repositoryFailure(err) + } + return storageKeys, nil +} + const freightSyncSelect = `SELECT id, creator_subject, created_by_user_id, mode, order_number, created_from, created_to, watermark_through, query_sha256, status, diff --git a/backend-api/internal/transport/httpapi/admin_handlers.go b/backend-api/internal/transport/httpapi/admin_handlers.go index eb26117..ceadbae 100644 --- a/backend-api/internal/transport/httpapi/admin_handlers.go +++ b/backend-api/internal/transport/httpapi/admin_handlers.go @@ -75,6 +75,7 @@ func registerAdminAPI(routes gin.IRoutes, services AdminServices) error { ) routes.GET("/api/v1/freight-orders", handler.listFreightOrders) routes.GET("/api/v1/freight-orders/:id", handler.freightOrderDetail) + routes.DELETE("/api/v1/freight-orders/:id", handler.deleteFreightOrder) } if services.FreightImages != nil { routes.GET( diff --git a/backend-api/internal/transport/httpapi/admin_handlers_test.go b/backend-api/internal/transport/httpapi/admin_handlers_test.go index d53f9b5..8c7b3e3 100644 --- a/backend-api/internal/transport/httpapi/admin_handlers_test.go +++ b/backend-api/internal/transport/httpapi/admin_handlers_test.go @@ -534,6 +534,92 @@ func TestAdminFreightAPIImportsAllItemsWithoutPII(t *testing.T) { !strings.Contains(replay.Body.String(), createBody.Sync.ID) { t.Fatalf("replay status/body = %d / %s", replay.Code, replay.Body) } + deletedOrderID := listBody.Items[0].ID + deletedImageURL := detailBody.Items[0].ImageURL + deleteResponse := performAdminRequest( + t, + router, + http.MethodDelete, + "/api/v1/freight-orders/"+deletedOrderID, + "", + nil, + "", + ) + if deleteResponse.Code != http.StatusNoContent || + deleteResponse.Body.Len() != 0 || + deleteResponse.Header().Get("Cache-Control") != "no-store" { + t.Fatalf( + "delete status/body/headers = %d / %s / %#v", + deleteResponse.Code, + deleteResponse.Body, + deleteResponse.Header(), + ) + } + deletedDetail := performAdminRequest( + t, + router, + http.MethodGet, + "/api/v1/freight-orders/"+deletedOrderID, + "", + nil, + "", + ) + deletedImage := performAdminRequest( + t, + router, + http.MethodGet, + deletedImageURL, + "", + nil, + "", + ) + repeatedDelete := performAdminRequest( + t, + router, + http.MethodDelete, + "/api/v1/freight-orders/"+deletedOrderID, + "", + nil, + "", + ) + if deletedDetail.Code != http.StatusNotFound || + deletedImage.Code != http.StatusNotFound || + repeatedDelete.Code != http.StatusNoContent { + t.Fatalf( + "after delete detail/image/replay = %d / %d / %d", + deletedDetail.Code, + deletedImage.Code, + repeatedDelete.Code, + ) + } + reimport := performAdminRequest( + t, + router, + http.MethodPost, + "/api/v1/freight-syncs", + "application/json", + strings.NewReader( + `{"mode":"ORDER_NUMBER","order_number":"SOURCE-12"}`, + ), + "freight-sync-after-delete", + ) + if reimport.Code != http.StatusCreated { + t.Fatalf("reimport status/body = %d / %s", reimport.Code, reimport.Body) + } + reimportedList := performAdminRequest( + t, + router, + http.MethodGet, + "/api/v1/freight-orders", + "", + nil, + "", + ) + decodeResponse(t, reimportedList, &listBody) + if len(listBody.Items) != 1 || + listBody.Items[0].ID == deletedOrderID { + t.Fatalf("reimported orders = %+v", listBody.Items) + } } func TestAdminFreightDateSyncAdvancesInspectableWatermark(t *testing.T) { @@ -687,6 +773,26 @@ func TestAdminProcurementAPIProducesImmutablePendingTask(t *testing.T) { createRequest.Body, ) } + deleteInUse := performAdminRequest( + t, + fixture.router, + http.MethodDelete, + "/api/v1/freight-orders/"+orderBody.Items[0].ID, + "", + nil, + "", + ) + if deleteInUse.Code != http.StatusConflict || + !strings.Contains( + deleteInUse.Body.String(), + `"code":"FREIGHT_ORDER_IN_USE"`, + ) { + t.Fatalf( + "delete in-use status/body = %d / %s", + deleteInUse.Code, + deleteInUse.Body, + ) + } var requestBody struct { Request struct { ID string `json:"id"` diff --git a/backend-api/internal/transport/httpapi/freight_handlers.go b/backend-api/internal/transport/httpapi/freight_handlers.go index e1e5c63..f3ede97 100644 --- a/backend-api/internal/transport/httpapi/freight_handlers.go +++ b/backend-api/internal/transport/httpapi/freight_handlers.go @@ -255,6 +255,23 @@ func (h *adminHandlers) freightOrderDetail(ctx *gin.Context) { ctx.JSON(http.StatusOK, response) } +func (h *adminHandlers) deleteFreightOrder(ctx *gin.Context) { + err := h.services.Freight.DeleteOrder( + ctx.Request.Context(), + usecase.DeleteFreightOrderCommand{ + CreatorSubject: localAdminSubject, + ActorUserID: adminActorUserID(ctx), + OrderID: ctx.Param("id"), + }, + ) + if err != nil { + writeUsecaseError(ctx, err) + return + } + ctx.Header("Cache-Control", "no-store") + ctx.Status(http.StatusNoContent) +} + func freightSyncResponse(run domain.FreightSyncRun) gin.H { return gin.H{ "id": run.ID, diff --git a/backend-api/internal/transport/webui/handler.go b/backend-api/internal/transport/webui/handler.go index a290783..31ab968 100644 --- a/backend-api/internal/transport/webui/handler.go +++ b/backend-api/internal/transport/webui/handler.go @@ -89,6 +89,11 @@ func (h *Handler) RegisterProtected(routes gin.IRoutes) { routes.GET("/freight/import", SecurityHeaders(), h.ImportFreight) routes.POST("/freight/import", SecurityHeaders(), h.CreateFreightImport) routes.GET("/freight/:id", SecurityHeaders(), h.FreightDetail) + routes.POST( + "/freight/:id/delete", + SecurityHeaders(), + h.DeleteFreightOrder, + ) } if _, ok := h.service.(ProcurementService); ok { routes.POST( @@ -527,6 +532,57 @@ func (h *Handler) FreightDetail(ctx *gin.Context) { }) } +func (h *Handler) DeleteFreightOrder(ctx *gin.Context) { + ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 16<<10) + if err := ctx.Request.ParseForm(); err != nil || !validCSRF(ctx) { + h.renderError( + ctx, + http.StatusForbidden, + "请求已失效", + "请返回货运详情后重新操作。", + ) + return + } + if ctx.PostForm("confirm_delete") != "1" { + h.renderError( + ctx, + http.StatusUnprocessableEntity, + "必须确认删除", + "请确认只删除本系统中的货运信息后再提交。", + ) + return + } + orderID := strings.TrimSpace(ctx.Param("id")) + service := h.service.(FreightService) + err := service.DeleteFreightOrder( + ctx.Request.Context(), + DeleteFreightOrderInput{ + ActorUserID: actorUserID(ctx.Request.Context()), + OrderID: orderID, + }, + ) + if errors.Is(err, ErrFreightOrderInUse) { + ctx.Redirect( + http.StatusSeeOther, + "/freight/"+pathEscape(orderID)+"?notice=delete-conflict", + ) + return + } + if err != nil { + h.renderServiceError( + ctx, + err, + "货运单删除失败,请稍后重试。", + ) + return + } + h.logEvent( + "freight_order_deleted order_id=" + orderID + + " actor_user_id=" + actorUserID(ctx.Request.Context()), + ) + ctx.Redirect(http.StatusSeeOther, "/freight?notice=deleted") +} + func (h *Handler) CreateFreightProcurementRequest(ctx *gin.Context) { ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 16<<10) if err := ctx.Request.ParseForm(); err != nil || !validCSRF(ctx) { @@ -662,6 +718,10 @@ func freightNotice(value string) string { return "需求状态或来源已变化,当前不能生成任务。" case "import-succeeded": return "ERP 货运单已同步,货运信息和商品明细已更新。" + case "deleted": + return "货运单已从本系统删除,不影响顺运宝 ERP。" + case "delete-conflict": + return "该货运单已进入采购流程,不能删除。" default: return "" } diff --git a/backend-api/internal/transport/webui/handler_test.go b/backend-api/internal/transport/webui/handler_test.go index a00a26e..6244205 100644 --- a/backend-api/internal/transport/webui/handler_test.go +++ b/backend-api/internal/transport/webui/handler_test.go @@ -1301,6 +1301,102 @@ func TestFreightDetailCreatesProcurementTaskWithCSRF(t *testing.T) { } } +func TestFreightDeleteRequiresConfirmationAndHandlesConflict(t *testing.T) { + service := &fakeFreightService{ + fakeService: &fakeService{}, + orderDetail: FreightOrderDetail{ + Order: FreightOrder{ + ID: testTaskID, + ExternalStockID: "12", + SourceCode: "SOURCE-12", + }, + }, + } + logged := make([]string, 0) + router := newTestRouterWithLogger( + t, + service, + func(value string) { + logged = append(logged, value) + }, + ) + detail := performRequest( + t, + router, + http.MethodGet, + "/freight/"+testTaskID, + nil, + "", + ) + if detail.Code != http.StatusOK || + !strings.Contains(detail.Body.String(), "删除本地货运单") || + !strings.Contains(detail.Body.String(), "data-freight-delete-dialog") { + t.Fatalf("delete controls status/body = %d / %s", detail.Code, detail.Body) + } + cookie := csrfCookie(t, detail) + + withoutCSRF := performRequest( + t, + router, + http.MethodPost, + "/freight/"+testTaskID+"/delete", + strings.NewReader("confirm_delete=1"), + "application/x-www-form-urlencoded", + ) + if withoutCSRF.Code != http.StatusForbidden { + t.Fatalf("delete without CSRF status = %d", withoutCSRF.Code) + } + + values := url.Values{ + "csrf_token": {cookie.Value}, + "confirm_delete": {"1"}, + } + request := httptest.NewRequest( + http.MethodPost, + "/freight/"+testTaskID+"/delete", + strings.NewReader(values.Encode()), + ) + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + request.AddCookie(cookie) + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + if response.Code != http.StatusSeeOther || + response.Header().Get("Location") != "/freight?notice=deleted" || + service.deleteInput.OrderID != testTaskID || + len(logged) != 1 || + !strings.Contains(logged[0], "freight_order_deleted order_id=") { + t.Fatalf( + "delete response/input/log = %d / %q / %+v / %#v", + response.Code, + response.Header().Get("Location"), + service.deleteInput, + logged, + ) + } + + service.deleteErr = ErrFreightOrderInUse + request = httptest.NewRequest( + http.MethodPost, + "/freight/"+testTaskID+"/delete", + strings.NewReader(values.Encode()), + ) + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + request.AddCookie(cookie) + response = httptest.NewRecorder() + router.ServeHTTP(response, request) + if response.Code != http.StatusSeeOther || + response.Header().Get("Location") != + "/freight/"+testTaskID+"?notice=delete-conflict" || + len(logged) != 1 { + t.Fatalf( + "conflicting delete status/location/log = %d / %q / %#v", + response.Code, + response.Header().Get("Location"), + logged, + ) + } +} + func TestFreightPresentationFormatsMoneyAndImageStates(t *testing.T) { value := int64(12905) if got := formatMinorCurrency(&value, "TWD"); got != "TWD 129.05" { @@ -1348,6 +1444,8 @@ type fakeFreightService struct { *fakeService orders []FreightOrder orderDetail FreightOrderDetail + deleteInput DeleteFreightOrderInput + deleteErr error sync FreightSync createInput CreateFreightSyncInput createResult FreightSync @@ -1448,6 +1546,14 @@ func (service *fakeFreightService) GetFreightOrder( return service.orderDetail, service.err } +func (service *fakeFreightService) DeleteFreightOrder( + _ context.Context, + input DeleteFreightOrderInput, +) error { + service.deleteInput = input + return service.deleteErr +} + func (service *fakeFreightService) GetFreightSync( context.Context, string, diff --git a/backend-api/internal/transport/webui/static/admin.js b/backend-api/internal/transport/webui/static/admin.js index 3b8c6d1..ceeb6cc 100644 --- a/backend-api/internal/transport/webui/static/admin.js +++ b/backend-api/internal/transport/webui/static/admin.js @@ -119,4 +119,23 @@ openCancel.focus(); }); } + + const openFreightDelete = document.querySelector( + "[data-open-freight-delete]", + ); + const freightDeleteDialog = document.querySelector( + "[data-freight-delete-dialog]", + ); + if (openFreightDelete && freightDeleteDialog) { + openFreightDelete.addEventListener("click", () => { + freightDeleteDialog.showModal(); + freightDeleteDialog.querySelector("[data-keep-freight-order]").focus(); + }); + freightDeleteDialog + .querySelector("[data-keep-freight-order]") + .addEventListener("click", () => { + freightDeleteDialog.close(); + openFreightDelete.focus(); + }); + } })(); diff --git a/backend-api/internal/transport/webui/templates/freight-detail.gohtml b/backend-api/internal/transport/webui/templates/freight-detail.gohtml index 8d25a02..6906522 100644 --- a/backend-api/internal/transport/webui/templates/freight-detail.gohtml +++ b/backend-api/internal/transport/webui/templates/freight-detail.gohtml @@ -13,7 +13,11 @@
ERP ID:{{.Detail.Order.ExternalStockID}} · 来源版本 {{.Detail.Order.Revision}}
- 返回列表 +