+ {showClear ? (
+
+ ) : loading ? (
) : (

@@ -418,6 +446,11 @@ const ProductData = ({
const [unitsError, setUnitsError] = React.useState(null);
const [isSavingDraft, setIsSavingDraft] = React.useState(false);
+ const latestDraftRef = React.useRef({ products: [], deletedProducts: [], remarks: '' });
+ const draftKeyRef = React.useRef('');
+ const restoredDraftKeyRef = React.useRef('');
+ const restoredDraftSignatureRef = React.useRef('');
+
const location = useLocation(); // Add this line
@@ -441,6 +474,57 @@ const ProductData = ({
return '';
});
+ const getLocalDraftKey = React.useCallback(() => {
+ if (typeof window === 'undefined') return '';
+
+ const currentSubmissionStr = localStorage.getItem('currentSubmission');
+ let currentSubmission = null;
+ try {
+ currentSubmission = currentSubmissionStr ? JSON.parse(currentSubmissionStr) : null;
+ } catch (e) {
+ currentSubmission = null;
+ }
+
+ const establishmentId =
+ currentSubmission?.establishment_id ||
+ currentSubmission?.establishment?.id ||
+ localStorage.getItem('establishment_id') ||
+ localStorage.getItem('establishmentId') ||
+ '';
+
+ const effectiveQuarter = quarter || localStorage.getItem('currentQuarter') || localStorage.getItem('current_quarter') || '';
+ const effectiveYear = year || localStorage.getItem('currentYear') || localStorage.getItem('current_year') || '';
+
+ if (!establishmentId || !effectiveQuarter || !effectiveYear) return '';
+ const formattedQuarter = String(effectiveQuarter).startsWith('Q')
+ ? String(effectiveQuarter)
+ : `Q${effectiveQuarter}`;
+
+ return `productDataDraft:${establishmentId}:${effectiveYear}:${formattedQuarter}`;
+ }, [quarter, year]);
+
+ const persistLocalDraft = React.useCallback((payload) => {
+ if (typeof window === 'undefined') return;
+ const key = draftKeyRef.current || getLocalDraftKey();
+ if (!key) return;
+
+ try {
+ localStorage.setItem(
+ key,
+ JSON.stringify({
+ ...payload,
+ updatedAt: Date.now(),
+ })
+ );
+ } catch (e) {
+ // ignore localStorage errors
+ }
+ }, [getLocalDraftKey]);
+
+ const flushLocalDraft = React.useCallback(() => {
+ persistLocalDraft(latestDraftRef.current);
+ }, [persistLocalDraft]);
+
const handleRemarksChange = (e) => {
const value = e.target.value;
setRemarks(value);
@@ -449,6 +533,109 @@ const ProductData = ({
}
};
+ // Keep latest draft snapshot in a ref (used by beforeunload/offline handlers)
+ React.useEffect(() => {
+ latestDraftRef.current = {
+ products,
+ deletedProducts,
+ remarks,
+ };
+ }, [products, deletedProducts, remarks]);
+
+ // Initialize draft key and restore any existing draft for this establishment/period
+ React.useEffect(() => {
+ if (typeof window === 'undefined') return;
+ const key = getLocalDraftKey();
+ draftKeyRef.current = key;
+ if (!key) return;
+
+ try {
+ const raw = localStorage.getItem(key);
+ if (!raw) return;
+ const parsed = JSON.parse(raw);
+ if (!parsed) return;
+
+ const parsedSignature = JSON.stringify({
+ products: Array.isArray(parsed.products) ? parsed.products : [],
+ deletedProducts: Array.isArray(parsed.deletedProducts) ? parsed.deletedProducts : [],
+ remarks: typeof parsed.remarks === 'string' ? parsed.remarks : '',
+ });
+
+ // Restore only when the incoming props don't already have meaningful user-entered data
+ const hasExistingData = Array.isArray(products) && products.some((p) => {
+ if (!p) return false;
+ return Boolean(
+ p.product || p.productId || p.product_id ||
+ p.unit || p.unit_id ||
+ p.capacity ||
+ p.octQuantity || p.novQuantity || p.decQuantity ||
+ p.janQuantity || p.febQuantity || p.marQuantity ||
+ p.aprQuantity || p.mayQuantity || p.junQuantity ||
+ p.octCost || p.novCost || p.decCost ||
+ p.janCost || p.febCost || p.marCost ||
+ p.aprCost || p.mayCost || p.junCost ||
+ p.variationReason || p.zeroTargetReason || p.remarks
+ );
+ });
+
+ if (!hasExistingData && Array.isArray(parsed.products)) {
+ // If we've already restored this exact payload for this key, don't keep re-applying.
+ if (restoredDraftKeyRef.current === key && restoredDraftSignatureRef.current === parsedSignature) {
+ return;
+ }
+ onProductsChange(parsed.products);
+ if (Array.isArray(parsed.deletedProducts)) setDeletedProducts(parsed.deletedProducts);
+ if (typeof parsed.remarks === 'string') setRemarks(parsed.remarks);
+ restoredDraftKeyRef.current = key;
+ restoredDraftSignatureRef.current = parsedSignature;
+ }
+ } catch (e) {
+ // ignore parse errors
+ }
+ }, [getLocalDraftKey, onProductsChange, products]);
+
+ // Debounced autosave to localStorage whenever user changes anything
+ React.useEffect(() => {
+ if (typeof window === 'undefined') return;
+ const key = draftKeyRef.current || getLocalDraftKey();
+ if (!key) return;
+
+ const handle = setTimeout(() => {
+ persistLocalDraft({ products, deletedProducts, remarks });
+ }, 400);
+
+ return () => clearTimeout(handle);
+ }, [products, deletedProducts, remarks, getLocalDraftKey, persistLocalDraft]);
+
+ // Accidental exit handling: save on tab close, refresh, background, offline
+ React.useEffect(() => {
+ if (typeof window === 'undefined') return;
+
+ const onBeforeUnload = () => {
+ flushLocalDraft();
+ };
+
+ const onVisibilityChange = () => {
+ if (document.visibilityState === 'hidden') {
+ flushLocalDraft();
+ }
+ };
+
+ const onOffline = () => {
+ flushLocalDraft();
+ };
+
+ window.addEventListener('beforeunload', onBeforeUnload);
+ document.addEventListener('visibilitychange', onVisibilityChange);
+ window.addEventListener('offline', onOffline);
+
+ return () => {
+ window.removeEventListener('beforeunload', onBeforeUnload);
+ document.removeEventListener('visibilitychange', onVisibilityChange);
+ window.removeEventListener('offline', onOffline);
+ };
+ }, [flushLocalDraft]);
+
useEffect(() => {
const fetchQuarterPeriods = async () => {
try {
@@ -564,9 +751,9 @@ useEffect(() => {
junCost: product.forecast_cost_period_three?.toString() || '0',
// Variation reasons
- variationReason: product.variation_reason_master_id?.toString() || '0',
+ variationReason: product.variation_reason_master_id !== null && product.variation_reason_master_id !== undefined ? product.variation_reason_master_id.toString() : null,
otherVariationReason: product.other_variation_reason || '',
- zeroTargetReason: product.zero_target_reason_master_id?.toString() || '',
+ zeroTargetReason: product.zero_target_reason_master_id !== null && product.zero_target_reason_master_id !== undefined ? product.zero_target_reason_master_id.toString() : null,
otherZeroTargetReason: product.other_zero_target_reason || '',
remarks: product.remarks || '',
@@ -730,9 +917,9 @@ const existingProduct = currentSubmission.products?.find(p =>
// Next quarter forecast (Q2) - December
forecast_quantity: product.decQuantity || '0',
forecast_cost: product.decCost || '0',
- variation_reason_master_id: product.variationReason || '0',
+ variation_reason_master_id: product.variationReason === null ? null : (product.variationReason || '0'),
other_variation_reason: product.otherVariationReason || '0',
- zero_target_reason_master_id: product.zeroTargetReason || '0',
+ zero_target_reason_master_id: product.zeroTargetReason === null ? null : (product.zeroTargetReason || '0'),
other_zero_target_reason: product.otherZeroTargetReason || '0',
remarks: product.remarks || remarks || '' // Use product.remarks if available, otherwise use the component's remarks state
};
@@ -775,9 +962,9 @@ const existingProduct = currentSubmission.products?.find(p =>
current_cost: product.novCost || '0',
forecast_quantity: product.decQuantity || '0',
forecast_cost: product.decCost || '0',
- variation_reason_master_id: product.variationReason || '0',
+ variation_reason_master_id: product.variationReason === null ? null : (product.variationReason || '0'),
other_variation_reason: product.otherVariationReason || '0',
- zero_target_reason_master_id: product.zeroTargetReason || '0',
+ zero_target_reason_master_id: product.zeroTargetReason === null ? null : (product.zeroTargetReason || '0'),
other_zero_target_reason: product.otherZeroTargetReason || '0',
remarks: product.remarks || remarks || '',
is_active: false // Mark as inactive
@@ -893,9 +1080,9 @@ useEffect(() => {
junCost: product.forecast_cost_period_three || '0',
// Variation reasons
- variationReason: product.variation_reason_master_id?.toString() || '',
+ variationReason: product.variation_reason_master_id !== null && product.variation_reason_master_id !== undefined ? product.variation_reason_master_id.toString() : null,
otherVariationReason: product.other_variation_reason || '',
- zeroTargetReason: product.zero_target_reason_master_id?.toString() || '',
+ zeroTargetReason: product.zero_target_reason_master_id !== null && product.zero_target_reason_master_id !== undefined ? product.zero_target_reason_master_id.toString() : null,
otherZeroTargetReason: product.other_zero_target_reason || '',
remarks: product.remarks || ''
};
@@ -1295,8 +1482,8 @@ useEffect(() => {
// Dynamic validation for variation reason based on cost variation
const mandatoryVariationReason = isVariationReasonMandatory(product);
if (mandatoryVariationReason) {
- // Check if variation reason is selected
- if (!product.variationReason || product.variationReason === '0' || product.variationReason === '') {
+ // Check if variation reason is selected (null means no selection)
+ if (product.variationReason === null || product.variationReason === undefined || product.variationReason === '') {
errors[`variationReason_${index}`] = 'Reason for variation is required when cost variation exceeds ±10%';
isValid = false;
}
@@ -1718,8 +1905,8 @@ useEffect(() => {
const newErrors = { ...formErrors };
if (mandatoryVariationReason) {
- // Check if variation reason is selected
- if (!updatedProduct.variationReason || updatedProduct.variationReason === '0' || updatedProduct.variationReason === '') {
+ // Check if variation reason is selected (null means no selection)
+ if (updatedProduct.variationReason === null || updatedProduct.variationReason === undefined || updatedProduct.variationReason === '') {
newErrors[variationErrorKey] = 'Reason for variation is required when cost variation exceeds ±10%';
} else {
delete newErrors[variationErrorKey];
@@ -1759,8 +1946,8 @@ useEffect(() => {
if (mandatoryVariationReason) {
if (field === 'variationReason') {
- // Check if variation reason is selected
- if (!updatedProduct.variationReason || updatedProduct.variationReason === '0' || updatedProduct.variationReason === '') {
+ // Check if variation reason is selected (null means no selection)
+ if (updatedProduct.variationReason === null || updatedProduct.variationReason === undefined || updatedProduct.variationReason === '') {
newErrors[variationErrorKey] = 'Reason for variation is required when cost variation exceeds ±10%';
} else {
delete newErrors[variationErrorKey];
@@ -2566,12 +2753,14 @@ useEffect(() => {
Reason for Variation
{isVariationReasonMandatory(p) &&
*}
-