Add ACMCC app source, Supabase backend, and project config

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 20:19:26 -04:00
parent 313b51b412
commit 183fe0a93c
1422 changed files with 259271 additions and 0 deletions
+277
View File
@@ -0,0 +1,277 @@
import React, { useState, useRef } from 'react';
import Papa from 'papaparse';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { ScrollArea } from '@/components/ui/scroll-area';
import { HelpCircle, Upload, AlertTriangle, CheckCircle, Loader2, X } from 'lucide-react';
import { supabase } from '@/integrations/supabase/client';
import { useAuth } from '@/contexts/AuthContext';
import { useToast } from '@/hooks/use-toast';
export default function ChecklistCSVImportDialog({ open, onOpenChange, onSuccess }) {
const { user } = useAuth();
const { toast } = useToast();
const fileInputRef = useRef(null);
const db = supabase;
const [file, setFile] = useState(null);
const [parsedData, setParsedData] = useState([]);
const [error, setError] = useState(null);
const [importing, setImporting] = useState(false);
const [step, setStep] = useState('upload');
const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
if (selectedFile) {
if (selectedFile.type !== 'text/csv' && !selectedFile.name.endsWith('.csv')) {
setError('Please upload a valid CSV file.');
return;
}
setFile(selectedFile);
parseCSV(selectedFile);
}
};
const parseCSV = (file) => {
setError(null);
Papa.parse(file, {
header: true,
skipEmptyLines: true,
complete: (results) => {
if (results.errors.length > 0) {
setError(`Error parsing CSV: ${results.errors[0].message}`);
return;
}
const headers = results.meta.fields;
const requiredHeaders = ['Checklist Name', 'Items'];
const missingHeaders = requiredHeaders.filter(h => !headers.includes(h));
if (missingHeaders.length > 0) {
setError(`Missing required columns: ${missingHeaders.join(', ')}. Please check the tooltip for format.`);
return;
}
const processed = results.data.map((row, index) => ({
id: index,
title: row['Checklist Name'] || '',
itemsRaw: row['Items'] || '',
description: row['Description'] || '',
valid: !!row['Checklist Name'] && !!row['Items']
}));
setParsedData(processed);
setStep('preview');
},
error: (err) => {
setError(`Failed to read file: ${err.message}`);
}
});
};
const handleCellEdit = (index, field, value) => {
const newData = [...parsedData];
newData[index][field] = value;
newData[index].valid = !!newData[index].title && !!newData[index].itemsRaw;
setParsedData(newData);
};
const removeRow = (index) => {
setParsedData(prev => prev.filter((_, i) => i !== index));
};
const handleImport = async () => {
const validRows = parsedData.filter(r => r.valid);
if (validRows.length === 0) {
setError("No valid data to import.");
return;
}
setImporting(true);
try {
const records = validRows.map(row => {
const itemsArray = row.itemsRaw.split('|').map(itemText => ({
text: itemText.trim(),
required: false,
id: Math.random().toString(36).substr(2, 9)
})).filter(i => i.text.length > 0);
return {
title: row.title,
description: row.description,
items: itemsArray,
created_by: user?.id,
};
});
const { error: insertError } = await db
.from('checklists')
.insert(records);
if (insertError) throw insertError;
toast({
title: 'Import Successful',
description: `Successfully imported ${records.length} checklist templates.`,
});
if (onSuccess) onSuccess();
handleClose();
} catch (err) {
console.error('Import error:', err);
setError(`Database error: ${err.message}`);
} finally {
setImporting(false);
}
};
const handleClose = () => {
onOpenChange(false);
setFile(null);
setParsedData([]);
setError(null);
setStep('upload');
if (fileInputRef.current) fileInputRef.current.value = '';
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[800px]">
<DialogHeader>
<DialogTitle>Import Checklists from CSV</DialogTitle>
<DialogDescription>
Upload a CSV file to bulk create checklist templates.
</DialogDescription>
</DialogHeader>
{error && (
<Alert variant="destructive" className="mb-4">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{step === 'upload' && (
<div className="py-8 space-y-6">
<div
className="border-2 border-dashed border-slate-200 rounded-lg p-10 text-center hover:bg-slate-50 transition-colors cursor-pointer"
onClick={() => fileInputRef.current?.click()}
>
<Upload className="mx-auto h-12 w-12 text-slate-400 mb-3" />
<p className="text-sm font-medium text-slate-900">Click to upload CSV</p>
<p className="text-xs text-slate-500 mt-1">or drag and drop file here</p>
<input
type="file"
accept=".csv"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
/>
</div>
<div className="bg-blue-50 p-4 rounded-md flex items-start gap-3 text-sm text-blue-700">
<HelpCircle className="h-5 w-5 shrink-0 mt-0.5" />
<div className="space-y-2">
<p className="font-semibold">CSV Format Requirements:</p>
<ul className="list-disc list-inside space-y-1 text-xs">
<li>Headers must be exactly: <strong>Checklist Name, Items, Description</strong></li>
<li><strong>Items</strong> should be separated by a pipe character (|) e.g. "Task 1|Task 2|Task 3"</li>
<li><strong>Description</strong> is optional.</li>
</ul>
<div className="mt-2 text-xs bg-white p-2 rounded border border-blue-200 font-mono">
Checklist Name,Items,Description<br/>
"Weekly Audit","Check Lights|Check Doors|Empty Trash","Weekly facility check"<br/>
"Safety Insp","Fire Extinguishers|Exits Clear","Monthly safety"
</div>
</div>
</div>
</div>
)}
{step === 'preview' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium">Preview Data ({parsedData.length} rows)</h4>
<Button variant="ghost" size="sm" onClick={() => setStep('upload')}>Upload Different File</Button>
</div>
<ScrollArea className="h-[300px] border rounded-md">
<Table>
<TableHeader>
<TableRow>
<TableHead>Checklist Name *</TableHead>
<TableHead>Items (Pipe Separated) *</TableHead>
<TableHead>Description</TableHead>
<TableHead className="w-[50px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{parsedData.map((row, idx) => (
<TableRow key={idx} className={!row.valid ? "bg-red-50" : ""}>
<TableCell>
<Input
value={row.title}
onChange={(e) => handleCellEdit(idx, 'title', e.target.value)}
className="h-8 text-xs"
placeholder="Required"
/>
</TableCell>
<TableCell>
<Input
value={row.itemsRaw}
onChange={(e) => handleCellEdit(idx, 'itemsRaw', e.target.value)}
className="h-8 text-xs"
placeholder="Item 1|Item 2..."
/>
</TableCell>
<TableCell>
<Input
value={row.description}
onChange={(e) => handleCellEdit(idx, 'description', e.target.value)}
className="h-8 text-xs"
/>
</TableCell>
<TableCell>
<Button variant="ghost" size="icon" className="h-6 w-6 text-slate-400 hover:text-red-500" onClick={() => removeRow(idx)}>
<X className="h-3 w-3" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</ScrollArea>
<div className="flex justify-between items-center text-xs text-slate-500">
<span>* Required fields</span>
<span>{parsedData.filter(r => !r.valid).length} invalid rows will be skipped</span>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={handleClose}>Cancel</Button>
{step === 'preview' && (
<Button onClick={handleImport} disabled={importing || parsedData.filter(r => r.valid).length === 0}>
{importing && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Import {parsedData.filter(r => r.valid).length} Templates
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}