Compare commits

..

5 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
23da976494 Remove translation changes from non-English locale files
Only en.json should have the new translation keys. Translators will add translations to other language files later.

Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-12-27 22:29:18 +00:00
copilot-swe-agent[bot]
aa48c958d7 Improve error handling and comments in WipeInventory method
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-27 15:03:43 +00:00
copilot-swe-agent[bot]
2bd6d0a9e5 Add Wipe Inventory feature with backend and frontend implementation
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-27 14:52:04 +00:00
copilot-swe-agent[bot]
88275620f2 Initial plan for Wipe Inventory feature
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-27 14:47:42 +00:00
copilot-swe-agent[bot]
5a058250e6 Initial plan 2025-12-27 14:43:31 +00:00
30 changed files with 114 additions and 1682 deletions

View File

@@ -8,7 +8,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/sysadminsmedia/homebox/backend v0.0.0-20251212183312-2d1d3d927bfd h1:QULUJSgHc4rSlTjb2qYT6FIgwDWFCqEpnYqc/ltsrkk=
github.com/sysadminsmedia/homebox/backend v0.0.0-20251212183312-2d1d3d927bfd/go.mod h1:jB+tPmHtPDM1VnAjah0gvcRfP/s7c+rtQwpA8cvZD/U=
github.com/sysadminsmedia/homebox/backend v0.0.0-20251226222718-473027c1aea3 h1:O7Sy/SfxuqxaeR4kUK/sRhHPeKrmraszRyK7ROJZ7Qw=
github.com/sysadminsmedia/homebox/backend v0.0.0-20251226222718-473027c1aea3/go.mod h1:9zHHw5TNttw5Kn4Wks+SxwXmJPz6PgGNbnB4BtF1Z4c=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -94,3 +94,16 @@ func (ctrl *V1Controller) HandleSetPrimaryPhotos() errchain.HandlerFunc {
func (ctrl *V1Controller) HandleCreateMissingThumbnails() errchain.HandlerFunc {
return actionHandlerFactory("create missing thumbnails", ctrl.repo.Attachments.CreateMissingThumbnails)
}
// HandleWipeInventory godoc
//
// @Summary Wipe Inventory
// @Description Deletes all items in the inventory
// @Tags Actions
// @Produce json
// @Success 200 {object} ActionAmountResult
// @Router /v1/actions/wipe-inventory [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleWipeInventory() errchain.HandlerFunc {
return actionHandlerFactory("wipe inventory", ctrl.repo.Items.WipeInventory)
}

View File

