diff --git a/src/api/file-uploads.jsx b/src/api/file-uploads.jsx index d629846..52bc8bd 100644 --- a/src/api/file-uploads.jsx +++ b/src/api/file-uploads.jsx @@ -5,9 +5,18 @@ const API_BASE_URL = import.meta.env.VITE_API_SERVER; // Get file from uploads directory const getFile = async (folder, filename) => { + const token = localStorage.getItem('token'); + if (!token) { + throw new Error('No authentication token found'); + } + const response = await axios.get(`${API_BASE_URL}/file-uploads/${folder}/${encodeURIComponent(filename)}`, { - responseType: 'blob' + responseType: 'blob', + headers: { + 'Authorization': `Bearer ${token.replace(/"/g, '')}` + } }); + return response.data; }; diff --git a/src/api/jadwal-shift.jsx b/src/api/jadwal-shift.jsx index ce221b3..9373fca 100644 --- a/src/api/jadwal-shift.jsx +++ b/src/api/jadwal-shift.jsx @@ -3,7 +3,7 @@ import { SendRequest } from '../components/Global/ApiRequest'; const getAllJadwalShift = async (queryParams) => { const response = await SendRequest({ method: 'get', - prefix: `jadwal-shift?${queryParams.toString()}`, + prefix: `user-schedule?${queryParams.toString()}`, }); return response.data; @@ -12,7 +12,7 @@ const getAllJadwalShift = async (queryParams) => { const getJadwalShiftById = async (id) => { const response = await SendRequest({ method: 'get', - prefix: `jadwal-shift/${id}`, + prefix: `user-schedule/${id}`, }); return response.data; @@ -21,7 +21,7 @@ const getJadwalShiftById = async (id) => { const createJadwalShift = async (queryParams) => { const response = await SendRequest({ method: 'post', - prefix: `jadwal-shift`, + prefix: `user-schedule`, params: queryParams, }); @@ -31,7 +31,7 @@ const createJadwalShift = async (queryParams) => { const updateJadwalShift = async (id, queryParams) => { const response = await SendRequest({ method: 'put', - prefix: `jadwal-shift/${id}`, + prefix: `user-schedule/${id}`, params: queryParams, }); @@ -41,7 +41,7 @@ const updateJadwalShift = async (id, queryParams) => { const deleteJadwalShift = async (id) => { const response = await SendRequest({ method: 'delete', - prefix: `jadwal-shift/${id}`, + prefix: `user-schedule/${id}`, }); return response.data; }; diff --git a/src/layout/LayoutSidebar.jsx b/src/layout/LayoutSidebar.jsx index e9a3149..1b17d95 100644 --- a/src/layout/LayoutSidebar.jsx +++ b/src/layout/LayoutSidebar.jsx @@ -17,7 +17,7 @@ const LayoutSidebar = () => { // console.log(collapsed, type); }} style={{ - background: 'linear-gradient(180deg, #FF8C42 0%, #FF6B35 100%)', + background: 'linear-gradient(180deg, #1BAA56 0%,rgb(5, 75, 34) 100%)', overflow: 'auto', height: '100vh', position: 'fixed', diff --git a/src/pages/history/event/component/ListHistoryEvent.jsx b/src/pages/history/event/component/ListHistoryEvent.jsx index 624357a..c52e02b 100644 --- a/src/pages/history/event/component/ListHistoryEvent.jsx +++ b/src/pages/history/event/component/ListHistoryEvent.jsx @@ -22,18 +22,18 @@ const ListHistoryEvent = memo(function ListHistoryEvent(props) { }, { title: 'Tag Name', - dataIndex: 'tag_name', - key: 'tag_name', + dataIndex: 'tagname', + key: 'tagname', width: '40%', }, { title: 'Description', - dataIndex: 'condition', - key: 'condition', + dataIndex: 'description', + key: 'description', width: '20%', render: (_, record) => ( ), }, diff --git a/src/pages/jadwalShift/component/DetailJadwalShift.jsx b/src/pages/jadwalShift/component/DetailJadwalShift.jsx index 83d705f..300377b 100644 --- a/src/pages/jadwalShift/component/DetailJadwalShift.jsx +++ b/src/pages/jadwalShift/component/DetailJadwalShift.jsx @@ -1,173 +1,278 @@ import React, { useEffect, useState } from 'react'; -import { - Modal, - Typography, - Button, - ConfigProvider, - Form, - Select, - Spin, - Input -} from 'antd'; -import { NotifOk, NotifAlert } from '../../../components/Global/ToastNotif'; -import { updateJadwalShift, createJadwalShift } from '../../../api/jadwal-shift.jsx'; +import { Modal, Select, Typography, Button, ConfigProvider } from 'antd'; +import { NotifOk } from '../../../components/Global/ToastNotif'; +import { createJadwalShift, updateJadwalShift } from '../../../api/jadwal-shift'; +import { getAllUser } from '../../../api/user'; +import { getAllShift } from '../../../api/master-shift'; +import { validateRun } from '../../../Utils/validate'; const { Text } = Typography; const { Option } = Select; const DetailJadwalShift = (props) => { - const [form] = Form.useForm(); const [confirmLoading, setConfirmLoading] = useState(false); const [employees, setEmployees] = useState([]); - const [loadingEmployees, setLoadingEmployees] = useState(false); + const [shifts, setShifts] = useState([]); + const [loadingData, setLoadingData] = useState(false); - const isReadOnly = props.actionMode === 'preview'; + const defaultData = { + id: '', + user_id: null, + shift_id: null, + schedule_id: '', + user_phone: null, + }; + + const [formData, setFormData] = useState(defaultData); + + const handleSelectChange = (name, value) => { + const updates = { [name]: value }; + + if (name === 'user_id') { + const selectedEmployee = employees.find((emp) => emp.user_id === value); + updates.user_phone = selectedEmployee?.user_phone || '-'; + } + + setFormData({ + ...formData, + ...updates, + }); + }; const handleCancel = () => { + props.setSelectedData(null); props.setActionMode('list'); }; - const fetchEmployees = async () => { - setLoadingEmployees(true); + const fetchData = async () => { + setLoadingData(true); try { - // Data dummy untuk dropdown karyawan - const dummyEmployees = [ - { employee_id: '101', nama_employee: 'Andi Pratama' }, - { employee_id: '102', nama_employee: 'Budi Santoso' }, - { employee_id: '103', nama_employee: 'Citra Lestari' }, - { employee_id: '104', nama_employee: 'Dewi Anggraini' }, - { employee_id: '105', nama_employee: 'Eko Wahyudi' }, - { employee_id: '106', nama_employee: 'Fitriani' }, - ]; - setEmployees(dummyEmployees); + const params = new URLSearchParams({ + page: 1, + limit: 100, + }); + + const [usersResponse, shiftsResponse] = await Promise.all([ + getAllUser(params), + getAllShift(params), + ]); + + const userData = usersResponse?.data || usersResponse || []; + const shiftData = shiftsResponse?.data || shiftsResponse || []; + + setEmployees(Array.isArray(userData) ? userData : []); + setShifts(Array.isArray(shiftData) ? shiftData : []); } catch (error) { - NotifAlert({ icon: 'error', title: 'Gagal', message: 'Gagal memuat daftar karyawan.' }); + NotifOk({ + icon: 'error', + title: 'Gagal', + message: 'Gagal memuat data karyawan atau shift.', + }); } finally { - setLoadingEmployees(false); + setLoadingData(false); } }; const handleSave = async () => { + setConfirmLoading(true); + + // Daftar aturan validasi + const validationRules = [ + { field: 'user_id', label: 'Nama Karyawan', required: true }, + { field: 'shift_id', label: 'Shift', required: true }, + ]; + + if ( + validateRun(formData, validationRules, (errorMessages) => { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: errorMessages, + }); + setConfirmLoading(false); + }) + ) + return; + try { - const values = await form.validateFields(); - let payload; - let responseMessage; + const payload = { + user_id: formData.user_id, + shift_id: formData.shift_id, + }; - setConfirmLoading(true); - if (props.actionMode === 'edit') { - payload = { ...props.selectedData, ...values }; - // await updateJadwalShift(payload.schedule_id, payload); - console.log("Updating schedule with payload:", payload); - responseMessage = 'Jadwal berhasil diperbarui.'; - } else { // 'add' mode - payload = { - employee_id: values.employee_id, - shift_name: props.selectedData.shift_name, - schedule_date: new Date().toISOString().split('T')[0], // Example date - }; - // await createJadwalShift(payload); - console.log("Creating schedule with payload:", payload); - responseMessage = 'User berhasil ditambahkan ke jadwal.'; + // Add schedule_id only if editing and it exists + if (props.actionMode === 'edit' && formData.schedule_id) { + payload.schedule_id = formData.schedule_id; } - await new Promise(resolve => setTimeout(resolve, 500)); // Simulasi API call - NotifOk({ icon: 'success', title: 'Berhasil', message: responseMessage }); - props.setActionMode('list'); // Menutup modal dan memicu refresh di parent + const response = + props.actionMode === 'edit' + ? await updateJadwalShift(formData.id, payload) + : await createJadwalShift(payload); + + if (response && (response.statusCode === 200 || response.statusCode === 201)) { + const action = props.actionMode === 'edit' ? 'diubah' : 'ditambahkan'; + + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: `Jadwal berhasil ${action}.`, + }); + + props.setActionMode('list'); + } else { + NotifOk({ + icon: 'error', + title: 'Gagal', + message: response?.message || 'Terjadi kesalahan saat menyimpan data.', + }); + } } catch (error) { - const message = error.response?.data?.message || 'Gagal memperbarui jadwal.'; - NotifAlert({ icon: 'error', title: 'Gagal', message }); + NotifOk({ + icon: 'error', + title: 'Error', + message: error.message || 'Terjadi kesalahan pada server.', + }); } finally { setConfirmLoading(false); } }; useEffect(() => { - // Hanya jalankan jika modal untuk 'edit' atau 'preview' terbuka if (props.showModal) { - fetchEmployees(); - if (props.actionMode === 'edit' || props.actionMode === 'preview') { - form.setFieldsValue({ - employee_id: props.selectedData.employee_id, - shift_name: props.selectedData.shift_name, - }); - } else if (props.actionMode === 'add') { - form.setFieldsValue({ - shift_name: props.selectedData.shift_name, - employee_id: null, // Reset employee selection - }); - } + fetchData(); } - }, [props.actionMode, props.showModal, props.selectedData, form]); + + if (props.selectedData) { + setFormData({ + id: props.selectedData.id || '', + user_id: props.selectedData.user_id || null, + shift_id: props.selectedData.shift_id || null, + schedule_id: props.selectedData.schedule_id || '', + user_phone: props.selectedData.whatsapp || props.selectedData.user_phone || null, + }); + } else { + setFormData(defaultData); + } + }, [props.showModal, props.selectedData, props.actionMode]); return ( - - {!isReadOnly && ( - - )} + + + + + {!props.readOnly && ( + + )} + , ]} > - -
- {props.actionMode === 'add' ? ( - <> - - - - - - - - ) : ( - <> - - - - - - - - )} -
-
+ {formData && ( +
+
+ Nama Karyawan + * + +
+ +
+ No. Telepon +
+ {formData.user_phone || 'Pilih karyawan terlebih dahulu'} +
+
+ +
+ Shift + * + +
+
+ )}
); }; diff --git a/src/pages/jadwalShift/component/ListJadwalShift.jsx b/src/pages/jadwalShift/component/ListJadwalShift.jsx index 57c11f3..e0249c8 100644 --- a/src/pages/jadwalShift/component/ListJadwalShift.jsx +++ b/src/pages/jadwalShift/component/ListJadwalShift.jsx @@ -1,5 +1,17 @@ import React, { memo, useState, useEffect } from 'react'; -import { Space, ConfigProvider, Button, Row, Col, Card, Input, Typography, Spin, Divider, Checkbox, Select } from 'antd'; +import { + Space, + ConfigProvider, + Button, + Row, + Col, + Card, + Input, + Typography, + Divider, + Checkbox, + Select, +} from 'antd'; import { PlusOutlined, SearchOutlined, @@ -9,32 +21,25 @@ import { } from '@ant-design/icons'; import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif'; import { useNavigate } from 'react-router-dom'; -import { getAllJadwalShift, deleteJadwalShift, updateJadwalShift } from '../../../api/jadwal-shift.jsx'; +import { getAllJadwalShift, deleteJadwalShift, updateJadwalShift } from '../../../api/jadwal-shift'; +import { getAllShift } from '../../../api/master-shift'; const { Title, Text } = Typography; const ListJadwalShift = memo(function ListJadwalShift(props) { + const [trigerFilter, setTrigerFilter] = useState(false); + const defaultFilter = { criteria: '' }; + const [formDataFilter, setFormDataFilter] = useState(defaultFilter); const [groupedSchedules, setGroupedSchedules] = useState({}); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); const [searchValue, setSearchValue] = useState(''); - const [isEditMode, setIsEditMode] = useState(false); const [selectedSchedules, setSelectedSchedules] = useState([]); - const [editingShift, setEditingShift] = useState(null); // State for shift-specific edit mode - const [pendingChanges, setPendingChanges] = useState({}); // State for bulk shift edits - const [employeeOptions, setEmployeeOptions] = useState([]); // State for employee dropdown + const [editingShift, setEditingShift] = useState(null); + const [pendingChanges, setPendingChanges] = useState({}); + const [employeeOptions, setEmployeeOptions] = useState([]); + const [shiftOptions, setShiftOptions] = useState([]); const navigate = useNavigate(); - // Function to format timestamp without moment.js - const formatTimestamp = (timestamp) => { - const date = new Date(timestamp); - const optionsDate = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; - const optionsTime = { hour: '2-digit', minute: '2-digit', hour12: false }; - - const formattedDate = date.toLocaleDateString('id-ID', optionsDate); - const formattedTime = date.toLocaleTimeString('id-ID', optionsTime); - return `${formattedDate} pukul ${formattedTime}`; - }; - const formatRelativeTimestamp = (timestamp) => { const now = new Date(); const date = new Date(timestamp); @@ -50,97 +55,157 @@ const ListJadwalShift = memo(function ListJadwalShift(props) { dayString = date.toLocaleDateString('id-ID', { day: 'numeric', month: 'long' }); } - const timeString = date.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', hour12: false }).replace('.', ':'); + const timeString = date + .toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', hour12: false }) + .replace('.', ':'); return `${dayString}, ${timeString}`; }; const fetchData = async () => { setLoading(true); try { - // const params = new URLSearchParams({ ... }); - // const response = await getAllJadwalShift(params); - - // ================== START: DUMMY DATA FOR VISUAL PREVIEW ================== - // This section creates dummy schedules and users to ensure all shifts are populated. - // The actual API call is commented out for now. - - const mockSchedules = [ - { schedule_id: 1, employee_id: '101', shift_name: 'PAGI', nama_employee: 'Andi Pratama', whatsapp: '081234567890', updated_by: 'Admin Super', updated_at: '2024-05-21T10:00:00' }, - { schedule_id: 2, employee_id: '102', shift_name: 'PAGI', nama_employee: 'Budi Santoso', whatsapp: '081234567891', updated_by: 'Admin Super', updated_at: '2024-05-21T10:00:00' }, - { schedule_id: 3, employee_id: '103', shift_name: 'SIANG', nama_employee: 'Citra Lestari', whatsapp: '081234567892', updated_by: 'John Doe', updated_at: '2024-05-21T09:45:00' }, - { schedule_id: 4, employee_id: '104', shift_name: 'SIANG', nama_employee: 'Dewi Anggraini', whatsapp: '081234567893', updated_by: 'John Doe', updated_at: '2024-05-21T09:45:00' }, - { schedule_id: 5, employee_id: '105', shift_name: 'MALAM', nama_employee: 'Eko Wahyudi', whatsapp: '081234567894', updated_by: 'Jane Smith', updated_at: '2024-05-20T22:15:00' }, - { schedule_id: 6, employee_id: '106', shift_name: 'MALAM', nama_employee: 'Fitriani', whatsapp: '081234567895', updated_by: 'Jane Smith', updated_at: '2024-05-20T22:15:00' }, - ]; - - // Dummy employee data for dropdowns - const dummyEmployees = [ - { employee_id: '101', nama_employee: 'Andi Pratama' }, - { employee_id: '102', nama_employee: 'Budi Santoso' }, - { employee_id: '103', nama_employee: 'Citra Lestari' }, - { employee_id: '104', nama_employee: 'Dewi Anggraini' }, - { employee_id: '105', nama_employee: 'Eko Wahyudi' }, - { employee_id: '106', nama_employee: 'Fitriani' }, - ]; - setEmployeeOptions(dummyEmployees); - // =================== END: DUMMY DATA FOR VISUAL PREVIEW =================== - - - const grouped = mockSchedules.reduce((acc, schedule) => { - const shiftName = schedule.shift_name.toUpperCase().trim(); - if (!acc[shiftName]) { - acc[shiftName] = { users: [], lastUpdate: { user: 'N/A', timestamp: '1970-01-01T00:00:00Z' } }; - } - acc[shiftName].users.push(schedule); - - // Find the latest update timestamp for the shift - const currentUpdate = new Date(schedule.updated_at || schedule.created_at); - const lastUpdate = new Date(acc[shiftName].lastUpdate.timestamp); - if (currentUpdate > lastUpdate) { - acc[shiftName].lastUpdate = { - user: schedule.updated_by || 'N/A', - timestamp: currentUpdate.toISOString() - }; - } - return acc; - }, {}); - - const finalGrouped = { - 'PAGI': grouped['PAGI'] || { users: [], lastUpdate: { user: 'N/A', timestamp: new Date().toISOString() } }, - 'SIANG': grouped['SIANG'] || { users: [], lastUpdate: { user: 'N/A', timestamp: new Date().toISOString() } }, - 'MALAM': grouped['MALAM'] || { users: [], lastUpdate: { user: 'N/A', timestamp: new Date().toISOString() } }, + const paging = { + page: 1, + limit: 1000, }; - setGroupedSchedules(finalGrouped); + const params = new URLSearchParams({ ...paging, ...formDataFilter }); + // Fetch both schedules and shifts data + const [schedulesResponse, shiftsResponse] = await Promise.all([ + getAllJadwalShift(params), + getAllShift(params), + ]); + + // Handle nested data structure from backend + const rawData = schedulesResponse?.data || schedulesResponse || []; + const shifts = shiftsResponse?.data || shiftsResponse || []; + + setShiftOptions(shifts); + + // Parse backend response structure: [{ shift: { shift_id, shift_name, users: [...] } }] + const grouped = {}; + const allUsers = []; + + rawData.forEach((item) => { + if (item.shift && item.shift.shift_name) { + const shift = item.shift; + const shiftName = shift.shift_name.toUpperCase().trim(); + + // Initialize shift group + if (!grouped[shiftName]) { + grouped[shiftName] = { + shift_id: shift.shift_id, + users: [], + lastUpdate: { user: 'N/A', timestamp: '1970-01-01T00:00:00Z' }, + }; + } + + // Process users in this shift + if (shift.users && Array.isArray(shift.users)) { + shift.users.forEach((user) => { + const normalizedUser = { + id: user.user_schedule_id, + user_schedule_id: user.user_schedule_id, + user_id: user.user_id, + shift_id: shift.shift_id, + shift_name: shift.shift_name, + nama_employee: user.user_fullname || user.user_name || 'Unknown', + whatsapp: user.user_phone || '-', + user_fullname: user.user_fullname, + user_name: user.user_name, + user_phone: user.user_phone, + updated_at: user.updated_at, + created_at: user.created_at, + updated_by: user.updated_by, + }; + + grouped[shiftName].users.push(normalizedUser); + allUsers.push(normalizedUser); + + // Update last update timestamp + const currentUpdate = new Date( + user.updated_at || user.created_at || new Date() + ); + const lastUpdate = new Date(grouped[shiftName].lastUpdate.timestamp); + if (currentUpdate > lastUpdate) { + grouped[shiftName].lastUpdate = { + user: user.updated_by || 'N/A', + timestamp: currentUpdate.toISOString(), + }; + } + }); + } + } + }); + + setEmployeeOptions(allUsers); + + // Add empty shifts that don't have users yet + shifts.forEach((shift) => { + const shiftName = shift.shift_name.toUpperCase().trim(); + if (!grouped[shiftName]) { + grouped[shiftName] = { + shift_id: shift.shift_id, + users: [], + lastUpdate: { user: 'N/A', timestamp: new Date().toISOString() }, + }; + } + }); + + setGroupedSchedules(grouped); } catch (error) { - console.error('Error processing dummy data:', error); - NotifAlert({ // Changed to NotifAlert for consistency - icon: 'error', // Changed to error icon - title: 'Gagal Memuat Data', // Changed title - message: 'Terjadi kesalahan saat memuat data jadwal shift.', // Changed message + NotifAlert({ + icon: 'error', + title: 'Gagal Memuat Data', + message: 'Terjadi kesalahan saat memuat data jadwal shift.', }); } finally { - // Add a small delay to simulate network loading - setTimeout(() => setLoading(false), 500); + setLoading(false); } }; useEffect(() => { const token = localStorage.getItem('token'); if (token) { - fetchData(); + if (props.actionMode === 'list') { + setFormDataFilter(defaultFilter); + doFilter(); + } } else { navigate('/signin'); } - }, [searchValue, props.actionMode]); // Refetch when searchValue changes or after modal closes + }, [props.actionMode]); - const handleSearch = (value) => { - setSearchValue(value); + useEffect(() => { + if (props.actionMode === 'list') { + fetchData(); + } + }, [trigerFilter]); + + const doFilter = () => { + setTrigerFilter((prev) => !prev); + }; + + const handleSearch = () => { + setFormDataFilter({ criteria: searchValue }); + setTrigerFilter((prev) => !prev); }; const handleSearchClear = () => { setSearchValue(''); + setFormDataFilter({ criteria: '' }); + setTrigerFilter((prev) => !prev); + }; + + const showPreviewModal = (param) => { + props.setSelectedData(param); + props.setActionMode('preview'); + }; + + const showEditModal = (param = null) => { + props.setSelectedData(param); + props.setActionMode('edit'); }; const showAddModal = (param = null) => { @@ -148,28 +213,31 @@ const ListJadwalShift = memo(function ListJadwalShift(props) { props.setActionMode('add'); }; - const handleAction = (mode, record) => { - props.setSelectedData(record); - props.setActionMode(mode); - }; - - const showDeleteDialog = (user) => { + const showDeleteDialog = (param) => { NotifConfirmDialog({ icon: 'question', title: 'Konfirmasi Hapus', - message: `Hapus jadwal untuk karyawan "${user.nama_employee}"?`, - onConfirm: () => handleDelete(user.schedule_id), + message: `Jadwal untuk karyawan "${param.nama_employee}" akan dihapus?`, + onConfirm: () => handleDelete(param.id), + onCancel: () => props.setSelectedData(null), }); }; - const handleDelete = async (schedule_id) => { - try { - await deleteJadwalShift(schedule_id); - NotifOk({ icon: 'success', title: 'Berhasil', message: 'Jadwal berhasil dihapus.' }); - fetchData(); // Refresh data - } catch (error) { - console.error("Failed to delete schedule:", error); - NotifAlert({ icon: "error", title: "Gagal", message: "Gagal menghapus jadwal." }); + const handleDelete = async (id) => { + const response = await deleteJadwalShift(id); + if (response.statusCode === 200) { + NotifAlert({ + icon: 'success', + title: 'Berhasil', + message: 'Jadwal berhasil dihapus.', + }); + doFilter(); + } else { + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: response?.message || 'Gagal menghapus jadwal.', + }); } }; @@ -185,52 +253,79 @@ const ListJadwalShift = memo(function ListJadwalShift(props) { setSelectedSchedules([]); }; - const handleSelectSchedule = (scheduleId, isChecked) => { + const handleSelectSchedule = (id, isChecked) => { if (isChecked) { - setSelectedSchedules(prev => [...prev, scheduleId]); + setSelectedSchedules((prev) => [...prev, id]); } else { - setSelectedSchedules(prev => prev.filter(id => id !== scheduleId)); + setSelectedSchedules((prev) => prev.filter((scheduleId) => scheduleId !== id)); } }; const handleBulkUpdateChange = (scheduleId, field, value) => { - setPendingChanges(prev => ({ + setPendingChanges((prev) => ({ ...prev, [scheduleId]: { ...prev[scheduleId], [field]: value, - } + }, })); }; const handleBulkSave = async () => { if (Object.keys(pendingChanges).length === 0) { - NotifAlert({ icon: 'info', title: 'Tidak Ada Perubahan', message: 'Tidak ada perubahan untuk disimpan.' }); + NotifAlert({ + icon: 'info', + title: 'Tidak Ada Perubahan', + message: 'Tidak ada perubahan untuk disimpan.', + }); cancelShiftEditMode(); return; } - const updatePromises = Object.keys(pendingChanges).map(scheduleId => { - const originalSchedule = groupedSchedules[editingShift].users.find(u => u.schedule_id.toString() === scheduleId); - const updatedData = { ...originalSchedule, ...pendingChanges[scheduleId] }; - // return updateJadwalShift(scheduleId, updatedData); // UNCOMMENT FOR REAL API - console.log(`Simulating update for schedule ${scheduleId}:`, updatedData); // DUMMY LOG - return Promise.resolve(); // DUMMY PROMISE + const updatePromises = Object.keys(pendingChanges).map((id) => { + const originalSchedule = groupedSchedules[editingShift].users.find( + (u) => u.id.toString() === id + ); + const changes = pendingChanges[id]; + + // Build payload according to backend schema + const payload = { + user_id: changes.user_id || originalSchedule.user_id, + shift_id: changes.shift_id || originalSchedule.shift_id, + }; + + if (originalSchedule.schedule_id) { + payload.schedule_id = originalSchedule.schedule_id; + } + + return updateJadwalShift(id, payload); }); try { await Promise.all(updatePromises); - NotifOk({ icon: 'success', title: 'Berhasil', message: 'Semua perubahan berhasil disimpan.' }); - fetchData(); + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: 'Semua perubahan berhasil disimpan.', + }); + doFilter(); cancelShiftEditMode(); } catch (error) { - NotifAlert({ icon: 'error', title: 'Gagal', message: 'Gagal menyimpan beberapa perubahan.' }); + NotifAlert({ + icon: 'error', + title: 'Gagal', + message: 'Gagal menyimpan beberapa perubahan.', + }); } }; const handleBulkDelete = () => { if (selectedSchedules.length === 0) { - NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Pilih setidaknya satu jadwal untuk dihapus.' }); + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Pilih setidaknya satu jadwal untuk dihapus.', + }); return; } NotifConfirmDialog({ @@ -238,11 +333,13 @@ const ListJadwalShift = memo(function ListJadwalShift(props) { title: `Konfirmasi Hapus`, message: `Anda yakin ingin menghapus ${selectedSchedules.length} jadwal yang dipilih?`, onConfirm: async () => { - await Promise.all(selectedSchedules.map(id => deleteJadwalShift(id))); - NotifOk({ icon: 'success', title: 'Berhasil', message: `${selectedSchedules.length} jadwal berhasil dihapus.` }); - fetchData(); - // Exit both edit modes - setIsEditMode(false); + await Promise.all(selectedSchedules.map((id) => deleteJadwalShift(id))); + NotifOk({ + icon: 'success', + title: 'Berhasil', + message: `${selectedSchedules.length} jadwal berhasil dihapus.`, + }); + doFilter(); setEditingShift(null); setSelectedSchedules([]); }, @@ -257,106 +354,75 @@ const ListJadwalShift = memo(function ListJadwalShift(props) { - - {isEditMode ? ( - - - Mode Edit Halaman - - - - - - - - ) : ( - <> - - + + { + const value = e.target.value; + setSearchValue(value); + if (value === '') { + handleSearchClear(); + } + }} + onSearch={handleSearch} + allowClear + enterButton={ + - - - - { - const value = e.target.value; - setSearchValue(value); - if (value === '') { - handleSearchClear(); - } - }} - onSearch={handleSearch} - allowClear - enterButton={ - + @@ -377,23 +447,26 @@ const ListJadwalShift = memo(function ListJadwalShift(props) { } onClick={() => handleShiftEditMode(shiftName)} - disabled={editingShift !== null || isEditMode} + disabled={editingShift !== null} > Edit @@ -410,81 +483,321 @@ const ListJadwalShift = memo(function ListJadwalShift(props) { )} - {/* Horizontal scrollable container for employee cards */} -
+
{groupedSchedules[shiftName].users.length > 0 ? ( - groupedSchedules[shiftName].users.map(user => ( + groupedSchedules[shiftName].users.map((user) => ( - {isEditMode && editingShift === null && ( // Checkbox for global delete mode only - handleSelectSchedule(user.schedule_id, e.target.checked)} - onClick={(e) => e.stopPropagation()} // Prevent card click - /> - )} - {editingShift === shiftName || (isEditMode && editingShift === null) ? ( // Global or Shift-specific Edit Mode + {editingShift === shiftName ? ( // EDIT MODE VIEW -
- - - - - handleSelectSchedule(user.schedule_id, e.target.checked)} - /> +
+
+ + handleSelectSchedule( + user.id, + e.target.checked + ) + } + style={{ + transform: 'scale(1.4)', + }} + /> +
+
+
+ + KARYAWAN + + +
+
+ + NO. TELEPON + + + emp.user_id === + pendingChanges[ + user + .id + ]?.user_id + )?.user_phone || + user.whatsapp + : user.whatsapp + } + readOnly + style={{ + backgroundColor: + '#f5f5f5', + color: '#595959', + cursor: 'not-allowed', + }} + size="large" + /> +
+
+ + SHIFT + + +
+
) : ( // NORMAL VIEW -
-
- {user.nama_employee} - {user.whatsapp} +
+
+ + {user.nama_employee} + + + {user.whatsapp} +
-
- - Terakhir diperbarui
- {formatRelativeTimestamp(groupedSchedules[shiftName].lastUpdate.timestamp)}
- oleh {groupedSchedules[shiftName].lastUpdate.user} +
+ + + Terakhir diperbarui + {' '} +
+ {formatRelativeTimestamp( + user.updated_at || + user.created_at || + new Date() + )}{' '} +
+ oleh {user.updated_by || 'N/A'}
-
@@ -492,14 +805,15 @@ const ListJadwalShift = memo(function ListJadwalShift(props) { )) ) : ( - Tidak ada karyawan yang dijadwalkan untuk shift ini. + + Tidak ada karyawan yang dijadwalkan untuk shift ini. + )}
)) - )} -
- + )} +
); diff --git a/src/pages/master/brandDevice/AddBrandDevice.jsx b/src/pages/master/brandDevice/AddBrandDevice.jsx index 0cf4a91..cec0645 100644 --- a/src/pages/master/brandDevice/AddBrandDevice.jsx +++ b/src/pages/master/brandDevice/AddBrandDevice.jsx @@ -36,6 +36,7 @@ const AddBrandDevice = () => { const [loading, setLoading] = useState(false); const [formData, setFormData] = useState(defaultData); const [errorCodes, setErrorCodes] = useState([]); + const [errorCodeIcon, setErrorCodeIcon] = useState(null); const { solutionFields, @@ -49,16 +50,29 @@ const AddBrandDevice = () => { handleSolutionStatusChange, resetSolutionFields, checkFirstSolutionValid, - setSolutionsForExistingRecord + setSolutionsForExistingRecord, } = useErrorCodeLogic(errorCodeForm, fileList); useEffect(() => { setBreadcrumbItems([ { title: • Master }, { - title: navigate('/master/brand-device')}>Brand Device + title: ( + navigate('/master/brand-device')} + > + Brand Device + + ), + }, + { + title: ( + + Tambah Brand Device + + ), }, - { title: Tambah Brand Device } ]); }, [setBreadcrumbItems, navigate]); @@ -71,25 +85,31 @@ const AddBrandDevice = () => { await brandForm.validateFields(); setCurrentStep(1); } catch (error) { - NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk brand device!' }); + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Harap isi semua kolom wajib untuk brand device!', + }); } }; const handleFinish = async () => { setConfirmLoading(true); try { - const transformedErrorCodes = errorCodes.map(ec => ({ + const transformedErrorCodes = errorCodes.map((ec) => ({ error_code: ec.error_code, error_code_name: ec.error_code_name || '', error_code_description: ec.error_code_description || '', + error_code_color: ec.error_code_color || '#000000', + path_icon: ec.path_icon || '', is_active: ec.status !== undefined ? ec.status : true, - solution: (ec.solution || []).map(sol => ({ + solution: (ec.solution || []).map((sol) => ({ solution_name: sol.solution_name, type_solution: sol.type_solution, text_solution: sol.text_solution || '', path_solution: sol.path_solution || '', - is_active: sol.is_active !== false - })) + is_active: sol.is_active !== false, + })), })); const finalFormData = { @@ -98,23 +118,28 @@ const AddBrandDevice = () => { brand_model: formData.brand_model || '', brand_manufacture: formData.brand_manufacture, is_active: formData.is_active, - error_code: transformedErrorCodes.length > 0 ? transformedErrorCodes : [ - { - error_code: "DEFAULT", - error_code_name: "Default Error Code", - error_code_description: "Default error description", - is_active: true, - solution: [ - { - solution_name: "Default Solution", - type_solution: "text", - text_solution: "Default solution text", - path_solution: "", - is_active: true - } - ] - } - ] + error_code: + transformedErrorCodes.length > 0 + ? transformedErrorCodes + : [ + { + error_code: 'DEFAULT', + error_code_name: 'Default Error Code', + error_code_description: 'Default error description', + error_code_color: '#000000', + path_icon: '', + is_active: true, + solution: [ + { + solution_name: 'Default Solution', + type_solution: 'text', + text_solution: 'Default solution text', + path_solution: '', + is_active: true, + }, + ], + }, + ], }; const response = await createBrand(finalFormData); @@ -135,9 +160,9 @@ const AddBrandDevice = () => { } } catch (error) { NotifAlert({ - icon: "error", - title: "Gagal", - message: error.message || "Gagal menyimpan data. Silakan coba lagi.", + icon: 'error', + title: 'Gagal', + message: error.message || 'Gagal menyimpan data. Silakan coba lagi.', }); } finally { setConfirmLoading(false); @@ -149,9 +174,11 @@ const AddBrandDevice = () => { error_code: record.error_code, error_code_name: record.error_code_name, error_code_description: record.error_code_description, + error_code_color: record.error_code_color, status: record.status, }); setFileList(record.fileList || []); + setErrorCodeIcon(record.errorCodeIcon || null); setIsErrorCodeFormReadOnly(true); setEditingErrorCodeKey(null); @@ -165,9 +192,11 @@ const AddBrandDevice = () => { error_code: record.error_code, error_code_name: record.error_code_name, error_code_description: record.error_code_description, + error_code_color: record.error_code_color, status: record.status, }); setFileList(record.fileList || []); + setErrorCodeIcon(record.errorCodeIcon || null); setIsErrorCodeFormReadOnly(false); setEditingErrorCodeKey(record.key); @@ -177,21 +206,29 @@ const AddBrandDevice = () => { }; const handleAddErrorCode = async (newErrorCode) => { + // Include the current icon in the error code + const errorCodeWithIcon = { + ...newErrorCode, + errorCodeIcon: errorCodeIcon + }; + if (editingErrorCodeKey) { - const updatedCodes = errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item); + const updatedCodes = errorCodes.map((item) => + item.key === editingErrorCodeKey ? errorCodeWithIcon : item + ); setErrorCodes(updatedCodes); NotifOk({ icon: 'success', title: 'Berhasil', - message: 'Error code berhasil diupdate!' + message: 'Error code berhasil diupdate!', }); } else { - const updatedCodes = [...errorCodes, newErrorCode]; + const updatedCodes = [...errorCodes, errorCodeWithIcon]; setErrorCodes(updatedCodes); NotifOk({ icon: 'success', title: 'Berhasil', - message: 'Error code berhasil ditambahkan!' + message: 'Error code berhasil ditambahkan!', }); } @@ -203,9 +240,10 @@ const AddBrandDevice = () => { errorCodeForm.setFieldsValue({ status: true, solution_status_0: true, - solution_type_0: 'text' + solution_type_0: 'text', }); setFileList([]); + setErrorCodeIcon(null); resetSolutionFields(); setIsErrorCodeFormReadOnly(false); setEditingErrorCodeKey(null); @@ -220,16 +258,16 @@ const AddBrandDevice = () => { NotifAlert({ icon: 'warning', title: 'Perhatian', - message: 'Setiap brand harus memiliki minimal 1 error code!' + message: 'Setiap brand harus memiliki minimal 1 error code!', }); return; } - setErrorCodes(errorCodes.filter(item => item.key !== key)); + setErrorCodes(errorCodes.filter((item) => item.key !== key)); NotifOk({ icon: 'success', title: 'Berhasil', - message: 'Error code berhasil dihapus!' + message: 'Error code berhasil dihapus!', }); }; @@ -248,12 +286,17 @@ const AddBrandDevice = () => { const handleSolutionFileUpload = async (file) => { try { - const isAllowedType = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'].includes(file.type); + const isAllowedType = [ + 'application/pdf', + 'image/jpeg', + 'image/png', + 'image/gif', + ].includes(file.type); if (!isAllowedType) { NotifAlert({ icon: 'error', title: 'Error', - message: `${file.name} bukan file PDF atau gambar yang diizinkan.` + message: `${file.name} bukan file PDF atau gambar yang diizinkan.`, }); return; } @@ -271,17 +314,17 @@ const AddBrandDevice = () => { file.solution_name = file.name; file.solutionId = solutionFields[0]; file.type_solution = fileType; - setFileList(prevList => [...prevList, file]); + setFileList((prevList) => [...prevList, file]); NotifOk({ icon: 'success', title: 'Berhasil', - message: `${file.name} berhasil diupload!` + message: `${file.name} berhasil diupload!`, }); } else { NotifAlert({ icon: 'error', title: 'Gagal', - message: `Gagal mengupload ${file.name}` + message: `Gagal mengupload ${file.name}`, }); } } catch (error) { @@ -289,23 +332,33 @@ const AddBrandDevice = () => { NotifAlert({ icon: 'error', title: 'Error', - message: `Gagal mengupload ${file.name}. Silakan coba lagi.` + message: `Gagal mengupload ${file.name}. Silakan coba lagi.`, }); } }; const handleFileRemove = (file) => { - const newFileList = fileList.filter(item => item.uid !== file.uid); + const newFileList = fileList.filter((item) => item.uid !== file.uid); setFileList(newFileList); }; + const handleErrorCodeIconUpload = (iconData) => { + setErrorCodeIcon(iconData); + }; + + const handleErrorCodeIconRemove = () => { + setErrorCodeIcon(null); + }; + const renderStepContent = () => { if (currentStep === 0) { return ( setFormData(prev => ({...prev, ...allValues}))} + onValuesChange={(changedValues, allValues) => + setFormData((prev) => ({ ...prev, ...allValues })) + } isEdit={false} /> ); @@ -314,17 +367,22 @@ const AddBrandDevice = () => { if (currentStep === 1) { return ( - + {isErrorCodeFormReadOnly ? 'View Error Code' - : (editingErrorCodeKey ? 'Edit Error Code' : 'Tambah Error Code') - } + : editingErrorCodeKey + ? 'Edit Error Code' + : 'Tambah Error Code'}
{ onCreateNewErrorCode={handleCreateNewErrorCode} onResetForm={resetErrorCodeForm} errorCodes={errorCodes} + errorCodeIcon={errorCodeIcon} + onErrorCodeIconUpload={handleErrorCodeIconUpload} + onErrorCodeIconRemove={handleErrorCodeIconRemove} /> - + { return ( - Tambah Brand Device + + Tambah Brand Device + -
- {renderStepContent()} -
+
{renderStepContent()}
{ ); }; -export default AddBrandDevice; \ No newline at end of file +export default AddBrandDevice; diff --git a/src/pages/master/brandDevice/EditBrandDevice.jsx b/src/pages/master/brandDevice/EditBrandDevice.jsx index 105193e..c379b98 100644 --- a/src/pages/master/brandDevice/EditBrandDevice.jsx +++ b/src/pages/master/brandDevice/EditBrandDevice.jsx @@ -4,6 +4,7 @@ import { Divider, Typography, Button, Steps, Form, Row, Col, Card, Spin, Modal } import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif'; import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; import { getBrandById, updateBrand } from '../../../api/master-brand'; +import { getFileUrl } from '../../../api/file-uploads'; import BrandForm from './component/BrandForm'; import ErrorCodeForm from './component/ErrorCodeForm'; import ErrorCodeTable from './component/ListErrorCode'; @@ -37,6 +38,7 @@ const EditBrandDevice = () => { const [loading, setLoading] = useState(true); const [formData, setFormData] = useState(defaultData); const [errorCodes, setErrorCodes] = useState([]); + const [errorCodeIcon, setErrorCodeIcon] = useState(null); const { solutionFields, @@ -50,7 +52,7 @@ const EditBrandDevice = () => { handleSolutionStatusChange, resetSolutionFields, checkFirstSolutionValid, - setSolutionsForExistingRecord + setSolutionsForExistingRecord, } = useErrorCodeLogic(errorCodeForm, fileList); useEffect(() => { @@ -61,7 +63,8 @@ const EditBrandDevice = () => { return; } - const savedPhase = location.state?.phase || localStorage.getItem(`brand_device_edit_${id}_last_phase`); + const savedPhase = + location.state?.phase || localStorage.getItem(`brand_device_edit_${id}_last_phase`); if (savedPhase) { setCurrentStep(parseInt(savedPhase)); localStorage.removeItem(`brand_device_edit_${id}_last_phase`); @@ -70,9 +73,22 @@ const EditBrandDevice = () => { setBreadcrumbItems([ { title: • Master }, { - title: navigate('/master/brand-device')}>Brand Device + title: ( + navigate('/master/brand-device')} + > + Brand Device + + ), + }, + { + title: ( + + Edit Brand Device + + ), }, - { title: Edit Brand Device } ]); try { @@ -90,20 +106,34 @@ const EditBrandDevice = () => { brand_code: brandData.brand_code, }; - const existingErrorCodes = brandData.error_code ? brandData.error_code.map((ec, index) => ({ - key: `existing-${ec.error_code_id}`, - error_code_id: ec.error_code_id, - error_code: ec.error_code, - error_code_name: ec.error_code_name || '', - error_code_description: ec.error_code_description || '', - status: ec.is_active, - solution: ec.solution || [] - })) : []; + const existingErrorCodes = brandData.error_code + ? brandData.error_code.map((ec, index) => ({ + key: `existing-${ec.error_code_id}`, + error_code_id: ec.error_code_id, + error_code: ec.error_code, + error_code_name: ec.error_code_name || '', + error_code_description: ec.error_code_description || '', + error_code_color: ec.error_code_color || '#000000', + path_icon: ec.path_icon || '', + status: ec.is_active, + solution: ec.solution || [], + errorCodeIcon: ec.path_icon ? { + name: 'icon', + uploadPath: ec.path_icon, + url: (() => { + const pathParts = ec.path_icon.split('/'); + const folder = pathParts[0]; + const filename = pathParts.slice(1).join('/'); + return getFileUrl(folder, filename); + })(), + type_solution: 'image' + } : null, + })) + : []; setFormData(newFormData); brandForm.setFieldsValue(newFormData); setErrorCodes(existingErrorCodes); - } else { NotifAlert({ icon: 'error', @@ -135,7 +165,11 @@ const EditBrandDevice = () => { await brandForm.validateFields(); setCurrentStep(1); } catch (error) { - NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk brand device!' }); + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Harap isi semua kolom wajib untuk brand device!', + }); } }; @@ -148,19 +182,21 @@ const EditBrandDevice = () => { brand_model: formData.brand_model || '', brand_manufacture: formData.brand_manufacture, is_active: formData.is_active, - error_code: errorCodes.map(ec => ({ + error_code: errorCodes.map((ec) => ({ error_code: ec.error_code, error_code_name: ec.error_code_name || '', error_code_description: ec.error_code_description || '', + error_code_color: ec.error_code_color || '#000000', + path_icon: ec.errorCodeIcon?.uploadPath || ec.path_icon || '', is_active: ec.status !== undefined ? ec.status : true, - solution: (ec.solution || []).map(sol => ({ + solution: (ec.solution || []).map((sol) => ({ solution_name: sol.solution_name, type_solution: sol.type_solution, text_solution: sol.text_solution || '', path_solution: sol.path_solution || '', - is_active: sol.is_active !== false - })) - })) + is_active: sol.is_active !== false, + })), + })), }; const response = await updateBrand(id, finalFormData); @@ -182,9 +218,9 @@ const EditBrandDevice = () => { } } catch (error) { NotifAlert({ - icon: "error", - title: "Gagal", - message: error.message || "Gagal mengupdate data. Silakan coba lagi.", + icon: 'error', + title: 'Gagal', + message: error.message || 'Gagal mengupdate data. Silakan coba lagi.', }); } finally { setConfirmLoading(false); @@ -196,8 +232,10 @@ const EditBrandDevice = () => { error_code: record.error_code, error_code_name: record.error_code_name, error_code_description: record.error_code_description, + error_code_color: record.error_code_color, status: record.status, }); + setErrorCodeIcon(record.errorCodeIcon || null); setIsErrorCodeFormReadOnly(true); setEditingErrorCodeKey(record.key); @@ -211,8 +249,10 @@ const EditBrandDevice = () => { error_code: record.error_code, error_code_name: record.error_code_name, error_code_description: record.error_code_description, + error_code_color: record.error_code_color, status: record.status, }); + setErrorCodeIcon(record.errorCodeIcon || null); setIsErrorCodeFormReadOnly(false); setEditingErrorCodeKey(record.key); @@ -226,22 +266,29 @@ const EditBrandDevice = () => { } }; - const handleAddErrorCode = (newErrorCode) => { + // Include the current icon in the error code + const errorCodeWithIcon = { + ...newErrorCode, + errorCodeIcon: errorCodeIcon + }; + let updatedErrorCodes; if (editingErrorCodeKey) { - updatedErrorCodes = errorCodes.map(item => item.key === editingErrorCodeKey ? newErrorCode : item); + updatedErrorCodes = errorCodes.map((item) => + item.key === editingErrorCodeKey ? errorCodeWithIcon : item + ); NotifOk({ icon: 'success', title: 'Berhasil', - message: 'Error code berhasil diupdate!' + message: 'Error code berhasil diupdate!', }); } else { - updatedErrorCodes = [...errorCodes, newErrorCode]; + updatedErrorCodes = [...errorCodes, errorCodeWithIcon]; NotifOk({ icon: 'success', title: 'Berhasil', - message: 'Error code berhasil ditambahkan!' + message: 'Error code berhasil ditambahkan!', }); } @@ -254,9 +301,10 @@ const EditBrandDevice = () => { errorCodeForm.setFieldsValue({ status: true, solution_status_0: true, - solution_type_0: 'text' + solution_type_0: 'text', }); setFileList([]); + setErrorCodeIcon(null); resetSolutionFields(); setIsErrorCodeFormReadOnly(false); setEditingErrorCodeKey(null); @@ -267,17 +315,17 @@ const EditBrandDevice = () => { NotifAlert({ icon: 'warning', title: 'Perhatian', - message: 'Setiap brand harus memiliki minimal 1 error code!' + message: 'Setiap brand harus memiliki minimal 1 error code!', }); return; } - const updatedErrorCodes = errorCodes.filter(item => item.key !== key); + const updatedErrorCodes = errorCodes.filter((item) => item.key !== key); setErrorCodes(updatedErrorCodes); NotifOk({ icon: 'success', title: 'Berhasil', - message: 'Error code berhasil dihapus!' + message: 'Error code berhasil dihapus!', }); }; @@ -285,6 +333,14 @@ const EditBrandDevice = () => { resetErrorCodeForm(); }; + const handleErrorCodeIconUpload = (iconData) => { + setErrorCodeIcon(iconData); + }; + + const handleErrorCodeIconRemove = () => { + setErrorCodeIcon(null); + }; + const handleFileView = (pathSolution, fileType) => { localStorage.setItem(`brand_device_edit_${id}_last_phase`, currentStep.toString()); @@ -297,7 +353,7 @@ const EditBrandDevice = () => { editingErrorCodeKey: editingErrorCodeKey, isErrorCodeFormReadOnly: isErrorCodeFormReadOnly, solutionsToDelete: Array.from(solutionsToDelete), - currentSolutionData: window.currentSolutionData || {} + currentSolutionData: window.currentSolutionData || {}, }; localStorage.setItem(`brand_device_edit_${id}_temp_data`, JSON.stringify(tempData)); @@ -314,11 +370,11 @@ const EditBrandDevice = () => { }; const handleSolutionFileUpload = (file) => { - setFileList(prevList => [...prevList, file]); + setFileList((prevList) => [...prevList, file]); }; const handleFileRemove = (file) => { - const newFileList = fileList.filter(item => item.uid !== file.uid); + const newFileList = fileList.filter((item) => item.uid !== file.uid); setFileList(newFileList); }; @@ -328,7 +384,9 @@ const EditBrandDevice = () => { setFormData(prev => ({...prev, ...allValues}))} + onValuesChange={(changedValues, allValues) => + setFormData((prev) => ({ ...prev, ...allValues })) + } isEdit={true} /> ); @@ -337,17 +395,24 @@ const EditBrandDevice = () => { if (currentStep === 1) { return ( - + {isErrorCodeFormReadOnly - ? (editingErrorCodeKey ? 'View Error Code' : 'Error Code Form') - : (editingErrorCodeKey ? 'Edit Error Code' : 'Tambah Error Code') - } + ? editingErrorCodeKey + ? 'View Error Code' + : 'Error Code Form' + : editingErrorCodeKey + ? 'Edit Error Code' + : 'Tambah Error Code'}
{ onCreateNewErrorCode={handleCreateNewErrorCode} onResetForm={resetErrorCodeForm} errorCodes={errorCodes} + errorCodeIcon={errorCodeIcon} + onErrorCodeIconUpload={handleErrorCodeIconUpload} + onErrorCodeIconRemove={handleErrorCodeIconRemove} /> - + ({ - key: `loading-${index}`, - error_code: 'Loading...', - error_code_name: 'Loading...', - solution: [] - })) : - errorCodes + errorCodes={ + loading + ? Array.from({ length: 3 }, (_, index) => ({ + key: `loading-${index}`, + error_code: 'Loading...', + error_code_name: 'Loading...', + solution: [], + })) + : errorCodes } loading={loading} onPreview={handlePreviewErrorCode} @@ -399,32 +468,41 @@ const EditBrandDevice = () => { return ( - Edit Brand Device + + Edit Brand Device +
{loading && ( -
+
)} -
+
{renderStepContent()}
@@ -442,4 +520,4 @@ const EditBrandDevice = () => { ); }; -export default EditBrandDevice; \ No newline at end of file +export default EditBrandDevice; diff --git a/src/pages/master/brandDevice/ViewBrandDevice.jsx b/src/pages/master/brandDevice/ViewBrandDevice.jsx index e8b369d..9e5096d 100644 --- a/src/pages/master/brandDevice/ViewBrandDevice.jsx +++ b/src/pages/master/brandDevice/ViewBrandDevice.jsx @@ -1,11 +1,10 @@ -import React, { memo, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useNavigate, useParams, useLocation } from 'react-router-dom'; -import { Typography, Card, Row, Col, Tag, Button, Space, Descriptions, Divider, Table, Steps, Collapse, Switch, Skeleton, Spin, Modal } from 'antd'; -import { ArrowLeftOutlined, EditOutlined, DeleteOutlined, FileTextOutlined, FilePdfOutlined, EyeOutlined } from '@ant-design/icons'; +import { Typography, Card, Row, Col, Tag, Button, Space, Descriptions, Divider, Steps, Collapse, Switch, Spin, Modal, Empty } from 'antd'; +import { ArrowLeftOutlined, FileTextOutlined, FilePdfOutlined, EyeOutlined } from '@ant-design/icons'; import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; -import { NotifConfirmDialog, NotifOk, NotifAlert } from '../../../components/Global/ToastNotif'; -import { getBrandById, deleteBrand } from '../../../api/master-brand'; -import TableList from '../../../components/Global/TableList'; +import { NotifOk, NotifAlert } from '../../../components/Global/ToastNotif'; +import { getBrandById } from '../../../api/master-brand'; const { Title, Text } = Typography; const { Step } = Steps; @@ -19,9 +18,8 @@ const ViewBrandDevice = () => { const [brandData, setBrandData] = useState(null); const [loading, setLoading] = useState(true); const [currentStep, setCurrentStep] = useState(0); - const [errorCodesTriger, setErrorCodesTriger] = useState(0); + const [activeErrorKeys, setActiveErrorKeys] = useState([]); - useEffect(() => { const fetchBrandData = async () => { const token = localStorage.getItem('token'); @@ -44,10 +42,8 @@ const ViewBrandDevice = () => { setLoading(true); const response = await getBrandById(id); - if (response && response.statusCode === 200) { setBrandData(response.data); - setErrorCodesTriger(prev => prev + 1); } else { NotifAlert({ icon: 'error', @@ -73,264 +69,21 @@ const ViewBrandDevice = () => { fetchBrandData(); }, [id, setBreadcrumbItems, navigate, location.state]); - // const handleEdit = () => { - // navigate(`/master/brand-device/edit/${id}`); - // }; - - // const handleDelete = () => { - // NotifConfirmDialog({ - // icon: 'question', - // title: 'Konfirmasi Hapus', - // message: `Brand Device "${brandData?.brand_name}" akan dihapus?`, - // onConfirm: async () => { - // try { - // const response = await deleteBrand(id); - - // if (response && response.statusCode === 200) { - // NotifOk({ - // icon: 'success', - // title: 'Berhasil', - // message: response.message || 'Brand Device berhasil dihapus.', - // }); - // navigate('/master/brand-device'); - // } else { - // NotifAlert({ - // icon: 'error', - // title: 'Gagal', - // message: response?.message || 'Gagal menghapus Brand Device', - // }); - // } - // } catch (error) { - // console.error('Delete Brand Device Error:', error); - // NotifAlert({ - // icon: 'error', - // title: 'Error', - // message: error.message || 'Gagal menghapus Brand Device', - // }); - // } - // }, - // onCancel: () => {}, - // }); - // }; - - // Fungsi untuk membuka file viewer di halaman baru const handleFileView = (fileName, fileType) => { - console.log('handleFileView called with:', { fileName, fileType }); - - // Save current phase before navigating to file viewer localStorage.setItem(`brand_device_${id}_last_phase`, currentStep.toString()); - // Extract only the filename without folder prefix let actualFileName = fileName; if (fileName && fileName.includes('/')) { const parts = fileName.split('/'); - actualFileName = parts[parts.length - 1]; // Get the last part (actual filename) + actualFileName = parts[parts.length - 1]; } - console.log('Processed filename:', { original: fileName, actual: actualFileName }); - const encodedFileName = encodeURIComponent(actualFileName); const fileTypeParam = fileType === 'image' ? 'image' : 'pdf'; const navigationPath = `/master/brand-device/view/${id}/files/${fileTypeParam}/${encodedFileName}`; - - console.log('Navigating to:', navigationPath); navigate(navigationPath); }; - - // if (loading) { - // return ( - //
- // - //
- // ); - // } - - if (!brandData && !loading) { - return
Brand Device not found
; - } - - // Error code table columns configuration - const errorCodeColumns = [ - { - title: 'No', - key: 'no', - width: '5%', - align: 'center', - render: (_, __, index) => index + 1, - }, - { - title: 'Error Code', - dataIndex: 'error_code', - key: 'error_code', - width: '15%', - render: (text) => text || '-', - }, - { - title: 'Error Code Name', - dataIndex: 'error_code_name', - key: 'error_code_name', - width: '20%', - render: (text) => text || '-', - }, - { - title: 'Description', - dataIndex: 'error_code_description', - key: 'error_code_description', - width: '25%', - render: (text) => text || '-', - }, - { - title: 'Solutions', - dataIndex: 'solution', - key: 'solution', - width: '20%', - render: (solutions) => ( -
- {solutions && solutions.length > 0 ? ( -
- {solutions.length} solution(s) -
- {solutions.slice(0, 2).map((sol, index) => ( -
- {sol.type_solution === 'text' ? ( - • {sol.solution_name} - ) : ( - • {sol.solution_name} ({sol.type_solution}) - )} -
- ))} - {solutions.length > 2 && ( -
- ...and {solutions.length - 2} more -
- )} -
-
- ) : ( - No solutions - )} -
- ) - }, - { - title: 'Status', - dataIndex: 'is_active', - key: 'is_active', - width: '10%', - align: 'center', - render: (_, { is_active }) => ( - - {is_active ? 'Active' : 'Inactive'} - - ), - }, - { - title: 'Action', - key: 'action', - align: 'center', - width: '5%', - render: (_, record) => ( - - )} - - )} -
- - - ))} - - ) : ( - No solutions available - )} -
- ), - }); - }} - style={{ color: '#1890ff' }} - /> - ), - }, - ]; - - // Mock data function for error codes - const getErrorCodesData = async () => { - const errorCodes = brandData?.error_code || []; - return { - data: errorCodes, - paging: { - current_page: 1, - current_limit: 10, - total_limit: errorCodes.length, - total_page: 1, - } - }; - }; - const renderStepContent = () => { if (currentStep === 0) { return ( @@ -444,26 +197,140 @@ const ViewBrandDevice = () => { } if (currentStep === 1) { - const errorCodesCount = loading ? 3 : (brandData?.error_code?.length || 0); + const errorCodes = brandData?.error_code || []; return (
- Error Codes ({errorCodesCount}) + Error Codes ({errorCodes.length}) - {errorCodesCount > 0 ? ( - + + {errorCodes.length > 0 ? ( + + {errorCodes.map((errorCode, index) => ( + +
+ {errorCode.error_code} + + - {errorCode.error_code_name} + +
+
+ + {errorCode.is_active ? 'Active' : 'Inactive'} + + + {errorCode.solution?.length || 0} solution(s) + +
+
+ } + > +
+
+ Description: +
+ {errorCode.error_code_description || 'No description'} +
+
+ +
+ Solutions: + {errorCode.solution && errorCode.solution.length > 0 ? ( +
+ {errorCode.solution.map((solution) => ( + + + + + {solution.type_solution === 'pdf' ? ( + + ) : solution.type_solution === 'image' ? ( + + ) : ( + + )} + {solution.solution_name} + + + + + {solution.type_solution ? solution.type_solution.toUpperCase() : 'TEXT'} + + + + +
+ {solution.type_solution === 'text' ? ( + + {solution.text_solution} + + ) : ( +
+ + File: {solution.path_document || solution.path_solution || 'Document'} + + {(solution.path_document || solution.path_solution) && ( + + )} +
+ )} +
+
+ ))} +
+ ) : ( +
+ No solutions available +
+ )} +
+
+ + ))} + ) : ( - !loading && No error codes available + !loading && ( + No error codes available + } + /> + ) )}
); @@ -483,7 +350,7 @@ const ViewBrandDevice = () => { @@ -496,9 +363,7 @@ const ViewBrandDevice = () => { - {/* Content area with blur overlay during loading */}
- {/* Overlay with blur effect during loading - only on content area */} {loading && (
{ )}
- + ); }; diff --git a/src/pages/master/brandDevice/ViewFilePage.jsx b/src/pages/master/brandDevice/ViewFilePage.jsx index f6c5b03..4476450 100644 --- a/src/pages/master/brandDevice/ViewFilePage.jsx +++ b/src/pages/master/brandDevice/ViewFilePage.jsx @@ -94,13 +94,15 @@ const ViewFilePage = () => { setPdfLoading(true); const folder = getFolderFromFileType('pdf'); try { - const response = await getFile(folder, decodedFileName); - const blobUrl = window.URL.createObjectURL(response.data); + const blobData = await getFile(folder, decodedFileName); + console.log('PDF blob data received:', blobData); + const blobUrl = window.URL.createObjectURL(blobData); setPdfBlobUrl(blobUrl); console.log('PDF blob URL created successfully:', blobUrl); } catch (pdfError) { console.error('Error loading PDF:', pdfError); - setError('Failed to load PDF file'); + setError('Failed to load PDF file: ' + (pdfError.message || pdfError)); + setPdfBlobUrl(null); } finally { setPdfLoading(false); } @@ -194,7 +196,7 @@ const ViewFilePage = () => { const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension); const isPdf = fileExtension === 'pdf'; - const fileUrl = loading ? null : getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName); + // const fileUrl = loading ? null : getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName); // Show placeholder when loading if (loading) { @@ -260,7 +262,7 @@ const ViewFilePage = () => { return (
{actualFileName} { } if (isPdf) { - const displayUrl = pdfBlobUrl || fileUrl; + const displayUrl = pdfBlobUrl || getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName); return (
@@ -342,13 +344,15 @@ const ViewFilePage = () => { setPdfLoading(true); const folder = getFolderFromFileType('pdf'); getFile(folder, actualFileName) - .then(response => { - const blobUrl = window.URL.createObjectURL(response.data); + .then(blobData => { + console.log('Retry PDF blob data:', blobData); + const blobUrl = window.URL.createObjectURL(blobData); setPdfBlobUrl(blobUrl); }) .catch(error => { console.error('Error retrying PDF load:', error); - setError('Failed to load PDF file'); + setError('Failed to load PDF file: ' + (error.message || error)); + setPdfBlobUrl(null); }) .finally(() => { setPdfLoading(false); @@ -370,7 +374,7 @@ const ViewFilePage = () => {
Preview tidak tersedia untuk jenis file ini
{actualFileName}
-
diff --git a/src/pages/master/brandDevice/component/ErrorCodeForm.jsx b/src/pages/master/brandDevice/component/ErrorCodeForm.jsx index c86bf7a..fbfaff2 100644 --- a/src/pages/master/brandDevice/component/ErrorCodeForm.jsx +++ b/src/pages/master/brandDevice/component/ErrorCodeForm.jsx @@ -1,7 +1,18 @@ -import { Form, Divider, Button, Switch, Input, ConfigProvider, Typography } from 'antd'; -import { PlusOutlined } from '@ant-design/icons'; +import { + Form, + Divider, + Button, + Switch, + Input, + ConfigProvider, + Typography, + Upload, + message, +} from 'antd'; +import { PlusOutlined, UploadOutlined, DeleteOutlined } from '@ant-design/icons'; import { NotifAlert } from '../../../../components/Global/ToastNotif'; import SolutionField from './SolutionField'; +import { uploadFile, getFileUrl } from '../../../../api/file-uploads'; const { Text } = Typography; @@ -24,10 +35,70 @@ const ErrorCodeForm = ({ onFileView, onCreateNewErrorCode, onResetForm, - errorCodes + errorCodes, + errorCodeIcon, + onErrorCodeIconUpload, + onErrorCodeIconRemove, }) => { const statusValue = Form.useWatch('status', errorCodeForm); + const handleIconUpload = async (file) => { + // Check if file is an image + const isImage = file.type.startsWith('image/'); + if (!isImage) { + message.error('You can only upload image files!'); + return Upload.LIST_IGNORE; + } + + // Check file size (max 2MB) + const isLt2M = file.size / 1024 / 1024 < 2; + if (!isLt2M) { + message.error('Image must be smaller than 2MB!'); + return Upload.LIST_IGNORE; + } + + try { + const fileExtension = file.name.split('.').pop().toLowerCase(); + const isImageFile = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes( + fileExtension + ); + const fileType = isImageFile ? 'image' : 'pdf'; + const folder = 'images'; + + const uploadResponse = await uploadFile(file, folder); + const iconPath = + uploadResponse.data?.path_icon || uploadResponse.data?.path_solution || ''; + + if (iconPath) { + // Extract folder and filename from the path + const pathParts = iconPath.split('/'); + const folder = pathParts[0]; + const filename = pathParts.slice(1).join('/'); + + onErrorCodeIconUpload({ + name: file.name, + uploadPath: iconPath, + url: getFileUrl(folder, filename), // Use the same endpoint as file uploads + type_solution: fileType, + solutionId: 'icon', + }); + message.success(`${file.name} uploaded successfully!`); + } else { + message.error('Failed to upload icon'); + } + } catch (error) { + console.error('Error uploading icon:', error); + message.error('Failed to upload icon'); + } + + return false; // Prevent default upload behavior + }; + + const handleRemoveIcon = () => { + onErrorCodeIconRemove(); + message.success('Icon removed'); + }; + const handleAddErrorCode = async () => { try { const values = await errorCodeForm.validateFields(); @@ -41,7 +112,7 @@ const ErrorCodeForm = ({ const solutionName = values[`solution_name_${fieldId}`]; const textSolution = values[`text_solution_${fieldId}`]; - const filesForSolution = fileList.filter(file => file.solutionId === fieldId); + const filesForSolution = fileList.filter((file) => file.solutionId === fieldId); const solutionType = values[`solution_type_${fieldId}`] || solutionTypes[fieldId]; if (solutionType === 'text') { @@ -51,11 +122,12 @@ const ErrorCodeForm = ({ type_solution: 'text', text_solution: textSolution.trim(), path_solution: '', - is_active: solutionStatuses[fieldId] !== false + is_active: solutionStatuses[fieldId] !== false, }; if (window.currentSolutionData && window.currentSolutionData[fieldId]) { - solutionData.brand_code_solution_id = window.currentSolutionData[fieldId].brand_code_solution_id; + solutionData.brand_code_solution_id = + window.currentSolutionData[fieldId].brand_code_solution_id; } solutions.push(solutionData); @@ -63,15 +135,22 @@ const ErrorCodeForm = ({ } else if (solutionType === 'file') { filesForSolution.forEach((file) => { const solutionData = { - solution_name: solutionName || file.solution_name || file.name || `Solution ${fieldId}`, - type_solution: file.type_solution || (file.type.startsWith('image/') ? 'image' : 'pdf'), + solution_name: + solutionName || + file.solution_name || + file.name || + `Solution ${fieldId}`, + type_solution: + file.type_solution || + (file.type.startsWith('image/') ? 'image' : 'pdf'), text_solution: '', path_solution: file.uploadPath, - is_active: solutionStatuses[fieldId] !== false + is_active: solutionStatuses[fieldId] !== false, }; if (window.currentSolutionData && window.currentSolutionData[fieldId]) { - solutionData.brand_code_solution_id = window.currentSolutionData[fieldId].brand_code_solution_id; + solutionData.brand_code_solution_id = + window.currentSolutionData[fieldId].brand_code_solution_id; } solutions.push(solutionData); @@ -83,7 +162,8 @@ const ErrorCodeForm = ({ NotifAlert({ icon: 'warning', title: 'Perhatian', - message: 'Setiap error code harus memiliki minimal 1 solution (text atau file)!' + message: + 'Setiap error code harus memiliki minimal 1 solution (text atau file)!', }); return; } @@ -92,15 +172,20 @@ const ErrorCodeForm = ({ error_code: values.error_code, error_code_name: values.error_code_name, error_code_description: values.error_code_description, + error_code_color: values.error_code_color || '#000000', + path_icon: errorCodeIcon?.uploadPath || '', status: values.status === undefined ? true : values.status, solution: solutions, - key: editingErrorCodeKey || `temp-${Date.now()}` + key: editingErrorCodeKey || `temp-${Date.now()}`, }; onAddErrorCode(newErrorCode); - } catch (error) { - NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib (error code + minimal 1 solution)!' }); + NotifAlert({ + icon: 'warning', + title: 'Perhatian', + message: 'Harap isi semua kolom wajib (error code + minimal 1 solution)!', + }); } }; @@ -109,14 +194,21 @@ const ErrorCodeForm = ({ errorCodeForm.setFieldsValue({ status: true, solution_status_0: true, - solution_type_0: 'text' + solution_type_0: 'text', }); onResetForm(); }; return ( <> -
+
@@ -143,25 +235,110 @@ const ErrorCodeForm = ({ }, }} > - )}
- + - + - + +
+
+ Icon: + {errorCodeIcon ? ( +
+ Error Code Icon +
+
+ {errorCodeIcon.name.length > 15 + ? errorCodeIcon.name.substring(0, 15) + '...' + : errorCodeIcon.name} +
+ {!isErrorCodeFormReadOnly && ( + + )} +
+
+ ) : ( + + + + )} +
+
+ Color: + + + +
+
+
+ Choose color and upload icon (max 2MB, JPG/PNG/GIF) +
+
+ + @@ -175,7 +352,7 @@ const ErrorCodeForm = ({ solutionType={solutionTypes[fieldId]} solutionStatus={solutionStatuses[fieldId]} isReadOnly={isErrorCodeFormReadOnly} - fileList={fileList.filter(file => file.solutionId === fieldId)} + fileList={fileList.filter((file) => file.solutionId === fieldId)} onRemove={() => onRemoveSolutionField(fieldId)} onSolutionTypeChange={(type) => onSolutionTypeChange(fieldId, type)} onSolutionStatusChange={(status) => onSolutionStatusChange(fieldId, status)} @@ -200,21 +377,17 @@ const ErrorCodeForm = ({ {!isErrorCodeFormReadOnly && editingErrorCodeKey && ( - + )} {isErrorCodeFormReadOnly && editingErrorCodeKey && ( - + )} ); }; -export default ErrorCodeForm; \ No newline at end of file +export default ErrorCodeForm; diff --git a/src/pages/master/brandDevice/component/SolutionField.jsx b/src/pages/master/brandDevice/component/SolutionField.jsx index c0692e9..7df25d7 100644 --- a/src/pages/master/brandDevice/component/SolutionField.jsx +++ b/src/pages/master/brandDevice/component/SolutionField.jsx @@ -18,36 +18,53 @@ const SolutionField = ({ onFileUpload, currentSolutionData, onFileView, - errorCodeForm + errorCodeForm, }) => { useEffect(() => { if (currentSolutionData && errorCodeForm) { if (currentSolutionData.solution_name) { - errorCodeForm.setFieldValue(`solution_name_${fieldId}`, currentSolutionData.solution_name); + errorCodeForm.setFieldValue( + `solution_name_${fieldId}`, + currentSolutionData.solution_name + ); } if (currentSolutionData.type_solution === 'text' && currentSolutionData.text_solution) { - errorCodeForm.setFieldValue(`text_solution_${fieldId}`, currentSolutionData.text_solution); + errorCodeForm.setFieldValue( + `text_solution_${fieldId}`, + currentSolutionData.text_solution + ); } if (currentSolutionData.type_solution) { - const formValue = currentSolutionData.type_solution === 'image' || currentSolutionData.type_solution === 'pdf' ? 'file' : currentSolutionData.type_solution; + const formValue = + currentSolutionData.type_solution === 'image' || + currentSolutionData.type_solution === 'pdf' + ? 'file' + : currentSolutionData.type_solution; errorCodeForm.setFieldValue(`solution_type_${fieldId}`, formValue); } - if (currentSolutionData.is_active !== undefined) { - errorCodeForm.setFieldValue(`solution_status_${fieldId}`, currentSolutionData.is_active); + // Only set status if it's not already set to prevent overwriting user changes + const currentStatus = errorCodeForm.getFieldValue(`solution_status_${fieldId}`); + if (currentSolutionData.is_active !== undefined && currentStatus === undefined) { + errorCodeForm.setFieldValue( + `solution_status_${fieldId}`, + currentSolutionData.is_active + ); } } }, [currentSolutionData, fieldId, errorCodeForm]); const handleBeforeUpload = async (file) => { - const isAllowedType = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'].includes(file.type); + const isAllowedType = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'].includes( + file.type + ); if (!isAllowedType) { NotifAlert({ icon: 'error', title: 'Error', - message: `${file.name} bukan file PDF atau gambar yang diizinkan.` + message: `${file.name} bukan file PDF atau gambar yang diizinkan.`, }); return Upload.LIST_IGNORE; } @@ -72,13 +89,13 @@ const SolutionField = ({ NotifOk({ icon: 'success', title: 'Berhasil', - message: `${file.name} berhasil diupload!` + message: `${file.name} berhasil diupload!`, }); } else { NotifAlert({ icon: 'error', title: 'Gagal', - message: `Gagal mengupload ${file.name}` + message: `Gagal mengupload ${file.name}`, }); } } catch (error) { @@ -86,7 +103,7 @@ const SolutionField = ({ NotifAlert({ icon: 'error', title: 'Error', - message: `Gagal mengupload ${file.name}. Silakan coba lagi.` + message: `Gagal mengupload ${file.name}. Silakan coba lagi.`, }); } @@ -101,10 +118,17 @@ const SolutionField = ({ padding: 16, border: '1px solid #d9d9d9', borderRadius: 8, - transition: 'all 0.3s ease' + transition: 'all 0.3s ease', }} > -
+
Solution {index + 1} -
- ); - } - return null; - })()} + + - )} + ) : ( + <> + {/* Show existing file info for both preview and edit mode */} + {currentSolutionData && + currentSolutionData.type_solution !== 'text' && + currentSolutionData.path_solution && ( + + {(() => { + const solution = currentSolutionData; + const fileName = + solution.file_upload_name || + solution.path_solution?.split('/')[1] || + 'File'; + const fileType = solution.type_solution; - - file.solutionId === fieldId), - // Add existing file to fileList if it exists - ...(currentSolutionData && currentSolutionData.type_solution !== 'text' && currentSolutionData.path_solution ? [{ - uid: `existing-${fieldId}`, - name: currentSolutionData.file_upload_name || currentSolutionData.path_solution?.split('/')[1] || 'File', - status: 'done', - url: null, // We'll use the path_solution for viewing - solutionId: fieldId, - type_solution: currentSolutionData.type_solution, - uploadPath: currentSolutionData.path_solution, - existingFile: true - }] : []) - ]} - onRemove={(file) => { - }} - beforeUpload={handleBeforeUpload} - > - - - - - ); + if (fileType !== 'text' && solution.path_solution) { + return ( +
+ + {fileType === 'image' + ? '[Image]' + : '[Document]'}{' '} + {fileName} + + +
+ ); + } + return null; + })()} +
+ )} + + + file.solutionId === fieldId), + // Add existing file to fileList if it exists + ...(currentSolutionData && + currentSolutionData.type_solution !== 'text' && + currentSolutionData.path_solution + ? [ + { + uid: `existing-${fieldId}`, + name: + currentSolutionData.file_upload_name || + currentSolutionData.path_solution?.split( + '/' + )[1] || + 'File', + status: 'done', + url: null, // We'll use the path_solution for viewing + solutionId: fieldId, + type_solution: + currentSolutionData.type_solution, + uploadPath: currentSolutionData.path_solution, + existingFile: true, + }, + ] + : []), + ]} + onRemove={(file) => {}} + beforeUpload={handleBeforeUpload} + > + + + + + ); }}
); }; -export default SolutionField; \ No newline at end of file +export default SolutionField; diff --git a/src/pages/master/brandDevice/hooks/errorCode.js b/src/pages/master/brandDevice/hooks/errorCode.js index 99ce5ec..f1d3e9f 100644 --- a/src/pages/master/brandDevice/hooks/errorCode.js +++ b/src/pages/master/brandDevice/hooks/errorCode.js @@ -194,6 +194,9 @@ export const useErrorCodeLogic = (errorCodeForm, fileList) => { }; const handleSolutionStatusChange = (fieldId, status) => { + // Update form immediately + errorCodeForm.setFieldValue(`solution_status_${fieldId}`, status); + // Then update local state setSolutionStatuses(prev => ({ ...prev, [fieldId]: status diff --git a/src/pages/master/unit/component/DetailUnit.jsx b/src/pages/master/unit/component/DetailUnit.jsx index f3e56cb..0022959 100644 --- a/src/pages/master/unit/component/DetailUnit.jsx +++ b/src/pages/master/unit/component/DetailUnit.jsx @@ -181,7 +181,7 @@ const DetailUnit = (props) => {