mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-24 06:28:34 +01:00
Compare commits
170 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 | ||
|
|
39163f3cfc | ||
|
|
43676ab407 | ||
|
|
639f795b9a | ||
|
|
fc95d2cab8 | ||
|
|
695b6d68e6 | ||
|
|
b6c265098d | ||
|
|
2a80d348bd | ||
|
|
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 | ||
|
|
0bcb155756 | ||
|
|
12219522ab | ||
|
|
13864997ab | ||
|
|
2ab2766534 | ||
|
|
e051352070 | ||
|
|
3bf1e50620 | ||
|
|
42f3c88396 | ||
|
|
c9f31ef934 | ||
|
|
01f54aeb52 | ||
|
|
2d1016d362 | ||
|
|
b48c961ac1 | ||
|
|
4d47567995 | ||
|
|
6b2e3accf7 | ||
|
|
c1f8520c4f | ||
|
|
41eb99ec40 | ||
|
|
97a74127fb | ||
|
|
a9396167bf | ||
|
|
3385e5684e | ||
|
|
bb9672214c | ||
|
|
1b93672417 | ||
|
|
8b1cedd4a8 | ||
|
|
2c34047b6d | ||
|
|
f0942f0714 | ||
|
|
967e574ea8 | ||
|
|
625730f37c | ||
|
|
0a72fa95b3 | ||
|
|
c39a65ec21 | ||
|
|
9403fb27e0 | ||
|
|
ab34791737 | ||
|
|
d03e60d580 | ||
|
|
6fcf9965bb | ||
|
|
d53dcd37e6 | ||
|
|
f3531cacb3 | ||
|
|
10cdca94dc | ||
|
|
0b2b7bc4fd | ||
|
|
105f63487b | ||
|
|
f3c745e42e | ||
|
|
f3e7d7a19b |
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."
|
||||
21
Dockerfile
21
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
|
||||
@@ -51,7 +62,7 @@ HEALTHCHECK --interval=30s \
|
||||
--timeout=5s \
|
||||
--start-period=5s \
|
||||
--retries=3 \
|
||||
CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7745/api/v1/status" ]
|
||||
CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "-O -", "http://localhost:7745/api/v1/status" ]
|
||||
VOLUME [ "/data" ]
|
||||
|
||||
ENTRYPOINT [ "/app/api" ]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -54,7 +61,7 @@ HEALTHCHECK --interval=30s \
|
||||
--timeout=5s \
|
||||
--start-period=5s \
|
||||
--retries=3 \
|
||||
CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7745/api/v1/status" ]
|
||||
CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "-O -", "http://localhost:7745/api/v1/status" ]
|
||||
VOLUME [ "/data" ]
|
||||
|
||||
# Drop root and run as low-privileged user
|
||||
|
||||
15
README.md
15
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>
|
||||
@@ -16,13 +16,15 @@
|
||||
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.
|
||||
- _Blazingly Fast_ - Homebox is written in Go, which makes it extremely fast and requires minimal resources to deploy. In general idle memory usage is less than 50MB for the whole container.
|
||||
- _Blazingly Fast_ - Homebox is written in Go, which makes it extremely fast and requires minimal resources to deploy. In general, idle memory usage is less than 50MB for the whole container.
|
||||
- _Portable_ - Homebox is designed to be portable and run on anywhere. We use SQLite and an embedded Web UI to make it easy to deploy, use, and backup.
|
||||
|
||||
# Screenshots
|
||||
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
|
||||
@@ -47,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)
|
||||
|
||||
@@ -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 (
|
||||
@@ -87,12 +94,6 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func BaseURLFunc(prefix string) func(s string) string {
|
||||
return func(s string) string {
|
||||
return prefix + "/v1" + s
|
||||
}
|
||||
}
|
||||
|
||||
func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, bus *eventbus.EventBus, options ...func(*V1Controller)) *V1Controller {
|
||||
ctrl := &V1Controller{
|
||||
repo: repos,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -87,12 +87,22 @@ func (ctrl *V1Controller) HandleLocationDelete() errchain.HandlerFunc {
|
||||
|
||||
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}})
|
||||
if err != nil {
|
||||
return repo.LocationOut{}, err
|
||||
}
|
||||
|
||||
for _, item := range items.Items {
|
||||
totalPrice.Add(totalPrice, big.NewInt(int64(item.PurchasePrice*100)))
|
||||
// Convert item.Quantity to float64 for multiplication
|
||||
quantity := float64(item.Quantity)
|
||||
itemTotal := big.NewInt(int64(item.PurchasePrice * quantity * 100))
|
||||
totalPrice.Add(totalPrice, itemTotal)
|
||||
}
|
||||
|
||||
totalPriceFloat := new(big.Float).SetInt(totalPrice)
|
||||
@@ -101,14 +111,15 @@ func (ctrl *V1Controller) GetLocationWithPrice(auth context.Context, GID uuid.UU
|
||||
|
||||
// Add price from child locations
|
||||
for _, childLocation := range location.Children {
|
||||
var childLocation, err = ctrl.GetLocationWithPrice(auth, GID, childLocation.ID)
|
||||
var childLocationWithPrice repo.LocationOut
|
||||
childLocationWithPrice, err = ctrl.GetLocationWithPrice(auth, GID, childLocation.ID)
|
||||
if err != nil {
|
||||
return repo.LocationOut{}, err
|
||||
}
|
||||
location.TotalPrice += childLocation.TotalPrice
|
||||
location.TotalPrice += childLocationWithPrice.TotalPrice
|
||||
}
|
||||
|
||||
return location, err
|
||||
return location, nil
|
||||
}
|
||||
|
||||
// HandleLocationGet godoc
|
||||
|
||||
@@ -3,12 +3,7 @@ package main
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"fmt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/hay-kot/httpkit/errchain"
|
||||
httpSwagger "github.com/swaggo/http-swagger/v2" // http-swagger middleware
|
||||
@@ -18,6 +13,11 @@ import (
|
||||
_ "github.com/sysadminsmedia/homebox/backend/app/api/static/docs"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/authroles"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const prefix = "/api"
|
||||
@@ -47,8 +47,6 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
||||
// =========================================================================
|
||||
// API Version 1
|
||||
|
||||
v1Base := v1.BaseURLFunc(prefix)
|
||||
|
||||
v1Ctrl := v1.NewControllerV1(
|
||||
a.services,
|
||||
a.repos,
|
||||
@@ -56,112 +54,114 @@ 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.Get(v1Base("/status"), chain.ToHandlerFunc(v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildTime: buildTime,
|
||||
})))
|
||||
r.Route(prefix+"/v1", func(r chi.Router) {
|
||||
r.Get("/status", chain.ToHandlerFunc(v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildTime: buildTime,
|
||||
})))
|
||||
|
||||
r.Get(v1Base("/currencies"), chain.ToHandlerFunc(v1Ctrl.HandleCurrency()))
|
||||
r.Get("/currencies", chain.ToHandlerFunc(v1Ctrl.HandleCurrency()))
|
||||
|
||||
providers := []v1.AuthProvider{
|
||||
providers.NewLocalProvider(a.services.User),
|
||||
}
|
||||
providers := []v1.AuthProvider{
|
||||
providers.NewLocalProvider(a.services.User),
|
||||
}
|
||||
|
||||
r.Post(v1Base("/users/register"), chain.ToHandlerFunc(v1Ctrl.HandleUserRegistration()))
|
||||
r.Post(v1Base("/users/login"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogin(providers...)))
|
||||
r.Post("/users/register", chain.ToHandlerFunc(v1Ctrl.HandleUserRegistration()))
|
||||
r.Post("/users/login", chain.ToHandlerFunc(v1Ctrl.HandleAuthLogin(providers...)))
|
||||
|
||||
userMW := []errchain.Middleware{
|
||||
a.mwAuthToken,
|
||||
a.mwRoles(RoleModeOr, authroles.RoleUser.String()),
|
||||
}
|
||||
userMW := []errchain.Middleware{
|
||||
a.mwAuthToken,
|
||||
a.mwRoles(RoleModeOr, authroles.RoleUser.String()),
|
||||
}
|
||||
|
||||
r.Get(v1Base("/ws/events"), chain.ToHandlerFunc(v1Ctrl.HandleCacheWS(), userMW...))
|
||||
r.Get(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelf(), userMW...))
|
||||
r.Put(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfUpdate(), userMW...))
|
||||
r.Delete(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfDelete(), userMW...))
|
||||
r.Post(v1Base("/users/logout"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogout(), userMW...))
|
||||
r.Get(v1Base("/users/refresh"), chain.ToHandlerFunc(v1Ctrl.HandleAuthRefresh(), userMW...))
|
||||
r.Put(v1Base("/users/self/change-password"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfChangePassword(), userMW...))
|
||||
r.Get("/ws/events", chain.ToHandlerFunc(v1Ctrl.HandleCacheWS(), userMW...))
|
||||
r.Get("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelf(), userMW...))
|
||||
r.Put("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfUpdate(), userMW...))
|
||||
r.Delete("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfDelete(), userMW...))
|
||||
r.Post("/users/logout", chain.ToHandlerFunc(v1Ctrl.HandleAuthLogout(), userMW...))
|
||||
r.Get("/users/refresh", chain.ToHandlerFunc(v1Ctrl.HandleAuthRefresh(), userMW...))
|
||||
r.Put("/users/self/change-password", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfChangePassword(), userMW...))
|
||||
|
||||
r.Post(v1Base("/groups/invitations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsCreate(), userMW...))
|
||||
r.Get(v1Base("/groups/statistics"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...))
|
||||
r.Get(v1Base("/groups/statistics/purchase-price"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...))
|
||||
r.Get(v1Base("/groups/statistics/locations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLocations(), userMW...))
|
||||
r.Get(v1Base("/groups/statistics/labels"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLabels(), userMW...))
|
||||
r.Post("/groups/invitations", chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsCreate(), userMW...))
|
||||
r.Get("/groups/statistics", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...))
|
||||
r.Get("/groups/statistics/purchase-price", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...))
|
||||
r.Get("/groups/statistics/locations", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLocations(), userMW...))
|
||||
r.Get("/groups/statistics/labels", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLabels(), userMW...))
|
||||
|
||||
// TODO: I don't like /groups being the URL for users
|
||||
r.Get(v1Base("/groups"), chain.ToHandlerFunc(v1Ctrl.HandleGroupGet(), userMW...))
|
||||
r.Put(v1Base("/groups"), chain.ToHandlerFunc(v1Ctrl.HandleGroupUpdate(), userMW...))
|
||||
// TODO: I don't like /groups being the URL for users
|
||||
r.Get("/groups", chain.ToHandlerFunc(v1Ctrl.HandleGroupGet(), userMW...))
|
||||
r.Put("/groups", chain.ToHandlerFunc(v1Ctrl.HandleGroupUpdate(), userMW...))
|
||||
|
||||
r.Post(v1Base("/actions/ensure-asset-ids"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureAssetID(), userMW...))
|
||||
r.Post(v1Base("/actions/zero-item-time-fields"), chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...))
|
||||
r.Post(v1Base("/actions/ensure-import-refs"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
|
||||
r.Post(v1Base("/actions/set-primary-photos"), chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
|
||||
r.Post("/actions/ensure-asset-ids", chain.ToHandlerFunc(v1Ctrl.HandleEnsureAssetID(), userMW...))
|
||||
r.Post("/actions/zero-item-time-fields", chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...))
|
||||
r.Post("/actions/ensure-import-refs", chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
|
||||
r.Post("/actions/set-primary-photos", chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
|
||||
|
||||
r.Get(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
|
||||
r.Post(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))
|
||||
r.Get(v1Base("/locations/tree"), chain.ToHandlerFunc(v1Ctrl.HandleLocationTreeQuery(), userMW...))
|
||||
r.Get(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGet(), userMW...))
|
||||
r.Put(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationUpdate(), userMW...))
|
||||
r.Delete(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationDelete(), userMW...))
|
||||
r.Get("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
|
||||
r.Post("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))
|
||||
r.Get("/locations/tree", chain.ToHandlerFunc(v1Ctrl.HandleLocationTreeQuery(), userMW...))
|
||||
r.Get("/locations/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLocationGet(), userMW...))
|
||||
r.Put("/locations/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLocationUpdate(), userMW...))
|
||||
r.Delete("/locations/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLocationDelete(), userMW...))
|
||||
|
||||
r.Get(v1Base("/labels"), chain.ToHandlerFunc(v1Ctrl.HandleLabelsGetAll(), userMW...))
|
||||
r.Post(v1Base("/labels"), chain.ToHandlerFunc(v1Ctrl.HandleLabelsCreate(), userMW...))
|
||||
r.Get(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelGet(), userMW...))
|
||||
r.Put(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelUpdate(), userMW...))
|
||||
r.Delete(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelDelete(), userMW...))
|
||||
r.Get("/labels", chain.ToHandlerFunc(v1Ctrl.HandleLabelsGetAll(), userMW...))
|
||||
r.Post("/labels", chain.ToHandlerFunc(v1Ctrl.HandleLabelsCreate(), userMW...))
|
||||
r.Get("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelGet(), userMW...))
|
||||
r.Put("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelUpdate(), userMW...))
|
||||
r.Delete("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelDelete(), userMW...))
|
||||
|
||||
r.Get(v1Base("/items"), chain.ToHandlerFunc(v1Ctrl.HandleItemsGetAll(), userMW...))
|
||||
r.Post(v1Base("/items"), chain.ToHandlerFunc(v1Ctrl.HandleItemsCreate(), userMW...))
|
||||
r.Post(v1Base("/items/import"), chain.ToHandlerFunc(v1Ctrl.HandleItemsImport(), userMW...))
|
||||
r.Get(v1Base("/items/export"), chain.ToHandlerFunc(v1Ctrl.HandleItemsExport(), userMW...))
|
||||
r.Get(v1Base("/items/fields"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldNames(), userMW...))
|
||||
r.Get(v1Base("/items/fields/values"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldValues(), userMW...))
|
||||
r.Get("/items", chain.ToHandlerFunc(v1Ctrl.HandleItemsGetAll(), userMW...))
|
||||
r.Post("/items", chain.ToHandlerFunc(v1Ctrl.HandleItemsCreate(), userMW...))
|
||||
r.Post("/items/import", chain.ToHandlerFunc(v1Ctrl.HandleItemsImport(), userMW...))
|
||||
r.Get("/items/export", chain.ToHandlerFunc(v1Ctrl.HandleItemsExport(), userMW...))
|
||||
r.Get("/items/fields", chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldNames(), userMW...))
|
||||
r.Get("/items/fields/values", chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldValues(), userMW...))
|
||||
|
||||
r.Get(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemGet(), userMW...))
|
||||
r.Get(v1Base("/items/{id}/path"), chain.ToHandlerFunc(v1Ctrl.HandleItemFullPath(), userMW...))
|
||||
r.Put(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
|
||||
r.Patch(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...))
|
||||
r.Delete(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
|
||||
r.Get("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemGet(), userMW...))
|
||||
r.Get("/items/{id}/path", chain.ToHandlerFunc(v1Ctrl.HandleItemFullPath(), userMW...))
|
||||
r.Put("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
|
||||
r.Patch("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...))
|
||||
r.Delete("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
|
||||
|
||||
r.Post(v1Base("/items/{id}/attachments"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
|
||||
r.Put(v1Base("/items/{id}/attachments/{attachment_id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
|
||||
r.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentDelete(), userMW...))
|
||||
r.Post("/items/{id}/attachments", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
|
||||
r.Put("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
|
||||
r.Delete("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentDelete(), userMW...))
|
||||
|
||||
r.Get(v1Base("/items/{id}/maintenance"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...))
|
||||
r.Post(v1Base("/items/{id}/maintenance"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...))
|
||||
r.Put(v1Base("/items/{id}/maintenance/{entry_id}"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...))
|
||||
r.Delete(v1Base("/items/{id}/maintenance/{entry_id}"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryDelete(), userMW...))
|
||||
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(v1Base("/assets/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleAssetGet(), userMW...))
|
||||
r.Get("/assets/{id}", chain.ToHandlerFunc(v1Ctrl.HandleAssetGet(), userMW...))
|
||||
|
||||
// Notifiers
|
||||
r.Get(v1Base("/notifiers"), chain.ToHandlerFunc(v1Ctrl.HandleGetUserNotifiers(), userMW...))
|
||||
r.Post(v1Base("/notifiers"), chain.ToHandlerFunc(v1Ctrl.HandleCreateNotifier(), userMW...))
|
||||
r.Put(v1Base("/notifiers/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleUpdateNotifier(), userMW...))
|
||||
r.Delete(v1Base("/notifiers/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleDeleteNotifier(), userMW...))
|
||||
r.Post(v1Base("/notifiers/test"), chain.ToHandlerFunc(v1Ctrl.HandlerNotifierTest(), userMW...))
|
||||
// Notifiers
|
||||
r.Get("/notifiers", chain.ToHandlerFunc(v1Ctrl.HandleGetUserNotifiers(), userMW...))
|
||||
r.Post("/notifiers", chain.ToHandlerFunc(v1Ctrl.HandleCreateNotifier(), userMW...))
|
||||
r.Put("/notifiers/{id}", chain.ToHandlerFunc(v1Ctrl.HandleUpdateNotifier(), userMW...))
|
||||
r.Delete("/notifiers/{id}", chain.ToHandlerFunc(v1Ctrl.HandleDeleteNotifier(), userMW...))
|
||||
r.Post("/notifiers/test", chain.ToHandlerFunc(v1Ctrl.HandlerNotifierTest(), userMW...))
|
||||
|
||||
// Asset-Like endpoints
|
||||
assetMW := []errchain.Middleware{
|
||||
a.mwAuthToken,
|
||||
a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
|
||||
}
|
||||
// Asset-Like endpoints
|
||||
assetMW := []errchain.Middleware{
|
||||
a.mwAuthToken,
|
||||
a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
|
||||
}
|
||||
|
||||
r.Get(
|
||||
v1Base("/qrcode"),
|
||||
chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...),
|
||||
)
|
||||
r.Get(
|
||||
v1Base("/items/{id}/attachments/{attachment_id}"),
|
||||
chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentGet(), assetMW...),
|
||||
)
|
||||
r.Get("/qrcode", chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...))
|
||||
r.Get(
|
||||
"/items/{id}/attachments/{attachment_id}",
|
||||
chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentGet(), assetMW...),
|
||||
)
|
||||
|
||||
// Reporting Services
|
||||
r.Get(v1Base("/reporting/bill-of-materials"), chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...))
|
||||
// Reporting Services
|
||||
r.Get("/reporting/bill-of-materials", chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...))
|
||||
|
||||
r.NotFound(http.NotFound)
|
||||
})
|
||||
|
||||
r.NotFound(chain.ToHandlerFunc(notFoundHandler()))
|
||||
}
|
||||
|
||||
@@ -7,25 +7,25 @@ toolchain go1.22.0
|
||||
require (
|
||||
ariga.io/atlas v0.19.1
|
||||
entgo.io/ent v0.12.5
|
||||
github.com/ardanlabs/conf/v3 v3.1.7
|
||||
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.0
|
||||
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.23
|
||||
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.27.0
|
||||
modernc.org/sqlite v1.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -65,14 +65,13 @@ require (
|
||||
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/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // 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/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
|
||||
)
|
||||
|
||||
@@ -10,8 +10,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 +27,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 +54,23 @@ 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-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 +81,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 +110,15 @@ 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-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
||||
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
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/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,8 +132,8 @@ 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -145,39 +144,39 @@ 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=
|
||||
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/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/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/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/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.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=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
@@ -194,16 +193,16 @@ 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/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/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
@@ -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{}{}
|
||||
@@ -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,6 +203,7 @@ 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,
|
||||
PurchaseFrom: item.PurchaseFrom,
|
||||
@@ -219,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,
|
||||
}
|
||||
}
|
||||
@@ -252,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)
|
||||
|
||||
@@ -329,7 +329,7 @@ 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) {
|
||||
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
|
||||
@@ -337,7 +337,7 @@ func (svc *ItemService) ExportCSV(ctx context.Context, GID uuid.UUID) ([][]strin
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
||||
description: "A simple home inventory management software",
|
||||
lastUpdated: true,
|
||||
sitemap: {
|
||||
hostname: 'https://homebox.sysadminsmedia.com',
|
||||
hostname: 'https://homebox.software',
|
||||
},
|
||||
|
||||
locales: {
|
||||
@@ -27,7 +27,8 @@ 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: {
|
||||
@@ -36,6 +37,8 @@ export default defineConfig({
|
||||
text: 'Getting Started',
|
||||
items: [
|
||||
{ text: 'Quick Start', link: '/en/quick-start' },
|
||||
{ text: 'Installation', link: '/en/installation' },
|
||||
{ text: 'Configure Homebox', link: '/en/configure-homebox' },
|
||||
{ text: 'Tips and Tricks', link: '/en/tips-tricks' }
|
||||
]
|
||||
},
|
||||
@@ -56,8 +59,8 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
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' },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1683,12 +1683,14 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"example": "admin@admin.com",
|
||||
"description": "string",
|
||||
"name": "username",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "admin",
|
||||
"description": "string",
|
||||
"name": "password",
|
||||
"in": "formData"
|
||||
|
||||
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,23 +47,28 @@ 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.
|
||||
For documentation contributions you only need NodeJS and PNPM.
|
||||
For documentation contributions, you only need Node.js and PNPM.
|
||||
|
||||
::: info Notes
|
||||
- Languages are seperated by folder (e.g `/en`, `/fr`, etc.)
|
||||
- Languages are separated by folder (e.g `/en`, `/fr`, etc.)
|
||||
- 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.
|
||||
|
||||
[](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 |
@@ -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)
|
||||
@@ -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
|
||||
@@ -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 bg-base-100 shadow-xl rounded-lg">
|
||||
<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-visible">
|
||||
<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>
|
||||
|
||||
@@ -4,28 +4,33 @@
|
||||
<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>
|
||||
</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>
|
||||
@@ -49,6 +54,10 @@
|
||||
type: String,
|
||||
default: "name",
|
||||
},
|
||||
uniqueField: {
|
||||
type: String,
|
||||
default: "id",
|
||||
},
|
||||
selectFirst: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -57,19 +66,26 @@
|
||||
|
||||
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);
|
||||
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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<label class="label">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
</label>
|
||||
<textarea ref="el" v-model="value" class="textarea w-full textarea-bordered h-28" :placeholder="placeholder" />
|
||||
<textarea ref="el" v-model="value" class="textarea textarea-bordered h-28 w-full" :placeholder="placeholder" />
|
||||
<label v-if="limit" class="label">
|
||||
<span class="label-text-alt"></span>
|
||||
<span class="label-text-alt"> {{ valueLen }}/{{ limit }}</span>
|
||||
@@ -16,7 +16,7 @@
|
||||
<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
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<label class="label">
|
||||
<span class="label-text">{{ label }}</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" class="input input-bordered col-span-3 mt-2 w-full" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -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,51 @@
|
||||
<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" />
|
||||
<FormMultiselect v-model="form.labels" label="Labels" :items="labels ?? []" />
|
||||
|
||||
<div class="modal-action">
|
||||
<div class="flex justify-center">
|
||||
<div>
|
||||
<label for="photo" class="btn">{{ $t("components.item.create_modal.photo_button") }}</label>
|
||||
<input id="photo" type="file" accept="image/*" style="visibility: hidden" @change="previewImage" />
|
||||
</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 +58,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 +103,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 +145,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (loading.value) {
|
||||
toast.error("Already creating an item");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
if (shift.value) {
|
||||
close = false;
|
||||
}
|
||||
@@ -127,16 +167,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 text-lg 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"
|
||||
@@ -12,21 +12,21 @@
|
||||
<FormTextArea v-model="form.description" label="Label Description" />
|
||||
<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 +71,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,6 +1,6 @@
|
||||
<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"
|
||||
@@ -13,21 +13,21 @@
|
||||
<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 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>
|
||||
@@ -73,6 +73,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,7 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 border-2 root">
|
||||
<div class="root border-2 p-4">
|
||||
<LocationTreeNode v-for="item in locs" :key="item.id" :item="item" :tree-id="treeId" />
|
||||
</div>
|
||||
</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>
|
||||
@@ -31,13 +31,13 @@
|
||||
{{ 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 />
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Ref } from "vue";
|
||||
import type { TableHeader } from "components/Item/View/Table.types";
|
||||
import type { DaisyTheme } from "~~/lib/data/themes";
|
||||
|
||||
export type ViewType = "table" | "card" | "tree";
|
||||
@@ -9,6 +10,10 @@ export type LocationViewPreferences = {
|
||||
editorAdvancedView: boolean;
|
||||
itemDisplayView: ViewType;
|
||||
theme: DaisyTheme;
|
||||
itemsPerTablePage: number;
|
||||
tableHeaders?: TableHeader[];
|
||||
displayHeaderDecor: boolean;
|
||||
language?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -24,6 +29,9 @@ export function useViewPreferences(): Ref<LocationViewPreferences> {
|
||||
editorAdvancedView: false,
|
||||
itemDisplayView: "card",
|
||||
theme: "homebox",
|
||||
itemsPerTablePage: 10,
|
||||
displayHeaderDecor: true,
|
||||
language: null,
|
||||
},
|
||||
{ mergeDefaults: true }
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ export function useRouteQuery(q: string, def: any): WritableComputedRef<any> {
|
||||
case "string":
|
||||
return computed({
|
||||
get: () => {
|
||||
const qv = route.query[q];
|
||||
const qv = first.value;
|
||||
if (Array.isArray(qv)) {
|
||||
return qv[0];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<main class="w-full min-h-screen bg-blue-100 grid place-items-center">
|
||||
<main class="grid min-h-screen w-full place-items-center bg-blue-100">
|
||||
<slot></slot>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -13,22 +13,28 @@
|
||||
<div class="drawer drawer-mobile">
|
||||
<input id="my-drawer-2" v-model="drawerToggle" type="checkbox" class="drawer-toggle" />
|
||||
<div class="drawer-content justify-center bg-base-300 pt-20 lg:pt-0">
|
||||
<AppHeaderDecor class="-mt-10 hidden lg:block" />
|
||||
<AppHeaderDecor v-if="preferences.displayHeaderDecor" class="-mt-10 hidden lg:block" />
|
||||
<!-- Button -->
|
||||
<div class="navbar z-[99] lg:hidden top-0 fixed bg-primary shadow-md drawer-button">
|
||||
<label for="my-drawer-2" class="btn btn-square btn-ghost text-base-100 drawer-button lg:hidden">
|
||||
<MdiMenu class="h-6 w-6" />
|
||||
<div class="navbar drawer-button fixed top-0 z-[99] bg-primary shadow-md lg:hidden">
|
||||
<label for="my-drawer-2" class="btn btn-square btn-ghost drawer-button text-base-100 lg:hidden">
|
||||
<MdiMenu class="size-6" />
|
||||
</label>
|
||||
<NuxtLink to="/home">
|
||||
<h2 class="text-3xl font-bold tracking-tight text-base-100 flex">
|
||||
<h2 class="flex text-3xl font-bold tracking-tight text-base-100">
|
||||
HomeB
|
||||
<AppLogo class="w-8 -mb-3" />
|
||||
<AppLogo class="-mb-3 w-8" />
|
||||
x
|
||||
</h2>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
<footer v-if="status" class="bottom-0 w-full bg-base-300 pb-4 text-center text-secondary-content">
|
||||
<p class="text-center text-sm">
|
||||
{{ $t("global.version", { version: status.build.version }) }} ~
|
||||
{{ $t("global.build", { build: status.build.commit }) }}
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
@@ -36,26 +42,26 @@
|
||||
<label for="my-drawer-2" class="drawer-overlay"></label>
|
||||
|
||||
<!-- Top Section -->
|
||||
<div class="w-60 py-5 md:py-10 bg-base-200 flex flex-grow-1 flex-col">
|
||||
<div class="flex w-60 flex-col bg-base-200 py-5 md:py-10">
|
||||
<div class="space-y-8">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<p>Welcome, {{ username }}</p>
|
||||
<p>{{ $t("global.welcome", { username: username }) }}</p>
|
||||
<NuxtLink class="avatar placeholder" to="/home">
|
||||
<div class="bg-base-300 text-neutral-content rounded-full w-24 p-4">
|
||||
<div class="w-24 rounded-full bg-base-300 p-4 text-neutral-content">
|
||||
<AppLogo />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="flex flex-col bg-base-200">
|
||||
<div class="mx-auto w-40 mb-6">
|
||||
<div class="dropdown overflow visible w-40">
|
||||
<label tabindex="0" class="btn btn-primary btn-block text-lg text-no-transform">
|
||||
<div class="mx-auto mb-6 w-40">
|
||||
<div class="dropdown visible w-40">
|
||||
<label tabindex="0" class="text-no-transform btn btn-primary btn-block text-lg">
|
||||
<span>
|
||||
<MdiPlus class="mr-1 -ml-1" />
|
||||
<MdiPlus class="-ml-1 mr-1" />
|
||||
</span>
|
||||
Create
|
||||
{{ $t("global.create") }}
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-40">
|
||||
<ul tabindex="0" class="dropdown-content menu rounded-box w-40 bg-base-100 p-2 shadow">
|
||||
<li v-for="btn in dropdown" :key="btn.name">
|
||||
<button @click="btn.action">
|
||||
{{ btn.name }}
|
||||
@@ -64,7 +70,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="flex flex-col mx-auto gap-2 w-40 menu">
|
||||
<ul class="menu mx-auto flex w-40 flex-col gap-2">
|
||||
<li v-for="n in nav" :key="n.id" class="text-xl" @click="unfocus">
|
||||
<NuxtLink
|
||||
v-if="n.to"
|
||||
@@ -74,7 +80,7 @@
|
||||
'bg-secondary text-secondary-content': n.active?.value,
|
||||
}"
|
||||
>
|
||||
<component :is="n.icon" class="h-6 w-6 mr-4" />
|
||||
<component :is="n.icon" class="mr-4 size-6" />
|
||||
{{ n.name }}
|
||||
</NuxtLink>
|
||||
</li>
|
||||
@@ -83,7 +89,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Bottom -->
|
||||
<button class="mt-auto mx-2 hover:bg-base-300 p-3 rounded-btn" @click="logout">Sign Out</button>
|
||||
<button class="rounded-btn mx-2 mt-auto p-3 hover:bg-base-300" @click="logout">
|
||||
{{ $t("global.sign_out") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,12 +109,19 @@
|
||||
import MdiMagnify from "~icons/mdi/magnify";
|
||||
import MdiAccount from "~icons/mdi/account";
|
||||
import MdiCog from "~icons/mdi/cog";
|
||||
|
||||
const username = computed(() => authCtx.user?.name || "User");
|
||||
|
||||
const preferences = useViewPreferences();
|
||||
|
||||
const pubApi = usePublicApi();
|
||||
const { data: status } = useAsyncData(async () => {
|
||||
const { data } = await pubApi.status();
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
// Preload currency format
|
||||
useFormatCurrency();
|
||||
|
||||
const modals = reactive({
|
||||
item: false,
|
||||
location: false,
|
||||
|
||||
128
frontend/locales/ca.json
Normal file
128
frontend/locales/ca.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"title": "Importa un fitxer CSV",
|
||||
"upload": "Puja",
|
||||
"description": "Importa un fitxer CSV que contingui els articles, etiquetes i ubicacions. \nConsulteu la documentació per a més informació sobre el format requerit.",
|
||||
"change_warning": "El comportament de les importacions amb import_refs existents ha canviat. Si hi ha un import_refs al fitxer CSV, \nl'article s'actualitzarà amb els valors del fitxer CSV."
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Crea un article",
|
||||
"photo_button": "Foto 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Targeta",
|
||||
"items": "Articles",
|
||||
"no_items": "No hi ha articles a mostrar",
|
||||
"table": "Taula"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Crea una etiqueta"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Crea una ubicació"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "URL de la pàgina"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Força de la contrasenya"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Construcció: { build }",
|
||||
"confirm": "Confirma",
|
||||
"create": "Crea",
|
||||
"create_and_add": "Crea i afegeix-ne un altre",
|
||||
"created": "Creat",
|
||||
"email": "Correu electrònic",
|
||||
"follow_dev": "Segueix al desenvolupador",
|
||||
"github": "Projecte de GitHub",
|
||||
"items": "Articles",
|
||||
"join_discord": "Uniu-vos a Discord",
|
||||
"locations": "Ubicacions",
|
||||
"name": "Nom",
|
||||
"read_docs": "Llegiu la documentació",
|
||||
"search": "Cerca",
|
||||
"sign_out": "Tanca la sessió",
|
||||
"submit": "Envia",
|
||||
"welcome": "Us donem la benvinguda, { username }",
|
||||
"labels": "Etiquetes",
|
||||
"password": "Contrasenya",
|
||||
"version": "Versió {version}"
|
||||
},
|
||||
"index": {
|
||||
"joining_group": "Us uniu a un grup existent!",
|
||||
"dont_join_group": "Voleu unir-vos al grup?",
|
||||
"login": "Inici de sessió",
|
||||
"disabled_registration": "El registre és desactivat",
|
||||
"register": "Registra-m'hi",
|
||||
"remember_me": "Recorda'm",
|
||||
"set_email": "Quin és el seu correu electrònic?",
|
||||
"set_name": "Com us dieu?",
|
||||
"set_password": "Definiu la contrasenya",
|
||||
"tagline": "Feu el seguiment, organitzeu i gestioneu les vostres coses."
|
||||
},
|
||||
"items": {
|
||||
"negate_labels": "Nega les etiquetes seleccionades",
|
||||
"no_results": "No s'ha trobat cap element",
|
||||
"tip_1": "Els filtres d'ubicació i etiquetes utilitzen l'operació «O». Si se'n selecciona més d'un, \nnomés se'n requerirà un per a coincidència.",
|
||||
"add": "Afegeix",
|
||||
"field_selector": "Selector del camp",
|
||||
"field_value": "Valor del camp",
|
||||
"first": "Primer",
|
||||
"include_archive": "Inclou els articles arxivats",
|
||||
"last": "Últim",
|
||||
"next_page": "Pàgina següent",
|
||||
"options": "Opcions",
|
||||
"order_by": "Ordena per",
|
||||
"tip_2": "Les cerques amb el prefix «#» sol·licitaran un ID d'un actiu (per exemple, «#000-001»)",
|
||||
"tip_3": "Els filtres de camp utilitzen l'operació «O». Si se'n selecciona més d'un, \nnomés se'n requerirà un per a coincidència.",
|
||||
"tips": "Consells",
|
||||
"tips_sub": "Consells de cerca",
|
||||
"updated_at": "Actualitzat a",
|
||||
"query_id": "S'està consultant el número d'identificació de l'actiu: { id }",
|
||||
"pages": "Pàgina { page } de { totalPages }",
|
||||
"prev_page": "Pàgina anterior",
|
||||
"reset_search": "Reinicia la cerca",
|
||||
"results": "{ total } resultats",
|
||||
"created_at": "Creat a",
|
||||
"custom_fields": "Camps personalitzats"
|
||||
},
|
||||
"profile": {
|
||||
"active": "Actiu",
|
||||
"change_password": "Canvia contrasenya",
|
||||
"delete_account_sub": "Elimina el compte i totes les dades associades. Aquesta acció no es pot desfer.",
|
||||
"enabled": "Habilitat",
|
||||
"currency_format": "Format de moneda",
|
||||
"current_password": "Contrasenya actual",
|
||||
"delete_account": "Suprimeix el compte",
|
||||
"gen_invite": "Genera un enllaç d'invitació",
|
||||
"group_settings": "Configuració del grup",
|
||||
"group_settings_sub": "Configuració del grup compartit. És possible que hàgiu d'actualitzar la pàgina per aplicar la configuració.",
|
||||
"theme_settings_sub": "La configuració del tema s'emmagatzema a l'emmagatzematge local del navegador. Podeu canviar el tema en qualsevol moment. \nSi teniu problemes per definir el tema, proveu d'actualitzar el navegador.",
|
||||
"notifier_modal": "{ type, select, true {Edita} false {Crea} other {Altres}} Notificació",
|
||||
"notifiers_sub": "Rebeu notificacions per als pròxims recordatoris de manteniment",
|
||||
"test": "Prova",
|
||||
"theme_settings": "Configuracions del tema",
|
||||
"update_group": "Actualitza el grup",
|
||||
"url": "URL",
|
||||
"user_profile_sub": "Convida usuaris i gestiona el compte.",
|
||||
"inactive": "Inactiu",
|
||||
"new_password": "Contrasenya nova",
|
||||
"notifiers": "Notificadors",
|
||||
"user_profile": "Perfil d'usuari"
|
||||
}
|
||||
}
|
||||
158
frontend/locales/de.json
Normal file
158
frontend/locales/de.json
Normal file
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"change_warning": "Das Verhalten beim Importieren vorhandener import_refs hat sich geändert. Wenn ein import_ref in der CSV-Datei vorhanden ist, \nwird der Gegenstand mit den Werten in der CSV-Datei aktualisiert.",
|
||||
"description": "Importiere eine CSV-Datei, die deine Gegenstände, Etiketten und Standorte enthält. Schau in die Dokumentation für weitere Informationen\nzum erforderlichen Format.",
|
||||
"title": "CSV-Datei importieren",
|
||||
"upload": "Hochladen"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "Seiten-URL"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Passwortstärke"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Gegenstand erstellen",
|
||||
"photo_button": "Foto 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Karte",
|
||||
"items": "Gegenstände",
|
||||
"no_items": "Keine Gegenstände anzuzeigen",
|
||||
"table": "Tabelle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Etikett erstellen"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Standort erstellen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Build: { build }",
|
||||
"confirm": "Bestätigen",
|
||||
"create": "Erstellen",
|
||||
"create_and_add": "Erstellen und weiteren hinzufügen",
|
||||
"created": "Erstellt",
|
||||
"email": "E-Mail",
|
||||
"follow_dev": "Dem Entwickler folgen",
|
||||
"github": "GitHub-Projekt",
|
||||
"items": "Gegenstände",
|
||||
"join_discord": "Discord beitreten",
|
||||
"labels": "Etiketten",
|
||||
"locations": "Lagerorte",
|
||||
"name": "Name",
|
||||
"password": "Passwort",
|
||||
"read_docs": "Dokumentation lesen",
|
||||
"search": "Suche",
|
||||
"sign_out": "Abmelden",
|
||||
"submit": "Einreichen",
|
||||
"version": "Version: { version }",
|
||||
"welcome": "Willkommen, { username }"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "Registrierung deaktiviert",
|
||||
"dont_join_group": "Möchtest du nicht einer Gruppe beitreten?",
|
||||
"joining_group": "Du trittst einer bereits bestehenden Gruppe bei!",
|
||||
"login": "Anmelden",
|
||||
"register": "Registrieren",
|
||||
"remember_me": "Angemeldet bleiben",
|
||||
"set_email": "Was ist deine E-Mail?",
|
||||
"set_name": "Wie heißt du?",
|
||||
"set_password": "Setze dein Passwort",
|
||||
"tagline": "Verfolgen, Organisieren und Verwalten deiner Sachen."
|
||||
},
|
||||
"items": {
|
||||
"add": "Hinzufügen",
|
||||
"created_at": "Erstellt am",
|
||||
"custom_fields": "Benutzerdefinierte Felder",
|
||||
"field_selector": "Feldauswahl",
|
||||
"field_value": "Feldwert",
|
||||
"first": "Erste",
|
||||
"include_archive": "Archivierte Elemente einschließen",
|
||||
"last": "Letzte",
|
||||
"negate_labels": "Ausgewählte Etiketten negieren",
|
||||
"next_page": "Nächste Seite",
|
||||
"no_results": "Keine Elemente gefunden",
|
||||
"options": "Optionen",
|
||||
"order_by": "Sortieren nach",
|
||||
"pages": "Seite { page } von { totalPages }",
|
||||
"prev_page": "Vorherige Seite",
|
||||
"query_id": "Abfrage Asset-ID-Nummer: { id }",
|
||||
"reset_search": "Suche zurücksetzen",
|
||||
"results": "{ total } Ergebnisse",
|
||||
"tip_1": "Standort- und Etikettenfilter verwenden die 'ODER'-Operation. Wenn mehr als eines ausgewählt ist, wird\n nur eines für eine Übereinstimmung benötigt.",
|
||||
"tip_2": "Suchen, die mit '#' beginnen, fragen nach einer Asset-ID (Beispiel '#000-001')",
|
||||
"tip_3": "Feldfilter verwenden die 'ODER'-Operation. Wenn mehr als eines ausgewählt ist, wird nur eines\n für eine Übereinstimmung benötigt.",
|
||||
"tips": "Tipps",
|
||||
"tips_sub": "Suchtipps",
|
||||
"updated_at": "Aktualisiert am"
|
||||
},
|
||||
"profile": {
|
||||
"active": "Aktiv",
|
||||
"change_password": "Passwort ändern",
|
||||
"currency_format": "Währungsformat",
|
||||
"current_password": "Aktuelles Passwort",
|
||||
"delete_account": "Konto löschen",
|
||||
"delete_account_sub": "Lösche dein Konto und alle zugehörigen Daten. Dies kann nicht rückgängig gemacht werden.",
|
||||
"enabled": "Aktiviert",
|
||||
"gen_invite": "Einladungslink generieren",
|
||||
"group_settings": "Gruppeneinstellungen",
|
||||
"group_settings_sub": "Geteilte Gruppeneinstellungen. Möglicherweise musst du die Seite neu laden, damit einige Einstellungen wirksam werden.",
|
||||
"inactive": "Inaktiv",
|
||||
"new_password": "Neues Passwort",
|
||||
"notifier_modal": "{ type, select, true {Bearbeiten} false {Erstellen} other {Andere}} Notifier",
|
||||
"notifiers": "Melder",
|
||||
"notifiers_sub": "Erhalte Benachrichtigungen über bevorstehende Wartungserinnerungen",
|
||||
"test": "Test",
|
||||
"theme_settings": "Themes",
|
||||
"theme_settings_sub": "Theme-Einstellungen werden im lokalen Speicher deines Browsers gespeichert. Du kannst das Theme jederzeit ändern. Wenn du Probleme hast, dein Theme einzustellen, versuche, die Seite neu zu laden.",
|
||||
"update_group": "Gruppe aktualisieren",
|
||||
"url": "URL",
|
||||
"user_profile": "Benutzerprofil",
|
||||
"user_profile_sub": "Lade Benutzer ein und verwalte dein Konto.",
|
||||
"language": "Sprache",
|
||||
"update_language": "Sprache ändern"
|
||||
},
|
||||
"tools": {
|
||||
"import_export_set": {
|
||||
"import_button": "Inventar importieren",
|
||||
"import": "Inventar importieren",
|
||||
"export_sub": "Exportiert das Standard-CSV-Format für Homebox. Damit werden alle Artikel in deinem Inventar exportiert.",
|
||||
"export": "Inventar exportieren",
|
||||
"import_sub": "Importiert das Standard-CSV-Format für Homebox. Ohne eine '<code>'HB.import_ref'</code>' Spalte werden vorhandenen Artikel in Ihrem Bestand '<b>'nicht'</b>' überschrieben, sondern nur neue Artikel hinzugefügt. Zeilen mit einer '<code>'HB.import_ref'</code>' Spalte werden mit vorhandenen Artikeln mit der gleichen import_ref zusammengeführt, sofern vorhanden."
|
||||
},
|
||||
"reports_set": {
|
||||
"bill_of_materials_button": "Stückliste generieren",
|
||||
"asset_labels_button": "Etikettengenerator",
|
||||
"bill_of_materials": "Stückliste",
|
||||
"bill_of_materials_sub": "Erzeugt eine CSV-Datei (Comma Separated Values), die in ein Tabellenkalkulationsprogramm importiert werden kann. Dies ist eine Zusammenfassung des Bestands mit grundlegenden Artikel- und Preisinformationen.",
|
||||
"asset_labels_sub": "Erzeugt eine druckbare PDF-Datei mit Etiketten für eine Reihe von Asset-IDs. Diese sind nicht spezifisch für deine Inventargegenstände, so dass du die Etiketten im Voraus ausdrucken und bei Erhalt auf deinen Inventargegenständen anbringen kannst.",
|
||||
"asset_labels": "Asset-ID-Etiketten"
|
||||
},
|
||||
"import_export": "Import/Export",
|
||||
"reports_sub": "Erstelle verschiedene Berichte für dein Inventar.",
|
||||
"import_export_sub": "Importieren und exportieren des Inventars in und aus einer CSV-Datei. Dies ist nützlich, um das Inventar auf eine neue Instanz von Homebox zu migrieren.",
|
||||
"reports": "Berichte",
|
||||
"actions": "Inventar Aktionen",
|
||||
"actions_sub": "Aktionen in großen Mengen auf das Inventar anwenden. Diese Aktionen sind unumkehrbar. '<b>'Sei vorsichtig.'</b>'",
|
||||
"actions_set": {
|
||||
"ensure_ids_button": "Sicherstellen von Asset-IDs",
|
||||
"ensure_ids": "Sicherstellen von Asset-IDs",
|
||||
"ensure_import_refs": "Sicherstellen, dass Import-Referenzen importiert wurden"
|
||||
}
|
||||
}
|
||||
}
|
||||
189
frontend/locales/en.json
Normal file
189
frontend/locales/en.json
Normal file
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"change_warning": "Behavior for imports with existing import_refs has changed. If an import_ref is present in the CSV file, the \nitem will be updated with the values in the CSV file.",
|
||||
"description": "Import a CSV file containing your items, labels, and locations. See documentation for more information on the \nrequired format.",
|
||||
"title": "Import CSV File",
|
||||
"upload": "Upload"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "Page URL"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Password Strength"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Create Item",
|
||||
"photo_button": "Photo 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Card",
|
||||
"items": "Items",
|
||||
"no_items": "No Items to Display",
|
||||
"table": "Table"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Create Label"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Create Location"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Build: { build }",
|
||||
"confirm": "Confirm",
|
||||
"create": "Create",
|
||||
"create_and_add": "Create and Add Another",
|
||||
"created": "Created",
|
||||
"email": "Email",
|
||||
"follow_dev": "Follow the Developer",
|
||||
"github": "GitHub Project",
|
||||
"items": "Items",
|
||||
"join_discord": "Join the Discord",
|
||||
"labels": "Labels",
|
||||
"locations": "Locations",
|
||||
"name": "Name",
|
||||
"password": "Password",
|
||||
"read_docs": "Read the Docs",
|
||||
"search": "Search",
|
||||
"sign_out": "Sign Out",
|
||||
"submit": "Submit",
|
||||
"version": "Version: { version }",
|
||||
"welcome": "Welcome, { username }"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "Registration Disabled",
|
||||
"dont_join_group": "Don't want to join a group?",
|
||||
"joining_group": "You're Joining an Existing Group!",
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"remember_me": "Remember Me",
|
||||
"set_email": "What's your email?",
|
||||
"set_name": "What's your name?",
|
||||
"set_password": "Set your password",
|
||||
"tagline": "Track, Organize, and Manage your Things."
|
||||
},
|
||||
"items": {
|
||||
"add": "Add",
|
||||
"created_at": "Created At",
|
||||
"custom_fields": "Custom Fields",
|
||||
"field_selector": "Field Selector",
|
||||
"field_value": "Field Value",
|
||||
"first": "First",
|
||||
"include_archive": "Include Archived Items",
|
||||
"last": "Last",
|
||||
"negate_labels": "Negate Selected Labels",
|
||||
"next_page": "Next Page",
|
||||
"no_results": "No Items Found",
|
||||
"options": "Options",
|
||||
"order_by": "Order By",
|
||||
"pages": "Page { page } of { totalPages }",
|
||||
"prev_page": "Previous Page",
|
||||
"query_id": "Querying Asset ID Number: { id }",
|
||||
"reset_search": "Reset Search",
|
||||
"results": "{ total } Results",
|
||||
"tip_1": "Location and label filters use the 'OR' operation. If more than one is selected only one will be\n required for a match.",
|
||||
"tip_2": "Searches prefixed with '#'' will query for a asset ID (example '#000-001')",
|
||||
"tip_3": "Field filters use the 'OR' operation. If more than one is selected only one will be required for a\n match.",
|
||||
"tips": "Tips",
|
||||
"tips_sub": "Search Tips",
|
||||
"updated_at": "Updated At"
|
||||
},
|
||||
"profile": {
|
||||
"active": "Active",
|
||||
"change_password": "Change Password",
|
||||
"currency_format": "Currency Format",
|
||||
"current_password": "Current Password",
|
||||
"delete_account": "Delete Account",
|
||||
"delete_account_sub": "Delete your account and all its associated data. This can not be undone.",
|
||||
"enabled": "Enabled",
|
||||
"gen_invite": "Generate Invite Link",
|
||||
"group_settings": "Group Settings",
|
||||
"group_settings_sub": "Shared Group Settings. You may need to refresh your browser for some settings to apply.",
|
||||
"inactive": "Inactive",
|
||||
"new_password": "New Password",
|
||||
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Notifier",
|
||||
"notifiers": "Notifiers",
|
||||
"notifiers_sub": "Get notifications for upcoming maintenance reminders",
|
||||
"test": "Test",
|
||||
"theme_settings": "Theme Settings",
|
||||
"theme_settings_sub": "Theme settings are stored in your browser's local storage. You can change the theme at any time. If you're\n having trouble setting your theme try refreshing your browser.",
|
||||
"update_group": "Update Group",
|
||||
"url": "URL",
|
||||
"user_profile": "User Profile",
|
||||
"user_profile_sub": "Invite users, and manage your account.",
|
||||
"display_header": "{ currentValue, select, true {Hide Header} false {Show Header} other {Not Hit}}",
|
||||
"language": "Language",
|
||||
"update_language": "Update Language"
|
||||
},
|
||||
"tools": {
|
||||
"reports": "Reports",
|
||||
"reports_sub": "Generate different reports for your inventory.",
|
||||
"reports_set": {
|
||||
"asset_labels": "Asset ID Labels",
|
||||
"asset_labels_sub": "Generates a printable PDF of labels for a range of Asset ID. These are not specific to your inventory so you are able to print labels ahead of time and apply them to your inventory when you receive them.",
|
||||
"asset_labels_button": "Label Generator",
|
||||
"bill_of_materials": "Bill of Materials",
|
||||
"bill_of_materials_sub": "Generates a CSV (Comma Separated Values) file that can be imported into a spreadsheet program. This is a summary of your inventory with basic item and pricing information.",
|
||||
"bill_of_materials_button": "Generate BOM"
|
||||
},
|
||||
"import_export": "Import/Export",
|
||||
"import_export_sub": "Import and export your inventory to and from a CSV file. This is useful for migrating your inventory to a new instance of Homebox.",
|
||||
"import_export_set": {
|
||||
"import": "Import Inventory",
|
||||
"import_sub": "Imports the standard CSV format for Homebox. Without an '<code>'HB.import_ref'</code>' column, this will '<b>'not'</b>' overwrite any existing items in your inventory, only add new items. Rows with an '<code>'HB.import_ref'</code>' column are merged into existing items with the same import_ref, if one exists.",
|
||||
"import_button": "Import Inventory",
|
||||
"export": "Export Inventory",
|
||||
"export_sub": "Exports the standard CSV format for Homebox. This will export all items in your inventory.",
|
||||
"export_button": "Export Inventory"
|
||||
},
|
||||
"actions": "Inventory Actions",
|
||||
"actions_sub": "Apply Actions to your inventory in bulk. These are irreversible actions. '<b>'Be careful.'</b>'",
|
||||
"actions_set": {
|
||||
"ensure_ids": "Ensure Asset IDs",
|
||||
"ensure_ids_sub": "Ensures that all items in your inventory have a valid asset_id field. This is done by finding the highest current asset_id field in the database and applying the next value to each item that has an unset asset_id field. This is done in order of the created_at field.",
|
||||
"ensure_ids_button": "Ensure Asset IDs",
|
||||
"ensure_import_refs": "Ensure Import Refs",
|
||||
"ensure_import_refs_sub": "Ensures that all items in your inventory have a valid import_ref field. This is done by randomly generating a 8 character string for each item that has an unset import_ref field.",
|
||||
"ensure_import_refs_button": "Ensure Import Refs",
|
||||
"zero_datetimes": "Zero Item Date Times",
|
||||
"zero_datetimes_sub": "Resets the time value for all date time fields in your inventory to the beginning of the date. This is to fix a bug that was introduced early on in the development of the site that caused the time value to be stored with the time which caused issues with date fields displaying accurate values. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'See Github Issue #236 for more details.'</a>'",
|
||||
"zero_datetimes_button": "Zero Item Date Times",
|
||||
"set_primary_photo": "Set Primary Photo",
|
||||
"set_primary_photo_sub": "In version v0.10.0 of Homebox, the primary image field was added to attachments of type photo. This action will set the primary image field to the first image in the attachments array in the database, if it is not already set. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'See GitHub PR #576'</a>'",
|
||||
"set_primary_photo_button": "Set Primary Photo"
|
||||
}
|
||||
},
|
||||
"languages": {
|
||||
"ca": "Catalan",
|
||||
"de": "German",
|
||||
"en": "English",
|
||||
"es": "Spanish",
|
||||
"fr": "French",
|
||||
"hu": "Hungarian",
|
||||
"it": "Italian",
|
||||
"nl": "Dutch",
|
||||
"pl": "Polish",
|
||||
"pt-BR": "Portuguese (Brazil)",
|
||||
"ru": "Russian",
|
||||
"sl": "Slovenian",
|
||||
"sv": "Swedish",
|
||||
"tr": "Turkish",
|
||||
"zh-CN": "Chinese (Simplified)",
|
||||
"zh-HK": "Chinese (Hong Kong)",
|
||||
"zh-MO": "Chinese (Macau)",
|
||||
"zh-TW": "Chinese (Traditional)"
|
||||
}
|
||||
}
|
||||
128
frontend/locales/es.json
Normal file
128
frontend/locales/es.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"change_warning": "Se ha modificado el comportamiento de las importaciones con import_refs existentes. Si existe una import_ref en el archivo CSV, el \nelemento se actualizará con los valores del archivo CSV.",
|
||||
"description": "Importa un archivo CSV que contenga tus elementos, etiquetas y ubicaciones. Consulta la documentación para obtener más información sobre el \nformato requerido.",
|
||||
"title": "Importar Archivo CSV",
|
||||
"upload": "Subir"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "URL de página"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Seguridad de la contraseña"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Crear Elemento",
|
||||
"photo_button": "Foto 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Tarjeta",
|
||||
"items": "Elementos",
|
||||
"no_items": "No hay elementos para mostrar",
|
||||
"table": "Tabla"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Crear Etiqueta"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Crear Ubicación"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Compilación: { build }",
|
||||
"confirm": "Confirmar",
|
||||
"create": "Crear",
|
||||
"created": "Creado",
|
||||
"email": "Email",
|
||||
"github": "Proyecto GitHub",
|
||||
"items": "Elementos",
|
||||
"join_discord": "Únete al Discord",
|
||||
"labels": "Etiquetas",
|
||||
"locations": "Ubicaciones",
|
||||
"name": "Nombre",
|
||||
"password": "Contraseña",
|
||||
"read_docs": "Lee la Documentación",
|
||||
"search": "Buscar",
|
||||
"sign_out": "Cerrar Sesión",
|
||||
"submit": "Enviar",
|
||||
"welcome": "Bienvenido/a, { username }",
|
||||
"create_and_add": "Crear y Añadir Otro",
|
||||
"follow_dev": "Seguir al Desarrollador",
|
||||
"version": "Versión: { version }"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "Registro Desactivado",
|
||||
"dont_join_group": "¿No quieres unirte a un grupo?",
|
||||
"login": "Iniciar sesión",
|
||||
"register": "Registrarse",
|
||||
"remember_me": "Recuérdame",
|
||||
"set_email": "¿Cuál es tu email?",
|
||||
"set_name": "¿Cómo te llamas?",
|
||||
"set_password": "Establece tu contraseña",
|
||||
"tagline": "Rastrea, Organiza y Gestiona tus Cosas.",
|
||||
"joining_group": "¡Te estás uniendo a un grupo existente!"
|
||||
},
|
||||
"items": {
|
||||
"add": "Añadir",
|
||||
"created_at": "Creado El",
|
||||
"custom_fields": "Campos Personalizados",
|
||||
"field_selector": "Selector de Campo",
|
||||
"field_value": "Valor del Campo",
|
||||
"first": "Primer",
|
||||
"include_archive": "Incluir Elementos Archivados",
|
||||
"last": "Último",
|
||||
"negate_labels": "Negar Etiquetas Seleccionadas",
|
||||
"next_page": "Siguiente Página",
|
||||
"no_results": "No se Encontraron Elementos",
|
||||
"options": "Opciones",
|
||||
"order_by": "Ordenar Por",
|
||||
"pages": "Página { page } de { totalPages }",
|
||||
"prev_page": "Anterior Página",
|
||||
"query_id": "Consultar Número ID del Activo: { id }",
|
||||
"reset_search": "Restablecer Búsqueda",
|
||||
"results": "{ total } Resultados",
|
||||
"tip_2": "Las búsquedas precedidas de \"#\" buscarán un ID de activo (por ejemplo, \"#000-001\")",
|
||||
"tip_3": "Los filtros de campo utilizan el operador \"OR\". Si se selecciona más de uno, sólo se requerirá uno para una\n coincidencia.",
|
||||
"tip_1": "Los filtros de ubicación y etiquetas utilizan el operador \"OR\". Si se selecciona más de uno, sólo uno será\n necesario para obtener una coincidencia.",
|
||||
"tips": "Sugerencias",
|
||||
"tips_sub": "Sugerencias de Búsqueda",
|
||||
"updated_at": "Actualizado El"
|
||||
},
|
||||
"profile": {
|
||||
"active": "Activo",
|
||||
"change_password": "Cambiar Contraseña",
|
||||
"currency_format": "Formato Divisa",
|
||||
"current_password": "Contraseña Actual",
|
||||
"delete_account": "Eliminar Cuenta",
|
||||
"enabled": "Habilitado",
|
||||
"gen_invite": "Generar Enlace de Invitación",
|
||||
"group_settings": "Ajustes de Grupo",
|
||||
"group_settings_sub": "Configuración de Grupo Compartido. Es posible que tengas que actualizar tu navegador para que se apliquen algunos ajustes.",
|
||||
"inactive": "Inactivo",
|
||||
"new_password": "Nueva Contraseña",
|
||||
"notifiers": "Notificaciones",
|
||||
"notifiers_sub": "Recibe notificaciones de los próximos recordatorios de mantenimiento",
|
||||
"test": "Probar",
|
||||
"theme_settings": "Ajustes de Tema",
|
||||
"update_group": "Actualizar Grupo",
|
||||
"url": "URL",
|
||||
"user_profile": "Perfil de Usuario",
|
||||
"user_profile_sub": "Invita a usuarios y gestiona tu cuenta.",
|
||||
"delete_account_sub": "Elimina tu cuenta y todos sus datos asociados. Esto no se puede deshacer.",
|
||||
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Notificación",
|
||||
"theme_settings_sub": "La configuración del tema se guarda en el almacenamiento local de tu navegador. Puedes cambiar el tema en cualquier momento. Si tienes\n problemas para configurar el tema, prueba a actualizar el navegador."
|
||||
}
|
||||
}
|
||||
138
frontend/locales/fr.json
Normal file
138
frontend/locales/fr.json
Normal file
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"global": {
|
||||
"email": "Courriel",
|
||||
"read_docs": "Lire la documentation",
|
||||
"password": "Mot de passe",
|
||||
"github": "Projet GitHub",
|
||||
"join_discord": "Rejoindre le Discord",
|
||||
"follow_dev": "Suivre le développeur",
|
||||
"version": "Version : { version }",
|
||||
"build": "Assemblage : { build }",
|
||||
"create": "Créer",
|
||||
"submit": "Soumettre",
|
||||
"create_and_add": "Créer et en ajouter un autre",
|
||||
"confirm": "Confirmer",
|
||||
"sign_out": "Déconnexion",
|
||||
"welcome": "Bienvenue, { username }",
|
||||
"created": "Créé",
|
||||
"labels": "Étiquettes",
|
||||
"locations": "Emplacements",
|
||||
"name": "Nom",
|
||||
"search": "Rechercher",
|
||||
"items": "Articles"
|
||||
},
|
||||
"index": {
|
||||
"set_email": "Quel est vôtre courriel ?",
|
||||
"set_name": "Quel est votre nom ?",
|
||||
"login": "Se connecter",
|
||||
"register": "S'enregistrer",
|
||||
"remember_me": "Se souvenir de moi",
|
||||
"set_password": "Définir votre mot de passe",
|
||||
"dont_join_group": "Vous ne voulez pas joindre un groupe ?",
|
||||
"tagline": "Répertoriez, organisez et gérez vos affaires.",
|
||||
"disabled_registration": "Les inscriptions sont désactivées",
|
||||
"joining_group": "Vous rejoignez un groupe existant !"
|
||||
},
|
||||
"components": {
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Créer une étiquette"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"view": {
|
||||
"selectable": {
|
||||
"items": "Articles",
|
||||
"no_items": "Nb. d’articles à afficher",
|
||||
"table": "Tableau",
|
||||
"card": "Carte"
|
||||
}
|
||||
},
|
||||
"create_modal": {
|
||||
"title": "Créer un article",
|
||||
"photo_button": "Photo 📷"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Créer un emplacement"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"password_score": {
|
||||
"password_strength": "Solidité du mot de passe"
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "URL de la page"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"title": "Importer un fichier CSV",
|
||||
"upload": "Téléverser",
|
||||
"description": "Importer un fichier CSV contenant vos articles, étiquettes, et emplacements. Voir la documentation pour plus d’informations sur le format requis.",
|
||||
"change_warning": "Le comportement lors d’importations avec des import_ref existants a changé. Si une valeur pour import_ref est présente dans le fichier CSV, alors l’élément sera mis à jour avec celle-ci."
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"notifiers_sub": "Recevez des notifications pour vous prévenir des prochaines maintenances.",
|
||||
"active": "Actif",
|
||||
"enabled": "Activé",
|
||||
"test": "Test",
|
||||
"current_password": "Mot de passe actuel",
|
||||
"new_password": "Nouveau mot de passe",
|
||||
"change_password": "Changer de mot de passe",
|
||||
"inactive": "Inactif",
|
||||
"notifiers": "Notifications",
|
||||
"gen_invite": "Générer un lien d’invitation",
|
||||
"user_profile": "Profil utilisateur",
|
||||
"user_profile_sub": "Gérez votre compte et invitez des utilisateurs.",
|
||||
"url": "URL",
|
||||
"delete_account": "Effacer le compte",
|
||||
"delete_account_sub": "Supprimer le compte et toutes ses données. Aucune récupération possible.",
|
||||
"group_settings": "Paramètres du groupe",
|
||||
"group_settings_sub": "Paramètres du groupe partagé. Il peut être nécessaire de recharger la page pour qu’ils deviennent effectifs.",
|
||||
"currency_format": "Format de la devise",
|
||||
"update_group": "Mettre à jour le groupe",
|
||||
"theme_settings": "Paramètres du thème",
|
||||
"theme_settings_sub": "Les paramètres du thème sont stockés dans le navigateur. Vous pouvez les changer à tout moment. Si vous\nrencontrez des problèmes, il est conseillé de rafraichir la page.",
|
||||
"notifier_modal": "Notifications { type, select, true {Edit} false {Create} other {Other}}",
|
||||
"update_language": "Mettre à jour la langue",
|
||||
"language": "Langue"
|
||||
},
|
||||
"items": {
|
||||
"add": "Ajouter",
|
||||
"created_at": "Créé à",
|
||||
"custom_fields": "Champs personnalisés",
|
||||
"field_selector": "Sélecteur de champ",
|
||||
"field_value": "Valeur du champ",
|
||||
"first": "Premier",
|
||||
"include_archive": "Inclure les éléments archivés",
|
||||
"last": "Dernier",
|
||||
"negate_labels": "Négliger les étiquettes sélectionnées",
|
||||
"next_page": "Page suivante",
|
||||
"no_results": "Aucun élément trouvé",
|
||||
"options": "Options",
|
||||
"order_by": "Trier par",
|
||||
"pages": "Page { page } sur { totalPages }",
|
||||
"prev_page": "Page précédente",
|
||||
"query_id": "Interrogation du numéro d'identification de l'actif : { id }",
|
||||
"reset_search": "Réinitialiser la recherche",
|
||||
"tip_1": "Les filtres d’emplacement et d’étiquette utilisent l’opérateur « OU ».\nUn seul des filtres n’a besoin de correspondre pour retourner un résultat.",
|
||||
"tip_2": "Les recherches préfixées par '#'' rechercheront un ID d'actif (exemple '#000-001')",
|
||||
"tip_3": "Les filtres de champ utilisent l’opérateur « OU ».\nUn seul des filtres n’a besoin de correspondre pour retourner un résultat.",
|
||||
"tips": "Conseils",
|
||||
"tips_sub": "Conseils pour la recherche",
|
||||
"updated_at": "Mis à jour le",
|
||||
"results": "{ total } résultats"
|
||||
},
|
||||
"tools": {
|
||||
"reports_set": {
|
||||
"bill_of_materials": "Nomenclature",
|
||||
"asset_labels_button": "Générateur d’étiquettes"
|
||||
},
|
||||
"reports": "Rapports",
|
||||
"reports_sub": "Générez différents rapports pour votre inventaire."
|
||||
}
|
||||
}
|
||||
84
frontend/locales/hu.json
Normal file
84
frontend/locales/hu.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"upload": "Feltöltés",
|
||||
"title": "CSV fájl importálása"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "Oldal URL"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Jelszó erőssége"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Tétel létrehozása",
|
||||
"photo_button": "Fénykép 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Kártya",
|
||||
"items": "Tételek",
|
||||
"no_items": "Nincs megjeleníthető tétel",
|
||||
"table": "Táblázat"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Címke létrehozása"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Helyszín létrehozása"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Build: { build }",
|
||||
"confirm": "Megerősítés",
|
||||
"create": "Létrehozás",
|
||||
"create_and_add": "Létrehozás és újabb hozzáadása",
|
||||
"created": "Létrehozva",
|
||||
"email": "Email",
|
||||
"follow_dev": "Fejlesztő követése",
|
||||
"github": "GitHub Projekt",
|
||||
"items": "Tételek",
|
||||
"join_discord": "Csatlakozás a Discordhoz",
|
||||
"labels": "Címkék",
|
||||
"locations": "Helyszínek",
|
||||
"name": "Név",
|
||||
"password": "Jelszó",
|
||||
"read_docs": "Dokumentáció elolvasása",
|
||||
"search": "Keresés",
|
||||
"sign_out": "Kilépés",
|
||||
"submit": "Elküldés",
|
||||
"version": "Verzió: { version }",
|
||||
"welcome": "Üdv, { username }"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "Regisztráció kikapcsolva",
|
||||
"dont_join_group": "Nem akarsz csoporthoz csatlakozni?",
|
||||
"joining_group": "Egy létező csoporthoz csatlakozol!",
|
||||
"login": "Belépés",
|
||||
"register": "Regisztráció",
|
||||
"remember_me": "Emlékezzen rám",
|
||||
"set_email": "Mi az email címed?",
|
||||
"set_name": "Mi a neved?",
|
||||
"set_password": "Jelszó beállítása",
|
||||
"tagline": "Kövesd, rendszerezd és igazgasd a tárgyaidat."
|
||||
},
|
||||
"items": {
|
||||
"add": "Hozzáadás",
|
||||
"created_at": "Létrehozás dátuma",
|
||||
"field_selector": "Mezőválasztó",
|
||||
"field_value": "Mező értéke",
|
||||
"first": "Első",
|
||||
"custom_fields": "Egyedi mezők"
|
||||
}
|
||||
}
|
||||
127
frontend/locales/it.json
Normal file
127
frontend/locales/it.json
Normal file
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"change_warning": "Il comportamento per le importazioni con import_ref esistenti è cambiato. Se un import_ref è presente nel file CSV,\n l'elemento verrà aggiornato con i valori presenti nel file CSV.",
|
||||
"description": "Importa un file CSV contenente gli oggetti, le etichette e le posizioni. Vedi la documentazione per ulteriori informazioni sul \nformato richiesto.",
|
||||
"title": "Importa File CSV",
|
||||
"upload": "Carica"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "URL della Pagina"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Complessità della Password"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Crea Oggetto"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Scheda",
|
||||
"items": "Oggetti",
|
||||
"no_items": "Nessun Oggetto da Visualizzare",
|
||||
"table": "Tabella"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Crea Etichetta"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Crea Posizione"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Build: { build }",
|
||||
"confirm": "Conferma",
|
||||
"create": "Crea",
|
||||
"create_and_add": "Crea e aggiungi un altro",
|
||||
"created": "Creato",
|
||||
"email": "Email",
|
||||
"follow_dev": "Segui lo Sviluppatore",
|
||||
"github": "Progetto GitHub",
|
||||
"items": "Articoli",
|
||||
"join_discord": "Unisciti a Discord",
|
||||
"labels": "Etichette",
|
||||
"locations": "Posizioni",
|
||||
"name": "Nome",
|
||||
"password": "Password",
|
||||
"read_docs": "Leggi la Documentazione",
|
||||
"search": "Cerca",
|
||||
"sign_out": "Disconnetti",
|
||||
"submit": "Invia",
|
||||
"version": "Versione: { version }",
|
||||
"welcome": "Benvenuto, { username }"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "Registrazione Disabilitata",
|
||||
"dont_join_group": "Non vuoi unirti a un gruppo?",
|
||||
"joining_group": "Stai unendoti a un gruppo esistente!",
|
||||
"login": "Accedi",
|
||||
"register": "Registrati",
|
||||
"remember_me": "Ricordami",
|
||||
"set_email": "Qual è la tua email?",
|
||||
"set_name": "Come ti chiami?",
|
||||
"set_password": "Imposta la tua password",
|
||||
"tagline": "Tieni traccia, Organizza e Gestisci le tue Cose."
|
||||
},
|
||||
"items": {
|
||||
"add": "Aggiungi",
|
||||
"created_at": "Creato Il",
|
||||
"custom_fields": "Campi Personalizzati",
|
||||
"field_selector": "Selettore di Campo",
|
||||
"field_value": "Valore del Campo",
|
||||
"first": "Primo",
|
||||
"include_archive": "Includi Articoli Archiviati",
|
||||
"last": "Ultimo",
|
||||
"negate_labels": "Negare Etichette Selezionate",
|
||||
"next_page": "Pagina Successiva",
|
||||
"no_results": "Nessun Articolo Trovato",
|
||||
"options": "Opzioni",
|
||||
"order_by": "Ordina Per",
|
||||
"pages": "Pagina { page } di { totalPages }",
|
||||
"prev_page": "Pagina Precedente",
|
||||
"query_id": "ID dell'Asset in Ricerca: { id }",
|
||||
"reset_search": "Reimposta Ricerca",
|
||||
"results": "{ total } Risultati",
|
||||
"tip_1": "I filtri di posizione ed etichetta utilizzano l'operazione 'OR'. Se ne viene selezionato più\n di uno, ne sarà richiesto solo uno per una corrispondenza.",
|
||||
"tip_2": "Le ricerche con prefisso '#' cercheranno un ID asset (esempio '#000-001')",
|
||||
"tip_3": "I filtri di campo utilizzano l'operazione 'OR'. Se ne viene selezionato più di uno, ne sarà\n richiesto solo uno per una corrispondenza.",
|
||||
"tips": "Suggerimenti",
|
||||
"tips_sub": "Suggerimenti per la Ricerca",
|
||||
"updated_at": "Aggiornato Il"
|
||||
},
|
||||
"profile": {
|
||||
"active": "Attivo",
|
||||
"change_password": "Cambia Password",
|
||||
"currency_format": "Formato Valuta",
|
||||
"current_password": "Password Corrente",
|
||||
"delete_account": "Elimina Account",
|
||||
"delete_account_sub": "Elimina il tuo account e tutti i dati associati. Questa operazione non può essere annullata.",
|
||||
"enabled": "Abilitato",
|
||||
"gen_invite": "Genera Link di Invito",
|
||||
"group_settings": "Impostazioni Gruppo",
|
||||
"group_settings_sub": "Impostazioni Gruppo Condivise. Potrebbe essere necessario aggiornare il browser affinché alcune impostazioni vengano applicate.",
|
||||
"inactive": "Inattivo",
|
||||
"new_password": "Nuova Password",
|
||||
"notifier_modal": "{ type, select, true {Modifica} false {Crea} other {Altro}} Notifier",
|
||||
"notifiers": "Notifiche",
|
||||
"notifiers_sub": "Ricevi notifiche per i prossimi promemoria di manutenzione",
|
||||
"test": "Test",
|
||||
"theme_settings": "Impostazioni Tema",
|
||||
"theme_settings_sub": "Le impostazioni del tema sono memorizzate nella memoria locale del tuo browser. Puoi cambiare il tema \nin qualsiasi momento. Se hai problemi a impostare il tuo tema, prova a ricaricare la pagina.",
|
||||
"update_group": "Aggiorna Gruppo",
|
||||
"url": "URL",
|
||||
"user_profile": "Profilo Utente",
|
||||
"user_profile_sub": "Invita utenti e gestisci il tuo account."
|
||||
}
|
||||
}
|
||||
128
frontend/locales/nl.json
Normal file
128
frontend/locales/nl.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"global": {
|
||||
"version": "Versie: { version }",
|
||||
"github": "GitHub Project",
|
||||
"join_discord": "Sluit je aan bij de Discord",
|
||||
"follow_dev": "Volg de ontwikkelaar",
|
||||
"read_docs": "Lees de documentatie",
|
||||
"email": "E-mail",
|
||||
"submit": "Indienen",
|
||||
"confirm": "Bevestigen",
|
||||
"create": "Maken",
|
||||
"create_and_add": "Maak en voeg nog een toe",
|
||||
"password": "Wachtwoord",
|
||||
"build": "Bouw: { build }",
|
||||
"created": "Gemaakt",
|
||||
"welcome": "Welkom, { username }",
|
||||
"sign_out": "Log uit",
|
||||
"labels": "etiketten",
|
||||
"locations": "Locaties",
|
||||
"name": "Naam",
|
||||
"items": "artikelen",
|
||||
"search": "Zoeken"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "Registratie uitgeschakeld",
|
||||
"login": "Log in",
|
||||
"register": "Registreer",
|
||||
"remember_me": "Onthoud mij",
|
||||
"set_email": "Wat is je mailadres?",
|
||||
"set_password": "Stel je wachtwoord in",
|
||||
"set_name": "Wat is je naam?",
|
||||
"joining_group": "Je neemt deel aan een bestaande groep!",
|
||||
"tagline": "Volg, Organiseer en beheer je dingen.",
|
||||
"dont_join_group": "Wil je niet aan een groep deelnemen?"
|
||||
},
|
||||
"components": {
|
||||
"global": {
|
||||
"password_score": {
|
||||
"password_strength": "Wachtwoord sterkte"
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "Pagina URL"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"title": "Importeer CSV bestand",
|
||||
"change_warning": "Gedrag voor importeren met bestaande import_refs is veranderd. Als een import_refs reeds bestaat in het CSV bestand, het\nobject zal worden aangepast met de waardes van het CSV bestand.",
|
||||
"upload": "Upload",
|
||||
"description": "Importeer een CSV bestand met je objecten, labels en locaties. Zie documentatie voor meer informatie m.b.t.\n vereist format."
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Maak object",
|
||||
"photo_button": "Foto 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"items": "Objecten",
|
||||
"card": "Kaart",
|
||||
"table": "Tabel",
|
||||
"no_items": "Geen objecten om te tonen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Maak label"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Maak locatie"
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"gen_invite": "Genereer Uitnodigingslink",
|
||||
"notifier_modal": "{ type, select, true {Bewerk} false {Creëer} other {overig}} Notifier",
|
||||
"change_password": "Verander Wachtwoord",
|
||||
"current_password": "Huidig Wachtwoord",
|
||||
"new_password": "Nieuw Wachtwoord",
|
||||
"url": "URL",
|
||||
"enabled": "ingeschakeld",
|
||||
"test": "Test",
|
||||
"user_profile": "Gebruikers Profiel",
|
||||
"user_profile_sub": "Nodig gebruikers uit, en beheer je account.",
|
||||
"active": "Actief",
|
||||
"inactive": "Inactief",
|
||||
"notifiers_sub": "Krijg notificaties voor opkomende onderhouds herinneringen",
|
||||
"group_settings_sub": "Gedeelde groepsinstellingen",
|
||||
"group_settings": "Groeps Instellingen",
|
||||
"notifiers": "Notificatie",
|
||||
"currency_format": "Valutanotatie",
|
||||
"update_group": "Groep bijwerken",
|
||||
"theme_settings": "Theme instellingen",
|
||||
"theme_settings_sub": "Thema-instellingen worden opgeslagen in de lokale opslag van uw browser. Je kan deze wijzigen op elk moment. \nAls je problemen hebt met de instellingen kun je je browser verversen.",
|
||||
"delete_account": "Verwijder account",
|
||||
"delete_account_sub": "Verwijder je account en alle geassocieerde data. Deze actie kan niet ongedaan worden."
|
||||
},
|
||||
"items": {
|
||||
"tip_1": "Locatie- en labelfilters gebruiken de 'OF' -werking. Als er meer dan een is geselecteerd,\nis er maar een nodig voor een overeenkomst.",
|
||||
"tip_2": "Zoekopdrachten voorafgegaan door '#'' zullen om een object-ID vragen (bijvoorbeeld '#000-001')",
|
||||
"tip_3": "Veldfilters gebruiken de 'OF' -bewerking. Indien meer dan 1 is geselecteerd\nzal er maar 1 nodig zijn voor een match.",
|
||||
"tips_sub": "Zoektips",
|
||||
"updated_at": "Bijgewerkt op",
|
||||
"pages": "Pagina { page } van { totalPages }",
|
||||
"prev_page": "Vorige pagina",
|
||||
"query_id": "ID-nummer van object opvragen: { id }",
|
||||
"reset_search": "Reset Zoeken",
|
||||
"tips": "Tips",
|
||||
"no_results": "Geen Items Gevonden",
|
||||
"options": "Opties",
|
||||
"order_by": "Sorteren op",
|
||||
"results": "{ total } Resultaten",
|
||||
"add": "Toevoegen",
|
||||
"created_at": "Aangemaakt op",
|
||||
"custom_fields": "Aangepaste velden",
|
||||
"field_selector": "Veld selectie",
|
||||
"field_value": "Veldwaarde",
|
||||
"first": "Eerst",
|
||||
"include_archive": "Inclusief gearchiveerde items",
|
||||
"last": "Achternaam",
|
||||
"negate_labels": "Negeer Geselecteerde Etiketten",
|
||||
"next_page": "Volgende pagina"
|
||||
}
|
||||
}
|
||||
128
frontend/locales/pl.json
Normal file
128
frontend/locales/pl.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"description": "Zaimportuj plik CSV zawierający Twoje przedmioty, etykiety i lokalizacje. Zobacz dokumentację, aby uzyskać \nwięcej informacji na temat wymaganego formatu.",
|
||||
"title": "Zaimportuj plik CSV",
|
||||
"upload": "Prześlij",
|
||||
"change_warning": "Zachowanie przy imporcie z istniejącymi import_ref zostało zmienione. Jeśli import_ref jest obecny w pliku CSV, \nprzedmiot zostanie zaktualizowany zgodnie z wartościami w pliku CSV."
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "Adres URL strony"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Siła hasła"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Utwórz przedmiot",
|
||||
"photo_button": "Zdjęcie 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Karta",
|
||||
"items": "Przedmioty",
|
||||
"no_items": "Brak przedmiotów do wyświetlenia",
|
||||
"table": "Tabela"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Stwórz nową etykietę"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Utwórz lokalizację"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Kompilacja: {build}",
|
||||
"follow_dev": "Śledź dewelopera",
|
||||
"github": "Projekt na GitHubie",
|
||||
"items": "Przedmioty",
|
||||
"version": "Wersja:{version}",
|
||||
"welcome": "Witaj, {username}",
|
||||
"confirm": "Potwierdź",
|
||||
"create": "Utwórz",
|
||||
"create_and_add": "Utwórz i dodaj kolejny",
|
||||
"created": "Utworzone",
|
||||
"email": "E-mail",
|
||||
"join_discord": "Dołącz do Discorda",
|
||||
"labels": "Etykiety",
|
||||
"locations": "Lokalizacje",
|
||||
"name": "Nazwa",
|
||||
"password": "Hasło",
|
||||
"read_docs": "Przeczytaj dokumentację",
|
||||
"search": "Wyszukaj",
|
||||
"sign_out": "Wyloguj się",
|
||||
"submit": "Wyślij"
|
||||
},
|
||||
"index": {
|
||||
"set_password": "Ustaw swoje hasło",
|
||||
"dont_join_group": "Nie chcesz dołączyć do grupy?",
|
||||
"disabled_registration": "Rejestracja jest wyłączona",
|
||||
"joining_group": "Dołączasz do istniejącej grupy!",
|
||||
"login": "Zaloguj się",
|
||||
"register": "Zarejestruj się",
|
||||
"remember_me": "Zapamiętaj mnie",
|
||||
"set_email": "Jaki jest Twój adres e-mail?",
|
||||
"set_name": "Jak się nazywasz?",
|
||||
"tagline": "Śledź, organizuj i zarządzaj swoimi rzeczami."
|
||||
},
|
||||
"items": {
|
||||
"created_at": "Data utworzenia",
|
||||
"field_selector": "Selektor pól",
|
||||
"field_value": "Wartość pola",
|
||||
"first": "Pierwszy",
|
||||
"include_archive": "Uwzględnij zarchiwizowane przedmioty",
|
||||
"negate_labels": "Neguj wybrane etykiety",
|
||||
"no_results": "Nie znaleziono przedmiotów",
|
||||
"query_id": "Zapytanie o numer identyfikacyjny zasobu: { id }",
|
||||
"results": "{ total } wyniki",
|
||||
"tip_3": "Filtry pól używają operacji 'LUB'. Jeśli wybrano więcej niż jeden, wystarczy jeden, \naby uzyskać dopasowanie.",
|
||||
"updated_at": "Zaktualizowano",
|
||||
"tip_1": "Filtry lokalizacji i etykiet używają operacji 'LUB'. Jeśli wybrano więcej niż jeden, wystarczy jeden, \naby uzyskać dopasowanie.",
|
||||
"pages": "Strona {page} z {totalPages}",
|
||||
"add": "Dodaj",
|
||||
"custom_fields": "Pola niestandardowe",
|
||||
"last": "Ostatni",
|
||||
"next_page": "Następna strona",
|
||||
"options": "Opcje",
|
||||
"prev_page": "Poprzednia strona",
|
||||
"reset_search": "Zresetuj wyszukiwanie",
|
||||
"tip_2": "Wyszukiwania poprzedzone prefiksem \"#\" będą wysyłać zapytanie o identyfikator zasobu (na przykład \"#000-001\")",
|
||||
"tips": "Wskazówki",
|
||||
"tips_sub": "Wskazówki wyszukiwania",
|
||||
"order_by": "Ułóż według"
|
||||
},
|
||||
"profile": {
|
||||
"active": "Aktywny",
|
||||
"change_password": "Zmiana hasła",
|
||||
"currency_format": "Format waluty",
|
||||
"delete_account": "Usuń konto",
|
||||
"delete_account_sub": "Usuń swoje konto oraz wszystkie powiązane z nim dane. Tego nie można cofnąć.",
|
||||
"current_password": "Bieżące hasło",
|
||||
"group_settings_sub": "Ustawienia grupy udostępnione. Możesz potrzebować odświeżyć przeglądarkę, aby niektóre ustawienia zostały zastosowane.",
|
||||
"inactive": "Nieaktywny",
|
||||
"new_password": "Nowe hasło",
|
||||
"notifier_modal": "{type, select, true {Edytuj} false {Utwórz} other {Inny}} Powiadomiacz",
|
||||
"enabled": "Włączone",
|
||||
"gen_invite": "Wygeneruj link z zaproszeniem",
|
||||
"group_settings": "Ustawienia grupy",
|
||||
"notifiers": "Powiadomiacze",
|
||||
"notifiers_sub": "Otrzymuj powiadomienia o nadchodzących przypomnieniach o konserwacji",
|
||||
"theme_settings_sub": "Ustawienia motywu są przechowywane w lokalnej pamięci przeglądarki. Możesz zmienić motyw w dowolnym momencie. \nJeśli masz problemy z ustawieniem motywu, spróbuj odświeżyć przeglądarkę.",
|
||||
"test": "Test",
|
||||
"theme_settings": "Ustawienia tematu",
|
||||
"update_group": "Zaktualizuj grupę",
|
||||
"url": "Adres URL",
|
||||
"user_profile": "Profil użytkownika",
|
||||
"user_profile_sub": "Zaproś użytkowników i zarządzaj swoim kontem."
|
||||
}
|
||||
}
|
||||
1
frontend/locales/pt-BR.json
Normal file
1
frontend/locales/pt-BR.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
128
frontend/locales/ru.json
Normal file
128
frontend/locales/ru.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"title": "Импорт CSV файла",
|
||||
"change_warning": "Изменено поведение для импортов с существующими import_ref. Если в CSV-файле присутствует import_ref, \nто элемент будет обновлен значениями из CSV-файла.",
|
||||
"description": "Импортируйте CSV-файл, содержащий ваши предметы, ярлыки и местоположения. Для получения информации о требуемом формате \nсм. документацию.",
|
||||
"upload": "Загрузить"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "URL-адрес страницы"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Сложность пароля"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Создать элемент",
|
||||
"photo_button": "Фото 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"items": "Элементы",
|
||||
"no_items": "Нет элементов для отображения",
|
||||
"table": "Таблица",
|
||||
"card": "Карта"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Создать локацию"
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Создать ярлык"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"email": "Email",
|
||||
"items": "Элементы",
|
||||
"build": "Сборка: { build }",
|
||||
"confirm": "Подтвердить",
|
||||
"create": "Создать",
|
||||
"create_and_add": "Создать и добавить еще",
|
||||
"created": "Создано",
|
||||
"follow_dev": "Следить за разработчиком",
|
||||
"github": "Проект Github",
|
||||
"labels": "Ярлыки",
|
||||
"join_discord": "Присоединяйтесь к Discord",
|
||||
"locations": "Локации",
|
||||
"name": "Имя",
|
||||
"password": "Пароль",
|
||||
"read_docs": "Прочитать документацию",
|
||||
"search": "Поиск",
|
||||
"sign_out": "Выйти",
|
||||
"submit": "Отправить",
|
||||
"version": "Версия: { version }",
|
||||
"welcome": "Добро пожаловать, { username }"
|
||||
},
|
||||
"index": {
|
||||
"login": "Войти",
|
||||
"register": "Зарегистрироваться",
|
||||
"set_email": "Какой у вас адрес электронной почты?",
|
||||
"set_name": "Как вас зовут?",
|
||||
"set_password": "Установите пароль",
|
||||
"tagline": "Отслеживайте, упорядочивайте и управляйте своими вещами.",
|
||||
"disabled_registration": "Регистрация отключена",
|
||||
"dont_join_group": "Не хотите ли вступить в группу?",
|
||||
"remember_me": "Запомнить меня",
|
||||
"joining_group": "Вы присоединяетесь к уже существующей группе!"
|
||||
},
|
||||
"items": {
|
||||
"add": "Добавить",
|
||||
"created_at": "Создано в",
|
||||
"custom_fields": "Настраиваемые поля",
|
||||
"first": "Первый",
|
||||
"pages": "Страница {page} из {totalPages}",
|
||||
"prev_page": "Предыдущая страница",
|
||||
"reset_search": "Сбросить Поиск",
|
||||
"field_selector": "Поле выбора",
|
||||
"field_value": "Значение поля",
|
||||
"last": "Последний",
|
||||
"negate_labels": "Снять выбранные ярлыки",
|
||||
"next_page": "Следующая страница",
|
||||
"no_results": "Элементы не найдены",
|
||||
"options": "Параметры",
|
||||
"results": "{ total } Результатов",
|
||||
"tips": "Подсказки",
|
||||
"tips_sub": "Поисковые подсказки",
|
||||
"updated_at": "Обновлено в",
|
||||
"tip_3": "Фильтры по полю используют операцию «ИЛИ». Если выбрано несколько фильтров, для совпадения\n требуется только один.",
|
||||
"tip_2": "Поисковые запросы с префиксом \"#\" должны включать в себя ID актива (прим. '#000-001')",
|
||||
"include_archive": "Включить архивированные элементы",
|
||||
"order_by": "Сортировка по",
|
||||
"query_id": "Запрос идентификационного номера актива: { id }",
|
||||
"tip_1": "При фильтрации по локации и по ярлыкам используется логический оператор «ИЛИ». Если выбрано несколько фильтров, то для срабатывания\n требуется лишь одно совпадение."
|
||||
},
|
||||
"profile": {
|
||||
"new_password": "Новый пароль",
|
||||
"update_group": "Обновить группу",
|
||||
"active": "Активный",
|
||||
"change_password": "Изменить пароль",
|
||||
"currency_format": "Формат валюты",
|
||||
"current_password": "Текущий пароль",
|
||||
"delete_account": "Удалить аккаунт",
|
||||
"group_settings": "Настройки группы",
|
||||
"inactive": "Неактивный",
|
||||
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Уведомитель",
|
||||
"notifiers": "Уведомители",
|
||||
"notifiers_sub": "Получить уведомление о предстоящем обслуживании",
|
||||
"test": "Тест",
|
||||
"theme_settings": "Настройки темы",
|
||||
"url": "URL",
|
||||
"user_profile": "Профиль пользователя",
|
||||
"user_profile_sub": "Приглашайте пользователей и управляйте своим аккаунтом.",
|
||||
"enabled": "Активен",
|
||||
"theme_settings_sub": "Настройки темы хранятся в локальном хранилище браузера. Вы можете изменить тему в любое время. Если у вас\n не удается установить тему, попробуйте перезапустить браузер.",
|
||||
"delete_account_sub": "Удалить свой аккаунт и все связанные с ним данные. Это действие невозможно отменить.",
|
||||
"gen_invite": "Сгенерировать ссылку-приглашение",
|
||||
"group_settings_sub": "Настройки общей группы. Для применения изменений возможно потребуется перезагрузить страницу."
|
||||
}
|
||||
}
|
||||
128
frontend/locales/sl.json
Normal file
128
frontend/locales/sl.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"global": {
|
||||
"follow_dev": "Sledi razvijjalcu",
|
||||
"build": "Gradnja: [ build ]",
|
||||
"confirm": "Potrdi",
|
||||
"create": "Ustvari",
|
||||
"create_and_add": "Ustvari in dodaj še enega",
|
||||
"created": "Ustvarjeno",
|
||||
"email": "E-pošta",
|
||||
"github": "GitHub projekt",
|
||||
"items": "Predmeti",
|
||||
"join_discord": "Pridruži se na Discord",
|
||||
"labels": "Oznake",
|
||||
"locations": "Lokacije",
|
||||
"name": "Naziv",
|
||||
"password": "Geslo",
|
||||
"read_docs": "Preberite dokumentacijo",
|
||||
"search": "Iskanje",
|
||||
"sign_out": "Odjava",
|
||||
"submit": "Pošlji",
|
||||
"version": "Verzija: { version }",
|
||||
"welcome": "Dobrodošel, { username }"
|
||||
},
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"change_warning": "Vedenje pri uvozih z obstoječimi import_refs se je spremenilo. Če je import_ref prisoten v datoteki CSV, bo\nelement posodobljen z vrednostmi v datoteki CSV.",
|
||||
"description": "Uvozite datoteko CSV, ki vsebuje vaše predmete, oznake in lokacije. Poglejte si dokumentacijo za več informacij o\nzahtevani obliki.",
|
||||
"title": "Uvozi CSV datoteko",
|
||||
"upload": "Naloži"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "URL strani"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Moč gesla"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Ustvari predmet",
|
||||
"photo_button": "Fotografija 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Kartica",
|
||||
"items": "Predmeti",
|
||||
"no_items": "Ni predmetov za prikaz",
|
||||
"table": "Tabela"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Ustvari oznako"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Ustvari lokacijo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "Registracija je onemogočena",
|
||||
"dont_join_group": "Se ne želite pridružiti skupini?",
|
||||
"joining_group": "Pridružujete se obstoječi skupini!",
|
||||
"login": "Prijava",
|
||||
"register": "Registriraj se",
|
||||
"remember_me": "Zapomni si me",
|
||||
"set_email": "Kakšen je vaš e-poštni naslov?",
|
||||
"set_password": "Nastavite geslo",
|
||||
"tagline": "Sledite, organizirajte in upravljajte svoje stvari.",
|
||||
"set_name": "Kako vam je ime?"
|
||||
},
|
||||
"items": {
|
||||
"created_at": "Ustvarjeno ob",
|
||||
"include_archive": "Vključi arhivirane predmete",
|
||||
"last": "Zadnji",
|
||||
"negate_labels": "Negiraj izbrane oznake",
|
||||
"no_results": "Ni najdenih predmetov",
|
||||
"options": "Možnosti",
|
||||
"pages": "Stran { page } od { totalPages }",
|
||||
"tip_2": "Iskanja s predpono '#' bodo iskala ID sredstva (primer '#000-001')",
|
||||
"tip_3": "Filtri polj uporabljajo operacijo 'ALI'. Če je izbranih več kot eno, bo za ujemanje dovolj samo\n eno.",
|
||||
"tips": "Nasveti",
|
||||
"updated_at": "Posodobljeno ob",
|
||||
"custom_fields": "Polje po meri",
|
||||
"results": "Rezultatov: { total }",
|
||||
"tip_1": "Filtri lokacij in oznak uporabljajo operacijo 'ALI'. Če je izbranih več kot ena, bo izbrana samo ena\n potrebna za ujemanje.",
|
||||
"add": "Dodaj",
|
||||
"field_selector": "Izbirnik polj",
|
||||
"field_value": "Vrednost polja",
|
||||
"first": "Prvi",
|
||||
"next_page": "Naslednja stran",
|
||||
"prev_page": "Prejšnja stran",
|
||||
"query_id": "Poizvedovanje po identifikacijski številki sredstva: { id }",
|
||||
"reset_search": "Ponastavi iskanje",
|
||||
"tips_sub": "Nasveti za iskanje",
|
||||
"order_by": "Razvrsti po"
|
||||
},
|
||||
"profile": {
|
||||
"gen_invite": "Ustvari povezavo povabila",
|
||||
"group_settings_sub": "Nastavitve skupine v skupni rabi. Morda boste morali osvežiti brskalnik, da bodo nekatere nastavitve veljale.",
|
||||
"notifiers": "Obveščevalci",
|
||||
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Obveščevalec",
|
||||
"notifiers_sub": "Prejemajte obvestila za prihajajoče opomnike o vzdrževanju",
|
||||
"theme_settings_sub": "Nastavitve teme so shranjene v lokalni shrambi vašega brskalnika. Temo lahko kadar koli spremenite. Če\n imate težave z nastavitvijo teme, poskusite osvežiti brskalnik.",
|
||||
"update_group": "Posodobi skupino",
|
||||
"url": "URL",
|
||||
"delete_account_sub": "Izbrišite svoj račun in vse z njim povezane podatke. Tega ni mogoče razveljaviti.",
|
||||
"active": "Aktivno",
|
||||
"change_password": "Sprememba gesla",
|
||||
"currency_format": "Oblika valute",
|
||||
"current_password": "Trenutno geslo",
|
||||
"delete_account": "Izbriši račun",
|
||||
"enabled": "Omogočeno",
|
||||
"group_settings": "Nastavitve skupine",
|
||||
"inactive": "Neaktivno",
|
||||
"new_password": "Novo geslo",
|
||||
"test": "Preizkus",
|
||||
"theme_settings": "Nastavitve teme",
|
||||
"user_profile": "Profil uporabnika",
|
||||
"user_profile_sub": "Povabite uporabnike in upravljajte svoj račun."
|
||||
}
|
||||
}
|
||||
128
frontend/locales/sv.json
Normal file
128
frontend/locales/sv.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"profile": {
|
||||
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Anmälare",
|
||||
"change_password": "Ändra Lösenord",
|
||||
"current_password": "Nuvarande lösenord",
|
||||
"new_password": "Nytt lösenord",
|
||||
"notifiers_sub": "Få aviseringar om kommande underhållspåminnelser",
|
||||
"url": "URL",
|
||||
"test": "Test",
|
||||
"gen_invite": "Skapa inbjudningslänk",
|
||||
"user_profile": "Användarprofil",
|
||||
"user_profile_sub": "Bjud in användare och hantera ditt konto.",
|
||||
"active": "Aktiv",
|
||||
"inactive": "Inaktiv",
|
||||
"notifiers": "Notiser",
|
||||
"enabled": "Aktiverad",
|
||||
"currency_format": "Valuta format",
|
||||
"delete_account": "Radera konto",
|
||||
"delete_account_sub": "Ta bort ditt konto och alla tillhörande data. Detta kan inte ångras.",
|
||||
"group_settings": "Grupp inställningar",
|
||||
"group_settings_sub": "Inställningar för delad grupp. Du kan behöva uppdatera din webbläsare för att vissa inställningar ska gälla.",
|
||||
"theme_settings": "Temainställningar",
|
||||
"theme_settings_sub": "Temainställningar sparas i din webbläsares lokala lagring. Du kan ändra tema när du vill. Om du\nhar problem att ställa in tema, pröva att ladda om din webbläsare.",
|
||||
"update_group": "Uppdatera grupp"
|
||||
},
|
||||
"index": {
|
||||
"set_name": "Vad heter du?",
|
||||
"joining_group": "Du går med i en befintlig grupp!",
|
||||
"dont_join_group": "Vill du inte gå med i en grupp?",
|
||||
"tagline": "Spåra, organisera och hantera dina saker.",
|
||||
"disabled_registration": "Registrering avaktiverad",
|
||||
"login": "Logga in",
|
||||
"register": "Registrera",
|
||||
"remember_me": "Kom ihåg mig",
|
||||
"set_email": "Vad är din e-post?",
|
||||
"set_password": "Ställ in ditt lösenord"
|
||||
},
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"upload": "Ladda upp",
|
||||
"title": "Importera CSV fil",
|
||||
"description": "Importera en CSV-fil som innehåller dina föremål, etiketter och platser. Se dokumentationen för mer information om \nönskat format.",
|
||||
"change_warning": "Beteendet för importer med befintliga import_refs har ändrats. Om en import_ref finns i CSV-filen, \nobjektet kommer att uppdateras med värdena i CSV-filen."
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Kort",
|
||||
"items": "Föremål",
|
||||
"table": "Tabell",
|
||||
"no_items": "Inga föremål att visa"
|
||||
}
|
||||
},
|
||||
"create_modal": {
|
||||
"title": "Skapa föremål",
|
||||
"photo_button": "Foto 📷"
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Skapa etikett"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Skapa plats"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"password_score": {
|
||||
"password_strength": "Lösenordsstyrka"
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "Sidans URL"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Byggd: { build }",
|
||||
"github": "GitHub Projekt",
|
||||
"join_discord": "Gå med i Discord",
|
||||
"follow_dev": "Följ utvecklaren",
|
||||
"read_docs": "Läs dokumenten",
|
||||
"password": "Lösenord",
|
||||
"email": "Epost",
|
||||
"submit": "Skicka",
|
||||
"confirm": "Godkänn",
|
||||
"create": "Skapa",
|
||||
"created": "Skapad",
|
||||
"welcome": "Välkommen, { username }",
|
||||
"sign_out": "Logga ut",
|
||||
"create_and_add": "Skapa och lägg till en annan",
|
||||
"version": "Version: { version }",
|
||||
"items": "Föremål",
|
||||
"labels": "Etiketter",
|
||||
"locations": "Platser",
|
||||
"name": "Namn",
|
||||
"search": "Sök"
|
||||
},
|
||||
"items": {
|
||||
"add": "Lägg till",
|
||||
"created_at": "Skapat",
|
||||
"custom_fields": "Egna fält",
|
||||
"field_selector": "Fält alternativ",
|
||||
"field_value": "Fält värde",
|
||||
"first": "Första",
|
||||
"include_archive": "Inkludera arkiverade föremål",
|
||||
"last": "Sista",
|
||||
"negate_labels": "Negera valda etiketter",
|
||||
"next_page": "Nästa sida",
|
||||
"no_results": "Inga föremål hittades",
|
||||
"options": "Alternativ",
|
||||
"order_by": "Ordning via",
|
||||
"pages": "Sida { page } av { totalPages }",
|
||||
"prev_page": "Föregående sida",
|
||||
"query_id": "Fråga efter tillgångs-ID-nummer: { id }",
|
||||
"reset_search": "Återställ sökning",
|
||||
"tip_1": "Platser och etiketter filter använder 'OR' funktionen. Om fler än en är valda, endast en kommer\nkrävas för en träff.",
|
||||
"tip_2": "Sökningar med prefixet '#'' kommer att fråga efter ett tillgångs-ID (exempel '#000-001')",
|
||||
"tip_3": "Fältfilter använder 'OR' funktion. Om fler än en är valda endast en kommer att bli krävande för en\nträff.",
|
||||
"tips": "Tips",
|
||||
"tips_sub": "Sök Tips",
|
||||
"updated_at": "Uppdaterad",
|
||||
"results": "{ total } Resultat"
|
||||
}
|
||||
}
|
||||
128
frontend/locales/tr.json
Normal file
128
frontend/locales/tr.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"global": {
|
||||
"version": "Versiyon:{ version }",
|
||||
"password": "Şifre",
|
||||
"create": "Oluştur",
|
||||
"github": "GitHub projesi",
|
||||
"join_discord": "Discord'a Katılın",
|
||||
"follow_dev": "Geliştiriciyi takip edin",
|
||||
"read_docs": "Dokümanları okuyun",
|
||||
"email": "Elektronik posta",
|
||||
"submit": "Gönder",
|
||||
"confirm": "Onaylayın",
|
||||
"build": "Sürüm: { build }",
|
||||
"create_and_add": "Oluştur ve Bir Tane Daha Ekle",
|
||||
"created": "Oluşturuldu",
|
||||
"items": "Öğeler",
|
||||
"labels": "Etiketler",
|
||||
"locations": "Konumlar",
|
||||
"name": "İsim",
|
||||
"search": "Ara",
|
||||
"sign_out": "Oturumu kapat",
|
||||
"welcome": "Hoşgeldib, { username }"
|
||||
},
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"change_warning": "Mevcut import_refs ile içe aktarmaların davranışı değişti. CSV dosyasında bir import_ref varsa, \nöğe CSV dosyasındaki değerlerle güncellenecektir.",
|
||||
"title": "CSV dosyasını içeri aktar",
|
||||
"upload": "Yükle",
|
||||
"description": "Öğelerinizi, etiketlerinizi ve konumlarınızı içeren bir CSV dosyasını içe aktarın. Daha fazla\nbilgi için dökümanları okuyun."
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"password_score": {
|
||||
"password_strength": "Şifre güvenlik seviyesi"
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "Sayfa URL'si"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Eşya Oluştur",
|
||||
"photo_button": "Fotoğraf 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"items": "Öğeler",
|
||||
"card": "Kart",
|
||||
"table": "Tablo",
|
||||
"no_items": "Görüntülecek Öge Yok"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Etiket oluştur"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Konum oluştur"
|
||||
}
|
||||
}
|
||||
},
|
||||
"index": {
|
||||
"remember_me": "Beni Hatırla",
|
||||
"tagline": "Eşyalarınızı Takip Edin, Düzenleyin ve Yönetin.",
|
||||
"disabled_registration": "Kayıt olma devre dışı",
|
||||
"login": "Oturum Aç",
|
||||
"register": "Kaydolun",
|
||||
"set_email": "E-posta adresiniz nedir?",
|
||||
"set_password": "Şifrenizi belirleyin",
|
||||
"set_name": "Adın ne?",
|
||||
"joining_group": "Mevcut bir gruba katılıyorsunuz!",
|
||||
"dont_join_group": "Bir gruba katılmak istemiyor musunuz?"
|
||||
},
|
||||
"items": {
|
||||
"add": "Ekle",
|
||||
"created_at": "Oluşturulma",
|
||||
"custom_fields": "Özel Alanlar",
|
||||
"field_selector": "alan seçici",
|
||||
"field_value": "Alan Değeri",
|
||||
"first": "Birinci",
|
||||
"include_archive": "Arşivlenen Öğeleri Dahil Et",
|
||||
"last": "Son",
|
||||
"negate_labels": "Seçili Etiketleri Yoksay",
|
||||
"next_page": "Sonraki Sayfa",
|
||||
"no_results": "Öğe Bulunamadı",
|
||||
"options": "Seçenekler",
|
||||
"order_by": "Sıralama ölçütü",
|
||||
"pages": "Sayfa { page }/{ totalPages }",
|
||||
"prev_page": "Önceki Sayfa",
|
||||
"query_id": "Varlık Kimlik Numarası Sorgulanıyor: { id }",
|
||||
"tip_1": "Konum ve etiket filtreleri 'veya' işlemini kullanır. Eğer birden fazla seçilirse sadece biri \neşleştirme için kullanılacaktır.",
|
||||
"reset_search": "Aramayı Sıfırla",
|
||||
"tips": "İpuçları",
|
||||
"results": "{ total } Sonuç",
|
||||
"tip_2": "'#' ile başlayan aramalar bir varlık kimliğini sorgular (örneğin '#000-001')",
|
||||
"tip_3": "Alan filtreleri 'VEYA' işlemini kullanır. Birden fazla seçenek seçilirse, eşleşme için yalnızca birinin \nkarşılanması yeterlidir.",
|
||||
"tips_sub": "Arama İpuçları",
|
||||
"updated_at": "Güncellendiği Zaman"
|
||||
},
|
||||
"profile": {
|
||||
"url": "URL",
|
||||
"user_profile": "Kullanıcı Profili",
|
||||
"user_profile_sub": "Kullanıcıları davet edin ve hesabınızı yönetin.",
|
||||
"active": "Aktif",
|
||||
"change_password": "Şifre Değiştir",
|
||||
"currency_format": "Para Birimi Biçimi",
|
||||
"current_password": "Mevcut Şifre",
|
||||
"delete_account": "Hesabı Sil",
|
||||
"delete_account_sub": "Hesabınızı ve ona bağlı tüm verileri silin. Bu işlem geri alınamaz.",
|
||||
"enabled": "Etkinleştirildi",
|
||||
"gen_invite": "Davet Bağlantısı Oluştur",
|
||||
"group_settings": "Grup Ayarları",
|
||||
"group_settings_sub": "Paylaşılan Grup Ayarları. Bazı ayarların uygulanabilmesi için tarayıcınızı yenilemeniz gerekebilir.",
|
||||
"inactive": "Etkin Değil",
|
||||
"new_password": "Yeni Şifre",
|
||||
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Bildirici",
|
||||
"notifiers": "Bildirimde Bulunanlar",
|
||||
"test": "Test",
|
||||
"theme_settings": "Tema Ayarları",
|
||||
"theme_settings_sub": "Tema ayarları tarayıcınızın yerel depolama alanında saklanır. Temayı istediğiniz zaman değiştirebilirsiniz. \nTemanızı ayarlamakta sorun yaşıyorsanız, tarayıcınızı yenilemeyi deneyin.",
|
||||
"update_group": "Grubu Güncelle",
|
||||
"notifiers_sub": "Yaklaşan bakım hatırlatmaları için bildirimler alın"
|
||||
}
|
||||
}
|
||||
130
frontend/locales/zh-CN.json
Normal file
130
frontend/locales/zh-CN.json
Normal file
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"title": "导入 CSV 文件",
|
||||
"upload": "上传",
|
||||
"description": "导入包含物品、标签和位置的CSV文件。更多有关信息,\n请参阅文档查看所需格式。",
|
||||
"change_warning": "导入行为会导致现有的 import_refs 的字段被覆盖。\n如果 CSV 文件中存在 import_ref,则将使用 CSV 文件中的值更新该项。"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "创建物品",
|
||||
"photo_button": "照片 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "卡片模式",
|
||||
"items": "物品",
|
||||
"no_items": "没有可展示的物品",
|
||||
"table": "表格模式"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "创建位置"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "页面URL"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "密码强度"
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "创建标签"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"create_and_add": "保存并继续创建",
|
||||
"email": "邮箱",
|
||||
"items": "物品",
|
||||
"build": "编译:{build}",
|
||||
"confirm": "确认",
|
||||
"create": "创建",
|
||||
"created": "已创建",
|
||||
"follow_dev": "关注开发者",
|
||||
"github": "Github项目",
|
||||
"join_discord": "加入Discord讨论",
|
||||
"labels": "标签",
|
||||
"sign_out": "注销",
|
||||
"version": "版本:{version}",
|
||||
"welcome": "欢迎,{username}",
|
||||
"name": "名称",
|
||||
"password": "密码",
|
||||
"read_docs": "查阅文档",
|
||||
"search": "搜索",
|
||||
"submit": "提交",
|
||||
"locations": "位置"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "已禁用注册",
|
||||
"dont_join_group": "不想加入群组?",
|
||||
"joining_group": "您正在加入现有群组!",
|
||||
"login": "登录",
|
||||
"register": "注册",
|
||||
"set_name": "你的名称叫什么?",
|
||||
"remember_me": "记住我",
|
||||
"set_email": "您的电子邮箱是什么?",
|
||||
"tagline": "跟踪、整理和管理您的物品。",
|
||||
"set_password": "设置密码"
|
||||
},
|
||||
"items": {
|
||||
"last": "最后一页",
|
||||
"negate_labels": "取消选中的标签",
|
||||
"next_page": "下一页",
|
||||
"no_results": "没有可显示的物品",
|
||||
"options": "选项",
|
||||
"prev_page": "上一页",
|
||||
"add": "添加",
|
||||
"field_selector": "字段选择",
|
||||
"first": "第一页",
|
||||
"order_by": "排序方式",
|
||||
"pages": "第{page}页,共{totalPages}页",
|
||||
"query_id": "查询资产ID: {id}",
|
||||
"created_at": "创建于",
|
||||
"custom_fields": "自定义字段",
|
||||
"field_value": "字段值",
|
||||
"include_archive": "包括已存档的项目",
|
||||
"reset_search": "重置搜索",
|
||||
"results": "{ total } 条结果",
|
||||
"tip_1": "位置和标签过滤器使用“或”操作。如果选择了多个位置或标签,\n则只需要任意一个匹配上即可。",
|
||||
"tip_2": "以“#”为前缀的搜索将变成查询资产ID(例如“#000-001” )",
|
||||
"tip_3": "字段过滤器使用“或”操作。如果选择了多个位置或标签,\n则只需要任意一个匹配上即可。",
|
||||
"tips": "建议",
|
||||
"tips_sub": "搜索提示",
|
||||
"updated_at": "更新于"
|
||||
},
|
||||
"profile": {
|
||||
"group_settings_sub": "共享组设置。您可能需要刷新浏览器来让某些设置生效。",
|
||||
"notifier_modal": "{ type, select, true {编辑} false {创建} other {Other}} 通知器",
|
||||
"active": "活跃",
|
||||
"delete_account_sub": "删除您的帐户及其所有相关数据。这是无法撤消的。",
|
||||
"inactive": "非活跃",
|
||||
"theme_settings_sub": "主题设置存储在浏览器的本地存储中。您可以随时更改主题。\n如果您在设置主题时遇到问题,请尝试刷新浏览器。",
|
||||
"update_group": "更新组",
|
||||
"change_password": "更改密码",
|
||||
"currency_format": "货币格式",
|
||||
"current_password": "原密码",
|
||||
"delete_account": "删除帐户",
|
||||
"enabled": "已启用",
|
||||
"gen_invite": "生成邀请链接",
|
||||
"group_settings": "组设置",
|
||||
"new_password": "新密码",
|
||||
"notifiers": "通知器",
|
||||
"notifiers_sub": "获取即将到来的维护提醒通知",
|
||||
"test": "测试",
|
||||
"theme_settings": "主题设置",
|
||||
"url": "网址",
|
||||
"user_profile": "用户资料",
|
||||
"user_profile_sub": "邀请用户共同管理您的资产。",
|
||||
"update_language": "更新语言",
|
||||
"language": "语言"
|
||||
}
|
||||
}
|
||||
128
frontend/locales/zh-HK.json
Normal file
128
frontend/locales/zh-HK.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"items": {
|
||||
"include_archive": "包括存檔項目",
|
||||
"tip_1": "位置和標籤過濾器使用“OR”運算。如果選擇了多個,則一次配對\n只會使用一個。",
|
||||
"pages": "第{ page } 頁(共{ totalPages })",
|
||||
"tip_3": "字段過濾器使用“OR”運算。如果選擇了多個,則一次配對只需要選擇\n一個。",
|
||||
"field_value": "欄位值",
|
||||
"field_selector": "欄位選擇器",
|
||||
"custom_fields": "自訂欄位",
|
||||
"first": "第一項",
|
||||
"add": "添加",
|
||||
"created_at": "創建於",
|
||||
"last": "最後一項",
|
||||
"negate_labels": "取消選定的標籤",
|
||||
"next_page": "下一頁",
|
||||
"no_results": "沒有找到項目",
|
||||
"options": "選項",
|
||||
"order_by": "排序",
|
||||
"prev_page": "上一頁",
|
||||
"query_id": "查詢資產ID號碼:{ id }",
|
||||
"reset_search": "重置搜尋",
|
||||
"results": "{ total }結果",
|
||||
"tips": "提示",
|
||||
"tips_sub": "搜尋技巧",
|
||||
"updated_at": "更新於",
|
||||
"tip_2": "以「#」為前綴的搜尋將查詢資產 ID(例如「#000-001」)"
|
||||
},
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"description": "匯入包含您的商品、標籤和位置的 CSV 檔案。有關所需格式的更多信息\n,請參閱文件。",
|
||||
"title": "匯入CSV檔案",
|
||||
"upload": "上傳",
|
||||
"change_warning": "現有 import_refs 的導入行為已變更。如果 CSV 檔案中存在 import_ref,則\n項目將使用 CSV 檔案中的值進行更新。"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "頁面網址"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "密碼強度"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "新增項目",
|
||||
"photo_button": "相片 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "卡牌風",
|
||||
"items": "項目",
|
||||
"no_items": "沒有可顯示的項目",
|
||||
"table": "表格"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "新增標籤"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "新增地點"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "建立{ build }",
|
||||
"confirm": "確認",
|
||||
"create": "新增",
|
||||
"create_and_add": "創建並添加另一個",
|
||||
"created": "已建立",
|
||||
"email": "電子信箱",
|
||||
"github": "GitHub項目",
|
||||
"items": "項目",
|
||||
"join_discord": "加入Discord",
|
||||
"labels": "標籤",
|
||||
"locations": "地點",
|
||||
"name": "名字",
|
||||
"password": "密碼",
|
||||
"read_docs": "閱讀文件",
|
||||
"search": "搜尋",
|
||||
"sign_out": "登出",
|
||||
"submit": "提交",
|
||||
"version": "版本:{ version }",
|
||||
"follow_dev": "關注開發者",
|
||||
"welcome": "歡迎,{ username }"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "已禁用註冊",
|
||||
"dont_join_group": "不想加入群組?",
|
||||
"joining_group": "您正在加入現有群組!",
|
||||
"login": "登入",
|
||||
"register": "註冊",
|
||||
"remember_me": "記住帳號",
|
||||
"set_email": "你的電子郵件是什麼?",
|
||||
"set_name": "你叫什麼名字?",
|
||||
"tagline": "追蹤、組織和管理您的物品。",
|
||||
"set_password": "設定您的密碼"
|
||||
},
|
||||
"profile": {
|
||||
"change_password": "變更密碼",
|
||||
"currency_format": "貨幣格式",
|
||||
"current_password": "當前密碼",
|
||||
"delete_account": "刪除帳戶",
|
||||
"enabled": "啟用",
|
||||
"gen_invite": "產生邀請連結",
|
||||
"group_settings": "群組設定",
|
||||
"group_settings_sub": "共享群組設定。您可能需要刷新瀏覽器才能套用設定。",
|
||||
"inactive": "不活躍",
|
||||
"new_password": "新密碼",
|
||||
"active": "活躍",
|
||||
"delete_account_sub": "刪除您的帳戶及其所有相關資料。此操作無法撤回。",
|
||||
"notifiers": "通知者",
|
||||
"notifiers_sub": "獲取即將到來的維護提醒的通知",
|
||||
"test": "測試",
|
||||
"theme_settings": "主題設定",
|
||||
"update_group": "更新群組",
|
||||
"url": "網址",
|
||||
"user_profile": "使用者資料",
|
||||
"user_profile_sub": "邀請使用者並管理您的帳戶。",
|
||||
"theme_settings_sub": "主題設定儲存在瀏覽器的本機儲存中。您可以隨時變更主題。如果你是\n 設定主題時遇到問題,請嘗試刷新瀏覽器。",
|
||||
"notifier_modal": "通知 { type, select, true {Edit} false {Create} other {Other}}"
|
||||
}
|
||||
}
|
||||
128
frontend/locales/zh-MO.json
Normal file
128
frontend/locales/zh-MO.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"items": {
|
||||
"include_archive": "包括存檔項目",
|
||||
"tip_1": "位置和標籤過濾器使用“OR”運算。如果選擇了多個,則一次配對\n只會使用一個。",
|
||||
"pages": "第{ page } 頁(共{ totalPages })",
|
||||
"tip_3": "字段過濾器使用“OR”運算。如果選擇了多個,則一次配對只需要選擇\n一個。",
|
||||
"field_value": "欄位值",
|
||||
"field_selector": "欄位選擇器",
|
||||
"custom_fields": "自訂欄位",
|
||||
"first": "第一項",
|
||||
"add": "添加",
|
||||
"created_at": "創建於",
|
||||
"last": "最後一項",
|
||||
"negate_labels": "取消選定的標籤",
|
||||
"next_page": "下一頁",
|
||||
"no_results": "沒有找到項目",
|
||||
"options": "選項",
|
||||
"order_by": "排序",
|
||||
"prev_page": "上一頁",
|
||||
"query_id": "查詢資產ID號碼:{ id }",
|
||||
"reset_search": "重置搜尋",
|
||||
"results": "{ total }結果",
|
||||
"tips": "提示",
|
||||
"tips_sub": "搜尋技巧",
|
||||
"updated_at": "更新於",
|
||||
"tip_2": "以「#」為前綴的搜尋將查詢資產 ID(例如「#000-001」)"
|
||||
},
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"description": "匯入包含您的商品、標籤和位置的 CSV 檔案。有關所需格式的更多信息\n,請參閱文件。",
|
||||
"title": "匯入CSV檔案",
|
||||
"upload": "上傳",
|
||||
"change_warning": "現有 import_refs 的導入行為已變更。如果 CSV 檔案中存在 import_ref,則\n項目將使用 CSV 檔案中的值進行更新。"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "頁面網址"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "密碼強度"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "新增項目",
|
||||
"photo_button": "相片 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "卡牌風",
|
||||
"items": "項目",
|
||||
"no_items": "沒有可顯示的項目",
|
||||
"table": "表格"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "新增標籤"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "新增地點"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "建立{ build }",
|
||||
"confirm": "確認",
|
||||
"create": "新增",
|
||||
"create_and_add": "創建並添加另一個",
|
||||
"created": "已建立",
|
||||
"email": "電子信箱",
|
||||
"github": "GitHub項目",
|
||||
"items": "項目",
|
||||
"join_discord": "加入Discord",
|
||||
"labels": "標籤",
|
||||
"locations": "地點",
|
||||
"name": "名字",
|
||||
"password": "密碼",
|
||||
"read_docs": "閱讀文件",
|
||||
"search": "搜尋",
|
||||
"sign_out": "登出",
|
||||
"submit": "提交",
|
||||
"version": "版本:{ version }",
|
||||
"follow_dev": "關注開發者",
|
||||
"welcome": "歡迎,{ username }"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "已禁用註冊",
|
||||
"dont_join_group": "不想加入群組?",
|
||||
"joining_group": "您正在加入現有群組!",
|
||||
"login": "登入",
|
||||
"register": "註冊",
|
||||
"remember_me": "記住帳號",
|
||||
"set_email": "你的電子郵件是什麼?",
|
||||
"set_name": "你叫什麼名字?",
|
||||
"tagline": "追蹤、組織和管理您的物品。",
|
||||
"set_password": "設定您的密碼"
|
||||
},
|
||||
"profile": {
|
||||
"change_password": "變更密碼",
|
||||
"currency_format": "貨幣格式",
|
||||
"current_password": "當前密碼",
|
||||
"delete_account": "刪除帳戶",
|
||||
"enabled": "啟用",
|
||||
"gen_invite": "產生邀請連結",
|
||||
"group_settings": "群組設定",
|
||||
"group_settings_sub": "共享群組設定。您可能需要刷新瀏覽器才能套用設定。",
|
||||
"inactive": "不活躍",
|
||||
"new_password": "新密碼",
|
||||
"active": "活躍",
|
||||
"delete_account_sub": "刪除您的帳戶及其所有相關資料。此操作無法撤回。",
|
||||
"notifiers": "通知者",
|
||||
"notifiers_sub": "獲取即將到來的維護提醒的通知",
|
||||
"test": "測試",
|
||||
"theme_settings": "主題設定",
|
||||
"update_group": "更新群組",
|
||||
"url": "網址",
|
||||
"user_profile": "使用者資料",
|
||||
"user_profile_sub": "邀請使用者並管理您的帳戶。",
|
||||
"theme_settings_sub": "主題設定儲存在瀏覽器的本機儲存中。您可以隨時變更主題。如果你是\n 設定主題時遇到問題,請嘗試刷新瀏覽器。",
|
||||
"notifier_modal": "通知 { type, select, true {Edit} false {Create} other {Other}}"
|
||||
}
|
||||
}
|
||||
128
frontend/locales/zh-TW.json
Normal file
128
frontend/locales/zh-TW.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"items": {
|
||||
"include_archive": "包括存檔項目",
|
||||
"tip_1": "位置和標籤過濾器使用“OR”運算。如果選擇了多個,則一次配對\n只會使用一個。",
|
||||
"pages": "第{ page } 頁(共{ totalPages })",
|
||||
"tip_3": "字段過濾器使用“OR”運算。如果選擇了多個,則一次配對只需要選擇\n一個。",
|
||||
"field_value": "欄位值",
|
||||
"field_selector": "欄位選擇器",
|
||||
"custom_fields": "自訂欄位",
|
||||
"first": "第一項",
|
||||
"add": "添加",
|
||||
"created_at": "創建於",
|
||||
"last": "最後一項",
|
||||
"negate_labels": "取消選定的標籤",
|
||||
"next_page": "下一頁",
|
||||
"no_results": "沒有找到項目",
|
||||
"options": "選項",
|
||||
"order_by": "排序",
|
||||
"prev_page": "上一頁",
|
||||
"query_id": "查詢資產ID號碼:{ id }",
|
||||
"reset_search": "重置搜尋",
|
||||
"results": "{ total }結果",
|
||||
"tips": "提示",
|
||||
"tips_sub": "搜尋技巧",
|
||||
"updated_at": "更新於",
|
||||
"tip_2": "以「#」為前綴的搜尋將查詢資產 ID(例如「#000-001」)"
|
||||
},
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"description": "匯入包含您的商品、標籤和位置的 CSV 檔案。有關所需格式的更多信息\n,請參閱文件。",
|
||||
"title": "匯入CSV檔案",
|
||||
"upload": "上傳",
|
||||
"change_warning": "現有 import_refs 的導入行為已變更。如果 CSV 檔案中存在 import_ref,則\n項目將使用 CSV 檔案中的值進行更新。"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "頁面網址"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "密碼強度"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "新增項目",
|
||||
"photo_button": "相片 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "卡牌風",
|
||||
"items": "項目",
|
||||
"no_items": "沒有可顯示的項目",
|
||||
"table": "表格"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "新增標籤"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "新增地點"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "建立{ build }",
|
||||
"confirm": "確認",
|
||||
"create": "新增",
|
||||
"create_and_add": "創建並添加另一個",
|
||||
"created": "已建立",
|
||||
"email": "電子信箱",
|
||||
"github": "GitHub項目",
|
||||
"items": "項目",
|
||||
"join_discord": "加入Discord",
|
||||
"labels": "標籤",
|
||||
"locations": "地點",
|
||||
"name": "名字",
|
||||
"password": "密碼",
|
||||
"read_docs": "閱讀文件",
|
||||
"search": "搜尋",
|
||||
"sign_out": "登出",
|
||||
"submit": "提交",
|
||||
"version": "版本:{ version }",
|
||||
"follow_dev": "關注開發者",
|
||||
"welcome": "歡迎,{ username }"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "已禁用註冊",
|
||||
"dont_join_group": "不想加入群組?",
|
||||
"joining_group": "您正在加入現有群組!",
|
||||
"login": "登入",
|
||||
"register": "註冊",
|
||||
"remember_me": "記住帳號",
|
||||
"set_email": "你的電子郵件是什麼?",
|
||||
"set_name": "你叫什麼名字?",
|
||||
"tagline": "追蹤、組織和管理您的物品。",
|
||||
"set_password": "設定您的密碼"
|
||||
},
|
||||
"profile": {
|
||||
"change_password": "變更密碼",
|
||||
"currency_format": "貨幣格式",
|
||||
"current_password": "當前密碼",
|
||||
"delete_account": "刪除帳戶",
|
||||
"enabled": "啟用",
|
||||
"gen_invite": "產生邀請連結",
|
||||
"group_settings": "群組設定",
|
||||
"group_settings_sub": "共享群組設定。您可能需要刷新瀏覽器才能套用設定。",
|
||||
"inactive": "不活躍",
|
||||
"new_password": "新密碼",
|
||||
"active": "活躍",
|
||||
"delete_account_sub": "刪除您的帳戶及其所有相關資料。此操作無法撤回。",
|
||||
"notifiers": "通知者",
|
||||
"notifiers_sub": "獲取即將到來的維護提醒的通知",
|
||||
"test": "測試",
|
||||
"theme_settings": "主題設定",
|
||||
"update_group": "更新群組",
|
||||
"url": "網址",
|
||||
"user_profile": "使用者資料",
|
||||
"user_profile_sub": "邀請使用者並管理您的帳戶。",
|
||||
"theme_settings_sub": "主題設定儲存在瀏覽器的本機儲存中。您可以隨時變更主題。如果你是\n 設定主題時遇到問題,請嘗試刷新瀏覽器。",
|
||||
"notifier_modal": "通知 { type, select, true {Edit} false {Create} other {Other}}"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ import { defineNuxtConfig } from "nuxt/config";
|
||||
// https://v3.nuxtjs.org/api/configuration/nuxt.config
|
||||
export default defineNuxtConfig({
|
||||
ssr: false,
|
||||
build: {
|
||||
transpile: ["vue-i18n"],
|
||||
},
|
||||
modules: [
|
||||
"@nuxtjs/tailwindcss",
|
||||
"@pinia/nuxt",
|
||||
|
||||
@@ -14,50 +14,54 @@
|
||||
"test:watch": " TEST_SHUTDOWN_API_SERVER=false vitest --config ./test/vitest.config.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.0.0",
|
||||
"@iconify-json/mdi": "^1.1.64",
|
||||
"@nuxtjs/eslint-config-typescript": "^12.0.0",
|
||||
"@types/dompurify": "^3.0.0",
|
||||
"@types/markdown-it": "^13.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@iconify-json/mdi": "^1.2.0",
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@nuxtjs/eslint-config-typescript": "^12.1.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/markdown-it": "^13.0.9",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vite-pwa/nuxt": "^0.5.0",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-vue": "^9.4.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-tailwindcss": "^3.17.4",
|
||||
"eslint-plugin-vue": "^9.28.0",
|
||||
"h3": "^1.7.1",
|
||||
"intl-messageformat": "^10.5.14",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"nuxt": "3.6.5",
|
||||
"prettier": "^3.2.5",
|
||||
"typescript": "^5.0.0",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^5.5.4",
|
||||
"unplugin-icons": "^0.18.5",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vitest": "^1.0.0"
|
||||
"vitest": "^1.6.0",
|
||||
"vue-i18n": "^9.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.9",
|
||||
"@nuxtjs/tailwindcss": "^6.1.3",
|
||||
"@pinia/nuxt": "^0.5.0",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.0",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.4",
|
||||
"@headlessui/vue": "^1.7.22",
|
||||
"@nuxtjs/tailwindcss": "^6.12.1",
|
||||
"@pinia/nuxt": "^0.5.4",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/lunr": "^2.3.7",
|
||||
"@vuepic/vue-datepicker": "^8.1.1",
|
||||
"@vueuse/nuxt": "^10.0.0",
|
||||
"@vueuse/router": "^10.0.0",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"daisyui": "^2.24.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"dompurify": "^3.0.0",
|
||||
"h3": "^1.7.1",
|
||||
"@vuepic/vue-datepicker": "^8.8.1",
|
||||
"@vueuse/nuxt": "^10.11.1",
|
||||
"@vueuse/router": "^10.11.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"daisyui": "^2.52.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"dompurify": "^3.1.6",
|
||||
"h3": "^1.12.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
"lunr": "^2.3.9",
|
||||
"markdown-it": "^14.0.0",
|
||||
"pinia": "^2.0.21",
|
||||
"postcss": "^8.4.16",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"vue": "v3.4.8",
|
||||
"vue-router": "4"
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinia": "^2.2.2",
|
||||
"postcss": "^8.4.45",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"vue": "3.4.8",
|
||||
"vue-router": "^4.4.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="text-blue-500 font-extrabold flex flex-col text-center">
|
||||
<h1 class="flex flex-col text-center font-extrabold text-blue-500">
|
||||
<span class="text-7xl">404.</span>
|
||||
<span class="text-5xl mt-5"> Page Not Found </span>
|
||||
<span class="mt-5 text-5xl"> Page Not Found </span>
|
||||
</h1>
|
||||
</template>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<BaseContainer>
|
||||
<section v-if="!pending">
|
||||
<BaseSectionHeader class="mb-5"> This Asset Id is associated with multiple items</BaseSectionHeader>
|
||||
<div class="grid gap-2 grid-cols-1 sm:grid-cols-2">
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<ItemCard v-for="item in items" :key="item.id" :item="item" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -38,23 +38,23 @@
|
||||
<Subtitle> Recently Added </Subtitle>
|
||||
|
||||
<BaseCard v-if="breakpoints.lg">
|
||||
<ItemViewTable :items="itemTable.items" />
|
||||
<ItemViewTable :items="itemTable.items" disable-controls />
|
||||
</BaseCard>
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-else class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<ItemCard v-for="item in itemTable.items" :key="item.id" :item="item" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Subtitle> Storage Locations </Subtitle>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 card md:grid-cols-3 gap-4">
|
||||
<div class="card grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
<LocationCard v-for="location in locations" :key="location.id" :location="location" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Subtitle> Labels </Subtitle>
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<LabelChip v-for="label in labels" :key="label.id" size="lg" :label="label" class="shadow-md" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import MdiGithub from "~icons/mdi/github";
|
||||
import MdiTwitter from "~icons/mdi/twitter";
|
||||
import MdiDiscord from "~icons/mdi/discord";
|
||||
import MdiFolder from "~icons/mdi/folder";
|
||||
import MdiAccount from "~icons/mdi/account";
|
||||
@@ -8,6 +7,7 @@
|
||||
import MdiLogin from "~icons/mdi/login";
|
||||
import MdiArrowRight from "~icons/mdi/arrow-right";
|
||||
import MdiLock from "~icons/mdi/lock";
|
||||
import MdiMastodon from "~icons/mdi/mastodon";
|
||||
|
||||
useHead({
|
||||
title: "Homebox | Organize and Tag Your Stuff",
|
||||
@@ -77,6 +77,15 @@
|
||||
|
||||
async function registerUser() {
|
||||
loading.value = true;
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
if (!emailRegex.test(email.value)) {
|
||||
toast.error("Invalid email address");
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await api.register({
|
||||
name: username.value,
|
||||
email: email.value,
|
||||
@@ -126,9 +135,9 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col min-h-screen">
|
||||
<div class="fill-primary min-w-full absolute top-0 z-[-1]">
|
||||
<div class="bg-primary flex-col flex min-h-[20vh]" />
|
||||
<div class="flex min-h-screen flex-col">
|
||||
<div class="absolute top-0 z-[-1] min-w-full fill-primary">
|
||||
<div class="flex min-h-[20vh] flex-col bg-primary" />
|
||||
<svg
|
||||
class="fill-primary drop-shadow-xl"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -142,49 +151,64 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<header class="p-4 sm:px-6 lg:p-14 sm:py-6 sm:flex sm:items-end mx-auto">
|
||||
<header class="mx-auto p-4 sm:flex sm:items-end sm:p-6 lg:p-14">
|
||||
<div>
|
||||
<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>
|
||||
<p class="ml-1 text-lg text-base-content/50">Track, Organize, and Manage your Things.</p>
|
||||
<p class="ml-1 text-lg text-base-content/50">{{ $t("index.tagline") }}</p>
|
||||
</div>
|
||||
<div class="flex mt-6 sm:mt-0 gap-4 ml-auto text-neutral-content">
|
||||
<a class="tooltip" data-tip="Project Github" href="https://github.com/sysadminsmedia/homebox" target="_blank">
|
||||
<MdiGithub class="h-8 w-8" />
|
||||
<div class="ml-auto mt-6 flex gap-4 text-neutral-content sm:mt-0">
|
||||
<a
|
||||
class="tooltip"
|
||||
:data-tip="$t('global.github')"
|
||||
href="https://github.com/sysadminsmedia/homebox"
|
||||
target="_blank"
|
||||
>
|
||||
<MdiGithub class="size-8" />
|
||||
</a>
|
||||
<a href="https://twitter.com/haybytes" class="tooltip" data-tip="Follow The Developer" target="_blank">
|
||||
<MdiTwitter class="h-8 w-8" />
|
||||
<a
|
||||
href="https://noc.social/@sysadminszone"
|
||||
class="tooltip"
|
||||
:data-tip="$t('global.follow_dev')"
|
||||
target="_blank"
|
||||
>
|
||||
<MdiMastodon class="size-8" />
|
||||
</a>
|
||||
<a href="https://discord.gg/tuncmNrE4z" class="tooltip" data-tip="Join The Discord" target="_blank">
|
||||
<MdiDiscord class="h-8 w-8" />
|
||||
<a href="https://discord.gg/aY4DCkpNA9" class="tooltip" :data-tip="$t('global.join_discord')" target="_blank">
|
||||
<MdiDiscord class="size-8" />
|
||||
</a>
|
||||
<a href="https://hay-kot.github.io/homebox/" class="tooltip" data-tip="Read The Docs" target="_blank">
|
||||
<MdiFolder class="h-8 w-8" />
|
||||
<a
|
||||
href="https://homebox.software/en/"
|
||||
class="tooltip"
|
||||
:data-tip="$t('global.read_docs')"
|
||||
target="_blank"
|
||||
>
|
||||
<MdiFolder class="size-8" />
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<div class="grid p-6 sm:place-items-center min-h-[50vh]">
|
||||
<div class="grid min-h-[50vh] p-6 sm:place-items-center">
|
||||
<div>
|
||||
<Transition name="slide-fade">
|
||||
<form v-if="registerForm" @submit.prevent="registerUser">
|
||||
<div class="card w-max-[500px] md:w-[500px] bg-base-100 shadow-xl">
|
||||
<div class="card bg-base-100 shadow-xl md:w-[500px]">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl align-center">
|
||||
<MdiAccount class="mr-1 w-7 h-7" />
|
||||
Register
|
||||
<h2 class="card-title text-2xl">
|
||||
<MdiAccount class="mr-1 size-7" />
|
||||
{{ $t("index.register") }}
|
||||
</h2>
|
||||
<FormTextField v-model="email" label="Set your email?" />
|
||||
<FormTextField v-model="username" label="What's your name?" />
|
||||
<div v-if="!(groupToken == '')" class="pt-4 pb-1 text-center">
|
||||
<p>You're Joining an Existing Group!</p>
|
||||
<FormTextField v-model="email" :label="$t('index.set_email')" />
|
||||
<FormTextField v-model="username" :label="$t('index.set_name')" />
|
||||
<div v-if="!(groupToken == '')" class="pb-1 pt-4 text-center">
|
||||
<p>{{ $t("index.joining_group") }}</p>
|
||||
<button type="button" class="text-xs underline" @click="groupToken = ''">
|
||||
Don't Want To Join a Group?
|
||||
{{ $t("index.dont_join_group") }}
|
||||
</button>
|
||||
</div>
|
||||
<FormPassword v-model="password" label="Set your password" />
|
||||
<FormPassword v-model="password" :label="$t('index.set_password')" />
|
||||
<PasswordScore v-model:valid="canRegister" :password="password" />
|
||||
<div class="card-actions justify-end">
|
||||
<button
|
||||
@@ -193,28 +217,32 @@
|
||||
:class="loading ? 'loading' : ''"
|
||||
:disabled="loading || !canRegister"
|
||||
>
|
||||
Register
|
||||
{{ $t("index.register") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form v-else @submit.prevent="login">
|
||||
<div class="card w-max-[500px] md:w-[500px] bg-base-100 shadow-xl">
|
||||
<div class="card bg-base-100 shadow-xl md:w-[500px]">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl align-center">
|
||||
<MdiAccount class="mr-1 w-7 h-7" />
|
||||
Login
|
||||
<h2 class="card-title text-2xl">
|
||||
<MdiAccount class="mr-1 size-7" />
|
||||
{{ $t("index.login") }}
|
||||
</h2>
|
||||
<template v-if="status && status.demo">
|
||||
<p class="text-xs italic text-center">This is a demo instance</p>
|
||||
<p class="text-xs text-center"><b>Email</b> demo@example.com</p>
|
||||
<p class="text-xs text-center"><b>Password</b> demo</p>
|
||||
<p class="text-center text-xs italic">This is a demo instance</p>
|
||||
<p class="text-center text-xs">
|
||||
<b>{{ $t("global.email") }}</b> demo@example.com
|
||||
</p>
|
||||
<p class="text-center text-xs">
|
||||
<b>{{ $t("global.password") }}</b> demo
|
||||
</p>
|
||||
</template>
|
||||
<FormTextField v-model="email" label="Email" />
|
||||
<FormPassword v-model="loginPassword" label="Password" />
|
||||
<FormTextField v-model="email" :label="$t('global.email')" />
|
||||
<FormPassword v-model="loginPassword" :label="$t('global.password')" />
|
||||
<div class="max-w-[140px]">
|
||||
<FormCheckbox v-model="remember" label="Remember Me" />
|
||||
<FormCheckbox v-model="remember" :label="$t('index.remember_me')" />
|
||||
</div>
|
||||
<div class="card-actions justify-end">
|
||||
<button
|
||||
@@ -223,36 +251,39 @@
|
||||
:class="loading ? 'loading' : ''"
|
||||
:disabled="loading"
|
||||
>
|
||||
Login
|
||||
{{ $t("index.login") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Transition>
|
||||
<div class="text-center mt-6">
|
||||
<div class="mt-6 text-center">
|
||||
<BaseButton
|
||||
v-if="status && status.allowRegistration"
|
||||
class="btn-primary btn-wide"
|
||||
@click="() => toggleLogin()"
|
||||
>
|
||||
<template #icon>
|
||||
<MdiAccountPlus v-if="!registerForm" class="w-5 h-5 swap-off" />
|
||||
<MdiLogin v-else class="w-5 h-5 swap-off" />
|
||||
<MdiArrowRight class="w-5 h-5 swap-on" />
|
||||
<MdiAccountPlus v-if="!registerForm" class="swap-off size-5" />
|
||||
<MdiLogin v-else class="swap-off size-5" />
|
||||
<MdiArrowRight class="swap-on size-5" />
|
||||
</template>
|
||||
{{ registerForm ? "Login" : "Register" }}
|
||||
{{ registerForm ? $t("index.login") : $t("index.register") }}
|
||||
</BaseButton>
|
||||
<p v-else class="text-base-content italic text-sm inline-flex items-center gap-2">
|
||||
<MdiLock class="w-4 h-4 inline-block" />
|
||||
Registration Disabled
|
||||
<p v-else class="inline-flex items-center gap-2 text-sm italic text-base-content">
|
||||
<MdiLock class="inline-block size-4" />
|
||||
{{ $t("index.disabled_registration") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer v-if="status" class="mt-auto text-center w-full bottom-0 pb-4">
|
||||
<p class="text-center text-sm">Version: {{ status.build.version }} ~ Build: {{ status.build.commit }}</p>
|
||||
<footer v-if="status" class="bottom-0 mt-auto w-full pb-4 text-center">
|
||||
<p class="text-center text-sm">
|
||||
{{ $t("global.version", { version: status.build.version }) }} ~
|
||||
{{ $t("global.build", { build: status.build.commit }) }}
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -176,6 +176,10 @@
|
||||
name: "Insured",
|
||||
text: item.value?.insured ? "Yes" : "No",
|
||||
},
|
||||
{
|
||||
name: "Archived",
|
||||
text: item.value?.archived ? "Yes" : "No",
|
||||
},
|
||||
{
|
||||
name: "Notes",
|
||||
type: "markdown",
|
||||
@@ -441,42 +445,42 @@
|
||||
<template>
|
||||
<BaseContainer v-if="item" class="pb-8">
|
||||
<Title>{{ item.name }}</Title>
|
||||
<dialog ref="refDialog" class="z-[999] fixed bg-transparent">
|
||||
<dialog ref="refDialog" class="fixed z-[999] bg-transparent">
|
||||
<div ref="refDialogBody" class="relative">
|
||||
<div class="absolute right-0 -mt-3 -mr-3 sm:-mt-4 sm:-mr-4 space-x-1">
|
||||
<a class="btn btn-sm sm:btn-md btn-primary btn-circle" :href="dialoged.src" download>
|
||||
<MdiDownload class="h-5 w-5" />
|
||||
<div class="absolute right-0 -mr-3 -mt-3 space-x-1 sm:-mr-4 sm:-mt-4">
|
||||
<a class="btn btn-circle btn-primary btn-sm sm:btn-md" :href="dialoged.src" download>
|
||||
<MdiDownload class="size-5" />
|
||||
</a>
|
||||
<button class="btn btn-sm sm:btn-md btn-primary btn-circle" @click="closeDialog()">
|
||||
<MdiClose class="h-5 w-5" />
|
||||
<button class="btn btn-circle btn-primary btn-sm sm:btn-md" @click="closeDialog()">
|
||||
<MdiClose class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<img class="max-w-[80vw] max-h-[80vh]" :src="dialoged.src" />
|
||||
<img class="max-h-[80vh] max-w-[80vw]" :src="dialoged.src" />
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<section>
|
||||
<div class="bg-base-100 rounded p-3">
|
||||
<div class="rounded bg-base-100 p-3">
|
||||
<header class="mb-2">
|
||||
<div class="flex flex-wrap items-end gap-2">
|
||||
<div class="avatar placeholder mb-auto">
|
||||
<div class="bg-neutral-focus text-neutral-content rounded-full w-12">
|
||||
<MdiPackageVariant class="h-7 w-7" />
|
||||
<div class="w-12 rounded-full bg-neutral-focus text-neutral-content">
|
||||
<MdiPackageVariant class="size-7" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="fullpath && fullpath.length > 0" class="text-sm breadcrumbs pt-0 pb-0">
|
||||
<div v-if="fullpath && fullpath.length > 0" class="breadcrumbs py-0 text-sm">
|
||||
<ul class="text-base-content/70">
|
||||
<li v-for="part in fullpath" :key="part.id">
|
||||
<NuxtLink :to="`/${part.type}/${part.id}`"> {{ part.name }}</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 class="text-2xl pb-1">
|
||||
<h1 class="pb-1 text-2xl">
|
||||
{{ item ? item.name : "" }}
|
||||
</h1>
|
||||
<div class="flex gap-1 flex-wrap text-xs">
|
||||
<div class="flex flex-wrap gap-1 text-xs">
|
||||
<div>
|
||||
Created
|
||||
<DateTime :date="item?.createdAt" />
|
||||
@@ -491,12 +495,12 @@
|
||||
</div>
|
||||
</header>
|
||||
<div class="divider my-0 mb-1"></div>
|
||||
<div class="p-1 prose max-w-[100%]">
|
||||
<div class="prose max-w-full p-1">
|
||||
<Markdown v-if="item && item.description" class="text-base" :source="item.description"> </Markdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between mb-6 mt-3">
|
||||
<div class="mb-6 mt-3 flex flex-wrap items-center justify-between">
|
||||
<div class="btn-group">
|
||||
<NuxtLink
|
||||
v-for="t in tabs"
|
||||
@@ -516,7 +520,7 @@
|
||||
<BaseCard v-if="!hasNested" collapsable>
|
||||
<template #title> Details </template>
|
||||
<template #title-actions>
|
||||
<div class="flex flex-wrap justify-between items-center mt-2 gap-4">
|
||||
<div class="mt-2 flex flex-wrap items-center justify-between gap-4">
|
||||
<label class="label cursor-pointer">
|
||||
<input v-model="preferences.showEmpty" type="checkbox" class="toggle toggle-primary" />
|
||||
<span class="label-text ml-4"> Show Empty </span>
|
||||
@@ -528,13 +532,13 @@
|
||||
<template #quantity="{ detail }">
|
||||
{{ detail.text }}
|
||||
<span
|
||||
class="opacity-0 group-hover:opacity-100 ml-4 my-0 duration-75 transition-opacity inline-flex gap-2"
|
||||
class="my-0 ml-4 inline-flex gap-2 opacity-0 transition-opacity duration-75 group-hover:opacity-100"
|
||||
>
|
||||
<button class="btn btn-circle btn-xs" @click="adjustQuantity(-1)">
|
||||
<MdiMinus class="h-3 w-3" />
|
||||
<MdiMinus class="size-3" />
|
||||
</button>
|
||||
<button class="btn btn-circle btn-xs" @click="adjustQuantity(1)">
|
||||
<MdiPlus class="h-3 w-3" />
|
||||
<MdiPlus class="size-3" />
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
@@ -546,10 +550,10 @@
|
||||
<BaseCard v-if="photos && photos.length > 0">
|
||||
<template #title> Photos </template>
|
||||
<div
|
||||
class="container border-t border-gray-300 p-4 flex flex-wrap gap-2 mx-auto max-h-[500px] overflow-y-scroll scroll-bg"
|
||||
class="scroll-bg container mx-auto flex max-h-[500px] flex-wrap gap-2 overflow-y-scroll border-t border-gray-300 p-4"
|
||||
>
|
||||
<button v-for="(img, i) in photos" :key="i" @click="openDialog(img)">
|
||||
<img class="rounded max-h-[200px]" :src="img.src" />
|
||||
<img class="max-h-[200px] rounded" :src="img.src" />
|
||||
</button>
|
||||
</div>
|
||||
</BaseCard>
|
||||
@@ -587,7 +591,7 @@
|
||||
</template>
|
||||
</DetailsSection>
|
||||
<div v-else>
|
||||
<p class="text-base-content/70 px-6 pb-4">No attachments found</p>
|
||||
<p class="px-6 pb-4 text-base-content/70">No attachments found</p>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
|
||||
@@ -425,7 +425,7 @@
|
||||
name="text"
|
||||
:items="attachmentOpts"
|
||||
/>
|
||||
<div v-if="editState.type == 'photo'" class="flex gap-2 mt-3">
|
||||
<div v-if="editState.type == 'photo'" class="mt-3 flex gap-2">
|
||||
<input v-model="editState.primary" type="checkbox" class="checkbox" />
|
||||
<p class="text-sm">
|
||||
<span class="font-semibold">Primary Photo</span>
|
||||
@@ -439,9 +439,9 @@
|
||||
</BaseModal>
|
||||
|
||||
<section class="relative">
|
||||
<div class="my-4 justify-end flex gap-2 items-center sticky z-10 top-1">
|
||||
<div class="mr-auto tooltip tooltip-right" data-tip="Show Advanced View Options">
|
||||
<label class="label cursor-pointer mr-auto">
|
||||
<div class="sticky top-1 z-10 my-4 flex items-center justify-end gap-2">
|
||||
<div class="tooltip tooltip-right mr-auto" data-tip="Show Advanced View Options">
|
||||
<label class="label mr-auto cursor-pointer">
|
||||
<input v-model="preferences.editorAdvancedView" type="checkbox" class="toggle toggle-primary" />
|
||||
<span class="label-text ml-4"> Advanced </span>
|
||||
</label>
|
||||
@@ -452,7 +452,7 @@
|
||||
</template>
|
||||
Save
|
||||
</BaseButton>
|
||||
<BaseButton class="btn btn-sm btn-error" @click="deleteItem()">
|
||||
<BaseButton class="btn btn-error btn-sm" @click="deleteItem()">
|
||||
<MdiDelete class="mr-2" />
|
||||
Delete
|
||||
</BaseButton>
|
||||
@@ -461,9 +461,9 @@
|
||||
<BaseCard class="overflow-visible">
|
||||
<template #title> Edit Details </template>
|
||||
<template #title-actions>
|
||||
<div class="flex flex-wrap justify-between items-center mt-2 gap-4"></div>
|
||||
<div class="mt-2 flex flex-wrap items-center justify-between gap-4"></div>
|
||||
</template>
|
||||
<div class="px-5 pt-2 border-t mb-6 grid md:grid-cols-2 gap-4">
|
||||
<div class="mb-6 grid gap-4 border-t px-5 pt-2 md:grid-cols-2">
|
||||
<LocationSelector v-model="item.location" />
|
||||
<FormMultiselect v-model="item.labels" label="Labels" :items="labels ?? []" />
|
||||
<Autocomplete
|
||||
@@ -478,8 +478,8 @@
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-300 sm:p-0">
|
||||
<div v-for="field in mainFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
|
||||
<div class="pt-2 px-4 pb-4 sm:px-6 border-b border-gray-300">
|
||||
<div v-for="field in mainFields" :key="field.ref" class="grid grid-cols-1 sm:divide-y sm:divide-gray-300">
|
||||
<div class="border-b border-gray-300 px-4 pb-4 pt-2 sm:px-6">
|
||||
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<FormTextField
|
||||
v-else-if="field.type === 'text'"
|
||||
@@ -513,25 +513,25 @@
|
||||
|
||||
<BaseCard>
|
||||
<template #title> Custom Fields </template>
|
||||
<div class="px-5 border-t divide-y divide-gray-300 space-y-4">
|
||||
<div class="space-y-4 divide-y divide-gray-300 border-t px-5">
|
||||
<div
|
||||
v-for="(field, idx) in item.fields"
|
||||
:key="`field-${idx}`"
|
||||
class="grid grid-cols-2 md:grid-cols-4 gap-2"
|
||||
class="grid grid-cols-2 gap-2 md:grid-cols-4"
|
||||
>
|
||||
<!-- <FormSelect v-model:value="field.type" label="Field Type" :items="fieldTypes" value-key="value" /> -->
|
||||
<FormTextField v-model="field.name" label="Name" />
|
||||
<div class="flex items-end col-span-3">
|
||||
<div class="col-span-3 flex items-end">
|
||||
<FormTextField v-model="field.textValue" label="Value" />
|
||||
<div class="tooltip" data-tip="Delete">
|
||||
<button class="btn btn-sm btn-square mb-2 ml-2" @click="item.fields.splice(idx, 1)">
|
||||
<button class="btn btn-square btn-sm mb-2 ml-2" @click="item.fields.splice(idx, 1)">
|
||||
<MdiDelete />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-5 pb-4 mt-4 flex justify-end">
|
||||
<div class="mt-4 flex justify-end px-5 pb-4">
|
||||
<BaseButton size="sm" @click="addField"> Add </BaseButton>
|
||||
</div>
|
||||
</BaseCard>
|
||||
@@ -539,7 +539,7 @@
|
||||
<div
|
||||
v-if="preferences.editorAdvancedView"
|
||||
ref="attDropZone"
|
||||
class="overflow-visible card bg-base-100 shadow-xl sm:rounded-lg"
|
||||
class="card overflow-visible bg-base-100 shadow-xl sm:rounded-lg"
|
||||
>
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6">Attachments</h3>
|
||||
@@ -555,7 +555,7 @@
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
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"
|
||||
@click="clickUpload"
|
||||
>
|
||||
<input ref="refAttachmentInput" hidden type="file" @change="uploadImage" />
|
||||
@@ -570,20 +570,20 @@
|
||||
:key="attachment.id"
|
||||
class="grid grid-cols-6 justify-between py-3 pl-3 pr-4 text-sm"
|
||||
>
|
||||
<p class="my-auto col-span-4">
|
||||
<p class="col-span-4 my-auto">
|
||||
{{ attachment.document.title }}
|
||||
</p>
|
||||
<p class="my-auto">
|
||||
{{ capitalize(attachment.type) }}
|
||||
</p>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<div class="flex justify-end gap-2">
|
||||
<div class="tooltip" data-tip="Delete">
|
||||
<button class="btn btn-sm btn-square" @click="deleteAttachment(attachment.id)">
|
||||
<button class="btn btn-square btn-sm" @click="deleteAttachment(attachment.id)">
|
||||
<MdiDelete />
|
||||
</button>
|
||||
</div>
|
||||
<div class="tooltip" data-tip="Edit">
|
||||
<button class="btn btn-sm btn-square" @click="openAttachmentEditDialog(attachment)">
|
||||
<button class="btn btn-square btn-sm" @click="openAttachmentEditDialog(attachment)">
|
||||
<MdiPencil />
|
||||
</button>
|
||||
</div>
|
||||
@@ -593,7 +593,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="preferences.editorAdvancedView" class="overflow-visible card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div v-if="preferences.editorAdvancedView" class="card overflow-visible bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6">Purchase Details</h3>
|
||||
</div>
|
||||
@@ -601,9 +601,9 @@
|
||||
<div
|
||||
v-for="field in purchaseFields"
|
||||
:key="field.ref"
|
||||
class="sm:divide-y sm:divide-gray-300 grid grid-cols-1"
|
||||
class="grid grid-cols-1 sm:divide-y sm:divide-gray-300"
|
||||
>
|
||||
<div class="pt-2 px-4 pb-4 sm:px-6 border-b border-gray-300">
|
||||
<div class="border-b border-gray-300 px-4 pb-4 pt-2 sm:px-6">
|
||||
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<FormTextField
|
||||
v-else-if="field.type === 'text'"
|
||||
@@ -635,7 +635,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="preferences.editorAdvancedView" class="overflow-visible card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div v-if="preferences.editorAdvancedView" class="card overflow-visible bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6">Warranty Details</h3>
|
||||
</div>
|
||||
@@ -643,9 +643,9 @@
|
||||
<div
|
||||
v-for="field in warrantyFields"
|
||||
:key="field.ref"
|
||||
class="sm:divide-y sm:divide-gray-300 grid grid-cols-1"
|
||||
class="grid grid-cols-1 sm:divide-y sm:divide-gray-300"
|
||||
>
|
||||
<div class="pt-2 px-4 pb-4 sm:px-6 border-b border-gray-300">
|
||||
<div class="border-b border-gray-300 px-4 pb-4 pt-2 sm:px-6">
|
||||
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<FormTextField
|
||||
v-else-if="field.type === 'text'"
|
||||
@@ -677,13 +677,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="preferences.editorAdvancedView" class="overflow-visible card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div v-if="preferences.editorAdvancedView" class="card overflow-visible bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6">Sold Details</h3>
|
||||
</div>
|
||||
<div class="border-t border-gray-300 sm:p-0">
|
||||
<div v-for="field in soldFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
|
||||
<div class="pt-2 pb-4 px-4 sm:px-6 border-b border-gray-300">
|
||||
<div v-for="field in soldFields" :key="field.ref" class="grid grid-cols-1 sm:divide-y sm:divide-gray-300">
|
||||
<div class="border-b border-gray-300 px-4 pb-4 pt-2 sm:px-6">
|
||||
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<FormTextField
|
||||
v-else-if="field.type === 'text'"
|
||||
|
||||
@@ -188,7 +188,7 @@
|
||||
<DatePicker v-model="entry.scheduledDate" label="Scheduled Date" />
|
||||
<FormTextArea v-model="entry.description" label="Notes" />
|
||||
<FormTextField v-model="entry.cost" autofocus label="Cost" />
|
||||
<div class="py-2 flex justify-end">
|
||||
<div class="flex justify-end py-2">
|
||||
<BaseButton type="submit" class="ml-2 mt-2">
|
||||
<template #icon>
|
||||
<MdiPost />
|
||||
@@ -200,11 +200,11 @@
|
||||
</BaseModal>
|
||||
|
||||
<section class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<StatCard
|
||||
v-for="stat in stats"
|
||||
:key="stat.id"
|
||||
class="stats block shadow-xl border-l-primary"
|
||||
class="stats block border-l-primary shadow-xl"
|
||||
:title="stat.title"
|
||||
:value="stat.value"
|
||||
:type="stat.type"
|
||||
@@ -228,7 +228,7 @@
|
||||
</div>
|
||||
<div class="container space-y-6">
|
||||
<BaseCard v-for="e in log.entries" :key="e.id">
|
||||
<BaseSectionHeader class="p-6 border-b border-b-gray-300">
|
||||
<BaseSectionHeader class="border-b border-b-gray-300 p-6">
|
||||
<span class="text-base-content">
|
||||
{{ e.name }}
|
||||
</span>
|
||||
@@ -253,7 +253,7 @@
|
||||
<div class="p-6">
|
||||
<Markdown :source="e.description" />
|
||||
</div>
|
||||
<div class="flex justify-end p-4 gap-1">
|
||||
<div class="flex justify-end gap-1 p-4">
|
||||
<BaseButton size="sm" @click="openEditDialog(e)">
|
||||
<template #icon>
|
||||
<MdiEdit />
|
||||
@@ -274,7 +274,7 @@
|
||||
class="relative block w-full rounded-lg border-2 border-dashed border-base-content p-12 text-center"
|
||||
@click="newEntry()"
|
||||
>
|
||||
<MdiWrenchClock class="h-16 w-16 inline" />
|
||||
<MdiWrenchClock class="inline size-16" />
|
||||
<span class="mt-2 block text-sm font-medium text-gray-900"> Create Your First Entry </span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
<template>
|
||||
<BaseContainer cmp="section">
|
||||
<BaseSectionHeader> Add an Item To Your Inventory </BaseSectionHeader>
|
||||
<form class="max-w-3xl mx-auto my-5 space-y-6" @submit.prevent="submit">
|
||||
<div class="divider collapse-title px-0 cursor-pointer">Required Information</div>
|
||||
<div class="bg-base-200 card">
|
||||
<form class="mx-auto my-5 max-w-3xl space-y-6" @submit.prevent="submit">
|
||||
<div class="collapse-title divider cursor-pointer px-0">Required Information</div>
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body">
|
||||
<FormTextField v-model="form.name" label="Name" />
|
||||
<FormTextArea v-model="form.description" label="Description" limit="1000" />
|
||||
@@ -76,7 +76,7 @@
|
||||
</div>
|
||||
<div v-if="show.sold" class="card bg-base-200">
|
||||
<div class="card-body">
|
||||
<div class="grid md:grid-cols-2 gap-2">
|
||||
<div class="grid gap-2 md:grid-cols-2">
|
||||
<FormTextField v-model="form.soldTime" label="Sold Time" />
|
||||
<FormTextField v-model="form.soldPrice" label="Sold Price" />
|
||||
<FormTextField v-model="form.soldTo" label="Sold To" />
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
const includeArchived = useRouteQuery("archived", false);
|
||||
const fieldSelector = useRouteQuery("fieldSelector", false);
|
||||
const negateLabels = useRouteQuery("negateLabels", false);
|
||||
const orderBy = useRouteQuery("orderBy", "name");
|
||||
|
||||
const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
|
||||
const hasNext = computed(() => page.value * pageSize.value < total.value);
|
||||
@@ -157,6 +158,12 @@
|
||||
return data;
|
||||
});
|
||||
|
||||
watch(includeArchived, (newV, oldV) => {
|
||||
if (newV !== oldV) {
|
||||
search();
|
||||
}
|
||||
});
|
||||
|
||||
watch(fieldSelector, (newV, oldV) => {
|
||||
if (newV === false && oldV === true) {
|
||||
fieldTuples.value = [];
|
||||
@@ -169,6 +176,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
watch(orderBy, (newV, oldV) => {
|
||||
if (newV !== oldV) {
|
||||
search();
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchValues(field: string): Promise<string[]> {
|
||||
if (fieldValuesCache.value[field]) {
|
||||
return fieldValuesCache.value[field];
|
||||
@@ -201,6 +214,7 @@
|
||||
pageSize: pageSize.value,
|
||||
includeArchived: includeArchived.value ? "true" : "false",
|
||||
negateLabels: negateLabels.value ? "true" : "false",
|
||||
orderBy: orderBy.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -231,6 +245,7 @@
|
||||
includeArchived: includeArchived.value,
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
orderBy: orderBy.value,
|
||||
fields,
|
||||
});
|
||||
|
||||
@@ -278,6 +293,7 @@
|
||||
archived: includeArchived.value ? "true" : "false",
|
||||
fieldSelector: fieldSelector.value ? "true" : "false",
|
||||
negateLabels: negateLabels.value ? "true" : "false",
|
||||
orderBy: orderBy.value,
|
||||
pageSize: pageSize.value,
|
||||
page: page.value,
|
||||
q: query.value,
|
||||
@@ -311,6 +327,7 @@
|
||||
fieldSelector: "false",
|
||||
pageSize: 10,
|
||||
page: 1,
|
||||
orderBy: "name",
|
||||
q: "",
|
||||
loc: [],
|
||||
lab: [],
|
||||
@@ -325,11 +342,11 @@
|
||||
<template>
|
||||
<BaseContainer class="mb-16">
|
||||
<div v-if="locations && labels">
|
||||
<div class="flex flex-wrap md:flex-nowrap gap-4 items-end">
|
||||
<div class="flex flex-wrap items-end gap-4 md:flex-nowrap">
|
||||
<div class="w-full">
|
||||
<FormTextField v-model="query" placeholder="Search" />
|
||||
<div v-if="byAssetId" class="text-sm pl-2 pt-2">
|
||||
<p>Querying Asset ID Number: {{ parsedAssetId }}</p>
|
||||
<FormTextField v-model="query" :placeholder="$t('global.search')" />
|
||||
<div v-if="byAssetId" class="pl-2 pt-2 text-sm">
|
||||
<p>{{ $t("items.query_id", { id: parsedAssetId }) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<BaseButton class="btn-block md:w-auto" @click.prevent="submit">
|
||||
@@ -337,69 +354,77 @@
|
||||
<MdiLoading v-if="loading" class="animate-spin" />
|
||||
<MdiMagnify v-else />
|
||||
</template>
|
||||
Search
|
||||
{{ $t("global.search") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap md:flex-nowrap gap-2 w-full py-2">
|
||||
<SearchFilter v-model="selectedLocations" label="Locations" :options="locationFlatTree">
|
||||
<div class="flex w-full flex-wrap gap-2 py-2 md:flex-nowrap">
|
||||
<SearchFilter v-model="selectedLocations" :label="$t('global.locations')" :options="locationFlatTree">
|
||||
<template #display="{ item }">
|
||||
<div>
|
||||
<div class="flex w-full">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div v-if="item.name != item.treeString" class="text-xs mt-1">
|
||||
<div v-if="item.name != item.treeString" class="mt-1 text-xs">
|
||||
{{ item.treeString }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SearchFilter>
|
||||
<SearchFilter v-model="selectedLabels" label="Labels" :options="labels" />
|
||||
<SearchFilter v-model="selectedLabels" :label="$t('global.labels')" :options="labels" />
|
||||
<div class="dropdown">
|
||||
<label tabindex="0" class="btn btn-xs">Options</label>
|
||||
<label tabindex="0" class="btn btn-xs">{{ $t("items.options") }}</label>
|
||||
<div
|
||||
tabindex="0"
|
||||
class="dropdown-content mt-1 max-h-72 p-4 w-64 overflow-auto shadow bg-base-100 rounded-md -translate-x-24"
|
||||
class="dropdown-content mt-1 max-h-72 w-64 -translate-x-24 overflow-auto rounded-md bg-base-100 p-4 shadow"
|
||||
>
|
||||
<label class="label cursor-pointer mr-auto">
|
||||
<input v-model="includeArchived" type="checkbox" class="toggle toggle-sm toggle-primary" />
|
||||
<span class="label-text ml-4"> Include Archived Items </span>
|
||||
<label class="label mr-auto cursor-pointer">
|
||||
<input v-model="includeArchived" type="checkbox" class="toggle toggle-primary toggle-sm" />
|
||||
<span class="label-text ml-4"> {{ $t("items.include_archive") }} </span>
|
||||
</label>
|
||||
<label class="label cursor-pointer mr-auto">
|
||||
<input v-model="fieldSelector" type="checkbox" class="toggle toggle-sm toggle-primary" />
|
||||
<span class="label-text ml-4"> Field Selector </span>
|
||||
<label class="label mr-auto cursor-pointer">
|
||||
<input v-model="fieldSelector" type="checkbox" class="toggle toggle-primary toggle-sm" />
|
||||
<span class="label-text ml-4"> {{ $t("items.field_selector") }} </span>
|
||||
</label>
|
||||
<label class="label cursor-pointer mr-auto">
|
||||
<input v-model="negateLabels" type="checkbox" class="toggle toggle-sm toggle-primary" />
|
||||
<span class="label-text ml-4"> Negate selected labels </span>
|
||||
<label class="label mr-auto cursor-pointer">
|
||||
<input v-model="negateLabels" type="checkbox" class="toggle toggle-primary toggle-sm" />
|
||||
<span class="label-text ml-4"> {{ $t("items.negate_labels") }} </span>
|
||||
</label>
|
||||
<label class="label mr-auto cursor-pointer">
|
||||
<select v-model="orderBy" class="select select-bordered select-sm">
|
||||
<option value="name" selected>{{ $t("global.name") }}</option>
|
||||
<option value="createdAt">{{ $t("items.created_at") }}</option>
|
||||
<option value="updatedAt">{{ $t("items.updated_at") }}</option>
|
||||
</select>
|
||||
<span class="label-text ml-4"> {{ $t("items.order_by") }} </span>
|
||||
</label>
|
||||
<hr class="my-2" />
|
||||
<BaseButton class="btn-block btn-sm" @click="reset"> Reset Search</BaseButton>
|
||||
<BaseButton class="btn-sm btn-block" @click="reset"> {{ $t("items.reset_search") }} </BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown ml-auto dropdown-end">
|
||||
<label tabindex="0" class="btn btn-xs">Tips</label>
|
||||
<div class="dropdown dropdown-end ml-auto">
|
||||
<label tabindex="0" class="btn btn-xs">{{ $t("items.tips") }}</label>
|
||||
<div
|
||||
tabindex="0"
|
||||
class="dropdown-content mt-1 p-4 w-[325px] text-sm overflow-auto shadow bg-base-100 rounded-md"
|
||||
class="dropdown-content mt-1 w-[325px] overflow-auto rounded-md bg-base-100 p-4 text-sm shadow"
|
||||
>
|
||||
<p class="text-base">Search Tips</p>
|
||||
<p class="text-base">{{ $t("items.tips_sub") }}</p>
|
||||
<ul class="mt-1 list-disc pl-6">
|
||||
<li>
|
||||
Location and label filters use the 'OR' operation. If more than one is selected only one will be
|
||||
required for a match.
|
||||
{{ $t("items.tip_1") }}
|
||||
</li>
|
||||
<li>Searches prefixed with '#'' will query for a asset ID (example '#000-001')</li>
|
||||
<li>
|
||||
Field filters use the 'OR' operation. If more than one is selected only one will be required for a
|
||||
match.
|
||||
{{ $t("items.tip_2") }}
|
||||
</li>
|
||||
<li>
|
||||
{{ $t("items.tip_3") }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="fieldSelector" class="py-4 space-y-2">
|
||||
<p>Custom Fields</p>
|
||||
<div v-if="fieldSelector" class="space-y-2 py-4">
|
||||
<p>{{ $t("items.custom_fields") }}</p>
|
||||
<div v-for="(f, idx) in fieldTuples" :key="idx" class="flex flex-wrap gap-2">
|
||||
<div class="form-control w-full max-w-xs">
|
||||
<label class="label">
|
||||
@@ -407,7 +432,7 @@
|
||||
</label>
|
||||
<select
|
||||
v-model="fieldTuples[idx][0]"
|
||||
class="select-bordered select"
|
||||
class="select select-bordered"
|
||||
:items="allFields ?? []"
|
||||
@change="fetchValues(f[0])"
|
||||
>
|
||||
@@ -416,52 +441,56 @@
|
||||
</div>
|
||||
<div class="form-control w-full max-w-xs">
|
||||
<label class="label">
|
||||
<span class="label-text">Field Value</span>
|
||||
<span class="label-text">{{ $t("items.field_value") }}</span>
|
||||
</label>
|
||||
<select v-model="fieldTuples[idx][1]" class="select-bordered select" :items="fieldValuesCache[f[0]]">
|
||||
<select v-model="fieldTuples[idx][1]" class="select select-bordered" :items="fieldValuesCache[f[0]]">
|
||||
<option v-for="v in fieldValuesCache[f[0]]" :key="v" :value="v">{{ v }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-square btn-sm md:ml-0 ml-auto mt-auto mb-2"
|
||||
class="btn btn-square btn-sm mb-2 ml-auto mt-auto md:ml-0"
|
||||
@click="fieldTuples.splice(idx, 1)"
|
||||
>
|
||||
<MdiDelete class="w-5 h-5" />
|
||||
<MdiDelete class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<BaseButton type="button" class="btn-sm mt-2" @click="() => fieldTuples.push(['', ''])"> Add</BaseButton>
|
||||
<BaseButton type="button" class="btn-sm mt-2" @click="() => fieldTuples.push(['', ''])">
|
||||
{{ $t("items.add") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="mt-10">
|
||||
<BaseSectionHeader ref="itemsTitle"> Items </BaseSectionHeader>
|
||||
<p class="text-base font-medium flex items-center">
|
||||
{{ total }} Results
|
||||
<span class="text-base ml-auto"> Page {{ page }} of {{ totalPages }}</span>
|
||||
<BaseSectionHeader ref="itemsTitle"> {{ $t("global.items") }} </BaseSectionHeader>
|
||||
<p class="flex items-center text-base font-medium">
|
||||
{{ $t("items.results", { total: total }) }}
|
||||
<span class="ml-auto text-base"> {{ $t("items.pages", { page: page, totalPages: totalPages }) }} </span>
|
||||
</p>
|
||||
|
||||
<div ref="cardgrid" class="grid mt-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div ref="cardgrid" class="mt-4 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="hidden first:inline text-xl">No Items Found</div>
|
||||
<div class="hidden text-xl first:inline">{{ $t("items.no_results") }}</div>
|
||||
</div>
|
||||
<div v-if="items.length > 0 && (hasNext || hasPrev)" class="mt-10 flex gap-2 flex-col items-center">
|
||||
<div v-if="items.length > 0 && (hasNext || hasPrev)" class="mt-10 flex flex-col items-center gap-2">
|
||||
<div class="flex">
|
||||
<div class="btn-group">
|
||||
<button :disabled="!hasPrev" class="btn text-no-transform" @click="prev">
|
||||
<MdiChevronLeft class="mr-1 h-6 w-6" name="mdi-chevron-left" />
|
||||
Prev
|
||||
<button :disabled="!hasPrev" class="text-no-transform btn" @click="prev">
|
||||
<MdiChevronLeft class="mr-1 size-6" name="mdi-chevron-left" />
|
||||
{{ $t("items.prev_page") }}
|
||||
</button>
|
||||
<button v-if="hasPrev" class="btn text-no-transform" @click="page = 1">First</button>
|
||||
<button v-if="hasNext" class="btn text-no-transform" @click="page = totalPages">Last</button>
|
||||
<button :disabled="!hasNext" class="btn text-no-transform" @click="next">
|
||||
Next
|
||||
<MdiChevronRight class="ml-1 h-6 w-6" name="mdi-chevron-right" />
|
||||
<button v-if="hasPrev" class="text-no-transform btn" @click="page = 1">{{ $t("items.first") }}</button>
|
||||
<button v-if="hasNext" class="text-no-transform btn" @click="page = totalPages">
|
||||
{{ $t("items.last") }}
|
||||
</button>
|
||||
<button :disabled="!hasNext" class="text-no-transform btn" @click="next">
|
||||
{{ $t("items.next_page") }}
|
||||
<MdiChevronRight class="ml-1 size-6" name="mdi-chevron-right" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm font-bold">Page {{ page }} of {{ totalPages }}</p>
|
||||
<p class="text-sm font-bold">{{ $t("items.pages", { page: page, totalPages: totalPages }) }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</BaseContainer>
|
||||
|
||||
@@ -106,28 +106,28 @@
|
||||
</BaseModal>
|
||||
|
||||
<BaseContainer v-if="label">
|
||||
<div class="bg-base-100 rounded p-3">
|
||||
<div class="rounded bg-base-100 p-3">
|
||||
<header class="mb-2">
|
||||
<div class="flex flex-wrap items-end gap-2">
|
||||
<div class="avatar placeholder mb-auto">
|
||||
<div class="bg-neutral-focus text-neutral-content rounded-full w-12">
|
||||
<MdiPackageVariant class="h-7 w-7" />
|
||||
<div class="w-12 rounded-full bg-neutral-focus text-neutral-content">
|
||||
<MdiPackageVariant class="size-7" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl pb-1 flex items-center gap-3">
|
||||
<h1 class="flex items-center gap-3 pb-1 text-2xl">
|
||||
{{ label ? label.name : "" }}
|
||||
|
||||
<div
|
||||
v-if="items && items.totalPrice"
|
||||
class="text-xs bg-secondary text-secondary-content rounded-full px-2 py-1"
|
||||
class="rounded-full bg-secondary px-2 py-1 text-xs text-secondary-content"
|
||||
>
|
||||
<div>
|
||||
<Currency :amount="items.totalPrice" />
|
||||
</div>
|
||||
</div>
|
||||
</h1>
|
||||
<div class="flex gap-1 flex-wrap text-xs">
|
||||
<div class="flex flex-wrap gap-1 text-xs">
|
||||
<div>
|
||||
Created
|
||||
<DateTime :date="label?.createdAt" />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user