328 lines
20 KiB
TypeScript
328 lines
20 KiB
TypeScript
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
|
import { Head, Link, useForm, usePage, router } from '@inertiajs/react';
|
|
import { Button } from '@/Components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/Components/ui/card';
|
|
import { Input } from '@/Components/ui/input';
|
|
import { Label } from '@/Components/ui/label';
|
|
import {
|
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
|
} from '@/Components/ui/select';
|
|
import { ArrowLeft, Building2, Save, Unlink } from 'lucide-react';
|
|
import { Contractor, User, PageProps } from '@/types';
|
|
import { FormEvent } from 'react';
|
|
|
|
interface Props extends PageProps {
|
|
user: User;
|
|
contractors: Contractor[];
|
|
isPlatformAdmin: boolean;
|
|
}
|
|
|
|
export default function Edit({ user, contractors, isPlatformAdmin }: Props) {
|
|
const { data, setData, put, processing, errors } = useForm({
|
|
name: user.name,
|
|
email: user.email,
|
|
password: '',
|
|
password_confirmation: '',
|
|
user_type: user.user_type,
|
|
status: user.status,
|
|
department: user.employee_profile?.department || '',
|
|
position: user.employee_profile?.position || '',
|
|
employee_code: user.employee_profile?.employee_code || '',
|
|
hire_date: user.employee_profile?.hire_date
|
|
? user.employee_profile.hire_date.slice(0, 10)
|
|
: '',
|
|
phone: (user.employee_profile?.phone || user.customer_profile?.phone) || '',
|
|
address: (user.employee_profile?.address || user.customer_profile?.address) || '',
|
|
company_name: user.customer_profile?.company_name || '',
|
|
contact_person: user.customer_profile?.contact_person || '',
|
|
tin_number: user.customer_profile?.tin_number || '',
|
|
spatie_role: user.roles?.[0]?.name || user.user_type,
|
|
});
|
|
|
|
const contractorForm = useForm({
|
|
contractor_id: user.contractor_id ? String(user.contractor_id) : 'none',
|
|
});
|
|
|
|
const handleLinkContractor = (e: FormEvent) => {
|
|
e.preventDefault();
|
|
contractorForm.patch(route('users.link-contractor', user.ulid));
|
|
};
|
|
|
|
const handleUnlink = () => {
|
|
if (!confirm('Remove contractor link from this user?')) return;
|
|
contractorForm.setData('contractor_id', 'none');
|
|
router.patch(route('users.link-contractor', user.ulid), {
|
|
contractor_id: null,
|
|
});
|
|
};
|
|
|
|
const handleSubmit = (e: FormEvent) => {
|
|
e.preventDefault();
|
|
put(route('users.update', user.ulid));
|
|
};
|
|
|
|
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-4">
|
|
<Link href={route('users.index')}>
|
|
<Button variant="ghost" size="icon-sm">
|
|
<ArrowLeft className="h-4 w-4" />
|
|
</Button>
|
|
</Link>
|
|
<h2 className="text-xl font-semibold leading-tight text-gray-800">
|
|
Edit User: {user.name}
|
|
</h2>
|
|
</div>
|
|
}
|
|
>
|
|
<Head title={`Edit ${user.name}`} />
|
|
|
|
<div className="py-6">
|
|
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
|
|
<form onSubmit={handleSubmit}>
|
|
{Object.keys(errors).length > 0 && (
|
|
<div className="rounded-md bg-red-50 p-4 border border-red-200 mb-6">
|
|
<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>
|
|
)}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>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)} />
|
|
{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)} />
|
|
{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="password">New Password</Label>
|
|
<Input id="password" type="password" value={data.password} onChange={(e) => setData('password', e.target.value)} placeholder="Leave blank to keep current" />
|
|
{errors.password && <p className="mt-1 text-sm text-red-500">{errors.password}</p>}
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="password_confirmation">Confirm New Password</Label>
|
|
<Input id="password_confirmation" type="password" value={data.password_confirmation} onChange={(e) => setData('password_confirmation', e.target.value)} />
|
|
</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"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
{isPlatformAdmin && <SelectItem value="admin">Admin</SelectItem>}
|
|
<SelectItem value="employee">Employee</SelectItem>
|
|
<SelectItem value="contractor">Contractor</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</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"><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>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<Label>Status *</Label>
|
|
<Select value={data.status} onValueChange={(v) => { if (v) setData('status', v); }} items={[{ value: 'active', label: 'Active' }, { value: 'inactive', label: 'Inactive' }, { value: 'suspended', label: 'Suspended' }]}>
|
|
<SelectTrigger id="status"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="active">Active</SelectItem>
|
|
<SelectItem value="inactive">Inactive</SelectItem>
|
|
<SelectItem value="suspended">Suspended</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{isEmployee && (
|
|
<Card className="mt-6">
|
|
<CardHeader><CardTitle>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)} />
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="position">Position</Label>
|
|
<Input id="position" value={data.position} onChange={(e) => setData('position', e.target.value)} />
|
|
</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)} />
|
|
{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)} />
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<Label htmlFor="phone">Phone</Label>
|
|
<Input id="phone" value={data.phone} onChange={(e) => setData('phone', e.target.value)} />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="address">Address</Label>
|
|
<Input id="address" value={data.address} onChange={(e) => setData('address', e.target.value)} />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{isCustomer && (
|
|
<Card className="mt-6">
|
|
<CardHeader><CardTitle>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)} />
|
|
</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)} />
|
|
</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)} />
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="phone">Phone</Label>
|
|
<Input id="phone" value={data.phone} onChange={(e) => setData('phone', e.target.value)} />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="address">Address</Label>
|
|
<Input id="address" value={data.address} onChange={(e) => setData('address', e.target.value)} />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
<div className="mt-6 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" /> Update User
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
|
|
{/* ── Contractor Link — MUST be outside main form (no nested forms) ── */}
|
|
{isPlatformAdmin && contractors.length > 0 && (
|
|
<Card className="mt-6 border-blue-200 bg-blue-50/40">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-base text-blue-800">
|
|
<Building2 className="h-4 w-4" />
|
|
Contractor Link
|
|
</CardTitle>
|
|
<p className="text-sm text-blue-600">
|
|
Link this user to a contractor so they can access the bidding portal.
|
|
</p>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{user.contractor && (
|
|
<div className="mb-3 flex items-center justify-between rounded-md bg-white border px-3 py-2">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Building2 className="h-4 w-4 text-blue-500" />
|
|
<span className="font-medium">{(user.contractor as any).company_name}</span>
|
|
<span className="text-xs text-gray-400 capitalize">({(user.contractor as any).type})</span>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="ghost"
|
|
className="text-red-500 hover:text-red-700"
|
|
onClick={handleUnlink}
|
|
disabled={contractorForm.processing}
|
|
>
|
|
<Unlink className="mr-1 h-3.5 w-3.5" /> Remove
|
|
</Button>
|
|
</div>
|
|
)}
|
|
<form onSubmit={handleLinkContractor} className="flex gap-2">
|
|
<div className="flex-1">
|
|
<Select
|
|
value={contractorForm.data.contractor_id}
|
|
onValueChange={(v) => contractorForm.setData('contractor_id', v || 'none')}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="— Select contractor —">
|
|
{contractorForm.data.contractor_id && contractorForm.data.contractor_id !== 'none'
|
|
? contractors.find(c => String(c.id) === contractorForm.data.contractor_id)?.company_name ?? contractorForm.data.contractor_id
|
|
: null}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">— No contractor —</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>
|
|
</div>
|
|
<Button type="submit" size="sm" disabled={contractorForm.processing}>
|
|
<Save className="mr-1 h-3.5 w-3.5" />
|
|
{user.contractor ? 'Update' : 'Link'}
|
|
</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
);
|
|
}
|