Merge remote-tracking branch 'origin/main'

This commit is contained in:
Matthew Kilgore
2025-06-07 12:41:11 -04:00
12 changed files with 560 additions and 134 deletions

View File

@@ -16,6 +16,9 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with:
go-version: "1.23"
cache-dependency-path: backend/go.mod
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:

View File

@@ -27,7 +27,8 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.21" go-version: "1.23"
cache-dependency-path: backend/go.mod
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:

View File

@@ -12,7 +12,8 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.21" go-version: "1.23"
cache-dependency-path: backend/go.mod
- name: Install Task - name: Install Task
uses: arduino/setup-task@v1 uses: arduino/setup-task@v1

View File

@@ -60,7 +60,8 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.21" go-version: "1.23"
cache-dependency-path: backend/go.mod
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
@@ -110,7 +111,8 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.21" go-version: "1.23"
cache-dependency-path: backend/go.mod
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:

View File

@@ -95,7 +95,7 @@ tasks:
desc: Runs all go tests using gotestsum - supports passing gotestsum args desc: Runs all go tests using gotestsum - supports passing gotestsum args
dir: backend dir: backend
cmds: cmds:
- gotestsum {{ .CLI_ARGS }} ./... - go test {{ .CLI_ARGS }} ./...
go:coverage: go:coverage:
desc: Runs all go tests with -race flag and generates a coverage report desc: Runs all go tests with -race flag and generates a coverage report

View File

@@ -190,10 +190,23 @@ func (svc *UserService) Login(ctx context.Context, username, password string, ex
return UserAuthTokenDetail{}, ErrorInvalidLogin return UserAuthTokenDetail{}, ErrorInvalidLogin
} }
if !hasher.CheckPasswordHash(password, usr.PasswordHash) { check, rehash := hasher.CheckPasswordHash(password, usr.PasswordHash)
if !check {
return UserAuthTokenDetail{}, ErrorInvalidLogin return UserAuthTokenDetail{}, ErrorInvalidLogin
} }
if rehash {
hash, err := hasher.HashPassword(password)
if err != nil {
log.Err(err).Msg("Failed to hash password")
return UserAuthTokenDetail{}, err
}
err = svc.repos.Users.ChangePassword(ctx, usr.ID, hash)
if err != nil {
return UserAuthTokenDetail{}, err
}
}
return svc.createSessionToken(ctx, usr.ID, extendedSession) return svc.createSessionToken(ctx, usr.ID, extendedSession)
} }
@@ -227,7 +240,8 @@ func (svc *UserService) ChangePassword(ctx Context, current string, new string)
return false return false
} }
if !hasher.CheckPasswordHash(current, usr.PasswordHash) { match, _ := hasher.CheckPasswordHash(current, usr.PasswordHash)
if !match {
log.Err(errors.New("current password is incorrect")).Msg("Failed to change password") log.Err(errors.New("current password is incorrect")).Msg("Failed to change password")
return false return false
} }

View File

@@ -1,14 +1,35 @@
package hasher package hasher
import ( import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt" "fmt"
"os" "os"
"strings"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
var enabled = true var enabled = true
type params struct {
memory uint32
iterations uint32
parallelism uint8
saltLength uint32
keyLength uint32
}
var p = &params{
memory: 64 * 1024,
iterations: 3,
parallelism: 2,
saltLength: 16,
keyLength: 32,
}
func init() { // nolint: gochecknoinits func init() { // nolint: gochecknoinits
disableHas := os.Getenv("UNSAFE_DISABLE_PASSWORD_PROJECTION") == "yes_i_am_sure" disableHas := os.Getenv("UNSAFE_DISABLE_PASSWORD_PROJECTION") == "yes_i_am_sure"
@@ -18,20 +39,108 @@ func init() { // nolint: gochecknoinits
} }
} }
func GenerateRandomBytes(n uint32) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return nil, err
}
return b, nil
}
func HashPassword(password string) (string, error) { func HashPassword(password string) (string, error) {
if !enabled { if !enabled {
return password, nil return password, nil
} }
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) salt, err := GenerateRandomBytes(p.saltLength)
return string(bytes), err if err != nil {
return "", err
}
hash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength)
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
encodedHash := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, 64*1024, 3, 2, b64Salt, b64Hash)
return encodedHash, err
} }
func CheckPasswordHash(password, hash string) bool { // CheckPasswordHash checks if the provided password matches the hash.
// Additionally, it returns a boolean indicating whether the password should be rehashed.
func CheckPasswordHash(password, hash string) (bool, bool) {
if !enabled { if !enabled {
return password == hash return password == hash, false
} }
// Compare Argon2id hash first
match, err := comparePasswordAndHash(password, hash)
if err != nil || !match {
// If argon2id hash fails or doesn't match, try bcrypt
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil if err == nil {
// If bcrypt hash matches, return true and indicate rehashing
return true, true
} else {
// If both fail, return false and indicate no rehashing
return false, false
}
}
return match, false
}
func comparePasswordAndHash(password, encodedHash string) (match bool, err error) {
// Extract the parameters, salt and derived key from the encoded password
// hash.
p, salt, hash, err := decodeHash(encodedHash)
if err != nil {
return false, err
}
// Derive the key from the other password using the same parameters.
otherHash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength)
// Check that the contents of the hashed passwords are identical. Note
// that we are using the subtle.ConstantTimeCompare() function for this
// to help prevent timing attacks.
if subtle.ConstantTimeCompare(hash, otherHash) == 1 {
return true, nil
}
return false, nil
}
func decodeHash(encodedHash string) (p *params, salt, hash []byte, err error) {
vals := strings.Split(encodedHash, "$")
if len(vals) != 6 {
return nil, nil, nil, fmt.Errorf("invalid hash format")
}
var version int
_, err = fmt.Sscanf(vals[2], "v=%d", &version)
if err != nil {
return nil, nil, nil, err
}
if version != argon2.Version {
return nil, nil, nil, fmt.Errorf("unsupported argon2 version: %d", version)
}
p = &params{}
_, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &p.memory, &p.iterations, &p.parallelism)
if err != nil {
return nil, nil, nil, err
}
salt, err = base64.RawStdEncoding.Strict().DecodeString(vals[4])
if err != nil {
return nil, nil, nil, err
}
p.saltLength = uint32(len(salt))
hash, err = base64.RawStdEncoding.Strict().DecodeString(vals[5])
if err != nil {
return nil, nil, nil, err
}
p.keyLength = uint32(len(hash))
return p, salt, hash, nil
} }