@@ -108,6 +108,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
r.Post("/actions/ensure-import-refs", chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
r.Post("/actions/set-primary-photos", chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
r.Post("/actions/create-missing-thumbnails", chain.ToHandlerFunc(v1Ctrl.HandleCreateMissingThumbnails(), userMW...))
r.Post("/actions/wipe-inventory", chain.ToHandlerFunc(v1Ctrl.HandleWipeInventory(), userMW...))
r.Get("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
r.Post("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))

View File

@@ -10,31 +10,22 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/pubsub v1.50.0 h1:hnYpOIxVlgVD1Z8LN7est4DQZK3K6tvZNurZjIVjUe0=
cloud.google.com/go/pubsub v1.50.0/go.mod h1:Di2Y+nqXBpIS+dXUEJPQzLh8PbIQZMLE9IVUFhf2zmM=
cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM=
cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk=
cloud.google.com/go/pubsub/v2 v2.2.1 h1:3brZcshL3fIiD1qOxAE2QW9wxsfjioy014x4yC9XuYI=
cloud.google.com/go/pubsub/v2 v2.2.1/go.mod h1:O5f0KHG9zDheZAd3z5rlCRhxt2JQtB+t/IYLKK3Bpvw=
cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI=
cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU=
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U=
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk=
@@ -88,8 +79,6 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/ardanlabs/conf/v3 v3.9.0 h1:aRBYHeD39/OkuaEXYIEoi4wvF3OnS7jUAPxXyLfEu20=
github.com/ardanlabs/conf/v3 v3.9.0/go.mod h1:XlL9P0quWP4m1weOVFmlezabinbZLI05niDof/+Ochk=
github.com/ardanlabs/conf/v3 v3.10.0 h1:qIrJ/WBmH/hFQ/IX4xH9LX9LzwK44T9aEOy78M+4S+0=
github.com/ardanlabs/conf/v3 v3.10.0/go.mod h1:XlL9P0quWP4m1weOVFmlezabinbZLI05niDof/+Ochk=
github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
@@ -183,14 +172,10 @@ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzP
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gen2brain/avif v0.4.4 h1:Ga/ss7qcWWQm2bxFpnjYjhJsNfZrWs5RsyklgFjKRSE=
github.com/gen2brain/avif v0.4.4/go.mod h1:/XCaJcjZraQwKVhpu9aEd9aLOssYOawLvhMBtmHVGqk=
github.com/gen2brain/heic v0.4.6 h1:sNh3mfaEZLmDJnFc5WoLxCzh/wj5GwfJScPfvF5CNJE=
github.com/gen2brain/heic v0.4.6/go.mod h1:ECnpqbqLu0qSje4KSNWUUDK47UPXPzl80T27GWGEL5I=
github.com/gen2brain/heic v0.4.7 h1:xw/e9R3HdIvb+uEhRDMRJdviYnB3ODe/VwL8SYLaMGc=
github.com/gen2brain/heic v0.4.7/go.mod h1:ECnpqbqLu0qSje4KSNWUUDK47UPXPzl80T27GWGEL5I=
github.com/gen2brain/jpegxl v0.4.5 h1:TWpVEn5xkIfsswzkjHBArd0Cc9AE0tbjBSoa0jDsrbo=
@@ -210,16 +195,10 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8=
github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
@@ -249,8 +228,6 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
@@ -290,8 +267,6 @@ github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
@@ -362,8 +337,6 @@ github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak=
github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI=
github.com/nats-io/nats-server/v2 v2.9.23 h1:6Wj6H6QpP9FMlpCyWUaNu2yeZ/qGj+mdRkZ1wbikExU=
github.com/nats-io/nats-server/v2 v2.9.23/go.mod h1:wEjrEy9vnqIGE4Pqz4/c75v9Pmaq7My2IgFmnykc4C0=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
@@ -380,8 +353,6 @@ github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.23 h1:oJE7T90aYBGtFNrI8+KbETnPymobAhzRrR8Mu8n1yfU=
github.com/pierrec/lz4/v4 v4.1.23/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -437,8 +408,6 @@ github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSy
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
@@ -476,26 +445,16 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -516,21 +475,13 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -540,18 +491,12 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -567,8 +512,6 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -578,8 +521,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
@@ -587,8 +528,6 @@ golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -597,28 +536,16 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA=
google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs=
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -640,8 +567,6 @@ modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk=
modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/libc v1.67.2 h1:ZbNmly1rcbjhot5jlOZG0q4p5VwFfjwWqZ5rY2xxOXo=
modernc.org/libc v1.67.2/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
@@ -652,8 +577,6 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/sqlite v1.41.0 h1:bJXddp4ZpsqMsNN1vS0jWo4IJTZzb8nWpcgvyCFG9Ck=
modernc.org/sqlite v1.41.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=

View File

@@ -809,6 +809,51 @@ func (e *ItemsRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID)
return err
}
func (e *ItemsRepository) WipeInventory(ctx context.Context, gid uuid.UUID) (int, error) {
// Get all items for the group
items, err := e.db.Item.Query().
Where(item.HasGroupWith(group.ID(gid))).
WithAttachments().
All(ctx)
if err != nil {
return 0, err
}
deleted := 0
// Delete each item with its attachments
// Note: We manually delete attachments and items instead of calling DeleteByGroup
// to continue processing remaining items even if some deletions fail
for _, itm := range items {
// Delete all attachments first
for _, att := range itm.Edges.Attachments {
err := e.attachments.Delete(ctx, gid, itm.ID, att.ID)
if err != nil {
log.Err(err).Str("attachment_id", att.ID.String()).Msg("failed to delete attachment during wipe inventory")
// Continue with other attachments even if one fails
}
}
// Delete the item
_, err = e.db.Item.
Delete().
Where(
item.ID(itm.ID),
item.HasGroupWith(group.ID(gid)),
).Exec(ctx)
if err != nil {
log.Err(err).Str("item_id", itm.ID.String()).Msg("failed to delete item during wipe inventory")
// Skip to next item without incrementing counter
continue
}
// Only increment counter if deletion succeeded
deleted++
}
e.publishMutationEvent(gid)
return deleted, nil
}
func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data ItemUpdate) (ItemOut, error) {
q := e.db.Item.Update().Where(item.ID(data.ID), item.HasGroupWith(group.ID(gid))).
SetName(data.Name).

View File

@@ -1,173 +0,0 @@
<template>
<BaseModal :dialog-id="DialogID.CreateInvite" title="Create Invite">
<form class="flex min-w-0 flex-col gap-2" @submit.prevent="createInvite()">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<Label for="invite-role">Role</Label>
<Select :model-value="form.role" @update:model-value="v => (form.role = String(v))">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="owner">owner</SelectItem>
<SelectItem value="admin">admin</SelectItem>
<SelectItem value="editor">editor</SelectItem>
<SelectItem value="viewer">viewer</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex flex-col gap-2">
<Label for="invite-expires">Expires</Label>
<div :class="form.no_expiry ? 'opacity-50 pointer-events-none' : ''">
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
class="w-full justify-start text-left font-normal"
:class="!form.expires_at && 'text-muted-foreground'"
>
<CalendarIcon class="mr-2 size-4" />
{{ formattedExpires ? formattedExpires : "Pick a date" }}
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Calendar :model-value="localExpires as any" @update:model-value="val => (localExpires = val)" />
</PopoverContent>
</Popover>
</div>
</div>
<div class="flex flex-col gap-2">
<Label for="invite-max-uses">Max Uses</Label>
<Input
id="invite-max-uses"
v-model.number="form.max_uses"
type="number"
min="1"
:disabled="form.unlimited_uses"
/>
</div>
<div class="mt-2 flex flex-wrap items-center gap-4">
<div class="flex items-center gap-2">
<Checkbox id="no-expiry" v-model="form.no_expiry" />
<Label for="no-expiry" class="select-none">No expiry</Label>
</div>
<div class="flex items-center gap-2">
<Checkbox id="unlimited-uses" v-model="form.unlimited_uses" />
<Label for="unlimited-uses" class="select-none">Unlimited uses</Label>
</div>
</div>
</div>
<div class="mt-4 flex flex-row-reverse gap-2">
<Button type="submit">Generate Invite</Button>
<Button variant="outline" type="button" @click="cancel">Cancel</Button>
</div>
</form>
</BaseModal>
</template>
<script setup lang="ts">
import { reactive, computed, ref, watch } from "vue";
import BaseModal from "@/components/App/CreateModal.vue";
import { useDialogHotkey, useDialog } from "~/components/ui/dialog-provider";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Calendar } from "~/components/ui/calendar";
import { Popover, PopoverTrigger, PopoverContent } from "~/components/ui/popover";
import { Checkbox } from "~/components/ui/checkbox";
import { Calendar as CalendarIcon } from "lucide-vue-next";
import { format } from "date-fns";
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "~/components/ui/select";
import { Label } from "~/components/ui/label";
import { api, type Invite } from "~/mock/collections";
import { toast } from "~/components/ui/sonner";
import { DialogID } from "~/components/ui/dialog-provider/utils";
useDialogHotkey(DialogID.CreateInvite, { code: "Digit9" });
const { closeDialog } = useDialog();
const form = reactive({
role: "viewer",
expires_at: undefined as unknown,
max_uses: 1,
no_expiry: false,
unlimited_uses: false,
});
// local date ref to satisfy Calendar's expected Date type
const localExpires = ref<Date | undefined>(form.expires_at as Date | undefined);
watch(
() => form.expires_at,
v => {
localExpires.value = (v as Date) || undefined;
}
);
watch(localExpires, v => {
form.expires_at = v as unknown;
});
const formattedExpires = computed(() => {
const v = form.expires_at as Date | string | undefined | null;
if (!v) return null;
if (v instanceof Date) return format(v, "PPP");
try {
const d = new Date(String(v));
if (!isNaN(d.getTime())) return format(d, "PPP");
} catch (e) {
// fallthrough
}
return String(v);
});
function reset() {
form.role = "viewer";
form.expires_at = undefined;
form.max_uses = 1;
form.no_expiry = false;
form.unlimited_uses = false;
}
function cancel() {
reset();
closeDialog(DialogID.CreateInvite);
}
function generateCode(length = 6) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let out = "";
for (let i = 0; i < length; i++) out += chars.charAt(Math.floor(Math.random() * chars.length));
return out;
}
function createInvite() {
const collectionId = api.getCollections()[0]?.id ?? "";
const invite: Partial<Invite> = {
id: generateCode(6),
collectionId,
role: form.role as Invite["role"],
created_at: new Date().toISOString(),
expires_at: form.no_expiry
? undefined
: form.expires_at
? form.expires_at instanceof Date
? form.expires_at.toISOString()
: String(form.expires_at)
: undefined,
max_uses: form.unlimited_uses ? undefined : form.max_uses || undefined,
uses: 0,
};
api.addInvite(invite);
toast.success("Invite created");
reset();
closeDialog(DialogID.CreateInvite, true);
}
</script>

View File

@@ -1,287 +0,0 @@
<script setup lang="ts">
import { reactive, ref, onMounted, onUnmounted } from "vue";
import type { User as MockUser, Collection as MockCollection } from "~/mock/collections";
import { api } from "~/mock/collections";
import {
Dialog,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { DialogID } from "@/components/ui/dialog-provider/utils";
import { useDialog } from "@/components/ui/dialog-provider";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select";
import MdiClose from "~icons/mdi/close";
// dialog provider
const { closeDialog, registerOpenDialogCallback } = useDialog();
// local collections snapshot used for checkbox list
const availableCollections = ref<MockCollection[]>(api.getCollections() as MockCollection[]);
const isNew = ref(true);
const localEditing = reactive<MockUser>({
id: String(Date.now()),
name: "",
email: "",
role: "user",
password_set: false,
collections: [],
});
const localCollectionIds = ref<string[]>([]);
const localNewPassword = ref("");
const newAddCollectionId = ref<string>("");
onMounted(() => {
const cleanup = registerOpenDialogCallback(DialogID.EditUser, params => {
// refresh available collections each time
availableCollections.value = api.getCollections() as MockCollection[];
if (params && (params as { userId?: string }).userId) {
const u = api.getUser(params.userId!);
if (u) {
Object.assign(localEditing, u as MockUser);
localCollectionIds.value = (u.collections ?? []).map(c => c.id);
isNew.value = false;
} else {
reset();
isNew.value = true;
}
} else {
// new user
reset();
isNew.value = true;
}
localNewPassword.value = "";
});
onUnmounted(cleanup);
});
type Membership = { id: string; role: "owner" | "admin" | "editor" | "viewer" };
function getCollectionName(id: string) {
const found = availableCollections.value.find(c => c.id === id);
return found ? found.name : id;
}
// localEditing will be set when dialog opens via registerOpenDialogCallback
function close() {
reset();
closeDialog(DialogID.EditUser);
}
function onSave() {
if (!localEditing.name.trim() || !localEditing.email.trim()) {
alert("Name and email are required");
return;
}
if (localNewPassword.value && localEditing) localEditing.password_set = true;
const existing = api.getUser(localEditing.id);
if (existing) {
const updated = {
...existing,
name: localEditing.name,
email: localEditing.email,
role: localEditing.role,
password_set: localEditing.password_set,
} as MockUser;
api.updateUser(updated);
} else {
const toCreate = { ...localEditing, collections: [] } as MockUser;
const created = api.addUser(toCreate);
localCollectionIds.value.forEach(id => api.addUserToCollection(created.id, id, "viewer"));
}
// close and signal caller to refresh
closeDialog(DialogID.EditUser, true);
reset();
}
function reset() {
localEditing.id = String(Date.now());
localEditing.name = "";
localEditing.email = "";
localEditing.role = "user";
localEditing.password_set = false;
localEditing.collections = [];
localCollectionIds.value = [];
localNewPassword.value = "";
}
function removeMembership(id: string) {
const existing = (localEditing.collections ?? []) as Membership[];
const found = existing.find((x: Membership) => x.id === id);
if (found?.role === "owner") {
const ok = confirm(
`This user is the owner of this collection.\nRemoving the owner will delete the collection. Continue?`
);
if (!ok) return;
}
const existsInApi = !!api.getUser(localEditing.id);
if (existsInApi) {
const ok = api.removeUserFromCollection(localEditing.id, id);
if (ok) {
localCollectionIds.value = localCollectionIds.value.filter(x => x !== id);
localEditing.collections = (localEditing.collections ?? []).filter((x: Membership) => x.id !== id);
const refreshed = api.getUser(localEditing.id);
if (refreshed) Object.assign(localEditing, refreshed as MockUser);
}
return;
}
// not in API yet — local only
localCollectionIds.value = localCollectionIds.value.filter(x => x !== id);
localEditing.collections = (localEditing.collections ?? []).filter((x: Membership) => x.id !== id);
}
function addMembership(id: string) {
if (!id) return;
const existsInApi = !!api.getUser(localEditing.id);
if (existsInApi) {
const mem = api.addUserToCollection(localEditing.id, id, "viewer");
if (mem) {
if (!localCollectionIds.value.includes(id)) localCollectionIds.value.push(id);
localEditing.collections = (localEditing.collections ?? []).filter((x: Membership) => x.id !== id);
localEditing.collections.push(mem as Membership);
const refreshed = api.getUser(localEditing.id);
if (refreshed) Object.assign(localEditing, refreshed as MockUser);
}
return;
}
// new user — add locally
if (!localCollectionIds.value.includes(id)) localCollectionIds.value.push(id);
localEditing.collections = (localEditing.collections ?? []).filter((x: Membership) => x.id !== id);
localEditing.collections.push({ id, role: "viewer" });
}
function updateMembershipRole(id: string, role: Membership["role"]) {
const existsInApi = !!api.getUser(localEditing.id);
if (existsInApi) {
// best-effort: remove then re-add with new role if API doesn't expose direct update
api.removeUserFromCollection(localEditing.id, id);
const mem = api.addUserToCollection(localEditing.id, id, role);
if (mem) {
const refreshed = api.getUser(localEditing.id);
if (refreshed) Object.assign(localEditing, refreshed as MockUser);
localCollectionIds.value = (localEditing.collections ?? []).map((c: Membership) => c.id);
}
return;
}
// local-only
const existing = (localEditing.collections ?? []) as Membership[];
const found = existing.find(x => x.id === id);
if (found) found.role = role;
}
</script>
<template>
<Dialog :dialog-id="DialogID.EditUser">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ isNew ? "Add User" : "Edit User" }}</DialogTitle>
<DialogDescription>Manage user details and collection memberships.</DialogDescription>
</DialogHeader>
<form class="flex flex-col gap-3" @submit.prevent="onSave">
<label class="block">
<div class="mb-1 text-sm">Name</div>
<Input v-model="localEditing.name" />
</label>
<label class="block">
<div class="mb-1 text-sm">Email</div>
<Input v-model="localEditing.email" />
</label>
<label class="block">
<div class="mb-1 text-sm">Password</div>
<Input v-model="localNewPassword" type="password" placeholder="Leave blank to keep" />
</label>
<div>
<div class="mb-1 text-sm">Collections</div>
<div class="flex flex-col gap-3">
<div
v-for="m in localEditing.collections ?? []"
:key="m.id"
class="flex items-center justify-between rounded-lg border py-1 pl-3 pr-1"
>
<div class="text-lg font-medium">
<Badge>
{{ getCollectionName(m.id) }}
</Badge>
</div>
<div class="flex items-center gap-3">
<Select v-model="m.role" @update:model-value="val => updateMembershipRole(m.id, val)">
<SelectTrigger class="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="owner">Owner</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="viewer">Viewer</SelectItem>
</SelectContent>
</Select>
<Button
variant="destructive"
size="icon"
class="ml-2"
:title="$t ? $t('global.remove') : 'Remove'"
@click.prevent="removeMembership(m.id)"
>
<MdiClose class="size-4" />
</Button>
</div>
</div>
<div class="mt-2 flex items-center gap-2">
<Select v-model="newAddCollectionId">
<SelectTrigger class="flex-1">
<SelectValue placeholder="Select collection to add" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="c in availableCollections.filter(c => !localCollectionIds.includes(c.id))"
:key="c.id"
:value="c.id"
>
{{ c.name }}
</SelectItem>
</SelectContent>
</Select>
<Button
type="button"
class="ml-2 w-10 px-0"
variant="default"
size="lg"
:disabled="!newAddCollectionId"
@click="addMembership(newAddCollectionId)"
>
+
</Button>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" type="button" @click="close">Cancel</Button>
<Button type="submit">Save</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</template>

