lavoce #3

Merged
bragaz_rexita merged 14 commits from lavoce into main 2025-10-22 05:59:58 +00:00
53 changed files with 3444 additions and 3192 deletions

View File

@@ -1,4 +1,5 @@
VITE_API_SERVER=http://36.66.16.49:9528/api
VITE_API_SERVER=http://localhost:9530/api
# VITE_API_SERVER=https://117.102.231.130:9528/api
VITE_MQTT_SERVER=ws://localhost:1884
VITE_MQTT_USERNAME=
VITE_MQTT_PASSWORD=

View File

@@ -14,6 +14,7 @@ import IndexDevice from './pages/master/device/IndexDevice';
import IndexTag from './pages/master/tag/IndexTag';
import IndexUnit from './pages/master/unit/IndexUnit';
import IndexBrandDevice from './pages/master/brandDevice/IndexBrandDevice';
import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice';
import IndexPlantSection from './pages/master/plantSection/IndexPlantSection';
import IndexStatus from './pages/master/status/IndexStatus';
import IndexShift from './pages/master/shift/IndexShift';
@@ -35,6 +36,13 @@ import IndexUser from './pages/user/IndexUser';
import IndexMember from './pages/shiftManagement/member/IndexMember';
import SvgTest from './pages/home/SvgTest';
import SvgOverview from './pages/home/SvgOverview';
import SvgCompressorA from './pages/home/SvgCompressorA';
import SvgCompressorB from './pages/home/SvgCompressorB';
import SvgCompressorC from './pages/home/SvgCompressorC';
import SvgAirDryerA from './pages/home/SvgAirDryerA';
import SvgAirDryerB from './pages/home/SvgAirDryerB';
import SvgAirDryerC from './pages/home/SvgAirDryerC';
const App = () => {
return (
@@ -52,11 +60,22 @@ const App = () => {
<Route path="blank" element={<Blank />} />
</Route>
<Route path="/dashboard-svg" element={<ProtectedRoute />}>
<Route path="overview" element={<SvgOverview />} />
<Route path="compressor-a" element={<SvgCompressorA />} />
<Route path="compressor-b" element={<SvgCompressorB />} />
<Route path="compressor-c" element={<SvgCompressorC />} />
<Route path="airdryer-a" element={<SvgAirDryerA />} />
<Route path="airdryer-b" element={<SvgAirDryerB />} />
<Route path="airdryer-c" element={<SvgAirDryerC />} />
</Route>
<Route path="/master" element={<ProtectedRoute />}>
<Route path="device" element={<IndexDevice />} />
<Route path="tag" element={<IndexTag />} />
<Route path="unit" element={<IndexUnit />} />
<Route path="brand-device" element={<IndexBrandDevice />} />
<Route path="brand-device/add" element={<AddBrandDevice />} />
<Route path="plant-section" element={<IndexPlantSection />} />
<Route path="shift" element={<IndexShift />} />
<Route path="status" element={<IndexStatus />} />

71
src/Utils/validate.js Normal file
View File

@@ -0,0 +1,71 @@
// utils/validate.js
// Daftar aturan validasi
const validationRules = [
{ field: 'name', label: 'Nama', required: true },
{
field: 'email',
label: 'Email',
required: true,
pattern: /\S+@\S+\.\S+/,
patternMessage: 'Format email tidak valid',
},
{
field: 'age',
label: 'Umur',
required: true,
validator: (v) => !isNaN(v) && Number(v) >= 18,
message: 'Umur harus angka dan minimal 18 tahun',
},
];
/**
* Fungsi validasi dinamis berbasis array objek aturan.
* @param {Object} data - data form (misal { name: '', email: '' })
* @param {Array} rules - array aturan validasi
* @returns {Object} errors - object berisi pesan error per field
*/
export const validateRun = (data, rules, onError) => {
const errors = {};
const messages = [];
const ipRegex = /^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$/;
rules.forEach((rule) => {
const value = data[rule.field]?.toString().trim();
const fieldErrors = [];
if (rule.required && !value) {
fieldErrors.push(`${rule.label} wajib diisi`);
}
// ✅ IP Address check
if (rule.ip && value && !ipRegex.test(value)) {
fieldErrors.push(`${rule.label} harus berupa alamat IP yang valid`);
}
if (rule.pattern && value && !rule.pattern.test(value)) {
fieldErrors.push(rule.patternMessage || `${rule.label} tidak valid`);
}
if (rule.validator && value && !rule.validator(value)) {
fieldErrors.push(rule.message || `${rule.label} tidak valid`);
}
// Gabungkan error satu field jadi satu string (pisah baris)
if (fieldErrors.length > 0) {
errors[rule.field] = fieldErrors.join("\n");
messages.push(...fieldErrors);
}
});
// Jika ada error total, tampilkan callback dan return false
if (messages.length > 0) {
if (onError) onError(messages.join("\n"));
return true;
}
return false
};

View File

@@ -1,48 +0,0 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getTotal = async (query = '') => {
const prefix = `${query ? `?${query}` : ''}`;
const fullUrl = `${import.meta.env.VITE_API_SERVER}/dashboard/${prefix}`;
try {
const response = await SendRequest({
method: 'get',
prefix: `dashboard/${prefix}`,
});
return response;
} catch (error) {
console.error(`[API Call] Failed: GET ${fullUrl}`, error);
throw error;
}
};
const getTotalPermit = async (query = '') => {
const prefix = `dashboard/permit-total${query ? `?${query}` : ''}`;
const fullUrl = `${import.meta.env.VITE_API_SERVER}/${prefix}`;
try {
const response = await SendRequest({
method: 'get',
prefix,
});
return response;
} catch (error) {
console.error(`[API Call] Failed: GET ${fullUrl}`, error);
throw error;
}
};
const getTotalPermitPerYear = async (query = '') => {
const prefix = `dashboard/permit-breakdown${query ? `?${query}` : ''}`;
const fullUrl = `${import.meta.env.VITE_API_SERVER}/${prefix}`;
try {
const response = await SendRequest({
method: 'get',
prefix,
});
return response;
} catch (error) {
console.error(`[API Call] Failed: GET ${fullUrl}`, error);
throw error;
}
};
export { getTotal, getTotalPermit, getTotalPermitPerYear };

View File

@@ -1,55 +1,41 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getAllJadwalShift = async (queryParams) => {
try {
const response = await SendRequest({
method: 'get',
prefix: `jadwal-shift?${queryParams.toString()}`,
});
return response;
} catch (error) {
console.error('getAllJadwalShift error:', error);
return {
status: 500,
data: {
data: [],
paging: {
page: 1,
limit: 10,
total: 0,
page_total: 0
},
total: 0
},
error: error.message
};
}
const response = await SendRequest({
method: 'get',
prefix: `jadwal-shift?${queryParams.toString()}`,
});
return response.data;
};
const getJadwalShiftById = async (id) => {
const response = await SendRequest({
method: 'get',
prefix: `jadwal-shift/${id}`,
});
return response.data;
};
const createJadwalShift = async (queryParams) => {
const response = await SendRequest({
method: 'post',
prefix: `jadwal-shift`,
data: queryParams,
params: queryParams,
});
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message
};
return response.data;
};
const updateJadwalShift = async (id, queryParams) => {
const response = await SendRequest({
method: 'put',
prefix: `jadwal-shift/${id}`,
data: queryParams,
params: queryParams,
});
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message
};
return response.data;
};
const deleteJadwalShift = async (id) => {
@@ -57,11 +43,13 @@ const deleteJadwalShift = async (id) => {
method: 'delete',
prefix: `jadwal-shift/${id}`,
});
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message
};
return response.data;
};
export { getAllJadwalShift, createJadwalShift, updateJadwalShift, deleteJadwalShift };
export {
getAllJadwalShift,
getJadwalShiftById,
createJadwalShift,
updateJadwalShift,
deleteJadwalShift,
};

View File

@@ -1,45 +1,50 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getAllApd = async (queryParams) => {
const getAllBrands = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `apd?${queryParams.toString()}`,
prefix: `brand?${queryParams.toString()}`,
});
return response;
return response.data;
};
const createApd = async (queryParams) => {
const getBrandById = async (id) => {
const response = await SendRequest({
method: 'get',
prefix: `brand/${id}`,
});
return response.data;
};
const createBrand = async (queryParams) => {
const response = await SendRequest({
method: 'post',
prefix: `apd`,
prefix: `brand`,
params: queryParams,
});
return response.data;
};
const updateApd = async (vendor_id, queryParams) => {
const updateBrand = async (id, queryParams) => {
const response = await SendRequest({
method: 'put',
prefix: `apd/${vendor_id}`,
prefix: `brand/${id}`,
params: queryParams,
});
return response.data;
};
const deleteApd = async (queryParams) => {
const deleteBrand = async (id) => {
const response = await SendRequest({
method: 'delete',
prefix: `apd/${queryParams}`,
prefix: `brand/${id}`,
});
return response.data;
};
const getJenisPermit = async () => {
const response = await SendRequest({
method: 'get',
prefix: `apd/jenis-permit`,
});
return response.data;
};
export { getAllApd, createApd, updateApd, deleteApd, getJenisPermit };
export { getAllBrands, getBrandById, createBrand, updateBrand, deleteBrand };

View File

@@ -1,88 +1,12 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getAllDevice = async (queryParams) => {
try {
const response = await SendRequest({
method: 'get',
prefix: `device?${queryParams.toString()}`,
});
console.log('getAllDevice response:', response);
console.log('Query params:', queryParams.toString());
const response = await SendRequest({
method: 'get',
prefix: `device?${queryParams.toString()}`,
});
// Backend response structure:
// {
// statusCode: 200,
// data: [...devices],
// paging: {
// current_page: 1,
// current_limit: 10,
// total_limit: 50,
// total_page: 5
// }
// }
// Check if backend returns paginated data
if (response.paging) {
const totalData = response.data?.[0]?.total_data || response.rows || response.data?.length || 0;
return {
status: response.statusCode || 200,
data: {
data: response.data || [],
paging: {
page: response.paging.current_page || 1,
limit: response.paging.current_limit || 10,
total: totalData,
page_total: response.paging.total_page || Math.ceil(totalData / (response.paging.current_limit || 10))
},
total: totalData
}
};
}
// Fallback: If backend returns all data without pagination (old behavior)
const params = Object.fromEntries(queryParams);
const currentPage = parseInt(params.page) || 1;
const currentLimit = parseInt(params.limit) || 10;
const allData = response.data || [];
const totalData = allData.length;
// Client-side pagination
const startIndex = (currentPage - 1) * currentLimit;
const endIndex = startIndex + currentLimit;
const paginatedData = allData.slice(startIndex, endIndex);
return {
status: response.statusCode || 200,
data: {
data: paginatedData,
paging: {
page: currentPage,
limit: currentLimit,
total: totalData,
page_total: Math.ceil(totalData / currentLimit)
},
total: totalData
}
};
} catch (error) {
console.error('getAllDevice error:', error);
return {
status: 500,
data: {
data: [],
paging: {
page: 1,
limit: 10,
total: 0,
page_total: 0
},
total: 0
},
error: error.message
};
}
return response.data;
};
const getDeviceById = async (id) => {
@@ -90,6 +14,7 @@ const getDeviceById = async (id) => {
method: 'get',
prefix: `device/${id}`,
});
return response.data;
};
@@ -99,71 +24,27 @@ const createDevice = async (queryParams) => {
prefix: `device`,
params: queryParams,
});
console.log('createDevice full response:', response);
console.log('createDevice payload sent:', queryParams);
// Backend returns: { statusCode, message, rows, data: [device_object] }
// Check if response is empty array (error from SendRequest)
if (Array.isArray(response) && response.length === 0) {
return {
statusCode: 500,
data: null,
message: 'Request failed',
rows: 0
};
}
// Extract first item from data array
return {
statusCode: response.statusCode || 200,
data: response.data?.[0] || response.data,
message: response.message,
rows: response.rows
};
return response.data;
};
const updateDevice = async (device_id, queryParams) => {
const updateDevice = async (id, queryParams) => {
const response = await SendRequest({
method: 'put',
prefix: `device/${device_id}`,
prefix: `device/${id}`,
params: queryParams,
});
console.log('updateDevice full response:', response);
console.log('updateDevice payload sent:', queryParams);
// Backend returns: { statusCode, message, rows, data: [device_object] }
// Check if response is empty array (error from SendRequest)
if (Array.isArray(response) && response.length === 0) {
return {
statusCode: 500,
data: null,
message: 'Request failed',
rows: 0
};
}
// Extract first item from data array
return {
statusCode: response.statusCode || 200,
data: response.data?.[0] || response.data,
message: response.message,
rows: response.rows
};
return response.data;
};
const deleteDevice = async (queryParams) => {
const deleteDevice = async (id) => {
const response = await SendRequest({
method: 'delete',
prefix: `device/${queryParams}`,
prefix: `device/${id}`,
});
console.log('deleteDevice full response:', response);
// Backend returns: { statusCode, message, rows: null, data: true }
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message,
rows: response.rows
};
return response.data;
};
export { getAllDevice, getDeviceById, createDevice, updateDevice, deleteDevice };

View File

@@ -1,97 +1,12 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getAllPlantSection = async (queryParams) => {
try {
// Ensure queryParams is URLSearchParams object
const params = queryParams instanceof URLSearchParams ? queryParams : new URLSearchParams(queryParams);
const response = await SendRequest({
method: 'get',
prefix: `plant-sub-section?${queryParams.toString()}`,
});
const response = await SendRequest({
method: 'get',
prefix: `plant-sub-section?${params.toString()}`,
});
console.log('getAllPlantSection response:', response);
console.log('Query params:', params.toString());
// Backend response structure:
// {
// statusCode: 200,
// data: [...plantSections],
// paging: {
// current_page: 1,
// current_limit: 10,
// total_limit: 50,
// total_page: 5
// }
// }
// Check if backend returns paginated data
if (response.paging) {
// Extract total_data from first record, or fallback to total_limit or rows
const totalData = response.data?.[0]?.total_data || response.paging.total_limit || response.rows || response.data?.length || 0;
// Use total_limit as total count, handle 0 values for page/limit
const currentPage = response.paging.current_page || 1;
const currentLimit = response.paging.current_limit || 10;
const totalPages = response.paging.total_page || Math.ceil(totalData / currentLimit);
return {
status: response.statusCode || 200,
data: {
data: response.data || [],
paging: {
page: currentPage,
limit: currentLimit,
total: totalData,
page_total: totalPages
},
total: totalData
}
};
}
// Fallback: If backend returns all data without pagination (old behavior)
const parsedParams = Object.fromEntries(params);
const currentPage = parseInt(parsedParams.page) || 1;
const currentLimit = parseInt(parsedParams.limit) || 10;
const allData = response.data || [];
const totalData = allData.length;
// Client-side pagination
const startIndex = (currentPage - 1) * currentLimit;
const endIndex = startIndex + currentLimit;
const paginatedData = allData.slice(startIndex, endIndex);
return {
status: response.statusCode || 200,
data: {
data: paginatedData,
paging: {
page: currentPage,
limit: currentLimit,
total: totalData,
page_total: Math.ceil(totalData / currentLimit)
},
total: totalData
}
};
} catch (error) {
console.error('getAllPlantSection error:', error);
return {
status: 500,
data: {
data: [],
paging: {
page: 1,
limit: 10,
total: 0,
page_total: 0
},
total: 0
},
error: error.message
};
}
return response.data;
};
const getPlantSectionById = async (id) => {
@@ -99,6 +14,7 @@ const getPlantSectionById = async (id) => {
method: 'get',
prefix: `plant-sub-section/${id}`,
});
return response.data;
};
@@ -108,80 +24,33 @@ const createPlantSection = async (queryParams) => {
prefix: `plant-sub-section`,
params: queryParams,
});
console.log('createPlantSection full response:', response);
console.log('createPlantSection payload sent:', queryParams);
// Check if response has error flag
if (response.error) {
return {
statusCode: response.statusCode || 500,
data: null,
message: response.message || 'Request failed',
rows: 0
};
}
// Backend returns: { statusCode, message, rows, data: [plantSection_object] }
return {
statusCode: response.statusCode || 200,
data: response.data?.[0] || response.data,
message: response.message,
rows: response.rows
};
return response.data;
};
const updatePlantSection = async (plant_section_id, queryParams) => {
const updatePlantSection = async (id, queryParams) => {
const response = await SendRequest({
method: 'put',
prefix: `plant-sub-section/${plant_section_id}`,
prefix: `plant-sub-section/${id}`,
params: queryParams,
});
console.log('updatePlantSection full response:', response);
console.log('updatePlantSection payload sent:', queryParams);
// Check if response has error flag
if (response.error) {
return {
statusCode: response.statusCode || 500,
data: null,
message: response.message || 'Request failed',
rows: 0
};
}
// Backend returns: { statusCode, message, rows, data: [plantSection_object] }
return {
statusCode: response.statusCode || 200,
data: response.data?.[0] || response.data,
message: response.message,
rows: response.rows
};
return response.data;
};
const deletePlantSection = async (queryParams) => {
const deletePlantSection = async (id) => {
const response = await SendRequest({
method: 'delete',
prefix: `plant-sub-section/${queryParams}`,
prefix: `plant-sub-section/${id}`,
});
console.log('deletePlantSection full response:', response);
// Check if response has error flag
if (response.error) {
return {
statusCode: response.statusCode || 500,
data: null,
message: response.message || 'Request failed',
rows: 0
};
}
// Backend returns: { statusCode, message, rows: null, data: true }
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message,
rows: response.rows
};
return response.data;
};
export { getAllPlantSection, getPlantSectionById, createPlantSection, updatePlantSection, deletePlantSection };
export {
getAllPlantSection,
getPlantSectionById,
createPlantSection,
updatePlantSection,
deletePlantSection,
};

View File

@@ -1,29 +1,12 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getAllShift = async (queryParams) => {
try {
const response = await SendRequest({
method: 'get',
prefix: `shift?${queryParams.toString()}`,
});
return response;
} catch (error) {
console.error('getAllShift error:', error);
return {
status: 500,
data: {
data: [],
paging: {
page: 1,
limit: 10,
total: 0,
page_total: 0
},
total: 0
},
error: error.message
};
}
const response = await SendRequest({
method: 'get',
prefix: `shift?${queryParams.toString()}`,
});
return response.data;
};
const getShiftById = async (id) => {
@@ -31,6 +14,7 @@ const getShiftById = async (id) => {
method: 'get',
prefix: `shift/${id}`,
});
return response.data;
};
@@ -38,26 +22,20 @@ const createShift = async (queryParams) => {
const response = await SendRequest({
method: 'post',
prefix: `shift`,
data: queryParams,
params: queryParams,
});
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message
};
return response.data;
};
const updateShift = async (id, queryParams) => {
const response = await SendRequest({
method: 'put',
prefix: `shift/${id}`,
data: queryParams,
params: queryParams,
});
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message
};
return response.data;
};
const deleteShift = async (id) => {
@@ -65,11 +43,8 @@ const deleteShift = async (id) => {
method: 'delete',
prefix: `shift/${id}`,
});
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message
};
return response.data;
};
export { getAllShift, getShiftById, createShift, updateShift, deleteShift };

50
src/api/master-status.jsx Normal file
View File

@@ -0,0 +1,50 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getAllStatuss = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `status?${queryParams.toString()}`,
});
return response.data;
};
const getStatusById = async (id) => {
const response = await SendRequest({
method: 'get',
prefix: `status/${id}`,
});
return response.data;
};
const createStatus = async (queryParams) => {
const response = await SendRequest({
method: 'post',
prefix: `status`,
params: queryParams,
});
return response.data;
};
const updateStatus = async (id, queryParams) => {
const response = await SendRequest({
method: 'put',
prefix: `status/${id}`,
params: queryParams,
});
return response.data;
};
const deleteStatus = async (id) => {
const response = await SendRequest({
method: 'delete',
prefix: `status/${id}`,
});
return response.data;
};
export { getAllStatuss, getStatusById, createStatus, updateStatus, deleteStatus };

View File

@@ -1,93 +1,12 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getAllTag = async (queryParams) => {
try {
const response = await SendRequest({
method: 'get',
prefix: `tags?${queryParams.toString()}`,
});
const response = await SendRequest({
method: 'get',
prefix: `tags?${queryParams.toString()}`,
});
// Check if response has error
if (response.error) {
console.error('getAllTag error response:', response);
return {
status: response.statusCode || 500,
data: {
data: [],
paging: {
page: 1,
limit: 10,
total: 0,
page_total: 0
},
total: 0
},
error: response.message
};
}
// Check if backend returns paginated data
if (response.paging) {
const totalData = response.data?.[0]?.total_data || response.rows || response.data?.length || 0;
return {
status: response.statusCode || 200,
data: {
data: response.data || [],
paging: {
page: response.paging.current_page || 1,
limit: response.paging.current_limit || 10,
total: totalData,
page_total: response.paging.total_page || Math.ceil(totalData / (response.paging.current_limit || 10))
},
total: totalData
}
};
}
// Fallback: If backend returns all data without pagination (old behavior)
const params = Object.fromEntries(queryParams);
const currentPage = parseInt(params.page) || 1;
const currentLimit = parseInt(params.limit) || 10;
const allData = response.data || [];
const totalData = allData.length;
// Client-side pagination
const startIndex = (currentPage - 1) * currentLimit;
const endIndex = startIndex + currentLimit;
const paginatedData = allData.slice(startIndex, endIndex);
return {
status: response.statusCode || 200,
data: {
data: paginatedData,
paging: {
page: currentPage,
limit: currentLimit,
total: totalData,
page_total: Math.ceil(totalData / currentLimit)
},
total: totalData
}
};
} catch (error) {
console.error('getAllTag catch error:', error);
return {
status: 500,
data: {
data: [],
paging: {
page: 1,
limit: 10,
total: 0,
page_total: 0
},
total: 0
},
error: error.message
};
}
return response.data;
};
const getTagById = async (id) => {
@@ -95,6 +14,7 @@ const getTagById = async (id) => {
method: 'get',
prefix: `tags/${id}`,
});
return response.data;
};
@@ -105,74 +25,26 @@ const createTag = async (queryParams) => {
params: queryParams,
});
// Check if response has error flag
if (response.error) {
return {
statusCode: response.statusCode || 500,
data: null,
message: response.message || 'Request failed',
rows: 0
};
}
// Backend returns: { statusCode, message, rows, data: [tag_object] }
return {
statusCode: response.statusCode || 200,
data: response.data?.[0] || response.data,
message: response.message,
rows: response.rows
};
return response.data;
};
const updateTag = async (tag_id, queryParams) => {
const updateTag = async (id, queryParams) => {
const response = await SendRequest({
method: 'put',
prefix: `tags/${tag_id}`,
prefix: `tags/${id}`,
params: queryParams,
});
// Check if response has error flag
if (response.error) {
return {
statusCode: response.statusCode || 500,
data: null,
message: response.message || 'Request failed',
rows: 0
};
}
// Backend returns: { statusCode, message, rows, data: [tag_object] }
return {
statusCode: response.statusCode || 200,
data: response.data?.[0] || response.data,
message: response.message,
rows: response.rows
};
return response.data;
};
const deleteTag = async (queryParams) => {
const deleteTag = async (id) => {
const response = await SendRequest({
method: 'delete',
prefix: `tags/${queryParams}`,
prefix: `tags/${id}`,
});
// Check if response has error flag
if (response.error) {
return {
statusCode: response.statusCode || 500,
data: null,
message: response.message || 'Request failed',
rows: 0
};
}
// Backend returns: { statusCode, message, rows: null, data: true }
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message,
rows: response.rows
};
return response.data;
};
export { getAllTag, getTagById, createTag, updateTag, deleteTag };

View File

@@ -1,95 +1,12 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getAllUnit = async (queryParams) => {
try {
const response = await SendRequest({
method: 'get',
prefix: `unit?${queryParams.toString()}`,
});
console.log('getAllUnit response:', response);
console.log('Query params:', queryParams.toString());
const response = await SendRequest({
method: 'get',
prefix: `unit?${queryParams.toString()}`,
});
// Check if response has error
if (response.error) {
console.error('getAllUnit error response:', response);
return {
status: response.statusCode || 500,
data: {
data: [],
paging: {
page: 1,
limit: 10,
total: 0,
page_total: 0
},
total: 0
},
error: response.message
};
}
// Check if backend returns paginated data
if (response.paging) {
const totalData = response.data?.[0]?.total_data || response.rows || response.data?.length || 0;
return {
status: response.statusCode || 200,
data: {
data: response.data || [],
paging: {
page: response.paging.current_page || 1,
limit: response.paging.current_limit || 10,
total: totalData,
page_total: response.paging.total_page || Math.ceil(totalData / (response.paging.current_limit || 10))
},
total: totalData
}
};
}
// Fallback: If backend returns all data without pagination
const params = Object.fromEntries(queryParams);
const currentPage = parseInt(params.page) || 1;
const currentLimit = parseInt(params.limit) || 10;
const allData = response.data || [];
const totalData = allData.length;
// Client-side pagination
const startIndex = (currentPage - 1) * currentLimit;
const endIndex = startIndex + currentLimit;
const paginatedData = allData.slice(startIndex, endIndex);
return {
status: response.statusCode || 200,
data: {
data: paginatedData,
paging: {
page: currentPage,
limit: currentLimit,
total: totalData,
page_total: Math.ceil(totalData / currentLimit)
},
total: totalData
}
};
} catch (error) {
console.error('getAllUnit catch error:', error);
return {
status: 500,
data: {
data: [],
paging: {
page: 1,
limit: 10,
total: 0,
page_total: 0
},
total: 0
},
error: error.message
};
}
return response.data;
};
const getUnitById = async (id) => {
@@ -97,101 +14,37 @@ const getUnitById = async (id) => {
method: 'get',
prefix: `unit/${id}`,
});
return response.data;
};
const createUnit = async (queryParams) => {
// Map frontend fields to backend fields
const backendParams = {
unit_name: queryParams.name,
is_active: queryParams.is_active,
};
const response = await SendRequest({
method: 'post',
prefix: `unit`,
params: backendParams,
params: queryParams,
});
console.log('createUnit full response:', response);
console.log('createUnit payload sent:', backendParams);
// Check if response has error flag
if (response.error) {
return {
statusCode: response.statusCode || 500,
data: null,
message: response.message || 'Request failed',
rows: 0
};
}
// Backend returns: { statusCode, message, rows, data: [unit_object] }
return {
statusCode: response.statusCode || 200,
data: response.data?.[0] || response.data,
message: response.message,
rows: response.rows
};
return response.data;
};
const updateUnit = async (unit_id, queryParams) => {
// Map frontend fields to backend fields
const backendParams = {
unit_name: queryParams.name,
is_active: queryParams.is_active,
};
const updateUnit = async (id, queryParams) => {
const response = await SendRequest({
method: 'put',
prefix: `unit/${unit_id}`,
params: backendParams,
prefix: `unit/${id}`,
params: queryParams,
});
console.log('updateUnit full response:', response);
console.log('updateUnit payload sent:', backendParams);
// Check if response has error flag
if (response.error) {
return {
statusCode: response.statusCode || 500,
data: null,
message: response.message || 'Request failed',
rows: 0
};
}
// Backend returns: { statusCode, message, rows, data: [unit_object] }
return {
statusCode: response.statusCode || 200,
data: response.data?.[0] || response.data,
message: response.message,
rows: response.rows
};
return response.data;
};
const deleteUnit = async (queryParams) => {
const deleteUnit = async (id) => {
const response = await SendRequest({
method: 'delete',
prefix: `unit/${queryParams}`,
prefix: `unit/${id}`,
});
console.log('deleteUnit full response:', response);
// Check if response has error flag
if (response.error) {
return {
statusCode: response.statusCode || 500,
data: null,
message: response.message || 'Request failed',
rows: 0
};
}
// Backend returns: { statusCode, message, rows: null, data: true }
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message,
rows: response.rows
};
return response.data;
};
export { getAllUnit, getUnitById, createUnit, updateUnit, deleteUnit };

View File

@@ -1,70 +1,12 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getAllRole = async (queryParams) => {
try {
const response = await SendRequest({
method: 'get',
prefix: `roles?${queryParams.toString()}`,
});
const response = await SendRequest({
method: 'get',
prefix: `roles?${queryParams.toString()}`,
});
console.log('Role API Response:', response);
// Check if backend returns paginated data
if (response.paging) {
// Backend already provides pagination info
return {
status: response.statusCode || 200,
data: {
data: response.data || [],
paging: response.paging,
total: response.paging.total || 0
}
};
}
// Fallback: If backend returns all data without pagination
const params = Object.fromEntries(queryParams);
const currentPage = parseInt(params.page) || 1;
const currentLimit = parseInt(params.limit) || 10;
const allData = response.data || [];
const totalData = allData.length;
// Client-side pagination
const startIndex = (currentPage - 1) * currentLimit;
const endIndex = startIndex + currentLimit;
const paginatedData = allData.slice(startIndex, endIndex);
return {
status: response.statusCode || 200,
data: {
data: paginatedData,
paging: {
page: currentPage,
limit: currentLimit,
total: totalData,
page_total: Math.ceil(totalData / currentLimit),
},
total: totalData,
},
};
} catch (error) {
console.error('getAllRole error:', error);
return {
status: 500,
data: {
data: [],
paging: {
page: 1,
limit: 10,
total: 0,
page_total: 0
},
total: 0
},
error: error.message
};
}
return response.data;
};
const getRoleById = async (id) => {
@@ -72,6 +14,7 @@ const getRoleById = async (id) => {
method: 'get',
prefix: `roles/${id}`,
});
return response.data;
};
@@ -82,107 +25,26 @@ const createRole = async (queryParams) => {
params: queryParams,
});
console.log('Create Role API Response:', response);
// Check for error status (not 200, 201, or success)
const isSuccess =
response.statusCode === 200 || response.statusCode === 201 || response.status === 'success';
if (!isSuccess && response.statusCode >= 400) {
let errorMessage = response.message || 'Gagal menambahkan role';
// Handle SQL unique constraint violation
if (
errorMessage.includes('UNIQUE KEY constraint') ||
errorMessage.includes('duplicate key')
) {
errorMessage = `Role dengan nama "${queryParams.role_name}" sudah ada. Silakan gunakan nama lain.`;
}
return {
statusCode: response.statusCode,
data: response.data,
message: errorMessage,
};
}
// Return full response with statusCode
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message || 'Berhasil menambahkan role',
};
return response.data;
};
const updateRole = async (role_id, queryParams) => {
const updateRole = async (id, queryParams) => {
const response = await SendRequest({
method: 'put',
prefix: `roles/${role_id}`,
prefix: `roles/${id}`,
params: queryParams,
});
console.log('Update Role API Response:', response);
// Check for error status (not 200, 201, or success)
const isSuccess =
response.statusCode === 200 || response.statusCode === 201 || response.status === 'success';
if (!isSuccess && response.statusCode >= 400) {
let errorMessage = response.message || 'Gagal mengubah role';
// Handle SQL unique constraint violation
if (
errorMessage.includes('UNIQUE KEY constraint') ||
errorMessage.includes('duplicate key')
) {
errorMessage = `Role dengan nama "${queryParams.role_name}" sudah ada. Silakan gunakan nama lain.`;
}
return {
statusCode: response.statusCode,
data: response.data,
message: errorMessage,
};
}
// Return full response with statusCode
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message || 'Berhasil mengubah role',
};
return response.data;
};
const deleteRole = async (queryParams) => {
const deleteRole = async (id) => {
const response = await SendRequest({
method: 'delete',
prefix: `roles/${queryParams}`,
prefix: `roles/${id}`,
});
console.log('Delete API Response:', response);
// Check for errors
if (response.statusCode !== 200) {
let errorMessage = response.message || 'Gagal menghapus role';
// Handle foreign key constraint
if (errorMessage.includes('REFERENCE constraint') || errorMessage.includes('foreign key')) {
errorMessage = 'Role tidak dapat dihapus karena masih digunakan oleh user.';
}
return {
statusCode: response.statusCode,
data: response.data,
message: errorMessage,
};
}
// Return full response with statusCode
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message,
};
return response.data;
};
export { getAllRole, getRoleById, createRole, updateRole, deleteRole };

