Implement maintenance view (#235)

* implement backend to query maintenances

* improve backend API for maintenances

* add itemId and itemName (v1/maintenances)

* fix remaining todo in backend (/v1/maintenances)

* refactor: extract MaintenanceEditModal

* first draft (to be cleaned)

* revert dependency updates (not required)

* translation + fix deletion

* fix main menu css issues

* fix main menu "create" button (css)

* enhancement: make item name clickable

* fix: add page title (+ translate existing ones)

* fix: missing toast translation (when updating)

* bug fix: missing group check in backend (for new endpoint)

* backport from following PR (to avoid useless changes)

* maintenances => maintenance
This commit is contained in:
mcarbonne
2024-09-23 19:07:27 +02:00
committed by GitHub
parent 897f3842e0
commit a85c42b539
21 changed files with 1165 additions and 445 deletions

View File

@@ -13,7 +13,7 @@ import (
// HandleMaintenanceLogGet godoc
//
// @Summary Get Maintenance Log
// @Tags Maintenance
// @Tags Item Maintenance
// @Produce json
// @Success 200 {object} repo.MaintenanceLog
// @Router /v1/items/{id}/maintenance [GET]
@@ -30,7 +30,7 @@ func (ctrl *V1Controller) HandleMaintenanceLogGet() errchain.HandlerFunc {
// HandleMaintenanceEntryCreate godoc
//
// @Summary Create Maintenance Entry
// @Tags Maintenance
// @Tags Item Maintenance
// @Produce json
// @Param payload body repo.MaintenanceEntryCreate true "Entry Data"
// @Success 201 {object} repo.MaintenanceEntry
@@ -44,39 +44,3 @@ func (ctrl *V1Controller) HandleMaintenanceEntryCreate() errchain.HandlerFunc {
return adapters.ActionID("id", fn, http.StatusCreated)
}
// HandleMaintenanceEntryDelete godoc
//
// @Summary Delete Maintenance Entry
// @Tags Maintenance
// @Produce json
// @Success 204
// @Router /v1/items/{id}/maintenance/{entry_id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryDelete() errchain.HandlerFunc {
fn := func(r *http.Request, entryID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
err := ctrl.repo.MaintEntry.Delete(auth, entryID)
return nil, err
}
return adapters.CommandID("entry_id", fn, http.StatusNoContent)
}
// HandleMaintenanceEntryUpdate godoc
//
// @Summary Update Maintenance Entry
// @Tags Maintenance
// @Produce json
// @Param payload body repo.MaintenanceEntryUpdate true "Entry Data"
// @Success 200 {object} repo.MaintenanceEntry
// @Router /v1/items/{id}/maintenance/{entry_id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, entryID uuid.UUID, body repo.MaintenanceEntryUpdate) (repo.MaintenanceEntry, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.MaintEntry.Update(auth, entryID, body)
}
return adapters.ActionID("entry_id", fn, http.StatusOK)
}

View File

@@ -0,0 +1,65 @@
package v1
import (
"net/http"
"github.com/google/uuid"
"github.com/hay-kot/httpkit/errchain"
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
"github.com/sysadminsmedia/homebox/backend/internal/web/adapters"
)
// HandleMaintenanceGetAll godoc
//
// @Summary Query All Maintenance
// @Tags Maintenance
// @Produce json
// @Param filters query repo.MaintenanceFilters false "which maintenance to retrieve"
// @Success 200 {array} repo.MaintenanceEntryWithDetails[]
// @Router /v1/maintenance [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceGetAll() errchain.HandlerFunc {
fn := func(r *http.Request, filters repo.MaintenanceFilters) ([]repo.MaintenanceEntryWithDetails, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.MaintEntry.GetAllMaintenance(auth, auth.GID, filters)
}
return adapters.Query(fn, http.StatusOK)
}
// HandleMaintenanceEntryUpdate godoc
//
// @Summary Update Maintenance Entry
// @Tags Maintenance
// @Produce json
// @Param payload body repo.MaintenanceEntryUpdate true "Entry Data"
// @Success 200 {object} repo.MaintenanceEntry
// @Router /v1/maintenance/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, entryID uuid.UUID, body repo.MaintenanceEntryUpdate) (repo.MaintenanceEntry, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.MaintEntry.Update(auth, entryID, body)
}
return adapters.ActionID("id", fn, http.StatusOK)
}
// HandleMaintenanceEntryDelete godoc
//
// @Summary Delete Maintenance Entry
// @Tags Maintenance
// @Produce json
// @Success 204
// @Router /v1/maintenance/{id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryDelete() errchain.HandlerFunc {
fn := func(r *http.Request, entryID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
err := ctrl.repo.MaintEntry.Delete(auth, entryID)
return nil, err
}
return adapters.CommandID("id", fn, http.StatusNoContent)
}

View File

