diff --git a/backend/app/api/handlers/v1/v1_ctrl_actions.go b/backend/app/api/handlers/v1/v1_ctrl_actions.go index 37e2b72..ea490c0 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_actions.go +++ b/backend/app/api/handlers/v1/v1_ctrl_actions.go @@ -9,15 +9,15 @@ import ( "github.com/rs/zerolog/log" ) -type EnsureAssetIDResult struct { +type ActionAmountResult struct { Completed int `json:"completed"` } // HandleGroupInvitationsCreate godoc -// @Summary Get the current user +// @Summary Ensures all items in the database have an asset id // @Tags Group // @Produce json -// @Success 200 {object} EnsureAssetIDResult +// @Success 200 {object} ActionAmountResult // @Router /v1/actions/ensure-asset-ids [Post] // @Security Bearer func (ctrl *V1Controller) HandleEnsureAssetID() server.HandlerFunc { @@ -30,6 +30,27 @@ func (ctrl *V1Controller) HandleEnsureAssetID() server.HandlerFunc { return validate.NewRequestError(err, http.StatusInternalServerError) } - return server.Respond(w, http.StatusOK, EnsureAssetIDResult{Completed: totalCompleted}) + return server.Respond(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted}) + } +} + +// HandleItemDateZeroOut godoc +// @Summary Resets all item date fields to the beginning of the day +// @Tags Group +// @Produce json +// @Success 200 {object} ActionAmountResult +// @Router /v1/actions/zero-item-time-fields [Post] +// @Security Bearer +func (ctrl *V1Controller) HandleItemDateZeroOut() server.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) error { + ctx := services.NewContext(r.Context()) + + totalCompleted, err := ctrl.repo.Items.ZeroOutTimeFields(ctx, ctx.GID) + if err != nil { + log.Err(err).Msg("failed to ensure asset id") + return validate.NewRequestError(err, http.StatusInternalServerError) + } + + return server.Respond(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted}) } } diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index 04e4066..820de7d 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -88,6 +88,7 @@ func (a *app) mountRoutes(repos *repo.AllRepos) { a.server.Put(v1Base("/groups"), v1Ctrl.HandleGroupUpdate(), userMW...) a.server.Post(v1Base("/actions/ensure-asset-ids"), v1Ctrl.HandleEnsureAssetID(), userMW...) + a.server.Post(v1Base("/actions/zero-item-time-fields"), v1Ctrl.HandleItemDateZeroOut(), userMW...) a.server.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll(), userMW...) a.server.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate(), userMW...) diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index 0fcf591..5ea1b82 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -34,12 +34,36 @@ const docTemplate = `{ "tags": [ "Group" ], - "summary": "Get the current user", + "summary": "Ensures all items in the database have an asset id", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/v1.EnsureAssetIDResult" + "$ref": "#/definitions/v1.ActionAmountResult" + } + } + } + } + }, + "/v1/actions/zero-item-time-fields": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Resets all item date fields to the beginning of the day", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ActionAmountResult" } } } @@ -2365,6 +2389,14 @@ const docTemplate = `{ } } }, + "v1.ActionAmountResult": { + "type": "object", + "properties": { + "completed": { + "type": "integer" + } + } + }, "v1.ApiSummary": { "type": "object", "properties": { @@ -2416,14 +2448,6 @@ const docTemplate = `{ } } }, - "v1.EnsureAssetIDResult": { - "type": "object", - "properties": { - "completed": { - "type": "integer" - } - } - }, "v1.GroupInvitation": { "type": "object", "properties": { diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index 74bfeaa..d39650d 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -26,12 +26,36 @@ "tags": [ "Group" ], - "summary": "Get the current user", + "summary": "Ensures all items in the database have an asset id", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/v1.EnsureAssetIDResult" + "$ref": "#/definitions/v1.ActionAmountResult" + } + } + } + } + }, + "/v1/actions/zero-item-time-fields": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Resets all item date fields to the beginning of the day", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ActionAmountResult" } } } @@ -2357,6 +2381,14 @@ } } }, + "v1.ActionAmountResult": { + "type": "object", + "properties": { + "completed": { + "type": "integer" + } + } + }, "v1.ApiSummary": { "type": "object", "properties": { @@ -2408,14 +2440,6 @@ } } }, - "v1.EnsureAssetIDResult": { - "type": "object", - "properties": { - "completed": { - "type": "integer" - } - } - }, "v1.GroupInvitation": { "type": "object", "properties": { diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index fc3b9c1..be985fd 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -556,6 +556,11 @@ definitions: token: type: string type: object + v1.ActionAmountResult: + properties: + completed: + type: integer + type: object v1.ApiSummary: properties: build: @@ -589,11 +594,6 @@ definitions: new: type: string type: object - v1.EnsureAssetIDResult: - properties: - completed: - type: integer - type: object v1.GroupInvitation: properties: expiresAt: @@ -643,10 +643,24 @@ paths: "200": description: OK schema: - $ref: '#/definitions/v1.EnsureAssetIDResult' + $ref: '#/definitions/v1.ActionAmountResult' security: - Bearer: [] - summary: Get the current user + summary: Ensures all items in the database have an asset id + tags: + - Group + /v1/actions/zero-item-time-fields: + post: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/v1.ActionAmountResult' + security: + - Bearer: [] + summary: Resets all item date fields to the beginning of the day tags: - Group /v1/assets/{id}: diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index a62d347..e5dd607 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -673,3 +673,56 @@ func (e *ItemsRepository) GetAllCustomFieldNames(ctx context.Context, GID uuid.U return fieldNames, nil } + +// ZeroOutTimeFields is a helper function that can be invoked via the UI by a group member which will +// set all date fields to the beginning of the day. +// +// This is designed to resolve a long-time bug that has since been fixed with the time selector on the +// frontend. This function is intended to be used as a one-time fix for existing databases and may be +// removed in the future. +func (e *ItemsRepository) ZeroOutTimeFields(ctx context.Context, GID uuid.UUID) (int, error) { + q := e.db.Item.Query().Where( + item.HasGroupWith(group.ID(GID)), + item.Or( + item.PurchaseTimeNotNil(), + item.SoldTimeNotNil(), + item.WarrantyExpiresNotNil(), + ), + ) + + items, err := q.All(ctx) + if err != nil { + return -1, fmt.Errorf("ZeroOutTimeFields() -> failed to get items: %w", err) + } + + toDateOnly := func(t time.Time) time.Time { + return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) + } + + updated := 0 + + for _, i := range items { + updateQ := e.db.Item.Update().Where(item.ID(i.ID)) + + if !i.PurchaseTime.IsZero() { + updateQ.SetPurchaseTime(toDateOnly(i.PurchaseTime)) + } + + if !i.SoldTime.IsZero() { + updateQ.SetSoldTime(toDateOnly(i.SoldTime)) + } + + if !i.WarrantyExpires.IsZero() { + updateQ.SetWarrantyExpires(toDateOnly(i.WarrantyExpires)) + } + + _, err = updateQ.Save(ctx) + if err != nil { + return updated, fmt.Errorf("ZeroOutTimeFields() -> failed to update item: %w", err) + } + + updated++ + } + + return updated, nil +} diff --git a/frontend/assets/css/main.css b/frontend/assets/css/main.css index d83faf6..f76ff5f 100644 --- a/frontend/assets/css/main.css +++ b/frontend/assets/css/main.css @@ -4,4 +4,5 @@ .btn { text-transform: none !important; -} \ No newline at end of file +} + diff --git a/frontend/components/Form/DatePicker.vue b/frontend/components/Form/DatePicker.vue index f216cc7..d01381f 100644 --- a/frontend/components/Form/DatePicker.vue +++ b/frontend/components/Form/DatePicker.vue @@ -31,7 +31,11 @@ const selected = computed({ get() { // return modelValue as string as YYYY-MM-DD or null - return props.modelValue ? props.modelValue.toISOString().split("T")[0] : null; + if (validDate(props.modelValue)) { + return props.modelValue ? props.modelValue.toISOString().split("T")[0] : null; + } + + return null; }, set(value: string | null) { // emit update:modelValue with a Date object or null diff --git a/frontend/lib/api/classes/actions.ts b/frontend/lib/api/classes/actions.ts index be892b3..a65e059 100644 --- a/frontend/lib/api/classes/actions.ts +++ b/frontend/lib/api/classes/actions.ts @@ -1,10 +1,16 @@ import { BaseAPI, route } from "../base"; -import { EnsureAssetIDResult } from "../types/data-contracts"; +import { ActionAmountResult } from "../types/data-contracts"; export class ActionsAPI extends BaseAPI { ensureAssetIDs() { - return this.http.post({ + return this.http.post({ url: route("/actions/ensure-asset-ids"), }); } + + resetItemDateTimes() { + return this.http.post({ + url: route("/actions/zero-item-time-fields"), + }); + } } diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index e461e89..0a8538a 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -17,7 +17,7 @@ export interface DocumentOut { } export interface Group { - createdAt: string; + createdAt: Date; currency: string; id: string; name: string; @@ -39,7 +39,7 @@ export interface GroupUpdate { } export interface ItemAttachment { - createdAt: string; + createdAt: Date; document: DocumentOut; id: string; type: string; @@ -76,7 +76,7 @@ export interface ItemOut { assetId: string; attachments: ItemAttachment[]; children: ItemSummary[]; - createdAt: string; + createdAt: Date; description: string; fields: ItemField[]; id: string; @@ -96,7 +96,7 @@ export interface ItemOut { /** @example "0" */ purchasePrice: string; /** Purchase */ - purchaseTime: Date; + purchaseTime: string; quantity: number; serialNumber: string; soldNotes: string; @@ -112,7 +112,7 @@ export interface ItemOut { export interface ItemSummary { archived: boolean; - createdAt: string; + createdAt: Date; description: string; id: string; insured: boolean; @@ -148,7 +148,7 @@ export interface ItemUpdate { /** @example "0" */ purchasePrice: string; /** Purchase */ - purchaseTime: Date; + purchaseTime: string; quantity: number; /** Identifications */ serialNumber: string; @@ -169,7 +169,7 @@ export interface LabelCreate { } export interface LabelOut { - createdAt: string; + createdAt: Date; description: string; id: string; items: ItemSummary[]; @@ -178,7 +178,7 @@ export interface LabelOut { } export interface LabelSummary { - createdAt: string; + createdAt: Date; description: string; id: string; name: string; @@ -193,7 +193,7 @@ export interface LocationCreate { export interface LocationOut { children: LocationSummary[]; - createdAt: string; + createdAt: Date; description: string; id: string; items: ItemSummary[]; @@ -203,7 +203,7 @@ export interface LocationOut { } export interface LocationOutCount { - createdAt: string; + createdAt: Date; description: string; id: string; itemCount: number; @@ -212,7 +212,7 @@ export interface LocationOutCount { } export interface LocationSummary { - createdAt: string; + createdAt: Date; description: string; id: string; name: string; @@ -229,7 +229,7 @@ export interface LocationUpdate { export interface MaintenanceEntry { /** @example "0" */ cost: string; - date: Date; + date: string; description: string; id: string; name: string; @@ -238,7 +238,7 @@ export interface MaintenanceEntry { export interface MaintenanceEntryCreate { /** @example "0" */ cost: string; - date: Date; + date: string; description: string; name: string; } @@ -246,7 +246,7 @@ export interface MaintenanceEntryCreate { export interface MaintenanceEntryUpdate { /** @example "0" */ cost: string; - date: Date; + date: string; description: string; name: string; } @@ -258,7 +258,7 @@ export interface MaintenanceLog { itemId: string; } -export interface PaginationResultRepoItemSummary { +export interface PaginationResultItemSummary { items: ItemSummary[]; page: number; pageSize: number; @@ -302,7 +302,7 @@ export interface ValueOverTime { } export interface ValueOverTimeEntry { - date: Date; + date: string; name: string; value: number; } @@ -330,6 +330,10 @@ export interface UserRegistration { token: string; } +export interface ActionAmountResult { + completed: number; +} + export interface ApiSummary { build: Build; demo: boolean; @@ -350,18 +354,14 @@ export interface ChangePassword { new: string; } -export interface EnsureAssetIDResult { - completed: number; -} - export interface GroupInvitation { - expiresAt: Date; + expiresAt: string; token: string; uses: number; } export interface GroupInvitationCreate { - expiresAt: Date; + expiresAt: string; uses: number; } @@ -371,6 +371,6 @@ export interface ItemAttachmentToken { export interface TokenResponse { attachmentToken: string; - expiresAt: Date; + expiresAt: string; token: string; } diff --git a/frontend/pages/profile.vue b/frontend/pages/profile.vue index 10e84a4..a32df9c 100644 --- a/frontend/pages/profile.vue +++ b/frontend/pages/profile.vue @@ -180,6 +180,25 @@ notify.success(`${result.data.completed} assets have been updated.`); } + + async function resetItemDateTimes() { + const { isCanceled } = await confirm.open( + "Are you sure you want to reset all date and time values? This will take a while and cannot be undone." + ); + + if (isCanceled) { + return; + } + + const result = await api.actions.resetItemDateTimes(); + + if (result.error) { + notify.error("Failed to reset date and time values."); + return; + } + + notify.success(`${result.data.completed} assets have been updated.`); + } -
+

Manage Asset IDs

@@ -325,6 +344,20 @@
Ensure Asset IDs
+
+
+

Zero Item Date Times

+

+ Resets the time value for all date time fields in your inventory to the beginning of the date. This is + to fix a bug that was introduced early on in the development of the site that caused the time value to + be stored with the time which caused issues with date fields displaying accurate values. + + See Github Issue #236 for more details + +

+
+ Zero Item Date Times +