View File

@@ -1,142 +0,0 @@
<template>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button
variant="outline"
role="combobox"
:aria-expanded="open"
:size="sidebar.state.value === 'collapsed' ? 'icon' : undefined"
:class="sidebar.state.value === 'collapsed' ? 'size-10' : 'w-full justify-between'"
aria-label="Collections"
title="Collections"
>
<template v-if="sidebar.state.value === 'collapsed'">
<MdiHomeGroup class="size-5" />
</template>
<template v-else>
<span class="flex items-center truncate">
<span class="truncate">
{{ selectedCollection && selectedCollection.name ? selectedCollection.name : "Select collection" }}
</span>
<span v-if="selectedCollection?.role" class="ml-2">
<Badge class="whitespace-nowrap" :variant="roleVariant(selectedCollection?.role)">
{{ selectedCollection?.role }}
</Badge>
</span>
</span>
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</template>
</Button>
</PopoverTrigger>
<PopoverContent
:class="[sidebar.state.value === 'collapsed' ? 'min-w-48 p-0' : 'w-[--reka-popper-anchor-width] p-0']"
>
<Command :ignore-filter="true">
<CommandGroup>
<CommandItem as-child value="collection-settings">
<NuxtLink to="/collection" class="flex w-full items-center">
<Settings class="mr-2 size-4" />
Collection Settings
</NuxtLink>
</CommandItem>
<CommandItem value="create-collection" @select="() => {}">
<Plus class="mr-2 size-4" /> Create New Collection
</CommandItem>
<CommandItem value="join-collection" @select="() => {}">
<Plus class="mr-2 size-4" /> Join Existing Collection
</CommandItem>
</CommandGroup>
<CommandInput v-model="search" placeholder="Search collections..." :display-value="_ => ''" />
<CommandEmpty>No inventory found</CommandEmpty>
<CommandList>
<CommandGroup heading="Your Collections">
<CommandItem
v-for="collection in filteredCollections"
:key="collection.id"
:value="collection.id"
@select="selectCollection(collection)"
>
<Check :class="cn('mr-2 h-4 w-4', value === collection.id ? 'opacity-100' : 'opacity-0')" />
<div class="flex w-full items-center justify-between gap-2">
{{ collection.name }}
<div class="flex items-center gap-2">
<Badge class="whitespace-nowrap" variant="outline">{{ collection.count }}</Badge>
<Badge class="whitespace-nowrap" :variant="roleVariant(collection.role)">{{ collection.role }}</Badge>
</div>
</div>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</template>
<script setup lang="ts">
import { Check, ChevronsUpDown, Plus, Settings } from "lucide-vue-next";
import MdiHomeGroup from "~icons/mdi/home-group";
import fuzzysort from "fuzzysort";
import { api } from "~/mock/collections";
import type { Collection as MockCollection, User as MockUser } from "~/mock/collections";
import { Button } from "~/components/ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "~/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
import { Badge } from "@/components/ui/badge";
import { cn } from "~/lib/utils";
import { ref, computed, watch } from "vue";
import { useVModel } from "@vueuse/core";
import { useSidebar } from "@/components/ui/sidebar/utils";
// api.getCollections returns collection objects augmented with `count` and `role` for the current user
type CollectionSummary = MockCollection & { count: number; role: MockUser["collections"][number]["role"] };
type Props = {
modelValue?: string | null;
};
const props = defineProps<Props>();
const emit = defineEmits(["update:modelValue"]);
const open = ref(false);
const search = ref("");
const value = useVModel(props, "modelValue", emit);
// Use shared mock collections data via fake api (for current user)
const collectionsList = ref<CollectionSummary[]>(api.getCollections() as CollectionSummary[]);
function roleVariant(role: string | undefined) {
if (role === "owner") return "default";
if (role === "admin") return "secondary";
return "outline";
}
function selectCollection(collection: CollectionSummary) {
if (value.value !== collection.id) {
value.value = collection.id;
console.log(collection);
}
open.value = false;
}
const selectedCollection = computed(() => {
return collectionsList.value.find(o => o.id === value.value) ?? null;
});
const sidebar = useSidebar();
const filteredCollections = computed(() => {
const filtered = fuzzysort.go(search.value, collectionsList.value, { key: "name", all: true }).map(i => i.obj);
return filtered;
});
// Reset search when value is cleared
watch(
() => value.value,
() => {
if (!value.value) {
search.value = "";
}
}
);
</script>

