fix: add custom action for fixing broken date/times (#268)

This commit is contained in:
Hayden 2023-02-08 17:59:04 -09:00 committed by GitHub
parent bd933af874
commit ce2fc7712a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 240 additions and 59 deletions

View file

@ -9,15 +9,15 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
type EnsureAssetIDResult struct { type ActionAmountResult struct {
Completed int `json:"completed"` Completed int `json:"completed"`
} }
// HandleGroupInvitationsCreate godoc // HandleGroupInvitationsCreate godoc
// @Summary Get the current user // @Summary Ensures all items in the database have an asset id
// @Tags Group // @Tags Group
// @Produce json // @Produce json
// @Success 200 {object} EnsureAssetIDResult // @Success 200 {object} ActionAmountResult
// @Router /v1/actions/ensure-asset-ids [Post] // @Router /v1/actions/ensure-asset-ids [Post]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleEnsureAssetID() server.HandlerFunc { func (ctrl *V1Controller) HandleEnsureAssetID() server.HandlerFunc {
@ -30,6 +30,27 @@ func (ctrl *V1Controller) HandleEnsureAssetID() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) 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})
} }
} }

View file

@ -88,6 +88,7 @@ func (a *app) mountRoutes(repos *repo.AllRepos) {
a.server.Put(v1Base("/groups"), v1Ctrl.HandleGroupUpdate(), userMW...) a.server.Put(v1Base("/groups"), v1Ctrl.HandleGroupUpdate(), userMW...)
a.server.Post(v1Base("/actions/ensure-asset-ids"), v1Ctrl.HandleEnsureAssetID(), 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.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll(), userMW...)
a.server.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate(), userMW...) a.server.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate(), userMW...)

View file

@ -34,12 +34,36 @@ const docTemplate = `{
"tags": [ "tags": [
"Group" "Group"
], ],
"summary": "Get the current user", "summary": "Ensures all items in the database have an asset id",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "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": { "v1.ApiSummary": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -2416,14 +2448,6 @@ const docTemplate = `{
} }
} }
}, },
"v1.EnsureAssetIDResult": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.GroupInvitation": { "v1.GroupInvitation": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -26,12 +26,36 @@
"tags": [ "tags": [
"Group" "Group"
], ],
"summary": "Get the current user", "summary": "Ensures all items in the database have an asset id",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "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": { "v1.ApiSummary": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -2408,14 +2440,6 @@
} }
} }
}, },
"v1.EnsureAssetIDResult": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.GroupInvitation": { "v1.GroupInvitation": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -556,6 +556,11 @@ definitions:
token: token:
type: string type: string
type: object type: object
v1.ActionAmountResult:
properties:
completed:
type: integer
type: object
v1.ApiSummary: v1.ApiSummary:
properties: properties:
build: build:
@ -589,11 +594,6 @@ definitions:
new: new:
type: string type: string
type: object type: object
v1.EnsureAssetIDResult:
properties:
completed:
type: integer
type: object
v1.GroupInvitation: v1.GroupInvitation:
properties: properties:
expiresAt: expiresAt:
@ -643,10 +643,24 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/v1.EnsureAssetIDResult' $ref: '#/definitions/v1.ActionAmountResult'
security: security:
- Bearer: [] - 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: tags:
- Group - Group
/v1/assets/{id}: /v1/assets/{id}:

View file

@ -673,3 +673,56 @@ func (e *ItemsRepository) GetAllCustomFieldNames(ctx context.Context, GID uuid.U
return fieldNames, nil 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
}

View file

@ -5,3 +5,4 @@
.btn { .btn {
text-transform: none !important; text-transform: none !important;
} }

View file

