NASER add task functions

This commit is contained in:
mobinesaghi
2026-04-23 14:49:46 +03:30
parent 2419808175
commit 2bcf9f1b7e
47 changed files with 5793 additions and 1029 deletions
+6
View File
@@ -0,0 +1,6 @@
import * as Lexxy from "@37signals/lexxy"
// Rich Text Laravel uses a custom attachment tag name, so we must configure it here...
Lexxy.configure({ global: { attachmentTagName: "rich-text-attachment" } })
export default Lexxy;
+5
View File
@@ -0,0 +1,5 @@
import Trix from 'trix';
window.Trix = Trix;
export default Trix;
+16 -12
View File
@@ -464,19 +464,23 @@
closeModal()
}
})
document.addEventListener('DOMContentLoaded', function () {
function autoDismissFlashMessage() {
const errorBox = document.getElementById('error-box')
if (errorBox) {
// Fade out the error box after 3 seconds
setTimeout(() => {
errorBox.classList.add('opacity-0') // Add fade-out class
// Remove the error box after the fade-out transition ends
errorBox.addEventListener('transitionend', () => {
errorBox.remove()
})
}, 3000) // Duration before starting fade out
}
})
if (!errorBox) return
setTimeout(() => {
errorBox.classList.add('opacity-0')
errorBox.addEventListener('transitionend', () => {
errorBox.remove()
})
}, 5000)
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', autoDismissFlashMessage)
} else {
autoDismissFlashMessage()
}
</script>
@yield('script')
+364 -333
View File
@@ -38,7 +38,7 @@ if (!empty($user->tasks)) {
});
}
//dd(count($break = $user->attendanceBreaks));
if (count($user->attendanceBreaks) > 0){
if (count($user->attendanceBreaks) > 0) {
$break = $user->attendanceBreaks[0];
if ($break->break_end == null) {
$now = Carbon::now()->format('Y-m-d H:i:s');
@@ -68,7 +68,7 @@ if (count($user->attendanceBreaks) > 0){
{{-- </button>--}}
@if (!isset($_COOKIE['token-0']))
<form id="attendanceForm" class="form items-center" action="{{ url('attendance/enter') }}" method="post"
<form id="attendanceForm" class="form items-center" {{--action="{{ url('attendance/enter') }}" method="post"--}}
style="text-align: left">
@csrf
<p class="title text-center font-bold text-lg mb-2">سامانه گذارش کار</p>
@@ -89,6 +89,12 @@ if (count($user->attendanceBreaks) > 0){
style="width: 100%">
ورود/خروج
</button>
sghl<button id="submitBtn" disabled type="button"
class="bg-gray-400 text-white rounded cursor-not-allowed rounded-xl py-2 px-4 max-w-60 self-center"
style="width: 100%">
ورود/خروج
</button>
<div id="output" class="text-red-500 text-sm mt-2 text-center"></div>
<div class="flex justify-flex max-w-[240px]" style="align-items: center;">
<button type="button"
@@ -111,13 +117,13 @@ if (count($user->attendanceBreaks) > 0){
</button>
{{--<p>{{Carbon::now()}}</p>--}}
</form>
{{-- @if(session('open_task_modal'))--}}
{{-- <script>--}}
{{-- window.onload = () => {--}}
{{-- openTaskModal();--}}
{{-- };--}}
{{-- </script>--}}
{{-- @endif--}}
{{-- @if(session('open_task_modal'))--}}
{{-- <script>--}}
{{-- window.onload = () => {--}}
{{-- openTaskModal();--}}
{{-- };--}}
{{-- </script>--}}
{{-- @endif--}}
@else
@@ -253,19 +259,44 @@ if (count($user->attendanceBreaks) > 0){
attestation: "direct"
};
submitBtn.addEventListener('click', async () => {
// ابتدا اعتبارسنجی اثر انگشت توسط WebAuthn
if (!window.PublicKeyCredential) {
alert('مرورگر شما از احراز هویت بیومتریک پشتیبانی نمی‌کند.');
return;
}
// یک چالش تصادفی بسازید
const challenge = new Uint8Array(32);
window.crypto.getRandomValues(challenge);
// گزینه‌های احراز هویت (Authentication)
const getOptions = {
publicKey: {
challenge: challenge,
timeout: 60000,
rpId: window.location.hostname, // اختیاری؛ برای لوکال‌هاست خالی بگذارید
userVerification: "required",
allowCredentials: [] // خالی بگذارید تا هر کلید موجود نمایش داده شود
}
};
try {
const credential = await navigator.credentials.create({
publicKey: createOptions
});
// به جای create از get استفاده کنید
const assertion = await navigator.credentials.get(getOptions);
console.log('احراز هویت موفق:', assertion);
// اینجا فرم را سابمیت کنید یا هر عملیات دیگر
attendanceForm.submit();
} catch (error) {
output.textContent = error.name === 'NotAllowedError' ? 'احراز هویت لغو شد یا دسترسی مجاز نبود.' : 'عملیات ناموفق بود. لطفاً دوباره امتحان کنید.';
const message = error.name === 'NotAllowedError'
? 'احراز هویت لغو شد یا دسترسی مجاز نبود.'
: 'عملیات ناموفق بود. لطفاً دوباره امتحان کنید.';
const output = document.getElementById('output');
if (output) {
output.textContent = message;
} else {
alert(message);
}
console.error(error);
}
});
</script>
@@ -415,7 +446,6 @@ if (count($user->attendanceBreaks) > 0){
});
</script>
@endif
</div>
@@ -447,7 +477,7 @@ if (count($user->attendanceBreaks) > 0){
class=" example1 form-control border p-2 rounded date-picker-input" value=""
placeholder="تاریخ"
readonly
>
>
</div>
@@ -885,329 +915,330 @@ if (count($user->attendanceBreaks) > 0){
method: "POST",
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
})
.then(res => {
if (res.ok) {
window.location.reload();
} else {
window.location.reload();
// breakBtn.textContent = 'خطا';
// breakBtn.disabled = false;
// console.error('Server error on break action:', res.statusText);
}
})
.catch(err => {
window.location.reload();
// breakBtn.textContent = 'خطای شبکه';
// breakBtn.disabled = false;
// console.error('Network error:', err);
});
});
}
{{--let breakStarted = false;--}}
{{--let breakStartTime = null;--}}
{{--let breakTimer = null;--}}
{{--const breakBtn = document.getElementById('breakAttendance');--}}
{{--function formatLocalDatetime(date) {--}}
{{-- const year = date.getFullYear();--}}
{{-- const month = String(date.getMonth() + 1).padStart(2, '0');--}}
{{-- const day = String(date.getDate()).padStart(2, '0');--}}
{{-- const hours = String(date.getHours()).padStart(2, '0');--}}
{{-- const minutes = String(date.getMinutes()).padStart(2, '0');--}}
{{-- const seconds = String(date.getSeconds()).padStart(2, '0');--}}
{{-- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;--}}
{{--}--}}
{{--function formatDuration(totalSeconds) {--}}
{{-- const hours = Math.floor(totalSeconds / 3600);--}}
{{-- const minutes = Math.floor((totalSeconds % 3600) / 60);--}}
{{-- const h = String(hours);--}}
{{-- const i = String(minutes).padStart(2, '0');--}}
{{-- return `${h}:${i}`;--}}
{{--}--}}
{{--breakBtn.addEventListener('click', () => {--}}
{{-- if (!breakStarted) {--}}
{{-- // شروع استراحت 🚀--}}
{{-- breakStarted = true;--}}
{{-- breakStartTime = new Date();--}}
{{-- // تغییر ظاهر دکمه و شروع تایمر (همچنان M:S نمایش داده می شود)--}}
{{-- breakBtn.classList.remove('bg-gray-500', 'hover:bg-gray-600');--}}
{{-- breakBtn.classList.add('bg-red-500', 'hover:bg-red-600');--}}
{{-- breakBtn.textContent = '00:00';--}}
{{-- let seconds = 0;--}}
{{-- breakTimer = setInterval(() => {--}}
{{-- seconds++;--}}
{{-- // اینجا همچنان به صورت دقیقه:ثانیه نمایش داده می شود--}}
{{-- const m = String(Math.floor(seconds / 60)).padStart(2, '0');--}}
{{-- const s = String(seconds % 60).padStart(2, '0');--}}
{{-- breakBtn.textContent = ` ${m}:${s}`;--}}
{{-- }, 1000);--}}
{{-- //send start time--}}
{{-- let formData = new FormData();--}}
{{-- formData.append('start_time', formatLocalDatetime(breakStartTime));--}}
{{-- formData.append('_token', '{{ csrf_token() }}');--}}
{{-- fetch("{{ url('attendance/break') }}", {--}}
{{-- method: "POST",--}}
{{-- body: formData--}}
{{-- })--}}
{{-- .then(res => res.json())--}}
{{-- .then(data => console.log('✅ Break Started Logged:', data))--}}
{{-- .catch(err => console.error('💥 Error starting break:', err));--}}
{{-- } else {--}}
{{-- // پایان استراحت 🛑--}}
{{-- breakStarted = false;--}}
{{-- const breakEndTime = new Date();--}}
{{-- clearInterval(breakTimer);--}}
{{-- // 1. محاسبه مدت زمان استراحت (بر حسب ثانیه)--}}
{{-- const durationSec = Math.floor((breakEndTime - breakStartTime) / 1000);--}}
{{-- // 2. تبدیل زمان های شروع و پایان به فرمت رشته ای--}}
{{-- const breakStartTime_str = formatLocalDatetime(breakStartTime);--}}
{{-- const breakEndTime_str = formatLocalDatetime(breakEndTime);--}}
{{-- // 3. فرمت کردن مدت زمان به H:i (فقط برای نمایش در کنسول/هشدار)--}}
{{-- const duration_formatted_h_i = formatDuration(durationSec);--}}
{{-- // 4. نمایش دکمه به حالت اولیه--}}
{{-- breakBtn.classList.remove('bg-red-500', 'hover:bg-red-600');--}}
{{-- // breakBtn.classList.add('bg-gray-500', 'hover:bg-gray-600');--}}
{{-- breakBtn.textContent = 'استراحت';--}}
{{-- // 5. ارسال داده‌ها به سرور--}}
{{-- let formData = new FormData();--}}
{{-- formData.append('start_time', breakStartTime_str);--}}
{{-- formData.append('end_time', breakEndTime_str);--}}
{{-- formData.append('duration_seconds', durationSec);--}}
{{-- formData.append('_token', '{{ csrf_token() }}');--}}
{{-- fetch("{{ url('attendance/break') }}", {--}}
{{-- method: "POST",--}}
{{-- body: formData--}}
{{-- })--}}
{{-- .then(res => res.json())--}}
{{-- .then(data => console.log('✅ Break saved:', data))--}}
{{-- .catch(err => console.error('💥 Error:', err));--}}
{{-- }--}}
{{--});--}}
{{-- ATTENDANCE MODAL--}}
function clearTime(fieldId) {
document.getElementById(fieldId).value = '';
}
function closeModal() {
document.getElementById('modal-container').classList.add('hidden');
}
document.getElementById('modal-container').addEventListener('click', (e) => {
if (e.target === document.getElementById('modal-container')) {
closeModal();
'Accept': 'application/json'
}
});
function confirmSubmit() {
return confirm("آیا تغییرات اعمال شوند؟");
}
function openModal(title, id, enteredAt, exitedAt) {
const isNewEntry = id === 'new';
const modalTitleElement = document.getElementById('modal-title');
// ✅ بهترین روش: انتخاب مستقیم فیلد مخفی با ID آن
const hiddenIdInput = document.getElementById('edit_id');
// const hiddenIdInput = document.getElementById('delete_id');
// انتخاب فیلد ID برای فرم حذف
const deleteIdInput = document.getElementById('delete_id');
const submitButton = document.querySelector('#modal-content button[type="submit"]');
const hiddenDateInput = document.querySelector('input[name="date"]');
// مقداردهی به فرم حذف
if (deleteIdInput) {
deleteIdInput.value = id;
}
const newDateLabel = document.getElementById('new-date-label');
const newDateGroup = document.getElementById('new-date-group');
const displayDateInput = document.getElementById('display_date');
const panelLink = document.getElementById('panel-access-link');
panelLink.href = `/go_down/${id}`;
document.getElementById('modal-container').classList.remove('hidden');
const startTimeInput = document.querySelector('input[name="starttime"]');
const endTimeInput = document.querySelector('input[name="endtime"]');
// تنظیم ساعت‌ها
if (enteredAt && !isNaN(new Date(enteredAt).getTime())) {
startTimeInput.value = new Date(enteredAt).toLocaleTimeString('en-GB', {
hour: '2-digit',
minute: '2-digit'
});
} else {
startTimeInput.value = '09:00';
}
if (exitedAt && !isNaN(new Date(exitedAt).getTime())) {
endTimeInput.value = new Date(exitedAt).toLocaleTimeString('en-GB', {
hour: '2-digit',
minute: '2-digit'
});
}
// else {
// endTimeInput.value = '17:30';
// }
if (isNewEntry) {
// حالت ثبت جدید
modalTitleElement.innerText = 'ثبت روز کاری جدید';
newDateLabel.style.display = 'block';
newDateGroup.style.display = 'block';
displayDateInput.value = '';
hiddenDateInput.value = '';
// ✅ اصلاح: مقداردهی 'new' به فیلد ID
if (hiddenIdInput) hiddenIdInput.value = 'new';
submitButton.onclick = function () {
if (hiddenDateInput.value === '') {
alert("لطفاً یک تاریخ را از تقویم انتخاب کنید.");
return false;
}
return confirmSubmit();
};
} else {
// حالت ویرایش
modalTitleElement.innerText = 'تغییر ساعت کاری';
newDateLabel.style.display = 'none';
newDateGroup.style.display = 'none';
// ✅ اصلاح: استفاده از متغیر 'id' که از جدول فرستاده شده است
if (hiddenIdInput) hiddenIdInput.value = id;
if (hiddenDateInput) hiddenDateInput.value = enteredAt;
submitButton.onclick = function () {
return confirmSubmit();
};
}
//date and clock picker
$('.clockpicker').clockpicker({
autoclose: false,
placement: 'center',
donetext: 'ثبت',
vibrate: true,
});
// --- فعال‌سازی تقویم ---
$(".example1").persianDatepicker({
format: "YYYY/MM/DD",
altField: '#date',
altFormat: 'YYYY-MM-DD',
calendar: {
persian: {
locale: 'fa',
leapYearMode: "astronomical"
}
},
observer: true,
initialValue: false,
initialValueType: 'gregorian',
autoClose: true,
toolbox: {
calendarSwitch: {enabled: false},
todayButton: {enabled: true, text: {fa: "امروز"}}
},
});
}
$(document).ready(function () {
// 1. فعال‌سازی Clockpicker
$('.clockpicker').clockpicker({
autoclose: false,
placement: 'center',
donetext: 'ثبت',
vibrate: true,
// donetext: true,
});
$(".example1").persianDatepicker({
format: "YYYY/MM/DD",
altField: '#date',
altFormat: 'YYYY-MM-DD',
calendar: {
persian: {
locale: 'fa',
leapYearMode: "astronomical"
}
},
observer: true,
initialValue: false,
initialValueType: 'gregorian',
autoClose: true,
toolbox: {
calendarSwitch: {enabled: false},
todayButton: {enabled: true, text: {fa: "امروز"}}
},
onSelect: function (unix) {
// تبدیل تاریخ انتخابی به میلادی درست با timezone ایران
const d = new persianDate(unix).toCalendar('gregorian').toLocale('en');
console.log("Gregorian (Fixed):", d.format("YYYY-MM-DD"));
})
.then(res => {
if (res.status == 'error') {
console.log(res);
window.location.reload();
} else {
console.log(res);
breakBtn.textContent = 'خطا';
breakBtn.disabled = false;
console.error('Server error on break action:', res.statusText);
window.location.reload();
}
})
.catch(err => {
console.log(err);
window.location.reload();
// breakBtn.textContent = 'خطای شبکه';
// breakBtn.disabled = false;
// console.error('Network error:', err);
});
// 2. فعال‌سازی Persian Datepicker
// $('.example1').pDatepicker({
// format: 'YYYY/MM/DD',
// // placement: 'center',
// altField: '#date', // این فیلد مخفی، تاریخ میلادی تبدیل شده را دریافت می‌کند
// altFormat: 'YYYY-MM-DD', // میلادی ذخیره بشه
// // changeMonth: true,
// // changeYear: true,
// // showOtherMonths: true,
// // selectOtherMonths: true,
// autoClose: true,
//
// });
});
}
{{--let breakStarted = false;--}}
{{--let breakStartTime = null;--}}
{{--let breakTimer = null;--}}
{{--const breakBtn = document.getElementById('breakAttendance');--}}
{{--function formatLocalDatetime(date) {--}}
{{-- const year = date.getFullYear();--}}
{{-- const month = String(date.getMonth() + 1).padStart(2, '0');--}}
{{-- const day = String(date.getDate()).padStart(2, '0');--}}
{{-- const hours = String(date.getHours()).padStart(2, '0');--}}
{{-- const minutes = String(date.getMinutes()).padStart(2, '0');--}}
{{-- const seconds = String(date.getSeconds()).padStart(2, '0');--}}
{{-- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;--}}
{{--}--}}
{{--function formatDuration(totalSeconds) {--}}
{{-- const hours = Math.floor(totalSeconds / 3600);--}}
{{-- const minutes = Math.floor((totalSeconds % 3600) / 60);--}}
{{-- const h = String(hours);--}}
{{-- const i = String(minutes).padStart(2, '0');--}}
{{-- return `${h}:${i}`;--}}
{{--}--}}
{{--breakBtn.addEventListener('click', () => {--}}
{{-- if (!breakStarted) {--}}
{{-- // شروع استراحت 🚀--}}
{{-- breakStarted = true;--}}
{{-- breakStartTime = new Date();--}}
{{-- // تغییر ظاهر دکمه و شروع تایمر (همچنان M:S نمایش داده می شود)--}}
{{-- breakBtn.classList.remove('bg-gray-500', 'hover:bg-gray-600');--}}
{{-- breakBtn.classList.add('bg-red-500', 'hover:bg-red-600');--}}
{{-- breakBtn.textContent = '00:00';--}}
{{-- let seconds = 0;--}}
{{-- breakTimer = setInterval(() => {--}}
{{-- seconds++;--}}
{{-- // اینجا همچنان به صورت دقیقه:ثانیه نمایش داده می شود--}}
{{-- const m = String(Math.floor(seconds / 60)).padStart(2, '0');--}}
{{-- const s = String(seconds % 60).padStart(2, '0');--}}
{{-- breakBtn.textContent = ` ${m}:${s}`;--}}
{{-- }, 1000);--}}
{{-- //send start time--}}
{{-- let formData = new FormData();--}}
{{-- formData.append('start_time', formatLocalDatetime(breakStartTime));--}}
{{-- formData.append('_token', '{{ csrf_token() }}');--}}
{{-- fetch("{{ url('attendance/break') }}", {--}}
{{-- method: "POST",--}}
{{-- body: formData--}}
{{-- })--}}
{{-- .then(res => res.json())--}}
{{-- .then(data => console.log('✅ Break Started Logged:', data))--}}
{{-- .catch(err => console.error('💥 Error starting break:', err));--}}
{{-- } else {--}}
{{-- // پایان استراحت 🛑--}}
{{-- breakStarted = false;--}}
{{-- const breakEndTime = new Date();--}}
{{-- clearInterval(breakTimer);--}}
{{-- // 1. محاسبه مدت زمان استراحت (بر حسب ثانیه)--}}
{{-- const durationSec = Math.floor((breakEndTime - breakStartTime) / 1000);--}}
{{-- // 2. تبدیل زمان های شروع و پایان به فرمت رشته ای--}}
{{-- const breakStartTime_str = formatLocalDatetime(breakStartTime);--}}
{{-- const breakEndTime_str = formatLocalDatetime(breakEndTime);--}}
{{-- // 3. فرمت کردن مدت زمان به H:i (فقط برای نمایش در کنسول/هشدار)--}}
{{-- const duration_formatted_h_i = formatDuration(durationSec);--}}
{{-- // 4. نمایش دکمه به حالت اولیه--}}
{{-- breakBtn.classList.remove('bg-red-500', 'hover:bg-red-600');--}}
{{-- // breakBtn.classList.add('bg-gray-500', 'hover:bg-gray-600');--}}
{{-- breakBtn.textContent = 'استراحت';--}}
{{-- // 5. ارسال داده‌ها به سرور--}}
{{-- let formData = new FormData();--}}
{{-- formData.append('start_time', breakStartTime_str);--}}
{{-- formData.append('end_time', breakEndTime_str);--}}
{{-- formData.append('duration_seconds', durationSec);--}}
{{-- formData.append('_token', '{{ csrf_token() }}');--}}
{{-- fetch("{{ url('attendance/break') }}", {--}}
{{-- method: "POST",--}}
{{-- body: formData--}}
{{-- })--}}
{{-- .then(res => res.json())--}}
{{-- .then(data => console.log('✅ Break saved:', data))--}}
{{-- .catch(err => console.error('💥 Error:', err));--}}
{{-- }--}}
{{--});--}}
{{-- ATTENDANCE MODAL--}}
function clearTime(fieldId) {
document.getElementById(fieldId).value = '';
}
function closeModal() {
document.getElementById('modal-container').classList.add('hidden');
}
document.getElementById('modal-container').addEventListener('click', (e) => {
if (e.target === document.getElementById('modal-container')) {
closeModal();
}
});
function confirmSubmit() {
return confirm("آیا تغییرات اعمال شوند؟");
}
function openModal(title, id, enteredAt, exitedAt) {
const isNewEntry = id === 'new';
const modalTitleElement = document.getElementById('modal-title');
const hiddenIdInput = document.getElementById('edit_id');
// const hiddenIdInput = document.getElementById('delete_id');
const deleteIdInput = document.getElementById('delete_id');
const submitButton = document.querySelector('#modal-content button[type="submit"]');
const hiddenDateInput = document.querySelector('input[name="date"]');
// مقداردهی به فرم حذف
if (deleteIdInput) {
deleteIdInput.value = id;
}
const newDateLabel = document.getElementById('new-date-label');
const newDateGroup = document.getElementById('new-date-group');
const displayDateInput = document.getElementById('display_date');
const panelLink = document.getElementById('panel-access-link');
panelLink.href = `/go_down/${id}`;
document.getElementById('modal-container').classList.remove('hidden');
const startTimeInput = document.querySelector('input[name="starttime"]');
const endTimeInput = document.querySelector('input[name="endtime"]');
// تنظیم ساعت‌ها
if (enteredAt && !isNaN(new Date(enteredAt).getTime())) {
startTimeInput.value = new Date(enteredAt).toLocaleTimeString('en-GB', {
hour: '2-digit',
minute: '2-digit'
});
} else {
startTimeInput.value = '09:00';
}
if (exitedAt && !isNaN(new Date(exitedAt).getTime())) {
endTimeInput.value = new Date(exitedAt).toLocaleTimeString('en-GB', {
hour: '2-digit',
minute: '2-digit'
});
}
// else {
// endTimeInput.value = '17:30';
// }
if (isNewEntry) {
// حالت ثبت جدید
modalTitleElement.innerText = 'ثبت روز کاری جدید';
newDateLabel.style.display = 'block';
newDateGroup.style.display = 'block';
displayDateInput.value = '';
hiddenDateInput.value = '';
// ✅ اصلاح: مقداردهی 'new' به فیلد ID
if (hiddenIdInput) hiddenIdInput.value = 'new';
submitButton.onclick = function () {
if (hiddenDateInput.value === '') {
alert("لطفاً یک تاریخ را از تقویم انتخاب کنید.");
return false;
}
return confirmSubmit();
};
} else {
// حالت ویرایش
modalTitleElement.innerText = 'تغییر ساعت کاری';
newDateLabel.style.display = 'none';
newDateGroup.style.display = 'none';
// ✅ اصلاح: استفاده از متغیر 'id' که از جدول فرستاده شده است
if (hiddenIdInput) hiddenIdInput.value = id;
if (hiddenDateInput) hiddenDateInput.value = enteredAt;
submitButton.onclick = function () {
return confirmSubmit();
};
}
//date and clock picker
$('.clockpicker').clockpicker({
autoclose: false,
placement: 'center',
donetext: 'ثبت',
vibrate: true,
});
// --- فعال‌سازی تقویم ---
$(".example1").persianDatepicker({
format: "YYYY/MM/DD",
altField: '#date',
altFormat: 'YYYY-MM-DD',
calendar: {
persian: {
locale: 'fa',
leapYearMode: "astronomical"
}
},
observer: true,
initialValue: false,
initialValueType: 'gregorian',
autoClose: true,
toolbox: {
calendarSwitch: {enabled: false},
todayButton: {enabled: true, text: {fa: "امروز"}}
},
});
}
$(document).ready(function () {
// 1. فعال‌سازی Clockpicker
$('.clockpicker').clockpicker({
autoclose: false,
placement: 'center',
donetext: 'ثبت',
vibrate: true,
// donetext: true,
});
$(".example1").persianDatepicker({
format: "YYYY/MM/DD",
altField: '#date',
altFormat: 'YYYY-MM-DD',
calendar: {
persian: {
locale: 'fa',
leapYearMode: "astronomical"
}
},
observer: true,
initialValue: false,
initialValueType: 'gregorian',
autoClose: true,
toolbox: {
calendarSwitch: {enabled: false},
todayButton: {enabled: true, text: {fa: "امروز"}}
},
onSelect: function (unix) {
// تبدیل تاریخ انتخابی به میلادی درست با timezone ایران
const d = new persianDate(unix).toCalendar('gregorian').toLocale('en');
console.log("Gregorian (Fixed):", d.format("YYYY-MM-DD"));
}
});
// 2. فعال‌سازی Persian Datepicker
// $('.example1').pDatepicker({
// format: 'YYYY/MM/DD',
// // placement: 'center',
// altField: '#date', // این فیلد مخفی، تاریخ میلادی تبدیل شده را دریافت می‌کند
// altFormat: 'YYYY-MM-DD', // میلادی ذخیره بشه
// // changeMonth: true,
// // changeYear: true,
// // showOtherMonths: true,
// // selectOtherMonths: true,
// autoClose: true,
//
// });
});
</script>
+219 -63
View File
@@ -1,91 +1,247 @@
<?php
use App\Models\App;
use App\Models\User;
use App\Models\Tasks;
$users = User::get();
$apps = App::get();
if (isset($task)) {
$selectedAssignees = [];
if ($task && $task->assignees) {
$decoded = json_decode($task->assignees, true);
$selectedAssignees = is_array($decoded) ? $decoded : [];
}
}
?>
@extends('app')
@section('content')
<div class="w-full mt-[90px] justify-center container flex ">
<div class="bg-white w-[350px] p-6 rounded-xl ">
<h1 class="w-full bg-cyan-500 rounded-t-xl text-center p-1.5 font-bold text-slate-50">ایجاد وظیفه جدید</h1>
<form action="{{Route('create_api')}}" method="POST" class="grid grid-cols-2 gap-4 items-center">
<div class="bg-white w-[350px] rounded-xl ">
<h1 class="w-[350px] bg-cyan-500 rounded-t-xl text-center p-1.5 font-bold text-slate-50">{{isset($task) ? "ویرایش وظیفه" : "ایجاد وظیفه جدید"}}
</h1>
<form action="{{isset($task) ? Route('task.edit') : Route('task.create')}}" method="POST" class=" p-6 grid-cols-2 gap-4 items-center">
@csrf
<!-- نام Issue (الزامی) -->
<label for="name" class="font-medium text-sm">نام:</label>
<input class="rounded-lg border border-gray-300 p-2 w-full" type="text" id="name" name="name" required>
@if(isset($task))
<input type="hidden" id="id" name="id" value="{{$task->id}}">
@endif
<label for="title" class="font-medium text-sm"><span class="text-red-500">*</span>عنوان:</label>
<input class="rounded-lg border border-gray-300 p-2 w-full" value="{{$task->title ?? null}}" type="text"
id="title" name="title" required>
<!-- توضیحات HTML -->
<label for="description_html" class="font-medium text-sm">توضیحات:</label>
<textarea id="description_html" name="description_html"
class="rounded-lg border border-gray-300 p-2 w-full"></textarea>
<label for="description" class="font-medium text-sm">توضیحات:</label>
<textarea id="description" name="description"
class="rounded-lg border border-gray-300 p-2 w-full">{{$task->description ?? null}}</textarea>
<!-- اولویت -->
<label for="priority" class="font-medium text-sm">اولویت:</label>
<select id="priority" name="priority"
class="rounded-lg border border-gray-300 p-2 w-full">
<option value="none">ندارد</option>
<option value="urgent">فوری</option>
<option value="high">بالا</option>
<option value="medium">عادی</option>
<option value="low">پایین</option>
<select id="priority" name="priority" class="rounded-lg border border-gray-300 p-2 w-full">
<option value="none" {{ old('priority', $task->priority ?? '') == 'none' ? 'selected' : '' }}>ندارد</option>
<option value="low" {{ old('priority', $task->priority ?? '') == 'low' ? 'selected' : '' }}>کم</option>
<option value="medium" {{ old('priority', $task->priority ?? '') == 'medium' ? 'selected' : '' }}>عادی</option>
<option value="urgent" {{ old('priority', $task->priority ?? '') == 'urgent' ? 'selected' : '' }}>فوری</option>
<option value="high" {{ old('priority', $task->priority ?? '') == 'high' ? 'selected' : '' }}>زیاد</option>
</select>
<!-- تاریخ شروع -->
<label for="start_date" class="font-medium text-sm">تاریخ شروع:</label>
<input class="rounded-lg border border-gray-300 p-2 w-full" type="date" id="start_date"
name="start_date">
<div class="p-2">
<label for="start_date" class="font-medium text-sm">تاریخ شروع <span
class="text-red-500">*</span></label>
<input type="text" id="start_date" name="start_date_display"
class="rounded-lg border border-gray-300 p-2 " value="{{$task->start_date ?? null}}"
placeholder="تاریخ شروع" autocomplete="off" readonly required>
<input type="hidden" name="start_date" id="start_date_gregorian">
<!-- تاریخ هدف -->
<label for="target_date" class="font-medium text-sm">تاریخ هدف:</label>
<input class="rounded-lg border border-gray-300 p-2 w-full" type="date" id="target_date"
name="target_date">
<!-- تاریخ هدف -->
<label for="target_date" class="font-medium text-sm">تاریخ هدف</label>
{{-- <div class="relative">--}}
<input type="text" id="target_date" name="target_date_display"
class="rounded-lg border border-gray-300 p-2 pwt-datepicker-input-element mt-2"
value="{{$task->target_date ?? null}}"
placeholder="تاریخ هدف" autocomplete="off" readonly>
<button type="button" id="clear_target_date"
class="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-red-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
{{-- </div>--}}
<input type="hidden" name="target_date" id="target_date_gregorian">
</div>
<!-- وضعیت -->
<label for="state" class="font-medium text-sm">وضعیت:</label>
<select id="state" name="state"
class="rounded-lg border border-gray-300 p-2 w-full">
<option value="c73c2044-ea14-42cb-b08d-cecf3d1fdfb5">درحال انجام</option>
<option value="a9ce312f-cfca-491e-8e3d-1975aa6ebc10">در انتظار</option>
<option value="dea1fb57-a281-4fe7-8f3e-ee5567637e35">دارای خطا</option>
<option value="770fbe42-99f6-41db-9b9e-773d9cced1a5">در صف انجام</option>
<option value="b0e464e1-34eb-41ea-b2f7-3ef7483bc9f0">انجام‌شده</option>
<option value="d61d14a3-72f7-4f44-894b-efe8b2a75be1">لغوشده</option>
<label for="state" class="font-medium text-sm"><span class="text-red-500">*</span>وضعیت:</label>
<select id="state" name="state" class="rounded-lg border border-gray-300 p-2 w-full" required>
<option value="1" {{ old('state', $task->state ?? '') == 1 ? 'selected' : '' }}>در انتظار</option>
<option value="2" {{ old('state', $task->state ?? '') == 2 ? 'selected' : '' }}>در صف انجام</option>
<option value="3" {{ old('state', $task->state ?? '') == 3 ? 'selected' : '' }}>درحال انجام</option>
<option value="4" {{ old('state', $task->state ?? '') == 4 ? 'selected' : '' }}>انجام‌شده</option>
</select>
<!-- کاربران مسئول -->
{{-- <label for="assignees" class="font-medium text-sm">انتصاب کاربران:</label>--}}
<label class="font-medium text-sm">انتصاب کاربران:</label>
<div class="rounded-lg border border-gray-300 p-2 w-full max-h-32 overflow-y-auto space-y-2">
@foreach($users as $user)
<label class="flex items-center space-x-2">
<input type="checkbox" name="assignees[]" value="{{ $user->plain_id }}" class="rounded text-cyan-600 focus:ring-cyan-500">
<span>{{ $user->name }}</span>
</label>
@endforeach
<label for="assignees" class="font-medium text-sm">انتصاب
کاربران:</label>
<div class="relative" id="dropdown-container">
{{-- دکمه --}}
<button type="button" id="dropdown-btn"
class="w-full bg-white border border-gray-300 rounded-lg px-4 py-2 text-right flex justify-between items-center">
<span id="selected-count">انتخاب کاربران</span>
<svg class="w-4 h-4 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div id="dropdown-list"
class="hidden absolute z-20 mt-1 w-full bg-white border border-gray-300 rounded-lg shadow-lg">
<div class="p-2 border-b">
<input type="text" id="search-input"
class="w-full px-3 py-1 border border-gray-200 rounded-lg text-sm"
placeholder="جستجوی کاربر...">
</div>
{{-- لیست کاربران --}}
<div id="users-list" class="max-h-48 overflow-y-auto space-y-1 ">
@foreach($users as $user)
<label class="user-item flex items-center gap-2 p-1 hover:bg-gray-50 rounded cursor-pointer"
data-name="{{ $user->name }}">
<input type="checkbox"
name="assignees[]"
value="{{ $user->id }}"
class="user-checkbox rounded text-cyan-600"
{{ isset($task) && (in_array($user->id, $selectedAssignees)) ? 'checked' : '' }}>
<span>{{ $user->name }}</span>
</label>
@endforeach
</div>
</div>
</div>
<!-- برچسب‌ها -->
<label for="labels" class="font-medium text-sm">برچسب‌ها:</label>
<input class="rounded-lg border border-gray-300 p-2 w-full"
type="text" id="labels" name="labels[]" placeholder="UUID1, UUID2">
{{-- <!-- برچسب‌ها -->--}}
{{-- <label for="labels" class="font-medium text-sm">برچسب‌ها:</label>--}}
{{-- <input class="rounded-lg border border-gray-300 p-2 w-full"--}}
{{-- type="text" id="labels" name="labels[]" placeholder="UUID1, UUID2">--}}
<!-- Issue والد -->
<label for="parent" class="font-medium text-sm">پروژه والد:</label>
<select id="parent" name="parent" class="rounded-lg border border-gray-300 p-2 w-full">
@foreach($tasks as $task)
<option value="{{$task->task_id}}">{{$task->name}}</option>
@endforeach
<!-- اپ -->
<label for="app" class="font-medium text-sm">نام اپ:</label>
<select id="app" name="app" class="rounded-lg z-20 border border-gray-300 p-2 w-full">
<option value="">-- انتخاب اپ --</option>
@if(!empty($apps) && count($apps) > 0)
@foreach($apps as $app)
<option value="{{ $app->id }}" {{ old('app', $task->app ?? '') == $app->id ? 'selected' : '' }}>
{{ $app->name_fa }}
</option>
@endforeach
@endif
</select>
<!-- امتیاز تخمین -->
<label for="estimate_point" class="font-medium text-sm">Estimate Point (0-7):</label>
<input class="rounded-lg border border-gray-300 p-2 w-full" type="number" id="estimate_point"
name="estimate_point" min="0" max="7">
<!-- دکمه -->
<div></div>
<div class="inline-block flex pt-3 justify-center">
<button type="submit"
class="bg-cyan-600 text-white py-2 px-4 rounded-lg hover:bg-cyan-700 transition">
ایجاد وظیفه
</button>
class="bg-cyan-600 text-white py-2 px-4 rounded-lg hover:bg-cyan-700 transition <!--mr-24 mt-2.5-->">
{{isset($task) ? "ویرایش" : "ایجاد وظیفه جدید"}}</button>
</div>
</form>
</div>
</div>
<script>
const dropdownBtn = document.getElementById('dropdown-btn');
const dropdownList = document.getElementById('dropdown-list');
const searchInput = document.getElementById('search-input');
const usersList = document.getElementById('users-list');
const selectedCountSpan = document.getElementById('selected-count');
// باز و بسته کردن
dropdownBtn.addEventListener('click', (e) => {
e.stopPropagation();
dropdownList.classList.toggle('hidden');
if (!dropdownList.classList.contains('hidden')) {
searchInput.focus();
}
});
// بستن با کلیک بیرون
document.addEventListener('click', (e) => {
if (!dropdownBtn.contains(e.target) && !dropdownList.contains(e.target)) {
dropdownList.classList.add('hidden');
}
});
// قابلیت جستجو
searchInput.addEventListener('input', function () {
const searchText = this.value.toLowerCase();
const items = usersList.querySelectorAll('.user-item');
items.forEach(item => {
const name = item.getAttribute('data-name').toLowerCase();
if (name.includes(searchText)) {
item.style.display = 'flex';
} else {
item.style.display = 'none';
}
});
});
// آپدیت تعداد
function updateSelectedCount() {
const selected = document.querySelectorAll('.user-checkbox:checked');
const count = selected.length;
selectedCountSpan.textContent = count === 0 ? 'انتخاب کاربران' : count + ' کاربر انتخاب شده';
}
document.querySelectorAll('.user-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateSelectedCount);
});
updateSelectedCount();
$(document).ready(function () {
// تاریخ شروع با ساعت جاری
$('#start_date').persianDatepicker({
format: 'YYYY/MM/DD',
autoClose: true,
toolbox: {
calendarSwitch: {enabled: false},
todayButton: {enabled: true, text: {fa: 'امروز'}}
},
onSelect: function (unix) {
// تبدیل به میلادی
let gregorian = new persianDate(unix).toCalendar('gregorian').toLocale('en');
let dateStr = gregorian.format('YYYY-MM-DD');
$('#start_date_gregorian').val(dateStr);
console.log('Start Gregorian:', dateStr);
}
});
// تاریخ هدف
$('#target_date').persianDatepicker({
format: 'YYYY/MM/DD',
autoClose: true,
toolbox: {
calendarSwitch: {enabled: false},
todayButton: {enabled: true, text: {fa: 'امروز'}}
},
onSelect: function (unix) {
let gregorianDate = new persianDate(unix).toCalendar('gregorian').toLocale('en');
let dateStrTarget = gregorianDate.format('YYYY-MM-DD');
// let dateTimeStr = `${dateStr} 23:59:59`;
$('#target_date_gregorian').val(dateStrTarget);
console.log('Target DateTime (Gregorian):', dateStrTarget);
}
});
// دکمه پاک کردن تاریخ هدف
$('#clear_target_date').on('click', function () {
$('#target_date').val('');
$('#target_date_gregorian').val('');
});
});
</script>
@endsection
File diff suppressed because it is too large Load Diff
View File
@@ -0,0 +1,3 @@
<figure class="attachment attachment--content">
{!! trim($content->renderTrixContentAttachment($options)) !!}
</figure>
@@ -0,0 +1,8 @@
<div class="not-prose attachment-gallery attachment-gallery--{{ count($attachables) }}">
@foreach ($attachables as $attachable)
@include('rich-text-laravel::attachables._remote_image', [
'remoteImage' => $attachable,
'gallery' => true
])
@endforeach
</div>
@@ -0,0 +1 @@
<span>&#8864;</span>
@@ -0,0 +1,11 @@
<figure class="not-prose attachment attachment--file attachment--{{ $remoteFile->extension() }}">
<figcaption class="attachment__caption">
@if ($remoteFile->caption)
{{ $remoteFile->caption }}
@else
<span class="attachment__name">{{ $remoteFile->filename }}</span>
<span class="attachment__size">{{ $remoteFile->filesizeForHumans() }}</span>
<span class="attachment__download"><a class="!underline" href="{{ $remoteFile->url }}">{{ __('Download') }}</a></span>
@endif
</figcaption>
</figure>
@@ -0,0 +1,8 @@
<figure class="not-prose attachment attachment--preview attachment--{{ $remoteImage->extension() }}" @if ($gallery ?? false) data-trix-attributes='{"presentation":"gallery"}' @endif>
<img src="{{ $remoteImage->url }}" width="{{ $remoteImage->width }}" height="{{ $remoteImage->height }}" />
@if ($remoteImage->caption)
<figcaption class="attachment__caption">
{{ $remoteImage->caption }}
</figcaption>
@endif
</figure>
@@ -0,0 +1,5 @@
<div class="attachment-gallery attachment-gallery--{{ $attachmentGallery->count() }}">
@foreach ($attachmentGallery->attachments() as $attachment)
{!! $attachment->richTextRender() !!}
@endforeach
</div>
@@ -0,0 +1,13 @@
@props(['id', 'name' => null, 'value' => ''])
@php
$editor = \Tonysm\RichTextLaravel\RichTextLaravel::editorName();
@endphp
<x-dynamic-component
:component="'rich-text::' . $editor"
:id="$id"
:name="$name"
:value="$value"
{{ $attributes }}
>{{ $slot }}</x-dynamic-component>
@@ -0,0 +1,23 @@
@props(['id', 'name' => null, 'value' => ''])
<div
{{ $attributes->whereDoesntStartWith('wire:') }}
@if ($attributes->has('wire:model'))
x-data="{ content: $wire.entangle('{{ $attributes->wire('model')->value() }}') }"
x-on:lexxy:initialize="$nextTick(() => $refs.input.value = content)"
x-on:lexxy:change="content = $refs.input.value"
@endif
>
<lexxy-editor
id="{{ $id }}"
class="lexxy-content"
value="{{ $value }}"
@if ($name ?? false)
name="{{ $name }}"
@endif
@if ($attributes->has('wire:model'))
wire:ignore
x-ref="input"
@endif
>{{ $slot }}</lexxy-editor>
</div>
@@ -0,0 +1,24 @@
@props(['theme' => 'default'])
@inject('assets', \Tonysm\RichTextLaravel\AssetsManager::class)
@if (config('rich-text-laravel.editor') === 'lexxy')
<link {{ $attributes }} rel="stylesheet" href="{{ $assets->url('/lexxy.css') }}" />
<link {{ $attributes }} rel="stylesheet" href="{{ $assets->url('/lexxy-rich-text-laravel-attachments.css') }}" />
@if ($theme === 'daisyui')
<link {{ $attributes }} rel="stylesheet" href="{{ $assets->url('/lexxy-rich-text-laravel-daisyui.css') }}" />
@elseif ($theme === 'flux')
<link {{ $attributes }} rel="stylesheet" href="{{ $assets->url('/lexxy-rich-text-laravel-flux.css') }}" />
@endif
@elseif ($theme === 'richtextlaravel')
<link {{ $attributes }} rel="stylesheet" href="{{ $assets->url('/trix-rich-text-laravel.css') }}" />
<link {{ $attributes }} rel="stylesheet" href="{{ $assets->url('/trix-rich-text-laravel-attachments.css') }}" />
@else
<link {{ $attributes }} rel="stylesheet" href="{{ $assets->url('/trix.css') }}" />
<link {{ $attributes }} rel="stylesheet" href="{{ $assets->url('/trix-rich-text-laravel-attachments.css') }}" />
@if ($theme === 'daisyui')
<link {{ $attributes }} rel="stylesheet" href="{{ $assets->url('/trix-rich-text-laravel-daisyui.css') }}" />
@elseif ($theme === 'flux')
<link {{ $attributes }} rel="stylesheet" href="{{ $assets->url('/trix-rich-text-laravel-flux.css') }}" />
@endif
@endif
@@ -0,0 +1,41 @@
@props(['id', 'name' => null, 'value' => ''])
<div
{{ $attributes->whereDoesntStartWith('wire:') }}
@if ($attributes->has('wire:model'))
x-data="{ content: $wire.entangle('{{ $attributes->wire('model')->value() }}') }"
x-on:trix-initialize="$refs.input.value = content"
x-on:trix-change="content = $refs.input.value"
@endif
>
<input
type="hidden"
@if ($name ?? false)
name="{{ $name }}"
@endif
id="{{ $id }}_input"
value="{{ $value }}"
@if ($attributes->has('wire:model'))
x-on:change="$refs.input.value = $event.target.value"
{{ $attributes->whereStartsWith('wire:') }}
@endif
/>
<trix-toolbar
id="{{ $id }}_toolbar"
@if ($attributes->has('wire:model'))
wire:ignore
@endif
></trix-toolbar>
<trix-editor
id="{{ $id }}"
toolbar="{{ $id }}_toolbar"
input="{{ $id }}_input"
class="trix-content"
@if ($attributes->has('wire:model'))
x-ref="input"
wire:ignore
@endif
></trix-editor>
</div>
@@ -0,0 +1,5 @@
<div class="{{ e(\Tonysm\RichTextLaravel\RichTextLaravel::editorName()) . '-content' }}">
@if (trim($content = $content->renderWithAttachments()))
{!! $content !!}
@endif
</div>