View File

@@ -1,18 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { Primitive, type PrimitiveProps } from "reka-ui";
import { type ButtonVariants, buttonVariants } from ".";
import { cn } from "@/lib/utils";
import type { HTMLAttributes } from "vue";
import { Primitive, type PrimitiveProps } from "reka-ui";
import { type ButtonVariants, buttonVariants } from ".";
import { cn } from "@/lib/utils";
interface Props extends PrimitiveProps {
variant?: ButtonVariants["variant"];
size?: ButtonVariants["size"];
class?: HTMLAttributes["class"];
}
interface Props extends PrimitiveProps {
variant?: ButtonVariants["variant"];
size?: ButtonVariants["size"];
class?: HTMLAttributes["class"];
}
const props = withDefaults(defineProps<Props>(), {
as: "button",
});
const props = withDefaults(defineProps<Props>(), {
as: "button",
});
</script>
<template>

View File

@@ -1,57 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarRoot, type CalendarRootEmits, type CalendarRootProps, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNextButton, CalendarPrevButton } from '.'
const props = defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<CalendarRootEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<CalendarRoot
v-slot="{ grid, weekDays }"
:class="cn('p-3', props.class)"
v-bind="forwarded"
>
<CalendarHeader>
<CalendarPrevButton />
<CalendarHeading />
<CalendarNextButton />
</CalendarHeader>
<div class="flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
<CalendarGridHead>
<CalendarGridRow>
<CalendarHeadCell
v-for="day in weekDays" :key="day"
>
{{ day }}
</CalendarHeadCell>
</CalendarGridRow>
</CalendarGridHead>
<CalendarGridBody>
<CalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="mt-2 w-full">
<CalendarCell
v-for="weekDate in weekDates"
:key="weekDate.toString()"
:date="weekDate"
>
<CalendarCellTrigger
:day="weekDate"
:month="month.value"
/>
</CalendarCell>
</CalendarGridRow>
</CalendarGridBody>
</CalendarGrid>
</div>
</CalendarRoot>
</template>

View File

@@ -1,21 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarCell
:class="cn('relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-view])]:bg-accent/50', props.class)"
v-bind="forwardedProps"
>
<slot />
</CalendarCell>
</template>

View File

@@ -1,35 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarCellTrigger
:class="cn(
buttonVariants({ variant: 'ghost' }),
'h-9 w-9 p-0 font-normal',
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
// Selected
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
// Disabled
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
// Unavailable
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
// Outside months
'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
props.class,
)"
v-bind="forwardedProps"
>
<slot />
</CalendarCellTrigger>
</template>

View File

@@ -1,21 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarGrid, type CalendarGridProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarGrid
:class="cn('w-full border-collapse space-y-1', props.class)"
v-bind="forwardedProps"
>
<slot />
</CalendarGrid>
</template>

View File