View File

@@ -1,15 +1,11 @@
// user-admin.jsx
import axios from 'axios';
import { SendRequest } from '../components/Global/ApiRequest';
const baseURL = import.meta.env.VITE_API_SERVER;
const getAllUser = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `admin-user?${queryParams.toString()}`,
});
return response;
return response.data;
};
const getUserDetail = async (id) => {
@@ -17,7 +13,7 @@ const getUserDetail = async (id) => {
method: 'get',
prefix: `admin-user/${id}`,
});
return response;
return response.data;
};
const updateUser = async (id, data) => {
@@ -26,7 +22,7 @@ const updateUser = async (id, data) => {
prefix: `admin-user/${id}`,
params: data,
});
return response;
return response.data;
};
const deleteUser = async (id) => {
@@ -34,7 +30,7 @@ const deleteUser = async (id) => {
method: 'delete',
prefix: `admin-user/${id}`,
});
return response;
return response.data;
};
const approvalUser = async (id, queryParams) => {
@@ -43,42 +39,7 @@ const approvalUser = async (id, queryParams) => {
prefix: `admin-user/approve/${id}`,
params: queryParams,
});
return response;
return response.data;
};
const uploadFile = async (formData) => {
try {
const token = localStorage.getItem('token')?.replace(/"/g, '') || '';
const url = `${baseURL}/file-upload`;
const response = await axios.post(url, formData, {
headers: {
Authorization: `Bearer ${token}`,
'Accept-Language': 'en_US',
'Content-Type': 'multipart/form-data',
},
});
return {
statusCode: response.data?.statusCode ?? 0,
message: response.data?.message ?? '',
data: response.data?.data ?? {},
};
} catch (error) {
console.error('❌ ERROR di uploadFile:', error);
return {
statusCode: error?.response?.status || 500,
message: error?.response?.data?.message || 'Upload gagal',
data: {},
};
}
};
export { getAllUser, getUserDetail, updateUser, deleteUser, approvalUser, uploadFile };
export { getAllUser, getUserDetail, updateUser, deleteUser, approvalUser };

View File

@@ -1,97 +1,12 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getAllUser = async (queryParams) => {
try {
console.log('getAllUser queryParams:', queryParams.toString());
const response = await SendRequest({
method: 'get',
prefix: `user?${queryParams.toString()}`,
});
const response = await SendRequest({
method: 'get',
prefix: `user?${queryParams.toString()}`,
});
console.log('getAllUser response:', response);
// Backend now handles pagination, just return the response
// Expected backend response structure:
// {
// statusCode: 200,
// data: [...users],
// paging: { page, limit, total, page_total }
// }
// Check if backend returns paginated data
if (response.paging) {
// Filter out super admin users (is_sa = true)
const allData = response.data || [];
const filteredData = allData.filter(user => user.is_sa !== true && user.is_sa !== 1);
// Recalculate pagination info after filtering
const totalAfterFilter = filteredData.length;
const currentPage = response.paging.page || 1;
const currentLimit = response.paging.limit || 10;
return {
status: response.statusCode || 200,
data: {
data: filteredData,
paging: {
page: currentPage,
limit: currentLimit,
total: totalAfterFilter,
page_total: Math.ceil(totalAfterFilter / currentLimit)
},
total: totalAfterFilter
}
};
}
// Fallback: If backend returns all data without pagination (old behavior)
const params = Object.fromEntries(queryParams);
const currentPage = parseInt(params.page) || 1;
const currentLimit = parseInt(params.limit) || 10;
const allData = response.data || [];
// Filter out users with is_sa = true or 1 (client-side filtering)
const filteredData = allData.filter(user => user.is_sa !== true && user.is_sa !== 1);
const totalData = filteredData.length;
// Client-side pagination
const startIndex = (currentPage - 1) * currentLimit;
const endIndex = startIndex + currentLimit;
const paginatedData = filteredData.slice(startIndex, endIndex);
return {
status: response.statusCode || 200,
data: {
data: paginatedData,
paging: {
page: currentPage,
limit: currentLimit,
total: totalData,
page_total: Math.ceil(totalData / currentLimit)
},
total: totalData
}
};
} catch (error) {
console.error('getAllUser error:', error);
// Return empty data on error to prevent app crash
return {
status: 500,
data: {
data: [],
paging: {
page: 1,
limit: 10,
total: 0,
page_total: 0
},
total: 0
},
error: error.message
};
}
return response.data;
};
const getUserById = async (id) => {
@@ -99,6 +14,7 @@ const getUserById = async (id) => {
method: 'get',
prefix: `user/${id}`,
});
return response.data;
};
@@ -108,12 +24,8 @@ const createUser = async (queryParams) => {
prefix: `user`,
params: queryParams,
});
// Return full response with statusCode
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message
};
return response.data;
};
const updateUser = async (user_id, queryParams) => {
@@ -122,12 +34,8 @@ const updateUser = async (user_id, queryParams) => {
prefix: `user/${user_id}`,
params: queryParams,
});
// Return full response with statusCode
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message
};
return response.data;
};
const deleteUser = async (queryParams) => {
@@ -135,12 +43,8 @@ const deleteUser = async (queryParams) => {
method: 'delete',
prefix: `user/${queryParams}`,
});
// Return full response with statusCode
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message
};
return response.data;
};
const approveUser = async (user_id) => {
@@ -148,12 +52,8 @@ const approveUser = async (user_id) => {
method: 'put',
prefix: `user/${user_id}/approve`,
});
// Return full response with statusCode
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message
};
return response.data;
};
const rejectUser = async (user_id) => {
@@ -161,12 +61,8 @@ const rejectUser = async (user_id) => {
method: 'put',
prefix: `user/${user_id}/reject`,
});
// Return full response with statusCode
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message
};
return response.data;
};
const toggleActiveUser = async (user_id, is_active) => {
@@ -174,15 +70,11 @@ const toggleActiveUser = async (user_id, is_active) => {
method: 'put',
prefix: `user/${user_id}`,
params: {
is_active: is_active
is_active: is_active,
},
});
// Return full response with statusCode
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message
};
return response.data;
};
const changePassword = async (user_id, new_password) => {
@@ -190,18 +82,21 @@ const changePassword = async (user_id, new_password) => {
method: 'put',
prefix: `user/change-password/${user_id}`,
params: {
new_password: new_password
new_password: new_password,
},
});
console.log('Change Password Response:', response);
// Return full response with statusCode
return {
statusCode: response.statusCode || 200,
data: response.data,
message: response.message || 'Password berhasil diubah'
};
return response.data;
};
export { getAllUser, getUserById, createUser, updateUser, deleteUser, approveUser, rejectUser, toggleActiveUser, changePassword };
export {
getAllUser,
getUserById,
createUser,
updateUser,
deleteUser,
approveUser,
rejectUser,
toggleActiveUser,
changePassword,
};

View File

