387 lines
24 KiB
TypeScript
387 lines
24 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[];
|
|
}
|
|
|
|
export default function Create({ contractors }: Props) {
|
|
const { auth } = usePage<PageProps & { auth: { user: AuthUser } }>().props;
|
|
const isPlatformAdmin = auth.user.contractor_id === null;
|
|
|
|
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: '',
|
|
hire_date: '',
|
|
phone: '',
|
|
address: '',
|
|
company_name: '',
|
|
contact_person: '',
|
|
tin_number: '',
|
|
spatie_role: 'employee',
|
|
});
|
|
|
|
const [generatedPassword, setGeneratedPassword] = useState('');
|
|
|
|
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) ── */}
|
|
{isPlatformAdmin && contractors.length > 0 && (
|
|
<Card className="border-blue-200 bg-blue-50/50">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-base text-blue-800">
|
|
<Building2 className="h-4 w-4" />
|
|
Assign to Contractor
|
|
</CardTitle>
|
|
<p className="text-sm text-blue-600">
|
|
Leave blank to create a platform-level user (no contractor affiliation).
|
|
</p>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Select
|
|
value={String(data.contractor_id)}
|
|
onValueChange={(v) => setData('contractor_id', v === 'none' ? '' : Number(v))}
|
|
items={[
|
|
{ value: 'none', label: '— No contractor (platform user) —' },
|
|
...contractors.map((c) => ({ value: String(c.id), label: c.company_name })),
|
|
]}
|
|
>
|
|
<SelectTrigger id="contractor_id">
|
|
<SelectValue placeholder="— No contractor (platform user) —" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">— No contractor (platform user) —</SelectItem>
|
|
{contractors.map((c) => (
|
|
<SelectItem key={c.id} value={String(c.id)}>
|
|
{c.company_name}
|
|
<span className="ml-2 text-xs text-gray-400 capitalize">({c.type})</span>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.contractor_id && <p className="mt-1 text-sm text-red-500">{errors.contractor_id}</p>}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* ── Provisioning Mode ── */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Invitation Method</CardTitle>
|
|
<p className="text-sm text-gray-500">Choose how this team member receives their access credentials.</p>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{(['direct', 'email'] as const).map((method) => (
|
|
<button
|
|
key={method}
|
|
type="button"
|
|
onClick={() => setData('invite_method', method)}
|
|
className={`flex flex-col items-center gap-2 rounded-xl border-2 p-4 text-sm transition-all ${
|
|
data.invite_method === method
|
|
? 'border-blue-500 bg-blue-50 text-blue-700'
|
|
: 'border-gray-200 text-gray-600 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
{method === 'direct'
|
|
? <><UserPlus className="h-5 w-5" /><span className="font-semibold">Direct Creation</span><span className="text-xs opacity-70">You manage the password</span></>
|
|
: <><Send className="h-5 w-5" /><span className="font-semibold">Email Invite</span><span className="text-xs opacity-70">Credentials sent via email</span></>
|
|
}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</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>User Type *</Label>
|
|
<Select value={data.user_type} onValueChange={(v) => {
|
|
if (v) {
|
|
setData('user_type', v);
|
|
if (v !== 'contractor') setData('spatie_role', v);
|
|
}
|
|
}}>
|
|
<SelectTrigger id="user_type" className="mt-1"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
{isPlatformAdmin && <SelectItem value="admin">Admin</SelectItem>}
|
|
<SelectItem value="employee">Employee</SelectItem>
|
|
<SelectItem value="contractor">Contractor</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.user_type && <p className="mt-1 text-sm text-red-500">{errors.user_type}</p>}
|
|
</div>
|
|
|
|
{data.user_type === 'contractor' && (
|
|
<div>
|
|
<Label>Role Level *</Label>
|
|
<Select value={data.spatie_role} onValueChange={(v) => { if (v) setData('spatie_role', v); }}>
|
|
<SelectTrigger id="spatie_role" className="mt-1"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="main-contractor-admin">Main Contractor Admin</SelectItem>
|
|
<SelectItem value="main-contractor-user">Main Contractor User</SelectItem>
|
|
<SelectItem value="contractor">Sub Contractor</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>
|
|
<Input id="employee_code" value={data.employee_code} onChange={(e) => setData('employee_code', e.target.value)} placeholder="e.g., EMP-0002" className="mt-1" />
|
|
{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}>
|
|
{data.invite_method === 'email'
|
|
? <><Mail className="mr-2 h-4 w-4" />{processing ? 'Sending...' : 'Send Invite'}</>
|
|
: <><Save className="mr-2 h-4 w-4" />{processing ? 'Creating...' : 'Create User'}</>
|
|
}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
);
|
|
}
|