@@ -1,11 +0,0 @@
<script lang="ts" setup>
import { CalendarGridBody, type CalendarGridBodyProps } from 'reka-ui'
const props = defineProps<CalendarGridBodyProps>()
</script>
<template>
<CalendarGridBody v-bind="props">
<slot />
</CalendarGridBody>
</template>

View File

@@ -1,11 +0,0 @@
<script lang="ts" setup>
import { CalendarGridHead, type CalendarGridHeadProps } from 'reka-ui'
const props = defineProps<CalendarGridHeadProps>()
</script>
<template>
<CalendarGridHead v-bind="props">
<slot />
</CalendarGridHead>
</template>

View File

@@ -1,18 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarGridRow, type CalendarGridRowProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarGridRow :class="cn('flex', props.class)" v-bind="forwardedProps">
<slot />
</CalendarGridRow>
</template>

View File

@@ -1,18 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarHeadCell, type CalendarHeadCellProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeadCell :class="cn('w-9 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)" v-bind="forwardedProps">
<slot />
</CalendarHeadCell>
</template>

View File

@@ -1,18 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarHeader, type CalendarHeaderProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeader :class="cn('relative flex w-full items-center justify-between pt-1', props.class)" v-bind="forwardedProps">
<slot />
</CalendarHeader>
</template>

View File

@@ -1,28 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarHeading, type CalendarHeadingProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeadingProps & { class?: HTMLAttributes['class'] }>()
defineSlots<{
default: (props: { headingValue: string }) => any
}>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeading
v-slot="{ headingValue }"
:class="cn('text-sm font-medium', props.class)"
v-bind="forwardedProps"
>
<slot :heading-value>
{{ headingValue }}
</slot>
</CalendarHeading>
</template>

View File

@@ -1,29 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ChevronRight } from 'lucide-vue-next'
import { CalendarNext, type CalendarNextProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = defineProps<CalendarNextProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarNext
:class="cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class,
)"
v-bind="forwardedProps"
>
<slot>
<ChevronRight class="h-4 w-4" />
</slot>
</CalendarNext>
</template>

View File

@@ -1,29 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ChevronLeft } from 'lucide-vue-next'
import { CalendarPrev, type CalendarPrevProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = defineProps<CalendarPrevProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarPrev
:class="cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class,
)"
v-bind="forwardedProps"
>
<slot>
<ChevronLeft class="h-4 w-4" />
</slot>
</CalendarPrev>
</template>

View File

@@ -1,12 +0,0 @@
export { default as Calendar } from './Calendar.vue'
export { default as CalendarCell } from './CalendarCell.vue'
export { default as CalendarCellTrigger } from './CalendarCellTrigger.vue'
export { default as CalendarGrid } from './CalendarGrid.vue'
export { default as CalendarGridBody } from './CalendarGridBody.vue'
export { default as CalendarGridHead } from './CalendarGridHead.vue'
export { default as CalendarGridRow } from './CalendarGridRow.vue'
export { default as CalendarHeadCell } from './CalendarHeadCell.vue'
export { default as CalendarHeader } from './CalendarHeader.vue'
export { default as CalendarHeading } from './CalendarHeading.vue'
export { default as CalendarNextButton } from './CalendarNextButton.vue'
export { default as CalendarPrevButton } from './CalendarPrevButton.vue'

View File

@@ -1,12 +1,7 @@
import { computed, type ComputedRef } from "vue";
import { createContext } from "reka-ui";
import { useMagicKeys, useActiveElement } from "@vueuse/core";
import type {
BarcodeProduct,
ItemSummary,
MaintenanceEntry,
MaintenanceEntryWithDetails,
} from "~~/lib/api/types/data-contracts";
import type { BarcodeProduct, ItemSummary, MaintenanceEntry, MaintenanceEntryWithDetails } from "~~/lib/api/types/data-contracts";
export enum DialogID {
AttachmentEdit = "attachment-edit",
@@ -29,8 +24,6 @@ export enum DialogID {
PageQRCode = "page-qr-code",
UpdateLabel = "update-label",
UpdateLocation = "update-location",
CreateInvite = "create-invite",
EditUser = "edit-user",
UpdateTemplate = "update-template",
ItemChangeDetails = "item-table-updater",
}
@@ -58,7 +51,6 @@ export type DialogParamsMap = {
attachmentId: string;
};
[DialogID.CreateItem]?: { product?: BarcodeProduct };
[DialogID.EditUser]?: { userId?: string };
[DialogID.ProductImport]?: { barcode?: string };
[DialogID.EditMaintenance]:
| { type: "create"; itemId: string | string[] }
@@ -78,9 +70,7 @@ export type DialogParamsMap = {
export type DialogResultMap = {
[DialogID.ItemImage]?: { action: "delete"; id: string };
[DialogID.EditMaintenance]?: boolean;
[DialogID.CreateInvite]?: boolean;
[DialogID.ItemChangeDetails]?: boolean;
[DialogID.EditUser]?: boolean;
};
/** Helpers to split IDs by requirement */

View File

@@ -8,7 +8,6 @@
<ModalConfirm />
<OutdatedModal v-if="status" :status="status" />
<ItemCreateModal />
<CreateInviteModal />
<LabelCreateModal />
<LocationCreateModal />
<ItemBarcodeModal />
@@ -25,8 +24,6 @@
<AppLogo />
</div>
</NuxtLink>
<AppCollectionSelector v-model:model-value="selectedCollectionId" />
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
@@ -122,23 +119,12 @@
}"
>
<div class="flex h-1/2 items-center gap-2 sm:h-auto">
<div>
<SidebarTrigger variant="default" />
</div>
<!-- <div>
<Button size="icon">
<AppLogo class="size-8" />
</Button>
</div> -->
<SidebarTrigger variant="default" />
<NuxtLink to="/home">
<AppHeaderText class="h-6" />
</NuxtLink>
<!-- <AppOrgSelector v-model:model-value="selectedOrg" /> -->
</div>
<div class="sm:grow" />
<!-- <div class="flex items-center">
<AppOrgSelector v-model:model-value="selectedOrg" />
</div> -->
<div class="flex h-1/2 grow items-center justify-end gap-2 sm:h-auto">
<Input
v-model:model-value="search"
@@ -234,15 +220,11 @@
import LabelCreateModal from "~/components/Label/CreateModal.vue";
import LocationCreateModal from "~/components/Location/CreateModal.vue";
import ItemBarcodeModal from "~/components/Item/BarcodeModal.vue";
import CreateInviteModal from "~/components/Admin/CreateInviteModal.vue";
import AppQuickMenuModal from "~/components/App/QuickMenuModal.vue";
import AppScannerModal from "~/components/App/ScannerModal.vue";
import AppLogo from "~/components/App/Logo.vue";
import AppHeaderDecor from "~/components/App/HeaderDecor.vue";
import AppHeaderText from "~/components/App/HeaderText.vue";
import AppCollectionSelector from "~/components/App/CollectionSelector.vue";
const selectedCollectionId = ref<string>("c1");
const { t, locale } = useI18n();
const username = computed(() => authCtx.user?.name || "User");
@@ -377,13 +359,6 @@
name: computed(() => t("menu.tools")),
to: "/tools",
},
{
icon: MdiAccount,
id: 7,
active: computed(() => route.path === "/admin"),
name: computed(() => t("menu.admin")),
to: "/admin",
},
];
const quickMenuActions = reactive([

View File

@@ -31,4 +31,10 @@ export class ActionsAPI extends BaseAPI {
url: route("/actions/create-missing-thumbnails"),
});
}
wipeInventory() {
return this.http.post<void, ActionAmountResult>({
url: route("/actions/wipe-inventory"),
});
}
}