@@ -1,170 +1,171 @@
import axios from "axios";
import Swal from "sweetalert2";
import axios from 'axios';
import Swal from 'sweetalert2';
const baseURL = import.meta.env.VITE_API_SERVER;
const instance = axios.create({
baseURL,
withCredentials: true,
baseURL,
withCredentials: true,
});
// axios khusus refresh
const refreshApi = axios.create({
baseURL,
withCredentials: true,
baseURL,
withCredentials: true,
});
instance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
(response) => response,
async (error) => {
const originalRequest = error.config;
console.error("🚨 Response Error Interceptor:", {
status: error.response?.status,
url: originalRequest.url,
message: error.response?.data?.message,
hasRetried: originalRequest._retry
});
console.error('🚨 Response Error Interceptor:', {
status: error.response?.status,
url: originalRequest.url,
message: error.response?.data?.message,
hasRetried: originalRequest._retry,
});
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
console.log("🔄 Refresh token dipanggil...");
const refreshRes = await refreshApi.post("/auth/refresh-token");
try {
console.log('🔄 Refresh token dipanggil...');
const refreshRes = await refreshApi.post('/auth/refresh-token');
const newAccessToken = refreshRes.data.data.accessToken;
localStorage.setItem("token", newAccessToken);
console.log("✅ Token refreshed successfully");
const newAccessToken = refreshRes.data.data.accessToken;
localStorage.setItem('token', newAccessToken);
console.log('✅ Token refreshed successfully');
// update token di header
instance.defaults.headers.common["Authorization"] = `Bearer ${newAccessToken}`;
originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`;
// update token di header
instance.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
console.log("🔁 Retrying original request...");
return instance(originalRequest);
} catch (refreshError) {
console.error("❌ Refresh token gagal:", refreshError.response?.data || refreshError.message);
localStorage.clear();
window.location.href = "/signin";
}
console.log('🔁 Retrying original request...');
return instance(originalRequest);
} catch (refreshError) {
console.error(
'❌ Refresh token gagal:',
refreshError.response?.data || refreshError.message
);
localStorage.clear();
window.location.href = '/signin';
}
}
return Promise.reject(error);
}
return Promise.reject(error);
}
);
async function ApiRequest({
method = "GET",
params = {},
prefix = "/",
token = true,
} = {}) {
const isFormData = params instanceof FormData;
async function ApiRequest({ method = 'GET', params = {}, prefix = '/', token = true } = {}) {
const isFormData = params instanceof FormData;
const request = {
method,
url: prefix,
data: params,
headers: {
"Accept-Language": "en_US",
...(isFormData ? {} : { "Content-Type": "application/json" }),
},
};
const request = {
method,
url: prefix,
data: params,
headers: {
'Accept-Language': 'en_US',
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
},
};
const rawToken = localStorage.getItem("token");
if (token && rawToken) {
const cleanToken = rawToken.replace(/"/g, "");
request.headers["Authorization"] = `Bearer ${cleanToken}`;
console.log("🔐 Sending request with token:", cleanToken.substring(0, 20) + "...");
} else {
console.warn("⚠️ No token found in localStorage");
}
console.log("📤 API Request:", { method, url: prefix, hasToken: !!rawToken });
try {
const response = await instance(request);
console.log("✅ API Response:", { url: prefix, status: response.status, statusCode: response.data?.statusCode });
return { ...response, error: false };
} catch (error) {
const status = error?.response?.status || 500;
const message = error?.response?.data?.message || error.message || "Something Wrong";
console.error("❌ API Error:", {
url: prefix,
status,
message,
fullError: error?.response?.data
});
if (status !== 401) {
await cekError(status, message);
const rawToken = localStorage.getItem('token');
if (token && rawToken) {
const cleanToken = rawToken.replace(/"/g, '');
request.headers['Authorization'] = `Bearer ${cleanToken}`;
console.log('🔐 Sending request with token:', cleanToken.substring(0, 20) + '...');
} else {
console.warn('⚠️ No token found in localStorage');
}
return { ...error.response, error: true };
}
console.log('📤 API Request:', { method, url: prefix, hasToken: !!rawToken });
try {
const response = await instance(request);
console.log('✅ API Response:', {
url: prefix,
status: response.status,
statusCode: response.data?.statusCode,
});
return { ...response, error: false };
} catch (error) {
const status = error?.response?.status || 500;
const message = error?.response?.data?.message || error.message || 'Something Wrong';
console.error('❌ API Error:', {
url: prefix,
status,
message,
fullError: error?.response?.data,
});
if (status !== 401) {
await cekError(status, message);
}
return { ...error.response, error: true };
}
}
async function cekError(status, message = "") {
if (status === 403) {
await Swal.fire({
icon: "warning",
title: "Forbidden",
text: message,
});
} else if (status >= 500) {
await Swal.fire({
icon: "error",
title: "Server Error",
text: message,
});
} else {
await Swal.fire({
icon: "warning",
title: "Peringatan",
text: message,
});
}
async function cekError(status, message = '') {
if (status === 403) {
await Swal.fire({
icon: 'warning',
title: 'Forbidden',
text: message,
});
} else if (status >= 500) {
await Swal.fire({
icon: 'error',
title: 'Server Error',
text: message,
});
} else {
await Swal.fire({
icon: 'warning',
title: 'Peringatan',
text: message,
});
}
}
const SendRequest = async (queryParams) => {
try {
const response = await ApiRequest(queryParams);
console.log("📦 SendRequest response:", {
hasError: response.error,
status: response.status,
statusCode: response.data?.statusCode,
data: response.data
});
try {
const response = await ApiRequest(queryParams);
console.log('📦 SendRequest response:', {
hasError: response.error,
status: response.status,
statusCode: response.data?.statusCode,
data: response.data,
});
// If ApiRequest returned error flag, return error structure
if (response.error) {
const errorMsg = response.data?.message || response.statusText || "Request failed";
console.error("❌ SendRequest error response:", errorMsg);
// If ApiRequest returned error flag, return error structure
if (response.error) {
const errorMsg = response.data?.message || response.statusText || 'Request failed';
console.error('❌ SendRequest error response:', errorMsg);
// Return consistent error structure instead of empty array
return {
statusCode: response.status || 500,
message: errorMsg,
data: null,
error: true
};
// Return consistent error structure instead of empty array
return {
statusCode: response.status || 500,
message: errorMsg,
data: null,
error: true,
};
}
return response || { statusCode: 200, data: [], message: 'Success' };
} catch (error) {
console.error('❌ SendRequest catch error:', error);
// Don't show Swal here, let the calling code handle it
// This allows better error handling in each API call
return {
statusCode: 500,
message: error.message || 'Something went wrong',
data: null,
error: true,
};
}
return response?.data || { statusCode: 200, data: [], message: "Success" };
} catch (error) {
console.error("❌ SendRequest catch error:", error);
// Don't show Swal here, let the calling code handle it
// This allows better error handling in each API call
return {
statusCode: 500,
message: error.message || "Something went wrong",
data: null,
error: true
};
}
};
export { ApiRequest, SendRequest };

View File

@@ -37,7 +37,7 @@ const CardList = ({
return (
<Row gutter={[16, 16]} style={{ marginTop: '16px', justifyContent: 'left' }}>
{data.map((item) => (
<Col xs={24} sm={24} md={12} lg={8} key={item.device_id}>
<Col xs={24} sm={24} md={12} lg={6} key={item.device_id}>
<Card
title={
<div
@@ -80,16 +80,18 @@ const CardList = ({
]}
>
<div style={{ textAlign: 'left' }}>
{column.map((itemCard) => (
<>
{!itemCard.hidden && !itemCard.render && (
<p>
{column.map((itemCard, index) => (
<React.Fragment key={index}>
{!itemCard.hidden && itemCard.title !== 'No' && itemCard.title !== 'Aksi' && (
<p style={{ margin: '8px 0' }}>
<Text strong>{itemCard.title}:</Text>{' '}
{item[itemCard.key]}
{itemCard.render
? itemCard.render(item[itemCard.dataIndex], item, index)
: item[itemCard.dataIndex] || item[itemCard.key] || '-'
}
</p>
)}
{itemCard.render && itemCard.render}
</>
</React.Fragment>
))}
</div>
</Card>

View File

@@ -1,18 +1,6 @@
import React, { memo, useState, useEffect, useRef } from 'react';
import { Table, Pagination, Row, Col, Card, Grid, Button, Typography, Tag, Segmented } from 'antd';
import {
PlusOutlined,
FilterOutlined,
EditOutlined,
DeleteOutlined,
EyeOutlined,
SearchOutlined,
FilePdfOutlined,
AppstoreOutlined,
TableOutlined,
} from '@ant-design/icons';
import { setFilterData } from './DataFilter';
import CardDevice from '../../pages/master/device/component/CardDevice';
import { AppstoreOutlined, TableOutlined } from '@ant-design/icons';
import CardList from './CardList';
const { Text } = Typography;
@@ -33,16 +21,12 @@ const TableList = memo(function TableList({
const [gridLoading, setGridLoading] = useState(false);
const [data, setData] = useState([]);
const [pagingResponse, setPagingResponse] = useState({
totalData: '',
perPage: '',
totalPage: '',
});
const [pagination, setPagination] = useState({
current: 1,
limit: 10,
total: 0,
current_page: 1,
current_limit: 10,
total_limit: 0,
total_page: 1,
});
const [viewMode, setViewMode] = useState('card');
@@ -50,42 +34,41 @@ const TableList = memo(function TableList({
const { useBreakpoint } = Grid;
useEffect(() => {
filter(1, 10);
filter(1, pagination.current_limit);
}, [triger]);
const filter = async (currentPage, pageSize) => {
setGridLoading(true);
const paging = {
page: currentPage,
limit: pageSize,
page: Number(currentPage),
limit: Number(pageSize),
};
const param = new URLSearchParams({ ...paging, ...queryParams });
const resData = await getData(param);
setData(resData?.data ?? []);
const pagingData = resData?.paging;
if (pagingData) {
setPagination((prev) => ({
...prev,
current_page: pagingData.current_page || 1,
current_limit: pagingData.current_limit || 10,
total_limit: pagingData.total_limit || 0,
total_page: pagingData.total_page || 1,
}));
}
if (resData) {
setTimeout(() => {
setGridLoading(false);
}, 900);
}
setData(resData.data.data ?? []);
setFilterData(resData.data.data ?? []);
if (resData.status == 200) {
setPagingResponse({
totalData: resData.paging.total_limit,
perPage: resData.paging.page_total,
totalPage: resData.paging.total_page,
});
setPagination((prev) => ({
...prev,
current: resData.paging.current_page,
limit: resData.paging.current_limit,
total: resData.paging.total_limit,
}));
} else {
setGridLoading(false);
return;
}
};
@@ -93,7 +76,7 @@ const TableList = memo(function TableList({
setPagination((prev) => ({
...prev,
current: page,
pageSize,
limit: pageSize,
}));
filter(page, pageSize);
};
@@ -138,8 +121,8 @@ const TableList = memo(function TableList({
<Row justify="space-between" align="middle">
<Col>
<div>
Menampilkan {pagingResponse.totalPage} Data dari {pagingResponse.perPage}{' '}
Halaman
Menampilkan {pagination.current_limit} data halaman{' '}
{pagination.current_page} dari total {pagination.total_limit} data
</div>
</Col>
<Col>
@@ -147,9 +130,9 @@ const TableList = memo(function TableList({
showSizeChanger
onChange={handlePaginationChange}
onShowSizeChange={handlePaginationChange}
current={pagination.current}
pageSize={pagination.pageSize}
total={pagination.total}
current={pagination.current_page}
pageSize={pagination.current_limit}
total={pagination.total_limit}
/>
</Col>
</Row>

View File

@@ -16,6 +16,7 @@ const NotifOk = ({ icon, title, message }) => {
icon: icon,
title: title,
text: message,
html: message.replace(/\n/g, '<br/>'),
});
};

View File

@@ -26,6 +26,9 @@ import {
TeamOutlined,
ClockCircleOutlined,
CalendarOutlined,
DesktopOutlined,
NodeExpandOutlined,
GroupOutlined,
} from '@ant-design/icons';
const { Text } = Typography;
@@ -40,6 +43,48 @@ const allItems = [
</Link>
),
},
{
key: 'dashboard-svg',
icon: <GroupOutlined style={{ fontSize: '19px' }} />,
label: 'Dashboard',
children: [
{
key: 'dashboard-svg-overview',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/overview">Overview</Link>,
},
{
key: 'dashboard-svg-compressor-a',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/compressor-a">Compressor A</Link>,
},
{
key: 'dashboard-svg-compressor-b',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/compressor-b">Compressor B</Link>,
},
{
key: 'dashboard-svg-compressor-c',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/compressor-c">Compressor C</Link>,
},
{
key: 'dashboard-svg-airdryer-a',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/airdryer-a">Air Dryer A</Link>,
},
{
key: 'dashboard-svg-airdryer-b',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/airdryer-b">Air Dryer B</Link>,
},
{
key: 'dashboard-svg-airdryer-c',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/airdryer-c">Air Dryer C</Link>,
},
],
},
{
key: 'master',
icon: <DatabaseOutlined style={{ fontSize: '19px' }} />,
@@ -60,16 +105,16 @@ const allItems = [
icon: <MobileOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/master/device">Device</Link>,
},
{
key: 'master-tag',
icon: <TagOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/master/tag">Tag</Link>,
},
{
key: 'master-unit',
icon: <AppstoreOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/master/unit">Unit</Link>,
},
{
key: 'master-tag',
icon: <TagOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/master/tag">Tag</Link>,
},
{
key: 'master-status',
icon: <SafetyOutlined style={{ fontSize: '19px' }} />,
@@ -163,6 +208,7 @@ const LayoutMenu = () => {
if (pathname === '/notification') return 'notification';
if (pathname === '/event-alarm') return 'event-alarm';
if (pathname === '/jadwal-shift') return 'jadwal-shift';
if (pathname === '/dashboard-svg') return 'dashboard-svg';
// Handle master routes
if (pathname.startsWith('/master/')) {
@@ -170,6 +216,12 @@ const LayoutMenu = () => {
return `master-${subPath}`;
}
// Handle master routes
if (pathname.startsWith('/dashboard-svg/')) {
const subPath = pathParts[1];
return `dashboard-svg-${subPath}`;
}
// Handle history routes
if (pathname.startsWith('/history/')) {
const subPath = pathParts[1];
@@ -188,6 +240,7 @@ const LayoutMenu = () => {
// Function to get parent key from menu key
const getParentKey = (key) => {
if (key.startsWith('master-')) return 'master';
if (key.startsWith('dashboard-svg-')) return 'dashboard-svg';
if (key.startsWith('history-')) return 'history';
if (key.startsWith('shift-')) return 'shift-management';
return null;

View File

@@ -1,461 +0,0 @@
// import React, { useState } from 'react';
// import {
// Flex,
// Input,
// InputNumber,
// Form,
// Button,
// Card,
// Space,
// Upload,
// Divider,
// Tooltip,
// message,
// Select,
// } from 'antd';
// import {
// UploadOutlined,
// UserOutlined,
// IdcardOutlined,
// PhoneOutlined,
// LockOutlined,
// InfoCircleOutlined,
// MailOutlined,
// } from '@ant-design/icons';
// const { Item } = Form;
// const { Option } = Select;
// import sypiu_ggcp from 'assets/sypiu_ggcp.jpg';
// import { useNavigate } from 'react-router-dom';
// import { register, uploadFile, checkUsername } from '../../api/auth';
// import { NotifAlert } from '../../components/Global/ToastNotif';
// const Registration = () => {
// const [form] = Form.useForm();
// const navigate = useNavigate();
// const [loading, setLoading] = useState(false);
// const [fileListKontrak, setFileListKontrak] = useState([]);
// const [fileListHsse, setFileListHsse] = useState([]);
// const [fileListIcon, setFileListIcon] = useState([]);
// // Daftar jenis vendor
// const vendorTypes = [
// { vendor_type: 1, vendor_type_name: 'One-Time' },
// { vendor_type: 2, vendor_type_name: 'Rutin' },
// ];
// const onFinish = async (values) => {
// setLoading(true);
// try {
// if (!fileListKontrak.length || !fileListHsse.length) {
// message.error('Harap unggah Lampiran Kontrak Kerja dan HSSE Plan!');
// setLoading(false);
// return;
// }
// const formData = new FormData();
// formData.append('path_kontrak', fileListKontrak[0].originFileObj);
// formData.append('path_hse_plant', fileListHsse[0].originFileObj);
// if (fileListIcon.length) {
// formData.append('path_icon', fileListIcon[0].originFileObj);
// }
// const uploadResponse = await uploadFile(formData);
// if (!uploadResponse.data?.pathKontrak && !uploadResponse.data?.pathHsePlant) {
// message.error(uploadResponse.message || 'Gagal mengunggah file.');
// setLoading(false);
// return;
// }
// const params = new URLSearchParams({ username: values.username });
// const usernameCheck = await checkUsername(params);
// if (usernameCheck.data.data && usernameCheck.data.data.available === false) {
// NotifAlert({
// icon: 'error',
// title: 'Gagal',
// message: usernameCheck.data.message || 'Terjadi kesalahan, silakan coba lagi',
// });
// setLoading(false);
// return;
// }
// const registerData = {
// nama_perusahaan: values.namaPerusahaan,
// no_kontak_wo: values.noKontakWo,
// path_kontrak: uploadResponse.data.pathKontrak || '',
// durasi: values.durasiPekerjaan,
// nilai_csms: values.nilaiCsms.toString(),
// vendor_type: values.jenisVendor, // Tambahkan jenis vendor ke registerData
// path_hse_plant: uploadResponse.data.pathHsePlant || '',
// nama_leader: values.penanggungJawab,
// no_identitas: values.noIdentitas,
// no_hp: values.noHandphone,
// email_register: values.username,
// password_register: values.password,
// };
// const response = await register(registerData);
// if (response.data?.id_register) {
// message.success('Data berhasil disimpan!');
// try {
// form.resetFields();
// setFileListKontrak([]);
// setFileListHsse([]);
// setFileListIcon([]);
// navigate('/registration-submitted');
// } catch (postSuccessError) {
// message.warning(
// 'Registrasi berhasil, tetapi ada masalah setelahnya. Silakan ke halaman login secara manual.'
// );
// }
// } else {
// message.error(response.message || 'Pendaftaran gagal, silakan coba lagi.');
// }
// } catch (error) {
// console.error('Error saat registrasi:', error);
// NotifAlert({
// icon: 'error',
// title: 'Gagal',
// message: error.message || 'Terjadi kesalahan, silakan coba lagi',
// });
// } finally {
// setLoading(false);
// }
// };
// const onCancel = () => {
// form.resetFields();
// setFileListKontrak([]);
// setFileListHsse([]);
// setFileListIcon([]);
// navigate('/signin');
// };
// const handleChangeKontrak = ({ fileList }) => {
// setFileListKontrak(fileList);
// };
// const handleChangeHsse = ({ fileList }) => {
// setFileListHsse(fileList);
// };
// const handleChangeIcon = ({ fileList }) => {
// setFileListIcon(fileList);
// };
// const beforeUpload = (file, fieldname) => {
// const isValidType = [
// 'image/jpeg',
// 'image/jpg',
// 'image/png',
// fieldname !== 'path_icon' ? 'application/pdf' : null,
// ]
// .filter(Boolean)
// .includes(file.type);
// const isNotEmpty = file.size > 0;
// const isSizeValid = file.size / 1024 / 1024 < 10;
// if (!isValidType) {
// message.error(
// `Hanya file ${
// fieldname === 'path_icon' ? 'JPG/PNG' : 'PDF/JPG/PNG'
// } yang diperbolehkan!`
// );
// return false;
// }
// if (!isNotEmpty) {
// message.error('File tidak boleh kosong!');
// return false;
// }
// if (!isSizeValid) {
// message.error('Ukuran file maksimal 10MB!');
// return false;
// }
// return true;
// };
// return (
// <Flex
// align="center"
// justify="center"
// style={{
// minHeight: '100vh',
// backgroundImage: `url(${sypiu_ggcp})`,
// backgroundSize: 'cover',
// backgroundPosition: 'center',
// padding: '20px',
// }}
// >
// <Card
// style={{
// width: '100%',
// maxWidth: 800,
// background: 'rgba(255, 255, 255, 0.9)',
// backdropFilter: 'blur(10px)',
// borderRadius: '12px',
// boxShadow: '0 8px 16px rgba(0, 0, 0, 0.1)',
// padding: '24px',
// }}
// title={
// <Flex align="center" justify="space-between">
// <h2 style={{ margin: 0, color: '#1a3c34' }}>Formulir Pendaftaran</h2>
// <Button
// type="link"
// icon={<InfoCircleOutlined />}
// onClick={() => navigate('/signin')}
// >
// Kembali
// </Button>
// </Flex>
// }
// >
// <Form
// form={form}
// onFinish={onFinish}
// layout="horizontal"
// labelCol={{ span: 8 }}
// wrapperCol={{ span: 16 }}
// labelAlign="left"
// style={{ maxWidth: 800 }}
// >
// {/* Informasi Perusahaan */}
// <Divider
// orientation="left"
// orientationMargin={0}
// style={{
// color: '#23A55A',
// fontWeight: 'bold',
// marginLeft: 0,
// paddingLeft: 0,
// }}
// >
// Informasi Perusahaan
// </Divider>
// <Item
// label="Nama Perusahaan"
// name="namaPerusahaan"
// rules={[{ required: true, message: 'Masukkan Nama Perusahaan!' }]}
// >
// <Input
// prefix={<UserOutlined />}
// placeholder="Masukkan Nama Perusahaan"
// size="large"
// />
// </Item>
// <Item
// label="Durasi Pekerjaan (Hari)"
// name="durasiPekerjaan"
// rules={[{ required: true, message: 'Masukkan Durasi Pekerjaan!' }]}
// >
// <InputNumber
// min={1}
// style={{ width: '100%' }}
// placeholder="Masukkan Durasi Pekerjaan"
// size="large"
// />
// </Item>
// <Item
// label="No Kontrak Kerja / Agreement"
// name="noKontakWo"
// rules={[
// { required: true, message: 'Masukkan No Kontrak Kerja / Agreement!' },
// ]}
// >
// <Input
// style={{
// width: '100%',
// }}
// placeholder="Masukkan No Kontrak Kerja / Agreement"
// size="large"
// />
// </Item>
// <Item
// label="Lampiran Kontrak Kerja"
// name="lampiranKontrak"
// rules={[{ required: true, message: 'Unggah Lampiran Kontrak Kerja!' }]}
// >
// <Upload
// beforeUpload={(file) => beforeUpload(file, 'path_kontrak')}
// fileList={fileListKontrak}
// onChange={handleChangeKontrak}
// maxCount={1}
// >
// <Button icon={<UploadOutlined />} size="large">
// Unggah PDF/JPG
// </Button>
// </Upload>
// </Item>
// <Item
// label="HSSE Plan"
// name="hssePlan"
// rules={[{ required: true, message: 'Unggah HSSE Plan!' }]}
// >
// <Upload
// beforeUpload={(file) => beforeUpload(file, 'path_hse_plant')}
// fileList={fileListHsse}
// onChange={handleChangeHsse}
// maxCount={1}
// >
// <Button icon={<UploadOutlined />} size="large">
// Unggah PDF/JPG
// </Button>
// </Upload>
// </Item>
// <Item
// label="Nilai CSMS"
// name="nilaiCsms"
// rules={[{ required: true, message: 'Masukkan Nilai CSMS!' }]}
// >
// <InputNumber
// min={0}
// max={100}
// style={{ width: '100%' }}
// placeholder="Masukkan Nilai CSMS"
// size="large"
// />
// </Item>
// <Item
// label="Jenis Vendor"
// name="jenisVendor"
// rules={[{ required: true, message: 'Pilih Jenis Vendor!' }]}
// >
// <Select
// placeholder="Pilih Jenis Vendor"
// size="large"
// style={{ width: '100%' }}
// >
// {vendorTypes.map((vendor) => (
// <Option key={vendor.vendor_type} value={vendor.vendor_type}>
// {vendor.vendor_type_name}
// </Option>
// ))}
// </Select>
// </Item>
// {/* Informasi Penanggung Jawab */}
// <Divider
// orientation="left"
// orientationMargin={0}
// style={{
// color: '#23A55A',
// fontWeight: 'bold',
// marginLeft: 0,
// paddingLeft: 0,
// }}
// >
// Informasi Penanggung Jawab
// </Divider>
// <Item
// label="Nama Penanggung Jawab"
// name="penanggungJawab"
// rules={[{ required: true, message: 'Masukkan Nama Penanggung Jawab!' }]}
// >
// <Input
// prefix={<UserOutlined />}
// placeholder="Masukkan Nama Penanggung Jawab"
// size="large"
// />
// </Item>
// <Item
// label="No Handphone"
// name="noHandphone"
// rules={[
// { required: true, message: 'Masukkan No Handphone!' },
// {
// pattern: /^(\+62|0)[0-9]{9,12}$/,
// message:
// 'Format nomor telepon tidak valid! (Contoh: +62.... atau 0....)',
// },
// ]}
// >
// <Input
// prefix={<PhoneOutlined />}
// placeholder="Masukkan No Handphone (+62)"
// size="large"
// />
// </Item>
// <Item
// label="No Identitas"
// name="noIdentitas"
// rules={[{ required: true, message: 'Masukkan No Identitas!' }]}
// >
// <Input
// prefix={<IdcardOutlined />}
// placeholder="Masukkan No Identitas"
// size="large"
// />
// </Item>
// {/* Akun Pengguna */}
// <Divider
// orientation="left"
// orientationMargin={0}
// style={{
// color: '#23A55A',
// fontWeight: 'bold',
// marginLeft: 0,
// paddingLeft: 0,
// }}
// >
// Akun Pengguna (digunakan sebagai user login SYPIU)
// </Divider>
// <Item
// label="Email"
// name="username"
// rules={[
// { required: true, message: 'Masukkan Email!' },
// { type: 'email', message: 'Format email tidak valid!' },
// ]}
// >
// <Input
// prefix={<MailOutlined />}
// placeholder="Masukkan Email"
// size="large"
// />
// </Item>
// <Item
// label="Password"
// name="password"
// rules={[
// { required: true, message: 'Masukkan Password!' },
// { min: 6, message: 'Password minimal 6 karakter!' },
// ]}
// >
// <Input.Password
// prefix={<LockOutlined />}
// placeholder="Masukkan Password"
// size="large"
// />
// </Item>
// {/* Tombol */}
// <Item wrapperCol={{ offset: 8, span: 16 }}>
// <Space style={{ marginTop: '24px', width: '100%' }}>
// <Button
// type="primary"
// htmlType="submit"
// size="large"
// loading={loading}
// style={{
// backgroundColor: '#23A55A',
// borderColor: '#23A55A',
// width: 120,
// }}
// >
// Simpan
// </Button>
// <Button onClick={onCancel} size="large" style={{ width: 120 }}>
// Batal
// </Button>
// </Space>
// </Item>
// </Form>
// </Card>
// </Flex>
// );
// };
// export default Registration;

View File

@@ -31,8 +31,9 @@ const SignIn = () => {
prefix: 'auth/generate-captcha',
token: false,
});
setCaptchaSvg(res.data.svg || '');
setCaptchaText(res.data.text || '');
setCaptchaSvg(res.data?.data?.svg || '');
setCaptchaText(res.data?.data?.text || '');
} catch (err) {
console.error('Error fetching captcha:', err);
}
@@ -57,8 +58,8 @@ const SignIn = () => {
withCredentials: true,
});
const user = res?.data?.user || res?.user;
const accessToken = res?.data?.accessToken || res?.tokens?.accessToken;
const user = res?.data?.data?.user || res?.user;
const accessToken = res?.data?.data?.accessToken || res?.tokens?.accessToken;
if (user && accessToken) {
localStorage.setItem('token', accessToken);

View File

@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
import { Card, Typography, Flex } from 'antd';
import { setValSvg } from '../../components/Global/MqttConnection';
import SvgTemplate from './SvgTemplate';
import SvgViewer from './SvgViewer';
const { Text } = Typography;
const filePathSvg = '/svg/air_dryer_A_rev.svg';
const topicMqtt = 'PIU_GGCP/Devices/PB';
const SvgAirDryerA = () => {
return (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgAirDryerA;

View File

@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
import { Card, Typography, Flex } from 'antd';
import { setValSvg } from '../../components/Global/MqttConnection';
import SvgTemplate from './SvgTemplate';
import SvgViewer from './SvgViewer';
const { Text } = Typography;
const filePathSvg = '/svg/air_dryer_B_rev.svg';
const topicMqtt = 'PIU_GGCP/Devices/PB';
const SvgAirDryerB = () => {
return (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgAirDryerB;

View File

@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
import { Card, Typography, Flex } from 'antd';
import { setValSvg } from '../../components/Global/MqttConnection';
import SvgTemplate from './SvgTemplate';
import SvgViewer from './SvgViewer';
const { Text } = Typography;
const filePathSvg = '/svg/air_dryer_C_rev.svg';
const topicMqtt = 'PIU_GGCP/Devices/PB';
const SvgAirDryerC = () => {
return (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgAirDryerC;

View File

@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
import { Card, Typography, Flex } from 'antd';
import { setValSvg } from '../../components/Global/MqttConnection';
import SvgTemplate from './SvgTemplate';
import SvgViewer from './SvgViewer';
const { Text } = Typography;
const filePathSvg = '/svg/test-new.svg';
const topicMqtt = 'PIU_GGCP/Devices/PB';
const SvgCompressorA = () => {
return (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgCompressorA;

View File

@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
import { Card, Typography, Flex } from 'antd';
import { setValSvg } from '../../components/Global/MqttConnection';
import SvgTemplate from './SvgTemplate';
import SvgViewer from './SvgViewer';
const { Text } = Typography;
const filePathSvg = '/svg/test-new.svg';
const topicMqtt = 'PIU_GGCP/Devices/PB';
const SvgCompressorB = () => {
return (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgCompressorB;

View File

@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
import { Card, Typography, Flex } from 'antd';
import { setValSvg } from '../../components/Global/MqttConnection';
import SvgTemplate from './SvgTemplate';
import SvgViewer from './SvgViewer';
const { Text } = Typography;
const filePathSvg = '/svg/test-new.svg';
const topicMqtt = 'PIU_GGCP/Devices/PB';
const SvgCompressorC = () => {
return (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgCompressorC;

View File

@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
import { Card, Typography, Flex } from 'antd';
import { setValSvg } from '../../components/Global/MqttConnection';
import SvgTemplate from './SvgTemplate';
import SvgViewer from './SvgViewer';
const { Text } = Typography;
const filePathSvg = '/svg/test-new.svg';
const topicMqtt = 'PIU_GGCP/Devices/PB';
const SvgOverview = () => {
return (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgOverview;

View File

@@ -0,0 +1,19 @@
const SvgTemplate = ({ children }) => {
return (
<div
style={{
height: '80vh', // penuh 1 layar
width: '80vw', // penuh 1 layar lebar
overflow: 'hidden', // hilangkan scroll
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff', // opsional
}}
>
{children}
</div>
);
};
export default SvgTemplate;

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { Card, Typography, Flex } from 'antd';
// import { ReactSVG } from 'react-svg';
import { setValSvg } from '../../components/Global/MqttConnection';
import { ReactSVG } from 'react-svg';
const { Text } = Typography;

View File

@@ -0,0 +1,19 @@
// SvgViewer.jsx
import { ReactSVG } from 'react-svg';
const SvgViewer = ({ filePathSvg, topicMqtt, setValSvg }) => {
return (
<ReactSVG
src={filePathSvg}
beforeInjection={(svg) => {
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
if (setValSvg) setValSvg(topicMqtt, svg);
}}
style={{ width: '100%', height: '100%' }}
/>
);
};
export default SvgViewer;

View File

@@ -10,68 +10,42 @@ import {
import { NotifAlert, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
import { useNavigate } from 'react-router-dom';
import TableList from '../../../components/Global/TableList';
// --- DUMMY DATA (Initial State) --- //
const initialDummyData = [
{
id: 1,
nama_shift: 'Shift Pagi',
jam_masuk: '07:00',
jam_pulang: '15:00',
username: 'd.sanjaya',
nama_employee: 'Dede Sanjaya',
whatsapp: '081234567890'
},
{
id: 2,
nama_shift: 'Shift Siang',
jam_masuk: '15:00',
jam_pulang: '23:00',
username: 'a.wijaya',
nama_employee: 'Andi Wijaya',
whatsapp: '081234567891'
},
{
id: 3,
nama_shift: 'Shift Malam',
jam_masuk: '23:00',
jam_pulang: '07:00',
username: 'b.cahya',
nama_employee: 'Budi Cahya',
whatsapp: '081234567892'
},
];
import { getAllJadwalShift, deleteJadwalShift } from '../../../api/jadwal-shift';
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
{
title: 'Nama Karyawan',
dataIndex: 'nama_employee',
key: 'nama_employee',
},
{
title: 'Username',
dataIndex: 'username',
key: 'username',
title: 'Tanggal Jadwal',
dataIndex: 'schedule_date',
key: 'schedule_date',
render: (date) => date ? new Date(date).toLocaleDateString('id-ID') : '-',
},
{
title: 'Nama Shift',
dataIndex: 'nama_shift',
key: 'nama_shift',
dataIndex: 'shift_name',
key: 'shift_name',
render: (text) => text || '-',
},
{
title: 'Jam Masuk',
dataIndex: 'jam_masuk',
key: 'jam_masuk',
dataIndex: 'start_time',
key: 'start_time',
render: (time) => time || '-',
},
{
title: 'Jam Pulang',
dataIndex: 'jam_pulang',
key: 'jam_pulang',
dataIndex: 'end_time',
key: 'end_time',
render: (time) => time || '-',
},
{
title: 'Whatsapp',
dataIndex: 'whatsapp',
key: 'whatsapp',
title: 'Status',
dataIndex: 'is_active',
key: 'is_active',
render: (isActive) => (
<Tag color={isActive ? 'green' : 'red'}>
{isActive ? 'Aktif' : 'Tidak Aktif'}
</Tag>
),
},
{
title: 'Aksi',
@@ -101,39 +75,42 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
];
const ListJadwalShift = memo(function ListJadwalShift(props) {
const [dataSource, setDataSource] = useState(initialDummyData);
const [trigerFilter, setTrigerFilter] = useState(false);
const defaultFilter = { criteria: '' };
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
const [searchValue, setSearchValue] = useState('');
const navigate = useNavigate();
// --- DUMMY API --- //
const getDummyData = (queryParams) => {
return new Promise((resolve) => {
const { criteria } = queryParams;
let data = dataSource;
if (criteria) {
data = dataSource.filter(item =>
item.nama_employee.toLowerCase().includes(criteria.toLowerCase()) ||
item.username.toLowerCase().includes(criteria.toLowerCase())
);
}
setTimeout(() => {
resolve({
status: 200,
data: {
data: data,
paging: {
page: 1,
limit: 10,
total: data.length,
page_total: 1
}
const getData = async (queryParams) => {
try {
const params = new URLSearchParams({
page: queryParams.page || 1,
limit: queryParams.limit || 10,
criteria: queryParams.criteria || ''
});
const response = await getAllJadwalShift(params);
return response;
} catch (error) {
console.error('Error fetching jadwal shift:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Gagal mengambil data jadwal shift.',
});
return {
status: 500,
data: {
data: [],
paging: {
page: 1,
limit: 10,
total: 0,
page_total: 0
}
});
}, 100);
});
}
};
}
};
useEffect(() => {
@@ -146,7 +123,7 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
} else {
navigate('/signin');
}
}, [props.actionMode, dataSource]); // Added dataSource to dependency array
}, [props.actionMode]);
const doFilter = () => {
setTrigerFilter((prev) => !prev);
@@ -179,23 +156,41 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
};
const showDeleteDialog = (param) => {
const dateStr = param.schedule_date ? new Date(param.schedule_date).toLocaleDateString('id-ID') : 'tanggal tidak diketahui';
NotifConfirmDialog({
icon: 'question',
title: 'Konfirmasi Hapus',
message: `Jadwal shift untuk "${param.nama_employee}" akan dihapus?`,
onConfirm: () => handleDelete(param.id),
message: `Jadwal shift tanggal ${dateStr} akan dihapus?`,
onConfirm: () => handleDelete(param.schedule_id),
onCancel: () => props.setSelectedData(null),
});
};
const handleDelete = (id) => {
setDataSource(prevData => prevData.filter(item => item.id !== id));
NotifAlert({
icon: 'success',
title: 'Berhasil',
message: 'Data Jadwal Shift berhasil dihapus.',
});
doFilter(); // Trigger a re-fetch from the new state
const handleDelete = async (id) => {
try {
const response = await deleteJadwalShift(id);
if (response.statusCode === 200) {
NotifAlert({
icon: 'success',
title: 'Berhasil',
message: 'Data Jadwal Shift berhasil dihapus.',
});
doFilter();
} else {
NotifAlert({
icon: 'error',
title: 'Error',
message: response.message || 'Gagal menghapus data jadwal shift.',
});
}
} catch (error) {
console.error('Error deleting jadwal shift:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Terjadi kesalahan saat menghapus data.',
});
}
};
return (
@@ -206,7 +201,7 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
<Row justify="space-between" align="middle" gutter={[8, 8]}>
<Col xs={24} sm={24} md={12} lg={12}>
<Input.Search
placeholder="Cari berdasarkan nama atau username..."
placeholder="Cari jadwal shift..."
value={searchValue}
onChange={(e) => {
const value = e.target.value;
@@ -263,11 +258,11 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
<TableList
mobile
cardColor={'#42AAFF'}
header={'nama_employee'}
header={'schedule_date'}
showPreviewModal={showPreviewModal}
showEditModal={showEditModal}
showDeleteDialog={showDeleteDialog}
getData={getDummyData}
getData={getData}
queryParams={formDataFilter}
columns={columns(showPreviewModal, showEditModal, showDeleteDialog)}
triger={trigerFilter}

View File

@@ -0,0 +1,151 @@
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Card, Typography, Button, Modal, Form, Input, message } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import TableList from '../../../components/Global/TableList';
// import { getAllErrorCodesByBrand, createErrorCode, updateErrorCode, deleteErrorCode } from '../../api/master-errorcode'; // Mock this later
const { Title } = Typography;
// Mock API functions for now
const mockApi = {
errorCodes: [
{ error_code_id: 1, brand_id: 1, error_code: 'E-001', description: 'Paper Jam' },
{ error_code_id: 2, brand_id: 1, error_code: 'E-002', description: 'Low Ink' },
],
getAllErrorCodesByBrand: async (brandId) => {
return { status: 200, data: { data: mockApi.errorCodes.filter(ec => ec.brand_id == brandId) } };
},
createErrorCode: async (data) => {
const newId = Math.max(...mockApi.errorCodes.map(ec => ec.error_code_id)) + 1;
const newErrorCode = { ...data, error_code_id: newId };
mockApi.errorCodes.push(newErrorCode);
return { statusCode: 201, data: newErrorCode };
},
updateErrorCode: async (id, data) => {
const index = mockApi.errorCodes.findIndex(ec => ec.error_code_id === id);
if (index !== -1) {
mockApi.errorCodes[index] = { ...mockApi.errorCodes[index], ...data };
return { statusCode: 200, data: mockApi.errorCodes[index] };
}
return { statusCode: 404, message: 'Not Found' };
},
deleteErrorCode: async (id) => {
const index = mockApi.errorCodes.findIndex(ec => ec.error_code_id === id);
if (index !== -1) {
mockApi.errorCodes.splice(index, 1);
return { statusCode: 200 };
}
return { statusCode: 404, message: 'Not Found' };
}
};
const ErrorCodePage = () => {
const { brandId } = useParams();
const navigate = useNavigate();
const [form] = Form.useForm();
const [errorCodes, setErrorCodes] = useState([]);
const [loading, setLoading] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
const [editingErrorCode, setEditingErrorCode] = useState(null);
const fetchData = async () => {
setLoading(true);
const response = await mockApi.getAllErrorCodesByBrand(brandId);
if (response.status === 200) {
setErrorCodes(response.data.data);
}
setLoading(false);
};
useEffect(() => {
fetchData();
}, [brandId]);
const columns = [
{ title: 'Error Code', dataIndex: 'error_code', key: 'error_code' },
{ title: 'Description', dataIndex: 'description', key: 'description' },
{
title: 'Action',
key: 'action',
render: (_, record) => (
<>
<Button type="link" onClick={() => handleEdit(record)}>Edit</Button>
<Button type="link" danger onClick={() => handleDelete(record.error_code_id)}>Delete</Button>
</>
),
},
];
const handleAdd = () => {
setEditingErrorCode(null);
form.resetFields();
setIsModalVisible(true);
};
const handleEdit = (errorCode) => {
setEditingErrorCode(errorCode);
form.setFieldsValue(errorCode);
setIsModalVisible(true);
};
const handleDelete = async (id) => {
await mockApi.deleteErrorCode(id);
message.success('Error code deleted successfully');
fetchData();
};
const handleModalOk = async () => {
try {
const values = await form.validateFields();
if (editingErrorCode) {
await mockApi.updateErrorCode(editingErrorCode.error_code_id, values);
message.success('Error code updated successfully');
} else {
await mockApi.createErrorCode({ ...values, brand_id: brandId });
message.success('Error code created successfully');
}
setIsModalVisible(false);
fetchData();
} catch (error) {
console.log('Validate Failed:', error);
}
};
return (
<Card>
<Title level={4}>Manage Error Codes for Brand ID: {brandId}</Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAdd}
style={{ marginBottom: 16 }}
>
Add Error Code
</Button>
<TableList
columns={columns}
getData={async () => ({ data: { data: errorCodes } })}
triger={brandId}
/>
<Modal
title={editingErrorCode ? 'Edit Error Code' : 'Add Error Code'}
visible={isModalVisible}
onOk={handleModalOk}
onCancel={() => setIsModalVisible(false)}
>
<Form form={form} layout="vertical">
<Form.Item name="error_code" label="Error Code" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="description" label="Description" rules={[{ required: true }]}>
<Input.TextArea />
</Form.Item>
</Form>
</Modal>
</Card>
);
};
export default ErrorCodePage;

View File

@@ -0,0 +1,59 @@
import React, { useState } from 'react';
import { Form, Input, Button, Typography, Card, message } from 'antd';
import { useNavigate } from 'react-router-dom';
import { createBrand } from '../../api/master-brand';
const { Title } = Typography;
const FormBrand = () => {
const [form] = Form.useForm();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const onFinish = async (values) => {
setLoading(true);
try {
const response = await createBrand(values);
if (response.statusCode === 200 || response.statusCode === 201) {
message.success('Brand created successfully!');
const newBrandId = response.data.brand_id;
// Redirect to the error code page for the new brand
navigate(`/master/brand/${newBrandId}/error-codes`);
} else {
message.error(response.message || 'Failed to create brand.');
}
} catch (error) {
message.error('An error occurred while creating the brand.');
console.error(error);
}
setLoading(false);
};
return (
<Card>
<Title level={4}>Add New Brand</Title>
<Form
form={form}
layout="vertical"
onFinish={onFinish}
autoComplete="off"
>
<Form.Item
name="brand_name"
label="Brand Name"
rules={[{ required: true, message: 'Please input the brand name!' }]}
>
<Input placeholder="Enter brand name" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>
Lanjut ke Error Code
</Button>
</Form.Item>
</Form>
</Card>
);
};
export default FormBrand;

View File

@@ -0,0 +1,337 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Input, Divider, Typography, Switch, Button, Steps, Form, message, Table, Row, Col, Radio, Card, Tag, Upload, ConfigProvider } from 'antd';
import { PlusOutlined, UploadOutlined } from '@ant-design/icons';
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
const { Text, Title } = Typography;
const { Step } = Steps;
// Mock API for Error Codes (can be moved to a separate file later)
const mockErrorCodeApi = {
errorCodes: [],
createErrorCode: async (data) => {
const newId = mockErrorCodeApi.errorCodes.length > 0 ? Math.max(...mockErrorCodeApi.errorCodes.map(ec => ec.error_code_id)) + 1 : 1;
const newErrorCode = { ...data, error_code_id: newId };
mockErrorCodeApi.errorCodes.push(newErrorCode);
return { statusCode: 201, data: newErrorCode };
},
};
const AddBrandDevice = () => {
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [brandForm] = Form.useForm();
const [errorCodeForm] = Form.useForm();
const [confirmLoading, setConfirmLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [anotherSolutionType, setAnotherSolutionType] = useState(null);
const [fileList, setFileList] = useState([]);
// Watch for form values changes to update the switch color
const statusValue = Form.useWatch('status', errorCodeForm);
const defaultData = {
brandName: '',
brandType: '',
manufacturer: '',
model: '',
status: true,
brand_code: '',
country: '',
description: '',
};
const [formData, setFormData] = useState(defaultData);
const [errorCodes, setErrorCodes] = useState([]);
const handleCancel = () => {
navigate('/master/brand-device');
};
const handleNextStep = async () => {
try {
await brandForm.validateFields();
setCurrentStep(1);
} catch (error) {
console.log('Validate Failed:', error);
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk brand device!' });
}
};
const handleFinish = async () => {
if (errorCodes.length === 0) {
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Silakan tambahkan minimal satu error code.' });
return;
}
setConfirmLoading(true);
try {
const finalFormData = { ...formData, status: formData.status ? 'Active' : 'Inactive' };
console.log("Saving brand device:", finalFormData);
await new Promise((resolve) => setTimeout(resolve, 500));
const newBrandDeviceId = Date.now();
console.log("Brand device saved with ID:", newBrandDeviceId);
console.log("Saving error codes:", errorCodes);
for (const errorCode of errorCodes) {
if (errorCode.another_solution === 'image' && errorCode.image) {
console.log(`Uploading image for error code ${errorCode.error_code}:`, errorCode.image.name);
}
await mockErrorCodeApi.createErrorCode({
...errorCode,
brand_device_id: newBrandDeviceId
});
console.log("Saved error code:", errorCode.error_code);
}
setConfirmLoading(false);
NotifOk({ icon: 'success', title: 'Berhasil', message: 'Brand Device dan Error Code berhasil disimpan.' });
navigate('/master/brand-device');
} catch (error) {
setConfirmLoading(false);
console.error("Failed to save data:", error);
NotifAlert({
icon: "error",
title: "Gagal",
message: "Gagal menyimpan data. Silakan coba lagi.",
});
}
};
const handleAddErrorCode = async () => {
try {
const values = await errorCodeForm.validateFields();
const newErrorCode = {
...values,
status: values.status === undefined ? true : values.status,
image: fileList.length > 0 ? fileList[0] : null,
key: `temp-${Date.now()}`
};
setErrorCodes([...errorCodes, newErrorCode]);
message.success('Error code berhasil ditambahkan');
errorCodeForm.resetFields();
setAnotherSolutionType(null);
setFileList([]);
} catch (error) {
console.log('Validate Failed:', error);
NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Harap isi semua kolom wajib untuk error code!' });
}
};
const handleDeleteErrorCode = (key) => {
setErrorCodes(errorCodes.filter(item => item.key !== key));
message.success('Error code berhasil dihapus');
};
const uploadProps = {
onRemove: (file) => {
setFileList([]);
},
beforeUpload: (file) => {
setFileList([file]);
return false; // Prevent auto-upload
},
fileList,
};
const errorCodeColumns = [
{ title: 'Error Code', dataIndex: 'error_code', key: 'error_code' },
{ title: 'Trouble Description', dataIndex: 'description', key: 'description' },
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status) => (
<Tag color={status ? '#23A55A' : 'red'}>
{status ? 'Active' : 'Inactive'}
</Tag>
),
},
{
title: 'Action',
key: 'action',
render: (_, record) => (
<Button type="link" danger onClick={() => handleDeleteErrorCode(record.key)}>Delete</Button>
),
},
];
useEffect(() => {
brandForm.setFieldsValue(formData);
}, [formData, brandForm]);
useEffect(() => {
setBreadcrumbItems([
{ title: <Text strong style={{ fontSize: '14px' }}> Master</Text> },
{ title: <Text strong style={{ fontSize: '14px' }} onClick={() => navigate('/master/brand-device')}>Brand Device</Text> },
{ title: <Text strong style={{ fontSize: '14px' }}>Tambah Brand Device</Text> }
]);
}, [setBreadcrumbItems, navigate]);
const renderStepContent = () => {
if (currentStep === 0) {
return (
<Form layout="vertical" form={brandForm} onValuesChange={(changedValues, allValues) => setFormData(prev => ({...prev, ...allValues}))} initialValues={formData}>
<Form.Item label="Status" name="status" valuePropName="checked">
<Switch
checked={formData.status}
style={{ backgroundColor: formData.status ? '#23A55A' : '#bfbfbf' }}
/>
</Form.Item>
<Form.Item label="Brand Code" name="brand_code">
<Input placeholder={'Brand Code Auto Fill'} disabled style={{ backgroundColor: '#f5f5f5', cursor: 'not-allowed' }} />
</Form.Item>
<Form.Item label="Brand Name" name="brandName" rules={[{ required: true, message: 'Brand Name wajib diisi!' }]}>
<Input />
</Form.Item>
<Form.Item label="Brand Type" name="brandType" rules={[{ required: true, message: 'Type wajib diisi!' }]}>
<Input />
</Form.Item>
<Form.Item label="Model" name="model" rules={[{ required: true, message: 'Model wajib diisi!' }]}>
<Input />
</Form.Item>
<Form.Item label="Manufacturer" name="manufacturer" rules={[{ required: true, message: 'Manufacturer wajib diisi!' }]}>
<Input />
</Form.Item>
<Form.Item label="Country" name="country" rules={[{ required: true, message: 'Country wajib diisi!' }]}>
<Input />
</Form.Item>
<Form.Item label="Description" name="description">
<Input.TextArea rows={4} placeholder="Enter Description (Optional)" />
</Form.Item>
</Form>
);
}
if (currentStep === 1) {
return (
<div>
<Title level={5}>Tambah Error Code {errorCodes.length + 1}</Title>
<Form form={errorCodeForm} layout="vertical" initialValues={{ status: true }}>
<Form.Item label="Status" name="status" valuePropName="checked">
<Switch
style={{ backgroundColor: statusValue ? '#23A55A' : '#bfbfbf' }}
/>
</Form.Item>
<Form.Item name="error_code" label="Error Code" rules={[{ required: true, message: 'Error Code wajib diisi' }]}>
<Input />
</Form.Item>
<Form.Item name="description" label="Trouble Description" rules={[{ required: true, message: 'Trouble Description wajib diisi' }]}>
<Input.TextArea />
</Form.Item>
<Form.Item name="detected_method" label="Detected Method" rules={[{ required: true, message: 'Detected Method wajib diisi' }]}>
<Input />
</Form.Item>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="indicator_light" label="Indicator Light">
<Input />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="detector" label="Detector">
<Input />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="auto_shutdown" label="Auto Shutdown">
<Input />
</Form.Item>
</Col>
</Row>
<Form.Item name="what_action_to_take" label="What Action to Take" rules={[{ required: true, message: 'What Action to Take wajib diisi' }]}>
<Input.TextArea />
</Form.Item>
<Form.Item name="another_solution" label="Another Solution (opsional)">
<Radio.Group onChange={(e) => setAnotherSolutionType(e.target.value)}>
<Radio value="image">Image</Radio>
<Radio value="other">Other</Radio>
</Radio.Group>
</Form.Item>
{anotherSolutionType === 'image' && (
<Form.Item label="Upload Image">
<Upload {...uploadProps}>
<Button icon={<UploadOutlined />}>Click to Upload</Button>
</Upload>
</Form.Item>
)}
{anotherSolutionType === 'other' && (
<Form.Item name="another_solution_text" label="Enter Solution Text">
<Input.TextArea rows={4} />
</Form.Item>
)}
<Form.Item>
<Button type="dashed" icon={<PlusOutlined />} onClick={handleAddErrorCode} style={{ width: '100%' }}>
Tambah Error Code Lain
</Button>
</Form.Item>
</Form>
<Divider />
<Title level={5}>Daftar Error Code</Title>
<Table columns={errorCodeColumns} dataSource={errorCodes} rowKey="key" pagination={false} />
</div>
);
}
return null;
};
return (
<Card>
<Title level={4}>Tambah Brand Device</Title>
<Divider />
<Steps current={currentStep} style={{ marginBottom: 24 }}>
<Step title="Brand Device Details" />
<Step title="Error Codes" />
</Steps>
<div style={{ marginTop: 24 }}>
{renderStepContent()}
</div>
<Divider />
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<ConfigProvider
theme={{
token: { colorBgContainer: '#E9F6EF' },
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
<Button onClick={handleCancel}>Batal</Button>
{currentStep > 0 && (
<Button onClick={() => setCurrentStep(currentStep - 1)} style={{ marginRight: 8 }}>Kembali</Button>
)}
</ConfigProvider>
<ConfigProvider
theme={{
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
defaultHoverBg: '#209652', // A slightly darker shade for hover
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#23a55a',
},
},
}}
>
{currentStep < 1 && (
<Button loading={confirmLoading} onClick={handleNextStep}>Lanjut</Button>
)}
{currentStep === 1 && (
<Button loading={confirmLoading} onClick={handleFinish}>Simpan</Button>
)}
</ConfigProvider>
</div>
</Card>
);
};
export default AddBrandDevice;

View File

@@ -1,8 +1,6 @@
import React, { memo, useState, useEffect } from 'react';
import React, { memo, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import ListBrandDevice from './component/ListBrandDevice';
import DetailBrandDevice from './component/DetailBrandDevice';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { Typography } from 'antd';
@@ -12,35 +10,6 @@ const IndexBrandDevice = memo(function IndexBrandDevice() {
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [actionMode, setActionMode] = useState('list');
const [selectedData, setSelectedData] = useState(null);
const [readOnly, setReadOnly] = useState(false);
const [showModal, setShowmodal] = useState(false);
const setMode = (param) => {
setActionMode(param);
switch (param) {
case 'add':
setReadOnly(false);
setShowmodal(true);
break;
case 'edit':
setReadOnly(false);
setShowmodal(true);
break;
case 'preview':
setReadOnly(true);
setShowmodal(true);
break;
default:
setShowmodal(false);
break;
}
};
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
@@ -55,23 +24,9 @@ const IndexBrandDevice = memo(function IndexBrandDevice() {
return (
<React.Fragment>
<ListBrandDevice
actionMode={actionMode}
setActionMode={setMode}
selectedData={selectedData}
setSelectedData={setSelectedData}
readOnly={readOnly}
/>
<DetailBrandDevice
setActionMode={setMode}
selectedData={selectedData}
setSelectedData={setSelectedData}
readOnly={readOnly}
showModal={showModal}
actionMode={actionMode}
/>
<ListBrandDevice />
</React.Fragment>
);
});
export default IndexBrandDevice;
export default IndexBrandDevice;

View File

@@ -1,310 +0,0 @@
import { useEffect, useState } from 'react';
import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, Select } from 'antd';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
const { Text } = Typography;
const DetailBrandDevice = (props) => {
const [confirmLoading, setConfirmLoading] = useState(false);
const defaultData = {
brand_id: '',
brandName: '',
brandType: '',
manufacturer: '',
model: '',
status: 'Active',
};
const [FormData, setFormData] = useState(defaultData);
const handleCancel = () => {
props.setSelectedData(null);
props.setActionMode('list');
};
const handleSave = async () => {
setConfirmLoading(true);
// Validasi required fields
if (!FormData.brandName) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Kolom Brand Name Tidak Boleh Kosong',
});
setConfirmLoading(false);
return;
}
if (!FormData.brandType) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Kolom Type Tidak Boleh Kosong',
});
setConfirmLoading(false);
return;
}
if (!FormData.manufacturer) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Kolom Manufacturer Tidak Boleh Kosong',
});
setConfirmLoading(false);
return;
}
if (!FormData.model) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Kolom Model Tidak Boleh Kosong',
});
setConfirmLoading(false);
return;
}
if (!FormData.status) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Kolom Status Tidak Boleh Kosong',
});
setConfirmLoading(false);
return;
}
const payload = {
brandName: FormData.brandName,
brandType: FormData.brandType,
manufacturer: FormData.manufacturer,
model: FormData.model,
status: FormData.status,
};
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 500));
const response = {
statusCode: FormData.brand_id ? 200 : 201,
data: {
brandName: FormData.brandName,
},
};
console.log('Save Brand Device Response:', response);
// Check if response is successful
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `Data Brand Device "${
response.data?.brandName || FormData.brandName
}" berhasil ${FormData.brand_id ? 'diubah' : 'ditambahkan'}.`,
});
props.setActionMode('list');
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Terjadi kesalahan saat menyimpan data.',
});
}
} catch (error) {
console.error('Save Brand Device Error:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: error.message || 'Terjadi kesalahan pada server. Coba lagi nanti.',
});
}
setConfirmLoading(false);
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData({
...FormData,
[name]: value,
});
};
const handleSelectChange = (name, value) => {
setFormData({
...FormData,
[name]: value,
});
};
const handleStatusToggle = (event) => {
const isChecked = event;
setFormData({
...FormData,
status: isChecked ? true : false,
});
};
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
if (props.selectedData != null) {
setFormData(props.selectedData);
} else {
setFormData(defaultData);
}
} else {
// navigate('/signin'); // Uncomment if useNavigate is imported
}
}, [props.showModal]);
return (
<Modal
title={`${
props.actionMode === 'add'
? 'Tambah'
: props.actionMode === 'preview'
? 'Preview'
: 'Edit'
} Brand Device`}
open={props.showModal}
onCancel={handleCancel}
footer={[
<>
<ConfigProvider
theme={{
token: { colorBgContainer: '#E9F6EF' },
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
<Button onClick={handleCancel}>Batal</Button>
</ConfigProvider>
<ConfigProvider
theme={{
token: {
colorBgContainer: '#209652',
},
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#23a55a',
},
},
}}
>
{!props.readOnly && (
<Button loading={confirmLoading} onClick={handleSave}>
Simpan
</Button>
)}
</ConfigProvider>
</>,
]}
>
{FormData && (
<div>
<div>
<div>
<Text strong>Status</Text>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: '8px',
}}
>
<div style={{ marginRight: '8px' }}>
<Switch
disabled={props.readOnly}
style={{
backgroundColor:
FormData.status === true ? '#23A55A' : '#bfbfbf',
}}
checked={FormData.status === true}
onChange={handleStatusToggle}
/>
</div>
<div>
<Text>{FormData.status === true ? 'Active' : 'Inactive'}</Text>
</div>
</div>
</div>
<Divider style={{ margin: '12px 0' }} />
<div hidden>
<Text strong>Brand ID</Text>
<Input
name="brand_id"
value={FormData.brand_id}
onChange={handleInputChange}
disabled
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Brand Name</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="brandName"
value={FormData.brandName}
onChange={handleInputChange}
placeholder="Enter Brand Name"
readOnly={props.readOnly}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Type</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="brandType"
value={FormData.brandType}
onChange={handleInputChange}
placeholder="Enter Type (e.g., PLC)"
readOnly={props.readOnly}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Manufacturer</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="manufacturer"
value={FormData.manufacturer}
onChange={handleInputChange}
placeholder="Enter Manufacturer"
readOnly={props.readOnly}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Model</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="model"
value={FormData.model}
onChange={handleInputChange}
placeholder="Enter Model"
readOnly={props.readOnly}
/>
</div>
</div>
)}
</Modal>
);
};
export default DetailBrandDevice;