@@ -4,6 +4,12 @@ import (
"embed"
"errors"
"fmt"
"io"
"mime"
"net/http"
"path"
"path/filepath"
"github.com/go-chi/chi/v5"
"github.com/hay-kot/httpkit/errchain"
httpSwagger "github.com/swaggo/http-swagger/v2" // http-swagger middleware
@@ -13,11 +19,6 @@ import (
_ "github.com/sysadminsmedia/homebox/backend/app/api/static/docs"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/authroles"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
"io"
"mime"
"net/http"
"path"
"path/filepath"
)
const prefix = "/api"
@@ -133,11 +134,14 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
r.Get("/items/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...))
r.Post("/items/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...))
r.Put("/items/{id}/maintenance/{entry_id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...))
r.Delete("/items/{id}/maintenance/{entry_id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryDelete(), userMW...))
r.Get("/assets/{id}", chain.ToHandlerFunc(v1Ctrl.HandleAssetGet(), userMW...))
// Maintenance
r.Get("/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceGetAll(), userMW...))
r.Put("/maintenance/{id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...))
r.Delete("/maintenance/{id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryDelete(), userMW...))
// Notifiers
r.Get("/notifiers", chain.ToHandlerFunc(v1Ctrl.HandleGetUserNotifiers(), userMW...))
r.Post("/notifiers", chain.ToHandlerFunc(v1Ctrl.HandleCreateNotifier(), userMW...))

View File

@@ -917,7 +917,7 @@ const docTemplate = `{
"application/json"
],
"tags": [
"Maintenance"
"Item Maintenance"
],
"summary": "Get Maintenance Log",
"responses": {
@@ -939,7 +939,7 @@ const docTemplate = `{
"application/json"
],
"tags": [
"Maintenance"
"Item Maintenance"
],
"summary": "Create Maintenance Entry",
"parameters": [
@@ -963,60 +963,6 @@ const docTemplate = `{
}
}
},
"/v1/items/{id}/maintenance/{entry_id}": {
"put": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Update Maintenance Entry",
"parameters": [
{
"description": "Entry Data",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/repo.MaintenanceEntryUpdate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/repo.MaintenanceEntry"
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Delete Maintenance Entry",
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/items/{id}/path": {
"get": {
"security": [
@@ -1409,6 +1355,104 @@ const docTemplate = `{
}
}
},
"/v1/maintenance": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Query All Maintenance",
"parameters": [
{
"enum": [
"scheduled",
"completed",
"both"
],
"type": "string",
"x-enum-varnames": [
"MaintenanceFilterStatusScheduled",
"MaintenanceFilterStatusCompleted",
"MaintenanceFilterStatusBoth"
],
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.MaintenanceEntryWithDetails"
}
}
}
}
}
},
"/v1/maintenance/{id}": {
"put": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Update Maintenance Entry",
"parameters": [
{
"description": "Entry Data",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/repo.MaintenanceEntryUpdate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/repo.MaintenanceEntry"
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Delete Maintenance Entry",
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/notifiers": {
"get": {
"security": [
@@ -2476,6 +2520,9 @@ const docTemplate = `{
"parent": {
"$ref": "#/definitions/repo.LocationSummary"
},
"totalPrice": {
"type": "number"
},
"updatedAt": {
"type": "string"
}
@@ -2611,6 +2658,49 @@ const docTemplate = `{
}
}
},
"repo.MaintenanceEntryWithDetails": {
"type": "object",
"properties": {
"completedDate": {
"type": "string"
},
"cost": {
"type": "string",
"example": "0"
},
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"itemID": {
"type": "string"
},
"itemName": {
"type": "string"
},
"name": {
"type": "string"
},
"scheduledDate": {
"type": "string"
}
}
},
"repo.MaintenanceFilterStatus": {
"type": "string",
"enum": [
"scheduled",
"completed",
"both"
],
"x-enum-varnames": [
"MaintenanceFilterStatusScheduled",
"MaintenanceFilterStatusCompleted",
"MaintenanceFilterStatusBoth"
]
},
"repo.MaintenanceLog": {
"type": "object",
"properties": {

View File

@@ -910,7 +910,7 @@
"application/json"
],
"tags": [
"Maintenance"
"Item Maintenance"
],
"summary": "Get Maintenance Log",
"responses": {
@@ -932,7 +932,7 @@
"application/json"
],
"tags": [
"Maintenance"
"Item Maintenance"
],
"summary": "Create Maintenance Entry",
"parameters": [
@@ -956,60 +956,6 @@
}
}
},
"/v1/items/{id}/maintenance/{entry_id}": {
"put": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Update Maintenance Entry",
"parameters": [
{
"description": "Entry Data",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/repo.MaintenanceEntryUpdate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/repo.MaintenanceEntry"
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Delete Maintenance Entry",
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/items/{id}/path": {
"get": {
"security": [
@@ -1402,6 +1348,104 @@
}
}
},
"/v1/maintenance": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Query All Maintenance",
"parameters": [
{
"enum": [
"scheduled",
"completed",
"both"
],
"type": "string",
"x-enum-varnames": [
"MaintenanceFilterStatusScheduled",
"MaintenanceFilterStatusCompleted",
"MaintenanceFilterStatusBoth"
],
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.MaintenanceEntryWithDetails"
}
}
}
}
}
},
"/v1/maintenance/{id}": {
"put": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Update Maintenance Entry",
"parameters": [
{
"description": "Entry Data",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/repo.MaintenanceEntryUpdate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/repo.MaintenanceEntry"
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Delete Maintenance Entry",
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/notifiers": {
"get": {
"security": [
@@ -2607,6 +2651,49 @@
}
}
},
"repo.MaintenanceEntryWithDetails": {
"type": "object",
"properties": {
"completedDate": {
"type": "string"
},
"cost": {
"type": "string",
"example": "0"
},
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"itemID": {
"type": "string"
},
"itemName": {
"type": "string"
},
"name": {
"type": "string"
},
"scheduledDate": {
"type": "string"
}
}
},
"repo.MaintenanceFilterStatus": {
"type": "string",
"enum": [
"scheduled",
"completed",
"both"
],
"x-enum-varnames": [
"MaintenanceFilterStatusScheduled",
"MaintenanceFilterStatusCompleted",
"MaintenanceFilterStatusBoth"
]
},
"repo.MaintenanceLog": {
"type": "object",
"properties": {
@@ -2710,9 +2797,6 @@
},
"total": {
"type": "integer"
},
"totalPrice": {
"type": "number"
}
}
},
@@ -2995,4 +3079,4 @@
"in": "header"
}
}
}
}

View File