View File

@@ -735,6 +735,10 @@
"set_primary_photo_button": "Set Primary Photo",
"set_primary_photo_confirm": "Are you sure you want to set primary photos? This can take a while and cannot be undone.",
"set_primary_photo_sub": "In version v0.10.0 of Homebox, the primary image field was added to attachments of type photo. This action will set the primary image field to the first image in the attachments array in the database, if it is not already set. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'See GitHub PR #576'</a>'",
"wipe_inventory": "Wipe Inventory",
"wipe_inventory_button": "Wipe Inventory",
"wipe_inventory_confirm": "Are you sure you want to wipe your entire inventory? This will delete all items and cannot be undone.",
"wipe_inventory_sub": "Permanently deletes all items in your inventory. This action is irreversible and will remove all item data including attachments and photos.",
"zero_datetimes": "Zero Item Date Times",
"zero_datetimes_button": "Zero Item Date Times",
"zero_datetimes_confirm": "Are you sure you want to reset all date and time values? This can take a while and cannot be undone.",
@@ -768,7 +772,9 @@
"failed_ensure_ids": "Failed to ensure asset IDs.",
"failed_ensure_import_refs": "Failed to ensure import refs.",
"failed_set_primary_photos": "Failed to set primary photos.",
"failed_zero_datetimes": "Failed to reset date and time values."
"failed_wipe_inventory": "Failed to wipe inventory.",
"failed_zero_datetimes": "Failed to reset date and time values.",
"wipe_inventory_success": "Successfully wiped inventory. { results } items deleted."
}
}
}

View File

@@ -1,216 +0,0 @@
export type Collection = { id: string; name: string };
export type User = {
id: string;
name: string;
email: string;
created_at?: string;
role: "admin" | "user" | string;
password_set?: boolean;
oidc_set?: boolean;
collections: {
id: string;
role: "owner" | "admin" | "editor" | "viewer";
}[];
};
export const collections: Collection[] = [
{ id: "c1", name: "Personal Inventory" },
{ id: "c2", name: "Office Equipment" },
{ id: "c3", name: "Workshop Tools" },
];
export const users: User[] = [
{
id: "1",
name: "Alice Admin",
email: "alice@example.com",
created_at: new Date(new Date().setFullYear(new Date().getFullYear() - 2)).toISOString(),
role: "admin",
password_set: true,
collections: [
{ id: collections[0]!.id, role: "owner" },
{ id: collections[1]!.id, role: "admin" },
{ id: collections[2]!.id, role: "editor" },
],
},
{
id: "2",
name: "Bob User",
email: "bob@example.com",
created_at: new Date(new Date().setFullYear(new Date().getFullYear() - 1)).toISOString(),
role: "user",
password_set: true,
oidc_set: true,
collections: [
{ id: collections[1]!.id, role: "owner" },
{ id: collections[2]!.id, role: "admin" },
],
},
{
id: "3",
name: "Charlie",
email: "charlie@example.com",
created_at: new Date().toISOString(),
role: "user",
password_set: false,
// collections[3] was out of range (only 0..2 exist). Use collections[2].
collections: [{ id: collections[2]!.id, role: "owner" }],
},
];
export type Invite = {
id: string;
collectionId: string;
role?: "owner" | "admin" | "editor" | "viewer";
created_at?: string;
expires_at?: string;
max_uses?: number;
uses?: number;
};
export const invites: Invite[] = [
{
id: "i1",
collectionId: collections[0]!.id,
role: "viewer",
created_at: new Date().toISOString(),
expires_at: new Date(new Date().setDate(new Date().getDate() + 7)).toISOString(),
max_uses: 5,
uses: 2,
},
];
// Simple in-memory fake API operating on the above arrays.
export const api = {
// by is the person who is requesting the collections, include the number of members and their role
getCollections(by: string = "1") {
const user = users.find(u => u.id === by);
if (!user) return [];
return user.collections
.map(c => {
const collection = collections.find(col => col.id === c.id);
if (!collection) return null;
// find number of people with access to this collection
const count = users.reduce((acc, u) => {
const hasAccess = u.collections.some(uc => uc.id === collection.id);
return acc + (hasAccess ? 1 : 0);
}, 0);
return {
...collection,
count,
role: c.role,
};
})
.filter(Boolean);
},
getUsers(): User[] {
return users;
},
getInvites(): Invite[] {
return invites;
},
getUser(id: string) {
return users.find(u => u.id === id);
},
addUser(input: Partial<User>) {
const u: User = {
id: input.id ?? String(Date.now()),
name: input.name ?? "",
email: input.email ?? "",
role: input.role ?? "user",
password_set: input.password_set ?? false,
oidc_set: input.oidc_set ?? false,
collections: input.collections ?? [],
};
users.unshift(u);
return u;
},
updateUser(updated: User) {
const idx = users.findIndex(u => u.id === updated.id);
if (idx >= 0) users.splice(idx, 1, { ...updated });
return updated;
},
deleteUser(id: string) {
const idx = users.findIndex(u => u.id === id);
if (idx >= 0) {
users.splice(idx, 1);
return true;
}
return false;
},
addInvite(input: Partial<Invite>) {
const inv: Invite = {
id: input.id ?? `i${Date.now()}`,
collectionId: input.collectionId ?? collections[0]!.id,
role: input.role ?? "viewer",
created_at: new Date().toISOString(),
expires_at: input.expires_at ? input.expires_at : undefined,
max_uses: input.max_uses ? input.max_uses : undefined,
uses: 0,
};
invites.unshift(inv);
return inv;
},
deleteInvite(id: string) {
const idx = invites.findIndex(i => i.id === id);
if (idx >= 0) invites.splice(idx, 1);
return idx >= 0;
},
addCollection(input: Partial<Collection>) {
const col: Collection = { id: input.id ?? `c${Date.now()}`, name: input.name ?? "New Collection" };
collections.push(col);
// add user[0] to collection
users[0]!.collections.push({ id: col.id, role: "owner" });
return col;
},
updateCollection(updated: Collection) {
const idx = collections.findIndex(c => c.id === updated.id);
if (idx >= 0) collections.splice(idx, 1, { ...updated });
return updated;
},
addUserToCollection(userId: string, collectionId: string, role: "owner" | "admin" | "editor" | "viewer" = "viewer") {
const u = users.find(x => x.id === userId);
if (!u) return null;
const exists = u.collections.find(c => c.id === collectionId);
if (exists) {
exists.role = role;
return exists;
}
const mem = { id: collectionId, role } as { id: string; role: "owner" | "admin" | "editor" | "viewer" };
u.collections.push(mem);
return mem;
},
removeUserFromCollection(userId: string, collectionId: string) {
const u = users.find(x => x.id === userId);
if (!u) return false;
const idx = u.collections.findIndex(c => c.id === collectionId);
if (idx >= 0) {
const wasOwner = u.collections[idx]!.role === "owner";
u.collections.splice(idx, 1);
// if removed owner, and no other owners exist for that collection, delete the collection
if (wasOwner) {
const stillOwner = users.some(other =>
(other.collections ?? []).some(c => c.id === collectionId && c.role === "owner")
);
if (!stillOwner) {
// remove collection
const cidx = collections.findIndex(c => c.id === collectionId);
if (cidx >= 0) collections.splice(cidx, 1);
// remove membership from all users
users.forEach(mu => {
mu.collections = (mu.collections ?? []).filter(c => c.id !== collectionId);
});
// remove invites to that collection
for (let i = invites.length - 1; i >= 0; i--) {
if (invites[i]!.collectionId === collectionId) invites.splice(i, 1);
}
}
}
return true;
}
return false;
},
};
export default { collections, users, invites, api };