View File

@@ -10,6 +10,7 @@ import {
import { NotifAlert, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
import { useNavigate } from 'react-router-dom';
import TableList from '../../../../components/Global/TableList';
import { getAllBrands } from '../../../../api/master-brand';
// Dummy data
const initialBrandDeviceData = [
@@ -145,55 +146,6 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
const navigate = useNavigate();
// Dummy data function to simulate API call - now uses state
const getAllBrandDevice = async (params) => {
// Simulate API delay
await new Promise((resolve) => setTimeout(resolve, 300));
// Extract URLSearchParams - TableList sends URLSearchParams object
const searchParam = params.get('search') || '';
const page = parseInt(params.get('page')) || 1;
const limit = parseInt(params.get('limit')) || 10;
console.log('getAllBrandDevice called with:', { searchParam, page, limit });
// Filter by search
let filteredBrandDevices = brandDeviceData;
if (searchParam) {
const searchLower = searchParam.toLowerCase();
filteredBrandDevices = brandDeviceData.filter(
(brand) =>
brand.brandName.toLowerCase().includes(searchLower) ||
brand.brandType.toLowerCase().includes(searchLower) ||
brand.manufacturer.toLowerCase().includes(searchLower) ||
brand.model.toLowerCase().includes(searchLower)
);
}
// Pagination logic
const totalData = filteredBrandDevices.length;
const totalPages = Math.ceil(totalData / limit);
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedData = filteredBrandDevices.slice(startIndex, endIndex);
// Return structure that matches TableList expectation
return {
status: 200,
statusCode: 200,
data: {
data: paginatedData,
total: totalData,
paging: {
page: page,
limit: limit,
total: totalData,
page_total: totalPages,
},
},
};
};
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
@@ -231,11 +183,6 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
props.setActionMode('edit');
};
const showAddModal = (param = null) => {
props.setSelectedData(param);
props.setActionMode('add');
};
const showDeleteDialog = (param) => {
NotifConfirmDialog({
icon: 'question',
@@ -320,7 +267,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
>
<Button
icon={<PlusOutlined />}
onClick={() => showAddModal()}
onClick={() => navigate('/master/brand-device/add')}
size="large"
>
Tambah Brand Device
@@ -338,7 +285,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
showPreviewModal={showPreviewModal}
showEditModal={showEditModal}
showDeleteDialog={showDeleteDialog}
getData={getAllBrandDevice}
getData={getAllBrands}
queryParams={formDataFilter}
columns={columns(showPreviewModal, showEditModal, showDeleteDialog)}
triger={trigerFilter}
@@ -350,4 +297,4 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
);
});
export default ListBrandDevice;
export default ListBrandDevice;

View File

@@ -1,20 +1,8 @@
import React, { useEffect, useState } from 'react';
import {
Modal,
Input,
Divider,
Typography,
Switch,
Button,
ConfigProvider,
Radio,
Select,
} from 'antd';
import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider } from 'antd';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import { createApd, getJenisPermit, updateApd } from '../../../../api/master-apd';
import { createDevice, updateDevice } from '../../../../api/master-device';
import { Checkbox } from 'antd';
const CheckboxGroup = Checkbox.Group;
import { validateRun } from '../../../../Utils/validate';
const { Text } = Typography;
const { TextArea } = Input;
@@ -27,155 +15,61 @@ const DetailDevice = (props) => {
device_code: '',
device_name: '',
is_active: true,
device_location: 'Building A',
device_location: '',
device_description: '',
ip_address: '',
};
const [FormData, setFormData] = useState(defaultData);
const [jenisPermit, setJenisPermit] = useState([]);
const [checkedList, setCheckedList] = useState([]);
const onChange = (list) => {
setCheckedList(list);
};
const onChangeRadio = (e) => {
setFormData({
...FormData,
type_input: e.target.value,
});
};
const getDataJenisPermit = async () => {
setCheckedList([]);
const result = await getJenisPermit();
const data = result.data ?? [];
const names = data.map((item) => ({
value: item.id_jenis_permit,
label: item.nama_jenis_permit,
}));
setJenisPermit(names);
};
const [formData, setFormData] = useState(defaultData);
const handleCancel = () => {
props.setSelectedData(null);
props.setActionMode('list');
};
const validateIPAddress = (ip) => {
const ipRegex =
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
return ipRegex.test(ip);
};
const handleSave = async () => {
setConfirmLoading(true);
// Validasi required fields
// if (!FormData.device_code) {
// NotifOk({
// icon: 'warning',
// title: 'Peringatan',
// message: 'Kolom Device Code Tidak Boleh Kosong',
// });
// setConfirmLoading(false);
// return;
// }
// Daftar aturan validasi
const validationRules = [
{ field: 'device_name', label: 'Device Name', required: true },
{ field: 'ip_address', label: 'Ip Address', required: true, ip: true },
];
if (!FormData.device_name) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Kolom Device Name Tidak Boleh Kosong',
});
setConfirmLoading(false);
if (
validateRun(formData, validationRules, (errorMessages) => {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: errorMessages,
});
setConfirmLoading(false);
})
)
return;
}
if (!FormData.device_location) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Kolom Device Location Tidak Boleh Kosong',
});
setConfirmLoading(false);
return;
}
if (!FormData.ip_address) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Kolom IP Address Tidak Boleh Kosong',
});
setConfirmLoading(false);
return;
}
// Validasi format IP
if (!validateIPAddress(FormData.ip_address)) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Format IP Address Tidak Valid',
});
setConfirmLoading(false);
return;
}
if (props.permitDefault && checkedList.length === 0) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Kolom Jenis Permit Tidak Boleh Kosong',
});
setConfirmLoading(false);
return;
}
// Backend validation schema doesn't include device_code
const payload = {
device_name: FormData.device_name,
is_active: FormData.is_active,
device_location: FormData.device_location,
ip_address: FormData.ip_address,
};
// For CREATE: device_description is required (cannot be empty)
// For UPDATE: device_description is optional
if (!FormData.device_id) {
// Creating - ensure description is not empty
payload.device_description = FormData.device_description || '-';
} else {
// Updating - include description as-is
payload.device_description = FormData.device_description;
}
console.log('Payload to send:', payload);
try {
let response;
if (!FormData.device_id) {
response = await createDevice(payload);
} else {
response = await updateDevice(FormData.device_id, payload);
}
const payload = {
device_name: formData.device_name,
is_active: formData.is_active,
device_location: formData.device_location,
device_description: formData.device_description,
ip_address: formData.ip_address,
};
console.log('Save Device Response:', response);
const response = formData.device_id
? await updateDevice(formData.device_id, payload)
: await createDevice(payload);
// Check if response is successful
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
// Response.data is now a single object (already extracted from array)
const deviceName = response.data?.device_name || FormData.device_name;
const deviceName = response.data?.device_name || formData.device_name;
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `Data Device "${deviceName}" berhasil ${
FormData.device_id ? 'diubah' : 'ditambahkan'
formData.device_id ? 'diubah' : 'ditambahkan'
}.`,
});
@@ -202,7 +96,7 @@ const DetailDevice = (props) => {
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData({
...FormData,
...formData,
[name]: value,
});
};
@@ -210,35 +104,22 @@ const DetailDevice = (props) => {
const handleStatusToggle = (event) => {
const isChecked = event;
setFormData({
...FormData,
...formData,
is_active: isChecked ? true : false,
});
};
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
// Only call getDataJenisPermit if permitDefault is enabled
if (props.permitDefault) {
getDataJenisPermit();
}
if (props.selectedData != null) {
setFormData(props.selectedData);
if (props.permitDefault && props.selectedData.jenis_permit_default_arr) {
setCheckedList(props.selectedData.jenis_permit_default_arr);
}
} else {
setFormData(defaultData);
}
if (props.selectedData) {
setFormData(props.selectedData);
} else {
// navigate('/signin'); // Uncomment if useNavigate is imported
setFormData(defaultData);
}
}, [props.showModal]);
}, [props.showModal, props.selectedData, props.actionMode]);
return (
<Modal
// title={`${FormData.id_apd === '' ? 'Tambah' : 'Edit'} APD`}
// title={`${formData.id_apd === '' ? 'Tambah' : 'Edit'} APD`}
title={`${
props.actionMode === 'add'
? 'Tambah'
@@ -293,7 +174,7 @@ const DetailDevice = (props) => {
</React.Fragment>,
]}
>
{FormData && (
{formData && (
<div>
<div>
<div>
@@ -311,14 +192,14 @@ const DetailDevice = (props) => {
disabled={props.readOnly}
style={{
backgroundColor:
FormData.is_active === true ? '#23A55A' : '#bfbfbf',
formData.is_active === true ? '#23A55A' : '#bfbfbf',
}}
checked={FormData.is_active === true}
checked={formData.is_active === true}
onChange={handleStatusToggle}
/>
</div>
<div>
<Text>{FormData.is_active === true ? 'Running' : 'Offline'}</Text>
<Text>{formData.is_active === true ? 'Running' : 'Offline'}</Text>
</div>
</div>
</div>
@@ -327,28 +208,32 @@ const DetailDevice = (props) => {
<Text strong>Device ID</Text>
<Input
name="device_id"
value={FormData.device_id}
value={formData.device_id}
onChange={handleInputChange}
disabled
/>
</div>
{/* <div style={{ marginBottom: 12 }}>
{/* Device Code - Auto Increment & Read Only */}
<div style={{ marginBottom: 12 }}>
<Text strong>Device Code</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="device_code"
value={FormData.device_code}
onChange={handleInputChange}
placeholder="Enter Device Code"
readOnly={props.readOnly}
value={formData.device_code}
placeholder={'Device Code Auto Fill'}
disabled
style={{
backgroundColor: '#f5f5f5',
cursor: 'not-allowed',
color: formData.device_code ? '#000000' : '#bfbfbf',
}}
/>
</div> */}
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Device Name</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="device_name"
value={FormData.device_name}
value={formData.device_name}
onChange={handleInputChange}
placeholder="Enter Device Name"
readOnly={props.readOnly}
@@ -356,11 +241,10 @@ const DetailDevice = (props) => {
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Device Location</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
type="text"
name="device_location"
value={FormData.device_location}
value={formData.device_location}
onChange={handleInputChange}
placeholder="Enter Device Location"
readOnly={props.readOnly}
@@ -371,7 +255,7 @@ const DetailDevice = (props) => {
<Text style={{ color: 'red' }}> *</Text>
<Input
name="ip_address"
value={FormData.ip_address}
value={formData.ip_address}
onChange={handleInputChange}
placeholder="e.g. 192.168.1.1"
readOnly={props.readOnly}
@@ -381,26 +265,13 @@ const DetailDevice = (props) => {
<Text strong>Device Description</Text>
<TextArea
name="device_description"
value={FormData.device_description}
value={formData.device_description}
onChange={handleInputChange}
placeholder="Enter Device Description (Optional)"
readOnly={props.readOnly}
rows={4}
/>
</div>
{props.permitDefault && (
<div>
<Text strong>Jenis Permit</Text>
<Text style={{ color: 'red' }}> *</Text>
<CheckboxGroup
options={jenisPermit}
value={checkedList}
onChange={onChange}
disabled={props.readOnly}
/>
</div>
)}
</div>
)}
</Modal>

View File

@@ -48,13 +48,13 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
},
{
title: 'Status',
dataIndex: 'device_status',
key: 'device_status',
dataIndex: 'is_active',
key: 'is_active',
width: '10%',
align: 'center',
render: (_, { device_status }) => (
render: (_, { is_active }) => (
<>
{device_status === true ? (
{is_active === true ? (
<Tag color={'green'} key={'status'}>
Running
</Tag>

View File

@@ -1,15 +1,8 @@
import React, { useEffect, useState } from 'react';
import {
Modal,
Input,
Typography,
Switch,
Button,
ConfigProvider,
Divider,
} from 'antd';
import { Modal, Input, Typography, Switch, Button, ConfigProvider, Divider } from 'antd';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import { createPlantSection, updatePlantSection } from '../../../../api/master-plant-section';
import { validateRun } from '../../../../Utils/validate';
const { Text } = Typography;
@@ -23,12 +16,12 @@ const DetailPlantSection = (props) => {
is_active: true,
};
const [FormData, setFormData] = useState(defaultData);
const [formData, setFormData] = useState(defaultData);
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData({
...FormData,
...formData,
[name]: value,
});
};
@@ -41,52 +34,53 @@ const DetailPlantSection = (props) => {
const handleSave = async () => {
setConfirmLoading(true);
if (!FormData.sub_section_name) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Kolom Plant Sub Section Name Tidak Boleh Kosong',
});
setConfirmLoading(false);
// Daftar aturan validasi
const validationRules = [
{ field: 'sub_section_name', label: 'Plant Sub Section Name', required: true },
];
if (
validateRun(formData, validationRules, (errorMessages) => {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: errorMessages,
});
setConfirmLoading(false);
})
)
return;
}
try {
let response;
let payload;
const payload = {
is_active: formData.is_active,
sub_section_name: formData.sub_section_name,
};
if (props.actionMode === 'edit') {
payload = {
is_active: FormData.is_active,
sub_section_name: FormData.sub_section_name
};
response = await updatePlantSection(FormData.sub_section_id, payload);
} else {
// Backend generates the code, so we only send the name and status
payload = {
sub_section_name: FormData.sub_section_name,
is_active: FormData.is_active,
}
response = await createPlantSection(payload);
}
const response =
props.actionMode === 'edit'
? await updatePlantSection(formData.sub_section_id, payload)
: await createPlantSection(payload);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
const action = props.actionMode === 'edit' ? 'diubah' : 'ditambahkan';
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `Data Plant Section berhasil ${action}.`,
});
props.setActionMode('list');
} else {
NotifAlert({
NotifOk({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Terjadi kesalahan saat menyimpan data.',
});
}
} catch (error) {
NotifAlert({
NotifOk({
icon: 'error',
title: 'Error',
message: error.message || 'Terjadi kesalahan pada server.',
@@ -95,26 +89,25 @@ const DetailPlantSection = (props) => {
setConfirmLoading(false);
}
};
const handleStatusToggle = (checked) => {
setFormData({
...FormData,
...formData,
is_active: checked,
});
};
useEffect(() => {
if (props.selectedData) {
setFormData(props.selectedData);
} else {
setFormData(defaultData);
}
}, [props.showModal, props.selectedData]);
}, [props.showModal, props.selectedData, props.actionMode]);
return (
<Modal
title={`${
title={`${
props.actionMode === 'add'
? 'Tambah'
: props.actionMode === 'preview'
@@ -158,7 +151,7 @@ const DetailPlantSection = (props) => {
</React.Fragment>,
]}
>
{FormData && (
{formData && (
<div>
<div>
<div>
@@ -169,38 +162,41 @@ const DetailPlantSection = (props) => {
<Switch
disabled={props.readOnly}
style={{
backgroundColor: FormData.is_active ? '#23A55A' : '#bfbfbf',
backgroundColor: formData.is_active ? '#23A55A' : '#bfbfbf',
}}
checked={FormData.is_active}
checked={formData.is_active}
onChange={handleStatusToggle}
/>
</div>
<div>
<Text>
{FormData.is_active ? 'Active' : 'Inactive'}
</Text>
<Text>{formData.is_active ? 'Active' : 'Inactive'}</Text>
</div>
</div>
</div>
<Divider style={{ margin: '12px 0' }} />
{props.actionMode !== 'add' && (
<div style={{ marginBottom: 12 }}>
<Text strong>Plant Section Code</Text>
<Input
name="sub_section_code"
value={FormData.sub_section_code}
disabled
/>
</div>
)}
{/* Plant Section Code - Auto Increment & Read Only */}
<div style={{ marginBottom: 12 }}>
<Text strong>Plant Section Code</Text>
<Input
name="sub_section_code"
value={formData.sub_section_code || ''}
placeholder={'Plant Sub Section Code Auto Fill'}
disabled
style={{
backgroundColor: '#f5f5f5',
cursor: 'not-allowed',
color: formData.sub_section_code ? '#000000' : '#bfbfbf',
}}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Plant Sub Section Name</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="sub_section_name"
value={FormData.sub_section_name}
value={formData.sub_section_name}
onChange={handleInputChange}
placeholder="Enter Plant Sub Section Name"
readOnly={props.readOnly}
@@ -212,4 +208,4 @@ const DetailPlantSection = (props) => {
);
};
export default DetailPlantSection;
export default DetailPlantSection;

View File

@@ -1,83 +1,76 @@
import { useState, useEffect } from 'react';
import React, { memo, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import ListShift from './component/ListShift';
import DetailShift from './component/DetailShift';
import { getAllShift } from '../../../api/master-shift';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { Typography } from 'antd';
const { Text } = Typography;
const IndexShift = memo(function IndexShift() {
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const IndexShift = () => {
const [actionMode, setActionMode] = useState('list');
const [selectedData, setSelectedData] = useState(null);
const [shiftData, setShiftData] = useState([]);
const [loading, setLoading] = useState(false);
const [readOnly, setReadOnly] = useState(false);
const [showModal, setShowModal] = useState(false);
const fetchData = async () => {
setLoading(true);
try {
const localData = localStorage.getItem('shiftData');
if (localData) {
setShiftData(JSON.parse(localData));
} else {
const response = await getAllShift();
if (response.data) {
setShiftData(response.data.data);
localStorage.setItem('shiftData', JSON.stringify(response.data.data));
}
}
} catch (error) {
console.error("Error fetching shift data:", error);
const setMode = (param) => {
setActionMode(param);
switch (param) {
case 'add':
setReadOnly(false);
setShowModal(true);
break;
case 'edit':
setReadOnly(false);
setShowModal(true);
break;
case 'preview':
setReadOnly(true);
setShowModal(true);
break;
default:
setShowModal(false);
break;
}
setLoading(false);
};
useEffect(() => {
fetchData();
const token = localStorage.getItem('token');
if (token) {
setBreadcrumbItems([
{ title: <Text strong style={{ fontSize: '14px' }}> Master</Text> },
{ title: <Text strong style={{ fontSize: '14px' }}>Shift</Text> }
]);
} else {
navigate('/signin');
}
}, []);
const handleAddShift = (newShift) => {
const newData = { ...newShift, id: Date.now() }; // Simulate adding an ID
const updatedData = [newData, ...shiftData];
setShiftData(updatedData);
localStorage.setItem('shiftData', JSON.stringify(updatedData));
setActionMode('list');
};
const handleUpdateShift = (updatedShift) => {
const updatedData = shiftData.map(shift => shift.id === updatedShift.id ? updatedShift : shift);
setShiftData(updatedData);
localStorage.setItem('shiftData', JSON.stringify(updatedData));
setActionMode('list');
};
const handleDeleteShift = (id) => {
const updatedData = shiftData.filter(shift => shift.id !== id);
setShiftData(updatedData);
localStorage.setItem('shiftData', JSON.stringify(updatedData));
};
return (
<>
{actionMode === 'list' && (
<ListShift
setActionMode={setActionMode}
setSelectedData={setSelectedData}
shiftData={shiftData}
loading={loading}
onDelete={handleDeleteShift}
fetchData={fetchData}
/>
)}
{(actionMode === 'add' || actionMode === 'edit' || actionMode === 'preview') && (
<DetailShift
actionMode={actionMode}
selectedData={selectedData}
setActionMode={setActionMode}
setSelectedData={setSelectedData}
onAdd={handleAddShift}
onUpdate={handleUpdateShift}
/>
)}
</>
<React.Fragment>
<ListShift
actionMode={actionMode}
setActionMode={setMode}
selectedData={selectedData}
setSelectedData={setSelectedData}
readOnly={readOnly}
/>
<DetailShift
setActionMode={setMode}
selectedData={selectedData}
setSelectedData={setSelectedData}
readOnly={readOnly}
showModal={showModal}
actionMode={actionMode}
/>
</React.Fragment>
);
};
});
export default IndexShift;

View File

@@ -2,18 +2,22 @@ import { useEffect, useState } from 'react';
import { Modal, Input, Typography, Switch, Button, ConfigProvider, Divider } from 'antd';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import { createShift, updateShift } from '../../../../api/master-shift';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
const { Text } = Typography;
const DetailShift = (props) => {
const [confirmLoading, setConfirmLoading] = useState(false);
const readOnly = props.actionMode === 'preview';
const defaultData = {
id: '',
nama_shift: '',
jam_shift: '',
status: true, // default to active
shift_id: '',
shift_name: '',
start_time: '',
end_time: '',
is_active: true,
};
const [FormData, setFormData] = useState(defaultData);
@@ -26,68 +30,225 @@ const DetailShift = (props) => {
const handleSave = async () => {
setConfirmLoading(true);
if (!FormData.nama_shift) {
NotifOk({ icon: 'warning', title: 'Peringatan', message: 'Kolom Nama Shift Tidak Boleh Kosong' });
// Validasi required fields
if (!FormData.shift_name || FormData.shift_name.trim() === '') {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Kolom Nama Shift Tidak Boleh Kosong',
});
setConfirmLoading(false);
return;
}
if (!FormData.jam_shift) {
NotifOk({ icon: 'warning', title: 'Peringatan', message: 'Kolom Jam Shift Tidak Boleh Kosong' });
if (!FormData.start_time || FormData.start_time.trim() === '') {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Kolom Jam Mulai Tidak Boleh Kosong',
});
setConfirmLoading(false);
return;
}
const payload = {
nama_shift: FormData.nama_shift,
jam_shift: FormData.jam_shift,
status: FormData.status,
};
if (!FormData.end_time || FormData.end_time.trim() === '') {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Kolom Jam Selesai Tidak Boleh Kosong',
});
setConfirmLoading(false);
return;
}
// Validate time format
const timePattern = /^([01]\d|2[0-3]):([0-5]\d)(:[0-5]\d)?$/;
if (!timePattern.test(FormData.start_time)) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message:
'Format Jam Mulai tidak valid. Gunakan format HH:mm atau HH:mm:ss (contoh: 08:00)',
});
setConfirmLoading(false);
return;
}
if (!timePattern.test(FormData.end_time)) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message:
'Format Jam Selesai tidak valid. Gunakan format HH:mm atau HH:mm:ss (contoh: 17:00)',
});
setConfirmLoading(false);
return;
}
try {
if (FormData.id) {
props.onUpdate(payload);
if (FormData.shift_id) {
// Update existing shift
const payload = {
shift_name: FormData.shift_name,
start_time: FormData.start_time,
end_time: FormData.end_time,
is_active: FormData.is_active,
};
const response = await updateShift(FormData.shift_id, payload);
console.log('updateShift response:', response);
if (response.statusCode === 200) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `Data Shift "${FormData.shift_name}" berhasil diubah.`,
});
props.setActionMode('list');
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response.message || 'Gagal mengubah data Shift.',
});
}
} else {
props.onAdd(payload);
// Create new shift
const payload = {
shift_name: FormData.shift_name,
start_time: FormData.start_time,
end_time: FormData.end_time,
is_active: FormData.is_active,
};
const response = await createShift(payload);
console.log('createShift response:', response);
if (response.statusCode === 200 || response.statusCode === 201) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `Data Shift "${FormData.shift_name}" berhasil ditambahkan.`,
});
props.setActionMode('list');
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response.message || 'Gagal menambahkan data Shift.',
});
}
}
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `Data Shift "${payload.nama_shift}" berhasil ${FormData.id ? 'diubah' : 'ditambahkan'}.`,
});
} catch (error) {
console.error('Save Shift Error:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: error.message || 'Terjadi kesalahan pada server. Coba lagi nanti.',
message: error.message || 'Terjadi kesalahan saat menyimpan data.',
});
}
setConfirmLoading(false);
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData({ ...FormData, [name]: value });
// Helper function to format time input
const formatTimeInput = (value) => {
if (!value) return value;
// Remove any whitespace
value = value.trim();
// If user inputs single digit hour like "8:00", convert to "08:00"
const timeRegex = /^(\d{1,2}):(\d{2})(:\d{2})?$/;
const match = value.match(timeRegex);
if (match) {
const hours = match[1].padStart(2, '0');
const minutes = match[2];
const seconds = match[3] || '';
return `${hours}:${minutes}${seconds}`;
}
return value;
};
const handleStatusToggle = (checked) => {
setFormData({ ...FormData, status: checked });
const handleInputChange = (e) => {
const { name, value } = e.target;
// Just set the value without formatting during typing
setFormData({
...FormData,
[name]: value,
});
};
// Format time when user leaves the input field (onBlur)
const handleTimeBlur = (e) => {
const { name, value } = e.target;
if (name === 'start_time' || name === 'end_time') {
const formattedValue = formatTimeInput(value);
setFormData({
...FormData,
[name]: formattedValue,
});
}
};
const handleStatusToggle = (isChecked) => {
setFormData({
...FormData,
is_active: isChecked,
});
};
// Helper function to extract time from ISO timestamp using dayjs
const extractTime = (timeString) => {
if (!timeString) return '';
// If it's ISO timestamp like "1970-01-01T08:00:00.000Z"
if (timeString.includes('T')) {
return dayjs.utc(timeString).format('HH:mm');
}
// If it's already in HH:mm:ss format, remove seconds
if (timeString.includes(':')) {
const parts = timeString.split(':');
return `${parts[0]}:${parts[1]}`;
}
return timeString;
};
useEffect(() => {
if (props.selectedData) {
setFormData(props.selectedData);
} else {
setFormData(defaultData);
const token = localStorage.getItem('token');
if (token) {
if (props.selectedData != null) {
// Only set fields that are in defaultData
const filteredData = {
shift_id: props.selectedData.shift_id || '',
shift_name: props.selectedData.shift_name || '',
start_time: extractTime(props.selectedData.start_time) || '',
end_time: extractTime(props.selectedData.end_time) || '',
is_active: props.selectedData.is_active ?? true,
};
setFormData(filteredData);
} else {
setFormData(defaultData);
}
}
}, [props.actionMode, props.selectedData]);
}, [props.showModal]);
return (
<Modal
title={`${props.actionMode === 'add' ? 'Tambah' : props.actionMode === 'preview' ? 'Preview' : 'Edit'} Shift`}
open={props.actionMode !== 'list'}
title={`${
props.actionMode === 'add'
? 'Tambah'
: props.actionMode === 'preview'
? 'Preview'
: 'Edit'
} Shift`}
open={props.showModal}
onCancel={handleCancel}
footer={[
<>
@@ -100,7 +261,6 @@ const DetailShift = (props) => {
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
@@ -123,7 +283,7 @@ const DetailShift = (props) => {
},
}}
>
{!readOnly && (
{!props.readOnly && (
<Button loading={confirmLoading} onClick={handleSave}>
Simpan
</Button>
@@ -134,7 +294,8 @@ const DetailShift = (props) => {
>
{FormData && (
<div>
<div>
{/* Status Toggle */}
<div style={{ marginBottom: 12 }}>
<div>
<Text strong>Status</Text>
</div>
@@ -147,42 +308,66 @@ const DetailShift = (props) => {
>
<div style={{ marginRight: '8px' }}>
<Switch
disabled={readOnly}
disabled={props.readOnly}
style={{
backgroundColor:
FormData.status === true ? '#23A55A' : '#bfbfbf',
FormData.is_active === true ? '#23A55A' : '#bfbfbf',
}}
checked={FormData.status === true}
checked={FormData.is_active === true}
onChange={handleStatusToggle}
/>
</div>
<div>
<Text>{FormData.status === true ? 'Active' : 'Inactive'}</Text>
<Text>{FormData.is_active === true ? 'Active' : 'Inactive'}</Text>
</div>
</div>
</div>
<Divider style={{ margin: '12px 0' }} />
<div style={{ marginBottom: 12 }}>
<Text strong>Nama Shift</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="nama_shift"
value={FormData.nama_shift}
name="shift_name"
value={FormData.shift_name}
onChange={handleInputChange}
placeholder="Masukkan Nama Shift"
readOnly={readOnly}
readOnly={props.readOnly}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Jam Shift</Text>
<Text strong>Jam Mulai</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="jam_shift"
value={FormData.jam_shift}
name="start_time"
value={FormData.start_time}
onChange={handleInputChange}
placeholder="Contoh: 08:00 - 17:00"
readOnly={readOnly}
placeholder="Masukkan Jam Mulai"
readOnly={props.readOnly}
maxLength={8}
/>
<Text
type="secondary"
style={{ fontSize: '12px', display: 'block', marginTop: '4px' }}
>
Contoh: 08:00 atau 08:00:00
</Text>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Jam Selesai</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="end_time"
value={FormData.end_time}
onChange={handleInputChange}
placeholder="Masukkan Jam Selesai"
readOnly={props.readOnly}
maxLength={8}
/>
<Text
type="secondary"
style={{ fontSize: '12px', display: 'block', marginTop: '4px' }}
>
Contoh: 17:00 atau 17:00:00
</Text>
</div>
</div>
)}
@@ -190,4 +375,4 @@ const DetailShift = (props) => {
);
};
export default DetailShift;
export default DetailShift;

View File

@@ -1,183 +1,305 @@
import React, { memo, useState, useEffect } from 'react';
import { Button, Col, Row, Input, ConfigProvider, Card, Tag, Table, Space } from 'antd';
import { Space, Tag, ConfigProvider, Button, Row, Col, Card, Input } from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
SearchOutlined,
EyeOutlined,
SyncOutlined,
SearchOutlined,
} from '@ant-design/icons';
import { NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
import { NotifAlert, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
import { useNavigate } from 'react-router-dom';
import TableList from '../../../../components/Global/TableList';
import { getAllShift, deleteShift } from '../../../../api/master-shift';
const ListShift = (props) => {
// Helper function to extract time from ISO timestamp
const extractTime = (timeString) => {
if (!timeString) return '-';
// If it's ISO timestamp like "1970-01-01T08:00:00.000Z"
if (timeString.includes('T')) {
const date = new Date(timeString);
const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
// If it's already in HH:mm or HH:mm:ss format
if (timeString.includes(':')) {
const parts = timeString.split(':');
return `${parts[0]}:${parts[1]}`; // Return HH:mm only
}
return timeString;
};
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
{
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{
title: 'Nama Shift',
dataIndex: 'shift_name',
key: 'shift_name',
width: '20%',
},
{
title: 'Jam Mulai',
dataIndex: 'start_time',
key: 'start_time',
width: '15%',
render: (time) => extractTime(time),
},
{
title: 'Jam Selesai',
dataIndex: 'end_time',
key: 'end_time',
width: '15%',
render: (time) => extractTime(time),
},
{
title: 'Status',
dataIndex: 'is_active',
key: 'is_active',
width: '10%',
align: 'center',
render: (_, { is_active }) => {
const color = is_active ? 'green' : 'red';
const text = is_active ? 'Active' : 'Inactive';
return (
<Tag color={color} key={'status'}>
{text}
</Tag>
);
},
},
{
title: 'Aksi',
key: 'aksi',
align: 'center',
width: '20%',
render: (_, record) => (
<Space>
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => showPreviewModal(record)}
style={{
color: '#1890ff',
borderColor: '#1890ff',
}}
/>
<Button
type="text"
icon={<EditOutlined />}
onClick={() => showEditModal(record)}
style={{
color: '#faad14',
borderColor: '#faad14',
}}
/>
<Button
danger
type="text"
icon={<DeleteOutlined />}
onClick={() => showDeleteDialog(record)}
style={{
borderColor: '#ff4d4f',
}}
/>
</Space>
),
},
];
const ListShift = memo(function ListShift(props) {
const [trigerFilter, setTrigerFilter] = useState(false);
const defaultFilter = {
criteria: '',
};
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
const [searchValue, setSearchValue] = useState('');
const navigate = useNavigate();
useEffect(() => {
const token = localStorage.getItem('token');
if (!token) {
if (token) {
if (props.actionMode == 'list') {
doFilter();
}
} else {
navigate('/signin');
}
}, [navigate]);
}, [props.actionMode]);
const handleSearch = (value) => {
// This will be handled by the parent component if server-side search is needed
console.log('Search value:', value);
const doFilter = () => {
setTrigerFilter((prev) => !prev);
};
const handleSearch = () => {
setFormDataFilter((prev) => ({ ...prev, criteria: searchValue }));
doFilter();
};
const handleSearchClear = () => {
setSearchValue('');
// This will be handled by the parent component if server-side search is needed
setFormDataFilter((prev) => ({ ...prev, criteria: '' }));
doFilter();
};
const showPreviewModal = (record) => {
props.setSelectedData(record);
const showPreviewModal = (param) => {
props.setSelectedData(param);
props.setActionMode('preview');
};
const showEditModal = (record) => {
props.setSelectedData(record);
const showEditModal = (param = null) => {
props.setSelectedData(param);
props.setActionMode('edit');
};
const showAddModal = () => {
props.setSelectedData(null);
const showAddModal = (param = null) => {
props.setSelectedData(param);
props.setActionMode('add');
};
const showDeleteDialog = (record) => {
const showDeleteDialog = (param) => {
NotifConfirmDialog({
icon: 'question',
title: 'Konfirmasi',
message: `Apakah anda yakin ingin menghapus data shift "${record.nama_shift}"?`,
onConfirm: () => props.onDelete(record.id),
message: `Apakah anda yakin hapus data "${param.shift_name}" ?`,
onConfirm: () => handleDelete(param),
onCancel: () => props.setSelectedData(null),
});
};
const columns = [
{
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{
title: 'Nama Shift',
dataIndex: 'nama_shift',
key: 'nama_shift',
},
{
title: 'Jam Shift',
dataIndex: 'jam_shift',
key: 'jam_shift',
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
align: 'center',
render: (status) => (
<Tag color={status ? 'green' : 'red'}>
{status ? 'Active' : 'Inactive'}
</Tag>
),
},
{
title: 'Aksi',
key: 'action',
align: 'center',
width: '15%',
render: (_, record) => (
<Space>
<Button
icon={<EyeOutlined />}
onClick={() => showPreviewModal(record)}
style={{
color: '#1890ff',
borderColor: '#1890ff',
}}
/>
<Button
icon={<EditOutlined />}
onClick={() => showEditModal(record)}
style={{
color: '#faad14',
borderColor: '#faad14',
}}
/>
<Button
danger
icon={<DeleteOutlined />}
onClick={() => showDeleteDialog(record)}
style={{
borderColor: '#ff4d4f',
}}
/>
</Space>
),
},
];
const handleDelete = async (param) => {
try {
const response = await deleteShift(param.shift_id);
console.log('deleteShift response:', response);
const filteredData = props.shiftData.filter(item =>
item.nama_shift.toLowerCase().includes(searchValue.toLowerCase())
);
if (response.statusCode === 200) {
NotifAlert({
icon: 'success',
title: 'Berhasil',
message: `Data Shift "${param.shift_name}" berhasil dihapus.`,
});
// Refresh table
doFilter();
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response.message || 'Gagal menghapus data Shift.',
});
}
} catch (error) {
console.error('Delete Shift Error:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: error.message || 'Terjadi kesalahan saat menghapus data.',
});
}
};
// Function untuk dipanggil dari DetailShift setelah create/update
const refreshData = () => {
doFilter();
};
// Pass refresh function to props
if (props.setRefreshData) {
props.setRefreshData(refreshData);
}
return (
<Card>
<Row justify="space-between" align="middle" gutter={[16, 16]}>
<Col xs={24} sm={12} md={10} lg={8}>
<Input.Search
placeholder="Cari nama shift..."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onSearch={handleSearch}
allowClear={{
clearIcon: <span onClick={handleSearchClear}>x</span>,
}}
enterButton={<Button type="primary" icon={<SearchOutlined />} style={{ backgroundColor: '#23A55A', borderColor: '#23A55A' }}>Cari</Button>}
size="large"
/>
</Col>
<Col>
<ConfigProvider
theme={{
token: { colorBgContainer: '#E9F6EF' },
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
<Button
icon={<PlusOutlined />}
onClick={showAddModal}
size="large"
>
Tambah Shift
</Button>
</ConfigProvider>
</Col>
</Row>
<Row style={{ marginTop: 16 }}>
<Col span={24}>
<Table
columns={columns}
dataSource={filteredData}
loading={props.loading}
rowKey="id"
/>
</Col>
</Row>
</Card>
<React.Fragment>
<Card>
<Row>
<Col xs={24}>
<Row justify="space-between" align="middle" gutter={[8, 8]}>
<Col xs={24} sm={24} md={12} lg={12}>
<Input.Search
placeholder="Search shift by name..."
value={searchValue}
onChange={(e) => {
const value = e.target.value;
setSearchValue(value);
// Auto search when clearing by backspace/delete
if (value === '') {
handleSearchClear();
}
}}
onSearch={handleSearch}
allowClear
onClear={handleSearchClear}
enterButton={
<Button
type="primary"
icon={<SearchOutlined />}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
}}
>
Search
</Button>
}
size="large"
/>
</Col>
<Col>
<Space wrap size="small">
<ConfigProvider
theme={{
token: { colorBgContainer: '#E9F6EF' },
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
<Button
icon={<PlusOutlined />}
onClick={() => showAddModal()}
size="large"
>
Tambah Data
</Button>
</ConfigProvider>
</Space>
</Col>
</Row>
</Col>
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
<TableList
mobile
cardColor={'#42AAFF'}
header={'shift_name'}
showPreviewModal={showPreviewModal}
showEditModal={showEditModal}
showDeleteDialog={showDeleteDialog}
getData={getAllShift}
queryParams={formDataFilter}
columns={columns(showPreviewModal, showEditModal, showDeleteDialog)}
triger={trigerFilter}
/>
</Col>
</Row>
</Card>
</React.Fragment>
);
};
});
export default memo(ListShift);
export default ListShift;

View File

@@ -5,87 +5,23 @@ import { Typography } from 'antd';
import ListStatus from './component/ListStatus';
import DetailStatus from './component/DetailStatus';
import { NotifConfirmDialog, NotifAlert } from '../../../components/Global/ToastNotif';
const { Text } = Typography;
// Mock Data
const initialData = [
{
key: '3',
statusCode: 3,
statusName: 'Done',
description: 'Indicates that the process is complete.',
},
{
key: '1',
statusCode: 1,
statusName: 'Warning',
description: 'Indicates a warning condition.',
},
{
key: '2',
statusCode: 2,
statusName: 'Alarm',
description: 'Indicates an alarm condition.',
},
{
key: '4',
statusCode: 4,
statusName: 'Critical',
description: 'Indicates a critical condition.',
},
];
const IndexStatus = memo(function IndexStatus() {
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [data, setData] = useState(initialData);
const [actionMode, setActionMode] = useState('list');
const [selectedData, setSelectedData] = useState(null);
const [isModalVisible, setIsModalVisible] = useState(false);
const [readOnly, setReadOnly] = useState(false);
// Mock API function
const getAllStatus = async (params) => {
const { page = 1, limit = 10, search = '' } = Object.fromEntries(params.entries());
let filteredData = data;
if (search) {
filteredData = data.filter(item =>
item.statusName.toLowerCase().includes(search.toLowerCase())
);
}
const start = (page - 1) * limit;
const end = start + limit;
const paginatedData = filteredData.slice(start, end);
return new Promise(resolve => {
setTimeout(() => {
resolve({
status: 200,
data: {
data: paginatedData,
total: filteredData.length,
paging: {
page: parseInt(page),
limit: parseInt(limit),
total: filteredData.length,
},
},
});
}, 500);
});
};
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
setBreadcrumbItems([
{ title: <Text strong style={{ fontSize: '14px' }}> Master</Text> },
{ title: <Text strong style={{ fontSize: '14px' }}>Status</Text> }
{ title: <Text strong> Master</Text> },
{ title: <Text strong>Status</Text> }
]);
} else {
navigate('/signin');
@@ -101,67 +37,24 @@ const IndexStatus = memo(function IndexStatus() {
}
}, [actionMode]);
const handleDataSaved = (values) => {
let newData = [...data];
if (values.key) { // Editing
const index = newData.findIndex((item) => values.key === item.key);
if (index > -1) {
newData.splice(index, 1, values);
}
} else { // Adding
const newKey = (Math.max(...data.map(item => parseInt(item.key))) + 1).toString();
newData = [{ key: newKey, ...values }, ...newData];
}
setData(newData);
};
const handleEdit = (record) => {
setSelectedData(record);
setActionMode('edit');
};
const handlePreview = (record) => {
setSelectedData(record);
setActionMode('preview');
};
const handleDelete = (record) => {
NotifConfirmDialog({
icon: 'question',
title: 'Konfirmasi',
message: `Apakah anda yakin ingin menghapus status "${record.statusName}"?`,
onConfirm: () => {
const newData = data.filter((item) => item.key !== record.key);
setData(newData);
NotifAlert({
icon: 'success',
title: 'Berhasil',
message: `Status "${record.statusName}" berhasil dihapus.`,
});
},
});
};
return (
<React.Fragment>
<ListStatus
setActionMode={setActionMode}
handleEdit={handleEdit}
handleDelete={handleDelete}
handlePreview={handlePreview}
getAllStatus={getAllStatus}
data={data}
/>
{actionMode === 'list' &&
<ListStatus
setActionMode={setActionMode}
setSelectedData={setSelectedData}
actionMode={actionMode}
/>
}
<DetailStatus
showModal={isModalVisible}
setActionMode={setActionMode}
selectedData={selectedData}
readOnly={readOnly}
onDataSaved={handleDataSaved}
setSelectedData={setSelectedData}
readOnly={readOnly}
/>
</React.Fragment>
);
});
export default IndexStatus;
export default IndexStatus;

View File

@@ -1,66 +1,113 @@
import React, { useEffect, useState } from 'react';
import { Modal, Input, Divider, Typography, Button, ConfigProvider, InputNumber, Form } from 'antd';
import { Modal, Input, Divider, Typography, Button, ConfigProvider, InputNumber, Switch } from 'antd';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import { validateRun } from '../../../../Utils/validate';
import { createStatus, updateStatus } from '../../../../api/master-status';
const { Text } = Typography;
const { TextArea } = Input;
const DetailStatus = (props) => {
const [form] = Form.useForm();
const [confirmLoading, setConfirmLoading] = useState(false);
const defaultData = {
key: '',
statusCode: '',
statusName: '',
description: '',
status_id: '',
status_number: null,
status_name: '',
status_color: '',
status_description: '',
is_active: true,
};
const [FormData, setFormData] = useState(defaultData);
const [formData, setFormData] = useState(defaultData);
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const handleInputNumberChange = (value) => {
setFormData({ ...formData, status_number: value });
};
const handleStatusToggle = (checked) => {
setFormData({ ...formData, is_active: checked });
};
const handleCancel = () => {
props.setSelectedData(null);
props.setActionMode('list');
form.resetFields();
};
const handleSave = async () => {
try {
const values = await form.validateFields();
setConfirmLoading(true);
setConfirmLoading(true);
const validationRules = [
{ field: 'status_number', label: 'Status Number', required: true },
{ field: 'status_name', label: 'Status Name', required: true },
{ field: 'status_color', label: 'Status Color', required: true },
{ field: 'status_description', label: 'Description', required: true },
];
if (
validateRun(formData, validationRules, (errorMessages) => {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: errorMessages,
});
setConfirmLoading(false);
})
) {
return;
}
try {
const payload = {
key: FormData.key,
...values,
status_number: formData.status_number,
status_name: formData.status_name,
status_color: formData.status_color,
status_description: formData.status_description,
is_active: formData.is_active,
};
props.onDataSaved(payload);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `Data Status "${payload.statusName}" berhasil ${
payload.key ? 'diubah' : 'ditambahkan'
}.`,
});
const response = formData.status_id
? await updateStatus(formData.status_id, payload)
: await createStatus(payload);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
const action = formData.status_id ? 'diubah' : 'ditambahkan';
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `Data Status "${payload.status_name}" berhasil ${action}.`,
});
props.setActionMode('list');
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal menyimpan data.',
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Error',
message: error.message || 'Terjadi kesalahan pada server.',
});
} finally {
setConfirmLoading(false);
props.setActionMode('list');
form.resetFields();
} catch (errorInfo) {
console.log('Failed:', errorInfo);
}
};
useEffect(() => {
if (props.selectedData) {
setFormData(props.selectedData);
form.setFieldsValue(props.selectedData);
setFormData({ ...defaultData, ...props.selectedData });
} else {
setFormData(defaultData);
form.resetFields();
}
}, [props.showModal, props.selectedData, form]);
}, [props.showModal, props.selectedData]);
return (
<Modal
@@ -78,81 +125,72 @@ const DetailStatus = (props) => {
footer={
!props.readOnly && (
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px', paddingTop: '15px' }}>
<ConfigProvider
theme={{
token: { colorPrimary: '#23A55A' },
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: 'white',
defaultHoverBg: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
<Button onClick={handleCancel}>Batal</Button>
</ConfigProvider>
<ConfigProvider
theme={{
token: { colorPrimary: '#23A55A' },
components: {
Button: {
defaultBg: '#23A55A',
defaultColor: 'white',
defaultBorderColor: '#23A55A',
defaultHoverColor: 'white',
defaultHoverBg: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
<Button type="primary" loading={confirmLoading} onClick={handleSave}>
Simpan
</Button>
</ConfigProvider>
<Button onClick={handleCancel}>Batal</Button>
<Button type="primary" loading={confirmLoading} onClick={handleSave}>
Simpan
</Button>
</div>
)
}
>
<Divider />
<Form form={form} layout="vertical" name="detailStatusForm">
<Form.Item
name="statusCode"
label={<Text strong>Status Code</Text>}
rules={[{ required: true, message: 'Silakan masukkan kode status!' }]}
>
<InputNumber
placeholder="Masukan code status"
readOnly={props.readOnly}
style={{ width: '100%' }}
<div style={{ marginBottom: 12 }}>
<Text strong>Status</Text>
<div style={{ display: 'flex', alignItems: 'center', marginTop: '8px' }}>
<Switch
disabled={props.readOnly}
checked={formData.is_active}
onChange={handleStatusToggle}
/>
</Form.Item>
<Form.Item
name="statusName"
label={<Text strong>Status Name</Text>}
rules={[{ required: true, message: 'Silakan masukkan nama status!' }]}
>
<Input
placeholder="Masukan nama status"
readOnly={props.readOnly}
/>
</Form.Item>
<Form.Item
name="description"
label={<Text strong>Description</Text>}
rules={[{ required: true, message: 'Silakan masukkan deskripsi!' }]}
>
<TextArea
placeholder="Masukan deskripsi"
readOnly={props.readOnly}
rows={4}
/>
</Form.Item>
</Form>
<Text style={{ marginLeft: 8 }}>{formData.is_active ? 'Active' : 'Inactive'}</Text>
</div>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Status Number</Text>
<Text style={{ color: 'red' }}> *</Text>
<InputNumber
name="status_number"
value={formData.status_number}
placeholder="Masukan nomor status"
readOnly={props.readOnly}
style={{ width: '100%' }}
onChange={handleInputNumberChange}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Status Name</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="status_name"
value={formData.status_name}
placeholder="Masukan nama status"
readOnly={props.readOnly}
onChange={handleInputChange}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Status Color</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="status_color"
value={formData.status_color}
placeholder="Masukan warna status (e.g., hijau, #00ff00)"
readOnly={props.readOnly}
onChange={handleInputChange}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Description</Text>
<Text style={{ color: 'red' }}> *</Text>
<TextArea
name="status_description"
value={formData.status_description}
placeholder="Masukan deskripsi"
readOnly={props.readOnly}
rows={4}
onChange={handleInputChange}
/>
</div>
</Modal>
);
};

View File

@@ -1,68 +1,152 @@
import React from 'react';
import { Card, Button, Row, Col, Typography, Space, ConfigProvider } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
import React, { memo, useState, useEffect } from 'react';
import { Space, ConfigProvider, Button, Row, Col, Card, Input, Segmented, Table, Pagination } from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
EyeOutlined,
SearchOutlined,
AppstoreOutlined,
TableOutlined,
} from '@ant-design/icons';
import { NotifAlert, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
import { useNavigate } from 'react-router-dom';
import { deleteStatus, getAllStatuss } from '../../../../api/master-status';
const { Title, Text } = Typography;
const ListStatus = memo(function ListStatus(props) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [viewMode, setViewMode] = useState('card');
const [trigerFilter, setTrigerFilter] = useState(false);
const [searchValue, setSearchValue] = useState('');
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
const navigate = useNavigate();
const ListStatus = ({
setActionMode,
handleEdit,
handleDelete,
handlePreview,
data,
}) => {
const getCardStyle = (statusName) => {
let color;
switch (statusName.toLowerCase()) {
case 'done':
color = '#52c41a'; // green
break;
case 'warning':
color = '#faad14'; // orange
break;
case 'alarm':
color = '#f5222d'; // red
break;
case 'critical':
color = '#000000'; // black
break;
default:
color = '#d9d9d9'; // default antd border color
const fetchData = async (page = 1, pageSize = 10) => {
setLoading(true);
try {
const params = new URLSearchParams();
params.append('page', page);
params.append('limit', pageSize);
if (searchValue) {
params.append('search', searchValue);
}
const response = await getAllStatuss(params);
setData(response.data || []);
setPagination(prev => ({ ...prev, total: response.paging?.total || 0, current: page, pageSize: pageSize }));
} catch (error) {
console.error("Failed to fetch status data:", error);
setData([]);
} finally {
setLoading(false);
}
return { border: `2px solid ${color}` };
};
const getTitleStyle = (statusName) => {
let backgroundColor;
switch (statusName.toLowerCase()) {
case 'done':
backgroundColor = '#52c41a'; // green
break;
case 'warning':
backgroundColor = '#faad14'; // orange
break;
case 'alarm':
backgroundColor = '#f5222d'; // red
break;
case 'critical':
backgroundColor = '#000000'; // black
break;
default:
backgroundColor = 'transparent';
useEffect(() => {
const token = localStorage.getItem('token');
if (!token) {
navigate('/signin');
return;
}
return {
backgroundColor,
color: '#fff',
padding: '2px 8px',
borderRadius: '4px',
display: 'inline-block'
};
fetchData(pagination.current, pagination.pageSize);
}, [props.actionMode, trigerFilter, navigate]);
const doFilter = () => {
setTrigerFilter(prev => !prev);
};
const handleSearch = (value) => {
setSearchValue(value);
setPagination(prev => ({ ...prev, current: 1 })); // Reset to first page on search
doFilter();
};
const handlePaginationChange = (page, pageSize) => {
fetchData(page, pageSize);
};
const showPreviewModal = (record) => {
props.setSelectedData(record);
props.setActionMode('preview');
};
const showEditModal = (record) => {
props.setSelectedData(record);
props.setActionMode('edit');
};
const showAddModal = () => {
props.setSelectedData(null);
props.setActionMode('add');
};
const showDeleteDialog = (record) => {
NotifConfirmDialog({
icon: 'question',
title: 'Konfirmasi Hapus',
message: `Status "${record.status_name}" akan dihapus?`,
onConfirm: () => handleDelete(record.status_id),
});
};
const handleDelete = async (status_id) => {
try {
const response = await deleteStatus(status_id);
if (response.statusCode === 200) {
NotifAlert({ icon: 'success', title: 'Berhasil', message: 'Data Status berhasil dihapus.' });
doFilter();
} else {
NotifAlert({ icon: 'error', title: 'Gagal', message: response?.message || 'Gagal Menghapus Data' });
}
} catch (error) {
NotifAlert({ icon: 'error', title: 'Error', message: error.message });
}
};
const columns = [
{ title: 'Number', dataIndex: 'status_number', key: 'status_number', width: '15%' },
{ title: 'Name', dataIndex: 'status_name', key: 'status_name', width: '25%' },
{ title: 'Description', dataIndex: 'status_description', key: 'status_description', width: '40%' },
{
title: 'Aksi', key: 'aksi', align: 'center', width: '20%',
render: (_, record) => (
<Space>
<Button type="text" icon={<EyeOutlined />} onClick={() => showPreviewModal(record)} />
<Button type="text" icon={<EditOutlined />} onClick={() => showEditModal(record)} />
<Button danger type="text" icon={<DeleteOutlined />} onClick={() => showDeleteDialog(record)} />
</Space>
),
},
];
const getCardStyle = (color) => {
return { border: `2px solid ${color || '#d9d9d9'}`, height: '100%' };
};
const getTitleStyle = (color) => {
return { backgroundColor: color || 'transparent', color: '#fff', padding: '2px 8px', borderRadius: '4px', display: 'inline-block' };
};
return (
<div style={{ padding: 24, minHeight: 360 }}>
<Row justify="end" style={{ marginBottom: 16 }}>
<Card>
<Row justify="space-between" align="middle" gutter={[16, 16]}>
<Col xs={24} sm={24} md={12} lg={12}>
<Input.Search
placeholder="Search by status name..."
onSearch={handleSearch}
allowClear
enterButton={
<Button
type="primary"
icon={<SearchOutlined />}
style={{ backgroundColor: '#23A55A', borderColor: '#23A55A' }}
>
Search
</Button>
}
size="large"
/>
</Col>
<Col>
<ConfigProvider
theme={{
@@ -78,37 +162,64 @@ const ListStatus = ({
},
}}
>
<Button
icon={<PlusOutlined />}
onClick={() => setActionMode('add')}
>
<Button icon={<PlusOutlined />} onClick={showAddModal} size="large">
Tambah Data
</Button>
</ConfigProvider>
</Col>
</Row>
<Row gutter={[16, 16]}>
{data.map(item => (
<Col xs={24} sm={12} md={8} lg={6} key={item.key}>
<Card
title={<span style={getTitleStyle(item.statusName)}>{item.statusName}</span>}
style={getCardStyle(item.statusName)}
actions={[
<Space size="middle" style={{ display: 'flex', justifyContent: 'center' }}>
<Button style={{ border: '1px solid #1890ff', color: '#1890ff', borderRadius: '6px', padding: '4px 8px' }} icon={<EyeOutlined />} onClick={() => handlePreview(item)} />
<Button style={{ border: '1px solid #faad14', color: '#faad14', borderRadius: '6px', padding: '4px 8px' }} icon={<EditOutlined />} onClick={() => handleEdit(item)} />
<Button danger style={{ border: '1px solid red', borderRadius: '6px', padding: '4px 8px' }} icon={<DeleteOutlined />} onClick={() => handleDelete(item)} />
</Space>
]}
>
<p><Text strong>Code:</Text> {item.statusCode}</p>
<p><Text strong>Description:</Text> {item.description}</p>
</Card>
</Col>
))}
<Row style={{ marginTop: 16 }}>
<Col>
<Segmented
options={[{ value: 'card', icon: <AppstoreOutlined /> }, { value: 'table', icon: <TableOutlined /> }]}
value={viewMode}
onChange={setViewMode}
/>
</Col>
</Row>
</div>
<div style={{ marginTop: 24 }}>
{viewMode === 'card' ? (
<Row gutter={[16, 16]}>
{data.map(item => (
<Col xs={24} sm={12} md={8} lg={6} key={item.status_id}>
<Card
title={<span style={getTitleStyle(item.status_color)}>{item.status_name}</span>}
style={getCardStyle(item.status_color)}
actions={[
<EyeOutlined key="preview" onClick={() => showPreviewModal(item)} />,
<EditOutlined key="edit" onClick={() => showEditModal(item)} />,
<DeleteOutlined key="delete" onClick={() => showDeleteDialog(item)} />,
]}
>
<p><b>Number:</b> {item.status_number}</p>
<p><b>Description:</b> {item.status_description}</p>
</Card>
</Col>
))}
</Row>
) : (
<>
<Table
columns={columns}
dataSource={data.map(item => ({ ...item, key: item.status_id }))}
pagination={false}
loading={loading}
/>
<Pagination
style={{ marginTop: 16, textAlign: 'right' }}
current={pagination.current}
pageSize={pagination.pageSize}
total={pagination.total}
onChange={handlePaginationChange}
showSizeChanger
/>
</>
)}
</div>
</Card>
);
};
});
export default ListStatus;

View File

@@ -26,12 +26,19 @@ const DetailTag = (props) => {
unit: '',
is_active: true,
is_alarm: false,
is_report: false,
is_history: false,
lim_low_crash: '',
lim_low: '',
lim_high: '',
lim_high_crash: '',
device_id: null,
sub_section_id: null,
};
const [FormData, setFormData] = useState(defaultData);
const [nextTagCode, setNextTagCode] = useState('Auto-fill');
const handleCancel = () => {
props.setSelectedData(null);
@@ -120,13 +127,13 @@ const DetailTag = (props) => {
return;
}
// Validasi data type harus Diskrit atau Analog
const validDataTypes = ['Diskrit', 'Analog'];
// Validasi data type harus Discrete atau Analog
const validDataTypes = ['Discrete', 'Analog'];
if (!validDataTypes.includes(FormData.data_type)) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: `Data Type harus "Diskrit" atau "Analog". Nilai "${FormData.data_type}" tidak valid. Silakan pilih dari dropdown.`,
message: `Data Type harus "Discrete" atau "Analog". Nilai "${FormData.data_type}" tidak valid. Silakan pilih dari dropdown.`,
});
setConfirmLoading(false);
return;
@@ -153,6 +160,17 @@ const DetailTag = (props) => {
return;
}
// Plant Sub Section validation
if (!FormData.sub_section_id) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Plant Sub Section harus dipilih',
});
setConfirmLoading(false);
return;
}
// Prepare payload berdasarkan backend validation schema
const payload = {
tag_name: FormData.tag_name.trim(),
@@ -161,9 +179,25 @@ const DetailTag = (props) => {
unit: FormData.unit.trim(),
is_active: FormData.is_active,
is_alarm: FormData.is_alarm,
is_report: FormData.is_report,
is_history: FormData.is_history,
device_id: parseInt(FormData.device_id),
};
// Add limit fields only if they have values
if (FormData.lim_low_crash !== '' && FormData.lim_low_crash !== null) {
payload.lim_low_crash = parseFloat(FormData.lim_low_crash);
}
if (FormData.lim_low !== '' && FormData.lim_low !== null) {
payload.lim_low = parseFloat(FormData.lim_low);
}
if (FormData.lim_high !== '' && FormData.lim_high !== null) {
payload.lim_high = parseFloat(FormData.lim_high);
}
if (FormData.lim_high_crash !== '' && FormData.lim_high_crash !== null) {
payload.lim_high_crash = parseFloat(FormData.lim_high_crash);
}
// Add sub_section_id only if it's selected
if (FormData.sub_section_id) {
payload.sub_section_id = parseInt(FormData.sub_section_id);
@@ -184,14 +218,17 @@ const DetailTag = (props) => {
// Check if response is successful
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
// response.data is already an object (converted in master-tag.jsx API)
const tagCode = response.data?.tag_code || '';
const tagName = response.data?.tag_name || FormData.tag_name || '';
const tagDisplay = tagCode ? `${tagCode} - ${tagName}` : tagName;
NotifOk({
icon: 'success',
title: 'Berhasil',
message:
response.message ||
`Data Tag "${response.data?.tag_name || FormData.tag_name}" berhasil ${
FormData.tag_id ? 'diubah' : 'ditambahkan'
}.`,
message: `Data Tag "${tagDisplay}" berhasil ${
FormData.tag_id ? 'diubah' : 'ditambahkan'
}.`,
});
props.setActionMode('list');
@@ -252,6 +289,20 @@ const DetailTag = (props) => {
});
};
const handleReportToggle = (isChecked) => {
setFormData({
...FormData,
is_report: isChecked,
});
};
const handleHistoryToggle = (isChecked) => {
setFormData({
...FormData,
is_history: isChecked,
});
};
const loadDevices = async () => {
setLoadingDevices(true);
try {
@@ -314,6 +365,42 @@ const DetailTag = (props) => {
}
};
const generateNextTagCode = async () => {
try {
const params = new URLSearchParams({ limit: 10000 });
const response = await getAllTag(params);
if (response && response.data && response.data.data) {
const tags = response.data.data;
if (tags.length === 0) {
setNextTagCode('TAG001');
return;
}
// Extract numeric part from tag codes and find the maximum
const tagNumbers = tags
.map((tag) => {
const match = tag.tag_code?.match(/tag(\d+)/i);
return match ? parseInt(match[1], 10) : 0;
})
.filter((num) => !isNaN(num));
const maxNumber = tagNumbers.length > 0 ? Math.max(...tagNumbers) : 0;
const nextNumber = maxNumber + 1;
// Format with leading zeros (TAG001, TAG002, etc.)
const nextCode = `TAG${String(nextNumber).padStart(3, '0')}`;
setNextTagCode(nextCode);
} else {
setNextTagCode('TAG001');
}
} catch (error) {
console.error('Error generating next tag code:', error);
setNextTagCode('Auto-fill');
}
};
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
@@ -322,6 +409,11 @@ const DetailTag = (props) => {
loadDevices();
loadPlantSubSections();
loadUnits();
// Generate next tag code only for add mode
if (props.actionMode === 'add' && !props.selectedData) {
generateNextTagCode();
}
}
if (props.selectedData != null) {
@@ -335,6 +427,12 @@ const DetailTag = (props) => {
unit: props.selectedData.unit || '',
is_active: props.selectedData.is_active ?? true,
is_alarm: props.selectedData.is_alarm ?? false,
is_report: props.selectedData.is_report ?? false,
is_history: props.selectedData.is_history ?? false,
lim_low_crash: props.selectedData.lim_low_crash ?? '',
lim_low: props.selectedData.lim_low ?? '',
lim_high: props.selectedData.lim_high ?? '',
lim_high_crash: props.selectedData.lim_high_crash ?? '',
device_id: props.selectedData.device_id || null,
device_code: props.selectedData.device_code || '',
device_name: props.selectedData.device_name || '',
@@ -347,7 +445,7 @@ const DetailTag = (props) => {
} else {
// navigate('/signin'); // Uncomment if useNavigate is imported
}
}, [props.showModal]);
}, [props.showModal, props.actionMode]);
return (
<Modal
@@ -360,6 +458,7 @@ const DetailTag = (props) => {
} Tag`}
open={props.showModal}
onCancel={handleCancel}
width={800}
footer={[
<>
<ConfigProvider
@@ -413,19 +512,6 @@ const DetailTag = (props) => {
disabled
/>
</div>
{/* Tag Code hanya ditampilkan saat EDIT atau PREVIEW */}
{(props.actionMode === 'edit' || props.actionMode === 'preview') && (
<div style={{ marginBottom: 12 }}>
<Text strong>Tag Code</Text>
<Input
name="tag_code"
value={FormData.tag_code}
onChange={handleInputChange}
placeholder="Auto Generate"
disabled
/>
</div>
)}
{/* Status dan Alarm dalam satu baris */}
<div style={{ marginBottom: 12 }}>
<div
@@ -500,6 +586,94 @@ const DetailTag = (props) => {
</div>
</div>
</div>
{/* Report dan History dalam satu baris */}
<div style={{ marginBottom: 12 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
gap: '16px',
}}
>
{/* Report Toggle */}
<div style={{ flex: 1 }}>
<div>
<Text strong>Report</Text>
<Text style={{ color: 'red' }}> *</Text>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: '8px',
}}
>
<div style={{ marginRight: '8px' }}>
<Switch
disabled={props.readOnly}
style={{
backgroundColor:
FormData.is_report === true
? '#23A55A'
: '#bfbfbf',
}}
checked={FormData.is_report === true}
onChange={handleReportToggle}
/>
</div>
<div>
<Text>{FormData.is_report === true ? 'Yes' : 'No'}</Text>
</div>
</div>
</div>
{/* History Toggle */}
<div style={{ flex: 1 }}>
<div>
<Text strong>History</Text>
<Text style={{ color: 'red' }}> *</Text>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: '8px',
}}
>
<div style={{ marginRight: '8px' }}>
<Switch
disabled={props.readOnly}
style={{
backgroundColor:
FormData.is_history === true
? '#23A55A'
: '#bfbfbf',
}}
checked={FormData.is_history === true}
onChange={handleHistoryToggle}
/>
</div>
<div>
<Text>{FormData.is_history === true ? 'Yes' : 'No'}</Text>
</div>
</div>
</div>
</div>
</div>
{/* Tag Code - Auto Increment & Read Only */}
<div style={{ marginBottom: 12 }}>
<Text strong>Tag Code</Text>
<Input
name="tag_code"
value={FormData.tag_code || nextTagCode}
placeholder={nextTagCode}
disabled
style={{
backgroundColor: '#f5f5f5',
cursor: 'not-allowed',
color: FormData.tag_code ? '#000000' : '#bfbfbf',
}}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Tag Number</Text>
<Text style={{ color: 'red' }}> *</Text>
@@ -532,7 +706,7 @@ const DetailTag = (props) => {
onChange={(value) => handleSelectChange('data_type', value)}
disabled={props.readOnly}
>
<Select.Option value="Diskrit">Diskrit</Select.Option>
<Select.Option value="Discrete">Discrete</Select.Option>
<Select.Option value="Analog">Analog</Select.Option>
</Select>
</div>
@@ -562,8 +736,58 @@ const DetailTag = (props) => {
))}
</Select>
</div>
{/* Limit Fields */}
<div style={{ marginBottom: 12 }}>
<Text strong>Limit Low Crash</Text>
<Input
name="lim_low_crash"
value={FormData.lim_low_crash}
onChange={handleInputChange}
placeholder="Enter Limit Low Crash"
readOnly={props.readOnly}
type="number"
step="any"
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Limit Low</Text>
<Input
name="lim_low"
value={FormData.lim_low}
onChange={handleInputChange}
placeholder="Enter Limit Low"
readOnly={props.readOnly}
type="number"
step="any"
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Limit High</Text>
<Input
name="lim_high"
value={FormData.lim_high}
onChange={handleInputChange}
placeholder="Enter Limit High"
readOnly={props.readOnly}
type="number"
step="any"
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Limit High Crash</Text>
<Input
name="lim_high_crash"
value={FormData.lim_high_crash}
onChange={handleInputChange}
placeholder="Enter Limit High Crash"
readOnly={props.readOnly}
type="number"
step="any"
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Plant Sub Section</Text>
<Text style={{ color: 'red' }}> *</Text>
<Select
style={{ width: '100%' }}
placeholder="Select Plant Sub Section"

View File

@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react';
import { Modal, Input, Typography, Button, ConfigProvider, Switch } from 'antd';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import React, { useEffect, useState } from 'react';
import { Modal, Input, Typography, Switch, Button, ConfigProvider, Divider } from 'antd';
import { NotifOk } from '../../../../components/Global/ToastNotif';
import { createUnit, updateUnit } from '../../../../api/master-unit';
import { validateRun } from '../../../../Utils/validate';
const { Text } = Typography;
@@ -15,7 +16,7 @@ const DetailUnit = (props) => {
is_active: true,
};
const [FormData, setFormData] = useState(defaultData);
const [formData, setFormData] = useState(defaultData);
const handleCancel = () => {
props.setSelectedData(null);
@@ -25,115 +26,84 @@ const DetailUnit = (props) => {
const handleSave = async () => {
setConfirmLoading(true);
// Validasi required fields
if (!FormData.unit_name || FormData.unit_name.trim() === '') {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Kolom Name Tidak Boleh Kosong',
});
setConfirmLoading(false);
// Daftar aturan validasi
const validationRules = [
{ field: 'unit_name', label: 'Unit Name', required: true },
];
if (
validateRun(formData, validationRules, (errorMessages) => {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: errorMessages,
});
setConfirmLoading(false);
})
)
return;
}
try {
if (FormData.unit_id) {
// Update existing unit
const payload = {
name: FormData.unit_name,
is_active: FormData.is_active,
};
const payload = {
is_active: formData.is_active,
unit_name: formData.unit_name,
};
const response = await updateUnit(FormData.unit_id, payload);
console.log('updateUnit response:', response);
const response =
props.actionMode === 'edit'
? await updateUnit(formData.unit_id, payload)
: await createUnit(payload);
if (response.statusCode === 200) {
// Get updated data to show unit_code in notification
const unitCode = response.data?.unit_code || FormData.unit_code;
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `Data Unit "${unitCode} - ${FormData.unit_name}" berhasil diubah.`,
});
props.setActionMode('list');
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response.message || 'Gagal mengubah data Unit.',
});
}
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
const action = props.actionMode === 'edit' ? 'diubah' : 'ditambahkan';
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `Data Unit berhasil ${action}.`,
});
props.setActionMode('list');
} else {
// Create new unit
const payload = {
name: FormData.unit_name,
is_active: FormData.is_active,
};
const response = await createUnit(payload);
console.log('createUnit response:', response);
if (response.statusCode === 200 || response.statusCode === 201) {
// Get unit_code from response
const unitCode = response.data?.unit_code || 'N/A';
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `Data Unit "${unitCode} - ${FormData.unit_name}" berhasil ditambahkan.`,
});
props.setActionMode('list');
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response.message || 'Gagal menambahkan data Unit.',
});
}
NotifOk({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Terjadi kesalahan saat menyimpan data.',
});
}
} catch (error) {
console.error('Save Unit Error:', error);
NotifAlert({
NotifOk({
icon: 'error',
title: 'Error',
message: error.message || 'Terjadi kesalahan saat menyimpan data.',
message: error.message || 'Terjadi kesalahan pada server.',
});
} finally {
setConfirmLoading(false);
}
setConfirmLoading(false);
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData({
...FormData,
...formData,
[name]: value,
});
};
const handleStatusToggle = (isChecked) => {
const handleStatusToggle = (checked) => {
setFormData({
...FormData,
is_active: isChecked,
...formData,
is_active: checked,
});
};
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
if (props.selectedData != null) {
// Only set fields that are in defaultData
const filteredData = {
unit_id: props.selectedData.unit_id || '',
unit_code: props.selectedData.unit_code || '',
unit_name: props.selectedData.unit_name || '',
is_active: props.selectedData.is_active ?? true,
};
setFormData(filteredData);
} else {
setFormData(defaultData);
}
if (props.selectedData) {
setFormData(props.selectedData);
} else {
setFormData(defaultData);
}
}, [props.showModal]);
}, [props.showModal, props.selectedData, props.actionMode]);
return (
<Modal
@@ -147,34 +117,27 @@ const DetailUnit = (props) => {
open={props.showModal}
onCancel={handleCancel}
footer={[
<>
<React.Fragment key="modal-footer">
<ConfigProvider
theme={{
token: { colorBgContainer: '#E9F6EF' },
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
},
},
}}
>
<Button onClick={handleCancel}>Batal</Button>
<Button onClick={handleCancel}>{props.readOnly ? 'Tutup' : 'Batal'}</Button>
</ConfigProvider>
<ConfigProvider
theme={{
token: {
colorBgContainer: '#209652',
},
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#23a55a',
},
},
}}
@@ -185,60 +148,55 @@ const DetailUnit = (props) => {
</Button>
)}
</ConfigProvider>
</>,
</React.Fragment>,
]}
>
{FormData && (
{formData && (
<div>
{/* Status Toggle */}
<div style={{ marginBottom: 12 }}>
<div>
<div>
<Text strong>Status</Text>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: '8px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', marginTop: '8px' }}>
<div style={{ marginRight: '8px' }}>
<Switch
disabled={props.readOnly}
style={{
backgroundColor:
FormData.is_active === true
? '#23A55A'
: '#bfbfbf',
backgroundColor: formData.is_active ? '#23A55A' : '#bfbfbf',
}}
checked={FormData.is_active === true}
checked={formData.is_active}
onChange={handleStatusToggle}
/>
</div>
<div>
<Text>
{FormData.is_active === true ? 'Active' : 'Inactive'}
</Text>
<Text>{formData.is_active ? 'Active' : 'Inactive'}</Text>
</div>
</div>
</div>
{/* Unit Code - Display only for edit/preview */}
{FormData.unit_code && (
<div style={{ marginBottom: 12 }}>
<Text strong>Unit Code</Text>
<Input
name="unit_code"
value={FormData.unit_code}
disabled
/>
</div>
)}
<Divider style={{ margin: '12px 0' }} />
{/* Unit Code - Auto Increment & Read Only */}
<div style={{ marginBottom: 12 }}>
<Text strong>Name</Text>
<Text strong>Unit Code</Text>
<Input
name="unit_code"
value={formData.unit_code || ''}
placeholder={'Unit Code Auto Fill'}
disabled
style={{
backgroundColor: '#f5f5f5',
cursor: 'not-allowed',
color: formData.unit_code ? '#000000' : '#bfbfbf',
}}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Unit Name</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="unit_name"
value={FormData.unit_name}
value={formData.unit_name}
onChange={handleInputChange}
placeholder="Enter Unit Name"
readOnly={props.readOnly}
@@ -250,4 +208,4 @@ const DetailUnit = (props) => {
);
};
export default DetailUnit;
export default DetailUnit;

238
svg/air_dryer_A_rev.svg Normal file
View File

@@ -0,0 +1,238 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 500" xmlns:bx="https://boxy-svg.com">
<defs>
<bx:grid x="0" y="0" width="25" height="25"/>
</defs>
<rect y="10.407" width="972.648" height="440.159" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" x="12.119"/>
<g transform="matrix(0.826913, 0, 0, 0.698383, 74.03907, 53.375034)">
<ellipse style="stroke: rgb(0, 0, 0); fill: rgb(243, 243, 243);" cx="315" cy="183.068" rx="45" ry="45"/>
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);" cx="315" cy="449.112" rx="45" ry="45"/>
<rect x="270" y="180" width="90" height="270" style="stroke: rgb(0, 0, 0); fill: rgb(243, 243, 243);"/>
</g>
<g transform="matrix(0.826913, 0, 0, 0.698383, 500.726135, 53.375034)">
<ellipse style="stroke: rgb(0, 0, 0); fill: rgb(243, 243, 243);" cx="315" cy="183.068" rx="45" ry="45"/>
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);" cx="315" cy="449.112" rx="45" ry="45"/>
<rect x="270" y="180" width="90" height="270" style="stroke: rgb(0, 0, 0); fill: rgb(243, 243, 243);"/>
</g>
<rect x="371.728" y="182.483" width="8.269" height="6.984" style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-box: fill-box; transform-origin: 50% 50%;" d="M 551.111 -16.154 L 551.202 389.079" transform="matrix(0, -1.184039, 0.844567, 0, 0.000036, 0.000096)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 479.043 149.568 L 495.765 149.568 L 495.765 166.035 L 479.043 166.035 L 479.043 149.568 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 598.485px 226.003px;" d="M 478.737 156.666 L 495.763 156.666"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 465.169 193.204 L 511.13 179.759 L 511.13 193.204 L 465.169 179.759 L 465.169 193.204 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 487.934 186.514 L 487.934 157.095"/>
<rect x="715.724" y="182.138" width="8.269" height="6.984" style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;"/>
<g transform="matrix(0.826913, 0, 0, 0.698383, -2.11712, 3.138935)">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 660.838px 251.447px;" d="M 660.846 262.894 L 660.846 240"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 490.992px 230.229px;" d="M 646.097 240.002 L 676.271 240.002"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 443.701px 171.141px;" d="M 443.542 155.983 L 443.859 186.298"/>
<g transform="matrix(0.826913, 0, 0, 0.698383, 0.443817, 3.138935)">
<rect x="752" y="355.455" width="42.438" height="3.527" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
<rect x="756.328" y="359.271" width="34.034" height="53.968" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
<rect x="756.146" y="352.019" width="34.034" height="3.38" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);" cx="773.446" cy="384.7" rx="11.751" ry="11.009"/>
</g>
<rect x="461.861" y="211.956" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px;" x="561" y="309.954" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp SP</text>
<rect x="461.861" y="221.924" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="564.279" y="330.561" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="609.476" y="330.521" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
<rect x="461.424" y="242.149" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="567.471" y="352.188" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp</text>
<rect x="461.424" y="252.117" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="563.75" y="373.795" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="Dry1_HeatTempCelsius">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="608.947" y="373.755" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
<rect x="535.456" y="242.272" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="659" y="352.363" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp</text>
<rect x="535.456" y="252.24" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="653.279" y="373.97" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="698.476" y="373.93" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1; font-weight: bold;" x="748" y="347.676" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER</text>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 678.512px 258.693px;" d="M 678.467 229.321 L 678.558 288.066" transform="matrix(0, 1.184039, -0.844567, 0, -0.000022, -0.000005)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 703.162px 309.166px;" d="M 703.004 258.049 L 703.32 360.282"/>
<rect x="371.639" y="358.212" width="8.269" height="6.984" style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 692.133px 361.256px;" d="M 692.126 397.022 L 692.14 325.49" transform="matrix(0, -1.184039, 0.844567, 0, 0.000011, 0.00005)"/>
<rect x="715.635" y="357.867" width="8.269" height="6.984" style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 798px 493.641px;" d="M 661.975 334.874 L 661.975 360.911"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 552.59px 334.782px;" d="M 552.508 244.429 L 552.665 425.136" transform="matrix(0, 1.184039, -0.844567, 0, -0.000058, 0.000018)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 601.786px 283.137px;" d="M 601.741 312.51 L 601.832 253.764" transform="matrix(0, 1.184039, -0.844567, 0, -0.000063, 0.000028)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 577.266px 308.682px;" d="M 577.237 334.87 L 577.295 282.492" transform="matrix(-1, 0, 0, -1, -0.000041, -0.00003)"/>
<g transform="matrix(-0.491177, 0, 0, 0.523644, 491.13504, 29.785091)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 582.89 L 30 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 98.289 582.89 L 118.071 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 600 L 49.894 565.78 L 98.289 600 L 98.289 565.78"/>
<circle style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="1094.77" cy="561.359" r="39" transform="matrix(0.17796, 0, 0, 0.155258, -141.765747, 481.674255)"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 406.945px 361.188px;" d="M 406.92 329.322 L 406.969 393.054" transform="matrix(0, 1.184039, -0.844567, 0, 0.00001, 0.00007)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 521.472px 494.156px;" d="M 433.307 334.116 L 433.308 362.382"/>
<g transform="matrix(0.491177, 0, 0, 0.523644, 419.010895, 56.68491)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 582.89 L 30 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 98.289 582.89 L 118.071 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 600 L 49.894 565.78 L 98.289 600 L 98.289 565.78"/>
<circle style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="1094.77" cy="561.359" r="39" transform="matrix(0.17796, 0, 0, 0.155258, -141.765747, 481.674255)"/>
</g>
<g transform="matrix(0.491177, 0, 0, 0.523644, 603.674133, 29.692444)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 582.89 L 30 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 98.289 582.89 L 118.071 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 600 L 49.894 565.78 L 98.289 600 L 98.289 565.78"/>
<circle style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="1094.77" cy="561.359" r="39" transform="matrix(0.17796, 0, 0, 0.155258, -141.765747, 481.674255)"/>
</g>
<g transform="matrix(-0.491177, 0, 0, 0.523644, 677.00824, 56.209183)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 582.89 L 30 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 98.289 582.89 L 118.071 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 600 L 49.894 565.78 L 98.289 600 L 98.289 565.78"/>
<circle style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="1094.77" cy="561.359" r="39" transform="matrix(0.17796, 0, 0, 0.155258, -141.765747, 481.674255)"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 548.454px 361.62px;" d="M 548.372 271.267 L 548.529 451.974" transform="matrix(0, 1.184039, -0.844567, 0, 0.00006, -0.000036)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 578.392px 383.459px;" d="M 578.329 405.407 L 578.456 361.51" transform="matrix(-1, 0, 0, -1, -0.000055, -0.000003)"/>
<g transform="matrix(0.826913, 0, 0, -0.698383, 0.257882, 545.083069)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 660.838px 251.447px;" d="M 660.846 262.894 L 660.846 240"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 490.992px 230.229px;" d="M 646.097 240.002 L 676.271 240.002"/>
</g>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.525; transform-origin: 704.638px 576.106px;" d="M 621.828 405.49 L 634.485 405.49"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.525; transform-origin: 584.749px 405.496px;" d="M 591.042 405.497 L 578.456 405.498"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 1.525; transform-origin: 606.436px 405.492px;" d="M 591.042 398.364 L 591.042 412.623 L 621.831 398.364 L 621.831 412.623 L 591.042 398.364 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.525; transform-origin: 599.432px 413.831px;" d="M 592.464 422.169 L 592.464 405.493 L 606.401 405.493" transform="matrix(-1, 0, 0, -1, -0.000078, -0.000049)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 648.376px 405.65px;" d="M 648.369 435.621 L 648.383 375.678" transform="matrix(0, -1.184039, 0.844567, 0, 0.000012, -0.00004)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 673.882px 406.067px;" d="M 673.869 398.172 L 673.893 413.963" transform="matrix(-1, 0, 0, -1, -0.000092, -0.000022)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 692.821px 404.67px;" d="M 692.786 392.381 L 692.854 416.962" transform="matrix(0, 1.184039, -0.844567, 0, 0.000001, -0.000097)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 703.406px 383.726px;" d="M 703.396 405.334 L 703.415 362.116"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 681.807px 406.051px;" d="M 681.793 398.157 L 681.818 413.948" transform="matrix(-1, 0, 0, -1, 0.000055, 0.000046)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 677.987px 397.326px;" d="M 677.977 403.041 L 677.995 391.61"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 677.933px 411.78px;" d="M 677.924 417.977 L 677.941 405.582"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="650.811" cy="385.504" rx="10.336" ry="8.73"/>
<path d="M -100.83 -89.11 H -94.198 L -94.198 -91.071 L -85.458 -88.012 L -94.198 -84.954 L -94.198 -86.915 H -100.83 V -89.11 Z" bx:shape="arrow -100.83 -91.071 15.372 6.118 2.195 8.74 0 1@7c71f9c2" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); transform-box: fill-box; transform-origin: 50% 50%; stroke-width: 0.763;" transform="matrix(0.000343, -1, 1, 0.000266, 743.834961, 473.853179)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 651.034px 399.693px;" d="M 651.025 405.408 L 651.042 393.977"/>
<path d="M -546.834 -405.87 L -543.785 -398.138 L -550.56 -398.138 L -546.834 -405.87 Z" bx:shape="triangle -550.56 -405.87 6.775 7.732 0.55 0 1@11f2e68a" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); transform-box: fill-box; transform-origin: 50% 50%; stroke-width: 0.763;" transform="matrix(-1, 0, 0, -1, 1094.344971, 804.007996)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 546.953px 390.056px;" d="M 546.94 382.161 L 546.965 397.952" transform="matrix(-1, 0, 0, -1, -0.000013, 0.000046)"/>
<rect x="427.269" y="377.282" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="532.167" y="545.681" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dew Temp</text>
<rect x="427.269" y="387.25" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.446" y="567.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="567.643" y="567.248" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°C</text>
<rect x="427.27" y="412.201" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="532.168" y="595.681" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dew Temp</text>
<rect x="427.27" y="422.169" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.447" y="617.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="567.644" y="617.248" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1; text-anchor: middle; font-weight: bolder;" x="602.463" y="573.003" transform="matrix(0.826913, 0, 0, 0.698383, 24.207672, -7.192523)">AIR<tspan x="602.4630126953125" dy="1em"></tspan>OUTLET</text>
<g transform="matrix(-0.387768, 0, 0, -0.200385, 743.634644, -199.991287)" style="transform-origin: 72.2405px 412.5px;">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 53.913 425 L 53.913 400 L 90.568 425 L 90.568 400"/>
</g>
<g style="transform-origin: 227.882px 539.536px;" transform="matrix(0, 0.626201, -0.563979, 0, 588.703491, -326.882202)">
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 212.224 550 L 243.484 529.073 L 243.484 550 L 212.224 529.073 L 212.224 550 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 212.224 539.499 L 200 539.499"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 243.484 539.499 L 255.764 539.499"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 807.67px 264.838px;" d="M 807.657 276.289 L 807.693 253.387" transform="matrix(0, -1.184039, 0.844567, 0, 0.000035, 0.000091)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 816.592px 247.829px;" d="M 816.56 230.114 L 816.655 265.543" transform="matrix(-1, 0, 0, -1, 0.000101, 0.000009)"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="840.215" cy="233.608" rx="10.336" ry="8.73"/>
<path d="M -100.83 -89.11 H -94.198 L -94.198 -91.071 L -85.458 -88.012 L -94.198 -84.954 L -94.198 -86.915 H -100.83 V -89.11 Z" bx:shape="arrow -100.83 -91.071 15.372 6.118 2.195 8.74 0 1@7c71f9c2" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: -93.144px -88.013px;" transform="matrix(0.000343, -1, 1, 0.000266, 933.237163, 321.956912)"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="839.876" cy="254.847" rx="10.336" ry="8.73"/>
<path d="M -100.83 -89.11 H -94.198 L -94.198 -91.071 L -85.458 -88.012 L -94.198 -84.954 L -94.198 -86.915 H -100.83 V -89.11 Z" bx:shape="arrow -100.83 -91.071 15.372 6.118 2.195 8.74 0 1@7c71f9c2" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: -93.144px -88.013px;" transform="matrix(0.000343, -1, 1, 0.000266, 932.895305, 343.197025)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 823.288px 234.151px;" d="M 823.282 242.163 L 823.294 226.137" transform="matrix(0, 1.184039, -0.844567, 0, -0.000084, 0.000084)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 822.751px 254.694px;" d="M 822.745 262.706 L 822.763 246.68" transform="matrix(0, 1.184039, -0.844567, 0, -0.00002, 0.000032)"/>
<path d="M -100.83 -89.11 H -94.198 L -94.198 -91.071 L -85.458 -88.012 L -94.198 -84.954 L -94.198 -86.915 H -100.83 V -89.11 Z" bx:shape="arrow -100.83 -91.071 15.372 6.118 2.195 8.74 0 1@7c71f9c2" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: -93.144px -88.013px;" transform="matrix(0.000343, -1, 1, 0.000266, 909.661358, 277.142276)"/>
<g transform="matrix(0.387768, 0, 0, -0.200385, 207.60318, -199.315506)" style="transform-origin: 72.2406px 412.5px;">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 53.913 425 L 53.913 400 L 90.568 425 L 90.568 400"/>
</g>
<g style="transform-origin: 227.882px 539.536px;" transform="matrix(0, 0.626201, 0.563979, 0, 51.251434, -326.206329)">
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 212.224 550 L 243.484 529.073 L 243.484 550 L 212.224 529.073 L 212.224 550 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 212.224 539.499 L 200 539.499"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 243.484 539.499 L 255.764 539.499"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 288.049px 265.514px;" d="M 288.037 254.064 L 288.073 276.966" transform="matrix(0, -1.184039, 0.844567, 0, -0.000019, 0.000042)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 279.127px 248.505px;" d="M 279.095 266.219 L 279.19 230.79"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="-255.5" cy="234.284" rx="10.336" ry="8.73" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
<path d="M 100.83 93.032 H 107.463 L 107.463 91.071 L 116.203 94.13 L 107.463 97.189 L 107.463 95.228 H 100.83 V 93.032 Z" bx:shape="arrow 100.83 91.071 15.372 6.118 2.195 8.74 0 1@116e726f" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 108.517px 94.13px;" transform="matrix(-0.000343, -1, -1, 0.000266, 147.108874, 140.490195)"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="-255.84" cy="255.523" rx="10.336" ry="8.73" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
<path d="M 100.83 93.032 H 107.463 L 107.463 91.071 L 116.203 94.13 L 107.463 97.189 L 107.463 95.228 H 100.83 V 93.032 Z" bx:shape="arrow 100.83 91.071 15.372 6.118 2.195 8.74 0 1@116e726f" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 108.517px 94.13px;" transform="matrix(-0.000343, -1, -1, 0.000266, 147.450457, 161.730307)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 272.43px 234.827px;" d="M 272.424 226.815 L 272.436 242.841" transform="matrix(0, 1.184039, -0.844567, 0, 0.000012, -0.000015)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 272.968px 255.37px;" d="M 272.962 247.358 L 272.98 263.384" transform="matrix(0, 1.184039, -0.844567, 0, -0.000011, 0.000034)"/>
<path d="M 100.83 93.032 H 107.463 L 107.463 91.071 L 116.203 94.13 L 107.463 97.189 L 107.463 95.228 H 100.83 V 93.032 Z" bx:shape="arrow 100.83 91.071 15.372 6.118 2.195 8.74 0 1@116e726f" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 108.517px 94.13px;" transform="matrix(-0.000343, -1, -1, 0.000266, 170.68468, 95.675559)"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1; text-anchor: middle; font-weight: bolder;" x="602.463" y="573.003" transform="matrix(0.826913, 0, 0, 0.698383, 47.595016, -269.416931)">AIR<tspan x="602.4630126953125" dy="1em"></tspan>INLET</text>
<path d="M -544.544 -165.9 L -541.495 -158.168 L -548.27 -158.168 L -544.544 -165.9 Z" bx:shape="triangle -548.27 -165.9 6.775 7.732 0.55 0 1@12a1c9af" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: -544.876px -162.033px;" transform="matrix(-1, 0, 0, -1, 1089.751221, 324.066467)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 544.653px 150.086px;" d="M 544.639 142.192 L 544.664 157.983" transform="matrix(-1, 0, 0, -1, -0.000086, 0.000018)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-box: fill-box; transform-origin: 50% 50%;" d="M 404.306 125.878 L 416.234 125.878 L 416.234 148.965 L 404.306 148.965 L 404.306 125.878 Z" transform="matrix(0, -1.184039, 0.844567, 0, -0.000039, -0.000007)"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 498.152px 180.679px;" d="M 408.92 144.738 L 408.92 130.357"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-box: fill-box; transform-origin: 50% 50%;" d="M 427.836 146.217 L 460.619 127.367 L 460.619 146.217 L 427.836 127.367 L 427.836 146.217 Z" transform="matrix(0, -1.184039, 0.844567, 0, 0.000009, 0.000009)"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-box: fill-box; transform-origin: 50% 50%;" d="M 426.851 157.594 L 426.851 116.351" transform="matrix(0, -1.18404, 0.844567, 0, -0.000439, -0.000081)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 647.335px 171.745px;" d="M 647.177 186.902 L 647.494 156.588" transform="matrix(-1, 0, 0, -1, -0.000021, -0.000027)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 680.766px 138.025px;" d="M 674.802 149.569 L 686.73 149.569 L 686.73 126.482 L 674.802 126.482 L 674.802 149.569 Z" transform="matrix(0, -1.184039, 0.844567, 0, 0.000004, 0.000043)"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 897.148px 118.148px;" d="M 682.129 145.344 L 682.129 130.964"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 646.809px 137.396px;" d="M 630.417 127.971 L 663.201 146.821 L 663.201 127.971 L 630.417 146.821 L 630.417 127.971 Z" transform="matrix(0, -1.184039, 0.844567, 0, -0.000004, -0.000033)"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 664.185px 137.577px;" d="M 664.185 116.956 L 664.186 158.199" transform="matrix(0, -1.184039, 0.844567, 0, -0.000026, -0.000041)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 590.855 148.509 L 607.577 148.509 L 607.577 164.977 L 590.855 164.977 L 590.855 148.509 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 733.701px 224.488px;" d="M 590.555 155.608 L 607.58 155.608"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 576.981 192.146 L 622.941 178.701 L 622.941 192.146 L 576.981 178.701 L 576.981 192.146 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 599.746 185.455 L 599.746 156.037"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 640.474 90.952 L 652.913 90.952 L 652.913 107.419 L 640.474 107.419 L 640.474 90.952 Z"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 646.819px 113.115px;" d="M 646.799 107.896 L 646.839 118.334"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 650.466px 95.308px;" d="M 650.455 97.624 L 650.476 92.992" transform="matrix(0, -1.184039, 0.844567, 0, 0.000035, 0.00002)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 642.857px 102.712px;" d="M 642.846 105.028 L 642.867 100.396" transform="matrix(0, -1.184039, 0.844567, 0, 0.000009, 0.000035)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 437.929 90.244 L 450.367 90.244 L 450.367 106.712 L 437.929 106.712 L 437.929 90.244 Z"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 444.274px 112.408px;" d="M 444.253 107.189 L 444.293 117.627"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 447.919px 94.601px;" d="M 447.909 96.917 L 447.93 92.285" transform="matrix(0, -1.184039, 0.844567, 0, -0.000022, 0.000005)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 440.31px 102.004px;" d="M 440.3 104.321 L 440.32 99.689" transform="matrix(0, -1.184039, 0.844567, 0, -0.000028, -0.000004)"/>
<rect x="43.443" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="53.987" y="423.091" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">RUN HOUR</text>
<rect x="126.135" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="424.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="424.066" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
<rect x="43.443" y="308.191" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; stroke-width: 1; font-weight: bold;" x="53.987" y="461.382" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">PURGE HOUR</text>
<rect x="126.135" y="308.191" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="463.397" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="463.357" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
<rect x="43.443" y="333.129" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; font-weight: 700; white-space: pre;" x="53.987" y="498.091" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER HOUR</text>
<rect x="126.135" y="333.129" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="499.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="499.066" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
<rect x="43.65" y="360.147" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(248, 213, 14);"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; font-weight: 700; white-space: pre; stroke-width: 1;" x="54.237" y="536.777" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Alarm Info</text>
<rect x="126.341" y="360.147" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.3" y="537.792" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225.25" y="537.752" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
<rect x="43.443" y="160.275" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="56.987" y="250.585" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">RT or LT Dry</text>
<rect x="126.135" y="160.275" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="180.05" y="251.6" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">LT Dry</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="177" y="251.56" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">RT Dry</text>
<rect x="43.443" y="187.715" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; stroke-width: 1; font-weight: bold;" x="53.987" y="288.876" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Opmode</text>
<rect x="126.135" y="187.715" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HTD</text>
<rect x="43.443" y="214.051" width="165.383" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; font-weight: 700; white-space: pre; stroke-width: 1;" x="53.987" y="322.585" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Step</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="177.05" y="323.6" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="233" y="323.56" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">s</text>
<rect x="43.443" y="241.422" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; font-weight: 700; white-space: pre; stroke-width: 1;" x="54.237" y="364.271" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Cycle Timer</text>
<rect x="126.341" y="241.068" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="168.775" y="365.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225.25" y="365.246" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">s</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; font-weight: 700; white-space: pre; stroke-width: 1;" x="141.894" y="324.069" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Time</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="92.151" y="325.554" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">##</text>
<rect x="870.356" y="142.816" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="1060.06" y="224.103" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dryer Status</text>
<rect x="870.356" y="170.304" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.702" cy="192.401" rx="20.673" ry="17.46"/>
<rect x="870.356" y="230.113" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="1069.06" y="349.103" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Shutdown</text>
<rect x="870.356" y="257.602" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.702" cy="279.699" rx="20.673" ry="17.46"/>
<rect x="870.356" y="317.411" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="1059.06" y="474.103" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Alarm Status</text>
<rect x="870.356" y="344.9" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.702" cy="366.997" rx="20.673" ry="17.46"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="380.451" y="296.591" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="334.165" cy="232.104" rx="13.582" ry="12.517"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="379.214" y="423.395" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.623" cy="320.662" rx="13.582" ry="12.517"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="897.237" y="299.014" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.502" cy="233.796" rx="13.582" ry="12.517"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="896" y="425.818" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="762.96" cy="322.354" rx="13.582" ry="12.517"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 35px; stroke-width: 1; font-weight: bold;" x="348.875" y="78.242" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">AIR DRYER UNIT A (01-CL-10532-A)</text>
</svg>

