mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-24 14:31:55 +01:00
Compare commits
272 Commits
katos/purc
...
v0.15.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f386d1213f | ||
|
|
9b7b00e8f2 | ||
|
|
055f0219a8 | ||
|
|
9d3f3cf1da | ||
|
|
da8cc19838 | ||
|
|
a3d5485c1d | ||
|
|
865661097c | ||
|
|
ab48f55335 | ||
|
|
49f52cada4 | ||
|
|
059bc5f16c | ||
|
|
a8eab724f9 | ||
|
|
eed967e9b4 | ||
|
|
2eb421455b | ||
|
|
e0d86ce745 | ||
|
|
ba8f32cec8 | ||
|
|
4af6bf210e | ||
|
|
1de3ecda19 | ||
|
|
4a0039c838 | ||
|
|
02a34ed1a0 | ||
|
|
dc23a1ae23 | ||
|
|
4fea927880 | ||
|
|
ff874ac472 | ||
|
|
ba48a615cd | ||
|
|
5aa389b13c | ||
|
|
b500f6b51f | ||
|
|
2ea9ed0476 | ||
|
|
68ee701e46 | ||
|
|
dc88406bcc | ||
|
|
ca85d4b483 | ||
|
|
17ecfa2c66 | ||
|
|
cabaa07384 | ||
|
|
d9b05e872c | ||
|
|
bd60c36240 | ||
|
|
1fc9843450 | ||
|
|
4e260c5a8b | ||
|
|
55a9355046 | ||
|
|
5bee3dd429 | ||
|
|
a85c42b539 | ||
|
|
897f3842e0 | ||
|
|
791d390187 | ||
|
|
e85b1a44b1 | ||
|
|
ffd59d535c | ||
|
|
e32a1041b1 | ||
|
|
c16269bcb4 | ||
|
|
81d57d4c47 | ||
|
|
bdb3b5dc24 | ||
|
|
666903e663 | ||
|
|
24deb462a8 | ||
|
|
85b54c4ccf | ||
|
|
ca33d499ab | ||
|
|
52ebff7ca4 | ||
|
|
4cf0b0a9df | ||
|
|
0d92d2718d | ||
|
|
22937dd314 | ||
|
|
26a99b6bad | ||
|
|
4bf7f3e00c | ||
|
|
269c5ea7e3 | ||
|
|
bac6212188 | ||
|
|
7a7d00220a | ||
|
|
a20f94b68a | ||
|
|
7c4c373851 | ||
|
|
8b0684a0ca | ||
|
|
c903a9df13 | ||
|
|
a5d4e4f491 | ||
|
|
f3116e4729 | ||
|
|
c0b88142d8 | ||
|
|
19adc6a441 | ||
|
|
d9fa7a846c | ||
|
|
aa30fba7af | ||
|
|
edbb4b9fa0 | ||
|
|
7d8534ffc1 | ||
|
|
28b79beb76 | ||
|
|
8a7af9a98d | ||
|
|
47600ade0e | ||
|
|
2af0cfeb49 | ||
|
|
f6690dc80f | ||
|
|
9de5cf8c58 | ||
|
|
9e66bf0bc1 | ||
|
|
5f27f5d9cc | ||
|
|
0a1667ed24 | ||
|
|
6348aeac6e | ||
|
|
7e9bd7f44b | ||
|
|
cf780393c2 | ||
|
|
e94f436ba6 | ||
|
|
0c6be05b05 | ||
|
|
9297ac59ae | ||
|
|
983dedfdad | ||
|
|
d159908b91 | ||
|
|
1b53a5235c | ||
|
|
f3f709748e | ||
|
|
64ceffefe9 | ||
|
|
4b24653b86 | ||
|
|
6a0ebb76ea | ||
|
|
8e273730be | ||
|
|
5c09953e4c | ||
|
|
6866dc76c0 | ||
|
|
de16f09108 | ||
|
|
3f3ca345fd | ||
|
|
9275d2db9c | ||
|
|
d88b04b66f | ||
|
|
62e6b08baf | ||
|
|
6fa37cb474 | ||
|
|
d63d6e94dd | ||
|
|
0c8ce366eb | ||
|
|
3da3025935 | ||
|
|
d1a57e3ec5 | ||
|
|
969ef1941b | ||
|
|
214b16a26e | ||
|
|
405d0c7487 | ||
|
|
6800c2112e | ||
|
|
5fc7b3e25b | ||
|
|
88dc943b6b | ||
|
|
f8482b1c64 | ||
|
|
a6e49295e0 | ||
|
|
8ef1b8b6ce | ||
|
|
404791a344 | ||
|
|
073aade67f | ||
|
|
784cc409d4 | ||
|
|
9e3f82fbac | ||
|
|
19c6d4dec5 | ||
|
|
b18f0c790b | ||
|
|
fb62f51958 | ||
|
|
dafc6aa13f | ||
|
|
93f13b1e80 | ||
|
|
5de649d85f | ||
|
|
b37cf24f09 | ||
|
|
cf2edc8d34 | ||
|
|
f113de180b | ||
|
|
adb4b52752 | ||
|
|
baf8912dda | ||
|
|
489deda6a8 | ||
|
|
2fee607327 | ||
|
|
c428a22b5b | ||
|
|
42c01adb98 | ||
|
|
209bb2932c | ||
|
|
ec9cdb391a | ||
|
|
a6aafeb374 | ||
|
|
ffb538ef21 | ||
|
|
15925de2f0 | ||
|
|
25d72044e9 | ||
|
|
6b598383d3 | ||
|
|
25c76522d6 | ||
|
|
c0e2aa5c62 | ||
|
|
d0b9f742ae | ||
|
|
80d56829c5 | ||
|
|
0946310f60 | ||
|
|
7c855cf55d | ||
|
|
0ab95fb670 | ||
|
|
1e81b4bab4 | ||
|
|
67c50068d9 | ||
|
|
c3628e36f7 | ||
|
|
526799c6da | ||
|
|
4ef7529533 | ||
|
|
b06d670dff | ||
|
|
02c0453ff3 | ||
|
|
09358aa5b2 | ||
|
|
229d4db996 | ||
|
|
184be32f3a | ||
|
|
dbe77ea19d | ||
|
|
85e5c7e8e7 | ||
|
|
3c273b370d | ||
|
|
343e56b440 | ||
|
|
3a949aee5a | ||
|
|
1601e52c9c | ||
|
|
760cc8e35c | ||
|
|
6051e1fb8b | ||
|
|
7b146947df | ||
|
|
5497a10f9f | ||
|
|
3e6f4b3657 | ||
|
|
7baf58ad61 | ||
|
|
d72437d18c | ||
|
|
ea57981953 | ||
|
|
c2d0cce02d | ||
|
|
9f7a119e95 | ||
|
|
0dacc97e99 | ||
|
|
52a44da56b | ||
|
|
7114f262c2 | ||
|
|
7647ea96d1 | ||
|
|
593da25cdb | ||
|
|
f22bce7ccb | ||
|
|
1688773bba | ||
|
|
b56b5d2400 | ||
|
|
33ee208071 | ||
|
|
fe880cc2c7 | ||
|
|
cffe57b74e | ||
|
|
66882d6fd9 | ||
|
|
050f22f051 | ||
|
|
7891af3a9a | ||
|
|
40cbccf50a | ||
|
|
0348da362c | ||
|
|
f0a3780f3a | ||
|
|
8058743c2f | ||
|
|
39163f3cfc | ||
|
|
43676ab407 | ||
|
|
a6bdadedb1 | ||
|
|
639f795b9a | ||
|
|
fc95d2cab8 | ||
|
|
695b6d68e6 | ||
|
|
b6c265098d | ||
|
|
2a80d348bd | ||
|
|
d08dafd965 | ||
|
|
c6542de93d | ||
|
|
5928678564 | ||
|
|
fac52ca122 | ||
|
|
e9d270269f | ||
|
|
0a4c5fbb28 | ||
|
|
2bfb0283d9 | ||
|
|
94e8aee36f | ||
|
|
8051956a2e | ||
|
|
c7020503be | ||
|
|
28edce96d9 | ||
|
|
b4481fcc84 | ||
|
|
2be2bebb4e | ||
|
|
d4bb8def62 | ||
|
|
7442cb01b7 | ||
|
|
95ba8275e8 | ||
|
|
2f4a0dd212 | ||
|
|
52a621e9ba | ||
|
|
1f77fad829 | ||
|
|
8d93a3f56e | ||
|
|
9c572e7ab2 | ||
|
|
1f15e74730 | ||
|
|
7570a04c02 | ||
|
|
fc2e89c448 | ||
|
|
be216ff7fe | ||
|
|
388208571b | ||
|
|
1891903007 | ||
|
|
41a7e73ff4 | ||
|
|
81d9fb0700 | ||
|
|
76312d6eb6 | ||
|
|
f31528c841 | ||
|
|
6d869fdece | ||
|
|
d0784a7773 | ||
|
|
791f843bc8 | ||
|
|
a0cdb231fd | ||
|
|
e0004842e6 | ||
|
|
fdbfa0e76f | ||
|
|
005516013f | ||
|
|
9ec3dd4b16 | ||
|
|
4dacf981a9 | ||
|
|
9f7b76b37d | ||
|
|
0d51558e74 | ||
|
|
236c257892 | ||
|
|
3540ce4297 | ||
|
|
fef026ed47 | ||
|
|
0bcb155756 | ||
|
|
12219522ab | ||
|
|
6001bf90c5 | ||
|
|
13864997ab | ||
|
|
2ab2766534 | ||
|
|
e051352070 | ||
|
|
3bf1e50620 | ||
|
|
42f3c88396 | ||
|
|
c9f31ef934 | ||
|
|
01f54aeb52 | ||
|
|
f51de34355 | ||
|
|
4e9647651c | ||
|
|
2d1016d362 | ||
|
|
b48c961ac1 | ||
|
|
4d47567995 | ||
|
|
6b2e3accf7 | ||
|
|
c1f8520c4f | ||
|
|
41eb99ec40 | ||
|
|
97a74127fb | ||
|
|
a9396167bf | ||
|
|
3385e5684e | ||
|
|
bb9672214c | ||
|
|
1b93672417 | ||
|
|
8b1cedd4a8 | ||
|
|
2c34047b6d | ||
|
|
f0942f0714 | ||
|
|
967e574ea8 |
16
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
16
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: "Bug Report"
|
||||
description: "Submit a bug report for the current release"
|
||||
labels: ["bug"]
|
||||
labels: ["🕷️ bug"]
|
||||
projects: ["sysadminsmedia/2"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
@@ -20,6 +20,8 @@ body:
|
||||
required: true
|
||||
- label: I already read the docs and didn't find an answer.
|
||||
required: true
|
||||
- label: I can replicate the issue inside the Demo install.
|
||||
required: true
|
||||
- type: input
|
||||
id: homebox-version
|
||||
attributes:
|
||||
@@ -55,6 +57,18 @@ body:
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: arch
|
||||
attributes:
|
||||
label: OS Architechture
|
||||
description: What type of processor are you running on.
|
||||
multiple: true
|
||||
options:
|
||||
- x86_64 (AMD, Intel)
|
||||
- ARM64
|
||||
- ARM/v7
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: os-details
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: "Feature Request"
|
||||
description: "Submit a feature request for the current release"
|
||||
labels: ["enhancement"]
|
||||
labels: ["⬆️ enhancement"]
|
||||
projects: ["sysadminsmedia/2"]
|
||||
body:
|
||||
- type: textarea
|
||||
|
||||
65
.github/scripts/update_currencies.py
vendored
Normal file
65
.github/scripts/update_currencies.py
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
|
||||
def fetch_currencies():
|
||||
try:
|
||||
response = requests.get('https://restcountries.com/v3.1/all')
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.Timeout:
|
||||
print("Request to the API timed out.")
|
||||
return []
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"An error occurred while making the request: {e}")
|
||||
return []
|
||||
|
||||
try:
|
||||
countries = response.json()
|
||||
except json.JSONDecodeError:
|
||||
print("Failed to decode JSON from the response.")
|
||||
return []
|
||||
|
||||
currencies_list = []
|
||||
for country in countries:
|
||||
country_name = country.get('name', {}).get('common')
|
||||
country_currencies = country.get('currencies', {})
|
||||
for currency_code, currency_info in country_currencies.items():
|
||||
symbol = currency_info.get('symbol', '')
|
||||
currencies_list.append({
|
||||
'code': currency_code,
|
||||
'local': country_name,
|
||||
'symbol': symbol,
|
||||
'name': currency_info.get('name')
|
||||
})
|
||||
|
||||
return currencies_list
|
||||
|
||||
def save_currencies(currencies, file_path):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(currencies, f, ensure_ascii=False, indent=4)
|
||||
except IOError as e:
|
||||
print(f"An error occurred while writing to the file: {e}")
|
||||
|
||||
def load_existing_currencies(file_path):
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except (IOError, json.JSONDecodeError):
|
||||
return [] # Return an empty list if file doesn't exist or is invalid
|
||||
|
||||
def main():
|
||||
save_path = 'backend/internal/core/currencies/currencies.json'
|
||||
|
||||
existing_currencies = load_existing_currencies(save_path)
|
||||
new_currencies = fetch_currencies()
|
||||
|
||||
if new_currencies == existing_currencies:
|
||||
print("Currencies up-to-date with API, skipping commit.")
|
||||
else:
|
||||
save_currencies(new_currencies, save_path)
|
||||
print("Currencies updated and saved.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,7 +2,7 @@ name: Docker publish rootless
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '00 6 * * *'
|
||||
- cron: '00 0 * * *'
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
@@ -92,8 +92,8 @@ jobs:
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
COMMIT=${{ github.sha }}
|
||||
|
||||
6
.github/workflows/docker-publish.yaml
vendored
6
.github/workflows/docker-publish.yaml
vendored
@@ -2,7 +2,7 @@ name: Docker publish
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '00 6 * * *'
|
||||
- cron: '00 0 * * *'
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
@@ -89,8 +89,8 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
COMMIT=${{ github.sha }}
|
||||
|
||||
100
.github/workflows/update-currencies.yml
vendored
Normal file
100
.github/workflows/update-currencies.yml
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
name: Update Currencies
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
update-currencies:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.8'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
|
||||
- name: Run currency fetch script
|
||||
run: python .github/scripts/update_currencies.py
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
run: |
|
||||
if [[ $(git status --porcelain) ]]; then
|
||||
echo "Changes detected."
|
||||
echo "changes=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "No changes detected."
|
||||
echo "changes=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Delete existing update-currencies branch
|
||||
run: |
|
||||
if git show-ref --verify --quiet refs/heads/update-currencies; then
|
||||
git branch -D update-currencies
|
||||
echo "Deleted existing update-currencies branch."
|
||||
else
|
||||
echo "No existing update-currencies branch to delete."
|
||||
fi
|
||||
|
||||
- name: Create new update-currencies branch
|
||||
if: env.changes == 'true'
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Create a new branch
|
||||
git checkout -b update-currencies
|
||||
git add backend/internal/core/currencies/currencies.json
|
||||
git commit -m "Update currencies.json"
|
||||
|
||||
# Fetch the latest changes from the remote
|
||||
git fetch origin
|
||||
|
||||
# Attempt to rebase with the latest changes
|
||||
if git show-ref --verify --quiet refs/remotes/origin/update-currencies; then
|
||||
if ! git rebase origin/update-currencies; then
|
||||
echo "Rebase conflicts occurred. Please resolve them manually."
|
||||
echo "To resolve conflicts, check out the 'update-currencies' branch locally."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No existing remote branch 'update-currencies'. Skipping rebase."
|
||||
fi
|
||||
|
||||
# Push the new branch to the remote
|
||||
if ! git push --set-upstream origin update-currencies; then
|
||||
echo "Push failed, trying to fetch and rebase again."
|
||||
git fetch origin
|
||||
if git show-ref --verify --quiet refs/remotes/origin/update-currencies; then
|
||||
if ! git rebase origin/update-currencies; then
|
||||
echo "Second rebase failed. Please resolve manually."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No existing remote branch 'update-currencies'. Skipping rebase."
|
||||
fi
|
||||
if ! git push --set-upstream origin update-currencies; then
|
||||
echo "Second push failed. Please resolve manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create a pull request
|
||||
curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-X POST \
|
||||
-d '{"title": "Update currencies", "head": "update-currencies", "base": "main"}' \
|
||||
https://api.github.com/repos/${{ github.repository }}/pulls
|
||||
|
||||
- name: Notify no changes
|
||||
if: env.changes == 'false'
|
||||
run: echo "Currencies up-to-date with API, skipping commit."
|
||||
19
Dockerfile
19
Dockerfile
@@ -1,13 +1,23 @@
|
||||
# Node dependencies
|
||||
FROM node:18-alpine AS frontend-dependencies
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile --shamefully-hoist
|
||||
|
||||
# Build Nuxt
|
||||
FROM node:18-alpine AS frontend-builder
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile --shamefully-hoist
|
||||
COPY frontend .
|
||||
COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
||||
RUN pnpm build
|
||||
|
||||
FROM golang:alpine AS builder-dependencies
|
||||
WORKDIR /go/src/app
|
||||
COPY ./backend .
|
||||
RUN go mod download
|
||||
|
||||
# Build API
|
||||
FROM golang:alpine AS builder
|
||||
ARG BUILD_TIME
|
||||
@@ -19,10 +29,11 @@ RUN apk update && \
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY ./backend .
|
||||
RUN go get -d -v ./...
|
||||
RUN rm -rf ./app/api/public
|
||||
COPY --from=frontend-builder /app/.output/public ./app/api/static/public
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||
COPY --from=builder-dependencies /go/pkg/mod /go/pkg/mod
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=linux go build \
|
||||
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
|
||||
-o /go/bin/api \
|
||||
-v ./app/api/*.go
|
||||
|
||||
@@ -1,35 +1,42 @@
|
||||
# Node dependencies
|
||||
FROM node:18-alpine AS frontend-dependencies
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile --shamefully-hoist
|
||||
|
||||
# Build Nuxt
|
||||
FROM node:18-alpine AS frontend-builder
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile --shamefully-hoist
|
||||
COPY frontend .
|
||||
COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
||||
RUN pnpm build
|
||||
|
||||
FROM golang:alpine AS builder-dependencies
|
||||
WORKDIR /go/src/app
|
||||
COPY ./backend .
|
||||
RUN go mod download
|
||||
|
||||
# Build API
|
||||
FROM golang:alpine AS builder
|
||||
ARG BUILD_TIME
|
||||
ARG COMMIT
|
||||
ARG VERSION
|
||||
ARG BUSYBOX_VERSION=1.36.1-r31
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add --update git build-base gcc g++
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY ./backend .
|
||||
RUN go get -d -v ./...
|
||||
RUN rm -rf ./app/api/public
|
||||
COPY --from=frontend-builder /app/.output/public ./app/api/static/public
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||
COPY --from=builder-dependencies /go/pkg/mod /go/pkg/mod
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=linux go build \
|
||||
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
|
||||
-o /go/bin/api \
|
||||
-v ./app/api/*.go && \
|
||||
chmod +x /go/bin/api && \
|
||||
# create a directory so that we can copy it in the next stage
|
||||
mkdir /data
|
||||
-v ./app/api/*.go
|
||||
|
||||
FROM gcr.io/distroless/java:latest
|
||||
|
||||
|
||||
11
README.md
11
README.md
@@ -4,9 +4,9 @@
|
||||
|
||||
<h1 align="center" style="margin-top: -10px"> HomeBox </h1>
|
||||
<p align="center" style="width: 100;">
|
||||
<a href="https://homebox.sysadminsmedia.com">Docs</a>
|
||||
<a href="https://homebox.software/en/">Docs</a>
|
||||
|
|
||||
<a href="https://homebox.fly.dev">Demo</a>
|
||||
<a href="https://demo.homebox.software">Demo</a>
|
||||
|
|
||||
<a href="https://discord.gg/aY4DCkpNA9">Discord</a>
|
||||
</p>
|
||||
@@ -24,7 +24,7 @@ Check out screenshots of the project [here](https://imgur.com/a/5gLWt2j).
|
||||
|
||||
## Quick Start
|
||||
|
||||
[Configuration & Docker Compose](https://homebox.sysadminsmedia.com/en/quick-start.html)
|
||||
[Configuration & Docker Compose](https://homebox.software/en/quick-start.html)
|
||||
|
||||
```bash
|
||||
# If using the rootless image, ensure data
|
||||
@@ -49,6 +49,11 @@ Contributions are what make the open source community such an amazing place to l
|
||||
|
||||
If you are not a coder, you can still contribute financially. Financial contributions help me prioritize working on this project over others and helps me know that there is a real demand for project development.
|
||||
|
||||
## Help us Translate
|
||||
We want to make sure that Homebox is available in as many languages as possible. If you are interested in helping us translate Homebox, please help us via our [Weblate instance](https://translate.sysadminsmedia.com/projects/homebox/).
|
||||
|
||||
[](http://translate.sysadminsmedia.com/engage/homebox/)
|
||||
|
||||
## Credits
|
||||
|
||||
- Original project by [@hay-kot](https://github.com/hay-kot)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
run:
|
||||
timeout: 10m
|
||||
skip-dirs:
|
||||
- internal/data/ent.*
|
||||
linters-settings:
|
||||
goconst:
|
||||
min-len: 5
|
||||
@@ -45,7 +43,7 @@ linters:
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- copyloopvar
|
||||
- gochecknoinits
|
||||
- goconst
|
||||
- gocritic
|
||||
@@ -71,4 +69,6 @@ linters:
|
||||
- sqlclosecheck
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
exclude-dirs:
|
||||
- internal/data/ent.*
|
||||
fix: true
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -9,7 +10,7 @@ import (
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
|
||||
)
|
||||
|
||||
func (a *app) SetupDemo() {
|
||||
func (a *app) SetupDemo() error {
|
||||
csvText := `HB.import_ref,HB.location,HB.labels,HB.quantity,HB.name,HB.description,HB.insured,HB.serial_number,HB.model_number,HB.manufacturer,HB.notes,HB.purchase_from,HB.purchase_price,HB.purchase_time,HB.lifetime_warranty,HB.warranty_expires,HB.warranty_details,HB.sold_to,HB.sold_price,HB.sold_time,HB.sold_notes
|
||||
,Garage,IOT;Home Assistant; Z-Wave,1,Zooz Universal Relay ZEN17,"Zooz 700 Series Z-Wave Universal Relay ZEN17 for Awnings, Garage Doors, Sprinklers, and More | 2 NO-C-NC Relays (20A, 10A) | Signal Repeater | Hub Required (Compatible with SmartThings and Hubitat)",,,ZEN17,Zooz,,Amazon,39.95,10/13/2021,,,,,,,
|
||||
,Living Room,IOT;Home Assistant; Z-Wave,1,Zooz Motion Sensor,"Zooz Z-Wave Plus S2 Motion Sensor ZSE18 with Magnetic Mount, Works with Vera and SmartThings",,,ZSE18,Zooz,,Amazon,29.95,10/15/2021,,,,,,,
|
||||
@@ -33,34 +34,34 @@ func (a *app) SetupDemo() {
|
||||
_, err := a.services.User.Login(ctx, registration.Email, registration.Password, false)
|
||||
if err == nil {
|
||||
log.Info().Msg("Demo user already exists, skipping setup")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug().Msg("Demo user does not exist, setting up demo")
|
||||
_, err = a.services.User.RegisterUser(ctx, registration)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to register demo user")
|
||||
log.Fatal().Msg("Failed to setup demo")
|
||||
return errors.New("failed to setup demo")
|
||||
}
|
||||
|
||||
token, err := a.services.User.Login(ctx, registration.Email, registration.Password, false)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to login demo user")
|
||||
log.Fatal().Msg("Failed to setup demo")
|
||||
return
|
||||
return errors.New("failed to setup demo")
|
||||
}
|
||||
self, err := a.services.User.GetSelf(ctx, token.Raw)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get self")
|
||||
log.Fatal().Msg("Failed to setup demo")
|
||||
return
|
||||
return errors.New("failed to setup demo")
|
||||
}
|
||||
|
||||
_, err = a.services.Items.CsvImport(ctx, self.GroupID, strings.NewReader(csvText))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to import CSV")
|
||||
log.Fatal().Msg("Failed to setup demo")
|
||||
return errors.New("failed to setup demo")
|
||||
}
|
||||
|
||||
log.Info().Msg("Demo setup complete")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -57,6 +57,12 @@ func WithSecureCookies(secure bool) func(*V1Controller) {
|
||||
}
|
||||
}
|
||||
|
||||
func WithURL(url string) func(*V1Controller) {
|
||||
return func(ctrl *V1Controller) {
|
||||
ctrl.url = url
|
||||
}
|
||||
}
|
||||
|
||||
type V1Controller struct {
|
||||
cookieSecure bool
|
||||
repo *repo.AllRepos
|
||||
@@ -65,6 +71,7 @@ type V1Controller struct {
|
||||
isDemo bool
|
||||
allowRegistration bool
|
||||
bus *eventbus.EventBus
|
||||
url string
|
||||
}
|
||||
|
||||
type (
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -333,7 +334,7 @@ func (ctrl *V1Controller) HandleItemsExport() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
csvData, err := ctrl.svc.Items.ExportCSV(r.Context(), ctx.GID)
|
||||
csvData, err := ctrl.svc.Items.ExportCSV(r.Context(), ctx.GID, getHBURL(r.Header.Get("Referer"), ctrl.url))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to export items")
|
||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
@@ -347,3 +348,26 @@ func (ctrl *V1Controller) HandleItemsExport() errchain.HandlerFunc {
|
||||
return writer.WriteAll(csvData)
|
||||
}
|
||||
}
|
||||
|
||||
func getHBURL(refererHeader, fallback string) (hbURL string) {
|
||||
hbURL = refererHeader
|
||||
if hbURL == "" {
|
||||
hbURL = fallback
|
||||
}
|
||||
|
||||
return stripPathFromURL(hbURL)
|
||||
}
|
||||
|
||||
// stripPathFromURL removes the path from a URL.
|
||||
// ex. https://example.com/tools -> https://example.com
|
||||
func stripPathFromURL(rawURL string) string {
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to parse URL")
|
||||
return ""
|
||||
}
|
||||
|
||||
strippedURL := url.URL{Scheme: parsedURL.Scheme, Host: parsedURL.Host}
|
||||
|
||||
return strippedURL.String()
|
||||
}
|
||||
|
||||
@@ -85,15 +85,15 @@ func (ctrl *V1Controller) HandleLocationDelete() errchain.HandlerFunc {
|
||||
return adapters.CommandID("id", fn, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (ctrl *V1Controller) GetLocationWithPrice(auth context.Context, GID uuid.UUID, ID uuid.UUID) (repo.LocationOut, error) {
|
||||
var location, err = ctrl.repo.Locations.GetOneByGroup(auth, GID, ID)
|
||||
func (ctrl *V1Controller) GetLocationWithPrice(auth context.Context, gid uuid.UUID, id uuid.UUID) (repo.LocationOut, error) {
|
||||
var location, err = ctrl.repo.Locations.GetOneByGroup(auth, gid, id)
|
||||
if err != nil {
|
||||
return repo.LocationOut{}, err
|
||||
}
|
||||
|
||||
// Add direct child items price
|
||||
totalPrice := new(big.Int)
|
||||
items, err := ctrl.repo.Items.QueryByGroup(auth, GID, repo.ItemQuery{LocationIDs: []uuid.UUID{ID}})
|
||||
items, err := ctrl.repo.Items.QueryByGroup(auth, gid, repo.ItemQuery{LocationIDs: []uuid.UUID{id}})
|
||||
if err != nil {
|
||||
return repo.LocationOut{}, err
|
||||
}
|
||||
@@ -112,7 +112,7 @@ func (ctrl *V1Controller) GetLocationWithPrice(auth context.Context, GID uuid.UU
|
||||
// Add price from child locations
|
||||
for _, childLocation := range location.Children {
|
||||
var childLocationWithPrice repo.LocationOut
|
||||
childLocationWithPrice, err = ctrl.GetLocationWithPrice(auth, GID, childLocation.ID)
|
||||
childLocationWithPrice, err = ctrl.GetLocationWithPrice(auth, gid, childLocation.ID)
|
||||
if err != nil {
|
||||
return repo.LocationOut{}, err
|
||||
}
|
||||
|
||||
@@ -13,15 +13,16 @@ import (
|
||||
// HandleMaintenanceLogGet godoc
|
||||
//
|
||||
// @Summary Get Maintenance Log
|
||||
// @Tags Maintenance
|
||||
// @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)
|
||||
@@ -30,7 +31,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 +45,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)
|
||||
}
|
||||
|
||||
65
backend/app/api/handlers/v1/v1_ctrl_maintenance.go
Normal file
65
backend/app/api/handlers/v1/v1_ctrl_maintenance.go
Normal 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)
|
||||
}
|
||||
@@ -115,16 +115,17 @@ func run(cfg *config.Config) error {
|
||||
|
||||
err = c.Schema.Create(context.Background(), options...)
|
||||
if err != nil {
|
||||
log.Fatal().
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("driver", "sqlite").
|
||||
Str("url", cfg.Storage.SqliteURL).
|
||||
Msg("failed creating schema resources")
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.RemoveAll(temp)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to remove temporary directory for database migrations")
|
||||
log.Error().Err(err).Msg("failed to remove temporary directory for database migrations")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -139,10 +140,11 @@ func run(cfg *config.Config) error {
|
||||
|
||||
content, err := os.ReadFile(cfg.Options.CurrencyConfig)
|
||||
if err != nil {
|
||||
log.Fatal().
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("path", cfg.Options.CurrencyConfig).
|
||||
Msg("failed to read currency config file")
|
||||
return err
|
||||
}
|
||||
|
||||
collectFuncs = append(collectFuncs, currencies.CollectJSON(bytes.NewReader(content)))
|
||||
@@ -150,9 +152,10 @@ func run(cfg *config.Config) error {
|
||||
|
||||
currencies, err := currencies.CollectionCurrencies(collectFuncs...)
|
||||
if err != nil {
|
||||
log.Fatal().
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("failed to collect currencies")
|
||||
return err
|
||||
}
|
||||
|
||||
app.bus = eventbus.New()
|
||||
@@ -211,7 +214,10 @@ func run(cfg *config.Config) error {
|
||||
// TODO: Remove through external API that does setup
|
||||
if cfg.Demo {
|
||||
log.Info().Msg("Running in demo mode, creating demo data")
|
||||
app.SetupDemo()
|
||||
err := app.SetupDemo()
|
||||
if err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
@@ -54,6 +55,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
||||
v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize),
|
||||
v1.WithRegistration(a.conf.Options.AllowRegistration),
|
||||
v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode
|
||||
v1.WithURL(fmt.Sprintf("%s:%s", a.conf.Web.Host, a.conf.Web.Port)),
|
||||
)
|
||||
|
||||
r.Route(prefix+"/v1", func(r chi.Router) {
|
||||
@@ -132,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...))
|
||||
|
||||
@@ -917,14 +917,34 @@ const docTemplate = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Maintenance"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -939,7 +959,7 @@ const docTemplate = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Maintenance"
|
||||
"Item Maintenance"
|
||||
],
|
||||
"summary": "Create Maintenance Entry",
|
||||
"parameters": [
|
||||
@@ -963,60 +983,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 +1375,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 +2540,9 @@ const docTemplate = `{
|
||||
"parent": {
|
||||
"$ref": "#/definitions/repo.LocationSummary"
|
||||
},
|
||||
"totalPrice": {
|
||||
"type": "number"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -2611,26 +2678,49 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.MaintenanceLog": {
|
||||
"repo.MaintenanceEntryWithDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"costAverage": {
|
||||
"type": "number"
|
||||
"completedDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"costTotal": {
|
||||
"type": "number"
|
||||
"cost": {
|
||||
"type": "string",
|
||||
"example": "0"
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.MaintenanceEntry"
|
||||
}
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"itemId": {
|
||||
"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.NotifierCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -2672,6 +2762,10 @@ const docTemplate = `{
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "URL field is not exposed to the client",
|
||||
"type": "string"
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -910,14 +910,34 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Maintenance"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -932,7 +952,7 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Maintenance"
|
||||
"Item Maintenance"
|
||||
],
|
||||
"summary": "Create Maintenance Entry",
|
||||
"parameters": [
|
||||
@@ -956,60 +976,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 +1368,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": [
|
||||
@@ -2145,9 +2209,6 @@
|
||||
"purchaseFrom": {
|
||||
"type": "string"
|
||||
},
|
||||
"purchaseFrom": {
|
||||
"type": "string"
|
||||
},
|
||||
"purchasePrice": {
|
||||
"type": "string",
|
||||
"example": "0"
|
||||
@@ -2610,26 +2671,49 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.MaintenanceLog": {
|
||||
"repo.MaintenanceEntryWithDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"costAverage": {
|
||||
"type": "number"
|
||||
"completedDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"costTotal": {
|
||||
"type": "number"
|
||||
"cost": {
|
||||
"type": "string",
|
||||
"example": "0"
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.MaintenanceEntry"
|
||||
}
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"itemId": {
|
||||
"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.NotifierCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -2671,6 +2755,10 @@
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "URL field is not exposed to the client",
|
||||
"type": "string"
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -2713,9 +2801,6 @@
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"totalPrice": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2998,4 +3083,4 @@
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,8 +170,6 @@ definitions:
|
||||
x-omitempty: true
|
||||
purchaseFrom:
|
||||
type: string
|
||||
purchaseMethod:
|
||||
type: string
|
||||
purchasePrice:
|
||||
example: "0"
|
||||
type: string
|
||||
@@ -392,6 +390,8 @@ definitions:
|
||||
type: string
|
||||
parent:
|
||||
$ref: '#/definitions/repo.LocationSummary'
|
||||
totalPrice:
|
||||
type: number
|
||||
updatedAt:
|
||||
type: string
|
||||
type: object
|
||||
@@ -481,19 +481,36 @@ definitions:
|
||||
scheduledDate:
|
||||
type: string
|
||||
type: object
|
||||
repo.MaintenanceLog:
|
||||
repo.MaintenanceEntryWithDetails:
|
||||
properties:
|
||||
costAverage:
|
||||
type: number
|
||||
costTotal:
|
||||
type: number
|
||||
entries:
|
||||
items:
|
||||
$ref: '#/definitions/repo.MaintenanceEntry'
|
||||
type: array
|
||||
itemId:
|
||||
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.NotifierCreate:
|
||||
properties:
|
||||
isActive:
|
||||
@@ -522,6 +539,9 @@ definitions:
|
||||
type: string
|
||||
updatedAt:
|
||||
type: string
|
||||
url:
|
||||
description: URL field is not exposed to the client
|
||||
type: string
|
||||
userId:
|
||||
type: string
|
||||
type: object
|
||||
@@ -1219,18 +1239,32 @@ 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
|
||||
tags:
|
||||
- Maintenance
|
||||
- Item Maintenance
|
||||
post:
|
||||
parameters:
|
||||
- description: Entry Data
|
||||
@@ -1250,39 +1284,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:
|
||||
@@ -1583,6 +1585,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:
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
module github.com/sysadminsmedia/homebox/backend
|
||||
|
||||
go 1.22
|
||||
|
||||
toolchain go1.22.0
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.19.1
|
||||
entgo.io/ent v0.12.5
|
||||
github.com/ardanlabs/conf/v3 v3.1.7
|
||||
entgo.io/ent v0.14.1
|
||||
github.com/ardanlabs/conf/v3 v3.1.8
|
||||
github.com/containrrr/shoutrrr v0.8.0
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/go-playground/validator/v10 v10.18.0
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
github.com/go-playground/validator/v10 v10.22.1
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/schema v1.4.1
|
||||
github.com/hay-kot/httpkit v0.0.9
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/olahol/melody v1.1.4
|
||||
github.com/hay-kot/httpkit v0.0.11
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/olahol/melody v1.2.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.32.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/swaggo/http-swagger/v2 v2.0.2
|
||||
github.com/swaggo/swag v1.16.3
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.2
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.2.2
|
||||
golang.org/x/crypto v0.23.0
|
||||
modernc.org/sqlite v1.29.2
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.4
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.2.4
|
||||
golang.org/x/crypto v0.28.0
|
||||
modernc.org/sqlite v1.33.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -63,16 +61,16 @@ require (
|
||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||
github.com/zclconf/go-cty v1.14.1 // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
modernc.org/libc v1.41.0 // indirect
|
||||
modernc.org/libc v1.55.3 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
||||
|
||||
111
backend/go.sum
111
backend/go.sum
@@ -1,7 +1,11 @@
|
||||
ariga.io/atlas v0.19.1 h1:QzBHkakwzEhmPWOzNhw8Yr/Bbicj6Iq5hwEoNI/Jr9A=
|
||||
ariga.io/atlas v0.19.1/go.mod h1:VPlcXdd4w2KqKnH54yEZcry79UAhpaWaxEsmn5JRNoE=
|
||||
ariga.io/atlas v0.28.0 h1:qmn9tUyJypJkIw+X3ECUwDtkMTiFupgstHbjRN4xGH0=
|
||||
ariga.io/atlas v0.28.0/go.mod h1:LOOp18LCL9r+VifvVlJqgYJwYl271rrXD9/wIyzJ8sw=
|
||||
entgo.io/ent v0.12.5 h1:KREM5E4CSoej4zeGa88Ou/gfturAnpUv0mzAjch1sj4=
|
||||
entgo.io/ent v0.12.5/go.mod h1:Y3JVAjtlIk8xVZYSn3t3mf8xlZIn5SAOXZQxD6kKI+Q=
|
||||
entgo.io/ent v0.14.1 h1:fUERL506Pqr92EPHJqr8EYxbPioflJo6PudkrEA8a/s=
|
||||
entgo.io/ent v0.14.1/go.mod h1:MH6XLG0KXpkcDQhKiHfANZSzR55TJyPL5IGNpI8wpco=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
@@ -10,8 +14,8 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||
github.com/ardanlabs/conf/v3 v3.1.7 h1:p232cF68TafoA5U9ZlbxUIhGJtGNdKHBXF80Fdqb5t0=
|
||||
github.com/ardanlabs/conf/v3 v3.1.7/go.mod h1:zclexWKe0NVj6LHQ8NgDDZ7bQ1spE0KeKPFficdtAjU=
|
||||
github.com/ardanlabs/conf/v3 v3.1.8 h1:r0KUV9/Hni5XdeWR2+A1BiedIDnry5CjezoqgJ0rnFQ=
|
||||
github.com/ardanlabs/conf/v3 v3.1.8/go.mod h1:OIi6NK95fj8jKFPdZ/UmcPlY37JBg99hdP9o5XmNK9c=
|
||||
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
|
||||
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
@@ -27,8 +31,8 @@ github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
@@ -54,24 +58,25 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
|
||||
github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
|
||||
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a h1:RYfmiM0zluBJOiPDJseKLEN4BapJ42uSi9SZBQ2YyiA=
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||
@@ -82,8 +87,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI=
|
||||
github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
||||
github.com/hay-kot/httpkit v0.0.9 h1:hu2TPY9awmIYWXxWGubaXl2U61pPvaVsm9YwboBRGu0=
|
||||
github.com/hay-kot/httpkit v0.0.9/go.mod h1:AD22YluZrvBDxmtB3Pw2SOyp3A2PZqcmBZa0+COrhoU=
|
||||
github.com/hay-kot/httpkit v0.0.11 h1:ZdB2uqsFBSDpfUoClGK5c5orjBjQkEVSXh7fZX5FKEk=
|
||||
github.com/hay-kot/httpkit v0.0.11/go.mod h1:0kZdk5/swzdfqfg2c6pBWimcgeJ9PTyO97EbHnYl2Sw=
|
||||
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
||||
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
@@ -111,15 +116,21 @@ 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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
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/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/olahol/melody v1.1.4 h1:RQHfKZkQmDxI0+SLZRNBCn4LiXdqxLKRGSkT8Dyoe/E=
|
||||
github.com/olahol/melody v1.1.4/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
|
||||
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=
|
||||
@@ -133,10 +144,14 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
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=
|
||||
@@ -145,41 +160,55 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
||||
github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg=
|
||||
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
|
||||
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
||||
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.2 h1:0comk6jEwi0oWNhKEmzx4JI+Q7XIneAApmFSMKWmSVc=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.2/go.mod h1:2Qsk2APUCPne0TsRo40DIkI5MYnbzYKCnKGEFWrxd24=
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.2.2 h1:gyzunKXgC0ZUpKqQFUImbAEwewAiwNCkxFEKZV80Kt4=
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.2.2/go.mod h1:bbVRiBJSRPj4UBZP/biLG7JSd9kHqXjErk1eakAMnRA=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.4 h1:cXdYlrhzHzVAnJHiwr/T6lAUmS9MtEStjEZBjArrvnc=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.4/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.2.4 h1:41e/aLr1AMVWlug6oUMkDg2r0+dv5ofB7UaTkekKZBc=
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.2.4/go.mod h1:H8nLSGYUWBpNyBPjDcJzAanMzYBBYMFtrU2lwoSRn+k=
|
||||
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
||||
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||
github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA=
|
||||
github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
|
||||
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -194,16 +223,20 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
|
||||
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
|
||||
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
||||
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/sqlite v1.29.2 h1:xgBSyA3gemwgP31PWFfFjtBorQNYpeypGdoSDjXhrgI=
|
||||
modernc.org/sqlite v1.29.2/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||
modernc.org/sqlite v1.33.0 h1:WWkA/T2G17okiLGgKAj4/RMIvgyMT19yQ038160IeYk=
|
||||
modernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8=
|
||||
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
|
||||
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,7 @@ func bootstrap() {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
func MainNoExit(m *testing.M) int {
|
||||
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
|
||||
if err != nil {
|
||||
log.Fatalf("failed opening connection to sqlite: %v", err)
|
||||
@@ -77,5 +77,9 @@ func TestMain(m *testing.M) {
|
||||
UID: tUser.ID,
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
return m.Run()
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(MainNoExit(m))
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ type ExportCSVRow struct {
|
||||
LabelStr LabelString `csv:"HB.labels"`
|
||||
AssetID repo.AssetID `csv:"HB.asset_id"`
|
||||
Archived bool `csv:"HB.archived"`
|
||||
URL string `csv:"HB.url"`
|
||||
|
||||
Name string `csv:"HB.name"`
|
||||
Quantity int `csv:"HB.quantity"`
|
||||
|
||||
@@ -153,7 +153,7 @@ func (s *IOSheet) Read(data io.Reader) error {
|
||||
}
|
||||
|
||||
// ReadItems writes the sheet to a writer.
|
||||
func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.UUID, repos *repo.AllRepos) error {
|
||||
func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, gid uuid.UUID, repos *repo.AllRepos, hbURL string) error {
|
||||
s.Rows = make([]ExportCSVRow, len(items))
|
||||
|
||||
extraHeaders := map[string]struct{}{}
|
||||
@@ -164,7 +164,7 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.
|
||||
// TODO: Support fetching nested locations
|
||||
locID := item.Location.ID
|
||||
|
||||
locPaths, err := repos.Locations.PathForLoc(context.Background(), GID, locID)
|
||||
locPaths, err := repos.Locations.PathForLoc(context.Background(), gid, locID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("could not get location path")
|
||||
return err
|
||||
@@ -178,6 +178,8 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.
|
||||
labelString[i] = l.Name
|
||||
}
|
||||
|
||||
url := generateItemURL(item, hbURL)
|
||||
|
||||
customFields := make([]ExportItemFields, len(item.Fields))
|
||||
|
||||
for i, f := range item.Fields {
|
||||
@@ -201,11 +203,11 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.
|
||||
Description: item.Description,
|
||||
Insured: item.Insured,
|
||||
Archived: item.Archived,
|
||||
URL: url,
|
||||
|
||||
PurchasePrice: item.PurchasePrice,
|
||||
PurchaseMethod: item.PurchaseMethod,
|
||||
PurchaseFrom: item.PurchaseFrom,
|
||||
PurchaseTime: item.PurchaseTime,
|
||||
PurchasePrice: item.PurchasePrice,
|
||||
PurchaseFrom: item.PurchaseFrom,
|
||||
PurchaseTime: item.PurchaseTime,
|
||||
|
||||
Manufacturer: item.Manufacturer,
|
||||
ModelNumber: item.ModelNumber,
|
||||
@@ -220,6 +222,7 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.
|
||||
SoldPrice: item.SoldPrice,
|
||||
SoldNotes: item.SoldNotes,
|
||||
|
||||
Notes: item.Notes,
|
||||
Fields: customFields,
|
||||
}
|
||||
}
|
||||
@@ -253,6 +256,14 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateItemURL(item repo.ItemOut, d string) string {
|
||||
url := ""
|
||||
if item.ID != uuid.Nil {
|
||||
url = fmt.Sprintf("%s/item/%s", d, item.ID.String())
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// CSV writes the current sheet to a 2d array, for compatibility with TSV/CSV files.
|
||||
func (s *IOSheet) CSV() ([][]string, error) {
|
||||
memcsv := make([][]string, len(s.Rows)+1)
|
||||
|
||||
@@ -38,13 +38,13 @@ func (svc *ItemService) Create(ctx Context, item repo.ItemCreate) (repo.ItemOut,
|
||||
return svc.repo.Items.Create(ctx, ctx.GID, item)
|
||||
}
|
||||
|
||||
func (svc *ItemService) EnsureAssetID(ctx context.Context, GID uuid.UUID) (int, error) {
|
||||
items, err := svc.repo.Items.GetAllZeroAssetID(ctx, GID)
|
||||
func (svc *ItemService) EnsureAssetID(ctx context.Context, gid uuid.UUID) (int, error) {
|
||||
items, err := svc.repo.Items.GetAllZeroAssetID(ctx, gid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
highest, err := svc.repo.Items.GetHighestAssetID(ctx, GID)
|
||||
highest, err := svc.repo.Items.GetHighestAssetID(ctx, gid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (svc *ItemService) EnsureAssetID(ctx context.Context, GID uuid.UUID) (int,
|
||||
for _, item := range items {
|
||||
highest++
|
||||
|
||||
err = svc.repo.Items.SetAssetID(ctx, GID, item.ID, highest)
|
||||
err = svc.repo.Items.SetAssetID(ctx, gid, item.ID, highest)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -64,8 +64,8 @@ func (svc *ItemService) EnsureAssetID(ctx context.Context, GID uuid.UUID) (int,
|
||||
return finished, nil
|
||||
}
|
||||
|
||||
func (svc *ItemService) EnsureImportRef(ctx context.Context, GID uuid.UUID) (int, error) {
|
||||
ids, err := svc.repo.Items.GetAllZeroImportRef(ctx, GID)
|
||||
func (svc *ItemService) EnsureImportRef(ctx context.Context, gid uuid.UUID) (int, error) {
|
||||
ids, err := svc.repo.Items.GetAllZeroImportRef(ctx, gid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func (svc *ItemService) EnsureImportRef(ctx context.Context, GID uuid.UUID) (int
|
||||
for _, itemID := range ids {
|
||||
ref := uuid.New().String()[0:8]
|
||||
|
||||
err = svc.repo.Items.Patch(ctx, GID, itemID, repo.ItemPatch{ImportRef: &ref})
|
||||
err = svc.repo.Items.Patch(ctx, gid, itemID, repo.ItemPatch{ImportRef: &ref})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func serializeLocation[T ~[]string](location T) string {
|
||||
// 1. If the item does not exist, it is created.
|
||||
// 2. If the item has a ImportRef and it exists it is skipped
|
||||
// 3. Locations and Labels are created if they do not exist.
|
||||
func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Reader) (int, error) {
|
||||
func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data io.Reader) (int, error) {
|
||||
sheet := reporting.IOSheet{}
|
||||
|
||||
err := sheet.Read(data)
|
||||
@@ -109,7 +109,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Re
|
||||
|
||||
labelMap := make(map[string]uuid.UUID)
|
||||
{
|
||||
labels, err := svc.repo.Labels.GetAll(ctx, GID)
|
||||
labels, err := svc.repo.Labels.GetAll(ctx, gid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -124,7 +124,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Re
|
||||
|
||||
locationMap := make(map[string]uuid.UUID)
|
||||
{
|
||||
locations, err := svc.repo.Locations.Tree(ctx, GID, repo.TreeQuery{WithItems: false})
|
||||
locations, err := svc.repo.Locations.Tree(ctx, gid, repo.TreeQuery{WithItems: false})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -153,7 +153,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Re
|
||||
// Asset ID Pre-Check
|
||||
highestAID := repo.AssetID(-1)
|
||||
if svc.autoIncrementAssetID {
|
||||
highestAID, err = svc.repo.Items.GetHighestAssetID(ctx, GID)
|
||||
highestAID, err = svc.repo.Items.GetHighestAssetID(ctx, gid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -169,7 +169,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Re
|
||||
// ========================================
|
||||
// Preflight check for existing item
|
||||
if row.ImportRef != "" {
|
||||
exists, err := svc.repo.Items.CheckRef(ctx, GID, row.ImportRef)
|
||||
exists, err := svc.repo.Items.CheckRef(ctx, gid, row.ImportRef)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error checking for existing item with ref %q: %w", row.ImportRef, err)
|
||||
}
|
||||
@@ -188,7 +188,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Re
|
||||
|
||||
id, ok := labelMap[label]
|
||||
if !ok {
|
||||
newLabel, err := svc.repo.Labels.Create(ctx, GID, repo.LabelCreate{Name: label})
|
||||
newLabel, err := svc.repo.Labels.Create(ctx, gid, repo.LabelCreate{Name: label})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -220,7 +220,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Re
|
||||
parentID = locationMap[parentPath]
|
||||
}
|
||||
|
||||
newLocation, err := svc.repo.Locations.Create(ctx, GID, repo.LocationCreate{
|
||||
newLocation, err := svc.repo.Locations.Create(ctx, gid, repo.LocationCreate{
|
||||
ParentID: parentID,
|
||||
Name: pathElement,
|
||||
})
|
||||
@@ -261,12 +261,12 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Re
|
||||
LabelIDs: labelIds,
|
||||
}
|
||||
|
||||
item, err = svc.repo.Items.Create(ctx, GID, newItem)
|
||||
item, err = svc.repo.Items.Create(ctx, gid, newItem)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
default:
|
||||
item, err = svc.repo.Items.GetByRef(ctx, GID, row.ImportRef)
|
||||
item, err = svc.repo.Items.GetByRef(ctx, gid, row.ImportRef)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -298,7 +298,6 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Re
|
||||
Archived: row.Archived,
|
||||
|
||||
PurchasePrice: row.PurchasePrice,
|
||||
PurchaseFrom: row.PurchaseMethod,
|
||||
PurchaseFrom: row.PurchaseFrom,
|
||||
PurchaseTime: row.PurchaseTime,
|
||||
|
||||
@@ -319,7 +318,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Re
|
||||
Fields: fields,
|
||||
}
|
||||
|
||||
item, err = svc.repo.Items.UpdateByGroup(ctx, GID, updateItem)
|
||||
item, err = svc.repo.Items.UpdateByGroup(ctx, gid, updateItem)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -330,15 +329,15 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Re
|
||||
return finished, nil
|
||||
}
|
||||
|
||||
func (svc *ItemService) ExportCSV(ctx context.Context, GID uuid.UUID) ([][]string, error) {
|
||||
items, err := svc.repo.Items.GetAll(ctx, GID)
|
||||
func (svc *ItemService) ExportCSV(ctx context.Context, gid uuid.UUID, hbURL string) ([][]string, error) {
|
||||
items, err := svc.repo.Items.GetAll(ctx, gid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sheet := reporting.IOSheet{}
|
||||
|
||||
err = sheet.ReadItems(ctx, items, GID, svc.repo)
|
||||
err = sheet.ReadItems(ctx, items, gid, svc.repo, hbURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -346,8 +345,8 @@ func (svc *ItemService) ExportCSV(ctx context.Context, GID uuid.UUID) ([][]strin
|
||||
return sheet.CSV()
|
||||
}
|
||||
|
||||
func (svc *ItemService) ExportBillOfMaterialsCSV(ctx context.Context, GID uuid.UUID) ([]byte, error) {
|
||||
items, err := svc.repo.Items.GetAll(ctx, GID)
|
||||
func (svc *ItemService) ExportBillOfMaterialsCSV(ctx context.Context, gid uuid.UUID) ([]byte, error) {
|
||||
items, err := svc.repo.Items.GetAll(ctx, gid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -132,13 +132,13 @@ func (svc *UserService) GetSelf(ctx context.Context, requestToken string) (repo.
|
||||
return svc.repos.AuthTokens.GetUserFromToken(ctx, hash)
|
||||
}
|
||||
|
||||
func (svc *UserService) UpdateSelf(ctx context.Context, ID uuid.UUID, data repo.UserUpdate) (repo.UserOut, error) {
|
||||
err := svc.repos.Users.Update(ctx, ID, data)
|
||||
func (svc *UserService) UpdateSelf(ctx context.Context, id uuid.UUID, data repo.UserUpdate) (repo.UserOut, error) {
|
||||
err := svc.repos.Users.Update(ctx, id, data)
|
||||
if err != nil {
|
||||
return repo.UserOut{}, err
|
||||
}
|
||||
|
||||
return svc.repos.Users.GetOneID(ctx, ID)
|
||||
return svc.repos.Users.GetOneID(ctx, id)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -217,8 +217,8 @@ func (svc *UserService) RenewToken(ctx context.Context, token string) (UserAuthT
|
||||
// DeleteSelf deletes the user that is currently logged based of the provided UUID
|
||||
// There is _NO_ protection against deleting the wrong user, as such this should only
|
||||
// be used when the identify of the user has been confirmed.
|
||||
func (svc *UserService) DeleteSelf(ctx context.Context, ID uuid.UUID) error {
|
||||
return svc.repos.Users.Delete(ctx, ID)
|
||||
func (svc *UserService) DeleteSelf(ctx context.Context, id uuid.UUID) error {
|
||||
return svc.repos.Users.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (svc *UserService) ChangePassword(ctx Context, current string, new string) (ok bool) {
|
||||
|
||||
@@ -76,8 +76,6 @@ func (Item) Fields() []ent.Field {
|
||||
|
||||
// ------------------------------------
|
||||
// item purchase
|
||||
field.String("purchase_method").
|
||||
Optional(),
|
||||
field.Time("purchase_time").
|
||||
Optional(),
|
||||
field.String("purchase_from").
|
||||
|
||||
@@ -17,7 +17,7 @@ CREATE INDEX `documenttoken_token` ON `document_tokens` (`token`);
|
||||
-- create "groups" table
|
||||
CREATE TABLE `groups` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `currency` text NOT NULL DEFAULT 'usd', PRIMARY KEY (`id`));
|
||||
-- create "items" table
|
||||
CREATE TABLE `items` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `description` text NULL, `import_ref` text NULL, `notes` text NULL, `quantity` integer NOT NULL DEFAULT 1, `insured` bool NOT NULL DEFAULT false, `serial_number` text NULL, `model_number` text NULL, `manufacturer` text NULL, `lifetime_warranty` bool NOT NULL DEFAULT false, `warranty_expires` datetime NULL, `warranty_details` text NULL, `purchase_method` text NULL, `purchase_time` datetime NULL, `purchase_from` text NULL, `purchase_price` real NOT NULL DEFAULT 0, `sold_time` datetime NULL, `sold_to` text NULL, `sold_price` real NOT NULL DEFAULT 0, `sold_notes` text NULL, `group_items` uuid NOT NULL, `location_items` uuid NULL, PRIMARY KEY (`id`), CONSTRAINT `items_groups_items` FOREIGN KEY (`group_items`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `items_locations_items` FOREIGN KEY (`location_items`) REFERENCES `locations` (`id`) ON DELETE CASCADE);
|
||||
CREATE TABLE `items` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `description` text NULL, `import_ref` text NULL, `notes` text NULL, `quantity` integer NOT NULL DEFAULT 1, `insured` bool NOT NULL DEFAULT false, `serial_number` text NULL, `model_number` text NULL, `manufacturer` text NULL, `lifetime_warranty` bool NOT NULL DEFAULT false, `warranty_expires` datetime NULL, `warranty_details` text NULL, `purchase_time` datetime NULL, `purchase_from` text NULL, `purchase_price` real NOT NULL DEFAULT 0, `sold_time` datetime NULL, `sold_to` text NULL, `sold_price` real NOT NULL DEFAULT 0, `sold_notes` text NULL, `group_items` uuid NOT NULL, `location_items` uuid NULL, PRIMARY KEY (`id`), CONSTRAINT `items_groups_items` FOREIGN KEY (`group_items`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `items_locations_items` FOREIGN KEY (`location_items`) REFERENCES `locations` (`id`) ON DELETE CASCADE);
|
||||
-- create index "item_name" to table: "items"
|
||||
CREATE INDEX `item_name` ON `items` (`name`);
|
||||
-- create index "item_manufacturer" to table: "items"
|
||||
|
||||
@@ -16,9 +16,9 @@ func (aid AssetID) Int() int {
|
||||
return int(aid)
|
||||
}
|
||||
|
||||
func ParseAssetIDBytes(d []byte) (AID AssetID, ok bool) {
|
||||
d = bytes.Replace(d, []byte(`"`), []byte(``), -1)
|
||||
d = bytes.Replace(d, []byte(`-`), []byte(``), -1)
|
||||
func ParseAssetIDBytes(d []byte) (aid AssetID, ok bool) {
|
||||
d = bytes.ReplaceAll(d, []byte(`"`), []byte(``))
|
||||
d = bytes.ReplaceAll(d, []byte(`-`), []byte(``))
|
||||
|
||||
aidInt, err := strconv.Atoi(string(d))
|
||||
if err != nil {
|
||||
@@ -28,7 +28,7 @@ func ParseAssetIDBytes(d []byte) (AID AssetID, ok bool) {
|
||||
return AssetID(aidInt), true
|
||||
}
|
||||
|
||||
func ParseAssetID(s string) (AID AssetID, ok bool) {
|
||||
func ParseAssetID(s string) (aid AssetID, ok bool) {
|
||||
return ParseAssetIDBytes([]byte(s))
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ func (aid *AssetID) UnmarshalJSON(d []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
d = bytes.Replace(d, []byte(`"`), []byte(``), -1)
|
||||
d = bytes.Replace(d, []byte(`-`), []byte(``), -1)
|
||||
d = bytes.ReplaceAll(d, []byte(`"`), []byte(``))
|
||||
d = bytes.ReplaceAll(d, []byte(`-`), []byte(``))
|
||||
|
||||
aidInt, err := strconv.Atoi(string(d))
|
||||
if err != nil {
|
||||
|
||||
@@ -39,7 +39,7 @@ func bootstrap() {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
func MainNoExit(m *testing.M) int {
|
||||
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
|
||||
if err != nil {
|
||||
log.Fatalf("failed opening connection to sqlite: %v", err)
|
||||
@@ -59,6 +59,9 @@ func TestMain(m *testing.M) {
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
bootstrap()
|
||||
|
||||
os.Exit(m.Run())
|
||||
return m.Run()
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(MainNoExit(m))
|
||||
}
|
||||
|
||||
@@ -109,12 +109,12 @@ func (r *GroupRepository) GetAllGroups(ctx context.Context) ([]Group, error) {
|
||||
return r.groupMapper.MapEachErr(r.db.Group.Query().All(ctx))
|
||||
}
|
||||
|
||||
func (r *GroupRepository) StatsLocationsByPurchasePrice(ctx context.Context, GID uuid.UUID) ([]TotalsByOrganizer, error) {
|
||||
func (r *GroupRepository) StatsLocationsByPurchasePrice(ctx context.Context, gid uuid.UUID) ([]TotalsByOrganizer, error) {
|
||||
var v []TotalsByOrganizer
|
||||
|
||||
err := r.db.Location.Query().
|
||||
Where(
|
||||
location.HasGroupWith(group.ID(GID)),
|
||||
location.HasGroupWith(group.ID(gid)),
|
||||
).
|
||||
GroupBy(location.FieldID, location.FieldName).
|
||||
Aggregate(func(sq *sql.Selector) string {
|
||||
@@ -131,12 +131,12 @@ func (r *GroupRepository) StatsLocationsByPurchasePrice(ctx context.Context, GID
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (r *GroupRepository) StatsLabelsByPurchasePrice(ctx context.Context, GID uuid.UUID) ([]TotalsByOrganizer, error) {
|
||||
func (r *GroupRepository) StatsLabelsByPurchasePrice(ctx context.Context, gid uuid.UUID) ([]TotalsByOrganizer, error) {
|
||||
var v []TotalsByOrganizer
|
||||
|
||||
err := r.db.Label.Query().
|
||||
Where(
|
||||
label.HasGroupWith(group.ID(GID)),
|
||||
label.HasGroupWith(group.ID(gid)),
|
||||
).
|
||||
GroupBy(label.FieldID, label.FieldName).
|
||||
Aggregate(func(sq *sql.Selector) string {
|
||||
@@ -157,7 +157,7 @@ func (r *GroupRepository) StatsLabelsByPurchasePrice(ctx context.Context, GID uu
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, GID uuid.UUID, start, end time.Time) (*ValueOverTime, error) {
|
||||
func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID, start, end time.Time) (*ValueOverTime, error) {
|
||||
// Get the Totals for the Start and End of the Given Time Period
|
||||
q := `
|
||||
SELECT
|
||||
@@ -180,7 +180,7 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, GID uuid.UUID,
|
||||
var maybeStart *float64
|
||||
var maybeEnd *float64
|
||||
|
||||
row := r.db.Sql().QueryRowContext(ctx, q, GID, sqliteDateFormat(start), GID, sqliteDateFormat(end))
|
||||
row := r.db.Sql().QueryRowContext(ctx, q, gid, sqliteDateFormat(start), gid, sqliteDateFormat(end))
|
||||
err := row.Scan(&maybeStart, &maybeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -198,7 +198,7 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, GID uuid.UUID,
|
||||
// Get Created Date and Price of all items between start and end
|
||||
err = r.db.Item.Query().
|
||||
Where(
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
item.HasGroupWith(group.ID(gid)),
|
||||
item.CreatedAtGTE(start),
|
||||
item.CreatedAtLTE(end),
|
||||
item.Archived(false),
|
||||
@@ -226,7 +226,7 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, GID uuid.UUID,
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
func (r *GroupRepository) StatsGroup(ctx context.Context, GID uuid.UUID) (GroupStatistics, error) {
|
||||
func (r *GroupRepository) StatsGroup(ctx context.Context, gid uuid.UUID) (GroupStatistics, error) {
|
||||
q := `
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM users WHERE group_users = ?) AS total_users,
|
||||
@@ -242,7 +242,7 @@ func (r *GroupRepository) StatsGroup(ctx context.Context, GID uuid.UUID) (GroupS
|
||||
) AS total_with_warranty
|
||||
`
|
||||
var stats GroupStatistics
|
||||
row := r.db.Sql().QueryRowContext(ctx, q, GID, GID, GID, GID, GID, GID)
|
||||
row := r.db.Sql().QueryRowContext(ctx, q, gid, gid, gid, gid, gid, gid)
|
||||
|
||||
var maybeTotalItemPrice *float64
|
||||
var maybeTotalWithWarranty *int
|
||||
@@ -264,8 +264,8 @@ func (r *GroupRepository) GroupCreate(ctx context.Context, name string) (Group,
|
||||
Save(ctx))
|
||||
}
|
||||
|
||||
func (r *GroupRepository) GroupUpdate(ctx context.Context, ID uuid.UUID, data GroupUpdate) (Group, error) {
|
||||
entity, err := r.db.Group.UpdateOneID(ID).
|
||||
func (r *GroupRepository) GroupUpdate(ctx context.Context, id uuid.UUID, data GroupUpdate) (Group, error) {
|
||||
entity, err := r.db.Group.UpdateOneID(id).
|
||||
SetName(data.Name).
|
||||
SetCurrency(strings.ToLower(data.Currency)).
|
||||
Save(ctx)
|
||||
|
||||
@@ -70,8 +70,8 @@ type (
|
||||
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable,x-omitempty"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
AssetID AssetID `json:"assetId" swaggertype:"string"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Description string `json:"description" validate:"max=1000"`
|
||||
Quantity int `json:"quantity"`
|
||||
Insured bool `json:"insured"`
|
||||
Archived bool `json:"archived"`
|
||||
@@ -91,14 +91,13 @@ type (
|
||||
WarrantyDetails string `json:"warrantyDetails"`
|
||||
|
||||
// Purchase
|
||||
PurchaseMethod string `json:"purchaseMethod"`
|
||||
PurchaseTime types.Date `json:"purchaseTime"`
|
||||
PurchaseFrom string `json:"purchaseFrom"`
|
||||
PurchasePrice float64 `json:"purchasePrice,string"`
|
||||
PurchaseTime types.Date `json:"purchaseTime"`
|
||||
PurchaseFrom string `json:"purchaseFrom" validate:"max=255"`
|
||||
PurchasePrice float64 `json:"purchasePrice,string"`
|
||||
|
||||
// Sold
|
||||
SoldTime types.Date `json:"soldTime"`
|
||||
SoldTo string `json:"soldTo"`
|
||||
SoldTo string `json:"soldTo" validate:"max=255"`
|
||||
SoldPrice float64 `json:"soldPrice,string"`
|
||||
SoldNotes string `json:"soldNotes"`
|
||||
|
||||
@@ -148,9 +147,8 @@ type (
|
||||
WarrantyDetails string `json:"warrantyDetails"`
|
||||
|
||||
// Purchase
|
||||
PurchaseMethod string `json:"purchaseMethod"`
|
||||
PurchaseTime types.Date `json:"purchaseTime"`
|
||||
PurchaseFrom string `json:"purchaseFrom"`
|
||||
PurchaseTime types.Date `json:"purchaseTime"`
|
||||
PurchaseFrom string `json:"purchaseFrom"`
|
||||
|
||||
// Sold
|
||||
SoldTime types.Date `json:"soldTime"`
|
||||
@@ -263,8 +261,8 @@ func mapItemOut(item *ent.Item) ItemOut {
|
||||
Manufacturer: item.Manufacturer,
|
||||
|
||||
// Purchase
|
||||
PurchaseTime: types.DateFromTime(item.PurchaseTime),
|
||||
PurchaseFrom: item.PurchaseFrom,
|
||||
PurchaseTime: types.DateFromTime(item.PurchaseTime),
|
||||
PurchaseFrom: item.PurchaseFrom,
|
||||
|
||||
// Sold
|
||||
SoldTime: types.DateFromTime(item.SoldTime),
|
||||
@@ -279,9 +277,9 @@ func mapItemOut(item *ent.Item) ItemOut {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) publishMutationEvent(GID uuid.UUID) {
|
||||
func (e *ItemsRepository) publishMutationEvent(gid uuid.UUID) {
|
||||
if e.bus != nil {
|
||||
e.bus.Publish(eventbus.EventItemMutation, eventbus.GroupMutationEvent{GID: GID})
|
||||
e.bus.Publish(eventbus.EventItemMutation, eventbus.GroupMutationEvent{GID: gid})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,13 +305,13 @@ func (e *ItemsRepository) GetOne(ctx context.Context, id uuid.UUID) (ItemOut, er
|
||||
return e.getOne(ctx, item.ID(id))
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) CheckRef(ctx context.Context, GID uuid.UUID, ref string) (bool, error) {
|
||||
q := e.db.Item.Query().Where(item.HasGroupWith(group.ID(GID)))
|
||||
func (e *ItemsRepository) CheckRef(ctx context.Context, gid uuid.UUID, ref string) (bool, error) {
|
||||
q := e.db.Item.Query().Where(item.HasGroupWith(group.ID(gid)))
|
||||
return q.Where(item.ImportRef(ref)).Exist(ctx)
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) GetByRef(ctx context.Context, GID uuid.UUID, ref string) (ItemOut, error) {
|
||||
return e.getOne(ctx, item.ImportRef(ref), item.HasGroupWith(group.ID(GID)))
|
||||
func (e *ItemsRepository) GetByRef(ctx context.Context, gid uuid.UUID, ref string) (ItemOut, error) {
|
||||
return e.getOne(ctx, item.ImportRef(ref), item.HasGroupWith(group.ID(gid)))
|
||||
}
|
||||
|
||||
// GetOneByGroup returns a single item by ID. If the item does not exist, an error is returned.
|
||||
@@ -500,9 +498,9 @@ func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemOut,
|
||||
All(ctx))
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) GetAllZeroAssetID(ctx context.Context, GID uuid.UUID) ([]ItemSummary, error) {
|
||||
func (e *ItemsRepository) GetAllZeroAssetID(ctx context.Context, gid uuid.UUID) ([]ItemSummary, error) {
|
||||
q := e.db.Item.Query().Where(
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
item.HasGroupWith(group.ID(gid)),
|
||||
item.AssetID(0),
|
||||
).Order(
|
||||
ent.Asc(item.FieldCreatedAt),
|
||||
@@ -511,9 +509,9 @@ func (e *ItemsRepository) GetAllZeroAssetID(ctx context.Context, GID uuid.UUID)
|
||||
return mapItemsSummaryErr(q.All(ctx))
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) GetHighestAssetID(ctx context.Context, GID uuid.UUID) (AssetID, error) {
|
||||
func (e *ItemsRepository) GetHighestAssetID(ctx context.Context, gid uuid.UUID) (AssetID, error) {
|
||||
q := e.db.Item.Query().Where(
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
item.HasGroupWith(group.ID(gid)),
|
||||
).Order(
|
||||
ent.Desc(item.FieldAssetID),
|
||||
).Limit(1)
|
||||
@@ -529,10 +527,10 @@ func (e *ItemsRepository) GetHighestAssetID(ctx context.Context, GID uuid.UUID)
|
||||
return AssetID(result.AssetID), nil
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) SetAssetID(ctx context.Context, GID uuid.UUID, ID uuid.UUID, assetID AssetID) error {
|
||||
func (e *ItemsRepository) SetAssetID(ctx context.Context, gid uuid.UUID, id uuid.UUID, assetID AssetID) error {
|
||||
q := e.db.Item.Update().Where(
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
item.ID(ID),
|
||||
item.HasGroupWith(group.ID(gid)),
|
||||
item.ID(id),
|
||||
)
|
||||
|
||||
_, err := q.SetAssetID(int(assetID)).Save(ctx)
|
||||
@@ -548,7 +546,7 @@ func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data ItemCr
|
||||
SetLocationID(data.LocationID).
|
||||
SetAssetID(int(data.AssetID))
|
||||
|
||||
if data.LabelIDs != nil && len(data.LabelIDs) > 0 {
|
||||
if len(data.LabelIDs) > 0 {
|
||||
q.AddLabelIDs(data.LabelIDs...)
|
||||
}
|
||||
|
||||
@@ -586,8 +584,8 @@ func (e *ItemsRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) UpdateByGroup(ctx context.Context, GID uuid.UUID, data ItemUpdate) (ItemOut, error) {
|
||||
q := e.db.Item.Update().Where(item.ID(data.ID), item.HasGroupWith(group.ID(GID))).
|
||||
func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data ItemUpdate) (ItemOut, error) {
|
||||
q := e.db.Item.Update().Where(item.ID(data.ID), item.HasGroupWith(group.ID(gid))).
|
||||
SetName(data.Name).
|
||||
SetDescription(data.Description).
|
||||
SetLocationID(data.LocationID).
|
||||
@@ -698,16 +696,16 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, GID uuid.UUID, data
|
||||
}
|
||||
}
|
||||
|
||||
e.publishMutationEvent(GID)
|
||||
e.publishMutationEvent(gid)
|
||||
return e.GetOne(ctx, data.ID)
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) GetAllZeroImportRef(ctx context.Context, GID uuid.UUID) ([]uuid.UUID, error) {
|
||||
func (e *ItemsRepository) GetAllZeroImportRef(ctx context.Context, gid uuid.UUID) ([]uuid.UUID, error) {
|
||||
var ids []uuid.UUID
|
||||
|
||||
err := e.db.Item.Query().
|
||||
Where(
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
item.HasGroupWith(group.ID(gid)),
|
||||
item.Or(
|
||||
item.ImportRefEQ(""),
|
||||
item.ImportRefIsNil(),
|
||||
@@ -722,11 +720,11 @@ func (e *ItemsRepository) GetAllZeroImportRef(ctx context.Context, GID uuid.UUID
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) Patch(ctx context.Context, GID, ID uuid.UUID, data ItemPatch) error {
|
||||
func (e *ItemsRepository) Patch(ctx context.Context, gid, id uuid.UUID, data ItemPatch) error {
|
||||
q := e.db.Item.Update().
|
||||
Where(
|
||||
item.ID(ID),
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
item.ID(id),
|
||||
item.HasGroupWith(group.ID(gid)),
|
||||
)
|
||||
|
||||
if data.ImportRef != nil {
|
||||
@@ -737,11 +735,11 @@ func (e *ItemsRepository) Patch(ctx context.Context, GID, ID uuid.UUID, data Ite
|
||||
q.SetQuantity(*data.Quantity)
|
||||
}
|
||||
|
||||
e.publishMutationEvent(GID)
|
||||
e.publishMutationEvent(gid)
|
||||
return q.Exec(ctx)
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) GetAllCustomFieldValues(ctx context.Context, GID uuid.UUID, name string) ([]string, error) {
|
||||
func (e *ItemsRepository) GetAllCustomFieldValues(ctx context.Context, gid uuid.UUID, name string) ([]string, error) {
|
||||
type st struct {
|
||||
Value string `json:"text_value"`
|
||||
}
|
||||
@@ -750,7 +748,7 @@ func (e *ItemsRepository) GetAllCustomFieldValues(ctx context.Context, GID uuid.
|
||||
|
||||
err := e.db.Item.Query().
|
||||
Where(
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
item.HasGroupWith(group.ID(gid)),
|
||||
).
|
||||
QueryFields().
|
||||
Where(
|
||||
@@ -771,7 +769,7 @@ func (e *ItemsRepository) GetAllCustomFieldValues(ctx context.Context, GID uuid.
|
||||
return valueStrings, nil
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) GetAllCustomFieldNames(ctx context.Context, GID uuid.UUID) ([]string, error) {
|
||||
func (e *ItemsRepository) GetAllCustomFieldNames(ctx context.Context, gid uuid.UUID) ([]string, error) {
|
||||
type st struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
@@ -780,7 +778,7 @@ func (e *ItemsRepository) GetAllCustomFieldNames(ctx context.Context, GID uuid.U
|
||||
|
||||
err := e.db.Item.Query().
|
||||
Where(
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
item.HasGroupWith(group.ID(gid)),
|
||||
).
|
||||
QueryFields().
|
||||
Unique(true).
|
||||
@@ -804,9 +802,9 @@ func (e *ItemsRepository) GetAllCustomFieldNames(ctx context.Context, GID uuid.U
|
||||
// This is designed to resolve a long-time bug that has since been fixed with the time selector on the
|
||||
// frontend. This function is intended to be used as a one-time fix for existing databases and may be
|
||||
// removed in the future.
|
||||
func (e *ItemsRepository) ZeroOutTimeFields(ctx context.Context, GID uuid.UUID) (int, error) {
|
||||
func (e *ItemsRepository) ZeroOutTimeFields(ctx context.Context, gid uuid.UUID) (int, error) {
|
||||
q := e.db.Item.Query().Where(
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
item.HasGroupWith(group.ID(gid)),
|
||||
item.Or(
|
||||
item.PurchaseTimeNotNil(),
|
||||
item.PurchaseFromLT("0002-01-01"),
|
||||
@@ -875,11 +873,11 @@ func (e *ItemsRepository) ZeroOutTimeFields(ctx context.Context, GID uuid.UUID)
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) SetPrimaryPhotos(ctx context.Context, GID uuid.UUID) (int, error) {
|
||||
func (e *ItemsRepository) SetPrimaryPhotos(ctx context.Context, gid uuid.UUID) (int, error) {
|
||||
// All items where there is no primary photo
|
||||
itemIDs, err := e.db.Item.Query().
|
||||
Where(
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
item.HasGroupWith(group.ID(gid)),
|
||||
item.HasAttachmentsWith(
|
||||
attachment.TypeEQ(attachment.TypePhoto),
|
||||
attachment.Not(
|
||||
|
||||
@@ -236,7 +236,6 @@ func TestItemsRepository_Update(t *testing.T) {
|
||||
ModelNumber: fk.Str(10),
|
||||
Manufacturer: fk.Str(10),
|
||||
PurchaseTime: types.DateFromTime(time.Now()),
|
||||
PurchaseMethod: fk.Str(10),
|
||||
PurchaseFrom: fk.Str(10),
|
||||
PurchasePrice: 300.99,
|
||||
SoldTime: types.DateFromTime(time.Now()),
|
||||
@@ -263,7 +262,6 @@ func TestItemsRepository_Update(t *testing.T) {
|
||||
assert.Equal(t, updateData.Manufacturer, got.Manufacturer)
|
||||
// assert.Equal(t, updateData.PurchaseTime, got.PurchaseTime)
|
||||
assert.Equal(t, updateData.PurchaseFrom, got.PurchaseFrom)
|
||||
assert.Equal(t, updateData.PurchaseMethod, got.PurchaseMethod)
|
||||
assert.InDelta(t, updateData.PurchasePrice, got.PurchasePrice, 0.01)
|
||||
// assert.Equal(t, updateData.SoldTime, got.SoldTime)
|
||||
assert.Equal(t, updateData.SoldTo, got.SoldTo)
|
||||
|
||||
@@ -65,9 +65,9 @@ func mapLabelOut(label *ent.Label) LabelOut {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LabelRepository) publishMutationEvent(GID uuid.UUID) {
|
||||
func (r *LabelRepository) publishMutationEvent(gid uuid.UUID) {
|
||||
if r.bus != nil {
|
||||
r.bus.Publish(eventbus.EventLabelMutation, eventbus.GroupMutationEvent{GID: GID})
|
||||
r.bus.Publish(eventbus.EventLabelMutation, eventbus.GroupMutationEvent{GID: gid})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +79,8 @@ func (r *LabelRepository) getOne(ctx context.Context, where ...predicate.Label)
|
||||
)
|
||||
}
|
||||
|
||||
func (r *LabelRepository) GetOne(ctx context.Context, ID uuid.UUID) (LabelOut, error) {
|
||||
return r.getOne(ctx, label.ID(ID))
|
||||
func (r *LabelRepository) GetOne(ctx context.Context, id uuid.UUID) (LabelOut, error) {
|
||||
return r.getOne(ctx, label.ID(id))
|
||||
}
|
||||
|
||||
func (r *LabelRepository) GetOneByGroup(ctx context.Context, gid, ld uuid.UUID) (LabelOut, error) {
|
||||
@@ -125,13 +125,13 @@ func (r *LabelRepository) update(ctx context.Context, data LabelUpdate, where ..
|
||||
Save(ctx)
|
||||
}
|
||||
|
||||
func (r *LabelRepository) UpdateByGroup(ctx context.Context, GID uuid.UUID, data LabelUpdate) (LabelOut, error) {
|
||||
_, err := r.update(ctx, data, label.ID(data.ID), label.HasGroupWith(group.ID(GID)))
|
||||
func (r *LabelRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data LabelUpdate) (LabelOut, error) {
|
||||
_, err := r.update(ctx, data, label.ID(data.ID), label.HasGroupWith(group.ID(gid)))
|
||||
if err != nil {
|
||||
return LabelOut{}, err
|
||||
}
|
||||
|
||||
r.publishMutationEvent(GID)
|
||||
r.publishMutationEvent(gid)
|
||||
return r.GetOne(ctx, data.ID)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -90,9 +90,9 @@ func mapLocationOut(location *ent.Location) LocationOut {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LocationRepository) publishMutationEvent(GID uuid.UUID) {
|
||||
func (r *LocationRepository) publishMutationEvent(gid uuid.UUID) {
|
||||
if r.bus != nil {
|
||||
r.bus.Publish(eventbus.EventLocationMutation, eventbus.GroupMutationEvent{GID: GID})
|
||||
r.bus.Publish(eventbus.EventLocationMutation, eventbus.GroupMutationEvent{GID: gid})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ type LocationQuery struct {
|
||||
}
|
||||
|
||||
// GetAll returns all locations with item count field populated
|
||||
func (r *LocationRepository) GetAll(ctx context.Context, GID uuid.UUID, filter LocationQuery) ([]LocationOutCount, error) {
|
||||
func (r *LocationRepository) GetAll(ctx context.Context, gid uuid.UUID, filter LocationQuery) ([]LocationOutCount, error) {
|
||||
query := `--sql
|
||||
SELECT
|
||||
id,
|
||||
@@ -132,7 +132,7 @@ func (r *LocationRepository) GetAll(ctx context.Context, GID uuid.UUID, filter L
|
||||
query = strings.Replace(query, "{{ FILTER_CHILDREN }}", "", 1)
|
||||
}
|
||||
|
||||
rows, err := r.db.Sql().QueryContext(ctx, query, GID)
|
||||
rows, err := r.db.Sql().QueryContext(ctx, query, gid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -168,19 +168,19 @@ func (r *LocationRepository) getOne(ctx context.Context, where ...predicate.Loca
|
||||
Only(ctx))
|
||||
}
|
||||
|
||||
func (r *LocationRepository) Get(ctx context.Context, ID uuid.UUID) (LocationOut, error) {
|
||||
return r.getOne(ctx, location.ID(ID))
|
||||
func (r *LocationRepository) Get(ctx context.Context, id uuid.UUID) (LocationOut, error) {
|
||||
return r.getOne(ctx, location.ID(id))
|
||||
}
|
||||
|
||||
func (r *LocationRepository) GetOneByGroup(ctx context.Context, GID, ID uuid.UUID) (LocationOut, error) {
|
||||
return r.getOne(ctx, location.ID(ID), location.HasGroupWith(group.ID(GID)))
|
||||
func (r *LocationRepository) GetOneByGroup(ctx context.Context, gid, id uuid.UUID) (LocationOut, error) {
|
||||
return r.getOne(ctx, location.ID(id), location.HasGroupWith(group.ID(gid)))
|
||||
}
|
||||
|
||||
func (r *LocationRepository) Create(ctx context.Context, GID uuid.UUID, data LocationCreate) (LocationOut, error) {
|
||||
func (r *LocationRepository) Create(ctx context.Context, gid uuid.UUID, data LocationCreate) (LocationOut, error) {
|
||||
q := r.db.Location.Create().
|
||||
SetName(data.Name).
|
||||
SetDescription(data.Description).
|
||||
SetGroupID(GID)
|
||||
SetGroupID(gid)
|
||||
|
||||
if data.ParentID != uuid.Nil {
|
||||
q.SetParentID(data.ParentID)
|
||||
@@ -191,8 +191,8 @@ func (r *LocationRepository) Create(ctx context.Context, GID uuid.UUID, data Loc
|
||||
return LocationOut{}, err
|
||||
}
|
||||
|
||||
location.Edges.Group = &ent.Group{ID: GID} // bootstrap group ID
|
||||
r.publishMutationEvent(GID)
|
||||
location.Edges.Group = &ent.Group{ID: gid} // bootstrap group ID
|
||||
r.publishMutationEvent(gid)
|
||||
return mapLocationOut(location), nil
|
||||
}
|
||||
|
||||
@@ -216,28 +216,28 @@ func (r *LocationRepository) update(ctx context.Context, data LocationUpdate, wh
|
||||
return r.Get(ctx, data.ID)
|
||||
}
|
||||
|
||||
func (r *LocationRepository) UpdateByGroup(ctx context.Context, GID, ID uuid.UUID, data LocationUpdate) (LocationOut, error) {
|
||||
v, err := r.update(ctx, data, location.ID(ID), location.HasGroupWith(group.ID(GID)))
|
||||
func (r *LocationRepository) UpdateByGroup(ctx context.Context, gid, id uuid.UUID, data LocationUpdate) (LocationOut, error) {
|
||||
v, err := r.update(ctx, data, location.ID(id), location.HasGroupWith(group.ID(gid)))
|
||||
if err != nil {
|
||||
return LocationOut{}, err
|
||||
}
|
||||
|
||||
r.publishMutationEvent(GID)
|
||||
r.publishMutationEvent(gid)
|
||||
return v, err
|
||||
}
|
||||
|
||||
// delete should only be used after checking that the location is owned by the
|
||||
// group. Otherwise, use DeleteByGroup
|
||||
func (r *LocationRepository) delete(ctx context.Context, ID uuid.UUID) error {
|
||||
return r.db.Location.DeleteOneID(ID).Exec(ctx)
|
||||
func (r *LocationRepository) delete(ctx context.Context, id uuid.UUID) error {
|
||||
return r.db.Location.DeleteOneID(id).Exec(ctx)
|
||||
}
|
||||
|
||||
func (r *LocationRepository) DeleteByGroup(ctx context.Context, GID, ID uuid.UUID) error {
|
||||
_, err := r.db.Location.Delete().Where(location.ID(ID), location.HasGroupWith(group.ID(GID))).Exec(ctx)
|
||||
func (r *LocationRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID) error {
|
||||
_, err := r.db.Location.Delete().Where(location.ID(id), location.HasGroupWith(group.ID(gid))).Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.publishMutationEvent(GID)
|
||||
r.publishMutationEvent(gid)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -274,7 +274,7 @@ type ItemPath struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (r *LocationRepository) PathForLoc(ctx context.Context, GID, locID uuid.UUID) ([]ItemPath, error) {
|
||||
func (r *LocationRepository) PathForLoc(ctx context.Context, gid, locID uuid.UUID) ([]ItemPath, error) {
|
||||
query := `WITH RECURSIVE location_path AS (
|
||||
SELECT id, name, location_children
|
||||
FROM locations
|
||||
@@ -291,7 +291,7 @@ func (r *LocationRepository) PathForLoc(ctx context.Context, GID, locID uuid.UUI
|
||||
SELECT id, name
|
||||
FROM location_path`
|
||||
|
||||
rows, err := r.db.Sql().QueryContext(ctx, query, locID, GID)
|
||||
rows, err := r.db.Sql().QueryContext(ctx, query, locID, gid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -321,7 +321,7 @@ func (r *LocationRepository) PathForLoc(ctx context.Context, GID, locID uuid.UUI
|
||||
return locations, nil
|
||||
}
|
||||
|
||||
func (r *LocationRepository) Tree(ctx context.Context, GID uuid.UUID, tq TreeQuery) ([]TreeItem, error) {
|
||||
func (r *LocationRepository) Tree(ctx context.Context, gid uuid.UUID, tq TreeQuery) ([]TreeItem, error) {
|
||||
query := `
|
||||
WITH recursive location_tree(id, NAME, parent_id, level, node_type) AS
|
||||
(
|
||||
@@ -403,7 +403,7 @@ func (r *LocationRepository) Tree(ctx context.Context, GID uuid.UUID, tq TreeQue
|
||||
query = strings.ReplaceAll(query, "{{ WITH_ITEMS_FROM }}", "")
|
||||
}
|
||||
|
||||
rows, err := r.db.Sql().QueryContext(ctx, query, GID)
|
||||
rows, err := r.db.Sql().QueryContext(ctx, query, gid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
72
backend/internal/data/repo/repo_maintenance.go
Normal file
72
backend/internal/data/repo/repo_maintenance.go
Normal 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
|
||||
}
|
||||
@@ -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 (
|
||||
@@ -84,11 +77,11 @@ func mapMaintenanceEntry(entry *ent.MaintenanceEntry) MaintenanceEntry {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MaintenanceEntryRepository) GetScheduled(ctx context.Context, GID uuid.UUID, dt types.Date) ([]MaintenanceEntry, error) {
|
||||
func (r *MaintenanceEntryRepository) GetScheduled(ctx context.Context, gid uuid.UUID, dt types.Date) ([]MaintenanceEntry, error) {
|
||||
entries, err := r.db.MaintenanceEntry.Query().
|
||||
Where(
|
||||
maintenanceentry.HasItemWith(
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
item.HasGroupWith(group.ID(gid)),
|
||||
),
|
||||
maintenanceentry.ScheduledDate(dt.Time()),
|
||||
maintenanceentry.Or(
|
||||
@@ -118,8 +111,8 @@ func (r *MaintenanceEntryRepository) Create(ctx context.Context, itemID uuid.UUI
|
||||
return mapMaintenanceEntryErr(item, err)
|
||||
}
|
||||
|
||||
func (r *MaintenanceEntryRepository) Update(ctx context.Context, ID uuid.UUID, input MaintenanceEntryUpdate) (MaintenanceEntry, error) {
|
||||
item, err := r.db.MaintenanceEntry.UpdateOneID(ID).
|
||||
func (r *MaintenanceEntryRepository) Update(ctx context.Context, id uuid.UUID, input MaintenanceEntryUpdate) (MaintenanceEntry, error) {
|
||||
item, err := r.db.MaintenanceEntry.UpdateOneID(id).
|
||||
SetDate(input.CompletedDate.Time()).
|
||||
SetScheduledDate(input.ScheduledDate.Time()).
|
||||
SetName(input.Name).
|
||||
@@ -130,78 +123,34 @@ 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 {
|
||||
return r.db.MaintenanceEntry.DeleteOneID(ID).Exec(ctx)
|
||||
func (r *MaintenanceEntryRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return r.db.MaintenanceEntry.DeleteOneID(id).Exec(ctx)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ type (
|
||||
|
||||
Name string `json:"name"`
|
||||
IsActive bool `json:"isActive"`
|
||||
URL string `json:"-"` // URL field is not exposed to the client
|
||||
URL string `json:"url"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -114,7 +114,7 @@ func (r *NotifierRepository) Update(ctx context.Context, userID uuid.UUID, id uu
|
||||
return r.mapper.MapErr(notifier, err)
|
||||
}
|
||||
|
||||
func (r *NotifierRepository) Delete(ctx context.Context, userID uuid.UUID, ID uuid.UUID) error {
|
||||
_, err := r.db.Notifier.Delete().Where(notifier.UserID(userID), notifier.ID(ID)).Exec(ctx)
|
||||
func (r *NotifierRepository) Delete(ctx context.Context, userID uuid.UUID, id uuid.UUID) error {
|
||||
_, err := r.db.Notifier.Delete().Where(notifier.UserID(userID), notifier.ID(id)).Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -60,9 +60,9 @@ func mapUserOut(user *ent.User) UserOut {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetOneID(ctx context.Context, ID uuid.UUID) (UserOut, error) {
|
||||
func (r *UserRepository) GetOneID(ctx context.Context, id uuid.UUID) (UserOut, error) {
|
||||
return mapUserOutErr(r.db.User.Query().
|
||||
Where(user.ID(ID)).
|
||||
Where(user.ID(id)).
|
||||
WithGroup().
|
||||
Only(ctx))
|
||||
}
|
||||
@@ -101,9 +101,9 @@ func (r *UserRepository) Create(ctx context.Context, usr UserCreate) (UserOut, e
|
||||
return r.GetOneID(ctx, entUser.ID)
|
||||
}
|
||||
|
||||
func (r *UserRepository) Update(ctx context.Context, ID uuid.UUID, data UserUpdate) error {
|
||||
func (r *UserRepository) Update(ctx context.Context, id uuid.UUID, data UserUpdate) error {
|
||||
q := r.db.User.Update().
|
||||
Where(user.ID(ID)).
|
||||
Where(user.ID(id)).
|
||||
SetName(data.Name).
|
||||
SetEmail(data.Email)
|
||||
|
||||
@@ -130,6 +130,6 @@ func (r *UserRepository) GetSuperusers(ctx context.Context) ([]*ent.User, error)
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) ChangePassword(ctx context.Context, UID uuid.UUID, pw string) error {
|
||||
return r.db.User.UpdateOneID(UID).SetPassword(pw).Exec(ctx)
|
||||
func (r *UserRepository) ChangePassword(ctx context.Context, uid uuid.UUID, pw string) error {
|
||||
return r.db.User.UpdateOneID(uid).SetPassword(pw).Exec(ctx)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ func (tp *TemplateProps) Set(key, value string) {
|
||||
func DefaultTemplateData() TemplateProps {
|
||||
return TemplateProps{
|
||||
Defaults: TemplateDefaults{
|
||||
CompanyName: "Haybytes.com",
|
||||
CompanyName: "sysadminsmedia.com",
|
||||
CompanyAddress: "123 Main St, Anytown, CA 12345",
|
||||
CompanyURL: "https://haybytes.com",
|
||||
CompanyURL: "https://sysadminsmedia.com",
|
||||
ActivateAccountURL: "https://google.com",
|
||||
UnsubscribeURL: "https://google.com",
|
||||
},
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import enMenu from "./menus/en.mjs";
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
ignoreDeadLinks: [
|
||||
/^https?:\/\/localhost:7745/,
|
||||
],
|
||||
|
||||
title: "HomeBox",
|
||||
description: "A simple home inventory management software",
|
||||
lastUpdated: true,
|
||||
sitemap: {
|
||||
hostname: 'https://homebox.sysadminsmedia.com',
|
||||
hostname: 'https://homebox.software',
|
||||
},
|
||||
|
||||
head: [
|
||||
['link', { rel: 'icon', href: '/favicon.svg' }],
|
||||
['meta', { name: 'theme-color', content: '#3eaf7c' }],
|
||||
['meta', { name: 'og:title', content: 'HomeBox' }],
|
||||
['meta', { name: 'og:description', content: 'A simple home inventory management software' }],
|
||||
['meta', { name: 'og:image', content: '/homebox-email-banner.jpg' }],
|
||||
['meta', { name: 'twitter:card', content: 'summary' }],
|
||||
],
|
||||
|
||||
locales: {
|
||||
en: {
|
||||
label: 'English',
|
||||
@@ -27,37 +41,17 @@ export default defineConfig({
|
||||
},
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: 'API', link: 'https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/sysadminsmedia/homebox/main/docs/docs/api/openapi-2.0.json' }
|
||||
{ text: 'API', link: 'https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/sysadminsmedia/homebox/main/docs/docs/api/openapi-2.0.json' },
|
||||
{ text: 'Demo', link: 'https://demo.homebox.software' },
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
'/en/': [
|
||||
{
|
||||
text: 'Getting Started',
|
||||
items: [
|
||||
{ text: 'Quick Start', link: '/en/quick-start' },
|
||||
{ text: 'Tips and Tricks', link: '/en/tips-tricks' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Advanced',
|
||||
items: [
|
||||
{ text: 'Import CSV', link: '/en/import-csv' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Contributing',
|
||||
items: [
|
||||
{ text: 'Get Started', link: '/en/contribute/get-started' },
|
||||
{ text: 'Bounty Program', link: '/en/contribute/bounty' }
|
||||
]
|
||||
}
|
||||
]
|
||||
'/en/': enMenu,
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'discord', link: 'https://discord.gg/aY4DCkpNA9' },
|
||||
{ icon: 'github', link: 'https://github.com/sysadminsmedia/homebox' },
|
||||
{ icon: 'discord', link: 'https://discord.homebox.software' },
|
||||
{ icon: 'github', link: 'https://git.homebox.software' },
|
||||
{ icon: 'mastodon', link: 'https://noc.social/@sysadminszone' },
|
||||
]
|
||||
}
|
||||
|
||||
25
docs/.vitepress/menus/en.mts
Normal file
25
docs/.vitepress/menus/en.mts
Normal file
@@ -0,0 +1,25 @@
|
||||
export default [
|
||||
{
|
||||
text: 'Getting Started',
|
||||
items: [
|
||||
{text: 'Quick Start', link: '/en/quick-start'},
|
||||
{text: 'Installation', link: '/en/installation'},
|
||||
{text: 'Organizing Your Items', link: '/en/organizing-items'},
|
||||
{text: 'Configure Homebox', link: '/en/configure-homebox'},
|
||||
{text: 'Tips and Tricks', link: '/en/tips-tricks'}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Advanced',
|
||||
items: [
|
||||
{text: 'Import CSV', link: '/en/import-csv'},
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Contributing',
|
||||
items: [
|
||||
{text: 'Get Started', link: '/en/contribute/get-started'},
|
||||
{text: 'Bounty Program', link: '/en/contribute/bounty'}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -910,14 +910,34 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Maintenance"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -932,7 +952,7 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Maintenance"
|
||||
"Item Maintenance"
|
||||
],
|
||||
"summary": "Create Maintenance Entry",
|
||||
"parameters": [
|
||||
@@ -956,60 +976,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 +1368,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": [
|
||||
@@ -1683,12 +1747,14 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"example": "admin@admin.com",
|
||||
"description": "string",
|
||||
"name": "username",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "admin",
|
||||
"description": "string",
|
||||
"name": "password",
|
||||
"in": "formData"
|
||||
@@ -2140,9 +2206,6 @@
|
||||
"x-nullable": true,
|
||||
"x-omitempty": true
|
||||
},
|
||||
"purchaseMethod": {
|
||||
"type": "string"
|
||||
},
|
||||
"purchaseFrom": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2608,26 +2671,49 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.MaintenanceLog": {
|
||||
"repo.MaintenanceEntryWithDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"costAverage": {
|
||||
"type": "number"
|
||||
"completedDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"costTotal": {
|
||||
"type": "number"
|
||||
"cost": {
|
||||
"type": "string",
|
||||
"example": "0"
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.MaintenanceEntry"
|
||||
}
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"itemId": {
|
||||
"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.NotifierCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -2669,6 +2755,10 @@
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "URL field is not exposed to the client",
|
||||
"type": "string"
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -2711,9 +2801,6 @@
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"totalPrice": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2996,4 +3083,4 @@
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
docs/en/configure-homebox.md
Normal file
59
docs/en/configure-homebox.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Configure Homebox
|
||||
|
||||
## Env Variables & Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------------------ | ---------------------- | ---------------------------------------------------------------------------------- |
|
||||
| HBOX_MODE | `production` | application mode used for runtime behavior can be one of: `development`, `production` |
|
||||
| HBOX_WEB_PORT | 7745 | port to run the web server on, if you're using docker do not change this |
|
||||
| HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this |
|
||||
| HBOX_OPTIONS_ALLOW_REGISTRATION | true | allow users to register themselves |
|
||||
| HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID | true | auto-increments the asset_id field for new items |
|
||||
| HBOX_OPTIONS_CURRENCY_CONFIG | | json configuration file containing additional currencie |
|
||||
| HBOX_WEB_MAX_UPLOAD_SIZE | 10 | maximum file upload size supported in MB |
|
||||
| HBOX_WEB_READ_TIMEOUT | 10s | Read timeout of HTTP sever |
|
||||
| HBOX_WEB_WRITE_TIMEOUT | 10s | Write timeout of HTTP server |
|
||||
| HBOX_WEB_IDLE_TIMEOUT | 30s | Idle timeout of HTTP server |
|
||||
| HBOX_STORAGE_DATA | /data/ | path to the data directory, do not change this if you're using docker |
|
||||
| HBOX_STORAGE_SQLITE_URL | /data/homebox.db?_fk=1 | sqlite database url, if you're using docker do not change this |
|
||||
| HBOX_LOG_LEVEL | `info` | log level to use, can be one of `trace`, `debug`, `info`, `warn`, `error`, `critical` |
|
||||
| HBOX_LOG_FORMAT | `text` | log format to use, can be one of: `text`, `json` |
|
||||
| HBOX_MAILER_HOST | | email host to use, if not set no email provider will be used |
|
||||
| HBOX_MAILER_PORT | 587 | email port to use |
|
||||
| HBOX_MAILER_USERNAME | | email user to use |
|
||||
| HBOX_MAILER_PASSWORD | | email password to use |
|
||||
| HBOX_MAILER_FROM | | email from address to use |
|
||||
| HBOX_SWAGGER_HOST | 7745 | swagger host to use, if not set swagger will be disabled |
|
||||
| HBOX_SWAGGER_SCHEMA | `http` | swagger schema to use, can be one of: `http`, `https` |
|
||||
|
||||
::: tip "CLI Arguments"
|
||||
If you're deploying without docker you can use command line arguments to configure the application. Run `homebox --help` for more information.
|
||||
|
||||
```sh
|
||||
Usage: api [options] [arguments]
|
||||
|
||||
OPTIONS
|
||||
--mode/$HBOX_MODE <string> (default: development)
|
||||
--web-port/$HBOX_WEB_PORT <string> (default: 7745)
|
||||
--web-host/$HBOX_WEB_HOST <string>
|
||||
--web-max-upload-size/$HBOX_WEB_MAX_UPLOAD_SIZE <int> (default: 10)
|
||||
--storage-data/$HBOX_STORAGE_DATA <string> (default: ./.data)
|
||||
--storage-sqlite-url/$HBOX_STORAGE_SQLITE_URL <string> (default: ./.data/homebox.db?_fk=1)
|
||||
--log-level/$HBOX_LOG_LEVEL <string> (default: info)
|
||||
--log-format/$HBOX_LOG_FORMAT <string> (default: text)
|
||||
--mailer-host/$HBOX_MAILER_HOST <string>
|
||||
--mailer-port/$HBOX_MAILER_PORT <int>
|
||||
--mailer-username/$HBOX_MAILER_USERNAME <string>
|
||||
--mailer-password/$HBOX_MAILER_PASSWORD <string>
|
||||
--mailer-from/$HBOX_MAILER_FROM <string>
|
||||
--swagger-host/$HBOX_SWAGGER_HOST <string> (default: localhost:7745)
|
||||
--swagger-scheme/$HBOX_SWAGGER_SCHEME <string> (default: http)
|
||||
--demo/$HBOX_DEMO <bool>
|
||||
--debug-enabled/$HBOX_DEBUG_ENABLED <bool> (default: false)
|
||||
--debug-port/$HBOX_DEBUG_PORT <string> (default: 4000)
|
||||
--options-allow-registration/$HBOX_OPTIONS_ALLOW_REGISTRATION <bool> (default: true)
|
||||
--options-auto-increment-asset-id/$HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID <bool> (default: true)
|
||||
--options-currency-config/$HBOX_OPTIONS_CURRENCY_CONFIG <string>
|
||||
--help/-h display this help message
|
||||
```
|
||||
:::
|
||||
@@ -47,24 +47,33 @@ type checking `task ui:check`
|
||||
|
||||
## Documentation
|
||||
We use [Vitepress](https://vitepress.dev/) for the web documentation of homebox. Anyone is welcome to contribute the documentation if they wish.
|
||||
Anyone is welcome to contribute the documentation if they wish. For documentation contributions, you only need Node.js and PNPM.
|
||||
For documentation contributions, you only need Node.js and PNPM.
|
||||
|
||||
::: info Notes
|
||||
- Languages are separated by folder (e.g `/en`, `/fr`, etc.)
|
||||
- The Sidebar must be updated on a per language basis
|
||||
+ The Sidebar must be updated on a per-language basis
|
||||
- Each languages files can be named independently (slugs can match the language)
|
||||
- Each language's files can be named independently (slugs can match the language)
|
||||
- The `public/_redirects` file is used to redirect the default to english
|
||||
- Redirects can also be configured per language by adding `Language=` after the redirect code
|
||||
:::
|
||||
|
||||
## Translations
|
||||
We use our own [Weblate instance](https://translate.sysadminsmedia.com/projects/homebox/) for translations. If you would like to help translate Homebox, please visit the
|
||||
Weblate instance and help us translate the project. We accept translations for any language.
|
||||
|
||||
If you add a new language, please go to the English translation, press the `Add new translation string` button and then
|
||||
use `languages.<language_code>` as the key. For example, if you are adding a French translation, the key would be `languages.fr`.
|
||||
And then the string should be the name of the language in English. This is used to display the language in the language switcher.
|
||||
|
||||
[](http://translate.sysadminsmedia.com/engage/homebox/)
|
||||
|
||||
## Branch Flow
|
||||
We use the `main` branch as the development branch. All PRs should be made to the `main` branch form a feature branch.
|
||||
We use the `main` branch as the development branch. All PRs should be made to the `main` branch from a feature branch.
|
||||
To create a pull request you can use the following steps:
|
||||
|
||||
1. Fork the repo and create a new branch from `main`
|
||||
2. If you added code that should be tested, add tests
|
||||
3. If you've changed APIs update the documentation
|
||||
3. If you've changed APIs, update the documentation
|
||||
4. Ensure that the test suite and linters pass
|
||||
5. Create your PR
|
||||
|
||||
|
||||
BIN
docs/en/images/home-screen.png
Normal file
BIN
docs/en/images/home-screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
@@ -63,7 +63,6 @@ Specifying import refs also allows you to update existing items via the CSV impo
|
||||
| HB.model_number | String | Model of the item |
|
||||
| HB.manufacturer | String | Manufacturer of the item |
|
||||
| HB.notes | String (1000) | General notes about the product |
|
||||
| HB.purchase_method | String | Method of how the item was purchased |
|
||||
| HB.purchase_from | String | Name of the place the item was purchased from |
|
||||
| HB.purchase_price | Float64 | |
|
||||
| HB.purchase_time | Date | Date the item was purchased |
|
||||
|
||||
@@ -15,6 +15,9 @@ hero:
|
||||
- theme: alt
|
||||
text: Tips and Tricks
|
||||
link: /en/tips-tricks
|
||||
- theme: alt
|
||||
text: Try It Out
|
||||
link: https://demo.homebox.software
|
||||
|
||||
features:
|
||||
- title: Add/Update/Delete Items
|
||||
@@ -28,9 +31,11 @@ features:
|
||||
- title: Custom labeling and locations
|
||||
details: Use custom labels and locations to organize items
|
||||
- title: Multi-Tenant Support
|
||||
details: All users are in a group, and can only see what's in the group. Invite family memebers or share an instance with friends.
|
||||
details: All users are in a group, and can only see what's in the group. Invite family members or share an instance with friends.
|
||||
---
|
||||
|
||||

|
||||
|
||||
Homebox is the inventory and organization system built for the Home User! With a focus on simplicity and ease of use, Homebox is the perfect solution for your home inventory, organization, and management needs. While developing this project, I've tried to keep the following principles in mind:
|
||||
|
||||
- _Simple_ - Homebox is designed to be simple and easy to use. No complicated setup or configuration required. Use either a single docker container, or deploy yourself by compiling the binary for your platform of choice.
|
||||
|
||||
99
docs/en/installation.md
Normal file
99
docs/en/installation.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Installation
|
||||
|
||||
There are two main ways to run the application.
|
||||
|
||||
1. As a [Docker](https://www.docker.com/) container.
|
||||
2. Using the correct executable for your platform by downloading it from the [Releases](https://github.com/sysadminsmedia/homebox/releases).
|
||||
|
||||
|
||||
## Docker
|
||||
|
||||
The following instructions assume Docker is already installed on your system. See [(Docker's official installation guide)](https://docs.docker.com/engine/install/)
|
||||
|
||||
The official image is `ghcr.io/sysadminsmedia/homebox:latest`. For each image there are two tags, respectively the regular tag and $TAG-rootless, which uses a non-root image.
|
||||
|
||||
### Docker Run
|
||||
|
||||
Great for testing out the application, but not recommended for stable use. Checkout the docker-compose below for the recommended deployment.
|
||||
|
||||
|
||||
```sh
|
||||
# If using the rootless image, ensure data
|
||||
# folder has correct permissions
|
||||
$ mkdir -p /path/to/data/folder
|
||||
$ chown 65532:65532 -R /path/to/data/folder
|
||||
# ---------------------------------------
|
||||
# Run the image
|
||||
$ docker run -d \
|
||||
--name homebox \
|
||||
--restart unless-stopped \
|
||||
--publish 3100:7745 \
|
||||
--env TZ=Europe/Bucharest \
|
||||
--volume /path/to/data/folder/:/data \
|
||||
ghcr.io/sysadminsmedia/homebox:latest
|
||||
# ghcr.io/sysadminsmedia/homebox:latest-rootless
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
1. Create a `docker-compose.yml` file.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
homebox:
|
||||
image: ghcr.io/sysadminsmedia/homebox:latest
|
||||
# image: ghcr.io/sysadminsmedia/homebox:latest-rootless
|
||||
container_name: homebox
|
||||
restart: always
|
||||
environment:
|
||||
- HBOX_LOG_LEVEL=info
|
||||
- HBOX_LOG_FORMAT=text
|
||||
- HBOX_WEB_MAX_UPLOAD_SIZE=10
|
||||
volumes:
|
||||
- homebox-data:/data/
|
||||
ports:
|
||||
- 3100:7745
|
||||
|
||||
volumes:
|
||||
homebox-data:
|
||||
driver: local
|
||||
```
|
||||
|
||||
::: info
|
||||
If you use the `rootless` image, and instead of using named volumes you would prefer using a hostMount directly (e.g., `volumes: [ /path/to/data/folder:/data ]`) you need to `chown` the chosen directory in advance to the `65532` user (as shown in the Docker example above).
|
||||
:::
|
||||
|
||||
::: warning
|
||||
If you have previously set up docker compose with the `HBOX_WEB_READ_TIMEOUT`, `HBOX_WEB_WRITE_TIMEOUT`, or `HBOX_IDLE_TIMEOUT` options, and you were previously using the hay-kot image, please note that you will have to add an `s` for seconds or `m` for minutes to the end of the integers. A dependency update removed the defaultation to seconds and it now requires an explicit duration time.
|
||||
:::
|
||||
|
||||
2. While in the same folder as docker-compose.yml, start the container by running:
|
||||
|
||||
```bash
|
||||
docker compose up --detach
|
||||
```
|
||||
|
||||
3. Navigate to `http://server.local.ip.address:3100/` to access the web interface. (replace with the right IP address).
|
||||
|
||||
You can learn more about Docker by [reading the official Docker documentation.](https://docs.docker.com/)
|
||||
|
||||
## Windows
|
||||
|
||||
1. Download the appropriate release for your CPU architecture from the [releases page on GitHub](https://github.com/sysadminsmedia/homebox/releases).
|
||||
2. Extract the archive.
|
||||
3. Run `homebox.exe`. This will start the server on port 7745.
|
||||
4. You can test it by accessing http://localhost:7745.
|
||||
|
||||
## Linux
|
||||
|
||||
1. Download the appropriate release for your CPU architecture from the [releases page on GitHub](https://github.com/sysadminsmedia/homebox/releases).
|
||||
2. Extract the archive.
|
||||
3. Run the `homebox` executable.
|
||||
4. The web interface will be accessible on port 7745 by default. Access the page by navigating to `http://server.local.ip.address:7745/` (replace with the right ip address)
|
||||
|
||||
## macOS
|
||||
|
||||
1. Download the appropriate release for your CPU architecture from the [releases page on GitHub](https://github.com/sysadminsmedia/homebox/releases). (Use `homebox_Darwin_x86_64.tar.gz` for Intel-based macs and `homebox_Darwin_arm64.tar.gz` for Apple Silicon)
|
||||
2. Extract the archive.
|
||||
3. Run the `homebox` executable.
|
||||
4. The web interface will be accessible on port 7745 by default. Access the page by navigating to `http://local.ip.address:7745/` (replace with the right ip address)
|
||||
80
docs/en/organizing-items.md
Normal file
80
docs/en/organizing-items.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Organizing Your Items
|
||||
|
||||
Homebox allows you to organize your items using locations and labels.
|
||||
|
||||
## Items
|
||||
|
||||
Items represent an item or asset you want to track in Homebox.
|
||||
|
||||
To create an item, click on the Create button in the main menu, the select Item / Asset. The following parameters are available:
|
||||
- Parent Location (Required) - The location in which the item is stored
|
||||
- Item Name (Required) - Name of the item
|
||||
- Item Description (Optional) - Description of the item
|
||||
- Labels (Optional) - Here, you can add as many Labels to the item as you like for further organization
|
||||
- Photo (Optional) - Allows you to add a photo of the item. Additional photos can be added after the item has been created.
|
||||
|
||||
::: details Additional Fields
|
||||
Once the item is created, additional fields become available. These are:
|
||||
|
||||
**Details Section**
|
||||
- Quantity
|
||||
- Serial Number
|
||||
- Model Number
|
||||
- Manufacturer
|
||||
- Notes
|
||||
- Insured (Checkbox)
|
||||
- Archived (Checkbox)
|
||||
- Asset-ID (Populated by default)
|
||||
|
||||
**Purchase Details Section**
|
||||
- Purchased From
|
||||
- Purchase Price
|
||||
- Purchase Date
|
||||
|
||||
**Warranty Details Section**
|
||||
- Lifetime Warranty (Checkbox)
|
||||
- Warranty Expires
|
||||
- Warranty Notes
|
||||
|
||||
**Sold Details Section**
|
||||
- Sold to
|
||||
- Sold Price
|
||||
- Sold At
|
||||
:::
|
||||
|
||||
You can also add custom fields by scrolling down to the Custom Fields section and clicking the add button. Custom fields are added on a per-item basis, not globally.
|
||||
|
||||
You might want to add attachments to an item (such as photos, a user manual, a warranty, a receipt, etc.) Scroll down to the Attachments section, and either click on the box or drag and drop to add files. Once a file has been added, click edit to edit its details.
|
||||
|
||||
> [!TIP]
|
||||
> To set the primary image (the image that appears in the Search view) for the item, click on the edit button next to the uploaded image and ensure the Primary Photo checkbox is checked. By default, the first photo you upload will automatically be the Primary Photo.
|
||||
|
||||
|
||||
## Locations
|
||||
|
||||
*Items* are stored in *locations*. Locations can be nested to create as many locations as you need. Homebox creates some default locations initially, but you can easily change, delete or rearrange them.
|
||||
|
||||
To create a location, click the Create button in the main menu, then select Location.
|
||||
|
||||
Locations have 3 parameters:
|
||||
- Name
|
||||
- Description (Optional)
|
||||
- Parent Location (Optional -Leave blank if this should be a top-level Location, or select the location you want this location to be nested under)
|
||||
|
||||
To view all locations and the items stored in them, select Locations in the main menu.
|
||||
|
||||
## Labels
|
||||
|
||||
Labels allow you to organize items independently of location. For example, you might have electronic devices all over your house. What if you wanted to see a list of every Electronic Device you own without having to go through every single location?
|
||||
|
||||
Labels enable this. In the example above, if you tag all electronic devices with the "Electronics" label when you create them, you can easily see the list of all your Electronics by going to the related page.
|
||||
|
||||
To create a Label, click the Create button in the menu, then select Label. Labels have the following parameters:
|
||||
- Name
|
||||
- Description (Optional)
|
||||
|
||||
To see all items related to a specific label (or specific combination of labels):
|
||||
- From the Home page, scroll to the bottom and select the Label you want to see.
|
||||
- Alternatively, navigate to the Search page and use the Labels dropdown to filter by one or more labels.
|
||||
|
||||
Items can have as many labels as you wish. The example above is only one example of how labels can be used for organization. Experiment with what labels work best for you!
|
||||
@@ -1,113 +1,12 @@
|
||||
# Quick Start
|
||||
|
||||
## Docker Run
|
||||
1. Install Homebox either by using [the latest Docker image](https://ghcr.io/sysadminsmedia/homebox:latest), or by downloading the correct executable for your Operating System from the [Releases](https://github.com/sysadminsmedia/homebox/releases). (See [Installation](./installation) for more details)
|
||||
|
||||
Great for testing out the application, but not recommended for stable use. Checkout the docker-compose for the recommended deployment.
|
||||
2. Browse to `http://SERVER_IP:3100` (if Using Docker) or `http://SERVER_IP:7745` (if installed locally) to access the included web User Interface.
|
||||
|
||||
For each image there are two tags, respectively the regular tag and $TAG-rootless, which uses a non-root image.
|
||||
3. Register your first user.
|
||||
|
||||
```sh
|
||||
# If using the rootless image, ensure data
|
||||
# folder has correct permissions
|
||||
$ mkdir -p /path/to/data/folder
|
||||
$ chown 65532:65532 -R /path/to/data/folder
|
||||
# ---------------------------------------
|
||||
# Run the image
|
||||
$ docker run -d \
|
||||
--name homebox \
|
||||
--restart unless-stopped \
|
||||
--publish 3100:7745 \
|
||||
--env TZ=Europe/Bucharest \
|
||||
--volume /path/to/data/folder/:/data \
|
||||
ghcr.io/sysadminsmedia/homebox:latest
|
||||
# ghcr.io/sysadminsmedia/homebox:latest-rootless
|
||||
4. Login with the user you just created and start adding your locations and items!
|
||||
|
||||
```
|
||||
|
||||
## Docker-Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
homebox:
|
||||
image: ghcr.io/sysadminsmedia/homebox:latest
|
||||
# image: ghcr.io/sysadminsmedia/homebox:latest-rootless
|
||||
container_name: homebox
|
||||
restart: always
|
||||
environment:
|
||||
- HBOX_LOG_LEVEL=info
|
||||
- HBOX_LOG_FORMAT=text
|
||||
- HBOX_WEB_MAX_UPLOAD_SIZE=10
|
||||
volumes:
|
||||
- homebox-data:/data/
|
||||
ports:
|
||||
- 3100:7745
|
||||
|
||||
volumes:
|
||||
homebox-data:
|
||||
driver: local
|
||||
```
|
||||
::: info
|
||||
If you use the `rootless` image, and instead of using named volumes you would prefer using a hostMount directly (e.g., `volumes: [ /path/to/data/folder:/data ]`) you need to `chown` the chosen directory in advance to the `65532` user (as shown in the Docker example above).
|
||||
:::
|
||||
|
||||
::: warning
|
||||
If you have previously set up docker compose with the `HBOX_WEB_READ_TIMEOUT`, `HBOX_WEB_WRITE_TIMEOUT`, or `HBOX_IDLE_TIMEOUT` options, and you were previously using the hay-kot image, please note that you will have to add an `s` for seconds or `m` for minutes to the end of the integers. A dependency update removed the defaultation to seconds and it now requires an explicit duration time.
|
||||
:::
|
||||
|
||||
## Env Variables & Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------------------ | ---------------------- | ---------------------------------------------------------------------------------- |
|
||||
| HBOX_MODE | production | application mode used for runtime behavior can be one of: development, production |
|
||||
| HBOX_WEB_PORT | 7745 | port to run the web server on, if you're using docker do not change this |
|
||||
| HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this |
|
||||
| HBOX_OPTIONS_ALLOW_REGISTRATION | true | allow users to register themselves |
|
||||
| HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID | true | auto-increments the asset_id field for new items |
|
||||
| HBOX_OPTIONS_CURRENCY_CONFIG | | json configuration file containing additional currencie |
|
||||
| HBOX_WEB_MAX_UPLOAD_SIZE | 10 | maximum file upload size supported in MB |
|
||||
| HBOX_WEB_READ_TIMEOUT | 10s | Read timeout of HTTP sever |
|
||||
| HBOX_WEB_WRITE_TIMEOUT | 10s | Write timeout of HTTP server |
|
||||
| HBOX_WEB_IDLE_TIMEOUT | 30s | Idle timeout of HTTP server |
|
||||
| HBOX_STORAGE_DATA | /data/ | path to the data directory, do not change this if you're using docker |
|
||||
| HBOX_STORAGE_SQLITE_URL | /data/homebox.db?_fk=1 | sqlite database url, if you're using docker do not change this |
|
||||
| HBOX_LOG_LEVEL | info | log level to use, can be one of trace, debug, info, warn, error, critical |
|
||||
| HBOX_LOG_FORMAT | text | log format to use, can be one of: text, json |
|
||||
| HBOX_MAILER_HOST | | email host to use, if not set no email provider will be used |
|
||||
| HBOX_MAILER_PORT | 587 | email port to use |
|
||||
| HBOX_MAILER_USERNAME | | email user to use |
|
||||
| HBOX_MAILER_PASSWORD | | email password to use |
|
||||
| HBOX_MAILER_FROM | | email from address to use |
|
||||
| HBOX_SWAGGER_HOST | 7745 | swagger host to use, if not set swagger will be disabled |
|
||||
| HBOX_SWAGGER_SCHEMA | http | swagger schema to use, can be one of: http, https |
|
||||
|
||||
::: tip "CLI Arguments"
|
||||
If you're deploying without docker you can use command line arguments to configure the application. Run `homebox --help` for more information.
|
||||
|
||||
```sh
|
||||
Usage: api [options] [arguments]
|
||||
|
||||
OPTIONS
|
||||
--mode/$HBOX_MODE <string> (default: development)
|
||||
--web-port/$HBOX_WEB_PORT <string> (default: 7745)
|
||||
--web-host/$HBOX_WEB_HOST <string>
|
||||
--web-max-upload-size/$HBOX_WEB_MAX_UPLOAD_SIZE <int> (default: 10)
|
||||
--storage-data/$HBOX_STORAGE_DATA <string> (default: ./.data)
|
||||
--storage-sqlite-url/$HBOX_STORAGE_SQLITE_URL <string> (default: ./.data/homebox.db?_fk=1)
|
||||
--log-level/$HBOX_LOG_LEVEL <string> (default: info)
|
||||
--log-format/$HBOX_LOG_FORMAT <string> (default: text)
|
||||
--mailer-host/$HBOX_MAILER_HOST <string>
|
||||
--mailer-port/$HBOX_MAILER_PORT <int>
|
||||
--mailer-username/$HBOX_MAILER_USERNAME <string>
|
||||
--mailer-password/$HBOX_MAILER_PASSWORD <string>
|
||||
--mailer-from/$HBOX_MAILER_FROM <string>
|
||||
--swagger-host/$HBOX_SWAGGER_HOST <string> (default: localhost:7745)
|
||||
--swagger-scheme/$HBOX_SWAGGER_SCHEME <string> (default: http)
|
||||
--demo/$HBOX_DEMO <bool>
|
||||
--debug-enabled/$HBOX_DEBUG_ENABLED <bool> (default: false)
|
||||
--debug-port/$HBOX_DEBUG_PORT <string> (default: 4000)
|
||||
--options-allow-registration/$HBOX_OPTIONS_ALLOW_REGISTRATION <bool> (default: true)
|
||||
--options-auto-increment-asset-id/$HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID <bool> (default: true)
|
||||
--options-currency-config/$HBOX_OPTIONS_CURRENCY_CONFIG <string>
|
||||
--help/-h display this help message
|
||||
```
|
||||
:::
|
||||
> [!TIP]
|
||||
> If you want other users to see your items and locations, they will need to sign up using your invite link, otherwise they will only see their own items. Go to the **Profile** section in the left navigation bar and under **User Profile**, click **Generate Invite Link**.
|
||||
@@ -21,6 +21,8 @@ Homebox provides the option to auto-set asset IDs, this is the default behavior.
|
||||
|
||||
Example ID: `000-001`
|
||||
|
||||
To search for an Asset ID: type `#` in the search bar followed by the ID you're searching for, e.g. `#000-001`.
|
||||
|
||||
Asset IDs are partially managed by Homebox, but have a flexible implementation to allow for unique use cases. IDs are non-unique at the database level, so there is nothing stopping a user from manually setting duplicate IDs for various items. There are two recommended approaches to manage Asset IDs:
|
||||
|
||||
### 1. Auto Incrementing IDs
|
||||
|
||||
@@ -11,6 +11,7 @@ module.exports = {
|
||||
"@nuxtjs/eslint-config-typescript",
|
||||
"plugin:vue/vue3-recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:tailwindcss/recommended",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
|
||||
1
frontend/.npmrc
Normal file
1
frontend/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
shamefully-hoist=true
|
||||
@@ -27,4 +27,4 @@
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #9B9B9B;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,23 +68,23 @@
|
||||
<LabelCreateModal v-model="modals.label" />
|
||||
<LocationCreateModal v-model="modals.location" />
|
||||
|
||||
<div class="bg-neutral absolute shadow-xl top-0 h-[20rem] max-h-96 -z-10 w-full"></div>
|
||||
<div class="absolute top-0 -z-10 h-80 max-h-96 w-full bg-neutral shadow-xl"></div>
|
||||
|
||||
<BaseContainer cmp="header" class="py-6 max-w-none">
|
||||
<BaseContainer cmp="header" class="max-w-none py-6">
|
||||
<BaseContainer>
|
||||
<NuxtLink to="/home">
|
||||
<h2 class="mt-1 text-4xl font-bold tracking-tight text-neutral-content sm:text-5xl lg:text-6xl flex">
|
||||
<h2 class="mt-1 flex text-4xl font-bold tracking-tight text-neutral-content sm:text-5xl lg:text-6xl">
|
||||
HomeB
|
||||
<AppLogo class="w-12 -mb-4" />
|
||||
<AppLogo class="-mb-4 w-12" />
|
||||
x
|
||||
</h2>
|
||||
</NuxtLink>
|
||||
<div class="ml-1 mt-2 text-lg text-neutral-content/75 space-x-2">
|
||||
<div class="ml-1 mt-2 space-x-2 text-lg text-neutral-content/75">
|
||||
<template v-for="link in links">
|
||||
<NuxtLink
|
||||
v-if="!link.action"
|
||||
:key="link.name"
|
||||
class="hover:text-base-content transition-color duration-200 italic"
|
||||
class="italic transition-colors duration-200 hover:text-base-content"
|
||||
:to="link.href"
|
||||
>
|
||||
{{ link.name }}
|
||||
@@ -93,7 +93,7 @@
|
||||
v-else
|
||||
:key="link.name + 'link'"
|
||||
for="location-form-modal"
|
||||
class="hover:text-base-content transition-color duration-200 italic"
|
||||
class="italic transition-colors duration-200 hover:text-base-content"
|
||||
@click="link.action"
|
||||
>
|
||||
{{ link.name }}
|
||||
@@ -101,15 +101,15 @@
|
||||
<span v-if="!link.last" :key="link.name"> / </span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex mt-6">
|
||||
<div class="mt-6 flex">
|
||||
<div class="dropdown">
|
||||
<label tabindex="0" class="btn btn-primary btn-sm">
|
||||
<span>
|
||||
<MdiPlus class="mr-1 -ml-1" />
|
||||
<MdiPlus class="-ml-1 mr-1" />
|
||||
</span>
|
||||
Create
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<ul tabindex="0" class="dropdown-content menu rounded-box w-52 bg-base-100 p-2 shadow">
|
||||
<li v-for="btn in dropdown" :key="btn.name">
|
||||
<button @click="btn.action">
|
||||
{{ btn.name }}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
<template>
|
||||
<BaseModal v-model="dialog">
|
||||
<template #title> Import CSV File </template>
|
||||
<template #title> {{ $t("components.app.import_dialog.title") }} </template>
|
||||
<p>
|
||||
Import a CSV file containing your items, labels, and locations. See documentation for more information on the
|
||||
required format.
|
||||
{{ $t("components.app.import_dialog.description") }}
|
||||
</p>
|
||||
<div class="alert alert-warning shadow-lg mt-4">
|
||||
<div class="alert alert-warning mt-4 shadow-lg">
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current flex-shrink-0 h-6 w-6 mb-auto"
|
||||
class="mb-auto size-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
@@ -21,8 +20,7 @@
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm">
|
||||
Behavior for imports with existing import_refs has changed. If an import_ref is present in the CSV file, the
|
||||
item will be updated with the values in the CSV file.
|
||||
{{ $t("components.app.import_dialog.change_warning") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,16 +30,16 @@
|
||||
<input ref="importRef" type="file" class="hidden" accept=".csv,.tsv" @change="setFile" />
|
||||
|
||||
<BaseButton type="button" @click="uploadCsv">
|
||||
<MdiUpload class="h-5 w-5 mr-2" />
|
||||
Upload
|
||||
<MdiUpload class="mr-2 size-5" />
|
||||
{{ $t("components.app.import_dialog.upload") }}
|
||||
</BaseButton>
|
||||
<p class="text-center pt-4 -mb-5">
|
||||
<p class="-mb-5 pt-4 text-center">
|
||||
{{ importCsv?.name }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<BaseButton type="submit" :disabled="!importCsv"> Submit </BaseButton>
|
||||
<BaseButton type="submit" :disabled="!importCsv"> {{ $t("global.submit") }} </BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="force-above fixed top-2 right-2 w-[300px]">
|
||||
<div class="fixed right-2 top-2 z-[9999] w-[300px]">
|
||||
<TransitionGroup name="notify" tag="div">
|
||||
<div
|
||||
v-for="(notify, index) in notifications.slice(0, 4)"
|
||||
@@ -14,14 +14,14 @@
|
||||
>
|
||||
<div class="flex gap-1">
|
||||
<template v-if="notify.type == 'success'">
|
||||
<MdiCheckboxMarkedCircle class="h-5 w-5" />
|
||||
<MdiCheckboxMarkedCircle class="size-5" />
|
||||
</template>
|
||||
<template v-if="notify.type == 'info'">
|
||||
<MdiInformationSlabCircle class="h-5 w-5" />
|
||||
<MdiInformationSlabCircle class="size-5" />
|
||||
</template>
|
||||
|
||||
<template v-if="notify.type == 'error'">
|
||||
<MdiAlert class="h-5 w-5" />
|
||||
<MdiAlert class="size-5" />
|
||||
</template>
|
||||
{{ notify.message }}
|
||||
</div>
|
||||
@@ -41,10 +41,6 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.force-above {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.notify-move,
|
||||
.notify-enter-active,
|
||||
.notify-leave-active {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="card rounded-lg bg-base-100 shadow-xl">
|
||||
<div v-if="$slots.title" class="px-4 py-5 sm:px-6">
|
||||
<component :is="collapsable ? 'button' : 'div'" v-on="collapsable ? { click: toggle } : {}">
|
||||
<h3 class="text-lg font-medium leading-6 flex items-center">
|
||||
<h3 class="flex items-center text-lg font-medium leading-6">
|
||||
<slot name="title"></slot>
|
||||
<template v-if="collapsable">
|
||||
<span class="ml-2 swap swap-rotate" :class="`${collapsed ? 'swap-active' : ''}`">
|
||||
<MdiChevronRight class="h-6 w-6 swap-on" />
|
||||
<MdiChevronDown class="h-6 w-6 swap-off" />
|
||||
<span class="swap swap-rotate ml-2" :class="`${collapsed ? 'swap-active' : ''}`">
|
||||
<MdiChevronRight class="swap-on size-6" />
|
||||
<MdiChevronDown class="swap-off size-6" />
|
||||
</span>
|
||||
</template>
|
||||
</h3>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="cmp" class="container max-w-6xl mx-auto px-3">
|
||||
<component :is="cmp" class="container mx-auto mt-10 max-w-6xl px-3">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="z-[999]">
|
||||
<input :id="modalId" v-model="modal" type="checkbox" class="modal-toggle" />
|
||||
<div class="modal modal-bottom sm:modal-middle overflow-visible">
|
||||
<div class="modal-box overflow-visible relative">
|
||||
<button :for="modalId" class="btn btn-sm btn-circle absolute right-2 top-2" @click="close">✕</button>
|
||||
<div class="modal modal-bottom overflow-visible sm:modal-middle">
|
||||
<div class="modal-box relative overflow-auto">
|
||||
<button :for="modalId" class="btn btn-circle btn-sm absolute right-2 top-2" @click="close">✕</button>
|
||||
|
||||
<h3 class="font-bold text-lg">
|
||||
<h3 class="text-lg font-bold">
|
||||
<slot name="title"></slot>
|
||||
</h3>
|
||||
<slot> </slot>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="pb-3">
|
||||
<h3
|
||||
class="text-3xl font-bold tracking-tight flex items-center"
|
||||
class="flex items-center text-3xl font-bold tracking-tight"
|
||||
:class="{
|
||||
'text-neutral-content': dark,
|
||||
'text-content': !dark,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-10 py-6">
|
||||
<div class="grid grid-cols-1 gap-10 py-6 md:grid-cols-4">
|
||||
<div class="col-span-3">
|
||||
<h4 class="mb-1 text-lg font-semibold">
|
||||
<slot name="title"></slot>
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<input
|
||||
v-model="internalSearch"
|
||||
tabindex="0"
|
||||
class="input w-full items-center flex flex-wrap border border-gray-400 rounded-lg"
|
||||
class="input flex w-full flex-wrap items-center rounded-lg border border-gray-400"
|
||||
@keyup.enter="selectFirst"
|
||||
/>
|
||||
<button
|
||||
v-if="!!modelValue && Object.keys(modelValue).length !== 0"
|
||||
style="transform: translateY(-50%)"
|
||||
class="top-1/2 absolute right-2 btn btn-xs btn-circle no-animation"
|
||||
class="btn btn-circle btn-xs no-animation absolute right-2 top-1/2"
|
||||
@click="clear"
|
||||
>
|
||||
x
|
||||
@@ -23,7 +23,7 @@
|
||||
<ul
|
||||
tabindex="0"
|
||||
style="display: inline"
|
||||
class="dropdown-content mb-1 menu shadow border border-gray-400 rounded bg-base-100 w-full z-[9999] max-h-60 overflow-y-scroll"
|
||||
class="dropdown-content menu z-[9999] mb-1 max-h-60 w-full overflow-y-scroll rounded border border-gray-400 bg-base-100 shadow"
|
||||
>
|
||||
<li v-for="(obj, idx) in filtered" :key="idx">
|
||||
<div type="button" @click="select(obj)">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="relative">
|
||||
<ComboboxInput
|
||||
:display-value="i => extractDisplay(i as SupportValues)"
|
||||
class="w-full input input-bordered"
|
||||
class="input input-bordered w-full"
|
||||
@change="search = $event.target.value"
|
||||
/>
|
||||
<button
|
||||
@@ -16,14 +16,14 @@
|
||||
class="absolute inset-y-0 right-6 flex items-center rounded-r-md px-2 focus:outline-none"
|
||||
@click="clear"
|
||||
>
|
||||
<MdiClose class="w-5 h-5" />
|
||||
<MdiClose class="size-5" />
|
||||
</button>
|
||||
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none">
|
||||
<MdiChevronDown class="w-5 h-5" />
|
||||
<MdiChevronDown class="size-5" />
|
||||
</ComboboxButton>
|
||||
<ComboboxOptions
|
||||
v-if="computedItems.length > 0"
|
||||
class="absolute dropdown-content z-10 mt-2 max-h-60 w-full overflow-auto rounded-md card bg-base-100 border border-gray-400"
|
||||
class="card dropdown-content absolute z-10 mt-2 max-h-60 w-full overflow-auto rounded-md border border-gray-400 bg-base-100"
|
||||
>
|
||||
<ComboboxOption
|
||||
v-for="item in computedItems"
|
||||
@@ -34,7 +34,7 @@
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9 duration-75 ease-in-out transition-colors',
|
||||
'relative cursor-default select-none py-2 pl-3 pr-9 transition-colors duration-75 ease-in-out',
|
||||
active ? 'bg-primary text-primary-content' : 'text-base-content',
|
||||
]"
|
||||
>
|
||||
@@ -45,11 +45,11 @@
|
||||
<span
|
||||
v-if="selected"
|
||||
:class="[
|
||||
'absolute inset-y-0 right-0 flex text-primary items-center pr-4',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4 text-primary',
|
||||
active ? 'text-primary-content' : 'bg-primary',
|
||||
]"
|
||||
>
|
||||
<MdiCheck class="h-5 w-5" aria-hidden="true" />
|
||||
<MdiCheck class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</slot>
|
||||
</li>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div v-if="!inline" class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text"> {{ label }}</span>
|
||||
<span class="label-text"> {{ label }} </span>
|
||||
</label>
|
||||
<VueDatePicker v-model="selected" :enable-time-picker="false" clearable :dark="isDark" />
|
||||
<VueDatePicker v-model="selected" :enable-time-picker="false" clearable :dark="isDark" :teleport="true" />
|
||||
</div>
|
||||
<div v-else class="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
|
||||
<label class="label">
|
||||
<span class="label-text"> {{ label }} </span>
|
||||
</label>
|
||||
<VueDatePicker v-model="selected" :enable-time-picker="false" clearable :dark="isDark" />
|
||||
<VueDatePicker v-model="selected" :enable-time-picker="false" clearable :dark="isDark" :teleport="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -4,33 +4,48 @@
|
||||
<span class="label-text">{{ label }}</span>
|
||||
</label>
|
||||
<div class="dropdown dropdown-top sm:dropdown-end">
|
||||
<div tabindex="0" class="w-full min-h-[48px] flex gap-2 p-4 flex-wrap border border-gray-400 rounded-lg">
|
||||
<div tabindex="0" class="flex min-h-[48px] w-full flex-wrap gap-2 rounded-lg border border-gray-400 p-4">
|
||||
<span v-for="itm in value" :key="name != '' ? itm[name] : itm" class="badge">
|
||||
{{ name != "" ? itm[name] : itm }}
|
||||
</span>
|
||||
<button
|
||||
v-if="value.length > 0"
|
||||
type="button"
|
||||
class="absolute inset-y-0 right-6 flex items-center rounded-r-md px-2 focus:outline-none"
|
||||
@click="clear"
|
||||
>
|
||||
<MdiClose class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<ul
|
||||
<div
|
||||
tabindex="0"
|
||||
style="display: inline"
|
||||
class="dropdown-content mb-1 menu shadow border border-gray-400 rounded bg-base-100 w-full z-[9999] max-h-60 overflow-y-scroll"
|
||||
class="dropdown-content menu z-[9999] mb-1 w-full rounded border border-gray-400 bg-base-100 shadow"
|
||||
>
|
||||
<li
|
||||
v-for="(obj, idx) in items"
|
||||
:key="idx"
|
||||
:class="{
|
||||
bordered: selected[idx],
|
||||
}"
|
||||
>
|
||||
<button type="button" @click="toggle(idx)">
|
||||
{{ name != "" ? obj[name] : obj }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="m-2">
|
||||
<input v-model="search" placeholder="Search…" class="input input-bordered input-sm w-full" />
|
||||
</div>
|
||||
<ul class="max-h-60 overflow-y-scroll">
|
||||
<li
|
||||
v-for="(obj, idx) in filteredItems"
|
||||
:key="idx"
|
||||
:class="{
|
||||
bordered: selected.includes(obj[props.uniqueField]),
|
||||
}"
|
||||
>
|
||||
<button type="button" @click="toggle(obj[props.uniqueField])">
|
||||
{{ name != "" ? obj[name] : obj }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MdiClose from "~icons/mdi/close";
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
const props = defineProps({
|
||||
label: {
|
||||
@@ -49,6 +64,10 @@
|
||||
type: String,
|
||||
default: "name",
|
||||
},
|
||||
uniqueField: {
|
||||
type: String,
|
||||
default: "id",
|
||||
},
|
||||
selectFirst: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -57,19 +76,30 @@
|
||||
|
||||
const value = useVModel(props, "modelValue", emit);
|
||||
|
||||
const selected = computed<Record<number, boolean>>(() => {
|
||||
const obj: Record<number, boolean> = {};
|
||||
value.value.forEach(itm => {
|
||||
const idx = props.items.findIndex(item => item[props.name] === itm.name);
|
||||
obj[idx] = true;
|
||||
const search = ref("");
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
if (!search.value) {
|
||||
return props.items;
|
||||
}
|
||||
|
||||
return props.items.filter(item => {
|
||||
return item[props.name].toLowerCase().includes(search.value.toLowerCase());
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
|
||||
function toggle(index: number) {
|
||||
const item = props.items[index];
|
||||
if (selected.value[index]) {
|
||||
value.value = value.value.filter(itm => itm.name !== item.name);
|
||||
function clear() {
|
||||
value.value = [];
|
||||
}
|
||||
|
||||
const selected = computed<string[]>(() => {
|
||||
return value.value.map(itm => itm[props.uniqueField]);
|
||||
});
|
||||
|
||||
function toggle(uniqueField: string) {
|
||||
const item = props.items.find(itm => itm[props.uniqueField] === uniqueField);
|
||||
if (selected.value.includes(item[props.uniqueField])) {
|
||||
value.value = value.value.filter(itm => itm[props.uniqueField] !== item[props.uniqueField]);
|
||||
} else {
|
||||
value.value = [...value.value, item];
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<FormTextField v-model="value" placeholder="Password" :label="label" :type="inputType"> </FormTextField>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex p-1 ml-1 justify-center mt-auto mb-3 tooltip absolute top-11 right-3"
|
||||
class="tooltip absolute right-3 top-11 mb-3 ml-1 mt-auto inline-flex justify-center p-1"
|
||||
data-tip="Toggle Password Show"
|
||||
@click="toggle()"
|
||||
>
|
||||
<MdiEye name="mdi-eye" class="h-5 w-5" />
|
||||
<MdiEye name="mdi-eye" class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,21 +2,35 @@
|
||||
<div v-if="!inline" class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
<span
|
||||
:class="{
|
||||
'text-red-600':
|
||||
typeof value === 'string' &&
|
||||
((maxLength && value.length > maxLength) || (minLength && value.length < minLength)),
|
||||
}"
|
||||
>
|
||||
{{ typeof value === "string" && (maxLength || minLength) ? `${value.length}/${maxLength}` : "" }}
|
||||
</span>
|
||||
</label>
|
||||
<textarea ref="el" v-model="value" class="textarea w-full textarea-bordered h-28" :placeholder="placeholder" />
|
||||
<label v-if="limit" class="label">
|
||||
<span class="label-text-alt"></span>
|
||||
<span class="label-text-alt"> {{ valueLen }}/{{ limit }}</span>
|
||||
</label>
|
||||
<textarea ref="el" v-model="value" class="textarea textarea-bordered h-28 w-full" :placeholder="placeholder" />
|
||||
</div>
|
||||
<div v-else class="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
|
||||
<label class="label">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
<span
|
||||
:class="{
|
||||
'text-red-600':
|
||||
typeof value === 'string' &&
|
||||
((maxLength && value.length > maxLength) || (minLength && value.length < minLength)),
|
||||
}"
|
||||
>
|
||||
{{ typeof value === "string" && (maxLength || minLength) ? `${value.length}/${maxLength}` : "" }}
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
ref="el"
|
||||
v-model="value"
|
||||
class="textarea textarea-bordered w-full col-span-3 mt-3 h-28"
|
||||
class="textarea textarea-bordered col-span-3 mt-3 h-28 w-full"
|
||||
auto-grow
|
||||
:placeholder="placeholder"
|
||||
auto-height
|
||||
@@ -39,10 +53,6 @@
|
||||
type: String,
|
||||
default: "text",
|
||||
},
|
||||
limit: {
|
||||
type: [Number, String],
|
||||
default: null,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
@@ -51,6 +61,14 @@
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxLength: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
minLength: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const el = ref();
|
||||
|
||||
@@ -1,15 +1,46 @@
|
||||
<template>
|
||||
<div v-if="!inline" class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
<span class="label-text"> {{ label }} </span>
|
||||
<span
|
||||
:class="{
|
||||
'text-red-600':
|
||||
typeof value === 'string' &&
|
||||
((maxLength && value.length > maxLength) || (minLength && value.length < minLength)),
|
||||
}"
|
||||
>
|
||||
{{ typeof value === "string" && (maxLength || minLength) ? `${value.length}/${maxLength}` : "" }}
|
||||
</span>
|
||||
</label>
|
||||
<input ref="input" v-model="value" :placeholder="placeholder" :type="type" class="input input-bordered w-full" />
|
||||
<input
|
||||
ref="input"
|
||||
v-model="value"
|
||||
:placeholder="placeholder"
|
||||
:type="type"
|
||||
:required="required"
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
|
||||
<label class="label">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
<span class="label-text"> {{ label }} </span>
|
||||
<span
|
||||
:class="{
|
||||
'text-red-600':
|
||||
typeof value === 'string' &&
|
||||
((maxLength && value.length > maxLength) || (minLength && value.length < minLength)),
|
||||
}"
|
||||
>
|
||||
{{ typeof value === "string" && (maxLength || minLength) ? `${value.length}/${maxLength}` : "" }}
|
||||
</span>
|
||||
</label>
|
||||
<input v-model="value" :placeholder="placeholder" class="input input-bordered col-span-3 w-full mt-2" />
|
||||
<input
|
||||
v-model="value"
|
||||
:placeholder="placeholder"
|
||||
:type="type"
|
||||
:required="required"
|
||||
class="input input-bordered col-span-3 mt-2 w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -23,6 +54,10 @@
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
required: {
|
||||
type: [Boolean],
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
@@ -39,6 +74,14 @@
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
maxLength: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
minLength: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const input = ref<HTMLElement | null>(null);
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
class="flex items-center justify-between py-3 pl-3 pr-4 text-sm"
|
||||
>
|
||||
<div class="flex w-0 flex-1 items-center">
|
||||
<MdiPaperclip class="h-5 w-5 flex-shrink-0 text-gray-400" aria-hidden="true" />
|
||||
<MdiPaperclip class="size-5 shrink-0 text-gray-400" aria-hidden="true" />
|
||||
<span class="ml-2 w-0 flex-1 truncate"> {{ attachment.document.title }}</span>
|
||||
</div>
|
||||
<div class="ml-4 flex-shrink-0">
|
||||
<div class="ml-4 shrink-0">
|
||||
<a class="tooltip mr-2" data-tip="Download" :href="attachmentURL(attachment.id)" target="_blank">
|
||||
<MdiDownload class="h-5 w-5" />
|
||||
<MdiDownload class="size-5" />
|
||||
</a>
|
||||
<a class="tooltip" data-tip="Open" :href="attachmentURL(attachment.id)" target="_blank">
|
||||
<MdiOpenInNew class="h-5 w-5" />
|
||||
<MdiOpenInNew class="size-5" />
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
<template>
|
||||
<NuxtLink class="group card rounded-md border border-gray-300" :to="`/item/${item.id}`">
|
||||
<div class="relative h-[200px]">
|
||||
<img v-if="imageUrl" class="h-[200px] w-full object-cover rounded-t shadow-sm border-gray-300" :src="imageUrl" />
|
||||
<img v-if="imageUrl" class="h-[200px] w-full rounded-t border-gray-300 object-cover shadow-sm" :src="imageUrl" />
|
||||
<div class="absolute bottom-1 left-1">
|
||||
<NuxtLink
|
||||
v-if="item.location"
|
||||
class="text-sm hover:link badge shadow-md rounded-md"
|
||||
class="badge rounded-md text-sm shadow-md hover:link"
|
||||
:to="`/location/${item.location.id}`"
|
||||
>
|
||||
{{ item.location.name }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-b p-4 pt-2 flex-grow col-span-4 flex flex-col gap-y-1 bg-base-100">
|
||||
<h2 class="text-lg font-bold two-line">{{ item.name }}</h2>
|
||||
<div class="col-span-4 flex grow flex-col gap-y-1 rounded-b bg-base-100 p-4 pt-2">
|
||||
<h2 class="line-clamp-2 text-ellipsis text-lg font-bold">{{ item.name }}</h2>
|
||||
<div class="divider my-0"></div>
|
||||
<div class="flex justify-between gap-2">
|
||||
<div class="flex gap-2">
|
||||
<div v-if="item.insured" class="tooltip z-10" data-tip="Insured">
|
||||
<MdiShieldCheck class="h-5 w-5 text-primary" />
|
||||
<MdiShieldCheck class="size-5 text-primary" />
|
||||
</div>
|
||||
<div v-if="item.archived" class="tooltip z-10" data-tip="Archived">
|
||||
<MdiArchive class="size-5 text-red-700" />
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<div class="tooltip" data-tip="Quantity">
|
||||
<span class="badge h-5 w-5 badge-primary badge-sm text-xs">
|
||||
<span class="badge badge-primary badge-sm size-5 text-xs">
|
||||
{{ item.quantity }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Markdown class="mb-2 text-clip three-line" :source="item.description" />
|
||||
<div class="flex gap-2 flex-wrap -mr-1 mt-auto justify-end">
|
||||
<Markdown class="mb-2 line-clamp-3 text-ellipsis" :source="item.description" />
|
||||
<div class="-mr-1 mt-auto flex flex-wrap justify-end gap-2">
|
||||
<LabelChip v-for="label in top3" :key="label.id" :label="label" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,6 +40,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { ItemOut, ItemSummary } from "~~/lib/api/types/data-contracts";
|
||||
import MdiShieldCheck from "~icons/mdi/shield-check";
|
||||
import MdiArchive from "~icons/mdi/archive";
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
@@ -59,22 +64,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
.three-line {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.two-line {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
</style>
|
||||
<style lang="css"></style>
|
||||
|
||||
@@ -1,34 +1,60 @@
|
||||
<template>
|
||||
<BaseModal v-model="modal">
|
||||
<template #title> Create Item </template>
|
||||
<template #title> {{ $t("components.item.create_modal.title") }} </template>
|
||||
<form @submit.prevent="create()">
|
||||
<LocationSelector v-model="form.location" />
|
||||
<FormTextField ref="nameInput" v-model="form.name" :trigger-focus="focused" :autofocus="true" label="Item Name" />
|
||||
<FormTextArea v-model="form.description" label="Item Description" />
|
||||
<FormTextField
|
||||
ref="nameInput"
|
||||
v-model="form.name"
|
||||
:trigger-focus="focused"
|
||||
:autofocus="true"
|
||||
label="Item Name"
|
||||
:max-length="255"
|
||||
:min-length="1"
|
||||
/>
|
||||
<FormTextArea v-model="form.description" label="Item Description" :max-length="1000" />
|
||||
<FormMultiselect v-model="form.labels" label="Labels" :items="labels ?? []" />
|
||||
<div class="modal-action">
|
||||
<div class="flex justify-center">
|
||||
|
||||
<div class="modal-action mb-6">
|
||||
<div>
|
||||
<label for="photo" class="btn">{{ $t("components.item.create_modal.photo_button") }}</label>
|
||||
<input id="photo" class="hidden" type="file" accept="image/png,image/jpeg,image/gif" @change="previewImage" />
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<div>
|
||||
<BaseButton class="rounded-r-none" :loading="loading" type="submit">
|
||||
<template #icon>
|
||||
<MdiPackageVariant class="swap-off h-5 w-5" />
|
||||
<MdiPackageVariantClosed class="swap-on h-5 w-5" />
|
||||
<MdiPackageVariant class="swap-off size-5" />
|
||||
<MdiPackageVariantClosed class="swap-on size-5" />
|
||||
</template>
|
||||
Create
|
||||
{{ $t("global.create") }}
|
||||
</BaseButton>
|
||||
<div class="dropdown dropdown-top">
|
||||
<label tabindex="0" class="btn rounded-l-none rounded-r-xl">
|
||||
<MdiChevronDown class="h-5 w-5" name="mdi-chevron-down" />
|
||||
<MdiChevronDown class="size-5" name="mdi-chevron-down" />
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-64 right-0">
|
||||
<ul tabindex="0" class="dropdown-content menu rounded-box right-0 w-64 bg-base-100 p-2 shadow">
|
||||
<li>
|
||||
<button type="button" @click="create(false)">Create and Add Another</button>
|
||||
<button type="button" @click="create(false)">{{ $t("global.create_and_add") }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- photo preview area is AFTER the create button, to avoid pushing the button below the screen on small displays -->
|
||||
<div class="border-t border-gray-300 p-4">
|
||||
<template v-if="form.preview">
|
||||
<p class="mb-0">File name: {{ form.photo?.name }}</p>
|
||||
<img
|
||||
:src="form.preview"
|
||||
class="h-[100px] w-full rounded-t border-gray-300 object-cover shadow-sm"
|
||||
alt="Uploaded Photo"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</form>
|
||||
<p class="text-sm text-center mt-4">
|
||||
<p class="mt-4 text-center text-sm">
|
||||
use <kbd class="kbd kbd-xs">Shift</kbd> + <kbd class="kbd kbd-xs"> Enter </kbd> to create and add another
|
||||
</p>
|
||||
</BaseModal>
|
||||
@@ -41,6 +67,7 @@
|
||||
import MdiPackageVariant from "~icons/mdi/package-variant";
|
||||
import MdiPackageVariantClosed from "~icons/mdi/package-variant-closed";
|
||||
import MdiChevronDown from "~icons/mdi/chevron-down";
|
||||
import { AttachmentTypes } from "~~/lib/api/types/non-generated";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -85,10 +112,25 @@
|
||||
description: "",
|
||||
color: "", // Future!
|
||||
labels: [] as LabelOut[],
|
||||
preview: null as string | null,
|
||||
photo: null as File | null,
|
||||
});
|
||||
|
||||
const { shift } = useMagicKeys();
|
||||
|
||||
function previewImage(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
form.preview = e.target?.result as string;
|
||||
};
|
||||
const file = input.files[0];
|
||||
form.photo = file;
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
whenever(
|
||||
() => modal.value,
|
||||
() => {
|
||||
@@ -112,6 +154,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (loading.value) {
|
||||
toast.error("Already creating an item");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
if (shift.value) {
|
||||
close = false;
|
||||
}
|
||||
@@ -127,16 +176,32 @@
|
||||
const { error, data } = await api.items.create(out);
|
||||
loading.value = false;
|
||||
if (error) {
|
||||
loading.value = false;
|
||||
toast.error("Couldn't create item");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Item created");
|
||||
|
||||
// if the photo was provided, upload it
|
||||
if (form.photo) {
|
||||
const { error } = await api.items.attachments.add(data.id, form.photo, form.photo.name, AttachmentTypes.Photo);
|
||||
|
||||
if (error) {
|
||||
loading.value = false;
|
||||
toast.error("Failed to upload Photo");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Photo uploaded");
|
||||
}
|
||||
|
||||
// Reset
|
||||
form.name = "";
|
||||
form.description = "";
|
||||
form.color = "";
|
||||
form.preview = null;
|
||||
form.photo = null;
|
||||
focused.value = false;
|
||||
loading.value = false;
|
||||
|
||||
|
||||
@@ -28,24 +28,24 @@
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-2 flex justify-between items-center">
|
||||
Items
|
||||
<BaseSectionHeader class="mb-2 flex items-center justify-between">
|
||||
{{ $t("components.item.view.selectable.items") }}
|
||||
<template #description>
|
||||
<div v-if="!viewSet" class="dropdown dropdown-hover dropdown-left">
|
||||
<div v-if="!viewSet" class="dropdown dropdown-left dropdown-hover">
|
||||
<label tabindex="0" class="btn btn-ghost m-1">
|
||||
<MdiDotsVertical class="h-7 w-7" />
|
||||
<MdiDotsVertical class="size-7" />
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-32">
|
||||
<ul tabindex="0" class="dropdown-content menu rounded-box w-32 bg-base-100 p-2 shadow">
|
||||
<li>
|
||||
<button @click="setViewPreference('card')">
|
||||
<MdiCardTextOutline class="h-5 w-5" />
|
||||
Card
|
||||
<MdiCardTextOutline class="size-5" />
|
||||
{{ $t("components.item.view.selectable.card") }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button @click="setViewPreference('table')">
|
||||
<MdiTable class="h-5 w-5" />
|
||||
Table
|
||||
<MdiTable class="size-5" />
|
||||
{{ $t("components.item.view.selectable.table") }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -57,9 +57,9 @@
|
||||
<ItemViewTable :items="items" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
<ItemCard v-for="item in items" :key="item.id" :item="item" />
|
||||
<div class="first:block hidden text-lg">No Items to Display</div>
|
||||
<div class="hidden first:block">{{ $t("components.item.view.selectable.no_items") }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
@@ -5,6 +5,8 @@ export type TableHeader = {
|
||||
value: keyof ItemSummary;
|
||||
sortable?: boolean;
|
||||
align?: "left" | "center" | "right";
|
||||
enabled: boolean;
|
||||
type?: "price" | "boolean" | "name" | "location" | "date";
|
||||
};
|
||||
|
||||
export type TableData = Record<string, any>;
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="h in headers"
|
||||
v-for="h in headers.filter(h => h.enabled)"
|
||||
:key="h.value"
|
||||
class="text-no-transform text-sm bg-neutral text-neutral-content cursor-pointer"
|
||||
class="text-no-transform cursor-pointer bg-neutral text-sm text-neutral-content"
|
||||
@click="sortBy(h.value)"
|
||||
>
|
||||
<div
|
||||
@@ -24,8 +24,8 @@
|
||||
:class="`inline-flex ${sortByProperty === h.value ? '' : 'opacity-0'}`"
|
||||
>
|
||||
<span class="swap swap-rotate" :class="{ 'swap-active': pagination.descending }">
|
||||
<MdiArrowDown class="swap-on h-5 w-5" />
|
||||
<MdiArrowUp class="swap-off h-5 w-5" />
|
||||
<MdiArrowDown class="swap-on size-5" />
|
||||
<MdiArrowUp class="swap-off size-5" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,7 +35,7 @@
|
||||
<tbody>
|
||||
<tr v-for="(d, i) in data" :key="d.id" class="hover cursor-pointer" @click="navigateTo(`/item/${d.id}`)">
|
||||
<td
|
||||
v-for="h in headers"
|
||||
v-for="h in headers.filter(h => h.enabled)"
|
||||
:key="`${h.value}-${i}`"
|
||||
class="bg-base-100"
|
||||
:class="{
|
||||
@@ -44,17 +44,25 @@
|
||||
'text-left': h.align === 'left',
|
||||
}"
|
||||
>
|
||||
<template v-if="cell(h) === 'cell-name'">
|
||||
<template v-if="h.type === 'name'">
|
||||
<NuxtLink class="hover" :to="`/item/${d.id}`">
|
||||
{{ d.name }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-else-if="cell(h) === 'cell-purchasePrice'">
|
||||
<template v-else-if="h.type === 'price'">
|
||||
<Currency :amount="d.purchasePrice" />
|
||||
</template>
|
||||
<template v-else-if="cell(h) === 'cell-insured'">
|
||||
<MdiCheck v-if="d.insured" class="text-green-500 h-5 w-5 inline" />
|
||||
<MdiClose v-else class="text-red-500 h-5 w-5 inline" />
|
||||
<template v-else-if="h.type === 'boolean'">
|
||||
<MdiCheck v-if="d.insured" class="inline size-5 text-green-500" />
|
||||
<MdiClose v-else class="inline size-5 text-red-500" />
|
||||
</template>
|
||||
<template v-else-if="h.type === 'location'">
|
||||
<NuxtLink v-if="d.location" class="hover:link" :to="`/location/${d.location.id}`">
|
||||
{{ d.location.name }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-else-if="h.type === 'date'">
|
||||
<DateTime :date="d[h.value]" datetime-type="date" />
|
||||
</template>
|
||||
<slot v-else :name="cell(h)" v-bind="{ item: d }">
|
||||
{{ extractValue(d, h.value) }}
|
||||
@@ -63,7 +71,55 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="hasPrev || hasNext" class="border-t p-3 justify-end flex">
|
||||
<div
|
||||
class="flex items-center justify-end gap-3 border-t p-3"
|
||||
:class="{
|
||||
hidden: disableControls,
|
||||
}"
|
||||
>
|
||||
<div class="dropdown dropdown-top dropdown-hover">
|
||||
<label tabindex="0" class="btn btn-square btn-outline btn-sm m-1">
|
||||
<MdiTableCog />
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content rounded-box flex w-64 flex-col gap-2 bg-base-100 p-2 pl-3 shadow">
|
||||
<li>Headers:</li>
|
||||
<li v-for="(h, i) in headers" class="flex flex-row items-center gap-1">
|
||||
<button
|
||||
class="btn btn-square btn-ghost btn-xs"
|
||||
:class="{
|
||||
'btn-disabled': i === 0,
|
||||
}"
|
||||
@click="moveHeader(i, i - 1)"
|
||||
>
|
||||
<MdiArrowUp />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-square btn-ghost btn-xs"
|
||||
:class="{
|
||||
'btn-disabled': i === headers.length - 1,
|
||||
}"
|
||||
@click="moveHeader(i, i + 1)"
|
||||
>
|
||||
<MdiArrowDown />
|
||||
</button>
|
||||
<input
|
||||
:id="h.value"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-primary"
|
||||
:checked="h.enabled"
|
||||
@change="toggleHeader(h.value)"
|
||||
/>
|
||||
<label class="label-text" :for="h.value"> {{ h.text }} </label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="hidden md:block">Rows per page</div>
|
||||
<select v-model.number="pagination.rowsPerPage" class="select select-primary select-sm">
|
||||
<option :value="10">10</option>
|
||||
<option :value="25">25</option>
|
||||
<option :value="50">50</option>
|
||||
<option :value="100">100</option>
|
||||
</select>
|
||||
<div class="btn-group">
|
||||
<button :disabled="!hasPrev" class="btn btn-sm" @click="prev()">«</button>
|
||||
<button class="btn btn-sm">Page {{ pagination.page }}</button>
|
||||
@@ -80,30 +136,67 @@
|
||||
import MdiArrowUp from "~icons/mdi/arrow-up";
|
||||
import MdiCheck from "~icons/mdi/check";
|
||||
import MdiClose from "~icons/mdi/close";
|
||||
import MdiTableCog from "~icons/mdi/table-cog";
|
||||
|
||||
type Props = {
|
||||
items: ItemSummary[];
|
||||
disableControls?: boolean;
|
||||
};
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const sortByProperty = ref<keyof ItemSummary | "">("");
|
||||
|
||||
const headers = computed<TableHeader[]>(() => {
|
||||
return [
|
||||
{ text: "Name", value: "name" },
|
||||
{ text: "Quantity", value: "quantity", align: "center" },
|
||||
{ text: "Insured", value: "insured", align: "center" },
|
||||
{ text: "Price", value: "purchasePrice" },
|
||||
] as TableHeader[];
|
||||
});
|
||||
const preferences = useViewPreferences();
|
||||
|
||||
const defaultHeaders = [
|
||||
{ text: "Name", value: "name", enabled: true, type: "name" },
|
||||
{ text: "Quantity", value: "quantity", align: "center", enabled: true },
|
||||
{ text: "Insured", value: "insured", align: "center", enabled: true, type: "boolean" },
|
||||
{ text: "Price", value: "purchasePrice", align: "center", enabled: true, type: "price" },
|
||||
{ text: "Location", value: "location", align: "center", enabled: false, type: "location" },
|
||||
{ text: "Archived", value: "archived", align: "center", enabled: false, type: "boolean" },
|
||||
{ text: "Created At", value: "createdAt", align: "center", enabled: false, type: "date" },
|
||||
{ text: "Updated At", value: "updatedAt", align: "center", enabled: false, type: "date" },
|
||||
] satisfies TableHeader[];
|
||||
|
||||
const headers = ref<TableHeader[]>(
|
||||
(preferences.value.tableHeaders ?? []).concat(
|
||||
defaultHeaders.filter(h => !preferences.value.tableHeaders?.find(h2 => h2.value === h.value))
|
||||
)
|
||||
);
|
||||
|
||||
console.log(headers.value);
|
||||
|
||||
const toggleHeader = (value: string) => {
|
||||
const header = headers.value.find(h => h.value === value);
|
||||
if (header) {
|
||||
header.enabled = !header.enabled; // Toggle the 'enabled' state
|
||||
}
|
||||
|
||||
preferences.value.tableHeaders = headers.value;
|
||||
};
|
||||
const moveHeader = (from: number, to: number) => {
|
||||
const header = headers.value[from];
|
||||
headers.value.splice(from, 1);
|
||||
headers.value.splice(to, 0, header);
|
||||
|
||||
preferences.value.tableHeaders = headers.value;
|
||||
};
|
||||
|
||||
const pagination = reactive({
|
||||
descending: false,
|
||||
page: 1,
|
||||
rowsPerPage: 10,
|
||||
rowsPerPage: preferences.value.itemsPerTablePage,
|
||||
rowsNumber: 0,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => pagination.rowsPerPage,
|
||||
newRowsPerPage => {
|
||||
preferences.value.itemsPerTablePage = newRowsPerPage;
|
||||
}
|
||||
);
|
||||
|
||||
const next = () => pagination.page++;
|
||||
const hasNext = computed<boolean>(() => {
|
||||
return pagination.page * pagination.rowsPerPage < props.items.length;
|
||||
@@ -189,4 +282,20 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
:where(.table *:first-child) :where(*:first-child) :where(th, td):first-child {
|
||||
border-top-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
:where(.table *:first-child) :where(*:first-child) :where(th, td):last-child {
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
:where(.table *:last-child) :where(*:last-child) :where(th, td):first-child {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
:where(.table *:last-child) :where(*:last-child) :where(th, td):last-child {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
:class="{
|
||||
'badge-lg p-4': size === 'lg',
|
||||
'p-3': size !== 'sm' && size !== 'lg',
|
||||
'p-2 badge-sm': size === 'sm',
|
||||
'badge-sm p-2': size === 'sm',
|
||||
}"
|
||||
:to="`/label/${label.id}`"
|
||||
>
|
||||
<label class="swap swap-rotate" :class="isActive ? 'swap-active' : ''">
|
||||
<MdiArrowRight class="mr-2 swap-on" />
|
||||
<MdiTagOutline class="mr-2 swap-off" />
|
||||
<MdiArrowRight class="swap-on mr-2" />
|
||||
<MdiTagOutline class="swap-off mr-2" />
|
||||
</label>
|
||||
{{ label.name }}
|
||||
</NuxtLink>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BaseModal v-model="modal">
|
||||
<template #title> Create Label </template>
|
||||
<template #title>{{ $t("components.label.create_modal.title") }}</template>
|
||||
<form @submit.prevent="create()">
|
||||
<FormTextField
|
||||
ref="locationNameRef"
|
||||
@@ -8,25 +8,27 @@
|
||||
:trigger-focus="focused"
|
||||
:autofocus="true"
|
||||
label="Label Name"
|
||||
:max-length="255"
|
||||
:min-length="1"
|
||||
/>
|
||||
<FormTextArea v-model="form.description" label="Label Description" />
|
||||
<FormTextArea v-model="form.description" label="Label Description" :max-length="255" />
|
||||
<div class="modal-action">
|
||||
<div class="flex justify-center">
|
||||
<BaseButton class="rounded-r-none" :loading="loading" type="submit"> Create </BaseButton>
|
||||
<BaseButton class="rounded-r-none" :loading="loading" type="submit"> {{ $t("global.create") }} </BaseButton>
|
||||
<div class="dropdown dropdown-top">
|
||||
<label tabindex="0" class="btn rounded-l-none rounded-r-xl">
|
||||
<MdiChevronDown class="h-5 w-5" />
|
||||
<MdiChevronDown class="size-5" />
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-64 right-0">
|
||||
<ul tabindex="0" class="dropdown-content menu rounded-box right-0 w-64 bg-base-100 p-2 shadow">
|
||||
<li>
|
||||
<button type="button" @click="create(false)">Create and Add Another</button>
|
||||
<button type="button" @click="create(false)">{{ $t("global.create_and_add") }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<p class="text-sm text-center mt-4">
|
||||
<p class="mt-4 text-center text-sm">
|
||||
use <kbd class="kbd kbd-xs">Shift</kbd> + <kbd class="kbd kbd-xs"> Enter </kbd> to create and add another
|
||||
</p>
|
||||
</BaseModal>
|
||||
@@ -71,6 +73,12 @@
|
||||
const { shift } = useMagicKeys();
|
||||
|
||||
async function create(close = true) {
|
||||
if (loading.value) {
|
||||
toast.error("Already creating a label");
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
|
||||
if (shift.value) {
|
||||
close = false;
|
||||
}
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
<NuxtLink
|
||||
ref="card"
|
||||
:to="`/location/${location.id}`"
|
||||
class="card bg-base-100 text-base-content rounded-md transition duration-300 shadow-md"
|
||||
class="card rounded-md bg-base-100 text-base-content shadow-md transition duration-300"
|
||||
>
|
||||
<div
|
||||
class="card-body"
|
||||
:class="{
|
||||
'p-4': !dense,
|
||||
'py-2 px-3': dense,
|
||||
'px-3 py-2': dense,
|
||||
}"
|
||||
>
|
||||
<h2 class="flex items-center justify-between gap-2">
|
||||
<label class="swap swap-rotate" :class="isActive ? 'swap-active' : ''">
|
||||
<MdiArrowRight class="swap-on h-6 w-6" />
|
||||
<MdiMapMarkerOutline class="swap-off h-6 w-6" />
|
||||
<MdiArrowRight class="swap-on size-6" />
|
||||
<MdiMapMarkerOutline class="swap-off size-6" />
|
||||
</label>
|
||||
<span class="mx-auto">
|
||||
{{ location.name }}
|
||||
</span>
|
||||
<span class="badge badge-primary h-6 badge-lg" :class="{ 'opacity-0': !hasCount }">
|
||||
<span class="badge badge-primary badge-lg h-6" :class="{ 'opacity-0': !hasCount }">
|
||||
{{ count }}
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
<template>
|
||||
<BaseModal v-model="modal">
|
||||
<template #title> Create Location </template>
|
||||
<template #title>{{ $t("components.location.create_modal.title") }}</template>
|
||||
<form @submit.prevent="create()">
|
||||
<FormTextField
|
||||
ref="locationNameRef"
|
||||
v-model="form.name"
|
||||
:trigger-focus="focused"
|
||||
:autofocus="true"
|
||||
:required="true"
|
||||
label="Location Name"
|
||||
:max-length="255"
|
||||
:min-length="1"
|
||||
/>
|
||||
<FormTextArea v-model="form.description" label="Location Description" />
|
||||
<FormTextArea v-model="form.description" label="Location Description" :max-length="1000" />
|
||||
<LocationSelector v-model="form.parent" />
|
||||
<div class="modal-action">
|
||||
<div class="flex justify-center">
|
||||
<BaseButton class="rounded-r-none" type="submit" :loading="loading"> Create </BaseButton>
|
||||
<BaseButton class="rounded-r-none" type="submit" :loading="loading">{{ $t("global.create") }}</BaseButton>
|
||||
<div class="dropdown dropdown-top">
|
||||
<label tabindex="0" class="btn rounded-l-none rounded-r-xl">
|
||||
<MdiChevronDown class="h-5 w-5" />
|
||||
<MdiChevronDown class="size-5" />
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-64 right-0">
|
||||
<ul tabindex="0" class="dropdown-content menu rounded-box bg-base-100 right-0 w-64 p-2 shadow">
|
||||
<li>
|
||||
<button type="button" @click="create(false)">Create and Add Another</button>
|
||||
<button type="button" @click="create(false)">{{ $t("global.create_and_add") }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<p class="text-sm text-center mt-4">
|
||||
<p class="mt-4 text-center text-sm">
|
||||
use <kbd class="kbd kbd-xs">Shift</kbd> + <kbd class="kbd kbd-xs"> Enter </kbd> to create and add another
|
||||
</p>
|
||||
</BaseModal>
|
||||
@@ -73,6 +76,10 @@
|
||||
const { shift } = useMagicKeys();
|
||||
|
||||
async function create(close = true) {
|
||||
if (loading.value) {
|
||||
toast.error("Already creating a location");
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
|
||||
if (shift.value) {
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
v-if="selected"
|
||||
:class="['absolute inset-y-0 right-0 flex items-center pr-4', active ? 'text-white' : 'text-primary']"
|
||||
>
|
||||
<MdiCheck class="h-5 w-5" aria-hidden="true" />
|
||||
<MdiCheck class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="cast(item.value).name != cast(item.value).treeString" class="text-xs mt-1">
|
||||
<div v-if="cast(item.value).name != cast(item.value).treeString" class="mt-1 text-xs">
|
||||
{{ cast(item.value).treeString }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,19 +40,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="node flex items-center gap-1 rounded p-1"
|
||||
class="flex items-center gap-1 rounded p-1"
|
||||
:class="{
|
||||
'cursor-pointer hover:bg-base-200': hasChildren,
|
||||
}"
|
||||
@click="openRef = !openRef"
|
||||
>
|
||||
<div
|
||||
class="p-1/2 rounded mr-1 flex items-center justify-center"
|
||||
class="mr-1 flex items-center justify-center rounded p-0.5"
|
||||
:class="{
|
||||
'hover:bg-base-200': hasChildren,
|
||||
}"
|
||||
>
|
||||
<div v-if="!hasChildren" class="h-6 w-6"></div>
|
||||
<div v-if="!hasChildren" class="size-6"></div>
|
||||
<label
|
||||
v-else
|
||||
class="swap swap-rotate"
|
||||
@@ -60,13 +60,13 @@
|
||||
'swap-active': openRef,
|
||||
}"
|
||||
>
|
||||
<MdiChevronRight name="mdi-chevron-right" class="h-6 w-6 swap-off" />
|
||||
<MdiChevronDown name="mdi-chevron-down" class="h-6 w-6 swap-on" />
|
||||
<MdiChevronRight name="mdi-chevron-right" class="swap-off size-6" />
|
||||
<MdiChevronDown name="mdi-chevron-down" class="swap-on size-6" />
|
||||
</label>
|
||||
</div>
|
||||
<MdiMapMarker v-if="item.type === 'location'" class="h-4 w-4" />
|
||||
<MdiPackageVariant v-else class="h-4 w-4" />
|
||||
<NuxtLink class="hover:link text-lg" :to="link" @click.stop>{{ item.name }} </NuxtLink>
|
||||
<MdiMapMarker v-if="item.type === 'location'" class="size-4" />
|
||||
<MdiPackageVariant v-else class="size-4" />
|
||||
<NuxtLink class="text-lg hover:link" :to="link" @click.stop>{{ item.name }} </NuxtLink>
|
||||
</div>
|
||||
<div v-if="openRef" class="ml-4">
|
||||
<LocationTreeNode v-for="child in item.children" :key="child.id" :item="child" :tree-id="treeId" />
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 border-2 root">
|
||||
<div class="root border-2 p-4">
|
||||
<p v-if="locs.length === 0" class="text-center text-sm">
|
||||
{{ $t("location.tree.no_locations") }}
|
||||
</p>
|
||||
<LocationTreeNode v-for="item in locs" :key="item.id" :item="item" :tree-id="treeId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
164
frontend/components/Maintenance/EditModal.vue
Normal file
164
frontend/components/Maintenance/EditModal.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<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");
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<BaseModal v-model="isRevealed" readonly @cancel="cancel(false)">
|
||||
<template #title> Confirm </template>
|
||||
<template #title> {{ $t("global.confirm") }} </template>
|
||||
<div>
|
||||
<p>{{ text }}</p>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<BaseButton type="submit" @click="confirm(true)"> Confirm </BaseButton>
|
||||
<BaseButton type="submit" @click="confirm(true)"> {{ $t("global.confirm") }} </BaseButton>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
<template>
|
||||
<div ref="el" class="dropdown" :class="{ 'dropdown-open': dropdownOpen }">
|
||||
<button ref="btn" tabindex="0" class="btn btn-xs" @click="toggle">
|
||||
{{ label }} {{ len }} <MdiChevronDown class="h-4 w-4" />
|
||||
{{ label }} {{ len }} <MdiChevronDown class="size-4" />
|
||||
</button>
|
||||
<div tabindex="0" class="dropdown-content mt-1 w-64 shadow bg-base-100 rounded-md">
|
||||
<div class="pt-4 px-4 shadow-sm mb-1">
|
||||
<input v-model="search" type="text" placeholder="Search…" class="input input-sm input-bordered w-full mb-2" />
|
||||
<div tabindex="0" class="dropdown-content mt-1 w-64 rounded-md bg-base-100 shadow">
|
||||
<div class="mb-1 px-4 pt-4 shadow-sm">
|
||||
<input v-model="search" type="text" placeholder="Search…" class="input input-bordered input-sm mb-2 w-full" />
|
||||
</div>
|
||||
<div class="overflow-y-auto max-h-72 divide-y">
|
||||
<div class="max-h-72 divide-y overflow-y-auto">
|
||||
<label
|
||||
v-for="v in selectedView"
|
||||
:key="v"
|
||||
class="cursor-pointer px-4 label flex justify-between hover:bg-base-200"
|
||||
class="label flex cursor-pointer justify-between px-4 hover:bg-base-200"
|
||||
>
|
||||
<span class="label-text mr-2">
|
||||
<slot name="display" v-bind="{ item: v }">
|
||||
{{ v[display] }}
|
||||
</slot>
|
||||
</span>
|
||||
<input v-model="selected" type="checkbox" :value="v" class="checkbox checkbox-sm checkbox-primary" />
|
||||
<input v-model="selected" type="checkbox" :value="v" class="checkbox checkbox-primary checkbox-sm" />
|
||||
</label>
|
||||
<hr v-if="selected.length > 0" />
|
||||
<label
|
||||
v-for="v in unselected"
|
||||
:key="v"
|
||||
class="cursor-pointer px-4 label flex justify-between hover:bg-base-200"
|
||||
class="label flex cursor-pointer justify-between px-4 hover:bg-base-200"
|
||||
>
|
||||
<span class="label-text mr-2">
|
||||
<slot name="display" v-bind="{ item: v }">
|
||||
{{ v[display] }}
|
||||
</slot>
|
||||
</span>
|
||||
<input v-model="selected" type="checkbox" :value="v" class="checkbox checkbox-sm checkbox-primary" />
|
||||
<input v-model="selected" type="checkbox" :value="v" class="checkbox checkbox-primary checkbox-sm" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,6 +45,7 @@
|
||||
options: any[];
|
||||
display?: string;
|
||||
modelValue: any[];
|
||||
uniqueField: string;
|
||||
};
|
||||
|
||||
const btn = ref<HTMLButtonElement>();
|
||||
@@ -75,6 +76,7 @@
|
||||
label: "",
|
||||
display: "name",
|
||||
modelValue: () => [],
|
||||
uniqueField: "id",
|
||||
});
|
||||
|
||||
const len = computed(() => {
|
||||
@@ -95,9 +97,12 @@
|
||||
const unselected = computed(() => {
|
||||
return props.options.filter(o => {
|
||||
if (searchFold.value.length > 0) {
|
||||
return o[props.display].toLowerCase().includes(searchFold.value) && !selected.value.includes(o);
|
||||
return (
|
||||
o[props.display].toLowerCase().includes(searchFold.value) &&
|
||||
selected.value.every(s => s[props.uniqueField] !== o[props.uniqueField])
|
||||
);
|
||||
}
|
||||
return !selected.value.includes(o);
|
||||
return selected.value.every(s => s[props.uniqueField] !== o[props.uniqueField]);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="border-t border-gray-300 px-4 py-5 sm:p-0">
|
||||
<dl class="sm:divide-y sm:divide-gray-300">
|
||||
<div v-for="(detail, i) in details" :key="i" class="py-4 sm:grid group sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<div v-for="(detail, i) in details" :key="i" class="group py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-base-content">
|
||||
{{ detail.name }}
|
||||
</dt>
|
||||
<dd class="text-sm text-base-content text-start sm:col-span-2">
|
||||
<dd class="text-start text-sm text-base-content sm:col-span-2">
|
||||
<slot :name="detail.slot || detail.name" v-bind="{ detail }">
|
||||
<DateTime
|
||||
v-if="detail.type == 'date'"
|
||||
@@ -14,9 +14,9 @@
|
||||
/>
|
||||
<Currency v-else-if="detail.type == 'currency'" :amount="detail.text" />
|
||||
<template v-else-if="detail.type === 'link'">
|
||||
<div class="tooltip tooltip-primary tooltip-top" :data-tip="detail.href">
|
||||
<div class="tooltip tooltip-top tooltip-primary" :data-tip="detail.href">
|
||||
<a class="btn btn-primary btn-xs" :href="detail.href" target="_blank">
|
||||
<MdiOpenInNew class="mr-2 swap-on" />
|
||||
<MdiOpenInNew class="swap-on mr-2" />
|
||||
{{ detail.text }}
|
||||
</a>
|
||||
</div>
|
||||
@@ -27,17 +27,17 @@
|
||||
</ClientOnly>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="flex items-center">
|
||||
<span class="flex items-center text-wrap">
|
||||
{{ detail.text }}
|
||||
<span
|
||||
v-if="detail.copyable"
|
||||
class="opacity-0 group-hover:opacity-100 ml-4 my-0 duration-75 transition-opacity"
|
||||
class="my-0 ml-4 opacity-0 transition-opacity duration-75 group-hover:opacity-100"
|
||||
>
|
||||
<CopyText
|
||||
v-if="detail.text.toString()"
|
||||
:text="detail.text.toString()"
|
||||
:icon-size="16"
|
||||
class="btn btn-xs btn-ghost btn-circle"
|
||||
class="btn btn-circle btn-ghost btn-xs"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
ref="el"
|
||||
class="h-24 w-full border-2 border-primary border-dashed grid place-content-center"
|
||||
class="grid h-24 w-full place-content-center border-2 border-dashed border-primary"
|
||||
:class="isOverDropZone ? 'bg-primary bg-opacity-10' : ''"
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="markdown" v-html="raw"></div>
|
||||
<div class="markdown text-wrap" v-html="raw"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<MdiQrcode />
|
||||
</label>
|
||||
</slot>
|
||||
<div tabindex="0" class="card compact dropdown-content shadow-lg bg-base-100 rounded-box w-64">
|
||||
<div tabindex="0" class="card dropdown-content compact rounded-box w-64 bg-base-100 shadow-lg">
|
||||
<div class="card-body">
|
||||
<h2 class="text-center">Page URL</h2>
|
||||
<h2 class="text-center">{{ $t("components.global.page_qr_code.page_url") }}</h2>
|
||||
<img :src="getQRCodeUrl()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="py-4">
|
||||
<p class="text-sm">Password Strength: {{ message }}</p>
|
||||
<p class="text-sm">{{ $t("components.global.password_score.password_strength") }}: {{ message }}</p>
|
||||
<progress
|
||||
class="progress w-full progress-bar"
|
||||
class="progress w-full"
|
||||
:value="score"
|
||||
max="100"
|
||||
:class="{
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<template>
|
||||
<div class="grow-1 max-w-full"></div>
|
||||
<div class="max-w-full"></div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="stats bg-neutral shadow rounded-md">
|
||||
<div class="stat text-neutral-content text-center space-y-1 p-3">
|
||||
<div class="stat-title">{{ title }}</div>
|
||||
<div class="stats rounded-md bg-neutral shadow">
|
||||
<div class="stat space-y-1 p-3 text-center text-neutral-content">
|
||||
<div class="stat-title text-neutral-content">{{ title }}</div>
|
||||
<div class="stat-value text-2xl">
|
||||
<Currency v-if="type === 'currency'" :amount="value" />
|
||||
<template v-if="type === 'number'">{{ value }}</template>
|
||||
@@ -27,8 +27,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
[data-theme="homebox"] .stat-title {
|
||||
color: hsl(0 0% 90/0.6);
|
||||
}
|
||||
</style>
|
||||
<style></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<h3 class="flex gap-2 items-center mb-3 pl-1 text-lg">
|
||||
<h3 class="mb-3 flex items-center gap-2 pl-1 text-lg">
|
||||
<slot />
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<th
|
||||
v-for="h in headers"
|
||||
:key="h.value"
|
||||
class="text-no-transform text-sm bg-neutral text-neutral-content"
|
||||
class="text-no-transform bg-neutral text-sm text-neutral-content"
|
||||
:class="{
|
||||
'text-center': h.align === 'center',
|
||||
'text-right': h.align === 'right',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user