You've already forked reloading-manager
No changes made
This commit is contained in:
1
frontend/.env
Normal file
1
frontend/.env
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API=http://localhost:8080
|
||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
1
frontend/.nvmrc
Normal file
1
frontend/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
v22.14.0
|
||||
18
frontend/README.md
Normal file
18
frontend/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
14
frontend/index.html
Normal file
14
frontend/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Load Data Manager</title>
|
||||
<script src="https://kit.fontawesome.com/1a962b66c9.js" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
2951
frontend/package-lock.json
generated
Normal file
2951
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
frontend/package.json
Normal file
31
frontend/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primeuix/themes": "^1.0.0",
|
||||
"@tailwindcss/postcss": "^4.0.15",
|
||||
"@tailwindcss/vite": "^4.0.15",
|
||||
"@vuelidate/core": "^2.0.3",
|
||||
"@vuelidate/validators": "^2.0.4",
|
||||
"axios": "^1.8.4",
|
||||
"primevue": "^4.3.2",
|
||||
"sass": "^1.86.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"tailwindcss": "^4.0.15",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.2",
|
||||
"vue-tsc": "^2.2.8"
|
||||
}
|
||||
}
|
||||
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
22
frontend/src/App.vue
Normal file
22
frontend/src/App.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<Toast />
|
||||
<TopMenubar />
|
||||
<div class="flex flex-row m-10 justify-around">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
const TopMenubar = defineAsyncComponent(() => import('./components/TopMenubar.vue'));
|
||||
const Toast = defineAsyncComponent(() => import('primevue/toast'));
|
||||
|
||||
</script>
|
||||
<style>
|
||||
@import "tailwindcss";
|
||||
|
||||
html, body {
|
||||
font-family: 'Nunito', sans-serif;
|
||||
}
|
||||
</style>
|
||||
1
frontend/src/assets/vue.svg
Normal file
1
frontend/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
0
frontend/src/axios/index.ts
Normal file
0
frontend/src/axios/index.ts
Normal file
109
frontend/src/components/TopMenubar.vue
Normal file
109
frontend/src/components/TopMenubar.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div>
|
||||
<Menubar :model="items">
|
||||
<template #start>
|
||||
<router-link to="/">
|
||||
Load Data Manager
|
||||
</router-link>
|
||||
<i class="ml-3 mr-3 fa-thin fa-gun"></i>
|
||||
::
|
||||
</template>
|
||||
</Menubar>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
import { MenuItem } from 'primevue/menuitem'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { icons } from '../lib/icons.ts'
|
||||
|
||||
const Menubar = defineAsyncComponent(() => import('primevue/menubar'))
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const items = ref<MenuItem[]>([
|
||||
{
|
||||
label: 'Loads',
|
||||
items: [
|
||||
{
|
||||
label: 'New',
|
||||
command: () => router.push('/loads/add'),
|
||||
icon: icons.add
|
||||
},
|
||||
{
|
||||
label: 'Edit/Search',
|
||||
command: () => router.push('/loads/search'),
|
||||
icon: icons.edit
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Components',
|
||||
items: [
|
||||
{
|
||||
label: 'Bullets',
|
||||
items: [
|
||||
{
|
||||
label: 'New',
|
||||
command: () => router.push('/bullets/add'),
|
||||
icon: icons.add
|
||||
},
|
||||
{
|
||||
label: 'List',
|
||||
command: () => router.push('/bullets'),
|
||||
icon: icons.list
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Powders',
|
||||
items: [
|
||||
{
|
||||
label: 'New',
|
||||
command: () => router.push('/powders/add'),
|
||||
icon: icons.add
|
||||
},
|
||||
{
|
||||
label: 'List',
|
||||
command: () => router.push('/powders'),
|
||||
icon: icons.list
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Primers',
|
||||
items: [
|
||||
{
|
||||
label: 'New',
|
||||
command: () => router.push('/primers/add'),
|
||||
icon: icons.add
|
||||
},
|
||||
{
|
||||
label: 'List',
|
||||
command: () => router.push('/primers'),
|
||||
icon: icons.list
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Manufacturers',
|
||||
items: [
|
||||
{
|
||||
label: 'Manage Manufacturers',
|
||||
command: () => router.push('/manufacturers'),
|
||||
icon: icons.add
|
||||
},
|
||||
{
|
||||
label: 'Add Manufacturer',
|
||||
command: () => router.push('/manufacturers/add'),
|
||||
icon: icons.edit
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
separator: true
|
||||
}
|
||||
])
|
||||
</script>
|
||||
8
frontend/src/lib/icons.ts
Normal file
8
frontend/src/lib/icons.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const icons = {
|
||||
add: 'fa-thin fa-sharp fa-plus',
|
||||
edit: 'fa-thin fa-sharp fa-pencil',
|
||||
list: 'fa-thin fa-sharp fa-list',
|
||||
upload: 'fa-thin fa-sharp fa-upload',
|
||||
save: 'fa-thin fa-sharp fa-floppy-disk',
|
||||
delete: 'fa-thin fa-sharp fa-trash',
|
||||
}
|
||||
20
frontend/src/main.ts
Normal file
20
frontend/src/main.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createApp } from 'vue'
|
||||
import Aura from '@primeuix/themes/aura'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import PrimeVue from 'primevue/config'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: Aura,
|
||||
},
|
||||
})
|
||||
|
||||
import ToastService from 'primevue/toastservice'
|
||||
|
||||
app.use(ToastService)
|
||||
|
||||
app.mount('#app')
|
||||
16
frontend/src/pages/Index.vue
Normal file
16
frontend/src/pages/Index.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<Card>
|
||||
<template #title> Welcome to Your Load Database </template>
|
||||
<template #content>
|
||||
<p>
|
||||
This is the place to track and create loads for all of your ammunition. Use the links at the top to navigate.
|
||||
</p>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
const Card = defineAsyncComponent(() => import('primevue/card'))
|
||||
|
||||
</script>
|
||||
166
frontend/src/pages/bullets/Add.vue
Normal file
166
frontend/src/pages/bullets/Add.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<Card class="w-full md:w-1/2">
|
||||
<template #title>Add New Bullet</template>
|
||||
<template #content>
|
||||
<div class="w-full">
|
||||
<label>Manufacturer</label>
|
||||
<Dropdown :options="manufacturers" option-label="name" option-value="id" v-model="bullet.manufacturer_id"
|
||||
class="w-full" />
|
||||
</div>
|
||||
<div class="m-5" v-if="bullet.manufacturer_id === '-- Add --'">
|
||||
<div class="w-full m-1">
|
||||
<label>Manufacturer Name</label>
|
||||
<div class="w-full">
|
||||
<InputText v-model="add.name" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full m-1">
|
||||
<label>Manufacturer Url</label>
|
||||
<div class="w-full">
|
||||
<InputText v-model="add.url" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full mt-3">
|
||||
<label>Bullet Name</label>
|
||||
<InputText v-model="bullet.name" class="w-full" />
|
||||
</div>
|
||||
<div class="w-full mt-3">
|
||||
<label>Bullet Diameter</label>
|
||||
<InputNumber v-model="bullet.diameter" prefix="." :min="0" :max="999" class="w-full" />
|
||||
</div>
|
||||
<div class="w-full mt-3">
|
||||
<label>Grain Weight</label>
|
||||
<InputNumber v-model="bullet.weight" suffix=" gr" :min="0" :max="1000" class="w-full" />
|
||||
</div>
|
||||
<div class="w-full mt-3">
|
||||
<label>Picture</label>
|
||||
<FileUpload v-model="file" mode="basic" @select="fileSelected" customUpload />
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="mt-3">
|
||||
<Button :loading="loading" @click="create" label="Add" :icon="icons.add" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
import axios, { AxiosError } from 'axios'
|
||||
import { Manufacturer } from '../../types/manufacturer'
|
||||
import { Response } from '../../types/Response'
|
||||
import { FileUploadSelectEvent } from 'primevue/fileupload'
|
||||
import { Bullet } from '../../types/bullet'
|
||||
import { ToastMessageOptions } from 'primevue/toast'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { icons } from '../../lib/icons.ts'
|
||||
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
|
||||
const Dropdown = defineAsyncComponent(() => import('primevue/dropdown'))
|
||||
const InputText = defineAsyncComponent(() => import('primevue/inputtext'))
|
||||
const InputNumber = defineAsyncComponent(() => import('primevue/inputnumber'))
|
||||
const FileUpload = defineAsyncComponent(() => import('primevue/fileupload'))
|
||||
const Button = defineAsyncComponent(() => import('primevue/button'))
|
||||
const Card = defineAsyncComponent(() => import('primevue/card'))
|
||||
|
||||
const add = ref({
|
||||
name: '',
|
||||
url: '',
|
||||
})
|
||||
|
||||
const bullet = ref({
|
||||
name: '',
|
||||
diameter: 0,
|
||||
manufacturer_id: '',
|
||||
weight: 0,
|
||||
meta: {},
|
||||
})
|
||||
|
||||
const file = ref('')
|
||||
const manufacturers = ref<Manufacturer[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const loadManufacturers = async () => {
|
||||
await axios.get(`${import.meta.env.VITE_API}/manufacturer`).then((r: Response<Manufacturer[]>) => {
|
||||
manufacturers.value.push({
|
||||
name: '-- Add --',
|
||||
id: '-- Add --',
|
||||
} as Manufacturer)
|
||||
|
||||
if (!r.data.payload) {
|
||||
return
|
||||
}
|
||||
|
||||
r.data.payload.forEach((m: Manufacturer) => {
|
||||
manufacturers.value.push(m)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const fileSelected = (event: FileUploadSelectEvent) => {
|
||||
file.value = event.files[0]
|
||||
}
|
||||
|
||||
const createManufacturer = async () => {
|
||||
return axios.post(`${import.meta.env.VITE_API}/manufacturer`, {
|
||||
name: add.value.name,
|
||||
url: add.value.url,
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
}).then((r: Response<Manufacturer>) => {
|
||||
manufacturers.value.push(r.data.payload)
|
||||
bullet.value.manufacturer_id = r.data.payload.id
|
||||
})
|
||||
}
|
||||
|
||||
const create = async () => {
|
||||
loading.value = true
|
||||
if (bullet.value.manufacturer_id === '-- Add --') {
|
||||
try {
|
||||
await createManufacturer()
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: e,
|
||||
life: 3000,
|
||||
} as ToastMessageOptions)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let formData = new FormData()
|
||||
formData.append('photo', file.value)
|
||||
formData.append('manufacturer_id', bullet.value.manufacturer_id)
|
||||
formData.append('weight', bullet.value.weight.toString())
|
||||
formData.append('name', bullet.value.name)
|
||||
formData.append('diameter', bullet.value.diameter.toString())
|
||||
|
||||
axios.post(`${import.meta.env.VITE_API}/bullet`,
|
||||
formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
},
|
||||
).then((r: Response<string>) => {
|
||||
router.push(`/bullets/edit/${r.data.payload}`)
|
||||
}).catch((e: AxiosError<{ message: string }>) => {
|
||||
loading.value = false
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: e.response?.data.message,
|
||||
life: 3000,
|
||||
} as ToastMessageOptions)
|
||||
})
|
||||
}
|
||||
|
||||
loadManufacturers()
|
||||
</script>
|
||||
178
frontend/src/pages/bullets/Edit.vue
Normal file
178
frontend/src/pages/bullets/Edit.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<Card class="w-full md:w-1/2">
|
||||
<template #title>
|
||||
Edit Bullet
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="grid-cols-1">
|
||||
<div>
|
||||
<label>Manufacturer</label>
|
||||
<Dropdown
|
||||
class="w-full"
|
||||
:options="manufacturers"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
v-model="bullet.manufacturer_id"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label>Bullet Name</label>
|
||||
<InputText class="w-full" v-model="bullet.name" />
|
||||
</div>
|
||||
<div class="w-full mt-3">
|
||||
<label>Bullet Diameter</label>
|
||||
<InputNumber v-model="bullet.diameter" prefix="." :min="0" :max="999" class="w-full" />
|
||||
</div>
|
||||
<div class="w-full mt-3">
|
||||
<label>Grain Weight</label>
|
||||
<InputNumber v-model="bullet.weight" suffix=" gr" :min="0" :max="1000" class="w-full" />
|
||||
</div>
|
||||
<div class="w-full mt-3">
|
||||
<label>Picture</label>
|
||||
<FileUpload v-model="file" mode="basic" @select="fileSelected" customUpload>
|
||||
<template #uploadicon>
|
||||
<i :class="icons.upload" class="mr-3" />
|
||||
</template>
|
||||
</FileUpload>
|
||||
<div class="mt-3">
|
||||
<Image v-if="!loading" :src="pictureUrl" alt="picture" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Button
|
||||
type="button"
|
||||
:loading="loading"
|
||||
label="Save"
|
||||
@click="save"
|
||||
:icon="icons.save"
|
||||
class="mr-3"
|
||||
/>
|
||||
<Button
|
||||
severity="danger"
|
||||
label="Delete"
|
||||
:icon="icons.delete"
|
||||
@click="deleteBullet"
|
||||
/>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { Manufacturer } from '../../types/manufacturer'
|
||||
import { Response } from '../../types/Response'
|
||||
import { FileUploadSelectEvent } from 'primevue/fileupload'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { icons } from '../../lib/icons.ts'
|
||||
|
||||
const Dropdown = defineAsyncComponent(() => import('primevue/dropdown'))
|
||||
const InputText = defineAsyncComponent(() => import('primevue/inputtext'))
|
||||
const InputNumber = defineAsyncComponent(() => import('primevue/inputnumber'))
|
||||
const FileUpload = defineAsyncComponent(() => import('primevue/fileupload'))
|
||||
const Image = defineAsyncComponent(() => import('primevue/image'))
|
||||
const Button = defineAsyncComponent(() => import('primevue/button'))
|
||||
const Card = defineAsyncComponent(() => import('primevue/card'))
|
||||
|
||||
interface BulletResponse {
|
||||
id: string
|
||||
name: string
|
||||
diameter: number
|
||||
weight: number
|
||||
manufacturer: {
|
||||
id: string
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
|
||||
const loading = ref(false)
|
||||
const bullet = ref({
|
||||
diameter: 0,
|
||||
weight: 0,
|
||||
name: '',
|
||||
manufacturer_id: '',
|
||||
})
|
||||
const file = ref('')
|
||||
const fileSelected = (event: FileUploadSelectEvent) => {
|
||||
console.log(event.files[0])
|
||||
file.value = event.files[0]
|
||||
}
|
||||
const manufacturers = ref<Manufacturer[]>([])
|
||||
|
||||
const fetchManufacturers = async () => {
|
||||
const response = await axios.get<any, Response<Manufacturer[]>>(`${import.meta.env.VITE_API}/manufacturer`)
|
||||
manufacturers.value = response.data.payload
|
||||
}
|
||||
|
||||
const pictureUrl = computed(() => {
|
||||
const cache = new Date().getMilliseconds()
|
||||
return import.meta.env.VITE_API + `/bullet/${route.params.id}/photo?cache=${cache}`
|
||||
})
|
||||
|
||||
const deleteBullet = () => {
|
||||
axios.delete(`${import.meta.env.VITE_API}/bullet/${route.params.id}`).then(() => {
|
||||
router.push('/bullets')
|
||||
}).catch((error) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: error.response.data.message,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const get = async () => {
|
||||
const response = await axios.get<any, Response<BulletResponse>>(
|
||||
`${import.meta.env.VITE_API}/bullet/${route.params.id}`)
|
||||
bullet.value.manufacturer_id = response.data.payload.manufacturer.id
|
||||
bullet.value.name = response.data.payload.name
|
||||
bullet.value.diameter = response.data.payload.diameter
|
||||
bullet.value.weight = response.data.payload.weight
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
loading.value = true
|
||||
|
||||
let formData = new FormData()
|
||||
formData.append('photo', file.value)
|
||||
formData.append('manufacturer_id', bullet.value.manufacturer_id)
|
||||
formData.append('weight', bullet.value.weight.toString())
|
||||
formData.append('name', bullet.value.name)
|
||||
formData.append('diameter', bullet.value.diameter.toString())
|
||||
axios.post(`${import.meta.env.VITE_API}/bullet/${route.params.id}`,
|
||||
formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
},
|
||||
).then(() => {
|
||||
loading.value = false
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'saved!',
|
||||
detail: 'bullet saved',
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
get()
|
||||
}).catch((e) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'error',
|
||||
detail: e.response?.data.message,
|
||||
life: 3000,
|
||||
})
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
fetchManufacturers()
|
||||
get()
|
||||
</script>
|
||||
48
frontend/src/pages/bullets/List.vue
Normal file
48
frontend/src/pages/bullets/List.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<Card class="w-full md:w-2/3">
|
||||
<template #title>
|
||||
Bullets
|
||||
</template>
|
||||
<template #content>
|
||||
<DataTable :value="bullets">
|
||||
<template #empty>
|
||||
<div class="p-text-center p-text-secondary p-text-bold p-text-uppercase">No bullets found</div>
|
||||
</template>
|
||||
<Column header="Bullet">
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.manufacturer.name }}
|
||||
{{ slotProps.data.weight }}gr
|
||||
{{ slotProps.data.name }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="Edit">
|
||||
<template #body="slotProps">
|
||||
<router-link class="p-button p-button-link p-button-sm" :to="`/bullets/edit/${slotProps.data.id}`">
|
||||
<i :class="icons.edit"></i>
|
||||
</router-link>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { Bullet } from '../../types/bullet'
|
||||
import {Response} from "../../types/Response";
|
||||
import Column from 'primevue/column'
|
||||
import { icons } from '../../lib/icons.ts'
|
||||
|
||||
const DataTable = defineAsyncComponent(() => import('primevue/datatable'))
|
||||
const Card = defineAsyncComponent(() => import('primevue/card'))
|
||||
|
||||
const bullets = ref<Bullet[]>([])
|
||||
|
||||
const fetchBullets = async () => {
|
||||
const response = await axios.get<any, Response<Bullet[]>>(`${import.meta.env.VITE_API}/bullet`)
|
||||
bullets.value = response.data.payload
|
||||
}
|
||||
|
||||
fetchBullets()
|
||||
</script>
|
||||
297
frontend/src/pages/loads/Add.vue
Normal file
297
frontend/src/pages/loads/Add.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<Card class="md:w-2/3 w-full">
|
||||
<template #title>
|
||||
Add New Load
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="grid grid-cols-1">
|
||||
<div>
|
||||
Cartridge
|
||||
</div>
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="grow mr-3">
|
||||
<Select v-model="load.cartridge" :options="cartridges" optionLabel="label" optionValue="value"
|
||||
class="w-full" />
|
||||
<Message v-if="v$.$dirty && v$.cartridge.$invalid" :value="false" size="small" severity="error"
|
||||
variant="simple">Cartridge Required
|
||||
</Message>
|
||||
</div>
|
||||
<div class="cursor-pointer" @click="addCartridgeDialog = true">
|
||||
<i :class="icons.add" class="fa-2x" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 mr-3">
|
||||
Bullet
|
||||
</div>
|
||||
<div class="w-full mt-3">
|
||||
<Select v-model="load.bullet" :invalid="v$.$dirty && v$.bullet.$invalid" :options="bullets"
|
||||
optionLabel="label" optionValue="value" class="w-full" />
|
||||
<Message v-if="v$.$dirty && v$.bullet.$invalid" :value="false" size="small" severity="error" variant="simple">
|
||||
Bullet Required
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 mr-3">
|
||||
Powder
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="grow mr-3">
|
||||
<Select v-model="load.powder" :options="powders" optionLabel="label" optionValue="value" class="w-full" />
|
||||
<Message v-if="v$.$dirty && v$.powder.$invalid" :value="false" size="small" severity="error"
|
||||
variant="simple">Powder Required
|
||||
</Message>
|
||||
</div>
|
||||
<div>
|
||||
<InputNumber v-model="load.powderGrs" placeholder="00.0 gr" suffix=" gr" :minFractionDigits="1"
|
||||
:maxFractionDigits="1" :min="0"
|
||||
:max="1000" class="w-full" />
|
||||
<Message v-if="v$.$dirty && v$.powderGrs.$invalid" :value="false" size="small" severity="error"
|
||||
variant="simple">Grains Required
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 mr-3">
|
||||
Primer
|
||||
</div>
|
||||
<div class="w-full mt-3">
|
||||
<Select v-model="load.primer" :options="primers" optionLabel="label" optionValue="value" class="w-full" />
|
||||
<Message v-if="v$.$dirty && v$.primer.$invalid" :value="false" size="small" severity="error"
|
||||
variant="simple">Primer Required
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 mr-3">
|
||||
COL
|
||||
</div>
|
||||
<div class="w-full mt-3">
|
||||
<InputMask placeholder="0.000" v-model="load.col" mask="9.999" class="w-full" />
|
||||
<Message v-if="v$.$dirty && v$.col.$invalid" :value="false" size="small" severity="error"
|
||||
variant="simple">COL Required
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<div class="w-full mt-5">
|
||||
<label>Picture</label>
|
||||
<Message v-if="v$.$dirty && !file" :value="false" size="small" severity="error"
|
||||
variant="simple">Picture Required
|
||||
</Message>
|
||||
<FileUpload v-model="file" mode="basic" @select="fileSelected" customUpload />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Button label="Add" :icon="icons.add" @click="add" />
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Dialog @update:visible="addCartridgeDialog = false" modal :visible="addCartridgeDialog" :style="{ width: '35rem' }">
|
||||
<template #header>
|
||||
Add Cartridge
|
||||
</template>
|
||||
<div class="grid grid-cols-1">
|
||||
<div>
|
||||
Name
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<InputText :invalid="cartridgeName === ''" v-model="cartridgeName" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button :disabled="cartridgeName === ''" label="Add" :icon="icons.add" @click="addCartridgeName" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
</template>
|
||||
<script lang="ts">
|
||||
interface Select {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface Cartridge {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { Bullet } from '../../types/bullet'
|
||||
import { defineAsyncComponent, onMounted, ref } from 'vue'
|
||||
import { Primers } from '../../types/primers'
|
||||
import { Powder } from '../../types/powder'
|
||||
import axios from 'axios'
|
||||
import { Response } from '../../types/Response'
|
||||
import { icons } from '../../lib/icons.ts'
|
||||
import { FileUploadSelectEvent } from 'primevue/fileupload'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required } from '@vuelidate/validators'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import router from '../../router'
|
||||
|
||||
const Card = defineAsyncComponent(() => import('primevue/card'))
|
||||
const Select = defineAsyncComponent(() => import('primevue/select'))
|
||||
const Button = defineAsyncComponent(() => import('primevue/button'))
|
||||
const InputNumber = defineAsyncComponent(() => import('primevue/inputnumber'))
|
||||
const InputText = defineAsyncComponent(() => import('primevue/inputtext'))
|
||||
const InputMask = defineAsyncComponent(() => import('primevue/inputmask'))
|
||||
const FileUpload = defineAsyncComponent(() => import('primevue/fileupload'))
|
||||
const Dialog = defineAsyncComponent(() => import('primevue/dialog'))
|
||||
const Message = defineAsyncComponent(() => import('primevue/message'))
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const bullets = ref<Select[]>([])
|
||||
const primers = ref<Select[]>([])
|
||||
const powders = ref<Select[]>([])
|
||||
const cartridges = ref<Select[]>([])
|
||||
|
||||
const cartridgeName = ref('')
|
||||
const addCartridgeDialog = ref(false)
|
||||
|
||||
const file = ref<File | null>(null)
|
||||
const fileSelected = (e: FileUploadSelectEvent) => {
|
||||
file.value = e.files[0]
|
||||
}
|
||||
|
||||
const load = ref({
|
||||
bullet: '',
|
||||
cartridge: '',
|
||||
powder: '',
|
||||
powderGrs: 0,
|
||||
primer: '',
|
||||
col: '',
|
||||
})
|
||||
|
||||
const v$ = useVuelidate({
|
||||
bullet: { required },
|
||||
cartridge: { required },
|
||||
powder: { required },
|
||||
powderGrs: {
|
||||
required, min: function (val: number | null) {
|
||||
if (!val) return false
|
||||
|
||||
return val > 0
|
||||
},
|
||||
},
|
||||
primer: { required },
|
||||
col: {
|
||||
required, min: function (val: string | null) {
|
||||
if (!val) return false
|
||||
|
||||
// regex to validate COL does not container _
|
||||
return !val.includes('_')
|
||||
},
|
||||
},
|
||||
}, load.value)
|
||||
|
||||
const fetchBullets = async () => {
|
||||
const response = await axios.get<any, Response<Bullet[]>>(`${import.meta.env.VITE_API}/bullet`)
|
||||
response.data.payload.forEach((bullet: Bullet) => {
|
||||
bullets.value.push({ label: `${bullet.manufacturer.name} ${bullet.weight}gr ${bullet.name}`, value: bullet.id })
|
||||
})
|
||||
}
|
||||
|
||||
const fetchPrimers = async () => {
|
||||
const response = await axios.get<any, Response<Primers[]>>(`${import.meta.env.VITE_API}/primer`)
|
||||
response.data.payload.forEach((primer: Primers) => {
|
||||
primers.value.push({ label: `${primer.manufacturer.name} ${primer.name}`, value: primer.id })
|
||||
})
|
||||
}
|
||||
|
||||
const fetchPowders = async () => {
|
||||
const response = await axios.get<any, Response<Powder[]>>(`${import.meta.env.VITE_API}/powder`)
|
||||
response.data.payload.forEach((powder: Powder) => {
|
||||
powders.value.push({ label: `${powder.manufacturer.name} ${powder.name}`, value: powder.id })
|
||||
})
|
||||
}
|
||||
|
||||
const fetchCartridges = async () => {
|
||||
cartridges.value = []
|
||||
|
||||
const response = await axios.get<any, Response<Cartridge[]>>(`${import.meta.env.VITE_API}/cartridge`)
|
||||
response.data.payload.forEach((cartridge: Cartridge) => {
|
||||
cartridges.value.push({ label: `${cartridge.name}`, value: cartridge.id })
|
||||
})
|
||||
}
|
||||
|
||||
const addCartridgeName = async () => {
|
||||
if (cartridgeName.value === '') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.post<any, Response<Cartridge>>(`${import.meta.env.VITE_API}/cartridge`, {
|
||||
name: cartridgeName.value,
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Error adding cartridge',
|
||||
})
|
||||
}
|
||||
|
||||
await fetchCartridges()
|
||||
|
||||
cartridgeName.value = ''
|
||||
addCartridgeDialog.value = false
|
||||
}
|
||||
|
||||
const add = async () => {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid || !file.value) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Please correct all fields',
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('bullet_id', load.value.bullet)
|
||||
formData.append('cartridge_id', load.value.cartridge)
|
||||
formData.append('powder_id', load.value.powder)
|
||||
formData.append('powder_gr', load.value.powderGrs.toString())
|
||||
formData.append('primer_id', load.value.primer)
|
||||
formData.append('col', load.value.col)
|
||||
formData.append('photo', file.value)
|
||||
|
||||
try {
|
||||
const response = await axios.post<any, Response<string>>(`${import.meta.env.VITE_API}/load`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Success',
|
||||
detail: 'Load added',
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
await router.push(`/loads/edit/${response.data.payload}`)
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Error adding load',
|
||||
life: 3000,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchBullets()
|
||||
fetchPrimers()
|
||||
fetchPowders()
|
||||
fetchCartridges()
|
||||
})
|
||||
|
||||
</script>
|
||||
140
frontend/src/pages/loads/Search.vue
Normal file
140
frontend/src/pages/loads/Search.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<Card class="w-11/12">
|
||||
<template #title>
|
||||
Loads
|
||||
</template>
|
||||
<template #content>
|
||||
<DataTable
|
||||
:value="loads"
|
||||
filterDisplay="row"
|
||||
paginator
|
||||
lazy
|
||||
:rows="50"
|
||||
:totalRecords="total"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="p-4">
|
||||
<p>No loads found</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column field="cartridge" header="Name" :sortable="true" :showFilterMenu="false">
|
||||
<template #filter>
|
||||
<Select />
|
||||
</template>
|
||||
<template #body="{ data }">
|
||||
{{ data.cartridge }}
|
||||
</template>
|
||||
|
||||
</Column>
|
||||
|
||||
<Column field="bulletManufacturer" header="Bullet Manufacturer" :sortable="true" :showFilterMenu="false">
|
||||
<template #filter>
|
||||
<Select />
|
||||
</template>
|
||||
<template #body="{ data }">
|
||||
{{ data.bullet.manufacturer.name }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="Bullet" :sortable="true" :showFilterMenu="false">
|
||||
<template #filter>
|
||||
<Select />
|
||||
</template>
|
||||
<template #body="{ data }">
|
||||
{{ data.bullet.name }}
|
||||
{{ data.bullet.weight }}gr
|
||||
.{{ data.bullet.diameter }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="name" header="Primer Manufacturer" :sortable="true">
|
||||
<template #body="{ data }">
|
||||
{{ data.primer.manufacturer.name }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="name" header="Primer" :sortable="true">
|
||||
<template #body="{ data }">
|
||||
{{ data.primer.name }}
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="name" header="Powder Manufacturer" :sortable="true">
|
||||
<template #body="{ data }">
|
||||
{{ data.powder.manufacturer.name }}
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="name" header="Powder" :sortable="true">
|
||||
<template #body="{ data }">
|
||||
{{ data.powder.name }}
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="name" header="Powder Grs" :sortable="true">
|
||||
<template #body="{ data }">
|
||||
{{ data.powder_gr }}gr
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="name" header="Edit">
|
||||
<template #body="{ data }">
|
||||
<Button size="small" text :icon="icons.edit" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Bullet } from '../../types/bullet'
|
||||
import { Powder } from '../../types/powder'
|
||||
import { Primers } from '../../types/primers'
|
||||
|
||||
interface LoadResponse {
|
||||
total: number
|
||||
results: Load[]
|
||||
}
|
||||
|
||||
interface Load {
|
||||
id: string
|
||||
bullet: Bullet
|
||||
cartridge: string
|
||||
powder: Powder
|
||||
powder_gr: number
|
||||
primer: Primers
|
||||
col: number
|
||||
}
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, ref } from 'vue'
|
||||
import { Response } from '../../types/Response'
|
||||
import axios from 'axios'
|
||||
import Column from 'primevue/column'
|
||||
import { icons } from '../../lib/icons.ts'
|
||||
|
||||
const DataTable = defineAsyncComponent(() => import('primevue/datatable'))
|
||||
const Card = defineAsyncComponent(() => import('primevue/card'))
|
||||
const Button = defineAsyncComponent(() => import('primevue/button'))
|
||||
const Select = defineAsyncComponent(() => import('primevue/select'))
|
||||
|
||||
const loads = ref<Load[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const loading = ref(true)
|
||||
|
||||
const fetchLoads = async () => {
|
||||
loading.value = true
|
||||
|
||||
const resp = await axios.get<any, Response<LoadResponse>>(`${import.meta.env.VITE_API}/load`)
|
||||
|
||||
loads.value = resp.data.payload.results
|
||||
total.value = resp.data.payload.total
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchLoads()
|
||||
})
|
||||
|
||||
</script>
|
||||
72
frontend/src/pages/manufacturers/Add.vue
Normal file
72
frontend/src/pages/manufacturers/Add.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<Card class="w-full md:w-1/2">
|
||||
<template #title>
|
||||
Add Manufacturer
|
||||
</template>
|
||||
<template #content>
|
||||
<div>
|
||||
<div>
|
||||
<label for="name">Name</label>
|
||||
</div>
|
||||
<InputText id="name" v-model="manufacturer.name" class="w-full" />
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label for="url">Url</label>
|
||||
</div>
|
||||
<InputText id="url" v-model="manufacturer.url" class="w-full" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end mt-4">
|
||||
<Button label="Add" @click="add" :loading="loading" :icon="icons.add" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { Response } from '../../types/Response'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { icons } from '../../lib/icons.ts'
|
||||
|
||||
const Card = defineAsyncComponent(() => import('primevue/card'))
|
||||
const InputText = defineAsyncComponent(() => import('primevue/inputtext'))
|
||||
const Button = defineAsyncComponent(() => import('primevue/button'))
|
||||
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const manufacturer = ref({
|
||||
name: '',
|
||||
url: '',
|
||||
})
|
||||
|
||||
const add = async () => {
|
||||
loading.value = true
|
||||
axios.post(`${import.meta.env.VITE_API}/manufacturer`, {
|
||||
name: manufacturer.value.name,
|
||||
url: manufacturer.value.url,
|
||||
}).then((response: Response<string>) => {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Added Manufacturer',
|
||||
life: 3000,
|
||||
})
|
||||
if (response) {
|
||||
router.push(`/manufacturers/edit/${response.data.payload}`)
|
||||
}
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
}).catch((e) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Something went wrong',
|
||||
detail: e.response?.data.message,
|
||||
life: 3000,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
100
frontend/src/pages/manufacturers/Edit.vue
Normal file
100
frontend/src/pages/manufacturers/Edit.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<Card class="w-full md:w-1/2">
|
||||
<template #title>
|
||||
Edit Manufacturer
|
||||
</template>
|
||||
<template #content>
|
||||
<div>
|
||||
<div>
|
||||
<label for="name">Name</label>
|
||||
</div>
|
||||
<InputText id="name" v-model="manufacturer.name" class="w-full" />
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label for="url">Url</label>
|
||||
</div>
|
||||
<InputText id="url" v-model="manufacturer.url" class="w-full" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end mt-4">
|
||||
<Button label="Save" @click="save" :loading="loading" :icon="icons.save" />
|
||||
<Button severity="danger" label="Delete" @click="del" class="ml-2" :icon="icons.delete" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, ref } from 'vue'
|
||||
import axios from "axios";
|
||||
import {Response} from "../../types/Response";
|
||||
import {Manufacturer} from "../../types/manufacturer";
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { icons } from '../../lib/icons.ts'
|
||||
|
||||
const Card = defineAsyncComponent(() => import('primevue/card'))
|
||||
const InputText = defineAsyncComponent(() => import('primevue/inputtext'))
|
||||
const Button = defineAsyncComponent(() => import('primevue/button'))
|
||||
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(false)
|
||||
const manufacturer = ref({
|
||||
name: '',
|
||||
url: '',
|
||||
})
|
||||
|
||||
const get = async () => {
|
||||
loading.value = true
|
||||
axios.get(`${import.meta.env.VITE_API}/manufacturer/${route.params.id}`).then((response: Response<Manufacturer>) => {
|
||||
manufacturer.value.name = response.data.payload.name
|
||||
manufacturer.value.url = response.data.payload.url
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const del = async () => {
|
||||
axios.delete(`${import.meta.env.VITE_API}/manufacturer/${route.params.id}`).then(() => {
|
||||
router.push({path: '/manufacturers'})
|
||||
}).catch((e) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Something went wrong',
|
||||
detail: e.response?.data.message,
|
||||
life: 3000
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
loading.value = true
|
||||
axios.post(`${import.meta.env.VITE_API}/manufacturer/${route.params.id}`, {
|
||||
name: manufacturer.value.name,
|
||||
url: manufacturer.value.url,
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
}).catch((e) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Something went wrong',
|
||||
detail: e.response?.data.message,
|
||||
life: 3000
|
||||
})
|
||||
}).then(() => {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Successfully Saved',
|
||||
life: 3000
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
get()
|
||||
})
|
||||
|
||||
</script>
|
||||
56
frontend/src/pages/manufacturers/List.vue
Normal file
56
frontend/src/pages/manufacturers/List.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<DataTable size="small" :value="manufacturers" tableStyle="min-width: 50rem">
|
||||
<template #empty>
|
||||
<div class="p-text-center">
|
||||
<p>No manufacturers found.</p>
|
||||
</div>
|
||||
</template>
|
||||
<Column header="Name">
|
||||
<template #body="slotProps">
|
||||
<a v-if="slotProps.data.url" target="_blank" :href="slotProps.data.url">{{ slotProps.data.name }}</a>
|
||||
<span v-else>{{ slotProps.data.name }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="Edit">
|
||||
<template #body="slotProps">
|
||||
<router-link
|
||||
:to="`/manufacturers/edit/${slotProps.data.id}`"
|
||||
class="p-button p-button-link p-button-sm"
|
||||
>Edit</router-link>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<Button @click="router.push('/manufacturers/add')" label="Add" :icon="icons.add" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
import axios from "axios";
|
||||
import {Response} from "../../types/Response";
|
||||
import {Manufacturer} from "../../types/manufacturer";
|
||||
import Column from 'primevue/column';
|
||||
import { useRouter } from 'vue-router'
|
||||
import { icons } from '../../lib/icons.ts'
|
||||
|
||||
const DataTable = defineAsyncComponent(() => import('primevue/datatable'));
|
||||
const Button = defineAsyncComponent(() => import('primevue/button'));
|
||||
const router = useRouter()
|
||||
|
||||
const manufacturers = ref<Manufacturer[]>([])
|
||||
|
||||
const fetchManufacturers = async () => {
|
||||
axios.get(`${import.meta.env.VITE_API}/manufacturer`).then((response: Response<Manufacturer[]>) => {
|
||||
if (response.data.payload) {
|
||||
manufacturers.value = response.data.payload
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fetchManufacturers()
|
||||
|
||||
</script>
|
||||
95
frontend/src/pages/powders/Add.vue
Normal file
95
frontend/src/pages/powders/Add.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<Card class="w-full md:w-1/2">
|
||||
<template #title>
|
||||
Add Powder
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div class="w-full">
|
||||
<label>Manufacturer</label>
|
||||
<Dropdown :options="manufacturers" option-label="name" option-value="id" v-model="powder.manufacturer_id"
|
||||
class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Name</label>
|
||||
<InputText v-model="powder.name" class="w-full" />
|
||||
</div>
|
||||
<div class="w-full mt-3">
|
||||
<label>Picture</label>
|
||||
<FileUpload v-model="file" mode="basic" @select="fileSelected" customUpload />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Button :loading="loading" label="Add" @click="add" :icon="icons.save" />
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, ref } from 'vue'
|
||||
import { fetchManufacturers } from '../../services/manufacturers.ts'
|
||||
import { Manufacturer } from '../../types/manufacturer'
|
||||
import { FileUploadSelectEvent } from 'primevue/fileupload'
|
||||
import axios from 'axios'
|
||||
import router from '../../router'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { Response } from '../../types/Response'
|
||||
import { icons } from '../../lib/icons.ts'
|
||||
|
||||
const Card = defineAsyncComponent(() => import('primevue/card'))
|
||||
const Dropdown = defineAsyncComponent(() => import('primevue/dropdown'))
|
||||
const InputText = defineAsyncComponent(() => import('primevue/inputtext'))
|
||||
const Button = defineAsyncComponent(() => import('primevue/button'))
|
||||
const FileUpload = defineAsyncComponent(() => import('primevue/fileupload'))
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const manufacturers = ref<Manufacturer[]>([])
|
||||
const file = ref<File | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const powder = ref({
|
||||
manufacturer_id: '',
|
||||
name: '',
|
||||
})
|
||||
|
||||
const fileSelected = (event: FileUploadSelectEvent) => {
|
||||
file.value = event.files[0]
|
||||
}
|
||||
|
||||
const add = () => {
|
||||
loading.value = true
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('manufacturer_id', powder.value.manufacturer_id)
|
||||
formData.append('name', powder.value.name)
|
||||
if (file.value) {
|
||||
formData.append('photo', file.value)
|
||||
}
|
||||
|
||||
axios.post(`${import.meta.env.VITE_API}/powder`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}).
|
||||
then((r: Response<{ id: string }>) => {
|
||||
router.push(`/powders/edit/${r.data.payload.id}`)
|
||||
}).catch(() => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'An error occurred while adding the powder',
|
||||
life: 3000,
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const resp = await fetchManufacturers()
|
||||
manufacturers.value.push(...resp.payload)
|
||||
})
|
||||
|
||||
</script>
|
||||
133
frontend/src/pages/powders/Edit.vue
Normal file
133
frontend/src/pages/powders/Edit.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<Card class="w-full md:w-1/2">
|
||||
<template #title>
|
||||
<h2>Edit Powder</h2>
|
||||
</template>
|
||||
<template #content>
|
||||
<div v-if="powder" class="flex flex-wrap flex-col md:flex-row">
|
||||
<div class="w-full mt-5">
|
||||
<label>Manufacturer</label>
|
||||
<Select :options="manufacturers" option-label="name" option-value="id" v-model="powder.manufacturer.id"
|
||||
class="w-full" />
|
||||
</div>
|
||||
<div class="w-full mt-5">
|
||||
<label for="name">Name</label>
|
||||
<InputText v-model="powder.name" type="text" id="name" class="w-full" />
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label>Picture</label>
|
||||
<FileUpload v-model="file" mode="basic" @select="fileSelected" customUpload>
|
||||
<template #uploadicon>
|
||||
<i :class="icons.upload" class="mr-3"></i>
|
||||
</template>
|
||||
</FileUpload>
|
||||
<div class="mt-3">
|
||||
<Image v-if="!loading" :src="pictureUrl" alt="picture" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Button class="mr-3" :loading="loading" @click="update" label="Save" :icon="icons.save" />
|
||||
<Button severity="danger" :loading="loading" @click="deletePowder" label="Delete" :icon="icons.delete" />
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Response } from '../../types/Response'
|
||||
import { Powder } from '../../types/powder'
|
||||
import { Manufacturer } from '../../types/manufacturer'
|
||||
import { fetchManufacturers } from '../../services/manufacturers.ts'
|
||||
import { FileUploadSelectEvent } from 'primevue/fileupload'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { icons } from '../../lib/icons.ts'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
|
||||
const Card = defineAsyncComponent(() => import('primevue/card'))
|
||||
const InputText = defineAsyncComponent(() => import('primevue/inputtext'))
|
||||
const Select = defineAsyncComponent(() => import('primevue/select'))
|
||||
const Image = defineAsyncComponent(() => import('primevue/image'))
|
||||
const FileUpload = defineAsyncComponent(() => import('primevue/fileupload'))
|
||||
const Button = defineAsyncComponent(() => import('primevue/button'))
|
||||
|
||||
const powder = ref<Powder | null>(null)
|
||||
const manufacturers = ref<Manufacturer[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const getPowder = async () => {
|
||||
axios.get(`${import.meta.env.VITE_API}/powder/${route.params.id}`).then((response: Response<Powder>) => {
|
||||
powder.value = response.data.payload
|
||||
}).catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
|
||||
const file = ref('')
|
||||
const fileSelected = (event: FileUploadSelectEvent) => {
|
||||
file.value = event.files[0]
|
||||
}
|
||||
|
||||
const pictureUrl = computed(() => {
|
||||
const cache = new Date().getMilliseconds()
|
||||
return import.meta.env.VITE_API + `/powder/${route.params.id}/photo?cache=${cache}`
|
||||
})
|
||||
|
||||
const deletePowder = () => {
|
||||
axios.delete(`${import.meta.env.VITE_API}/powder/${route.params.id}`).then(() => {
|
||||
router.push('/powders')
|
||||
}).catch((error) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: error.response.data.message,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
loading.value = true
|
||||
|
||||
let formData = new FormData()
|
||||
formData.append('photo', file.value)
|
||||
formData.append('name', powder.value?.name || '')
|
||||
formData.append('manufacturer_id', powder.value?.manufacturer.id || '')
|
||||
|
||||
axios.post(`${import.meta.env.VITE_API}/powder/${route.params.id}`,
|
||||
formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
},
|
||||
).then(() => {
|
||||
loading.value = false
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'saved!',
|
||||
detail: 'powder saved',
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
getPowder()
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'error!',
|
||||
detail: 'powder not saved',
|
||||
life: 3000,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
getPowder().then()
|
||||
const resp = await fetchManufacturers()
|
||||
manufacturers.value.push(...resp.payload)
|
||||
})
|
||||
</script>
|
||||
57
frontend/src/pages/powders/List.vue
Normal file
57
frontend/src/pages/powders/List.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<Card class="w-full md:w-2/3">
|
||||
<template #title>
|
||||
<h2>Powders</h2>
|
||||
</template>
|
||||
<template #content>
|
||||
<DataTable :value="powders">
|
||||
<template #empty>
|
||||
<div class="p-4">
|
||||
<p>No powders found</p>
|
||||
</div>
|
||||
</template>
|
||||
<Column field="manufacturer" header="Name">
|
||||
<template #body="{data}">
|
||||
<span>{{ data.manufacturer.name }} {{ data.name }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="actions" header="Actions">
|
||||
<template #body="{data}">
|
||||
<Button
|
||||
text
|
||||
size="small"
|
||||
:icon="icons.edit"
|
||||
@click="router.push(`/powders/edit/${data.id}`)"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import axios from 'axios'
|
||||
import { Response } from '../../types/Response'
|
||||
import { Powder } from '../../types/powder'
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
import Column from 'primevue/column'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { icons } from '../../lib/icons.ts'
|
||||
|
||||
const Card = defineAsyncComponent(() => import('primevue/card'))
|
||||
const DataTable = defineAsyncComponent(() => import('primevue/datatable'))
|
||||
const Button = defineAsyncComponent(() => import('primevue/button'))
|
||||
|
||||
const powders = ref<Powder[]>([])
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const get = () => {
|
||||
axios.get(`${import.meta.env.VITE_API}/powder`).then((r: Response<Powder[]>) => {
|
||||
powders.value = r.data.payload
|
||||
})
|
||||
}
|
||||
|
||||
get()
|
||||
|
||||
</script>
|
||||
93
frontend/src/pages/primers/Add.vue
Normal file
93
frontend/src/pages/primers/Add.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<Card class="w-full md:w-1/2">
|
||||
<template #title>
|
||||
Add Primer
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div class="w-full">
|
||||
<label>Manufacturer</label>
|
||||
<Select :options="manufacturers" option-label="name" option-value="id" v-model="primer.manufacturer_id"
|
||||
class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Name</label>
|
||||
<InputText v-model="primer.name" class="w-full" />
|
||||
</div>
|
||||
<div class="w-full mt-3">
|
||||
<label>Picture</label>
|
||||
<FileUpload v-model="file" mode="basic" @select="fileSelected" customUpload />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Button :loading="loading" label="Add" :icon="icons.add" @click="add" />
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, ref } from 'vue'
|
||||
import { Manufacturer } from '../../types/manufacturer'
|
||||
import { fetchManufacturers } from '../../services/manufacturers.ts'
|
||||
import axios from 'axios'
|
||||
import { icons } from '../../lib/icons.ts'
|
||||
import { FileUploadSelectEvent } from 'primevue/fileupload'
|
||||
import { Response } from '../../types/Response'
|
||||
import router from '../../router'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const Card = defineAsyncComponent(() => import('primevue/card'))
|
||||
const InputText = defineAsyncComponent(() => import('primevue/inputtext'))
|
||||
const Select = defineAsyncComponent(() => import('primevue/select'))
|
||||
const Button = defineAsyncComponent(() => import('primevue/button'))
|
||||
const FileUpload = defineAsyncComponent(() => import('primevue/fileupload'))
|
||||
|
||||
const manufacturers = ref<Manufacturer[]>([])
|
||||
const primer = ref({
|
||||
manufacturer_id: '',
|
||||
name: '',
|
||||
})
|
||||
|
||||
const file = ref<File | null>(null)
|
||||
const loading = ref(false)
|
||||
const fileSelected = (event: FileUploadSelectEvent) => {
|
||||
file.value = event.files[0]
|
||||
}
|
||||
|
||||
const add = () => {
|
||||
loading.value = true
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('manufacturer_id', primer.value.manufacturer_id)
|
||||
formData.append('name', primer.value.name)
|
||||
if (file.value) {
|
||||
formData.append('photo', file.value)
|
||||
}
|
||||
|
||||
axios.post(`${import.meta.env.VITE_API}/primer`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}).
|
||||
then((r: Response<{ id: string }>) => {
|
||||
router.push(`/primers/edit/${r.data.payload.id}`)
|
||||
}).catch(() => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'An error occurred while adding the powder',
|
||||
life: 3000,
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const resp = await fetchManufacturers()
|
||||
manufacturers.value.push(...resp.payload)
|
||||
})
|
||||
</script>
|
||||
142
frontend/src/pages/primers/Edit.vue
Normal file
142
frontend/src/pages/primers/Edit.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<Card class="w-full md:w-1/2">
|
||||
<template #title>
|
||||
Edit Primer
|
||||
</template>
|
||||
<template #content>
|
||||
<div v-if="primer" class="flex flex-row flex-wrap">
|
||||
<div class="w-full">
|
||||
<label>Manufacturer</label>
|
||||
<Select :options="manufacturers" option-label="name" option-value="id" v-model="primer.manufacturer.id"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full mt-5">
|
||||
<label for="name">Name</label>
|
||||
<InputText v-model="primer.name" type="text" id="name" class="w-full" />
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label>Picture</label>
|
||||
<FileUpload v-model="file" mode="basic" @select="fileSelected" customUpload>
|
||||
<template #uploadicon>
|
||||
<i :class="icons.upload" class="mr-3"></i>
|
||||
</template>
|
||||
</FileUpload>
|
||||
<div class="mt-3">
|
||||
<Image v-if="!loading" :src="pictureUrl" alt="picture" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Button class="mr-3" label="Save" :icon="icons.save" @click="update" />
|
||||
<Button severity="danger" label="Delete" :icon="icons.delete" @click="deletePrimer" />
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
|
||||
import { Manufacturer } from '../../types/manufacturer'
|
||||
import { fetchManufacturers } from '../../services/manufacturers.ts'
|
||||
import { Response} from '../../types/Response'
|
||||
import { Primers } from '../../types/primers'
|
||||
import { icons } from '../../lib/icons.ts'
|
||||
import { FileUploadSelectEvent } from 'primevue/fileupload'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
|
||||
const Card = defineAsyncComponent(() => import('primevue/card'))
|
||||
const Select = defineAsyncComponent(() => import('primevue/select'))
|
||||
const InputText = defineAsyncComponent(() => import('primevue/inputtext'))
|
||||
const Image = defineAsyncComponent(() => import('primevue/image'))
|
||||
const FileUpload = defineAsyncComponent(() => import('primevue/fileupload'))
|
||||
const Button = defineAsyncComponent(() => import('primevue/button'))
|
||||
|
||||
const manufacturers = ref<Manufacturer[]>([])
|
||||
const primer = ref<Primers | null>(null)
|
||||
const loading = ref(false)
|
||||
const file = ref<File | null>(null)
|
||||
const fileSelected = (event: FileUploadSelectEvent) => {
|
||||
file.value = event.files[0]
|
||||
}
|
||||
|
||||
const pictureUrl = computed(() => {
|
||||
const cache = new Date().getMilliseconds()
|
||||
return import.meta.env.VITE_API + `/primer/${route.params.id}/photo?cache=${cache}`
|
||||
})
|
||||
|
||||
const deletePrimer = () => {
|
||||
axios.delete(`${import.meta.env.VITE_API}/primer/${route.params.id}`).then(() => {
|
||||
router.push('/primers')
|
||||
}).catch((error) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: error.response.data.message,
|
||||
life: 3000
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const getPrimer = () => {
|
||||
axios.get(`${import.meta.env.VITE_API}/primer/${route.params.id}`)
|
||||
.then((response: Response<Primers>) => {
|
||||
primer.value = response.data.payload
|
||||
}).catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
loading.value = true
|
||||
|
||||
if (!primer.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('name', primer.value.name)
|
||||
formData.append('manufacturer_id', primer.value.manufacturer.id)
|
||||
|
||||
if (file.value) {
|
||||
formData.append('photo', file.value)
|
||||
}
|
||||
|
||||
axios.post(`${import.meta.env.VITE_API}/primer/${route.params.id}`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}).then(() => {
|
||||
file.value = null
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Success',
|
||||
detail: 'Primer updated successfully',
|
||||
life: 3000
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: error.response.data.message,
|
||||
life: 3000
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
getPrimer()
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await fetchManufacturers()
|
||||
manufacturers.value = response.payload
|
||||
})
|
||||
</script>
|
||||
61
frontend/src/pages/primers/List.vue
Normal file
61
frontend/src/pages/primers/List.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<Card class="w-2/3">
|
||||
<template #title>
|
||||
Primers
|
||||
</template>
|
||||
<template #content>
|
||||
<DataTable :value="primers">
|
||||
<template #empty>
|
||||
<div class="p-4">
|
||||
<p>No primers found</p>
|
||||
</div>
|
||||
</template>
|
||||
<Column field="name" header="Name">
|
||||
<template #body="{ data }">
|
||||
{{ data.manufacturer.name }} {{ data.name }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="name" header="Edit">
|
||||
<template #body="{ data }">
|
||||
<Button
|
||||
text
|
||||
small
|
||||
color="primary"
|
||||
:icon="icons.edit"
|
||||
@click="router.push(`/primers/edit/${data.id}`)"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import axios from 'axios'
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
import Column from 'primevue/column'
|
||||
import { Response } from '../../types/Response'
|
||||
import { Primers } from '../../types/primers'
|
||||
import { icons } from '../../lib/icons.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const Card = defineAsyncComponent(() => import('primevue/card'))
|
||||
const Button = defineAsyncComponent(() => import('primevue/button'))
|
||||
const DataTable = defineAsyncComponent(() => import('primevue/datatable'))
|
||||
|
||||
const primers = ref<Primers[]>([])
|
||||
|
||||
const getPrimers = () => {
|
||||
axios.get(`${import.meta.env.VITE_API}/primer`)
|
||||
.then((response: Response<Primers[]>) => {
|
||||
primers.value = response.data.payload
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
|
||||
getPrimers()
|
||||
</script>
|
||||
71
frontend/src/router/index.ts
Normal file
71
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('../pages/Index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/bullets/add',
|
||||
component: () => import('../pages/bullets/Add.vue'),
|
||||
},
|
||||
{
|
||||
path: '/bullets/edit/:id',
|
||||
component: () => import('../pages/bullets/Edit.vue'),
|
||||
},
|
||||
{
|
||||
path: '/bullets',
|
||||
component: () => import('../pages/bullets/List.vue'),
|
||||
},
|
||||
{
|
||||
path: '/powders',
|
||||
component: () => import('../pages/powders/List.vue'),
|
||||
},
|
||||
{
|
||||
path: '/powders/add',
|
||||
component: () => import('../pages/powders/Add.vue'),
|
||||
},
|
||||
{
|
||||
path: '/powders/edit/:id',
|
||||
component: () => import('../pages/powders/Edit.vue'),
|
||||
},
|
||||
{
|
||||
path: '/primers',
|
||||
component: () => import('../pages/primers/List.vue'),
|
||||
},
|
||||
{
|
||||
path: '/primers/add',
|
||||
component: () => import('../pages/primers/Add.vue'),
|
||||
},
|
||||
{
|
||||
path: '/primers/edit/:id',
|
||||
component: () => import('../pages/primers/Edit.vue'),
|
||||
},
|
||||
{
|
||||
path: '/manufacturers',
|
||||
component: () => import('../pages/manufacturers/List.vue'),
|
||||
},
|
||||
{
|
||||
path: '/manufacturers/add',
|
||||
component: () => import('../pages/manufacturers/Add.vue'),
|
||||
},
|
||||
{
|
||||
path: '/manufacturers/edit/:id',
|
||||
component: () => import('../pages/manufacturers/Edit.vue'),
|
||||
},
|
||||
{
|
||||
path: '/loads/add',
|
||||
component: () => import('../pages/loads/Add.vue'),
|
||||
},
|
||||
{
|
||||
path: '/loads/search',
|
||||
component: () => import('../pages/loads/Search.vue'),
|
||||
},
|
||||
] as RouteRecordRaw[]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
export default router
|
||||
10
frontend/src/services/manufacturers.ts
Normal file
10
frontend/src/services/manufacturers.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import axios from 'axios'
|
||||
import { Response } from '../types/Response'
|
||||
import { Manufacturer } from '../types/manufacturer'
|
||||
|
||||
export const fetchManufacturers = async () => {
|
||||
const response = await axios.get<any, Response<Manufacturer[]>>(
|
||||
`${import.meta.env.VITE_API}/manufacturer`)
|
||||
|
||||
return response.data
|
||||
}
|
||||
8
frontend/src/types/Response.d.ts
vendored
Normal file
8
frontend/src/types/Response.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import {AxiosResponse} from "axios";
|
||||
|
||||
export interface Response<T> extends AxiosResponse {
|
||||
data: {
|
||||
payload: T
|
||||
status: 'OK' | 'ERROR'
|
||||
}
|
||||
}
|
||||
8
frontend/src/types/bullet.d.ts
vendored
Normal file
8
frontend/src/types/bullet.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Manufacturer } from './manufacturer'
|
||||
|
||||
export interface Bullet {
|
||||
id: string
|
||||
name: string
|
||||
weight: number
|
||||
manufacturer: Manufacturer
|
||||
}
|
||||
5
frontend/src/types/manufacturer.d.ts
vendored
Normal file
5
frontend/src/types/manufacturer.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Manufacturer {
|
||||
id: string
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
8
frontend/src/types/powder.d.ts
vendored
Normal file
8
frontend/src/types/powder.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Manufacturer } from './manufacturer'
|
||||
|
||||
export interface Powder {
|
||||
id: string;
|
||||
name: string;
|
||||
meta: string;
|
||||
manufacturer: Manufacturer;
|
||||
}
|
||||
7
frontend/src/types/primers.d.ts
vendored
Normal file
7
frontend/src/types/primers.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Manufacturer } from './manufacturer'
|
||||
|
||||
export interface Primers {
|
||||
id: string
|
||||
name: string
|
||||
manufacturer: Manufacturer
|
||||
}
|
||||
10
frontend/src/vite-env.d.ts
vendored
Normal file
10
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import {ComponentCustomProperties as GCS} from "@vue/runtime-core";
|
||||
import {Account} from "appwrite";
|
||||
|
||||
declare module "@vue/runtime-core" {
|
||||
interface ComponentCustomProperties extends GCS {
|
||||
$account: Account
|
||||
}
|
||||
}
|
||||
11
frontend/tailwind.config.js
Normal file
11
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
26
frontend/tsconfig.json
Normal file
26
frontend/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
8
frontend/vite.config.ts
Normal file
8
frontend/vite.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), tailwindcss()],
|
||||
})
|
||||
Reference in New Issue
Block a user