public with sharing class TimesheetController {
// ------------------
// CONFIG – EDIT ME
// ------------------
private static final String TIMELOG_SOBJ = 'Time__c';
private static final String F_PROJECT = 'Project__c';
private static final String F_MILESTONE = 'Milestone__c';
private static final String F_TASK = 'Project_Task__c';
private static final String F_WORKDATE = 'Request_Date__c';
private static final String F_MINUTES = 'Hours__c';
private static final String F_DESC = 'Description__c';
private static final String F_BILLING = 'Billing_Type__c';
private static final String F_User = 'User__c';
private static final String TASK_SOBJ = 'Project_Task__c';
private static final String TASK_F_PROJECT = 'Project__c';
private static final String TASK_F_MILESTONE = 'Milestone__c';
// ------------------
// DTOs
// ------------------
public class InitResponse {
@AuraEnabled public String weekStartIso; // yyyy-MM-dd (Sunday)
@AuraEnabled public List<DateLabel> dates; // 7 days of the week
@AuraEnabled public Map<String,Object> prefillRow; // optional row prefill
}
public class DateLabel {
@AuraEnabled public String iso; // yyyy-MM-dd
@AuraEnabled public String label; // e.g., "Sep 21 Sun"
}
// ------------------
// INIT
// ------------------
@AuraEnabled(cacheable=true)
public static InitResponse initData(Id taskId, String anchorISO) {
InitResponse out = new InitResponse();
Date anchor = (String.isBlank(anchorISO)) ? Date.today() : Date.valueOf(anchorISO);
// Compute Sunday-of-week for the given anchor date
Integer dow = dayOfWeek1to7(anchor); // 1=Sun .. 7=Sat
Integer back = dow - 1; // how many days to step back to Sunday
Date weekStart = anchor.addDays(-back);
out.weekStartIso = String.valueOf(weekStart);
// Build 7 day labels
out.dates = new List<DateLabel>();
for (Integer i = 0; i < 7; i++) {
Date di = weekStart.addDays(i);
DateTime dti = DateTime.newInstance(di.year(), di.month(), di.day(), 0, 0, 0);
DateLabel dl = new DateLabel();
dl.iso = String.valueOf(di); // yyyy-MM-dd
dl.label = dti.format('MMM dd') + ' ' + dti.format('EEE'); // "Sep 21 Sun"
out.dates.add(dl);
}
// Prefill from clicked Project Task
if (taskId != null) {
SObject sob = Database.query(
'SELECT Id,' + TASK_F_PROJECT + ',' + TASK_F_MILESTONE +
' FROM ' + TASK_SOBJ + ' WHERE Id = :taskId LIMIT 1'
);
Map<String,Object> row = new Map<String,Object>();
row.put('projectId', (Id) sob.get(TASK_F_PROJECT));
row.put('milestoneId', (Id) sob.get(TASK_F_MILESTONE));
row.put('taskId', taskId);
row.put('billable', 'Billable');
row.put('projectOptions', new List<Object>());
row.put('milestoneOptions', new List<Object>());
row.put('taskOptions', new List<Object>());
row.put('hours', new Map<String,String>{
'd0'=>'00:00','d1'=>'00:00','d2'=>'00:00','d3'=>'00:00',
'd4'=>'00:00','d5'=>'00:00','d6'=>'00:00'
});
row.put('desc', new Map<String,String>());
row.put('descButtonLabel', 'Add Description');
row.put('total', '00:00');
out.prefillRow = row;
}
return out;
}
// ------------------
// LOOKUP SEARCH
// ------------------
@AuraEnabled(cacheable=true)
public static List<SObject> searchProjects(String q) {
String likeQ = String.isBlank(q) ? '%' : '%' + escapeLike(q) + '%';
return Database.query(
'SELECT Id, Name FROM Project__c WHERE Name LIKE :likeQ ORDER BY Name LIMIT 50'
);
}
@AuraEnabled(cacheable=true)
public static Integer getEditableDaysCount() {
try {
String labelValue = System.Label.Weekly_Timesheet_Days;
return String.isNotBlank(labelValue) ? Integer.valueOf(labelValue) : 0;
} catch (Exception e) {
System.debug('Error retrieving Weekly_Timesheet_Days label: ' + e.getMessage());
return 0;
}
}
@AuraEnabled(cacheable=true)
public static List<SObject> searchMilestones(Id projectId, String q) {
if (projectId == null) return new List<SObject>();
String likeQ = String.isBlank(q) ? '%' : '%' + escapeLike(q) + '%';
return Database.query(
'SELECT Id, Name FROM Milestone__c ' +
'WHERE Project__c = :projectId AND Name LIKE :likeQ ' +
'ORDER BY Name LIMIT 50'
);
}
@AuraEnabled(cacheable=true)
public static List<SObject> searchTasks(Id projectId, Id milestoneId, String q) {
if (projectId == null) return new List<SObject>();
String likeQ = String.isBlank(q) ? '%' : '%' + escapeLike(q) + '%';
String soql = 'SELECT Id, Name,Billing_Type__c FROM ' + TASK_SOBJ +
' WHERE ' + TASK_F_PROJECT + ' = :projectId';
if (milestoneId != null) {
soql += ' AND ' + TASK_F_MILESTONE + ' = :milestoneId';
}
soql += ' AND Name LIKE :likeQ ORDER BY Name LIMIT 50';
return Database.query(soql);
}
private static String escapeLike(String s) {
return s.replace('\\','\\\\').replace('%','\\%').replace('_','\\_');
}
// ------------------
// SAVE
// ------------------
public class UIRow {
public Id projectId;
public Id milestoneId;
public Id taskId;
public String billable; // 'Billable' | 'Non-Billable'
public List<Entry> entries;
}
public class Entry {
public String iso; // yyyy-MM-dd
public String hhmm; // "HH:mm"
public String description;
}
@AuraEnabled
public static void saveWeek(String weekStartISO, String rowsJSON) {
if (String.isBlank(rowsJSON)) return;
List<UIRow> rows = (List<UIRow>) JSON.deserialize(rowsJSON, List<UIRow>.class);
List<SObject> inserts = new List<SObject>();
for (UIRow r : rows) {
if (r == null || r.entries == null) continue;
for (Entry e : r.entries) {
if (e == null) continue;
Decimal mins = Decimal.valueOf(e.hhmm.replace(':', '.'));
if (mins == 0) continue;
SObject tl = Schema.getGlobalDescribe().get(TIMELOG_SOBJ).newSObject();
// if (r.projectId != null) tl.put(F_PROJECT, r.projectId);
// if (r.milestoneId != null) tl.put(F_MILESTONE, r.milestoneId);
if (r.taskId != null) tl.put(F_TASK,r.taskId);
tl.put(F_WORKDATE, Date.valueOf(e.iso));
tl.put(F_MINUTES, mins);
if (!String.isBlank(e.description)) tl.put(F_DESC, e.description);
if (!String.isBlank(r.billable)) {
tl.put(F_BILLING, r.billable);
tl.put(F_User, UserInfo.getUserId());
}
inserts.add(tl);
}
}
if (!inserts.isEmpty()) insert inserts;
}
private static Integer hhmmToMinutes(String hhmm) {
if (String.isBlank(hhmm) || !hhmm.contains(':')) return 0;
List<String> parts = hhmm.split(':');
return Integer.valueOf(parts[0]) * 60 + Integer.valueOf(parts[1]);
}
// ------------------
// Helpers
// ------------------
// Returns 1..7 for Sun..Sat using DateTime.format('E') to derive weekday safely
private static Integer dayOfWeek1to7(Date d) {
DateTime dt = DateTime.newInstance(d.year(), d.month(), d.day(), 0, 0, 0);
String name = dt.format('EEE'); // Sun, Mon, Tue, ...
if (name == 'Sun') return 1;
else if (name == 'Mon') return 2;
else if (name == 'Tue') return 3;
else if (name == 'Wed') return 4;
else if (name == 'Thu') return 5;
else if (name == 'Fri') return 6;
else return 7; // Sat
}
}