View File

@@ -1,199 +0,0 @@
<!-- TODO:
- make collection on hover show role and colour based on role
-->
<script setup lang="ts">
import { ref, computed } from "vue";
import { useI18n } from "vue-i18n";
import { useConfirm } from "~/composables/use-confirm";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TableEmpty } from "~/components/ui/table";
import { Button } from "~/components/ui/button";
import { Card } from "@/components/ui/card";
import BaseContainer from "@/components/Base/Container.vue";
import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
import MdiPencil from "~icons/mdi/pencil";
import MdiDelete from "~icons/mdi/delete";
import MdiCheck from "~icons/mdi/check";
import MdiClose from "~icons/mdi/close";
// import MdiOpenInNew from "~icons/mdi/open-in-new";
// Badge component for collections display
import { Badge } from "@/components/ui/badge";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import UserFormDialog from "@/components/Admin/UserFormDialog.vue";
import { useDialog } from "@/components/ui/dialog-provider";
import { DialogID } from "@/components/ui/dialog-provider/utils";
import { api, type Collection as MockCollection, type User } from "~/mock/collections";
// api.getCollections returns collections augmented with `count` and the current user's `role`
type CollectionSummary = MockCollection & { count: number; role: User["collections"][number]["role"] };
const collections = ref<CollectionSummary[]>(api.getCollections() as CollectionSummary[]);
const users = ref<User[]>(api.getUsers());
const query = ref("");
const filtered = computed(() => {
const q = query.value.trim().toLowerCase();
if (!q) return users.value;
return users.value.filter(u => {
return `${u.name} ${u.email} ${u.role}`.toLowerCase().includes(q);
});
});
const { openDialog } = useDialog();
const confirm = useConfirm();
const { t } = useI18n();
// editing state handled in dialog component; role toggle logic applied on save
// helper to compute auth type for display
// authType removed — not used in the template
function authType(u: User) {
const parts: string[] = [];
if (u.password_set) parts.push("Password");
if (u.oidc_subject) parts.push("OIDC");
return parts.length ? parts.join(" & ") : "None";
}
function openAdd() {
openDialog(DialogID.EditUser, {
onClose: result => {
if (result) {
users.value = api.getUsers();
collections.value = api.getCollections();
}
},
});
}
function openEdit(u: User) {
openDialog(DialogID.EditUser, {
params: { userId: u.id },
onClose: result => {
if (result) {
users.value = api.getUsers();
collections.value = api.getCollections();
}
},
});
}
async function confirmDelete(u: User) {
const { isCanceled } = await confirm.open({
message: t("global.delete_confirm") + " " + `${u.name} (${u.email})?`,
});
if (isCanceled) return;
api.deleteUser(u.id);
users.value = api.getUsers();
// TODO: call backend API to delete user when available
}
// no more toggleActive; active is not used
function collectionName(id: string) {
const col = collections.value.find(c => c.id === id);
return col ? col.name : id;
}
function roleVariant(role: string | undefined) {
if (role === "owner") return "default";
if (role === "admin") return "secondary";
return "outline";
}
// dialog handles editing state now via dialog provider
</script>
<template>
<BaseContainer class="flex flex-col gap-4">
<BaseSectionHeader>
<span>User Management</span>
<div class="ml-auto">
<Button @click="openAdd">Add User</Button>
</div>
</BaseSectionHeader>
<Card class="p-0">
<Table class="w-full">
<TableHeader>
<TableRow>
<TableHead class="min-w-[160px]">{{ t("global.name") }}</TableHead>
<TableHead class="min-w-[220px]">{{ t("global.email") }}</TableHead>
<TableHead class="min-w-[96px] text-center">Is Admin</TableHead>
<TableHead class="min-w-[220px]">Collections</TableHead>
<TableHead class="min-w-[96px] text-center">{{ t("global.details") }}</TableHead>
<TableHead class="w-40 text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="filtered.length">
<TableRow v-for="u in filtered" :key="u.id">
<TableCell>{{ u.name }}</TableCell>
<TableCell>{{ u.email }}</TableCell>
<TableCell class="text-center align-middle">
<div class="flex size-full items-center justify-center font-medium">
<MdiCheck v-if="u.role === 'admin'" class="text-primary" />
<MdiClose v-else class="text-destructive" />
</div>
</TableCell>
<TableCell>
<div class="flex flex-wrap items-center gap-2">
<template v-if="u.collections && u.collections.length">
<TooltipProvider :delay-duration="0">
<template v-for="c in u.collections" :key="c.id">
<Tooltip>
<TooltipTrigger as-child>
<Badge class="whitespace-nowrap" :variant="roleVariant(c.role)">{{
collectionName(c.id)
}}</Badge>
</TooltipTrigger>
<TooltipContent>
<p class="text-sm">{{ c.role }}</p>
</TooltipContent>
</Tooltip>
</template>
</TooltipProvider>
</template>
<span v-else class="text-muted-foreground">-</span>
</div>
</TableCell>
<TableCell class="text-center align-middle">
<div class="flex size-full items-center justify-center">
<span>{{ authType(u) }}</span>
</div>
</TableCell>
<TableCell class="text-right align-middle">
<div class="flex size-full items-center justify-end gap-2">
<Button size="icon" variant="outline" class="size-8" :title="t('global.edit')" @click="openEdit(u)">
<MdiPencil class="size-4" />
</Button>
<Button
size="icon"
variant="destructive"
class="size-8"
:title="t('global.delete')"
@click="confirmDelete(u)"
>
<MdiDelete class="size-4" />
</Button>
</div>
</TableCell>
</TableRow>
</template>
<template v-else>
<TableEmpty :colspan="6">
<p>{{ $t("items.selector.no_results") }}</p>
</TableEmpty>
</template>
</TableBody>
</Table>
</Card>
<!-- Add / Edit form modal (moved to component) -->
<UserFormDialog />
</BaseContainer>
</template>

View File