@@ -390,6 +390,8 @@ definitions:
type: string
parent:
$ref: '#/definitions/repo.LocationSummary'
totalPrice:
type: number
updatedAt:
type: string
type: object
@@ -479,6 +481,36 @@ definitions:
scheduledDate:
type: string
type: object
repo.MaintenanceEntryWithDetails:
properties:
completedDate:
type: string
cost:
example: "0"
type: string
description:
type: string
id:
type: string
itemID:
type: string
itemName:
type: string
name:
type: string
scheduledDate:
type: string
type: object
repo.MaintenanceFilterStatus:
enum:
- scheduled
- completed
- both
type: string
x-enum-varnames:
- MaintenanceFilterStatusScheduled
- MaintenanceFilterStatusCompleted
- MaintenanceFilterStatusBoth
repo.MaintenanceLog:
properties:
costAverage:
@@ -1228,7 +1260,7 @@ paths:
- Bearer: []
summary: Get Maintenance Log
tags:
- Maintenance
- Item Maintenance
post:
parameters:
- description: Entry Data
@@ -1248,39 +1280,7 @@ paths:
- Bearer: []
summary: Create Maintenance Entry
tags:
- Maintenance
/v1/items/{id}/maintenance/{entry_id}:
delete:
produces:
- application/json
responses:
"204":
description: No Content
security:
- Bearer: []
summary: Delete Maintenance Entry
tags:
- Maintenance
put:
parameters:
- description: Entry Data
in: body
name: payload
required: true
schema:
$ref: '#/definitions/repo.MaintenanceEntryUpdate'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/repo.MaintenanceEntry'
security:
- Bearer: []
summary: Update Maintenance Entry
tags:
- Maintenance
- Item Maintenance
/v1/items/{id}/path:
get:
parameters:
@@ -1581,6 +1581,66 @@ paths:
summary: Get Locations Tree
tags:
- Locations
/v1/maintenance:
get:
parameters:
- enum:
- scheduled
- completed
- both
in: query
name: status
type: string
x-enum-varnames:
- MaintenanceFilterStatusScheduled
- MaintenanceFilterStatusCompleted
- MaintenanceFilterStatusBoth
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/repo.MaintenanceEntryWithDetails'
type: array
security:
- Bearer: []
summary: Query All Maintenance
tags:
- Maintenance
/v1/maintenance/{id}:
delete:
produces:
- application/json
responses:
"204":
description: No Content
security:
- Bearer: []
summary: Delete Maintenance Entry
tags:
- Maintenance
put:
parameters:
- description: Entry Data
in: body
name: payload
required: true
schema:
$ref: '#/definitions/repo.MaintenanceEntryUpdate'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/repo.MaintenanceEntry'
security:
- Bearer: []
summary: Update Maintenance Entry
tags:
- Maintenance
/v1/notifiers:
get:
produces:

View File

@@ -110,6 +110,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
@@ -119,6 +121,8 @@ github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJm
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olahol/melody v1.2.1 h1:xdwRkzHxf+B0w4TKbGpUSSkV516ZucQZJIWLztOWICQ=
github.com/olahol/melody v1.2.1/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
@@ -136,6 +140,10 @@ github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

View File

@@ -48,8 +48,8 @@ type (
LocationOut struct {
Parent *LocationSummary `json:"parent,omitempty"`
LocationSummary
Children []LocationSummary `json:"children"`
TotalPrice float64 `json:"totalPrice"`
Children []LocationSummary `json:"children"`
TotalPrice float64 `json:"totalPrice"`
}
)

View File

@@ -0,0 +1,72 @@
package repo
import (
"context"
"time"
"github.com/google/uuid"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/group"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/item"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/maintenanceentry"
)
type (
MaintenanceEntryWithDetails struct {
MaintenanceEntry
ItemName string `json:"itemName"`
ItemID uuid.UUID `json:"itemID"`
}
)
var (
mapEachMaintenanceEntryWithDetails = mapTEachFunc(mapMaintenanceEntryWithDetails)
)
func mapMaintenanceEntryWithDetails(entry *ent.MaintenanceEntry) MaintenanceEntryWithDetails {
return MaintenanceEntryWithDetails{
MaintenanceEntry: mapMaintenanceEntry(entry),
ItemName: entry.Edges.Item.Name,
ItemID: entry.ItemID,
}
}
type MaintenanceFilterStatus string
const (
MaintenanceFilterStatusScheduled MaintenanceFilterStatus = "scheduled"
MaintenanceFilterStatusCompleted MaintenanceFilterStatus = "completed"
MaintenanceFilterStatusBoth MaintenanceFilterStatus = "both"
)
type MaintenanceFilters struct {
Status MaintenanceFilterStatus `json:"status" schema:"status"`
}
func (r *MaintenanceEntryRepository) GetAllMaintenance(ctx context.Context, groupID uuid.UUID, filters MaintenanceFilters) ([]MaintenanceEntryWithDetails, error) {
query := r.db.MaintenanceEntry.Query().Where(
maintenanceentry.HasItemWith(
item.HasGroupWith(group.IDEQ(groupID)),
),
)
if filters.Status == MaintenanceFilterStatusScheduled {
query = query.Where(maintenanceentry.Or(
maintenanceentry.DateIsNil(),
maintenanceentry.DateEQ(time.Time{}),
))
} else if filters.Status == MaintenanceFilterStatusCompleted {
query = query.Where(
maintenanceentry.Not(maintenanceentry.Or(
maintenanceentry.DateIsNil(),
maintenanceentry.DateEQ(time.Time{})),
))
}
entries, err := query.WithItem().Order(maintenanceentry.ByScheduledDate()).All(ctx)
if err != nil {
return nil, err
}
return mapEachMaintenanceEntryWithDetails(entries), nil
}

View File