View File

@@ -1,11 +1,14 @@
package hasher package hasher
import "testing" import (
"testing"
)
func TestHashPassword(t *testing.T) { func TestHashPassword(t *testing.T) {
t.Parallel() t.Parallel()
type args struct { type args struct {
password string password string
invalidInputs []string
} }
tests := []struct { tests := []struct {
name string name string
@@ -16,12 +19,28 @@ func TestHashPassword(t *testing.T) {
name: "letters_and_numbers", name: "letters_and_numbers",
args: args{ args: args{
password: "password123456788", password: "password123456788",
invalidInputs: []string{"testPassword", "AnotherBadPassword", "ThisShouldNeverWork", "1234567890"},
}, },
}, },
{ {
name: "letters_number_and_special", name: "letters_number_and_special",
args: args{ args: args{
password: "!2afj3214pofajip3142j;fa", password: "!2afj3214pofajip3142j;fa",
invalidInputs: []string{"testPassword", "AnotherBadPassword", "ThisShouldNeverWork", "1234567890"},
},
},
{
name: "extra_long_password",
args: args{
password: "this_is_a_very_long_password_that_should_be_hashed_properly_and_still_work_with_the_check_function",
invalidInputs: []string{"testPassword", "AnotherBadPassword", "ThisShouldNeverWork", "1234567890"},
},
},
{
name: "empty_password",
args: args{
password: "",
invalidInputs: []string{"testPassword", "AnotherBadPassword", "ThisShouldNeverWork", "1234567890"},
}, },
}, },
} }
@@ -32,9 +51,17 @@ func TestHashPassword(t *testing.T) {
t.Errorf("HashPassword() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("HashPassword() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
if !CheckPasswordHash(tt.args.password, got) { check, _ := CheckPasswordHash(tt.args.password, got)
if !check {
t.Errorf("CheckPasswordHash() failed to validate password=%v against hash=%v", tt.args.password, got) t.Errorf("CheckPasswordHash() failed to validate password=%v against hash=%v", tt.args.password, got)
} }
for _, invalid := range tt.args.invalidInputs {
check, _ := CheckPasswordHash(invalid, got)
if check {
t.Errorf("CheckPasswordHash() improperly validated password=%v against hash=%v", invalid, got)
}
}
}) })
} }
} }

View File

@@ -41,6 +41,7 @@
class="absolute left-0 top-0 size-full cursor-pointer opacity-0" class="absolute left-0 top-0 size-full cursor-pointer opacity-0"
type="file" type="file"
accept="image/png,image/jpeg,image/gif,image/avif,image/webp;capture=camera" accept="image/png,image/jpeg,image/gif,image/avif,image/webp;capture=camera"
capture="environment"
multiple multiple
@change="previewImage" @change="previewImage"
/> />

View File

