Merge branch 'dev' of bitbucket.org:jubilian/nhance into dev

This commit is contained in:
VENKATESHWARAN 2026-02-26 14:05:57 +05:30
commit 462cb3b18f
15 changed files with 975 additions and 754 deletions

View File

@ -30,6 +30,10 @@ class Acl
'#^/util/download_log#' => ['roles' => [ADMIN_ROLE_ID]],
'#^/metaDashboardDemo#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/metaTpaDashboardDemo#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/sales/dashboard#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
'#^/sales#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
// ===================== PUBLIC DOWNLOADS / FORMS =====================
'#^/download-#' => ['public' => true],

View File

@ -11,10 +11,11 @@ $routes->options('(:any)', function() {
});
$routes->group('sheet', function ($routes) {
$routes->get('(:any)', 'GoogleSheetController::editor/$1');
$routes->get('(:any)/fetch', 'GoogleSheetController::fetch/$1');
$routes->post('(:any)/save', 'GoogleSheetController::save/$1');
$routes->get('(:any)/download', 'GoogleSheetController::download/$1');
$routes->post('create', 'GoogleSheetController::create');
$routes->get('(:segment)/fetch', 'GoogleSheetController::fetch/$1');
$routes->post('(:segment)/save', 'GoogleSheetController::save/$1');
$routes->get('(:segment)/download', 'GoogleSheetController::download/$1');
$routes->get('(:segment)', 'GoogleSheetController::editor/$1');
});
@ -912,7 +913,7 @@ $routes->group('sales', function($routes) {
$routes->get('/', 'SalesController::index');
$routes->get('activities', 'SalesController::activities');
$routes->get('loadactivities', 'SalesController::loadactivities');
$routes->get('page/(:segment)', 'SalesController::noPage/$1');
@ -996,6 +997,7 @@ $routes->group('sales', function($routes) {
//Dashboard
$routes->get('dashboard', 'SalesController::dashboard');
$routes->get('branchLevelDashboard', 'SalesController::branchLevelDashboard');
$routes->get('salesManagerLevelDashboard', 'SalesController::salesManagerLevelDashboard');
});

View File

@ -6,6 +6,35 @@ use CodeIgniter\Controller;
class GoogleSheetController extends Controller
{
protected GoogleSheetLib $sheetLib;
public $config = [
'permissions' => [
'editors' => [
'vitvelz@gmail.com', 'velz1990@gmail.com','venkateshraman786@gmail.com'
// 'group:rfq-editors@company.com'
],
'viewers' => [],
],
'protections' => [
[
'range' => 'RFQ Page!B12:C12',
'users' => [ //allowed users to edit this cell range
'velz1990@gmail.com','vitvelz@gmail.com','firebase-adminsdk-mcdfe@nhance-ee8d1.iam.gserviceaccount.com'
],
'groups' => []
],
[
'range' => 'Claims Page!A1',
'users' => [
'velz1990@gmail.com','venkateshraman786@gmail.com','firebase-adminsdk-mcdfe@nhance-ee8d1.iam.gserviceaccount.com'
],
'groups' => []
]
],
'parentFolderId' => '19uySI-PSFQfFpZvMSnBtirXbwfCTXFBL', // RFQ folder
];
public function __construct()
{
@ -23,10 +52,16 @@ class GoogleSheetController extends Controller
/* ---------- FETCH ---------- */
public function fetch(string $sheetId)
{
// $data = $this->sheetLib->read($sheetId);
$data = $this->sheetLib->read($sheetId);
// print_rr($data);die;
// return $this->response->setJSON($data);
echo 'Hi';
return $this->response->setJSON($data);
// echo 'Hi';
}
public function sample(string $sheetId)
{
echo $sheetId;
}
/* ---------- SAVE ---------- */
@ -63,4 +98,39 @@ class GoogleSheetController extends Controller
)
->setBody($content);
}
public function create()
{
try {
$config = config('RfqConfig');
// #$lib = new RfqGoogleSheetLib();
// $templateId = $this->request->getPost('templateId');
// if (!$templateId) {
// throw new \Exception('Template ID missing');
// }
// $newSheetId = $this->sheetLib->copyTemplate(
// $templateId,
// 'RFQ_' . date('Ymd_His'),
// $this->config['parentFolderId']
// );
$newSheetId = '1GXNDNXoWriClb5HCqPYd2GAY1T8aie_Hos0yAOoTaC0';
// $this->sheetLib->applyPermissions($newSheetId, $this->config['permissions']);
$this->sheetLib->applyProtections($newSheetId, $this->config['protections']);
return $this->response->setJSON([
'status' => 'success',
'url' => $this->sheetLib->sheetUrl($newSheetId)
]);
} catch (\Throwable $e) {
return $this->response
->setStatusCode(500)
->setJSON([
'status' => 'error',
'message' => $e->getMessage()
]);
}
}
}

View File

@ -35,12 +35,11 @@ class SalesController extends BaseController
$data = $this->getSalesStaffData();
$data['tab_name'] = 'Leads';
$data['page_name'] = 'Leads';
$data['page_name'] = 'Leads';
return $this->loadLayout('sales/tracker_view', $data);
}
public function activities(){
public function loadactivities(){
$data = $this->getSalesStaffData();
$data['tab_name'] = 'Activities';
@ -49,6 +48,7 @@ class SalesController extends BaseController
$data['leads'] = $this->leadModel->select('lead_id, company_name')
->orderBy('lead_id', 'DESC')
->findAll();
return $this->loadLayout('sales/activity_view', $data);
}
@ -59,42 +59,36 @@ class SalesController extends BaseController
private function getSalesStaffData(): array
{
$db = \Config\Database::connect();
$logged_user_id = get_session_userid();
$logged_user_id = get_session_userid();
$role = get_role_id();
$team_id = user_team();
$data = [
'users' => [],
'sales_manager_ids' => []
];
// Get the Branch ID, Role, Team, and Name of the logged-in user
$row = $db->table('user_profiles up')
->select('up.id, up.first_name, up.last_name, up.nhance_branch_id, up.role, ut.team_id')
->join('user_teams ut', 'ut.user_id = up.id', 'left')
->where('up.is_active', 1)
->where('up.id', $logged_user_id)
->get()
->getRow();
$row = $db->table('user_profiles')->select('*')
->where('is_active', 1)->where('id', $logged_user_id)
->get()->getRow();
$nhance_branch_id = $row ? $row->nhance_branch_id : null;
$role = $row ? $row->role : null;
$team_id = $row ? $row->team_id : null;
// Is the logged-in user a Sales Manager? (Role 4, Team 5)
if ($role == 4 && $team_id == 5) {
if ($role == 4 && in_array(5, $team_id)) {
$data['sales_manager_ids'] = [$logged_user_id];
$data['users'] = [
[
'id' => $row->id,
'sales_manager' => trim($row->first_name . ' ' . $row->last_name),
'first_name' => $row->first_name,
'nhance_branch_id' => $nhance_branch_id
]
];
}
// Otherwise fetch ALL sales managers in this branch
elseif ($nhance_branch_id) {
elseif (in_array($role,[1,2,3,4,5])) {
$data['users'] = $db->table('user_profiles up')
->select('up.id, up.first_name, up.last_name, up.nhance_branch_id')
@ -779,11 +773,59 @@ class SalesController extends BaseController
// ==================== Dashboard ====================
public function dashboard(){
public function branchLevelDashboard()
$logged_user_id = get_session_userid();
$role = get_role_id();
$team_id = user_team();
$payload = $this->request->getGet();
$db = \Config\Database::connect();
$row = $db->table('user_profiles')
->select('*')
->where('is_active', 1)
->where('id', $logged_user_id)
->get()
->getRowArray();
$nhance_branch_id = $row ? $row['nhance_branch_id'] : null;
// dd($logged_user_id, $nhance_branch_id, $role, $team_id );
if (in_array($role,[5])) {
$sales_manager_ids = array_column(
$db->table('user_profiles up')
->select('up.id')
->join('user_teams ut', 'ut.user_id = up.id')
->where([
'up.is_active' => 1,
'ut.is_active' => 1,
'up.role' => 4,
'ut.team_id' => 5,
'up.nhance_branch_id' => $nhance_branch_id
])
->get()
->getResultArray(),
'id'
);
$this->branchLevelDashboard($nhance_branch_id,$sales_manager_ids);
}
elseif ($role == 4 && in_array(5, $team_id)) {
$sales_manager_ids = [$logged_user_id];
$this->salesManagerLevelDashboard($logged_user_id,$sales_manager_ids, $payload);
}
}
public function branchLevelDashboard($branchId,$sales_manager_ids)
{
$data['tab_name'] = 'Dashboard';
$data['page_name'] = 'Dashboard';
// Hardcoded branch ID as requested
$branchId = 1;
// $branchId = 1;
try {
// 1. Lead Statistics
@ -798,11 +840,24 @@ class SalesController extends BaseController
// 3. Team Performance (Aggregating activity counts per user)
$db = \Config\Database::connect();
// Final safe check
if (empty($sales_manager_ids)) {
// No valid IDs — skip queries or return empty
$teamPerformance = [];
$recentActivities = [];
$leadsOverview = [];
} else {
if (empty($sales_manager_ids) || !is_array($sales_manager_ids)) {
$sales_manager_ids = array_filter((array) $sales_manager_ids); // removes null, "", 0
}
$teamPerformance = $db->table('user_profiles as u')
->select('u.first_name, u.last_name, u.profile as role,
(SELECT COUNT(*) FROM sales_activities WHERE assigned_to = u.id) as total_acts,
(SELECT COUNT(*) FROM sales_activities WHERE assigned_to = u.id AND status = "completed") as done_acts')
->where('u.nhance_branch_id', $branchId)
->whereIn('u.id', $sales_manager_ids)
->where('u.is_active', 1)
->get()->getResultArray();
@ -812,11 +867,48 @@ class SalesController extends BaseController
->orderBy('sales_activities.scheduled_date', 'DESC')
->limit(6)
->findAll();
$sales_manager_ids = array_values(array_map('intval', $sales_manager_ids));
$recentActivities = $db->table('sales_activities sa')
->select('sa.activity_id,sa.lead_id,sa.activity_type,sa.scheduled_date,sa.status,sa.assigned_to,sal.company_name,up.first_name AS assigned_to_name')
->join('(SELECT lead_id, MAX(company_name) AS company_name FROM sales_actual_leads GROUP BY lead_id) sal','sal.lead_id = sa.lead_id','left')
->join('user_profiles up', 'up.id = sa.assigned_to', 'left')
->orderBy('sa.scheduled_date', 'DESC')
->limit(6)
->whereIn('sa.assigned_to', $sales_manager_ids)
->get()->getResultArray();
// 5. All Leads Overview
$leadsOverview = $db->table('sales_actual_leads sal')
->select('sal.lead_id,sal.company_name,sal.status,up.first_name AS assigned_to,
COUNT(DISTINCT sa.activity_id) AS activities,
COUNT(DISTINCT l.id) AS opportunities
')
->join('user_profiles up', 'up.id = sal.assigned_to', 'left')
->join('sales_activities sa', 'sa.lead_id = sal.lead_id', 'left')
->join('leads l', 'l.actual_lead_id = sal.lead_id', 'left')
->groupBy('sal.lead_id, sal.company_name, sal.status, up.first_name')
->having('COUNT(DISTINCT sa.activity_id) + COUNT(DISTINCT l.id) >', 0) // ← this line
->orderBy('sal.created_at', 'DESC')
->whereIn('sal.assigned_to', $sales_manager_ids)
->get()
->getResultArray();
}
// 6. activityBreakdown
$total = $db->table('sales_activities')->countAll();
$activityBreakdown = $db->table('sales_activities')
->select("activity_type, COUNT(*) AS total, ROUND(COUNT(*) * 100.0 / {$total}, 0) AS percentage", false)
->groupBy('activity_type')
->orderBy('total', 'DESC')
->get()
->getResultArray();
$teamPerformance = array_filter($teamPerformance, function($row) {
return ($row['total_acts'] + $row['done_acts']) > 0;
});
// 5. All Leads Overview
$leadsOverview = $this->leadModel->select('sales_actual_leads.*, user_profiles.first_name, user_profiles.last_name')
->join('user_profiles', 'user_profiles.id = sales_actual_leads.assigned_to', 'left')
->findAll();
$data = [
'total_leads' => $stats['total'],
@ -825,7 +917,10 @@ class SalesController extends BaseController
'pipeline_value' => '15.0L', // Hardcoded placeholder from PDF [cite: 14]
'team' => $teamPerformance,
'recent_acts' => $recentActivities,
'leads_overview' => $leadsOverview
'leads_overview' => $leadsOverview,
'activity_breakdown'=> $activityBreakdown,
'tab_name' => "Sales Dashboard",
'page_name' => "Sales Dashboard"
];
// dd($data);
@ -839,16 +934,15 @@ class SalesController extends BaseController
}
}
public function salesManagerLevelDashboard()
public function salesManagerLevelDashboard($userId,$sales_manager_ids, $payload = [])
{
$userId = get_session_userid();
// $userId = get_session_userid();
// $userId = 1;
$db = \Config\Database::connect();
try {
$payload = $this->request->getGet();
// $payload = $this->request->getGet();
$current_fin_year = $payload['fy'] ?? getCurrentFinancialYear();
@ -858,8 +952,12 @@ class SalesController extends BaseController
->orderBy('fy_year', 'desc')
->get()
->getResultArray();
$fin_years = array_column($fin_years, 'fy_year');
$fin_years[] = '2024-2025';
if(empty($fin_years)){
$fin_years[] = $current_fin_year;
}
$target = $db->table('sales_target')
->where('user_id', $userId)
@ -907,7 +1005,9 @@ class SalesController extends BaseController
'recent_leads' => $recentLeads,
'fin_years' => $fin_years,
'display_fin_years' => format_financial_year($current_fin_year),
'user_name' => get_session_userdata()->first_namee ?? ''
'user_name' => get_session_userdata()->first_namee ?? '',
'tab_name' => "Sales Dashboard",
'page_name' => "Sales Dashboard"
];
// dd($data);

View File

@ -1002,7 +1002,7 @@ class TestingController extends BaseController
// 'dashboard' => 1
'dashboard' => $database_id
],
'exp' => time() + (10 * 60) // 10 minutes
'exp' => time() + (10 * 60), // 10 minutes
];
if(!empty($policy_id)){

View File

@ -4,6 +4,7 @@ use Google_Client;
use Google_Service_Sheets;
use Google_Service_Drive;
use Google_Service_Sheets_ValueRange;
use Google_Service_Sheets_BatchUpdateSpreadsheetRequest;
class GoogleSheetLib
{
@ -11,13 +12,14 @@ class GoogleSheetLib
protected Google_Service_Sheets $sheets;
protected Google_Service_Drive $drive;
public function __construct()
{
$this->client = new Google_Client();
// Service account JSON
$this->client->setAuthConfig(
ROOTPATH . 'gdrive-demo-394007-5b1d856b0c5b.json'
ROOTPATH . 'nhance-ee8d1-e3c5269b1ec7.json'
);
// IMPORTANT for service account
@ -32,11 +34,12 @@ class GoogleSheetLib
// Init services
$this->sheets = new Google_Service_Sheets($this->client);
$this->drive = new Google_Service_Drive($this->client);
}
/* ===================== READ ===================== */
public function read(string $spreadsheetId, string $range = 'Sheet1')
public function read(string $spreadsheetId, string $range = 'RFQ Page')
{
$response = $this->sheets
->spreadsheets_values
@ -77,4 +80,205 @@ class GoogleSheetLib
return $response->getBody()->getContents();
}
/* ================= COPY TEMPLATE ================= */
public function copyTemplate(string $templateId, string $name, string $folderId): string
{
$file = $this->drive->files->copy(
$templateId,
new \Google_Service_Drive_DriveFile([
'name' => $name,
'parents' => [$folderId],
]),[
'supportsAllDrives' => true,
'fields' => 'id, name, parents'
]
);
return $file->id;
}
/* ================= PERMISSIONS ================= */
public function applyPermissions(string $fileId, array $permissions)
{
foreach ($permissions['editors'] as $email) {
$this->createPermission($fileId, $email, 'writer');
}
foreach ($permissions['viewers'] as $email) {
$this->createPermission($fileId, $email, 'reader');
}
}
private function createPermission(string $fileId, string $email, string $role)
{
$type = str_starts_with($email, 'group:') ? 'group' : 'user';
$email = str_replace('group:', '', $email);
$this->drive->permissions->create(
$fileId,
new \Google_Service_Drive_Permission([
'type' => $type,
'role' => $role,
'emailAddress' => $email
]),
['sendNotificationEmail' => false,'supportsAllDrives' => true]
);
}
/* ================= PROTECTIONS ================= */
public function applyProtectionsold(string $spreadsheetId, array $ranges)
{
$spreadsheet = $this->sheets->spreadsheets->get($spreadsheetId);
$sheetId = $spreadsheet->getSheets()[0]->getProperties()->getSheetId();
$requests = [];
foreach ($ranges as $range) {
[$sheetName, $a1] = explode('!', $range);
$requests[] = [
'addProtectedRange' => [
'protectedRange' => [
'range' => [
'sheetId' => $sheetId
],
'warningOnly' => false
]
]
];
}
$this->sheets->spreadsheets->batchUpdate(
$spreadsheetId,
new Google_Service_Sheets_BatchUpdateSpreadsheetRequest([
'requests' => $requests
])
);
}
public function applyProtections(string $spreadsheetId, array $protections)
{
// Fetch spreadsheet metadata
$spreadsheet = $this->sheets->spreadsheets->get(
$spreadsheetId,
['fields' => 'sheets(properties(sheetId,title,gridProperties))']
);
// Map sheet names
$sheetMap = [];
foreach ($spreadsheet->getSheets() as $sheet) {
$props = $sheet->getProperties();
$sheetMap[$props->getTitle()] = [
'sheetId' => $props->getSheetId(),
'rowCount' => $props->getGridProperties()->getRowCount(),
'colCount' => $props->getGridProperties()->getColumnCount(),
];
}
$requests = [];
foreach ($protections as $protection) {
$rangeStr = $protection['range'];
if (!str_contains($rangeStr, '!')) {
throw new \Exception("Invalid range format: {$rangeStr}");
}
[$sheetName, $a1] = explode('!', $rangeStr, 2);
if (!isset($sheetMap[$sheetName])) {
throw new \Exception("Sheet not found: {$sheetName}");
}
$sheetMeta = $sheetMap[$sheetName];
$gridRange = $this->convertA1ToGridRange(
$a1,
$sheetMeta['sheetId'],
$sheetMeta['rowCount'],
$sheetMeta['colCount']
);
$protectedRange = [
'range' => $gridRange,
'description' => 'RFQ Protected Area',
'warningOnly' => false,
'editors' => [
'users' => $protection['users'] ?? [],
'groups' => $protection['groups'] ?? []
]
];
$requests[] = [
'addProtectedRange' => [
'protectedRange' => $protectedRange
]
];
}
if (!empty($requests)) {
$batch = new \Google_Service_Sheets_BatchUpdateSpreadsheetRequest([
'requests' => $requests
]);
$this->sheets->spreadsheets->batchUpdate($spreadsheetId, $batch);
}
return true;
}
/* ================= URL ================= */
public function sheetUrl(string $sheetId): string
{
return "https://docs.google.com/spreadsheets/d/{$sheetId}/edit";
}
private function convertA1ToGridRange($a1, $sheetId, $maxRows, $maxCols)
{
if (preg_match('/^([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?$/i', $a1, $m)) {
$startCol = $this->colToIndex($m[1]);
$startRow = intval($m[2]) - 1;
if (!empty($m[3])) {
$endCol = $this->colToIndex($m[3]) + 1;
$endRow = intval($m[4]);
} else {
$endCol = $startCol + 1;
$endRow = $startRow + 1;
}
return [
'sheetId' => $sheetId,
'startRowIndex' => $startRow,
'endRowIndex' => $endRow,
'startColumnIndex' => $startCol,
'endColumnIndex' => $endCol
];
}
throw new \Exception("Unsupported A1 format: {$a1}");
}
private function colToIndex($letters)
{
$letters = strtoupper($letters);
$index = 0;
for ($i = 0; $i < strlen($letters); $i++) {
$index = $index * 26 + (ord($letters[$i]) - 64);
}
return $index - 1;
}
}

View File

@ -16,6 +16,28 @@
<button onclick="save()">💾 Save</button>
<button onclick="download()"> Download</button>
<button onclick="createRfq()">🚀 Create RFQ</button>
<div id="result" style="margin-top:15px;"></div>
<script>
function createRfq() {
fetch('<?php echo base_url() ?>' +'/sheet/create', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'templateId=1efHU8EBkjTZTzOx115xRrtgz3kwvvVp0p5GbrvREfiw'
})
.then(r => r.json())
.then(res => {
if (res.status === 'success') {
document.getElementById('result').innerHTML =
`<a href="${res.url}" target="_blank">Open RFQ Sheet</a>`;
} else {
alert(res.message);
}
});
}
</script>
<hr>
@ -26,9 +48,9 @@
const sheetId = "<?= esc($sheetId) ?>";
alert('second');
/* ---------- LOAD ---------- */
fetch('<?php echo base_url() ?>' + `/sheet/${sheetId}/fetch`)
.then(r => r.json())
fetch('<?php echo base_url() ?>' + `sheet/${sheetId}/fetch`)
.then(r => r.json())
// .then(r => r.json())
.then(render);
function render(data) {

View File

@ -2077,7 +2077,7 @@
</a>
</li>
<?php } ?>
<?php if (in_array(get_role_id(), [1, 2, 3, 5])) { ?>
<?php if (in_array(get_role_id(), [1, 2, 3, 4]) || in_array(SALES_TEAM_ID, user_team())) { ?>
<li class="li-seperate" id="app-sales-tracker-li">
<a id="app_sales_tracker" href="#sidebarSalesTrackermenu" data-toggle="collapse" class="waves-effect img-inactive" style="color: grey;">
<img
@ -2087,13 +2087,13 @@
<div class="collapse" id="sidebarSalesTrackermenu">
<ul class="nav-second-level">
<li>
<a href="<?= base_url('/sales/page/Dashboard') ?>"> <i class="ri-dashboard-line"></i> Dashboard </a>
<a href="<?= base_url('/sales/dashboard') ?>"> <i class="ri-dashboard-line"></i> Dashboard </a>
</li>
<li>
<a href="<?= base_url('/sales') ?>"> <i class="ri-group-line"></i> Leads </a>
</li>
<li>
<a href="<?= base_url('/sales/activities') ?>"><i class="ri-flashlight-line"></i> Activities </a>
<a href="<?= base_url('/sales/loadactivities') ?>"><i class="ri-flashlight-line"></i> Activities </a>
</li>
<!-- <li>
<a href="<?= base_url('/sales/page/Opportunities') ?>"> <i class="ri-checkbox-circle-line"></i> Opportunities </a>

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,38 @@
.stat-change { font-size: 11px; margin-top: 8px; font-weight: 600; }
.text-success { color: #48bb78; }
.card-hero {
background: white;
padding: 25px;
border-radius: 12px;
border: 1px solid #edf2f7;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
position: relative;
overflow: hidden;
transition: transform 0.2s ease;
}
/* The Gradient Overlay Effect */
.card-hero::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px; /* Thin line at the top */
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
}
.card-hero:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
}
/* Optional: Subtle Background Gradient */
.card-hero.gradient-bg {
background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
}
.main-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; margin-bottom: 30px; }
.table-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
@ -29,10 +61,18 @@
.status-pill { padding: 4px 12px; border-radius: 20px; font-size: 11px; font-weight: 700; display: inline-block; }
.status-completed { background: #f0fff4; color: #38a169; }
.status-pending { background: #fffaf0; color: #dd6b20; }
.status-new { background: #e3f2fd; color: #1976d2; }
.status-potential { background: #fff3e0; color: #f57c00; }
.status-prospects { background: #e8f5e9; color: #388e3c; }
.status-not-a-prospects { background: #ffebee; color: #d32f2f; }
.empty-state { text-align: center; padding: 60px 20px; color: #999; }
.empty-icon { width: 80px; height: 80px; margin: 0 auto 20px; background: #f5f5f5; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 36px; }
</style>
<div class="dash-container">
<div class="top-header">
<!-- <div class="top-header">
<div>
<h2 style="font-weight: 800; color: #1a202c; font-size: 24px;">Dashboard</h2>
<p style="color: #718096; font-size: 14px; margin-top: 4px;">Overview of your branch</p>
@ -40,29 +80,34 @@
<div style="position: relative;">
<input type="text" placeholder="Search leads, activities....." style="background:white; padding:12px 20px; border-radius:10px; border:1px solid #e2e8f0; width: 350px; font-size: 13px;">
</div>
</div>
</div> -->
<div class="stat-cards">
<div class="card">
<div class="stat-val">4</div>
<div class="card-hero">
<div class="stat-val"><?php echo $total_leads; ?></div>
<div class="stat-label">Total Leads</div>
<div class="stat-change text-success"> 12% this month</div>
<!-- <div class="stat-change text-success"> 12% this month</div> -->
</div>
<div class="card">
<div class="stat-val">6</div>
<div class="card-hero">
<div class="stat-val"><?php echo $total_activities ?></div>
<div class="stat-label">Total Activities</div>
<div class="stat-change text-success"> 5% this month</div>
<!-- <div class="stat-change text-success"> 5% this month</div> -->
</div>
<div class="card">
<div class="stat-val">1</div>
<div class="card-hero">
<div class="stat-val"><?php echo ($total_activities - $completed_acts) ?></div>
<div class="stat-label">Pending Activities</div>
<!-- <div class="stat-change text-success"> 15% this month</div> -->
</div>
<div class="card-hero">
<div class="stat-val"><?php echo $completed_acts ?></div>
<div class="stat-label">Completed Activities</div>
<div class="stat-change text-success"> 15% this month</div>
<!-- <div class="stat-change text-success"> 15% this month</div> -->
</div>
<div class="card">
<!-- <div class="card">
<div class="stat-val">₹15.0L</div>
<div class="stat-label">Pipeline Value</div>
<div class="stat-change text-success"> 18% this month</div>
</div>
</div> -->
</div>
<div class="main-grid">
@ -81,62 +126,117 @@
</tr>
</thead>
<tbody>
<tr>
<td><strong>Venba Infotech</strong></td>
<td>
<div style="color: #ff6b35; font-weight: 700; font-size: 11px;">CALL</div>
<div style="font-size: 12px; color: #718096;">John Doe</div>
</td>
<td style="color: #4a5568; font-weight: 500;">20 Feb 2026</td>
<td><span class="status-pill status-completed">Completed</span></td>
</tr>
<tr>
<td><strong>Acme Corporation</strong></td>
<td>
<div style="color: #ff6b35; font-weight: 700; font-size: 11px;">EMAIL</div>
<div style="font-size: 12px; color: #718096;">John Doe</div>
</td>
<td style="color: #4a5568; font-weight: 500;">20 Feb 2026</td>
<td><span class="status-pill status-pending">Pending</span></td>
</tr>
<?php if (!empty($recent_acts)): ?>
<?php foreach ($recent_acts as $a): ?>
<?php
$activityIcons = [
'Call' => '📞',
'Email' => '✉️',
'Meeting' => '📅',
'Visit' => '🚗',
'Demo' => '🎬',
'Share' => '📄',
'Todo' => '✓'
];
$icon = $activityIcons[$a['activity_type']] ?? '📌';
$statusClass = strtolower(str_replace(' ', '-', $a['status']));
$formattedDate = date('d M Y', strtotime($a['scheduled_date']));
?>
<tr>
<td><strong><?= esc($a['company_name']) ?></strong></td>
<td>
<div style="color: #ff6b35; font-weight: 700; font-size: 11px;">
<?= $icon ?> <?= strtoupper(esc($a['activity_type'])) ?>
</div>
<div style="font-size: 12px; color: #718096;">
<?= esc($a['assigned_to_name'] ?? 'ID: ' . $a['assigned_to']) ?>
</div>
</td>
<td style="color: #4a5568; font-weight: 500;"><?= $formattedDate ?></td>
<td><span class="status-pill status-<?= $statusClass ?>"><?= ucfirst(esc($a['status'])) ?></span></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="4">
<div class="empty-state">
<div class="empty-icon"></div> No activities found
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div>
<div class="card">
<h3 style="font-size: 16px; font-weight: 700; margin-bottom: 15px;">Sales Team Performance</h3>
<div class="team-member">
<div class="member-img" style="background: #ff6b35;">V</div>
<div style="flex: 1;">
<div style="font-weight: 700; font-size: 14px;">Venkat 3.0</div>
<div style="font-size: 11px; color: #718096;">Sales Manager</div>
</div>
<div class="act-stats">
<div class="total">6 acts</div>
<div class="done">1 done</div>
</div>
</div>
<div class="team-member">
<div class="member-img" style="background: #4a5568;">P</div>
<div style="flex: 1;">
<div style="font-weight: 700; font-size: 14px;">Pavi_the_Staff V</div>
<div style="font-size: 11px; color: #718096;">Sales Staff</div>
</div>
<div class="act-stats">
<div class="total">0 acts</div>
<div class="done">0 done</div>
</div>
</div>
</div>
<?php foreach ($team as $member) {
$firstLetter = strtoupper(substr($member['first_name'], 0, 1));
$fullName = $member['first_name'] . ' ' . $member['last_name'];
$totalActs = $member['total_acts'];
$doneActs = $member['done_acts'];
$role = $member['role']; // fix your DB first
// Random or consistent color based on name
$colors = ['#ff6b35', '#667eea', '#48bb78', '#ed8936', '#9f7aea'];
$colorIndex = abs(crc32($member['first_name'])) % count($colors);
$color = $colors[$colorIndex];
echo "
<div class='team-member'>
<div class='member-img' style='background: {$color};'>{$firstLetter}</div>
<div style='flex: 1;'>
<div style='font-weight: 700; font-size: 14px;'>{$fullName}</div>
<div style='font-size: 11px; color: #718096;'>{$role}</div>
</div>
<div class='act-stats'>
<div class='total'>{$totalActs} acts</div>
<div class='done'>{$doneActs} done</div>
</div>
</div>";
} ?>
<div class="card breakdown-card">
<h3 style="font-size: 15px; font-weight: 700; margin-bottom: 20px;">Activity Breakdown</h3>
<div class="breakdown-item"><span><span class="dot" style="background: #ff6b35;"></span> Call</span><strong>42%</strong></div>
<!-- <div class="breakdown-item"><span><span class="dot" style="background: #ff6b35;"></span> Call</span><strong>42%</strong></div>
<div class="breakdown-item"><span><span class="dot" style="background: #48bb78;"></span> Email</span><strong>25%</strong></div>
<div class="breakdown-item"><span><span class="dot" style="background: #4299e1;"></span> Meeting</span><strong>18%</strong></div>
<div class="breakdown-item"><span><span class="dot" style="background: #ecc94b;"></span> Visit</span><strong>10%</strong></div>
<div class="breakdown-item"><span><span class="dot" style="background: #9f7aea;"></span> Demo</span><strong>5%</strong></div>
<div class="breakdown-item"><span><span class="dot" style="background: #9f7aea;"></span> Demo</span><strong>5%</strong></div> -->
<?php
$activityConfig = [
'Call' => ['color' => '#ff6b35', 'icon' => '📞'],
'Email' => ['color' => '#48bb78', 'icon' => '✉️'],
'Meeting' => ['color' => '#4299e1', 'icon' => '📅'],
'Visit' => ['color' => '#ecc94b', 'icon' => '🚗'],
'Demo' => ['color' => '#9f7aea', 'icon' => '🎬'],
'Share' => ['color' => '#ed8936', 'icon' => '📄'],
'Todo' => ['color' => '#718096', 'icon' => '✓'],
];
?>
<?php if (!empty($activity_breakdown)): ?>
<?php foreach ($activity_breakdown as $item): ?>
<?php
$type = $item['activity_type'];
$color = $activityConfig[$type]['color'] ?? '#718096';
$icon = $activityConfig[$type]['icon'] ?? '📌';
?>
<div class="breakdown-item">
<span>
<span class="dot" style="background: <?= $color ?>;"></span>
<?= $icon ?> <?= esc($type) ?>
</span>
<strong><?= $item['percentage'] ?>%</strong>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="empty-state">No activities found</div>
<?php endif; ?>
</div>
</div>
</div>
@ -157,20 +257,24 @@
</tr>
</thead>
<tbody>
<tr>
<td><strong>Venba Infotech</strong></td>
<td><span style="background: #edf2f7; color: #4a5568; font-size: 10px; padding: 3px 10px; border-radius: 12px; font-weight: 700; text-transform: uppercase;">New</span></td>
<td style="color: #4a5568;">John Doe</td>
<td><div style="text-align: center; font-weight: 700;">1</div></td>
<td><div style="text-align: center; font-weight: 700;">1</div></td>
</tr>
<tr>
<td><strong>Acme Corporation</strong></td>
<td><span style="background: #fff3e0; color: #f57c00; font-size: 10px; padding: 3px 10px; border-radius: 12px; font-weight: 700; text-transform: uppercase;">Potential</span></td>
<td style="color: #4a5568;">John Doe</td>
<td><div style="text-align: center; font-weight: 700;">1</div></td>
<td><div style="text-align: center; font-weight: 700;">0</div></td>
</tr>
<?php if (!empty($leads_overview)): ?>
<?php foreach ($leads_overview as $lead): ?>
<?php $statusClass = strtolower(str_replace(' ', '-', $lead['status'])); ?>
<tr>
<td><strong><?= esc($lead['company_name']) ?></strong></td>
<td><span class="status-pill status-<?= $statusClass ?>"><?= esc($lead['status']) ?></span></td>
<td><?= esc($lead['assigned_to'] ?? 'Unassigned') ?></td>
<td><div style="text-align: center; font-weight: 700;"><?= $lead['activities'] ?></div></td>
<td><div style="text-align: center; font-weight: 700;"><?= $lead['opportunities'] ?></div></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="5">
<div class="empty-state">No leads found</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>

View File

@ -19,6 +19,35 @@
.lead-initial { width: 35px; height: 35px; background: #f0f0f0; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; color: #ff6b35; }
.status-pill { font-size: 10px; padding: 2px 10px; border-radius: 12px; font-weight: 600; }
/* Modals */
.modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 2000; align-items: center; justify-content: center; }
.modal.active { display: flex; }
.modal-content { background: white; border-radius: 12px; width: 90%; max-width: 800px; max-height: 90vh; overflow-y: auto; display: flex; flex-direction: column; }
.modal-header { padding: 5px 25px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; }
.modal-body { padding: 25px; flex: 1; }
.form-group { margin-bottom: 20px; }
.form-label { display: block; margin-bottom: 8px; font-size: 14px; font-weight: 500; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.empty-state { text-align: center; padding: 60px 20px; color: #999; }
.empty-icon { width: 80px; height: 80px; margin: 0 auto 20px; background: #f5f5f5; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 36px; }
input, select, textarea { background-color: white !important; border-radius: 6px !important; box-shadow: none !important; border-color: #ddd !important; }
input, select, textarea { width: 400px; padding: 10px 15px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 14px; outline: none; }
input:focus, select:focus, textarea:focus { border-color: #ff6b35; outline: none; /* removes blue browser outline */ }
.modal .form-control { transition: 0.2s ease; }
.modal .form-control:focus { border-color: #ff6b35; box-shadow: 0 0 0 2px rgba(255, 107, 53, 0.2);
outline: none;
}
.btn-primary { background: #ff6b35; color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
.btn-primary:hover { background: #ff5722; transform: translateY(-1px); }
.btn-complete { background: #4caf50; color: white; border: none;padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s;}
.btn-complete:hover { background: #4caf50; transform: translateY(-1px); }
.btn-view { background: #f0f0f0; color: #666; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s;}
.btn-view:hover { background: #f0f0f0; transform: translateY(-1px); }
</style>
<div class="my-dash">
@ -82,7 +111,7 @@
<div style="font-weight:600; font-size:14px; color: #333;"><?= $u['company_name'] ?></div>
<div style="font-size:11px; color:#999; margin-top: 3px;"><?= strtoupper($u['activity_type'] ?? 'EMAIL') ?> - <?= date('d Mar Y', strtotime($u['scheduled_date'])) ?></div>
</div>
<button class="badge-done"> Mark as completed</button>
<button class="badge-done" onclick="openComp(<?= $u['activity_id'] ?>,<?= $u['lead_id'] ?>)"> Mark as completed</button>
</div>
<?php endforeach; ?>
</div>
@ -105,20 +134,141 @@
</div>
</div>
<div class="modal" id="completeModal">
<div class="modal-content" style="max-width: 675px;">
<div class="modal-header">
<h3>Complete Activity</h3>
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('completeModal')">&times;</button>
</div>
<form class="parsley-examples" id="completeForm" enctype="multipart/form-data">
<hr class="my-0">
<div class="modal-body p-4">
<input type="hidden" id="comp_id">
<input type="hidden" id="lead_id">
<div class="form-group">
<label class="form-label">Outcome / Notes <span class="text-danger">*</span></label>
<textarea id="comp_notes" class="search-input" style="width:100%; height:80px;" required placeholder="What happened? Any next steps?" rows="3" required></textarea>
</div>
</div>
<div class="modal-footer">
<div class="form-group text-right mb-0">
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('completeModal')">Cancel</button>
<button type="submit" class="btn-primary">Submit Outcome</button>
</div>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const params = new URLSearchParams(window.location.search);
const fy = params.get('fy');
if (fy) {
document.getElementById('financial_year').value = fy;
}
});
const API = '<?= base_url('sales') ?>';
function openModal(id) { document.getElementById(id).classList.add('active'); }
function onFinancialYearChange(select) {
const url = new URL(window.location.href);
url.searchParams.set('fy', select.value);
window.location.href = url.toString();
}
function openComp(id, lead_id) {
document.getElementById('completeForm').reset();
document.getElementById('comp_id').value = id;
document.getElementById('lead_id').value = lead_id;
openModal('completeModal');
}
document.getElementById('completeForm').onsubmit = async (e) => {
e.preventDefault();
// 1. Grab button variables first, DO NOT disable yet
const submitBtn = e.target.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.innerText;
// 2. Capture values into variables
const activityId = document.getElementById('comp_id').value;
const leadId = document.getElementById('lead_id').value;
// This creates a TRUE or FALSE boolean
let compNotes = document.getElementById('comp_notes').value.trim();
// --- VALIDATION CHECKS (Button is still normal here) ---
if (compNotes === '') {
toastr.warning('Completion notes is required');
return;
}
// --- ALL VALIDATION PASSED: Now disable button ---
submitBtn.disabled = true;
submitBtn.innerText = 'Submitting...';
try {
const payload = {
completion_notes: compNotes, // Use the already trimmed variable
create_followup: 'no',
};
// Mark current activity complete
const res = await fetch(`${API}/activities/${activityId}/complete`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
if (res.ok) {
toastr.success('Updated Successfully');
// Final Actions
closeModal('completeModal');
e.target.reset();
window.location.reload();
} else {
let err = await res.json();
if (res.status === 400) {
let errorMessages = "";
let seenMessages = [];
if (err.messages) {
Object.entries(err.messages).forEach(([field, message]) => {
if (!seenMessages.includes(message)) {
errorMessages += `• ${message}<br>`;
seenMessages.push(message);
}
});
toastr.error(errorMessages, 'Validation Error', { allowHtml: true });
} else {
toastr.warning(err.message || 'Validation failed', 'Warning');
}
} else {
toastr.error(err.message || 'Error completing activity');
}
}
} catch (err) {
toastr.error("A network error occurred.");
} finally {
// ALWAYS runs to reset button
submitBtn.disabled = false;
submitBtn.innerText = originalBtnText;
}
};
function closeModal(id) {
const modal = document.getElementById(id);
if (modal) {
modal.classList.remove('active'); // Hide the modal
const form = modal.querySelector('form'); // Find the form inside this specific modal
}
}
document.addEventListener('DOMContentLoaded', function () {
const params = new URLSearchParams(window.location.search);
const fy = params.get('fy');
if (fy) {
document.getElementById('financial_year').value = fy;
}
});
function onFinancialYearChange(select) {
const url = new URL(window.location.href);
url.searchParams.set('fy', select.value);
window.location.href = url.toString();
}
</script>

View File

@ -276,15 +276,17 @@
<div class="modal-body p-4">
<input type="hidden" id="act_lead_id">
<label class="form-label">Activity Type</label>
<label class="form-label">Activity Type <span class="text-danger">*</span></label>
<div class="activity-types" id="typeButtons">
<button type="button" class="activity-type-btn active" onclick="selectType('Call', this)">📞 Call</button>
<button type="button" class="activity-type-btn" onclick="selectType('Email', this)">✉️ Email</button>
<button type="button" class="activity-type-btn" onclick="selectType('Meeting', this)">📅 Meeting</button>
<button type="button" class="activity-type-btn" onclick="selectType('Visit', this)">🚗 Visit</button>
<button type="button" class="activity-type-btn" onclick="selectType('Demo', this)">🎬 Demo</button>
<button type="button" class="activity-type-btn" onclick="selectType('Share', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn" onclick="selectType('Todo', this)"> To Do</button>
<button type="button" class="activity-type-btn d_activity_type active" data-type="Call" onclick="selectType('Call', this)">📞 Call</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Email" onclick="selectType('Email', this)">✉️ Email</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Meeting" onclick="selectType('Meeting', this)">📅 Meeting</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Visit" onclick="selectType('Visit', this)">🚗 Visit</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Demo" onclick="selectType('Demo', this)">🎬 Demo</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Share" onclick="selectType('Share', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Todo" onclick="selectType('Todo', this)"> To Do</button>
</div>
<div class="form-group">
<label class="form-label">Notes <span class="text-danger">*</span></label>
@ -650,11 +652,11 @@ function closeModal(id) {
// Specific cleanup for your "Activity" logic
if (id === 'activityModal') {
selectedType = 'Call'; // Reset your global activity type variable
$('.activity-type-btn').removeClass('active'); // Remove 'active' from all activity buttons
$(`.activity-type-btn[data-type="${selectedType}"]`).addClass('active'); // Find the specific button for 'Call' and make it active
selectedType = 'Call';
$('#typeButtons .d_activity_type').removeClass('active');
$('#typeButtons .d_activity_type[data-type="Call"]').addClass('active'); // ✅
}
// Specific cleanup for "Complete" modal (hidden follow-up sections)
if (id === 'completeModal') {
const fUpSection = document.getElementById('f_up');
@ -666,7 +668,7 @@ function closeModal(id) {
// Specific action for leadDetailModal
if (id === 'leadDetailModal') {
switchTab('activity'); // Reset the tab back to 'activity'
document.getElementById('btn_add_opportunity').classList.style.display = 'none'; // Hide the button
document.getElementById('btn_add_opportunity').style.display = 'none';
}
}
}
@ -1294,11 +1296,15 @@ document.getElementById('activityForm').onsubmit = async (e) => {
toastr.warning('Notes is required');
return;
}
if (!selectedType) {
toastr.warning('Please select activity type');
const hasDefaultActiveType = document.querySelector('#typeButtons .d_activity_type.active');
if (!hasDefaultActiveType) {
toastr.warning('Please select an activity type.');
return;
}
if (!selectedType) {
toastr.warning('Please select an activity type.');
return;
}
if (!feFormat.trim()) {
toastr.warning('Scheduled date & time is required');
@ -1337,6 +1343,11 @@ document.getElementById('activityForm').onsubmit = async (e) => {
if (res.ok) {
toastr.success('Activity Created Successfully');
selectedType = 'Call'; // Reset your global activity type variable
// Remove 'active' from all activity buttons
$('.activity-type-btn').removeClass('active');
// Add 'active' to the specific button using backticks for the template literal
$(`.activity-type-btn[onclick*="'${selectedType}'"]`).addClass('active');
closeModal('activityModal');
viewDetail(document.getElementById('act_lead_id').value); // Make sure lead_id is passed correctly
} else {
@ -1382,8 +1393,13 @@ document.getElementById('completeForm').onsubmit = async (e) => {
let fAssigned = document.getElementById('f_assigned_to').value.trim();
// FIX: Check your global variable, not the undefined DOM element
const hasActiveType = document.querySelector('#f_typeButtons .f_activity_type.active');
if (!hasActiveType) {
toastr.warning('Please select the next activity type.');
return;
}
if (!selectedFollowUpActivityType) {
toastr.warning('Please Pickup Next Activity Type');
toastr.warning('Please select the next activity type.');
return;
}
// if (!selectedFollowUpActivityType) {
@ -1564,7 +1580,9 @@ document.getElementById('do_follow').addEventListener('change', function () {
}
});
document.getElementById('btnSaveContact').onclick = async (e) => {
const btnSaveContact = document.getElementById('btnSaveContact');
if (btnSaveContact) {
btnSaveContact.onclick = async (e) => {
// Use .value instead of .val()
let name = document.getElementById('contact_name').value.trim();
let mobile = document.getElementById('contact_mobile').value.trim();
@ -1661,8 +1679,11 @@ document.getElementById('btnSaveContact').onclick = async (e) => {
toastr.error('An error occurred');
}
};
document.getElementById('savedContactsContainer').onclick = async (e) => {
}
const savedContactsContainer = document.getElementById('savedContactsContainer');
if (savedContactsContainer) {
savedContactsContainer.onclick = async (e) => {
// Check if the clicked element is a Remove button
if (e.target.classList.contains('btnRemoveContact')) {
const btn = e.target;
@ -1693,6 +1714,7 @@ document.getElementById('savedContactsContainer').onclick = async (e) => {
}
}
};
}
async function openEditLeadModal(id) {
// 1. Reset UI State

View File

@ -1,13 +0,0 @@
{
"type": "service_account",
"project_id": "gdrive-demo-394007",
"private_key_id": "5b1d856b0c5b13e52b5210d381ce7ae02204f666",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9tqkwe8XEuvDJ\ngDAKHn7FzFgkmor9sEZPkjofVJ1dK0RpD1mMVw38BzzMsEo8Y8aojNKj9FcgJPI+\nMiSwvoHDVGxPuyz2Q7o8BS7WdM83Sn69CBGn+0s+YjsyQ5pQBeu1YZ3erjckyA1M\nX0a0qXSFRm7dN0pwDIk0SP9/pl5iAKEtakuXn/Q+lSIpHz6OUgQy8bCjn91poMtF\nhL1YR/7k8ZttjQiFSCTzQ6e3IJmVbqY9UZ2hc4zVicLVSLM2x17M1CCVrsXoceOj\nQoYEeCqP2qJjGA29CB66xpXetuOFcll/ZHiNBhSekOBbJIKFG8WfTKaFn4hu2HX6\nzbJLse5FAgMBAAECggEAXK8qxWcS3eQ+0xLvZWI0qUoGHgvqr7o4/5L/FmNuZiBH\nUdSP+UJmsKSQjafq/Mn6OkpidntfPXMPbldtGXRZTSanq+RUORQpnj0h/uAehHK+\nrHeOuLTKs/Wl2g6xCzt5RqokSLBwfGXIKXG6x3SqWppoe2cR1OArAAJR4PlUzyeM\nP0HkZcVMDrXkshpbi/7yk1Yol5CTJjUrXT4cH2eFSih+eu5UxI/uEdxu86XnaB+V\nBDS94nSQffaMem3YLRSQpPWMHJts3NM2eoxpVy3NqbyHH0Jzr47T5+pdnk5AZX8v\nMu7L0FYgUmGli8W9/jV43lUi9z147EpNC1ugySICGQKBgQDeT3i9mBO1HQB+3y05\n4ao9YKWtR7RHTIiMBkekRs58xxWIq75+bsJtLMy0GsHOJHbpdfDU2AtcasYXVm/L\nc960AR7Qm0mdhQi5CG7XfFTvkc+RhsFoCAYOnbdInr1D+s7NsyOgdYvzh9HYIiJ/\nm8MzeGQia/4tlO+UNq7UK0sfowKBgQDadpe0OCynXEiziSY/aBT9mrjplXJSJUeR\nX5/pUrBV++mYt+LJU0Q+4op0Qf+PUJwp72O1v3T9h4ox2BcrYUMI17dJZ5HG46BG\n6Sjh+mZzLCTN9L6AVgRzK/5CUpsL+oPpClzGQK+1uJ+YKZD086YBuQi2JiG6uBxk\n7VLzATZ49wKBgQC3ZHgGb95SGoq+Hv4AMdluqLwEJpLh/pDmcofHTWIqLVHmXUfY\npSZfSgXUzf3zQMGX9mOmMlOs+ahQuE2hWQTvGb2B+ZjRCV4Yxowp17d5qp/BPZlv\naK8Wf6Ujk1AvNEhGCPHq/Q1m6TSDSCWNf8GYREjW3J/immrJqhKvlMd0YQKBgQCM\nf2CpQsdVCwCmljnG5YU6ZFsvvjE7q0YPtFP/lnJZmh1tXjW4DJkDaGZqxlc5MDp+\nrbqOlIcE1jqGO9cKyw51jWYPC1CxfIsDj8f/LS7eOzGgUxqBJtDN0SlANigI2CAl\nq8hmqAtY71eUYIcdQeUtjnaPzo46q1V3gzmplsoVmQKBgDCzaBbFY1bKW+xPPC98\nutYS7cGYtTq241zafSXx/qmpZB2QwFsR3rmZ9msCEBb3TY4Ew+hUi7+SP3ycSU1s\nkwIO/9DaZhFBRA9jbwbu4zdB2Niamfo79epqJ8vhJ6TA2d5xRSGbHWkGymWV/e0p\na74mh5hQ0lqI4MSeArFrJwwF\n-----END PRIVATE KEY-----\n",
"client_email": "gsheet@gdrive-demo-394007.iam.gserviceaccount.com",
"client_id": "108808196910972902964",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/gsheet%40gdrive-demo-394007.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@ -1,13 +0,0 @@
{
"type": "service_account",
"project_id": "nhance-app-4fedb",
"private_key_id": "e764472bb4a0c9226d172f7aebdd2eb785c5fca8",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCzyKk6yQqLZHnL\n8+qRwMHtIhV3kR6bYuCol7LTU/aRyTZpFafewJ/6y5bMvvHaRSaohuuTjDhp5H+w\nf7DAiWO06C/YdgZ/tv3dPM8FnkOEnwxUXVgJskK+srJ4g6zsYkT+0DNI0qMJRW5m\n0jv+HKPJ0axCAPcUZUVtraV/HFHF7pJp4oWMFD5X7Jhv7kbW3TetJofSN3SvRvr8\nyfCiVGzqgF0Gx0Tgd25GTMOuRXTGoTlQeDHFMm19g8YY1de0Ol9WqFey6E8e6a/h\nw8KHGdywgsI+q5yF0EIUdxKc6U9Bea25k0YnKHSCsJjJTVlKW+xlxR6wA1yuBb66\n2dAhEzldAgMBAAECggEACEssKpb5dadVPdt+PJ2r58CueD1JfRL8i/p98hhjY9bo\niJEXSFDfI6aYFp22ZYKfWNK3wMimr5SuPIB5F6C4gn3b+y95ERPQtg/AkyDgB39M\nBjZNW4EnabAWFWlc2FJknjnd+JcmLnhoy+h/gpnKoOkbYISDFKQWJfJtRvLE/1on\nhKvnhEkO++93HZp6KPYA21BQIWt6q6gYjjjwxQIHE0Vg30bZC8ciNVaVlmOnpoKW\nlsQ3+TL+b6vDsSxez4hSqWlua7wWL6JgKvyGw6FGiDLGjvTEvVQ0cvfykCDmO5td\nWvVihTMOMXEMx+iVAUOXJLrSPOFGXxSHL9jilqCn0QKBgQDu4P64sE5T15rA+83B\neRMyQUfLru/oxyQ7pAbgRk4nwQj1mXcdD3/RpHF6jg0rt0msRjkzkqyWiJN0qsZ9\nlQY7h0I8pu8dxFhbd7LLcPaNV+OpNAokG06dBK8qhuMcZBlRfvJ3ye5L/Dk0Z7A4\nPw6/OxySns+kzfxtPdpJTNeqFQKBgQDAq2BXm+MsjqjZm2Z1LFMgZcjRLUu4ylMC\n9W3jqd35dBypcafAJ3TqhPLsx+HyWuC+GLMWohx3XGU7oZabGg1zuWAS8A4hnogZ\nk47OT3OYHDaT65HFmSAGJy1UFVvESUdiVzPqO3Xn9dYvRIw6By2b09w+kD1oxmE0\nvAh5UFAMKQKBgHg06plfxxqzzWE9lS290qYgaZOaxYla5OXKRdeIKX4hynNktab/\nDLAfUyd74i7UfhNeBxznu0fJFILKCTZazpcYGoHQ81UEX/4vPt7XSoqX5q1XzZ6b\nyzSCje8Vj6XSzVbQTg1vpSXBl7vCTdAQE0ix40/48L6bFWKXlIf8Ti59AoGAd8UR\nKuT5H6W/SSbVVlmrgyRC5eWmgMUlPV2cZj4egwevGZZRlZ3xjCgBazgGOUZNF177\nVUKJN5n0RFF68ggL/LhsBNm9ryCBsoSL7axuS0yekO2LvK4nvit0fiSY2zhCa9uR\npjY4YW3tK40NLrVvoMLe0vWPdyZ7HvEUw3UtjaECgYAMoonkNACD7EyWesAQzjpD\ntAL5ByXtFkMqMMr8ELLoDU2D5+qzb6gu0+GuYzGvxtYQHz2L+cP7yonn8IRBrAO1\njZCi2Mn1qOYecSGBnv6rotcagMALXwExkymjjVxPLiOae7A01sxSFDpqtRBww2JA\ngFsHNMpkQ1KmZu7LW0q1fg==\n-----END PRIVATE KEY-----\n",
"client_email": "only-drive-access@nhance-app-4fedb.iam.gserviceaccount.com",
"client_id": "116214376249781337850",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/only-drive-access%40nhance-app-4fedb.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "nhance-ee8d1",
"private_key_id": "e3c5269b1ec7d22e7a297f5273edf3eda6010c0a",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC4+7yRGBAow7GR\nrcBSwXVkBd7U001YC8nZ6GmbfhZSktn4pd4C7r/VGmJMcHdo4CVkt+omY9hEWy3Y\n2k7xvwTBJOxo+A4GENduzUpRucVD5UNs4CKajVmI97FV+2wgeLYR9lLswrSXkQvo\nCRDi/w0EwQ7ZdfPyYfuUUBSdAytiwtGHEJaHBQ4qanNvstFu6FJcCkaq+Sbn6Ool\nHN7lgS/V5NhuBakNOm8EBBaN8Xp13afjHvyFEHalnY8xpNNXNVlBljYIPzYFxXFN\nFL/Iz7XX8dBD0tVwfQGDpD5OCPh8FeIg39dgbRxU3Q+Awv3CX9eoEW1j3bPYo7Lw\nmh8K3fohAgMBAAECggEAEQXPCfxQmSrwOPg0zEjHDTyb3DKGGdw0p772YChy7gfB\nudrfR2s1SWtWo8y7tSTfVaZPcvEmnzYgZ3LB56EAIBs0pP1viO/20aVgBlpI99Q6\ntEobiEs9DViOpkHVs71EzMb1Emcugde6sUOLg5NPkHu8BDErERSJBFJ3bliy0heN\nwAwYKXCB3ro3ryUE5+pRS6ibt8vLEBbk3T3bEW8A1wfWnUo0V+Wf5KXsCO8BvD8Z\nE/i0JgpAkZksWs+keBSLbdWBONNV1GrbxezoZXA8aPPlcaD0bZdLDlszXguq8DGO\nbjr0SI7Hx78/jAQhiFkdNMkLvZMIoU7VVnQSibsBKwKBgQD04PMi6CO6kpKG/ZDw\nR6tuAU8DkUgSjvOquUeGkguyfhK/G5fg3w8yCpLAPoh8ZEGL4zZy+ZoAsHWLv44O\nmXjTOKut5o1puswM8vPobDAinddCWs6U6VWKkkRw27AuyKcnLCpvnRBg/Iw+lSTQ\nzG0KCd8Zn2+a0Cjo9hlF6F2pAwKBgQDBYmvV+cADTRjhostukDK71aKsRhMu1OH4\nRK2uX3f9rde1aSnIcp0PDzn24V7I9skT6zhOCPTF6SEFH35eO3z7kOA26hokOMb2\nz7Bh+EDnx5zQmWwcj0yNwbCeTl7gKWz2o3AuMc8zK6I2+buYJJnjoazUyqQtKhqz\nnPs3woY9CwKBgDHjwqVR9jWEtyWZc3YApAR9b8OiTbS6OxqFNPVNu+RZmygkTwUZ\nbNcdIFjaZKQzKMd/OxChmaaaTNhz5lVDH0KpQRDk79qim//nX5nysLvcvIZgScY8\n45ifxCHaIELnzmZEsUCcF0IrMcduS1nezDhHWpS1zt8TmcIcoXmEpdBRAoGAEtKs\nomAz415evJ+m43Ufqw7JTbFobpeEAzFUInPibwu7wkmhKoSVawDVaIVZP4Bd5BVy\nHo5anOTrNN9y4mMx8B6S5GV28+2e2CkxBuguESFpzxgP4NvF3MpskYwZSgJeO8d+\nxNBOVbG6kVVPgCiX3gM/mlq7DjZZ8P+nqC7D+C8CgYAMeN/ByvGaYSN5bVVntOcT\noDiMWfBBToc9irhNSZsCwghXmNugPKguDbRTi7n/Iqf/HljwUx0HBtXkIgZvDDiW\nQrGSCozd9g2WFx/2OSJv4GU+eUM1GFr0mLFDaAUNzvjwHTkG+2STof8VowaGRx68\n7Z50l5hXKv3i0n5ykhPWUg==\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-mcdfe@nhance-ee8d1.iam.gserviceaccount.com",
"client_id": "105441165052433248287",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-mcdfe%40nhance-ee8d1.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}