diff --git a/Modules/MaterialLogistics/app/Http/Controllers/MaterialController.php b/Modules/MaterialLogistics/app/Http/Controllers/MaterialController.php index fa0e666..40ebe85 100644 --- a/Modules/MaterialLogistics/app/Http/Controllers/MaterialController.php +++ b/Modules/MaterialLogistics/app/Http/Controllers/MaterialController.php @@ -3,17 +3,18 @@ namespace Modules\MaterialLogistics\Http\Controllers; use App\Http\Controllers\Controller; +use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Inertia\Inertia; use Modules\MasterData\Models\Material; +use Modules\MaterialLogistics\Models\InventoryBatch; use Modules\MaterialLogistics\Models\ProjectInventory; use Modules\MaterialLogistics\Models\Warehouse; use Modules\MaterialLogistics\Models\WarehouseStock; use Modules\MaterialLogistics\Services\WarehouseService; use Modules\ProjectManagement\Models\Project; use Modules\ProjectManagement\Models\Task; -use Modules\MaterialLogistics\Models\InventoryBatch; class MaterialController extends Controller { @@ -64,7 +65,7 @@ class MaterialController extends Controller 'warehouse:id,ulid,name,code', 'material' => function ($query) { $query->select('id', 'ulid', 'name', 'sku', 'unit', 'category', 'type') - ->with('components.component:id,name,unit'); + ->with('components.component:id,name,unit'); }, ]); @@ -81,7 +82,7 @@ class MaterialController extends Controller 'project:id,ulid,name,code', 'material' => function ($query) { $query->select('id', 'ulid', 'name', 'sku', 'unit', 'category', 'type') - ->with('components.component:id,name,unit'); + ->with('components.component:id,name,unit'); }, ]); @@ -115,14 +116,34 @@ class MaterialController extends Controller public function operations(Request $request, string $type) { $warehouses = Warehouse::active()->select('id', 'ulid', 'name', 'code')->get(); - $projects = Project::select('id', 'ulid', 'name', 'code')->latest()->get(); + + $projects = Project::select('id', 'ulid', 'name', 'code') + ->with(['personnel' => function ($query) { + $query->wherePivot('role', 'pm')->select('users.id', 'users.name'); + }]) + ->latest() + ->get() + ->map(function ($p) { + $pm = $p->personnel->first(); + + return [ + 'id' => $p->id, + 'ulid' => $p->ulid, + 'name' => $p->name, + 'code' => $p->code, + 'pm' => $pm ? [ + 'id' => $pm->id, + 'name' => $pm->name, + ] : null, + ]; + }); // Materials with their warehouse/site stocks to enforce limits $materials = Material::select('id', 'ulid', 'name', 'sku', 'unit', 'unit_cost') ->with(['warehouseStocks', 'projectInventories']) ->get(); - - $projectManagers = \App\Models\User::role('project_manager')->select('id', 'name')->get(); + + $projectManagers = User::role('Project Manager')->select('id', 'name')->get(); return Inertia::render('MaterialLogistics::Inventory/OperationsForm', [ 'warehouses' => $warehouses, @@ -155,33 +176,32 @@ class MaterialController extends Controller foreach ($validated['items'] as $item) { $material = Material::where('ulid', $item['material_ulid'])->firstOrFail(); $batchId = null; - - if (!empty($item['inventory_batch_ulid'])) { + + if (! empty($item['inventory_batch_ulid'])) { $batchId = InventoryBatch::where('ulid', $item['inventory_batch_ulid'])->firstOrFail()->id; } - + $this->warehouseService->dispatchToProject( - warehouse: $warehouse, - project: $project, + warehouse: $warehouse, + project: $project, material: $material, quantity: $item['quantity'], inventoryBatchId: $batchId, - performedBy: auth()->id(), - dispatchedBy: $validated['handler_id'], + performedBy: auth()->id(), + dispatchedBy: $validated['handler_id'], notes: $validated['notes'] ?? null, ); } DB::commit(); } catch (\Exception $e) { DB::rollBack(); + return back()->with('error', $e->getMessage()); } return back()->with('success', "Materials dispatched to {$project->name}."); } - - // --- Reserve for Task --- public function reserveForTask(Request $request) { diff --git a/Modules/MaterialLogistics/app/Http/Controllers/MaterialTransferController.php b/Modules/MaterialLogistics/app/Http/Controllers/MaterialTransferController.php index 6a7984d..35058d6 100644 --- a/Modules/MaterialLogistics/app/Http/Controllers/MaterialTransferController.php +++ b/Modules/MaterialLogistics/app/Http/Controllers/MaterialTransferController.php @@ -179,7 +179,7 @@ class MaterialTransferController extends Controller { abort_if($materialTransfer->status !== MaterialTransferStatus::Draft, 403, 'Only draft transfers can be submitted.'); - $approverIds = \App\Models\User::role('project_manager')->pluck('id')->toArray(); + $approverIds = \App\Models\User::role('Project Manager')->pluck('id')->toArray(); if (empty($approverIds)) { $approverIds = [1]; // Fallback to admin/first user } diff --git a/Modules/MaterialLogistics/resources/js/Pages/Inventory/OperationsForm.tsx b/Modules/MaterialLogistics/resources/js/Pages/Inventory/OperationsForm.tsx index a4a2c20..f3d957b 100644 --- a/Modules/MaterialLogistics/resources/js/Pages/Inventory/OperationsForm.tsx +++ b/Modules/MaterialLogistics/resources/js/Pages/Inventory/OperationsForm.tsx @@ -13,7 +13,7 @@ import { ArrowDownToLine, ArrowRightLeft, ArrowUpFromLine, RotateCcw, ChevronLeft, Loader2, Trash2, Search, PackageOpen } from 'lucide-react'; -import { FormEvent, useMemo, useState } from 'react'; +import { FormEvent, useMemo, useState, useEffect } from 'react'; interface WarehouseStock { warehouse_id: number; quantity: string; } interface ProjectInventory { project_id: number; on_hand_qty: string; allocated_qty: string; } @@ -24,7 +24,13 @@ interface MaterialRef { project_inventories: ProjectInventory[]; } interface WarehouseRef { id: number; ulid: string; name: string; code: string; } -interface ProjectRef { id: number; ulid: string; name: string; code: string; } +interface ProjectRef { + id: number; + ulid: string; + name: string; + code: string; + pm?: { id: number; name: string } | null; +} interface UserRef { id: number; name: string; } interface Props extends PageProps { @@ -147,7 +153,51 @@ export default function OperationsForm({ warehouses, projects, materials, projec const warehouseItems = useMemo(() => warehouses.map(w => ({ value: w.ulid, label: `${w.name} (${w.code})` })), [warehouses]); const projectItems = useMemo(() => projects.map(p => ({ value: p.ulid, label: `${p.name} (${p.code})` })), [projects]); - const personnelItems = useMemo(() => projectManagers.map(p => ({ value: String(p.id), label: p.name })), [projectManagers]); + + const selectedProject = useMemo(() => { + const pUlid = defaultType === 'dispatch' || defaultType === 'return' ? data.project_ulid : (data.location_type === 'project' ? data.location_ulid : ''); + return projects.find(p => p.ulid === pUlid); + }, [projects, data.project_ulid, data.location_ulid, data.location_type, defaultType]); + + const mergedProjectManagers = useMemo(() => { + const managers = [...projectManagers]; + if (selectedProject?.pm) { + const exists = managers.some(m => m.id === selectedProject.pm?.id); + if (!exists) { + managers.push(selectedProject.pm); + } + } + return managers; + }, [projectManagers, selectedProject]); + + const personnelItems = useMemo(() => mergedProjectManagers.map(p => ({ value: String(p.id), label: p.name })), [mergedProjectManagers]); + + const handleProjectSelection = (projectUlid: string | null, formField: 'project_ulid' | 'location_ulid') => { + if (!projectUlid) { + setData(prev => ({ + ...prev, + [formField]: '', + })); + return; + } + const project = projects.find(p => p.ulid === projectUlid); + const pmId = project?.pm?.id ? String(project.pm.id) : ''; + setData(prev => ({ + ...prev, + [formField]: projectUlid, + handler_id: pmId || prev.handler_id + })); + }; + + useEffect(() => { + const pUlid = defaultType === 'dispatch' || defaultType === 'return' ? data.project_ulid : (data.location_type === 'project' ? data.location_ulid : ''); + if (pUlid) { + const project = projects.find(p => p.ulid === pUlid); + if (project?.pm?.id && !data.handler_id) { + setData('handler_id', String(project.pm.id)); + } + } + }, [projects, defaultType]); // Filter materials for catalog const filteredMaterials = useMemo(() => { @@ -222,7 +272,7 @@ export default function OperationsForm({ warehouses, projects, materials, projec
- handleProjectSelection(v, 'project_ulid')} items={projectItems}> {projects.map(p => {p.name})} @@ -237,7 +287,7 @@ export default function OperationsForm({ warehouses, projects, materials, projec <>
- handleProjectSelection(v, 'project_ulid')} items={projectItems}> {projects.map(p => {p.name})} @@ -282,7 +332,16 @@ export default function OperationsForm({ warehouses, projects, materials, projec
- { + if (data.location_type === 'project') { + handleProjectSelection(v, 'location_ulid'); + } else { + updateForm('location_ulid', v); + } + }} + > {data.location_type === 'warehouse' @@ -300,7 +359,7 @@ export default function OperationsForm({ warehouses, projects, materials, projec {errors.handler_id &&

{errors.handler_id}

}
diff --git a/Modules/ProjectManagement/app/Enums/TaskStatus.php b/Modules/ProjectManagement/app/Enums/TaskStatus.php index e4403eb..7a36e64 100644 --- a/Modules/ProjectManagement/app/Enums/TaskStatus.php +++ b/Modules/ProjectManagement/app/Enums/TaskStatus.php @@ -8,6 +8,7 @@ enum TaskStatus: string case InProgress = 'in_progress'; case Completed = 'completed'; case Blocked = 'blocked'; + case Closed = 'closed'; public function label(): string { @@ -16,6 +17,7 @@ enum TaskStatus: string self::InProgress => 'In Progress', self::Completed => 'Completed', self::Blocked => 'Blocked', + self::Closed => 'Closed', }; } @@ -24,8 +26,9 @@ enum TaskStatus: string return match ($this) { self::Pending => [self::InProgress, self::Blocked], self::InProgress => [self::Completed, self::Blocked, self::Pending], - self::Completed => [], + self::Completed => [self::InProgress, self::Pending, self::Closed], self::Blocked => [self::Pending, self::InProgress], + self::Closed => [], }; } } diff --git a/Modules/ProjectManagement/app/Http/Controllers/ProjectController.php b/Modules/ProjectManagement/app/Http/Controllers/ProjectController.php index da458cf..87644f8 100644 --- a/Modules/ProjectManagement/app/Http/Controllers/ProjectController.php +++ b/Modules/ProjectManagement/app/Http/Controllers/ProjectController.php @@ -23,8 +23,8 @@ class ProjectController extends Controller $query = Project::query() ->with(['personnel:id,name']); - // Non-platform admins/owners only see projects they are assigned to - if ($user && !$user->hasRole('Super Admin') && !$user->hasRole('Owner')) { + // Non-platform admins only see projects they are assigned to + if ($user && !$user->hasRole('Super Admin')) { $query->whereHas('personnel', function ($q) use ($user) { $q->where('users.id', $user->id); }); @@ -101,6 +101,7 @@ class ProjectController extends Controller 'pending' => $project->tasks->where('status', 'pending')->count(), 'in_progress' => $project->tasks->where('status', 'in_progress')->count(), 'completed' => $project->tasks->where('status', 'completed')->count(), + 'closed' => $project->tasks->where('status', 'closed')->count(), ]; return Inertia::render('ProjectManagement::Projects/Overview', [ diff --git a/Modules/ProjectManagement/app/Models/Project.php b/Modules/ProjectManagement/app/Models/Project.php index 4e35cde..0db6404 100644 --- a/Modules/ProjectManagement/app/Models/Project.php +++ b/Modules/ProjectManagement/app/Models/Project.php @@ -4,18 +4,19 @@ namespace Modules\ProjectManagement\Models; use App\Models\User; use App\Traits\BelongsToTenant; -use Illuminate\Support\Str; use App\Traits\HasPublicIdentifier; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; -use Modules\BiddingManagement\Models\BidPackage; use Illuminate\Database\Eloquent\SoftDeletes; +use Modules\BiddingManagement\Models\BidPackage; use Modules\ContractorManagement\Models\Contractor; use Modules\MasterData\Models\MaterialGroup; -use Modules\ProjectManagement\Enums\ProjectStatus; +use Modules\MaterialLogistics\Models\ProjectInventory; use Modules\MaterialLogistics\Models\Warehouse; +use Modules\ProjectManagement\Enums\DelayReason; +use Modules\ProjectManagement\Enums\ProjectStatus; class Project extends Model { @@ -58,7 +59,8 @@ class Project extends Model static::creating(function (self $project) { if (empty($project->code)) { $year = now()->year; - $lastCode = static::withTrashed() + $lastCode = static::withoutGlobalScopes() + ->withTrashed() ->where('code', 'like', "PRJ-{$year}-%") ->orderByDesc('code') ->value('code'); @@ -87,7 +89,7 @@ class Project extends Model public function inventories(): HasMany { - return $this->hasMany(\Modules\MaterialLogistics\Models\ProjectInventory::class); + return $this->hasMany(ProjectInventory::class); } public function contractor(): BelongsTo @@ -134,7 +136,7 @@ class Project extends Model { $currentStatus = $this->status; - if (!in_array($newStatus, $currentStatus->allowedTransitions())) { + if (! in_array($newStatus, $currentStatus->allowedTransitions())) { throw new \InvalidArgumentException( "Cannot transition from {$currentStatus->label()} to {$newStatus->label()}" ); @@ -143,7 +145,7 @@ class Project extends Model // Guard: can't move to InProgress without assigned PM if ($newStatus === ProjectStatus::InProgress) { $hasPm = $this->personnel()->wherePivot('role', 'pm')->exists(); - if (!$hasPm) { + if (! $hasPm) { throw new \InvalidArgumentException('Cannot start project without a Project Manager assigned'); } } @@ -167,7 +169,10 @@ class Project extends Model public function getCapitalizationPercentageAttribute(): float { - if ($this->contract_value <= 0) return 0; + if ($this->contract_value <= 0) { + return 0; + } + return round(($this->total_capitalization / $this->contract_value) * 100, 2); } @@ -185,7 +190,9 @@ class Project extends Model public function getMilestoneCompletionAttribute(): float { $milestones = $this->milestones; - if ($milestones->isEmpty()) return 0; + if ($milestones->isEmpty()) { + return 0; + } return (float) $milestones ->where('actual_date', '!=', null) @@ -198,8 +205,8 @@ class Project extends Model $taskDelayHours = (float) $this->tasks->flatMap( fn (Task $task) => $task->delays - )->where('reason_type', \Modules\ProjectManagement\Enums\DelayReason::Weather) - ->sum('lost_hours'); + )->where('reason_type', DelayReason::Weather) + ->sum('lost_hours'); return $milestoneDays + (int) ceil($taskDelayHours / 8); } diff --git a/Modules/ProjectManagement/app/Models/ProjectMilestone.php b/Modules/ProjectManagement/app/Models/ProjectMilestone.php index a8926bd..c909f99 100644 --- a/Modules/ProjectManagement/app/Models/ProjectMilestone.php +++ b/Modules/ProjectManagement/app/Models/ProjectMilestone.php @@ -43,6 +43,11 @@ class ProjectMilestone extends Model return $this->belongsTo(Project::class); } + public function tasks(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Task::class, 'milestone_id'); + } + // --- Accessors --- public function getIsCompletedAttribute(): bool diff --git a/Modules/ProjectManagement/app/Models/Task.php b/Modules/ProjectManagement/app/Models/Task.php index 09b7f2a..fd63c34 100644 --- a/Modules/ProjectManagement/app/Models/Task.php +++ b/Modules/ProjectManagement/app/Models/Task.php @@ -32,6 +32,7 @@ class Task extends Model 'actual_start_date', 'actual_end_date', 'completion_percentage', + 'milestone_id', ]; protected function casts(): array @@ -54,6 +55,11 @@ class Task extends Model return $this->belongsTo(Project::class); } + public function milestone(): BelongsTo + { + return $this->belongsTo(ProjectMilestone::class, 'milestone_id'); + } + public function users(): BelongsToMany { return $this->belongsToMany(User::class); @@ -106,12 +112,26 @@ class Task extends Model $updates = ['status' => $newStatus]; - if ($newStatus === TaskStatus::InProgress && !$this->actual_start_date) { - $updates['actual_start_date'] = now(); + if ($newStatus === TaskStatus::InProgress) { + if (!$this->actual_start_date) { + $updates['actual_start_date'] = now(); + } + if ($currentStatus === TaskStatus::Completed) { + $updates['actual_end_date'] = null; + $updates['completion_percentage'] = 0; + } } - if ($newStatus === TaskStatus::Completed) { - $updates['actual_end_date'] = now(); + if ($newStatus === TaskStatus::Pending) { + $updates['actual_start_date'] = null; + $updates['actual_end_date'] = null; + $updates['completion_percentage'] = 0; + } + + if ($newStatus === TaskStatus::Completed || $newStatus === TaskStatus::Closed) { + if (!$this->actual_end_date) { + $updates['actual_end_date'] = now(); + } $updates['completion_percentage'] = 100; } diff --git a/Modules/ProjectManagement/app/Models/TaskActivity.php b/Modules/ProjectManagement/app/Models/TaskActivity.php index acee4b5..e3d88a8 100644 --- a/Modules/ProjectManagement/app/Models/TaskActivity.php +++ b/Modules/ProjectManagement/app/Models/TaskActivity.php @@ -3,13 +3,13 @@ namespace Modules\ProjectManagement\Models; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Concerns\HasUlids; +use App\Traits\HasPublicIdentifier; use Illuminate\Database\Eloquent\Relations\BelongsTo; use App\Models\User; class TaskActivity extends Model { - use HasUlids; + use HasPublicIdentifier; protected $fillable = [ 'task_id', diff --git a/Modules/ProjectManagement/database/migrations/2026_05_25_164000_add_milestone_id_to_tasks_table.php b/Modules/ProjectManagement/database/migrations/2026_05_25_164000_add_milestone_id_to_tasks_table.php new file mode 100644 index 0000000..eb6b5d5 --- /dev/null +++ b/Modules/ProjectManagement/database/migrations/2026_05_25_164000_add_milestone_id_to_tasks_table.php @@ -0,0 +1,27 @@ +foreignId('milestone_id') + ->nullable() + ->after('project_id') + ->constrained('project_milestones') + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('tasks', function (Blueprint $table) { + $table->dropForeign(['milestone_id']); + $table->dropColumn('milestone_id'); + }); + } +}; diff --git a/Modules/ProjectManagement/resources/js/Layouts/ProjectLayout.tsx b/Modules/ProjectManagement/resources/js/Layouts/ProjectLayout.tsx index d2cd0aa..05c416c 100644 --- a/Modules/ProjectManagement/resources/js/Layouts/ProjectLayout.tsx +++ b/Modules/ProjectManagement/resources/js/Layouts/ProjectLayout.tsx @@ -38,18 +38,20 @@ export default function ProjectLayout({ project, allowedTransitions, children, c +
-

- {project.name} -

-

{project.code}

+
+

+ {project.name} +

+ {statusLabel(project.status)} +
+

{project.code}

- {statusLabel(project.status)}
{allowedTransitions && allowedTransitions.map((t: any) => ( diff --git a/Modules/ProjectManagement/resources/js/Pages/Projects/Overview.tsx b/Modules/ProjectManagement/resources/js/Pages/Projects/Overview.tsx index 74bf3bf..b83dca2 100644 --- a/Modules/ProjectManagement/resources/js/Pages/Projects/Overview.tsx +++ b/Modules/ProjectManagement/resources/js/Pages/Projects/Overview.tsx @@ -17,7 +17,7 @@ interface StatusOption { value: string; label: string } interface Props extends PageProps { project: ProjectData; - taskStats: { total: number; pending: number; in_progress: number; completed: number }; + taskStats: { total: number; pending: number; in_progress: number; completed: number; closed?: number }; allowedTransitions: StatusOption[]; } @@ -125,18 +125,18 @@ export default function Overview({ project, taskStats, allowedTransitions }: Pro

Progress

- {taskStats.completed} + {(taskStats.completed || 0) + (taskStats.closed || 0)} / {taskStats.total} tasks completed

0 ? `${(taskStats.completed / taskStats.total) * 100}%` : '0%' }} + style={{ width: taskStats.total > 0 ? `${(((taskStats.completed || 0) + (taskStats.closed || 0)) / taskStats.total) * 100}%` : '0%' }} />

- {taskStats.total > 0 ? `${((taskStats.completed / taskStats.total) * 100).toFixed(0)}%` : '0%'} complete + {taskStats.total > 0 ? `${((((taskStats.completed || 0) + (taskStats.closed || 0)) / taskStats.total) * 100).toFixed(0)}%` : '0%'} complete

@@ -144,6 +144,12 @@ export default function Overview({ project, taskStats, allowedTransitions }: Pro

Task Distribution

+ {(taskStats.closed || 0) > 0 && ( +
+ )} {taskStats.completed > 0 && (
0 ? (
{[ + { label: 'Closed', count: taskStats.closed || 0, color: 'bg-orange-500', textColor: 'text-orange-600' }, { label: 'Completed', count: taskStats.completed, color: 'bg-emerald-500', textColor: 'text-emerald-600' }, { label: 'In Progress', count: taskStats.in_progress, color: 'bg-blue-500', textColor: 'text-blue-600' }, { label: 'Pending', count: taskStats.pending, color: 'bg-gray-300', textColor: 'text-gray-500' }, diff --git a/Modules/ProjectManagement/resources/js/Pages/Projects/Show.tsx b/Modules/ProjectManagement/resources/js/Pages/Projects/Show.tsx index e1403e9..01b914a 100644 --- a/Modules/ProjectManagement/resources/js/Pages/Projects/Show.tsx +++ b/Modules/ProjectManagement/resources/js/Pages/Projects/Show.tsx @@ -22,6 +22,7 @@ import { MapPin, Calendar, DollarSign, TrendingUp, Users, ClipboardList, Eye, Package, ChevronDown, ChevronUp, Download, Upload, Loader2, Clock, CloudRain, AlertTriangle, Milestone as MilestoneIcon, Timer, + Ban, RotateCcw, Lock, } from 'lucide-react'; import { PageProps } from '@/types'; import { FormEvent, Fragment, useMemo, useRef, useState } from 'react'; @@ -58,6 +59,7 @@ interface TaskItem { task_materials: TaskMaterialItem[]; delays: DelayItem[]; users?: { id: number; ulid: string; name: string }[]; + milestone?: { id: number; ulid: string; name: string } | null; } interface PersonnelItem { @@ -125,7 +127,7 @@ interface Props extends PageProps { employees: { id: number; ulid: string; name: string }[]; availableMaterials: MaterialOption[]; latestReports: ReportItem[]; - taskStats: { total: number; pending: number; in_progress: number; completed: number }; + taskStats: { total: number; pending: number; in_progress: number; completed: number; closed?: number }; milestones: MilestoneItem[]; milestoneStats: MilestoneStats; weatherConditions: WeatherOption[]; @@ -135,6 +137,23 @@ interface Props extends PageProps { } const statusLabel = (s: string) => s.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); +const renderStatusBadge = (status: string) => { + const label = status.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + switch (status) { + case 'pending': + return {label}; + case 'in_progress': + return {label}; + case 'completed': + return {label}; + case 'blocked': + return {label}; + case 'closed': + return {label}; + default: + return {label}; + } +}; const formatCurrency = (v: string) => new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(Number(v)); const roleItems = [{ value: 'pm', label: 'Project Manager' }, { value: 'engineer', label: 'Engineer' }, { value: 'laborer', label: 'Laborer' }, { value: 'member', label: 'Member' }]; @@ -178,7 +197,9 @@ export default function Show({ project, employees, availableMaterials, latestRep const [editMilestoneId, setEditMilestoneId] = useState(null); const [addDelayTaskId, setAddDelayTaskId] = useState(null); const taskForm = useForm({ + project_id: project?.id || '', name: '', description: '', assigned_users: [] as string[], labor_cost: '', estimated_hours: '', start_date: '', end_date: '', + milestone_id: '', }); const memberForm = useForm({ user_id: '', role: 'member' }); @@ -217,12 +238,12 @@ export default function Show({ project, employees, availableMaterials, latestRep }; const handleTaskTransition = (taskUlid: string, status: string) => { - router.patch(route('projects.tasks.transition', [project.ulid, taskUlid]), { status }); + router.patch(route('projects.tasks.transition', taskUlid), { status }); }; const handleDeleteTask = (taskUlid: string) => { if (confirm('Delete this task?')) { - router.delete(route('projects.tasks.destroy', [project.ulid, taskUlid])); + router.delete(route('projects.tasks.destroy', taskUlid)); } }; @@ -241,16 +262,18 @@ export default function Show({ project, employees, availableMaterials, latestRep return ( +
-

{project.name}

-

{project.code}

+
+

{project.name}

+ {statusLabel(project.status)} +
+

{project.code}

- {statusLabel(project.status)}
{allowedTransitions.map((t) => ( @@ -368,23 +391,23 @@ export default function Show({ project, employees, availableMaterials, latestRep {/* Task Progress Section */}
- {/* Main Progress Bar — Completed Only */} + {/* Main Progress Bar — Completed + Closed */}

Progress

- {taskStats.completed} + {(taskStats.completed || 0) + (taskStats.closed || 0)} / {taskStats.total} tasks completed

0 ? `${(taskStats.completed / taskStats.total) * 100}%` : '0%' }} + style={{ width: taskStats.total > 0 ? `${(((taskStats.completed || 0) + (taskStats.closed || 0)) / taskStats.total) * 100}%` : '0%' }} />

- {taskStats.total > 0 ? `${((taskStats.completed / taskStats.total) * 100).toFixed(0)}%` : '0%'} complete + {taskStats.total > 0 ? `${((((taskStats.completed || 0) + (taskStats.closed || 0)) / taskStats.total) * 100).toFixed(0)}%` : '0%'} complete

@@ -393,6 +416,12 @@ export default function Show({ project, employees, availableMaterials, latestRep

Task Distribution

+ {(taskStats.closed || 0) > 0 && ( +
+ )} {taskStats.completed > 0 && (
0 ? (
{[ + { label: 'Closed', count: taskStats.closed || 0, color: 'bg-orange-500', textColor: 'text-orange-600' }, { label: 'Completed', count: taskStats.completed, color: 'bg-emerald-500', textColor: 'text-emerald-600' }, { label: 'In Progress', count: taskStats.in_progress, color: 'bg-blue-500', textColor: 'text-blue-600' }, { label: 'Pending', count: taskStats.pending, color: 'bg-gray-300', textColor: 'text-gray-500' }, @@ -520,6 +550,7 @@ export default function Show({ project, employees, availableMaterials, latestRep e.preventDefault(); const fd = new FormData(e.currentTarget); router.post(route('projects.milestones.store', project.ulid), { + project_id: project.id, name: fd.get('name'), planned_date: fd.get('planned_date'), weight_percentage: fd.get('weight_percentage'), @@ -931,7 +962,7 @@ export default function Show({ project, employees, availableMaterials, latestRep - +
+
@@ -1054,23 +1099,68 @@ export default function Show({ project, employees, availableMaterials, latestRep {expandedTask === task.id ? : } - {task.name} + +
{task.name}
+ {task.milestone && ( +
+ + {task.milestone.name} + +
+ )} +
{task.users?.map(u => u.name).join(', ') || '-'} - {statusLabel(task.status)} + {renderStatusBadge(task.status)} {formatCurrency(task.labor_cost)} {formatCurrency(String(task.material_cost))} {formatCurrency(String(task.total_cost))}
{task.status === 'pending' && ( - + <> + + + )} {task.status === 'in_progress' && ( - + <> + + + + + )} + {task.status === 'completed' && ( + <> + + + + + )} + {task.status === 'blocked' && ( + <> + + + )}
+
diff --git a/Modules/ProjectManagement/resources/js/Pages/Projects/Team.tsx b/Modules/ProjectManagement/resources/js/Pages/Projects/Team.tsx index f8eb593..a2bffd9 100644 --- a/Modules/ProjectManagement/resources/js/Pages/Projects/Team.tsx +++ b/Modules/ProjectManagement/resources/js/Pages/Projects/Team.tsx @@ -55,10 +55,8 @@ export default function Team({ project, employees }: Props) {
Team Members - - + }> + Add Member Add Team Member diff --git a/Modules/RolesPermissions/database/seeders/RolesPermissionsDatabaseSeeder.php b/Modules/RolesPermissions/database/seeders/RolesPermissionsDatabaseSeeder.php index 1b989e4..aa8fcd0 100644 --- a/Modules/RolesPermissions/database/seeders/RolesPermissionsDatabaseSeeder.php +++ b/Modules/RolesPermissions/database/seeders/RolesPermissionsDatabaseSeeder.php @@ -30,7 +30,6 @@ class RolesPermissionsDatabaseSeeder extends Seeder // Clean up old roles that are no longer used $validRoles = [ 'Super Admin', - 'Owner', 'Contractor', 'Project Manager', 'Designer', @@ -41,21 +40,12 @@ class RolesPermissionsDatabaseSeeder extends Seeder // Create Roles $superAdmin = \Spatie\Permission\Models\Role::firstOrCreate(['name' => 'Super Admin']); - $owner = \Spatie\Permission\Models\Role::firstOrCreate(['name' => 'Owner']); $contractor = \Spatie\Permission\Models\Role::firstOrCreate(['name' => 'Contractor']); $projectManager = \Spatie\Permission\Models\Role::firstOrCreate(['name' => 'Project Manager']); $designer = \Spatie\Permission\Models\Role::firstOrCreate(['name' => 'Designer']); $siteTechnical = \Spatie\Permission\Models\Role::firstOrCreate(['name' => 'Site Technical']); $constructionSupervisor = \Spatie\Permission\Models\Role::firstOrCreate(['name' => 'Construction Supervisor']); - // Give basic permissions logically based on role name - $owner->syncPermissions([ - 'dashboard.access', - 'projects.access', - 'finance.access', - 'approvals.access', - ]); - $projectManager->syncPermissions([ 'dashboard.access', 'projects.access', diff --git a/Modules/TaskManagement/app/Http/Controllers/TaskController.php b/Modules/TaskManagement/app/Http/Controllers/TaskController.php index 7b04bda..45f53ea 100644 --- a/Modules/TaskManagement/app/Http/Controllers/TaskController.php +++ b/Modules/TaskManagement/app/Http/Controllers/TaskController.php @@ -23,8 +23,10 @@ class TaskController extends Controller if ($projectUlid) { $project = Project::where('ulid', $projectUlid)->firstOrFail(); $project->load([ + 'milestones' => fn ($q) => $q->orderBy('sort_order'), 'tasks' => fn ($q) => $q->with([ 'users:id,ulid,name', + 'milestone:id,ulid,name', 'taskMaterials.material' => function ($query) { $query->select('id', 'ulid', 'name', 'unit', 'unit_cost', 'type') ->with('components.component:id,name,unit'); @@ -99,6 +101,7 @@ class TaskController extends Controller 'estimated_hours' => 'nullable|numeric|min:0', 'start_date' => 'nullable|date', 'end_date' => 'nullable|date|after_or_equal:start_date', + 'milestone_id' => 'nullable|string|exists:project_milestones,ulid', ]); $project = Project::findOrFail($validated['project_id']); @@ -112,6 +115,17 @@ class TaskController extends Controller $validated['sort_order'] = $project->tasks()->count(); + if (empty($validated['labor_cost'])) { + $validated['labor_cost'] = 0; + } + if (empty($validated['estimated_hours'])) { + $validated['estimated_hours'] = 0; + } + + $validated['milestone_id'] = !empty($validated['milestone_id']) + ? \Modules\ProjectManagement\Models\ProjectMilestone::resolveUlidToId($validated['milestone_id']) + : null; + $task = Task::create($validated); $task->users()->sync($userIds); @@ -145,6 +159,7 @@ class TaskController extends Controller 'start_date' => 'nullable|date', 'end_date' => 'nullable|date|after_or_equal:start_date', 'completion_percentage' => 'nullable|numeric|min:0|max:100', + 'milestone_id' => 'nullable|string|exists:project_milestones,ulid', ]); $userIds = []; @@ -155,6 +170,23 @@ class TaskController extends Controller } $oldUserIds = $task->users()->pluck('users.id')->toArray(); + + if (array_key_exists('labor_cost', $validated) && empty($validated['labor_cost'])) { + $validated['labor_cost'] = 0; + } + if (array_key_exists('estimated_hours', $validated) && empty($validated['estimated_hours'])) { + $validated['estimated_hours'] = 0; + } + if (array_key_exists('actual_hours', $validated) && empty($validated['actual_hours'])) { + $validated['actual_hours'] = 0; + } + + if (array_key_exists('milestone_id', $validated)) { + $validated['milestone_id'] = !empty($validated['milestone_id']) + ? \Modules\ProjectManagement\Models\ProjectMilestone::resolveUlidToId($validated['milestone_id']) + : null; + } + $task->update($validated); $task->users()->sync($userIds); diff --git a/Modules/TaskManagement/resources/js/Components/KanbanBoard.tsx b/Modules/TaskManagement/resources/js/Components/KanbanBoard.tsx index ec48982..16db8d5 100644 --- a/Modules/TaskManagement/resources/js/Components/KanbanBoard.tsx +++ b/Modules/TaskManagement/resources/js/Components/KanbanBoard.tsx @@ -3,7 +3,7 @@ import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea import { Card, CardContent } from '@/Components/ui/card'; import { Badge } from '@/Components/ui/badge'; import { Avatar, AvatarFallback } from '@/Components/ui/avatar'; -import { DollarSign, Clock, Users } from 'lucide-react'; +import { Clock, Users, Lock } from 'lucide-react'; interface KanbanBoardProps { tasks: any[]; @@ -15,7 +15,8 @@ const COLUMNS = [ { id: 'pending', title: 'Pending' }, { id: 'in_progress', title: 'In Progress' }, { id: 'completed', title: 'Completed' }, - { id: 'blocked', title: 'Blocked' } + { id: 'blocked', title: 'Blocked' }, + { id: 'closed', title: 'Closed' } ]; export default function KanbanBoard({ tasks, onReorder, onTaskClick }: KanbanBoardProps) { @@ -33,7 +34,8 @@ export default function KanbanBoard({ tasks, onReorder, onTaskClick }: KanbanBoa pending: [], in_progress: [], completed: [], - blocked: [] + blocked: [], + closed: [] }; const sortedTasks = [...localTasks].sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)); @@ -117,7 +119,7 @@ export default function KanbanBoard({ tasks, onReorder, onTaskClick }: KanbanBoa className={`flex-1 p-3 space-y-3 overflow-y-auto min-h-0 transition-colors duration-200 ease-in-out ${snapshot.isDraggingOver ? 'bg-blue-50/50 ring-1 ring-blue-200/50' : ''}`} > {columnsData[column.id]?.map((task, index) => ( - + {(provided, snapshot) => (
onTaskClick(task.ulid)} className="outline-none" > - + -
- {task.name} +
+ {task.name} + {task.status === 'closed' && }
+ {task.description && ( +

+ {task.description} +

+ )}
- {formatCurrency(task.total_cost || 0)}
@@ -159,11 +172,18 @@ export default function KanbanBoard({ tasks, onReorder, onTaskClick }: KanbanBoa )}
- {task.task_materials?.length > 0 && ( - - {task.task_materials.length} Materials - - )} +
+ {task.milestone && ( + + {task.milestone.name} + + )} + {task.task_materials?.length > 0 && ( + + {task.task_materials.length} Materials + + )} +
diff --git a/Modules/TaskManagement/resources/js/Pages/Tasks/Index.tsx b/Modules/TaskManagement/resources/js/Pages/Tasks/Index.tsx index 2e135a5..7ff80b6 100644 --- a/Modules/TaskManagement/resources/js/Pages/Tasks/Index.tsx +++ b/Modules/TaskManagement/resources/js/Pages/Tasks/Index.tsx @@ -3,7 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/Components/ui/card'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/Components/ui/table'; import { Button } from '@/Components/ui/button'; import { Badge } from '@/Components/ui/badge'; -import { ChevronDown, ChevronUp, Play, CheckCircle2, Trash2, Download, Upload, Loader2, Plus, Package } from 'lucide-react'; +import { ChevronDown, ChevronUp, Play, CheckCircle2, Trash2, Download, Upload, Loader2, Plus, Package, Ban, RotateCcw, Lock } from 'lucide-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/Components/ui/dialog'; import { Input } from '@/Components/ui/input'; import { Form, FormField } from '@/Components/ui/form'; @@ -27,13 +27,32 @@ export default function Tasks({ project, employees, availableMaterials, delayRea const importRef = useRef(null); const taskForm = useForm({ + project_id: project?.id || '', name: '', description: '', assigned_users: [] as string[], labor_cost: '', estimated_hours: '', start_date: '', end_date: '', + milestone_id: '', }); const employeeSelectItems = useMemo(() => employees?.map((e: any) => ({ value: e.ulid, label: e.name })) || [], [employees]); const formatCurrency = (v: string) => new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(Number(v)); const statusLabel = (s: string) => s.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + const renderStatusBadge = (status: string) => { + const label = status.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + switch (status) { + case 'pending': + return {label}; + case 'in_progress': + return {label}; + case 'completed': + return {label}; + case 'blocked': + return {label}; + case 'closed': + return {label}; + default: + return {label}; + } + }; const handleAddTask = (e: FormEvent) => { e.preventDefault(); @@ -43,12 +62,12 @@ export default function Tasks({ project, employees, availableMaterials, delayRea }; const handleTaskTransition = (taskUlid: string, status: string) => { - router.patch(route('projects.tasks.transition', [project.ulid, taskUlid]), { status }); + router.patch(route('projects.tasks.transition', taskUlid), { status }); }; const handleDeleteTask = (taskUlid: string) => { if (confirm('Delete this task?')) { - router.delete(route('projects.tasks.destroy', [project.ulid, taskUlid])); + router.delete(route('projects.tasks.destroy', taskUlid)); } }; @@ -77,33 +96,33 @@ export default function Tasks({ project, employees, availableMaterials, delayRea return ( - +
- Tasks + Tasks
-
+
{ setImportOpen(open); if (!open && importRef.current) importRef.current.value = ''; }}> - + }> Import Tasks Import Tasks from Excel
{ @@ -132,8 +151,8 @@ export default function Tasks({ project, employees, availableMaterials, delayRea
- - + }> + Add Task Add Task @@ -153,26 +172,42 @@ export default function Tasks({ project, employees, availableMaterials, delayRea
+ + taskForm.setData('labor_cost', e.target.value)} /> + taskForm.setData('estimated_hours', e.target.value)} /> - - -
+ + + + + +
@@ -190,7 +225,8 @@ export default function Tasks({ project, employees, availableMaterials, delayRea onTaskClick={(ulid: string) => setDetailsTaskUlid(ulid)} /> ) : ( - +
+
@@ -215,23 +251,68 @@ export default function Tasks({ project, employees, availableMaterials, delayRea {expandedTask === task.id ? : } - {task.name} + +
{task.name}
+ {task.milestone && ( +
+ + {task.milestone.name} + +
+ )} +
{task.users?.map((u: any) => u.name).join(', ') || '-'} - {statusLabel(task.status)} + {renderStatusBadge(task.status)} {formatCurrency(task.labor_cost)} {formatCurrency(String(task.material_cost))} {formatCurrency(String(task.total_cost))}
{task.status === 'pending' && ( - + <> + + + )} {task.status === 'in_progress' && ( - + <> + + + + + )} + {task.status === 'completed' && ( + <> + + + + + )} + {task.status === 'blocked' && ( + <> + + + )}
- )} +
+ )} {/* Task Details Dialog for Kanban Mode */} !open && setDetailsTaskUlid(null)}> - + {detailsTask?.name} - Details {detailsTask && (
-
+ {/* Status and transition actions bar */} +
-

Status

- {statusLabel(detailsTask.status)} +

Status

+
{renderStatusBadge(detailsTask.status)}
-
-

Assignees

-

{detailsTask.users?.map((u: any) => u.name).join(', ') || 'Unassigned'}

-
-
-

Total Cost

-

{formatCurrency(detailsTask.total_cost)}

-
-
-

Estimated Hours

-

{detailsTask.estimated_hours || 0}h

+
+ {detailsTask.status === 'pending' && ( + <> + + + + )} + {detailsTask.status === 'in_progress' && ( + <> + + + + + )} + {detailsTask.status === 'completed' && ( + <> + + + + + )} + {detailsTask.status === 'blocked' && ( + <> + + + + )}
- + + {detailsTask.description && ( +
+

Description

+

{detailsTask.description}

+
+ )} + + {/* Task details grid */} +
+
+

Assignees

+

{detailsTask.users?.map((u: any) => u.name).join(', ') || 'Unassigned'}

+
+
+

Total Cost

+

{formatCurrency(detailsTask.total_cost)}

+
+
+

Estimated Hours

+

{detailsTask.estimated_hours || 0}h

+
+
+

Milestone

+

+ {detailsTask.milestone ? ( + + {detailsTask.milestone.name} + + ) : 'None'} +

+
+
+

diff --git a/Modules/TimelineScheduling/resources/js/Pages/Timeline/Index.tsx b/Modules/TimelineScheduling/resources/js/Pages/Timeline/Index.tsx index a2a1da0..249848d 100644 --- a/Modules/TimelineScheduling/resources/js/Pages/Timeline/Index.tsx +++ b/Modules/TimelineScheduling/resources/js/Pages/Timeline/Index.tsx @@ -5,252 +5,455 @@ import { Badge } from '@/Components/ui/badge'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/Components/ui/dialog'; import { Input } from '@/Components/ui/input'; import { useForm, router } from '@inertiajs/react'; -import { CheckCircle2, CloudRain, Clock, Milestone as MilestoneIcon, Plus, AlertTriangle, Pencil } from 'lucide-react'; -import { useState } from 'react'; +import { CheckCircle2, CloudRain, Clock, Milestone as MilestoneIcon, Plus, AlertTriangle, Trash2, Calendar } from 'lucide-react'; +import { useState, useEffect } from 'react'; export default function Timeline({ project, milestones, milestoneStats, weatherConditions }: any) { - const [addMilestoneOpen, setAddMilestoneOpen] = useState(false); - const [editMilestoneId, setEditMilestoneId] = useState(null); + const [selectedMilestone, setSelectedMilestone] = useState(null); + const [weatherImpacted, setWeatherImpacted] = useState(false); + + // Form for adding a milestone + const addForm = useForm({ + project_id: project?.id || '', + name: '', + planned_date: '', + weight_percentage: 5, + }); + + // Form for editing a milestone + const editForm = useForm({ + name: '', + planned_date: '', + actual_date: '', + weight_percentage: 0, + weather_impacted: false, + weather_condition: '', + weather_delay_days: 0, + weather_notes: '', + }); + + // Synchronize form when selected milestone changes + useEffect(() => { + if (selectedMilestone) { + editForm.setData({ + name: selectedMilestone.name || '', + planned_date: selectedMilestone.planned_date ? selectedMilestone.planned_date.split('T')[0] : '', + actual_date: selectedMilestone.actual_date ? selectedMilestone.actual_date.split('T')[0] : '', + weight_percentage: Number(selectedMilestone.weight_percentage) || 0, + weather_impacted: !!selectedMilestone.weather_impacted, + weather_condition: selectedMilestone.weather_condition || '', + weather_delay_days: selectedMilestone.weather_delay_days || 0, + weather_notes: selectedMilestone.weather_notes || '', + }); + setWeatherImpacted(!!selectedMilestone.weather_impacted); + } else { + addForm.reset(); + setWeatherImpacted(false); + } + }, [selectedMilestone]); if (!project) { return ; } + const handleAddSubmit = (e: React.FormEvent) => { + e.preventDefault(); + addForm.post(route('projects.milestones.store'), { + onSuccess: () => { + addForm.reset(); + setSelectedMilestone(null); + }, + preserveScroll: true, + }); + }; + + const handleEditSubmit = (e: React.FormEvent) => { + e.preventDefault(); + editForm.put(route('projects.milestones.update', selectedMilestone.ulid), { + onSuccess: () => { + // Keep the selection updated + }, + preserveScroll: true, + }); + }; + return (
{/* Summary Cards */}
- +
-

Milestone Progress

-

{milestoneStats?.completion_percentage?.toFixed(0) || 0}%

-
+

Milestone Progress

+

{milestoneStats?.completion_percentage?.toFixed(0) || 0}%

+
-

{milestoneStats?.completed || 0} / {milestoneStats?.total || 0} milestones

+

{milestoneStats?.completed || 0} / {milestoneStats?.total || 0} milestones

- +
-
0 ? 'bg-blue-50' : 'bg-gray-100'}`}> +
0 ? 'bg-blue-50' : 'bg-gray-50'}`}> 0 ? 'text-blue-600' : 'text-gray-400'}`} />
-

Weather Delays

-

{milestoneStats?.weather_delay_days || 0} day{(milestoneStats?.weather_delay_days || 0) !== 1 ? 's' : ''}

-

Total lost to weather

+

Weather Delays

+

{milestoneStats?.weather_delay_days || 0} day{(milestoneStats?.weather_delay_days || 0) !== 1 ? 's' : ''}

+

Total lost to weather

- +
-
- +
+
-

Total Delay Entries

-

+

Total Delay Entries

+

{project?.tasks?.reduce((sum: number, t: any) => sum + (t.delays?.length || 0), 0) || 0}

-

Across all tasks

+

Across all tasks

- {/* Milestones Card */} - - -
- - Milestones - - - - - - - Add Custom Milestone - { - e.preventDefault(); - const fd = new FormData(e.currentTarget); - router.post(route('projects.milestones.store', project.ulid), { - name: fd.get('name'), - planned_date: fd.get('planned_date'), - weight_percentage: fd.get('weight_percentage'), - }, { onSuccess: () => setAddMilestoneOpen(false), preserveScroll: true }); - }} className="space-y-4"> +
+
+ + {milestones?.length === 0 ? ( +
+ +

No milestones defined for this project yet.

+
+ ) : ( +
+ {/* Vertical timeline line */} +
+ +
+ {milestones?.map((m: any, idx: number) => { + const isSelected = selectedMilestone?.ulid === m.ulid; + return ( +
setSelectedMilestone(m)} + className={`relative flex items-start gap-4 p-3.5 rounded-xl border cursor-pointer transition-all duration-200 ${ + isSelected + ? 'border-emerald-500 bg-emerald-50/40 shadow-sm' + : 'border-transparent hover:bg-gray-50 hover:border-gray-200' + }`} + > + {/* Node dot */} +
+ {m.is_completed ? ( + + ) : m.is_overdue ? ( + + ) : ( + {idx + 1} + )} +
+ + {/* Content */} +
+
+

+ {m.name} +

+ + {Number(m.weight_percentage).toFixed(0)}% + + {m.weather_impacted && ( + + {m.weather_delay_days}d + + )} +
+
+ {m.planned_date && ( + Plan: {new Date(m.planned_date).toLocaleDateString('en-PH', { month: 'short', day: 'numeric', year: 'numeric' })} + )} + {m.actual_date && ( + + Done: {new Date(m.actual_date).toLocaleDateString('en-PH', { month: 'short', day: 'numeric', year: 'numeric' })} + {m.days_delayed > 0 && (+{m.days_delayed}d late)} + + )} +
+
+
+ ); + })} +
+
+ )} + + +
+ + {/* Right Panel: Entry of Milestones */} +
+ + + + + {selectedMilestone ? 'Edit Milestone Details' : 'Create New Milestone'} + + + + {selectedMilestone ? ( + /* EDIT FORM */ +
- - + + editForm.setData('name', e.target.value)} + required + className="mt-1 border-gray-300 focus:border-emerald-500 focus:ring-emerald-500" + /> + {editForm.errors.name &&

{editForm.errors.name}

}
- - + + editForm.setData('planned_date', e.target.value)} + className="mt-1 border-gray-300 focus:border-emerald-500 focus:ring-emerald-500" + /> + {editForm.errors.planned_date &&

{editForm.errors.planned_date}

}
- - + + editForm.setData('actual_date', e.target.value)} + className="mt-1 border-gray-300 focus:border-emerald-500 focus:ring-emerald-500" + /> + {editForm.errors.actual_date &&

{editForm.errors.actual_date}

}
-
- +
+ + editForm.setData('weight_percentage', Number(e.target.value))} + className="mt-1 border-gray-300 focus:border-emerald-500 focus:ring-emerald-500" + /> + {editForm.errors.weight_percentage &&

{editForm.errors.weight_percentage}

} +
+ +
+ + + {/* Dynamic Weather fields transition */} +
+
+
+ + + {editForm.errors.weather_condition &&

{editForm.errors.weather_condition}

} +
+
+ + editForm.setData('weather_delay_days', Number(e.target.value))} + className="mt-1 border-gray-300 focus:border-emerald-500 focus:ring-emerald-500" + /> + {editForm.errors.weather_delay_days &&

{editForm.errors.weather_delay_days}

} +
+
+
+ +