Skip to content

Instantly share code, notes, and snippets.

@mdmunir
Last active April 25, 2025 09:00
Show Gist options
  • Save mdmunir/b3167aba91908b6d1ae8a5d30a1b90aa to your computer and use it in GitHub Desktop.
Save mdmunir/b3167aba91908b6d1ae8a5d30a1b90aa to your computer and use it in GitHub Desktop.
Export clickup time entry ke excel
<?php
date_default_timezone_set('Asia/Jakarta');
$config = [
'team_id' => 'XXX',
'token' => 'XXX',
'columns' => [
['field' => 'start', 'label' => 'Tanggal', 'width' => 100, 'formatter' => function ($val) {
return date('Y-m-d', $val / 1000);
}],
['field' => 'start', 'label' => 'Jam', 'width' => 120, 'formatter' => function ($val, $row) {
return date('H:i', $val / 1000). ' - ' . date('H:i', $row['end'] / 1000);
}],
// ['field' => 'end', 'label' => 'Jam Selesai', 'width' => 100, 'formatter' => function ($val) {
// return date('H:i', $val / 1000);
// }],
['field' => 'user.username', 'label' => 'Nama', 'width' => 200,],
['field' => 'task.name', 'label' => 'Task', 'width' => 350, 'formatter' => function ($val, $row) {
$url = $row['task_url'];
return "<a href='{$url}' target='_blank'>$val</>";
}],
['field' => 'description', 'label' => 'Keterangan', 'width' => 450],
['field' => 'duration', 'label' => 'Durasi', 'width' => 100, 'formatter' => function ($val) {
$total = (int) ($val / 60000);
$menit = (int) ($total % 60);
$jam = (int) ($total / 60);
return sprintf('%d:%02d', $jam, $menit);
}],
]
];
class App
{
public $token;
public $team_id;
public $columns = [];
protected $defaults = [];
protected $errorHandler;
private $_memoryReserve;
public function __construct($config = [])
{
$this->registerErrorHandler();
foreach ($config as $key => $value) {
$this->$key = $value;
}
$this->defaults['start'] = date('Y-m-01');
$this->defaults['end'] = date('Y-m-d');
}
protected function apiCall($query = [])
{
$url = "https://api.clickup.com/api/v2/team/{$this->team_id}/time_entries";
$headers = [
"Authorization: {$this->token}",
'Accept: application/json',
];
if ($query) {
$query = http_build_query($query);
$url = $url . (strpos($url, '?') === false ? '?' : '&') . $query;
}
$options = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HEADER => true,
CURLOPT_ENCODING => 'utf-8',
CURLOPT_MAXREDIRS => 10,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_TIMEOUT => 1800,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => $headers,
];
$handle = curl_init();
curl_setopt_array($handle, $options);
$response = curl_exec($handle);
$errno = curl_errno($handle);
$errmsg = curl_error($handle);
$info = curl_getinfo($handle);
curl_close($handle);
// Ambil response header dan format ulang untuk mendapatkan kode status HTTP
$rawheaders = preg_split('/\r\n|\r|\n/', trim(substr($response, 0, $info['header_size'])));
preg_match('/^(HTTP\/[\d\.]+) (\d{3}) (.+?)$/', array_shift($rawheaders), $httpstatus);
$outputHeaders = [];
$format = null;
foreach ($rawheaders as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
$parts = explode(':', $line);
if (!empty($parts[1])) {
$outputHeaders[$parts[0]] = $parts[1];
if (strtolower($parts[0]) == 'content-type') {
$format = $parts[1];
}
}
}
// Ambil response body
$body = substr($response, $info['header_size']);
$content = $body;
if ($format && stripos($format, 'json') !== false) {
$body = json_decode($body, true);
} else {
$_data = json_decode($body, true);
if (json_last_error() == JSON_ERROR_NONE) {
$body = $_data;
}
}
// Siapkan output
$isOke = (int) $errno == 0 && ((int) $httpstatus[2] >= 200 && (int) $httpstatus[2] < 400);
$return = [
'isOk' => $isOke,
'isError' => !$isOke,
'isErrorRequest' => (int) $errno > 0,
'isErrorResponse' => count($httpstatus) && (int) $httpstatus[2] >= 400,
'error' => array(
'code' => (int) $errno,
'strcode' => $errno ? curl_strerror($errno) : null,
'message' => $errno ? $errmsg : null,
),
'status' => array(
'code' => count($httpstatus) ? (int) $httpstatus[2] : 0,
'message' => count($httpstatus) ? $httpstatus[3] : null,
'protocol' => count($httpstatus) ? $httpstatus[1] : null,
),
'headers' => $outputHeaders,
'reqHeaders' => implode("\n", array_values($headers)),
'data' => $body,
'content' => $content,
];
return $return;
}
public function getInput($attr, $input = false)
{
$default = static::getArrayVal($this->defaults, $attr);
$value = empty($_GET[$attr]) ? $default : $_GET[$attr];
$value2 = ($value == date('Y-m-d', strtotime($value))) ? $value : $default;
return $input ? "<input id='$attr' name='$attr' type='date' value='$value2' style='max-midth:350px;'>" : $value2;
}
protected function getData()
{
$query = [
'start_date' => strtotime($this->getInput('start')) * 1000,
'end_date' => strtotime($this->getInput('end') . ' 23:59:59') * 1000,
];
$res = $this->apiCall($query);
if ($res['isOk']) {
return $res['data']['data'];
}
return [];
}
public function renderHeader()
{
$tr = [];
foreach ($this->columns as $column) {
$width = empty($column['width']) ? '' : "width=\"{$column['width']}\"";
$tr[] = "<th $width>{$column['label']}</th>";
}
return '<tr>' . implode("\n", $tr) . '</tr>';
}
public function renderData()
{
$lines = [];
foreach ($this->getData() as $row) {
$tr = [];
foreach ($this->columns as $column) {
$value = static::getArrayVal($row, $column['field']);
if (isset($column['formatter'])) {
$value = call_user_func($column['formatter'], $value, $row);
}
$tr[] = "<td>{$value}</td>";
}
$lines[] = '<tr>' . implode("\n", $tr) . '</tr>';
}
return implode("\n", $lines);
}
public static function getArrayVal($array, $key, $default = null)
{
if (array_key_exists($key, $array)) {
return $array[$key];
}
if ($key && ($pos = strrpos($key, '.')) !== false) {
$array = static::getArrayVal($array, substr($key, 0, $pos), $default);
$key = substr($key, $pos + 1);
}
if (is_array($array) && array_key_exists($key, $array)) {
return $array[$key];
}
return $default;
}
public function registerErrorHandler()
{
if (!defined('NO_ERROR_HANDLER') || !NO_ERROR_HANDLER) {
ini_set('display_errors', false);
set_exception_handler([$this, 'handleException']);
set_error_handler([$this, 'handleError']);
$this->_memoryReserve = str_repeat('x', 262144);
register_shutdown_function([$this, 'handleFatalError']);
}
}
/**
*
* @param \Exception $exception
*/
public function handleException($exception)
{
restore_error_handler();
restore_exception_handler();
$str = get_class($exception) . ': "' . $exception->getMessage() . '" at ' . $exception->getFile() .
':' . $exception->getLine() . "\n" . $exception->getTraceAsString();
if (ob_get_level()) {
ob_end_clean();
}
if (PHP_SAPI === 'cli') {
echo $str;
} else {
$http = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1';
header("$http 500");
echo "<pre>\n$str\n</pre>";
die();
}
}
public function handleError($code, $message, $file, $line)
{
if (error_reporting() & $code) {
$exception = new \ErrorException($message, $code, $code, $file, $line);
// in case error appeared in __toString method we can't throw any exception
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
array_shift($trace);
foreach ($trace as $frame) {
if ($frame['function'] === '__toString') {
$this->handleException($exception);
if (defined('HHVM_VERSION')) {
flush();
}
exit(1);
}
}
throw $exception;
}
return false;
//exit(1);
}
public function handleFatalError()
{
unset($this->_memoryReserve);
$error = error_get_last();
if (isset($error['type']) && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR,
E_COMPILE_WARNING])) {
$this->handleError($error['type'], $error['message'], $error['file'], $error['line']);
}
}
}
$app = new App($config);
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Clickup Timesheet</title>
<style>
table.border {
border-collapse: collapse;
width: 100%;
}
table.border th, table.border td {
border: 1px solid black;
padding: 8px;
text-align: left;
}
table.border th {
background-color: #f2f2f2;
}
</style>
</head>
<body>
<div class="container">
<h1>Clickup Timesheet</h1>
<form method="GET">
<table>
<tbody>
<tr style="text-align: left;">
<th width="100">Start Date</th>
<td><?= $app->getInput('start', true) ?></td>
</tr>
<tr>
<th >End Date</th>
<td><?= $app->getInput('end', true) ?></td>
</tr>
<tr>
<th ></th>
<td><button type="submit">Submit</button></td>
</tr>
</tbody>
</table>
</form>
<button type="button" id="btn-save">Save</button>
<table class="border" id="data">
<thead>
<?= $app->renderHeader() ?>
</thead>
<tbody>
<?= $app->renderData() ?>
</tbody>
</table>
</div>
</body>
<script>
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("btn-save").addEventListener('click', function () {
var html = document.getElementById("data").outerHTML;
const blob = new Blob([html], {type: "application/vnd.ms-excel"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "timesheet_<?= $app->getInput('end') ?>.xls";
a.click();
URL.revokeObjectURL(url);
});
});
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment