mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2026-01-04 20:14:54 +01:00
Merge pull request #246 from mcarbonne/feat_maintenance_complete_and_duplicate
Feature: improve maintenance view with new actions
This commit is contained in:
@@ -15,13 +15,14 @@ import (
|
||||
// @Summary Get Maintenance Log
|
||||
// @Tags Item Maintenance
|
||||
// @Produce json
|
||||
// @Success 200 {object} repo.MaintenanceLog
|
||||
// @Param filters query repo.MaintenanceFilters false "which maintenance to retrieve"
|
||||
// @Success 200 {array} repo.MaintenanceEntryWithDetails[]
|
||||
// @Router /v1/items/{id}/maintenance [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleMaintenanceLogGet() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, ID uuid.UUID, q repo.MaintenanceLogQuery) (repo.MaintenanceLog, error) {
|
||||
fn := func(r *http.Request, ID uuid.UUID, filters repo.MaintenanceFilters) ([]repo.MaintenanceEntryWithDetails, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
return ctrl.repo.MaintEntry.GetLog(auth, auth.GID, ID, q)
|
||||
return ctrl.repo.MaintEntry.GetMaintenanceByItemID(auth, auth.GID, ID, filters)
|
||||
}
|
||||
|
||||
return adapters.QueryID("id", fn, http.StatusOK)
|
||||
|
||||
@@ -920,11 +920,31 @@ const docTemplate = `{
|
||||
"Item Maintenance"
|
||||
],
|
||||
"summary": "Get Maintenance Log",
|
||||
"parameters": [
|
||||
{
|
||||
"enum": [
|
||||
"scheduled",
|
||||
"completed",
|
||||
"both"
|
||||
],
|
||||
"type": "string",
|
||||
"x-enum-varnames": [
|
||||
"MaintenanceFilterStatusScheduled",
|
||||
"MaintenanceFilterStatusCompleted",
|
||||
"MaintenanceFilterStatusBoth"
|
||||
],
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.MaintenanceLog"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.MaintenanceEntryWithDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2701,26 +2721,6 @@ const docTemplate = `{
|
||||
"MaintenanceFilterStatusBoth"
|
||||
]
|
||||
},
|
||||
"repo.MaintenanceLog": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"costAverage": {
|
||||
"type": "number"
|
||||
},
|
||||
"costTotal": {
|
||||
"type": "number"
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.MaintenanceEntry"
|
||||
}
|
||||
},
|
||||
"itemId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.NotifierCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -913,11 +913,31 @@
|
||||
"Item Maintenance"
|
||||
],
|
||||
"summary": "Get Maintenance Log",
|
||||
"parameters": [
|
||||
{
|
||||
"enum": [
|
||||
"scheduled",
|
||||
"completed",
|
||||
"both"
|
||||
],
|
||||
"type": "string",
|
||||
"x-enum-varnames": [
|
||||
"MaintenanceFilterStatusScheduled",
|
||||
"MaintenanceFilterStatusCompleted",
|
||||
"MaintenanceFilterStatusBoth"
|
||||
],
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.MaintenanceLog"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.MaintenanceEntryWithDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2694,26 +2714,6 @@
|
||||
"MaintenanceFilterStatusBoth"
|
||||
]
|
||||
},
|
||||
"repo.MaintenanceLog": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"costAverage": {
|
||||
"type": "number"
|
||||
},
|
||||
"costTotal": {
|
||||
"type": "number"
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.MaintenanceEntry"
|
||||
}
|
||||
},
|
||||
"itemId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.NotifierCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -511,19 +511,6 @@ definitions:
|
||||
- MaintenanceFilterStatusScheduled
|
||||
- MaintenanceFilterStatusCompleted
|
||||
- MaintenanceFilterStatusBoth
|
||||
repo.MaintenanceLog:
|
||||
properties:
|
||||
costAverage:
|
||||
type: number
|
||||
costTotal:
|
||||
type: number
|
||||
entries:
|
||||
items:
|
||||
$ref: '#/definitions/repo.MaintenanceEntry'
|
||||
type: array
|
||||
itemId:
|
||||
type: string
|
||||
type: object
|
||||
repo.NotifierCreate:
|
||||
properties:
|
||||
isActive:
|
||||
@@ -1249,13 +1236,27 @@ paths:
|
||||
- Items Attachments
|
||||
/v1/items/{id}/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:
|
||||
$ref: '#/definitions/repo.MaintenanceLog'
|
||||
items:
|
||||
$ref: '#/definitions/repo.MaintenanceEntryWithDetails'
|
||||
type: array
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Get Maintenance Log
|
||||
|
||||
@@ -59,13 +59,6 @@ type (
|
||||
Description string `json:"description"`
|
||||
Cost float64 `json:"cost,string"`
|
||||
}
|
||||
|
||||
MaintenanceLog struct {
|
||||
ItemID uuid.UUID `json:"itemId"`
|
||||
CostAverage float64 `json:"costAverage"`
|
||||
CostTotal float64 `json:"costTotal"`
|
||||
Entries []MaintenanceEntry `json:"entries"`
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -130,76 +123,32 @@ func (r *MaintenanceEntryRepository) Update(ctx context.Context, id uuid.UUID, i
|
||||
return mapMaintenanceEntryErr(item, err)
|
||||
}
|
||||
|
||||
type MaintenanceLogQuery struct {
|
||||
Completed bool `json:"completed" schema:"completed"`
|
||||
Scheduled bool `json:"scheduled" schema:"scheduled"`
|
||||
}
|
||||
|
||||
func (r *MaintenanceEntryRepository) GetLog(ctx context.Context, groupID, itemID uuid.UUID, query MaintenanceLogQuery) (MaintenanceLog, error) {
|
||||
log := MaintenanceLog{
|
||||
ItemID: itemID,
|
||||
}
|
||||
|
||||
q := r.db.MaintenanceEntry.Query().Where(
|
||||
func (r *MaintenanceEntryRepository) GetMaintenanceByItemID(ctx context.Context, groupID, itemID uuid.UUID, filters MaintenanceFilters) ([]MaintenanceEntryWithDetails, error) {
|
||||
query := r.db.MaintenanceEntry.Query().Where(
|
||||
maintenanceentry.ItemID(itemID),
|
||||
maintenanceentry.HasItemWith(
|
||||
item.HasGroupWith(group.IDEQ(groupID)),
|
||||
),
|
||||
)
|
||||
|
||||
if query.Completed {
|
||||
q = q.Where(maintenanceentry.And(
|
||||
maintenanceentry.DateNotNil(),
|
||||
maintenanceentry.DateNEQ(time.Time{}),
|
||||
if filters.Status == MaintenanceFilterStatusScheduled {
|
||||
query = query.Where(maintenanceentry.Or(
|
||||
maintenanceentry.DateIsNil(),
|
||||
maintenanceentry.DateEQ(time.Time{}),
|
||||
))
|
||||
} else if query.Scheduled {
|
||||
q = q.Where(maintenanceentry.And(
|
||||
maintenanceentry.Or(
|
||||
} else if filters.Status == MaintenanceFilterStatusCompleted {
|
||||
query = query.Where(
|
||||
maintenanceentry.Not(maintenanceentry.Or(
|
||||
maintenanceentry.DateIsNil(),
|
||||
maintenanceentry.DateEQ(time.Time{}),
|
||||
),
|
||||
maintenanceentry.ScheduledDateNotNil(),
|
||||
maintenanceentry.ScheduledDateNEQ(time.Time{}),
|
||||
))
|
||||
maintenanceentry.DateEQ(time.Time{})),
|
||||
))
|
||||
}
|
||||
entries, err := query.WithItem().Order(maintenanceentry.ByScheduledDate()).All(ctx)
|
||||
|
||||
entries, err := q.Order(ent.Desc(maintenanceentry.FieldDate)).
|
||||
All(ctx)
|
||||
if err != nil {
|
||||
return MaintenanceLog{}, err
|
||||
return []MaintenanceEntryWithDetails{}, err
|
||||
}
|
||||
|
||||
log.Entries = mapEachMaintenanceEntry(entries)
|
||||
|
||||
var maybeTotal *float64
|
||||
var maybeAverage *float64
|
||||
|
||||
statement := `
|
||||
SELECT
|
||||
SUM(cost_total) AS total_of_totals,
|
||||
AVG(cost_total) AS avg_of_averages
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
strftime('%m-%Y', date) AS my,
|
||||
SUM(cost) AS cost_total
|
||||
FROM
|
||||
maintenance_entries
|
||||
WHERE
|
||||
item_id = ?
|
||||
GROUP BY
|
||||
my
|
||||
)`
|
||||
|
||||
row := r.db.Sql().QueryRowContext(ctx, statement, itemID)
|
||||
err = row.Scan(&maybeTotal, &maybeAverage)
|
||||
if err != nil {
|
||||
return MaintenanceLog{}, err
|
||||
}
|
||||
|
||||
log.CostAverage = orDefault(maybeAverage, 0)
|
||||
log.CostTotal = orDefault(maybeTotal, 0)
|
||||
return log, nil
|
||||
return mapEachMaintenanceEntryWithDetails(entries), nil
|
||||
}
|
||||
|
||||
func (r *MaintenanceEntryRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
|
||||
@@ -60,27 +60,14 @@ func TestMaintenanceEntryRepository_GetLog(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get the log for the item
|
||||
log, err := tRepos.MaintEntry.GetLog(context.Background(), tGroup.ID, item.ID, MaintenanceLogQuery{
|
||||
Completed: true,
|
||||
})
|
||||
log, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item.ID, MaintenanceFilters{Status: MaintenanceFilterStatusCompleted})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get maintenance log: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, item.ID, log.ItemID)
|
||||
assert.Len(t, log.Entries, 10)
|
||||
assert.Len(t, log, 10)
|
||||
|
||||
// Calculate the average cost
|
||||
var total float64
|
||||
|
||||
for _, entry := range log.Entries {
|
||||
total += entry.Cost
|
||||
}
|
||||
|
||||
assert.InDelta(t, total, log.CostTotal, .001, "total cost should be equal to the sum of all entries")
|
||||
assert.InDelta(t, total/2, log.CostAverage, 001, "average cost should be the average of the two months")
|
||||
|
||||
for _, entry := range log.Entries {
|
||||
for _, entry := range log {
|
||||
err := tRepos.MaintEntry.Delete(context.Background(), entry.ID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -913,11 +913,31 @@
|
||||
"Item Maintenance"
|
||||
],
|
||||
"summary": "Get Maintenance Log",
|
||||
"parameters": [
|
||||
{
|
||||
"enum": [
|
||||
"scheduled",
|
||||
"completed",
|
||||
"both"
|
||||
],
|
||||
"type": "string",
|
||||
"x-enum-varnames": [
|
||||
"MaintenanceFilterStatusScheduled",
|
||||
"MaintenanceFilterStatusCompleted",
|
||||
"MaintenanceFilterStatusBoth"
|
||||
],
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.MaintenanceLog"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.MaintenanceEntryWithDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2694,26 +2714,6 @@
|
||||
"MaintenanceFilterStatusBoth"
|
||||
]
|
||||
},
|
||||
"repo.MaintenanceLog": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"costAverage": {
|
||||
"type": "number"
|
||||
},
|
||||
"costTotal": {
|
||||
"type": "number"
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.MaintenanceEntry"
|
||||
}
|
||||
},
|
||||
"itemId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.NotifierCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -135,5 +135,30 @@
|
||||
emit("changed");
|
||||
}
|
||||
|
||||
defineExpose({ openCreateModal, openUpdateModal, deleteEntry });
|
||||
async function complete(maintenanceEntry: MaintenanceEntry) {
|
||||
const { error } = await api.maintenance.update(maintenanceEntry.id, {
|
||||
name: maintenanceEntry.name,
|
||||
completedDate: new Date(Date.now()),
|
||||
scheduledDate: maintenanceEntry.scheduledDate ?? "null",
|
||||
description: maintenanceEntry.description,
|
||||
cost: maintenanceEntry.cost,
|
||||
});
|
||||
if (error) {
|
||||
toast.error(t("maintenance.toast.failed_to_update"));
|
||||
}
|
||||
emit("changed");
|
||||
}
|
||||
|
||||
function duplicate(maintenanceEntry: MaintenanceEntry | MaintenanceEntryWithDetails, itemId: string) {
|
||||
entry.id = null;
|
||||
entry.name = maintenanceEntry.name;
|
||||
entry.completedDate = null;
|
||||
entry.scheduledDate = null;
|
||||
entry.description = maintenanceEntry.description;
|
||||
entry.cost = maintenanceEntry.cost;
|
||||
entry.itemId = itemId;
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
defineExpose({ openCreateModal, openUpdateModal, deleteEntry, complete, duplicate });
|
||||
</script>
|
||||
|
||||
201
frontend/components/Maintenance/ListView.vue
Normal file
201
frontend/components/Maintenance/ListView.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { MaintenanceEntryWithDetails } from "~~/lib/api/types/data-contracts";
|
||||
import { MaintenanceFilterStatus } from "~~/lib/api/types/data-contracts";
|
||||
import type { StatsFormat } from "~~/components/global/StatCard/types";
|
||||
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 MdiPlus from "~icons/mdi/plus";
|
||||
import MdiWrenchClock from "~icons/mdi/wrench-clock";
|
||||
import MdiContentDuplicate from "~icons/mdi/content-duplicate";
|
||||
import MaintenanceEditModal from "~~/components/Maintenance/EditModal.vue";
|
||||
|
||||
const maintenanceFilterStatus = ref(MaintenanceFilterStatus.MaintenanceFilterStatusScheduled);
|
||||
const maintenanceEditModal = ref<InstanceType<typeof MaintenanceEditModal>>();
|
||||
|
||||
const api = useUserApi();
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
currentItemId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: maintenanceDataList, refresh: refreshList } = useAsyncData<MaintenanceEntryWithDetails[]>(
|
||||
async () => {
|
||||
const { data } =
|
||||
props.currentItemId !== undefined
|
||||
? await api.items.maintenance.getLog(props.currentItemId, { status: maintenanceFilterStatus.value })
|
||||
: await api.maintenance.getAll({ status: maintenanceFilterStatus.value });
|
||||
console.log(data);
|
||||
return data as MaintenanceEntryWithDetails[];
|
||||
},
|
||||
{
|
||||
watch: maintenanceFilterStatus,
|
||||
}
|
||||
);
|
||||
|
||||
const stats = computed(() => {
|
||||
console.log(maintenanceDataList);
|
||||
if (!maintenanceDataList.value) return [];
|
||||
|
||||
const count = maintenanceDataList.value ? maintenanceDataList.value.length || 0 : 0;
|
||||
let total = 0;
|
||||
maintenanceDataList.value.forEach(item => {
|
||||
total += parseFloat(item.cost);
|
||||
});
|
||||
|
||||
const average = count > 0 ? total / count : 0;
|
||||
|
||||
return [
|
||||
{
|
||||
id: "count",
|
||||
title: t("maintenance.total_entries"),
|
||||
value: count,
|
||||
type: "number" as StatsFormat,
|
||||
},
|
||||
{
|
||||
id: "total",
|
||||
title: t("maintenance.total_cost"),
|
||||
value: total,
|
||||
type: "currency" as StatsFormat,
|
||||
},
|
||||
{
|
||||
id: "average",
|
||||
title: t("maintenance.monthly_average"),
|
||||
value: average,
|
||||
type: "currency" as StatsFormat,
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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">
|
||||
<BaseButton
|
||||
size="sm"
|
||||
:class="`${maintenanceFilterStatus == MaintenanceFilterStatus.MaintenanceFilterStatusScheduled ? 'btn-active' : ''}`"
|
||||
@click="maintenanceFilterStatus = MaintenanceFilterStatus.MaintenanceFilterStatusScheduled"
|
||||
>
|
||||
{{ $t("maintenance.filter.scheduled") }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
size="sm"
|
||||
:class="`${maintenanceFilterStatus == MaintenanceFilterStatus.MaintenanceFilterStatusCompleted ? 'btn-active' : ''}`"
|
||||
@click="maintenanceFilterStatus = MaintenanceFilterStatus.MaintenanceFilterStatusCompleted"
|
||||
>
|
||||
{{ $t("maintenance.filter.completed") }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
size="sm"
|
||||
:class="`${maintenanceFilterStatus == MaintenanceFilterStatus.MaintenanceFilterStatusBoth ? 'btn-active' : ''}`"
|
||||
@click="maintenanceFilterStatus = MaintenanceFilterStatus.MaintenanceFilterStatusBoth"
|
||||
>
|
||||
{{ $t("maintenance.filter.both") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<BaseButton
|
||||
v-if="props.currentItemId"
|
||||
class="ml-auto"
|
||||
size="sm"
|
||||
@click="maintenanceEditModal?.openCreateModal(props.currentItemId)"
|
||||
>
|
||||
<template #icon>
|
||||
<MdiPlus />
|
||||
</template>
|
||||
{{ $t("maintenance.list.new") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<!-- begin -->
|
||||
<MaintenanceEditModal ref="maintenanceEditModal" @changed="refreshList"></MaintenanceEditModal>
|
||||
<div class="container space-y-6">
|
||||
<BaseCard v-for="e in maintenanceDataList" :key="e.id">
|
||||
<BaseSectionHeader class="border-b border-b-gray-300 p-6">
|
||||
<span class="text-base-content">
|
||||
<span v-if="!props.currentItemId">
|
||||
<NuxtLink class="hover:underline" :to="`/item/${(e as MaintenanceEntryWithDetails).itemID}`">
|
||||
{{ (e as MaintenanceEntryWithDetails).itemName }}
|
||||
</NuxtLink>
|
||||
-
|
||||
</span>
|
||||
{{ 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 flex-wrap justify-end gap-1 p-4">
|
||||
<BaseButton size="sm" @click="maintenanceEditModal?.openUpdateModal(e)">
|
||||
<template #icon>
|
||||
<MdiEdit />
|
||||
</template>
|
||||
{{ $t("maintenance.list.edit") }}
|
||||
</BaseButton>
|
||||
<BaseButton v-if="!validDate(e.completedDate)" size="sm" @click="maintenanceEditModal?.complete(e)">
|
||||
<template #icon>
|
||||
<MdiCheck />
|
||||
</template>
|
||||
{{ $t("maintenance.list.complete") }}
|
||||
</BaseButton>
|
||||
<BaseButton size="sm" @click="maintenanceEditModal?.duplicate(e, e.itemID)">
|
||||
<template #icon>
|
||||
<MdiContentDuplicate />
|
||||
</template>
|
||||
{{ $t("maintenance.list.duplicate") }}
|
||||
</BaseButton>
|
||||
<BaseButton size="sm" class="btn-error" @click="maintenanceEditModal?.deleteEntry(e.id)">
|
||||
<template #icon>
|
||||
<MdiDelete />
|
||||
</template>
|
||||
{{ $t("maintenance.list.delete") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</BaseCard>
|
||||
<div v-if="props.currentItemId" class="hidden first:block">
|
||||
<button
|
||||
type="button"
|
||||
class="border-base-content relative block w-full rounded-lg border-2 border-dashed p-12 text-center"
|
||||
@click="maintenanceEditModal?.openCreateModal(props.currentItemId)"
|
||||
>
|
||||
<MdiWrenchClock class="inline size-16" />
|
||||
<span class="mt-2 block text-sm font-medium text-gray-900"> {{ $t("maintenance.list.create_first") }} </span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -10,9 +10,10 @@ import type {
|
||||
ItemUpdate,
|
||||
MaintenanceEntry,
|
||||
MaintenanceEntryCreate,
|
||||
MaintenanceLog,
|
||||
MaintenanceEntryWithDetails,
|
||||
} from "../types/data-contracts";
|
||||
import type { AttachmentTypes, PaginationResult } from "../types/non-generated";
|
||||
import type { MaintenanceFilters } from "./maintenance.ts";
|
||||
import type { Requests } from "~~/lib/requests";
|
||||
|
||||
export type ItemsQuery = {
|
||||
@@ -65,14 +66,11 @@ export class FieldsAPI extends BaseAPI {
|
||||
}
|
||||
}
|
||||
|
||||
type MaintenanceEntryQuery = {
|
||||
scheduled?: boolean;
|
||||
completed?: boolean;
|
||||
};
|
||||
|
||||
export class ItemMaintenanceAPI extends BaseAPI {
|
||||
getLog(itemId: string, q: MaintenanceEntryQuery = {}) {
|
||||
return this.http.get<MaintenanceLog>({ url: route(`/items/${itemId}/maintenance`, q) });
|
||||
getLog(itemId: string, filters: MaintenanceFilters = {}) {
|
||||
return this.http.get<MaintenanceEntryWithDetails[]>({
|
||||
url: route(`/items/${itemId}/maintenance`, { status: filters.status?.toString() }),
|
||||
});
|
||||
}
|
||||
|
||||
create(itemId: string, data: MaintenanceEntryCreate) {
|
||||
|
||||
@@ -306,13 +306,6 @@ export enum MaintenanceFilterStatus {
|
||||
MaintenanceFilterStatusBoth = "both",
|
||||
}
|
||||
|
||||
export interface MaintenanceLog {
|
||||
costAverage: number;
|
||||
costTotal: number;
|
||||
entries: MaintenanceEntry[];
|
||||
itemId: string;
|
||||
}
|
||||
|
||||
export interface NotifierCreate {
|
||||
isActive: boolean;
|
||||
/**
|
||||
|
||||
@@ -140,7 +140,9 @@
|
||||
"create_first": "Create Your First Entry",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"new": "New"
|
||||
"new": "New",
|
||||
"complete": "Complete",
|
||||
"duplicate" : "Duplicate"
|
||||
},
|
||||
"modal": {
|
||||
"completed_date": "Completed Date",
|
||||
|
||||
@@ -140,7 +140,9 @@
|
||||
"create_first": "Créer votre première entrée",
|
||||
"delete": "Supprimer",
|
||||
"edit": "Modifier",
|
||||
"new": "Ajouter"
|
||||
"new": "Ajouter",
|
||||
"complete": "Terminer",
|
||||
"duplicate" : "Dupliquer"
|
||||
},
|
||||
"modal": {
|
||||
"completed_date": "Date d'achèvement",
|
||||
|
||||
@@ -1,158 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { StatsFormat } from "~~/components/global/StatCard/types";
|
||||
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;
|
||||
}>();
|
||||
|
||||
const api = useUserApi();
|
||||
const toast = useNotifier();
|
||||
|
||||
const scheduled = ref(true);
|
||||
|
||||
const maintenanceEditModal = ref<InstanceType<typeof MaintenanceEditModal>>();
|
||||
|
||||
watch(
|
||||
() => scheduled.value,
|
||||
() => {
|
||||
refreshLog();
|
||||
}
|
||||
);
|
||||
|
||||
const { data: log, refresh: refreshLog } = useAsyncData(async () => {
|
||||
const { data } = await api.items.maintenance.getLog(props.item.id, {
|
||||
scheduled: scheduled.value,
|
||||
completed: !scheduled.value,
|
||||
});
|
||||
return data;
|
||||
});
|
||||
|
||||
const count = computed(() => {
|
||||
if (!log.value) return 0;
|
||||
return log.value.entries.length;
|
||||
});
|
||||
const stats = computed(() => {
|
||||
if (!log.value) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
id: "count",
|
||||
title: t("maintenance.total_entries"),
|
||||
value: count.value || 0,
|
||||
type: "number" as StatsFormat,
|
||||
},
|
||||
{
|
||||
id: "total",
|
||||
title: t("maintenance.total_cost"),
|
||||
value: log.value.costTotal || 0,
|
||||
type: "currency" as StatsFormat,
|
||||
},
|
||||
{
|
||||
id: "average",
|
||||
title: t("maintenance.monthly_average"),
|
||||
value: log.value.costAverage || 0,
|
||||
type: "currency" as StatsFormat,
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="log">
|
||||
<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 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="`${scheduled ? 'btn-active' : ''}`" @click="scheduled = true">
|
||||
{{ $t("maintenance.filter.scheduled") }}
|
||||
</button>
|
||||
<button class="btn btn-sm" :class="`${scheduled ? '' : 'btn-active'}`" @click="scheduled = false">
|
||||
{{ $t("maintenance.filter.completed") }}
|
||||
</button>
|
||||
</div>
|
||||
<BaseButton class="ml-auto" size="sm" @click="maintenanceEditModal?.openCreateModal(props.item.id)">
|
||||
<template #icon>
|
||||
<MdiPlus />
|
||||
</template>
|
||||
{{ $t("maintenance.list.new") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div class="container space-y-6">
|
||||
<BaseCard v-for="e in log.entries" :key="e.id">
|
||||
<BaseSectionHeader class="border-b border-b-gray-300 p-6">
|
||||
<span class="text-base-content">
|
||||
{{ 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 class="hidden first:block">
|
||||
<button
|
||||
type="button"
|
||||
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">
|
||||
{{ $t("maintenance.list.create_first") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<BaseContainer class="mb-6 flex flex-col gap-8">
|
||||
<MaintenanceListView :current-item-id="props.item.id"></MaintenanceListView>
|
||||
</BaseContainer>
|
||||
</template>
|
||||
|
||||
@@ -1,137 +1,10 @@
|
||||
<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>
|
||||
<script setup lang="ts"></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>
|
||||
<MaintenanceListView></MaintenanceListView>
|
||||
</BaseContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user