After

Width:  |  Height:  |  Size: 40 KiB

238
svg/air_dryer_B_rev.svg Normal file
View File

@@ -0,0 +1,238 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 500" xmlns:bx="https://boxy-svg.com">
<defs>
<bx:grid x="0" y="0" width="25" height="25"/>
</defs>
<rect y="10.407" width="972.648" height="439.023" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" x="12.119"/>
<g transform="matrix(0.826913, 0, 0, 0.698383, 74.03907, 53.375034)">
<ellipse style="stroke: rgb(0, 0, 0); fill: rgb(243, 243, 243);" cx="315" cy="183.068" rx="45" ry="45"/>
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);" cx="315" cy="449.112" rx="45" ry="45"/>
<rect x="270" y="180" width="90" height="270" style="stroke: rgb(0, 0, 0); fill: rgb(243, 243, 243);"/>
</g>
<g transform="matrix(0.826913, 0, 0, 0.698383, 500.726135, 53.375034)">
<ellipse style="stroke: rgb(0, 0, 0); fill: rgb(243, 243, 243);" cx="315" cy="183.068" rx="45" ry="45"/>
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);" cx="315" cy="449.112" rx="45" ry="45"/>
<rect x="270" y="180" width="90" height="270" style="stroke: rgb(0, 0, 0); fill: rgb(243, 243, 243);"/>
</g>
<rect x="371.728" y="182.483" width="8.269" height="6.984" style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-box: fill-box; transform-origin: 50% 50%;" d="M 551.111 -16.154 L 551.202 389.079" transform="matrix(0, -1.184039, 0.844567, 0, 0.000036, 0.000096)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 479.043 149.568 L 495.765 149.568 L 495.765 166.035 L 479.043 166.035 L 479.043 149.568 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 598.485px 226.003px;" d="M 478.737 156.666 L 495.763 156.666"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 465.169 193.204 L 511.13 179.759 L 511.13 193.204 L 465.169 179.759 L 465.169 193.204 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 487.934 186.514 L 487.934 157.095"/>
<rect x="715.724" y="182.138" width="8.269" height="6.984" style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;"/>
<g transform="matrix(0.826913, 0, 0, 0.698383, -2.11712, 3.138935)">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 660.838px 251.447px;" d="M 660.846 262.894 L 660.846 240"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 490.992px 230.229px;" d="M 646.097 240.002 L 676.271 240.002"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 443.701px 171.141px;" d="M 443.542 155.983 L 443.859 186.298"/>
<g transform="matrix(0.826913, 0, 0, 0.698383, 0.443817, 3.138935)">
<rect x="752" y="355.455" width="42.438" height="3.527" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
<rect x="756.328" y="359.271" width="34.034" height="53.968" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
<rect x="756.146" y="352.019" width="34.034" height="3.38" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);" cx="773.446" cy="384.7" rx="11.751" ry="11.009"/>
</g>
<rect x="461.861" y="211.956" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px;" x="561" y="309.954" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp SP</text>
<rect x="461.861" y="221.924" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="564.279" y="330.561" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="609.476" y="330.521" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
<rect x="461.424" y="242.149" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="567.471" y="352.188" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp</text>
<rect x="461.424" y="252.117" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="563.75" y="373.795" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="Dry1_HeatTempCelsius">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="608.947" y="373.755" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
<rect x="535.456" y="242.272" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="659" y="352.363" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp</text>
<rect x="535.456" y="252.24" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="653.279" y="373.97" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="698.476" y="373.93" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1; font-weight: bold;" x="748" y="347.676" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER</text>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 678.512px 258.693px;" d="M 678.467 229.321 L 678.558 288.066" transform="matrix(0, 1.184039, -0.844567, 0, -0.000022, -0.000005)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 703.162px 309.166px;" d="M 703.004 258.049 L 703.32 360.282"/>
<rect x="371.639" y="358.212" width="8.269" height="6.984" style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 692.133px 361.256px;" d="M 692.126 397.022 L 692.14 325.49" transform="matrix(0, -1.184039, 0.844567, 0, 0.000011, 0.00005)"/>
<rect x="715.635" y="357.867" width="8.269" height="6.984" style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 798px 493.641px;" d="M 661.975 334.874 L 661.975 360.911"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 552.59px 334.782px;" d="M 552.508 244.429 L 552.665 425.136" transform="matrix(0, 1.184039, -0.844567, 0, -0.000058, 0.000018)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 601.786px 283.137px;" d="M 601.741 312.51 L 601.832 253.764" transform="matrix(0, 1.184039, -0.844567, 0, -0.000063, 0.000028)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 577.266px 308.682px;" d="M 577.237 334.87 L 577.295 282.492" transform="matrix(-1, 0, 0, -1, -0.000041, -0.00003)"/>
<g transform="matrix(-0.491177, 0, 0, 0.523644, 491.13504, 29.785091)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 582.89 L 30 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 98.289 582.89 L 118.071 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 600 L 49.894 565.78 L 98.289 600 L 98.289 565.78"/>
<circle style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="1094.77" cy="561.359" r="39" transform="matrix(0.17796, 0, 0, 0.155258, -141.765747, 481.674255)"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 406.945px 361.188px;" d="M 406.92 329.322 L 406.969 393.054" transform="matrix(0, 1.184039, -0.844567, 0, 0.00001, 0.00007)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 521.472px 494.156px;" d="M 433.307 334.116 L 433.308 362.382"/>
<g transform="matrix(0.491177, 0, 0, 0.523644, 419.010895, 56.68491)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 582.89 L 30 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 98.289 582.89 L 118.071 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 600 L 49.894 565.78 L 98.289 600 L 98.289 565.78"/>
<circle style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="1094.77" cy="561.359" r="39" transform="matrix(0.17796, 0, 0, 0.155258, -141.765747, 481.674255)"/>
</g>
<g transform="matrix(0.491177, 0, 0, 0.523644, 603.674133, 29.692444)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 582.89 L 30 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 98.289 582.89 L 118.071 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 600 L 49.894 565.78 L 98.289 600 L 98.289 565.78"/>
<circle style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="1094.77" cy="561.359" r="39" transform="matrix(0.17796, 0, 0, 0.155258, -141.765747, 481.674255)"/>
</g>
<g transform="matrix(-0.491177, 0, 0, 0.523644, 677.00824, 56.209183)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 582.89 L 30 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 98.289 582.89 L 118.071 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 600 L 49.894 565.78 L 98.289 600 L 98.289 565.78"/>
<circle style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="1094.77" cy="561.359" r="39" transform="matrix(0.17796, 0, 0, 0.155258, -141.765747, 481.674255)"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 548.454px 361.62px;" d="M 548.372 271.267 L 548.529 451.974" transform="matrix(0, 1.184039, -0.844567, 0, 0.00006, -0.000036)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 578.392px 383.459px;" d="M 578.329 405.407 L 578.456 361.51" transform="matrix(-1, 0, 0, -1, -0.000055, -0.000003)"/>
<g transform="matrix(0.826913, 0, 0, -0.698383, 0.257882, 545.083069)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 660.838px 251.447px;" d="M 660.846 262.894 L 660.846 240"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 490.992px 230.229px;" d="M 646.097 240.002 L 676.271 240.002"/>
</g>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.525; transform-origin: 704.638px 576.106px;" d="M 621.828 405.49 L 634.485 405.49"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.525; transform-origin: 584.749px 405.496px;" d="M 591.042 405.497 L 578.456 405.498"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 1.525; transform-origin: 606.436px 405.492px;" d="M 591.042 398.364 L 591.042 412.623 L 621.831 398.364 L 621.831 412.623 L 591.042 398.364 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.525; transform-origin: 599.432px 413.831px;" d="M 592.464 422.169 L 592.464 405.493 L 606.401 405.493" transform="matrix(-1, 0, 0, -1, -0.000078, -0.000049)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 648.376px 405.65px;" d="M 648.369 435.621 L 648.383 375.678" transform="matrix(0, -1.184039, 0.844567, 0, 0.000012, -0.00004)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 673.882px 406.067px;" d="M 673.869 398.172 L 673.893 413.963" transform="matrix(-1, 0, 0, -1, -0.000092, -0.000022)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 692.821px 404.67px;" d="M 692.786 392.381 L 692.854 416.962" transform="matrix(0, 1.184039, -0.844567, 0, 0.000001, -0.000097)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 703.406px 383.726px;" d="M 703.396 405.334 L 703.415 362.116"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 681.807px 406.051px;" d="M 681.793 398.157 L 681.818 413.948" transform="matrix(-1, 0, 0, -1, 0.000055, 0.000046)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 677.987px 397.326px;" d="M 677.977 403.041 L 677.995 391.61"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 677.933px 411.78px;" d="M 677.924 417.977 L 677.941 405.582"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="650.811" cy="385.504" rx="10.336" ry="8.73"/>
<path d="M -100.83 -89.11 H -94.198 L -94.198 -91.071 L -85.458 -88.012 L -94.198 -84.954 L -94.198 -86.915 H -100.83 V -89.11 Z" bx:shape="arrow -100.83 -91.071 15.372 6.118 2.195 8.74 0 1@7c71f9c2" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); transform-box: fill-box; transform-origin: 50% 50%; stroke-width: 0.763;" transform="matrix(0.000343, -1, 1, 0.000266, 743.834961, 473.853179)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 651.034px 399.693px;" d="M 651.025 405.408 L 651.042 393.977"/>
<path d="M -546.834 -405.87 L -543.785 -398.138 L -550.56 -398.138 L -546.834 -405.87 Z" bx:shape="triangle -550.56 -405.87 6.775 7.732 0.55 0 1@11f2e68a" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); transform-box: fill-box; transform-origin: 50% 50%; stroke-width: 0.763;" transform="matrix(-1, 0, 0, -1, 1094.344971, 804.007996)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 546.953px 390.056px;" d="M 546.94 382.161 L 546.965 397.952" transform="matrix(-1, 0, 0, -1, -0.000013, 0.000046)"/>
<rect x="427.269" y="377.282" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="532.167" y="545.681" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dew Temp</text>
<rect x="427.269" y="387.25" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.446" y="567.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="567.643" y="567.248" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°C</text>
<rect x="427.27" y="412.201" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="532.168" y="595.681" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dew Temp</text>
<rect x="427.27" y="422.169" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.447" y="617.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="567.644" y="617.248" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1; text-anchor: middle; font-weight: bolder;" x="602.463" y="573.003" transform="matrix(0.826913, 0, 0, 0.698383, 24.207672, -7.192523)">AIR<tspan x="602.4630126953125" dy="1em"></tspan>OUTLET</text>
<g transform="matrix(-0.387768, 0, 0, -0.200385, 743.634644, -199.991287)" style="transform-origin: 72.2405px 412.5px;">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 53.913 425 L 53.913 400 L 90.568 425 L 90.568 400"/>
</g>
<g style="transform-origin: 227.882px 539.536px;" transform="matrix(0, 0.626201, -0.563979, 0, 588.703491, -326.882202)">
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 212.224 550 L 243.484 529.073 L 243.484 550 L 212.224 529.073 L 212.224 550 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 212.224 539.499 L 200 539.499"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 243.484 539.499 L 255.764 539.499"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 807.67px 264.838px;" d="M 807.657 276.289 L 807.693 253.387" transform="matrix(0, -1.184039, 0.844567, 0, 0.000035, 0.000091)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 816.592px 247.829px;" d="M 816.56 230.114 L 816.655 265.543" transform="matrix(-1, 0, 0, -1, 0.000101, 0.000009)"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="840.215" cy="233.608" rx="10.336" ry="8.73"/>
<path d="M -100.83 -89.11 H -94.198 L -94.198 -91.071 L -85.458 -88.012 L -94.198 -84.954 L -94.198 -86.915 H -100.83 V -89.11 Z" bx:shape="arrow -100.83 -91.071 15.372 6.118 2.195 8.74 0 1@7c71f9c2" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: -93.144px -88.013px;" transform="matrix(0.000343, -1, 1, 0.000266, 933.237163, 321.956912)"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="839.876" cy="254.847" rx="10.336" ry="8.73"/>
<path d="M -100.83 -89.11 H -94.198 L -94.198 -91.071 L -85.458 -88.012 L -94.198 -84.954 L -94.198 -86.915 H -100.83 V -89.11 Z" bx:shape="arrow -100.83 -91.071 15.372 6.118 2.195 8.74 0 1@7c71f9c2" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: -93.144px -88.013px;" transform="matrix(0.000343, -1, 1, 0.000266, 932.895305, 343.197025)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 823.288px 234.151px;" d="M 823.282 242.163 L 823.294 226.137" transform="matrix(0, 1.184039, -0.844567, 0, -0.000084, 0.000084)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 822.751px 254.694px;" d="M 822.745 262.706 L 822.763 246.68" transform="matrix(0, 1.184039, -0.844567, 0, -0.00002, 0.000032)"/>
<path d="M -100.83 -89.11 H -94.198 L -94.198 -91.071 L -85.458 -88.012 L -94.198 -84.954 L -94.198 -86.915 H -100.83 V -89.11 Z" bx:shape="arrow -100.83 -91.071 15.372 6.118 2.195 8.74 0 1@7c71f9c2" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: -93.144px -88.013px;" transform="matrix(0.000343, -1, 1, 0.000266, 909.661358, 277.142276)"/>
<g transform="matrix(0.387768, 0, 0, -0.200385, 207.60318, -199.315506)" style="transform-origin: 72.2406px 412.5px;">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 53.913 425 L 53.913 400 L 90.568 425 L 90.568 400"/>
</g>
<g style="transform-origin: 227.882px 539.536px;" transform="matrix(0, 0.626201, 0.563979, 0, 51.251434, -326.206329)">
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 212.224 550 L 243.484 529.073 L 243.484 550 L 212.224 529.073 L 212.224 550 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 212.224 539.499 L 200 539.499"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 243.484 539.499 L 255.764 539.499"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 288.049px 265.514px;" d="M 288.037 254.064 L 288.073 276.966" transform="matrix(0, -1.184039, 0.844567, 0, -0.000019, 0.000042)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 279.127px 248.505px;" d="M 279.095 266.219 L 279.19 230.79"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="-255.5" cy="234.284" rx="10.336" ry="8.73" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
<path d="M 100.83 93.032 H 107.463 L 107.463 91.071 L 116.203 94.13 L 107.463 97.189 L 107.463 95.228 H 100.83 V 93.032 Z" bx:shape="arrow 100.83 91.071 15.372 6.118 2.195 8.74 0 1@116e726f" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 108.517px 94.13px;" transform="matrix(-0.000343, -1, -1, 0.000266, 147.108874, 140.490195)"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="-255.84" cy="255.523" rx="10.336" ry="8.73" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
<path d="M 100.83 93.032 H 107.463 L 107.463 91.071 L 116.203 94.13 L 107.463 97.189 L 107.463 95.228 H 100.83 V 93.032 Z" bx:shape="arrow 100.83 91.071 15.372 6.118 2.195 8.74 0 1@116e726f" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 108.517px 94.13px;" transform="matrix(-0.000343, -1, -1, 0.000266, 147.450457, 161.730307)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 272.43px 234.827px;" d="M 272.424 226.815 L 272.436 242.841" transform="matrix(0, 1.184039, -0.844567, 0, 0.000012, -0.000015)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 272.968px 255.37px;" d="M 272.962 247.358 L 272.98 263.384" transform="matrix(0, 1.184039, -0.844567, 0, -0.000011, 0.000034)"/>
<path d="M 100.83 93.032 H 107.463 L 107.463 91.071 L 116.203 94.13 L 107.463 97.189 L 107.463 95.228 H 100.83 V 93.032 Z" bx:shape="arrow 100.83 91.071 15.372 6.118 2.195 8.74 0 1@116e726f" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 108.517px 94.13px;" transform="matrix(-0.000343, -1, -1, 0.000266, 170.68468, 95.675559)"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1; text-anchor: middle; font-weight: bolder;" x="602.463" y="573.003" transform="matrix(0.826913, 0, 0, 0.698383, 47.595016, -269.416931)">AIR<tspan x="602.4630126953125" dy="1em"></tspan>INLET</text>
<path d="M -544.544 -165.9 L -541.495 -158.168 L -548.27 -158.168 L -544.544 -165.9 Z" bx:shape="triangle -548.27 -165.9 6.775 7.732 0.55 0 1@12a1c9af" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: -544.876px -162.033px;" transform="matrix(-1, 0, 0, -1, 1089.751221, 324.066467)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 544.653px 150.086px;" d="M 544.639 142.192 L 544.664 157.983" transform="matrix(-1, 0, 0, -1, -0.000086, 0.000018)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-box: fill-box; transform-origin: 50% 50%;" d="M 404.306 125.878 L 416.234 125.878 L 416.234 148.965 L 404.306 148.965 L 404.306 125.878 Z" transform="matrix(0, -1.184039, 0.844567, 0, -0.000039, -0.000007)"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 498.152px 180.679px;" d="M 408.92 144.738 L 408.92 130.357"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-box: fill-box; transform-origin: 50% 50%;" d="M 427.836 146.217 L 460.619 127.367 L 460.619 146.217 L 427.836 127.367 L 427.836 146.217 Z" transform="matrix(0, -1.184039, 0.844567, 0, 0.000009, 0.000009)"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-box: fill-box; transform-origin: 50% 50%;" d="M 426.851 157.594 L 426.851 116.351" transform="matrix(0, -1.18404, 0.844567, 0, -0.000439, -0.000081)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 647.335px 171.745px;" d="M 647.177 186.902 L 647.494 156.588" transform="matrix(-1, 0, 0, -1, -0.000021, -0.000027)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 680.766px 138.025px;" d="M 674.802 149.569 L 686.73 149.569 L 686.73 126.482 L 674.802 126.482 L 674.802 149.569 Z" transform="matrix(0, -1.184039, 0.844567, 0, 0.000004, 0.000043)"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 897.148px 118.148px;" d="M 682.129 145.344 L 682.129 130.964"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 646.809px 137.396px;" d="M 630.417 127.971 L 663.201 146.821 L 663.201 127.971 L 630.417 146.821 L 630.417 127.971 Z" transform="matrix(0, -1.184039, 0.844567, 0, -0.000004, -0.000033)"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 664.185px 137.577px;" d="M 664.185 116.956 L 664.186 158.199" transform="matrix(0, -1.184039, 0.844567, 0, -0.000026, -0.000041)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 590.855 148.509 L 607.577 148.509 L 607.577 164.977 L 590.855 164.977 L 590.855 148.509 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 733.701px 224.488px;" d="M 590.555 155.608 L 607.58 155.608"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 576.981 192.146 L 622.941 178.701 L 622.941 192.146 L 576.981 178.701 L 576.981 192.146 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 599.746 185.455 L 599.746 156.037"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 640.474 90.952 L 652.913 90.952 L 652.913 107.419 L 640.474 107.419 L 640.474 90.952 Z"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 646.819px 113.115px;" d="M 646.799 107.896 L 646.839 118.334"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 650.466px 95.308px;" d="M 650.455 97.624 L 650.476 92.992" transform="matrix(0, -1.184039, 0.844567, 0, 0.000035, 0.00002)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 642.857px 102.712px;" d="M 642.846 105.028 L 642.867 100.396" transform="matrix(0, -1.184039, 0.844567, 0, 0.000009, 0.000035)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 437.929 90.244 L 450.367 90.244 L 450.367 106.712 L 437.929 106.712 L 437.929 90.244 Z"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 444.274px 112.408px;" d="M 444.253 107.189 L 444.293 117.627"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 447.919px 94.601px;" d="M 447.909 96.917 L 447.93 92.285" transform="matrix(0, -1.184039, 0.844567, 0, -0.000022, 0.000005)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 440.31px 102.004px;" d="M 440.3 104.321 L 440.32 99.689" transform="matrix(0, -1.184039, 0.844567, 0, -0.000028, -0.000004)"/>
<rect x="43.443" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="53.987" y="423.091" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">RUN HOUR</text>
<rect x="126.135" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="424.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="424.066" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
<rect x="43.443" y="308.191" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; stroke-width: 1; font-weight: bold;" x="53.987" y="461.382" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">PURGE HOUR</text>
<rect x="126.135" y="308.191" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="463.397" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="463.357" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
<rect x="43.443" y="333.129" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; font-weight: 700; white-space: pre;" x="53.987" y="498.091" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER HOUR</text>
<rect x="126.135" y="333.129" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="499.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="499.066" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
<rect x="43.65" y="360.147" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(248, 213, 14);"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; font-weight: 700; white-space: pre; stroke-width: 1;" x="54.237" y="536.777" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Alarm Info</text>
<rect x="126.341" y="360.147" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.3" y="537.792" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225.25" y="537.752" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
<rect x="43.443" y="160.275" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="56.987" y="250.585" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">RT or LT Dry</text>
<rect x="126.135" y="160.275" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="180.05" y="251.6" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">LT Dry</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="177" y="251.56" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">RT Dry</text>
<rect x="43.443" y="187.715" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; stroke-width: 1; font-weight: bold;" x="53.987" y="288.876" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Opmode</text>
<rect x="126.135" y="187.715" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HTD</text>
<rect x="43.443" y="214.051" width="165.383" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; font-weight: 700; white-space: pre; stroke-width: 1;" x="53.987" y="322.585" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Step</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="177.05" y="323.6" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="233" y="323.56" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">s</text>
<rect x="43.443" y="241.422" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; font-weight: 700; white-space: pre; stroke-width: 1;" x="54.237" y="364.271" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Cycle Timer</text>
<rect x="126.341" y="241.068" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="168.775" y="365.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225.25" y="365.246" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">s</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; font-weight: 700; white-space: pre; stroke-width: 1;" x="141.894" y="324.069" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Time</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="92.151" y="325.554" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">##</text>
<rect x="870.356" y="142.816" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="1060.06" y="224.103" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dryer Status</text>
<rect x="870.356" y="170.304" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.702" cy="192.401" rx="20.673" ry="17.46"/>
<rect x="870.356" y="230.113" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="1069.06" y="349.103" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Shutdown</text>
<rect x="870.356" y="257.602" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.702" cy="279.699" rx="20.673" ry="17.46"/>
<rect x="870.356" y="317.411" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="1059.06" y="474.103" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Alarm Status</text>
<rect x="870.356" y="344.9" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.702" cy="366.997" rx="20.673" ry="17.46"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="380.451" y="296.591" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="334.165" cy="232.104" rx="13.582" ry="12.517"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="379.214" y="423.395" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.623" cy="320.662" rx="13.582" ry="12.517"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="897.237" y="299.014" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.502" cy="233.796" rx="13.582" ry="12.517"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="896" y="425.818" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="762.96" cy="322.354" rx="13.582" ry="12.517"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 35px; stroke-width: 1; font-weight: bold;" x="348.875" y="78.242" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">AIR DRYER UNIT B (01-CL-10535-B)</text>
</svg>