@@ -910,7 +910,7 @@
"application/json"
],
"tags": [
"Maintenance"
"Item Maintenance"
],
"summary": "Get Maintenance Log",
"responses": {
@@ -932,7 +932,7 @@
"application/json"
],
"tags": [
"Maintenance"
"Item Maintenance"
],
"summary": "Create Maintenance Entry",
"parameters": [
@@ -956,60 +956,6 @@
}
}
},
"/v1/items/{id}/maintenance/{entry_id}": {
"put": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Update Maintenance Entry",
"parameters": [
{
"description": "Entry Data",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/repo.MaintenanceEntryUpdate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/repo.MaintenanceEntry"
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Delete Maintenance Entry",
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/items/{id}/path": {
"get": {
"security": [
@@ -1402,6 +1348,104 @@
}
}
},
"/v1/maintenance": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Query All Maintenance",
"parameters": [
{
"enum": [
"scheduled",
"completed",
"both"
],
"type": "string",
"x-enum-varnames": [
"MaintenanceFilterStatusScheduled",
"MaintenanceFilterStatusCompleted",
"MaintenanceFilterStatusBoth"
],
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.MaintenanceEntryWithDetails"
}
}
}
}
}
},
"/v1/maintenance/{id}": {
"put": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Update Maintenance Entry",
"parameters": [
{
"description": "Entry Data",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/repo.MaintenanceEntryUpdate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/repo.MaintenanceEntry"
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Maintenance"
],
"summary": "Delete Maintenance Entry",
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/notifiers": {
"get": {
"security": [
@@ -2607,6 +2651,49 @@
}
}
},
"repo.MaintenanceEntryWithDetails": {
"type": "object",
"properties": {
"completedDate": {
"type": "string"
},
"cost": {
"type": "string",
"example": "0"
},
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"itemID": {
"type": "string"
},
"itemName": {
"type": "string"
},
"name": {
"type": "string"
},
"scheduledDate": {
"type": "string"
}
}
},
"repo.MaintenanceFilterStatus": {
"type": "string",
"enum": [
"scheduled",
"completed",
"both"
],
"x-enum-varnames": [
"MaintenanceFilterStatusScheduled",
"MaintenanceFilterStatusCompleted",
"MaintenanceFilterStatusBoth"
]
},
"repo.MaintenanceLog": {
"type": "object",
"properties": {
@@ -2710,9 +2797,6 @@
},
"total": {
"type": "integer"
},
"totalPrice": {
"type": "number"
}
}
},
@@ -2995,4 +3079,4 @@
"in": "header"
}
}
}
}

View File

@@ -0,0 +1,139 @@
<template>
<BaseModal v-model="visible">
<template #title>
{{ entry.id ? $t("maintenance.modal.edit_title") : $t("maintenance.modal.new_title") }}
</template>
<form @submit.prevent="dispatchFormSubmit">
<FormTextField v-model="entry.name" autofocus :label="$t('maintenance.modal.entry_name')" />
<DatePicker v-model="entry.completedDate" :label="$t('maintenance.modal.completed_date')" />
<DatePicker v-model="entry.scheduledDate" :label="$t('maintenance.modal.scheduled_date')" />
<FormTextArea v-model="entry.description" :label="$t('maintenance.modal.notes')" />
<FormTextField v-model="entry.cost" autofocus :label="$t('maintenance.modal.cost')" />
<div class="flex justify-end py-2">
<BaseButton type="submit" class="ml-2 mt-2">
<template #icon>
<MdiPost />
</template>
{{ entry.id ? $t("maintenance.modal.edit_action") : $t("maintenance.modal.new_action") }}
</BaseButton>
</div>
</form>
</BaseModal>
</template>
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import type { MaintenanceEntry, MaintenanceEntryWithDetails } from "~~/lib/api/types/data-contracts";
import MdiPost from "~icons/mdi/post";
import DatePicker from "~~/components/Form/DatePicker.vue";
const { t } = useI18n();
const api = useUserApi();
const toast = useNotifier();
const emit = defineEmits(["changed"]);
const visible = ref(false);
const entry = reactive({
id: null as string | null,
name: "",
completedDate: null as Date | null,
scheduledDate: null as Date | null,
description: "",
cost: "",
itemId: null as string | null,
});
async function dispatchFormSubmit() {
if (entry.id) {
await editEntry();
return;
}
await createEntry();
}
async function createEntry() {
if (!entry.itemId) {
return;
}
const { error } = await api.items.maintenance.create(entry.itemId, {
name: entry.name,
completedDate: entry.completedDate ?? "",
scheduledDate: entry.scheduledDate ?? "",
description: entry.description,
cost: parseFloat(entry.cost) ? entry.cost : "0",
});
if (error) {
toast.error(t("maintenance.toast.failed_to_create"));
return;
}
visible.value = false;
emit("changed");
}
async function editEntry() {
if (!entry.id) {
return;
}
const { error } = await api.maintenance.update(entry.id, {
name: entry.name,
completedDate: entry.completedDate ?? "null",
scheduledDate: entry.scheduledDate ?? "null",
description: entry.description,
cost: entry.cost,
});
if (error) {
toast.error(t("maintenance.toast.failed_to_update"));
return;
}
visible.value = false;
emit("changed");
}
const openCreateModal = (itemId: string) => {
entry.id = null;
entry.name = "";
entry.completedDate = null;
entry.scheduledDate = null;
entry.description = "";
entry.cost = "";
entry.itemId = itemId;
visible.value = true;
};
const openUpdateModal = (maintenanceEntry: MaintenanceEntry | MaintenanceEntryWithDetails) => {
entry.id = maintenanceEntry.id;
entry.name = maintenanceEntry.name;
entry.completedDate = new Date(maintenanceEntry.completedDate);
entry.scheduledDate = new Date(maintenanceEntry.scheduledDate);
entry.description = maintenanceEntry.description;
entry.cost = maintenanceEntry.cost;
entry.itemId = null;
visible.value = true;
};
const confirm = useConfirm();
async function deleteEntry(id: string) {
const result = await confirm.open(t("maintenance.modal.delete_confirmation"));
if (result.isCanceled) {
return;
}
const { error } = await api.maintenance.delete(id);
if (error) {
toast.error(t("maintenance.toast.failed_to_delete"));
return;
}
emit("changed");
}
defineExpose({ openCreateModal, openUpdateModal, deleteEntry });
</script>

View File