@ -31,7 +31,11 @@
const selected = computed({ const selected = computed({
get() { get() {
// return modelValue as string as YYYY-MM-DD or null // 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) { set(value: string | null) {
// emit update:modelValue with a Date object or null // emit update:modelValue with a Date object or null

View file

@ -1,10 +1,16 @@
import { BaseAPI, route } from "../base"; import { BaseAPI, route } from "../base";
import { EnsureAssetIDResult } from "../types/data-contracts"; import { ActionAmountResult } from "../types/data-contracts";
export class ActionsAPI extends BaseAPI { export class ActionsAPI extends BaseAPI {
ensureAssetIDs() { ensureAssetIDs() {
return this.http.post<void, EnsureAssetIDResult>({ return this.http.post<void, ActionAmountResult>({
url: route("/actions/ensure-asset-ids"), url: route("/actions/ensure-asset-ids"),
}); });
} }
resetItemDateTimes() {
return this.http.post<void, ActionAmountResult>({
url: route("/actions/zero-item-time-fields"),
});
}
} }

View file

@ -17,7 +17,7 @@ export interface DocumentOut {
} }
export interface Group { export interface Group {
createdAt: string; createdAt: Date;
currency: string; currency: string;
id: string; id: string;
name: string; name: string;
@ -39,7 +39,7 @@ export interface GroupUpdate {
} }
export interface ItemAttachment { export interface ItemAttachment {
createdAt: string; createdAt: Date;
document: DocumentOut; document: DocumentOut;
id: string; id: string;
type: string; type: string;
@ -76,7 +76,7 @@ export interface ItemOut {
assetId: string; assetId: string;
attachments: ItemAttachment[]; attachments: ItemAttachment[];
children: ItemSummary[]; children: ItemSummary[];
createdAt: string; createdAt: Date;
description: string; description: string;
fields: ItemField[]; fields: ItemField[];
id: string; id: string;
@ -96,7 +96,7 @@ export interface ItemOut {
/** @example "0" */ /** @example "0" */
purchasePrice: string; purchasePrice: string;
/** Purchase */ /** Purchase */
purchaseTime: Date; purchaseTime: string;
quantity: number; quantity: number;
serialNumber: string; serialNumber: string;
soldNotes: string; soldNotes: string;
@ -112,7 +112,7 @@ export interface ItemOut {
export interface ItemSummary { export interface ItemSummary {
archived: boolean; archived: boolean;
createdAt: string; createdAt: Date;
description: string; description: string;
id: string; id: string;
insured: boolean; insured: boolean;
@ -148,7 +148,7 @@ export interface ItemUpdate {
/** @example "0" */ /** @example "0" */
purchasePrice: string; purchasePrice: string;
/** Purchase */ /** Purchase */
purchaseTime: Date; purchaseTime: string;
quantity: number; quantity: number;
/** Identifications */ /** Identifications */
serialNumber: string; serialNumber: string;
@ -169,7 +169,7 @@ export interface LabelCreate {
} }
export interface LabelOut { export interface LabelOut {
createdAt: string; createdAt: Date;
description: string; description: string;
id: string; id: string;
items: ItemSummary[]; items: ItemSummary[];
@ -178,7 +178,7 @@ export interface LabelOut {
} }
export interface LabelSummary { export interface LabelSummary {
createdAt: string; createdAt: Date;
description: string; description: string;
id: string; id: string;
name: string; name: string;
@ -193,7 +193,7 @@ export interface LocationCreate {
export interface LocationOut { export interface LocationOut {
children: LocationSummary[]; children: LocationSummary[];
createdAt: string; createdAt: Date;
description: string; description: string;
id: string; id: string;
items: ItemSummary[]; items: ItemSummary[];
@ -203,7 +203,7 @@ export interface LocationOut {
} }
export interface LocationOutCount { export interface LocationOutCount {
createdAt: string; createdAt: Date;
description: string; description: string;
id: string; id: string;
itemCount: number; itemCount: number;
@ -212,7 +212,7 @@ export interface LocationOutCount {
} }
export interface LocationSummary { export interface LocationSummary {
createdAt: string; createdAt: Date;
description: string; description: string;
id: string; id: string;
name: string; name: string;
@ -229,7 +229,7 @@ export interface LocationUpdate {
export interface MaintenanceEntry { export interface MaintenanceEntry {
/** @example "0" */ /** @example "0" */
cost: string; cost: string;
date: Date; date: string;
description: string; description: string;
id: string; id: string;
name: string; name: string;
@ -238,7 +238,7 @@ export interface MaintenanceEntry {
export interface MaintenanceEntryCreate { export interface MaintenanceEntryCreate {
/** @example "0" */ /** @example "0" */
cost: string; cost: string;
date: Date; date: string;
description: string; description: string;
name: string; name: string;
} }
@ -246,7 +246,7 @@ export interface MaintenanceEntryCreate {
export interface MaintenanceEntryUpdate { export interface MaintenanceEntryUpdate {
/** @example "0" */ /** @example "0" */
cost: string; cost: string;
date: Date; date: string;
description: string; description: string;
name: string; name: string;
} }
@ -258,7 +258,7 @@ export interface MaintenanceLog {
itemId: string; itemId: string;
} }
export interface PaginationResultRepoItemSummary { export interface PaginationResultItemSummary {
items: ItemSummary[]; items: ItemSummary[];
page: number; page: number;
pageSize: number; pageSize: number;
@ -302,7 +302,7 @@ export interface ValueOverTime {
} }
export interface ValueOverTimeEntry { export interface ValueOverTimeEntry {
date: Date; date: string;
name: string; name: string;
value: number; value: number;
} }
@ -330,6 +330,10 @@ export interface UserRegistration {
token: string; token: string;
} }
export interface ActionAmountResult {
completed: number;
}
export interface ApiSummary { export interface ApiSummary {
build: Build; build: Build;
demo: boolean; demo: boolean;
@ -350,18 +354,14 @@ export interface ChangePassword {
new: string; new: string;
} }
export interface EnsureAssetIDResult {
completed: number;
}
export interface GroupInvitation { export interface GroupInvitation {
expiresAt: Date; expiresAt: string;
token: string; token: string;
uses: number; uses: number;
} }
export interface GroupInvitationCreate { export interface GroupInvitationCreate {
expiresAt: Date; expiresAt: string;
uses: number; uses: number;
} }
@ -371,6 +371,6 @@ export interface ItemAttachmentToken {
export interface TokenResponse { export interface TokenResponse {
attachmentToken: string; attachmentToken: string;
expiresAt: Date; expiresAt: string;
token: string; token: string;
} }

View file

@ -180,6 +180,25 @@
notify.success(`${result.data.completed} assets have been updated.`); 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.`);
}
</script> </script>
<template> <template>
@ -313,7 +332,7 @@
</template> </template>
</BaseSectionHeader> </BaseSectionHeader>
<div class="py-4 border-t-2 border-gray-300"> <div class="py-4 border-t-2 border-gray-300 space-y-8">
<div class="grid grid-cols-1 md:grid-cols-4 gap-10"> <div class="grid grid-cols-1 md:grid-cols-4 gap-10">
<div class="col-span-3"> <div class="col-span-3">
<h4>Manage Asset IDs</h4> <h4>Manage Asset IDs</h4>
@ -325,6 +344,20 @@
</div> </div>
<BaseButton class="btn-primary mt-auto" @click="ensureAssetIDs"> Ensure Asset IDs </BaseButton> <BaseButton class="btn-primary mt-auto" @click="ensureAssetIDs"> Ensure Asset IDs </BaseButton>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-10">
<div class="col-span-3">
<h4>Zero Item Date Times</h4>
<p class="text-sm">
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.
<a class="link" href="https://github.com/hay-kot/homebox/issues/236" target="_blank">
See Github Issue #236 for more details
</a>
</p>
</div>
<BaseButton class="btn-primary mt-auto" @click="resetItemDateTimes"> Zero Item Date Times </BaseButton>
</div>
</div> </div>
</template> </template>
</BaseCard> </BaseCard>