diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore new file mode 100644 index 00000000..d20c0fe4 --- /dev/null +++ b/.codegraph/.gitignore @@ -0,0 +1,5 @@ +# CodeGraph data files — local to each machine, not for committing. +# Ignore everything in .codegraph/ except this file itself, so transient +# files (the database, daemon.pid, sockets, logs) never show up in git. +* +!.gitignore diff --git a/.gitignore b/.gitignore index 74821944..ca424cc3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ dist/ node_modules/ tests/e2e/reports/ +.vite/ # local env files .env.local diff --git a/index.html b/index.html index d3bf425d..3f4a30a6 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + - - - - - - CoreUI Vue.js Admin Template + + + + + + ManjaPro 2 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + +
diff --git a/package-lock.json b/package-lock.json index 102665b8..e9cb8441 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,9 +19,11 @@ "@popperjs/core": "^2.11.8", "axios": "^1.18.1", "bootstrap": "^5.3.8", + "bootstrap-icons": "^1.13.1", "chart.js": "^4.5.1", "pinia": "^3.0.4", "simplebar-vue": "^2.4.2", + "sweetalert2": "^11.26.25", "vue": "^3.5.31", "vue-router": "^5.0.4" }, @@ -1568,6 +1570,22 @@ "@popperjs/core": "^2.11.8" } }, + "node_modules/bootstrap-icons": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz", + "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", @@ -3589,6 +3607,16 @@ "node": ">=8" } }, + "node_modules/sweetalert2": { + "version": "11.26.25", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.26.25.tgz", + "integrity": "sha512-+hunCOJdJ6FLj04T9YSLvvZXRjsvIkTeTKP2e4VF8CaBias961BTnWiSFAy7F/CM5eq3QK2Rraoc5Gzftslvkg==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/limonte" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", diff --git a/package.json b/package.json index eb02ea73..d1239c83 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,11 @@ "@popperjs/core": "^2.11.8", "axios": "^1.18.1", "bootstrap": "^5.3.8", + "bootstrap-icons": "^1.13.1", "chart.js": "^4.5.1", "pinia": "^3.0.4", "simplebar-vue": "^2.4.2", + "sweetalert2": "^11.26.25", "vue": "^3.5.31", "vue-router": "^5.0.4" }, diff --git a/src/_nav.js b/src/_nav.js index 388618b1..2466e512 100644 --- a/src/_nav.js +++ b/src/_nav.js @@ -21,6 +21,7 @@ * * @type {Array} */ + export default [ { component: 'CNavItem', @@ -88,7 +89,7 @@ export default [ component: 'CNavItem', name: 'Colors', to: '/theme/colors', - icon: 'cil-drop', + icon: 'cil-square', }, { component: 'CNavItem', diff --git a/src/api/axios.js b/src/api/axios.js new file mode 100644 index 00000000..2ecdb3db --- /dev/null +++ b/src/api/axios.js @@ -0,0 +1,22 @@ +import axios from 'axios' +import { getToken } from '@/utils/session' + +const api = axios.create({ + baseURL: 'https://api.radiq.my.id/api', + headers: { + 'Content-Type': 'application/json', + }, +}) + +api.interceptors.request.use( + (config) => { + const token = getToken() + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => Promise.reject(error), +) + +export default api diff --git a/src/assets/icons/index.js b/src/assets/icons/index.js index cc20da72..97be2201 100644 --- a/src/assets/icons/index.js +++ b/src/assets/icons/index.js @@ -80,6 +80,13 @@ import { cilUserFemale, cilUserFollow, cilXCircle, + cilSquare, // new add + cilBarcode, + cilBookmark, + cilAperture, + cilCamera, + cilDescription, + cilCog, } from '@coreui/icons' export const iconsSet = Object.assign( @@ -139,6 +146,13 @@ export const iconsSet = Object.assign( cilUserFemale, cilUserFollow, cilXCircle, + cilSquare, // new add + cilBarcode, + cilBookmark, + cilAperture, + cilCamera, + cilDescription, + cilCog, }, { cifUs, diff --git a/src/components/AppFooter.vue b/src/components/AppFooter.vue index b62df395..2c3c217e 100644 --- a/src/components/AppFooter.vue +++ b/src/components/AppFooter.vue @@ -1,14 +1,12 @@ diff --git a/src/components/AppSidebarNav.js b/src/components/AppSidebarNav.js index 1006a9c3..49889eb3 100644 --- a/src/components/AppSidebarNav.js +++ b/src/components/AppSidebarNav.js @@ -96,11 +96,12 @@ const AppSidebarNav = defineComponent({ }) : h('span', { class: 'nav-icon' }, h('span', { class: 'nav-icon-bullet' })), item.name, - item.external && h(resolveComponent('CIcon'), { - class: 'ms-2', - name: 'cil-external-link', - size: 'sm' - }), + item.external && + h(resolveComponent('CIcon'), { + class: 'ms-2', + name: 'cil-external-link', + size: 'sm', + }), item.badge && h( CBadge, diff --git a/src/components/materials/AssignMaterialModal.vue b/src/components/materials/AssignMaterialModal.vue new file mode 100644 index 00000000..4e5bcef8 --- /dev/null +++ b/src/components/materials/AssignMaterialModal.vue @@ -0,0 +1,247 @@ + + + + + diff --git a/src/components/materials/MaterialDetailModal.vue b/src/components/materials/MaterialDetailModal.vue new file mode 100644 index 00000000..075a1be6 --- /dev/null +++ b/src/components/materials/MaterialDetailModal.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/src/components/materials/MaterialHistory.vue b/src/components/materials/MaterialHistory.vue new file mode 100644 index 00000000..cd071b0f --- /dev/null +++ b/src/components/materials/MaterialHistory.vue @@ -0,0 +1,86 @@ + + + + diff --git a/src/components/materials/MaterialTable.vue b/src/components/materials/MaterialTable.vue new file mode 100644 index 00000000..26daed5c --- /dev/null +++ b/src/components/materials/MaterialTable.vue @@ -0,0 +1,99 @@ + + + diff --git a/src/components/materials/ReturnMaterialModal.vue b/src/components/materials/ReturnMaterialModal.vue new file mode 100644 index 00000000..f7d0abbb --- /dev/null +++ b/src/components/materials/ReturnMaterialModal.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/components/materials/TransferMaterialModal.vue b/src/components/materials/TransferMaterialModal.vue new file mode 100644 index 00000000..b9e5f12a --- /dev/null +++ b/src/components/materials/TransferMaterialModal.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/main.js b/src/main.js index b4e39eac..c0f2a071 100644 --- a/src/main.js +++ b/src/main.js @@ -17,6 +17,7 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' +import { setToken, hasToken } from '@/utils/session' import App from './App.vue' import router from './router' @@ -31,6 +32,9 @@ import DocsComponents from '@/components/DocsComponents' import DocsExample from '@/components/DocsExample' import DocsIcons from '@/components/DocsIcons' +import { CButton } from '@coreui/vue' +import { CBadge } from '@coreui/vue' + // Create Vue application instance const app = createApp(App) @@ -39,6 +43,8 @@ app.use(createPinia()) // State management app.use(router) // Router for SPA navigation app.use(CoreuiVue) // CoreUI component library +app.component('CButton', CButton) + // Provide icons globally app.provide('icons', icons) @@ -48,5 +54,10 @@ app.component('DocsComponents', DocsComponents) app.component('DocsExample', DocsExample) app.component('DocsIcons', DocsIcons) +// TODO: Hapus setelah fitur Login tersedia — token sementara untuk development +if (!hasToken()) { + setToken('UU7HVn7ECHe7MvqMTeTeEpoFdBA2wc0NHQX29WyG91fc6982') +} + // Mount application to DOM app.mount('#app') diff --git a/src/services/materialService.js b/src/services/materialService.js new file mode 100644 index 00000000..3c2f1dcc --- /dev/null +++ b/src/services/materialService.js @@ -0,0 +1,22 @@ +import api from '@/api/axios' + +export const materialService = { + fetchAll(params = {}) { + return api.get('/materials', { params }) + }, + fetchByUser(userId) { + return api.get(`/materials/user/${userId}`) + }, + fetchHistory(barcodeId) { + return api.get(`/materials/history/${barcodeId}`) + }, + assign(payload) { + return api.post('/materials/assign', payload) + }, + transfer(payload) { + return api.post('/materials/transfer', payload) + }, + returnMaterial(payload) { + return api.post('/materials/return', payload) + }, +} diff --git a/src/stores/materialStore.js b/src/stores/materialStore.js new file mode 100644 index 00000000..3f71d526 --- /dev/null +++ b/src/stores/materialStore.js @@ -0,0 +1,62 @@ +import { defineStore } from 'pinia' +import { materialService } from '@/services/materialService' + +export const useMaterialStore = defineStore('material', { + state: () => ({ + materials: [], + histories: [], + loading: false, + error: '', + }), + + actions: { + async fetchAll(params = {}) { + this.loading = true + this.error = '' + try { + const res = await materialService.fetchAll({ per_page: 1000, ...params }) + const resData = res.data + + if (Array.isArray(resData)) { + this.materials = resData + } else if (resData && Array.isArray(resData.data)) { + this.materials = resData.data + } else { + console.warn('fetchAll: unexpected response', resData) + this.materials = [] + } + } catch (err) { + this.error = err.response?.data?.message || 'Gagal mengambil data material.' + } finally { + this.loading = false + } + }, + async fetchByUser(userId) { + if (!userId) return + this.loading = true + this.error = '' + try { + const res = await materialService.fetchByUser(userId) + this.materials = res.data.data || [] + } catch (err) { + this.error = err.response?.data?.message || 'Gagal mengambil data material.' + } finally { + this.loading = false + } + }, + + async fetchHistory(barcodeId) { + if (!barcodeId) return + this.loading = true + this.error = '' + try { + const res = await materialService.fetchHistory(barcodeId) + this.histories = res.data.data || [] + } catch (err) { + this.error = err.response?.data?.message || 'Gagal mengambil history material.' + } finally { + this.loading = false + } + }, + }, +}) diff --git a/src/styles/style.scss b/src/styles/style.scss index c5891ac1..c30adb7b 100644 --- a/src/styles/style.scss +++ b/src/styles/style.scss @@ -1,5 +1,9 @@ @use '@coreui/coreui/scss/coreui' as * with ( - $enable-deprecation-messages: false + $enable-deprecation-messages: false, + $primary: #f28500, + $primary-text-emphasis: #9e5500, + $primary-bg-subtle: #fde6cc, + $primary-border-subtle: #f9b364 ); @use '@coreui/chartjs/scss/coreui-chartjs'; @use 'vendors/simplebar'; @@ -8,6 +12,13 @@ body { background-color: var(--cui-tertiary-bg); } +:root { + --cui-primary: #f28500; + --cui-primary-rgb: 242, 133, 0; + --cui-link-color: #f28500; + --cui-link-color-rgb: 242, 133, 0; +} + .wrapper { width: 100%; padding-inline: var(--cui-sidebar-occupy-start, 0) var(--cui-sidebar-occupy-end, 0); @@ -129,3 +140,65 @@ body { --cui-tertiary-bg: #16171d; --cui-border-color: #22242d; } + +// font bold untuk toast edit +.swal-title-bold { + font-weight: 700 !important; /* Bold */ + font-size: 16px; +} + +//materials +.timeline { + position: relative; + margin-left: 20px; +} + +.timeline::before { + content: ''; + position: absolute; + left: 14px; + top: 0; + bottom: 0; + width: 2px; + background: #d8dbe0; +} + +.timeline-item { + position: relative; + display: flex; + margin-bottom: 20px; +} + +.timeline-icon { + width: 30px; + height: 30px; + border-radius: 50%; + background: #ffffff; + border: 2px solid #d8dbe0; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; +} + +.timeline-content { + flex: 1; + margin-left: 15px; + background: #f8f9fa; + border-radius: 8px; + padding: 12px; +} + +.card { + transition: 0.3s; +} + +.table tbody tr { + transition: 0.2s; +} + +.table tbody tr:hover { + transform: scale(1.01); + + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} diff --git a/src/utils/session.js b/src/utils/session.js new file mode 100644 index 00000000..1b076023 --- /dev/null +++ b/src/utils/session.js @@ -0,0 +1,21 @@ +const TOKEN_KEY = 'UU7HVn7ECHe7MvqMTeTeEpoFdBA2wc0NHQX29WyG91fc6982' + +export function setToken(token) { + sessionStorage.setItem(TOKEN_KEY, token) +} + +export function getToken() { + return sessionStorage.getItem(TOKEN_KEY) +} + +export function removeToken() { + sessionStorage.removeItem(TOKEN_KEY) +} + +export function hasToken() { + return !!sessionStorage.getItem(TOKEN_KEY) +} + +export function clearSession() { + sessionStorage.clear() +} diff --git a/src/utils/swal.js b/src/utils/swal.js new file mode 100644 index 00000000..1b9554c3 --- /dev/null +++ b/src/utils/swal.js @@ -0,0 +1,53 @@ +import Swal from 'sweetalert2' + +export function showConfirm(title = 'Konfirmasi', text = 'Apakah Anda yakin?') { + return Swal.fire({ + title, + text, + icon: 'question', + showCancelButton: true, + confirmButtonColor: '#dc3545', + confirmButtonText: 'Ya, hapus!', + cancelButtonText: 'Batal', + theme: 'auto', + }) +} + +export function showSuccess(message = 'Data berhasil disimpan') { + Swal.fire({ + icon: 'success', + title: 'Berhasil', + text: message, + showConfirmButton: false, + timer: 2000, + timerProgressBar: true, + theme: 'auto', + }) +} + +export function showSuccessr() { + Swal.fire({ + position: 'top-end', + icon: 'success', + title: 'Data Berhasil Di Simpan', + showConfirmButton: false, + timer: 1500, + theme: 'auto', + toast: true, + width: '350px', + padding: '10px 18px', + customClass: { + title: 'swal-title-bold', + }, + }) +} + +export function showError() { + Swal.fire({ + icon: 'error', + title: 'Gagal', + text: 'Terjadi Kesalahan', + confirmButtonText: 'Tutup', + theme: 'auto', + }) +} diff --git a/src/views/tickets/Ticket.vue b/src/views/tickets/Ticket.vue index 770c5046..9e16c3d0 100644 --- a/src/views/tickets/Ticket.vue +++ b/src/views/tickets/Ticket.vue @@ -142,14 +142,11 @@ async function saveTicket() { } try { if (isEdit.value && editingIndex.value >= 0) { - const res = await fetch( - `https://api.radiq.my.id/api/tickets/${form.ticket_no}`, - { - method: 'PUT', - headers: { Authorization: token, 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }, - ) + const res = await fetch(`https://api.radiq.my.id/api/tickets/${form.ticket_no}`, { + method: 'PUT', + headers: { Authorization: token, 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) if (res.ok) store.updateTicket(editingIndex.value, form) } else { const res = await fetch('https://api.radiq.my.id/api/tickets', { @@ -227,7 +224,7 @@ function isPending(item) { Ticket - + + Tambah Ticket diff --git a/src/views/tickets/TicketIncident.vue b/src/views/tickets/TicketIncident.vue index 0b2b3f07..aac1192f 100644 --- a/src/views/tickets/TicketIncident.vue +++ b/src/views/tickets/TicketIncident.vue @@ -1,32 +1,21 @@