Files
GSB-Construction/Modules/UserManagement/resources/js/Pages/Users/Create.tsx

381 lines
23 KiB
TypeScript

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, useForm, usePage } from '@inertiajs/react';
import { Button } from '@/Components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/Components/ui/card';
import { Checkbox } from '@/Components/ui/checkbox';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/Components/ui/select';
import { Separator } from '@/Components/ui/separator';
import { ArrowLeft, Building2, KeyRound, Mail, RefreshCw, Save, Send, UserPlus } from 'lucide-react';
import { FormEvent, useState } from 'react';
import { Contractor, PageProps } from '@/types';
interface AuthUser {
contractor_id: number | null;
user_type: string;
}
interface Props extends PageProps {
contractors: Contractor[];
isSuperAdmin: boolean;
nextEmployeeCode: string;
roles?: string[];
}
export default function Create({ contractors, isSuperAdmin, nextEmployeeCode, roles = [] }: Props) {
const { auth } = usePage<PageProps & { auth: { user: AuthUser } }>().props;
const { data, setData, post, processing, errors } = useForm({
name: '',
email: '',
password: '',
password_confirmation: '',
user_type: 'employee',
status: 'active',
invite_method: 'direct' as 'direct' | 'email',
auto_password: true,
contractor_id: '' as string | number,
department: '',
position: '',
employee_code: nextEmployeeCode,
hire_date: '',
phone: '',
address: '',
company_name: '',
contact_person: '',
tin_number: '',
spatie_role: '',
});
const [generatedPassword, setGeneratedPassword] = useState('');
const roleLabels: Record<string, string> = {
'Main Contractor Admin': 'Contractor Admin',
admin: 'Admin',
};
const generatePassword = () => {
const chars = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#';
const pw = Array.from({ length: 10 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
setGeneratedPassword(pw);
setData(d => ({ ...d, password: pw, password_confirmation: pw }));
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
post(route('users.store'));
};
const isEmployee = data.user_type === 'admin' || data.user_type === 'employee';
const isCustomer = data.user_type === 'customer';
return (
<AuthenticatedLayout
header={
<div className="flex items-center gap-3">
<Link href={route('users.index')}>
<Button variant="ghost" size="icon-sm">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<UserPlus className="h-5 w-5 text-blue-500" />
<h2 className="text-xl font-semibold leading-tight text-gray-800">Add Team Member</h2>
</div>
}
>
<Head title="Add User" />
<div className="py-6">
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
<form onSubmit={handleSubmit} className="space-y-6">
{Object.keys(errors).length > 0 && (
<div className="rounded-md bg-red-50 p-4 border border-red-200">
<div className="flex">
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">There were validation errors with your submission</h3>
<div className="mt-2 text-sm text-red-700">
<ul className="list-disc space-y-1 pl-5">
{Object.entries(errors).map(([field, message]) => (
<li key={field}>{String(message)}</li>
))}
</ul>
</div>
</div>
</div>
</div>
)}
{/* ── Contractor Assignment (Platform Admin only) ── */}
{isSuperAdmin && contractors.length > 0 && (
<Card className="border-blue-200 bg-blue-50/50 shadow-sm">
<CardHeader className="p-4 pb-2">
<CardTitle className="flex items-center gap-2 text-sm font-semibold text-blue-900">
<Building2 className="h-4 w-4 text-blue-600" />
Assign to Contractor
</CardTitle>
<p className="text-xs text-blue-600">
Select an active contractor firm or leave blank for platform-level access.
</p>
</CardHeader>
<CardContent className="p-4 pt-1">
<Select
value={data.contractor_id === '' ? 'none' : String(data.contractor_id)}
onValueChange={(v) => setData('contractor_id', v === 'none' ? '' : Number(v))}
>
<SelectTrigger id="contractor_id" className="w-full bg-white border-blue-200">
<SelectValue>
{(() => {
if (!data.contractor_id || data.contractor_id === 'none') {
return <span className="text-gray-500"> No contractor (platform user) </span>;
}
const selected = contractors.find(c => String(c.id) === String(data.contractor_id));
return selected ? (
<span className="font-medium text-gray-800">{selected.company_name}</span>
) : <span className="text-gray-500"> No contractor (platform user) </span>;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> No contractor (platform user) </SelectItem>
{contractors.map((c) => (
<SelectItem key={c.id} value={String(c.id)}>
<span className="font-medium text-gray-800">{c.company_name}</span>
</SelectItem>
))}
</SelectContent>
</Select>
{errors.contractor_id && <p className="mt-1 text-xs text-red-500">{errors.contractor_id}</p>}
</CardContent>
</Card>
)}
{/* ── Account Info ── */}
<Card>
<CardHeader><CardTitle className="text-base">Account Information</CardTitle></CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<Label htmlFor="name">Full Name *</Label>
<Input id="name" value={data.name} onChange={(e) => setData('name', e.target.value)} className="mt-1" />
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name}</p>}
</div>
<div>
<Label htmlFor="email">Email *</Label>
<Input id="email" type="email" value={data.email} onChange={(e) => setData('email', e.target.value)} className="mt-1" />
{errors.email && <p className="mt-1 text-sm text-red-500">{errors.email}</p>}
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<Label htmlFor="spatie_role">User Role *</Label>
<Select
value={data.spatie_role}
onValueChange={(v) => {
if (v) {
const lower = v.toLowerCase();
let targetType = 'employee';
if (lower.includes('contractor')) {
targetType = 'contractor';
} else if (lower.includes('super admin') || lower === 'admin') {
targetType = 'admin';
}
setData(d => ({
...d,
spatie_role: v,
user_type: targetType,
}));
}
}}
>
<SelectTrigger id="spatie_role" className="mt-1">
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
{(roles && roles.length > 0 ? roles : ['Main Contractor Admin', 'Contractor Project Manager', 'Construction Supervisor', 'Site Technical']).map((r) => (
<SelectItem key={r} value={r}>{roleLabels[r] || r}</SelectItem>
))}
</SelectContent>
</Select>
{/* @ts-ignore */}
{errors.spatie_role && <p className="mt-1 text-sm text-red-500">{errors.spatie_role}</p>}
</div>
<div>
<Label>Status *</Label>
<Select value={data.status} onValueChange={(v) => { if (v) setData('status', v); }}>
<SelectTrigger id="status" className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
<SelectItem value="suspended">Suspended</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Password Block */}
<Separator />
<div>
<div className="mb-3 flex items-center justify-between">
<Label className="flex items-center gap-2">
<KeyRound className="h-4 w-4 text-gray-400" />
Temporary Password
</Label>
<label className="flex cursor-pointer items-center gap-2 text-sm text-gray-600">
<Checkbox
checked={data.auto_password}
onChange={(e) => {
const checked = e.target.checked;
setData(d => ({ ...d, auto_password: checked, password: '', password_confirmation: '' }));
setGeneratedPassword('');
}}
/>
Auto-generate password
</label>
</div>
{data.auto_password ? (
<div className="flex gap-2">
<Input
readOnly
value={generatedPassword}
placeholder="Click Generate to create a password"
className="font-mono text-sm"
/>
<Button type="button" variant="outline" onClick={generatePassword}>
<RefreshCw className="mr-2 h-4 w-4" /> Generate
</Button>
</div>
) : (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<Input
id="password"
type="password"
placeholder="Minimum 8 characters"
value={data.password}
onChange={(e) => setData('password', e.target.value)}
/>
{errors.password && <p className="mt-1 text-sm text-red-500">{errors.password}</p>}
</div>
<div>
<Input
id="password_confirmation"
type="password"
placeholder="Confirm password"
value={data.password_confirmation}
onChange={(e) => setData('password_confirmation', e.target.value)}
/>
</div>
</div>
)}
<p className="mt-2 text-xs text-gray-400">
The user will be required to change this password on their first login.
</p>
</div>
</CardContent>
</Card>
{/* ── Employee Profile ── */}
{isEmployee && (
<Card>
<CardHeader><CardTitle className="text-base">Employee Profile</CardTitle></CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<Label htmlFor="department">Department</Label>
<Input id="department" value={data.department} onChange={(e) => setData('department', e.target.value)} className="mt-1" />
</div>
<div>
<Label htmlFor="position">Position</Label>
<Input id="position" value={data.position} onChange={(e) => setData('position', e.target.value)} className="mt-1" />
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<Label htmlFor="employee_code">Employee Code</Label>
<div className="relative mt-1">
<Input
id="employee_code"
value={data.employee_code}
readOnly
tabIndex={-1}
className="font-mono bg-gray-50 cursor-not-allowed select-none pr-10"
/>
<span className="absolute inset-y-0 right-3 flex items-center pointer-events-none text-gray-400">
🔒
</span>
</div>
<p className="mt-1 text-xs text-gray-400">Auto-assigned cannot be changed manually.</p>
{errors.employee_code && <p className="mt-1 text-sm text-red-500">{errors.employee_code}</p>}
</div>
<div>
<Label htmlFor="hire_date">Hire Date</Label>
<Input id="hire_date" type="date" value={data.hire_date} onChange={(e) => setData('hire_date', e.target.value)} className="mt-1" />
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<Label htmlFor="emp_phone">Phone</Label>
<Input id="emp_phone" value={data.phone} onChange={(e) => setData('phone', e.target.value)} className="mt-1" />
</div>
<div>
<Label htmlFor="emp_address">Address</Label>
<Input id="emp_address" value={data.address} onChange={(e) => setData('address', e.target.value)} className="mt-1" />
</div>
</div>
</CardContent>
</Card>
)}
{/* ── Customer Profile ── */}
{isCustomer && (
<Card>
<CardHeader><CardTitle className="text-base">Customer Profile</CardTitle></CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<Label htmlFor="company_name">Company Name</Label>
<Input id="company_name" value={data.company_name} onChange={(e) => setData('company_name', e.target.value)} className="mt-1" />
</div>
<div>
<Label htmlFor="contact_person">Contact Person</Label>
<Input id="contact_person" value={data.contact_person} onChange={(e) => setData('contact_person', e.target.value)} className="mt-1" />
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<Label htmlFor="tin_number">TIN Number</Label>
<Input id="tin_number" value={data.tin_number} onChange={(e) => setData('tin_number', e.target.value)} className="mt-1" />
</div>
<div>
<Label htmlFor="cust_phone">Phone</Label>
<Input id="cust_phone" value={data.phone} onChange={(e) => setData('phone', e.target.value)} className="mt-1" />
</div>
</div>
</CardContent>
</Card>
)}
<div className="flex justify-end gap-3">
<Link href={route('users.index')}>
<Button variant="outline" type="button">Cancel</Button>
</Link>
<Button type="submit" disabled={processing}>
<Save className="mr-2 h-4 w-4" />{processing ? 'Creating...' : 'Create User'}
</Button>
</div>
</form>
</div>
</div>
</AuthenticatedLayout>
);
}