Ticket material
NPM Installation / build (16.x, ubuntu-latest) (push) Has been cancelled
NPM Installation / build (16.x, windows-latest) (push) Has been cancelled
NPM Installation / build (17.x, ubuntu-latest) (push) Has been cancelled
NPM Installation / build (17.x, windows-latest) (push) Has been cancelled
NPM Installation / build (18.x, ubuntu-latest) (push) Has been cancelled
NPM Installation / build (18.x, windows-latest) (push) Has been cancelled

This commit is contained in:
2026-07-03 16:26:00 +07:00
parent 63458f397d
commit bd77e1a4e7
30 changed files with 1507 additions and 279 deletions
+6 -9
View File
@@ -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) {
<CCard class="mb-4">
<CCardHeader>
<strong>Ticket</strong>
<CButton color="primary" size="sm" class="float-end" @click="openAddModal">
<CButton color="primary" class="float-end" @click="openAddModal">
+ Tambah Ticket
</CButton>
</CCardHeader>
+30 -78
View File
@@ -1,32 +1,21 @@
<script setup>
import axios from 'axios'
import { ref, reactive, computed, onMounted } from 'vue'
import { useTicketIncidentStore } from '@/stores/ticketIncident.js'
import { formatTanggal } from '@/utils/tglindo.js'
import { showConfirm, showSuccess, showSuccessr, showError } from '@/utils/swal.js'
import { CButton } from '@coreui/vue'
import api from '@/api/axios'
const store = useTicketIncidentStore()
const loading = ref(false)
const BASE_URL = 'https://api.radiq.my.id/api/ticket-incident-types'
const token = 'Bearer uZ1vM4UON3CsDV9niGD1gLS4sHpCxT9nzadkITmc6caf2ea2'
// Axios instance with default headers
const api = axios.create({
baseURL: BASE_URL,
headers: {
Authorization: token,
'Content-Type': 'application/json',
},
})
async function fetchTicketIncidentTypes() {
loading.value = true
try {
const response = await api.get('')
const response = await api.get('/ticket-incident-types')
store.setItems(response.data.data || [])
} catch (err) {
console.error('Failed to fetch:', err)
errorMsg.value = 'Gagal memuat data: ' + err.message
showError('Gagal memuat data: ' + err.message)
} finally {
loading.value = false
}
@@ -35,15 +24,11 @@ async function fetchTicketIncidentTypes() {
onMounted(fetchTicketIncidentTypes)
const modal = ref(false)
const deleteModal = ref(false)
const detailModal = ref(false)
const editingIndex = ref(-1)
const isEdit = ref(false)
const deleteIndex = ref(-1)
const saving = ref(false)
const errorMsg = ref('')
const successMsg = ref('')
let successTimer = null
const currentPage = ref(1)
const perPage = ref(10)
@@ -64,14 +49,6 @@ function changePerPage(val) {
currentPage.value = 1
}
function showSuccess(msg) {
successMsg.value = msg
if (successTimer) clearTimeout(successTimer)
successTimer = setTimeout(() => {
successMsg.value = ''
}, 3000)
}
const form = reactive({
name: '',
is_active: '1',
@@ -107,29 +84,33 @@ function openDetail(item) {
getTicketIncidentType(item.id)
}
function openDeleteModal(item) {
async function openDeleteModal(item) {
const idx = store.items.findIndex((t) => t.id === item.id)
if (idx === -1) return
const result = await showConfirm(
'Konfirmasi Hapus',
'Apakah Anda yakin ingin menghapus ticket incident ini?',
)
if (!result.isConfirmed) return
deleteIndex.value = idx
deleteModal.value = true
await deleteTicketIncidentType(item.id)
}
async function createTicketIncidentType() {
errorMsg.value = ''
saving.value = true
const body = {
name: form.name,
is_active: form.is_active === '1',
}
try {
const response = await api.post('', body)
const response = await api.post('/ticket-incident-types', body)
store.addItem(response.data.data)
modal.value = false
currentPage.value = totalPages.value
showSuccess('Data berhasil disimpan')
fetchTicketIncidentTypes() // Refresh data after creation
fetchTicketIncidentTypes()
} catch (err) {
errorMsg.value = err.response?.data?.message || 'Gagal menyimpan data: ' + err.message
showError(err.response?.data?.message || 'Gagal menyimpan data: ' + err.message)
} finally {
saving.value = false
}
@@ -137,30 +118,28 @@ async function createTicketIncidentType() {
async function getTicketIncidentType(id) {
try {
const response = await api.get(`/${id}`)
const response = await api.get(`/ticket-incident-types/${id}`)
detailItem.value = response.data.data
detailModal.value = true
} catch (err) {
console.error('Failed to fetch detail:', err)
errorMsg.value = 'Gagal memuat detail data: ' + err.message
showError('Gagal memuat detail data: ' + err.message)
}
}
async function updateTicketIncidentType(id) {
errorMsg.value = ''
saving.value = true
const body = {
name: form.name,
is_active: form.is_active === '1',
}
try {
const response = await api.put(`/${id}`, body)
const response = await api.put(`/ticket-incident-types/${id}`, body)
store.updateItem(editingIndex.value, response.data.data)
modal.value = false
showSuccess('Data berhasil diupdate')
fetchTicketIncidentTypes() // Refresh data after update
showSuccessr('Data Berhasil Di Simpan')
fetchTicketIncidentTypes()
} catch (err) {
errorMsg.value = err.response?.data?.message || 'Gagal mengupdate data: ' + err.message
showError(err.response?.data?.message || 'Gagal mengupdate data: ' + err.message)
} finally {
saving.value = false
}
@@ -168,17 +147,15 @@ async function updateTicketIncidentType(id) {
async function deleteTicketIncidentType(id) {
try {
await api.delete(`/${id}`)
await api.delete(`/ticket-incident-types/${id}`)
store.deleteItem(deleteIndex.value)
if (paginatedItems.length === 0 && currentPage.value > 1) currentPage.value--
showSuccess('Data berhasil dihapus')
fetchTicketIncidentTypes() // Refresh data after deletion
fetchTicketIncidentTypes()
} catch (err) {
console.error('Delete failed:', err)
errorMsg.value = 'Gagal menghapus data: ' + err.message
showError('Gagal menghapus data: ' + err.message)
} finally {
deleteIndex.value = -1
deleteModal.value = false
}
}
@@ -190,12 +167,6 @@ async function saveItem() {
await createTicketIncidentType()
}
}
async function confirmDelete() {
if (deleteIndex.value < 0) return
const item = store.items[deleteIndex.value]
await deleteTicketIncidentType(item.id)
}
</script>
<template>
@@ -204,15 +175,12 @@ async function confirmDelete() {
<CCard class="mb-4">
<CCardHeader>
<strong>Ticket Incident</strong>
<CButton color="warning" size="sm" class="float-end" @click="openAddModal">
<CButton color="primary" class="float-end" @click="openAddModal">
+ Tambah Ticket Incident
</CButton>
</CCardHeader>
<CCardBody>
<CAlert color="success" v-if="successMsg" dismissible @close="successMsg = ''">
{{ successMsg }}
</CAlert>
<div class="table-responsive">
<div class="">
<CTable striped hover small>
<CTableHead>
<CTableRow>
@@ -265,19 +233,20 @@ async function confirmDelete() {
@change="changePerPage(Number($event.target.value))"
>
<option :value="10">10</option>
<option :value="15">15</option>
<option :value="25">25</option>
<option :value="50">50</option>
<option :value="100">100</option>
</CFormSelect>
<span>dari {{ store.items.length }} data</span>
</div>
<CPagination v-if="totalPages > 1" size="sm" aria-label="pagination">
<CPagination v-if="totalPages > 1" aria-label="pagination">
<CPaginationItem
aria-label="Previous"
:disabled="currentPage === 1"
@click="goToPage(currentPage - 1)"
>
<span aria-hidden="true">&laquo;</span>
Previous
</CPaginationItem>
<CPaginationItem
v-for="page in totalPages"
@@ -292,7 +261,7 @@ async function confirmDelete() {
:disabled="currentPage === totalPages"
@click="goToPage(currentPage + 1)"
>
<span aria-hidden="true">&raquo;</span>
Next
</CPaginationItem>
</CPagination>
</div>
@@ -308,9 +277,6 @@ async function confirmDelete() {
</CModalHeader>
<CModalBody>
<CForm>
<CAlert color="danger" v-if="errorMsg" dismissible @close="errorMsg = ''">
{{ errorMsg }}
</CAlert>
<div class="mb-3">
<CFormLabel>Name</CFormLabel>
<CFormInput v-model="form.name" placeholder="Contoh: Kabel putus" />
@@ -333,20 +299,6 @@ async function confirmDelete() {
</CModalFooter>
</CModal>
<!-- Delete Confirmation Modal -->
<CModal :visible="deleteModal" @close="deleteModal = false" alignment="center">
<CModalHeader dismiss @close="deleteModal = false">
<CModalTitle>Konfirmasi Hapus</CModalTitle>
</CModalHeader>
<CModalBody>
<p class="mb-0">Apakah Anda yakin ingin menghapus ticket incident ini?</p>
</CModalBody>
<CModalFooter>
<CButton color="secondary" @click="deleteModal = false">Batal</CButton>
<CButton color="danger" @click="confirmDelete">Hapus</CButton>
</CModalFooter>
</CModal>
<!-- Detail Modal -->
<CModal :visible="detailModal" @close="detailModal = false" alignment="center">
<CModalHeader dismiss @close="detailModal = false">
+421 -47
View File
@@ -1,57 +1,431 @@
<script setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'
import Swal from 'sweetalert2'
import { useMaterialStore } from '@/stores/materialStore'
import { materialService } from '@/services/materialService'
import MaterialTable from '@/components/materials/MaterialTable.vue'
import MaterialHistory from '@/components/materials/MaterialHistory.vue'
import AssignMaterialModal from '@/components/materials/AssignMaterialModal.vue'
import TransferMaterialModal from '@/components/materials/TransferMaterialModal.vue'
import ReturnMaterialModal from '@/components/materials/ReturnMaterialModal.vue'
import MaterialDetailModal from '@/components/materials/MaterialDetailModal.vue'
const store = useMaterialStore()
const currentTab = ref('user')
const searchBarcode = ref('')
const searchMaterial = ref('')
const selectedUser = ref(null)
const selectedBarcode = ref('')
const currentPage = ref(1)
const perPage = ref(15)
const filterType = ref('')
const assignModal = ref(false)
const transferModal = ref(false)
const returnModal = ref(false)
const detailModal = ref(false)
const saving = ref(false)
const assignForm = reactive({
user_id: null,
barcode_id: '',
material_name: '',
type: 'serialized',
serial_number: '',
qty: 1,
unit: '',
})
const transferForm = reactive({
barcode_id: '',
from_user_id: null,
to_user_id: null,
})
const returnForm = reactive({
barcode_id: '',
user_id: null,
})
const detailItem = ref({})
const filteredMaterials = computed(() => {
return store.materials.filter((item) => {
const barcodeMatch =
!searchBarcode.value ||
item.barcode_id?.toLowerCase().includes(searchBarcode.value.toLowerCase())
const materialMatch =
!searchMaterial.value ||
item.material_name?.toLowerCase().includes(searchMaterial.value.toLowerCase())
const userMatch = !selectedUser.value || item.user_id == selectedUser.value
const typeMatch = !filterType.value || item.type === filterType.value
return barcodeMatch && materialMatch && userMatch && typeMatch
})
})
const isFilteringActive = computed(() => {
return (
searchBarcode.value !== '' ||
searchMaterial.value !== '' ||
selectedUser.value !== null ||
filterType.value !== ''
)
})
const currentMaterialsSource = computed(() => {
return isFilteringActive.value ? filteredMaterials.value : store.materials
})
const totalPages = computed(() => Math.ceil(currentMaterialsSource.value.length / perPage.value))
const paginatedMaterials = computed(() => {
const start = (currentPage.value - 1) * perPage.value
return currentMaterialsSource.value.slice(start, start + perPage.value)
})
const assignedMaterial = computed(
() => store.materials.filter((i) => i.status === 'Assigned').length,
)
const returnedMaterial = computed(
() => store.materials.filter((i) => i.status === 'Returned').length,
)
const serializedMaterial = computed(
() => store.materials.filter((i) => i.type === 'serialized').length,
)
const consumableMaterial = computed(
() => store.materials.filter((i) => i.type === 'consumable').length,
)
const totalMaterial = computed(() => store.materials.length)
watch(searchBarcode, () => {
currentPage.value = 1
})
watch(searchMaterial, () => {
currentPage.value = 1
})
watch(filterType, () => {
currentPage.value = 1
})
watch([selectedUser], async ([newSelectedUser]) => {
currentPage.value = 1
if (newSelectedUser) {
await store.fetchByUser(newSelectedUser)
} else {
await store.fetchAll()
}
})
watch(selectedBarcode, async (val) => {
if (val) await store.fetchHistory(val)
})
onMounted(() => {
store.fetchAll()
})
function openTransferModal(item) {
transferForm.barcode_id = item.barcode_id
transferForm.from_user_id = item.user_id
transferForm.to_user_id = null
transferModal.value = true
}
function openReturnModal(item) {
returnForm.barcode_id = item.barcode_id
returnForm.user_id = item.user_id
returnModal.value = true
}
function openDetail(item) {
detailItem.value = { ...item }
detailModal.value = true
}
async function assignMaterial() {
saving.value = true
try {
await materialService.assign({
...assignForm,
qty: Number(assignForm.qty),
})
assignModal.value = false
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: 'Material berhasil di-assign',
timer: 1500,
showConfirmButton: false,
})
const userId = selectedUser.value || assignForm.user_id
if (userId) {
selectedUser.value = userId
await store.fetchByUser(userId)
} else {
await store.fetchAll()
}
assignForm.barcode_id = ''
assignForm.user_id = null
assignForm.material_name = ''
assignForm.type = 'serialized'
assignForm.serial_number = ''
assignForm.qty = 1
assignForm.unit = ''
} catch (err) {
Swal.fire({ icon: 'error', title: 'Gagal', text: err.response?.data?.message || err.message })
} finally {
saving.value = false
}
}
async function transferMaterial() {
const confirm = await Swal.fire({
title: 'Transfer Material?',
text: 'Pastikan user tujuan sudah benar.',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Transfer',
cancelButtonText: 'Batal',
})
if (!confirm.isConfirmed) return
saving.value = true
try {
await materialService.transfer({ ...transferForm })
transferModal.value = false
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: 'Material berhasil dipindahkan',
timer: 1500,
showConfirmButton: false,
})
if (selectedUser.value) {
await store.fetchByUser(selectedUser.value)
} else {
await store.fetchAll()
}
if (selectedBarcode.value) await store.fetchHistory(selectedBarcode.value)
} catch (err) {
Swal.fire({ icon: 'error', title: 'Gagal', text: err.response?.data?.message || err.message })
} finally {
saving.value = false
}
}
async function returnMaterial() {
const confirm = await Swal.fire({
title: 'Return Material?',
text: 'Material akan dikembalikan.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Ya',
})
if (!confirm.isConfirmed) return
saving.value = true
try {
await materialService.returnMaterial({ ...returnForm })
returnModal.value = false
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: 'Material berhasil dikembalikan',
timer: 1500,
showConfirmButton: false,
})
if (selectedUser.value) {
await store.fetchByUser(selectedUser.value)
} else {
await store.fetchAll()
}
if (selectedBarcode.value) await store.fetchHistory(selectedBarcode.value)
} catch (err) {
Swal.fire({ icon: 'error', title: 'Gagal', text: err.response?.data?.message || err.message })
} finally {
saving.value = false
}
}
</script>
<template>
<CRow>
<CCol :xs="12">
<CCard class="mb-4">
<CCardHeader> <strong>Ticket Materials</strong> </CCardHeader>
<CCardHeader class="d-flex justify-content-between align-items-center">
<div>
<strong>Material Management</strong>
<div class="small text-body-secondary">
Assign, Transfer, Return dan History Material
</div>
</div>
<CButton color="primary" @click="assignModal = true">+ Tambah Material</CButton>
</CCardHeader>
<CCardBody>
<CTable striped hover>
<CTableHead>
<CTableRow>
<CTableHeaderCell scope="col">#</CTableHeaderCell>
<CTableHeaderCell scope="col">Material Name</CTableHeaderCell>
<CTableHeaderCell scope="col">Ticket</CTableHeaderCell>
<CTableHeaderCell scope="col">Quantity</CTableHeaderCell>
<CTableHeaderCell scope="col">Unit</CTableHeaderCell>
<CTableHeaderCell scope="col">Cost</CTableHeaderCell>
</CTableRow>
</CTableHead>
<CTableBody>
<CTableRow>
<CTableHeaderCell scope="row">1</CTableHeaderCell>
<CTableDataCell>Cable CAT6</CTableDataCell>
<CTableDataCell>Network setup</CTableDataCell>
<CTableDataCell>100</CTableDataCell>
<CTableDataCell>Meters</CTableDataCell>
<CTableDataCell>$150.00</CTableDataCell>
</CTableRow>
<CTableRow>
<CTableHeaderCell scope="row">2</CTableHeaderCell>
<CTableDataCell>RJ45 Connector</CTableDataCell>
<CTableDataCell>Network setup</CTableDataCell>
<CTableDataCell>50</CTableDataCell>
<CTableDataCell>Pcs</CTableDataCell>
<CTableDataCell>$25.00</CTableDataCell>
</CTableRow>
<CTableRow>
<CTableHeaderCell scope="row">3</CTableHeaderCell>
<CTableDataCell>SSD 512GB</CTableDataCell>
<CTableDataCell>Hardware upgrade</CTableDataCell>
<CTableDataCell>2</CTableDataCell>
<CTableDataCell>Pcs</CTableDataCell>
<CTableDataCell>$200.00</CTableDataCell>
</CTableRow>
<CTableRow>
<CTableHeaderCell scope="row">4</CTableHeaderCell>
<CTableDataCell>RAM 16GB</CTableDataCell>
<CTableDataCell>Hardware upgrade</CTableDataCell>
<CTableDataCell>2</CTableDataCell>
<CTableDataCell>Pcs</CTableDataCell>
<CTableDataCell>$180.00</CTableDataCell>
</CTableRow>
</CTableBody>
</CTable>
<CRow class="mb-4">
<CCol md
><CCard class="border-start border-primary border-4"
><CCardBody
><div>Total Material</div>
<h3>{{ totalMaterial }}</h3></CCardBody
></CCard
></CCol
>
<CCol md
><CCard class="border-start border-success border-4"
><CCardBody
><div>Assigned</div>
<h3>{{ assignedMaterial }}</h3></CCardBody
></CCard
></CCol
>
<CCol md
><CCard class="border-start border-secondary border-4"
><CCardBody
><div>Returned</div>
<h3>{{ returnedMaterial }}</h3></CCardBody
></CCard
></CCol
>
<CCol md
><CCard class="border-start border-info border-4"
><CCardBody
><div>Serialized</div>
<h3>{{ serializedMaterial }}</h3></CCardBody
></CCard
></CCol
>
<CCol md
><CCard class="border-start border-warning border-4"
><CCardBody
><div>Consumable</div>
<h3>{{ consumableMaterial }}</h3></CCardBody
></CCard
></CCol
>
</CRow>
<CTabs :active-item-key="currentTab" @change="(key) => (currentTab = key)">
<CTabList variant="tabs">
<CTab item-key="user">Material User</CTab>
<CTab item-key="history">History</CTab>
</CTabList>
<CTabPanel item-key="user" class="mt-4">
<CRow class="mb-3">
<CCol md="3"
><CFormLabel>Barcode</CFormLabel><CFormInput v-model="searchBarcode"
/></CCol>
<CCol md="3"
><CFormLabel>Material</CFormLabel><CFormInput v-model="searchMaterial"
/></CCol>
<CCol md="2">
<CFormLabel>Type</CFormLabel>
<CFormSelect v-model="filterType">
<option value="">Semua Type</option>
<option value="serialized">Serialized</option>
<option value="consumable">Consumable</option>
</CFormSelect>
</CCol>
<CCol md="2">
<CFormLabel>User ID</CFormLabel>
<CFormInput v-model="selectedUser" placeholder="Filter by User ID" />
</CCol>
<CCol md="2" class="d-flex align-items-end">
<CButton
color="secondary"
@click="
((searchBarcode = ''),
(searchMaterial = ''),
(selectedUser = null),
(filterType = ''))
"
>Reset</CButton
>
</CCol>
</CRow>
<MaterialTable
:data="paginatedMaterials"
:totalPages="totalPages"
:currentPage="currentPage"
:perPage="perPage"
:totalItems="currentMaterialsSource.length"
:loading="store.loading"
@update:perPage="
(val) => {
perPage = val
currentPage = 1
}
"
@page="(p) => (currentPage = p)"
@detail="openDetail"
@transfer="openTransferModal"
@return="openReturnModal"
/>
</CTabPanel>
<CTabPanel item-key="history" class="mt-4">
<CRow class="mb-3">
<CCol md="4">
<CFormLabel>Barcode</CFormLabel>
<CFormInput v-model="selectedBarcode" placeholder="Masukkan Barcode" />
</CCol>
<CCol md="2" class="d-flex align-items-end">
<CButton color="primary" @click="store.fetchHistory(selectedBarcode)"
>Lihat History</CButton
>
</CCol>
</CRow>
<MaterialHistory :histories="store.histories" />
</CTabPanel>
</CTabs>
</CCardBody>
</CCard>
</CCol>
</CRow>
<AssignMaterialModal
:visible="assignModal"
:form="assignForm"
:saving="saving"
@close="assignModal = false"
@save="assignMaterial"
/>
<TransferMaterialModal
:visible="transferModal"
:form="transferForm"
:saving="saving"
@close="transferModal = false"
@save="transferMaterial"
/>
<ReturnMaterialModal
:visible="returnModal"
:form="returnForm"
:saving="saving"
@close="returnModal = false"
@save="returnMaterial"
/>
<MaterialDetailModal
:visible="detailModal"
:item="detailItem"
@close="detailModal = false"
@transfer="openTransferModal"
@return="openReturnModal"
/>
</template>
+30 -73
View File
@@ -1,27 +1,22 @@
<script setup>
import axios from 'axios'
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useTicketTypeStore } from '@/stores/ticketType.js'
import { formatTanggal } from '@/utils/tglindo.js'
import { showConfirm, showSuccess, showSuccessr, showError } from '@/utils/swal.js'
import { CButton } from '@coreui/vue'
import { CBadge } from '@coreui/vue'
import api from '@/api/axios'
const store = useTicketTypeStore()
const loading = ref(false)
const token = 'Bearer uZ1vM4UON3CsDV9niGD1gLS4sHpCxT9nzadkITmc6caf2ea2'
const api = axios.create({
baseURL: 'https://api.radiq.my.id/api',
headers: { Authorization: token, 'Content-Type': 'application/json' },
})
async function fetchAllData() {
loading.value = true
try {
const res = await api.get('/ticket-types?per_page=1000')
store.setTicketTypes(res.data.data || [])
} catch (err) {
console.error('Failed to fetch data:', err)
errorMsg.value = 'Gagal memuat data: ' + err.message
showError('Gagal memuat data: ' + err.message)
} finally {
loading.value = false
}
@@ -32,15 +27,11 @@ onMounted(() => {
})
const modal = ref(false)
const deleteModal = ref(false)
const detailModal = ref(false)
const editingIndex = ref(-1)
const isEdit = ref(false)
const deleteIndex = ref(-1)
const saving = ref(false)
const errorMsg = ref('')
const successMsg = ref('')
let successTimer = null
const currentPage = ref(1)
const perPage = ref(10)
@@ -61,14 +52,6 @@ function changePerPage(val) {
currentPage.value = 1
}
function showSuccess(msg) {
successMsg.value = msg
if (successTimer) clearTimeout(successTimer)
successTimer = setTimeout(() => {
successMsg.value = ''
}, 3000)
}
const form = reactive({
code: '',
name: '',
@@ -99,8 +82,7 @@ async function getTicketType(id) {
detailItem.value = res.data.data
detailModal.value = true
} catch (err) {
console.error('Failed to fetch detail:', err)
errorMsg.value = 'Gagal memuat detail: ' + err.message
showError('Gagal memuat detail: ' + err.message)
}
}
@@ -133,15 +115,19 @@ function openEditModal(item) {
modal.value = true
}
function openDeleteModal(item) {
async function openDeleteModal(item) {
const idx = store.ticketTypes.findIndex((t) => t.code === item.code)
if (idx === -1) return
const result = await showConfirm(
'Konfirmasi Hapus',
'Apakah Anda yakin ingin menghapus ticket type ini?',
)
if (!result.isConfirmed) return
deleteIndex.value = idx
deleteModal.value = true
await deleteTicketType(item.id)
}
async function createTicketType() {
errorMsg.value = ''
saving.value = true
const body = {
code: form.code,
@@ -161,14 +147,13 @@ async function createTicketType() {
showSuccess('Data berhasil disimpan')
fetchAllData()
} catch (err) {
errorMsg.value = err.response?.data?.message || 'Gagal menyimpan data: ' + err.message
showError(err.response?.data?.message || 'Gagal menyimpan data: ' + err.message)
} finally {
saving.value = false
}
}
async function updateTicketType(id) {
errorMsg.value = ''
saving.value = true
const body = {
code: form.code,
@@ -184,10 +169,10 @@ async function updateTicketType(id) {
await api.put(`/ticket-types/${id}`, body)
store.updateTicketType(editingIndex.value, { ...body })
modal.value = false
showSuccess('Data berhasil diupdate')
showSuccessr('Data berhasil diupdate')
fetchAllData()
} catch (err) {
errorMsg.value = err.response?.data?.message || 'Gagal mengupdate data: ' + err.message
showError(err.response?.data?.message || 'Gagal mengupdate data: ' + err.message)
} finally {
saving.value = false
}
@@ -201,11 +186,9 @@ async function deleteTicketType(id) {
showSuccess('Data berhasil dihapus')
fetchAllData()
} catch (err) {
console.error('Delete failed:', err)
errorMsg.value = 'Gagal menghapus data: ' + err.message
showError('Gagal menghapus data: ' + err.message)
} finally {
deleteIndex.value = -1
deleteModal.value = false
}
}
@@ -217,12 +200,6 @@ async function saveItem() {
await createTicketType()
}
}
async function confirmDelete() {
if (deleteIndex.value < 0) return
const item = store.ticketTypes[deleteIndex.value]
await deleteTicketType(item.id)
}
</script>
<template>
@@ -231,15 +208,12 @@ async function confirmDelete() {
<CCard class="mb-4">
<CCardHeader>
<strong>Ticket Type</strong>
<CButton color="warning" size="sm" class="float-end" @click="openAddModal">
<CButton color="primary" class="float-end" @click="openAddModal">
+ Tambah Ticket Type
</CButton>
</CCardHeader>
<CCardBody>
<CAlert color="success" v-if="successMsg" dismissible @close="successMsg = ''">
{{ successMsg }}
</CAlert>
<div class="table-responsive">
<div class="">
<CTable striped hover small>
<CTableHead>
<CTableRow>
@@ -256,7 +230,7 @@ async function confirmDelete() {
(currentPage - 1) * perPage + index + 1
}}</CTableHeaderCell>
<CTableDataCell
><code>{{ item.code }}</code></CTableDataCell
><CBadge color="primary">{{ item.code }}</CBadge></CTableDataCell
>
<CTableDataCell>{{ item.name }}</CTableDataCell>
<CTableDataCell class="text-center">{{ item.sla_minutes }}</CTableDataCell>
@@ -291,13 +265,13 @@ async function confirmDelete() {
</CFormSelect>
<span>dari {{ store.ticketTypes.length }} data</span>
</div>
<CPagination v-if="totalPages > 1" size="sm" aria-label="pagination">
<CPagination v-if="totalPages > 1" aria-label="pagination">
<CPaginationItem
aria-label="Previous"
:disabled="currentPage === 1"
@click="goToPage(currentPage - 1)"
>
<span aria-hidden="true">&laquo;</span>
Previous
</CPaginationItem>
<CPaginationItem
v-for="page in totalPages"
@@ -312,7 +286,7 @@ async function confirmDelete() {
:disabled="currentPage === totalPages"
@click="goToPage(currentPage + 1)"
>
<span aria-hidden="true">&raquo;</span>
Next
</CPaginationItem>
</CPagination>
</div>
@@ -328,9 +302,6 @@ async function confirmDelete() {
</CModalHeader>
<CModalBody>
<CForm>
<CAlert color="danger" v-if="errorMsg" dismissible @close="errorMsg = ''">
{{ errorMsg }}
</CAlert>
<div class="mb-3">
<CFormLabel>Code</CFormLabel>
<CFormInput v-model="form.code" placeholder="Contoh: BUG" :disabled="isEdit" />
@@ -369,20 +340,6 @@ async function confirmDelete() {
</CModalFooter>
</CModal>
<!-- Delete Confirmation Modal -->
<CModal :visible="deleteModal" @close="deleteModal = false" alignment="center">
<CModalHeader dismiss @close="deleteModal = false">
<CModalTitle>Konfirmasi Hapus</CModalTitle>
</CModalHeader>
<CModalBody>
<p class="mb-0">Apakah Anda yakin ingin menghapus ticket type ini?</p>
</CModalBody>
<CModalFooter>
<CButton color="secondary" @click="deleteModal = false">Batal</CButton>
<CButton color="danger" @click="confirmDelete">Hapus</CButton>
</CModalFooter>
</CModal>
<!-- Detail Modal -->
<CModal :visible="detailModal" @close="detailModal = false" alignment="center">
<CModalHeader dismiss @close="detailModal = false">
@@ -394,7 +351,7 @@ async function confirmDelete() {
<CTableRow>
<CTableHeaderCell style="width: 40%">Code</CTableHeaderCell>
<CTableDataCell
><code>{{ detailItem.code }}</code></CTableDataCell
><b>{{ detailItem.code }}</b></CTableDataCell
>
</CTableRow>
<CTableRow>
@@ -404,7 +361,7 @@ async function confirmDelete() {
<CTableRow>
<CTableHeaderCell>Need Approval</CTableHeaderCell>
<CTableDataCell>
<CBadge :color="detailItem.need_approval ? 'warning' : 'secondary'">
<CBadge :color="detailItem.need_approval ? 'success' : 'secondary'">
{{ detailItem.need_approval ? 'Ya' : 'Tidak' }}
</CBadge>
</CTableDataCell>
@@ -412,7 +369,7 @@ async function confirmDelete() {
<CTableRow>
<CTableHeaderCell>Require Photo</CTableHeaderCell>
<CTableDataCell>
<CBadge :color="detailItem.require_photo ? 'warning' : 'secondary'">
<CBadge :color="detailItem.require_photo ? 'success' : 'secondary'">
{{ detailItem.require_photo ? 'Ya' : 'Tidak' }}
</CBadge>
</CTableDataCell>
@@ -420,7 +377,7 @@ async function confirmDelete() {
<CTableRow>
<CTableHeaderCell>Require Material</CTableHeaderCell>
<CTableDataCell>
<CBadge :color="detailItem.require_material ? 'warning' : 'secondary'">
<CBadge :color="detailItem.require_material ? 'success' : 'secondary'">
{{ detailItem.require_material ? 'Ya' : 'Tidak' }}
</CBadge>
</CTableDataCell>
@@ -428,7 +385,7 @@ async function confirmDelete() {
<CTableRow>
<CTableHeaderCell>Need Customer</CTableHeaderCell>
<CTableDataCell>
<CBadge :color="detailItem.need_customer ? 'warning' : 'secondary'">
<CBadge :color="detailItem.need_customer ? 'success' : 'secondary'">
{{ detailItem.need_customer ? 'Ya' : 'Tidak' }}
</CBadge>
</CTableDataCell>
@@ -436,7 +393,7 @@ async function confirmDelete() {
<CTableRow>
<CTableHeaderCell>Need Incident Type</CTableHeaderCell>
<CTableDataCell>
<CBadge :color="detailItem.need_incident_type ? 'warning' : 'secondary'">
<CBadge :color="detailItem.need_incident_type ? 'success' : 'secondary'">
{{ detailItem.need_incident_type ? 'Ya' : 'Tidak' }}
</CBadge>
</CTableDataCell>