diff --git a/src/equipment/router.py b/src/equipment/router.py index 6695d09..5c06862 100644 --- a/src/equipment/router.py +++ b/src/equipment/router.py @@ -12,6 +12,7 @@ from src.equipment.schema import ( EquipmentTop10, EquipmentTop10Pagination, EquipmentUpdate, + CountRemainingLifeResponse, ) from src.equipment.service import ( get_master_by_assetnum, @@ -20,6 +21,7 @@ from src.equipment.service import ( get_all, create, get_top_10_replacement_priorities, + get_count_remaining_life, update, delete, generate_all_transaction, @@ -113,6 +115,16 @@ async def simulate_equipment(db_session: DbSession, assetnum: str): return StreamingResponse(event_generator(), media_type='text/event-stream') +@router.get( + "/count-remaining-life", + response_model=StandardResponse[CountRemainingLifeResponse], +) +async def get_count_remaining_life(db_session: DbSession, common: CommonParameters): + count = await get_count_remaining_life(db_session=db_session, common=common) + return StandardResponse( + data=count, + message="Count remaining life retrieved successfully", + ) @router.get( "/top-10-replacement-priorities", diff --git a/src/equipment/schema.py b/src/equipment/schema.py index 55682b1..d6a6a30 100644 --- a/src/equipment/schema.py +++ b/src/equipment/schema.py @@ -145,3 +145,8 @@ class EquipmentDataMaster(EquipmentBase): class EquipmentPagination(Pagination): items: List[EquipmentDataMaster] = [] + +class CountRemainingLifeResponse(DefaultBase): + safe: int + warning: int + critical: int diff --git a/src/equipment/service.py b/src/equipment/service.py index 05d9700..1bc7972 100644 --- a/src/equipment/service.py +++ b/src/equipment/service.py @@ -411,6 +411,39 @@ async def get_top_10_economic_life(*, db_session: DbSession, common) -> list[Equ result = await search_filter_sort_paginate(model=query, **common) return result +async def get_count_remaining_life(*, db_session: DbSession, common) -> dict[str, int]: + """Count remaining life based on the category""" + current_year = datetime.datetime.now().year + + remaining_life = case( + ( + (Equipment.minimum_eac_year - current_year) >= 0, + (Equipment.minimum_eac_year - current_year), + ), + else_=0, + ) + + query = ( + Select( + func.sum(case((remaining_life <= 1, 1), else_=0)).label("red"), + func.sum(case(((remaining_life > 1) & (remaining_life < 5), 1), else_=0)).label("orange"), + func.sum(case((remaining_life >= 5, 1), else_=0)).label("green"), + ) + .select_from(Equipment) + .join(EquipmentMaster, Equipment.assetnum == EquipmentMaster.assetnum) + .filter(Equipment.minimum_eac_year != None) + .filter((Equipment.minimum_eac != None) & (Equipment.minimum_eac != 0)) + ) + + result = await db_session.execute(query) + row = result.mappings().one() + + return { + "safe": int(row["red"] or 0), + "warning": int(row["orange"] or 0), + "critical": int(row["green"] or 0), + } + async def get_top_10_replacement_priorities(*, db_session: DbSession, common) -> list[Equipment]: """Returns top 10 replacement priorities.""" query = ( diff --git a/src/modules/equipment/Eac.py b/src/modules/equipment/Eac.py index e91a793..5d72e61 100644 --- a/src/modules/equipment/Eac.py +++ b/src/modules/equipment/Eac.py @@ -352,7 +352,7 @@ def main(assetnum=None): print("Skipping None assetnum") continue - print(f"Processing asset: {row_assetnum}") + print(f"EAC Calculation asset: {row_assetnum}") eac.hitung_eac_equipment(row_assetnum) processed.append(row_assetnum) diff --git a/src/modules/equipment/Prediksi.py b/src/modules/equipment/Prediksi.py index 206cdab..c4fd0a7 100644 --- a/src/modules/equipment/Prediksi.py +++ b/src/modules/equipment/Prediksi.py @@ -126,35 +126,35 @@ class Prediksi: return fv_values # Fungsi untuk menghapus data proyeksi pada tahun tertentu - def __delete_predictions_from_db(self, equipment_id): - try: - connections = get_connection() - connection = ( - connections[0] if isinstance(connections, tuple) else connections - ) - if connection is None: - print("Database connection failed.") - return None - - cursor = connection.cursor() - - # Query untuk menghapus data berdasarkan tahun proyeksi - delete_query = """ - DELETE FROM lcc_equipment_tr_data - WHERE assetnum = %s AND is_actual = 0; - """ # Asumsikan kolom is_actual digunakan untuk membedakan data proyeksi dan data aktual - - # Eksekusi query delete - cursor.execute(delete_query, (equipment_id,)) - connection.commit() - # print(f"Data proyeksi untuk tahun {equipment_id} berhasil dihapus.") - - except Exception as e: - print(f"Error saat menghapus data proyeksi dari database: {e}") - - finally: - if connection: - connection.close() + # def __delete_predictions_from_db(self, equipment_id): + # try: + # connections = get_connection() + # connection = ( + # connections[0] if isinstance(connections, tuple) else connections + # ) + # if connection is None: + # print("Database connection failed.") + # return None + + # cursor = connection.cursor() + + # # Query untuk menghapus data berdasarkan tahun proyeksi + # delete_query = """ + # DELETE FROM lcc_equipment_tr_data + # WHERE assetnum = %s AND is_actual = 0; + # """ # Asumsikan kolom is_actual digunakan untuk membedakan data proyeksi dan data aktual + + # # Eksekusi query delete + # cursor.execute(delete_query, (equipment_id,)) + # connection.commit() + # # print(f"Data proyeksi untuk tahun {equipment_id} berhasil dihapus.") + + # except Exception as e: + # print(f"Error saat menghapus data proyeksi dari database: {e}") + + # finally: + # if connection: + # connection.close() # Fungsi untuk menyimpan data proyeksi ke database async def __insert_predictions_to_db(self, data, equipment_id, token): @@ -707,6 +707,75 @@ class Prediksi: # ====================================================================================================================================================== + async def _fetch_api_data(self, assetnum: str, year: int) -> dict: + url = self.RELIABILITY_APP_URL + endpoint = f"{url}/main/number-of-failures/{assetnum}/{int(year)}/{int(year)}" + async with httpx.AsyncClient() as client: + try: + current_token = getattr(self, "access_token", None) + response = await client.get( + endpoint, + timeout=30.0, + headers={"Authorization": f"Bearer {current_token}"} if current_token else {}, + ) + response.raise_for_status() + return response.json() + except httpx.HTTPStatusError as e: + status = getattr(e.response, "status_code", None) + # If we get a 401 or 403, try to refresh the access token and retry once + if status in (401, 403): + print(f"Received {status} from reliability API, attempting to refresh/re-login...") + # Try refreshing token first + new_access = await self.refresh_access_token() + # If refresh failed (e.g. refresh token expired), try full sign-in + if not new_access: + print("Refresh failed, attempting full sign-in...") + await self.sign_in() + new_access = getattr(self, "access_token", None) + + if new_access: + try: + response = await client.get( + endpoint, + timeout=30.0, + headers={"Authorization": f"Bearer {new_access}"}, + ) + response.raise_for_status() + return response.json() + except httpx.HTTPError as e2: + print(f"HTTP error occurred after retry: {e2}") + return {} + print(f"HTTP error occurred: {e}") + return {} + except httpx.HTTPError as e: + print(f"HTTP error occurred: {e}") + return {} + + def __get_man_hour_rate(self, staff_level: str = "junior"): + connection = None + try: + connections = get_connection() + connection = ( + connections[0] if isinstance(connections, tuple) else connections + ) + if connection is None: + return 0.0 + + cursor = connection.cursor() + # Takes from salary_per_hour_idr on specific staff_job_level + query = "SELECT salary_per_hour_idr FROM lcc_manpower_cost WHERE staff_job_level = %s LIMIT 1" + cursor.execute(query, (staff_level,)) + result = cursor.fetchone() + if result: + return float(result[0]) + return 0.0 + except Exception as e: + print(f"Error getting man hour rate for {staff_level}: {e}") + return 0.0 + finally: + if connection: + connection.close() + async def predict_equipment_data(self, assetnum, token): try: # Mengambil data dari database @@ -759,6 +828,7 @@ class Prediksi: # Mendapatkan rate dan tahun maksimal rate, max_year = self.__get_rate_and_max_year(assetnum) + man_hour_rate = self.__get_man_hour_rate() # Defaults to 'junior' pmt = 0 # Prediksi untuk setiap kolom @@ -788,13 +858,33 @@ class Prediksi: recent_vals = df[column].dropna() # Jika masih kosong, pakai default (interval minimal 1, lainnya 0) - if recent_vals.empty: + if "labor" in col_lower: + preds_list = [] + for yr in future_years: + failures_data = await self._fetch_api_data(assetnum, yr) + # Interval from number of failures + interval = 0.0 + if isinstance(failures_data, dict): + val = failures_data.get("data") + if val is not None: + try: + interval = float(val) + except Exception: + interval = 0.0 + + # interval * labor_time(3) * labor_human(1) * man_hour_rate + cost = rc_labor_cost(interval, 3.0, 1.0, man_hour_rate) + preds_list.append(cost) + preds = np.array(preds_list, dtype=float) + + elif recent_vals.empty: avg = 0.0 + preds = np.repeat(float(avg), n_future) else: avg = pd.to_numeric(recent_vals, errors="coerce").fillna(0).mean() avg = 0.0 if pd.isna(avg) else float(avg) - - preds = np.repeat(float(avg), n_future) + + preds = np.repeat(float(avg), n_future) # print(preds) else: # Untuk kolom non-cm, gunakan nilai dari last actual year bila ada, @@ -957,22 +1047,22 @@ async def main(RELIABILITY_APP_URL=RELIABILITY_APP_URL, assetnum=None, token=Non prediksi = Prediksi(RELIABILITY_APP_URL) # If token not provided, sign in to obtain access_token/refresh_token - # if token is None: - # signin_res = await prediksi.sign_in() - # if not getattr(prediksi, "access_token", None): - # print("Failed to obtain access token; aborting.") - # return - # else: - # # Use provided token as access token - # prediksi.access_token = token + if token is None: + signin_res = await prediksi.sign_in() + if not getattr(prediksi, "access_token", None): + print("Failed to obtain access token; aborting.") + return + else: + # Use provided token as access token + prediksi.access_token = token # If an assetnum was provided, run only for that assetnum if assetnum: - print(f"Processing single assetnum: {assetnum}") + print(f"Predicting single assetnum: {assetnum}") try: await prediksi.predict_equipment_data(assetnum, prediksi.access_token) except Exception as e: - print(f"Error processing {assetnum}: {e}") + print(f"Error Predicting {assetnum}: {e}") print("Selesai.") return @@ -993,11 +1083,11 @@ async def main(RELIABILITY_APP_URL=RELIABILITY_APP_URL, assetnum=None, token=Non if not current_asset or str(current_asset).strip() == "": print(f"[{idx}/{len(results)}] Skipping empty assetnum") continue - print(f"[{idx}/{len(results)}] Processing assetnum: {current_asset}") + print(f"[{idx}/{len(results)}] Predicting assetnum: {current_asset}") try: await prediksi.predict_equipment_data(current_asset, prediksi.access_token) except Exception as e: - print(f"Error processing {current_asset}: {e}") + print(f"Error Predicting {current_asset}: {e}") print("Selesai.") except Exception as e: diff --git a/src/modules/equipment/__pycache__/Eac.cpython-311.pyc b/src/modules/equipment/__pycache__/Eac.cpython-311.pyc index 08b9fa6..88ac350 100644 Binary files a/src/modules/equipment/__pycache__/Eac.cpython-311.pyc and b/src/modules/equipment/__pycache__/Eac.cpython-311.pyc differ diff --git a/src/modules/equipment/__pycache__/Prediksi.cpython-311.pyc b/src/modules/equipment/__pycache__/Prediksi.cpython-311.pyc index c2dfa40..4771647 100644 Binary files a/src/modules/equipment/__pycache__/Prediksi.cpython-311.pyc and b/src/modules/equipment/__pycache__/Prediksi.cpython-311.pyc differ diff --git a/src/modules/equipment/__pycache__/insert_actual_data.cpython-311.pyc b/src/modules/equipment/__pycache__/insert_actual_data.cpython-311.pyc index 1dee995..29b657d 100644 Binary files a/src/modules/equipment/__pycache__/insert_actual_data.cpython-311.pyc and b/src/modules/equipment/__pycache__/insert_actual_data.cpython-311.pyc differ diff --git a/src/modules/equipment/insert_actual_data.py b/src/modules/equipment/insert_actual_data.py index de5314f..1767020 100644 --- a/src/modules/equipment/insert_actual_data.py +++ b/src/modules/equipment/insert_actual_data.py @@ -954,6 +954,7 @@ async def query_data(target_assetnum: str = None): total_assets = len(results) processed_assets = 0 total_inserted = 0 + total_updated = 0 overall_start = datetime.now() print(f"Starting processing {total_assets} assets at {overall_start.isoformat()}") @@ -970,6 +971,7 @@ async def query_data(target_assetnum: str = None): processed_assets += 1 years_processed = 0 inserted_this_asset = 0 + updated_this_asset = 0 # CM data_cm = get_recursive_query(cursor_wo, assetnum, worktype="CM") @@ -1108,8 +1110,8 @@ async def query_data(target_assetnum: str = None): year, ), ) - inserted_this_asset += 1 - total_inserted += 1 + updated_this_asset += 1 + total_updated += 1 seq = seq + 1 # commit per asset to persist progress and free transaction @@ -1127,16 +1129,16 @@ async def query_data(target_assetnum: str = None): # progress per asset print( f"[{idx}/{total_assets}] Asset {assetnum} processed. " - f"Inserted this asset: {inserted_this_asset}. " + f"Inserted: {inserted_this_asset}, Updated: {updated_this_asset}. " f"Asset time: {asset_elapsed.total_seconds():.2f}s. " - f"Total inserted: {total_inserted}. " + f"Total Ins: {total_inserted}, Upd: {total_updated}. " f"Overall elapsed: {total_elapsed.total_seconds():.2f}s. " f"Progress: {pct_assets:.1f}%" ) # periodic summary every 10 assets if idx % 10 == 0 or idx == total_assets: - print(f"SUMMARY: {idx}/{total_assets} assets processed, {total_inserted} rows inserted, elapsed {total_elapsed.total_seconds():.1f}s") + print(f"SUMMARY: {idx}/{total_assets} assets processed, {total_inserted} inserted, {total_updated} updated, elapsed {total_elapsed.total_seconds():.1f}s") # Commit perubahan try: @@ -1147,7 +1149,7 @@ async def query_data(target_assetnum: str = None): except Exception: pass - print(f"Finished processing all assets. Total assets: {total_assets}, total inserted: {total_inserted}, total time: {(datetime.now()-overall_start).total_seconds():.2f}s") + print(f"Finished processing all assets. Total assets: {total_assets}, Total Inserted: {total_inserted}, Total Updated: {total_updated}, Total time: {(datetime.now()-overall_start).total_seconds():.2f}s") except Exception as e: print("Error saat menjalankan query:", e)