@@ -1,225 +0,0 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { api, type User as MockUser, type Invite as MockInvite } from "~/mock/collections";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
// Popover removed from invite UI; no longer importing
import { Button, ButtonGroup } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
import { Card } from "@/components/ui/card"; // Assuming you have a Card component
import { Badge } from "@/components/ui/badge"; // Assuming you have a Badge component
import { PlusCircle, Trash } from "lucide-vue-next"; // Icons
import { format } from "date-fns";
import CopyText from "@/components/global/CopyText.vue";
import BaseContainer from "@/components/Base/Container.vue";
import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
import { useDialog } from "~/components/ui/dialog-provider";
import { DialogID } from "~/components/ui/dialog-provider/utils";
const { openDialog } = useDialog();
const { t } = useI18n();
definePageMeta({
middleware: ["auth"],
});
useHead({
title: "HomeBox | " + t("menu.maintenance"),
});
// Use centralized mock data / fake API
const users = ref<MockUser[]>(api.getUsers());
const invites = ref<MockInvite[]>(api.getInvites());
// Current collection context (this page shows a single collection)
// For now use the first mock collection as the active collection
const currentCollectionId = api.getCollections()[0]?.id ?? "";
// New invite email input
// (declared below with invite inputs)
// Settings state
const collectionName = ref<string>("Personal Inventory");
const saved = ref(false);
// invite inputs (moved to dialog)
const page = ref(1);
const roles = ["owner", "admin", "editor", "viewer"];
function inviteUrl(code: string) {
if (typeof window === "undefined") return "";
return `${window.location.origin}?token=${code}`;
}
function getMembershipRole(user: MockUser) {
const mem = user.collections.find(c => c.id === currentCollectionId);
return mem?.role ?? "viewer";
}
function roleVariant(role: string) {
return role === "owner" ? "default" : role === "admin" ? "secondary" : "outline";
}
function handleRoleChange(userId: string, newRole: unknown) {
// Update the role for this user specific to the current collection
const roleStr = String(newRole || "viewer");
api.addUserToCollection(userId, currentCollectionId, roleStr as MockUser["collections"][number]["role"]);
users.value = api.getUsers();
}
function handleRemoveUser(userId: string) {
api.deleteUser(userId);
users.value = api.getUsers();
}
// Invite creation now handled by dialog component; keep helper removed.
function deleteInvite(inviteId: string) {
api.deleteInvite(inviteId);
invites.value = api.getInvites();
}
function saveSettings() {
// Stub: persist settings to API when implemented
console.log("Saving collection settings", collectionName.value);
saved.value = true;
setTimeout(() => (saved.value = false), 2000);
}
</script>
<template>
<div>
<BaseContainer class="flex flex-col gap-4">
<BaseSectionHeader> Collection Settings </BaseSectionHeader>
<ButtonGroup>
<Button size="sm" :variant="page == 1 ? 'default' : 'outline'" @click="page = 1"> Users </Button>
<Button size="sm" :variant="page == 2 ? 'default' : 'outline'" @click="page = 2"> Invites </Button>
<Button size="sm" :variant="page == 3 ? 'default' : 'outline'" @click="page = 3"> Settings </Button>
</ButtonGroup>
<Card v-if="page == 1" class="">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Role</TableHead>
<TableHead>Joined</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="user in users" :key="user.id">
<TableCell class="font-medium">
{{ user.name }}
</TableCell>
<TableCell>
<Select
:model-value="getMembershipRole(user)"
@update:model-value="newRole => handleRoleChange(user.id, newRole)"
>
<SelectTrigger>
<span class="flex items-center">
<Badge class="whitespace-nowrap" :variant="roleVariant(getMembershipRole(user))">{{
getMembershipRole(user)
}}</Badge>
</span>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="role in roles" :key="role" :value="role">
<div class="flex w-full items-center justify-between">
<Badge
class="whitespace-nowrap"
:variant="role === 'owner' ? 'default' : role === 'admin' ? 'secondary' : 'outline'"
>
{{ role }}
</Badge>
</div>
</SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell>
{{ (user as any).created_at ? format(new Date((user as any).created_at), "PPP") : "-" }}
</TableCell>
<TableCell class="text-right">
<div class="flex w-full items-center justify-end gap-2">
<Button variant="destructive" size="icon" @click="handleRemoveUser(user.id)">
<Trash class="size-4" />
</Button>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
</Card>
<Card v-if="page == 2" class="p-4">
<div class="flex flex-col gap-4">
<h3 class="text-lg font-semibold">Existing Invites</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>Code</TableHead>
<TableHead>Expires</TableHead>
<TableHead>Max Uses</TableHead>
<TableHead>Uses</TableHead>
<TableHead class="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="invite in invites" :key="invite.id">
<TableCell class="font-medium">{{ invite.id }}</TableCell>
<TableCell>{{ invite.expires_at ? format(new Date(invite.expires_at), "PPP") : "Never" }}</TableCell>
<TableCell>{{ invite.max_uses ?? "" }}</TableCell>
<TableCell>{{ invite.uses ?? 0 }}</TableCell>
<TableCell class="w-max">
<div class="flex items-center justify-end gap-2">
<CopyText :text="inviteUrl(invite.id)" />
<Button variant="destructive" size="icon" @click="deleteInvite(invite.id)">
<Trash class="size-4" />
</Button>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
<hr class="my-4" />
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">Create New Invite</h3>
<div class="w-56">
<Button
class="w-full"
@click="openDialog(DialogID.CreateInvite, { onClose: () => (invites.value = api.getInvites()) })"
>
<PlusCircle class="mr-2 size-4" /> Generate Invite
</Button>
</div>
</div>
</div>
</Card>
<Card v-if="page == 3" class="p-4">
<h3 class="text-lg font-semibold">Collection Settings</h3>
<div class="mt-4 grid items-end gap-4 md:grid-cols-2">
<div class="flex flex-col gap-2">
<Label for="collection-name">Name</Label>
<Input id="collection-name" v-model="collectionName" placeholder="Collection name" />
</div>
<div class="flex items-end">
<Button class="w-full" @click="saveSettings">Save</Button>
</div>
</div>
<p v-if="saved" class="mt-3 text-sm text-green-600">Saved</p>
</Card>
</BaseContainer>
</div>
</template>

View File

@@ -90,6 +90,12 @@
<div v-html="DOMPurify.sanitize($t('tools.actions_set.create_missing_thumbnails_sub'))" />
<template #button> {{ $t("tools.actions_set.create_missing_thumbnails_button") }} </template>
</DetailAction>
<DetailAction @action="wipeInventory">
<template #title> {{ $t("tools.actions_set.wipe_inventory") }} </template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="DOMPurify.sanitize($t('tools.actions_set.wipe_inventory_sub'))" />
<template #button> {{ $t("tools.actions_set.wipe_inventory_button") }} </template>
</DetailAction>
</div>
</BaseCard>
</BaseContainer>
@@ -220,6 +226,23 @@
toast.success(t("tools.toast.asset_success", { results: result.data.completed }));
}
async function wipeInventory() {
const { isCanceled } = await confirm.open(t("tools.actions_set.wipe_inventory_confirm"));
if (isCanceled) {
return;
}
const result = await api.actions.wipeInventory();
if (result.error) {
toast.error(t("tools.toast.failed_wipe_inventory"));
return;
}
toast.success(t("tools.toast.wipe_inventory_success", { results: result.data.completed }));
}
</script>
<style scoped></style>