add subcontractor task assign/list for admin and owner feature

This commit is contained in:
Satyam Rastogi
2026-05-18 17:16:11 +05:30
parent 686325dedb
commit c7a9849d1f
7 changed files with 1724 additions and 1 deletions
+354
View File
@@ -5000,6 +5000,176 @@ const KANBAN_PROJECT_DATA = {
},
};
// ---------------------------------------------------------------------------
// Subcontractor Management (Feature 8.2 Task Assignment)
// Subcontractors are independent of any single company so the same record can
// be referenced by tasks coming from multiple Admin/Owner orgs.
// ---------------------------------------------------------------------------
export const SUBCONTRACTOR_TASK_PRIORITIES = [
{ value: 'low', label: 'Low', color: '#6B7280' },
{ value: 'medium', label: 'Medium', color: '#3B82F6' },
{ value: 'high', label: 'High', color: '#EF4444' },
];
export const SUBCONTRACTOR_TASK_STATUSES = [
'Assigned',
'In Progress',
'Completed',
'Cancelled',
];
const MOCK_SUBCONTRACTORS = [
{
id: 'sub_001',
name: 'Carlos Subcontractor',
companyName: 'Electric Pro Services',
tradeType: 'Electrical',
email: 'carlos@electricpro.com',
phone: '469-555-9012',
status: 'active',
rating: 4.8,
companies: ['lynkeduppro'],
joinedAt: '2024-08-01',
},
{
id: 'sub_002',
name: 'Maya Patel',
companyName: 'Skyline Roof Crew',
tradeType: 'Roofing',
email: 'maya@skylineroof.com',
phone: '214-555-1247',
status: 'active',
rating: 4.6,
companies: ['lynkeduppro', 'acmebuilds'],
joinedAt: '2024-05-12',
},
{
id: 'sub_003',
name: 'Dwayne Holland',
companyName: 'Holland Painting Co.',
tradeType: 'Painting',
email: 'dwayne@hollandpaint.com',
phone: '972-555-8312',
status: 'active',
rating: 4.4,
companies: ['lynkeduppro'],
joinedAt: '2024-09-21',
},
{
id: 'sub_004',
name: 'Jennifer Wu',
companyName: 'Wu Plumbing',
tradeType: 'Plumbing',
email: 'jenn@wuplumbing.com',
phone: '469-555-3340',
status: 'active',
rating: 4.9,
companies: ['lynkeduppro', 'acmebuilds', 'planobuilders'],
joinedAt: '2024-02-10',
},
{
id: 'sub_005',
name: 'Greg Alston',
companyName: 'Alston Window Pros',
tradeType: 'Windows & Glazing',
email: 'greg@alstonwindows.com',
phone: '214-555-0299',
status: 'inactive',
rating: 4.1,
companies: ['lynkeduppro'],
joinedAt: '2023-11-15',
},
];
const MOCK_SUBCONTRACTOR_TASKS = [
{
id: 'sct_001',
companyId: 'lynkeduppro',
companyName: 'LynkedUp Pro Roofing',
subcontractorId: 'sub_002',
subcontractorName: 'Maya Patel',
assignedBy: 'owner_001',
assignedByName: 'Justin Johnson',
title: 'Replace damaged ridge cap',
location: '2612 Dunwick Dr, Plano, TX 75023',
description: 'Two ridge cap shingles missing on the south slope after wind storm. Replace with matching tile and seal.',
dueDate: '2026-05-25',
priority: 'high',
status: 'Assigned',
photos: [
{ id: 'sct_001_p1', name: 'Roof_Damage.jpg', url: '/assets/images/properties/Hail_Damaged_Shingles.jpg', annotation: 'Repair this corner' },
{ id: 'sct_001_p2', name: 'Roof_Wide.jpg', url: '/assets/images/properties/Storm_Worn_Roof.jpg', annotation: 'Inspect adjacent tiles too' },
],
createdAt: '2026-05-15T09:12:00Z',
updatedAt: '2026-05-15T09:12:00Z',
},
{
id: 'sct_002',
companyId: 'lynkeduppro',
companyName: 'LynkedUp Pro Roofing',
subcontractorId: 'sub_001',
subcontractorName: 'Carlos Subcontractor',
assignedBy: 'owner_001',
assignedByName: 'Justin Johnson',
title: 'Replace breaker panel',
location: '6613 Phoenix Pl, Plano, TX 75023',
description: 'Outdated 100A panel — upgrade to 200A. Permit pulled, materials staged on site.',
dueDate: '2026-05-22',
priority: 'medium',
status: 'In Progress',
photos: [
{ id: 'sct_002_p1', name: 'Old_Panel.jpg', url: '/assets/images/properties/Beige_Two_Story_House.jpg', annotation: 'Existing 100A panel — disconnect and remove' },
],
createdAt: '2026-05-10T15:40:00Z',
updatedAt: '2026-05-12T08:20:00Z',
},
{
id: 'sct_003',
companyId: 'lynkeduppro',
companyName: 'LynkedUp Pro Roofing',
subcontractorId: 'sub_003',
subcontractorName: 'Dwayne Holland',
assignedBy: 'ADM01',
assignedByName: 'Admin One',
title: 'Touch-up exterior paint',
location: '3913 Arizona Pl, Plano, TX 75023',
description: 'Front fascia and side trim need fresh coat after siding repair.',
dueDate: '2026-05-18',
priority: 'low',
status: 'Completed',
photos: [],
createdAt: '2026-05-05T11:00:00Z',
updatedAt: '2026-05-14T17:30:00Z',
},
];
const MOCK_NOTIFICATIONS = [
{
id: 'notif_001',
recipientUserId: 'sub_001',
recipientRole: 'SUBCONTRACTOR',
type: 'task_assigned',
message: 'You have been assigned a task by LynkedUp Pro Roofing',
taskId: 'sct_002',
fromCompanyId: 'lynkeduppro',
fromCompanyName: 'LynkedUp Pro Roofing',
isRead: false,
createdAt: '2026-05-10T15:40:00Z',
},
{
id: 'notif_002',
recipientUserId: 'sub_002',
recipientRole: 'SUBCONTRACTOR',
type: 'task_assigned',
message: 'You have been assigned a task by LynkedUp Pro Roofing',
taskId: 'sct_001',
fromCompanyId: 'lynkeduppro',
fromCompanyName: 'LynkedUp Pro Roofing',
isRead: false,
createdAt: '2026-05-15T09:12:00Z',
},
];
// --- CONTEXT SETUP ---
const MockStoreContext = createContext();
@@ -5028,6 +5198,11 @@ export const MockStoreProvider = ({ children }) => {
const [kanbanColumns, setKanbanColumns] = useState(KANBAN_COLUMNS_INITIAL);
const [kanbanLeads, setKanbanLeads] = useState(KANBAN_LEADS_INITIAL);
// Subcontractor management (Feature 8.2)
const [subcontractors, setSubcontractors] = useState(MOCK_SUBCONTRACTORS);
const [subcontractorTasks, setSubcontractorTasks] = useState(MOCK_SUBCONTRACTOR_TASKS);
const [notifications, setNotifications] = useState(MOCK_NOTIFICATIONS);
// Commission Settings Org defaults + per-user overrides
const [orgCommissionDefaults, setOrgCommissionDefaults] = useState({
type: 'percent_gross',
@@ -5211,6 +5386,161 @@ export const MockStoreProvider = ({ children }) => {
return true;
};
// Notifications
// Designed so a real-time transport (websocket, SSE, push) can later
// subscribe to `addNotification` without changing call sites.
const addNotification = (payload) => {
const notif = {
id: `notif_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
isRead: false,
createdAt: new Date().toISOString(),
...payload,
};
setNotifications(prev => [notif, ...prev]);
logger.info('Notification dispatched', { notif });
return notif;
};
const markNotificationRead = (notificationId) => {
setNotifications(prev => prev.map(n => n.id === notificationId ? { ...n, isRead: true } : n));
};
// Subcontractor Tasks (Feature 8.2)
const addSubcontractorTask = (input) => {
const subcontractor = subcontractors.find(s => s.id === input.subcontractorId);
if (!subcontractor) {
toast.error('Failed to assign task: subcontractor not found.');
return null;
}
const now = new Date().toISOString();
const task = {
id: `sct_${Date.now()}`,
companyId: input.companyId,
companyName: input.companyName,
subcontractorId: subcontractor.id,
subcontractorName: subcontractor.name,
assignedBy: input.assignedBy,
assignedByName: input.assignedByName,
title: input.title,
location: input.location,
description: input.description || '',
dueDate: input.dueDate,
priority: input.priority || 'medium',
status: 'Assigned',
photos: (input.photos || []).map((p, i) => ({
id: p.id || `${Date.now()}_${i}`,
name: p.name,
url: p.url,
annotation: p.annotation || '',
})),
createdAt: now,
updatedAt: now,
};
setSubcontractorTasks(prev => [task, ...prev]);
addNotification({
recipientUserId: subcontractor.id,
recipientRole: 'SUBCONTRACTOR',
type: 'task_assigned',
message: `You have been assigned a task by ${input.companyName}`,
taskId: task.id,
fromCompanyId: input.companyId,
fromCompanyName: input.companyName,
});
toast.success(`Task assigned to ${subcontractor.name}`);
return task;
};
const updateSubcontractorTask = (taskId, data) => {
let updatedSubId = null;
let prevSubId = null;
let companyName = null;
let companyId = null;
setSubcontractorTasks(prev => prev.map(t => {
if (t.id !== taskId) return t;
prevSubId = t.subcontractorId;
companyName = t.companyName;
companyId = t.companyId;
const nextSubId = data.subcontractorId ?? t.subcontractorId;
updatedSubId = nextSubId;
const nextSub = subcontractors.find(s => s.id === nextSubId);
return {
...t,
...data,
subcontractorId: nextSubId,
subcontractorName: nextSub?.name ?? t.subcontractorName,
photos: data.photos
? data.photos.map((p, i) => ({
id: p.id || `${Date.now()}_${i}`,
name: p.name,
url: p.url,
annotation: p.annotation || '',
}))
: t.photos,
updatedAt: new Date().toISOString(),
};
}));
// If subcontractor changed, dispatch a fresh notification to the new one.
if (updatedSubId && prevSubId && updatedSubId !== prevSubId) {
addNotification({
recipientUserId: updatedSubId,
recipientRole: 'SUBCONTRACTOR',
type: 'task_assigned',
message: `You have been assigned a task by ${companyName}`,
taskId,
fromCompanyId: companyId,
fromCompanyName: companyName,
});
}
toast.success('Task updated');
};
const cancelSubcontractorTask = (taskId) => {
setSubcontractorTasks(prev => prev.map(t => t.id === taskId
? { ...t, status: 'Cancelled', updatedAt: new Date().toISOString() }
: t,
));
toast.success('Task cancelled');
};
const reassignSubcontractorTask = (taskId, newSubcontractorId) => {
const subcontractor = subcontractors.find(s => s.id === newSubcontractorId);
if (!subcontractor) {
toast.error('Failed to reassign: subcontractor not found.');
return;
}
let companyName = null;
let companyId = null;
setSubcontractorTasks(prev => prev.map(t => {
if (t.id !== taskId) return t;
companyName = t.companyName;
companyId = t.companyId;
return {
...t,
subcontractorId: subcontractor.id,
subcontractorName: subcontractor.name,
status: t.status === 'Cancelled' ? 'Assigned' : t.status,
updatedAt: new Date().toISOString(),
};
}));
addNotification({
recipientUserId: subcontractor.id,
recipientRole: 'SUBCONTRACTOR',
type: 'task_assigned',
message: `You have been assigned a task by ${companyName}`,
taskId,
fromCompanyId: companyId,
fromCompanyName: companyName,
});
toast.success(`Task reassigned to ${subcontractor.name}`);
};
return (
<MockStoreContext.Provider value={{
properties,
@@ -5432,6 +5762,30 @@ export const MockStoreProvider = ({ children }) => {
resendOrgInvite: (memberId) => {
toast.success('Invite resent successfully');
},
// Subcontractor Management (Feature 8.2)
subcontractors,
addSubcontractor: (sub) => {
const newSub = {
...sub,
id: sub.id || `sub_${Date.now()}`,
status: sub.status || 'active',
companies: sub.companies || [],
joinedAt: sub.joinedAt || new Date().toISOString().slice(0, 10),
};
setSubcontractors(prev => [...prev, newSub]);
return newSub;
},
subcontractorTasks,
taskPriorities: SUBCONTRACTOR_TASK_PRIORITIES,
taskStatuses: SUBCONTRACTOR_TASK_STATUSES,
addSubcontractorTask,
updateSubcontractorTask,
cancelSubcontractorTask,
reassignSubcontractorTask,
notifications,
addNotification,
markNotificationRead,
}}>
{children}
</MockStoreContext.Provider>