After

Width:  |  Height:  |  Size: 40 KiB

238
svg/air_dryer_C_rev.svg Normal file
View File

@@ -0,0 +1,238 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 500" xmlns:bx="https://boxy-svg.com">
<defs>
<bx:grid x="0" y="0" width="25" height="25"/>
</defs>
<rect y="10.407" width="972.648" height="439.023" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" x="12.119"/>
<g transform="matrix(0.826913, 0, 0, 0.698383, 74.03907, 53.375034)">
<ellipse style="stroke: rgb(0, 0, 0); fill: rgb(243, 243, 243);" cx="315" cy="183.068" rx="45" ry="45"/>
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);" cx="315" cy="449.112" rx="45" ry="45"/>
<rect x="270" y="180" width="90" height="270" style="stroke: rgb(0, 0, 0); fill: rgb(243, 243, 243);"/>
</g>
<g transform="matrix(0.826913, 0, 0, 0.698383, 500.726135, 53.375034)">
<ellipse style="stroke: rgb(0, 0, 0); fill: rgb(243, 243, 243);" cx="315" cy="183.068" rx="45" ry="45"/>
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);" cx="315" cy="449.112" rx="45" ry="45"/>
<rect x="270" y="180" width="90" height="270" style="stroke: rgb(0, 0, 0); fill: rgb(243, 243, 243);"/>
</g>
<rect x="371.728" y="182.483" width="8.269" height="6.984" style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-box: fill-box; transform-origin: 50% 50%;" d="M 551.111 -16.154 L 551.202 389.079" transform="matrix(0, -1.184039, 0.844567, 0, 0.000036, 0.000096)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 479.043 149.568 L 495.765 149.568 L 495.765 166.035 L 479.043 166.035 L 479.043 149.568 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 598.485px 226.003px;" d="M 478.737 156.666 L 495.763 156.666"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 465.169 193.204 L 511.13 179.759 L 511.13 193.204 L 465.169 179.759 L 465.169 193.204 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 487.934 186.514 L 487.934 157.095"/>
<rect x="715.724" y="182.138" width="8.269" height="6.984" style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;"/>
<g transform="matrix(0.826913, 0, 0, 0.698383, -2.11712, 3.138935)">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 660.838px 251.447px;" d="M 660.846 262.894 L 660.846 240"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 490.992px 230.229px;" d="M 646.097 240.002 L 676.271 240.002"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 443.701px 171.141px;" d="M 443.542 155.983 L 443.859 186.298"/>
<g transform="matrix(0.826913, 0, 0, 0.698383, 0.443817, 3.138935)">
<rect x="752" y="355.455" width="42.438" height="3.527" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
<rect x="756.328" y="359.271" width="34.034" height="53.968" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
<rect x="756.146" y="352.019" width="34.034" height="3.38" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);" cx="773.446" cy="384.7" rx="11.751" ry="11.009"/>
</g>
<rect x="461.861" y="211.956" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px;" x="561" y="309.954" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp SP</text>
<rect x="461.861" y="221.924" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="564.279" y="330.561" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="609.476" y="330.521" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
<rect x="461.424" y="242.149" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="567.471" y="352.188" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp</text>
<rect x="461.424" y="252.117" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="563.75" y="373.795" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="Dry1_HeatTempCelsius">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="608.947" y="373.755" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
<rect x="535.456" y="242.272" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="659" y="352.363" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp</text>
<rect x="535.456" y="252.24" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="653.279" y="373.97" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="698.476" y="373.93" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1; font-weight: bold;" x="748" y="347.676" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER</text>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 678.512px 258.693px;" d="M 678.467 229.321 L 678.558 288.066" transform="matrix(0, 1.184039, -0.844567, 0, -0.000022, -0.000005)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 703.162px 309.166px;" d="M 703.004 258.049 L 703.32 360.282"/>
<rect x="371.639" y="358.212" width="8.269" height="6.984" style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 692.133px 361.256px;" d="M 692.126 397.022 L 692.14 325.49" transform="matrix(0, -1.184039, 0.844567, 0, 0.000011, 0.00005)"/>
<rect x="715.635" y="357.867" width="8.269" height="6.984" style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 798px 493.641px;" d="M 661.975 334.874 L 661.975 360.911"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 552.59px 334.782px;" d="M 552.508 244.429 L 552.665 425.136" transform="matrix(0, 1.184039, -0.844567, 0, -0.000058, 0.000018)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 601.786px 283.137px;" d="M 601.741 312.51 L 601.832 253.764" transform="matrix(0, 1.184039, -0.844567, 0, -0.000063, 0.000028)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 577.266px 308.682px;" d="M 577.237 334.87 L 577.295 282.492" transform="matrix(-1, 0, 0, -1, -0.000041, -0.00003)"/>
<g transform="matrix(-0.491177, 0, 0, 0.523644, 491.13504, 29.785091)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 582.89 L 30 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 98.289 582.89 L 118.071 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 600 L 49.894 565.78 L 98.289 600 L 98.289 565.78"/>
<circle style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="1094.77" cy="561.359" r="39" transform="matrix(0.17796, 0, 0, 0.155258, -141.765747, 481.674255)"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 406.945px 361.188px;" d="M 406.92 329.322 L 406.969 393.054" transform="matrix(0, 1.184039, -0.844567, 0, 0.00001, 0.00007)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 521.472px 494.156px;" d="M 433.307 334.116 L 433.308 362.382"/>
<g transform="matrix(0.491177, 0, 0, 0.523644, 419.010895, 56.68491)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 582.89 L 30 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 98.289 582.89 L 118.071 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 600 L 49.894 565.78 L 98.289 600 L 98.289 565.78"/>
<circle style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="1094.77" cy="561.359" r="39" transform="matrix(0.17796, 0, 0, 0.155258, -141.765747, 481.674255)"/>
</g>
<g transform="matrix(0.491177, 0, 0, 0.523644, 603.674133, 29.692444)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 582.89 L 30 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 98.289 582.89 L 118.071 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 600 L 49.894 565.78 L 98.289 600 L 98.289 565.78"/>
<circle style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="1094.77" cy="561.359" r="39" transform="matrix(0.17796, 0, 0, 0.155258, -141.765747, 481.674255)"/>
</g>
<g transform="matrix(-0.491177, 0, 0, 0.523644, 677.00824, 56.209183)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 582.89 L 30 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 98.289 582.89 L 118.071 582.89"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 49.894 600 L 49.894 565.78 L 98.289 600 L 98.289 565.78"/>
<circle style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="1094.77" cy="561.359" r="39" transform="matrix(0.17796, 0, 0, 0.155258, -141.765747, 481.674255)"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 548.454px 361.62px;" d="M 548.372 271.267 L 548.529 451.974" transform="matrix(0, 1.184039, -0.844567, 0, 0.00006, -0.000036)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 578.392px 383.459px;" d="M 578.329 405.407 L 578.456 361.51" transform="matrix(-1, 0, 0, -1, -0.000055, -0.000003)"/>
<g transform="matrix(0.826913, 0, 0, -0.698383, 0.257882, 545.083069)" style="">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 660.838px 251.447px;" d="M 660.846 262.894 L 660.846 240"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 490.992px 230.229px;" d="M 646.097 240.002 L 676.271 240.002"/>
</g>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.525; transform-origin: 704.638px 576.106px;" d="M 621.828 405.49 L 634.485 405.49"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.525; transform-origin: 584.749px 405.496px;" d="M 591.042 405.497 L 578.456 405.498"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 1.525; transform-origin: 606.436px 405.492px;" d="M 591.042 398.364 L 591.042 412.623 L 621.831 398.364 L 621.831 412.623 L 591.042 398.364 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.525; transform-origin: 599.432px 413.831px;" d="M 592.464 422.169 L 592.464 405.493 L 606.401 405.493" transform="matrix(-1, 0, 0, -1, -0.000078, -0.000049)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 648.376px 405.65px;" d="M 648.369 435.621 L 648.383 375.678" transform="matrix(0, -1.184039, 0.844567, 0, 0.000012, -0.00004)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 673.882px 406.067px;" d="M 673.869 398.172 L 673.893 413.963" transform="matrix(-1, 0, 0, -1, -0.000092, -0.000022)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 692.821px 404.67px;" d="M 692.786 392.381 L 692.854 416.962" transform="matrix(0, 1.184039, -0.844567, 0, 0.000001, -0.000097)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 703.406px 383.726px;" d="M 703.396 405.334 L 703.415 362.116"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 681.807px 406.051px;" d="M 681.793 398.157 L 681.818 413.948" transform="matrix(-1, 0, 0, -1, 0.000055, 0.000046)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 677.987px 397.326px;" d="M 677.977 403.041 L 677.995 391.61"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 677.933px 411.78px;" d="M 677.924 417.977 L 677.941 405.582"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="650.811" cy="385.504" rx="10.336" ry="8.73"/>
<path d="M -100.83 -89.11 H -94.198 L -94.198 -91.071 L -85.458 -88.012 L -94.198 -84.954 L -94.198 -86.915 H -100.83 V -89.11 Z" bx:shape="arrow -100.83 -91.071 15.372 6.118 2.195 8.74 0 1@7c71f9c2" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); transform-box: fill-box; transform-origin: 50% 50%; stroke-width: 0.763;" transform="matrix(0.000343, -1, 1, 0.000266, 743.834961, 473.853179)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 651.034px 399.693px;" d="M 651.025 405.408 L 651.042 393.977"/>
<path d="M -546.834 -405.87 L -543.785 -398.138 L -550.56 -398.138 L -546.834 -405.87 Z" bx:shape="triangle -550.56 -405.87 6.775 7.732 0.55 0 1@11f2e68a" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); transform-box: fill-box; transform-origin: 50% 50%; stroke-width: 0.763;" transform="matrix(-1, 0, 0, -1, 1094.344971, 804.007996)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 546.953px 390.056px;" d="M 546.94 382.161 L 546.965 397.952" transform="matrix(-1, 0, 0, -1, -0.000013, 0.000046)"/>
<rect x="427.269" y="377.282" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="532.167" y="545.681" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dew Temp</text>
<rect x="427.269" y="387.25" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.446" y="567.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="567.643" y="567.248" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°C</text>
<rect x="427.27" y="412.201" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="532.168" y="595.681" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dew Temp</text>
<rect x="427.27" y="422.169" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.447" y="617.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="567.644" y="617.248" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1; text-anchor: middle; font-weight: bolder;" x="602.463" y="573.003" transform="matrix(0.826913, 0, 0, 0.698383, 24.207672, -7.192523)">AIR<tspan x="602.4630126953125" dy="1em"></tspan>OUTLET</text>
<g transform="matrix(-0.387768, 0, 0, -0.200385, 743.634644, -199.991287)" style="transform-origin: 72.2405px 412.5px;">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 53.913 425 L 53.913 400 L 90.568 425 L 90.568 400"/>
</g>
<g style="transform-origin: 227.882px 539.536px;" transform="matrix(0, 0.626201, -0.563979, 0, 588.703491, -326.882202)">
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 212.224 550 L 243.484 529.073 L 243.484 550 L 212.224 529.073 L 212.224 550 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 212.224 539.499 L 200 539.499"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 243.484 539.499 L 255.764 539.499"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 807.67px 264.838px;" d="M 807.657 276.289 L 807.693 253.387" transform="matrix(0, -1.184039, 0.844567, 0, 0.000035, 0.000091)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 816.592px 247.829px;" d="M 816.56 230.114 L 816.655 265.543" transform="matrix(-1, 0, 0, -1, 0.000101, 0.000009)"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="840.215" cy="233.608" rx="10.336" ry="8.73"/>
<path d="M -100.83 -89.11 H -94.198 L -94.198 -91.071 L -85.458 -88.012 L -94.198 -84.954 L -94.198 -86.915 H -100.83 V -89.11 Z" bx:shape="arrow -100.83 -91.071 15.372 6.118 2.195 8.74 0 1@7c71f9c2" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: -93.144px -88.013px;" transform="matrix(0.000343, -1, 1, 0.000266, 933.237163, 321.956912)"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="839.876" cy="254.847" rx="10.336" ry="8.73"/>
<path d="M -100.83 -89.11 H -94.198 L -94.198 -91.071 L -85.458 -88.012 L -94.198 -84.954 L -94.198 -86.915 H -100.83 V -89.11 Z" bx:shape="arrow -100.83 -91.071 15.372 6.118 2.195 8.74 0 1@7c71f9c2" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: -93.144px -88.013px;" transform="matrix(0.000343, -1, 1, 0.000266, 932.895305, 343.197025)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 823.288px 234.151px;" d="M 823.282 242.163 L 823.294 226.137" transform="matrix(0, 1.184039, -0.844567, 0, -0.000084, 0.000084)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 822.751px 254.694px;" d="M 822.745 262.706 L 822.763 246.68" transform="matrix(0, 1.184039, -0.844567, 0, -0.00002, 0.000032)"/>
<path d="M -100.83 -89.11 H -94.198 L -94.198 -91.071 L -85.458 -88.012 L -94.198 -84.954 L -94.198 -86.915 H -100.83 V -89.11 Z" bx:shape="arrow -100.83 -91.071 15.372 6.118 2.195 8.74 0 1@7c71f9c2" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: -93.144px -88.013px;" transform="matrix(0.000343, -1, 1, 0.000266, 909.661358, 277.142276)"/>
<g transform="matrix(0.387768, 0, 0, -0.200385, 207.60318, -199.315506)" style="transform-origin: 72.2406px 412.5px;">
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 53.913 425 L 53.913 400 L 90.568 425 L 90.568 400"/>
</g>
<g style="transform-origin: 227.882px 539.536px;" transform="matrix(0, 0.626201, 0.563979, 0, 51.251434, -326.206329)">
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 212.224 550 L 243.484 529.073 L 243.484 550 L 212.224 529.073 L 212.224 550 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 212.224 539.499 L 200 539.499"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 243.484 539.499 L 255.764 539.499"/>
</g>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 288.049px 265.514px;" d="M 288.037 254.064 L 288.073 276.966" transform="matrix(0, -1.184039, 0.844567, 0, -0.000019, 0.000042)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 279.127px 248.505px;" d="M 279.095 266.219 L 279.19 230.79"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="-255.5" cy="234.284" rx="10.336" ry="8.73" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
<path d="M 100.83 93.032 H 107.463 L 107.463 91.071 L 116.203 94.13 L 107.463 97.189 L 107.463 95.228 H 100.83 V 93.032 Z" bx:shape="arrow 100.83 91.071 15.372 6.118 2.195 8.74 0 1@116e726f" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 108.517px 94.13px;" transform="matrix(-0.000343, -1, -1, 0.000266, 147.108874, 140.490195)"/>
<ellipse style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="-255.84" cy="255.523" rx="10.336" ry="8.73" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
<path d="M 100.83 93.032 H 107.463 L 107.463 91.071 L 116.203 94.13 L 107.463 97.189 L 107.463 95.228 H 100.83 V 93.032 Z" bx:shape="arrow 100.83 91.071 15.372 6.118 2.195 8.74 0 1@116e726f" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 108.517px 94.13px;" transform="matrix(-0.000343, -1, -1, 0.000266, 147.450457, 161.730307)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 272.43px 234.827px;" d="M 272.424 226.815 L 272.436 242.841" transform="matrix(0, 1.184039, -0.844567, 0, 0.000012, -0.000015)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 272.968px 255.37px;" d="M 272.962 247.358 L 272.98 263.384" transform="matrix(0, 1.184039, -0.844567, 0, -0.000011, 0.000034)"/>
<path d="M 100.83 93.032 H 107.463 L 107.463 91.071 L 116.203 94.13 L 107.463 97.189 L 107.463 95.228 H 100.83 V 93.032 Z" bx:shape="arrow 100.83 91.071 15.372 6.118 2.195 8.74 0 1@116e726f" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 108.517px 94.13px;" transform="matrix(-0.000343, -1, -1, 0.000266, 170.68468, 95.675559)"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1; text-anchor: middle; font-weight: bolder;" x="602.463" y="573.003" transform="matrix(0.826913, 0, 0, 0.698383, 47.595016, -269.416931)">AIR<tspan x="602.4630126953125" dy="1em"></tspan>INLET</text>
<path d="M -544.544 -165.9 L -541.495 -158.168 L -548.27 -158.168 L -544.544 -165.9 Z" bx:shape="triangle -548.27 -165.9 6.775 7.732 0.55 0 1@12a1c9af" style="fill: rgb(0, 0, 0); stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: -544.876px -162.033px;" transform="matrix(-1, 0, 0, -1, 1089.751221, 324.066467)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 544.653px 150.086px;" d="M 544.639 142.192 L 544.664 157.983" transform="matrix(-1, 0, 0, -1, -0.000086, 0.000018)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-box: fill-box; transform-origin: 50% 50%;" d="M 404.306 125.878 L 416.234 125.878 L 416.234 148.965 L 404.306 148.965 L 404.306 125.878 Z" transform="matrix(0, -1.184039, 0.844567, 0, -0.000039, -0.000007)"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 498.152px 180.679px;" d="M 408.92 144.738 L 408.92 130.357"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-box: fill-box; transform-origin: 50% 50%;" d="M 427.836 146.217 L 460.619 127.367 L 460.619 146.217 L 427.836 127.367 L 427.836 146.217 Z" transform="matrix(0, -1.184039, 0.844567, 0, 0.000009, 0.000009)"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-box: fill-box; transform-origin: 50% 50%;" d="M 426.851 157.594 L 426.851 116.351" transform="matrix(0, -1.18404, 0.844567, 0, -0.000439, -0.000081)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 647.335px 171.745px;" d="M 647.177 186.902 L 647.494 156.588" transform="matrix(-1, 0, 0, -1, -0.000021, -0.000027)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 680.766px 138.025px;" d="M 674.802 149.569 L 686.73 149.569 L 686.73 126.482 L 674.802 126.482 L 674.802 149.569 Z" transform="matrix(0, -1.184039, 0.844567, 0, 0.000004, 0.000043)"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 897.148px 118.148px;" d="M 682.129 145.344 L 682.129 130.964"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 646.809px 137.396px;" d="M 630.417 127.971 L 663.201 146.821 L 663.201 127.971 L 630.417 146.821 L 630.417 127.971 Z" transform="matrix(0, -1.184039, 0.844567, 0, -0.000004, -0.000033)"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 664.185px 137.577px;" d="M 664.185 116.956 L 664.186 158.199" transform="matrix(0, -1.184039, 0.844567, 0, -0.000026, -0.000041)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 590.855 148.509 L 607.577 148.509 L 607.577 164.977 L 590.855 164.977 L 590.855 148.509 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932; transform-origin: 733.701px 224.488px;" d="M 590.555 155.608 L 607.58 155.608"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 576.981 192.146 L 622.941 178.701 L 622.941 192.146 L 576.981 178.701 L 576.981 192.146 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 599.746 185.455 L 599.746 156.037"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 640.474 90.952 L 652.913 90.952 L 652.913 107.419 L 640.474 107.419 L 640.474 90.952 Z"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 646.819px 113.115px;" d="M 646.799 107.896 L 646.839 118.334"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 650.466px 95.308px;" d="M 650.455 97.624 L 650.476 92.992" transform="matrix(0, -1.184039, 0.844567, 0, 0.000035, 0.00002)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 642.857px 102.712px;" d="M 642.846 105.028 L 642.867 100.396" transform="matrix(0, -1.184039, 0.844567, 0, 0.000009, 0.000035)"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 0.932;" d="M 437.929 90.244 L 450.367 90.244 L 450.367 106.712 L 437.929 106.712 L 437.929 90.244 Z"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 444.274px 112.408px;" d="M 444.253 107.189 L 444.293 117.627"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 447.919px 94.601px;" d="M 447.909 96.917 L 447.93 92.285" transform="matrix(0, -1.184039, 0.844567, 0, -0.000022, 0.000005)"/>
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 0.763; transform-origin: 440.31px 102.004px;" d="M 440.3 104.321 L 440.32 99.689" transform="matrix(0, -1.184039, 0.844567, 0, -0.000028, -0.000004)"/>
<rect x="43.443" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="53.987" y="423.091" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">RUN HOUR</text>
<rect x="126.135" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="424.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="424.066" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
<rect x="43.443" y="308.191" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; stroke-width: 1; font-weight: bold;" x="53.987" y="461.382" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">PURGE HOUR</text>
<rect x="126.135" y="308.191" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="463.397" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="463.357" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
<rect x="43.443" y="333.129" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; font-weight: 700; white-space: pre;" x="53.987" y="498.091" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER HOUR</text>
<rect x="126.135" y="333.129" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="499.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="499.066" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
<rect x="43.65" y="360.147" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(248, 213, 14);"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; font-weight: 700; white-space: pre; stroke-width: 1;" x="54.237" y="536.777" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Alarm Info</text>
<rect x="126.341" y="360.147" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.3" y="537.792" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225.25" y="537.752" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
<rect x="43.443" y="160.275" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="56.987" y="250.585" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">RT or LT Dry</text>
<rect x="126.135" y="160.275" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="180.05" y="251.6" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">LT Dry</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="177" y="251.56" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">RT Dry</text>
<rect x="43.443" y="187.715" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; stroke-width: 1; font-weight: bold;" x="53.987" y="288.876" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Opmode</text>
<rect x="126.135" y="187.715" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HTD</text>
<rect x="43.443" y="214.051" width="165.383" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; font-weight: 700; white-space: pre; stroke-width: 1;" x="53.987" y="322.585" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Step</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="177.05" y="323.6" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="233" y="323.56" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">s</text>
<rect x="43.443" y="241.422" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; font-weight: 700; white-space: pre; stroke-width: 1;" x="54.237" y="364.271" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Cycle Timer</text>
<rect x="126.341" y="241.068" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="168.775" y="365.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225.25" y="365.246" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">s</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; font-weight: 700; white-space: pre; stroke-width: 1;" x="141.894" y="324.069" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Time</text>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="92.151" y="325.554" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">##</text>
<rect x="870.356" y="142.816" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="1060.06" y="224.103" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dryer Status</text>
<rect x="870.356" y="170.304" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.702" cy="192.401" rx="20.673" ry="17.46"/>
<rect x="870.356" y="230.113" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="1069.06" y="349.103" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Shutdown</text>
<rect x="870.356" y="257.602" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.702" cy="279.699" rx="20.673" ry="17.46"/>
<rect x="870.356" y="317.411" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="1059.06" y="474.103" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Alarm Status</text>
<rect x="870.356" y="344.9" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.702" cy="366.997" rx="20.673" ry="17.46"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="380.451" y="296.591" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="334.165" cy="232.104" rx="13.582" ry="12.517"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="379.214" y="423.395" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.623" cy="320.662" rx="13.582" ry="12.517"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="897.237" y="299.014" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.502" cy="233.796" rx="13.582" ry="12.517"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="896" y="425.818" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="762.96" cy="322.354" rx="13.582" ry="12.517"/>
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 35px; stroke-width: 1; font-weight: bold;" x="348.875" y="78.242" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">AIR DRYER UNIT C (01-CL-10539-C)</text>
</svg>

After

Width:  |  Height:  |  Size: 40 KiB