@@ -185,6 +185,7 @@
const preferences = useViewPreferences(); const preferences = useViewPreferences();
const defaultHeaders = [ const defaultHeaders = [
{ text: "items.asset_id", value: "assetId", enabled: false },
{ {
text: "items.name", text: "items.name",
value: "name", value: "name",

View File

@@ -2,32 +2,39 @@
"components": { "components": {
"app": { "app": {
"create_modal": { "create_modal": {
"createAndAddAnother": "Usa {shiftKey} + {enterKey} para criar e adicionar outro.", "createAndAddAnother": "Use {shiftKey} + {enterKey} para criar e adicionar outro.",
"enter": "Enter", "enter": "Enter",
"shift": "Shift" "shift": "Shift"
}, },
"import_dialog": { "import_dialog": {
"change_warning": "O comportamento para ficheiros importados com import_refs foi alterado. Se um import_ref está presente no ficheiro CSV, \no item vai ser atualizado com os valores no ficheiro CSV.", "change_warning": "O comportamento para importações com import_refs existente foi alterado. Se um import_ref estiver presente \nno ficheiro CSV, o item será atualizado com os valores do ficheiro CSV.",
"description": "Importe um ficheiro CSV com artigos, rótulos e localizações. Consulte a documentação para mais informações\nacerca do formato necessário.", "description": "Importe um ficheiro CSV contendo os seus itens, etiquetas e localizações. Consulte a documentação para mais \ninformações sobre o formato necessário.",
"title": "Importar ficheiro CSV", "title": "Importar Ficheiro CSV",
"toast": { "toast": {
"import_success": "Importação com sucesso!" "import_failed": "Falha na importação. Tente novamente mais tarde.",
"import_success": "Importação bem-sucedida!",
"please_select_file": "Selecione um ficheiro para importar."
} }
}, },
"outdated": { "outdated": {
"current_version": "Versão Atual", "current_version": "Versão Atual",
"dismiss": "Dispensar", "dismiss": "Dispensar",
"latest_version": "Versão mais recente", "latest_version": "Última Versão",
"new_version_available": "Nova Versão Disponível", "new_version_available": "Nova Versão Disponível",
"new_version_available_link": "Carregue aqui para ver as notas desta versão" "new_version_available_link": "Clique aqui para ver as notas da versão"
}
},
"form": {
"password": {
"toggle_show": "Alternar Visibilidade da Palavra-passe"
} }
}, },
"global": { "global": {
"copy_text": { "copy_text": {
"documentation": "Documentação", "documentation": "documentação",
"failed_to_copy": "Falha ao copiar o texto para a área de transferência", "failed_to_copy": "Falha ao copiar o texto para a área de transferência",
"https_required": "porque é necessário HTTPS", "https_required": "porque é necessário HTTPS",
"learn_more": "Saiba mais na nossa" "learn_more": "Saiba mais em"
}, },
"date_time": { "date_time": {
"ago": "{0} atrás", "ago": "{0} atrás",
@@ -35,7 +42,7 @@
"hour": "hora", "hour": "hora",
"hours": "horas", "hours": "horas",
"in": "em {0}", "in": "em {0}",
"just-now": "neste momento", "just-now": "agora mesmo",
"last-month": "mês passado", "last-month": "mês passado",
"last-week": "semana passada", "last-week": "semana passada",
"last-year": "ano passado", "last-year": "ano passado",
@@ -54,44 +61,75 @@
"yesterday": "ontem" "yesterday": "ontem"
}, },
"label_maker": { "label_maker": {
"browser_print": "Imprimir a partir do navegador", "browser_print": "Imprimir a partir do Navegador",
"confirm_description": "Tem a certeza que quer imprimir esta etiqueta?", "confirm_description": "Tem a certeza de que deseja imprimir esta etiqueta?",
"download": "Transferir Etiqueta", "download": "Transferir Etiqueta",
"print": "Imprimir etiqueta", "print": "Imprimir etiqueta",
"server_print": "Imprimir no Servidor", "server_print": "Imprimir no Servidor",
"titles": "Etiquetas", "titles": "Etiquetas",
"toast": { "toast": {
"load_status_failed": "Falha a carregar o estado", "load_status_failed": "Falha ao carregar o estado",
"print_failed": "Falha a criar a etiqueta", "print_failed": "Falha ao imprimir etiqueta",
"print_success": "Etiqueta impressa" "print_success": "Etiqueta impressa"
} }
}, },
"page_qr_code": { "page_qr_code": {
"page_url": "Endereço da página", "page_url": "URL da Página",
"qr_tooltip": "Mostrar o QR Code" "qr_tooltip": "Mostrar Código QR"
}, },
"password_score": { "password_score": {
"password_strength": "Força da Senha" "password_strength": "Robustez da Palavra-passe"
} }
}, },
"item": { "item": {
"attachments_list": {
"download": "Transferir",
"open_new_tab": "Abrir em novo separador"
},
"create_modal": { "create_modal": {
"delete_photo": "Eliminar foto",
"item_description": "Descrição do Item", "item_description": "Descrição do Item",
"item_name": "Nome do Item", "item_name": "Nome do Item",
"item_photo": "Imagem do Item📷", "item_photo": "Foto do Item 📷",
"item_quantity": "Quantidade do Item",
"parent_item": "Item Principal",
"rotate_photo": "Rodar foto",
"set_as_primary_photo": "Definir como foto { isPrimary, select, true {não-} false {} other {}}principal",
"title": "Criar Item", "title": "Criar Item",
"upload_photos": "Upload de Imagens" "toast": {
"already_creating": "Já está a criar um item",
"create_failed": "Não foi possível criar o item",
"create_success": "Item criado",
"failed_load_parent": "Falha ao carregar item principal - por favor, selecione manualmente",
"no_canvas_support": "O seu navegador não suporta operações de canvas",
"please_select_location": "Selecione uma localização.",
"rotate_failed": "Falha ao rodar imagem: { error }",
"rotate_process_failed": "Falha ao processar imagem rodada",
"some_photos_failed": "{count, plural, =0 {Sem fotos para carregar.} =1 {1 foto falhou ao carregar.} other {Algumas fotos falharam ao carregar.}}",
"upload_failed": "Falha ao carregar foto: { photoName }",
"upload_success": "{count, plural, =0 {Sem fotos carregadas.} =1 {Foto carregada com sucesso.} other {Todas as fotos foram carregadas com sucesso.}}",
"uploading_photos": "{count, plural, =0 {Sem fotos para carregar} =1 {A carregar 1 foto...} other {A carregar {count} fotos...}}"
},
"upload_photos": "Carregar Fotos",
"uploaded": "Foto Carregada"
},
"selector": {
"no_results": "Nenhum Resultado Encontrado",
"placeholder": "Selecionar...",
"search_placeholder": "Escreva para pesquisar..."
}, },
"view": { "view": {
"selectable": { "selectable": {
"card": "Cartão", "card": "Cartão",
"items": "Items", "items": "Itens",
"no_items": "Nenhum item para mostrar", "no_items": "Sem Itens para Exibir",
"table": "Tabela" "table": "Tabela"
}, },
"table": { "table": {
"headers": "Cabeçalhos",
"page": "Página", "page": "Página",
"rows_per_page": "Linhas por página" "rows_per_page": "Linhas por página",
"table_settings": "Definições da Tabela"
} }
} }
}, },
@@ -99,100 +137,154 @@
"create_modal": { "create_modal": {
"label_description": "Descrição da Etiqueta", "label_description": "Descrição da Etiqueta",
"label_name": "Nome da Etiqueta", "label_name": "Nome da Etiqueta",
"title": "Criar etiqueta" "title": "Criar Etiqueta",
"toast": {
"already_creating": "Já está a criar uma etiqueta",
"create_failed": "Não foi possível criar etiqueta",
"create_success": "Etiqueta criada",
"label_name_too_long": "O nome da etiqueta não pode ter mais de 50 caracteres"
}
}, },
"selector": { "selector": {
"select_labels": "Escolher Etiquetas" "select_labels": "Selecionar Etiquetas"
} }
}, },
"location": { "location": {
"create_modal": { "create_modal": {
"location_description": "Descrição do Local", "location_description": "Descrição da Localização",
"location_name": "Nome do Local", "location_name": "Nome da Localização",
"title": "Criar Localização" "title": "Criar Localização",
"toast": {
"already_creating": "Já está a criar uma localização",
"create_failed": "Não foi possível criar localização",
"create_success": "Localização criada"
}
}, },
"selector": { "selector": {
"no_location_found": "Nenhum Local Encontrado", "no_location_found": "Nenhuma localização encontrada",
"parent_location": "Localização ascendente", "parent_location": "Localização principal",
"search_location": "Pesquisar Localizações", "search_location": "Pesquisar Localizações",
"select_location": "Selecione uma localização" "select_location": "Selecionar uma Localização"
},
"tree": {
"no_locations": "Sem localizações disponíveis. Adicione novas localizações através do botão \n`<`span class=\"link-primary\"`>`Criar`<`/span`>` na barra de navegação."
} }
}, },
"quick_menu": { "quick_menu": {
"no_results": "Nenhum resultado encontrado.", "no_results": "Nenhum resultado encontrado.",
"shortcut_hint": "Utilize as teclas numéricas para selecionar rapidamente uma ação." "shortcut_hint": "Use as teclas numéricas para selecionar rapidamente uma ação."
} }
}, },
"global": { "global": {
"add": "Adicionar", "add": "Adicionar",
"build": "Build: { build }", "archived": "Arquivado",
"build": "Compilação: { build }",
"cancel": "Cancelar", "cancel": "Cancelar",
"confirm": "Confirmar", "confirm": "Confirmar",
"create": "Criar", "create": "Criar",
"create_and_add": "Criar e adicionar outro", "create_and_add": "Criar e Adicionar Outro",
"create_subitem": "Criar Subitem",
"created": "Criado", "created": "Criado",
"delete": "Apagar", "delete": "Eliminar",
"delete_confirm": "Tem a certeza de que deseja eliminar este item? ",
"demo_instance": "Esta é uma instância de demonstração",
"details": "Detalhes", "details": "Detalhes",
"duplicate": "Duplicar", "duplicate": "Duplicar",
"edit": "Editar", "edit": "Editar",
"email": "Email", "email": "Email",
"follow_dev": "Siga o programador", "follow_dev": "Siga o Desenvolvedor",
"github": "Código Fonte no Github", "footer": {
"items": "Items", "api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'API'</a>'",
"join_discord": "Juntar-se ao Discord", "version_link": "<a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/'{ version }\" target=\"_blank\"> Versão: { version } Compilação: { build } '</a>'"
"labels": "Marcadores", },
"github": "Projeto GitHub",
"insured": "Com seguro",
"items": "Itens",
"join_discord": "Junte-se ao Discord",
"labels": "Etiquetas",
"loading": "A Carregar...",
"locations": "Localizações", "locations": "Localizações",
"maintenance": "Manutenção", "maintenance": "Manutenção",
"name": "Nome", "name": "Nome",
"navigate": "Navegar", "navigate": "Navegar",
"password": "Senha", "password": "Palavra-passe",
"read_docs": "Ler os Documentos", "quantity": "Quantidade",
"read_docs": "Ler a Documentação",
"return_home": "Voltar à Página Inicial",
"save": "Guardar", "save": "Guardar",
"search": "Pesquisar", "search": "Pesquisar",
"sign_out": "Terminar sessão", "sign_out": "Terminar Sessão",
"submit": "Enviar", "submit": "Submeter",
"unknown": "Desconhecido",
"update": "Atualizar", "update": "Atualizar",
"updating": "A Atualizar",
"value": "Valor", "value": "Valor",
"version": "Versão: { version }", "version": "Versão: { version }",
"welcome": "Bem-vindo, { username }" "welcome": "Bem-vindo, { username }"
}, },
"home": { "home": {
"labels": "Etiquetas", "labels": "Etiquetas",
"quick_statistics": "Estatísticas rápidas", "quick_statistics": "Estatísticas Rápidas",
"recently_added": "Adicionados Recentemente", "recently_added": "Recentemente Adicionados",
"storage_locations": "Locais de Armazenamento", "storage_locations": "Localizações de Armazenamento",
"total_items": "Total de Items", "total_items": "Total de Itens",
"total_labels": "Total de Etiquetas", "total_labels": "Total de Etiquetas",
"total_locations": "Total de Locais", "total_locations": "Total de Localizações",
"total_value": "Valores Totais" "total_value": "Valor Total"
}, },
"index": { "index": {
"disabled_registration": "Registo Desativado", "disabled_registration": "Registo Desativado",
"dont_join_group": "Não quer juntar-se a um grupo?", "dont_join_group": "Não quer juntar-se a um grupo?",
"joining_group": "Está a juntar-se a um grupo existente!", "joining_group": "Está a Juntar-se a um Grupo Existente!",
"login": "Iniciar sessão", "login": "Iniciar Sessão",
"register": "Registar", "register": "Registar",
"remember_me": "Lembrar-me", "remember_me": "Lembrar-me",
"set_email": "Qual é o seu email?", "set_email": "Qual é o seu email?",
"set_name": "Como se chama?", "set_name": "Qual é o seu nome?",
"set_password": "Defina a sua senha", "set_password": "Defina a sua palavra-passe",
"tagline": "Acompanhe, organize e faça a gestão das suas coisas." "tagline": "Acompanhe, organize e faça a gestão das suas coisas.",
"title": "Organize e Etiquete os Seus Objetos",
"toast": {
"invalid_email": "Endereço de email inválido",
"invalid_email_password": "Email ou palavra-passe inválidos",
"login_success": "Sessão iniciada com sucesso",
"problem_registering": "Problema ao registar utilizador",
"user_registered": "Utilizador registado"
}
}, },
"items": { "items": {
"add": "Adicionar", "add": "Adicionar",
"advanced": "Avançado", "advanced": "Avançado",
"archived": "Arquivado", "archived": "Arquivado",
"asset_id": "Número de Património", "asset_id": "ID do Ativo",
"created_at": "Criado em", "associated_with_multiple": "Este ID de Ativo está associado a vários itens",
"custom_fields": "Campos personalizados", "attachment": "Anexo",
"attachments": "Anexos",
"changes_persisted_immediately": "Alterações nos anexos serão guardadas imediatamente",
"created_at": "Criado Em",
"custom_fields": "Campos Personalizados",
"delete_attachment_confirm": "Tem a certeza de que deseja eliminar este anexo?",
"delete_item_confirm": "Tem a certeza de que deseja eliminar este item?",
"description": "Descrição", "description": "Descrição",
"details": "Detalhes", "details": "Detalhes",
"drag_and_drop": "Arraste e largue ficheiros aqui ou clique para selecionar ficheiros",
"edit": {
"edit_attachment_dialog": {
"attachment_title": "Título do Anexo",
"attachment_type": "Tipo de Anexo",
"primary_photo": "Foto Principal",
"primary_photo_sub": "Esta opção só está disponível para fotos. Apenas uma foto pode ser principal. Ao selecionar esta, a atual será desmarcada.",
"select_type": "Selecionar um tipo",
"title": "Editar Anexo"
}
},
"edit_details": "Editar Detalhes", "edit_details": "Editar Detalhes",
"field_selector": "Selector de Campos", "field_selector": "Seletor de Campo",
"field_value": "Valor do Campo", "field_value": "Valor do Campo",
"first": "Primeiro", "first": "Primeiro",
"include_archive": "Incluir Items Arquivados", "include_archive": "Incluir Itens Arquivados",
"insured": "Assegurado", "insured": "Com seguro",
"invalid_asset_id": "ID de Ativo inválido",
"last": "Último", "last": "Último",
"lifetime_warranty": "Garantia Vitalícia", "lifetime_warranty": "Garantia Vitalícia",
"location": "Localização", "location": "Localização",
@@ -201,46 +293,84 @@
"manufacturer": "Fabricante", "manufacturer": "Fabricante",
"model_number": "Número do Modelo", "model_number": "Número do Modelo",
"name": "Nome", "name": "Nome",
"negate_labels": "Negar etiquetas selecionadas", "negate_labels": "Anular Etiquetas Selecionadas",
"next_page": "Página seguinte", "next_page": "Próxima Página",
"no_results": "Nenhum item encontrado", "no_attachments": "Sem anexos encontrados",
"no_results": "Nenhum Item Encontrado",
"notes": "Notas", "notes": "Notas",
"only_with_photo": "Apenas artigos com fotografia", "only_with_photo": "Apenas itens com foto",
"only_without_photo": "Apenas artigos sem fotografia", "only_without_photo": "Apenas itens sem foto",
"options": "Opções", "options": "Opções",
"order_by": "Ordenar por", "order_by": "Ordenar Por",
"pages": "Página { page } de { totalPages }", "pages": "Página { page } de { totalPages }",
"parent_item": "Item Principal",
"photo": "Foto", "photo": "Foto",
"photos": "Fotos", "photos": "Fotos",
"prev_page": "Página Anterior", "prev_page": "Página Anterior",
"purchase_date": "Data de Compra", "purchase_date": "Data de Compra",
"purchase_details": "Detalhes da Compra", "purchase_details": "Detalhes da Compra",
"purchase_price": "Preço de Compra", "purchase_price": "Preço de Compra",
"purchased_from": "Comprado em", "purchased_from": "Comprado Em",
"quantity": "Quantidade", "quantity": "Quantidade",
"query_id": "A consultar o número de identificação do ativo: { id }", "query_id": "A Consultar Número de ID de Ativo: { id }",
"receipt": "Recibo", "receipt": "Recibo",
"receipts": "Recibos", "receipts": "Recibos",
"reset_search": "Fazer Reset à Pesquisa", "reset_search": "Fazer Reset à Pesquisa",
"results": "{ total } Resultados", "results": "{ total } Resultados",
"select_field": "Selecionar um campo",
"serial_number": "Número de Série", "serial_number": "Número de Série",
"show_advanced_view_options": "Opções Avançadas", "show_advanced_view_options": "Mostrar Opções Avançadas de Visualização",
"sold_at": "Vendido em", "sold_at": "Vendido Em",
"sold_details": "Detalhes da Venda", "sold_details": "Detalhes da Venda",
"sold_price": "Preço de Venda", "sold_price": "Preço de Venda",
"sold_to": "Vendido a", "sold_to": "Vendido a",
"tip_1": "Os filtros de localização e etiqueta usam a operação 'OU'. Se forem selecionados vários, apenas\n um será utilizado para a pesquisa.", "sync_child_locations": "Sincronizar localizações dos subitens",
"tip_2": "As pesquisas prefixadas com '#' ' consultarão um ID de ativo (exemplo '#000-001')", "tip_1": "Os filtros de localização e etiqueta usam a operação 'OU'. Se mais do que um for selecionado, \nbasta que um corresponda.",
"tip_3": "Os filtros dos campos usal a operação 'OU'. Se mais do que um for seleccionado apenas um será necessário\npara fazer match.", "tip_2": "Pesquisas com o prefixo '#' irão procurar um ID de ativo (exemplo '#000-001')",
"tip_3": "Os filtros de campo usam a operação 'OU'. Se mais do que um for selecionado, \nbasta que um corresponda.",
"tips": "Dicas", "tips": "Dicas",
"tips_sub": "Dicas de pesquisa", "tips_sub": "Dicas de Pesquisa",
"updated_at": "Atualizado em", "toast": {
"asset_not_found": "Ativo não encontrado",
"attachment_deleted": "Anexo eliminado",
"attachment_updated": "Anexo atualizado",
"attachment_uploaded": "Anexo carregado",
"child_items_location_no_longer_synced": "As localizações dos subitens deixarão de estar sincronizadas com este item.",
"child_items_location_synced": "As localizações dos subitens foram sincronizadas com este item",
"child_location_desync": "Ao mudar a localização, a sincronização com a localização principal será desfeita",
"error_loading_parent_data": "Ocorreu um erro ao carregar os dados principais",
"failed_adjust_quantity": "Falha ao ajustar a quantidade",
"failed_delete_attachment": "Falha ao eliminar anexo",
"failed_delete_item": "Falha ao eliminar item",
"failed_duplicate_item": "Falha ao duplicar item",
"failed_load_asset": "Falha ao carregar ativo",
"failed_load_item": "Falha ao carregar item",
"failed_load_items": "Falha ao carregar itens",
"failed_save": "Falha ao guardar item",
"failed_save_no_location": "Falha ao guardar item: nenhuma localização selecionada",
"failed_search_items": "Falha ao pesquisar itens",
"failed_update_attachment": "Falha ao atualizar anexo",
"failed_upload_attachment": "Falha ao carregar anexo",
"item_deleted": "Item eliminado",
"item_saved": "Item guardado",
"quantity_cannot_negative": "A quantidade não pode ser negativa",
"sync_child_location": "O item principal selecionado sincroniza as localizações dos sub-itens com a sua. A localização foi atualizada."
},
"updated_at": "Atualizado Em",
"warranty": "Garantia", "warranty": "Garantia",
"warranty_details": "Detalhes de Garantia", "warranty_details": "Detalhes da Garantia",
"warranty_expires": "Garantia expira a" "warranty_expires": "Garantia expira a"
}, },
"labels": { "labels": {
"no_results": "Nenhuma etiqueta encontrada", "label_delete_confirm": "Tem a certeza de que deseja eliminar esta etiqueta? Esta ação não pode ser desfeita.",
"no_results": "Nenhuma Etiqueta Encontrada",
"toast": {
"failed_delete_label": "Falha ao eliminar etiqueta",
"failed_load_label": "Falha ao carregar etiqueta",
"failed_update_label": "Falha ao atualizar etiqueta",
"label_deleted": "Etiqueta eliminada",
"label_updated": "Etiqueta atualizada"
},
"update_label": "Atualizar Etiqueta" "update_label": "Atualizar Etiqueta"
}, },
"languages": { "languages": {
@@ -253,91 +383,228 @@
"fi-FI": "Finlandês", "fi-FI": "Finlandês",
"fr": "Francês", "fr": "Francês",
"hu": "Húngaro", "hu": "Húngaro",
"id-ID": "Indonésio",
"it": "Italiano", "it": "Italiano",
"ja-JP": "Japonês",
"ko-KR": "Coreano", "ko-KR": "Coreano",
"nb-NO": "Bonkal Norueguês", "lb-LU": "Luxemburguês (Luxemburgo)",
"lt-LT": "Lituano (Lituânia)",
"nb-NO": "Norueguês Bokmål",
"nl": "Holandês", "nl": "Holandês",
"pl": "Polaco", "pl": "Polaco",
"pt-BR": "Português do Brasil", "pt-BR": "Português (Brasil)",
"pt-PT": "Português (Portugal)", "pt-PT": "Português (Portugal)",
"ro-RO": "Romeno", "ro-RO": "Romeno",
"ru": "Russo", "ru": "Russo",
"sk-SK": "Eslováquio", "sk-SK": "Eslovaco",
"sl": "Esloveno", "sl": "Esloveno",
"sq-AL": "Albanês",
"sv": "Sueco", "sv": "Sueco",
"ta-IN": "Tamil", "ta-IN": "Tâmil",
"th-TH": "Tailandês", "th-TH": "Tailandês",
"tr": "Turco", "tr": "Turco",
"uk-UA": "Ucraniano",
"zh-CN": "Chinês (Simplificado)", "zh-CN": "Chinês (Simplificado)",
"zh-HK": "Chinês (Hong Kong)", "zh-HK": "Chinês (Hong Kong)",
"zh-MO": "Chinês (Macau)", "zh-MO": "Chinês (Macau)",
"zh-TW": "Chinês (Tradicional)" "zh-TW": "Chinês (Tradicional)"
}, },
"locations": { "locations": {
"no_results": "Nenhuma localização encontrada" "child_locations": "Localizações Filho",
"collapse_tree": "Recolher Árvore",
"expand_tree": "Expandir Árvore",
"location_items_delete_confirm": "Tem a certeza de que deseja eliminar esta localização e todos os seus itens? Esta ação não pode ser desfeita.",
"no_results": "Nenhuma Localização Encontrada",
"toast": {
"failed_delete_location": "Falha ao eliminar localização",
"failed_load_location": "Falha ao carregar localização",
"failed_update_location": "Falha ao atualizar localização",
"location_deleted": "Localização eliminada",
"location_updated": "Localização atualizada"
},
"update_location": "Atualizar Localização"
},
"maintenance": {
"filter": {
"both": "Ambos",
"completed": "Concluído",
"scheduled": "Agendado"
},
"list": {
"complete": "Concluir",
"create_first": "Criar a Sua Primeira Entrada",
"delete": "Eliminar",
"duplicate": "Duplicar",
"edit": "Editar",
"new": "Novo"
},
"modal": {
"completed_date": "Data de Conclusão",
"cost": "Custo",
"delete_confirmation": "Tem a certeza de que deseja eliminar esta entrada?",
"edit_action": "Atualizar",
"edit_title": "Editar Entrada",
"entry_name": "Nome da Entrada",
"new_action": "Criar",
"new_title": "Nova Entrada",
"notes": "Notas",
"scheduled_date": "Data Agendada"
},
"monthly_average": "Média Mensal",
"toast": {
"failed_to_create": "Falha ao criar entrada",
"failed_to_delete": "Falha ao eliminar entrada",
"failed_to_update": "Falha ao atualizar entrada"
},
"total_cost": "Custo Total",
"total_entries": "Total de Entradas"
},
"menu": {
"create_item": "Item / Ativo",
"create_label": "Etiqueta",
"create_location": "Localização",
"home": "Início",
"locations": "Localizações",
"maintenance": "Manutenção",
"profile": "Perfil",
"scanner": "Leitor",
"search": "Pesquisar",
"tools": "Ferramentas"
}, },
"profile": { "profile": {
"active": "Ativo", "active": "Ativo",
"change_password": "Alterar Senha", "change_password": "Alterar Palavra-passe",
"currency_format": "Moeda", "currency_format": "Formato de Moeda",
"current_password": "Senha Atual", "current_password": "Palavra-passe Atual",
"delete_account": "Eliminar Conta", "delete_account": "Eliminar Conta",
"delete_account_sub": "Eliminar a sua conta e todos os dados associados. Esta ação não pode ser revertida.", "delete_account_confirm": "Tem a certeza de que deseja eliminar a sua conta? Se for o último membro do grupo, todos os dados serão eliminados. Esta ação não pode ser desfeita.",
"delete_account_sub": "Eliminar a sua conta e todos os dados associados. Esta ação não pode ser desfeita.",
"delete_notifier_confirm": "Tem a certeza de que deseja eliminar este notificador?",
"display_legacy_header": "{ currentValue, select, true {Desativar Cabeçalho Legacy} false {Ativar Cabeçalho Legacy} other {Sem Resultado} }",
"enabled": "Ativado", "enabled": "Ativado",
"gen_invite": "Gerar link de convite", "example": "Exemplo",
"group_settings": "Definições de Grupo", "gen_invite": "Gerar Link de Convite",
"group_settings_sub": "Definições de Grupo Partilhadas. Pode ter de atualizar a página para algumas definições serem aplicadas.", "group_settings": "Definições do Grupo",
"group_settings_sub": "Definições Partilhadas do Grupo. Pode ser necessário atualizar a página para aplicar algumas definições.",
"inactive": "Inativo", "inactive": "Inativo",
"language": "Idioma", "language": "Idioma",
"new_password": "Nova Senha", "new_password": "Nova Palavra-passe",
"no_notifiers": "Nenhum notificador configurado", "no_notifiers": "Nenhum notificador configurado",
"no_override": "Sem substituição",
"notifier_modal": "{ type, select, true {Editar} false {Criar} other {Outro} } Notificador", "notifier_modal": "{ type, select, true {Editar} false {Criar} other {Outro} } Notificador",
"notifiers": "Notificadores", "notifiers": "Notificadores",
"notifiers_sub": "Receba notificações para os próximos lembretes de manutenção", "notifiers_sub": "Receba notificações para os próximos lembretes de manutenção",
"override_locale": "Substituir Data e Idioma da Moeda",
"test": "Testar", "test": "Testar",
"theme_settings": "Definições do tema", "theme_settings": "Definições do Tema",
"theme_settings_sub": "As configurações do tema são guardadas no armazenamento local do seu navegador. Pode alterar o tema em qualquer altura.\nSe encontrar algum problema com a alteração do tema, tente refrescar o browser.", "theme_settings_sub": "As definições do tema são guardadas no armazenamento local do seu navegador. Pode alterá-lo a qualquer\n momento. Se tiver problemas ao definir o tema, tente atualizar a página.",
"toast": {
"account_deleted": "A sua conta foi eliminada.",
"failed_change_password": "Falha ao alterar palavra-passe.",
"failed_create_notifier": "Falha ao criar notificador.",
"failed_delete_account": "Falha ao eliminar a sua conta.",
"failed_delete_notifier": "Falha ao eliminar notificador.",
"failed_get_currencies": "Falha ao obter moedas",
"failed_test_notifier": "Falha ao testar notificador.",
"failed_update_group": "Falha ao atualizar grupo",
"failed_update_notifier": "Falha ao atualizar notificador.",
"group_updated": "Grupo atualizado",
"notifier_test_success": "Teste do notificador bem-sucedido.",
"password_changed": "Palavra-passe alterada com sucesso."
},
"update_group": "Atualizar Grupo", "update_group": "Atualizar Grupo",
"update_language": "Atualizar Idioma", "update_language": "Atualizar Idioma",
"url": "URL", "url": "URL",
"user_profile": "Perfil de Utilizador", "user_profile": "Perfil de Utilizador",
"user_profile_sub": "Convide utilizadores e faça a gestão da sua conta." "user_profile_sub": "Convide utilizadores e faça a gestão da sua conta."
}, },
"reports": {
"label_generator": {
"asset_end": "Fim do Ativo",
"asset_start": "Início do Ativo",
"base_url": "URL Base",
"bordered_labels": "Etiquetas com Contorno",
"generate_page": "Gerar Página",
"input_placeholder": "Escreva aqui",
"instruction_1": "O Gerador de Etiquetas Homebox é uma ferramenta para ajudar a imprimir etiquetas para o seu inventário Homebox.\n Estas etiquetas são preparadas antecipadamente para poder imprimi-las em quantidade e aplicá-las depois",
"instruction_2": "Assim, estas etiquetas funcionam imprimindo um código QR com o URL e informação do AssetID na etiqueta. \n Se desativou os AssetIDs nas definições do Homebox, ainda pode usar esta ferramenta, mas os AssetIDs não irão referenciar nenhum item",
"instruction_3": "Esta funcionalidade está em fase inicial de desenvolvimento e pode mudar em futuras versões. \nSe tiver sugestões, envie através da '<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\">'Discussão no GitHub'</a>'",
"label_height": "Altura da Etiqueta",
"label_width": "Largura da Etiqueta",
"measure_type": "Tipo de Medida",
"page_bottom_padding": "Margem Inferior da Página",
"page_height": "Altura da Página",
"page_left_padding": "Margem Esquerda da Página",
"page_right_padding": "Margem Direita da Página",
"page_top_padding": "Margem Superior da Página",
"page_width": "Largura da Página",
"qr_code_example": "Exemplo de Código QR",
"tip_1": "As predefinições aqui são configuradas para as \n '<a href=\"https://www.avery.com/templates/5260\">'folhas de etiquetas Avery 5260'</a>'. \n Se estiver a usar outro tipo de folha, terá de ajustar as definições para se ajustarem às suas folhas.",
"tip_2": "Se estiver a personalizar a folha, as dimensões estão em polegadas. Ao criar a folha 5260, percebi que as dimensões\n usadas nos seus modelos não correspondiam ao necessário para imprimir corretamente dentro das caixas.\n '<b>'Prepare-se para alguma tentativa e erro.'</b>'",
"tip_3": "Ao imprimir, certifique-se de que: \n'<ol><li>'Defina as margens como 0 ou Nenhuma'</li><li>'Define o escalamento para 100%'</li><li>'Desativa a impressão frente e verso'</li><li>'Imprime uma página de teste antes de imprimir várias'</li></ol>'",
"tips": "Dicas",
"title": "Gerador de Etiquetas",
"toast": {
"page_too_small_card": "O tamanho da página é demasiado pequeno para o tamanho do cartão"
}
}
},
"scanner": {
"error": "Ocorreu um erro ao digitalizar",
"invalid_url": "URL de código de barras inválido",
"no_sources": "Sem fontes de vídeo disponíveis",
"permission_denied": "Permissão da câmara negada. Permita o acesso à câmara nas definições do navegador",
"select_video_source": "Escolha uma fonte de vídeo",
"title": "Leitor",
"unsupported": "A API Media Stream não é suportada sem HTTPS"
},
"tools": { "tools": {
"actions": "Ações de Inventário", "actions": "Ações de Inventário",
"actions_set": { "actions_set": {
"ensure_ids": "Garantir IDs de Ativos", "ensure_ids": "Garantir IDs dos Ativos",
"ensure_ids_button": "Garantir IDs de Ativos", "ensure_ids_button": "Garantir IDs dos Ativos",
"ensure_ids_sub": "Garante que todos os items no inventário tenham um campo asset_id válido. Para tal, procuramos o valor mais alto do asset_id na base de dados e aplicamos o próximo valor a cada item que não tem um asset_id definido, ordenando pelo campo created_at.", "ensure_ids_confirm": "Tem a certeza de que deseja garantir que todos os ativos têm um ID? Isto pode demorar e não pode ser desfeito.",
"ensure_import_refs": "Garantir Refs de Importação", "ensure_ids_sub": "Garante que todos os itens do inventário têm um campo asset_id válido. É feito encontrando o maior asset_id atual na base de dados e atribuindo o próximo valor a cada item sem asset_id definido. A ordem é baseada no campo created_at.",
"ensure_import_refs_button": "Garantir Refs de Importação", "ensure_import_refs": "Garantir Referências de Importação",
"ensure_import_refs_sub": "Garante que todos os itens do seu inventário tenham um campo import_ref válido", "ensure_import_refs_button": "Garantir Referências de Importação",
"set_primary_photo": "Definir foto principal", "ensure_import_refs_sub": "Garante que todos os itens do inventário têm um campo import_ref válido. É feito gerando aleatoriamente uma sequência de 8 caracteres para cada item sem import_ref definido.",
"set_primary_photo_button": "Definir foto principal", "set_primary_photo": "Definir Foto Principal",
"set_primary_photo_sub": "Na versão v0.10.0 do Homebox, o campo de imagem principal foi adicionado aos anexos do tipo foto. Esta acção vai definir a imagem principal para a primeira imagem na lista de anexos na base de dados, se ainda não estiver definido. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'GitHub PR #576'</a>'", "set_primary_photo_button": "Definir Foto Principal",
"zero_datetimes": "Zero Item Data e Hora", "set_primary_photo_confirm": "Tem a certeza de que deseja definir fotos principais? Isto pode demorar e não pode ser desfeito.",
"zero_datetimes_button": "Zero Data Horas do Item" "set_primary_photo_sub": "Na versão v0.10.0 do Homebox foi adicionado o campo de imagem principal aos anexos do tipo foto. Esta ação define a imagem principal como a primeira imagem no array de anexos da base de dados, caso ainda não esteja definida. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'Ver PR #576 no GitHub'</a>'",
"zero_datetimes": "Zerar Datas e Horas dos Itens",
"zero_datetimes_button": "Zerar Datas e Horas dos Itens",
"zero_datetimes_confirm": "Tem a certeza de que deseja repor todos os valores de data e hora? Isto pode demorar e não pode ser desfeito.",
"zero_datetimes_sub": "Repõe o valor da hora de todos os campos de data/hora do inventário para o início do dia. Isto corrige um erro inicial no site que causava problemas na apresentação correta das datas. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'Ver Issue #236 no GitHub para mais detalhes.'</a>'"
}, },
"actions_sub": "Aplicar Ações ao seu inventário em massa. Estas acções são irreversíveis. '<b>'Cuidado.'</b>'", "actions_sub": "Aplicar Ações em Massa no inventário. Estas ações são irreversíveis. '<b>'Tenha cuidado.'</b>'",
"import_export": "Importar/Exportar", "import_export": "Importar/Exportar",
"import_export_set": { "import_export_set": {
"export": "Exportar Inventário", "export": "Exportar Inventário",
"export_button": "Exportar Inventário", "export_button": "Exportar Inventário",
"export_sub": "Exporta o formato CSV padrão para o Homebox. Isto vai exportar todos os items do inventário.", "export_sub": "Exporta o formato CSV padrão para o Homebox. Isto vai exportar todos os items do inventário.",
"import": "Importar inventário", "import": "Importar Inventário",
"import_button": "Importar inventário", "import_button": "Importar Inventário",
"import_ref_confirm": "Tem a certeza de que deseja garantir que todos os ativos têm um import_ref? Isto pode demorar e não pode ser desfeito.",
"import_sub": "Importa o formato standard do CSV para o Homebox. Sem uma coluna '<code>'HB.import_ref'</code>' , isto '<b>'não '</b>' vai escrever por cima de items existentes no inventário, apenas adiciona novos. As linhas com uma coluna '<code>'HB.import_ref'</code>' vão ser fundidas com os items que tenham o mesmo import_ref, se existirem." "import_sub": "Importa o formato standard do CSV para o Homebox. Sem uma coluna '<code>'HB.import_ref'</code>' , isto '<b>'não '</b>' vai escrever por cima de items existentes no inventário, apenas adiciona novos. As linhas com uma coluna '<code>'HB.import_ref'</code>' vão ser fundidas com os items que tenham o mesmo import_ref, se existirem."
}, },
"import_export_sub": "Importe e exporte o seu inventário de e para um ficheiro CSV. Isto é útil para migrar o inventário para uma nova instância do Homebox.", "import_export_sub": "Importar e exportar o seu inventário de/para um ficheiro CSV. Útil para migração entre instâncias do Homebox.",
"reports": "Relatórios", "reports": "Relatórios",
"reports_set": { "reports_set": {
"asset_labels": "Etiquetas de ID do Ativo", "asset_labels": "Etiquetas de ID do Ativo",
"asset_labels_button": "Gerador de Rótulos", "asset_labels_button": "Gerador de Etiquetas",
"asset_labels_sub": "Gera um PDF imprimível de etiquetas para um um conjunto de Activos. Estas não são específicas do seu inventário por isso pode imprimi-las com antecedência e aplicar as mesmas ao inventário quando o receber.", "asset_labels_sub": "Gera um PDF imprimível de etiquetas para um conjunto de IDs de Activos. Estas não são específicas do seu inventário por isso pode imprimi-las com antecedência e aplicar as mesmas ao inventário quando o receber.",
"bill_of_materials": "Lista de materiais", "bill_of_materials": "Lista de Materiais",
"bill_of_materials_button": "Gerar BOM", "bill_of_materials_button": "Gerar Lista de Materiais",
"bill_of_materials_sub": "Gera um ficheiro CSV (Valores Separados por Vírgulas) que pode ser importado para uma folha de cálculo. Isto é um resumo do seu inventário com a informação do item e do preço." "bill_of_materials_sub": "Gera um ficheiro CSV (Valores Separados por Vírgulas) que pode ser importado para uma folha de cálculo. Isto é um resumo do seu inventário com a informação do item e do preço."
}, },
"reports_sub": "Gere relatórios diferentes para o seu inventário." "reports_sub": "Gerar diferentes relatórios para o seu inventário.",
"toast": {
"asset_success": "{ results } ativos foram atualizados.",
"failed_ensure_ids": "Falha ao garantir IDs dos ativos.",
"failed_ensure_import_refs": "Falha ao garantir referências de importação.",
"failed_set_primary_photos": "Falha ao definir fotos principais.",
"failed_zero_datetimes": "Falha ao repor datas e horas."
}
} }
} }

View File

@@ -325,7 +325,7 @@
if (preferences.value.showEmpty) { if (preferences.value.showEmpty) {
return true; return true;
} }
return item.value?.purchaseFrom || item.value?.purchasePrice !== 0; return item.value?.purchaseFrom || item.value?.purchasePrice !== 0 || validDate(item.value?.purchaseTime);
}); });
const purchaseDetails = computed<Details>(() => { const purchaseDetails = computed<Details>(() => {
@@ -358,7 +358,7 @@
if (preferences.value.showEmpty) { if (preferences.value.showEmpty) {
return true; return true;
} }
return item.value?.soldTo || item.value?.soldPrice !== 0; return item.value?.soldTo || item.value?.soldPrice !== 0 || validDate(item.value?.soldTime);
}); });
const soldDetails = computed<Details>(() => { const soldDetails = computed<Details>(() => {