@@ -12,15 +12,15 @@
<AppToast />
<div class="drawer drawer-mobile">
<input id="my-drawer-2" v-model="drawerToggle" type="checkbox" class="drawer-toggle" />
<div class="drawer-content justify-center bg-base-300 pt-20 lg:pt-0">
<div class="drawer-content bg-base-300 justify-center pt-20 lg:pt-0">
<AppHeaderDecor v-if="preferences.displayHeaderDecor" class="-mt-10 hidden lg:block" />
<!-- Button -->
<div class="navbar drawer-button fixed top-0 z-[99] bg-primary shadow-md lg:hidden">
<div class="navbar drawer-button bg-primary fixed top-0 z-[99] shadow-md lg:hidden">
<label for="my-drawer-2" class="btn btn-square btn-ghost drawer-button text-base-100 lg:hidden">
<MdiMenu class="size-6" />
</label>
<NuxtLink to="/home">
<h2 class="flex text-3xl font-bold tracking-tight text-base-100">
<h2 class="text-base-100 flex text-3xl font-bold tracking-tight">
HomeB
<AppLogo class="-mb-3 w-8" />
x
@@ -29,7 +29,7 @@
</div>
<slot></slot>
<footer v-if="status" class="bottom-0 w-full bg-base-300 pb-4 text-center text-secondary-content">
<footer v-if="status" class="bg-base-300 text-secondary-content bottom-0 w-full pb-4 text-center">
<p class="text-center text-sm">
{{ $t("global.version", { version: status.build.version }) }} ~
{{ $t("global.build", { build: status.build.commit }) }}
@@ -42,26 +42,26 @@
<label for="my-drawer-2" class="drawer-overlay"></label>
<!-- Top Section -->
<div class="flex w-60 flex-col bg-base-200 py-5 md:py-10">
<div class="bg-base-200 flex min-w-40 max-w-min flex-col p-5 md:py-10">
<div class="space-y-8">
<div class="flex flex-col items-center gap-4">
<p>{{ $t("global.welcome", { username: username }) }}</p>
<NuxtLink class="avatar placeholder" to="/home">
<div class="w-24 rounded-full bg-base-300 p-4 text-neutral-content">
<div class="bg-base-300 text-neutral-content w-24 rounded-full p-4">
<AppLogo />
</div>
</NuxtLink>
</div>
<div class="flex flex-col bg-base-200">
<div class="mx-auto mb-6 w-40">
<div class="dropdown visible w-40">
<div class="bg-base-200 flex flex-col">
<div class="mb-6">
<div class="dropdown visible w-full">
<label tabindex="0" class="text-no-transform btn btn-primary btn-block text-lg">
<span>
<MdiPlus class="-ml-1 mr-1" />
</span>
{{ $t("global.create") }}
</label>
<ul tabindex="0" class="dropdown-content menu rounded-box w-40 bg-base-100 p-2 shadow">
<ul tabindex="0" class="dropdown-content menu rounded-box bg-base-100 w-full p-2 shadow">
<li v-for="btn in dropdown" :key="btn.name">
<button @click="btn.action">
{{ btn.name }}
@@ -70,7 +70,7 @@
</ul>
</div>
</div>
<ul class="menu mx-auto flex w-40 flex-col gap-2">
<ul class="menu mx-auto flex flex-col gap-2">
<li v-for="n in nav" :key="n.id" class="text-xl" @click="unfocus">
<NuxtLink
v-if="n.to"
@@ -89,7 +89,7 @@
</div>
<!-- Bottom -->
<button class="rounded-btn mx-2 mt-auto p-3 hover:bg-base-300" @click="logout">
<button class="rounded-btn hover:bg-base-300 mx-2 mt-auto p-3" @click="logout">
{{ $t("global.sign_out") }}
</button>
</div>
@@ -99,6 +99,7 @@
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { useLabelStore } from "~~/stores/labels";
import { useLocationStore } from "~~/stores/locations";
import MdiMenu from "~icons/mdi/menu";
@@ -109,6 +110,9 @@
import MdiMagnify from "~icons/mdi/magnify";
import MdiAccount from "~icons/mdi/account";
import MdiCog from "~icons/mdi/cog";
import MdiWrench from "~icons/mdi/wrench";
const { t } = useI18n();
const username = computed(() => authCtx.user?.name || "User");
const preferences = useViewPreferences();
@@ -164,35 +168,42 @@
icon: MdiHome,
active: computed(() => route.path === "/home"),
id: 0,
name: "Home",
name: t("menu.home"),
to: "/home",
},
{
icon: MdiFileTree,
id: 4,
id: 1,
active: computed(() => route.path === "/locations"),
name: "Locations",
name: t("menu.locations"),
to: "/locations",
},
{
icon: MdiMagnify,
id: 3,
id: 2,
active: computed(() => route.path === "/items"),
name: "Search",
name: t("menu.search"),
to: "/items",
},
{
icon: MdiWrench,
id: 3,
active: computed(() => route.path === "/maintenance"),
name: t("menu.maintenance"),
to: "/maintenance",
},
{
icon: MdiAccount,
id: 1,
id: 4,
active: computed(() => route.path === "/profile"),
name: "Profile",
name: t("menu.profile"),
to: "/profile",
},
{
icon: MdiCog,
id: 6,
id: 5,
active: computed(() => route.path === "/tools"),
name: "Tools",
name: t("menu.tools"),
to: "/tools",
},
];

View File

