Merge pull request #246 from mcarbonne/feat_maintenance_complete_and_duplicate

Feature: improve maintenance view with new actions
This commit is contained in:
Katos
2024-09-28 19:45:45 +01:00
committed by GitHub
15 changed files with 343 additions and 456 deletions

View File

@@ -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)

View File

@@ -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": [

View File

@@ -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": [

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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": [

View File

@@ -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>

View 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>

View File

@@ -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) {

View File

@@ -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;
/**

View File

@@ -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",

View File

@@ -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",

View File

@@ -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>

View File

@@ -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>