@@ -10,7 +10,6 @@ import type {
ItemUpdate,
MaintenanceEntry,
MaintenanceEntryCreate,
MaintenanceEntryUpdate,
MaintenanceLog,
} from "../types/data-contracts";
import type { AttachmentTypes, PaginationResult } from "../types/non-generated";
@@ -71,7 +70,7 @@ type MaintenanceEntryQuery = {
completed?: boolean;
};
export class MaintenanceAPI extends BaseAPI {
export class ItemMaintenanceAPI extends BaseAPI {
getLog(itemId: string, q: MaintenanceEntryQuery = {}) {
return this.http.get<MaintenanceLog>({ url: route(`/items/${itemId}/maintenance`, q) });
}
@@ -82,29 +81,18 @@ export class MaintenanceAPI extends BaseAPI {
body: data,
});
}
delete(itemId: string, entryId: string) {
return this.http.delete<void>({ url: route(`/items/${itemId}/maintenance/${entryId}`) });
}
update(itemId: string, entryId: string, data: MaintenanceEntryUpdate) {
return this.http.put<MaintenanceEntryUpdate, MaintenanceEntry>({
url: route(`/items/${itemId}/maintenance/${entryId}`),
body: data,
});
}
}
export class ItemsApi extends BaseAPI {
attachments: AttachmentsAPI;
maintenance: MaintenanceAPI;
maintenance: ItemMaintenanceAPI;
fields: FieldsAPI;
constructor(http: Requests, token: string) {
super(http, token);
this.fields = new FieldsAPI(http);
this.attachments = new AttachmentsAPI(http);
this.maintenance = new MaintenanceAPI(http);
this.maintenance = new ItemMaintenanceAPI(http);
}
fullpath(id: string) {

View File

@@ -0,0 +1,30 @@
import { BaseAPI, route } from "../base";
import type {
MaintenanceEntry,
MaintenanceEntryWithDetails,
MaintenanceEntryUpdate,
MaintenanceFilterStatus,
} from "../types/data-contracts";
export interface MaintenanceFilters {
status?: MaintenanceFilterStatus;
}
export class MaintenanceAPI extends BaseAPI {
getAll(filters: MaintenanceFilters) {
return this.http.get<MaintenanceEntryWithDetails[]>({
url: route(`/maintenance`, { status: filters.status?.toString() }),
});
}
delete(id: string) {
return this.http.delete<void>({ url: route(`/maintenance/${id}`) });
}
update(id: string, data: MaintenanceEntryUpdate) {
return this.http.put<MaintenanceEntryUpdate, MaintenanceEntry>({
url: route(`/maintenance/${id}`),
body: data,
});
}
}

View File

@@ -288,6 +288,24 @@ export interface MaintenanceEntryUpdate {
scheduledDate: Date | string;
}
export interface MaintenanceEntryWithDetails {
completedDate: Date | string;
/** @example "0" */
cost: string;
description: string;
id: string;
itemID: string;
itemName: string;
name: string;
scheduledDate: Date | string;
}
export enum MaintenanceFilterStatus {
MaintenanceFilterStatusScheduled = "scheduled",
MaintenanceFilterStatusCompleted = "completed",
MaintenanceFilterStatusBoth = "both",
}
export interface MaintenanceLog {
costAverage: number;
costTotal: number;
@@ -330,7 +348,6 @@ export interface PaginationResultItemSummary {
page: number;
pageSize: number;
total: number;
totalPrice: number;
}
export interface TotalsByOrganizer {

View File

@@ -9,12 +9,14 @@ import { StatsAPI } from "./classes/stats";
import { AssetsApi } from "./classes/assets";
import { ReportsAPI } from "./classes/reports";
import { NotifiersAPI } from "./classes/notifiers";
import { MaintenanceAPI } from "./classes/maintenance";
import type { Requests } from "~~/lib/requests";
export class UserClient extends BaseAPI {
locations: LocationsApi;
labels: LabelsApi;
items: ItemsApi;
maintenance: MaintenanceAPI;
group: GroupApi;
user: UserApi;
actions: ActionsAPI;
@@ -29,6 +31,7 @@ export class UserClient extends BaseAPI {
this.locations = new LocationsApi(requests);
this.labels = new LabelsApi(requests);
this.items = new ItemsApi(requests, attachmentToken);
this.maintenance = new MaintenanceAPI(requests);
this.group = new GroupApi(requests);
this.user = new UserApi(requests);
this.actions = new ActionsAPI(requests);

View File

@@ -101,6 +101,51 @@
"tips_sub": "Search Tips",
"updated_at": "Updated At"
},
"menu":{
"home": "Home",
"locations": "Locations",
"search": "Search",
"maintenance": "Maintenance",
"profile": "Profile",
"tools": "Tools"
},
"maintenance": {
"total_entries": "Total Entries",
"total_cost": "Total Cost",
"monthly_average": "Monthly Average",
"list":
{
"new": "New",
"edit": "Edit",
"delete": "Delete",
"create_first": "Create Your First Entry"
},
"modal":
{
"new_title": "New Entry",
"edit_title": "Edit Entry",
"new_action": "Create",
"edit_action": "Update",
"delete_confirmation": "Are you sure you want to delete this entry?",
"entry_name": "Entry Name",
"completed_date": "Completed Date",
"scheduled_date": "Scheduled Date",
"notes": "Notes",
"cost": "Cost"
},
"filter":
{
"scheduled": "Scheduled",
"completed": "Completed",
"both": "Both"
},
"toast":
{
"failed_to_delete": "Failed to delete entry",
"failed_to_create": "Failed to create entry",
"failed_to_update" : "Failed to update entry"
}
},
"profile": {
"active": "Active",
"change_password": "Change Password",

View File

@@ -121,6 +121,51 @@
"zh-MO": "Chinois (Macao)",
"zh-TW": "Chinois (traditionnel)"
},
"menu":{
"home": "Accueil",
"locations": "Emplacements",
"search": "Recherche",
"maintenance": "Maintenance",
"profile": "Profil",
"tools": "Outils"
},
"maintenance": {
"total_entries": "Nombre d'entrées",
"total_cost": "Coût total",
"monthly_average": "Moyenne mensuelle",
"list":
{
"new": "Ajouter",
"edit": "Modifier",
"delete": "Supprimer",
"create_first": "Créer votre première entrée"
},
"modal":
{
"new_title": "Nouvelle entrée",
"edit_title": "Modifier l'entrée",
"new_action": "Créer",
"edit_action": "Modifier",
"delete_confirmation": "Êtes-vous certain de vouloir supprimer cette entrée ?",
"entry_name": "Nom",
"completed_date": "Date d'achèvement",
"scheduled_date": "Date prévue",
"notes": "Notes",
"cost": "Coût"
},
"filter":
{
"scheduled": "Prévues",
"completed": "Terminées",
"both": "Toutes"
},
"toast":
{
"failed_to_delete": "Échec de création de l'entrée",
"failed_to_create": "Échec de suppression de l'entrée",
"failed_to_update" : "Échec de mise à jour de l'entrée"
}
},
"profile": {
"active": "Actif",
"change_password": "Changer de mot de passe",

View File

@@ -1,15 +1,16 @@
<script setup lang="ts">
import DatePicker from "~~/components/Form/DatePicker.vue";
import { useI18n } from "vue-i18n";
import type { StatsFormat } from "~~/components/global/StatCard/types";
import type { ItemOut, MaintenanceEntry } from "~~/lib/api/types/data-contracts";
import MdiPost from "~icons/mdi/post";
import type { ItemOut } from "~~/lib/api/types/data-contracts";
import MdiPlus from "~icons/mdi/plus";
import MdiCheck from "~icons/mdi/check";
import MdiDelete from "~icons/mdi/delete";
import MdiEdit from "~icons/mdi/edit";
import MdiCalendar from "~icons/mdi/calendar";
import MdiWrenchClock from "~icons/mdi/wrench-clock";
import MaintenanceEditModal from "~~/components/Maintenance/EditModal.vue";
const { t } = useI18n();
const props = defineProps<{
item: ItemOut;
}>();
@@ -19,6 +20,8 @@
const scheduled = ref(true);
const maintenanceEditModal = ref<InstanceType<typeof MaintenanceEditModal>>();
watch(
() => scheduled.value,
() => {
@@ -44,167 +47,36 @@
return [
{
id: "count",
title: "Total Entries",
title: t("maintenance.total_entries"),
value: count.value || 0,
type: "number" as StatsFormat,
},
{
id: "total",
title: "Total Cost",
title: t("maintenance.total_cost"),
value: log.value.costTotal || 0,
type: "currency" as StatsFormat,
},
{
id: "average",
title: "Monthly Average",
title: t("maintenance.monthly_average"),
value: log.value.costAverage || 0,
type: "currency" as StatsFormat,
},
];
});
const entry = reactive({
id: null as string | null,
modal: false,
name: "",
completedDate: null as Date | null,
scheduledDate: null as Date | null,
description: "",
cost: "",
});
function newEntry() {
entry.modal = true;
}
function resetEntry() {
console.log("Resetting entry");
entry.id = null;
entry.name = "";
entry.completedDate = null;
entry.scheduledDate = null;
entry.description = "";
entry.cost = "";
}
watch(
() => entry.modal,
(v, pv) => {
if (pv === true && v === false) {
resetEntry();
}
}
);
// Calls either edit or create depending on entry.id being set
async function dispatchFormSubmit() {
if (entry.id) {
await editEntry();
return;
}
await createEntry();
}
async function createEntry() {
const { error } = await api.items.maintenance.create(props.item.id, {
name: entry.name,
completedDate: entry.completedDate ?? "",
scheduledDate: entry.scheduledDate ?? "",
description: entry.description,
cost: parseFloat(entry.cost) ? entry.cost : "0",
});
if (error) {
toast.error("Failed to create entry");
return;
}
entry.modal = false;
refreshLog();
resetEntry();
}
const confirm = useConfirm();
async function deleteEntry(id: string) {
const result = await confirm.open("Are you sure you want to delete this entry?");
if (result.isCanceled) {
return;
}
const { error } = await api.items.maintenance.delete(props.item.id, id);
if (error) {
toast.error("Failed to delete entry");
return;
}
refreshLog();
}
function openEditDialog(e: MaintenanceEntry) {
entry.id = e.id;
entry.name = e.name;
entry.completedDate = new Date(e.completedDate);
entry.scheduledDate = new Date(e.scheduledDate);
entry.description = e.description;
entry.cost = e.cost;
entry.modal = true;
}
async function editEntry() {
if (!entry.id) {
return;
}
const { error } = await api.items.maintenance.update(props.item.id, entry.id, {
name: entry.name,
completedDate: entry.completedDate ?? "null",
scheduledDate: entry.scheduledDate ?? "null",
description: entry.description,
cost: entry.cost,
});
if (error) {
toast.error("Failed to update entry");
return;
}
entry.modal = false;
refreshLog();
}
</script>
<template>
<div v-if="log">
<BaseModal v-model="entry.modal">
<template #title>
{{ entry.id ? "Edit Entry" : "New Entry" }}
</template>
<form @submit.prevent="dispatchFormSubmit">
<FormTextField v-model="entry.name" autofocus label="Entry Name" />
<DatePicker v-model="entry.completedDate" label="Completed Date" />
<DatePicker v-model="entry.scheduledDate" label="Scheduled Date" />
<FormTextArea v-model="entry.description" label="Notes" />
<FormTextField v-model="entry.cost" autofocus label="Cost" />
<div class="flex justify-end py-2">
<BaseButton type="submit" class="ml-2 mt-2">
<template #icon>
<MdiPost />
</template>
{{ entry.id ? "Update" : "Create" }}
</BaseButton>
</div>
</form>
</BaseModal>
<MaintenanceEditModal ref="maintenanceEditModal" @changed="refreshLog"></MaintenanceEditModal>
<section class="space-y-6">
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
<StatCard
v-for="stat in stats"
:key="stat.id"
class="stats block border-l-primary shadow-xl"
class="stats border-l-primary block shadow-xl"
:title="stat.title"
:value="stat.value"
:type="stat.type"
@@ -213,17 +85,17 @@
<div class="flex">
<div class="btn-group">
<button class="btn btn-sm" :class="`${scheduled ? 'btn-active' : ''}`" @click="scheduled = true">
Scheduled
{{ $t("maintenance.filter.scheduled") }}
</button>
<button class="btn btn-sm" :class="`${scheduled ? '' : 'btn-active'}`" @click="scheduled = false">
Completed
{{ $t("maintenance.filter.completed") }}
</button>
</div>
<BaseButton class="ml-auto" size="sm" @click="newEntry()">
<BaseButton class="ml-auto" size="sm" @click="maintenanceEditModal?.openCreateModal(props.item.id)">
<template #icon>
<MdiPlus />
</template>
New
{{ $t("maintenance.list.new") }}
</BaseButton>
</div>
<div class="container space-y-6">
@@ -254,28 +126,30 @@
<Markdown :source="e.description" />
</div>
<div class="flex justify-end gap-1 p-4">
<BaseButton size="sm" @click="openEditDialog(e)">
<BaseButton size="sm" @click="maintenanceEditModal?.openUpdateModal(e)">
<template #icon>
<MdiEdit />
</template>
Edit
{{ $t("maintenance.list.edit") }}
</BaseButton>
<BaseButton size="sm" @click="deleteEntry(e.id)">
<BaseButton size="sm" @click="maintenanceEditModal?.deleteEntry(e.id)">
<template #icon>
<MdiDelete />
</template>
Delete
{{ $t("maintenance.list.delete") }}
</BaseButton>
</div>
</BaseCard>
<div class="hidden first:block">
<button
type="button"
class="relative block w-full rounded-lg border-2 border-dashed border-base-content p-12 text-center"
@click="newEntry()"
class="border-base-content relative block w-full rounded-lg border-2 border-dashed p-12 text-center"
@click="maintenanceEditModal?.openCreateModal(props.item.id)"
>
<MdiWrenchClock class="inline size-16" />
<span class="mt-2 block text-sm font-medium text-gray-900"> Create Your First Entry </span>
<span class="mt-2 block text-sm font-medium text-gray-900">
{{ $t("maintenance.list.create_first") }}
</span>
</button>
</div>
</div>

View File

@@ -62,7 +62,7 @@
<template>
<BaseContainer class="mb-16">
<BaseSectionHeader> Locations </BaseSectionHeader>
<BaseSectionHeader> {{ $t("menu.locations") }} </BaseSectionHeader>
<BaseCard>
<div class="p-4">
<div class="mb-2 flex justify-end">

View File

@@ -0,0 +1,137 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import type { StatsFormat } from "~~/components/global/StatCard/types";
import { MaintenanceFilterStatus } from "~~/lib/api/types/data-contracts";
import MdiCheck from "~icons/mdi/check";
import MdiDelete from "~icons/mdi/delete";
import MdiEdit from "~icons/mdi/edit";
import MdiCalendar from "~icons/mdi/calendar";
import MaintenanceEditModal from "~~/components/Maintenance/EditModal.vue";
const { t } = useI18n();
const api = useUserApi();
const maintenanceFilter = ref(MaintenanceFilterStatus.MaintenanceFilterStatusScheduled);
const maintenanceEditModal = ref<InstanceType<typeof MaintenanceEditModal>>();
const { data: maintenanceData, refresh: refreshList } = useAsyncData(
async () => {
const { data } = await api.maintenance.getAll({ status: maintenanceFilter.value });
console.log(data);
return data;
},
{
watch: [maintenanceFilter],
}
);
const stats = computed(() => {
if (!maintenanceData.value) return [];
return [
{
id: "count",
title: t("maintenance.total_entries"),
value: maintenanceData.value ? maintenanceData.value.length || 0 : 0,
type: "number" as StatsFormat,
},
];
});
</script>
<template>
<div>
<BaseContainer class="mb-6 flex flex-col gap-8">
<BaseSectionHeader> {{ $t("menu.maintenance") }} </BaseSectionHeader>
<section class="space-y-6">
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
<StatCard
v-for="stat in stats"
:key="stat.id"
class="stats border-l-primary block shadow-xl"
:title="stat.title"
:value="stat.value"
:type="stat.type"
/>
</div>
<div class="flex">
<div class="btn-group">
<button
class="btn btn-sm"
:class="`${maintenanceFilter == MaintenanceFilterStatus.MaintenanceFilterStatusScheduled ? 'btn-active' : ''}`"
@click="maintenanceFilter = MaintenanceFilterStatus.MaintenanceFilterStatusScheduled"
>
{{ $t("maintenance.filter.scheduled") }}
</button>
<button
class="btn btn-sm"
:class="`${maintenanceFilter == MaintenanceFilterStatus.MaintenanceFilterStatusCompleted ? 'btn-active' : ''}`"
@click="maintenanceFilter = MaintenanceFilterStatus.MaintenanceFilterStatusCompleted"
>
{{ $t("maintenance.filter.completed") }}
</button>
<button
class="btn btn-sm"
:class="`${maintenanceFilter == MaintenanceFilterStatus.MaintenanceFilterStatusBoth ? 'btn-active' : ''}`"
@click="maintenanceFilter = MaintenanceFilterStatus.MaintenanceFilterStatusBoth"
>
{{ $t("maintenance.filter.both") }}
</button>
</div>
</div>
</section>
<section>
<!-- begin -->
<MaintenanceEditModal ref="maintenanceEditModal" @changed="refreshList"></MaintenanceEditModal>
<div class="container space-y-6">
<BaseCard v-for="e in maintenanceData" :key="e.id">
<BaseSectionHeader class="border-b border-b-gray-300 p-6">
<span class="text-base-content">
<NuxtLink class="hover:underline" :to="`/item/${e.itemID}`">
{{ e.itemName }}
</NuxtLink>
-
{{ e.name }}
</span>
<template #description>
<div class="flex flex-wrap gap-2">
<div v-if="validDate(e.completedDate)" class="badge p-3">
<MdiCheck class="mr-2" />
<DateTime :date="e.completedDate" format="human" datetime-type="date" />
</div>
<div v-else-if="validDate(e.scheduledDate)" class="badge p-3">
<MdiCalendar class="mr-2" />
<DateTime :date="e.scheduledDate" format="human" datetime-type="date" />
</div>
<div class="tooltip tooltip-primary" data-tip="Cost">
<div class="badge badge-primary p-3">
<Currency :amount="e.cost" />
</div>
</div>
</div>
</template>
</BaseSectionHeader>
<div class="p-6">
<Markdown :source="e.description" />
</div>
<div class="flex justify-end gap-1 p-4">
<BaseButton size="sm" @click="maintenanceEditModal?.openUpdateModal(e)">
<template #icon>
<MdiEdit />
</template>
{{ $t("maintenance.list.edit") }}
</BaseButton>
<BaseButton size="sm" @click="maintenanceEditModal?.deleteEntry(e.id)">
<template #icon>
<MdiDelete />
</template>
{{ $t("maintenance.list.delete") }}
</BaseButton>
</div>
</BaseCard>
</div>
</section>
</BaseContainer>
</div>
</template>